Merge branch 'main' into soloshortcut

This commit is contained in:
Jade (Rose) Rowland
2026-01-22 21:11:31 -05:00
23 changed files with 500 additions and 386 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ export const settings = {
background: '#222',
lineBackground: '#22222299',
foreground: '#fff',
muted: '#ffffff50',
muted: '#8a919966',
caret: '#ffcc00',
selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#036dd626',
+117 -89
View File
@@ -1055,91 +1055,88 @@ function _composeOp(a, b, func) {
return func(a, b);
}
// Make composers
(function () {
// pattern composers
const composers = {
set: [(a, b) => b],
keep: [(a) => a],
keepif: [(a, b) => (b ? a : undefined)],
// pattern composers
const COMPOSERS = {
set: [(a, b) => b],
keep: [(a) => a],
keepif: [(a, b) => (b ? a : undefined)],
// numerical functions
/**
*
* Assumes a pattern of numbers. Adds the given number to each item in the pattern.
* @name add
* @memberof Pattern
* @tags math
* @example
* // Here, the triad 0, 2, 4 is shifted by different amounts
* n("0 2 4".add("<0 3 4 0>")).scale("C:major")
* // Without add, the equivalent would be:
* // n("<[0 2 4] [3 5 7] [4 6 8] [0 2 4]>").scale("C:major")
* @example
* // You can also use add with notes:
* note("c3 e3 g3".add("<0 5 7 0>"))
* // Behind the scenes, the notes are converted to midi numbers:
* // note("48 52 55".add("<0 5 7 0>"))
*/
add: [numeralArgs((a, b) => a + b)], // support string concatenation
/**
*
* Like add, but the given numbers are subtracted.
* @name sub
* @memberof Pattern
* @tags math
* @example
* n("0 2 4".sub("<0 1 2 3>")).scale("C4:minor")
* // See add for more information.
*/
sub: [numeralArgs((a, b) => a - b)],
/**
*
* Multiplies each number by the given factor.
* @name mul
* @memberof Pattern
* @tags math
* @example
* "<1 1.5 [1.66, <2 2.33>]>*4".mul(150).freq()
*/
mul: [numeralArgs((a, b) => a * b)],
/**
*
* Divides each number by the given factor.
* @name div
* @memberof Pattern
* @tags math
*/
div: [numeralArgs((a, b) => a / b)],
mod: [numeralArgs(_mod)],
pow: [numeralArgs(Math.pow)],
log2: [numeralArgs(Math.log2)],
band: [numeralArgs((a, b) => a & b)],
bor: [numeralArgs((a, b) => a | b)],
bxor: [numeralArgs((a, b) => a ^ b)],
blshift: [numeralArgs((a, b) => a << b)],
brshift: [numeralArgs((a, b) => a >> b)],
// numerical functions
/**
*
* Assumes a pattern of numbers. Adds the given number to each item in the pattern.
* @name add
* @memberof Pattern
* @tags math
* @example
* // Here, the triad 0, 2, 4 is shifted by different amounts
* n("0 2 4".add("<0 3 4 0>")).scale("C:major")
* // Without add, the equivalent would be:
* // n("<[0 2 4] [3 5 7] [4 6 8] [0 2 4]>").scale("C:major")
* @example
* // You can also use add with notes:
* note("c3 e3 g3".add("<0 5 7 0>"))
* // Behind the scenes, the notes are converted to midi numbers:
* // note("48 52 55".add("<0 5 7 0>"))
*/
add: [numeralArgs((a, b) => a + b)], // support string concatenation
/**
*
* Like add, but the given numbers are subtracted.
* @name sub
* @memberof Pattern
* @tags math
* @example
* n("0 2 4".sub("<0 1 2 3>")).scale("C4:minor")
* // See add for more information.
*/
sub: [numeralArgs((a, b) => a - b)],
/**
*
* Multiplies each number by the given factor.
* @name mul
* @memberof Pattern
* @tags math
* @example
* "<1 1.5 [1.66, <2 2.33>]>*4".mul(150).freq()
*/
mul: [numeralArgs((a, b) => a * b)],
/**
*
* Divides each number by the given factor.
* @name div
* @memberof Pattern
* @tags math
*/
div: [numeralArgs((a, b) => a / b)],
mod: [numeralArgs(_mod)],
pow: [numeralArgs(Math.pow)],
log2: [numeralArgs(Math.log2)],
band: [numeralArgs((a, b) => a & b)],
bor: [numeralArgs((a, b) => a | b)],
bxor: [numeralArgs((a, b) => a ^ b)],
blshift: [numeralArgs((a, b) => a << b)],
brshift: [numeralArgs((a, b) => a >> b)],
// TODO - force numerical comparison if both look like numbers?
lt: [(a, b) => a < b],
gt: [(a, b) => a > b],
lte: [(a, b) => a <= b],
gte: [(a, b) => a >= b],
eq: [(a, b) => a == b],
eqt: [(a, b) => a === b],
ne: [(a, b) => a != b],
net: [(a, b) => a !== b],
and: [(a, b) => a && b],
or: [(a, b) => a || b],
// TODO - force numerical comparison if both look like numbers?
lt: [(a, b) => a < b],
gt: [(a, b) => a > b],
lte: [(a, b) => a <= b],
gte: [(a, b) => a >= b],
eq: [(a, b) => a == b],
eqt: [(a, b) => a === b],
ne: [(a, b) => a != b],
net: [(a, b) => a !== b],
and: [(a, b) => a && b],
or: [(a, b) => a || b],
// bitwise ops
func: [(a, b) => b(a)],
};
const hows = ['In', 'Out', 'Mix', 'Squeeze', 'SqueezeOut', 'Reset', 'Restart', 'Poly'];
// bitwise ops
func: [(a, b) => b(a)],
};
const _setupAlignments = () => {
// generate methods to do what and how
for (const [what, [op, preprocess]] of Object.entries(composers)) {
for (const [what, [op, preprocess]] of Object.entries(COMPOSERS)) {
// make plain version, e.g. pat._add(value) adds that plain value
// to all the values in pat
Pattern.prototype['_' + what] = function (value) {
@@ -1148,16 +1145,18 @@ function _composeOp(a, b, func) {
// make patternified monster version
Object.defineProperty(Pattern.prototype, what, {
// Set to configurable so we can update if the default alignment changes
configurable: true,
// a getter that returns a function, so 'pat' can be
// accessed by closures that are methods of that function..
get: function () {
const pat = this;
// wrap the 'in' function as default behaviour
const wrapper = (...other) => pat[what]['in'](...other);
const wrapper = (...other) => pat[what][DEFAULT_ALIGNMENT](...other);
// add methods to that function for each behaviour
for (const how of hows) {
for (const how of ALIGNMENTS) {
wrapper[how.toLowerCase()] = function (...other) {
var howpat = pat;
other = sequence(other);
@@ -1182,15 +1181,23 @@ function _composeOp(a, b, func) {
return wrapper;
},
});
// Default op to 'set', e.g. pat.squeeze(pat2) = pat.set.squeeze(pat2)
for (const how of hows) {
Pattern.prototype[how.toLowerCase()] = function (...args) {
return this.set[how.toLowerCase()](args);
};
}
}
};
let DEFAULT_ALIGNMENT = 'in';
const ALIGNMENTS = ['In', 'Out', 'Mix', 'Squeeze', 'SqueezeOut', 'Reset', 'Restart', 'Poly'];
const ALIGNMENT_KEYS = ALIGNMENTS.map((how) => how.toLowerCase());
// Make composers
(function () {
_setupAlignments();
// Default op to 'set', e.g. pat.squeeze(pat2) = pat.set.squeeze(pat2)
for (const how of ALIGNMENTS) {
Pattern.prototype[how.toLowerCase()] = function (...args) {
return this.set[how.toLowerCase()](args);
};
}
// binary composers
/**
* Applies the given structure to the pattern:
@@ -1249,6 +1256,27 @@ function _composeOp(a, b, func) {
};
})();
/**
* Sets the default method of combining events from two patterns (aka [alignment](https://strudel.cc/technical-manual/alignment/)) in Strudel.
* The default method is 'in', meaning that patterns to the left will (typically) dictate the event timings when combined with patterns to the right.
* By changing alignment to 'out', the opposite will happen. With 'mix', they will combine their event timings.
*
* Note that we say the _default_ method, because alignments can also be set explicitly with calls like
* 'add.mix', 'set.squeeze', etc.
*
* @param {string} method Default join method to use. Options: 'in', 'out', 'mix', 'squeeze', 'squeezeout', 'reset', 'restart', 'poly'
* @example
* setDefaultJoin('mix') // also try 'in', 'out', 'squeeze', etc.
* s("saw").vel("1 0.5").note("F A C E").delay("0 0.2 0.3")
*/
export const setDefaultJoin = (alignment) => {
alignment = alignment?.toLowerCase();
if (DEFAULT_ALIGNMENT !== alignment && ALIGNMENT_KEYS.includes(alignment)) {
DEFAULT_ALIGNMENT = alignment;
_setupAlignments();
}
};
// aliases
export const polyrhythm = stack;
export const pr = stack;
+29
View File
@@ -10874,6 +10874,35 @@ exports[`runs examples > example "seqPLoop" example index 0 1`] = `
]
`;
exports[`runs examples > example "setDefaultJoin" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:saw velocity:1 note:F delay:0 ]",
"[ 1/4 → 1/3 | s:saw velocity:1 note:A delay:0 ]",
"[ 1/3 → 1/2 | s:saw velocity:1 note:A delay:0.2 ]",
"[ 1/2 → 2/3 | s:saw velocity:0.5 note:C delay:0.2 ]",
"[ 2/3 → 3/4 | s:saw velocity:0.5 note:C delay:0.3 ]",
"[ 3/4 → 1/1 | s:saw velocity:0.5 note:E delay:0.3 ]",
"[ 1/1 → 5/4 | s:saw velocity:1 note:F delay:0 ]",
"[ 5/4 → 4/3 | s:saw velocity:1 note:A delay:0 ]",
"[ 4/3 → 3/2 | s:saw velocity:1 note:A delay:0.2 ]",
"[ 3/2 → 5/3 | s:saw velocity:0.5 note:C delay:0.2 ]",
"[ 5/3 → 7/4 | s:saw velocity:0.5 note:C delay:0.3 ]",
"[ 7/4 → 2/1 | s:saw velocity:0.5 note:E delay:0.3 ]",
"[ 2/1 → 9/4 | s:saw velocity:1 note:F delay:0 ]",
"[ 9/4 → 7/3 | s:saw velocity:1 note:A delay:0 ]",
"[ 7/3 → 5/2 | s:saw velocity:1 note:A delay:0.2 ]",
"[ 5/2 → 8/3 | s:saw velocity:0.5 note:C delay:0.2 ]",
"[ 8/3 → 11/4 | s:saw velocity:0.5 note:C delay:0.3 ]",
"[ 11/4 → 3/1 | s:saw velocity:0.5 note:E delay:0.3 ]",
"[ 3/1 → 13/4 | s:saw velocity:1 note:F delay:0 ]",
"[ 13/4 → 10/3 | s:saw velocity:1 note:A delay:0 ]",
"[ 10/3 → 7/2 | s:saw velocity:1 note:A delay:0.2 ]",
"[ 7/2 → 11/3 | s:saw velocity:0.5 note:C delay:0.2 ]",
"[ 11/3 → 15/4 | s:saw velocity:0.5 note:C delay:0.3 ]",
"[ 15/4 → 4/1 | s:saw velocity:0.5 note:E delay:0.3 ]",
]
`;
exports[`runs examples > example "setGainCurve" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:bd gain:0.5 ]",
+2
View File
@@ -1,7 +1,9 @@
import { afterEach } from 'vitest';
import { useRNG } from './packages/core/signal.mjs';
import { setDefaultJoin } from './packages/core/pattern.mjs';
afterEach(() => {
// Avoid bleed between tests
useRNG('legacy');
setDefaultJoin('in');
});
+2 -2
View File
@@ -1,5 +1,5 @@
import Loader from '@src/repl/components/Loader';
import { HorizontalPanel } from '@src/repl/components/panel/Panel';
import { BottomPanel } from '@src/repl/components/panel/Panel';
import { Code } from '@src/repl/components/Code';
import BigPlayButton from '@src/repl/components/BigPlayButton';
import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage';
@@ -20,7 +20,7 @@ export default function UdelsEditor(Props) {
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
</div>
<UserFacingErrorMessage error={error} />
<HorizontalPanel context={context} />
<BottomPanel context={context} />
</div>
);
}
+1 -1
View File
@@ -8,7 +8,7 @@ export function Code(Props) {
return (
<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"
ref={(el) => {
containerRef.current = el;
@@ -2,7 +2,7 @@ import Loader from '@src/repl/components/Loader';
import { Code } from '@src/repl/components/Code';
import BigPlayButton from '@src/repl/components/BigPlayButton';
import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage';
import { Header } from './Header';
import { MainPanel } from './panel/Panel';
// type Props = {
// context: replcontext,
@@ -14,7 +14,7 @@ export default function EmbeddedReplEditor(Props) {
return (
<div className="h-full flex flex-col relative" {...editorProps}>
<Loader active={pending} />
<Header context={context} embedded={true} />
<MainPanel context={context} embedded={true} />
<BigPlayButton started={started} handleTogglePlay={handleTogglePlay} />
<div className="grow flex relative overflow-hidden">
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
-116
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>
);
}
+11 -10
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 Loader from '@src/repl/components/Loader';
import { BottomPanel, MainPanel, RightPanel } from '@src/repl/components/panel/Panel';
import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage';
import { Header } from './Header';
import { useSettings } from '@src/settings.mjs';
// type Props = {
@@ -19,15 +18,17 @@ export default function ReplEditor(Props) {
return (
<div className="h-full flex flex-col relative" {...editorProps}>
<Loader active={pending} />
<Header context={context} isEmbedded={isEmbedded} />
<div className="grow flex relative overflow-hidden">
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
{!isZen && panelPosition === 'right' && <VerticalPanel context={context} />}
<PanelToggle isEmbedded={isEmbedded} isZen={isZen} />
<div className="flex flex-col grow overflow-hidden">
{/* <MainPanel context={context} isEmbedded={isEmbedded} className="hidden sm:block" /> */}
<MainPanel context={context} isEmbedded={isEmbedded} />
<div className="flex overflow-hidden h-full">
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
{!isZen && panelPosition === 'right' && <RightPanel context={context} />}
</div>
</div>
<UserFacingErrorMessage error={error} />
{!isZen && panelPosition === 'bottom' && <HorizontalPanel context={context} />}
{!isZen && panelPosition === 'bottom' && <BottomPanel context={context} />}
{/* <MainPanel context={context} isEmbedded={isEmbedded} className="block sm:hidden" /> */}
</div>
);
}
@@ -2,7 +2,7 @@ import cx from '@src/cx.mjs';
export function ActionButton({ children, label, labelIsHidden, className, ...buttonProps }) {
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}
{children}
</button>
@@ -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>
);
}
@@ -2,13 +2,23 @@ import cx from '@src/cx.mjs';
import { useSettings } from '../../../settings.mjs';
import { useStore } from '@nanostores/react';
import { $strudel_log_history } from '../useLogger';
import { useEffect, useRef } from 'react';
export function ConsoleTab() {
const log = useStore($strudel_log_history);
const { fontFamily } = useSettings();
const scrollRef = useRef();
// scroll to bottom when log changes
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [log]);
return (
<div id="console-tab" className="break-all w-full text-sm p-2 h-full" style={{ fontFamily }}>
<div className="bg-background h-full w-full overflow-auto space-y-1 p-2 rounded-md">
<div id="console-tab" className="break-all w-full h-full" style={{ fontFamily }}>
<div className="h-full w-full overflow-auto space-y-1 p-2 rounded-md" ref={scrollRef}>
{' '}
{/* bg-background */}
{log.map((l, i) => {
const message = linkify(l.message);
const color = l.data?.hap?.value?.color;
@@ -16,12 +26,13 @@ export function ConsoleTab() {
<div
key={l.id}
className={cx(
'whitespace-nowrap',
l.type === 'error' ? 'text-background bg-foreground' : 'text-foreground',
l.type === 'highlight' && 'underline',
)}
style={color ? { color } : {}}
>
<span dangerouslySetInnerHTML={{ __html: message }} />
<span dangerouslySetInnerHTML={{ __html: message }} className="whitespace-nowrap" />
{l.count ? ` (${l.count})` : ''}
</div>
);
@@ -45,7 +45,7 @@ export default function ExportTab(Props) {
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}>
<Textbox
onBlur={(e) => {
@@ -56,7 +56,7 @@ export default function ExportTab(Props) {
}}
disabled={exporting}
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 ?? ''}
/>
</FormItem>
+3 -3
View File
@@ -1,15 +1,15 @@
import cx from '@src/cx.mjs';
export function ButtonGroup({ value, onChange, items }) {
export function ButtonGroup({ value, onChange, items, wrap = false }) {
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) => (
<button
key={key}
id={key}
onClick={() => onChange(key)}
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 === arr.length - 1 && 'rounded-r-md',
// value === key ? 'bg-background' : 'bg-lineHighlight',
+154 -24
View File
@@ -1,25 +1,151 @@
import { Bars3Icon, PlayIcon, StopIcon, XMarkIcon } from '@heroicons/react/16/solid';
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 ExportTab from './ExportTab';
import { FilesTab } from './FilesTab';
import { PatternsTab } from './PatternsTab';
import { Reference } from './Reference';
import { SettingsTab } from './SettingsTab';
import { SoundsTab } from './SoundsTab';
import { useLogger } from '../useLogger';
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 { 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(
'flex-none text-black z-[100] text-sm select-none min-h-10 max-h-10',
!isZen && !isEmbedded && 'border-b border-muted bg-lineHighlight',
isZen ? 'h-12 w-8 fixed top-0 left-0' : '',
'flex items-center',
className,
)}
style={{ fontFamily }}
>
<div className={cx('flex w-full justify-between')}>
<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>
</div>
)}
</h1>
</div>
{!isZen && (
<div className="flex grow justify-end">
{!isButtonRowHidden && <MainMenu isEmbedded={isEmbedded} context={context} />}
<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">
<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')}
>
<span className={cx('flex items-center space-x-2')}>
{started ? <StopIcon className="w-5 h-5" /> : <PlayIcon className="w-5 h-5" />}
{!isEmbedded && <span>{pending ? '...' : started ? 'stop' : 'play'}</span>}
</span>
</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() {
const { isPanelOpen } = useSettings();
return (
isPanelOpen && (
<button
onClick={() => setIsPanelOpened(false)}
className={cx('px-4 py-2 text-foreground hover:opacity-50')}
className={cx('px-2 py-0 text-foreground hover:opacity-50')}
aria-label="Close Menu"
>
<XMarkIcon className="w-6 h-6" />
@@ -28,21 +154,21 @@ function PanelCloseButton() {
);
}
export function HorizontalPanel({ context }) {
export function BottomPanel({ context }) {
const { isPanelOpen, activeFooter: tab } = useSettings();
return (
<PanelNav
className={cx(
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">
<Tabs setTab={setTab} tab={tab} />
<div className="flex justify-between min-h-10 max-h-10 grid-cols-2 items-center border-t border-muted">
<PanelCloseButton />
<Tabs setTab={setTab} tab={tab} className={cx(isPanelOpen && 'border-l border-muted')} />
</div>
{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} />
</div>
)}
@@ -50,7 +176,7 @@ export function HorizontalPanel({ context }) {
);
}
export function VerticalPanel({ context }) {
export function RightPanel({ context }) {
const settings = useSettings();
const { activeFooter: tab, isPanelOpen } = settings;
if (!isPanelOpen) {
@@ -59,14 +185,16 @@ export function VerticalPanel({ context }) {
return (
<PanelNav
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 h-full overflow-hidden',
isPanelOpen ? `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="flex justify-between w-full ">
<Tabs setTab={setTab} tab={tab} />
<div className="flex justify-between w-full overflow-hidden border-b border-muted min-h-10 max-h-10">
<PanelCloseButton />
<Tabs setTab={setTab} tab={tab} className="border-l border-muted" />
</div>
<div className="overflow-auto h-full">
<PanelContent context={context} tab={tab} />
</div>
@@ -98,7 +226,7 @@ function PanelNav({ children, className, ...props }) {
}
}}
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}
>
{children}
@@ -134,7 +262,7 @@ function PanelTab({ label, isSelected, onClick }) {
<button
onClick={onClick}
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',
)}
>
@@ -146,7 +274,12 @@ function PanelTab({ label, isSelected, onClick }) {
function Tabs({ className }) {
const { isPanelOpen, activeFooter: tab } = useSettings();
return (
<div className={cx('flex select-none max-w-full overflow-auto items-center', className)}>
<div
className={cx(
'px-2 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) => {
const val = tabNames[key];
return <PanelTab key={key} isSelected={tab === val && isPanelOpen} label={key} onClick={() => setTab(val)} />;
@@ -162,11 +295,8 @@ export function PanelToggle({ isEmbedded, isZen }) {
!isZen &&
panelPosition === 'right' && (
<button
title="share"
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 ',
isPanelOpen && 'hidden',
)}
title="menu"
className={cx('border-l border-muted px-2 py-0 text-foreground hover:opacity-50')}
onClick={() => setIsPanelOpened(!isPanelOpen)}
>
<Bars3Icon className="w-6 h-6" />
@@ -8,7 +8,7 @@ import {
useViewingPatternData,
userPattern,
} from '../../../user_pattern_utils.mjs';
import { useMemo } from 'react';
import { useMemo, useRef } from 'react';
import { getMetadata } from '../../../metadata_parser.js';
import { useExamplePatterns } from '../../useExamplePatterns.jsx';
import { parseJSON, isUdels } from '../../util.mjs';
@@ -43,7 +43,7 @@ function PatternButton({ showOutline, onClick, pattern, showHiglight }) {
className={cx(
'mr-4 hover:opacity-50 cursor-pointer block',
showOutline && 'outline outline-1',
showHiglight && 'bg-selection',
showHiglight && 'ring-selection',
)}
onClick={onClick}
>
@@ -53,11 +53,10 @@ function PatternButton({ showOutline, onClick, pattern, showHiglight }) {
}
function PatternButtons({ patterns, activePattern, onClick, started }) {
const viewingPatternStore = useViewingPatternData();
const viewingPatternData = parseJSON(viewingPatternStore);
const viewingPatternData = useViewingPatternData();
const viewingPatternID = viewingPatternData.id;
return (
<div className="">
<div className="p-2">
{Object.values(patterns)
.reverse()
.map((pattern) => {
@@ -83,8 +82,8 @@ const updateCodeWindow = (context, patternData, reset = false) => {
export function PatternsTab({ context }) {
const [search, setSearch] = useState('');
const activePattern = useActivePattern();
const viewingPatternStore = useViewingPatternData();
const viewingPatternData = parseJSON(viewingPatternStore);
const viewingPatternData = useViewingPatternData();
const { userPatterns, patternAutoStart } = useSettings();
const viewingPatternID = viewingPatternData?.id;
@@ -121,74 +120,71 @@ export function PatternsTab({ context }) {
);
}),
);
}, [search, viewingPatternStore]);
}, [search, userPatterns]);
const importRef = useRef();
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 flex">
<Textbox className="w-full" placeholder="Search" value={search} onChange={setSearch} />
<div className="w-full h-full text-foreground flex flex-col overflow-hidden">
<Textbox className="w-full border-0" 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 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
label="delete-all"
onClick={() => {
const { data } = userPattern.clearAll();
updateCodeWindow(context, data);
}}
/>
</div>
<div className="overflow-auto">
{/* bg-background */}
{/* {patternFilter === patternFilterName.user && ( */}
<PatternButtons
onClick={(id) => {
updateCodeWindow(context, { ...userPatterns[id], collection: userPattern.collection }, patternAutoStart);
<div className="overflow-auto h-full bg-background p-2">
{/* {patternFilter === patternFilterName.user && ( */}
<PatternButtons
onClick={(id) => {
updateCodeWindow(context, { ...userPatterns[id], collection: userPattern.collection }, patternAutoStart);
if (context.started && activePattern === id) {
context.handleEvaluate();
}
}}
patterns={visiblePatterns}
started={context.started}
activePattern={activePattern}
viewingPatternID={viewingPatternID}
/>
{/* )} */}
</div>
if (context.started && activePattern === id) {
context.handleEvaluate();
}
}}
patterns={visiblePatterns}
started={context.started}
activePattern={activePattern}
viewingPatternID={viewingPatternID}
/>
{/* )} */}
</div>
</div>
);
+49 -37
View File
@@ -2,6 +2,7 @@ import { memo, useEffect, useMemo, useState, Fragment } from 'react';
import jsdocJson from '../../../../../doc.json';
import { Textbox } from '@src/repl/components/panel/SettingsTab';
import { settingsMap, useSettings } from '@src/settings.mjs';
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));
})();
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) => {
var div = document.createElement('div');
div.innerHTML = html;
@@ -45,40 +59,38 @@ const getInnerText = (html) => {
export const Reference = memo(function Reference() {
const [search, setSearch] = useState('');
const [selectedTag, setSelectedTag] = useState(null);
const [selectedFunction, setSelectedFunction] = useState(null);
const { referenceTag } = useSettings();
const toggleTag = (tag) => {
if (selectedTag === tag) {
setSelectedTag(null);
if (referenceTag === tag) {
setReferenceTag('all');
} else {
setSelectedTag(tag);
setReferenceTag(tag);
}
};
const searchVisibleFunctions = useMemo(() => {
return availableFunctions.filter((entry) => {
if (selectedTag) {
if (!(entry.tags || ['untagged']).includes(selectedTag)) {
if (referenceTag && referenceTag !== 'all') {
if (!(entry.tags || ['untagged']).includes(referenceTag)) {
return false;
}
}
if (!search) {
return true;
}
const lowerCaseSearch = search.toLowerCase();
return (
entry.name.toLowerCase().includes(lowerCaseSearch) ||
(entry.synonyms?.some((s) => s.toLowerCase().includes(lowerCaseSearch)) ?? false)
);
});
}, [search, selectedTag]);
}, [search, referenceTag]);
const detailVisibleFunctions = useMemo(() => {
return searchVisibleFunctions.filter((x) => {
if (selectedTag === null) {
if (referenceTag === null || referenceTag === 'all') {
if (search) {
return true;
}
@@ -87,19 +99,10 @@ export const Reference = memo(function Reference() {
return true;
}
});
}, [searchVisibleFunctions, selectedFunction, selectedTag]);
const tagCounts = {};
for (const doc of availableFunctions) {
(doc.tags || ['untagged']).forEach((t) => {
if (typeof t === 'string' && t) {
tagCounts[t] = (tagCounts[t] || 0) + 1;
}
});
}
}, [searchVisibleFunctions, selectedFunction, referenceTag]);
const onSearchTagFilterClick = () => {
setSelectedTag(null);
setReferenceTag('all');
setSelectedFunction(null);
};
@@ -111,36 +114,44 @@ export const Reference = memo(function Reference() {
}
}, [selectedFunction]);
let setReferenceTag = (value) => settingsMap.setKey('referenceTag', value);
return (
<div className="flex h-full w-full p-2 overflow-hidden">
<div className="h-full text-foreground flex flex-col gap-3 w-1/3 ">
<div className="flex h-full w-full overflow-hidden">
<div className="h-full text-foreground flex flex-col w-1/3 border-r border-muted">
{/* bg-background */}
<div className="w-full flex">
<Textbox
className="w-full"
placeholder="Search"
className="w-full border-0 border-b border-muted"
placeholder="Search..."
value={search}
onChange={(e) => {
setSelectedFunction(null);
setReferenceTag('all');
setSearch(e);
}}
/>
</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">
<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}
>
{selectedTag}
{referenceTag}
</span>
</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) => (
<Fragment key={`entry-${entry.name}`}>
<a
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' : '')
}
onClick={() => {
@@ -158,16 +169,17 @@ export const Reference = memo(function Reference() {
</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"
>
<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>
<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
that you can already make music with a small set of functions!
</p>
<div>
{/* <ButtonGroup wrap value={referenceTag} onChange={setReferenceTag} items={tagOptions}/>*/}
{Object.entries(tagCounts)
.sort(([a], [b]) => a.localeCompare(b))
.map(([t, count]) => (
@@ -175,7 +187,7 @@ export const Reference = memo(function Reference() {
<a
className={[
'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(' ')}
onClick={() => toggleTag(t)}
>
@@ -187,7 +199,7 @@ export const Reference = memo(function Reference() {
{detailVisibleFunctions.map((entry, i) => (
<section key={i} className="font-sans">
<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}
</h3>
{entry.tags && (
@@ -201,7 +213,7 @@ export const Reference = memo(function Reference() {
Synonyms: <code>{entry.synonyms_text}</code>
</p>
)}
{/* <small>{entry.meta.filename}</small> */}
<p dangerouslySetInnerHTML={{ __html: entry.description }}></p>
<ul>
{entry.params?.map(({ name, type, description }, i) => (
@@ -211,12 +223,12 @@ export const Reference = memo(function Reference() {
))}
</ul>
{entry.examples?.map((example, j) => (
<pre className="bg-background" key={j}>
<pre className="bg-background border border-muted" key={j}>
{example}
</pre>
))}
</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" />}
</div>
</div>
@@ -15,17 +15,17 @@ function cx(...classes) {
}
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 }) {
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 }) {
return (
<label className="text-sm">
<label className="text-xs">
<input
className={cx(
'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 }) {
return (
<div className="grid gap-2 text-sm">
<div className="grid gap-2 text-xs">
<label className="text-sm">{label}</label>
{children}
</div>
@@ -154,7 +154,7 @@ export function SettingsTab({ started }) {
const shouldAlwaysSync = isUdels();
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
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 && (
<FormItem label="Audio Output Device">
<AudioDeviceSelector
@@ -323,7 +323,7 @@ export function SettingsTab({ started }) {
value={isSyncEnabled}
/>
<Checkbox
label="Hide top buttons"
label="Hide action buttons"
onChange={(cbEvent) => settingsMap.setKey('isButtonRowHidden', cbEvent.target.checked)}
value={isButtonRowHidden}
/>
@@ -77,11 +77,12 @@ export function SoundsTab() {
numRef.current = 0;
});
return (
<div id="sounds-tab" className="px-4 flex gap-2 flex-col w-full h-full text-foreground">
<Textbox placeholder="Search" value={search} onChange={(v) => setSearch(v)} />
<div id="sounds-tab" className="flex flex-col w-full h-full text-foreground">
<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
wrap
value={soundsFilter}
onChange={(value) => settingsMap.setKey('soundsFilter', value)}
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 }]) => {
return (
<span
@@ -165,7 +166,7 @@ export function SoundsTab() {
);
})}
{!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')} />
<p>
To import sounds into strudel, they must be contained{' '}
@@ -6,7 +6,7 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
export function WelcomeTab({ context }) {
const { fontFamily } = useSettings();
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>
<p>
You have found <span className="underline">strudel</span>, a new live coding platform to write dynamic music
+1 -1
View File
@@ -21,7 +21,7 @@ function getUpdatedLog(log, event) {
} else {
log = log.concat([{ message, type, id, data }]);
}
return log.slice(-20);
return log.slice(-40);
}
export function useLogger() {
+9 -5
View File
@@ -2,6 +2,7 @@ import { persistentMap } from '@nanostores/persistent';
import { useStore } from '@nanostores/react';
import { register } from '@strudel/core';
import { isUdels } from './repl/util.mjs';
import { computed } from 'nanostores';
export const audioEngineTargets = {
webaudio: 'webaudio',
@@ -38,6 +39,7 @@ export const defaultSettings = {
latestCode: '',
isZen: false,
soundsFilter: soundFilterType.ALL,
referenceTag: 'all',
patternFilter: 'community',
// panelPosition: window.innerWidth > 1000 ? 'right' : 'bottom', //FIX: does not work on astro
panelPosition: 'right',
@@ -63,11 +65,7 @@ const settings_key = `strudel-settings${instance > 0 ? instance : ''}`;
export const settingsMap = persistentMap(settings_key, defaultSettings);
export const parseBoolean = (booleanlike) => ([true, 'true'].includes(booleanlike) ? true : false);
export function useSettings() {
const state = useStore(settingsMap);
export const $settings = computed(settingsMap, (state) => {
const userPatterns = JSON.parse(state.userPatterns);
Object.keys(userPatterns).forEach((key) => {
const data = userPatterns[key];
@@ -104,6 +102,12 @@ export function useSettings() {
? true
: 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);
+4 -7
View File
@@ -1,4 +1,4 @@
import { atom } from 'nanostores';
import { atom, computed } from 'nanostores';
import { useStore } from '@nanostores/react';
import { logger } from '@strudel/core';
import { nanoid } from 'nanoid';
@@ -36,12 +36,9 @@ export let $viewingPatternData = sessionAtom('viewingPatternData', {
created_at: Date.now(),
});
export const getViewingPatternData = () => {
return parseJSON($viewingPatternData.get());
};
export const useViewingPatternData = () => {
return useStore($viewingPatternData);
};
const $viewingPatterns = computed($viewingPatternData, (state) => parseJSON(state));
export const useViewingPatternData = () => useStore($viewingPatterns);
export const getViewingPatternData = () => $viewingPatterns.get();
export const setViewingPatternData = (data) => {
$viewingPatternData.set(JSON.stringify(data));