Run oxfmt across repo, add format script and docs

Add .oxfmtignore to skip generated bindings and wasm-pack output.
Add npm format script, update DEVELOPMENT.md for Vite+ toolchain,
and format all non-generated files with oxfmt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Gregory Schier
2026-03-13 10:15:49 -07:00
parent 45262edfbd
commit b4a1c418bb
664 changed files with 13638 additions and 13492 deletions

View File

@@ -1,4 +1,4 @@
import type { DragEndEvent, DragMoveEvent, DragStartEvent } from '@dnd-kit/core';
import type { DragEndEvent, DragMoveEvent, DragStartEvent } from "@dnd-kit/core";
import {
DndContext,
MeasuringStrategy,
@@ -7,10 +7,10 @@ import {
useDroppable,
useSensor,
useSensors,
} from '@dnd-kit/core';
import { type } from '@tauri-apps/plugin-os';
import classNames from 'classnames';
import type { ComponentType, MouseEvent, ReactElement, Ref, RefAttributes } from 'react';
} from "@dnd-kit/core";
import { type } from "@tauri-apps/plugin-os";
import classNames from "classnames";
import type { ComponentType, MouseEvent, ReactElement, Ref, RefAttributes } from "react";
import {
forwardRef,
memo,
@@ -20,14 +20,14 @@ import {
useMemo,
useRef,
useState,
} from 'react';
import { useKey, useKeyPressEvent } from 'react-use';
import type { HotKeyOptions, HotkeyAction } from '../../../hooks/useHotKey';
import { useHotKey } from '../../../hooks/useHotKey';
import { computeSideForDragMove } from '../../../lib/dnd';
import { jotaiStore } from '../../../lib/jotai';
import type { ContextMenuProps, DropdownItem } from '../Dropdown';
import { ContextMenu } from '../Dropdown';
} from "react";
import { useKey, useKeyPressEvent } from "react-use";
import type { HotKeyOptions, HotkeyAction } from "../../../hooks/useHotKey";
import { useHotKey } from "../../../hooks/useHotKey";
import { computeSideForDragMove } from "../../../lib/dnd";
import { jotaiStore } from "../../../lib/jotai";
import type { ContextMenuProps, DropdownItem } from "../Dropdown";
import { ContextMenu } from "../Dropdown";
import {
collapsedFamily,
draggingIdsFamily,
@@ -35,14 +35,14 @@ import {
hoveredParentFamily,
isCollapsedFamily,
selectedIdsFamily,
} from './atoms';
import type { SelectableTreeNode, TreeNode } from './common';
import { closestVisibleNode, equalSubtree, getSelectedItems, hasAncestor } from './common';
import { TreeDragOverlay } from './TreeDragOverlay';
import type { TreeItemClickEvent, TreeItemHandle, TreeItemProps } from './TreeItem';
import type { TreeItemListProps } from './TreeItemList';
import { TreeItemList } from './TreeItemList';
import { useSelectableItems } from './useSelectableItems';
} from "./atoms";
import type { SelectableTreeNode, TreeNode } from "./common";
import { closestVisibleNode, equalSubtree, getSelectedItems, hasAncestor } from "./common";
import { TreeDragOverlay } from "./TreeDragOverlay";
import type { TreeItemClickEvent, TreeItemHandle, TreeItemProps } from "./TreeItem";
import type { TreeItemListProps } from "./TreeItemList";
import { TreeItemList } from "./TreeItemList";
import { useSelectableItems } from "./useSelectableItems";
/** So we re-calculate after expanding a folder during drag */
const measuring = { droppable: { strategy: MeasuringStrategy.Always } };
@@ -51,7 +51,7 @@ export interface TreeProps<T extends { id: string }> {
root: TreeNode<T>;
treeId: string;
getItemKey: (item: T) => string;
getContextMenu?: (items: T[]) => ContextMenuProps['items'] | Promise<ContextMenuProps['items']>;
getContextMenu?: (items: T[]) => ContextMenuProps["items"] | Promise<ContextMenuProps["items"]>;
ItemInner: ComponentType<{ treeId: string; item: T }>;
ItemLeftSlotInner?: ComponentType<{ treeId: string; item: T }>;
ItemRightSlot?: ComponentType<{ treeId: string; item: T }>;
@@ -140,7 +140,7 @@ function TreeInner<T extends { id: string }>(
return false;
}
$el.focus();
$el.scrollIntoView({ block: 'nearest' });
$el.scrollIntoView({ block: "nearest" });
return true;
}, []);
@@ -241,7 +241,7 @@ function TreeInner<T extends { id: string }>(
};
}, [getContextMenu, selectableItems, setSelected, treeId]);
const handleSelect = useCallback<NonNullable<TreeItemProps<T>['onClick']>>(
const handleSelect = useCallback<NonNullable<TreeItemProps<T>["onClick"]>>(
(item, { shiftKey, metaKey, ctrlKey }) => {
const anchorSelectedId = jotaiStore.get(focusIdsFamily(treeId)).anchorId;
const selectedIdsAtom = selectedIdsFamily(treeId);
@@ -281,7 +281,7 @@ function TreeInner<T extends { id: string }>(
} else {
setSelected([item.id], true);
}
} else if (type() === 'macos' ? metaKey : ctrlKey) {
} else if (type() === "macos" ? metaKey : ctrlKey) {
const withoutCurr = selectedIds.filter((id) => id !== item.id);
if (withoutCurr.length === selectedIds.length) {
// It wasn't in there, so add it
@@ -299,7 +299,7 @@ function TreeInner<T extends { id: string }>(
[selectableItems, setSelected, treeId],
);
const handleClick = useCallback<NonNullable<TreeItemProps<T>['onClick']>>(
const handleClick = useCallback<NonNullable<TreeItemProps<T>["onClick"]>>(
(item, e) => {
if (e.shiftKey || e.ctrlKey || e.metaKey) {
handleSelect(item, e);
@@ -350,7 +350,7 @@ function TreeInner<T extends { id: string }>(
);
useKey(
(e) => e.key === 'ArrowUp' || e.key.toLowerCase() === 'k',
(e) => e.key === "ArrowUp" || e.key.toLowerCase() === "k",
(e) => {
if (!isTreeFocused()) return;
e.preventDefault();
@@ -361,7 +361,7 @@ function TreeInner<T extends { id: string }>(
);
useKey(
(e) => e.key === 'ArrowDown' || e.key.toLowerCase() === 'j',
(e) => e.key === "ArrowDown" || e.key.toLowerCase() === "j",
(e) => {
if (!isTreeFocused()) return;
e.preventDefault();
@@ -373,7 +373,7 @@ function TreeInner<T extends { id: string }>(
// If the selected item is a collapsed folder, expand it. Otherwise, select next item
useKey(
(e) => e.key === 'ArrowRight' || e.key === 'l',
(e) => e.key === "ArrowRight" || e.key === "l",
(e) => {
if (!isTreeFocused()) return;
e.preventDefault();
@@ -399,7 +399,7 @@ function TreeInner<T extends { id: string }>(
// If the selected item is in a folder, select its parent.
// If the selected item is an expanded folder, collapse it.
useKey(
(e) => e.key === 'ArrowLeft' || e.key === 'h',
(e) => e.key === "ArrowLeft" || e.key === "h",
(e) => {
if (!isTreeFocused()) return;
e.preventDefault();
@@ -422,7 +422,7 @@ function TreeInner<T extends { id: string }>(
[selectableItems, handleSelect],
);
useKeyPressEvent('Escape', async () => {
useKeyPressEvent("Escape", async () => {
if (!treeRef.current?.contains(document.activeElement)) return;
clearDragState();
const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId;
@@ -486,11 +486,11 @@ function TreeInner<T extends { id: string }>(
let hoveredParent = node.parent;
const dragIndex = selectableItems.findIndex((n) => n.node.item.id === item.id) ?? -1;
const hovered = selectableItems[dragIndex]?.node ?? null;
const hoveredIndex = dragIndex + (side === 'before' ? 0 : 1);
let hoveredChildIndex = overSelectableItem.index + (side === 'before' ? 0 : 1);
const hoveredIndex = dragIndex + (side === "before" ? 0 : 1);
let hoveredChildIndex = overSelectableItem.index + (side === "before" ? 0 : 1);
// Move into the folder if it's open and we're moving after it
if (hovered?.children != null && side === 'after') {
if (hovered?.children != null && side === "after") {
hoveredParent = hovered;
hoveredChildIndex = 0;
}
@@ -620,7 +620,7 @@ function TreeInner<T extends { id: string }>(
const treeItemListProps: Omit<
TreeItemListProps<T>,
'nodes' | 'treeId' | 'activeIdAtom' | 'hoveredParent' | 'hoveredIndex'
"nodes" | "treeId" | "activeIdAtom" | "hoveredParent" | "hoveredIndex"
> = {
getItemKey,
getContextMenu: handleGetContextMenu,
@@ -670,24 +670,24 @@ function TreeInner<T extends { id: string }>(
ref={treeRef}
className={classNames(
className,
'outline-none h-full',
'overflow-y-auto overflow-x-hidden',
'grid grid-rows-[auto_1fr]',
"outline-none h-full",
"overflow-y-auto overflow-x-hidden",
"grid grid-rows-[auto_1fr]",
)}
>
<div
className={classNames(
'[&_.tree-item.selected_.tree-item-inner]:text-text',
'[&:focus-within]:[&_.tree-item.selected]:bg-surface-active',
'[&:not(:focus-within)]:[&_.tree-item.selected:not([data-context-menu-open])]:bg-surface-highlight',
'[&_.tree-item.selected[data-context-menu-open]]:bg-surface-active',
"[&_.tree-item.selected_.tree-item-inner]:text-text",
"[&:focus-within]:[&_.tree-item.selected]:bg-surface-active",
"[&:not(:focus-within)]:[&_.tree-item.selected:not([data-context-menu-open])]:bg-surface-highlight",
"[&_.tree-item.selected[data-context-menu-open]]:bg-surface-active",
// Round the items, but only if the ends of the selection.
// Also account for the drop marker being in between items
'[&_.tree-item]:rounded-md',
'[&_.tree-item.selected+.tree-item.selected]:rounded-t-none',
'[&_.tree-item.selected+.drop-marker+.tree-item.selected]:rounded-t-none',
'[&_.tree-item.selected:has(+.tree-item.selected)]:rounded-b-none',
'[&_.tree-item.selected:has(+.drop-marker+.tree-item.selected)]:rounded-b-none',
"[&_.tree-item]:rounded-md",
"[&_.tree-item.selected+.tree-item.selected]:rounded-t-none",
"[&_.tree-item.selected+.drop-marker+.tree-item.selected]:rounded-t-none",
"[&_.tree-item.selected:has(+.tree-item.selected)]:rounded-b-none",
"[&_.tree-item.selected:has(+.drop-marker+.tree-item.selected)]:rounded-b-none",
)}
>
<TreeItemList
@@ -766,7 +766,7 @@ function TreeHotKey<T extends { id: string }>({
...options,
enable: () => {
if (enable == null) return true;
if (typeof enable === 'function') return enable();
if (typeof enable === "function") return enable();
return enable;
},
},
@@ -780,7 +780,7 @@ function TreeHotKeys<T extends { id: string }>({
selectableItems,
}: {
treeId: string;
hotkeys: TreeProps<T>['hotkeys'];
hotkeys: TreeProps<T>["hotkeys"];
selectableItems: SelectableTreeNode<T>[];
}) {
if (hotkeys == null) return null;

View File

@@ -1,9 +1,9 @@
import { DragOverlay } from '@dnd-kit/core';
import { useAtomValue } from 'jotai';
import { draggingIdsFamily } from './atoms';
import type { SelectableTreeNode } from './common';
import type { TreeProps } from './Tree';
import { TreeItemList } from './TreeItemList';
import { DragOverlay } from "@dnd-kit/core";
import { useAtomValue } from "jotai";
import { draggingIdsFamily } from "./atoms";
import type { SelectableTreeNode } from "./common";
import type { TreeProps } from "./Tree";
import { TreeItemList } from "./TreeItemList";
export function TreeDragOverlay<T extends { id: string }>({
treeId,
@@ -14,7 +14,7 @@ export function TreeDragOverlay<T extends { id: string }>({
}: {
treeId: string;
selectableItems: SelectableTreeNode<T>[];
} & Pick<TreeProps<T>, 'getItemKey' | 'ItemInner' | 'ItemLeftSlotInner'>) {
} & Pick<TreeProps<T>, "getItemKey" | "ItemInner" | "ItemLeftSlotInner">) {
const draggingItems = useAtomValue(draggingIdsFamily(treeId));
return (
<DragOverlay dropAnimation={null}>

View File

@@ -1,9 +1,9 @@
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import { memo } from 'react';
import { DropMarker } from '../../DropMarker';
import { hoveredParentDepthFamily, isCollapsedFamily, isIndexHoveredFamily } from './atoms';
import type { TreeNode } from './common';
import classNames from "classnames";
import { useAtomValue } from "jotai";
import { memo } from "react";
import { DropMarker } from "../../DropMarker";
import { hoveredParentDepthFamily, isCollapsedFamily, isIndexHoveredFamily } from "./atoms";
import type { TreeNode } from "./common";
export const TreeDropMarker = memo(function TreeDropMarker<T extends { id: string }>({
className,

View File

@@ -1,7 +1,7 @@
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import { memo } from 'react';
import { hoveredParentDepthFamily, isAncestorHoveredFamily } from './atoms';
import classNames from "classnames";
import { useAtomValue } from "jotai";
import { memo } from "react";
import { hoveredParentDepthFamily, isAncestorHoveredFamily } from "./atoms";
export const TreeIndentGuide = memo(function TreeIndentGuide({
treeId,
@@ -22,8 +22,8 @@ export const TreeIndentGuide = memo(function TreeIndentGuide({
// oxlint-disable-next-line react/no-array-index-key
key={i}
className={classNames(
'w-[calc(1rem+0.5px)] border-r border-r-text-subtlest',
!(parentDepth === i + 1 && isHovered) && 'opacity-30',
"w-[calc(1rem+0.5px)] border-r border-r-text-subtlest",
!(parentDepth === i + 1 && isHovered) && "opacity-30",
)}
/>
))}

View File

@@ -1,25 +1,25 @@
import type { DragMoveEvent } from '@dnd-kit/core';
import { useDndContext, useDndMonitor, useDraggable, useDroppable } from '@dnd-kit/core';
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import { selectAtom } from 'jotai/utils';
import type { DragMoveEvent } from "@dnd-kit/core";
import { useDndContext, useDndMonitor, useDraggable, useDroppable } from "@dnd-kit/core";
import classNames from "classnames";
import { useAtomValue } from "jotai";
import { selectAtom } from "jotai/utils";
import type {
MouseEvent,
PointerEvent,
FocusEvent as ReactFocusEvent,
KeyboardEvent as ReactKeyboardEvent,
} from 'react';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { computeSideForDragMove } from '../../../lib/dnd';
import { jotaiStore } from '../../../lib/jotai';
import type { ContextMenuProps, DropdownItem } from '../Dropdown';
import { ContextMenu } from '../Dropdown';
import { Icon } from '../Icon';
import { collapsedFamily, isCollapsedFamily, isLastFocusedFamily, isSelectedFamily } from './atoms';
import type { TreeNode } from './common';
import { getNodeKey } from './common';
import type { TreeProps } from './Tree';
import { TreeIndentGuide } from './TreeIndentGuide';
} from "react";
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { computeSideForDragMove } from "../../../lib/dnd";
import { jotaiStore } from "../../../lib/jotai";
import type { ContextMenuProps, DropdownItem } from "../Dropdown";
import { ContextMenu } from "../Dropdown";
import { Icon } from "../Icon";
import { collapsedFamily, isCollapsedFamily, isLastFocusedFamily, isSelectedFamily } from "./atoms";
import type { TreeNode } from "./common";
import { getNodeKey } from "./common";
import type { TreeProps } from "./Tree";
import { TreeIndentGuide } from "./TreeIndentGuide";
export interface TreeItemClickEvent {
shiftKey: boolean;
@@ -29,12 +29,12 @@ export interface TreeItemClickEvent {
export type TreeItemProps<T extends { id: string }> = Pick<
TreeProps<T>,
'ItemInner' | 'ItemLeftSlotInner' | 'ItemRightSlot' | 'treeId' | 'getEditOptions' | 'getItemKey'
"ItemInner" | "ItemLeftSlotInner" | "ItemRightSlot" | "treeId" | "getEditOptions" | "getItemKey"
> & {
node: TreeNode<T>;
className?: string;
onClick?: (item: T, e: TreeItemClickEvent) => void;
getContextMenu?: (item: T) => ContextMenuProps['items'] | Promise<ContextMenuProps['items']>;
getContextMenu?: (item: T) => ContextMenuProps["items"] | Promise<ContextMenuProps["items"]>;
depth: number;
setRef?: (item: T, n: TreeItemHandle | null) => void;
};
@@ -68,7 +68,7 @@ function TreeItem_<T extends { id: string }>({
const isCollapsed = useAtomValue(isCollapsedFamily({ treeId, itemId: node.item.id }));
const isLastSelected = useAtomValue(isLastFocusedFamily({ treeId, itemId: node.item.id }));
const [editing, setEditing] = useState<boolean>(false);
const [dropHover, setDropHover] = useState<null | 'drop' | 'animate'>(null);
const [dropHover, setDropHover] = useState<null | "drop" | "animate">(null);
const startedHoverTimeout = useRef<NodeJS.Timeout>(undefined);
const handle = useMemo<TreeItemHandle>(
() => ({
@@ -88,7 +88,7 @@ function TreeItem_<T extends { id: string }>({
return listItemRef.current.getBoundingClientRect();
},
scrollIntoView: () => {
listItemRef.current?.scrollIntoView({ block: 'nearest' });
listItemRef.current?.scrollIntoView({ block: "nearest" });
},
}),
[editing, getEditOptions],
@@ -162,13 +162,13 @@ function TreeItem_<T extends { id: string }>({
async (e: ReactKeyboardEvent<HTMLInputElement>) => {
e.stopPropagation(); // Don't trigger other tree keys (like arrows)
switch (e.key) {
case 'Enter':
case "Enter":
if (editing) {
e.preventDefault();
await handleSubmitNameEdit(e.currentTarget);
}
break;
case 'Escape':
case "Escape":
if (editing) {
e.preventDefault();
setEditing(false);
@@ -208,8 +208,8 @@ function TreeItem_<T extends { id: string }>({
const isFolder = node.children != null;
const hasChildren = (node.children?.length ?? 0) > 0;
const isCollapsed = jotaiStore.get(isCollapsedFamily({ treeId, itemId: node.item.id }));
if (isCollapsed && isFolder && hasChildren && side === 'after') {
setDropHover('animate');
if (isCollapsed && isFolder && hasChildren && side === "after") {
setDropHover("animate");
clearTimeout(startedHoverTimeout.current);
startedHoverTimeout.current = setTimeout(() => {
jotaiStore.set(isCollapsedFamily({ treeId, itemId: node.item.id }), false);
@@ -221,8 +221,8 @@ function TreeItem_<T extends { id: string }>({
);
});
}, HOVER_CLOSED_FOLDER_DELAY);
} else if (isFolder && !hasChildren && side === 'after') {
setDropHover('drop');
} else if (isFolder && !hasChildren && side === "after") {
setDropHover("drop");
} else {
clearDropHover();
}
@@ -238,7 +238,7 @@ function TreeItem_<T extends { id: string }>({
// Set data attribute on the list item to preserve active state
if (listItemRef.current) {
listItemRef.current.setAttribute('data-context-menu-open', 'true');
listItemRef.current.setAttribute("data-context-menu-open", "true");
}
const items = await getContextMenu(node.item);
@@ -250,7 +250,7 @@ function TreeItem_<T extends { id: string }>({
const handleCloseContextMenu = useCallback(() => {
// Remove data attribute when context menu closes
if (listItemRef.current) {
listItemRef.current.removeAttribute('data-context-menu-open');
listItemRef.current.removeAttribute("data-context-menu-open");
}
setShowContextMenu(null);
}, []);
@@ -290,20 +290,20 @@ function TreeItem_<T extends { id: string }>({
onContextMenu={handleContextMenu}
className={classNames(
className,
'tree-item',
'h-sm',
'grid grid-cols-[auto_minmax(0,1fr)]',
editing && 'ring-1 focus-within:ring-focus',
dropHover != null && 'relative z-10 ring-2 ring-primary',
dropHover === 'animate' && 'animate-blinkRing',
isSelected && 'selected',
"tree-item",
"h-sm",
"grid grid-cols-[auto_minmax(0,1fr)]",
editing && "ring-1 focus-within:ring-focus",
dropHover != null && "relative z-10 ring-2 ring-primary",
dropHover === "animate" && "animate-blinkRing",
isSelected && "selected",
)}
>
<TreeIndentGuide treeId={treeId} depth={depth} ancestorIds={ancestorIds} />
<div
className={classNames(
'text-text-subtle',
'grid grid-cols-[auto_minmax(0,1fr)_auto] gap-x-2 items-center rounded-md',
"text-text-subtle",
"grid grid-cols-[auto_minmax(0,1fr)_auto] gap-x-2 items-center rounded-md",
)}
>
{showContextMenu && (
@@ -321,12 +321,12 @@ function TreeItem_<T extends { id: string }>({
onClick={toggleCollapsed}
>
<Icon
icon={node.children.length === 0 ? 'dot' : 'chevron_right'}
icon={node.children.length === 0 ? "dot" : "chevron_right"}
className={classNames(
'transition-transform text-text-subtlest',
'ml-auto',
'w-[1rem] h-[1rem]',
!isCollapsed && node.children.length > 0 && 'rotate-90',
"transition-transform text-text-subtlest",
"ml-auto",
"w-[1rem] h-[1rem]",
!isCollapsed && node.children.length > 0 && "rotate-90",
)}
/>
</button>

View File

@@ -1,16 +1,16 @@
import type { CSSProperties } from 'react';
import { Fragment } from 'react';
import type { SelectableTreeNode } from './common';
import type { TreeProps } from './Tree';
import { TreeDropMarker } from './TreeDropMarker';
import type { TreeItemHandle, TreeItemProps } from './TreeItem';
import { TreeItem } from './TreeItem';
import type { CSSProperties } from "react";
import { Fragment } from "react";
import type { SelectableTreeNode } from "./common";
import type { TreeProps } from "./Tree";
import { TreeDropMarker } from "./TreeDropMarker";
import type { TreeItemHandle, TreeItemProps } from "./TreeItem";
import { TreeItem } from "./TreeItem";
export type TreeItemListProps<T extends { id: string }> = Pick<
TreeProps<T>,
'ItemInner' | 'ItemLeftSlotInner' | 'ItemRightSlot' | 'treeId' | 'getItemKey' | 'getEditOptions'
"ItemInner" | "ItemLeftSlotInner" | "ItemRightSlot" | "treeId" | "getItemKey" | "getEditOptions"
> &
Pick<TreeItemProps<T>, 'onClick' | 'getContextMenu'> & {
Pick<TreeItemProps<T>, "onClick" | "getContextMenu"> & {
nodes: SelectableTreeNode<T>[];
style?: CSSProperties;
className?: string;

View File

@@ -1,6 +1,6 @@
import { atom } from 'jotai';
import { atomFamily, selectAtom } from 'jotai/utils';
import { atomWithKVStorage } from '../../../lib/atoms/atomWithKVStorage';
import { atom } from "jotai";
import { atomFamily, selectAtom } from "jotai/utils";
import { atomWithKVStorage } from "../../../lib/atoms/atomWithKVStorage";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const selectedIdsFamily = atomFamily((_treeId: string) => {
@@ -58,7 +58,7 @@ export const isAncestorHoveredFamily = atomFamily(
(v) => v.parentId && ancestorIds.includes(v.parentId),
Object.is,
),
(a, b) => a.treeId === b.treeId && a.ancestorIds.join(',') === b.ancestorIds.join(','),
(a, b) => a.treeId === b.treeId && a.ancestorIds.join(",") === b.ancestorIds.join(","),
);
export const isIndexHoveredFamily = atomFamily(
@@ -76,12 +76,12 @@ export const hoveredParentDepthFamily = atomFamily((treeId: string) =>
);
export const collapsedFamily = atomFamily((workspaceId: string) => {
const key = ['sidebar_collapsed', workspaceId ?? 'n/a'];
const key = ["sidebar_collapsed", workspaceId ?? "n/a"];
return atomWithKVStorage<Record<string, boolean>>(key, {});
});
export const isCollapsedFamily = atomFamily(
({ treeId, itemId = 'n/a' }: { treeId: string; itemId: string | undefined }) =>
({ treeId, itemId = "n/a" }: { treeId: string; itemId: string | undefined }) =>
atom(
// --- getter ---
(get) => !!get(collapsedFamily(treeId))[itemId],
@@ -91,7 +91,7 @@ export const isCollapsedFamily = atomFamily(
const a = collapsedFamily(treeId);
const prevMap = get(a);
const prevValue = !!prevMap[itemId];
const value = typeof next === 'function' ? next(prevValue) : next;
const value = typeof next === "function" ? next(prevValue) : next;
if (value === prevValue) return; // no-op

View File

@@ -1,5 +1,5 @@
import { jotaiStore } from '../../../lib/jotai';
import { collapsedFamily, selectedIdsFamily } from './atoms';
import { jotaiStore } from "../../../lib/jotai";
import { collapsedFamily, selectedIdsFamily } from "./atoms";
export interface TreeNode<T extends { id: string }> {
children?: TreeNode<T>[];

View File

@@ -1,5 +1,5 @@
import { useMemo } from 'react';
import type { SelectableTreeNode, TreeNode } from './common';
import { useMemo } from "react";
import type { SelectableTreeNode, TreeNode } from "./common";
export function useSelectableItems<T extends { id: string }>(root: TreeNode<T>) {
return useMemo(() => {