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
+11 -14
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;
+2 -4
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
+7 -12
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);
}
}}
>
+2 -6
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
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);
}
-49
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]);
}
});
}
-22
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;
-14
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;