refactor settings to nanostores

This commit is contained in:
Felix Roos 2023-02-22 22:04:39 +01:00
parent ff99dbcd22
commit b67b049802
11 changed files with 81 additions and 122 deletions

View File

@ -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
View File

@ -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'}

View File

@ -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",

View File

@ -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;

View File

@ -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

View File

@ -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);
}
}}
>

View File

@ -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
View 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);
}

View File

@ -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]);
}
});
}

View File

@ -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;

View File

@ -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;