mirror of
https://github.com/eliasstepanik/strudel.git
synced 2026-01-11 05:38:35 +00:00
refactor settings to nanostores
This commit is contained in:
parent
ff99dbcd22
commit
b67b049802
@ -136,7 +136,7 @@ export default function CodeMirror({
|
||||
return staticExtensions;
|
||||
}, [keybindings]);
|
||||
return (
|
||||
<div style={{ fontSize, fontFamily }} className="w-full">
|
||||
<div style={{ fontSize: parseInt(fontSize), fontFamily }} className="w-full">
|
||||
<_CodeMirror
|
||||
value={value}
|
||||
theme={theme || strudelTheme}
|
||||
|
||||
40
pnpm-lock.yaml
generated
40
pnpm-lock.yaml
generated
@ -362,6 +362,8 @@ importers:
|
||||
'@docsearch/react': ^3.1.0
|
||||
'@headlessui/react': ^1.7.7
|
||||
'@heroicons/react': ^2.0.13
|
||||
'@nanostores/persistent': ^0.7.0
|
||||
'@nanostores/react': ^0.4.1
|
||||
'@strudel.cycles/core': workspace:*
|
||||
'@strudel.cycles/csound': workspace:*
|
||||
'@strudel.cycles/midi': workspace:*
|
||||
@ -387,6 +389,7 @@ importers:
|
||||
fraction.js: ^4.2.0
|
||||
html-escaper: ^3.0.3
|
||||
nanoid: ^4.0.0
|
||||
nanostores: ^0.7.4
|
||||
preact: ^10.7.3
|
||||
react: ^18.2.0
|
||||
react-dom: ^18.2.0
|
||||
@ -407,6 +410,8 @@ importers:
|
||||
'@docsearch/react': 3.3.2_y6lbs4o5th67cuzjdmtw5eqh7a
|
||||
'@headlessui/react': 1.7.8_biqbaboplfbrettd7655fr4n2y
|
||||
'@heroicons/react': 2.0.14_react@18.2.0
|
||||
'@nanostores/persistent': 0.7.0_nanostores@0.7.4
|
||||
'@nanostores/react': 0.4.1_nkfnbc2tpc77iht7asm3uqwau4
|
||||
'@strudel.cycles/core': link:../packages/core
|
||||
'@strudel.cycles/csound': link:../packages/csound
|
||||
'@strudel.cycles/midi': link:../packages/midi
|
||||
@ -430,6 +435,7 @@ importers:
|
||||
canvas: 2.11.0
|
||||
fraction.js: 4.2.0
|
||||
nanoid: 4.0.0
|
||||
nanostores: 0.7.4
|
||||
preact: 10.11.3
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0_react@18.2.0
|
||||
@ -3170,6 +3176,27 @@ packages:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/@nanostores/persistent/0.7.0_nanostores@0.7.4:
|
||||
resolution: {integrity: sha512-4PAInL/T1hbftZUJ0cmgdFHBMalUoq7BUXFBy7QfyMv/8X3LPTYNh/yxspL7+J+XM3UNvVI7IFRMMs6FBasjhQ==}
|
||||
engines: {node: ^14.0.0 || ^16.0.0 || >=18.0.0}
|
||||
peerDependencies:
|
||||
nanostores: ^0.7.0
|
||||
dependencies:
|
||||
nanostores: 0.7.4
|
||||
dev: false
|
||||
|
||||
/@nanostores/react/0.4.1_nkfnbc2tpc77iht7asm3uqwau4:
|
||||
resolution: {integrity: sha512-lsv0CYrMxczbXtoV/mxFVEoL/uVjEjseoP89srO/5yNAOkJka+dSFS7LYyWEbuvCPO7EgbtkvRpO5V+OztKQOw==}
|
||||
engines: {node: ^14.0.0 || ^16.0.0 || >=18.0.0}
|
||||
peerDependencies:
|
||||
nanostores: ^0.7.0
|
||||
react: '>=18.0.0'
|
||||
dependencies:
|
||||
nanostores: 0.7.4
|
||||
react: 18.2.0
|
||||
use-sync-external-store: 1.2.0_react@18.2.0
|
||||
dev: false
|
||||
|
||||
/@nodelib/fs.scandir/2.1.5:
|
||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||
engines: {node: '>= 8'}
|
||||
@ -9646,6 +9673,11 @@ packages:
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/nanostores/0.7.4:
|
||||
resolution: {integrity: sha512-MBeUVt7NBcXqh7AGT+KSr3O0X/995CZsvcP2QEMP+PXFwb07qv3Vjyq+EX0yS8f12Vv3Tn2g/BvK/OZoMhJlOQ==}
|
||||
engines: {node: ^14.0.0 || ^16.0.0 || >=18.0.0}
|
||||
dev: false
|
||||
|
||||
/napi-build-utils/1.0.2:
|
||||
resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==}
|
||||
dev: true
|
||||
@ -12851,6 +12883,14 @@ packages:
|
||||
punycode: 2.3.0
|
||||
dev: true
|
||||
|
||||
/use-sync-external-store/1.2.0_react@18.2.0:
|
||||
resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/utf-8-validate/5.0.10:
|
||||
resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==}
|
||||
engines: {node: '>=6.14.2'}
|
||||
|
||||
@ -21,6 +21,8 @@
|
||||
"@docsearch/react": "^3.1.0",
|
||||
"@headlessui/react": "^1.7.7",
|
||||
"@heroicons/react": "^2.0.13",
|
||||
"@nanostores/persistent": "^0.7.0",
|
||||
"@nanostores/react": "^0.4.1",
|
||||
"@strudel.cycles/core": "workspace:*",
|
||||
"@strudel.cycles/csound": "workspace:*",
|
||||
"@strudel.cycles/midi": "workspace:*",
|
||||
@ -44,6 +46,7 @@
|
||||
"canvas": "^2.11.0",
|
||||
"fraction.js": "^4.2.0",
|
||||
"nanoid": "^4.0.0",
|
||||
"nanostores": "^0.7.4",
|
||||
"preact": "^10.7.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
||||
@ -47,37 +47,34 @@ const base = BASE_URL;
|
||||
|
||||
<script>
|
||||
import { settings } from '../repl/themes.mjs';
|
||||
import { watch, get } from '../store.mjs';
|
||||
import { settingsMap } from '../settings.mjs';
|
||||
import { listenKeys } from 'nanostores';
|
||||
const themeStyle = document.createElement('style');
|
||||
themeStyle.id = 'strudel-theme';
|
||||
document.head.append(themeStyle);
|
||||
function getTheme(name) {
|
||||
function activateTheme(name) {
|
||||
if (!settings[name]) {
|
||||
console.warn('theme', name, 'has no settings');
|
||||
console.warn('theme', name, 'has no settings.. defaulting to strudelTheme settings');
|
||||
}
|
||||
return {
|
||||
name,
|
||||
settings: settings[name] || settings.strudelTheme,
|
||||
};
|
||||
}
|
||||
function setTheme(name) {
|
||||
const { settings } = getTheme(name);
|
||||
const themeSettings = settings[name] || settings.strudelTheme;
|
||||
// set css variables
|
||||
themeStyle.innerHTML = `:root {
|
||||
${Object.entries(settings)
|
||||
${Object.entries(themeSettings)
|
||||
// important to override fallback
|
||||
.map(([key, value]) => `--${key}: ${value} !important;`)
|
||||
.join('\n')}
|
||||
}`;
|
||||
// tailwind dark mode
|
||||
if (settings.light) {
|
||||
if (themeSettings.light) {
|
||||
document.documentElement.classList.remove('dark');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
}
|
||||
setTheme(get().theme);
|
||||
watch(setTheme, 'theme');
|
||||
|
||||
activateTheme(settingsMap.get().theme);
|
||||
listenKeys(settingsMap, ['theme'], ({ theme }) => activateTheme(theme));
|
||||
|
||||
// https://medium.com/quick-code/100vh-problem-with-ios-safari-92ab23c852a8
|
||||
const appHeight = () => {
|
||||
const doc = document.documentElement;
|
||||
|
||||
@ -4,7 +4,7 @@ import { useEffect, useState } from 'react';
|
||||
import { prebake } from '../repl/prebake';
|
||||
import { themes, settings } from '../repl/themes.mjs';
|
||||
import './MiniRepl.css';
|
||||
import useStore from '../useStore.mjs';
|
||||
import { useSettings } from '../settings.mjs';
|
||||
|
||||
let modules;
|
||||
if (typeof window !== 'undefined') {
|
||||
@ -28,9 +28,7 @@ if (typeof window !== 'undefined') {
|
||||
|
||||
export function MiniRepl({ tune, drawTime, punchcard, canvasHeight = 100 }) {
|
||||
const [Repl, setRepl] = useState();
|
||||
const {
|
||||
state: { theme },
|
||||
} = useStore();
|
||||
const { theme } = useSettings();
|
||||
useEffect(() => {
|
||||
// we have to load this package on the client
|
||||
// because codemirror throws an error on the server
|
||||
|
||||
@ -7,7 +7,7 @@ import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { loadedSamples } from './Repl';
|
||||
import { Reference } from './Reference';
|
||||
import { themes } from './themes.mjs';
|
||||
import useStore from '../useStore.mjs';
|
||||
import { useSettings, settingsMap } from '../settings.mjs';
|
||||
|
||||
export function Footer({ context }) {
|
||||
// const [activeFooter, setActiveFooter] = useState('console');
|
||||
@ -287,29 +287,24 @@ const fontFamilyOptions = {
|
||||
};
|
||||
|
||||
function SettingsTab() {
|
||||
const { state, update, reset } = useStore();
|
||||
const { theme, keybindings, fontSize, fontFamily } = state;
|
||||
const { theme, keybindings, fontSize, fontFamily } = useSettings();
|
||||
return (
|
||||
<div className="text-foreground p-4 space-y-4">
|
||||
<FormItem label="Theme">
|
||||
<SelectInput
|
||||
options={themeOptions}
|
||||
value={theme}
|
||||
onChange={(theme) => update((current) => ({ ...current, theme }))}
|
||||
/>
|
||||
<SelectInput options={themeOptions} value={theme} onChange={(theme) => settingsMap.setKey('theme', theme)} />
|
||||
</FormItem>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormItem label="Font Family">
|
||||
<SelectInput
|
||||
options={fontFamilyOptions}
|
||||
value={fontFamily}
|
||||
onChange={(fontFamily) => update((current) => ({ ...current, fontFamily }))}
|
||||
onChange={(fontFamily) => settingsMap.setKey('fontFamily', fontFamily)}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="Font Size">
|
||||
<NumberSlider
|
||||
value={fontSize}
|
||||
onChange={(fontSize) => update((current) => ({ ...current, fontSize }))}
|
||||
onChange={(fontSize) => settingsMap.setKey('fontSize', fontSize)}
|
||||
min={10}
|
||||
max={40}
|
||||
step={2}
|
||||
@ -319,7 +314,7 @@ function SettingsTab() {
|
||||
<FormItem label="Keybindings">
|
||||
<ButtonGroup
|
||||
value={keybindings}
|
||||
onChange={(keybindings) => update((current) => ({ ...current, keybindings }))}
|
||||
onChange={(keybindings) => settingsMap.setKey('keybindings', keybindings)}
|
||||
items={{ codemirror: 'Codemirror', vim: 'Vim', emacs: 'Emacs' }}
|
||||
></ButtonGroup>
|
||||
</FormItem>
|
||||
@ -328,7 +323,7 @@ function SettingsTab() {
|
||||
className="bg-background p-2 max-w-[300px] rounded-md hover:opacity-50"
|
||||
onClick={() => {
|
||||
if (confirm('Sure?')) {
|
||||
reset();
|
||||
settingsMap.setKey(defaultSettings);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@ -23,8 +23,7 @@ import { prebake } from './prebake.mjs';
|
||||
import * as tunes from './tunes.mjs';
|
||||
import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon';
|
||||
import { themes } from './themes.mjs';
|
||||
import useTheme from '../useTheme';
|
||||
import useStore from '../useStore.mjs';
|
||||
import { useSettings } from '../settings.mjs';
|
||||
|
||||
const initialTheme = localStorage.getItem('strudel-theme') || 'strudelTheme';
|
||||
|
||||
@ -119,10 +118,7 @@ export function Repl({ embedded = false }) {
|
||||
const [isZen, setIsZen] = useState(false);
|
||||
const [pending, setPending] = useState(false);
|
||||
|
||||
const { theme, themeSettings } = useTheme();
|
||||
const {
|
||||
state: { keybindings, fontSize, fontFamily },
|
||||
} = useStore();
|
||||
const { theme, keybindings, fontSize, fontFamily } = useSettings();
|
||||
|
||||
const { code, setCode, scheduler, evaluate, activateCode, isDirty, activeCode, pattern, started, stop, error } =
|
||||
useStrudel({
|
||||
|
||||
15
website/src/settings.mjs
Normal file
15
website/src/settings.mjs
Normal file
@ -0,0 +1,15 @@
|
||||
import { persistentMap } from '@nanostores/persistent';
|
||||
import { useStore } from '@nanostores/react';
|
||||
|
||||
export const defaultSettings = {
|
||||
keybindings: 'codemirror',
|
||||
theme: 'strudelTheme',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 18,
|
||||
};
|
||||
|
||||
export const settingsMap = persistentMap('strudel-settings', defaultSettings);
|
||||
|
||||
export function useSettings() {
|
||||
return useStore(settingsMap);
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
export const storeKey = 'strudel-settings';
|
||||
const defaults = {
|
||||
keybindings: 'codemirror',
|
||||
theme: 'strudelTheme',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 18,
|
||||
};
|
||||
|
||||
export function get(prop) {
|
||||
let state = {
|
||||
...defaults,
|
||||
...JSON.parse(localStorage.getItem(storeKey) || '{}'),
|
||||
};
|
||||
if (!prop) {
|
||||
return state;
|
||||
}
|
||||
return state[prop];
|
||||
}
|
||||
|
||||
export function set(next) {
|
||||
localStorage.setItem(storeKey, JSON.stringify(next));
|
||||
}
|
||||
|
||||
export function update(func) {
|
||||
const prev = get();
|
||||
const next = func(prev);
|
||||
set(next);
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(storeKey, {
|
||||
detail: { next, prev },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function reset() {
|
||||
update(() => defaults);
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
// import { useEvent } from '@strudel.cycles/react';
|
||||
import * as Store from './store.mjs';
|
||||
import {} from 'react';
|
||||
|
||||
function useStore() {
|
||||
const [state, setState] = useState(Store.get());
|
||||
useEvent(Store.storeKey, (e) => setState(e.detail.next));
|
||||
return { state, ...Store };
|
||||
}
|
||||
|
||||
// TODO: dedupe
|
||||
function useEvent(name, onTrigger, useCapture = false) {
|
||||
useEffect(() => {
|
||||
document.addEventListener(name, onTrigger, useCapture);
|
||||
return () => {
|
||||
document.removeEventListener(name, onTrigger, useCapture);
|
||||
};
|
||||
}, [onTrigger]);
|
||||
}
|
||||
|
||||
export default useStore;
|
||||
@ -1,14 +0,0 @@
|
||||
import { settings } from './repl/themes.mjs';
|
||||
import useStore from './useStore.mjs';
|
||||
|
||||
function useTheme() {
|
||||
const { state } = useStore();
|
||||
const theme = state.theme || 'strudelTheme';
|
||||
const themeSettings = settings[theme];
|
||||
return {
|
||||
theme: state.theme,
|
||||
themeSettings,
|
||||
};
|
||||
}
|
||||
|
||||
export default useTheme;
|
||||
Loading…
x
Reference in New Issue
Block a user