mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-13 14:26:58 -04:00
Merge branch 'main' into soloshortcut
This commit is contained in:
+1
-1
@@ -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
@@ -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;
|
||||
|
||||
@@ -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 ]",
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user