Merge main into proxy branch (formatting and docs)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Gregory Schier
2026-03-13 12:09:59 -07:00
parent 3c4035097a
commit 7314aedc71
712 changed files with 13408 additions and 13322 deletions

View File

@@ -1,7 +1,7 @@
{
"name": "@yaakapp-internal/ui",
"private": true,
"version": "1.0.0",
"private": true,
"type": "module",
"main": "src/index.ts",
"types": "src/index.ts"

View File

@@ -1,10 +1,10 @@
import classNames from 'classnames';
import type { ReactNode } from 'react';
import classNames from "classnames";
import type { ReactNode } from "react";
export interface BannerProps {
children: ReactNode;
className?: string;
color?: 'primary' | 'secondary' | 'success' | 'notice' | 'warning' | 'danger' | 'info';
color?: "primary" | "secondary" | "success" | "notice" | "warning" | "danger" | "info";
}
export function Banner({ children, className, color }: BannerProps) {
@@ -13,12 +13,12 @@ export function Banner({ children, className, color }: BannerProps) {
<div
className={classNames(
className,
color && 'bg-surface',
color && "bg-surface",
`x-theme-banner--${color}`,
'border border-border border-dashed',
'px-4 py-2 rounded-lg select-auto cursor-auto',
'overflow-auto text-text',
'mb-auto', // Don't stretch all the way down if the parent is in grid or flexbox
"border border-border border-dashed",
"px-4 py-2 rounded-lg select-auto cursor-auto",
"overflow-auto text-text",
"mb-auto", // Don't stretch all the way down if the parent is in grid or flexbox
)}
>
{children}

View File

@@ -8,10 +8,7 @@ import { LoadingIcon } from "./LoadingIcon";
type ButtonVariant = "border" | "solid";
type ButtonSize = "2xs" | "xs" | "sm" | "md" | "auto";
export type ButtonProps = Omit<
HTMLAttributes<HTMLButtonElement>,
"color" | "onChange"
> & {
export type ButtonProps = Omit<HTMLAttributes<HTMLButtonElement>, "color" | "onChange"> & {
innerClassName?: string;
color?: Color | "custom" | "default";
tone?: Color | "default";

View File

@@ -1,30 +1,30 @@
import classNames from 'classnames';
import type { CSSProperties } from 'react';
import { memo } from 'react';
import classNames from "classnames";
import type { CSSProperties } from "react";
import { memo } from "react";
interface Props {
className?: string;
style?: CSSProperties;
orientation?: 'horizontal' | 'vertical';
orientation?: "horizontal" | "vertical";
}
export const DropMarker = memo(
function DropMarker({ className, style, orientation = 'horizontal' }: Props) {
function DropMarker({ className, style, orientation = "horizontal" }: Props) {
return (
<div
style={style}
className={classNames(
className,
'absolute pointer-events-none z-50',
orientation === 'horizontal' && 'w-full',
orientation === 'vertical' && 'w-0 top-0 bottom-0',
"absolute pointer-events-none z-50",
orientation === "horizontal" && "w-full",
orientation === "vertical" && "w-0 top-0 bottom-0",
)}
>
<div
className={classNames(
'absolute bg-primary rounded-full',
orientation === 'horizontal' && 'left-2 right-2 -bottom-[0.1rem] h-[0.2rem]',
orientation === 'vertical' && '-left-[0.1rem] top-0 bottom-0 w-[0.2rem]',
"absolute bg-primary rounded-full",
orientation === "horizontal" && "left-2 right-2 -bottom-[0.1rem] h-[0.2rem]",
orientation === "vertical" && "-left-[0.1rem] top-0 bottom-0 w-[0.2rem]",
)}
/>
</div>

View File

@@ -1,5 +1,5 @@
import classNames from 'classnames';
import type { ReactNode } from 'react';
import classNames from "classnames";
import type { ReactNode } from "react";
interface Props {
children: ReactNode;
@@ -11,10 +11,10 @@ export function FormattedError({ children, className }: Props) {
<pre
className={classNames(
className,
'cursor-text select-auto',
'[&_*]:cursor-text [&_*]:select-auto',
'font-mono text-sm w-full bg-surface-highlight p-3 rounded',
'whitespace-pre-wrap border border-danger border-dashed overflow-x-auto',
"cursor-text select-auto",
"[&_*]:cursor-text [&_*]:select-auto",
"font-mono text-sm w-full bg-surface-highlight p-3 rounded",
"whitespace-pre-wrap border border-danger border-dashed overflow-x-auto",
)}
>
{children}

View File

@@ -1,13 +1,13 @@
import classNames from 'classnames';
import type { CSSProperties, HTMLAttributes, ReactNode } from 'react';
import { useMemo } from 'react';
import { useIsFullscreen } from '../hooks/useIsFullscreen';
import { HEADER_SIZE_LG, HEADER_SIZE_MD, WINDOW_CONTROLS_WIDTH } from '../lib/constants';
import { WindowControls } from './WindowControls';
import classNames from "classnames";
import type { CSSProperties, HTMLAttributes, ReactNode } from "react";
import { useMemo } from "react";
import { useIsFullscreen } from "../hooks/useIsFullscreen";
import { HEADER_SIZE_LG, HEADER_SIZE_MD, WINDOW_CONTROLS_WIDTH } from "../lib/constants";
import { WindowControls } from "./WindowControls";
interface HeaderSizeProps extends HTMLAttributes<HTMLDivElement> {
children?: ReactNode;
size: 'md' | 'lg';
size: "md" | "lg";
ignoreControlsSpacing?: boolean;
onlyXWindowControl?: boolean;
hideControls?: boolean;
@@ -35,12 +35,12 @@ export function HeaderSize({
const s = { ...style };
// Set the height (use min-height because scaling font size may make it larger
if (size === 'md') s.minHeight = HEADER_SIZE_MD;
if (size === 'lg') s.minHeight = HEADER_SIZE_LG;
if (size === "md") s.minHeight = HEADER_SIZE_MD;
if (size === "lg") s.minHeight = HEADER_SIZE_LG;
if (useNativeTitlebar) {
// No style updates when using native titlebar
} else if (osType === 'macos') {
} else if (osType === "macos") {
if (!isFullscreen) {
// Add large padding for window controls
s.paddingLeft = 76 / interfaceScale;
@@ -67,17 +67,17 @@ export function HeaderSize({
style={finalStyle}
className={classNames(
className,
'pt-[1px]', // Make up for bottom border
'select-none relative flex items-center',
'w-full border-b border-border-subtle min-w-0',
"pt-[1px]", // Make up for bottom border
"select-none relative flex items-center",
"w-full border-b border-border-subtle min-w-0",
)}
>
{/* NOTE: This needs display:grid or else the element shrinks (even though scrollable) */}
<div
data-tauri-drag-region
className={classNames(
'pointer-events-none h-full w-full overflow-x-auto hide-scrollbars grid',
'px-1', // Give it some space on either end for focus outlines
"pointer-events-none h-full w-full overflow-x-auto hide-scrollbars grid",
"px-1", // Give it some space on either end for focus outlines
)}
>
{children}

View File

@@ -1,20 +1,20 @@
import classNames from 'classnames';
import type { HTMLAttributes } from 'react';
import classNames from "classnames";
import type { HTMLAttributes } from "react";
interface Props extends HTMLAttributes<HTMLHeadingElement> {
level?: 1 | 2 | 3;
}
export function Heading({ className, level = 1, ...props }: Props) {
const Component = level === 1 ? 'h1' : level === 2 ? 'h2' : 'h3';
const Component = level === 1 ? "h1" : level === 2 ? "h2" : "h3";
return (
<Component
className={classNames(
className,
'font-semibold text-text',
level === 1 && 'text-2xl',
level === 2 && 'text-xl',
level === 3 && 'text-lg',
"font-semibold text-text",
level === 1 && "text-2xl",
level === 2 && "text-xl",
level === 3 && "text-lg",
)}
{...props}
/>

View File

@@ -1,5 +1,5 @@
import type { Color } from '@yaakapp-internal/plugins';
import classNames from 'classnames';
import type { Color } from "@yaakapp-internal/plugins";
import classNames from "classnames";
import {
AlarmClockIcon,
AlertTriangleIcon,
@@ -137,9 +137,9 @@ import {
WifiIcon,
WrenchIcon,
XIcon,
} from 'lucide-react';
import type { CSSProperties, HTMLAttributes } from 'react';
import { memo } from 'react';
} from "lucide-react";
import type { CSSProperties, HTMLAttributes } from "react";
import { memo } from "react";
const icons = {
alarm_clock: AlarmClockIcon,
@@ -287,17 +287,17 @@ export interface IconProps {
icon: keyof typeof icons;
className?: string;
style?: CSSProperties;
size?: '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
size?: "2xs" | "xs" | "sm" | "md" | "lg" | "xl";
spin?: boolean;
title?: string;
color?: Color | 'custom' | 'default';
color?: Color | "custom" | "default";
}
export const Icon = memo(function Icon({
icon,
color = 'default',
color = "default",
spin,
size = 'md',
size = "md",
style,
className,
title,
@@ -309,23 +309,23 @@ export const Icon = memo(function Icon({
title={title}
className={classNames(
className,
!spin && 'transform-gpu',
spin && 'animate-spin',
'flex-shrink-0',
size === 'xl' && 'h-6 w-6',
size === 'lg' && 'h-5 w-5',
size === 'md' && 'h-4 w-4',
size === 'sm' && 'h-3.5 w-3.5',
size === 'xs' && 'h-3 w-3',
size === '2xs' && 'h-2.5 w-2.5',
color === 'default' && 'inherit',
color === 'danger' && 'text-danger',
color === 'warning' && 'text-warning',
color === 'notice' && 'text-notice',
color === 'info' && 'text-info',
color === 'success' && 'text-success',
color === 'primary' && 'text-primary',
color === 'secondary' && 'text-secondary',
!spin && "transform-gpu",
spin && "animate-spin",
"flex-shrink-0",
size === "xl" && "h-6 w-6",
size === "lg" && "h-5 w-5",
size === "md" && "h-4 w-4",
size === "sm" && "h-3.5 w-3.5",
size === "xs" && "h-3 w-3",
size === "2xs" && "h-2.5 w-2.5",
color === "default" && "inherit",
color === "danger" && "text-danger",
color === "warning" && "text-warning",
color === "notice" && "text-notice",
color === "info" && "text-info",
color === "success" && "text-success",
color === "primary" && "text-primary",
color === "secondary" && "text-secondary",
)}
/>
);

View File

@@ -1,13 +1,13 @@
import classNames from 'classnames';
import type { HTMLAttributes } from 'react';
import classNames from "classnames";
import type { HTMLAttributes } from "react";
export function InlineCode({ className, ...props }: HTMLAttributes<HTMLSpanElement>) {
return (
<code
className={classNames(
className,
'font-mono text-shrink bg-surface-highlight border border-border-subtle flex-grow-0',
'px-1.5 py-0.5 rounded text shadow-inner break-words',
"font-mono text-shrink bg-surface-highlight border border-border-subtle flex-grow-0",
"px-1.5 py-0.5 rounded text shadow-inner break-words",
)}
{...props}
/>

View File

@@ -1,34 +1,34 @@
import classNames from 'classnames';
import classNames from "classnames";
interface Props {
size?: '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
size?: "2xs" | "xs" | "sm" | "md" | "lg" | "xl";
className?: string;
}
export function LoadingIcon({ size = 'md', className }: Props) {
export function LoadingIcon({ size = "md", className }: Props) {
const classes = classNames(
className,
'text-inherit flex-shrink-0',
size === 'xl' && 'h-6 w-6',
size === 'lg' && 'h-5 w-5',
size === 'md' && 'h-4 w-4',
size === 'sm' && 'h-3.5 w-3.5',
size === 'xs' && 'h-3 w-3',
size === '2xs' && 'h-2.5 w-2.5',
'animate-spin',
"text-inherit flex-shrink-0",
size === "xl" && "h-6 w-6",
size === "lg" && "h-5 w-5",
size === "md" && "h-4 w-4",
size === "sm" && "h-3.5 w-3.5",
size === "xs" && "h-3 w-3",
size === "2xs" && "h-2.5 w-2.5",
"animate-spin",
);
return (
<div
className={classNames(
classes,
'border-[currentColor] border-b-transparent rounded-full',
size === 'xl' && 'border-[0.2rem]',
size === 'lg' && 'border-[0.16rem]',
size === 'md' && 'border-[0.13rem]',
size === 'sm' && 'border-[0.1rem]',
size === 'xs' && 'border-[0.08rem]',
size === '2xs' && 'border-[0.06rem]',
"border-[currentColor] border-b-transparent rounded-full",
size === "xl" && "border-[0.2rem]",
size === "lg" && "border-[0.16rem]",
size === "md" && "border-[0.13rem]",
size === "sm" && "border-[0.1rem]",
size === "xs" && "border-[0.08rem]",
size === "2xs" && "border-[0.06rem]",
)}
/>
);

View File

@@ -1,9 +1,9 @@
import classNames from 'classnames';
import { FocusTrap } from 'focus-trap-react';
import * as m from 'motion/react-m';
import type { ReactNode } from 'react';
import { useRef } from 'react';
import { Portal } from './Portal';
import classNames from "classnames";
import { FocusTrap } from "focus-trap-react";
import * as m from "motion/react-m";
import type { ReactNode } from "react";
import { useRef } from "react";
import { Portal } from "./Portal";
interface Props {
children: ReactNode;
@@ -11,20 +11,20 @@ interface Props {
open: boolean;
onClose?: () => void;
zIndex?: keyof typeof zIndexes;
variant?: 'default' | 'transparent';
variant?: "default" | "transparent";
noBackdrop?: boolean;
}
const zIndexes: Record<number, string> = {
10: 'z-10',
20: 'z-20',
30: 'z-30',
40: 'z-40',
50: 'z-50',
10: "z-10",
20: "z-20",
30: "z-30",
40: "z-40",
50: "z-50",
};
export function Overlay({
variant = 'default',
variant = "default",
zIndex = 30,
open,
onClose,
@@ -63,7 +63,7 @@ export function Overlay({
>
<m.div
ref={containerRef}
className={classNames('fixed inset-0', zIndexes[zIndex])}
className={classNames("fixed inset-0", zIndexes[zIndex])}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
@@ -71,14 +71,14 @@ export function Overlay({
aria-hidden
onClick={onClose}
className={classNames(
'absolute inset-0',
variant === 'default' && 'bg-backdrop backdrop-blur-sm',
"absolute inset-0",
variant === "default" && "bg-backdrop backdrop-blur-sm",
)}
/>
{/* Show the draggable region at the top */}
{/* TODO: Figure out tauri drag region and also make clickable still */}
{variant === 'default' && (
{variant === "default" && (
<div data-tauri-drag-region className="absolute top-0 left-0 h-md right-0" />
)}
{children}

View File

@@ -1,6 +1,6 @@
import type { ReactNode } from 'react';
import { createPortal } from 'react-dom';
import { usePortal } from '../hooks/usePortal';
import type { ReactNode } from "react";
import { createPortal } from "react-dom";
import { usePortal } from "../hooks/usePortal";
interface Props {
children: ReactNode;

View File

@@ -1,6 +1,6 @@
import classNames from 'classnames';
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
import { useCallback, useRef, useState } from 'react';
import classNames from "classnames";
import type { CSSProperties, MouseEvent as ReactMouseEvent } from "react";
import { useCallback, useRef, useState } from "react";
const START_DISTANCE = 7;
@@ -18,8 +18,8 @@ interface Props {
onResizeEnd?: () => void;
onResizeMove?: (e: ResizeHandleEvent) => void;
onReset?: () => void;
side: 'left' | 'right' | 'top';
justify: 'center' | 'end' | 'start';
side: "left" | "right" | "top";
justify: "center" | "end" | "start";
}
export function ResizeHandle({
@@ -32,7 +32,7 @@ export function ResizeHandle({
onReset,
side,
}: Props) {
const vertical = side === 'top';
const vertical = side === "top";
const [isResizing, setIsResizing] = useState<boolean>(false);
const moveState = useRef<{
move: (e: MouseEvent) => void;
@@ -67,15 +67,15 @@ export function ResizeHandle({
function up() {
setIsResizing(false);
moveState.current = null;
document.documentElement.removeEventListener('mousemove', move);
document.documentElement.removeEventListener('mouseup', up);
document.documentElement.removeEventListener("mousemove", move);
document.documentElement.removeEventListener("mouseup", up);
onResizeEnd?.();
}
moveState.current = { calledStart: false, xStart: e.clientX, yStart: e.clientY, move, up };
document.documentElement.addEventListener('mousemove', move);
document.documentElement.addEventListener('mouseup', up);
document.documentElement.addEventListener("mousemove", move);
document.documentElement.addEventListener("mouseup", up);
},
[onResizeEnd, onResizeMove, onResizeStart, vertical],
);
@@ -88,22 +88,22 @@ export function ResizeHandle({
onPointerDown={handlePointerDown}
className={classNames(
className,
'group z-10 flex select-none transition-colors hover:bg-surface-active rounded-full',
vertical ? 'w-full h-1.5 cursor-row-resize' : 'h-full w-1.5 cursor-col-resize',
justify === 'center' && 'justify-center',
justify === 'end' && 'justify-end',
justify === 'start' && 'justify-start',
side === 'right' && 'right-0',
side === 'left' && 'left-0',
side === 'top' && 'top-0',
"group z-10 flex select-none transition-colors hover:bg-surface-active rounded-full",
vertical ? "w-full h-1.5 cursor-row-resize" : "h-full w-1.5 cursor-col-resize",
justify === "center" && "justify-center",
justify === "end" && "justify-end",
justify === "start" && "justify-start",
side === "right" && "right-0",
side === "left" && "left-0",
side === "top" && "top-0",
)}
>
{isResizing && (
<div
className={classNames(
'fixed -left-[100vw] -right-[100vw] -top-[100vh] -bottom-[100vh]',
vertical && 'cursor-row-resize',
!vertical && 'cursor-col-resize',
"fixed -left-[100vw] -right-[100vw] -top-[100vh] -bottom-[100vh]",
vertical && "cursor-row-resize",
!vertical && "cursor-col-resize",
)}
/>
)}

View File

@@ -1,17 +1,17 @@
import classNames from 'classnames';
import * as m from 'motion/react-m';
import type { CSSProperties, ReactNode } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useContainerSize } from '../hooks/useContainerSize';
import { Overlay } from './Overlay';
import type { ResizeHandleEvent } from './ResizeHandle';
import { ResizeHandle } from './ResizeHandle';
import classNames from "classnames";
import * as m from "motion/react-m";
import type { CSSProperties, ReactNode } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useContainerSize } from "../hooks/useContainerSize";
import { Overlay } from "./Overlay";
import type { ResizeHandleEvent } from "./ResizeHandle";
import { ResizeHandle } from "./ResizeHandle";
const FLOATING_BREAKPOINT = 600;
const side = { gridArea: 'side', minWidth: 0 };
const drag = { gridArea: 'drag' };
const body = { gridArea: 'body', minWidth: 0 };
const side = { gridArea: "side", minWidth: 0 };
const drag = { gridArea: "drag" };
const body = { gridArea: "body", minWidth: 0 };
interface Props {
width: number;
@@ -98,7 +98,7 @@ export function SidebarLayout({
if (floating) {
return (
<div ref={containerRef} className={classNames(className, 'w-full h-full min-h-0')}>
<div ref={containerRef} className={classNames(className, "w-full h-full min-h-0")}>
<Overlay
open={!floatingHidden}
portalName="sidebar"
@@ -123,7 +123,7 @@ export function SidebarLayout({
<div
ref={containerRef}
style={styles}
className={classNames(className, 'grid w-full h-full', !isResizing && 'transition-grid')}
className={classNames(className, "grid w-full h-full", !isResizing && "transition-grid")}
>
<div style={side} className="overflow-hidden">
{sidebar}

View File

@@ -1,16 +1,16 @@
import classNames from 'classnames';
import type { CSSProperties, ReactNode } from 'react';
import { useCallback, useMemo, useRef } from 'react';
import { useLocalStorage } from 'react-use';
import { useContainerSize } from '../hooks/useContainerSize';
import { clamp } from '../lib/clamp';
import type { ResizeHandleEvent } from './ResizeHandle';
import { ResizeHandle } from './ResizeHandle';
import classNames from "classnames";
import type { CSSProperties, ReactNode } from "react";
import { useCallback, useMemo, useRef } from "react";
import { useLocalStorage } from "react-use";
import { useContainerSize } from "../hooks/useContainerSize";
import { clamp } from "../lib/clamp";
import type { ResizeHandleEvent } from "./ResizeHandle";
import { ResizeHandle } from "./ResizeHandle";
export type SplitLayoutLayout = 'responsive' | 'horizontal' | 'vertical';
export type SplitLayoutLayout = "responsive" | "horizontal" | "vertical";
export interface SlotProps {
orientation: 'horizontal' | 'vertical';
orientation: "horizontal" | "vertical";
style: CSSProperties;
}
@@ -28,9 +28,9 @@ interface Props {
}
const baseProperties = { minWidth: 0 };
const areaL = { ...baseProperties, gridArea: 'left' };
const areaR = { ...baseProperties, gridArea: 'right' };
const areaD = { ...baseProperties, gridArea: 'drag' };
const areaL = { ...baseProperties, gridArea: "left" };
const areaR = { ...baseProperties, gridArea: "right" };
const areaD = { ...baseProperties, gridArea: "drag" };
const STACK_VERTICAL_WIDTH = 500;
@@ -40,7 +40,7 @@ export function SplitLayout({
secondSlot,
className,
storageKey,
layout = 'responsive',
layout = "responsive",
resizeHandleClassName,
defaultRatio = 0.5,
minHeightPx = 10,
@@ -59,7 +59,7 @@ export function SplitLayout({
const size = useContainerSize(containerRef);
const verticalBasedOnSize = size.width !== 0 && size.width < STACK_VERTICAL_WIDTH;
const vertical = layout !== 'horizontal' && (layout === 'vertical' || verticalBasedOnSize);
const vertical = layout !== "horizontal" && (layout === "vertical" || verticalBasedOnSize);
const styles = useMemo<CSSProperties>(() => {
return {
@@ -118,23 +118,23 @@ export function SplitLayout({
<div
ref={containerRef}
style={styles}
className={classNames(className, 'grid w-full h-full overflow-hidden')}
className={classNames(className, "grid w-full h-full overflow-hidden")}
>
{firstSlot({ style: areaL, orientation: vertical ? 'vertical' : 'horizontal' })}
{firstSlot({ style: areaL, orientation: vertical ? "vertical" : "horizontal" })}
{secondSlot && (
<>
<ResizeHandle
style={areaD}
className={classNames(
resizeHandleClassName,
vertical ? '-translate-y-1' : '-translate-x-1',
vertical ? "-translate-y-1" : "-translate-x-1",
)}
onResizeMove={handleResizeMove}
onReset={handleReset}
side={vertical ? 'top' : 'left'}
side={vertical ? "top" : "left"}
justify="center"
/>
{secondSlot({ style: areaR, orientation: vertical ? 'vertical' : 'horizontal' })}
{secondSlot({ style: areaR, orientation: vertical ? "vertical" : "horizontal" })}
</>
)}
</div>

View File

@@ -1,17 +1,17 @@
import classNames from 'classnames';
import type { ComponentType, ForwardedRef, HTMLAttributes, ReactNode } from 'react';
import { forwardRef } from 'react';
import classNames from "classnames";
import type { ComponentType, ForwardedRef, HTMLAttributes, ReactNode } from "react";
import { forwardRef } from "react";
const gapClasses = {
0: 'gap-0',
0.5: 'gap-0.5',
1: 'gap-1',
1.5: 'gap-1.5',
2: 'gap-2',
3: 'gap-3',
4: 'gap-4',
5: 'gap-5',
6: 'gap-6',
0: "gap-0",
0.5: "gap-0.5",
1: "gap-1",
1.5: "gap-1.5",
2: "gap-2",
3: "gap-3",
4: "gap-4",
5: "gap-5",
6: "gap-6",
};
interface HStackProps extends BaseStackProps {
@@ -19,14 +19,14 @@ interface HStackProps extends BaseStackProps {
}
export const HStack = forwardRef(function HStack(
{ className, space, children, alignItems = 'center', ...props }: HStackProps,
// biome-ignore lint/suspicious/noExplicitAny: none
{ className, space, children, alignItems = "center", ...props }: HStackProps,
// oxlint-disable-next-line no-explicit-any
ref: ForwardedRef<any>,
) {
return (
<BaseStack
ref={ref}
className={classNames(className, 'flex-row', space != null && gapClasses[space])}
className={classNames(className, "flex-row", space != null && gapClasses[space])}
alignItems={alignItems}
{...props}
>
@@ -41,13 +41,13 @@ export type VStackProps = BaseStackProps & {
export const VStack = forwardRef(function VStack(
{ className, space, children, ...props }: VStackProps,
// biome-ignore lint/suspicious/noExplicitAny: none
// oxlint-disable-next-line no-explicit-any
ref: ForwardedRef<any>,
) {
return (
<BaseStack
ref={ref}
className={classNames(className, 'flex-col', space != null && gapClasses[space])}
className={classNames(className, "flex-col", space != null && gapClasses[space])}
{...props}
>
{children}
@@ -56,34 +56,34 @@ export const VStack = forwardRef(function VStack(
});
type BaseStackProps = HTMLAttributes<HTMLElement> & {
as?: ComponentType | 'ul' | 'label' | 'form' | 'p';
as?: ComponentType | "ul" | "label" | "form" | "p";
space?: keyof typeof gapClasses;
alignItems?: 'start' | 'center' | 'stretch' | 'end';
justifyContent?: 'start' | 'center' | 'end' | 'between';
alignItems?: "start" | "center" | "stretch" | "end";
justifyContent?: "start" | "center" | "end" | "between";
wrap?: boolean;
};
const BaseStack = forwardRef(function BaseStack(
{ className, alignItems, justifyContent, wrap, children, as, ...props }: BaseStackProps,
// biome-ignore lint/suspicious/noExplicitAny: none
// oxlint-disable-next-line no-explicit-any
ref: ForwardedRef<any>,
) {
const Component = as ?? 'div';
const Component = as ?? "div";
return (
<Component
ref={ref}
className={classNames(
className,
'flex',
wrap && 'flex-wrap',
alignItems === 'center' && 'items-center',
alignItems === 'start' && 'items-start',
alignItems === 'stretch' && 'items-stretch',
alignItems === 'end' && 'items-end',
justifyContent === 'start' && 'justify-start',
justifyContent === 'center' && 'justify-center',
justifyContent === 'end' && 'justify-end',
justifyContent === 'between' && 'justify-between',
"flex",
wrap && "flex-wrap",
alignItems === "center" && "items-center",
alignItems === "start" && "items-start",
alignItems === "stretch" && "items-stretch",
alignItems === "end" && "items-end",
justifyContent === "start" && "justify-start",
justifyContent === "center" && "justify-center",
justifyContent === "end" && "justify-end",
justifyContent === "between" && "justify-between",
)}
{...props}
>

View File

@@ -1,5 +1,5 @@
import classNames from 'classnames';
import type { ReactNode } from 'react';
import classNames from "classnames";
import type { ReactNode } from "react";
export function Table({
children,
@@ -13,13 +13,13 @@ export function Table({
style?: React.CSSProperties;
}) {
return (
<div style={style} className={classNames('w-full', scrollable && 'h-full overflow-y-auto')}>
<div style={style} className={classNames("w-full", scrollable && "h-full overflow-y-auto")}>
<table
className={classNames(
className,
'w-full text-sm mb-auto min-w-full max-w-full',
'border-separate border-spacing-0',
scrollable && '[&_thead]:sticky [&_thead]:top-0 [&_thead]:z-10',
"w-full text-sm mb-auto min-w-full max-w-full",
"border-separate border-spacing-0",
scrollable && "[&_thead]:sticky [&_thead]:top-0 [&_thead]:z-10",
)}
>
{children}
@@ -41,7 +41,7 @@ export function TableHead({ children, className }: { children: ReactNode; classN
<thead
className={classNames(
className,
'bg-surface [&_th]:border-b [&_th]:border-b-surface-highlight',
"bg-surface [&_th]:border-b [&_th]:border-b-surface-highlight",
)}
>
{children}
@@ -56,18 +56,18 @@ export function TableRow({ children }: { children: ReactNode }) {
export function TableCell({
children,
className,
align = 'left',
align = "left",
}: {
children: ReactNode;
className?: string;
align?: 'left' | 'center' | 'right';
align?: "left" | "center" | "right";
}) {
return (
<td
className={classNames(
className,
'py-2 [&:not(:first-child)]:pl-4 whitespace-nowrap',
align === 'left' ? 'text-left' : align === 'center' ? 'text-center' : 'text-right',
"py-2 [&:not(:first-child)]:pl-4 whitespace-nowrap",
align === "left" ? "text-left" : align === "center" ? "text-center" : "text-right",
)}
>
{children}
@@ -83,7 +83,7 @@ export function TruncatedWideTableCell({
className?: string;
}) {
return (
<TableCell className={classNames(className, 'truncate max-w-0 w-full')}>{children}</TableCell>
<TableCell className={classNames(className, "truncate max-w-0 w-full")}>{children}</TableCell>
);
}
@@ -98,7 +98,7 @@ export function TableHeaderCell({
<th
className={classNames(
className,
'py-2 [&:not(:first-child)]:pl-4 text-left text-text-subtle',
"py-2 [&:not(:first-child)]:pl-4 text-left text-text-subtle",
)}
>
{children}

View File

@@ -1,8 +1,8 @@
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import classNames from 'classnames';
import { useState } from 'react';
import { WINDOW_CONTROLS_WIDTH } from '../lib/constants';
import { Button } from './Button';
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import classNames from "classnames";
import { useState } from "react";
import { WINDOW_CONTROLS_WIDTH } from "../lib/constants";
import { Button } from "./Button";
interface Props {
className?: string;
@@ -12,17 +12,26 @@ interface Props {
useNativeTitlebar: boolean;
}
export function WindowControls({ className, onlyX, osType, hideWindowControls, useNativeTitlebar }: Props) {
export function WindowControls({
className,
onlyX,
osType,
hideWindowControls,
useNativeTitlebar,
}: Props) {
const [maximized, setMaximized] = useState<boolean>(false);
// Never show controls on macOS or if hideWindowControls is true
if (osType === 'macos' || hideWindowControls || useNativeTitlebar) {
if (osType === "macos" || hideWindowControls || useNativeTitlebar) {
return null;
}
return (
<div
className={classNames(className, 'ml-4 absolute right-0 top-0 bottom-0 flex items-center justify-end')}
className={classNames(
className,
"ml-4 absolute right-0 top-0 bottom-0 flex items-center justify-end",
)}
style={{ width: WINDOW_CONTROLS_WIDTH }}
data-tauri-drag-region
>

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,24 +20,19 @@ import {
useMemo,
useRef,
useState,
} from 'react';
import { useKey, useKeyPressEvent } from 'react-use';
import { computeSideForDragMove } from '../../lib/dnd';
import { useStore } from 'jotai';
import {
draggingIdsFamily,
focusIdsFamily,
hoveredParentFamily,
selectedIdsFamily,
} from './atoms';
import { type CollapsedAtom, CollapsedAtomContext } from './context';
import type { ContextMenuRenderer, JotaiStore, 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 "react";
import { useKey, useKeyPressEvent } from "react-use";
import { computeSideForDragMove } from "../../lib/dnd";
import { useStore } from "jotai";
import { draggingIdsFamily, focusIdsFamily, hoveredParentFamily, selectedIdsFamily } from "./atoms";
import { type CollapsedAtom, CollapsedAtomContext } from "./context";
import type { ContextMenuRenderer, JotaiStore, 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 } };
@@ -137,7 +132,7 @@ function TreeInner<T extends { id: string }>(
return false;
}
$el.focus();
$el.scrollIntoView({ block: 'nearest' });
$el.scrollIntoView({ block: "nearest" });
return true;
}, []);
@@ -239,7 +234,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 = store.get(focusIdsFamily(treeId)).anchorId;
const selectedIdsAtom = selectedIdsFamily(treeId);
@@ -279,7 +274,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
@@ -297,7 +292,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);
@@ -348,7 +343,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();
@@ -359,7 +354,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();
@@ -371,7 +366,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();
@@ -397,7 +392,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();
@@ -420,7 +415,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 = store.get(focusIdsFamily(treeId)).lastId;
@@ -484,11 +479,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;
}
@@ -618,7 +613,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,
@@ -667,24 +662,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

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,10 +1,10 @@
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import { memo } from 'react';
import { DropMarker } from '../DropMarker';
import { hoveredParentDepthFamily, isIndexHoveredFamily } from './atoms';
import type { TreeNode } from './common';
import { useIsCollapsed } from './context';
import classNames from "classnames";
import { useAtomValue } from "jotai";
import { memo } from "react";
import { DropMarker } from "../DropMarker";
import { hoveredParentDepthFamily, isIndexHoveredFamily } from "./atoms";
import type { TreeNode } from "./common";
import { useIsCollapsed } from "./context";
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,
@@ -19,11 +19,11 @@ export const TreeIndentGuide = memo(function TreeIndentGuide({
<div className="flex">
{Array.from({ length: depth }).map((_, i) => (
<div
// biome-ignore lint/suspicious/noArrayIndexKey: none
// 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,22 +1,27 @@
import type { DragMoveEvent } from '@dnd-kit/core';
import { useDndContext, useDndMonitor, useDraggable, useDroppable } from '@dnd-kit/core';
import classNames from 'classnames';
import { useAtomValue, useStore } from 'jotai';
import type { DragMoveEvent } from "@dnd-kit/core";
import { useDndContext, useDndMonitor, useDraggable, useDroppable } from "@dnd-kit/core";
import classNames from "classnames";
import { useAtomValue, useStore } from "jotai";
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 { Icon } from '../Icon';
import { isLastFocusedFamily, isSelectedFamily } from './atoms';
import { useCollapsedAtom, useIsAncestorCollapsed, useIsCollapsed, useSetCollapsed } from './context';
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 { Icon } from "../Icon";
import { isLastFocusedFamily, isSelectedFamily } from "./atoms";
import {
useCollapsedAtom,
useIsAncestorCollapsed,
useIsCollapsed,
useSetCollapsed,
} from "./context";
import type { TreeNode } from "./common";
import { getNodeKey } from "./common";
import type { TreeProps } from "./Tree";
import { TreeIndentGuide } from "./TreeIndentGuide";
export interface TreeItemClickEvent {
shiftKey: boolean;
@@ -26,7 +31,13 @@ export interface TreeItemClickEvent {
export type TreeItemProps<T extends { id: string }> = Pick<
TreeProps<T>,
'ItemInner' | 'ItemLeftSlotInner' | 'ItemRightSlot' | 'treeId' | 'getEditOptions' | 'getItemKey' | 'renderContextMenu'
| "ItemInner"
| "ItemLeftSlotInner"
| "ItemRightSlot"
| "treeId"
| "getEditOptions"
| "getItemKey"
| "renderContextMenu"
> & {
node: TreeNode<T>;
className?: string;
@@ -69,7 +80,7 @@ function TreeItem_<T extends { id: string }>({
const setCollapsed = useSetCollapsed(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>(
() => ({
@@ -89,7 +100,7 @@ function TreeItem_<T extends { id: string }>({
return listItemRef.current.getBoundingClientRect();
},
scrollIntoView: () => {
listItemRef.current?.scrollIntoView({ block: 'nearest' });
listItemRef.current?.scrollIntoView({ block: "nearest" });
},
}),
[editing, getEditOptions],
@@ -154,13 +165,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);
@@ -201,8 +212,8 @@ function TreeItem_<T extends { id: string }>({
const hasChildren = (node.children?.length ?? 0) > 0;
const collapsedMap = store.get(collapsedAtom);
const itemCollapsed = !!collapsedMap[node.item.id];
if (itemCollapsed && isFolder && hasChildren && side === 'after') {
setDropHover('animate');
if (itemCollapsed && isFolder && hasChildren && side === "after") {
setDropHover("animate");
clearTimeout(startedHoverTimeout.current);
startedHoverTimeout.current = setTimeout(() => {
store.set(collapsedAtom, { ...store.get(collapsedAtom), [node.item.id]: false });
@@ -214,8 +225,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();
}
@@ -231,7 +242,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);
@@ -243,7 +254,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);
}, []);
@@ -283,20 +294,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 &&
@@ -313,12 +324,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,22 @@
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' | 'renderContextMenu'
| "ItemInner"
| "ItemLeftSlotInner"
| "ItemRightSlot"
| "treeId"
| "getItemKey"
| "getEditOptions"
| "renderContextMenu"
> &
Pick<TreeItemProps<T>, 'onClick' | 'getContextMenu'> & {
Pick<TreeItemProps<T>, "onClick" | "getContextMenu"> & {
nodes: SelectableTreeNode<T>[];
style?: CSSProperties;
className?: string;

View File

@@ -1,5 +1,5 @@
import { atom } from 'jotai';
import { atomFamily, selectAtom } from 'jotai/utils';
import { atom } from "jotai";
import { atomFamily, selectAtom } from "jotai/utils";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const selectedIdsFamily = atomFamily((_treeId: string) => {
@@ -57,7 +57,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(
@@ -73,4 +73,3 @@ export const hoveredParentDepthFamily = atomFamily((treeId: string) =>
(a, b) => Object.is(a, b), // prevents re-render unless the value changes
),
);

View File

@@ -1,7 +1,7 @@
import type { createStore } from 'jotai';
import type { ReactNode } from 'react';
import type { CollapsedAtom } from './context';
import { selectedIdsFamily } from './atoms';
import type { createStore } from "jotai";
import type { ReactNode } from "react";
import type { CollapsedAtom } from "./context";
import { selectedIdsFamily } from "./atoms";
export type JotaiStore = ReturnType<typeof createStore>;
@@ -71,7 +71,11 @@ export function hasAncestor<T extends { id: string }>(node: TreeNode<T>, ancesto
return hasAncestor(node.parent, ancestorId);
}
export function isVisibleNode<T extends { id: string }>(store: JotaiStore, collapsedAtom: CollapsedAtom, node: TreeNode<T>) {
export function isVisibleNode<T extends { id: string }>(
store: JotaiStore,
collapsedAtom: CollapsedAtom,
node: TreeNode<T>,
) {
const collapsed = store.get(collapsedAtom);
let p = node.parent;
while (p) {

View File

@@ -1,7 +1,7 @@
import type { WritableAtom } from 'jotai';
import { useAtomValue, useStore } from 'jotai';
import { selectAtom } from 'jotai/utils';
import { createContext, useCallback, useContext, useMemo } from 'react';
import type { WritableAtom } from "jotai";
import { useAtomValue, useStore } from "jotai";
import { selectAtom } from "jotai/utils";
import { createContext, useCallback, useContext, useMemo } from "react";
type CollapsedMap = Record<string, boolean>;
type SetAction = CollapsedMap | ((prev: CollapsedMap) => CollapsedMap);
@@ -11,14 +11,14 @@ export const CollapsedAtomContext = createContext<CollapsedAtom | null>(null);
export function useCollapsedAtom(): CollapsedAtom {
const atom = useContext(CollapsedAtomContext);
if (!atom) throw new Error('CollapsedAtomContext not provided');
if (!atom) throw new Error("CollapsedAtomContext not provided");
return atom;
}
export function useIsCollapsed(itemId: string | undefined) {
const collapsedAtom = useCollapsedAtom();
const derivedAtom = useMemo(
() => selectAtom(collapsedAtom, (map) => !!map[itemId ?? 'n/a'], Object.is),
() => selectAtom(collapsedAtom, (map) => !!map[itemId ?? "n/a"], Object.is),
[collapsedAtom, itemId],
);
return useAtomValue(derivedAtom);
@@ -29,10 +29,10 @@ export function useSetCollapsed(itemId: string | undefined) {
const store = useStore();
return useCallback(
(next: boolean | ((prev: boolean) => boolean)) => {
const key = itemId ?? 'n/a';
const key = itemId ?? "n/a";
const prevMap = store.get(collapsedAtom);
const prevValue = !!prevMap[key];
const value = typeof next === 'function' ? next(prevValue) : next;
const value = typeof next === "function" ? next(prevValue) : next;
if (value === prevValue) return;
store.set(collapsedAtom, { ...prevMap, [key]: value });
},

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(() => {

View File

@@ -1,5 +1,5 @@
import type { RefObject } from 'react';
import { useLayoutEffect, useState } from 'react';
import type { RefObject } from "react";
import { useLayoutEffect, useState } from "react";
export function useContainerSize(ref: RefObject<HTMLElement | null>) {
const [size, setSize] = useState<{ width: number; height: number }>({ width: 0, height: 0 });

View File

@@ -1,6 +1,6 @@
import { debounce } from '@yaakapp-internal/lib';
import type { Dispatch, SetStateAction } from 'react';
import { useMemo, useState } from 'react';
import { debounce } from "@yaakapp-internal/lib";
import type { Dispatch, SetStateAction } from "react";
import { useMemo, useState } from "react";
export function useDebouncedState<T>(
defaultValue: T,

View File

@@ -1,5 +1,5 @@
import { useEffect } from 'react';
import { useDebouncedState } from './useDebouncedState';
import { useEffect } from "react";
import { useDebouncedState } from "./useDebouncedState";
export function useDebouncedValue<T>(value: T, delay = 500) {
const [state, setState] = useDebouncedState<T>(value, delay);

View File

@@ -1,7 +1,7 @@
import { useQuery } from '@tanstack/react-query';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import { useWindowSize } from 'react-use';
import { useDebouncedValue } from './useDebouncedValue';
import { useQuery } from "@tanstack/react-query";
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { useWindowSize } from "react-use";
import { useDebouncedValue } from "./useDebouncedValue";
export function useIsFullscreen() {
const windowSize = useWindowSize();
@@ -13,7 +13,7 @@ export function useIsFullscreen() {
return (
useQuery({
queryKey: ['is_fullscreen', debouncedWindowWidth],
queryKey: ["is_fullscreen", debouncedWindowWidth],
queryFn: async () => {
return getCurrentWebviewWindow().isFullscreen();
},

View File

@@ -1,6 +1,6 @@
import { useRef } from 'react';
import { useRef } from "react";
const PORTAL_CONTAINER_ID = 'react-portal';
const PORTAL_CONTAINER_ID = "react-portal";
export function usePortal(name: string) {
const ref = useRef(getOrCreatePortal(name));
@@ -10,15 +10,15 @@ export function usePortal(name: string) {
function getOrCreatePortal(name: string) {
let portalContainer = document.getElementById(PORTAL_CONTAINER_ID);
if (!portalContainer) {
portalContainer = document.createElement('div');
portalContainer = document.createElement("div");
portalContainer.id = PORTAL_CONTAINER_ID;
document.body.appendChild(portalContainer);
}
let existing = portalContainer.querySelector(`:scope > [data-portal-name="${name}"]`);
if (!existing) {
const el = document.createElement('div');
el.setAttribute('data-portal-name', name);
const el = document.createElement("div");
el.setAttribute("data-portal-name", name);
portalContainer.appendChild(el);
existing = el;
}

View File

@@ -1,5 +1,5 @@
import { useRef, useState } from 'react';
import { useUnmount } from 'react-use';
import { useRef, useState } from "react";
import { useUnmount } from "react-use";
/** Returns a boolean that is true for a given number of milliseconds. */
export function useTimedBoolean(millis = 1500): [boolean, () => void] {

View File

@@ -1,26 +1,26 @@
export type { BannerProps } from './components/Banner';
export { Banner } from './components/Banner';
export type { ButtonProps } from './components/Button';
export { Button } from './components/Button';
export { DropMarker } from './components/DropMarker';
export { HeaderSize } from './components/HeaderSize';
export { Heading } from './components/Heading';
export type { IconProps } from './components/Icon';
export { Icon } from './components/Icon';
export type { IconButtonProps } from './components/IconButton';
export { IconButton } from './components/IconButton';
export { FormattedError } from './components/FormattedError';
export { InlineCode } from './components/InlineCode';
export { LoadingIcon } from './components/LoadingIcon';
export { Overlay } from './components/Overlay';
export { Portal } from './components/Portal';
export type { ResizeHandleEvent } from './components/ResizeHandle';
export { ResizeHandle } from './components/ResizeHandle';
export { SidebarLayout } from './components/SidebarLayout';
export type { SlotProps, SplitLayoutLayout } from './components/SplitLayout';
export { SplitLayout } from './components/SplitLayout';
export type { VStackProps } from './components/Stacks';
export { HStack, VStack } from './components/Stacks';
export type { BannerProps } from "./components/Banner";
export { Banner } from "./components/Banner";
export type { ButtonProps } from "./components/Button";
export { Button } from "./components/Button";
export { DropMarker } from "./components/DropMarker";
export { HeaderSize } from "./components/HeaderSize";
export { Heading } from "./components/Heading";
export type { IconProps } from "./components/Icon";
export { Icon } from "./components/Icon";
export type { IconButtonProps } from "./components/IconButton";
export { IconButton } from "./components/IconButton";
export { FormattedError } from "./components/FormattedError";
export { InlineCode } from "./components/InlineCode";
export { LoadingIcon } from "./components/LoadingIcon";
export { Overlay } from "./components/Overlay";
export { Portal } from "./components/Portal";
export type { ResizeHandleEvent } from "./components/ResizeHandle";
export { ResizeHandle } from "./components/ResizeHandle";
export { SidebarLayout } from "./components/SidebarLayout";
export type { SlotProps, SplitLayoutLayout } from "./components/SplitLayout";
export { SplitLayout } from "./components/SplitLayout";
export type { VStackProps } from "./components/Stacks";
export { HStack, VStack } from "./components/Stacks";
export {
Table,
TableBody,
@@ -29,20 +29,20 @@ export {
TableHeaderCell,
TableRow,
TruncatedWideTableCell,
} from './components/Table';
export { isSelectedFamily, selectedIdsFamily } from './components/tree/atoms';
export type { TreeNode } from './components/tree/common';
export type { TreeHandle, TreeProps } from './components/tree/Tree';
export { Tree } from './components/tree/Tree';
export type { TreeItemProps } from './components/tree/TreeItem';
export { WindowControls } from './components/WindowControls';
export { useContainerSize } from './hooks/useContainerSize';
export { useDebouncedState } from './hooks/useDebouncedState';
export { useDebouncedValue } from './hooks/useDebouncedValue';
export { useIsFullscreen } from './hooks/useIsFullscreen';
export { usePortal } from './hooks/usePortal';
export { useTimedBoolean } from './hooks/useTimedBoolean';
export { clamp } from './lib/clamp';
export { HEADER_SIZE_LG, HEADER_SIZE_MD, WINDOW_CONTROLS_WIDTH } from './lib/constants';
export { computeSideForDragMove } from './lib/dnd';
export { minPromiseMillis } from './lib/minPromiseMillis';
} from "./components/Table";
export { isSelectedFamily, selectedIdsFamily } from "./components/tree/atoms";
export type { TreeNode } from "./components/tree/common";
export type { TreeHandle, TreeProps } from "./components/tree/Tree";
export { Tree } from "./components/tree/Tree";
export type { TreeItemProps } from "./components/tree/TreeItem";
export { WindowControls } from "./components/WindowControls";
export { useContainerSize } from "./hooks/useContainerSize";
export { useDebouncedState } from "./hooks/useDebouncedState";
export { useDebouncedValue } from "./hooks/useDebouncedValue";
export { useIsFullscreen } from "./hooks/useIsFullscreen";
export { usePortal } from "./hooks/usePortal";
export { useTimedBoolean } from "./hooks/useTimedBoolean";
export { clamp } from "./lib/clamp";
export { HEADER_SIZE_LG, HEADER_SIZE_MD, WINDOW_CONTROLS_WIDTH } from "./lib/constants";
export { computeSideForDragMove } from "./lib/dnd";
export { minPromiseMillis } from "./lib/minPromiseMillis";

View File

@@ -1,4 +1,4 @@
export const HEADER_SIZE_MD = '30px';
export const HEADER_SIZE_LG = '40px';
export const HEADER_SIZE_MD = "30px";
export const HEADER_SIZE_LG = "40px";
export const WINDOW_CONTROLS_WIDTH = '10.5rem';
export const WINDOW_CONTROLS_WIDTH = "10.5rem";

View File

@@ -1,10 +1,10 @@
import type { DragMoveEvent } from '@dnd-kit/core';
import type { DragMoveEvent } from "@dnd-kit/core";
export function computeSideForDragMove(
id: string,
e: DragMoveEvent,
orientation: 'vertical' | 'horizontal' = 'vertical',
): 'before' | 'after' | null {
orientation: "vertical" | "horizontal" = "vertical",
): "before" | "after" | null {
if (e.over == null || e.over.id !== id) {
return null;
}
@@ -12,7 +12,7 @@ export function computeSideForDragMove(
const overRect = e.over.rect;
if (orientation === 'horizontal') {
if (orientation === "horizontal") {
// For horizontal layouts (tabs side-by-side), use left/right logic
const activeLeft =
e.active.rect.current.translated?.left ?? e.active.rect.current.initial.left + e.delta.x;
@@ -22,7 +22,7 @@ export function computeSideForDragMove(
const hoverRight = overRect.right;
const hoverMiddleX = hoverLeft + (hoverRight - hoverLeft) / 2;
return pointerX < hoverMiddleX ? 'before' : 'after'; // 'before' = left, 'after' = right
return pointerX < hoverMiddleX ? "before" : "after"; // 'before' = left, 'after' = right
} else {
// For vertical layouts, use top/bottom logic
const activeTop =
@@ -34,6 +34,6 @@ export function computeSideForDragMove(
const hoverMiddleY = (hoverBottom - hoverTop) / 2;
const hoverClientY = pointerY - hoverTop;
return hoverClientY < hoverMiddleY ? 'before' : 'after';
return hoverClientY < hoverMiddleY ? "before" : "after";
}
}