Merge branch 'main' into space-shell/helix-keybindings

This commit is contained in:
space-shell 2026-01-23 09:17:09 +01:00
commit 03b221a2a2
75 changed files with 719 additions and 611 deletions

5
.astro/settings.json Normal file
View file

@ -0,0 +1,5 @@
{
"devToolbar": {
"enabled": false
}
}

View file

@ -198,6 +198,7 @@ export function activateTheme(name) {
const themeSettings = settings[name] || settings.strudelTheme; const themeSettings = settings[name] || settings.strudelTheme;
// set css variables // set css variables
themeStyle.innerHTML = `:root { themeStyle.innerHTML = `:root {
color-scheme: ${themeSettings.light ? 'light' : 'dark'};
${Object.entries(themeSettings) ${Object.entries(themeSettings)
// important to override fallback // important to override fallback
.map(([key, value]) => `--${key}: ${value} !important;`) .map(([key, value]) => `--${key}: ${value} !important;`)

View file

@ -17,13 +17,15 @@ export const settings = {
background: 'white', background: 'white',
lineBackground: 'transparent', lineBackground: 'transparent',
foreground: deepPurple, foreground: deepPurple,
muted: deepPurple + '50',
caret: '#797977', caret: '#797977',
selection: yellowPink, selection: yellowPink,
selectionMatch: '#2B323D', selectionMatch: '#2B323D',
gutterBackground: grey, gutterBackground: pinkAccent,
gutterForeground: lightGrey, gutterForeground: lightGrey,
gutterBorder: 'transparent', gutterBorder: 'transparent',
lineHighlight: pinkAccent, lineHighlight: pinkAccent,
light: true,
}; };
export default createTheme({ export default createTheme({

View file

@ -31,6 +31,7 @@ const palette = palettes['Sour Watermelon B'];
export const settings = { export const settings = {
background: palette[3], background: palette[3],
foreground: palette[1], foreground: palette[1],
muted: palette[1] + '50',
caret: palette[0], caret: palette[0],
selection: palette[0], selection: palette[0],
selectionMatch: palette[1], selectionMatch: palette[1],

View file

@ -8,6 +8,7 @@ export const settings = {
background: '#282b2e', background: '#282b2e',
lineBackground: '#282b2e99', lineBackground: '#282b2e99',
foreground: '#a9b7c6', foreground: '#a9b7c6',
muted: '#a9b7c650',
caret: '#00FF00', caret: '#00FF00',
selection: '#343739', selection: '#343739',
selectionMatch: '#343739', selectionMatch: '#343739',
@ -19,6 +20,7 @@ export default createTheme({
settings: { settings: {
background: '#282b2e', background: '#282b2e',
foreground: '#a9b7c6', foreground: '#a9b7c6',
muted: '#a9b7c650',
caret: '#00FF00', caret: '#00FF00',
selection: '#4e5254', selection: '#4e5254',
selectionMatch: '#4e5254', selectionMatch: '#4e5254',

View file

@ -12,6 +12,7 @@ export const settings = {
background: hex[0], background: hex[0],
lineBackground: 'transparent', lineBackground: 'transparent',
foreground: hex[1], foreground: hex[1],
muted: hex[2],
selection: hex[2], selection: hex[2],
selectionMatch: hex[0], selectionMatch: hex[0],
gutterBackground: hex[0], gutterBackground: hex[0],

View file

@ -11,6 +11,7 @@ export const settings = {
background: '#272C35', background: '#272C35',
lineBackground: '#272C3599', lineBackground: '#272C3599',
foreground: 'hsl(220, 14%, 71%)', foreground: 'hsl(220, 14%, 71%)',
muted: 'hsl(220, 14%, 41%)',
caret: '#797977', caret: '#797977',
selection: '#ffffff30', selection: '#ffffff30',
selectionMatch: '#2B323D', selectionMatch: '#2B323D',
@ -25,6 +26,7 @@ export default createTheme({
settings: { settings: {
background: '#272C35', background: '#272C35',
foreground: '#9d9b97', foreground: '#9d9b97',
muted: 'hsl(220, 14%, 41%)',
caret: '#797977', caret: '#797977',
selection: '#3d4c64', selection: '#3d4c64',
selectionMatch: '#3d4c64', selectionMatch: '#3d4c64',

View file

@ -5,6 +5,7 @@ export const settings = {
background: '#21202e', background: '#21202e',
lineBackground: '#21202e99', lineBackground: '#21202e99',
foreground: '#edecee', foreground: '#edecee',
muted: '#edecee50',
caret: '#a277ff', caret: '#a277ff',
selection: '#3d375e7f', selection: '#3d375e7f',
selectionMatch: '#3d375e7f', selectionMatch: '#3d375e7f',
@ -18,6 +19,7 @@ export default createTheme({
settings: { settings: {
background: '#21202e', background: '#21202e',
foreground: '#edecee', foreground: '#edecee',
muted: '#edecee50',
caret: '#a277ff', caret: '#a277ff',
selection: '#5a51898f', selection: '#5a51898f',
selectionMatch: '#5a51898f', selectionMatch: '#5a51898f',

View file

@ -6,6 +6,7 @@ export const settings = {
background: '#FFFFFF', background: '#FFFFFF',
lineBackground: '#FFFFFF99', lineBackground: '#FFFFFF99',
foreground: '#000000', foreground: '#000000',
muted: '#00000050',
caret: '#FBAC52', caret: '#FBAC52',
selection: '#FFD420', selection: '#FFD420',
selectionMatch: '#FFD420', selectionMatch: '#FFD420',
@ -20,6 +21,7 @@ export default createTheme({
settings: { settings: {
background: '#FFFFFF', background: '#FFFFFF',
foreground: '#000000', foreground: '#000000',
muted: '#00000050',
caret: '#FBAC52', caret: '#FBAC52',
selection: '#FFD420', selection: '#FFD420',
selectionMatch: '#FFD420', selectionMatch: '#FFD420',

View file

@ -2,7 +2,8 @@ import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs'; import { createTheme } from './theme-helper.mjs';
export const settings = { export const settings = {
background: 'black', background: 'black',
foreground: 'white', // whats that? foreground: 'white',
muted: '#ffffff50',
caret: 'white', caret: 'white',
selection: '#ffffff20', selection: '#ffffff20',
selectionMatch: '#036dd626', selectionMatch: '#036dd626',

View file

@ -3,7 +3,8 @@ import { createTheme } from './theme-helper.mjs';
export const settings = { export const settings = {
background: '#051DB5', background: '#051DB5',
lineBackground: '#051DB550', lineBackground: '#051DB550',
foreground: 'white', // whats that? foreground: 'white',
muted: '#ffffff50',
caret: 'white', caret: 'white',
selection: 'rgba(128, 203, 196, 0.5)', selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#036dd626', selectionMatch: '#036dd626',

View file

@ -11,6 +11,7 @@ export const settings = {
background: hex[0], background: hex[0],
lineBackground: 'transparent', lineBackground: 'transparent',
foreground: hex[2], foreground: hex[2],
muted: hex[3],
selection: hex[3], selection: hex[3],
selectionMatch: hex[0], selectionMatch: hex[0],
gutterBackground: hex[0], gutterBackground: hex[0],

View file

@ -9,6 +9,7 @@ export const settings = {
background: '#242424', background: '#242424',
lineBackground: '#24242499', lineBackground: '#24242499',
foreground: '#f8f8f2', foreground: '#f8f8f2',
muted: '#f8f8f250',
caret: '#FFFFFF', caret: '#FFFFFF',
selection: 'rgba(255, 255, 255, 0.1)', selection: 'rgba(255, 255, 255, 0.1)',
selectionMatch: 'rgba(255, 255, 255, 0.2)', selectionMatch: 'rgba(255, 255, 255, 0.2)',
@ -23,6 +24,7 @@ export default createTheme({
settings: { settings: {
background: '#242424', background: '#242424',
foreground: '#f8f8f2', foreground: '#f8f8f2',
muted: '#f8f8f250',
caret: '#FFFFFF', caret: '#FFFFFF',
selection: 'rgba(255, 255, 255, 0.1)', selection: 'rgba(255, 255, 255, 0.1)',
selectionMatch: 'rgba(255, 255, 255, 0.2)', selectionMatch: 'rgba(255, 255, 255, 0.2)',

View file

@ -11,6 +11,7 @@ export const settings = {
background: '#282a36', background: '#282a36',
lineBackground: '#282a3699', lineBackground: '#282a3699',
foreground: '#f8f8f2', foreground: '#f8f8f2',
muted: '#f8f8f250',
caret: '#f8f8f0', caret: '#f8f8f0',
selection: 'rgba(255, 255, 255, 0.1)', selection: 'rgba(255, 255, 255, 0.1)',
selectionMatch: 'rgba(255, 255, 255, 0.2)', selectionMatch: 'rgba(255, 255, 255, 0.2)',
@ -27,6 +28,7 @@ export default createTheme({
settings: { settings: {
background: '#282a36', background: '#282a36',
foreground: '#f8f8f2', foreground: '#f8f8f2',
muted: '#f8f8f250',
caret: '#f8f8f0', caret: '#f8f8f0',
selection: 'rgba(255, 255, 255, 0.1)', selection: 'rgba(255, 255, 255, 0.1)',
selectionMatch: 'rgba(255, 255, 255, 0.2)', selectionMatch: 'rgba(255, 255, 255, 0.2)',

View file

@ -10,6 +10,7 @@ export const settings = {
background: '#2a2734', background: '#2a2734',
lineBackground: '#2a273499', lineBackground: '#2a273499',
foreground: '#eeebff', foreground: '#eeebff',
muted: '#eeebff50',
caret: '#ffad5c', caret: '#ffad5c',
selection: 'rgba(255, 255, 255, 0.1)', selection: 'rgba(255, 255, 255, 0.1)',
gutterBackground: '#2a2734', gutterBackground: '#2a2734',
@ -22,6 +23,7 @@ export default createTheme({
settings: { settings: {
background: '#2a2734', background: '#2a2734',
foreground: '#6c6783', foreground: '#6c6783',
muted: '#eeebff50',
caret: '#ffad5c', caret: '#ffad5c',
selection: '#9a86fd', selection: '#9a86fd',
selectionMatch: '#9a86fd', selectionMatch: '#9a86fd',

View file

@ -6,6 +6,7 @@ export const settings = {
background: '#fff', background: '#fff',
lineBackground: '#ffffff99', lineBackground: '#ffffff99',
foreground: '#000', foreground: '#000',
muted: '#00000050',
caret: '#FFFFFF', caret: '#FFFFFF',
selection: '#d7d4f0', selection: '#d7d4f0',
selectionMatch: '#d7d4f0', selectionMatch: '#d7d4f0',
@ -20,6 +21,7 @@ export default createTheme({
settings: { settings: {
background: '#fff', background: '#fff',
foreground: '#000', foreground: '#000',
muted: '#00000050',
caret: '#FFFFFF', caret: '#FFFFFF',
selection: '#d7d4f0', selection: '#d7d4f0',
selectionMatch: '#d7d4f0', selectionMatch: '#d7d4f0',

View file

@ -23,6 +23,7 @@ export const settings = {
background: hex[0], background: hex[0],
lineBackground: 'transparent', lineBackground: 'transparent',
foreground: hex[10], foreground: hex[10],
muted: hex[7],
selection: hex[8], selection: hex[8],
selectionMatch: hex[0], selectionMatch: hex[0],
gutterBackground: hex[3], gutterBackground: hex[3],

View file

@ -5,6 +5,7 @@ export const settings = {
background: '#0d1117', background: '#0d1117',
lineBackground: '#0d111799', lineBackground: '#0d111799',
foreground: '#c9d1d9', foreground: '#c9d1d9',
muted: '#c9d1d950',
caret: '#c9d1d9', caret: '#c9d1d9',
selection: '#003d73', selection: '#003d73',
selectionMatch: '#003d73', selectionMatch: '#003d73',
@ -16,6 +17,7 @@ export default createTheme({
settings: { settings: {
background: '#0d1117', background: '#0d1117',
foreground: '#c9d1d9', foreground: '#c9d1d9',
muted: '#c9d1d950',
caret: '#c9d1d9', caret: '#c9d1d9',
selection: '#003d73', selection: '#003d73',
selectionMatch: '#003d73', selectionMatch: '#003d73',

View file

@ -6,6 +6,7 @@ export const settings = {
background: '#fff', background: '#fff',
lineBackground: '#ffffff99', lineBackground: '#ffffff99',
foreground: '#24292e', foreground: '#24292e',
muted: '#24292e50',
selection: '#BBDFFF', selection: '#BBDFFF',
selectionMatch: '#BBDFFF', selectionMatch: '#BBDFFF',
gutterBackground: '#fff', gutterBackground: '#fff',
@ -17,6 +18,7 @@ export default createTheme({
settings: { settings: {
background: '#fff', background: '#fff',
foreground: '#24292e', foreground: '#24292e',
muted: '#24292e50',
selection: '#BBDFFF', selection: '#BBDFFF',
selectionMatch: '#BBDFFF', selectionMatch: '#BBDFFF',
gutterBackground: '#fff', gutterBackground: '#fff',

View file

@ -13,6 +13,7 @@ export const settings = {
background: hex[0], background: hex[0],
lineBackground: 'transparent', lineBackground: 'transparent',
foreground: hex[2], foreground: hex[2],
muted: hex[2] + '50',
selection: hex[4], selection: hex[4],
selectionMatch: hex[0], selectionMatch: hex[0],
gutterBackground: hex[0], gutterBackground: hex[0],

View file

@ -10,6 +10,7 @@ export const settings = {
background: '#282828', background: '#282828',
lineBackground: '#28282899', lineBackground: '#28282899',
foreground: '#ebdbb2', foreground: '#ebdbb2',
muted: '#ebdbb250',
caret: '#ebdbb2', caret: '#ebdbb2',
selection: '#bdae93', selection: '#bdae93',
selectionMatch: '#bdae93', selectionMatch: '#bdae93',
@ -23,6 +24,7 @@ export default createTheme({
settings: { settings: {
background: '#282828', background: '#282828',
foreground: '#ebdbb2', foreground: '#ebdbb2',
muted: '#ebdbb250',
caret: '#ebdbb2', caret: '#ebdbb2',
selection: '#b99d555c', selection: '#b99d555c',
selectionMatch: '#b99d555c', selectionMatch: '#b99d555c',

View file

@ -11,6 +11,7 @@ export const settings = {
background: '#fbf1c7', background: '#fbf1c7',
lineBackground: '#fbf1c799', lineBackground: '#fbf1c799',
foreground: '#3c3836', foreground: '#3c3836',
muted: '#3c383650',
caret: '#af3a03', caret: '#af3a03',
selection: '#ebdbb2', selection: '#ebdbb2',
selectionMatch: '#bdae93', selectionMatch: '#bdae93',
@ -25,6 +26,7 @@ export default createTheme({
settings: { settings: {
background: '#fbf1c7', background: '#fbf1c7',
foreground: '#3c3836', foreground: '#3c3836',
muted: '#3c383650',
caret: '#af3a03', caret: '#af3a03',
selection: '#bdae9391', selection: '#bdae9391',
selectionMatch: '#bdae9391', selectionMatch: '#bdae9391',

View file

@ -5,6 +5,7 @@ export const settings = {
background: '#212121', background: '#212121',
lineBackground: '#21212199', lineBackground: '#21212199',
foreground: '#bdbdbd', foreground: '#bdbdbd',
muted: '#bdbdbd50',
caret: '#a0a4ae', caret: '#a0a4ae',
selection: '#d7d4f0', selection: '#d7d4f0',
selectionMatch: '#d7d4f0', selectionMatch: '#d7d4f0',
@ -19,6 +20,7 @@ export default createTheme({
settings: { settings: {
background: '#212121', background: '#212121',
foreground: '#bdbdbd', foreground: '#bdbdbd',
muted: '#bdbdbd50',
caret: '#a0a4ae', caret: '#a0a4ae',
selection: '#d7d4f063', selection: '#d7d4f063',
selectionMatch: '#d7d4f063', selectionMatch: '#d7d4f063',

View file

@ -5,7 +5,8 @@ export const settings = {
light: true, light: true,
background: '#FAFAFA', background: '#FAFAFA',
lineBackground: '#FAFAFA99', lineBackground: '#FAFAFA99',
foreground: '#90A4AE', foreground: '#6182B8',
muted: '#6182B850',
caret: '#272727', caret: '#272727',
selection: '#80CBC440', selection: '#80CBC440',
selectionMatch: '#FAFAFA', selectionMatch: '#FAFAFA',
@ -19,6 +20,7 @@ export default createTheme({
settings: { settings: {
background: '#FAFAFA', background: '#FAFAFA',
foreground: '#90A4AE', foreground: '#90A4AE',
muted: '#6182B850',
caret: '#272727', caret: '#272727',
selection: '#80CBC440', selection: '#80CBC440',
selectionMatch: '#80CBC440', selectionMatch: '#80CBC440',

View file

@ -5,6 +5,7 @@ export const settings = {
background: '#272822', background: '#272822',
lineBackground: '#27282299', lineBackground: '#27282299',
foreground: '#FFFFFF', foreground: '#FFFFFF',
muted: '#FFFFFF50',
caret: '#FFFFFF', caret: '#FFFFFF',
selection: '#49483E', selection: '#49483E',
selectionMatch: '#49483E', selectionMatch: '#49483E',
@ -18,6 +19,7 @@ export default createTheme({
settings: { settings: {
background: '#272822', background: '#272822',
foreground: '#FFFFFF', foreground: '#FFFFFF',
muted: '#FFFFFF50',
caret: '#FFFFFF', caret: '#FFFFFF',
selection: '#49483E', selection: '#49483E',
selectionMatch: '#49483E', selectionMatch: '#49483E',

View file

@ -6,6 +6,7 @@ export const settings = {
background: '#f2f1f8', background: '#f2f1f8',
lineBackground: '#f2f1f899', lineBackground: '#f2f1f899',
foreground: '#0c006b', foreground: '#0c006b',
muted: '#0c006b50',
caret: '#5c49e9', caret: '#5c49e9',
selection: '#d5d1f2', selection: '#d5d1f2',
selectionMatch: '#d5d1f2', selectionMatch: '#d5d1f2',
@ -19,6 +20,7 @@ export default createTheme({
settings: { settings: {
background: '#f2f1f8', background: '#f2f1f8',
foreground: '#0c006b', foreground: '#0c006b',
muted: '#0c006b50',
caret: '#5c49e9', caret: '#5c49e9',
selection: '#d5d1f2', selection: '#d5d1f2',
selectionMatch: '#d5d1f2', selectionMatch: '#d5d1f2',

View file

@ -5,6 +5,7 @@ export const settings = {
background: '#2e3440', background: '#2e3440',
lineBackground: '#2e344099', lineBackground: '#2e344099',
foreground: '#FFFFFF', foreground: '#FFFFFF',
muted: '#FFFFFF50',
caret: '#FFFFFF', caret: '#FFFFFF',
selection: '#3b4252', selection: '#3b4252',
selectionMatch: '#e5e9f0', selectionMatch: '#e5e9f0',
@ -20,6 +21,7 @@ export default createTheme({
settings: { settings: {
background: '#2e3440', background: '#2e3440',
foreground: '#FFFFFF', foreground: '#FFFFFF',
muted: '#FFFFFF50',
caret: '#FFFFFF', caret: '#FFFFFF',
selection: '#00000073', selection: '#00000073',
selectionMatch: '#00000073', selectionMatch: '#00000073',

View file

@ -13,6 +13,7 @@ export const settings = {
background: hex[0], background: hex[0],
lineBackground: 'transparent', lineBackground: 'transparent',
foreground: hex[2], foreground: hex[2],
muted: hex[2] + '50',
selection: hex[4], selection: hex[4],
selectionMatch: hex[0], selectionMatch: hex[0],
gutterBackground: hex[0], gutterBackground: hex[0],

View file

@ -5,6 +5,7 @@ export const settings = {
background: '#002b36', background: '#002b36',
lineBackground: '#002b3699', lineBackground: '#002b3699',
foreground: '#93a1a1', foreground: '#93a1a1',
muted: '#93a1a150',
caret: '#839496', caret: '#839496',
selection: '#173541', selection: '#173541',
selectionMatch: '#aafe661a', selectionMatch: '#aafe661a',
@ -45,6 +46,7 @@ export default createTheme({
settings: { settings: {
background: c.background, background: c.background,
foreground: c.foreground, foreground: c.foreground,
muted: c.muted,
caret: c.cursor, caret: c.cursor,
selection: c.selection, selection: c.selection,
selectionMatch: c.selection, selectionMatch: c.selection,

View file

@ -8,6 +8,7 @@ export const settings = {
background: '#fdf6e3', background: '#fdf6e3',
lineBackground: '#fdf6e399', lineBackground: '#fdf6e399',
foreground: '#657b83', foreground: '#657b83',
muted: '#657b8350',
caret: '#586e75', caret: '#586e75',
selection: '#dfd9c8', selection: '#dfd9c8',
selectionMatch: '#dfd9c8', selectionMatch: '#dfd9c8',
@ -19,6 +20,7 @@ export const settings = {
const c = { const c = {
background: '#FDF6E3', background: '#FDF6E3',
foreground: '#657B83', foreground: '#657B83',
muted: '#657b8350',
selection: '#EEE8D5', selection: '#EEE8D5',
selectionMatch: '#EEE8D5', selectionMatch: '#EEE8D5',
cursor: '#657B83', cursor: '#657B83',

View file

@ -13,6 +13,7 @@ export const settings = {
background: '#000000', background: '#000000',
lineBackground: 'transparent', lineBackground: 'transparent',
foreground: hex[4], foreground: hex[4],
muted: hex[6],
selection: hex[6], selection: hex[6],
gutterBackground: hex[0], gutterBackground: hex[0],
gutterForeground: hex[5], gutterForeground: hex[5],

View file

@ -5,6 +5,7 @@ export const settings = {
background: '#222', background: '#222',
lineBackground: '#22222299', lineBackground: '#22222299',
foreground: '#fff', foreground: '#fff',
muted: '#8a919966',
caret: '#ffcc00', caret: '#ffcc00',
selection: 'rgba(128, 203, 196, 0.5)', selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#036dd626', selectionMatch: '#036dd626',

View file

@ -5,6 +5,7 @@ export const settings = {
background: '#303841', background: '#303841',
lineBackground: '#30384199', lineBackground: '#30384199',
foreground: '#FFFFFF', foreground: '#FFFFFF',
muted: '#FFFFFF50',
caret: '#FBAC52', caret: '#FBAC52',
selection: '#4C5964', selection: '#4C5964',
selectionMatch: '#3A546E', selectionMatch: '#3A546E',
@ -18,6 +19,7 @@ export default createTheme({
settings: { settings: {
background: '#303841', background: '#303841',
foreground: '#FFFFFF', foreground: '#FFFFFF',
muted: '#FFFFFF50',
caret: '#FBAC52', caret: '#FBAC52',
selection: '#4C5964', selection: '#4C5964',
selectionMatch: '#3A546E', selectionMatch: '#3A546E',

View file

@ -9,7 +9,8 @@ let colorD = '#f8fc55';
export const settings = { export const settings = {
background: '#000000', background: '#000000',
foreground: colorA, // whats that? foreground: colorA,
muted: colorA + '50',
caret: colorC, caret: colorC,
selection: colorD, selection: colorD,
selectionMatch: colorA, selectionMatch: colorA,

View file

@ -4,7 +4,8 @@ import { createTheme } from './theme-helper.mjs';
export const settings = { export const settings = {
background: '#24283b', background: '#24283b',
lineBackground: '#24283b99', lineBackground: '#24283b99',
foreground: '#7982a9', foreground: '#f9f2f9',
muted: '#f9f2f950',
caret: '#c0caf5', caret: '#c0caf5',
selection: '#6f7bb630', selection: '#6f7bb630',
selectionMatch: '#1f2335', selectionMatch: '#1f2335',
@ -18,7 +19,8 @@ export default createTheme({
theme: 'dark', theme: 'dark',
settings: { settings: {
background: '#24283b', background: '#24283b',
foreground: '#7982a9', foreground: '#f9f2f9',
muted: '#f9f2f950',
caret: '#c0caf5', caret: '#c0caf5',
selection: '#6f7bb630', selection: '#6f7bb630',
selectionMatch: '#343b5f', selectionMatch: '#343b5f',

View file

@ -4,7 +4,8 @@ import { createTheme } from './theme-helper.mjs';
export const settings = { export const settings = {
background: '#1a1b26', background: '#1a1b26',
lineBackground: '#1a1b2699', lineBackground: '#1a1b2699',
foreground: '#787c99', foreground: '#f8fcf9',
muted: '#f8fcf950',
caret: '#c0caf5', caret: '#c0caf5',
selection: '#515c7e40', selection: '#515c7e40',
selectionMatch: '#16161e', selectionMatch: '#16161e',
@ -18,7 +19,8 @@ export default createTheme({
theme: 'dark', theme: 'dark',
settings: { settings: {
background: '#1a1b26', background: '#1a1b26',
foreground: '#787c99', foreground: '#f8fcf9',
muted: '#f8fcf950',
caret: '#c0caf5', caret: '#c0caf5',
selection: '#515c7e40', selection: '#515c7e40',
selectionMatch: '#16161e', selectionMatch: '#16161e',

View file

@ -6,6 +6,7 @@ export const settings = {
background: '#e1e2e7', background: '#e1e2e7',
lineBackground: '#e1e2e799', lineBackground: '#e1e2e799',
foreground: '#3760bf', foreground: '#3760bf',
muted: '#3760bf50',
caret: '#3760bf', caret: '#3760bf',
selection: '#99a7df', selection: '#99a7df',
selectionMatch: '#99a7df', selectionMatch: '#99a7df',
@ -20,6 +21,7 @@ export default createTheme({
settings: { settings: {
background: '#e1e2e7', background: '#e1e2e7',
foreground: '#3760bf', foreground: '#3760bf',
muted: '#3760bf50',
caret: '#3760bf', caret: '#3760bf',
selection: '#99a7df', selection: '#99a7df',
selectionMatch: '#99a7df', selectionMatch: '#99a7df',

View file

@ -5,6 +5,7 @@ export const settings = {
background: '#1e1e1e', background: '#1e1e1e',
lineBackground: '#1e1e1e99', lineBackground: '#1e1e1e99',
foreground: '#fff', foreground: '#fff',
muted: '#ffffff50',
caret: '#c6c6c6', caret: '#c6c6c6',
selection: '#6199ff2f', selection: '#6199ff2f',
selectionMatch: '#72a1ff59', selectionMatch: '#72a1ff59',
@ -19,6 +20,7 @@ export default createTheme({
settings: { settings: {
background: '#1e1e1e', background: '#1e1e1e',
foreground: '#fff', foreground: '#fff',
muted: '#ffffff50',
caret: '#c6c6c6', caret: '#c6c6c6',
selection: '#6199ff2f', selection: '#6199ff2f',
selectionMatch: '#72a1ff59', selectionMatch: '#72a1ff59',

View file

@ -5,6 +5,7 @@ export const settings = {
background: '#ffffff', background: '#ffffff',
lineBackground: '#ffffff50', lineBackground: '#ffffff50',
foreground: '#383a42', foreground: '#383a42',
muted: '#383a4250',
caret: '#000', caret: '#000',
selection: '#add6ff', selection: '#add6ff',
selectionMatch: '#a8ac94', selectionMatch: '#a8ac94',
@ -20,6 +21,7 @@ export default createTheme({
settings: { settings: {
background: '#ffffff', background: '#ffffff',
foreground: '#383a42', foreground: '#383a42',
muted: '#383a4250',
caret: '#000', caret: '#000',
selection: '#add6ff', selection: '#add6ff',
selectionMatch: '#a8ac94', selectionMatch: '#a8ac94',

View file

@ -2,7 +2,8 @@ import { tags as t } from '@lezer/highlight';
import { createTheme } from './theme-helper.mjs'; import { createTheme } from './theme-helper.mjs';
export const settings = { export const settings = {
background: 'white', background: 'white',
foreground: 'black', // whats that? foreground: 'black',
muted: '#00000050',
caret: 'black', caret: 'black',
selection: 'rgba(128, 203, 196, 0.5)', selection: 'rgba(128, 203, 196, 0.5)',
selectionMatch: '#ffffff26', selectionMatch: '#ffffff26',

View file

@ -6,6 +6,7 @@ export const settings = {
background: '#fff', background: '#fff',
lineBackground: '#ffffff99', lineBackground: '#ffffff99',
foreground: '#3D3D3D', foreground: '#3D3D3D',
muted: '#3D3D3D50',
selection: '#BBDFFF', selection: '#BBDFFF',
selectionMatch: '#BBDFFF', selectionMatch: '#BBDFFF',
gutterBackground: '#fff', gutterBackground: '#fff',
@ -18,6 +19,7 @@ export default createTheme({
settings: { settings: {
background: '#fff', background: '#fff',
foreground: '#3D3D3D', foreground: '#3D3D3D',
muted: '#3D3D3D50',
selection: '#BBDFFF', selection: '#BBDFFF',
selectionMatch: '#BBDFFF', selectionMatch: '#BBDFFF',
gutterBackground: '#fff', gutterBackground: '#fff',

View file

@ -3046,6 +3046,7 @@ registerSubControls('lfo', [
['skew', 'sk'], ['skew', 'sk'],
['curve', 'cu'], ['curve', 'cu'],
['sync', 's'], ['sync', 's'],
['retrig', 'rt'],
['fxi'], ['fxi'],
]); ]);
registerSubControls('env', [ registerSubControls('env', [
@ -3132,13 +3133,14 @@ Pattern.prototype.modulate = function (type, config, idPat) {
* @param {string | Pattern} [config.control] Node to modulate. Aliases: c * @param {string | Pattern} [config.control] Node to modulate. Aliases: c
* @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc * @param {string | Pattern} [config.subControl] Sub-control name to append to the control key. Aliases: sc
* @param {number | Pattern} [config.rate] Modulation rate. Aliases: r * @param {number | Pattern} [config.rate] Modulation rate. Aliases: r
* @param {number | Pattern} [config.sync] Tempo-synced modulation rate. Aliases: s
* @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr * @param {number | Pattern} [config.depth] Relative modulation depth. Aliases: dep, dr
* @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da * @param {number | Pattern} [config.depthabs] Absolute modulation depth. Aliases: da
* @param {number | Pattern} [config.dcoffset] DC offset / bias for the waveform. Aliases: dc * @param {number | Pattern} [config.dcoffset] DC offset / bias for the waveform. Aliases: dc
* @param {number | Pattern} [config.shape] Shape index. Aliases: sh * @param {number | Pattern} [config.shape] Shape index. Aliases: sh
* @param {number | Pattern} [config.skew] Skew amount. Aliases: sk * @param {number | Pattern} [config.skew] Skew amount. Aliases: sk
* @param {number | Pattern} [config.curve] Exponential curve amount. Aliases: cu * @param {number | Pattern} [config.curve] Exponential curve amount. Aliases: cu
* @param {number | Pattern} [config.sync] Tempo-synced modulation rate. Aliases: s * @param {number | Pattern} [config.retrig] If > 0.5, the LFO will retrigger on each event. Aliases: rt
* @param {number | Pattern} [config.fxi] FX index to target * @param {number | Pattern} [config.fxi] FX index to target
* @param {string | Pattern} id ID to use for this modulator * @param {string | Pattern} id ID to use for this modulator
* @returns Pattern * @returns Pattern

View file

@ -1055,91 +1055,88 @@ function _composeOp(a, b, func) {
return func(a, b); return func(a, b);
} }
// Make composers // pattern composers
(function () { const COMPOSERS = {
// pattern composers set: [(a, b) => b],
const composers = { keep: [(a) => a],
set: [(a, b) => b], keepif: [(a, b) => (b ? a : undefined)],
keep: [(a) => a],
keepif: [(a, b) => (b ? a : undefined)],
// numerical functions // numerical functions
/** /**
* *
* Assumes a pattern of numbers. Adds the given number to each item in the pattern. * Assumes a pattern of numbers. Adds the given number to each item in the pattern.
* @name add * @name add
* @memberof Pattern * @memberof Pattern
* @tags math * @tags math
* @example * @example
* // Here, the triad 0, 2, 4 is shifted by different amounts * // Here, the triad 0, 2, 4 is shifted by different amounts
* n("0 2 4".add("<0 3 4 0>")).scale("C:major") * n("0 2 4".add("<0 3 4 0>")).scale("C:major")
* // Without add, the equivalent would be: * // Without add, the equivalent would be:
* // n("<[0 2 4] [3 5 7] [4 6 8] [0 2 4]>").scale("C:major") * // n("<[0 2 4] [3 5 7] [4 6 8] [0 2 4]>").scale("C:major")
* @example * @example
* // You can also use add with notes: * // You can also use add with notes:
* note("c3 e3 g3".add("<0 5 7 0>")) * note("c3 e3 g3".add("<0 5 7 0>"))
* // Behind the scenes, the notes are converted to midi numbers: * // Behind the scenes, the notes are converted to midi numbers:
* // note("48 52 55".add("<0 5 7 0>")) * // note("48 52 55".add("<0 5 7 0>"))
*/ */
add: [numeralArgs((a, b) => a + b)], // support string concatenation add: [numeralArgs((a, b) => a + b)], // support string concatenation
/** /**
* *
* Like add, but the given numbers are subtracted. * Like add, but the given numbers are subtracted.
* @name sub * @name sub
* @memberof Pattern * @memberof Pattern
* @tags math * @tags math
* @example * @example
* n("0 2 4".sub("<0 1 2 3>")).scale("C4:minor") * n("0 2 4".sub("<0 1 2 3>")).scale("C4:minor")
* // See add for more information. * // See add for more information.
*/ */
sub: [numeralArgs((a, b) => a - b)], sub: [numeralArgs((a, b) => a - b)],
/** /**
* *
* Multiplies each number by the given factor. * Multiplies each number by the given factor.
* @name mul * @name mul
* @memberof Pattern * @memberof Pattern
* @tags math * @tags math
* @example * @example
* "<1 1.5 [1.66, <2 2.33>]>*4".mul(150).freq() * "<1 1.5 [1.66, <2 2.33>]>*4".mul(150).freq()
*/ */
mul: [numeralArgs((a, b) => a * b)], mul: [numeralArgs((a, b) => a * b)],
/** /**
* *
* Divides each number by the given factor. * Divides each number by the given factor.
* @name div * @name div
* @memberof Pattern * @memberof Pattern
* @tags math * @tags math
*/ */
div: [numeralArgs((a, b) => a / b)], div: [numeralArgs((a, b) => a / b)],
mod: [numeralArgs(_mod)], mod: [numeralArgs(_mod)],
pow: [numeralArgs(Math.pow)], pow: [numeralArgs(Math.pow)],
log2: [numeralArgs(Math.log2)], log2: [numeralArgs(Math.log2)],
band: [numeralArgs((a, b) => a & b)], band: [numeralArgs((a, b) => a & b)],
bor: [numeralArgs((a, b) => a | b)], bor: [numeralArgs((a, b) => a | b)],
bxor: [numeralArgs((a, b) => a ^ b)], bxor: [numeralArgs((a, b) => a ^ b)],
blshift: [numeralArgs((a, b) => a << b)], blshift: [numeralArgs((a, b) => a << b)],
brshift: [numeralArgs((a, b) => a >> b)], brshift: [numeralArgs((a, b) => a >> b)],
// TODO - force numerical comparison if both look like numbers? // TODO - force numerical comparison if both look like numbers?
lt: [(a, b) => a < b], lt: [(a, b) => a < b],
gt: [(a, b) => a > b], gt: [(a, b) => a > b],
lte: [(a, b) => a <= b], lte: [(a, b) => a <= b],
gte: [(a, b) => a >= b], gte: [(a, b) => a >= b],
eq: [(a, b) => a == b], eq: [(a, b) => a == b],
eqt: [(a, b) => a === b], eqt: [(a, b) => a === b],
ne: [(a, b) => a != b], ne: [(a, b) => a != b],
net: [(a, b) => a !== b], net: [(a, b) => a !== b],
and: [(a, b) => a && b], and: [(a, b) => a && b],
or: [(a, b) => a || b], or: [(a, b) => a || b],
// bitwise ops // bitwise ops
func: [(a, b) => b(a)], func: [(a, b) => b(a)],
}; };
const hows = ['In', 'Out', 'Mix', 'Squeeze', 'SqueezeOut', 'Reset', 'Restart', 'Poly'];
const _setupAlignments = () => {
// generate methods to do what and how // generate methods to do what and how
for (const [what, [op, preprocess]] of Object.entries(composers)) { for (const [what, [op, preprocess]] of Object.entries(COMPOSERS)) {
// make plain version, e.g. pat._add(value) adds that plain value // make plain version, e.g. pat._add(value) adds that plain value
// to all the values in pat // to all the values in pat
Pattern.prototype['_' + what] = function (value) { Pattern.prototype['_' + what] = function (value) {
@ -1148,16 +1145,18 @@ function _composeOp(a, b, func) {
// make patternified monster version // make patternified monster version
Object.defineProperty(Pattern.prototype, what, { Object.defineProperty(Pattern.prototype, what, {
// Set to configurable so we can update if the default alignment changes
configurable: true,
// a getter that returns a function, so 'pat' can be // a getter that returns a function, so 'pat' can be
// accessed by closures that are methods of that function.. // accessed by closures that are methods of that function..
get: function () { get: function () {
const pat = this; const pat = this;
// wrap the 'in' function as default behaviour // wrap the 'in' function as default behaviour
const wrapper = (...other) => pat[what]['in'](...other); const wrapper = (...other) => pat[what][DEFAULT_ALIGNMENT](...other);
// add methods to that function for each behaviour // add methods to that function for each behaviour
for (const how of hows) { for (const how of ALIGNMENTS) {
wrapper[how.toLowerCase()] = function (...other) { wrapper[how.toLowerCase()] = function (...other) {
var howpat = pat; var howpat = pat;
other = sequence(other); other = sequence(other);
@ -1182,15 +1181,23 @@ function _composeOp(a, b, func) {
return wrapper; return wrapper;
}, },
}); });
// Default op to 'set', e.g. pat.squeeze(pat2) = pat.set.squeeze(pat2)
for (const how of hows) {
Pattern.prototype[how.toLowerCase()] = function (...args) {
return this.set[how.toLowerCase()](args);
};
}
} }
};
let DEFAULT_ALIGNMENT = 'in';
const ALIGNMENTS = ['In', 'Out', 'Mix', 'Squeeze', 'SqueezeOut', 'Reset', 'Restart', 'Poly'];
const ALIGNMENT_KEYS = ALIGNMENTS.map((how) => how.toLowerCase());
// Make composers
(function () {
_setupAlignments();
// Default op to 'set', e.g. pat.squeeze(pat2) = pat.set.squeeze(pat2)
for (const how of ALIGNMENTS) {
Pattern.prototype[how.toLowerCase()] = function (...args) {
return this.set[how.toLowerCase()](args);
};
}
// binary composers // binary composers
/** /**
* Applies the given structure to the pattern: * Applies the given structure to the pattern:
@ -1249,6 +1256,27 @@ function _composeOp(a, b, func) {
}; };
})(); })();
/**
* Sets the default method of combining events from two patterns (aka [alignment](https://strudel.cc/technical-manual/alignment/)) in Strudel.
* The default method is 'in', meaning that patterns to the left will (typically) dictate the event timings when combined with patterns to the right.
* By changing alignment to 'out', the opposite will happen. With 'mix', they will combine their event timings.
*
* Note that we say the _default_ method, because alignments can also be set explicitly with calls like
* 'add.mix', 'set.squeeze', etc.
*
* @param {string} method Default join method to use. Options: 'in', 'out', 'mix', 'squeeze', 'squeezeout', 'reset', 'restart', 'poly'
* @example
* setDefaultJoin('mix') // also try 'in', 'out', 'squeeze', etc.
* s("saw").vel("1 0.5").note("F A C E").delay("0 0.2 0.3")
*/
export const setDefaultJoin = (alignment) => {
alignment = alignment?.toLowerCase();
if (DEFAULT_ALIGNMENT !== alignment && ALIGNMENT_KEYS.includes(alignment)) {
DEFAULT_ALIGNMENT = alignment;
_setupAlignments();
}
};
// aliases // aliases
export const polyrhythm = stack; export const polyrhythm = stack;
export const pr = stack; export const pr = stack;

View file

@ -98,6 +98,7 @@ export const connectLFO = (id, params, nodeTracker) => {
fxi = 'main', fxi = 'main',
depth = 1, depth = 1,
depthabs, depthabs,
retrig = 0,
...filteredParams ...filteredParams
} = params; } = params;
const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker[fxi], subControl); const { targetParams, paramName } = getTargetParamsForControl(control, nodeTracker[fxi], subControl);
@ -109,7 +110,7 @@ export const connectLFO = (id, params, nodeTracker) => {
const modParams = { const modParams = {
...filteredParams, ...filteredParams,
frequency: sync !== undefined ? sync * cps : rate, frequency: sync !== undefined ? sync * cps : rate,
time: cycle / cps, time: retrig > 0.5 ? 0 : cycle / cps,
depth: depthValue, depth: depthValue,
min, min,
max, max,

View file

@ -10874,6 +10874,35 @@ exports[`runs examples > example "seqPLoop" example index 0 1`] = `
] ]
`; `;
exports[`runs examples > example "setDefaultJoin" example index 0 1`] = `
[
"[ 0/1 → 1/4 | s:saw velocity:1 note:F delay:0 ]",
"[ 1/4 → 1/3 | s:saw velocity:1 note:A delay:0 ]",
"[ 1/3 → 1/2 | s:saw velocity:1 note:A delay:0.2 ]",
"[ 1/2 → 2/3 | s:saw velocity:0.5 note:C delay:0.2 ]",
"[ 2/3 → 3/4 | s:saw velocity:0.5 note:C delay:0.3 ]",
"[ 3/4 → 1/1 | s:saw velocity:0.5 note:E delay:0.3 ]",
"[ 1/1 → 5/4 | s:saw velocity:1 note:F delay:0 ]",
"[ 5/4 → 4/3 | s:saw velocity:1 note:A delay:0 ]",
"[ 4/3 → 3/2 | s:saw velocity:1 note:A delay:0.2 ]",
"[ 3/2 → 5/3 | s:saw velocity:0.5 note:C delay:0.2 ]",
"[ 5/3 → 7/4 | s:saw velocity:0.5 note:C delay:0.3 ]",
"[ 7/4 → 2/1 | s:saw velocity:0.5 note:E delay:0.3 ]",
"[ 2/1 → 9/4 | s:saw velocity:1 note:F delay:0 ]",
"[ 9/4 → 7/3 | s:saw velocity:1 note:A delay:0 ]",
"[ 7/3 → 5/2 | s:saw velocity:1 note:A delay:0.2 ]",
"[ 5/2 → 8/3 | s:saw velocity:0.5 note:C delay:0.2 ]",
"[ 8/3 → 11/4 | s:saw velocity:0.5 note:C delay:0.3 ]",
"[ 11/4 → 3/1 | s:saw velocity:0.5 note:E delay:0.3 ]",
"[ 3/1 → 13/4 | s:saw velocity:1 note:F delay:0 ]",
"[ 13/4 → 10/3 | s:saw velocity:1 note:A delay:0 ]",
"[ 10/3 → 7/2 | s:saw velocity:1 note:A delay:0.2 ]",
"[ 7/2 → 11/3 | s:saw velocity:0.5 note:C delay:0.2 ]",
"[ 11/3 → 15/4 | s:saw velocity:0.5 note:C delay:0.3 ]",
"[ 15/4 → 4/1 | s:saw velocity:0.5 note:E delay:0.3 ]",
]
`;
exports[`runs examples > example "setGainCurve" example index 0 1`] = ` exports[`runs examples > example "setGainCurve" example index 0 1`] = `
[ [
"[ 0/1 → 1/4 | s:bd gain:0.5 ]", "[ 0/1 → 1/4 | s:bd gain:0.5 ]",

View file

@ -1,7 +1,9 @@
import { afterEach } from 'vitest'; import { afterEach } from 'vitest';
import { useRNG } from './packages/core/signal.mjs'; import { useRNG } from './packages/core/signal.mjs';
import { setDefaultJoin } from './packages/core/pattern.mjs';
afterEach(() => { afterEach(() => {
// Avoid bleed between tests // Avoid bleed between tests
useRNG('legacy'); useRNG('legacy');
setDefaultJoin('in');
}); });

View file

@ -33,6 +33,7 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
<!-- the following variables are just a fallback to make sure everything is readable without JS --> <!-- the following variables are just a fallback to make sure everything is readable without JS -->
<style is:global> <style is:global>
:root { :root {
color-scheme: dark;
--background: #222; --background: #222;
--lineBackground: #22222299; --lineBackground: #22222299;
--foreground: #fff; --foreground: #fff;

View file

@ -32,6 +32,7 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
<!-- the following variables are just a fallback to make sure everything is readable without JS --> <!-- the following variables are just a fallback to make sure everything is readable without JS -->
<style is:global> <style is:global>
:root { :root {
color-scheme: dark;
--background: #222; --background: #222;
--lineBackground: #22222299; --lineBackground: #22222299;
--foreground: #fff; --foreground: #fff;

View file

@ -1,5 +1,5 @@
import Loader from '@src/repl/components/Loader'; import Loader from '@src/repl/components/Loader';
import { HorizontalPanel } from '@src/repl/components/panel/Panel'; import { BottomPanel } from '@src/repl/components/panel/Panel';
import { Code } from '@src/repl/components/Code'; import { Code } from '@src/repl/components/Code';
import BigPlayButton from '@src/repl/components/BigPlayButton'; import BigPlayButton from '@src/repl/components/BigPlayButton';
import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage'; import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage';
@ -20,7 +20,7 @@ export default function UdelsEditor(Props) {
<Code containerRef={containerRef} editorRef={editorRef} init={init} /> <Code containerRef={containerRef} editorRef={editorRef} init={init} />
</div> </div>
<UserFacingErrorMessage error={error} /> <UserFacingErrorMessage error={error} />
<HorizontalPanel context={context} /> <BottomPanel context={context} />
</div> </div>
); );
} }

View file

@ -1,15 +1,3 @@
:root {
--background: #222;
--lineBackground: #22222299;
--foreground: #fff;
--caret: #ffcc00;
--selection: rgba(128, 203, 196, 0.5);
--selectionMatch: #036dd626;
--lineHighlight: #00000050;
--gutterBackground: transparent;
--gutterForeground: #8a919966;
}
.darken::before { .darken::before {
content: ' '; content: ' ';
position: fixed; position: fixed;

View file

@ -8,7 +8,7 @@ export function Code(Props) {
return ( return (
<section <section
className={'text-gray-100 cursor-text pb-0 overflow-auto grow'} className={'text-gray-100 cursor-text pb-0 overflow-auto grow z-10'}
id="code" id="code"
ref={(el) => { ref={(el) => {
containerRef.current = el; containerRef.current = el;

View file

@ -2,7 +2,7 @@ import Loader from '@src/repl/components/Loader';
import { Code } from '@src/repl/components/Code'; import { Code } from '@src/repl/components/Code';
import BigPlayButton from '@src/repl/components/BigPlayButton'; import BigPlayButton from '@src/repl/components/BigPlayButton';
import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage'; import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage';
import { Header } from './Header'; import { MainPanel } from './panel/Panel';
// type Props = { // type Props = {
// context: replcontext, // context: replcontext,
@ -14,7 +14,7 @@ export default function EmbeddedReplEditor(Props) {
return ( return (
<div className="h-full flex flex-col relative" {...editorProps}> <div className="h-full flex flex-col relative" {...editorProps}>
<Loader active={pending} /> <Loader active={pending} />
<Header context={context} embedded={true} /> <MainPanel context={context} embedded={true} />
<BigPlayButton started={started} handleTogglePlay={handleTogglePlay} /> <BigPlayButton started={started} handleTogglePlay={handleTogglePlay} />
<div className="grow flex relative overflow-hidden"> <div className="grow flex relative overflow-hidden">
<Code containerRef={containerRef} editorRef={editorRef} init={init} /> <Code containerRef={containerRef} editorRef={editorRef} init={init} />

View file

@ -1,150 +0,0 @@
import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon';
import StopCircleIcon from '@heroicons/react/20/solid/StopCircleIcon';
import cx from '@src/cx.mjs';
import { useSettings, setIsZen } from '../../settings.mjs';
import '../Repl.css';
const { BASE_URL } = import.meta.env;
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
export function Header({ context, embedded = false }) {
const { started, pending, isDirty, activeCode, handleTogglePlay, handleEvaluate, handleShuffle, handleShare } =
context;
const isEmbedded = typeof window !== 'undefined' && (embedded || window.location !== window.parent.location);
const { isZen, isButtonRowHidden, isCSSAnimationDisabled, fontFamily } = useSettings();
return (
<header
id="header"
className={cx(
'flex-none text-black z-[100] text-lg select-none h-20 md:h-14',
!isZen && !isEmbedded && 'bg-lineHighlight',
isZen ? 'h-12 w-8 fixed top-0 left-0' : 'sticky top-0 w-full py-1 justify-between',
isEmbedded ? 'flex' : 'md:flex',
)}
style={{ fontFamily }}
>
<div className="px-4 flex space-x-2 md:pt-0 select-none">
<h1
onClick={() => {
if (isEmbedded) window.open(window.location.href.replace('embed', ''));
}}
className={cx(
isEmbedded ? 'text-l cursor-pointer' : 'text-xl',
'text-foreground font-bold flex space-x-2 items-center',
)}
>
<div
className={cx(
'mt-[1px]',
started && !isCSSAnimationDisabled && 'animate-spin',
'cursor-pointer text-blue-500',
isZen && 'fixed top-2 right-4',
)}
onClick={() => {
if (!isEmbedded) {
setIsZen(!isZen);
}
}}
>
<span className="block text-foreground rotate-90"></span>
</div>
{!isZen && (
<div className="space-x-2">
<span className="">strudel</span>
<span className="text-sm font-medium">REPL</span>
{!isEmbedded && isButtonRowHidden && (
<a href={`${baseNoTrailing}/learn`} className="text-sm opacity-25 font-medium">
DOCS
</a>
)}
</div>
)}
</h1>
</div>
{!isZen && !isButtonRowHidden && (
<div className="flex max-w-full overflow-auto text-foreground px-1 md:px-2">
<button
onClick={handleTogglePlay}
title={started ? 'stop' : 'play'}
className={cx(
!isEmbedded ? 'p-2' : 'px-2',
'hover:opacity-50',
!started && !isCSSAnimationDisabled && 'animate-pulse',
)}
>
{!pending ? (
<span className={cx('flex items-center space-x-2')}>
{started ? <StopCircleIcon className="w-6 h-6" /> : <PlayCircleIcon className="w-6 h-6" />}
{!isEmbedded && <span>{started ? 'stop' : 'play'}</span>}
</span>
) : (
<>loading...</>
)}
</button>
<button
onClick={handleEvaluate}
title="update"
className={cx(
'flex items-center space-x-1',
!isEmbedded ? 'p-2' : 'px-2',
!isDirty || !activeCode ? 'opacity-50' : 'hover:opacity-50',
)}
>
{!isEmbedded && <span>update</span>}
</button>
{/* !isEmbedded && (
<button
title="shuffle"
className="hover:opacity-50 p-2 flex items-center space-x-1"
onClick={handleShuffle}
>
<span> shuffle</span>
</button>
) */}
{!isEmbedded && (
<button
title="share"
className={cx(
'cursor-pointer hover:opacity-50 flex items-center space-x-1',
!isEmbedded ? 'p-2' : 'px-2',
)}
onClick={handleShare}
>
<span>share</span>
</button>
)}
{!isEmbedded && (
<a
title="learn"
href={`${baseNoTrailing}/workshop/getting-started/`}
className={cx('hover:opacity-50 flex items-center space-x-1', !isEmbedded ? 'p-2' : 'px-2')}
>
<span>learn</span>
</a>
)}
{/* {isEmbedded && (
<button className={cx('hover:opacity-50 px-2')}>
<a href={window.location.href} target="_blank" rel="noopener noreferrer" title="Open in REPL">
🚀
</a>
</button>
)}
{isEmbedded && (
<button className={cx('hover:opacity-50 px-2')}>
<a
onClick={() => {
window.location.href = initialUrl;
window.location.reload();
}}
title="Reset"
>
💔
</a>
</button>
)} */}
</div>
)}
</header>
);
}

View file

@ -1,8 +1,7 @@
import Loader from '@src/repl/components/Loader';
import { HorizontalPanel, VerticalPanel } from '@src/repl/components/panel/Panel';
import { Code } from '@src/repl/components/Code'; import { Code } from '@src/repl/components/Code';
import Loader from '@src/repl/components/Loader';
import { BottomPanel, MainPanel, RightPanel } from '@src/repl/components/panel/Panel';
import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage'; import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage';
import { Header } from './Header';
import { useSettings } from '@src/settings.mjs'; import { useSettings } from '@src/settings.mjs';
// type Props = { // type Props = {
@ -14,17 +13,22 @@ export default function ReplEditor(Props) {
const { containerRef, editorRef, error, init, pending } = context; const { containerRef, editorRef, error, init, pending } = context;
const settings = useSettings(); const settings = useSettings();
const { panelPosition, isZen } = settings; const { panelPosition, isZen } = settings;
const isEmbedded = typeof window !== 'undefined' && window.location !== window.parent.location;
return ( return (
<div className="h-full flex flex-col relative" {...editorProps}> <div className="h-full flex flex-col relative" {...editorProps}>
<Loader active={pending} /> <Loader active={pending} />
<Header context={context} /> <div className="flex flex-col grow overflow-hidden">
<div className="grow flex relative overflow-hidden"> {/* <MainPanel context={context} isEmbedded={isEmbedded} className="hidden sm:block" /> */}
<Code containerRef={containerRef} editorRef={editorRef} init={init} /> <MainPanel context={context} isEmbedded={isEmbedded} />
{!isZen && panelPosition === 'right' && <VerticalPanel context={context} />} <div className="flex overflow-hidden h-full">
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
{!isZen && panelPosition === 'right' && <RightPanel context={context} />}
</div>
</div> </div>
<UserFacingErrorMessage error={error} /> <UserFacingErrorMessage error={error} />
{!isZen && panelPosition === 'bottom' && <HorizontalPanel context={context} />} {!isZen && panelPosition === 'bottom' && <BottomPanel context={context} />}
{/* <MainPanel context={context} isEmbedded={isEmbedded} className="block sm:hidden" /> */}
</div> </div>
); );
} }

View file

@ -2,7 +2,7 @@ import cx from '@src/cx.mjs';
export function ActionButton({ children, label, labelIsHidden, className, ...buttonProps }) { export function ActionButton({ children, label, labelIsHidden, className, ...buttonProps }) {
return ( return (
<button className={cx('hover:opacity-50 text-nowrap w-fit', className)} title={label} {...buttonProps}> <button className={cx('hover:opacity-50 text-xs text-nowrap w-fit', className)} title={label} {...buttonProps}>
{labelIsHidden !== true && label} {labelIsHidden !== true && label}
{children} {children}
</button> </button>
@ -13,16 +13,13 @@ export function SpecialActionButton(props) {
const { className, ...buttonProps } = props; const { className, ...buttonProps } = props;
return ( return (
<ActionButton <ActionButton {...buttonProps} className={cx('bg-background p-2 max-w-[300px] hover:opacity-50', className)} />
{...buttonProps}
className={cx('bg-background p-2 max-w-[300px] rounded-md hover:opacity-50', className)}
/>
); );
} }
export function ActionInput({ label, className, ...props }) { export function ActionInput({ label, className, ...props }) {
return ( return (
<label className={cx('inline-flex items-center cursor-pointer', className)}> <label className={cx('inline-flex items-center cursor-pointer ', className)}>
<input {...props} className="sr-only peer" /> <input {...props} className="sr-only peer" />
<span className="inline-flex items-center peer-hover:opacity-50">{label}</span> <span className="inline-flex items-center peer-hover:opacity-50">{label}</span>
@ -35,7 +32,7 @@ export function SpecialActionInput({ className, ...props }) {
<ActionInput <ActionInput
{...props} {...props}
className={className} className={className}
label={<span className="bg-background p-2 max-w-[300px] rounded-md">{props.label}</span>} label={<span className="bg-background p-2 max-w-[300px]">{props.label}</span>}
/> />
); );
} }

View file

@ -0,0 +1,19 @@
export function SidebarIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-panel-right-icon lucide-panel-right"
>
<rect width="18" height="18" x="3" y="3" rx="2" />
<path d="M15 3v18" />
</svg>
);
}

View file

@ -0,0 +1,13 @@
// Source: https://commons.wikimedia.org/wiki/File:CHAM_PUNCTUATION_SPIRAL.svg
// License: SIL Open Font License, Version 1.1. The license text is available at https://openfontlicense.org/open-font-license-official-text/
export function StrudelIcon({ className = '' }) {
return (
<svg className={className} width="100%" height="100%" viewBox="0 0 213 213">
<g>
<g id="_1" serif:id="1">
<path d="M199.905,102.467c-0,17.959 -3.695,33.45 -11.078,46.468c-7.369,13.024 -17.9,23.079 -31.586,30.173c-13.673,7.093 -29.994,10.64 -48.961,10.64c-19.964,0 -37.183,-3.874 -51.66,-11.617c-14.482,-7.749 -25.592,-18.775 -33.334,-33.103c-7.749,-14.322 -11.617,-31.407 -11.617,-51.248l17.143,-0c-0,16.95 3.187,31.503 9.561,43.666c6.373,12.15 15.485,21.48 27.346,27.989c11.874,6.509 26.061,9.766 42.561,9.766c15.485,0 28.798,-2.898 39.94,-8.687c11.154,-5.789 19.693,-14.11 25.624,-24.956c5.943,-10.858 8.918,-23.812 8.918,-38.86c-0,-20.85 -5.5,-37.202 -16.5,-49.063c-11,-11.874 -26.344,-17.811 -46.031,-17.811c-17.515,-0 -31.266,4.883 -41.251,14.649c-9.991,9.767 -14.983,23.337 -14.983,40.711c-0,15.781 4.015,28.13 12.054,37.035c8.031,8.893 19.07,13.339 33.103,13.339c12.452,0 22.186,-3.431 29.196,-10.306c7.023,-6.888 10.538,-16.333 10.538,-28.348c-0,-10.281 -2.57,-18.415 -7.711,-24.416c-5.14,-6.014 -12.413,-9.021 -21.82,-9.021c-7.813,-0 -13.93,2.178 -18.351,6.528c-4.407,4.337 -6.605,10.197 -6.605,17.579c0,1.44 0.064,2.815 0.206,4.112c4.485,-7.363 11.141,-11.051 19.97,-11.051c5.5,-0 9.875,1.561 13.133,4.678c3.27,3.103 4.909,7.324 4.909,12.67c-0,5.796 -1.966,10.506 -5.886,14.136c-3.906,3.617 -9.265,5.423 -16.063,5.423c-8.533,-0 -15.337,-2.814 -20.407,-8.456c-5.076,-5.654 -7.607,-13.114 -7.607,-22.386c-0,-11.135 3.431,-19.957 10.306,-26.472c6.888,-6.528 16.121,-9.792 27.706,-9.792c13.467,0 24.03,4.164 31.689,12.491c7.672,8.327 11.515,19.584 11.515,33.771c-0,16.057 -4.884,28.753 -14.65,38.089c-9.767,9.343 -23.195,14.008 -40.274,14.008c-12.594,-0 -23.491,-2.603 -32.692,-7.814c-9.188,-5.204 -16.243,-12.625 -21.178,-22.257c-4.921,-9.625 -7.376,-21.023 -7.376,-34.182c0,-14.342 2.93,-26.64 8.79,-36.907c5.873,-10.281 14.271,-18.203 25.187,-23.774c10.93,-5.584 23.928,-8.379 38.989,-8.379c16.359,0 30.469,3.258 42.33,9.767c11.874,6.515 20.985,15.819 27.346,27.911c6.374,12.08 9.561,26.518 9.561,43.307Zm-86.408,-6.939c-7.235,-0 -11.867,4.491 -13.904,13.467c2.756,3.033 6.528,4.549 11.308,4.549c7.659,0 11.489,-3.11 11.489,-9.329c-0,-2.757 -0.797,-4.89 -2.39,-6.4c-1.581,-1.523 -3.746,-2.287 -6.503,-2.287Z" />
</g>
</g>
</svg>
);
}

View file

@ -1,4 +1,4 @@
import { Textbox } from '../textbox/Textbox'; import { Textbox } from '@src/repl/components/panel/SettingsTab';
import cx from '@src/cx.mjs'; import cx from '@src/cx.mjs';
function IncButton({ children, className, ...buttonProps }) { function IncButton({ children, className, ...buttonProps }) {

View file

@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { SelectInput } from './SelectInput'; import { SelectInputDuplicate } from '@src/repl/components/panel/SettingsTab';
import { getAudioDevices } from '@strudel/webaudio'; import { getAudioDevices } from '@strudel/webaudio';
const initdevices = new Map(); const initdevices = new Map();
@ -29,7 +29,7 @@ export function AudioDeviceSelector({ audioDeviceName, onChange, isDisabled }) {
options.set(deviceName, deviceName); options.set(deviceName, deviceName);
}); });
return ( return (
<SelectInput <SelectInputDuplicate
isDisabled={isDisabled} isDisabled={isDisabled}
options={options} options={options}
onClick={onClick} onClick={onClick}

View file

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { audioEngineTargets } from '../../../settings.mjs'; import { audioEngineTargets } from '../../../settings.mjs';
import { SelectInput } from './SelectInput'; import { SelectInputDuplicate } from '@src/repl/components/panel/SettingsTab';
// Allows the user to select an audio interface for Strudel to play through // Allows the user to select an audio interface for Strudel to play through
export function AudioEngineTargetSelector({ target, onChange, isDisabled }) { export function AudioEngineTargetSelector({ target, onChange, isDisabled }) {
@ -12,8 +12,8 @@ export function AudioEngineTargetSelector({ target, onChange, isDisabled }) {
[audioEngineTargets.osc, audioEngineTargets.osc], [audioEngineTargets.osc, audioEngineTargets.osc],
]); ]);
return ( return (
<div className=" flex flex-col gap-1"> <div className="flex flex-col gap-1">
<SelectInput isDisabled={isDisabled} options={options} value={target} onChange={onTargetChange} /> <SelectInputDuplicate isDisabled={isDisabled} options={options} value={target} onChange={onTargetChange} />
{target === audioEngineTargets.osc && ( {target === audioEngineTargets.osc && (
<div> <div>
<p className="text-sm italic"> <p className="text-sm italic">

View file

@ -2,13 +2,23 @@ import cx from '@src/cx.mjs';
import { useSettings } from '../../../settings.mjs'; import { useSettings } from '../../../settings.mjs';
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import { $strudel_log_history } from '../useLogger'; import { $strudel_log_history } from '../useLogger';
import { useEffect, useRef } from 'react';
export function ConsoleTab() { export function ConsoleTab() {
const log = useStore($strudel_log_history); const log = useStore($strudel_log_history);
const { fontFamily } = useSettings(); const { fontFamily } = useSettings();
const scrollRef = useRef();
// scroll to bottom when log changes
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [log]);
return ( return (
<div id="console-tab" className="break-all w-full first-line:text-sm p-2 h-full" style={{ fontFamily }}> <div id="console-tab" className="break-all w-full h-full" style={{ fontFamily }}>
<div className="bg-background h-full w-full overflow-auto space-y-1 p-2 rounded-md"> <div className="h-full w-full overflow-auto space-y-1 p-2 rounded-md" ref={scrollRef}>
{' '}
{/* bg-background */}
{log.map((l, i) => { {log.map((l, i) => {
const message = linkify(l.message); const message = linkify(l.message);
const color = l.data?.hap?.value?.color; const color = l.data?.hap?.value?.color;
@ -16,12 +26,13 @@ export function ConsoleTab() {
<div <div
key={l.id} key={l.id}
className={cx( className={cx(
'whitespace-nowrap',
l.type === 'error' ? 'text-background bg-foreground' : 'text-foreground', l.type === 'error' ? 'text-background bg-foreground' : 'text-foreground',
l.type === 'highlight' && 'underline', l.type === 'highlight' && 'underline',
)} )}
style={color ? { color } : {}} style={color ? { color } : {}}
> >
<span dangerouslySetInnerHTML={{ __html: message }} /> <span dangerouslySetInnerHTML={{ __html: message }} className="whitespace-nowrap" />
{l.count ? ` (${l.count})` : ''} {l.count ? ` (${l.count})` : ''}
</div> </div>
); );

View file

@ -1,10 +1,7 @@
import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon';
import cx from '@src/cx.mjs'; import cx from '@src/cx.mjs';
import NumberInput from '@src/repl/components/NumberInput'; import { useState } from 'react';
import { useEffect, useState } from 'react'; import { Textbox } from '@src/repl/components/panel/SettingsTab';
import { Textbox } from '../textbox/Textbox';
import { getAudioContext } from '@strudel/webaudio'; import { getAudioContext } from '@strudel/webaudio';
import XMarkIcon from '@heroicons/react/24/outline/XMarkIcon';
function Checkbox({ label, value, onChange, disabled = false }) { function Checkbox({ label, value, onChange, disabled = false }) {
return ( return (
@ -48,7 +45,7 @@ export default function ExportTab(Props) {
return ( return (
<> <>
<div className="text-foreground w-full p-4 space-y-4"> <div className="text-foreground w-full space-y-4 p-4">
<FormItem label="File name" disabled={exporting}> <FormItem label="File name" disabled={exporting}>
<Textbox <Textbox
onBlur={(e) => { onBlur={(e) => {
@ -59,7 +56,7 @@ export default function ExportTab(Props) {
}} }}
disabled={exporting} disabled={exporting}
placeholder="Leave empty to use current date" placeholder="Leave empty to use current date"
className={cx('placeholder:opacity-50', exporting && 'opacity-50 border-opacity-50')} className={cx('placeholder-muted', exporting && 'opacity-50 border-opacity-50')}
value={downloadName ?? ''} value={downloadName ?? ''}
/> />
</FormItem> </FormItem>

View file

@ -1,15 +1,15 @@
import cx from '@src/cx.mjs'; import cx from '@src/cx.mjs';
export function ButtonGroup({ value, onChange, items }) { export function ButtonGroup({ value, onChange, items, wrap = false }) {
return ( return (
<div className="flex max-w-lg"> <div className={cx('flex max-w-lg space-x-0 text-xs', wrap && 'flex-wrap')}>
{Object.entries(items).map(([key, label], i, arr) => ( {Object.entries(items).map(([key, label], i, arr) => (
<button <button
key={key} key={key}
id={key} id={key}
onClick={() => onChange(key)} onClick={() => onChange(key)}
className={cx( className={cx(
'px-2 border-b h-8 whitespace-nowrap', 'px-2 border-b-2 h-8 whitespace-nowrap border-box max-h-8 hover:opacity-50',
// i === 0 && 'rounded-l-md', // i === 0 && 'rounded-l-md',
// i === arr.length - 1 && 'rounded-r-md', // i === arr.length - 1 && 'rounded-r-md',
// value === key ? 'bg-background' : 'bg-lineHighlight', // value === key ? 'bg-background' : 'bg-lineHighlight',

View file

@ -1,77 +1,204 @@
import { Bars3Icon, PlayIcon, StopIcon, XMarkIcon } from '@heroicons/react/16/solid';
import cx from '@src/cx.mjs'; import cx from '@src/cx.mjs';
import { setPanelPinned, setActiveFooter as setTab, setIsPanelOpened, useSettings } from '../../../settings.mjs'; import { StrudelIcon } from '@src/repl/components/icons/StrudelIcon';
import { useSettings, setIsZen, setIsPanelOpened, setActiveFooter as setTab } from '../../../settings.mjs';
import '../../Repl.css';
import { useLogger } from '../useLogger';
import { ConsoleTab } from './ConsoleTab'; import { ConsoleTab } from './ConsoleTab';
import ExportTab from './ExportTab';
import { FilesTab } from './FilesTab'; import { FilesTab } from './FilesTab';
import { PatternsTab } from './PatternsTab';
import { Reference } from './Reference'; import { Reference } from './Reference';
import { SettingsTab } from './SettingsTab'; import { SettingsTab } from './SettingsTab';
import { SoundsTab } from './SoundsTab'; import { SoundsTab } from './SoundsTab';
import { useLogger } from '../useLogger';
import { WelcomeTab } from './WelcomeTab'; import { WelcomeTab } from './WelcomeTab';
import { PatternsTab } from './PatternsTab';
import { ChevronLeftIcon, XMarkIcon } from '@heroicons/react/16/solid';
import ExportTab from './ExportTab';
const TAURI = typeof window !== 'undefined' && window.__TAURI__; const TAURI = typeof window !== 'undefined' && window.__TAURI__;
export function HorizontalPanel({ context }) { const { BASE_URL } = import.meta.env;
const settings = useSettings(); const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
const { isPanelOpen, activeFooter: tab } = settings;
export function LogoButton({ context, isEmbedded }) {
const { started } = context;
const { isZen, isCSSAnimationDisabled, fontFamily } = useSettings();
return (
<div
className={cx(
'mt-[1px]',
started && !isCSSAnimationDisabled && 'animate-spin',
'cursor-pointer text-blue-500',
isZen && 'fixed top-2 right-4',
)}
onClick={() => {
if (!isEmbedded) {
setIsZen(!isZen);
}
}}
>
<span className="block text-foreground rotate-90">
<StrudelIcon className="w-5 h-5 fill-foreground" />
</span>
</div>
);
}
export function MainPanel({ context, isEmbedded = false, className }) {
const { isZen, isButtonRowHidden, fontFamily } = useSettings();
return (
<nav
id="header"
className={cx(
'flex-none text-black z-[100] text-sm select-none min-h-10 max-h-10',
!isZen && !isEmbedded && 'border-b border-muted bg-lineHighlight',
isZen ? 'h-12 w-8 fixed top-0 left-0' : '',
'flex items-center',
className,
)}
style={{ fontFamily }}
>
<div className={cx('flex w-full justify-between')}>
<div className="px-3 py-1 flex space-x-2 select-none">
<h1
onClick={() => {
if (isEmbedded) window.open(window.location.href.replace('embed', ''));
}}
className={cx(
isEmbedded ? 'text-l cursor-pointer' : 'text-xl',
'text-foreground font-bold flex space-x-2 items-center',
)}
>
<LogoButton context={context} isEmbedded={isEmbedded} />
{!isZen && (
<div className="space-x-2 flex items-baseline">
<span className="hidden sm:block">strudel</span>
<span className="text-sm font-medium hidden sm:block">REPL</span>
</div>
)}
</h1>
</div>
{!isZen && (
<div className="flex grow justify-end">
{!isButtonRowHidden && <MainMenu isEmbedded={isEmbedded} context={context} />}
<PanelToggle isEmbedded={isEmbedded} isZen={isZen} />
</div>
)}
</div>
</nav>
);
}
export function Footer({ context, isEmbedded = false }) {
return (
<div className="border-t border-muted bg-lineHighlight block lg:hidden">
<MainMenu context={context} isEmbedded={isEmbedded} />
</div>
);
}
function MainMenu({ context, isEmbedded = false, className }) {
const { started, pending, isDirty, activeCode, handleTogglePlay, handleEvaluate, handleShare } = context;
const { isCSSAnimationDisabled } = useSettings();
return (
<div className={cx('flex text-sm max-w-full shrink-0 overflow-hidden text-foreground px-2 h-10', className)}>
<button
onClick={handleTogglePlay}
title={started ? 'stop' : 'play'}
className={cx('px-2 hover:opacity-50', !started && !isCSSAnimationDisabled && 'animate-pulse')}
>
<span className={cx('flex items-center space-x-2')}>
{started ? <StopIcon className="w-5 h-5" /> : <PlayIcon className="w-5 h-5" />}
{!isEmbedded && <span>{pending ? '...' : started ? 'stop' : 'play'}</span>}
</span>
</button>
<button
onClick={handleEvaluate}
title="update"
className={cx('flex items-center space-x-1 px-2', !isDirty || !activeCode ? 'opacity-50' : 'hover:opacity-50')}
>
{!isEmbedded && <span>update</span>}
</button>
{!isEmbedded && (
<button
title="share"
className={cx('cursor-pointer hover:opacity-50 flex items-center space-x-1 px-2')}
onClick={handleShare}
>
<span>share</span>
</button>
)}
{!isEmbedded && (
<a
title="learn"
href={`${baseNoTrailing}/workshop/getting-started/`}
className={cx('hover:opacity-50 flex items-center space-x-1', !isEmbedded ? 'p-2' : 'px-2')}
>
<span>learn</span>
</a>
)}
</div>
);
}
function PanelCloseButton() {
const { isPanelOpen } = useSettings();
return (
isPanelOpen && (
<button
onClick={() => setIsPanelOpened(false)}
className={cx('px-2 py-0 text-foreground hover:opacity-50')}
aria-label="Close Menu"
>
<XMarkIcon className="w-6 h-6" />
</button>
)
);
}
export function BottomPanel({ context }) {
const { isPanelOpen, activeFooter: tab } = useSettings();
return ( return (
<PanelNav <PanelNav
settings={settings} className={cx(
className={cx(isPanelOpen ? `min-h-[360px] max-h-[360px]` : 'min-h-12 max-h-12', 'overflow-hidden flex flex-col')} isPanelOpen ? `min-h-[360px] max-h-[360px]` : 'min-h-10 max-h-10',
'overflow-hidden flex flex-col relative',
)}
> >
<div className="flex justify-between min-h-10 max-h-10 grid-cols-2 items-center border-t border-muted">
<PanelCloseButton />
<Tabs setTab={setTab} tab={tab} className={cx(isPanelOpen && 'border-l border-muted')} />
</div>
{isPanelOpen && ( {isPanelOpen && (
<div className="flex h-full overflow-auto pr-10 "> <div className="w-full h-full overflow-auto border-t border-muted">
<PanelContent context={context} tab={tab} /> <PanelContent context={context} tab={tab} />
</div> </div>
)} )}
<div className="absolute right-4 pt-4">
<PanelActionButton settings={settings} />
</div>
<div className="flex justify-between min-h-12 max-h-12 grid-cols-2 items-center">
<Tabs setTab={setTab} tab={tab} />
</div>
</PanelNav> </PanelNav>
); );
} }
export function VerticalPanel({ context }) { export function RightPanel({ context }) {
const settings = useSettings(); const settings = useSettings();
const { activeFooter: tab, isPanelOpen } = settings; const { activeFooter: tab, isPanelOpen } = settings;
if (!isPanelOpen) {
return;
}
return ( return (
<PanelNav <PanelNav
settings={settings} settings={settings}
className={cx(isPanelOpen ? `min-w-[min(600px,80vw)] max-w-[min(600px,80vw)]` : 'min-w-12 max-w-12')} className={cx(
> 'border-l border-muted shrink-0 h-full overflow-hidden',
{isPanelOpen ? ( isPanelOpen ? `min-w-[min(600px,100vw)] max-w-[min(600px,80vw)]` : 'min-w-12 max-w-12',
<div className={cx('flex flex-col h-full')}>
<div className="flex justify-between w-full ">
<Tabs setTab={setTab} tab={tab} />
<PanelActionButton settings={settings} />
</div>
<div className="overflow-auto h-full">
<PanelContent context={context} tab={tab} />
</div>
</div>
) : (
<button
onClick={(e) => {
setIsPanelOpened(true);
}}
aria-label="open menu panel"
className={cx(
'flex flex-col hover:bg-lineBackground items-center cursor-pointer justify-center w-full h-full',
)}
>
<ChevronLeftIcon className="text-foreground opacity-50 w-6 h-6" />
</button>
)} )}
>
<div className={cx('flex flex-col h-full')}>
<div className="flex justify-between w-full overflow-hidden border-b border-muted min-h-10 max-h-10">
<PanelCloseButton />
<Tabs setTab={setTab} tab={tab} className="border-l border-muted" />
</div>
<div className="overflow-auto h-full">
<PanelContent context={context} tab={tab} />
</div>
</div>
</PanelNav> </PanelNav>
); );
} }
@ -89,8 +216,8 @@ if (TAURI) {
tabNames.files = 'files'; tabNames.files = 'files';
} }
function PanelNav({ children, className, settings, ...props }) { function PanelNav({ children, className, ...props }) {
const isHoverBehavior = settings.togglePanelTrigger === 'hover'; const settings = useSettings();
return ( return (
<nav <nav
onClick={() => { onClick={() => {
@ -98,18 +225,8 @@ function PanelNav({ children, className, settings, ...props }) {
setIsPanelOpened(true); setIsPanelOpened(true);
} }
}} }}
onMouseEnter={() => {
if (isHoverBehavior && !settings.isPanelOpen) {
setIsPanelOpened(true);
}
}}
onMouseLeave={() => {
if (isHoverBehavior && !settings.isPanelPinned) {
setIsPanelOpened(false);
}
}}
aria-label="Menu Panel" aria-label="Menu Panel"
className={cx('bg-lineHighlight group overflow-x-auto', className)} className={cx('h-full bg-lineHighlight group overflow-x-auto', className)}
{...props} {...props}
> >
{children} {children}
@ -145,7 +262,7 @@ function PanelTab({ label, isSelected, onClick }) {
<button <button
onClick={onClick} onClick={onClick}
className={cx( className={cx(
'h-8 px-2 text-foreground cursor-pointer hover:opacity-50 flex items-center space-x-1 border-b', 'h-10 px-2 text-sm border-t-2 border-t-transparent text-foreground cursor-pointer hover:opacity-50 flex items-center space-x-1 border-b-2',
isSelected ? 'border-foreground' : 'border-transparent', isSelected ? 'border-foreground' : 'border-transparent',
)} )}
> >
@ -154,65 +271,36 @@ function PanelTab({ label, isSelected, onClick }) {
</> </>
); );
} }
function Tabs({ setTab, tab, className }) { function Tabs({ className }) {
const { isPanelOpen, activeFooter: tab } = useSettings();
return ( return (
<div className={cx('flex select-none max-w-full overflow-auto pb-2', className)}> <div
className={cx(
'px-2 w-full flex select-none max-w-full h-10 max-h-10 min-h-10 overflow-auto items-center',
className,
)}
>
{Object.keys(tabNames).map((key) => { {Object.keys(tabNames).map((key) => {
const val = tabNames[key]; const val = tabNames[key];
return <PanelTab key={key} isSelected={tab === val} label={key} onClick={() => setTab(val)} />; return <PanelTab key={key} isSelected={tab === val && isPanelOpen} label={key} onClick={() => setTab(val)} />;
})} })}
</div> </div>
); );
} }
function PanelActionButton({ settings }) { export function PanelToggle({ isEmbedded, isZen }) {
const { togglePanelTrigger, isPanelPinned, isPanelOpen } = settings; const { panelPosition, isPanelOpen } = useSettings();
const isHoverBehavior = togglePanelTrigger === 'hover';
if (!isPanelOpen) {
return;
}
if (isHoverBehavior) {
return <PinButton pinned={isPanelPinned} />;
}
return <CloseButton onClick={() => setIsPanelOpened(false)} />;
}
function PinButton({ pinned }) {
return ( return (
<button !isEmbedded &&
onClick={() => setPanelPinned(!pinned)} !isZen &&
className={cx( panelPosition === 'right' && (
'text-foreground max-h-8 min-h-8 max-w-8 min-w-8 items-center justify-center p-1.5 group-hover:flex', <button
pinned ? 'flex' : 'hidden', title="menu"
)} className={cx('border-l border-muted px-2 py-0 text-foreground hover:opacity-50')}
aria-label="Pin Menu Panel" onClick={() => setIsPanelOpened(!isPanelOpen)}
>
<svg
stroke="currentColor"
fill={'currentColor'}
strokeWidth="0"
className="w-full h-full"
opacity={pinned ? 1 : '.3'}
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
> >
<path d="M9.828.722a.5.5 0 0 1 .354.146l4.95 4.95a.5.5 0 0 1 0 .707c-.48.48-1.072.588-1.503.588-.177 0-.335-.018-.46-.039l-3.134 3.134a6 6 0 0 1 .16 1.013c.046.702-.032 1.687-.72 2.375a.5.5 0 0 1-.707 0l-2.829-2.828-3.182 3.182c-.195.195-1.219.902-1.414.707s.512-1.22.707-1.414l3.182-3.182-2.828-2.829a.5.5 0 0 1 0-.707c.688-.688 1.673-.767 2.375-.72a6 6 0 0 1 1.013.16l3.134-3.133a3 3 0 0 1-.04-.461c0-.43.108-1.022.589-1.503a.5.5 0 0 1 .353-.146"></path> <Bars3Icon className="w-6 h-6" />
</svg> </button>
</button> )
);
}
function CloseButton({ onClick }) {
return (
<button
onClick={onClick}
className={cx(
'text-foreground max-h-8 min-h-8 max-w-8 min-w-8 items-center justify-center p-1.5 group-hover:flex',
)}
aria-label="Close Menu"
>
<XMarkIcon />
</button>
); );
} }

View file

@ -8,7 +8,7 @@ import {
useViewingPatternData, useViewingPatternData,
userPattern, userPattern,
} from '../../../user_pattern_utils.mjs'; } from '../../../user_pattern_utils.mjs';
import { useMemo } from 'react'; import { useMemo, useRef } from 'react';
import { getMetadata } from '../../../metadata_parser.js'; import { getMetadata } from '../../../metadata_parser.js';
import { useExamplePatterns } from '../../useExamplePatterns.jsx'; import { useExamplePatterns } from '../../useExamplePatterns.jsx';
import { parseJSON, isUdels } from '../../util.mjs'; import { parseJSON, isUdels } from '../../util.mjs';
@ -18,7 +18,7 @@ import { Pagination } from '../pagination/Pagination.jsx';
import { useState } from 'react'; import { useState } from 'react';
import { useDebounce } from '../usedebounce.jsx'; import { useDebounce } from '../usedebounce.jsx';
import cx from '@src/cx.mjs'; import cx from '@src/cx.mjs';
import { Textbox } from '../textbox/Textbox.jsx'; import { Textbox } from '@src/repl/components/panel/SettingsTab.jsx';
export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) { export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
const meta = useMemo(() => getMetadata(pattern.code), [pattern]); const meta = useMemo(() => getMetadata(pattern.code), [pattern]);
@ -43,7 +43,7 @@ function PatternButton({ showOutline, onClick, pattern, showHiglight }) {
className={cx( className={cx(
'mr-4 hover:opacity-50 cursor-pointer block', 'mr-4 hover:opacity-50 cursor-pointer block',
showOutline && 'outline outline-1', showOutline && 'outline outline-1',
showHiglight && 'bg-selection', showHiglight && 'ring-selection',
)} )}
onClick={onClick} onClick={onClick}
> >
@ -53,11 +53,10 @@ function PatternButton({ showOutline, onClick, pattern, showHiglight }) {
} }
function PatternButtons({ patterns, activePattern, onClick, started }) { function PatternButtons({ patterns, activePattern, onClick, started }) {
const viewingPatternStore = useViewingPatternData(); const viewingPatternData = useViewingPatternData();
const viewingPatternData = parseJSON(viewingPatternStore);
const viewingPatternID = viewingPatternData.id; const viewingPatternID = viewingPatternData.id;
return ( return (
<div className=""> <div className="p-2">
{Object.values(patterns) {Object.values(patterns)
.reverse() .reverse()
.map((pattern) => { .map((pattern) => {
@ -83,8 +82,8 @@ const updateCodeWindow = (context, patternData, reset = false) => {
export function PatternsTab({ context }) { export function PatternsTab({ context }) {
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const activePattern = useActivePattern(); const activePattern = useActivePattern();
const viewingPatternStore = useViewingPatternData(); const viewingPatternData = useViewingPatternData();
const viewingPatternData = parseJSON(viewingPatternStore);
const { userPatterns, patternAutoStart } = useSettings(); const { userPatterns, patternAutoStart } = useSettings();
const viewingPatternID = viewingPatternData?.id; const viewingPatternID = viewingPatternData?.id;
@ -121,74 +120,71 @@ export function PatternsTab({ context }) {
); );
}), }),
); );
}, [search, viewingPatternStore]); }, [search, userPatterns]);
const importRef = useRef();
return ( return (
<div className="px-4 w-full text-foreground space-y-2 flex flex-col overflow-hidden max-h-full h-full"> <div className="w-full h-full text-foreground flex flex-col overflow-hidden">
<div className="w-full flex"> <Textbox className="w-full border-0" placeholder="Search..." value={search} onChange={setSearch} />
<Textbox className="w-full" placeholder="Search" value={search} onChange={setSearch} /> <div className="px-2 shrink-0 h-8 space-x-4 flex max-w-full overflow-x-auto border-y border-muted">
<ActionButton
label="new"
onClick={() => {
const { data } = userPattern.createAndAddToDB();
updateCodeWindow(context, data);
}}
/>
<ActionButton
label="duplicate"
onClick={() => {
const { data } = userPattern.duplicate(viewingPatternData);
updateCodeWindow(context, data);
}}
/>
<ActionButton
label="delete"
onClick={() => {
const { data } = userPattern.delete(viewingPatternID);
updateCodeWindow(context, { ...data, collection: userPattern.collection });
}}
/>
<input
ref={importRef}
style={{ display: 'none' }}
type="file"
multiple
accept="text/plain,text/x-markdown,application/json"
onChange={(e) => importPatterns(e.target.files)}
/>
<ActionButton label="import" onClick={() => importRef.current.click()} />
<ActionButton label="export" onClick={exportPatterns} />
<ActionButton
label="delete-all"
onClick={() => {
const { data } = userPattern.clearAll();
updateCodeWindow(context, data);
}}
/>
</div> </div>
<div className="flex flex-col gap-2 flex-grow overflow-hidden h-full pb-2 ">
<div className="pr-4 space-x-4 flex max-w-full overflow-x-auto">
<ActionButton
label="new"
onClick={() => {
const { data } = userPattern.createAndAddToDB();
updateCodeWindow(context, data);
}}
/>
<ActionButton
label="duplicate"
onClick={() => {
const { data } = userPattern.duplicate(viewingPatternData);
updateCodeWindow(context, data);
}}
/>
<ActionButton
label="delete"
onClick={() => {
const { data } = userPattern.delete(viewingPatternID);
updateCodeWindow(context, { ...data, collection: userPattern.collection });
}}
/>
<label className="hover:opacity-50 cursor-pointer">
<input
style={{ display: 'none' }}
type="file"
multiple
accept="text/plain,text/x-markdown,application/json"
onChange={(e) => importPatterns(e.target.files)}
/>
import
</label>
<ActionButton label="export" onClick={exportPatterns} />
<ActionButton <div className="overflow-auto">
label="delete-all" {/* bg-background */}
onClick={() => { {/* {patternFilter === patternFilterName.user && ( */}
const { data } = userPattern.clearAll(); <PatternButtons
updateCodeWindow(context, data); onClick={(id) => {
}} updateCodeWindow(context, { ...userPatterns[id], collection: userPattern.collection }, patternAutoStart);
/>
</div>
<div className="overflow-auto h-full bg-background p-2 rounded-md"> if (context.started && activePattern === id) {
{/* {patternFilter === patternFilterName.user && ( */} context.handleEvaluate();
<PatternButtons }
onClick={(id) => { }}
updateCodeWindow(context, { ...userPatterns[id], collection: userPattern.collection }, patternAutoStart); patterns={visiblePatterns}
started={context.started}
if (context.started && activePattern === id) { activePattern={activePattern}
context.handleEvaluate(); viewingPatternID={viewingPatternID}
} />
}} {/* )} */}
patterns={visiblePatterns}
started={context.started}
activePattern={activePattern}
viewingPatternID={viewingPatternID}
/>
{/* )} */}
</div>
</div> </div>
</div> </div>
); );

View file

@ -1,7 +1,8 @@
import { memo, useEffect, useMemo, useState, Fragment } from 'react'; import { memo, useEffect, useMemo, useState, Fragment } from 'react';
import jsdocJson from '../../../../../doc.json'; import jsdocJson from '../../../../../doc.json';
import { Textbox } from '../textbox/Textbox'; import { Textbox } from '@src/repl/components/panel/SettingsTab';
import { settingsMap, useSettings } from '@src/settings.mjs';
const isValid = ({ name, description }) => name && !name.startsWith('_') && !!description; const isValid = ({ name, description }) => name && !name.startsWith('_') && !!description;
@ -37,6 +38,19 @@ const availableFunctions = (() => {
return functions.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name)); return functions.sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name));
})(); })();
const tagCounts = {};
// const tagOptions = { all: `all (${availableFunctions.length})` };
const tagOptions = { all: `all` };
for (const doc of availableFunctions) {
(doc.tags || ['untagged']).forEach((t) => {
if (typeof t === 'string' && t) {
tagCounts[t] = (tagCounts[t] || 0) + 1;
//tagOptions[t] = `${t} (${tagCounts[t]})`;
tagOptions[t] = t;
}
});
}
const getInnerText = (html) => { const getInnerText = (html) => {
var div = document.createElement('div'); var div = document.createElement('div');
div.innerHTML = html; div.innerHTML = html;
@ -45,40 +59,38 @@ const getInnerText = (html) => {
export const Reference = memo(function Reference() { export const Reference = memo(function Reference() {
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [selectedTag, setSelectedTag] = useState(null);
const [selectedFunction, setSelectedFunction] = useState(null); const [selectedFunction, setSelectedFunction] = useState(null);
const { referenceTag } = useSettings();
const toggleTag = (tag) => { const toggleTag = (tag) => {
if (selectedTag === tag) { if (referenceTag === tag) {
setSelectedTag(null); setReferenceTag('all');
} else { } else {
setSelectedTag(tag); setReferenceTag(tag);
} }
}; };
const searchVisibleFunctions = useMemo(() => { const searchVisibleFunctions = useMemo(() => {
return availableFunctions.filter((entry) => { return availableFunctions.filter((entry) => {
if (selectedTag) { if (referenceTag && referenceTag !== 'all') {
if (!(entry.tags || ['untagged']).includes(selectedTag)) { if (!(entry.tags || ['untagged']).includes(referenceTag)) {
return false; return false;
} }
} }
if (!search) { if (!search) {
return true; return true;
} }
const lowerCaseSearch = search.toLowerCase(); const lowerCaseSearch = search.toLowerCase();
return ( return (
entry.name.toLowerCase().includes(lowerCaseSearch) || entry.name.toLowerCase().includes(lowerCaseSearch) ||
(entry.synonyms?.some((s) => s.toLowerCase().includes(lowerCaseSearch)) ?? false) (entry.synonyms?.some((s) => s.toLowerCase().includes(lowerCaseSearch)) ?? false)
); );
}); });
}, [search, selectedTag]); }, [search, referenceTag]);
const detailVisibleFunctions = useMemo(() => { const detailVisibleFunctions = useMemo(() => {
return searchVisibleFunctions.filter((x) => { return searchVisibleFunctions.filter((x) => {
if (selectedTag === null) { if (referenceTag === null || referenceTag === 'all') {
if (search) { if (search) {
return true; return true;
} }
@ -87,19 +99,10 @@ export const Reference = memo(function Reference() {
return true; return true;
} }
}); });
}, [searchVisibleFunctions, selectedFunction, selectedTag]); }, [searchVisibleFunctions, selectedFunction, referenceTag]);
const tagCounts = {};
for (const doc of availableFunctions) {
(doc.tags || ['untagged']).forEach((t) => {
if (typeof t === 'string' && t) {
tagCounts[t] = (tagCounts[t] || 0) + 1;
}
});
}
const onSearchTagFilterClick = () => { const onSearchTagFilterClick = () => {
setSelectedTag(null); setReferenceTag('all');
setSelectedFunction(null); setSelectedFunction(null);
}; };
@ -111,36 +114,44 @@ export const Reference = memo(function Reference() {
} }
}, [selectedFunction]); }, [selectedFunction]);
let setReferenceTag = (value) => settingsMap.setKey('referenceTag', value);
return ( return (
<div className="flex h-full w-full p-2 overflow-hidden"> <div className="flex h-full w-full overflow-hidden">
<div className="h-full text-foreground flex flex-col gap-3 w-1/3 "> <div className="h-full text-foreground flex flex-col w-1/3 border-r border-muted">
{/* bg-background */}
<div className="w-full flex"> <div className="w-full flex">
<Textbox <Textbox
className="w-full" className="w-full border-0 border-b border-muted"
placeholder="Search" placeholder="Search..."
value={search} value={search}
onChange={(e) => { onChange={(e) => {
setSelectedFunction(null); setSelectedFunction(null);
setReferenceTag('all');
setSearch(e); setSearch(e);
}} }}
/> />
</div> </div>
{selectedTag && (
{/* <div className="flex shrink-0 flex-wrap w-full overflow-auto border-y border-muted">
<ButtonGroup wrap value={referenceTag} onChange={setReferenceTag} items={tagOptions}></ButtonGroup>
</div> */}
{referenceTag && referenceTag !== 'all' && (
<div className="w-72"> <div className="w-72">
<span <span
className="text-foreground border-2 border-gray-500 px-1 py-0.5 my-2 rounded-md cursor-pointer font-sans" className="text-foreground border border-muted border-t-0 border-l-0 px-1 py-0.5 my-2 cursor-pointer font-sans"
onClick={onSearchTagFilterClick} onClick={onSearchTagFilterClick}
> >
{selectedTag} {referenceTag}
</span> </span>
</div> </div>
)} )}
<div className="flex flex-col h-full overflow-y-auto gap-1.5 bg-background bg-opacity-50 rounded-md"> <div className="h-full p-2 overflow-y-auto bg-opacity-50">
{searchVisibleFunctions.map((entry, i) => ( {searchVisibleFunctions.map((entry, i) => (
<Fragment key={`entry-${entry.name}`}> <Fragment key={`entry-${entry.name}`}>
<a <a
className={ className={
'cursor-pointer flex-none hover:bg-lineHighlight overflow-x-hidden px-1 text-ellipsis ' + 'cursor-pointer hover:opacity-50 text-ellipsis block' +
(entry.name === selectedFunction ? 'bg-lineHighlight font-bold' : '') (entry.name === selectedFunction ? 'bg-lineHighlight font-bold' : '')
} }
onClick={() => { onClick={() => {
@ -158,24 +169,25 @@ export const Reference = memo(function Reference() {
</div> </div>
</div> </div>
<div <div
className="break-normal flex-col overflow-y-auto overflow-x-hidden p-2 flex relative" className="w-2/3 break-normal flex-col overflow-y-auto overflow-x-hidden p-2 flex relative"
id="reference-container" id="reference-container"
> >
<div className="prose dark:prose-invert min-w-full px-1 "> <div className="prose dark:prose-invert min-w-full px-1 text-sm">
<h2>API Reference</h2> <h2>API Reference</h2>
<p className="font-sans text-md"> <p className="font-sans text-md">
This is the long list of functions you can use. Remember that you don't need to remember all of those and This is the long list of functions you can use. Remember that you don't need to remember all of those and
that you can already make music with a small set of functions! that you can already make music with a small set of functions!
</p> </p>
<div> <div>
{/* <ButtonGroup wrap value={referenceTag} onChange={setReferenceTag} items={tagOptions}/>*/}
{Object.entries(tagCounts) {Object.entries(tagCounts)
.sort(([a], [b]) => a.localeCompare(b)) .sort(([a], [b]) => a.localeCompare(b))
.map(([t, count]) => ( .map(([t, count]) => (
<span key={t}> <span key={t}>
<a <a
className={[ className={[
'select-none text-white border-2 border-gray-500 px-1 py-0.5 my-2 cursor-pointer text-sm/8 rounded-md no-underline font-sans', 'select-none text-white border border-muted px-1 py-0.5 my-2 cursor-pointer text-sm/8 no-underline font-sans',
`${selectedTag === t ? 'bg-gray-500 text-black' : ''}`, `${referenceTag === t ? 'bg-muted text-foreground' : ''}`,
].join(' ')} ].join(' ')}
onClick={() => toggleTag(t)} onClick={() => toggleTag(t)}
> >
@ -187,11 +199,11 @@ export const Reference = memo(function Reference() {
{detailVisibleFunctions.map((entry, i) => ( {detailVisibleFunctions.map((entry, i) => (
<section key={i} className="font-sans"> <section key={i} className="font-sans">
<div className="flex flex-row items-center mt-8 justify-between"> <div className="flex flex-row items-center mt-8 justify-between">
<h3 className="font-mono my-0" id={`doc-${entry.name}`}> <h3 className="font-mono my-0 pt-4" id={`doc-${entry.name}`}>
{entry.name} {entry.name}
</h3> </h3>
{entry.tags && ( {entry.tags && (
<span className="ml-2 text-xs text-gray-400 border-2 border-gray-500 rounded-md px-1 py-0.5"> <span className="ml-2 text-xs text-foreground border border-muted px-1 py-0.5">
{entry.tags.join(', ')} {entry.tags.join(', ')}
</span> </span>
)} )}
@ -201,7 +213,7 @@ export const Reference = memo(function Reference() {
Synonyms: <code>{entry.synonyms_text}</code> Synonyms: <code>{entry.synonyms_text}</code>
</p> </p>
)} )}
{/* <small>{entry.meta.filename}</small> */}
<p dangerouslySetInnerHTML={{ __html: entry.description }}></p> <p dangerouslySetInnerHTML={{ __html: entry.description }}></p>
<ul> <ul>
{entry.params?.map(({ name, type, description }, i) => ( {entry.params?.map(({ name, type, description }, i) => (
@ -211,12 +223,12 @@ export const Reference = memo(function Reference() {
))} ))}
</ul> </ul>
{entry.examples?.map((example, j) => ( {entry.examples?.map((example, j) => (
<pre className="bg-background" key={j}> <pre className="bg-background border border-muted" key={j}>
{example} {example}
</pre> </pre>
))} ))}
</section> </section>
)) || <p className="font-sans">Searcb or select a tag to get started.</p>} )) || <p className="font-sans">Search or select a tag to get started.</p>}
{detailVisibleFunctions.length > 0 && <div className="h-screen" />} {detailVisibleFunctions.length > 0 && <div className="h-screen" />}
</div> </div>
</div> </div>

View file

@ -1,20 +0,0 @@
import React from 'react';
// value: ?ID, options: Map<ID, any>, onChange: ID => null, onClick: event => void, isDisabled: boolean
export function SelectInput({ value, options, onChange, onClick, isDisabled }) {
return (
<select
disabled={isDisabled}
onClick={onClick}
className="p-2 bg-background rounded-md text-foreground border-foreground"
value={value ?? ''}
onChange={(e) => onChange(e.target.value)}
>
{options.size == 0 && <option value={value}>{`${value ?? 'select an option'}`}</option>}
{Array.from(options.keys()).map((id) => (
<option key={id} className="bg-background" value={id}>
{options.get(id)}
</option>
))}
</select>
);
}

View file

@ -1,6 +1,5 @@
import { defaultSettings, settingsMap, useSettings } from '../../../settings.mjs'; import { defaultSettings, settingsMap, useSettings } from '../../../settings.mjs';
import { themes } from '@strudel/codemirror'; import { themes } from '@strudel/codemirror';
import { Textbox } from '../textbox/Textbox.jsx';
import { confirmAndReloadPage, isUdels } from '../../util.mjs'; import { confirmAndReloadPage, isUdels } from '../../util.mjs';
import { ButtonGroup } from './Forms.jsx'; import { ButtonGroup } from './Forms.jsx';
import { AudioDeviceSelector } from './AudioDeviceSelector.jsx'; import { AudioDeviceSelector } from './AudioDeviceSelector.jsx';
@ -10,22 +9,60 @@ import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony, setMultiChannelOrbits } from '@
import { SpecialActionButton } from '../button/action-button.jsx'; import { SpecialActionButton } from '../button/action-button.jsx';
import { ImportPrebakeScriptButton } from './ImportPrebakeScriptButton.jsx'; import { ImportPrebakeScriptButton } from './ImportPrebakeScriptButton.jsx';
function cx(...classes) {
// : Array<string | undefined>
return classes.filter(Boolean).join(' ');
}
const inputClass =
'bg-background text-xs h-8 max-h-8 border border-box rounded-0 text-foreground border-muted placeholder-muted focus:outline-none focus:ring-0 focus:border-foreground';
export function Textbox({ onChange, className, ...inputProps }) {
return (
<input className={cx('px-2', inputClass, className)} onChange={(e) => onChange(e.target.value)} {...inputProps} />
);
}
function Checkbox({ label, value, onChange, disabled = false }) { function Checkbox({ label, value, onChange, disabled = false }) {
return ( return (
<label> <label className="text-xs">
<input disabled={disabled} type="checkbox" checked={value} onChange={onChange} /> <input
className={cx(
'bg-background text-sm border border-muted focus:outline-none focus:ring-0 focus:border-foreground',
)}
disabled={disabled}
type="checkbox"
checked={value}
onChange={onChange}
/>
{' ' + label} {' ' + label}
</label> </label>
); );
} }
function SelectInput({ value, options, onChange }) { // value: ?ID, options: Map<ID, any>, onChange: ID => null, onClick: event => void, isDisabled: boolean
export function SelectInputDuplicate({ value, options, onChange, onClick, isDisabled }) {
return ( return (
<select <select
className="p-2 bg-background rounded-md text-foreground border-foreground" disabled={isDisabled}
value={value} onClick={onClick}
className={cx('p-2', inputClass)}
value={value ?? ''}
onChange={(e) => onChange(e.target.value)} onChange={(e) => onChange(e.target.value)}
> >
{options.size == 0 && <option value={value}>{`${value ?? 'select an option'}`}</option>}
{Array.from(options.keys()).map((id) => (
<option key={id} className="bg-background" value={id}>
{options.get(id)}
</option>
))}
</select>
);
}
function SelectInput({ value, options, onChange }) {
return (
<select className={cx('p-2', inputClass)} value={value} onChange={(e) => onChange(e.target.value)}>
{Object.entries(options).map(([k, label]) => ( {Object.entries(options).map(([k, label]) => (
<option key={k} className="bg-background" value={k}> <option key={k} className="bg-background" value={k}>
{label} {label}
@ -37,9 +74,9 @@ function SelectInput({ value, options, onChange }) {
function NumberSlider({ value, onChange, step = 1, ...rest }) { function NumberSlider({ value, onChange, step = 1, ...rest }) {
return ( return (
<div className="flex space-x-2 gap-1"> <div className="flex space-x-2 gap-1 overflow-hidden">
<input <input
className="p-2 grow" className="p-2 grow accent-foreground"
type="range" type="range"
value={value} value={value}
step={step} step={step}
@ -50,7 +87,7 @@ function NumberSlider({ value, onChange, step = 1, ...rest }) {
type="number" type="number"
value={value} value={value}
step={step} step={step}
className="w-16 bg-background rounded-md" className={cx('w-16', inputClass)}
onChange={(e) => onChange(Number(e.target.value))} onChange={(e) => onChange(Number(e.target.value))}
/> />
</div> </div>
@ -59,8 +96,8 @@ function NumberSlider({ value, onChange, step = 1, ...rest }) {
function FormItem({ label, children, sublabel }) { function FormItem({ label, children, sublabel }) {
return ( return (
<div className="grid gap-2"> <div className="grid gap-2 text-xs">
<label>{label}</label> <label className="text-sm">{label}</label>
{children} {children}
</div> </div>
); );
@ -107,7 +144,6 @@ export function SettingsTab({ started }) {
panelPosition, panelPosition,
audioDeviceName, audioDeviceName,
audioEngineTarget, audioEngineTarget,
togglePanelTrigger,
maxPolyphony, maxPolyphony,
multiChannelOrbits, multiChannelOrbits,
isTabIndentationEnabled, isTabIndentationEnabled,
@ -118,7 +154,7 @@ export function SettingsTab({ started }) {
const shouldAlwaysSync = isUdels(); const shouldAlwaysSync = isUdels();
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null; const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
return ( return (
<div className="text-foreground p-4 space-y-4 w-full" style={{ fontFamily }}> <div className="p-4 text-foreground space-y-4 w-full" style={{ fontFamily }}>
{canChangeAudioDevice && ( {canChangeAudioDevice && (
<FormItem label="Audio Output Device"> <FormItem label="Audio Output Device">
<AudioDeviceSelector <AudioDeviceSelector
@ -178,7 +214,7 @@ export function SettingsTab({ started }) {
<FormItem label="Theme"> <FormItem label="Theme">
<SelectInput options={themeOptions} value={theme} onChange={(theme) => settingsMap.setKey('theme', theme)} /> <SelectInput options={themeOptions} value={theme} onChange={(theme) => settingsMap.setKey('theme', theme)} />
</FormItem> </FormItem>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 font-sans"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormItem label="Font Family"> <FormItem label="Font Family">
<SelectInput <SelectInput
options={fontFamilyOptions} options={fontFamilyOptions}
@ -219,13 +255,6 @@ export function SettingsTab({ started }) {
items={{ bottom: 'Bottom', right: 'Right' }} items={{ bottom: 'Bottom', right: 'Right' }}
></ButtonGroup> ></ButtonGroup>
</FormItem> </FormItem>
<FormItem label="Open Panel on: ">
<ButtonGroup
value={togglePanelTrigger}
onChange={(value) => settingsMap.setKey('togglePanelTrigger', value)}
items={{ click: 'Click', hover: 'Hover' }}
/>
</FormItem>
<FormItem label="More Settings"> <FormItem label="More Settings">
<Checkbox <Checkbox
label="Enable bracket matching" label="Enable bracket matching"
@ -294,7 +323,7 @@ export function SettingsTab({ started }) {
value={isSyncEnabled} value={isSyncEnabled}
/> />
<Checkbox <Checkbox
label="Hide top buttons" label="Hide action buttons"
onChange={(cbEvent) => settingsMap.setKey('isButtonRowHidden', cbEvent.target.checked)} onChange={(cbEvent) => settingsMap.setKey('isButtonRowHidden', cbEvent.target.checked)}
value={isButtonRowHidden} value={isButtonRowHidden}
/> />

View file

@ -5,7 +5,7 @@ import { useMemo, useRef, useState } from 'react';
import { settingsMap, soundFilterType, useSettings } from '../../../settings.mjs'; import { settingsMap, soundFilterType, useSettings } from '../../../settings.mjs';
import { ButtonGroup } from './Forms.jsx'; import { ButtonGroup } from './Forms.jsx';
import ImportSoundsButton from './ImportSoundsButton.jsx'; import ImportSoundsButton from './ImportSoundsButton.jsx';
import { Textbox } from '../textbox/Textbox.jsx'; import { Textbox } from '@src/repl/components/panel/SettingsTab.jsx';
import { ActionButton } from '../button/action-button.jsx'; import { ActionButton } from '../button/action-button.jsx';
import { confirmDialog } from '@src/repl/util.mjs'; import { confirmDialog } from '@src/repl/util.mjs';
import { clearIDB, userSamplesDBConfig } from '@src/repl/idbutils.mjs'; import { clearIDB, userSamplesDBConfig } from '@src/repl/idbutils.mjs';
@ -77,11 +77,12 @@ export function SoundsTab() {
numRef.current = 0; numRef.current = 0;
}); });
return ( return (
<div id="sounds-tab" className="px-4 flex gap-2 flex-col w-full h-full text-foreground"> <div id="sounds-tab" className="flex flex-col w-full h-full text-foreground">
<Textbox placeholder="Search" value={search} onChange={(v) => setSearch(v)} /> <Textbox placeholder="Search..." className="border-0" value={search} onChange={(v) => setSearch(v)} />
<div className=" flex shrink-0 flex-wrap"> <div className="flex shrink-0 flex-wrap border-y border-muted">
<ButtonGroup <ButtonGroup
wrap
value={soundsFilter} value={soundsFilter}
onChange={(value) => settingsMap.setKey('soundsFilter', value)} onChange={(value) => settingsMap.setKey('soundsFilter', value)}
items={{ items={{
@ -114,7 +115,7 @@ export function SoundsTab() {
/> />
)} )}
<div className="min-h-0 max-h-full grow overflow-auto text-sm break-normal bg-background p-2 rounded-md"> <div className="min-h-0 max-h-full grow overflow-auto break-normal p-2">
{soundEntries.map(([name, { data, onTrigger }]) => { {soundEntries.map(([name, { data, onTrigger }]) => {
return ( return (
<span <span
@ -165,7 +166,7 @@ export function SoundsTab() {
); );
})} })}
{!soundEntries.length && soundsFilter === 'importSounds' ? ( {!soundEntries.length && soundsFilter === 'importSounds' ? (
<div className="prose dark:prose-invert min-w-full pt-2 pb-8 px-4"> <div className="prose dark:prose-invert min-w-full text-sm">
<ImportSoundsButton onComplete={() => settingsMap.setKey('soundsFilter', 'user')} /> <ImportSoundsButton onComplete={() => settingsMap.setKey('soundsFilter', 'user')} />
<p> <p>
To import sounds into strudel, they must be contained{' '} To import sounds into strudel, they must be contained{' '}

View file

@ -6,8 +6,8 @@ const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL
export function WelcomeTab({ context }) { export function WelcomeTab({ context }) {
const { fontFamily } = useSettings(); const { fontFamily } = useSettings();
return ( return (
<div className="prose dark:prose-invert min-w-full pt-2 font-sans pb-8 px-4 " style={{ fontFamily }}> <div className="prose dark:prose-invert min-w-full py-4 font-sans px-4 text-sm" style={{ fontFamily }}>
<h3> welcome</h3> <h3>welcome</h3>
<p> <p>
You have found <span className="underline">strudel</span>, a new live coding platform to write dynamic music You have found <span className="underline">strudel</span>, a new live coding platform to write dynamic music
pieces in the browser! It is free and open-source and made for beginners and experts alike. To get started: pieces in the browser! It is free and open-source and made for beginners and experts alike. To get started:
@ -30,7 +30,7 @@ export function WelcomeTab({ context }) {
</a>{' '} </a>{' '}
to ask any questions, give feedback or just say hello. to ask any questions, give feedback or just say hello.
</p> </p>
<h3> about</h3> <h3>about</h3>
<p> <p>
strudel is a JavaScript version of{' '} strudel is a JavaScript version of{' '}
<a href="https://tidalcycles.org/" target="_blank"> <a href="https://tidalcycles.org/" target="_blank">

View file

@ -1,14 +0,0 @@
import cx from '@src/cx.mjs';
export function Textbox({ onChange, className, ...inputProps }) {
return (
<input
className={cx(
'p-2 bg-background rounded-md border-foreground text-foreground placeholder-foreground',
className,
)}
onChange={(e) => onChange(e.target.value)}
{...inputProps}
/>
);
}

View file

@ -21,7 +21,7 @@ function getUpdatedLog(log, event) {
} else { } else {
log = log.concat([{ message, type, id, data }]); log = log.concat([{ message, type, id, data }]);
} }
return log.slice(-20); return log.slice(-40);
} }
export function useLogger() { export function useLogger() {

View file

@ -2,6 +2,7 @@ import { persistentMap } from '@nanostores/persistent';
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import { register } from '@strudel/core'; import { register } from '@strudel/core';
import { isUdels } from './repl/util.mjs'; import { isUdels } from './repl/util.mjs';
import { computed } from 'nanostores';
export const audioEngineTargets = { export const audioEngineTargets = {
webaudio: 'webaudio', webaudio: 'webaudio',
@ -38,12 +39,12 @@ export const defaultSettings = {
latestCode: '', latestCode: '',
isZen: false, isZen: false,
soundsFilter: soundFilterType.ALL, soundsFilter: soundFilterType.ALL,
referenceTag: 'all',
patternFilter: 'community', patternFilter: 'community',
// panelPosition: window.innerWidth > 1000 ? 'right' : 'bottom', //FIX: does not work on astro // panelPosition: window.innerWidth > 1000 ? 'right' : 'bottom', //FIX: does not work on astro
panelPosition: 'right', panelPosition: 'right',
isPanelPinned: false, isPanelPinned: false,
isPanelOpen: true, isPanelOpen: true,
togglePanelTrigger: 'click', //click | hover
userPatterns: '{}', userPatterns: '{}',
prebakeScript: '', prebakeScript: '',
audioEngineTarget: audioEngineTargets.webaudio, audioEngineTarget: audioEngineTargets.webaudio,
@ -64,11 +65,7 @@ const settings_key = `strudel-settings${instance > 0 ? instance : ''}`;
export const settingsMap = persistentMap(settings_key, defaultSettings); export const settingsMap = persistentMap(settings_key, defaultSettings);
export const parseBoolean = (booleanlike) => ([true, 'true'].includes(booleanlike) ? true : false); export const $settings = computed(settingsMap, (state) => {
export function useSettings() {
const state = useStore(settingsMap);
const userPatterns = JSON.parse(state.userPatterns); const userPatterns = JSON.parse(state.userPatterns);
Object.keys(userPatterns).forEach((key) => { Object.keys(userPatterns).forEach((key) => {
const data = userPatterns[key]; const data = userPatterns[key];
@ -105,6 +102,12 @@ export function useSettings() {
? true ? true
: parseBoolean(state.patternAutoStart), : parseBoolean(state.patternAutoStart),
}; };
});
export const parseBoolean = (booleanlike) => ([true, 'true'].includes(booleanlike) ? true : false);
export function useSettings() {
return useStore($settings);
} }
export const setActiveFooter = (tab) => settingsMap.setKey('activeFooter', tab); export const setActiveFooter = (tab) => settingsMap.setKey('activeFooter', tab);

View file

@ -1,4 +1,4 @@
import { atom } from 'nanostores'; import { atom, computed } from 'nanostores';
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import { logger } from '@strudel/core'; import { logger } from '@strudel/core';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
@ -36,12 +36,9 @@ export let $viewingPatternData = sessionAtom('viewingPatternData', {
created_at: Date.now(), created_at: Date.now(),
}); });
export const getViewingPatternData = () => { const $viewingPatterns = computed($viewingPatternData, (state) => parseJSON(state));
return parseJSON($viewingPatternData.get()); export const useViewingPatternData = () => useStore($viewingPatterns);
}; export const getViewingPatternData = () => $viewingPatterns.get();
export const useViewingPatternData = () => {
return useStore($viewingPatternData);
};
export const setViewingPatternData = (data) => { export const setViewingPatternData = (data) => {
$viewingPatternData.set(JSON.stringify(data)); $viewingPatternData.set(JSON.stringify(data));

View file

@ -24,6 +24,7 @@ module.exports = {
background: 'var(--background)', background: 'var(--background)',
lineBackground: 'var(--lineBackground)', lineBackground: 'var(--lineBackground)',
foreground: 'var(--foreground)', foreground: 'var(--foreground)',
muted: 'var(--muted)',
caret: 'var(--caret)', caret: 'var(--caret)',
selection: 'var(--selection)', selection: 'var(--selection)',
selectionMatch: 'var(--selectionMatch)', selectionMatch: 'var(--selectionMatch)',
@ -46,28 +47,28 @@ module.exports = {
'code::after': { 'code::after': {
content: 'none', content: 'none',
}, },
color: 'var(--foreground)', color: 'var(--foreground) !important',
a: { a: {
color: 'var(--foreground)', color: 'var(--foreground) !important',
}, },
h1: { h1: {
color: 'var(--foreground)', color: 'var(--foreground) !important',
}, },
h2: { h2: {
color: 'var(--foreground)', color: 'var(--foreground) !important',
}, },
h3: { h3: {
color: 'var(--foreground)', color: 'var(--foreground) !important',
}, },
h4: { h4: {
color: 'var(--foreground)', color: 'var(--foreground) !important',
}, },
pre: { pre: {
color: 'var(--foreground)', color: 'var(--foreground) !important',
background: 'var(--background)', background: 'var(--background) !important',
}, },
code: { code: {
color: 'var(--foreground)', color: 'var(--foreground) !important',
}, },
}, },
}, },