Move portal to shared ui lib

This commit is contained in:
Gregory Schier
2026-03-12 14:28:15 -07:00
parent 47f0daabff
commit cc504e0a1c
10 changed files with 52 additions and 14 deletions

View File

@@ -1,90 +0,0 @@
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;
portalName: string;
open: boolean;
onClose?: () => void;
zIndex?: keyof typeof zIndexes;
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',
};
export function Overlay({
variant = 'default',
zIndex = 30,
open,
onClose,
portalName,
noBackdrop,
children,
}: Props) {
const containerRef = useRef<HTMLDivElement>(null);
if (noBackdrop) {
return (
<Portal name={portalName}>
{open && (
<FocusTrap focusTrapOptions={{ clickOutsideDeactivates: true }}>
{/* NOTE: <div> wrapper is required for some reason, or FocusTrap complains */}
<div>{children}</div>
</FocusTrap>
)}
</Portal>
);
}
return (
<Portal name={portalName}>
{open && (
<FocusTrap
focusTrapOptions={{
// Allow outside click so we can click things like toasts
allowOutsideClick: true,
delayInitialFocus: true,
checkCanFocusTrap: async () => {
// Not sure why delayInitialFocus: true doesn't help, but having this no-op promise
// seems to be required to make things work.
},
}}
>
<m.div
ref={containerRef}
className={classNames('fixed inset-0', zIndexes[zIndex])}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
<div
aria-hidden
onClick={onClose}
className={classNames(
'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' && (
<div data-tauri-drag-region className="absolute top-0 left-0 h-md right-0" />
)}
{children}
</m.div>
</FocusTrap>
)}
</Portal>
);
}

View File

@@ -1,13 +0,0 @@
import type { ReactNode } from 'react';
import { createPortal } from 'react-dom';
import { usePortal } from '../hooks/usePortal';
interface Props {
children: ReactNode;
name: string;
}
export function Portal({ children, name }: Props) {
const portal = usePortal(name);
return createPortal(children, portal);
}

View File

@@ -4,7 +4,7 @@ import type { ReactNode } from 'react';
import { hideToast, toastsAtom } from '../lib/toast';
import { Toast, type ToastProps } from './core/Toast';
import { ErrorBoundary } from './ErrorBoundary';
import { Portal } from './Portal';
import { Portal } from '@yaakapp-internal/ui';
export type ToastInstance = {
id: string;

View File

@@ -1,5 +1,6 @@
import { type } from '@tauri-apps/plugin-os';
import { settingsAtom, workspacesAtom } from '@yaakapp-internal/models';
import { HeaderSize, Overlay, SidebarLayout } from '@yaakapp-internal/ui';
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import * as m from 'motion/react-m';
@@ -39,15 +40,12 @@ import { HStack } from './core/Stacks';
import { ErrorBoundary } from './ErrorBoundary';
import { FolderLayout } from './FolderLayout';
import { GrpcConnectionLayout } from './GrpcConnectionLayout';
import { HeaderSize, SidebarLayout } from '@yaakapp-internal/ui';
import { HttpRequestLayout } from './HttpRequestLayout';
import { Overlay } from './Overlay';
import Sidebar from './Sidebar';
import { SidebarActions } from './SidebarActions';
import { WebsocketRequestLayout } from './WebsocketRequestLayout';
import { WorkspaceHeader } from './WorkspaceHeader';
const head = { gridArea: 'head' };
const body = { gridArea: 'body' };
export function Workspace() {
@@ -85,10 +83,7 @@ export function Workspace() {
interfaceScale={settings.interfaceScale}
>
<div className="absolute inset-0 pointer-events-none">
<div
style={environmentBgStyle}
className="absolute inset-0 opacity-[0.07]"
/>
<div style={environmentBgStyle} className="absolute inset-0 opacity-[0.07]" />
<div
style={environmentBgStyle}
className="absolute left-0 right-0 -bottom-[1px] h-[1px] opacity-20"
@@ -132,7 +127,15 @@ export function Workspace() {
'grid grid-rows-[auto_1fr]',
)}
>
<HeaderSize hideControls size="lg" className="border-transparent flex items-center" osType={osType} hideWindowControls={settings.hideWindowControls} useNativeTitlebar={settings.useNativeTitlebar} interfaceScale={settings.interfaceScale}>
<HeaderSize
hideControls
size="lg"
className="border-transparent flex items-center"
osType={osType}
hideWindowControls={settings.hideWindowControls}
useNativeTitlebar={settings.useNativeTitlebar}
interfaceScale={settings.interfaceScale}
>
<SidebarActions />
</HeaderSize>
<ErrorBoundary name="Sidebar (Floating)">

View File

@@ -2,7 +2,7 @@ import classNames from 'classnames';
import * as m from 'motion/react-m';
import type { ReactNode } from 'react';
import { useMemo } from 'react';
import { Overlay } from '../Overlay';
import { Overlay } from '@yaakapp-internal/ui';
import { Heading } from './Heading';
import { IconButton } from './IconButton';
import type { DialogSize } from '@yaakapp-internal/plugins';

View File

@@ -32,7 +32,7 @@ import { generateId } from '../../lib/generateId';
import { getNodeText } from '../../lib/getNodeText';
import { jotaiStore } from '../../lib/jotai';
import { ErrorBoundary } from '../ErrorBoundary';
import { Overlay } from '../Overlay';
import { Overlay } from '@yaakapp-internal/ui';
import { Button } from './Button';
import { Hotkey } from './Hotkey';
import { Icon, LoadingIcon, type IconProps } from '@yaakapp-internal/ui';

View File

@@ -2,7 +2,7 @@ import classNames from 'classnames';
import type { CSSProperties, KeyboardEvent, ReactNode } from 'react';
import { useRef, useState } from 'react';
import { generateId } from '../../lib/generateId';
import { Portal } from '../Portal';
import { Portal } from '@yaakapp-internal/ui';
export interface TooltipProps {
children: ReactNode;

View File

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