add vim toggle to settings

+ added persistent global state store
+ refactored themes to use the new store
This commit is contained in:
Felix Roos 2023-02-19 01:51:31 +01:00
parent 4a3540cf2b
commit 014555fe5d
11 changed files with 120 additions and 120 deletions

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { useMemo } from 'react';
import _CodeMirror from '@uiw/react-codemirror'; import _CodeMirror from '@uiw/react-codemirror';
import { EditorView, Decoration } from '@codemirror/view'; import { EditorView, Decoration } from '@codemirror/view';
import { StateField, StateEffect } from '@codemirror/state'; import { StateField, StateEffect } from '@codemirror/state';
@ -83,9 +83,8 @@ const highlightField = StateField.define({
provide: (f) => EditorView.decorations.from(f), provide: (f) => EditorView.decorations.from(f),
}); });
const extensions = [ const staticExtensions = [
javascript(), javascript(),
vim(),
highlightField, highlightField,
flashField, flashField,
// javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }), // javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }),
@ -99,6 +98,7 @@ export default function CodeMirror({
onViewChanged, onViewChanged,
onSelectionChange, onSelectionChange,
theme, theme,
vimMode,
options, options,
editorDidMount, editorDidMount,
}) { }) {
@ -122,6 +122,7 @@ export default function CodeMirror({
}, },
[onSelectionChange], [onSelectionChange],
); );
const extensions = useMemo(() => [...staticExtensions, ...(vimMode ? [vim()] : [])], [vimMode]);
return ( return (
<> <>
<_CodeMirror <_CodeMirror

74
pnpm-lock.yaml generated
View File

@ -373,6 +373,7 @@ importers:
'@strudel.cycles/webaudio': workspace:* '@strudel.cycles/webaudio': workspace:*
'@strudel.cycles/xen': workspace:* '@strudel.cycles/xen': workspace:*
'@supabase/supabase-js': ^1.35.3 '@supabase/supabase-js': ^1.35.3
'@tailwindcss/forms': ^0.5.3
'@tailwindcss/typography': ^0.5.8 '@tailwindcss/typography': ^0.5.8
'@types/node': ^18.0.0 '@types/node': ^18.0.0
'@types/react': ^18.0.26 '@types/react': ^18.0.26
@ -417,6 +418,7 @@ importers:
'@strudel.cycles/webaudio': link:../packages/webaudio '@strudel.cycles/webaudio': link:../packages/webaudio
'@strudel.cycles/xen': link:../packages/xen '@strudel.cycles/xen': link:../packages/xen
'@supabase/supabase-js': 1.35.7 '@supabase/supabase-js': 1.35.7
'@tailwindcss/forms': 0.5.3_tailwindcss@3.2.4
'@tailwindcss/typography': 0.5.9_tailwindcss@3.2.4 '@tailwindcss/typography': 0.5.9_tailwindcss@3.2.4
'@types/node': 18.11.18 '@types/node': 18.11.18
'@types/react': 18.0.27 '@types/react': 18.0.27
@ -3575,6 +3577,15 @@ packages:
string.prototype.matchall: 4.0.8 string.prototype.matchall: 4.0.8
dev: true dev: true
/@tailwindcss/forms/0.5.3_tailwindcss@3.2.4:
resolution: {integrity: sha512-y5mb86JUoiUgBjY/o6FJSFZSEttfb3Q5gllE4xoKjAAD+vBrnIhE4dViwUuow3va8mpH4s9jyUbUbrRGoRdc2Q==}
peerDependencies:
tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1'
dependencies:
mini-svg-data-uri: 1.4.4
tailwindcss: 3.2.4
dev: false
/@tailwindcss/typography/0.5.9_tailwindcss@3.2.4: /@tailwindcss/typography/0.5.9_tailwindcss@3.2.4:
resolution: {integrity: sha512-t8Sg3DyynFysV9f4JDOVISGsjazNb48AeIYQwcL+Bsq5uf4RYL75C1giZ43KISjeDGBaTN3Kxh7Xj/vRSMJUUg==} resolution: {integrity: sha512-t8Sg3DyynFysV9f4JDOVISGsjazNb48AeIYQwcL+Bsq5uf4RYL75C1giZ43KISjeDGBaTN3Kxh7Xj/vRSMJUUg==}
peerDependencies: peerDependencies:
@ -9386,6 +9397,11 @@ packages:
engines: {node: '>=4'} engines: {node: '>=4'}
dev: true dev: true
/mini-svg-data-uri/1.4.4:
resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==}
hasBin: true
dev: false
/minimatch/3.1.2: /minimatch/3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
dependencies: dependencies:
@ -10428,17 +10444,6 @@ packages:
- supports-color - supports-color
dev: true dev: true
/postcss-import/14.1.0:
resolution: {integrity: sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==}
engines: {node: '>=10.0.0'}
peerDependencies:
postcss: ^8.0.0
dependencies:
postcss-value-parser: 4.2.0
read-cache: 1.0.0
resolve: 1.22.1
dev: false
/postcss-import/14.1.0_postcss@8.4.21: /postcss-import/14.1.0_postcss@8.4.21:
resolution: {integrity: sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==} resolution: {integrity: sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==}
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}
@ -10449,16 +10454,6 @@ packages:
postcss-value-parser: 4.2.0 postcss-value-parser: 4.2.0
read-cache: 1.0.0 read-cache: 1.0.0
resolve: 1.22.1 resolve: 1.22.1
dev: true
/postcss-js/4.0.0:
resolution: {integrity: sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==}
engines: {node: ^12 || ^14 || >= 16}
peerDependencies:
postcss: ^8.3.3
dependencies:
camelcase-css: 2.0.1
dev: false
/postcss-js/4.0.0_postcss@8.4.21: /postcss-js/4.0.0_postcss@8.4.21:
resolution: {integrity: sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==} resolution: {integrity: sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==}
@ -10468,23 +10463,6 @@ packages:
dependencies: dependencies:
camelcase-css: 2.0.1 camelcase-css: 2.0.1
postcss: 8.4.21 postcss: 8.4.21
dev: true
/postcss-load-config/3.1.4:
resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==}
engines: {node: '>= 10'}
peerDependencies:
postcss: '>=8.0.9'
ts-node: '>=9.0.0'
peerDependenciesMeta:
postcss:
optional: true
ts-node:
optional: true
dependencies:
lilconfig: 2.0.6
yaml: 1.10.2
dev: false
/postcss-load-config/3.1.4_postcss@8.4.21: /postcss-load-config/3.1.4_postcss@8.4.21:
resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==}
@ -10502,15 +10480,6 @@ packages:
postcss: 8.4.21 postcss: 8.4.21
yaml: 1.10.2 yaml: 1.10.2
/postcss-nested/6.0.0:
resolution: {integrity: sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==}
engines: {node: '>=12.0'}
peerDependencies:
postcss: ^8.2.14
dependencies:
postcss-selector-parser: 6.0.11
dev: false
/postcss-nested/6.0.0_postcss@8.4.21: /postcss-nested/6.0.0_postcss@8.4.21:
resolution: {integrity: sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==} resolution: {integrity: sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==}
engines: {node: '>=12.0'} engines: {node: '>=12.0'}
@ -10519,7 +10488,6 @@ packages:
dependencies: dependencies:
postcss: 8.4.21 postcss: 8.4.21
postcss-selector-parser: 6.0.11 postcss-selector-parser: 6.0.11
dev: true
/postcss-selector-parser/6.0.10: /postcss-selector-parser/6.0.10:
resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==}
@ -12142,8 +12110,6 @@ packages:
resolution: {integrity: sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==} resolution: {integrity: sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==}
engines: {node: '>=12.13.0'} engines: {node: '>=12.13.0'}
hasBin: true hasBin: true
peerDependencies:
postcss: ^8.0.9
dependencies: dependencies:
arg: 5.0.2 arg: 5.0.2
chokidar: 3.5.3 chokidar: 3.5.3
@ -12160,10 +12126,10 @@ packages:
object-hash: 3.0.0 object-hash: 3.0.0
picocolors: 1.0.0 picocolors: 1.0.0
postcss: 8.4.21 postcss: 8.4.21
postcss-import: 14.1.0 postcss-import: 14.1.0_postcss@8.4.21
postcss-js: 4.0.0 postcss-js: 4.0.0_postcss@8.4.21
postcss-load-config: 3.1.4 postcss-load-config: 3.1.4_postcss@8.4.21
postcss-nested: 6.0.0 postcss-nested: 6.0.0_postcss@8.4.21
postcss-selector-parser: 6.0.11 postcss-selector-parser: 6.0.11
postcss-value-parser: 4.2.0 postcss-value-parser: 4.2.0
quick-lru: 5.1.1 quick-lru: 5.1.1

View File

@ -34,6 +34,7 @@
"@strudel.cycles/webaudio": "workspace:*", "@strudel.cycles/webaudio": "workspace:*",
"@strudel.cycles/xen": "workspace:*", "@strudel.cycles/xen": "workspace:*",
"@supabase/supabase-js": "^1.35.3", "@supabase/supabase-js": "^1.35.3",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.8", "@tailwindcss/typography": "^0.5.8",
"@types/node": "^18.0.0", "@types/node": "^18.0.0",
"@types/react": "^18.0.26", "@types/react": "^18.0.26",

36
website/public/store.mjs Normal file
View File

@ -0,0 +1,36 @@
export const storeKey = 'strudel-settings';
export function get(prop) {
const state = JSON.parse(localStorage.getItem(storeKey));
if (!prop) {
return state;
}
return state[prop];
}
export function set(next) {
localStorage.setItem(storeKey, JSON.stringify(next));
}
export function updateState(func) {
const prev = get();
const next = func(prev);
set(next);
document.dispatchEvent(
new CustomEvent(storeKey, {
detail: { next, prev },
}),
);
}
export function watch(func, prop) {
document.addEventListener(storeKey, (e) => {
const { prev, next } = e.detail;
const hasPropChanged = (p) => next[p] !== prev[p];
if (!prop) {
func(next);
} else if (hasPropChanged(prop)) {
func(next[prop]);
}
});
}

View File

@ -48,7 +48,8 @@ const { strudelTheme } = settings;
</style> </style>
{pwaInfo && <Fragment set:html={pwaInfo.webManifest.linkTag} />} {pwaInfo && <Fragment set:html={pwaInfo.webManifest.linkTag} />}
<script define:vars={{ settings, strudelTheme }} is:inline> <script define:vars={{ settings, strudelTheme }} is:inline type="module">
import { watch, get } from './store.mjs';
const themeStyle = document.createElement('style'); const themeStyle = document.createElement('style');
themeStyle.id = 'strudel-theme'; themeStyle.id = 'strudel-theme';
document.head.append(themeStyle); document.head.append(themeStyle);
@ -76,9 +77,7 @@ const { strudelTheme } = settings;
} else { } else {
document.documentElement.classList.add('dark'); document.documentElement.classList.add('dark');
} }
// persist theme name
localStorage.setItem('strudel-theme', name || 'strudelTheme');
} }
setTheme(localStorage.getItem('strudel-theme')); setTheme(get().theme);
document.addEventListener('strudel-theme', (e) => setTheme(e.detail)); watch(setTheme, 'theme');
</script> </script>

View File

@ -36,7 +36,6 @@ export function MiniRepl({ tune, drawTime, punchcard, canvasHeight = 100 }) {
.then(([res]) => setRepl(() => res.MiniRepl)) .then(([res]) => setRepl(() => res.MiniRepl))
.catch((err) => console.error(err)); .catch((err) => console.error(err));
}, []); }, []);
// const { settings } = useTheme();
return Repl ? ( return Repl ? (
<div className="mb-4"> <div className="mb-4">
<Repl <Repl

View File

@ -7,11 +7,12 @@ import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
import { loadedSamples } from './Repl'; import { loadedSamples } from './Repl';
import { Reference } from './Reference'; import { Reference } from './Reference';
import { themes, themeColors } from './themes.mjs'; import { themes, themeColors } from './themes.mjs';
import useStore from '../useStore.mjs';
export function Footer({ context }) { export function Footer({ context }) {
// const [activeFooter, setActiveFooter] = useState('console'); // const [activeFooter, setActiveFooter] = useState('console');
// const { activeFooter, setActiveFooter, isZen } = useContext?.(ReplContext); // const { activeFooter, setActiveFooter, isZen } = useContext?.(ReplContext);
const { activeFooter, setActiveFooter, isZen, theme, setTheme } = context; const { activeFooter, setActiveFooter, isZen } = context;
const footerContent = useRef(); const footerContent = useRef();
const [log, setLog] = useState([]); const [log, setLog] = useState([]);
@ -93,7 +94,7 @@ export function Footer({ context }) {
{activeFooter === 'console' && <ConsoleTab log={log} />} {activeFooter === 'console' && <ConsoleTab log={log} />}
{activeFooter === 'samples' && <SamplesTab />} {activeFooter === 'samples' && <SamplesTab />}
{activeFooter === 'reference' && <Reference />} {activeFooter === 'reference' && <Reference />}
{activeFooter === 'settings' && <SettingsTab theme={theme} setTheme={setTheme} />} {activeFooter === 'settings' && <SettingsTab />}
</div> </div>
)} )}
</footer> </footer>
@ -205,37 +206,34 @@ function SamplesTab() {
</div> </div>
); );
} }
function SettingsTab({ theme, setTheme }) { function SettingsTab() {
/*<input type="checkbox" value={vimMode} onChange={(checked)=>{ const { state, update } = useStore();
console.log('vim mode toggle', checked) const { theme, vim } = state;
}}/>*/
return ( return (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-2 p-2"> <div className="text-foreground grid space-y-4 p-2">
{Object.entries(themes).map(([k, t]) => ( <label className="space-x-2">
<div <span>Theme</span>
key={k} <select
className={cx( className="p-2 bg-background rounded-md text-foreground"
'border-2 border-transparent cursor-pointer p-4 bg-background bg-opacity-25 rounded-md', value={theme}
theme === k ? '!border-foreground' : '', onChange={(e) => update((current) => ({ ...current, theme: e.target.value }))}
)}
onClick={() => {
setTheme(k);
document.dispatchEvent(
new CustomEvent('strudel-theme', {
detail: k,
}),
);
}}
> >
<div className="mb-2 w-full text-center text-foreground">{k}</div> {Object.entries(themes).map(([k, t]) => (
<div className="flex justify-stretch overflow-hidden rounded-md"> <option key={k} className="bg-background">
{themeColors(t).map((c, i) => ( {k}
<div key={i} className="grow h-6" style={{ background: c }} /> </option>
))} ))}
</div> </select>
</div> </label>
))} <label className="space-x-2">
<input
className="bg-background w-5 h-5 rounded-md"
type="checkbox"
checked={vim}
onChange={(e) => update((current) => ({ ...current, vim: e.target.checked }))}
/>
<span>Vim Mode</span>
</label>
</div> </div>
); );
} }

View File

@ -24,6 +24,7 @@ import * as tunes from './tunes.mjs';
import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon'; import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon';
import { themes } from './themes.mjs'; import { themes } from './themes.mjs';
import useTheme from '../useTheme'; import useTheme from '../useTheme';
import useStore from '../useStore.mjs';
const initialTheme = localStorage.getItem('strudel-theme') || 'strudelTheme'; const initialTheme = localStorage.getItem('strudel-theme') || 'strudelTheme';
@ -113,12 +114,16 @@ export const ReplContext = createContext(null);
export function Repl({ embedded = false }) { export function Repl({ embedded = false }) {
const isEmbedded = embedded || window.location !== window.parent.location; const isEmbedded = embedded || window.location !== window.parent.location;
const [view, setView] = useState(); // codemirror view const [view, setView] = useState(); // codemirror view
const [theme, setTheme] = useState(initialTheme);
const [lastShared, setLastShared] = useState(); const [lastShared, setLastShared] = useState();
const [activeFooter, setActiveFooter] = useState(''); const [activeFooter, setActiveFooter] = useState('');
const [isZen, setIsZen] = useState(false); const [isZen, setIsZen] = useState(false);
const [pending, setPending] = useState(false); const [pending, setPending] = useState(false);
const { theme, themeSettings } = useTheme();
const {
state: { vim },
} = useStore();
const { code, setCode, scheduler, evaluate, activateCode, isDirty, activeCode, pattern, started, stop, error } = const { code, setCode, scheduler, evaluate, activateCode, isDirty, activeCode, pattern, started, stop, error } =
useStrudel({ useStrudel({
initialCode: '// LOADING', initialCode: '// LOADING',
@ -172,15 +177,13 @@ export function Repl({ embedded = false }) {
), ),
); );
const { settings } = useTheme();
// highlighting // highlighting
useHighlighting({ useHighlighting({
view, view,
pattern, pattern,
active: started && !activeCode?.includes('strudel disable-highlighting'), active: started && !activeCode?.includes('strudel disable-highlighting'),
getTime: () => scheduler.now(), getTime: () => scheduler.now(),
color: settings?.foreground, color: themeSettings?.foreground,
}); });
// //
@ -263,8 +266,6 @@ export function Repl({ embedded = false }) {
handleShare, handleShare,
isZen, isZen,
setIsZen, setIsZen,
theme,
setTheme,
}; };
return ( return (
// bg-gradient-to-t from-blue-900 to-slate-900 // bg-gradient-to-t from-blue-900 to-slate-900
@ -281,6 +282,7 @@ export function Repl({ embedded = false }) {
<CodeMirror <CodeMirror
theme={themes[theme] || themes.strudelTheme} theme={themes[theme] || themes.strudelTheme}
value={code} value={code}
vimMode={vim}
onChange={handleChangeCode} onChange={handleChangeCode}
onViewChanged={setView} onViewChanged={setView}
onSelectionChange={handleSelectionChange} onSelectionChange={handleSelectionChange}

11
website/src/useStore.mjs Normal file
View File

@ -0,0 +1,11 @@
import { useState } from 'react';
import { useEvent } from '@strudel.cycles/react';
import * as Store from '../public/store.mjs';
function useStore() {
const [state, setState] = useState(Store.get());
useEvent(Store.storeKey, (e) => setState(e.detail.next));
return { state, update: Store.updateState };
}
export default useStore;

View File

@ -1,27 +1,14 @@
import { useState } from 'react';
import { settings } from './repl/themes.mjs'; import { settings } from './repl/themes.mjs';
import { useEffect } from 'react'; import useStore from './useStore.mjs';
function useTheme() { function useTheme() {
const [theme, setTheme] = useState(localStorage.getItem('strudel-theme')); const { state } = useStore();
useEvent('strudel-theme', (e) => setTheme(e.detail)); const theme = state.theme || 'strudelTheme';
const themeSettings = settings[theme || 'strudelTheme']; const themeSettings = settings[theme];
return { return {
theme, theme: state.theme,
setTheme, themeSettings,
settings: themeSettings,
isDark: !themeSettings.light,
isLight: !!themeSettings.light,
}; };
} }
// TODO: dedupe
function useEvent(name, onTrigger, useCapture = false) {
useEffect(() => {
document.addEventListener(name, onTrigger, useCapture);
return () => {
document.removeEventListener(name, onTrigger, useCapture);
};
}, [onTrigger]);
}
export default useTheme; export default useTheme;

View File

@ -41,5 +41,5 @@ module.exports = {
}, },
}, },
}, },
plugins: [require('@tailwindcss/typography')], plugins: [require('@tailwindcss/typography'), require('@tailwindcss/forms')],
}; };