Merge pull request 'crunch up layout' (#1938) from stylez into main

Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1938
This commit is contained in:
froos 2026-01-22 10:28:08 +01:00
commit 7f2860eb14
18 changed files with 363 additions and 288 deletions

View file

@ -5,7 +5,7 @@ export const settings = {
background: '#222', background: '#222',
lineBackground: '#22222299', lineBackground: '#22222299',
foreground: '#fff', foreground: '#fff',
muted: '#ffffff50', muted: '#8a919966',
caret: '#ffcc00', caret: '#ffcc00',
selection: 'rgba(128, 203, 196, 0.5)', selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#036dd626', selectionMatch: '#036dd626',

View file

@ -8,7 +8,7 @@ export function Code(Props) {
return ( return (
<section <section
className={'text-gray-100 cursor-text pb-0 overflow-auto grow'} className={'text-gray-100 cursor-text pb-0 overflow-auto grow z-10'}
id="code" id="code"
ref={(el) => { ref={(el) => {
containerRef.current = el; containerRef.current = el;

View file

@ -2,7 +2,7 @@ import Loader from '@src/repl/components/Loader';
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';
import { Header } from './Header'; import { MainPanel } from './panel/Panel';
// type Props = { // type Props = {
// context: replcontext, // context: replcontext,
@ -14,7 +14,7 @@ export default function EmbeddedReplEditor(Props) {
return ( return (
<div className="h-full flex flex-col relative" {...editorProps}> <div className="h-full flex flex-col relative" {...editorProps}>
<Loader active={pending} /> <Loader active={pending} />
<Header context={context} embedded={true} /> <MainPanel context={context} embedded={true} />
<BigPlayButton started={started} handleTogglePlay={handleTogglePlay} /> <BigPlayButton started={started} handleTogglePlay={handleTogglePlay} />
<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} />

View file

@ -1,116 +0,0 @@
import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon';
import StopCircleIcon from '@heroicons/react/20/solid/StopCircleIcon';
import cx from '@src/cx.mjs';
import { useSettings, setIsZen } from '../../settings.mjs';
import { StrudelIcon } from '@src/repl/components/icons/StrudelIcon';
import '../Repl.css';
const { BASE_URL } = import.meta.env;
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
export function Header({ context, isEmbedded = false }) {
const { started, pending, isDirty, activeCode, handleTogglePlay, handleEvaluate, handleShare } = context;
const { isZen, isButtonRowHidden, isCSSAnimationDisabled, fontFamily } = useSettings();
return (
<header
id="header"
className={cx(
'flex-none text-black z-[100] text-sm select-none min-h-10',
!isZen && !isEmbedded && 'bg-lineHighlight',
isZen ? 'h-12 w-8 fixed top-0 left-0' : 'sticky top-0 w-full justify-between',
'flex items-center',
)}
style={{ fontFamily }}
>
<div className={cx(isEmbedded ? 'flex' : 'sm:flex', 'w-full sm:justify-between')}>
<div className="px-3 pt-1 flex space-x-2 sm:pt-0 select-none">
<h1
onClick={() => {
if (isEmbedded) window.open(window.location.href.replace('embed', ''));
}}
className={cx(
isEmbedded ? 'text-l cursor-pointer' : 'text-xl',
'text-foreground font-bold flex space-x-2 items-center',
)}
>
<div
className={cx(
'mt-[1px]',
started && !isCSSAnimationDisabled && 'animate-spin',
'cursor-pointer text-blue-500',
isZen && 'fixed top-2 right-4',
)}
onClick={() => {
if (!isEmbedded) {
setIsZen(!isZen);
}
}}
>
<span className="block text-foreground rotate-90">
<StrudelIcon className="w-5 h-5 fill-foreground" />
</span>
</div>
{!isZen && (
<div className="space-x-2">
<span className="">strudel</span>
<span className="text-sm font-medium">REPL</span>
{!isEmbedded && isButtonRowHidden && (
<a href={`${baseNoTrailing}/learn`} className="text-sm opacity-25 font-medium">
DOCS
</a>
)}
</div>
)}
</h1>
</div>
{!isZen && !isButtonRowHidden && (
<div className="flex max-w-full overflow-auto text-foreground px-3 h-10">
<button
onClick={handleTogglePlay}
title={started ? 'stop' : 'play'}
className={cx('hover:opacity-50', !started && !isCSSAnimationDisabled && 'animate-pulse')}
>
{!pending ? (
<span className={cx('flex items-center space-x-2')}>
{started ? <StopCircleIcon className="w-5 h-5" /> : <PlayCircleIcon className="w-5 h-5" />}
{!isEmbedded && <span>{started ? 'stop' : 'play'}</span>}
</span>
) : (
<>loading...</>
)}
</button>
<button
onClick={handleEvaluate}
title="update"
className={cx(
'flex items-center space-x-1 px-2',
!isDirty || !activeCode ? 'opacity-50' : 'hover:opacity-50',
)}
>
{!isEmbedded && <span>update</span>}
</button>
{!isEmbedded && (
<button
title="share"
className={cx('cursor-pointer hover:opacity-50 flex items-center space-x-1 px-2')}
onClick={handleShare}
>
<span>share</span>
</button>
)}
{!isEmbedded && (
<a
title="learn"
href={`${baseNoTrailing}/workshop/getting-started/`}
className={cx('hover:opacity-50 flex items-center space-x-1', !isEmbedded ? 'p-2' : 'px-2')}
>
<span>learn</span>
</a>
)}
</div>
)}
</div>
</header>
);
}

View file

@ -1,8 +1,7 @@
import Loader from '@src/repl/components/Loader';
import { HorizontalPanel, VerticalPanel, PanelToggle } from '@src/repl/components/panel/Panel';
import { Code } from '@src/repl/components/Code'; import { Code } from '@src/repl/components/Code';
import Loader from '@src/repl/components/Loader';
import { HorizontalPanel, MainPanel, VerticalPanel } from '@src/repl/components/panel/Panel';
import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage'; import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage';
import { Header } from './Header';
import { useSettings } from '@src/settings.mjs'; import { useSettings } from '@src/settings.mjs';
// type Props = { // type Props = {
@ -19,15 +18,17 @@ export default function ReplEditor(Props) {
return ( return (
<div className="h-full flex flex-col relative" {...editorProps}> <div className="h-full flex flex-col relative" {...editorProps}>
<Loader active={pending} /> <Loader active={pending} />
<Header context={context} isEmbedded={isEmbedded} /> <div className="flex flex-col grow overflow-hidden">
{/* <MainPanel context={context} isEmbedded={isEmbedded} className="hidden sm:block" /> */}
<div className="grow flex relative overflow-hidden"> <MainPanel context={context} isEmbedded={isEmbedded} />
<Code containerRef={containerRef} editorRef={editorRef} init={init} /> <div className="flex overflow-hidden">
{!isZen && panelPosition === 'right' && <VerticalPanel context={context} />} <Code containerRef={containerRef} editorRef={editorRef} init={init} />
<PanelToggle isEmbedded={isEmbedded} isZen={isZen} /> {!isZen && panelPosition === 'right' && <VerticalPanel context={context} />}
</div>
</div> </div>
<UserFacingErrorMessage error={error} /> <UserFacingErrorMessage error={error} />
{!isZen && panelPosition === 'bottom' && <HorizontalPanel context={context} />} {!isZen && panelPosition === 'bottom' && <HorizontalPanel context={context} />}
{/* <MainPanel context={context} isEmbedded={isEmbedded} className="block sm:hidden" /> */}
</div> </div>
); );
} }

View file

@ -2,7 +2,7 @@ import cx from '@src/cx.mjs';
export function ActionButton({ children, label, labelIsHidden, className, ...buttonProps }) { export function ActionButton({ children, label, labelIsHidden, className, ...buttonProps }) {
return ( return (
<button className={cx('hover:opacity-50 text-nowrap w-fit text-sm ', className)} title={label} {...buttonProps}> <button className={cx('hover:opacity-50 text-xs text-nowrap w-fit', className)} title={label} {...buttonProps}>
{labelIsHidden !== true && label} {labelIsHidden !== true && label}
{children} {children}
</button> </button>

View file

@ -0,0 +1,19 @@
export function SidebarIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-panel-right-icon lucide-panel-right"
>
<rect width="18" height="18" x="3" y="3" rx="2" />
<path d="M15 3v18" />
</svg>
);
}

View file

@ -7,8 +7,10 @@ export function ConsoleTab() {
const log = useStore($strudel_log_history); const log = useStore($strudel_log_history);
const { fontFamily } = useSettings(); const { fontFamily } = useSettings();
return ( return (
<div id="console-tab" className="break-all w-full text-sm p-2 h-full" style={{ fontFamily }}> <div id="console-tab" className="break-all w-full h-full" style={{ fontFamily }}>
<div className="bg-background h-full w-full overflow-auto space-y-1 p-2 rounded-md"> <div className="h-full w-full overflow-auto space-y-1 p-2 rounded-md">
{' '}
{/* bg-background */}
{log.map((l, i) => { {log.map((l, i) => {
const message = linkify(l.message); const message = linkify(l.message);
const color = l.data?.hap?.value?.color; const color = l.data?.hap?.value?.color;

View file

@ -45,7 +45,7 @@ export default function ExportTab(Props) {
return ( return (
<> <>
<div className="text-foreground w-full p-4 space-y-4"> <div className="text-foreground w-full space-y-4 p-4">
<FormItem label="File name" disabled={exporting}> <FormItem label="File name" disabled={exporting}>
<Textbox <Textbox
onBlur={(e) => { onBlur={(e) => {
@ -56,7 +56,7 @@ export default function ExportTab(Props) {
}} }}
disabled={exporting} disabled={exporting}
placeholder="Leave empty to use current date" placeholder="Leave empty to use current date"
className={cx('placeholder:opacity-50', exporting && 'opacity-50 border-opacity-50')} className={cx('placeholder-muted', exporting && 'opacity-50 border-opacity-50')}
value={downloadName ?? ''} value={downloadName ?? ''}
/> />
</FormItem> </FormItem>

View file

@ -1,15 +1,15 @@
import cx from '@src/cx.mjs'; import cx from '@src/cx.mjs';
export function ButtonGroup({ value, onChange, items }) { export function ButtonGroup({ value, onChange, items, wrap = false }) {
return ( return (
<div className="flex max-w-lg space-x-0 text-sm"> <div className={cx('flex max-w-lg space-x-0 text-xs', wrap && 'flex-wrap')}>
{Object.entries(items).map(([key, label], i, arr) => ( {Object.entries(items).map(([key, label], i, arr) => (
<button <button
key={key} key={key}
id={key} id={key}
onClick={() => onChange(key)} onClick={() => onChange(key)}
className={cx( className={cx(
'px-2 border-b-2 h-8 whitespace-nowrap', 'px-2 border-b-2 h-8 whitespace-nowrap border-box max-h-8 hover:opacity-50',
// i === 0 && 'rounded-l-md', // i === 0 && 'rounded-l-md',
// i === arr.length - 1 && 'rounded-r-md', // i === arr.length - 1 && 'rounded-r-md',
// value === key ? 'bg-background' : 'bg-lineHighlight', // value === key ? 'bg-background' : 'bg-lineHighlight',

View file

@ -1,25 +1,168 @@
import { Bars3Icon, PlayIcon, StopIcon, XMarkIcon } from '@heroicons/react/16/solid';
import cx from '@src/cx.mjs'; import cx from '@src/cx.mjs';
import { setActiveFooter as setTab, setIsPanelOpened, useSettings } from '../../../settings.mjs'; import { StrudelIcon } from '@src/repl/components/icons/StrudelIcon';
import { useSettings, setIsZen, setIsPanelOpened, setActiveFooter as setTab } from '../../../settings.mjs';
import '../../Repl.css';
import { useLogger } from '../useLogger';
import { ConsoleTab } from './ConsoleTab'; import { ConsoleTab } from './ConsoleTab';
import ExportTab from './ExportTab';
import { FilesTab } from './FilesTab'; import { FilesTab } from './FilesTab';
import { PatternsTab } from './PatternsTab';
import { Reference } from './Reference'; import { Reference } from './Reference';
import { SettingsTab } from './SettingsTab'; import { SettingsTab } from './SettingsTab';
import { SoundsTab } from './SoundsTab'; import { SoundsTab } from './SoundsTab';
import { useLogger } from '../useLogger';
import { WelcomeTab } from './WelcomeTab'; import { WelcomeTab } from './WelcomeTab';
import { PatternsTab } from './PatternsTab';
import { Bars3Icon, XMarkIcon } from '@heroicons/react/16/solid';
import ExportTab from './ExportTab';
const TAURI = typeof window !== 'undefined' && window.__TAURI__; const TAURI = typeof window !== 'undefined' && window.__TAURI__;
const { BASE_URL } = import.meta.env;
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
export function LogoButton({ context, isEmbedded }) {
const { started } = context;
const { isZen, isCSSAnimationDisabled, fontFamily } = useSettings();
return (
<div
className={cx(
'mt-[1px]',
started && !isCSSAnimationDisabled && 'animate-spin',
'cursor-pointer text-blue-500',
isZen && 'fixed top-2 right-4',
)}
onClick={() => {
if (!isEmbedded) {
setIsZen(!isZen);
}
}}
>
<span className="block text-foreground rotate-90">
<StrudelIcon className="w-5 h-5 fill-foreground" />
</span>
</div>
);
}
export function MainPanel({ context, isEmbedded = false, className }) {
const { isZen, isButtonRowHidden, fontFamily } = useSettings();
return (
<nav
id="header"
className={cx(
//'border-t sm:border-b sm:border-t-0 border-muted',
'border-b border-muted',
'flex-none text-black z-[100] text-sm select-none min-h-10 max-h-10',
!isZen && !isEmbedded && 'bg-lineHighlight',
// isZen ? 'h-12 w-8 fixed top-0 left-0' : 'h-10 sticky top-0 w-full justify-between',
isZen ? 'h-12 w-8 fixed top-0 left-0' : '',
'flex items-center',
className,
)}
style={{ fontFamily }}
>
<div className={cx('flex w-full justify-between')}>
{' '}
{/*flex-wrap*/}
<div className="px-3 py-1 flex space-x-2 select-none">
<h1
onClick={() => {
if (isEmbedded) window.open(window.location.href.replace('embed', ''));
}}
className={cx(
isEmbedded ? 'text-l cursor-pointer' : 'text-xl',
'text-foreground font-bold flex space-x-2 items-center',
)}
>
<LogoButton context={context} isEmbedded={isEmbedded} />
{!isZen && (
<div className="space-x-2 flex items-baseline">
<span className="hidden sm:block">strudel</span>
<span className="text-sm font-medium hidden sm:block">REPL</span>
{/* !isEmbedded && isButtonRowHidden && (
<a href={`${baseNoTrailing}/learn`} className="text-sm opacity-25 font-medium">
DOCS
</a>
) */}
</div>
)}
</h1>
</div>
{!isZen && (
<div className="flex grow justify-end">
{!isButtonRowHidden && <MainMenu isEmbedded={isEmbedded} context={context} />}
{/* className="hidden sm:flex" */}
<PanelToggle isEmbedded={isEmbedded} isZen={isZen} />
</div>
)}
</div>
</nav>
);
}
export function Footer({ context, isEmbedded = false }) {
return (
<div className="border-t border-muted bg-lineHighlight block lg:hidden">
{/* block lg:hidden */}
<MainMenu context={context} isEmbedded={isEmbedded} />
</div>
);
}
function MainMenu({ context, isEmbedded = false, className }) {
const { started, pending, isDirty, activeCode, handleTogglePlay, handleEvaluate, handleShare } = context;
const { isCSSAnimationDisabled } = useSettings();
return (
<div className={cx('flex text-sm max-w-full shrink-0 overflow-hidden text-foreground px-2 h-10', className)}>
<button
onClick={handleTogglePlay}
title={started ? 'stop' : 'play'}
className={cx('px-2 hover:opacity-50', !started && !isCSSAnimationDisabled && 'animate-pulse')}
>
{/* {!pending ? ( */}
<span className={cx('flex items-center space-x-2')}>
{/* {started ? <StopCircleIcon className="w-5 h-5" /> : <PlayCircleIcon className="w-5 h-5" />} */}
{started ? <StopIcon className="w-5 h-5" /> : <PlayIcon className="w-5 h-5" />}
{!isEmbedded && <span>{started ? 'stop' : 'play'}</span>}
</span>
{/* ) : (
<>loading...</>
)} */}
</button>
<button
onClick={handleEvaluate}
title="update"
className={cx('flex items-center space-x-1 px-2', !isDirty || !activeCode ? 'opacity-50' : 'hover:opacity-50')}
>
{!isEmbedded && <span>update</span>}
</button>
{!isEmbedded && (
<button
title="share"
className={cx('cursor-pointer hover:opacity-50 flex items-center space-x-1 px-2')}
onClick={handleShare}
>
<span>share</span>
</button>
)}
{!isEmbedded && (
<a
title="learn"
href={`${baseNoTrailing}/workshop/getting-started/`}
className={cx('hover:opacity-50 flex items-center space-x-1', !isEmbedded ? 'p-2' : 'px-2')}
>
<span>learn</span>
</a>
)}
</div>
);
}
function PanelCloseButton() { function PanelCloseButton() {
const { isPanelOpen } = useSettings(); const { isPanelOpen } = useSettings();
return ( return (
isPanelOpen && ( isPanelOpen && (
<button <button
onClick={() => setIsPanelOpened(false)} onClick={() => setIsPanelOpened(false)}
className={cx('px-4 py-2 text-foreground hover:opacity-50')} className={cx('border-l border-muted px-2 py-0 text-foreground hover:opacity-50')}
aria-label="Close Menu" aria-label="Close Menu"
> >
<XMarkIcon className="w-6 h-6" /> <XMarkIcon className="w-6 h-6" />
@ -34,15 +177,15 @@ export function HorizontalPanel({ context }) {
<PanelNav <PanelNav
className={cx( className={cx(
isPanelOpen ? `min-h-[360px] max-h-[360px]` : 'min-h-10 max-h-10', isPanelOpen ? `min-h-[360px] max-h-[360px]` : 'min-h-10 max-h-10',
'overflow-hidden flex flex-col relative ', 'overflow-hidden flex flex-col relative',
)} )}
> >
<div className="flex justify-between min-h-10 max-h-10 grid-cols-2 items-center"> <div className="flex justify-between min-h-10 max-h-10 grid-cols-2 items-center border-t border-muted">
<Tabs setTab={setTab} tab={tab} />
<PanelCloseButton /> <PanelCloseButton />
<Tabs setTab={setTab} tab={tab} />
</div> </div>
{isPanelOpen && ( {isPanelOpen && (
<div className="flex h-full overflow-auto w-full"> <div className="w-full h-full overflow-auto border-t border-muted">
<PanelContent context={context} tab={tab} /> <PanelContent context={context} tab={tab} />
</div> </div>
)} )}
@ -59,15 +202,25 @@ export function VerticalPanel({ context }) {
return ( return (
<PanelNav <PanelNav
settings={settings} settings={settings}
className={cx(isPanelOpen ? `min-w-[min(600px,100vw)] max-w-[min(600px,80vw)]` : 'min-w-12 max-w-12')} className={cx(
//'border-l border-muted shrink-0',
'border-0 border-muted shrink-0 h-full overflow-hidden',
isPanelOpen
? //? `min-w-full max-w-full lg:min-w-[min(600px,100vw)] lg:max-w-[min(600px,80vw)]`
`min-w-[min(600px,100vw)] max-w-[min(600px,80vw)]`
: 'min-w-12 max-w-12',
)}
> >
<div className={cx('flex flex-col h-full')}> <div className={cx('flex flex-col h-full')}>
<div className="flex justify-between w-full "> <div className="flex justify-between w-full overflow-hidden border-b border-muted min-h-10 max-h-10">
<Tabs setTab={setTab} tab={tab} /> {/* <div className="block sm:hidden text-foreground px-3 py-2 border-l border-muted">
<LogoButton context={context} />
</div> */}
<PanelCloseButton /> <PanelCloseButton />
<Tabs setTab={setTab} tab={tab} />
{/* <PanelCloseButton /> */}
</div> </div>
<div className="overflow-auto h-full border-l border-muted">
<div className="overflow-auto h-full">
<PanelContent context={context} tab={tab} /> <PanelContent context={context} tab={tab} />
</div> </div>
</div> </div>
@ -98,7 +251,7 @@ function PanelNav({ children, className, ...props }) {
} }
}} }}
aria-label="Menu Panel" aria-label="Menu Panel"
className={cx('bg-lineHighlight group overflow-x-auto', className)} className={cx('h-full bg-lineHighlight group overflow-x-auto', className)}
{...props} {...props}
> >
{children} {children}
@ -134,7 +287,7 @@ function PanelTab({ label, isSelected, onClick }) {
<button <button
onClick={onClick} onClick={onClick}
className={cx( className={cx(
'h-8 px-2 text-sm text-foreground cursor-pointer hover:opacity-50 flex items-center space-x-1 border-b', 'h-10 px-2 text-sm border-t-2 border-t-transparent text-foreground cursor-pointer hover:opacity-50 flex items-center space-x-1 border-b-2',
isSelected ? 'border-foreground' : 'border-transparent', isSelected ? 'border-foreground' : 'border-transparent',
)} )}
> >
@ -146,7 +299,12 @@ function PanelTab({ label, isSelected, onClick }) {
function Tabs({ className }) { function Tabs({ className }) {
const { isPanelOpen, activeFooter: tab } = useSettings(); const { isPanelOpen, activeFooter: tab } = useSettings();
return ( return (
<div className={cx('flex select-none max-w-full overflow-auto items-center', className)}> <div
className={cx(
'px-2 border-l border-muted w-full flex select-none max-w-full h-10 max-h-10 min-h-10 overflow-auto items-center',
className,
)}
>
{Object.keys(tabNames).map((key) => { {Object.keys(tabNames).map((key) => {
const val = tabNames[key]; const val = tabNames[key];
return <PanelTab key={key} isSelected={tab === val && isPanelOpen} label={key} onClick={() => setTab(val)} />; return <PanelTab key={key} isSelected={tab === val && isPanelOpen} label={key} onClick={() => setTab(val)} />;
@ -164,11 +322,12 @@ export function PanelToggle({ isEmbedded, isZen }) {
<button <button
title="share" title="share"
className={cx( className={cx(
'absolute top-0 right-3 rounded-0 px-2 py-2 bg-background z-[1000] cursor-pointer hover:opacity-80 flex justify-center items-center space-x-1 text-foreground ', 'border-l border-muted px-2 py-0 text-foreground hover:opacity-50' /* , isPanelOpen && 'hidden' */,
isPanelOpen && 'hidden',
)} )}
onClick={() => setIsPanelOpened(!isPanelOpen)} onClick={() => setIsPanelOpened(!isPanelOpen)}
> >
{/* <span>menu</span> */}
{/* isPanelOpen ? <XMarkIcon className="w-6 h-6" /> : <Bars3Icon className="w-6 h-6" /> */}
<Bars3Icon className="w-6 h-6" /> <Bars3Icon className="w-6 h-6" />
</button> </button>
) )

View file

@ -8,7 +8,7 @@ import {
useViewingPatternData, useViewingPatternData,
userPattern, userPattern,
} from '../../../user_pattern_utils.mjs'; } from '../../../user_pattern_utils.mjs';
import { useMemo } from 'react'; import { useMemo, useRef } from 'react';
import { getMetadata } from '../../../metadata_parser.js'; import { getMetadata } from '../../../metadata_parser.js';
import { useExamplePatterns } from '../../useExamplePatterns.jsx'; import { useExamplePatterns } from '../../useExamplePatterns.jsx';
import { parseJSON, isUdels } from '../../util.mjs'; import { parseJSON, isUdels } from '../../util.mjs';
@ -43,7 +43,7 @@ function PatternButton({ showOutline, onClick, pattern, showHiglight }) {
className={cx( className={cx(
'mr-4 hover:opacity-50 cursor-pointer block', 'mr-4 hover:opacity-50 cursor-pointer block',
showOutline && 'outline outline-1', showOutline && 'outline outline-1',
showHiglight && 'bg-selection', showHiglight && 'ring-selection',
)} )}
onClick={onClick} onClick={onClick}
> >
@ -53,11 +53,10 @@ function PatternButton({ showOutline, onClick, pattern, showHiglight }) {
} }
function PatternButtons({ patterns, activePattern, onClick, started }) { function PatternButtons({ patterns, activePattern, onClick, started }) {
const viewingPatternStore = useViewingPatternData(); const viewingPatternData = useViewingPatternData();
const viewingPatternData = parseJSON(viewingPatternStore);
const viewingPatternID = viewingPatternData.id; const viewingPatternID = viewingPatternData.id;
return ( return (
<div className=""> <div className="p-2">
{Object.values(patterns) {Object.values(patterns)
.reverse() .reverse()
.map((pattern) => { .map((pattern) => {
@ -83,8 +82,8 @@ const updateCodeWindow = (context, patternData, reset = false) => {
export function PatternsTab({ context }) { export function PatternsTab({ context }) {
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const activePattern = useActivePattern(); const activePattern = useActivePattern();
const viewingPatternStore = useViewingPatternData(); const viewingPatternData = useViewingPatternData();
const viewingPatternData = parseJSON(viewingPatternStore);
const { userPatterns, patternAutoStart } = useSettings(); const { userPatterns, patternAutoStart } = useSettings();
const viewingPatternID = viewingPatternData?.id; const viewingPatternID = viewingPatternData?.id;
@ -121,74 +120,71 @@ export function PatternsTab({ context }) {
); );
}), }),
); );
}, [search, viewingPatternStore]); }, [search, userPatterns]);
const importRef = useRef();
return ( return (
<div className="px-4 w-full text-foreground space-y-2 flex flex-col overflow-hidden max-h-full h-full"> <div className="w-full h-full text-foreground flex flex-col overflow-hidden">
<div className="w-full flex"> <Textbox className="w-full border-0" placeholder="Search..." value={search} onChange={setSearch} />
<Textbox className="w-full" placeholder="Search" value={search} onChange={setSearch} /> <div className="px-2 shrink-0 h-8 space-x-4 flex max-w-full overflow-x-auto border-y border-muted">
<ActionButton
label="new"
onClick={() => {
const { data } = userPattern.createAndAddToDB();
updateCodeWindow(context, data);
}}
/>
<ActionButton
label="duplicate"
onClick={() => {
const { data } = userPattern.duplicate(viewingPatternData);
updateCodeWindow(context, data);
}}
/>
<ActionButton
label="delete"
onClick={() => {
const { data } = userPattern.delete(viewingPatternID);
updateCodeWindow(context, { ...data, collection: userPattern.collection });
}}
/>
<input
ref={importRef}
style={{ display: 'none' }}
type="file"
multiple
accept="text/plain,text/x-markdown,application/json"
onChange={(e) => importPatterns(e.target.files)}
/>
<ActionButton label="import" onClick={() => importRef.current.click()} />
<ActionButton label="export" onClick={exportPatterns} />
<ActionButton
label="delete-all"
onClick={() => {
const { data } = userPattern.clearAll();
updateCodeWindow(context, data);
}}
/>
</div> </div>
<div className="flex flex-col gap-2 flex-grow overflow-hidden h-full pb-2 ">
<div className="pr-4 space-x-4 flex max-w-full overflow-x-auto">
<ActionButton
label="new"
onClick={() => {
const { data } = userPattern.createAndAddToDB();
updateCodeWindow(context, data);
}}
/>
<ActionButton
label="duplicate"
onClick={() => {
const { data } = userPattern.duplicate(viewingPatternData);
updateCodeWindow(context, data);
}}
/>
<ActionButton
label="delete"
onClick={() => {
const { data } = userPattern.delete(viewingPatternID);
updateCodeWindow(context, { ...data, collection: userPattern.collection });
}}
/>
<label className="hover:opacity-50 cursor-pointer">
<input
style={{ display: 'none' }}
type="file"
multiple
accept="text/plain,text/x-markdown,application/json"
onChange={(e) => importPatterns(e.target.files)}
/>
import
</label>
<ActionButton label="export" onClick={exportPatterns} />
<ActionButton <div className="overflow-auto">
label="delete-all" {/* bg-background */}
onClick={() => { {/* {patternFilter === patternFilterName.user && ( */}
const { data } = userPattern.clearAll(); <PatternButtons
updateCodeWindow(context, data); onClick={(id) => {
}} updateCodeWindow(context, { ...userPatterns[id], collection: userPattern.collection }, patternAutoStart);
/>
</div>
<div className="overflow-auto h-full bg-background p-2"> if (context.started && activePattern === id) {
{/* {patternFilter === patternFilterName.user && ( */} context.handleEvaluate();
<PatternButtons }
onClick={(id) => { }}
updateCodeWindow(context, { ...userPatterns[id], collection: userPattern.collection }, patternAutoStart); patterns={visiblePatterns}
started={context.started}
if (context.started && activePattern === id) { activePattern={activePattern}
context.handleEvaluate(); viewingPatternID={viewingPatternID}
} />
}} {/* )} */}
patterns={visiblePatterns}
started={context.started}
activePattern={activePattern}
viewingPatternID={viewingPatternID}
/>
{/* )} */}
</div>
</div> </div>
</div> </div>
); );

View file

@ -2,6 +2,7 @@ import { memo, useEffect, useMemo, useState, Fragment } from 'react';
import jsdocJson from '../../../../../doc.json'; import jsdocJson from '../../../../../doc.json';
import { Textbox } from '@src/repl/components/panel/SettingsTab'; import { Textbox } from '@src/repl/components/panel/SettingsTab';
import { settingsMap, useSettings } from '@src/settings.mjs';
const isValid = ({ name, description }) => name && !name.startsWith('_') && !!description; const isValid = ({ name, description }) => name && !name.startsWith('_') && !!description;
@ -37,6 +38,19 @@ const availableFunctions = (() => {
return functions.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name)); return functions.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
})(); })();
const tagCounts = {};
// const tagOptions = { all: `all (${availableFunctions.length})` };
const tagOptions = { all: `all` };
for (const doc of availableFunctions) {
(doc.tags || ['untagged']).forEach((t) => {
if (typeof t === 'string' && t) {
tagCounts[t] = (tagCounts[t] || 0) + 1;
//tagOptions[t] = `${t} (${tagCounts[t]})`;
tagOptions[t] = t;
}
});
}
const getInnerText = (html) => { const getInnerText = (html) => {
var div = document.createElement('div'); var div = document.createElement('div');
div.innerHTML = html; div.innerHTML = html;
@ -45,40 +59,38 @@ const getInnerText = (html) => {
export const Reference = memo(function Reference() { export const Reference = memo(function Reference() {
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [selectedTag, setSelectedTag] = useState(null);
const [selectedFunction, setSelectedFunction] = useState(null); const [selectedFunction, setSelectedFunction] = useState(null);
const { referenceTag } = useSettings();
const toggleTag = (tag) => { const toggleTag = (tag) => {
if (selectedTag === tag) { if (referenceTag === tag) {
setSelectedTag(null); setReferenceTag('all');
} else { } else {
setSelectedTag(tag); setReferenceTag(tag);
} }
}; };
const searchVisibleFunctions = useMemo(() => { const searchVisibleFunctions = useMemo(() => {
return availableFunctions.filter((entry) => { return availableFunctions.filter((entry) => {
if (selectedTag) { if (referenceTag && referenceTag !== 'all') {
if (!(entry.tags || ['untagged']).includes(selectedTag)) { if (!(entry.tags || ['untagged']).includes(referenceTag)) {
return false; return false;
} }
} }
if (!search) { if (!search) {
return true; return true;
} }
const lowerCaseSearch = search.toLowerCase(); const lowerCaseSearch = search.toLowerCase();
return ( return (
entry.name.toLowerCase().includes(lowerCaseSearch) || entry.name.toLowerCase().includes(lowerCaseSearch) ||
(entry.synonyms?.some((s) => s.toLowerCase().includes(lowerCaseSearch)) ?? false) (entry.synonyms?.some((s) => s.toLowerCase().includes(lowerCaseSearch)) ?? false)
); );
}); });
}, [search, selectedTag]); }, [search, referenceTag]);
const detailVisibleFunctions = useMemo(() => { const detailVisibleFunctions = useMemo(() => {
return searchVisibleFunctions.filter((x) => { return searchVisibleFunctions.filter((x) => {
if (selectedTag === null) { if (referenceTag === null || referenceTag === 'all') {
if (search) { if (search) {
return true; return true;
} }
@ -87,19 +99,10 @@ export const Reference = memo(function Reference() {
return true; return true;
} }
}); });
}, [searchVisibleFunctions, selectedFunction, selectedTag]); }, [searchVisibleFunctions, selectedFunction, referenceTag]);
const tagCounts = {};
for (const doc of availableFunctions) {
(doc.tags || ['untagged']).forEach((t) => {
if (typeof t === 'string' && t) {
tagCounts[t] = (tagCounts[t] || 0) + 1;
}
});
}
const onSearchTagFilterClick = () => { const onSearchTagFilterClick = () => {
setSelectedTag(null); setReferenceTag('all');
setSelectedFunction(null); setSelectedFunction(null);
}; };
@ -111,36 +114,44 @@ export const Reference = memo(function Reference() {
} }
}, [selectedFunction]); }, [selectedFunction]);
let setReferenceTag = (value) => settingsMap.setKey('referenceTag', value);
return ( return (
<div className="flex h-full w-full p-2 overflow-hidden"> <div className="flex h-full w-full overflow-hidden">
<div className="h-full text-foreground flex flex-col gap-3 w-1/3 "> <div className="h-full text-foreground flex flex-col w-1/3 border-r border-muted">
{/* bg-background */}
<div className="w-full flex"> <div className="w-full flex">
<Textbox <Textbox
className="w-full" className="w-full border-0 border-b border-muted"
placeholder="Search" placeholder="Search..."
value={search} value={search}
onChange={(e) => { onChange={(e) => {
setSelectedFunction(null); setSelectedFunction(null);
setReferenceTag('all');
setSearch(e); setSearch(e);
}} }}
/> />
</div> </div>
{selectedTag && (
{/* <div className="flex shrink-0 flex-wrap w-full overflow-auto border-y border-muted">
<ButtonGroup wrap value={referenceTag} onChange={setReferenceTag} items={tagOptions}></ButtonGroup>
</div> */}
{referenceTag && referenceTag !== 'all' && (
<div className="w-72"> <div className="w-72">
<span <span
className="text-foreground border border-muted px-1 py-0.5 my-2 cursor-pointer font-sans" className="text-foreground border border-muted border-t-0 border-l-0 px-1 py-0.5 my-2 cursor-pointer font-sans"
onClick={onSearchTagFilterClick} onClick={onSearchTagFilterClick}
> >
{selectedTag} {referenceTag}
</span> </span>
</div> </div>
)} )}
<div className="flex flex-col h-full overflow-y-auto gap-1.5 bg-background bg-opacity-50"> <div className="h-full p-2 overflow-y-auto bg-opacity-50">
{searchVisibleFunctions.map((entry, i) => ( {searchVisibleFunctions.map((entry, i) => (
<Fragment key={`entry-${entry.name}`}> <Fragment key={`entry-${entry.name}`}>
<a <a
className={ className={
'cursor-pointer flex-none hover:bg-lineHighlight overflow-x-hidden px-1 text-ellipsis ' + 'cursor-pointer hover:opacity-50 text-ellipsis block' +
(entry.name === selectedFunction ? 'bg-lineHighlight font-bold' : '') (entry.name === selectedFunction ? 'bg-lineHighlight font-bold' : '')
} }
onClick={() => { onClick={() => {
@ -158,16 +169,17 @@ export const Reference = memo(function Reference() {
</div> </div>
</div> </div>
<div <div
className="break-normal flex-col overflow-y-auto overflow-x-hidden p-2 flex relative" className="w-2/3 break-normal flex-col overflow-y-auto overflow-x-hidden p-2 flex relative"
id="reference-container" id="reference-container"
> >
<div className="prose dark:prose-invert min-w-full px-1 "> <div className="prose dark:prose-invert min-w-full px-1 text-sm">
<h2>API Reference</h2> <h2>API Reference</h2>
<p className="font-sans text-md"> <p className="font-sans text-md">
This is the long list of functions you can use. Remember that you don't need to remember all of those and This is the long list of functions you can use. Remember that you don't need to remember all of those and
that you can already make music with a small set of functions! that you can already make music with a small set of functions!
</p> </p>
<div> <div>
{/* <ButtonGroup wrap value={referenceTag} onChange={setReferenceTag} items={tagOptions}/>*/}
{Object.entries(tagCounts) {Object.entries(tagCounts)
.sort(([a], [b]) => a.localeCompare(b)) .sort(([a], [b]) => a.localeCompare(b))
.map(([t, count]) => ( .map(([t, count]) => (
@ -175,7 +187,7 @@ export const Reference = memo(function Reference() {
<a <a
className={[ className={[
'select-none text-white border border-muted px-1 py-0.5 my-2 cursor-pointer text-sm/8 no-underline font-sans', 'select-none text-white border border-muted px-1 py-0.5 my-2 cursor-pointer text-sm/8 no-underline font-sans',
`${selectedTag === t ? 'bg-muted text-foreground' : ''}`, `${referenceTag === t ? 'bg-muted text-foreground' : ''}`,
].join(' ')} ].join(' ')}
onClick={() => toggleTag(t)} onClick={() => toggleTag(t)}
> >
@ -187,7 +199,7 @@ export const Reference = memo(function Reference() {
{detailVisibleFunctions.map((entry, i) => ( {detailVisibleFunctions.map((entry, i) => (
<section key={i} className="font-sans"> <section key={i} className="font-sans">
<div className="flex flex-row items-center mt-8 justify-between"> <div className="flex flex-row items-center mt-8 justify-between">
<h3 className="font-mono my-0" id={`doc-${entry.name}`}> <h3 className="font-mono my-0 pt-4" id={`doc-${entry.name}`}>
{entry.name} {entry.name}
</h3> </h3>
{entry.tags && ( {entry.tags && (
@ -201,7 +213,7 @@ export const Reference = memo(function Reference() {
Synonyms: <code>{entry.synonyms_text}</code> Synonyms: <code>{entry.synonyms_text}</code>
</p> </p>
)} )}
{/* <small>{entry.meta.filename}</small> */}
<p dangerouslySetInnerHTML={{ __html: entry.description }}></p> <p dangerouslySetInnerHTML={{ __html: entry.description }}></p>
<ul> <ul>
{entry.params?.map(({ name, type, description }, i) => ( {entry.params?.map(({ name, type, description }, i) => (
@ -211,12 +223,12 @@ export const Reference = memo(function Reference() {
))} ))}
</ul> </ul>
{entry.examples?.map((example, j) => ( {entry.examples?.map((example, j) => (
<pre className="bg-background" key={j}> <pre className="bg-background border border-muted" key={j}>
{example} {example}
</pre> </pre>
))} ))}
</section> </section>
)) || <p className="font-sans">Searcb or select a tag to get started.</p>} )) || <p className="font-sans">Search or select a tag to get started.</p>}
{detailVisibleFunctions.length > 0 && <div className="h-screen" />} {detailVisibleFunctions.length > 0 && <div className="h-screen" />}
</div> </div>
</div> </div>

View file

@ -15,17 +15,17 @@ function cx(...classes) {
} }
const inputClass = const inputClass =
'bg-background text-sm border rounded-0 text-foreground border-muted placeholder-foreground focus:outline-none focus:ring-0 focus:border-foreground'; 'bg-background text-xs h-8 max-h-8 border border-box rounded-0 text-foreground border-muted placeholder-muted focus:outline-none focus:ring-0 focus:border-foreground';
export function Textbox({ onChange, className, ...inputProps }) { export function Textbox({ onChange, className, ...inputProps }) {
return ( return (
<input className={cx('p-2', inputClass, className)} onChange={(e) => onChange(e.target.value)} {...inputProps} /> <input className={cx('px-2', inputClass, className)} onChange={(e) => onChange(e.target.value)} {...inputProps} />
); );
} }
function Checkbox({ label, value, onChange, disabled = false }) { function Checkbox({ label, value, onChange, disabled = false }) {
return ( return (
<label className="text-sm"> <label className="text-xs">
<input <input
className={cx( className={cx(
'bg-background text-sm border border-muted focus:outline-none focus:ring-0 focus:border-foreground', 'bg-background text-sm border border-muted focus:outline-none focus:ring-0 focus:border-foreground',
@ -96,7 +96,7 @@ function NumberSlider({ value, onChange, step = 1, ...rest }) {
function FormItem({ label, children, sublabel }) { function FormItem({ label, children, sublabel }) {
return ( return (
<div className="grid gap-2 text-sm"> <div className="grid gap-2 text-xs">
<label className="text-sm">{label}</label> <label className="text-sm">{label}</label>
{children} {children}
</div> </div>
@ -154,7 +154,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 w-full" style={{ fontFamily }}> <div className="p-4 text-foreground space-y-4 w-full" style={{ fontFamily }}>
{canChangeAudioDevice && ( {canChangeAudioDevice && (
<FormItem label="Audio Output Device"> <FormItem label="Audio Output Device">
<AudioDeviceSelector <AudioDeviceSelector
@ -323,7 +323,7 @@ export function SettingsTab({ started }) {
value={isSyncEnabled} value={isSyncEnabled}
/> />
<Checkbox <Checkbox
label="Hide top buttons" label="Hide action buttons"
onChange={(cbEvent) => settingsMap.setKey('isButtonRowHidden', cbEvent.target.checked)} onChange={(cbEvent) => settingsMap.setKey('isButtonRowHidden', cbEvent.target.checked)}
value={isButtonRowHidden} value={isButtonRowHidden}
/> />

View file

@ -77,11 +77,12 @@ export function SoundsTab() {
numRef.current = 0; numRef.current = 0;
}); });
return ( return (
<div id="sounds-tab" className="px-4 flex gap-2 flex-col w-full h-full text-foreground"> <div id="sounds-tab" className="flex flex-col w-full h-full text-foreground">
<Textbox placeholder="Search" value={search} onChange={(v) => setSearch(v)} /> <Textbox placeholder="Search..." className="border-0" value={search} onChange={(v) => setSearch(v)} />
<div className=" flex shrink-0 flex-wrap"> <div className="flex shrink-0 flex-wrap border-y border-muted">
<ButtonGroup <ButtonGroup
wrap
value={soundsFilter} value={soundsFilter}
onChange={(value) => settingsMap.setKey('soundsFilter', value)} onChange={(value) => settingsMap.setKey('soundsFilter', value)}
items={{ items={{
@ -114,7 +115,7 @@ export function SoundsTab() {
/> />
)} )}
<div className="min-h-0 max-h-full grow overflow-auto text-sm break-normal bg-background p-2 rounded-md"> <div className="min-h-0 max-h-full grow overflow-auto break-normal p-2">
{soundEntries.map(([name, { data, onTrigger }]) => { {soundEntries.map(([name, { data, onTrigger }]) => {
return ( return (
<span <span
@ -165,7 +166,7 @@ export function SoundsTab() {
); );
})} })}
{!soundEntries.length && soundsFilter === 'importSounds' ? ( {!soundEntries.length && soundsFilter === 'importSounds' ? (
<div className="prose dark:prose-invert min-w-full pt-2 pb-8 px-4"> <div className="prose dark:prose-invert min-w-full text-sm">
<ImportSoundsButton onComplete={() => settingsMap.setKey('soundsFilter', 'user')} /> <ImportSoundsButton onComplete={() => settingsMap.setKey('soundsFilter', 'user')} />
<p> <p>
To import sounds into strudel, they must be contained{' '} To import sounds into strudel, they must be contained{' '}

View file

@ -6,7 +6,7 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
export function WelcomeTab({ context }) { export function WelcomeTab({ context }) {
const { fontFamily } = useSettings(); const { fontFamily } = useSettings();
return ( return (
<div className="prose dark:prose-invert min-w-full pt-2 font-sans pb-8 px-4 " style={{ fontFamily }}> <div className="prose dark:prose-invert min-w-full py-4 font-sans px-4 text-sm" style={{ fontFamily }}>
<h3>welcome</h3> <h3>welcome</h3>
<p> <p>
You have found <span className="underline">strudel</span>, a new live coding platform to write dynamic music You have found <span className="underline">strudel</span>, a new live coding platform to write dynamic music

View file

@ -2,6 +2,7 @@ import { persistentMap } from '@nanostores/persistent';
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import { register } from '@strudel/core'; import { register } from '@strudel/core';
import { isUdels } from './repl/util.mjs'; import { isUdels } from './repl/util.mjs';
import { computed } from 'nanostores';
export const audioEngineTargets = { export const audioEngineTargets = {
webaudio: 'webaudio', webaudio: 'webaudio',
@ -38,6 +39,7 @@ export const defaultSettings = {
latestCode: '', latestCode: '',
isZen: false, isZen: false,
soundsFilter: soundFilterType.ALL, soundsFilter: soundFilterType.ALL,
referenceTag: 'all',
patternFilter: 'community', patternFilter: 'community',
// panelPosition: window.innerWidth > 1000 ? 'right' : 'bottom', //FIX: does not work on astro // panelPosition: window.innerWidth > 1000 ? 'right' : 'bottom', //FIX: does not work on astro
panelPosition: 'right', panelPosition: 'right',
@ -63,11 +65,7 @@ const settings_key = `strudel-settings${instance > 0 ? instance : ''}`;
export const settingsMap = persistentMap(settings_key, defaultSettings); export const settingsMap = persistentMap(settings_key, defaultSettings);
export const parseBoolean = (booleanlike) => ([true, 'true'].includes(booleanlike) ? true : false); export const $settings = computed(settingsMap, (state) => {
export function useSettings() {
const state = useStore(settingsMap);
const userPatterns = JSON.parse(state.userPatterns); const userPatterns = JSON.parse(state.userPatterns);
Object.keys(userPatterns).forEach((key) => { Object.keys(userPatterns).forEach((key) => {
const data = userPatterns[key]; const data = userPatterns[key];
@ -104,6 +102,12 @@ export function useSettings() {
? true ? true
: parseBoolean(state.patternAutoStart), : parseBoolean(state.patternAutoStart),
}; };
});
export const parseBoolean = (booleanlike) => ([true, 'true'].includes(booleanlike) ? true : false);
export function useSettings() {
return useStore($settings);
} }
export const setActiveFooter = (tab) => settingsMap.setKey('activeFooter', tab); export const setActiveFooter = (tab) => settingsMap.setKey('activeFooter', tab);

View file

@ -1,4 +1,4 @@
import { atom } from 'nanostores'; import { atom, computed } from 'nanostores';
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import { logger } from '@strudel/core'; import { logger } from '@strudel/core';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
@ -36,12 +36,9 @@ export let $viewingPatternData = sessionAtom('viewingPatternData', {
created_at: Date.now(), created_at: Date.now(),
}); });
export const getViewingPatternData = () => { const $viewingPatterns = computed($viewingPatternData, (state) => parseJSON(state));
return parseJSON($viewingPatternData.get()); export const useViewingPatternData = () => useStore($viewingPatterns);
}; export const getViewingPatternData = () => $viewingPatterns.get();
export const useViewingPatternData = () => {
return useStore($viewingPatternData);
};
export const setViewingPatternData = (data) => { export const setViewingPatternData = (data) => {
$viewingPatternData.set(JSON.stringify(data)); $viewingPatternData.set(JSON.stringify(data));