Merge pull request #467 from tidalcycles/vim-mode
settings tab with vim / emacs modes + additional themes and fonts
This commit is contained in:
commit
f349e36345
35 changed files with 1348 additions and 418 deletions
|
|
@ -1,66 +1,64 @@
|
|||
---
|
||||
// fetch all commits for just this page's path
|
||||
type Props = {
|
||||
path: string;
|
||||
path: string;
|
||||
};
|
||||
const { path } = Astro.props as Props;
|
||||
const resolvedPath = `examples/docs/${path}`;
|
||||
const url = `https://api.github.com/repos/withastro/astro/commits?path=${resolvedPath}`;
|
||||
const commitsURL = `https://github.com/withastro/astro/commits/main/${resolvedPath}`;
|
||||
const resolvedPath = `website/src/pages${path}.mdx`;
|
||||
const url = `https://api.github.com/repos/tidalcycles/strudel/commits?path=${resolvedPath}`;
|
||||
const commitsURL = `https://github.com/tidalcycles/strudel/commits/main/${resolvedPath}`;
|
||||
|
||||
type Commit = {
|
||||
author: {
|
||||
id: string;
|
||||
login: string;
|
||||
};
|
||||
author: {
|
||||
id: string;
|
||||
login: string;
|
||||
};
|
||||
};
|
||||
|
||||
async function getCommits(url: string) {
|
||||
try {
|
||||
const token = import.meta.env.SNOWPACK_PUBLIC_GITHUB_TOKEN ?? 'hello';
|
||||
if (!token) {
|
||||
throw new Error(
|
||||
'Cannot find "SNOWPACK_PUBLIC_GITHUB_TOKEN" used for escaping rate-limiting.'
|
||||
);
|
||||
}
|
||||
try {
|
||||
const token = import.meta.env.SNOWPACK_PUBLIC_GITHUB_TOKEN ?? 'hello';
|
||||
if (!token) {
|
||||
throw new Error('Cannot find "SNOWPACK_PUBLIC_GITHUB_TOKEN" used for escaping rate-limiting.');
|
||||
}
|
||||
|
||||
const auth = `Basic ${Buffer.from(token, 'binary').toString('base64')}`;
|
||||
const auth = `Basic ${Buffer.from(token, 'binary').toString('base64')}`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: auth,
|
||||
'User-Agent': 'astro-docs/1.0',
|
||||
},
|
||||
});
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: auth,
|
||||
'User-Agent': 'astro-docs/1.0',
|
||||
},
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(
|
||||
`Request to fetch commits failed. Reason: ${res.statusText}
|
||||
Message: ${data.message}`
|
||||
);
|
||||
}
|
||||
if (!res.ok) {
|
||||
throw new Error(
|
||||
`Request to fetch commits failed. Reason: ${res.statusText}
|
||||
Message: ${data.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
return data as Commit[];
|
||||
} catch (e) {
|
||||
console.warn(`[error] /src/components/AvatarList.astro
|
||||
return data as Commit[];
|
||||
} catch (e) {
|
||||
console.warn(`[error] /src/components/AvatarList.astro
|
||||
${(e as any)?.message ?? e}`);
|
||||
return [] as Commit[];
|
||||
}
|
||||
return [] as Commit[];
|
||||
}
|
||||
}
|
||||
|
||||
function removeDups(arr: Commit[]) {
|
||||
const map = new Map<string, Commit['author']>();
|
||||
const map = new Map<string, Commit['author']>();
|
||||
for (let item of arr) {
|
||||
const author = item.author;
|
||||
// Deduplicate based on author.id
|
||||
//map.set(author.id, { login: author.login, id: author.id });
|
||||
author && map.set(author.id, { login: author.login, id: author.id });
|
||||
}
|
||||
|
||||
for (let item of arr) {
|
||||
const author = item.author;
|
||||
// Deduplicate based on author.id
|
||||
map.set(author.id, { login: author.login, id: author.id });
|
||||
}
|
||||
|
||||
return [...map.values()];
|
||||
return [...map.values()];
|
||||
}
|
||||
|
||||
const data = await getCommits(url);
|
||||
|
|
@ -70,102 +68,102 @@ const additionalContributors = unique.length - recentContributors.length; // lis
|
|||
---
|
||||
|
||||
<!-- Thanks to @5t3ph for https://smolcss.dev/#smol-avatar-list! -->
|
||||
<div class="contributors">
|
||||
<ul class="avatar-list" style={`--avatar-count: ${recentContributors.length}`}>
|
||||
{
|
||||
recentContributors.map((item) => (
|
||||
<li>
|
||||
<a href={`https://github.com/${item.login}`}>
|
||||
<img
|
||||
alt={`Contributor ${item.login}`}
|
||||
title={`Contributor ${item.login}`}
|
||||
width="64"
|
||||
height="64"
|
||||
src={`https://avatars.githubusercontent.com/u/${item.id}`}
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
{
|
||||
additionalContributors > 0 && (
|
||||
<span>
|
||||
<a href={commitsURL}>{`and ${additionalContributors} additional contributor${
|
||||
additionalContributors > 1 ? 's' : ''
|
||||
}.`}</a>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
{unique.length === 0 && <a href={commitsURL}>Contributors</a>}
|
||||
<div class="contributors px-4 mb-8">
|
||||
<ul class="avatar-list" style={`--avatar-count: ${recentContributors.length}`}>
|
||||
{
|
||||
recentContributors.map((item) => (
|
||||
<li>
|
||||
<a href={`https://github.com/${item.login}`}>
|
||||
<img
|
||||
alt={`Contributor ${item.login}`}
|
||||
title={`Contributor ${item.login}`}
|
||||
width="64"
|
||||
height="64"
|
||||
src={`https://avatars.githubusercontent.com/u/${item.id}`}
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
{
|
||||
additionalContributors > 0 && (
|
||||
<span>
|
||||
<a href={commitsURL}>{`and ${additionalContributors} additional contributor${
|
||||
additionalContributors > 1 ? 's' : ''
|
||||
}.`}</a>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
{unique.length === 0 && <a href={commitsURL}>Contributors</a>}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.avatar-list {
|
||||
--avatar-size: 2.5rem;
|
||||
--avatar-count: 3;
|
||||
.avatar-list {
|
||||
--avatar-size: 2.5rem;
|
||||
--avatar-count: 3;
|
||||
|
||||
display: grid;
|
||||
list-style: none;
|
||||
/* Default to displaying most of the avatar to
|
||||
display: grid;
|
||||
list-style: none;
|
||||
/* Default to displaying most of the avatar to
|
||||
enable easier access on touch devices, ensuring
|
||||
the WCAG touch target size is met or exceeded */
|
||||
grid-template-columns: repeat(var(--avatar-count), max(44px, calc(var(--avatar-size) / 1.15)));
|
||||
/* `padding` matches added visual dimensions of
|
||||
grid-template-columns: repeat(var(--avatar-count), max(44px, calc(var(--avatar-size) / 1.15)));
|
||||
/* `padding` matches added visual dimensions of
|
||||
the `box-shadow` to help create a more accurate
|
||||
computed component size */
|
||||
padding: 0.08em;
|
||||
font-size: var(--avatar-size);
|
||||
}
|
||||
padding: 0.08em;
|
||||
font-size: var(--avatar-size);
|
||||
}
|
||||
|
||||
@media (any-hover: hover) and (any-pointer: fine) {
|
||||
.avatar-list {
|
||||
/* We create 1 extra cell to enable the computed
|
||||
@media (any-hover: hover) and (any-pointer: fine) {
|
||||
.avatar-list {
|
||||
/* We create 1 extra cell to enable the computed
|
||||
width to match the final visual width */
|
||||
grid-template-columns: repeat(calc(var(--avatar-count) + 1), calc(var(--avatar-size) / 1.75));
|
||||
}
|
||||
}
|
||||
grid-template-columns: repeat(calc(var(--avatar-count) + 1), calc(var(--avatar-size) / 1.75));
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-list li {
|
||||
width: var(--avatar-size);
|
||||
height: var(--avatar-size);
|
||||
}
|
||||
.avatar-list li {
|
||||
width: var(--avatar-size);
|
||||
height: var(--avatar-size);
|
||||
}
|
||||
|
||||
.avatar-list li:hover ~ li a,
|
||||
.avatar-list li:focus-within ~ li a {
|
||||
transform: translateX(33%);
|
||||
}
|
||||
.avatar-list li:hover ~ li a,
|
||||
.avatar-list li:focus-within ~ li a {
|
||||
transform: translateX(33%);
|
||||
}
|
||||
|
||||
.avatar-list img,
|
||||
.avatar-list a {
|
||||
display: block;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.avatar-list img,
|
||||
.avatar-list a {
|
||||
display: block;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.avatar-list a {
|
||||
transition: transform 180ms ease-in-out;
|
||||
}
|
||||
.avatar-list a {
|
||||
transition: transform 180ms ease-in-out;
|
||||
}
|
||||
|
||||
.avatar-list img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 0 0 0.05em #fff, 0 0 0 0.08em rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.avatar-list img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 0 0 0.05em #fff, 0 0 0 0.08em rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.avatar-list a:focus {
|
||||
outline: 2px solid transparent;
|
||||
/* Double-layer trick to work for dark and light backgrounds */
|
||||
box-shadow: 0 0 0 0.08em var(--theme-accent), 0 0 0 0.12em white;
|
||||
}
|
||||
.avatar-list a:focus {
|
||||
outline: 2px solid transparent;
|
||||
/* Double-layer trick to work for dark and light backgrounds */
|
||||
box-shadow: 0 0 0 0.08em var(--theme-accent), 0 0 0 0.12em white;
|
||||
}
|
||||
|
||||
.contributors {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.contributors {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.contributors > * + * {
|
||||
margin-left: 0.75rem;
|
||||
}
|
||||
.contributors > * + * {
|
||||
margin-left: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
---
|
||||
import AvatarList from './AvatarList.astro';
|
||||
type Props = {
|
||||
path: string;
|
||||
};
|
||||
const { path } = Astro.props as Props;
|
||||
---
|
||||
|
||||
<footer>
|
||||
<AvatarList path={path} />
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
footer {
|
||||
margin-top: auto;
|
||||
padding: 2rem;
|
||||
border-top: 3px solid var(--theme-divider);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,12 +1,9 @@
|
|||
---
|
||||
import { pwaInfo } from 'virtual:pwa-info';
|
||||
import '../styles/index.css';
|
||||
import { settings } from '../repl/themes.mjs';
|
||||
|
||||
const { BASE_URL } = import.meta.env;
|
||||
const base = BASE_URL;
|
||||
|
||||
const { strudelTheme } = settings;
|
||||
---
|
||||
|
||||
<!-- Global Metadata -->
|
||||
|
|
@ -48,37 +45,43 @@ const { strudelTheme } = settings;
|
|||
</style>
|
||||
{pwaInfo && <Fragment set:html={pwaInfo.webManifest.linkTag} />}
|
||||
|
||||
<script define:vars={{ settings, strudelTheme }} is:inline>
|
||||
<script>
|
||||
import { settings } from '../repl/themes.mjs';
|
||||
import { settingsMap } from '../settings.mjs';
|
||||
import { listenKeys } from 'nanostores';
|
||||
const themeStyle = document.createElement('style');
|
||||
themeStyle.id = 'strudel-theme';
|
||||
document.head.append(themeStyle);
|
||||
function getTheme(name) {
|
||||
function activateTheme(name) {
|
||||
if (!settings[name]) {
|
||||
console.warn('theme', name, 'has no settings');
|
||||
console.warn('theme', name, 'has no settings.. defaulting to strudelTheme settings');
|
||||
}
|
||||
return {
|
||||
name,
|
||||
settings: settings[name] || settings.strudelTheme,
|
||||
};
|
||||
}
|
||||
function setTheme(name) {
|
||||
const { settings } = getTheme(name);
|
||||
const themeSettings = settings[name] || settings.strudelTheme;
|
||||
// set css variables
|
||||
themeStyle.innerHTML = `:root {
|
||||
${Object.entries(settings)
|
||||
${Object.entries(themeSettings)
|
||||
// important to override fallback
|
||||
.map(([key, value]) => `--${key}: ${value} !important;`)
|
||||
.join('\n')}
|
||||
}`;
|
||||
// tailwind dark mode
|
||||
if (settings.light) {
|
||||
if (themeSettings.light) {
|
||||
document.documentElement.classList.remove('dark');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
// persist theme name
|
||||
localStorage.setItem('strudel-theme', name || 'strudelTheme');
|
||||
}
|
||||
setTheme(localStorage.getItem('strudel-theme'));
|
||||
document.addEventListener('strudel-theme', (e) => setTheme(e.detail));
|
||||
|
||||
activateTheme(settingsMap.get().theme);
|
||||
listenKeys(settingsMap, ['theme'], ({ theme }) => activateTheme(theme));
|
||||
|
||||
// https://medium.com/quick-code/100vh-problem-with-ios-safari-92ab23c852a8
|
||||
const appHeight = () => {
|
||||
const doc = document.documentElement;
|
||||
doc.style.setProperty('--app-height', `${window.innerHeight}px`);
|
||||
};
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('resize', appHeight);
|
||||
appHeight();
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import TableOfContents from './TableOfContents';
|
||||
import MoreMenu from './MoreMenu.astro';
|
||||
import type { MarkdownHeading } from 'astro';
|
||||
import AvatarList from '../Footer/AvatarList.astro';
|
||||
|
||||
type Props = {
|
||||
headings: MarkdownHeading[];
|
||||
|
|
@ -17,4 +18,5 @@ currentPage = currentPage.endsWith('/') ? currentPage.slice(0, -1) : currentPage
|
|||
<nav aria-labelledby="grid-right" class="w-64 text-foreground">
|
||||
<TableOfContents client:media="(min-width: 50em)" headings={headings} currentPage={currentPage} />
|
||||
<MoreMenu editHref={githubEditUrl} />
|
||||
<!-- <AvatarList path={currentPage} /> -->
|
||||
</nav>
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ import { useEffect, useState } from 'react';
|
|||
import { prebake } from '../repl/prebake';
|
||||
import { themes, settings } from '../repl/themes.mjs';
|
||||
import './MiniRepl.css';
|
||||
|
||||
const theme = localStorage.getItem('strudel-theme') || 'strudelTheme';
|
||||
import { useSettings } from '../settings.mjs';
|
||||
|
||||
let modules;
|
||||
if (typeof window !== 'undefined') {
|
||||
|
|
@ -29,6 +28,7 @@ if (typeof window !== 'undefined') {
|
|||
|
||||
export function MiniRepl({ tune, drawTime, punchcard, canvasHeight = 100 }) {
|
||||
const [Repl, setRepl] = useState();
|
||||
const { theme } = useSettings();
|
||||
useEffect(() => {
|
||||
// we have to load this package on the client
|
||||
// because codemirror throws an error on the server
|
||||
|
|
@ -36,7 +36,6 @@ export function MiniRepl({ tune, drawTime, punchcard, canvasHeight = 100 }) {
|
|||
.then(([res]) => setRepl(() => res.MiniRepl))
|
||||
.catch((err) => console.error(err));
|
||||
}, []);
|
||||
// const { settings } = useTheme();
|
||||
return Repl ? (
|
||||
<div className="mb-4">
|
||||
<Repl
|
||||
|
|
@ -46,7 +45,6 @@ export function MiniRepl({ tune, drawTime, punchcard, canvasHeight = 100 }) {
|
|||
punchcard={punchcard}
|
||||
canvasHeight={canvasHeight}
|
||||
theme={themes[theme]}
|
||||
highlightColor={settings[theme]?.foreground}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import LeftSidebar from '../components/LeftSidebar/LeftSidebar.astro';
|
|||
import RightSidebar from '../components/RightSidebar/RightSidebar.astro';
|
||||
import * as CONFIG from '../config';
|
||||
import type { MarkdownHeading } from 'astro';
|
||||
import Footer from '../components/Footer/Footer.astro';
|
||||
import '../docs/docs.css';
|
||||
|
||||
type Props = {
|
||||
|
|
@ -31,7 +30,7 @@ const githubEditUrl = `${CONFIG.GITHUB_EDIT_URL}/${currentFile}`;
|
|||
</title>
|
||||
</head>
|
||||
|
||||
<body class="h-screen text-gray-50 bg-background">
|
||||
<body class="h-app-height text-gray-50 bg-background">
|
||||
<div class="w-full h-full space-y-4 flex flex-col">
|
||||
<header class="max-w-full fixed top-0 w-full z-[100]">
|
||||
<Header currentPage={currentPage} />
|
||||
|
|
@ -49,7 +48,6 @@ const githubEditUrl = `${CONFIG.GITHUB_EDIT_URL}/${currentFile}`;
|
|||
</aside>
|
||||
</div>
|
||||
</main>
|
||||
<!-- <Footer path={currentFile} /> -->
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { Repl } from '../repl/Repl.jsx';
|
|||
<HeadCommon />
|
||||
<title>Strudel REPL</title>
|
||||
</head>
|
||||
<body class="bg-background">
|
||||
<body class="h-app-height bg-background">
|
||||
<Repl client:only="react" />
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -6,14 +6,13 @@ import { nanoid } from 'nanoid';
|
|||
import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { loadedSamples } from './Repl';
|
||||
import { Reference } from './Reference';
|
||||
import { themes, themeColors } from './themes.mjs';
|
||||
import { themes } from './themes.mjs';
|
||||
import { useSettings, settingsMap, setActiveFooter, defaultSettings } from '../settings.mjs';
|
||||
|
||||
export function Footer({ context }) {
|
||||
// const [activeFooter, setActiveFooter] = useState('console');
|
||||
// const { activeFooter, setActiveFooter, isZen } = useContext?.(ReplContext);
|
||||
const { activeFooter, setActiveFooter, isZen, theme, setTheme } = context;
|
||||
const footerContent = useRef();
|
||||
const [log, setLog] = useState([]);
|
||||
const { activeFooter, isZen } = useSettings();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (footerContent.current && activeFooter === 'console') {
|
||||
|
|
@ -56,8 +55,8 @@ export function Footer({ context }) {
|
|||
<div
|
||||
onClick={() => setActiveFooter(name)}
|
||||
className={cx(
|
||||
'h-8 px-2 text-foreground cursor-pointer hover:text-tertiary flex items-center space-x-1 border-b',
|
||||
activeFooter === name ? 'border-foreground hover:border-tertiary' : 'border-transparent',
|
||||
'h-8 px-2 text-foreground cursor-pointer hover:opacity-50 flex items-center space-x-1 border-b',
|
||||
activeFooter === name ? 'border-foreground' : 'border-transparent',
|
||||
)}
|
||||
>
|
||||
{label || name}
|
||||
|
|
@ -76,7 +75,7 @@ export function Footer({ context }) {
|
|||
<FooterTab name="samples" />
|
||||
<FooterTab name="console" />
|
||||
<FooterTab name="reference" />
|
||||
<FooterTab name="theme" />
|
||||
<FooterTab name="settings" />
|
||||
</div>
|
||||
{activeFooter !== '' && (
|
||||
<button onClick={() => setActiveFooter('')} className="text-foreground" aria-label="Close Panel">
|
||||
|
|
@ -89,113 +88,11 @@ export function Footer({ context }) {
|
|||
className="text-white font-mono text-sm h-[360px] flex-none overflow-auto max-w-full relative"
|
||||
ref={footerContent}
|
||||
>
|
||||
{activeFooter === 'intro' && (
|
||||
<div className="prose dark:prose-invert max-w-[600px] pt-2 font-sans pb-8 px-4">
|
||||
<h3>
|
||||
<span className={cx('animate-spin inline-block select-none')}>🌀</span> welcome
|
||||
</h3>
|
||||
<p>
|
||||
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:
|
||||
<br />
|
||||
<br />
|
||||
<span className="underline">1. hit play</span> - <span className="underline">2. change something</span>{' '}
|
||||
- <span className="underline">3. hit update</span>
|
||||
<br />
|
||||
If you don't like what you hear, try <span className="underline">shuffle</span>!
|
||||
</p>
|
||||
<p>
|
||||
To learn more about what this all means, check out the{' '}
|
||||
<a href="./learn/getting-started" target="_blank">
|
||||
interactive tutorial
|
||||
</a>
|
||||
. Also feel free to join the{' '}
|
||||
<a href="https://discord.com/invite/HGEdXmRkzT" target="_blank">
|
||||
tidalcycles discord channel
|
||||
</a>{' '}
|
||||
to ask any questions, give feedback or just say hello.
|
||||
</p>
|
||||
<h3>about</h3>
|
||||
<p>
|
||||
strudel is a JavaScript version of{' '}
|
||||
<a href="tidalcycles.org/" target="_blank">
|
||||
tidalcycles
|
||||
</a>
|
||||
, which is a popular live coding language for music, written in Haskell. You can find the source code at{' '}
|
||||
<a href="https://github.com/tidalcycles/strudel" target="_blank">
|
||||
github
|
||||
</a>
|
||||
. Please consider to{' '}
|
||||
<a href="https://opencollective.com/tidalcycles" target="_blank">
|
||||
support this project
|
||||
</a>{' '}
|
||||
to ensure ongoing development 💖
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{activeFooter === 'console' && (
|
||||
<div className="break-all px-4 dark:text-white text-stone-900">
|
||||
{log.map((l, i) => {
|
||||
const message = linkify(l.message);
|
||||
return (
|
||||
<div
|
||||
key={l.id}
|
||||
className={cx(l.type === 'error' && 'text-red-500', l.type === 'highlight' && 'text-highlight')}
|
||||
>
|
||||
<span dangerouslySetInnerHTML={{ __html: message }} />
|
||||
{l.count ? ` (${l.count})` : ''}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{activeFooter === 'samples' && (
|
||||
<div className="break-normal w-full px-4 dark:text-white text-stone-900">
|
||||
<span>{loadedSamples.length} banks loaded:</span>
|
||||
{loadedSamples.map(([name, samples]) => (
|
||||
<span key={name} className="cursor-pointer hover:text-tertiary" onClick={() => {}}>
|
||||
{' '}
|
||||
{name}(
|
||||
{Array.isArray(samples)
|
||||
? samples.length
|
||||
: typeof samples === 'object'
|
||||
? Object.values(samples).length
|
||||
: 1}
|
||||
){' '}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{activeFooter === 'intro' && <WelcomeTab />}
|
||||
{activeFooter === 'console' && <ConsoleTab log={log} />}
|
||||
{activeFooter === 'samples' && <SamplesTab />}
|
||||
{activeFooter === 'reference' && <Reference />}
|
||||
{activeFooter === 'theme' && (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-2 p-2">
|
||||
{Object.entries(themes).map(([k, t]) => (
|
||||
<div
|
||||
key={k}
|
||||
className={cx(
|
||||
'border-2 border-transparent cursor-pointer p-4 bg-background bg-opacity-25 rounded-md',
|
||||
theme === k ? '!border-foreground' : '',
|
||||
)}
|
||||
onClick={() => {
|
||||
setTheme(k);
|
||||
document.dispatchEvent(
|
||||
new CustomEvent('strudel-theme', {
|
||||
detail: k,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<div className="mb-2 w-full text-center text-foreground">{k}</div>
|
||||
<div className="flex justify-stretch overflow-hidden rounded-md">
|
||||
{themeColors(t).map((c, i) => (
|
||||
<div key={i} className="grow h-6" style={{ background: c }} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{activeFooter === 'settings' && <SettingsTab />}
|
||||
</div>
|
||||
)}
|
||||
</footer>
|
||||
|
|
@ -226,3 +123,211 @@ function linkify(inputText) {
|
|||
|
||||
return replacedText;
|
||||
}
|
||||
|
||||
function WelcomeTab() {
|
||||
return (
|
||||
<div className="prose dark:prose-invert max-w-[600px] pt-2 font-sans pb-8 px-4">
|
||||
<h3>
|
||||
<span className={cx('animate-spin inline-block select-none')}>🌀</span> welcome
|
||||
</h3>
|
||||
<p>
|
||||
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:
|
||||
<br />
|
||||
<br />
|
||||
<span className="underline">1. hit play</span> - <span className="underline">2. change something</span> -{' '}
|
||||
<span className="underline">3. hit update</span>
|
||||
<br />
|
||||
If you don't like what you hear, try <span className="underline">shuffle</span>!
|
||||
</p>
|
||||
<p>
|
||||
To learn more about what this all means, check out the{' '}
|
||||
<a href="./learn/getting-started" target="_blank">
|
||||
interactive tutorial
|
||||
</a>
|
||||
. Also feel free to join the{' '}
|
||||
<a href="https://discord.com/invite/HGEdXmRkzT" target="_blank">
|
||||
tidalcycles discord channel
|
||||
</a>{' '}
|
||||
to ask any questions, give feedback or just say hello.
|
||||
</p>
|
||||
<h3>about</h3>
|
||||
<p>
|
||||
strudel is a JavaScript version of{' '}
|
||||
<a href="tidalcycles.org/" target="_blank">
|
||||
tidalcycles
|
||||
</a>
|
||||
, which is a popular live coding language for music, written in Haskell. You can find the source code at{' '}
|
||||
<a href="https://github.com/tidalcycles/strudel" target="_blank">
|
||||
github
|
||||
</a>
|
||||
. Please consider to{' '}
|
||||
<a href="https://opencollective.com/tidalcycles" target="_blank">
|
||||
support this project
|
||||
</a>{' '}
|
||||
to ensure ongoing development 💖
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ConsoleTab({ log }) {
|
||||
return (
|
||||
<div id="console-tab" className="break-all px-4 dark:text-white text-stone-900">
|
||||
<pre>{`███████╗████████╗██████╗ ██╗ ██╗██████╗ ███████╗██╗
|
||||
██╔════╝╚══██╔══╝██╔══██╗██║ ██║██╔══██╗██╔════╝██║
|
||||
███████╗ ██║ ██████╔╝██║ ██║██║ ██║█████╗ ██║
|
||||
╚════██║ ██║ ██╔══██╗██║ ██║██║ ██║██╔══╝ ██║
|
||||
███████║ ██║ ██║ ██║╚██████╔╝██████╔╝███████╗███████╗
|
||||
╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝`}</pre>
|
||||
{log.map((l, i) => {
|
||||
const message = linkify(l.message);
|
||||
return (
|
||||
<div key={l.id} className={cx(l.type === 'error' && 'text-red-500', l.type === 'highlight' && 'underline')}>
|
||||
<span dangerouslySetInnerHTML={{ __html: message }} />
|
||||
{l.count ? ` (${l.count})` : ''}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SamplesTab() {
|
||||
return (
|
||||
<div id="samples-tab" className="break-normal w-full px-4 dark:text-white text-stone-900">
|
||||
<span>{loadedSamples.length} banks loaded:</span>
|
||||
{loadedSamples.map(([name, samples]) => (
|
||||
<span key={name} className="cursor-pointer hover:opacity-50" onClick={() => {}}>
|
||||
{' '}
|
||||
{name}(
|
||||
{Array.isArray(samples) ? samples.length : typeof samples === 'object' ? Object.values(samples).length : 1}){' '}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ButtonGroup({ value, onChange, items }) {
|
||||
return (
|
||||
<div className="flex grow border border-foreground rounded-md">
|
||||
{Object.entries(items).map(([key, label], i, arr) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => onChange(key)}
|
||||
className={cx(
|
||||
'p-2 grow',
|
||||
i === 0 && 'rounded-l-md',
|
||||
i === arr.length - 1 && 'rounded-r-md',
|
||||
value === key ? 'bg-background' : 'bg-lineHighlight',
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectInput({ value, options, onChange }) {
|
||||
return (
|
||||
<select
|
||||
className="p-2 bg-background rounded-md text-foreground"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
>
|
||||
{Object.entries(options).map(([k, label]) => (
|
||||
<option key={k} className="bg-background" value={k}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
function NumberSlider({ value, onChange, step = 1, ...rest }) {
|
||||
return (
|
||||
<div className="flex space-x-2 gap-1">
|
||||
<input
|
||||
className="p-2 grow"
|
||||
type="range"
|
||||
value={value}
|
||||
step={step}
|
||||
onChange={(e) => onChange(Number(e.target.value))}
|
||||
{...rest}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
value={value}
|
||||
step={step}
|
||||
className="w-16 bg-background rounded-md"
|
||||
onChange={(e) => onChange(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FormItem({ label, children }) {
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
<label>{label}</label>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const themeOptions = Object.fromEntries(Object.keys(themes).map((k) => [k, k]));
|
||||
const fontFamilyOptions = {
|
||||
monospace: 'monospace',
|
||||
BigBlueTerminal: 'BigBlueTerminal',
|
||||
x3270: 'x3270',
|
||||
PressStart: 'PressStart2P',
|
||||
};
|
||||
|
||||
function SettingsTab() {
|
||||
const { theme, keybindings, fontSize, fontFamily } = useSettings();
|
||||
return (
|
||||
<div className="text-foreground p-4 space-y-4">
|
||||
<FormItem label="Theme">
|
||||
<SelectInput options={themeOptions} value={theme} onChange={(theme) => settingsMap.setKey('theme', theme)} />
|
||||
</FormItem>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormItem label="Font Family">
|
||||
<SelectInput
|
||||
options={fontFamilyOptions}
|
||||
value={fontFamily}
|
||||
onChange={(fontFamily) => settingsMap.setKey('fontFamily', fontFamily)}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="Font Size">
|
||||
<NumberSlider
|
||||
value={fontSize}
|
||||
onChange={(fontSize) => settingsMap.setKey('fontSize', fontSize)}
|
||||
min={10}
|
||||
max={40}
|
||||
step={2}
|
||||
/>
|
||||
</FormItem>
|
||||
</div>
|
||||
<FormItem label="Keybindings">
|
||||
<ButtonGroup
|
||||
value={keybindings}
|
||||
onChange={(keybindings) => settingsMap.setKey('keybindings', keybindings)}
|
||||
items={{ codemirror: 'Codemirror', vim: 'Vim', emacs: 'Emacs' }}
|
||||
></ButtonGroup>
|
||||
</FormItem>
|
||||
<FormItem label="Reset Settings">
|
||||
<button
|
||||
className="bg-background p-2 max-w-[300px] rounded-md hover:opacity-50"
|
||||
onClick={() => {
|
||||
if (confirm('Sure?')) {
|
||||
settingsMap.set(defaultSettings);
|
||||
}
|
||||
}}
|
||||
>
|
||||
restore default settings
|
||||
</button>
|
||||
</FormItem>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import SparklesIcon from '@heroicons/react/20/solid/SparklesIcon';
|
|||
import StopCircleIcon from '@heroicons/react/20/solid/StopCircleIcon';
|
||||
import { cx } from '@strudel.cycles/react';
|
||||
import React, { useContext } from 'react';
|
||||
import { useSettings, setIsZen } from '../settings.mjs';
|
||||
// import { ReplContext } from './Repl';
|
||||
import './Repl.css';
|
||||
|
||||
|
|
@ -21,11 +22,9 @@ export function Header({ context }) {
|
|||
handleUpdate,
|
||||
handleShuffle,
|
||||
handleShare,
|
||||
isZen,
|
||||
setIsZen,
|
||||
} = context;
|
||||
const isEmbedded = embedded || window.location !== window.parent.location;
|
||||
// useContext(ReplContext)
|
||||
const { isZen } = useSettings();
|
||||
|
||||
return (
|
||||
<header
|
||||
|
|
@ -53,7 +52,11 @@ export function Header({ context }) {
|
|||
>
|
||||
<div
|
||||
className={cx('mt-[1px]', started && 'animate-spin', 'cursor-pointer')}
|
||||
onClick={() => !isEmbedded && setIsZen((z) => !z)}
|
||||
onClick={() => {
|
||||
if (!isEmbedded) {
|
||||
setIsZen(!isZen);
|
||||
}
|
||||
}}
|
||||
>
|
||||
🌀
|
||||
</div>
|
||||
|
|
@ -69,7 +72,7 @@ export function Header({ context }) {
|
|||
<button
|
||||
onClick={handleTogglePlay}
|
||||
title={started ? 'stop' : 'play'}
|
||||
className={cx(!isEmbedded ? 'p-2' : 'px-2', 'hover:text-tertiary', !started && 'animate-pulse')}
|
||||
className={cx(!isEmbedded ? 'p-2' : 'px-2', 'hover:opacity-50', !started && 'animate-pulse')}
|
||||
>
|
||||
{!pending ? (
|
||||
<span className={cx('flex items-center space-x-1', isEmbedded ? '' : 'w-16')}>
|
||||
|
|
@ -86,7 +89,7 @@ export function Header({ context }) {
|
|||
className={cx(
|
||||
'flex items-center space-x-1',
|
||||
!isEmbedded ? 'p-2' : 'px-2',
|
||||
!isDirty || !activeCode ? 'opacity-50' : 'hover:text-tertiary',
|
||||
!isDirty || !activeCode ? 'opacity-50' : 'hover:opacity-50',
|
||||
)}
|
||||
>
|
||||
{/* <CommandLineIcon className="w-6 h-6" /> */}
|
||||
|
|
@ -96,7 +99,7 @@ export function Header({ context }) {
|
|||
{!isEmbedded && (
|
||||
<button
|
||||
title="shuffle"
|
||||
className="hover:text-tertiary p-2 flex items-center space-x-1"
|
||||
className="hover:opacity-50 p-2 flex items-center space-x-1"
|
||||
onClick={handleShuffle}
|
||||
>
|
||||
<SparklesIcon className="w-6 h-6" />
|
||||
|
|
@ -107,7 +110,7 @@ export function Header({ context }) {
|
|||
<button
|
||||
title="share"
|
||||
className={cx(
|
||||
'cursor-pointer hover:text-tertiary flex items-center space-x-1',
|
||||
'cursor-pointer hover:opacity-50 flex items-center space-x-1',
|
||||
!isEmbedded ? 'p-2' : 'px-2',
|
||||
)}
|
||||
onClick={handleShare}
|
||||
|
|
@ -120,21 +123,21 @@ export function Header({ context }) {
|
|||
<a
|
||||
title="learn"
|
||||
href="./learn/getting-started"
|
||||
className={cx('hover:text-tertiary flex items-center space-x-1', !isEmbedded ? 'p-2' : 'px-2')}
|
||||
className={cx('hover:opacity-50 flex items-center space-x-1', !isEmbedded ? 'p-2' : 'px-2')}
|
||||
>
|
||||
<AcademicCapIcon className="w-6 h-6" />
|
||||
<span>learn</span>
|
||||
</a>
|
||||
)}
|
||||
{/* {isEmbedded && (
|
||||
<button className={cx('hover:text-tertiary px-2')}>
|
||||
<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:text-tertiary px-2')}>
|
||||
<button className={cx('hover:opacity-50 px-2')}>
|
||||
<a
|
||||
onClick={() => {
|
||||
window.location.href = initialUrl;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
#code .cm-scroller {
|
||||
padding-bottom: 50vh;
|
||||
font-family: inherit;
|
||||
}
|
||||
#code .cm-line > * {
|
||||
background: var(--lineBackground);
|
||||
|
|
|
|||
|
|
@ -23,9 +23,9 @@ import { prebake } from './prebake.mjs';
|
|||
import * as tunes from './tunes.mjs';
|
||||
import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon';
|
||||
import { themes } from './themes.mjs';
|
||||
import useTheme from '../useTheme';
|
||||
import { settingsMap, useSettings, setLatestCode } from '../settings.mjs';
|
||||
|
||||
const initialTheme = localStorage.getItem('strudel-theme') || 'strudelTheme';
|
||||
const { latestCode } = settingsMap.get();
|
||||
|
||||
initAudioOnFirstClick();
|
||||
|
||||
|
|
@ -113,25 +113,24 @@ export const ReplContext = createContext(null);
|
|||
export function Repl({ embedded = false }) {
|
||||
const isEmbedded = embedded || window.location !== window.parent.location;
|
||||
const [view, setView] = useState(); // codemirror view
|
||||
const [theme, setTheme] = useState(initialTheme);
|
||||
const [lastShared, setLastShared] = useState();
|
||||
const [activeFooter, setActiveFooter] = useState('');
|
||||
const [isZen, setIsZen] = useState(false);
|
||||
const [pending, setPending] = useState(false);
|
||||
|
||||
const { theme, keybindings, fontSize, fontFamily } = useSettings();
|
||||
|
||||
const { code, setCode, scheduler, evaluate, activateCode, isDirty, activeCode, pattern, started, stop, error } =
|
||||
useStrudel({
|
||||
initialCode: '// LOADING',
|
||||
defaultOutput: webaudioOutput,
|
||||
getTime,
|
||||
autolink: true,
|
||||
beforeEval: () => {
|
||||
cleanupUi();
|
||||
cleanupDraw();
|
||||
setPending(true);
|
||||
},
|
||||
afterEval: () => {
|
||||
afterEval: ({ code }) => {
|
||||
setPending(false);
|
||||
setLatestCode(code);
|
||||
},
|
||||
onToggle: (play) => !play && cleanupDraw(false),
|
||||
drawContext,
|
||||
|
|
@ -140,16 +139,13 @@ export function Repl({ embedded = false }) {
|
|||
// init code
|
||||
useEffect(() => {
|
||||
initCode().then((decoded) => {
|
||||
if (!decoded) {
|
||||
setActiveFooter('intro'); // TODO: get rid
|
||||
}
|
||||
logger(
|
||||
`Welcome to Strudel! ${
|
||||
decoded ? `I have loaded the code from the URL.` : `A random code snippet named "${name}" has been loaded!`
|
||||
} Press play or hit ctrl+enter to run it!`,
|
||||
'highlight',
|
||||
);
|
||||
setCode(decoded || randomTune);
|
||||
setCode(latestCode || decoded || randomTune);
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
|
@ -172,15 +168,12 @@ export function Repl({ embedded = false }) {
|
|||
),
|
||||
);
|
||||
|
||||
const { settings } = useTheme();
|
||||
|
||||
// highlighting
|
||||
useHighlighting({
|
||||
view,
|
||||
pattern,
|
||||
active: started && !activeCode?.includes('strudel disable-highlighting'),
|
||||
getTime: () => scheduler.now(),
|
||||
color: settings?.foreground,
|
||||
});
|
||||
|
||||
//
|
||||
|
|
@ -254,17 +247,11 @@ export function Repl({ embedded = false }) {
|
|||
isDirty,
|
||||
lastShared,
|
||||
activeCode,
|
||||
activeFooter,
|
||||
setActiveFooter,
|
||||
handleChangeCode,
|
||||
handleTogglePlay,
|
||||
handleUpdate,
|
||||
handleShuffle,
|
||||
handleShare,
|
||||
isZen,
|
||||
setIsZen,
|
||||
theme,
|
||||
setTheme,
|
||||
};
|
||||
return (
|
||||
// bg-gradient-to-t from-blue-900 to-slate-900
|
||||
|
|
@ -272,7 +259,7 @@ export function Repl({ embedded = false }) {
|
|||
<ReplContext.Provider value={context}>
|
||||
<div
|
||||
className={cx(
|
||||
'h-screen flex flex-col',
|
||||
'h-full flex flex-col',
|
||||
// 'bg-gradient-to-t from-green-900 to-slate-900', //
|
||||
)}
|
||||
>
|
||||
|
|
@ -281,6 +268,9 @@ export function Repl({ embedded = false }) {
|
|||
<CodeMirror
|
||||
theme={themes[theme] || themes.strudelTheme}
|
||||
value={code}
|
||||
keybindings={keybindings}
|
||||
fontSize={fontSize}
|
||||
fontFamily={fontFamily}
|
||||
onChange={handleChangeCode}
|
||||
onViewChanged={setView}
|
||||
onSelectionChange={handleSelectionChange}
|
||||
|
|
|
|||
|
|
@ -31,9 +31,19 @@ import {
|
|||
} from '@uiw/codemirror-themes-all';
|
||||
|
||||
import strudelTheme from '@strudel.cycles/react/src/themes/strudel-theme';
|
||||
import bluescreen, { settings as bluescreenSettings } from '@strudel.cycles/react/src/themes/bluescreen';
|
||||
import blackscreen, { settings as blackscreenSettings } from '@strudel.cycles/react/src/themes/blackscreen';
|
||||
import whitescreen, { settings as whitescreenSettings } from '@strudel.cycles/react/src/themes/whitescreen';
|
||||
import algoboy, { settings as algoboySettings } from '@strudel.cycles/react/src/themes/algoboy';
|
||||
import terminal, { settings as terminalSettings } from '@strudel.cycles/react/src/themes/terminal';
|
||||
|
||||
export const themes = {
|
||||
strudelTheme,
|
||||
bluescreen,
|
||||
blackscreen,
|
||||
whitescreen,
|
||||
algoboy,
|
||||
terminal,
|
||||
abcdef,
|
||||
androidstudio,
|
||||
atomone,
|
||||
|
|
@ -82,6 +92,11 @@ export const settings = {
|
|||
// gutterForeground: '#8a919966',
|
||||
gutterForeground: '#8a919966',
|
||||
},
|
||||
bluescreen: bluescreenSettings,
|
||||
blackscreen: blackscreenSettings,
|
||||
whitescreen: whitescreenSettings,
|
||||
algoboy: algoboySettings,
|
||||
terminal: terminalSettings,
|
||||
abcdef: {
|
||||
background: '#0f0f0f',
|
||||
lineBackground: '#0f0f0f50',
|
||||
|
|
@ -395,7 +410,6 @@ export const settings = {
|
|||
gutterBackground: '#1e1e1e',
|
||||
gutterForeground: '#838383',
|
||||
gutterActiveForeground: '#fff',
|
||||
fontFamily: 'Menlo, Monaco, Consolas, "Andale Mono", "Ubuntu Mono", "Courier New", monospace',
|
||||
},
|
||||
xcodeLight: {
|
||||
light: true,
|
||||
|
|
|
|||
28
website/src/settings.mjs
Normal file
28
website/src/settings.mjs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { persistentMap } from '@nanostores/persistent';
|
||||
import { useStore } from '@nanostores/react';
|
||||
|
||||
export const defaultSettings = {
|
||||
activeFooter: 'intro',
|
||||
keybindings: 'codemirror',
|
||||
theme: 'strudelTheme',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 18,
|
||||
latestCode: '',
|
||||
isZen: false,
|
||||
};
|
||||
|
||||
export const settingsMap = persistentMap('strudel-settings', defaultSettings);
|
||||
|
||||
export function useSettings() {
|
||||
const state = useStore(settingsMap);
|
||||
return {
|
||||
...state,
|
||||
isZen: [true, 'true'].includes(state.isZen) ? true : false,
|
||||
fontSize: Number(state.fontSize),
|
||||
};
|
||||
}
|
||||
|
||||
export const setActiveFooter = (tab) => settingsMap.setKey('activeFooter', tab);
|
||||
|
||||
export const setLatestCode = (code) => settingsMap.setKey('latestCode', code);
|
||||
export const setIsZen = (active) => settingsMap.setKey('isZen', !!active);
|
||||
|
|
@ -1,3 +1,16 @@
|
|||
@font-face {
|
||||
font-family: 'PressStart';
|
||||
src: url('/fonts/PressStart2P/PressStart2P-Regular.ttf');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'BigBlueTerminal';
|
||||
src: url('/fonts/BigBlueTerminal/BigBlue_TerminalPlus.TTF');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'x3270';
|
||||
src: url('/fonts/3270/3270-Regular.ttf');
|
||||
}
|
||||
|
||||
.cm-gutters {
|
||||
display: none !important;
|
||||
}
|
||||
|
|
@ -5,3 +18,11 @@
|
|||
.prose > h1:not(:first-child) {
|
||||
margin-top: 30px;
|
||||
}
|
||||
:root {
|
||||
--app-height: 100vh;
|
||||
}
|
||||
|
||||
#console-tab,
|
||||
#samples-tab {
|
||||
font-family: BigBlueTerminal, monospace;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
import { useState } from 'react';
|
||||
import { settings } from './repl/themes.mjs';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
function useTheme() {
|
||||
const [theme, setTheme] = useState(localStorage.getItem('strudel-theme'));
|
||||
useEvent('strudel-theme', (e) => setTheme(e.detail));
|
||||
const themeSettings = settings[theme || 'strudelTheme'];
|
||||
return {
|
||||
theme,
|
||||
setTheme,
|
||||
settings: themeSettings,
|
||||
isDark: !themeSettings.light,
|
||||
isLight: !!themeSettings.light,
|
||||
};
|
||||
}
|
||||
// TODO: dedupe
|
||||
function useEvent(name, onTrigger, useCapture = false) {
|
||||
useEffect(() => {
|
||||
document.addEventListener(name, onTrigger, useCapture);
|
||||
return () => {
|
||||
document.removeEventListener(name, onTrigger, useCapture);
|
||||
};
|
||||
}, [onTrigger]);
|
||||
}
|
||||
|
||||
export default useTheme;
|
||||
Loading…
Add table
Add a link
Reference in a new issue