feat: menu pinning

This commit is contained in:
Jade (Rose) Rowland 2024-10-13 23:24:54 -04:00
parent a91f4576f3
commit 96802a271c
7 changed files with 141 additions and 85 deletions

View File

@ -1,5 +1,5 @@
import Loader from '@src/repl/components/Loader'; import Loader from '@src/repl/components/Loader';
import { Panel } from '@src/repl/components/panel/Panel'; import { HorizontalPanel, } from '@src/repl/components/panel/Panel';
import { Code } from '@src/repl/components/Code'; import { Code } from '@src/repl/components/Code';
import BigPlayButton from '@src/repl/components/BigPlayButton'; import BigPlayButton from '@src/repl/components/BigPlayButton';
import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage'; import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage';
@ -20,7 +20,7 @@ export default function UdelsEditor(Props) {
<Code containerRef={containerRef} editorRef={editorRef} init={init} /> <Code containerRef={containerRef} editorRef={editorRef} init={init} />
</div> </div>
<UserFacingErrorMessage error={error} /> <UserFacingErrorMessage error={error} />
<Panel context={context} /> <HorizontalPanel context={context} />
</div> </div>
); );
} }

View File

@ -1,5 +1,5 @@
import Loader from '@src/repl/components/Loader'; import Loader from '@src/repl/components/Loader';
import { Panel } from '@src/repl/components/panel/Panel'; import { HorizontalPanel, VerticalPanel } from '@src/repl/components/panel/Panel';
import { Code } from '@src/repl/components/Code'; import { Code } from '@src/repl/components/Code';
import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage'; import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage';
import { Header } from './Header'; import { Header } from './Header';
@ -15,16 +15,17 @@ export default function ReplEditor(Props) {
const settings = useSettings(); const settings = useSettings();
const { panelPosition } = settings; const { panelPosition } = settings;
return ( return (
<div className="h-full flex flex-col relative"> <div className="h-full flex flex-col relative">
<Loader active={pending} /> <Loader active={pending} />
<Header context={context} /> <Header context={context} />
<div className="grow flex relative overflow-hidden"> <div className="grow flex relative overflow-hidden">
<Code containerRef={containerRef} editorRef={editorRef} init={init} /> <Code containerRef={containerRef} editorRef={editorRef} init={init} />
{panelPosition === 'right' && <Panel context={context} />} {panelPosition === 'right' && <VerticalPanel context={context} />}
</div> </div>
<UserFacingErrorMessage error={error} /> <UserFacingErrorMessage error={error} />
{panelPosition === 'bottom' && <Panel context={context} />} {panelPosition === 'bottom' && <HorizontalPanel context={context} />}
</div> </div>
); );
} }

View File

@ -2,7 +2,7 @@ import cx from '@src/cx.mjs';
export function ConsoleTab({ log }) { export function ConsoleTab({ log }) {
return ( return (
<div id="console-tab" className="break-all px-4 dark:text-white text-stone-900 text-sm"> <div id="console-tab" className="break-all px-4 dark:text-white text-stone-900 text-sm py-4">
<pre aria-hidden="true">{`███████╗████████╗██████╗ ██╗ ██╗██████╗ ███████╗██╗ <pre aria-hidden="true">{`███████╗████████╗██████╗ ██╗ ██╗██████╗ ███████╗██╗

View File

@ -1,10 +1,9 @@
import XMarkIcon from '@heroicons/react/20/solid/XMarkIcon';
import { logger } from '@strudel/core'; import { logger } from '@strudel/core';
import useEvent from '@src/useEvent.mjs'; import useEvent from '@src/useEvent.mjs';
import cx from '@src/cx.mjs'; import cx from '@src/cx.mjs';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { useCallback, useLayoutEffect, useEffect, useRef, useState } from 'react'; import { useCallback, useState } from 'react';
import { setActiveFooter, useSettings } from '../../../settings.mjs'; import { setActiveFooter as setTab, useSettings } from '../../../settings.mjs';
import { ConsoleTab } from './ConsoleTab'; import { ConsoleTab } from './ConsoleTab';
import { FilesTab } from './FilesTab'; import { FilesTab } from './FilesTab';
import { Reference } from './Reference'; import { Reference } from './Reference';
@ -12,33 +11,84 @@ import { SettingsTab } from './SettingsTab';
import { SoundsTab } from './SoundsTab'; import { SoundsTab } from './SoundsTab';
import { WelcomeTab } from './WelcomeTab'; import { WelcomeTab } from './WelcomeTab';
import { PatternsTab } from './PatternsTab'; import { PatternsTab } from './PatternsTab';
import useClient from '@src/useClient.mjs'; import { ChevronLeftIcon } from '@heroicons/react/16/solid';
// https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85
export const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
const TAURI = typeof window !== 'undefined' && window.__TAURI__; const TAURI = typeof window !== 'undefined' && window.__TAURI__;
export function Panel({ context }) { export function HorizontalPanel({ context }) {
const footerContent = useRef(); const [pinned, setPinned] = useState(true);
const settings = useSettings();
const tab = settings.activeFooter;
return (
<PanelNav
className={cx(
'hover:max-h-[360px] hover:min-h-[360px] flex flex-col overflow-hidden ',
pinned ? `min-h-[360px] max-h-[360px]` : 'min-h-8 max-h-8',
)}
>
<div className={cx('group-hover:flex flex-col h-full overflow-auto', pinned ? 'flex' : 'hidden')}>
<PanelContent context={context} tab={tab} />
</div>
<div className="flex justify-between min-h-8 max-h-8">
<Tabs setTab={setTab} tab={tab} pinned={pinned} />
<PinButton pinned={pinned} setPinned={setPinned} />
</div>
</PanelNav>
);
}
export function VerticalPanel({ context }) {
const [pinned, setPinned] = useState(true);
const settings = useSettings();
const tab = settings.activeFooter;
return (
<PanelNav
className={cx(
'hover:min-w-[600px] hover:max-w-[600px]',
pinned ? `min-w-[600px] max-w-[600px]` : ' min-w-8 max-w-8',
)}
>
<div className={cx('group-hover:flex flex-col h-full', pinned ? 'flex' : 'hidden')}>
<div className="flex justify-between">
<Tabs setTab={setTab} tab={tab} pinned={pinned} />
<PinButton pinned={pinned} setPinned={setPinned} />
</div>
<div className="overflow-auto">
<PanelContent context={context} tab={tab} />
</div>
</div>
<div className={cx(pinned ? 'hidden' : 'flex flex-col items-center justify-center h-full group-hover:hidden ')}>
<ChevronLeftIcon className="text-foreground opacity-50 w-6 h-6" />
</div>
</PanelNav>
);
}
const tabNames = {
welcome: 'intro',
patterns: 'patterns',
sounds: 'sounds',
console: 'console',
reference: 'reference',
settings: 'settings',
};
if (TAURI) {
tabNames.files = 'files';
}
function PanelNav({ children, className }) {
return (
<nav aria-label="Settings Menu" className={cx('bg-lineHighlight group transition-all', className)}>
{children}
</nav>
);
}
function PanelContent({ context, tab }) {
const [log, setLog] = useState([]); const [log, setLog] = useState([]);
const { activeFooter, isZen, panelPosition } = useSettings();
useIsomorphicLayoutEffect(() => {
if (footerContent.current && activeFooter === 'console') {
// scroll log box to bottom when log changes
footerContent.current.scrollTop = footerContent.current?.scrollHeight;
}
}, [log, activeFooter]);
useIsomorphicLayoutEffect(() => {
if (!footerContent.current) {
} else if (activeFooter === 'console') {
footerContent.current.scrollTop = footerContent.current?.scrollHeight;
} else {
footerContent.current.scrollTop = 0;
}
}, [activeFooter]);
useLogger( useLogger(
useCallback((e) => { useCallback((e) => {
const { message, type, data } = e.detail; const { message, type, data } = e.detail;
@ -60,66 +110,71 @@ export function Panel({ context }) {
}, []), }, []),
); );
const PanelTab = ({ children, name, label }) => ( switch (tab) {
case tabNames.patterns:
return <PatternsTab context={context} />;
case tabNames.console:
return <ConsoleTab log={log} />;
case tabNames.sounds:
return <SoundsTab />;
case tabNames.reference:
return <Reference />;
case tabNames.settings:
return <SettingsTab started={context.started} />;
case tabNames.files:
return <FilesTab />;
default:
return <WelcomeTab context={context} />;
}
}
function PanelTab({ label, isSelected, onClick }) {
return (
<> <>
<div <div
onClick={() => setActiveFooter(name)} onClick={onClick}
className={cx( className={cx(
'h-8 px-2 text-foreground cursor-pointer hover:opacity-50 flex items-center space-x-1 border-b', 'h-8 px-2 text-foreground cursor-pointer hover:opacity-50 flex items-center space-x-1 border-b',
activeFooter === name ? 'border-foreground' : 'border-transparent', isSelected ? 'border-foreground' : 'border-transparent',
)} )}
> >
{label || name} {label}
</div> </div>
{activeFooter === name && <>{children}</>}
</> </>
); );
const client = useClient(); }
if (isZen) { function Tabs({ setTab, tab }) {
return null;
}
const isActive = activeFooter !== '';
let positions = {
right: cx('max-w-full flex-grow-0 flex-none overflow-hidden', isActive ? 'w-[600px] h-full' : 'absolute right-0'),
bottom: cx('relative', isActive ? 'h-[360px] min-h-[360px]' : ''),
};
if (!client) {
return null;
}
return ( return (
<nav className={cx('bg-lineHighlight z-[10] flex flex-col', positions[panelPosition])}> <div className={cx('flex select-none max-w-full overflow-auto pb-2')}>
<div className="flex justify-between px-2"> {Object.keys(tabNames).map((key) => {
<div className={cx('flex select-none max-w-full overflow-auto', activeFooter && 'pb-2')}> const val = tabNames[key];
<PanelTab name="intro" label="welcome" /> return <PanelTab key={key} isSelected={tab === val} label={key} onClick={() => setTab(val)} />;
<PanelTab name="patterns" /> })}
<PanelTab name="sounds" /> </div>
<PanelTab name="console" /> );
<PanelTab name="reference" /> }
<PanelTab name="settings" />
{TAURI && <PanelTab name="files" />} function PinButton({ pinned, setPinned }) {
</div> return (
{activeFooter !== '' && ( <button
<button onClick={() => setActiveFooter('')} className="text-foreground px-2" aria-label="Close Panel"> onClick={() => setPinned(!pinned)}
<XMarkIcon className="w-5 h-5" /> className={cx(
</button> 'text-foreground w-8 h-8 items-center justify-center p-1.5 group-hover:flex',
)} pinned ? 'flex' : 'hidden',
</div>
{activeFooter !== '' && (
<div className="relative overflow-hidden">
<div className="text-white overflow-auto h-full max-w-full" ref={footerContent}>
{activeFooter === 'intro' && <WelcomeTab context={context} />}
{activeFooter === 'patterns' && <PatternsTab context={context} />}
{activeFooter === 'console' && <ConsoleTab log={log} />}
{activeFooter === 'sounds' && <SoundsTab />}
{activeFooter === 'reference' && <Reference />}
{activeFooter === 'settings' && <SettingsTab started={context.started} />}
{activeFooter === 'files' && <FilesTab />}
</div>
</div>
)} )}
</nav> aria-label="Pin Settings Menu"
>
<svg
stroke="currentColor"
fill={'currentColor'}
strokeWidth="0"
opacity={pinned ? 1 : '.3'}
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M9.828.722a.5.5 0 0 1 .354.146l4.95 4.95a.5.5 0 0 1 0 .707c-.48.48-1.072.588-1.503.588-.177 0-.335-.018-.46-.039l-3.134 3.134a6 6 0 0 1 .16 1.013c.046.702-.032 1.687-.72 2.375a.5.5 0 0 1-.707 0l-2.829-2.828-3.182 3.182c-.195.195-1.219.902-1.414.707s.512-1.22.707-1.414l3.182-3.182-2.828-2.829a.5.5 0 0 1 0-.707c.688-.688 1.673-.767 2.375-.72a6 6 0 0 1 1.013.16l3.134-3.133a3 3 0 0 1-.04-.461c0-.43.108-1.022.589-1.503a.5.5 0 0 1 .353-.146"></path>
</svg>
</button>
); );
} }

View File

@ -102,7 +102,7 @@ export function PatternsTab({ context }) {
const autoResetPatternOnChange = !isUdels(); const autoResetPatternOnChange = !isUdels();
return ( return (
<div className="px-4 w-full dark:text-white text-stone-900 space-y-2 pb-4 flex flex-col overflow-hidden max-h-full"> <div className="px-4 w-full dark:text-white text-stone-900 space-y-2 flex flex-col overflow-hidden max-h-full">
<ButtonGroup <ButtonGroup
value={patternFilter} value={patternFilter}
onChange={(value) => settingsMap.setKey('patternFilter', value)} onChange={(value) => settingsMap.setKey('patternFilter', value)}
@ -155,7 +155,7 @@ export function PatternsTab({ context }) {
</div> </div>
)} )}
<section className="flex overflow-y-scroll max-h-full flex-col"> <section className="flex overflow-y-scroll max-h-full flex-col pb-4">
{patternFilter === patternFilterName.user && ( {patternFilter === patternFilterName.user && (
<PatternButtons <PatternButtons
onClick={(id) => onClick={(id) =>

View File

@ -74,7 +74,7 @@ export function SoundsTab() {
></ButtonGroup> ></ButtonGroup>
<ImportSoundsButton onComplete={() => settingsMap.setKey('soundsFilter', 'user')} /> <ImportSoundsButton onComplete={() => settingsMap.setKey('soundsFilter', 'user')} />
</div> </div>
<div className="min-h-0 max-h-full grow overflow-auto font-mono text-sm break-normal"> <div className="min-h-0 max-h-full grow overflow-auto font-mono text-sm break-normal pb-4">
{soundEntries.map(([name, { data, onTrigger }]) => { {soundEntries.map(([name, { data, onTrigger }]) => {
return ( return (
<span <span