diff --git a/package-lock.json b/package-lock.json index b452e4d9..3895e6ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6740,6 +6740,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -9374,6 +9375,7 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "dev": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -11100,11 +11102,10 @@ "packages/react": { "name": "@strudel.cycles/react", "version": "0.0.0", + "peer": true, "dependencies": { "@codemirror/lang-javascript": "^0.19.0", - "react": "^17.0.2", "react-codemirror6": "^1.1.0", - "react-dom": "^17.0.2", "react-hook-inview": "^4.5.0" }, "devDependencies": { @@ -11113,8 +11114,14 @@ "@vitejs/plugin-react": "^1.3.0", "autoprefixer": "^10.4.7", "postcss": "^8.4.13", + "react": "^17.0.2", + "react-dom": "^17.0.2", "tailwindcss": "^3.0.24", "vite": "^2.9.9" + }, + "peerDependencies": { + "react": "^17.0.2", + "react-dom": "^17.0.2" } }, "packages/react/node_modules/@types/react": { @@ -11141,6 +11148,7 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "dev": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -11153,6 +11161,7 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "dev": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -13069,13 +13078,13 @@ "@types/react": "^17.0.2", "@types/react-dom": "^17.0.2", "@vitejs/plugin-react": "^1.3.0", - "autoprefixer": "10.4.7", - "postcss": "8.4.13", + "autoprefixer": "^10.4.7", + "postcss": "^8.4.13", "react": "^17.0.2", "react-codemirror6": "^1.1.0", "react-dom": "^17.0.2", "react-hook-inview": "^4.5.0", - "tailwindcss": "3.0.24", + "tailwindcss": "^3.0.24", "vite": "^2.9.9" }, "dependencies": { @@ -13103,6 +13112,7 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "dev": true, "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -13112,6 +13122,7 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "dev": true, "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -16579,6 +16590,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, "requires": { "js-tokens": "^3.0.0 || ^4.0.0" } @@ -17979,13 +17991,13 @@ "@types/react": "^17.0.2", "@types/react-dom": "^17.0.2", "@vitejs/plugin-react": "^1.3.0", - "autoprefixer": "10.4.7", - "postcss": "8.4.13", + "autoprefixer": "^10.4.7", + "postcss": "^8.4.13", "react": "^17.0.2", "react-codemirror6": "^1.1.0", "react-dom": "^17.0.2", "react-hook-inview": "^4.5.0", - "tailwindcss": "3.0.24", + "tailwindcss": "^3.0.24", "vite": "^2.9.9" }, "dependencies": { @@ -18013,6 +18025,7 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "dev": true, "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -18022,6 +18035,7 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "dev": true, "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -18610,6 +18624,7 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "dev": true, "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" diff --git a/packages/react/.gitignore b/packages/react/.gitignore index a547bf36..0b37e8f5 100644 --- a/packages/react/.gitignore +++ b/packages/react/.gitignore @@ -8,10 +8,11 @@ pnpm-debug.log* lerna-debug.log* node_modules -dist dist-ssr *.local +!dist + # Editor directories and files .vscode/* !.vscode/extensions.json diff --git a/packages/react/dist/index.cjs.js b/packages/react/dist/index.cjs.js new file mode 100644 index 00000000..684f64de --- /dev/null +++ b/packages/react/dist/index.cjs.js @@ -0,0 +1,5 @@ +"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});var s=require("react"),re=require("react-codemirror6"),x=require("@codemirror/view"),G=require("@codemirror/state"),oe=require("@codemirror/lang-javascript"),i=require("@codemirror/highlight"),ae=require("react-hook-inview"),Q=require("@strudel.cycles/eval"),ne=require("@strudel.cycles/core/util.mjs"),f=require("@strudel.cycles/tone"),A=require("@strudel.cycles/core");function ce(e){return e&&typeof e=="object"&&"default"in e?e:{default:e}}function M(e){if(e&&e.__esModule)return e;var o=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});return e&&Object.keys(e).forEach(function(a){if(a!=="default"){var t=Object.getOwnPropertyDescriptor(e,a);Object.defineProperty(o,a,t.get?t:{enumerable:!0,get:function(){return e[a]}})}}),o.default=e,Object.freeze(o)}var m=ce(s);const se="#abb2bf",le="#7d8799",ie="#ffffff",ue="#21252b",j="rgba(0, 0, 0, 0.5)",de="transparent",B="#353a42",fe="rgba(128, 203, 196, 0.2)",z="#ffcc00",ge=x.EditorView.theme({"&":{color:"#ffffff",backgroundColor:de,fontSize:"15px","z-index":11},".cm-content":{caretColor:z,lineHeight:"22px"},".cm-line":{background:"#2C323699"},"&.cm-focused .cm-cursor":{borderLeftColor:z},"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection":{backgroundColor:fe},".cm-panels":{backgroundColor:ue,color:"#ffffff"},".cm-panels.cm-panels-top":{borderBottom:"2px solid black"},".cm-panels.cm-panels-bottom":{borderTop:"2px solid black"},".cm-searchMatch":{backgroundColor:"#72a1ff59",outline:"1px solid #457dff"},".cm-searchMatch.cm-searchMatch-selected":{backgroundColor:"#6199ff2f"},".cm-activeLine":{backgroundColor:j},".cm-selectionMatch":{backgroundColor:"#aafe661a"},"&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket":{backgroundColor:"#bad0f847",outline:"1px solid #515a6b"},".cm-gutters":{background:"#2C323699",color:"#676e95",border:"none"},".cm-activeLineGutter":{backgroundColor:j},".cm-foldPlaceholder":{backgroundColor:"transparent",border:"none",color:"#ddd"},".cm-tooltip":{border:"none",backgroundColor:B},".cm-tooltip .cm-tooltip-arrow:before":{borderTopColor:"transparent",borderBottomColor:"transparent"},".cm-tooltip .cm-tooltip-arrow:after":{borderTopColor:B,borderBottomColor:B},".cm-tooltip-autocomplete":{"& > ul > li[aria-selected]":{backgroundColor:j,color:se}}},{dark:!0}),me=i.HighlightStyle.define([{tag:i.tags.keyword,color:"#c792ea"},{tag:i.tags.operator,color:"#89ddff"},{tag:i.tags.special(i.tags.variableName),color:"#eeffff"},{tag:i.tags.typeName,color:"#f07178"},{tag:i.tags.atom,color:"#f78c6c"},{tag:i.tags.number,color:"#ff5370"},{tag:i.tags.definition(i.tags.variableName),color:"#82aaff"},{tag:i.tags.string,color:"#c3e88d"},{tag:i.tags.special(i.tags.string),color:"#f07178"},{tag:i.tags.comment,color:le},{tag:i.tags.variableName,color:"#f07178"},{tag:i.tags.tagName,color:"#ff5370"},{tag:i.tags.bracket,color:"#a2a1a4"},{tag:i.tags.meta,color:"#ffcb6b"},{tag:i.tags.attributeName,color:"#c792ea"},{tag:i.tags.propertyName,color:"#c792ea"},{tag:i.tags.className,color:"#decb6b"},{tag:i.tags.invalid,color:ie}]),he=[ge,me],D=G.StateEffect.define(),be=G.StateField.define({create(){return x.Decoration.none},update(e,o){try{for(let a of o.effects)a.is(D)&&(e=x.Decoration.set(a.value.flatMap(t=>(t.context.locations||[]).map(({start:r,end:c})=>{const u=t.context.color||"#FFCA28";let g=o.newDoc.line(r.line).from+r.column,l=o.newDoc.line(c.line).from+c.column;const h=o.newDoc.length;return g>h||l>h?void 0:x.Decoration.mark({attributes:{style:`outline: 1px solid ${u}`}}).range(g,l)})).filter(Boolean),!0));return e}catch{return e}},provide:e=>x.EditorView.decorations.from(e)});function U({value:e,onChange:o,onViewChanged:a,onCursor:t,options:r,editorDidMount:c}){return m.default.createElement(m.default.Fragment,null,m.default.createElement(re.CodeMirror,{onViewChange:a,style:{display:"flex",flexDirection:"column",flex:"1 0 auto"},value:e,onChange:o,extensions:[oe.javascript(),he,be]}))}let F;const pe=(e,o)=>{const a=e.getDoc().getValue(),t=W(a,o);F?.clear(),F=e.getDoc().markText(...t,{css:"background-color: #00007720"})};function J(e,o){const a=o.split(` +`);let t=0,r=0;for(let c=0;ca.length)return 0;let t=0;for(let r=0;r0&&(e[r-1]==="("?t--:e[r-1]===")"&&t++,t!==-1);)r--;for(c=r,r=a,t=0;rJ(g,e))}var ve=Object.freeze(Object.defineProperty({__proto__:null,setHighlights:D,default:U,markParens:pe,offsetToPosition:J,positionToOffset:K,getCurrentParenArea:W},Symbol.toStringTag,{value:"Module"}));function ye(e){const{onEvent:o,onQuery:a,onSchedule:t,ready:r=!0,onDraw:c}=e,[u,g]=s.useState(!1),l=1,h=()=>Math.floor(f.Tone.getTransport().seconds/l),y=(v=h())=>{const q=new A.TimeSpan(v,v+1),R=a?.(new A.State(q))||[];t?.(R,v);const O=q.begin.valueOf();f.Tone.getTransport().cancel(O);const P=(v+1)*l-.5,N=Math.max(f.Tone.getTransport().seconds,P)+.1;f.Tone.getTransport().schedule(()=>{y(v+1)},N),R?.filter(p=>p.part.begin.equals(p.whole?.begin)).forEach(p=>{f.Tone.getTransport().schedule(w=>{o(w,p,f.Tone.getContext().currentTime),f.Tone.Draw.schedule(()=>{c?.(w,p)},w)},p.part.begin.valueOf())})};s.useEffect(()=>{r&&y()},[o,t,a,c,r]);const E=async()=>{g(!0),await f.Tone.start(),f.Tone.getTransport().start("+0.1")},b=()=>{f.Tone.getTransport().pause(),g(!1)};return{start:E,stop:b,onEvent:o,started:u,setStarted:g,toggle:()=>u?b():E(),query:y,activeCycle:h}}function Ce(e){return s.useEffect(()=>(window.addEventListener("message",e),()=>window.removeEventListener("message",e)),[e]),s.useCallback(o=>window.postMessage(o,"*"),[])}let we=()=>Math.floor((1+Math.random())*65536).toString(16).substring(1);const ke=e=>encodeURIComponent(btoa(e));function Te({tune:e,defaultSynth:o,autolink:a=!0,onEvent:t,onDraw:r}){const c=s.useMemo(()=>we(),[]),[u,g]=s.useState(e),[l,h]=s.useState(),[y,E]=s.useState(""),[b,C]=s.useState(),[v,q]=s.useState(!1),[R,O]=s.useState(""),[P,N]=s.useState(),p=s.useMemo(()=>u!==l||b,[u,l,b]),w=s.useCallback(d=>E(n=>n+`${n?` + +`:""}${d}`),[]),X=s.useMemo(()=>{if(l&&!l.includes("strudel disable-highlighting"))return(d,n)=>r?.(d,n,l)},[l,r]),T=ye({onDraw:X,onEvent:s.useCallback((d,n,Z)=>{try{t?.(n),n.context.logs?.length&&n.context.logs.forEach(w);const{onTrigger:_,velocity:ee}=n.context;if(_)_(d,n,Z,1,n.wholeOrPart().begin.valueOf(),n.duration.valueOf());else if(o){const te=ne.getPlayableNoteValue(n);o.triggerAttackRelease(te,n.duration.valueOf(),d,ee)}else throw new Error("no defaultSynth passed to useRepl.")}catch(_){console.warn(_),_.message="unplayable event: "+_?.message,w(_.message)}},[t,w,o]),onQuery:s.useCallback(d=>{try{return P?.query(d)||[]}catch(n){return console.warn(n),n.message="query error: "+n.message,C(n),[]}},[P]),onSchedule:s.useCallback((d,n)=>Y(d,n),[]),ready:!!P&&!!l}),H=Ce(({data:{from:d,type:n}})=>{n==="start"&&d!==c&&(T.setStarted(!1),h(void 0))}),V=s.useCallback(async(d=u)=>{if(l&&!p){C(void 0),T.start();return}try{q(!0);const n=await Q.evaluate(d);T.start(),H({type:"start",from:c}),N(()=>n.pattern),a&&(window.location.hash="#"+encodeURIComponent(btoa(u))),O(ke(u)),C(void 0),h(d),q(!1)}catch(n){n.message="evaluation error: "+n.message,console.warn(n),C(n)}},[l,p,u,T,a,c,H]),Y=(d,n)=>{d.length};return{pending:v,code:u,setCode:g,pattern:P,error:b,cycle:T,setPattern:N,dirty:p,log:y,togglePlay:()=>{T.started?T.stop():V()},setActiveCode:h,activateCode:V,activeCode:l,pushLog:w,hash:R}}function L(...e){return e.filter(Boolean).join(" ")}let S=[],I;function _e({view:e,pattern:o,active:a}){s.useEffect(()=>{if(e)if(o&&a){let r=function(){try{const c=f.Tone.getTransport().seconds,u=[I||c,c+1/60];I=c+1/60,S=S.filter(l=>l.whole.end>c);const g=o.queryArc(...u).filter(l=>l.hasOnset());S=S.concat(g),e.dispatch({effects:D.of(S)})}catch{e.dispatch({effects:D.of([])})}t=requestAnimationFrame(r)},t=requestAnimationFrame(r);return()=>{cancelAnimationFrame(t)}}else S=[],e.dispatch({effects:D.of([])})},[o,a,e])}const Me="_container_10e1g_1",Ee="_header_10e1g_5",Pe="_buttons_10e1g_9",Se="_button_10e1g_9",qe="_buttonDisabled_10e1g_17",xe="_error_10e1g_21",De="_body_10e1g_25";var k={container:Me,header:Ee,buttons:Pe,button:Se,buttonDisabled:qe,error:xe,body:De};function $({type:e}){return React.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",className:"sc-h-5 sc-w-5",viewBox:"0 0 20 20",fill:"currentColor"},{refresh:React.createElement("path",{fillRule:"evenodd",d:"M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z",clipRule:"evenodd"}),play:React.createElement("path",{fillRule:"evenodd",d:"M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z",clipRule:"evenodd"}),pause:React.createElement("path",{fillRule:"evenodd",d:"M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z",clipRule:"evenodd"})}[e])}Q.evalScope(f.Tone,Promise.resolve().then(function(){return M(require("@strudel.cycles/core"))}),Promise.resolve().then(function(){return M(require("@strudel.cycles/tone"))}),Promise.resolve().then(function(){return M(require("@strudel.cycles/tonal"))}),Promise.resolve().then(function(){return M(require("@strudel.cycles/mini"))}),Promise.resolve().then(function(){return M(require("@strudel.cycles/midi"))}),Promise.resolve().then(function(){return M(require("@strudel.cycles/xen"))}),Promise.resolve().then(function(){return M(require("@strudel.cycles/webaudio"))}));const Re=new f.Tone.PolySynth().chain(new f.Tone.Gain(.5),f.Tone.Destination).set({oscillator:{type:"triangle"},envelope:{release:.01}});function Ne({tune:e}){const{code:o,setCode:a,pattern:t,activateCode:r,error:c,cycle:u,dirty:g,togglePlay:l}=Te({tune:e,defaultSynth:Re,autolink:!1}),[h,y]=s.useState(),[E,b]=ae.useInView({threshold:.01}),C=s.useRef(),v=s.useMemo(()=>(b&&(C.current=!0),b||C.current),[b]);return _e({view:h,pattern:t,active:u.started}),m.default.createElement("div",{className:k.container,ref:E},m.default.createElement("div",{className:k.header},m.default.createElement("div",{className:k.buttons},m.default.createElement("button",{className:L(k.button,u.started?"sc-animate-pulse":""),onClick:()=>l()},m.default.createElement($,{type:u.started?"pause":"play"})),m.default.createElement("button",{className:L(g?k.button:k.buttonDisabled),onClick:()=>r()},m.default.createElement($,{type:"refresh"}))),c&&m.default.createElement("div",{className:k.error},c.message)),m.default.createElement("div",{className:k.body},v&&m.default.createElement(U,{value:o,onChange:a,onViewChanged:y})))}exports.CodeMirror=ve;exports.MiniRepl=Ne; diff --git a/packages/react/dist/index.es.js b/packages/react/dist/index.es.js new file mode 100644 index 00000000..e27a6f66 --- /dev/null +++ b/packages/react/dist/index.es.js @@ -0,0 +1,663 @@ +import React$1, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; +import { CodeMirror as CodeMirror$1 } from 'react-codemirror6'; +import { EditorView, Decoration } from '@codemirror/view'; +import { StateEffect, StateField } from '@codemirror/state'; +import { javascript } from '@codemirror/lang-javascript'; +import { HighlightStyle, tags } from '@codemirror/highlight'; +import { useInView } from 'react-hook-inview'; +import { evaluate, evalScope } from '@strudel.cycles/eval'; +import { getPlayableNoteValue } from '@strudel.cycles/core/util.mjs'; +import { Tone } from '@strudel.cycles/tone'; +import { TimeSpan, State } from '@strudel.cycles/core'; + +/* + Credits for color palette: + + Author: Mattia Astorino (http://github.com/equinusocio) + Website: https://material-theme.site/ +*/ + +const ivory = '#abb2bf', + stone = '#7d8799', // Brightened compared to original to increase contrast + invalid = '#ffffff', + darkBackground = '#21252b', + highlightBackground = 'rgba(0, 0, 0, 0.5)', + // background = '#292d3e', + background = 'transparent', + tooltipBackground = '#353a42', + selection = 'rgba(128, 203, 196, 0.2)', + cursor = '#ffcc00'; + +/// The editor theme styles for Material Palenight. +const materialPalenightTheme = EditorView.theme( + { + // done + '&': { + color: '#ffffff', + backgroundColor: background, + fontSize: '15px', + 'z-index': 11, + }, + + // done + '.cm-content': { + caretColor: cursor, + lineHeight: '22px', + }, + '.cm-line': { + background: '#2C323699', + }, + // done + '&.cm-focused .cm-cursor': { + borderLeftColor: cursor, + }, + + '&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection': { + backgroundColor: selection, + }, + + '.cm-panels': { backgroundColor: darkBackground, color: '#ffffff' }, + '.cm-panels.cm-panels-top': { borderBottom: '2px solid black' }, + '.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' }, + + // done, use onedarktheme + '.cm-searchMatch': { + backgroundColor: '#72a1ff59', + outline: '1px solid #457dff', + }, + '.cm-searchMatch.cm-searchMatch-selected': { + backgroundColor: '#6199ff2f', + }, + + '.cm-activeLine': { backgroundColor: highlightBackground }, + '.cm-selectionMatch': { backgroundColor: '#aafe661a' }, + + '&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': { + backgroundColor: '#bad0f847', + outline: '1px solid #515a6b', + }, + + // done + '.cm-gutters': { + background: '#2C323699', + color: '#676e95', + border: 'none', + }, + + '.cm-activeLineGutter': { + backgroundColor: highlightBackground, + }, + + '.cm-foldPlaceholder': { + backgroundColor: 'transparent', + border: 'none', + color: '#ddd', + }, + + '.cm-tooltip': { + border: 'none', + backgroundColor: tooltipBackground, + }, + '.cm-tooltip .cm-tooltip-arrow:before': { + borderTopColor: 'transparent', + borderBottomColor: 'transparent', + }, + '.cm-tooltip .cm-tooltip-arrow:after': { + borderTopColor: tooltipBackground, + borderBottomColor: tooltipBackground, + }, + '.cm-tooltip-autocomplete': { + '& > ul > li[aria-selected]': { + backgroundColor: highlightBackground, + color: ivory, + }, + }, + }, + { dark: true }, +); + +/// The highlighting style for code in the Material Palenight theme. +const materialPalenightHighlightStyle = HighlightStyle.define([ + { tag: tags.keyword, color: '#c792ea' }, + { tag: tags.operator, color: '#89ddff' }, + { tag: tags.special(tags.variableName), color: '#eeffff' }, + { tag: tags.typeName, color: '#f07178' }, + { tag: tags.atom, color: '#f78c6c' }, + { tag: tags.number, color: '#ff5370' }, + { tag: tags.definition(tags.variableName), color: '#82aaff' }, + { tag: tags.string, color: '#c3e88d' }, + { tag: tags.special(tags.string), color: '#f07178' }, + { tag: tags.comment, color: stone }, + { tag: tags.variableName, color: '#f07178' }, + { tag: tags.tagName, color: '#ff5370' }, + { tag: tags.bracket, color: '#a2a1a4' }, + { tag: tags.meta, color: '#ffcb6b' }, + { tag: tags.attributeName, color: '#c792ea' }, + { tag: tags.propertyName, color: '#c792ea' }, + { tag: tags.className, color: '#decb6b' }, + { tag: tags.invalid, color: invalid }, +]); + +/// Extension to enable the Material Palenight theme (both the editor theme and +/// the highlight style). +// : Extension +const materialPalenight = [materialPalenightTheme, materialPalenightHighlightStyle]; + +const setHighlights = StateEffect.define(); +const highlightField = StateField.define({ + create() { + return Decoration.none; + }, + update(highlights, tr) { + try { + for (let e of tr.effects) { + if (e.is(setHighlights)) { + highlights = Decoration.set(e.value.flatMap((hap) => (hap.context.locations || []).map(({ start, end }) => { + const color = hap.context.color || "#FFCA28"; + let from = tr.newDoc.line(start.line).from + start.column; + let to = tr.newDoc.line(end.line).from + end.column; + const l = tr.newDoc.length; + if (from > l || to > l) { + return; + } + const mark = Decoration.mark({ attributes: { style: `outline: 1px solid ${color}` } }); + return mark.range(from, to); + })).filter(Boolean), true); + } + } + return highlights; + } catch (err) { + return highlights; + } + }, + provide: (f) => EditorView.decorations.from(f) +}); +function CodeMirror({ value, onChange, onViewChanged, onCursor, options, editorDidMount }) { + return /* @__PURE__ */ React$1.createElement(React$1.Fragment, null, /* @__PURE__ */ React$1.createElement(CodeMirror$1, { + onViewChange: onViewChanged, + style: { + display: "flex", + flexDirection: "column", + flex: "1 0 auto" + }, + value, + onChange, + extensions: [ + javascript(), + materialPalenight, + highlightField + ] + })); +} +let parenMark; +const markParens = (editor, data) => { + const v = editor.getDoc().getValue(); + const marked = getCurrentParenArea(v, data); + parenMark?.clear(); + parenMark = editor.getDoc().markText(...marked, { css: "background-color: #00007720" }); +}; +function offsetToPosition(offset, code) { + const lines = code.split("\n"); + let line = 0; + let ch = 0; + for (let i = 0; i < offset; i++) { + if (ch === lines[line].length) { + line++; + ch = 0; + } else { + ch++; + } + } + return { line, ch }; +} +function positionToOffset(position, code) { + const lines = code.split("\n"); + if (position.line > lines.length) { + return 0; + } + let offset = 0; + for (let i = 0; i < position.line; i++) { + offset += lines[i].length + 1; + } + offset += position.ch; + return offset; +} +function getCurrentParenArea(code, caretPosition) { + const caret = positionToOffset(caretPosition, code); + let open, i, begin, end; + i = caret; + open = 0; + while (i > 0) { + if (code[i - 1] === "(") { + open--; + } else if (code[i - 1] === ")") { + open++; + } + if (open === -1) { + break; + } + i--; + } + begin = i; + i = caret; + open = 0; + while (i < code.length) { + if (code[i] === "(") { + open--; + } else if (code[i] === ")") { + open++; + } + if (open === 1) { + break; + } + i++; + } + end = i; + return [begin, end].map((o) => offsetToPosition(o, code)); +} + +var CodeMirror6 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({ + __proto__: null, + setHighlights: setHighlights, + 'default': CodeMirror, + markParens: markParens, + offsetToPosition: offsetToPosition, + positionToOffset: positionToOffset, + getCurrentParenArea: getCurrentParenArea +}, Symbol.toStringTag, { value: 'Module' })); + +/* +useCycle.mjs - +Copyright (C) 2022 Strudel contributors - see +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . +*/ + +/* export declare interface UseCycleProps { + onEvent: ToneEventCallback; + onQuery?: (state: State) => Hap[]; + onSchedule?: (events: Hap[], cycle: number) => void; + onDraw?: ToneEventCallback; + ready?: boolean; // if false, query will not be called on change props +} */ + +// function useCycle(props: UseCycleProps) { +function useCycle(props) { + // onX must use useCallback! + const { onEvent, onQuery, onSchedule, ready = true, onDraw } = props; + const [started, setStarted] = useState(false); + const cycleDuration = 1; + const activeCycle = () => Math.floor(Tone.getTransport().seconds / cycleDuration); + + // pull events with onQuery + count up to next cycle + const query = (cycle = activeCycle()) => { + const timespan = new TimeSpan(cycle, cycle + 1); + const events = onQuery?.(new State(timespan)) || []; + onSchedule?.(events, cycle); + // cancel events after current query. makes sure no old events are player for rescheduled cycles + // console.log('schedule', cycle); + // query next cycle in the middle of the current + const cancelFrom = timespan.begin.valueOf(); + Tone.getTransport().cancel(cancelFrom); + // const queryNextTime = (cycle + 1) * cycleDuration - 0.1; + const queryNextTime = (cycle + 1) * cycleDuration - 0.5; + + // if queryNextTime would be before current time, execute directly (+0.1 for safety that it won't miss) + const t = Math.max(Tone.getTransport().seconds, queryNextTime) + 0.1; + Tone.getTransport().schedule(() => { + query(cycle + 1); + }, t); + + // schedule events for next cycle + events + ?.filter((event) => event.part.begin.equals(event.whole?.begin)) + .forEach((event) => { + Tone.getTransport().schedule((time) => { + onEvent(time, event, Tone.getContext().currentTime); + Tone.Draw.schedule(() => { + // do drawing or DOM manipulation here + onDraw?.(time, event); + }, time); + }, event.part.begin.valueOf()); + }); + }; + + useEffect(() => { + ready && query(); + }, [onEvent, onSchedule, onQuery, onDraw, ready]); + + const start = async () => { + setStarted(true); + await Tone.start(); + Tone.getTransport().start('+0.1'); + }; + const stop = () => { + Tone.getTransport().pause(); + setStarted(false); + }; + const toggle = () => (started ? stop() : start()); + return { + start, + stop, + onEvent, + started, + setStarted, + toggle, + query, + activeCycle, + }; +} + +/* +usePostMessage.mjs - +Copyright (C) 2022 Strudel contributors - see +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . +*/ + +function usePostMessage(listener) { + useEffect(() => { + window.addEventListener('message', listener); + return () => window.removeEventListener('message', listener); + }, [listener]); + return useCallback((data) => window.postMessage(data, '*'), []); +} + +/* +useRepl.mjs - +Copyright (C) 2022 Strudel contributors - see +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . +*/ + +let s4 = () => { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); +}; +const generateHash = (code) => encodeURIComponent(btoa(code)); + +function useRepl({ tune, defaultSynth, autolink = true, onEvent, onDraw: onDrawProp }) { + const id = useMemo(() => s4(), []); + const [code, setCode] = useState(tune); + const [activeCode, setActiveCode] = useState(); + const [log, setLog] = useState(''); + const [error, setError] = useState(); + const [pending, setPending] = useState(false); + const [hash, setHash] = useState(''); + const [pattern, setPattern] = useState(); + const dirty = useMemo(() => code !== activeCode || error, [code, activeCode, error]); + const pushLog = useCallback((message) => setLog((log) => log + `${log ? '\n\n' : ''}${message}`), []); + + // below block allows disabling the highlighting by including "strudel disable-highlighting" in the code (as comment) + const onDraw = useMemo(() => { + if (activeCode && !activeCode.includes('strudel disable-highlighting')) { + return (time, event) => onDrawProp?.(time, event, activeCode); + } + }, [activeCode, onDrawProp]); + + // cycle hook to control scheduling + const cycle = useCycle({ + onDraw, + onEvent: useCallback( + (time, event, currentTime) => { + try { + onEvent?.(event); + if (event.context.logs?.length) { + event.context.logs.forEach(pushLog); + } + const { onTrigger, velocity } = event.context; + if (!onTrigger) { + if (defaultSynth) { + const note = getPlayableNoteValue(event); + defaultSynth.triggerAttackRelease(note, event.duration.valueOf(), time, velocity); + } else { + throw new Error('no defaultSynth passed to useRepl.'); + } + /* console.warn('no instrument chosen', event); + throw new Error(`no instrument chosen for ${JSON.stringify(event)}`); */ + } else { + onTrigger( + time, + event, + currentTime, + 1 /* cps */, + event.wholeOrPart().begin.valueOf(), + event.duration.valueOf(), + ); + } + } catch (err) { + console.warn(err); + err.message = 'unplayable event: ' + err?.message; + pushLog(err.message); // not with setError, because then we would have to setError(undefined) on next playable event + } + }, + [onEvent, pushLog, defaultSynth], + ), + onQuery: useCallback( + (state) => { + try { + return pattern?.query(state) || []; + } catch (err) { + console.warn(err); + err.message = 'query error: ' + err.message; + setError(err); + return []; + } + }, + [pattern], + ), + onSchedule: useCallback((_events, cycle) => logCycle(_events, cycle), []), + ready: !!pattern && !!activeCode, + }); + + const broadcast = usePostMessage(({ data: { from, type } }) => { + if (type === 'start' && from !== id) { + // console.log('message', from, type); + cycle.setStarted(false); + setActiveCode(undefined); + } + }); + + const activateCode = useCallback( + async (_code = code) => { + if (activeCode && !dirty) { + setError(undefined); + cycle.start(); + return; + } + try { + setPending(true); + const parsed = await evaluate(_code); + cycle.start(); + broadcast({ type: 'start', from: id }); + setPattern(() => parsed.pattern); + if (autolink) { + window.location.hash = '#' + encodeURIComponent(btoa(code)); + } + setHash(generateHash(code)); + setError(undefined); + setActiveCode(_code); + setPending(false); + } catch (err) { + err.message = 'evaluation error: ' + err.message; + console.warn(err); + setError(err); + } + }, + [activeCode, dirty, code, cycle, autolink, id, broadcast], + ); + // logs events of cycle + const logCycle = (_events, cycle) => { + if (_events.length) ; + }; + + const togglePlay = () => { + if (!cycle.started) { + activateCode(); + } else { + cycle.stop(); + } + }; + + return { + pending, + code, + setCode, + pattern, + error, + cycle, + setPattern, + dirty, + log, + togglePlay, + setActiveCode, + activateCode, + activeCode, + pushLog, + hash, + }; +} + +/* +cx.js - +Copyright (C) 2022 Strudel contributors - see +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . +*/ + +function cx(...classes) { + // : Array + return classes.filter(Boolean).join(' '); +} + +let highlights = []; // actively highlighted events +let lastEnd; + +function useHighlighting({ view, pattern, active }) { + useEffect(() => { + if (view) { + if (pattern && active) { + let frame = requestAnimationFrame(updateHighlights); + + function updateHighlights() { + try { + const audioTime = Tone.getTransport().seconds; + const span = [lastEnd || audioTime, audioTime + 1 / 60]; + lastEnd = audioTime + 1 / 60; + highlights = highlights.filter((hap) => hap.whole.end > audioTime); // keep only highlights that are still active + const haps = pattern.queryArc(...span).filter((hap) => hap.hasOnset()); + highlights = highlights.concat(haps); // add potential new onsets + view.dispatch({ effects: setHighlights.of(highlights) }); // highlight all still active + new active haps + } catch (err) { + // console.log('error in updateHighlights', err); + view.dispatch({ effects: setHighlights.of([]) }); + } + frame = requestAnimationFrame(updateHighlights); + } + + return () => { + cancelAnimationFrame(frame); + }; + } else { + highlights = []; + view.dispatch({ effects: setHighlights.of([]) }); + } + } + }, [pattern, active, view]); +} + +var tailwind = ''; + +const container = "_container_10e1g_1"; +const header = "_header_10e1g_5"; +const buttons = "_buttons_10e1g_9"; +const button = "_button_10e1g_9"; +const buttonDisabled = "_buttonDisabled_10e1g_17"; +const error = "_error_10e1g_21"; +const body = "_body_10e1g_25"; +var styles = { + container: container, + header: header, + buttons: buttons, + button: button, + buttonDisabled: buttonDisabled, + error: error, + body: body +}; + +function Icon({ type }) { + return /* @__PURE__ */ React.createElement("svg", { + xmlns: "http://www.w3.org/2000/svg", + className: "sc-h-5 sc-w-5", + viewBox: "0 0 20 20", + fill: "currentColor" + }, { + refresh: /* @__PURE__ */ React.createElement("path", { + fillRule: "evenodd", + d: "M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z", + clipRule: "evenodd" + }), + play: /* @__PURE__ */ React.createElement("path", { + fillRule: "evenodd", + d: "M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z", + clipRule: "evenodd" + }), + pause: /* @__PURE__ */ React.createElement("path", { + fillRule: "evenodd", + d: "M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z", + clipRule: "evenodd" + }) + }[type]); +} + +evalScope(Tone, import('@strudel.cycles/core'), import('@strudel.cycles/tone'), import('@strudel.cycles/tonal'), import('@strudel.cycles/mini'), import('@strudel.cycles/midi'), import('@strudel.cycles/xen'), import('@strudel.cycles/webaudio')); +const defaultSynth = new Tone.PolySynth().chain(new Tone.Gain(0.5), Tone.Destination).set({ + oscillator: { type: "triangle" }, + envelope: { + release: 0.01 + } +}); +function MiniRepl({ tune }) { + const { code, setCode, pattern, activateCode, error, cycle, dirty, togglePlay } = useRepl({ + tune, + defaultSynth, + autolink: false + }); + const [view, setView] = useState(); + const [ref, isVisible] = useInView({ + threshold: 0.01 + }); + const wasVisible = useRef(); + const show = useMemo(() => { + if (isVisible) { + wasVisible.current = true; + } + return isVisible || wasVisible.current; + }, [isVisible]); + useHighlighting({ view, pattern, active: cycle.started }); + return /* @__PURE__ */ React$1.createElement("div", { + className: styles.container, + ref + }, /* @__PURE__ */ React$1.createElement("div", { + className: styles.header + }, /* @__PURE__ */ React$1.createElement("div", { + className: styles.buttons + }, /* @__PURE__ */ React$1.createElement("button", { + className: cx(styles.button, cycle.started ? "sc-animate-pulse" : ""), + onClick: () => togglePlay() + }, /* @__PURE__ */ React$1.createElement(Icon, { + type: cycle.started ? "pause" : "play" + })), /* @__PURE__ */ React$1.createElement("button", { + className: cx(dirty ? styles.button : styles.buttonDisabled), + onClick: () => activateCode() + }, /* @__PURE__ */ React$1.createElement(Icon, { + type: "refresh" + }))), error && /* @__PURE__ */ React$1.createElement("div", { + className: styles.error + }, error.message)), /* @__PURE__ */ React$1.createElement("div", { + className: styles.body + }, show && /* @__PURE__ */ React$1.createElement(CodeMirror, { + value: code, + onChange: setCode, + onViewChanged: setView + }))); +} + +export { CodeMirror6 as CodeMirror, MiniRepl }; diff --git a/packages/react/dist/style.css b/packages/react/dist/style.css new file mode 100644 index 00000000..55d715b1 --- /dev/null +++ b/packages/react/dist/style.css @@ -0,0 +1 @@ +*,:before,:after{--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sc-h-5{height:1.25rem}.sc-w-5{width:1.25rem}@keyframes sc-pulse{50%{opacity:.5}}.sc-animate-pulse{animation:sc-pulse 2s cubic-bezier(.4,0,.6,1) infinite}._container_10e1g_1{overflow:hidden;border-radius:.375rem;--tw-bg-opacity: 1;background-color:rgb(68 76 87 / var(--tw-bg-opacity))}._header_10e1g_5{display:flex;justify-content:space-between;border-top-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity))}._buttons_10e1g_9{display:flex}._button_10e1g_9{display:flex;width:4rem;cursor:pointer;align-items:center;justify-content:center;border-right-width:1px;--tw-border-opacity: 1;border-color:rgb(100 116 139 / var(--tw-border-opacity));--tw-bg-opacity: 1;background-color:rgb(51 65 85 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}._button_10e1g_9:hover{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity))}._buttonDisabled_10e1g_17{display:flex;width:4rem;cursor:pointer;cursor:not-allowed;align-items:center;justify-content:center;--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity));padding:.25rem;--tw-text-opacity: 1;color:rgb(148 163 184 / var(--tw-text-opacity))}._error_10e1g_21{padding:.25rem;text-align:right;font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity))}._body_10e1g_25{position:relative;overflow:auto} diff --git a/packages/react/package.json b/packages/react/package.json index bf3e2174..d92ef7fc 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -2,6 +2,18 @@ "name": "@strudel.cycles/react", "private": true, "version": "0.0.0", + "main": "dist/index.cjs.js", + "module": "dist/index.es.js", + "exports": { + ".": { + "require": "./dist/index.cjs.js", + "import": "./dist/index.es.js" + }, + "./dist/style.css": "./dist/style.css" + }, + "files": [ + "dist" + ], "scripts": { "dev": "vite", "build": "vite build", diff --git a/packages/react/src/App.jsx b/packages/react/src/App.jsx index 90a214b8..9bbf56d5 100644 --- a/packages/react/src/App.jsx +++ b/packages/react/src/App.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import MiniRepl from './components/MiniRepl'; +import { MiniRepl } from './components/MiniRepl'; import 'tailwindcss/tailwind.css'; function App() { diff --git a/packages/react/src/components/Icon.tsx b/packages/react/src/components/Icon.tsx new file mode 100644 index 00000000..85f08f6e --- /dev/null +++ b/packages/react/src/components/Icon.tsx @@ -0,0 +1,31 @@ +export function Icon({ type }) { + return ( + + { + { + refresh: ( + + ), + play: ( + + ), + pause: ( + + ), + }[type] + } + + ); +} diff --git a/packages/react/src/components/MiniRepl.jsx b/packages/react/src/components/MiniRepl.jsx index 4d404b3d..e29513fe 100644 --- a/packages/react/src/components/MiniRepl.jsx +++ b/packages/react/src/components/MiniRepl.jsx @@ -1,14 +1,16 @@ -import React, { useState } from 'react'; +import React, { useState, useMemo, useRef } from 'react'; import { useInView } from 'react-hook-inview'; -import { Tone } from '@strudel.cycles/tone'; -import { evalScope } from '@strudel.cycles/eval'; import useRepl from '../hooks/useRepl.mjs'; import cx from '../cx'; import useHighlighting from '../hooks/useHighlighting.mjs'; import CodeMirror6 from './CodeMirror6'; import 'tailwindcss/tailwind.css'; +import styles from './MiniRepl.module.css'; +import { Icon } from './Icon'; +import { Tone } from '@strudel.cycles/tone'; +import { evalScope } from '@strudel.cycles/eval'; evalScope( Tone, import('@strudel.cycles/core'), @@ -27,85 +29,40 @@ const defaultSynth = new Tone.PolySynth().chain(new Tone.Gain(0.5), Tone.Destina }, }); -// "balanced" | "interactive" | "playback"; -// Tone.setContext(new Tone.Context({ latencyHint: 'playback', lookAhead: 1 })); -function MiniRepl({ tune, maxHeight = 500 }) { +export function MiniRepl({ tune }) { const { code, setCode, pattern, activateCode, error, cycle, dirty, togglePlay } = useRepl({ tune, defaultSynth, autolink: false, }); - const lines = code.split('\n').length; const [view, setView] = useState(); const [ref, isVisible] = useInView({ threshold: 0.01, }); + const wasVisible = useRef(); + const show = useMemo(() => { + if (isVisible) { + wasVisible.current = true; + } + return isVisible || wasVisible.current; + }, [isVisible]); useHighlighting({ view, pattern, active: cycle.started }); return ( -
-
-
- -
-
- {error && {error.message}} -
{' '} + {error &&
{error.message}
}
-
- {isVisible && } +
+ {show && }
- {/* */}
); } - -export default MiniRepl; diff --git a/packages/react/src/components/MiniRepl.module.css b/packages/react/src/components/MiniRepl.module.css new file mode 100644 index 00000000..20c023df --- /dev/null +++ b/packages/react/src/components/MiniRepl.module.css @@ -0,0 +1,27 @@ +.container { + @apply sc-rounded-md sc-overflow-hidden sc-bg-[#444C57]; +} + +.header { + @apply sc-flex sc-justify-between sc-bg-slate-700 sc-border-t sc-border-slate-500; +} + +.buttons { + @apply sc-flex; +} + +.button { + @apply sc-cursor-pointer sc-w-16 sc-flex sc-items-center sc-justify-center sc-p-1 sc-bg-slate-700 sc-border-r sc-border-slate-500 sc-text-white hover:sc-bg-slate-600; +} + +.buttonDisabled { + @apply sc-cursor-pointer sc-w-16 sc-flex sc-items-center sc-justify-center sc-p-1 sc-bg-slate-600 sc-text-slate-400 sc-cursor-not-allowed; +} + +.error { + @apply sc-text-right sc-p-1 sc-text-sm sc-text-red-200; +} + +.body { + @apply sc-overflow-auto sc-relative; +} diff --git a/packages/react/src/index.js b/packages/react/src/index.js index b088d8d6..70030c2c 100644 --- a/packages/react/src/index.js +++ b/packages/react/src/index.js @@ -1,4 +1,4 @@ // import 'tailwindcss/tailwind.css'; export * as CodeMirror from './components/CodeMirror6'; -export * as MiniRepl from './components/MiniRepl'; +export * from './components/MiniRepl'; diff --git a/packages/react/vite.config.js b/packages/react/vite.config.js index fa85f162..c32ca153 100644 --- a/packages/react/vite.config.js +++ b/packages/react/vite.config.js @@ -30,6 +30,7 @@ export default defineConfig({ '@strudel.cycles/midi', '@strudel.cycles/xen', '@strudel.cycles/serial', + '@strudel.cycles/webaudio', '@codemirror/view', '@codemirror/highlight', '@codemirror/state' diff --git a/tutorial/Tutorial.jsx b/tutorial/Tutorial.jsx index e123d5b9..738f7808 100644 --- a/tutorial/Tutorial.jsx +++ b/tutorial/Tutorial.jsx @@ -8,6 +8,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import Tutorial from './tutorial.mdx'; import './style.css'; +import '@strudel.cycles/react/dist/style.css'; ReactDOM.render( @@ -33,21 +34,3 @@ ReactDOM.render( , document.getElementById('root') ); - - -/* -// for pragmatic reasons, I just added the tailwind classes from MiniRepl here to make them work -// TODO: find a way to "export" tailwind classes from package -rounded-md overflow-hidden bg-[#444C57] -flex justify-between bg-slate-700 border-t border-slate-500 -flex -w-16 flex items-center justify-center p-1 bg-slate-700 border-r border-slate-500 text-white hover:bg-slate-600 -animate-pulse -h-5 w-5 -w-16 flex items-center justify-center p-1 border-slate-500 hover:bg-slate-600 -bg-slate-700 border-r border-slate-500 text-white -bg-slate-600 text-slate-400 cursor-not-allowed -text-right p-1 text-sm -text-red-200 -flex space-y-0 overflow-auto relative -*/ \ No newline at end of file diff --git a/tutorial/tutorial.mdx b/tutorial/tutorial.mdx index 3da582eb..4a5c08c9 100644 --- a/tutorial/tutorial.mdx +++ b/tutorial/tutorial.mdx @@ -1,4 +1,4 @@ -import MiniRepl from '@strudel.cycles/react/src/components/MiniRepl'; +import { MiniRepl } from '@strudel.cycles/react'; # What is Strudel?