Merge pull request #1193 from daslyfe/orgin/daslyfe/feat/pinmenu

Menu Panel Improvements!
This commit is contained in:
Jade (Rose) Rowland 2024-10-18 22:28:35 -04:00 committed by GitHub
commit 835c7b6879
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 185 additions and 117 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,7 +1,6 @@
import { useState, useRef, useCallback, useMemo, useEffect } from 'react'; import { useState, useRef, useCallback, useMemo, useEffect } from 'react';
import { Icon } from './Icon'; import { Icon } from './Icon';
import { silence, noteToMidi, _mod } from '@strudel/core'; import { silence, noteToMidi, _mod } from '@strudel/core';
import { clearHydra } from '@strudel/hydra';
import { getDrawContext, getPunchcardPainter } from '@strudel/draw'; import { getDrawContext, getPunchcardPainter } from '@strudel/draw';
import { transpiler } from '@strudel/transpiler'; import { transpiler } from '@strudel/transpiler';
import { getAudioContext, webaudioOutput, initAudioOnFirstClick } from '@strudel/webaudio'; import { getAudioContext, webaudioOutput, initAudioOnFirstClick } from '@strudel/webaudio';

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';
@ -21,10 +21,10 @@ export default function ReplEditor(Props) {
<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-2">
<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 { setPanelPinned, 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,87 @@ 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 settings = useSettings();
const { isPanelPinned: pinned, activeFooter: tab } = settings;
return (
<PanelNav
className={cx(
'hover:max-h-[360px] hover:min-h-[360px] justify-between flex flex-col',
pinned ? `min-h-[360px] max-h-[360px]` : 'min-h-10 max-h-10',
)}
>
<div className="flex h-full overflow-auto ">
<PanelContent context={context} tab={tab} />
</div>
<div className="flex justify-between min-h-10 max-h-10 pr-2 items-center">
<Tabs setTab={setTab} tab={tab} pinned={pinned} />
<PinButton pinned={pinned} setPinned={setPanelPinned} />
</div>
</PanelNav>
);
}
export function VerticalPanel({ context }) {
const settings = useSettings();
const { isPanelPinned: pinned, activeFooter: tab } = settings;
return (
<PanelNav
className={cx(
'hover:min-w-[min(600px,80vw)] hover:max-w-[min(600px,80vw)]',
pinned ? `min-w-[min(600px,80vw)] max-w-[min(600px,80vw)]` : 'min-w-8',
)}
>
<div className={cx('group-hover:flex flex-col h-full', pinned ? 'flex' : 'hidden')}>
<div className="flex justify-between w-full ">
<Tabs setTab={setTab} tab={tab} pinned={pinned} />
<PinButton pinned={pinned} setPinned={setPanelPinned} />
</div>
<div className="overflow-auto h-full">
<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',
reference: 'reference',
console: 'console',
settings: 'settings',
};
if (TAURI) {
tabNames.files = 'files';
}
function PanelNav({ children, className, ...props }) {
return (
<nav
aria-label="Settings Menu"
className={cx('bg-lineHighlight group transition-all overflow-x-auto', className)}
{...props}
>
{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 +113,72 @@ 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 max-h-8 min-h-8 max-w-8 min-w-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"
className="w-full h-full"
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

@ -77,7 +77,7 @@ function PatternButtons({ patterns, activePattern, onClick, started }) {
function ActionButton({ children, onClick, label, labelIsHidden }) { function ActionButton({ children, onClick, label, labelIsHidden }) {
return ( return (
<button className="hover:opacity-50" onClick={onClick} title={label}> <button className="hover:opacity-50 text-nowrap" onClick={onClick} title={label}>
{labelIsHidden !== true && label} {labelIsHidden !== true && label}
{children} {children}
</button> </button>
@ -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 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-auto max-h-full flex-grow flex-col">
{patternFilter === patternFilterName.user && ( {patternFilter === patternFilterName.user && (
<PatternButtons <PatternButtons
onClick={(id) => onClick={(id) =>

View File

@ -25,32 +25,37 @@ export function Reference() {
}, [search]); }, [search]);
return ( return (
<div className="flex h-full w-full pt-2 text-foreground overflow-hidden"> <div className="flex h-full w-full p-2 text-foreground overflow-hidden">
<div className="w-42 flex-none h-full overflow-y-auto overflow-x-hidden pr-4"> <div className="h-full flex flex-col gap-2 w-1/3 max-w-72 ">
<div class="w-full ml-2 mb-2 top-0 sticky"> <div class="w-full flex">
<input <input
className="w-full p-1 bg-background rounded-md" className="w-full p-1 bg-background rounded-md border-none"
placeholder="Search" placeholder="Search"
value={search} value={search}
onInput={(event) => setSearch(event.target.value)} onInput={(event) => setSearch(event.target.value)}
/> />
</div> </div>
{visibleFunctions.map((entry, i) => ( <div className="flex flex-col h-full overflow-y-auto gap-1.5 bg-background bg-opacity-50 rounded-md">
<a {visibleFunctions.map((entry, i) => (
key={i} <a
className="cursor-pointer block hover:bg-lineHighlight py-1 px-4" key={i}
onClick={() => { className="cursor-pointer flex-none hover:bg-lineHighlight overflow-x-hidden px-1 text-ellipsis"
const el = document.getElementById(`doc-${i}`); onClick={() => {
const container = document.getElementById('reference-container'); const el = document.getElementById(`doc-${i}`);
container.scrollTo(0, el.offsetTop); const container = document.getElementById('reference-container');
}} container.scrollTo(0, el.offsetTop);
> }}
{entry.name} {/* <span className="text-gray-600">{entry.meta.filename}</span> */} >
</a> {entry.name} {/* <span className="text-gray-600">{entry.meta.filename}</span> */}
))} </a>
))}
</div>
</div> </div>
<div className="break-normal w-full h-full overflow-auto pl-4 flex relative" id="reference-container"> <div
<div className="prose dark:prose-invert max-w-full pr-4"> className="break-normal flex-grow flex-col overflow-y-auto overflow-x-hidden px-2 flex relative"
id="reference-container"
>
<div className="prose dark:prose-invert min-w-full px-1 ">
<h2>API Reference</h2> <h2>API Reference</h2>
<p> <p>
This is the long list functions you can use! Remember that you don't need to remember all of those and that This is the long list functions you can use! Remember that you don't need to remember all of those and that

View File

@ -105,7 +105,7 @@ export function SettingsTab({ started }) {
const shouldAlwaysSync = isUdels(); const shouldAlwaysSync = isUdels();
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null; const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
return ( return (
<div className="text-foreground p-4 space-y-4"> <div className="text-foreground p-4 space-y-4 w-full">
{canChangeAudioDevice && ( {canChangeAudioDevice && (
<FormItem label="Audio Output Device"> <FormItem label="Audio Output Device">
<AudioDeviceSelector <AudioDeviceSelector

View File

@ -1,7 +1,7 @@
import useEvent from '@src/useEvent.mjs'; import useEvent from '@src/useEvent.mjs';
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import { getAudioContext, soundMap, connectToDestination } from '@strudel/webaudio'; import { getAudioContext, soundMap, connectToDestination } from '@strudel/webaudio';
import React, { useMemo, useRef, useState } from 'react'; import { useMemo, useRef, useState } from 'react';
import { settingsMap, useSettings } from '../../../settings.mjs'; import { settingsMap, useSettings } from '../../../settings.mjs';
import { ButtonGroup } from './Forms.jsx'; import { ButtonGroup } from './Forms.jsx';
import ImportSoundsButton from './ImportSoundsButton.jsx'; import ImportSoundsButton from './ImportSoundsButton.jsx';
@ -53,14 +53,13 @@ export function SoundsTab() {
return ( return (
<div id="sounds-tab" className="px-4 flex flex-col w-full h-full dark:text-white text-stone-900"> <div id="sounds-tab" className="px-4 flex flex-col w-full h-full dark:text-white text-stone-900">
<div className="w-full ml-2 mb-2 top-0 sticky"> <input
<input className="w-full p-1 bg-background rounded-md pb-2"
className="w-full p-1 bg-background rounded-md" placeholder="Search"
placeholder="Search" value={search}
value={search} onChange={(e) => setSearch(e.target.value)}
onChange={(e) => setSearch(e.target.value)} />
/>
</div>
<div className="pb-2 flex shrink-0 flex-wrap"> <div className="pb-2 flex shrink-0 flex-wrap">
<ButtonGroup <ButtonGroup
value={soundsFilter} value={soundsFilter}
@ -74,7 +73,8 @@ 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-2">
{soundEntries.map(([name, { data, onTrigger }]) => { {soundEntries.map(([name, { data, onTrigger }]) => {
return ( return (
<span <span

View File

@ -5,7 +5,7 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
export function WelcomeTab({ context }) { export function WelcomeTab({ context }) {
return ( return (
<div className="prose dark:prose-invert max-w-[600px] pt-2 font-sans pb-8 px-4"> <div className="prose dark:prose-invert min-w-full pt-2 font-sans pb-8 px-4 ">
<h3> <h3>
<span className={cx('animate-spin inline-block select-none')}>🌀</span> welcome <span className={cx('animate-spin inline-block select-none')}>🌀</span> welcome
</h3> </h3>

View File

@ -30,7 +30,8 @@ export const defaultSettings = {
isZen: false, isZen: false,
soundsFilter: 'all', soundsFilter: 'all',
patternFilter: 'community', patternFilter: 'community',
panelPosition: 'right', panelPosition: window.innerWidth > 1000 ? 'right' : 'bottom',
isPanelPinned: true,
userPatterns: '{}', userPatterns: '{}',
audioDeviceName: defaultAudioDeviceName, audioDeviceName: defaultAudioDeviceName,
audioEngineTarget: audioEngineTargets.webaudio, audioEngineTarget: audioEngineTargets.webaudio,
@ -60,6 +61,7 @@ export function useSettings() {
return { return {
...state, ...state,
isZen: parseBoolean(state.isZen), isZen: parseBoolean(state.isZen),
isPanelPinned: parseBoolean(state.isPanelPinned),
isBracketMatchingEnabled: parseBoolean(state.isBracketMatchingEnabled), isBracketMatchingEnabled: parseBoolean(state.isBracketMatchingEnabled),
isBracketClosingEnabled: parseBoolean(state.isBracketClosingEnabled), isBracketClosingEnabled: parseBoolean(state.isBracketClosingEnabled),
isLineNumbersDisplayed: parseBoolean(state.isLineNumbersDisplayed), isLineNumbersDisplayed: parseBoolean(state.isLineNumbersDisplayed),
@ -77,6 +79,7 @@ export function useSettings() {
} }
export const setActiveFooter = (tab) => settingsMap.setKey('activeFooter', tab); export const setActiveFooter = (tab) => settingsMap.setKey('activeFooter', tab);
export const setPanelPinned = (isPinned) => settingsMap.setKey('isPanelPinned', isPinned);
export const setIsZen = (active) => settingsMap.setKey('isZen', !!active); export const setIsZen = (active) => settingsMap.setKey('isZen', !!active);

View File

@ -52,6 +52,7 @@
} }
:root { :root {
--app-height: 100vh; --app-height: 100vh;
--app-width: 100vw;
} }
#console-tab { #console-tab {

View File

@ -34,6 +34,7 @@ module.exports = {
}, },
spacing: { spacing: {
'app-height': 'var(--app-height)', 'app-height': 'var(--app-height)',
'app-width': 'var(--app-width)',
}, },
typography(theme) { typography(theme) {
return { return {