mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-09 20:58:33 +00:00
basic astro docs
This commit is contained in:
parent
61657bd1b1
commit
bc7a84e462
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -5,5 +5,6 @@
|
||||
],
|
||||
"yaml.schemas": {
|
||||
"https://json.schemastore.org/github-workflow.json": "file:///home/felix/projects/strudel/.github/workflows/deploy.yml"
|
||||
}
|
||||
},
|
||||
"testing.automaticallyOpenPeekView": "never"
|
||||
}
|
||||
@ -2,7 +2,7 @@ export const logKey = 'strudel.log';
|
||||
|
||||
export function logger(message, type, data = {}) {
|
||||
console.log(`%c${message}`, 'background-color: black;color:white;border-radius:15px');
|
||||
if (typeof CustomEvent !== 'undefined') {
|
||||
if (typeof document !== 'undefined' && typeof CustomEvent !== 'undefined') {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(logKey, {
|
||||
detail: {
|
||||
|
||||
19
website/.gitignore
vendored
Normal file
19
website/.gitignore
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
# build output
|
||||
dist/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
4
website/.vscode/extensions.json
vendored
Normal file
4
website/.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
||||
11
website/.vscode/launch.json
vendored
Normal file
11
website/.vscode/launch.json
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "./node_modules/.bin/astro dev",
|
||||
"name": "Development server",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
174
website/README.md
Normal file
174
website/README.md
Normal file
@ -0,0 +1,174 @@
|
||||
# Astro Starter Kit: Docs Site
|
||||
|
||||
```bash
|
||||
npm create astro@latest -- --template docs
|
||||
```
|
||||
|
||||
[](https://stackblitz.com/github/withastro/astro/tree/latest/examples/docs)
|
||||
[](https://codesandbox.io/s/github/withastro/astro/tree/latest/examples/docs)
|
||||
|
||||

|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ **Full Markdown support**
|
||||
- ✅ **Responsive mobile-friendly design**
|
||||
- ✅ **Sidebar navigation**
|
||||
- ✅ **Search (powered by Algolia)**
|
||||
- ✅ **Multi-language i18n**
|
||||
- ✅ **Automatic table of contents**
|
||||
- ✅ **Automatic list of contributors**
|
||||
- ✅ (and, best of all) **dark mode**
|
||||
|
||||
## Commands Cheatsheet
|
||||
|
||||
All commands are run from the root of the project, from a terminal:
|
||||
|
||||
| Command | Action |
|
||||
| :--------------------- | :----------------------------------------------- |
|
||||
| `npm install` | Installs dependencies |
|
||||
| `npm run dev` | Starts local dev server at `localhost:3000` |
|
||||
| `npm run build` | Build your production site to `./dist/` |
|
||||
| `npm run preview` | Preview your build locally, before deploying |
|
||||
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||
| `npm run astro --help` | Get help using the Astro CLI |
|
||||
|
||||
To deploy your site to production, check out our [Deploy an Astro Website](https://docs.astro.build/guides/deploy) guide.
|
||||
|
||||
## New to Astro?
|
||||
|
||||
Welcome! Check out [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
|
||||
|
||||
## Customize This Theme
|
||||
|
||||
### Site metadata
|
||||
|
||||
`src/config.ts` contains several data objects that describe metadata about your site like title, description, default language, and Open Graph details. You can customize these to match your project.
|
||||
|
||||
### CSS styling
|
||||
|
||||
The theme's look and feel is controlled by a few key variables that you can customize yourself. You'll find them in the `src/styles/theme.css` CSS file.
|
||||
|
||||
If you've never worked with CSS variables before, give [MDN's guide on CSS variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties) a quick read.
|
||||
|
||||
This theme uses a "cool blue" accent color by default. To customize this for your project, change the `--theme-accent` variable to whatever color you'd like:
|
||||
|
||||
```diff
|
||||
/* src/styles/theme.css */
|
||||
:root {
|
||||
color-scheme: light;
|
||||
- --theme-accent: hsla(var(--color-blue), 1);
|
||||
+ --theme-accent: hsla(var(--color-red), 1); /* or: hsla(#FF0000, 1); */
|
||||
```
|
||||
|
||||
## Page metadata
|
||||
|
||||
Astro uses frontmatter in Markdown pages to choose layouts and pass properties to those layouts. If you are using the default layout, you can customize the page in many different ways to optimize SEO and other things. For example, you can use the `title` and `description` properties to set the document title, meta title, meta description, and Open Graph description.
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: Example title
|
||||
description: Really cool docs example that uses Astro
|
||||
layout: ../../layouts/MainLayout.astro
|
||||
---
|
||||
|
||||
# Page content...
|
||||
```
|
||||
|
||||
For more SEO related properties, look at `src/components/HeadSEO.astro`
|
||||
|
||||
### Sidebar navigation
|
||||
|
||||
The sidebar navigation is controlled by the `SIDEBAR` variable in your `src/config.ts` file. You can customize the sidebar by modifying this object. A default, starter navigation has already been created for you.
|
||||
|
||||
```ts
|
||||
export const SIDEBAR = {
|
||||
en: [
|
||||
{ text: "Section Header", header: true },
|
||||
{ text: "Introduction", link: "en/introduction" },
|
||||
{ text: "Page 2", link: "en/page-2" },
|
||||
{ text: "Page 3", link: "en/page-3" },
|
||||
|
||||
{ text: "Another Section", header: true },
|
||||
{ text: "Page 4", link: "en/page-4" },
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
Note the top-level `en` key: This is needed for multi-language support. You can change it to whatever language you'd like, or add new languages as you go. More details on this below.
|
||||
|
||||
### Multiple Languages support
|
||||
|
||||
The Astro docs template supports multiple languages out of the box. The default theme only shows `en` documentation, but you can enable multi-language support features by adding a second language to your project.
|
||||
|
||||
To add a new language to your project, you'll want to extend the current `src/pages/[lang]/...` layout:
|
||||
|
||||
```diff
|
||||
📂 src/pages
|
||||
┣ 📂 en
|
||||
┃ ┣ 📜 page-1.md
|
||||
┃ ┣ 📜 page-2.md
|
||||
┃ ┣ 📜 page-3.astro
|
||||
+ ┣ 📂 es
|
||||
+ ┃ ┣ 📜 page-1.md
|
||||
+ ┃ ┣ 📜 page-2.md
|
||||
+ ┃ ┣ 📜 page-3.astro
|
||||
```
|
||||
|
||||
You'll also need to add the new language name to the `KNOWN_LANGUAGES` map in your `src/config.ts` file. This will enable your new language switcher in the site header.
|
||||
|
||||
```diff
|
||||
// src/config.ts
|
||||
export const KNOWN_LANGUAGES = {
|
||||
English: 'en',
|
||||
+ Spanish: 'es',
|
||||
};
|
||||
```
|
||||
|
||||
Last step: you'll need to add a new entry to your sidebar, to create the table of contents for that language. While duplicating every page might not sound ideal to everyone, this extra control allows you to create entirely custom content for every language.
|
||||
|
||||
> Make sure the sidebar `link` value points to the correct language!
|
||||
|
||||
```diff
|
||||
// src/config.ts
|
||||
export const SIDEBAR = {
|
||||
en: [
|
||||
{ text: 'Section Header', header: true, },
|
||||
{ text: 'Introduction', link: 'en/introduction' },
|
||||
// ...
|
||||
],
|
||||
+ es: [
|
||||
+ { text: 'Encabezado de sección', header: true, },
|
||||
+ { text: 'Introducción', link: 'es/introduction' },
|
||||
+ // ...
|
||||
+ ],
|
||||
};
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
If you plan to use Spanish as the default language, you just need to modify the redirect path in `src/pages/index.astro`:
|
||||
|
||||
```diff
|
||||
<script>
|
||||
- window.location.pathname = `/en/introduction`;
|
||||
+ window.location.pathname = `/es/introduction`;
|
||||
</script>
|
||||
```
|
||||
|
||||
You can also remove the above script and write a landing page in Spanish instead.
|
||||
|
||||
### What if I don't plan to support multiple languages?
|
||||
|
||||
That's totally fine! Not all projects need (or can support) multiple languages. You can continue to use this theme without ever adding a second language.
|
||||
|
||||
If that single language is not English, you can just replace `en` in directory layouts and configurations with the preferred language.
|
||||
|
||||
### Search (Powered by Algolia)
|
||||
|
||||
[Algolia](https://www.algolia.com/) offers a free service to qualified open source projects called [DocSearch](https://docsearch.algolia.com/). If you are accepted to the DocSearch program, provide your API Key & index name in `src/config.ts` and a search box will automatically appear in your site header.
|
||||
|
||||
Note that Aglolia and Astro are not affiliated. We have no say over acceptance to the DocSearch program.
|
||||
|
||||
If you'd prefer to remove Algolia's search and replace it with your own, check out the `src/components/Header.astro` component to see where the component is added.
|
||||
21
website/astro.config.mjs
Normal file
21
website/astro.config.mjs
Normal file
@ -0,0 +1,21 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
import preact from '@astrojs/preact';
|
||||
import react from '@astrojs/react';
|
||||
|
||||
// https://astro.build/config
|
||||
import mdx from '@astrojs/mdx';
|
||||
|
||||
// https://astro.build/config
|
||||
|
||||
// https://astro.build/config
|
||||
import tailwind from "@astrojs/tailwind";
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
integrations: [
|
||||
// Enable Preact to support Preact JSX components.
|
||||
preact(),
|
||||
// Enable React for the Algolia search component.
|
||||
react(), mdx(), tailwind()],
|
||||
site: `https://strudel.tidalcycles.org`
|
||||
});
|
||||
11841
website/package-lock.json
generated
Normal file
11841
website/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
website/package.json
Normal file
35
website/package.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "@example/docs",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"check": "astro check && tsc",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@algolia/client-search": "^4.13.1",
|
||||
"@astrojs/mdx": "^0.13.0",
|
||||
"@astrojs/preact": "^1.2.0",
|
||||
"@astrojs/react": "^1.2.2",
|
||||
"@astrojs/tailwind": "^2.1.3",
|
||||
"@docsearch/css": "^3.1.0",
|
||||
"@docsearch/react": "^3.1.0",
|
||||
"@tailwindcss/typography": "^0.5.8",
|
||||
"@types/node": "^18.0.0",
|
||||
"@types/react": "^18.0.26",
|
||||
"@types/react-dom": "^18.0.9",
|
||||
"astro": "^1.7.2",
|
||||
"preact": "^10.7.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"tailwindcss": "^3.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"html-escaper": "^3.0.3"
|
||||
}
|
||||
}
|
||||
BIN
website/public/default-og-image.png
Normal file
BIN
website/public/default-og-image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 731 KiB |
13
website/public/favicon.svg
Normal file
13
website/public/favicon.svg
Normal file
@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 36 36">
|
||||
<path fill="#000" d="M22.25 4h-8.5a1 1 0 0 0-.96.73l-5.54 19.4a.5.5 0 0 0 .62.62l5.05-1.44a2 2 0 0 0 1.38-1.4l3.22-11.66a.5.5 0 0 1 .96 0l3.22 11.67a2 2 0 0 0 1.38 1.39l5.05 1.44a.5.5 0 0 0 .62-.62l-5.54-19.4a1 1 0 0 0-.96-.73Z"/>
|
||||
<path fill="url(#gradient)" d="M18 28a7.63 7.63 0 0 1-5-2c-1.4 2.1-.35 4.35.6 5.55.14.17.41.07.47-.15.44-1.8 2.93-1.22 2.93.6 0 2.28.87 3.4 1.72 3.81.34.16.59-.2.49-.56-.31-1.05-.29-2.46 1.29-3.25 3-1.5 3.17-4.83 2.5-6-.67.67-2.6 2-5 2Z"/>
|
||||
<defs>
|
||||
<linearGradient id="gradient" x1="16" x2="16" y1="32" y2="24" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#000"/>
|
||||
<stop offset="1" stop-color="#000" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<style>
|
||||
@media (prefers-color-scheme:dark){:root{filter:invert(100%)}}
|
||||
</style>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 873 B |
3
website/public/make-scrollable-code-focusable.js
Normal file
3
website/public/make-scrollable-code-focusable.js
Normal file
@ -0,0 +1,3 @@
|
||||
Array.from(document.getElementsByTagName('pre')).forEach((element) => {
|
||||
element.setAttribute('tabindex', '0');
|
||||
});
|
||||
171
website/src/components/Footer/AvatarList.astro
Normal file
171
website/src/components/Footer/AvatarList.astro
Normal file
@ -0,0 +1,171 @@
|
||||
---
|
||||
// fetch all commits for just this page's path
|
||||
type Props = {
|
||||
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}`;
|
||||
|
||||
type Commit = {
|
||||
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.'
|
||||
);
|
||||
}
|
||||
|
||||
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 data = await res.json();
|
||||
|
||||
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
|
||||
${(e as any)?.message ?? e}`);
|
||||
return [] as Commit[];
|
||||
}
|
||||
}
|
||||
|
||||
function removeDups(arr: Commit[]) {
|
||||
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 });
|
||||
}
|
||||
|
||||
return [...map.values()];
|
||||
}
|
||||
|
||||
const data = await getCommits(url);
|
||||
const unique = removeDups(data);
|
||||
const recentContributors = unique.slice(0, 3); // only show avatars for the 3 most recent contributors
|
||||
const additionalContributors = unique.length - recentContributors.length; // list the rest of them as # of extra contributors
|
||||
---
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<style>
|
||||
.avatar-list {
|
||||
--avatar-size: 2.5rem;
|
||||
--avatar-count: 3;
|
||||
|
||||
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
|
||||
the `box-shadow` to help create a more accurate
|
||||
computed component 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
|
||||
width to match the final visual width */
|
||||
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: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 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 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 > * + * {
|
||||
margin-left: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
19
website/src/components/Footer/Footer.astro
Normal file
19
website/src/components/Footer/Footer.astro
Normal file
@ -0,0 +1,19 @@
|
||||
---
|
||||
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>
|
||||
44
website/src/components/HeadCommon.astro
Normal file
44
website/src/components/HeadCommon.astro
Normal file
@ -0,0 +1,44 @@
|
||||
---
|
||||
import '../styles/theme.css';
|
||||
import '../styles/index.css';
|
||||
---
|
||||
|
||||
<!-- Global Metadata -->
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
|
||||
<link rel="sitemap" href="/sitemap.xml" />
|
||||
|
||||
<!-- Preload Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital@0;1&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- Scrollable a11y code helper -->
|
||||
<script src="/make-scrollable-code-focusable.js" is:inline></script>
|
||||
|
||||
<!-- This is intentionally inlined to avoid FOUC -->
|
||||
<script is:inline>
|
||||
const root = document.documentElement;
|
||||
const theme = localStorage.getItem('theme');
|
||||
if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
root.classList.add('theme-dark');
|
||||
} else {
|
||||
root.classList.remove('theme-dark');
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||||
<!-- <script async src="https://www.googletagmanager.com/gtag/js?id=G-TEL60V1WM9" is:inline></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-TEL60V1WM9');
|
||||
</script> -->
|
||||
46
website/src/components/HeadSEO.astro
Normal file
46
website/src/components/HeadSEO.astro
Normal file
@ -0,0 +1,46 @@
|
||||
---
|
||||
import { SITE, OPEN_GRAPH, Frontmatter } from '../config';
|
||||
|
||||
export interface Props {
|
||||
frontmatter: Frontmatter;
|
||||
canonicalUrl: URL;
|
||||
}
|
||||
|
||||
const { frontmatter, canonicalUrl } = Astro.props as Props;
|
||||
const formattedContentTitle = `${frontmatter.title} 🚀 ${SITE.title}`;
|
||||
const imageSrc = frontmatter.image?.src ?? OPEN_GRAPH.image.src;
|
||||
const canonicalImageSrc = new URL(imageSrc, Astro.site);
|
||||
const imageAlt = frontmatter.image?.alt ?? OPEN_GRAPH.image.alt;
|
||||
---
|
||||
|
||||
<!-- Page Metadata -->
|
||||
<link rel="canonical" href={canonicalUrl} />
|
||||
|
||||
<!-- OpenGraph Tags -->
|
||||
<meta property="og:title" content={formattedContentTitle} />
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:url" content={canonicalUrl} />
|
||||
<meta property="og:locale" content={frontmatter.ogLocale ?? SITE.defaultLanguage} />
|
||||
<meta property="og:image" content={canonicalImageSrc} />
|
||||
<meta property="og:image:alt" content={imageAlt} />
|
||||
<meta
|
||||
name="description"
|
||||
property="og:description"
|
||||
content={frontmatter.description ?? SITE.description}
|
||||
/>
|
||||
<meta property="og:site_name" content={SITE.title} />
|
||||
|
||||
<!-- Twitter Tags -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:site" content={OPEN_GRAPH.twitter} />
|
||||
<meta name="twitter:title" content={formattedContentTitle} />
|
||||
<meta name="twitter:description" content={frontmatter.description ?? SITE.description} />
|
||||
<meta name="twitter:image" content={canonicalImageSrc} />
|
||||
<meta name="twitter:image:alt" content={imageAlt} />
|
||||
|
||||
<!--
|
||||
TODO: Add json+ld data, maybe https://schema.org/APIReference makes sense?
|
||||
Docs: https://developers.google.com/search/docs/advanced/structured-data/intro-structured-data
|
||||
https://www.npmjs.com/package/schema-dts seems like a great resource for implementing this.
|
||||
Even better, there's a React component that integrates with `schema-dts`: https://github.com/google/react-schemaorg
|
||||
-->
|
||||
40
website/src/components/Header/AstroLogo.astro
Normal file
40
website/src/components/Header/AstroLogo.astro
Normal file
@ -0,0 +1,40 @@
|
||||
---
|
||||
type Props = {
|
||||
size: number;
|
||||
};
|
||||
const { size } = Astro.props as Props;
|
||||
---
|
||||
|
||||
<svg
|
||||
class="logo"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 256 256"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<style>
|
||||
#flame {
|
||||
fill: var(--theme-text-accent);
|
||||
}
|
||||
|
||||
#a {
|
||||
fill: var(--theme-text-accent);
|
||||
}
|
||||
</style>
|
||||
<title>Logo</title>
|
||||
<path
|
||||
id="a"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M163.008 18.929c1.944 2.413 2.935 5.67 4.917 12.181l43.309 142.27a180.277 180.277 0 00-51.778-17.53l-28.198-95.29a3.67 3.67 0 00-7.042.01l-27.857 95.232a180.225 180.225 0 00-52.01 17.557l43.52-142.281c1.99-6.502 2.983-9.752 4.927-12.16a15.999 15.999 0 016.484-4.798c2.872-1.154 6.271-1.154 13.07-1.154h31.085c6.807 0 10.211 0 13.086 1.157a16.004 16.004 0 016.487 4.806z"
|
||||
>
|
||||
</path>
|
||||
<path
|
||||
id="flame"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M168.19 180.151c-7.139 6.105-21.39 10.268-37.804 10.268-20.147 0-37.033-6.272-41.513-14.707-1.602 4.835-1.961 10.367-1.961 13.902 0 0-1.056 17.355 11.015 29.426 0-6.268 5.081-11.349 11.349-11.349 10.743 0 10.731 9.373 10.721 16.977v.679c0 11.542 7.054 21.436 17.086 25.606a23.27 23.27 0 01-2.339-10.2c0-11.008 6.463-15.107 13.974-19.87 5.976-3.79 12.616-8.001 17.192-16.449a31.024 31.024 0 003.743-14.82c0-3.299-.513-6.479-1.463-9.463z"
|
||||
>
|
||||
</path>
|
||||
</svg>
|
||||
149
website/src/components/Header/DefaultHeader.astro
Normal file
149
website/src/components/Header/DefaultHeader.astro
Normal file
@ -0,0 +1,149 @@
|
||||
---
|
||||
// import { getLanguageFromURL, KNOWN_LANGUAGE_CODES } from '../../languages';
|
||||
import * as CONFIG from '../../config';
|
||||
// import AstroLogo from './AstroLogo.astro';
|
||||
import SkipToContent from './SkipToContent.astro';
|
||||
import SidebarToggle from './SidebarToggle';
|
||||
// import LanguageSelect from './LanguageSelect';
|
||||
import Search from './Search';
|
||||
|
||||
type Props = {
|
||||
currentPage: string;
|
||||
};
|
||||
|
||||
const { currentPage } = Astro.props as Props;
|
||||
// const lang = getLanguageFromURL(currentPage);
|
||||
---
|
||||
|
||||
<header>
|
||||
<SkipToContent />
|
||||
<nav class="nav-wrapper" title="Top Navigation">
|
||||
<div class="menu-toggle">
|
||||
<SidebarToggle client:idle />
|
||||
</div>
|
||||
<div class="logo flex" style="overflow:visible">
|
||||
<a href="/">
|
||||
<div>🌀</div>
|
||||
<h1>{CONFIG.SITE.title ?? 'Documentation'}</h1>
|
||||
</a>
|
||||
</div>
|
||||
<div style="flex-grow: 1;"></div>
|
||||
{/* KNOWN_LANGUAGE_CODES.length > 1 && <LanguageSelect lang={lang} client:idle /> */}
|
||||
<div class="search-item">
|
||||
<Search client:idle />
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
header {
|
||||
z-index: 11;
|
||||
height: var(--theme-navbar-height);
|
||||
width: 100%;
|
||||
background-color: var(--theme-navbar-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.logo {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
width: 30px;
|
||||
font-size: 2rem;
|
||||
flex-shrink: 0;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
color: hsla(var(--color-base-white), 100%, 1);
|
||||
gap: 0.25em;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.logo a {
|
||||
display: flex;
|
||||
padding: 0.5em 0.25em;
|
||||
margin: -0.5em -0.25em;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.logo a {
|
||||
transition: color 100ms ease-out;
|
||||
color: var(--theme-text);
|
||||
}
|
||||
|
||||
.logo a:hover,
|
||||
.logo a:focus {
|
||||
color: var(--theme-text-accent);
|
||||
}
|
||||
|
||||
.logo h1 {
|
||||
display: none;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.nav-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
width: 100%;
|
||||
max-width: 82em;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 50em) {
|
||||
header {
|
||||
position: static;
|
||||
padding: 2rem 0rem;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.logo h1 {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/** Style Algolia */
|
||||
:root {
|
||||
--docsearch-primary-color: var(--theme-accent);
|
||||
--docsearch-logo-color: var(--theme-text);
|
||||
}
|
||||
|
||||
.search-item {
|
||||
display: none;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
flex-grow: 1;
|
||||
padding-right: 0.7rem;
|
||||
display: flex;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
@media (min-width: 50em) {
|
||||
.search-item {
|
||||
max-width: 400px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style is:global>
|
||||
.search-item > * {
|
||||
flex-grow: 1;
|
||||
}
|
||||
</style>
|
||||
64
website/src/components/Header/Header.astro
Normal file
64
website/src/components/Header/Header.astro
Normal file
@ -0,0 +1,64 @@
|
||||
---
|
||||
// import { getLanguageFromURL, KNOWN_LANGUAGE_CODES } from '../../languages';
|
||||
import * as CONFIG from '../../config';
|
||||
// import AstroLogo from './AstroLogo.astro';
|
||||
import SkipToContent from './SkipToContent.astro';
|
||||
import SidebarToggle from './SidebarToggle';
|
||||
// import LanguageSelect from './LanguageSelect';
|
||||
import Search from './Search';
|
||||
|
||||
type Props = {
|
||||
currentPage: string;
|
||||
};
|
||||
|
||||
const { currentPage } = Astro.props as Props;
|
||||
// const lang = getLanguageFromURL(currentPage);
|
||||
---
|
||||
|
||||
<SkipToContent />
|
||||
<nav class="flex justify-between py-2" title="Top Navigation">
|
||||
<!-- <div class="menu-toggle">
|
||||
<SidebarToggle client:idle />
|
||||
</div> -->
|
||||
<div class="flex overflow-visible" style="overflow:visible">
|
||||
<a href="/" class="flex items-center text-2xl space-x-2">
|
||||
<div>🌀</div>
|
||||
<h1>{CONFIG.SITE.title ?? 'Documentation'}</h1>
|
||||
</a>
|
||||
</div>
|
||||
<div style="flex-grow: 1;"></div>
|
||||
{/* KNOWN_LANGUAGE_CODES.length > 1 && <LanguageSelect lang={lang} client:idle /> */}
|
||||
<div class="search-item h-10">
|
||||
<Search client:idle />
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
/** Style Algolia */
|
||||
:root {
|
||||
--docsearch-primary-color: var(--theme-accent);
|
||||
--docsearch-logo-color: var(--theme-text);
|
||||
}
|
||||
|
||||
.search-item {
|
||||
display: none;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
flex-grow: 1;
|
||||
padding-right: 0.7rem;
|
||||
display: flex;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
@media (min-width: 50em) {
|
||||
.search-item {
|
||||
max-width: 400px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style is:global>
|
||||
.search-item > * {
|
||||
flex-grow: 1;
|
||||
}
|
||||
</style>
|
||||
47
website/src/components/Header/LanguageSelect.css
Normal file
47
website/src/components/Header/LanguageSelect.css
Normal file
@ -0,0 +1,47 @@
|
||||
.language-select {
|
||||
flex-grow: 1;
|
||||
width: 48px;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0.33em 0.5em;
|
||||
overflow: visible;
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
font-family: inherit;
|
||||
line-height: inherit;
|
||||
background-color: var(--theme-bg);
|
||||
border-color: var(--theme-text-lighter);
|
||||
color: var(--theme-text-light);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-radius: 0.25rem;
|
||||
outline: 0;
|
||||
cursor: pointer;
|
||||
transition-timing-function: ease-out;
|
||||
transition-duration: 0.2s;
|
||||
transition-property: border-color, color;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
padding-left: 30px;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
.language-select-wrapper .language-select:hover,
|
||||
.language-select-wrapper .language-select:focus {
|
||||
color: var(--theme-text);
|
||||
border-color: var(--theme-text-light);
|
||||
}
|
||||
.language-select-wrapper {
|
||||
color: var(--theme-text-light);
|
||||
position: relative;
|
||||
}
|
||||
.language-select-wrapper > svg {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
left: 10px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (min-width: 50em) {
|
||||
.language-select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
49
website/src/components/Header/LanguageSelect.tsx
Normal file
49
website/src/components/Header/LanguageSelect.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
/** @jsxImportSource react */
|
||||
import type { FunctionComponent } from 'react';
|
||||
import './LanguageSelect.css';
|
||||
import { KNOWN_LANGUAGES, langPathRegex } from '../../languages';
|
||||
|
||||
const LanguageSelect: FunctionComponent<{ lang: string }> = ({ lang }) => {
|
||||
return (
|
||||
<div className="language-select-wrapper">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
role="img"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 88.6 77.3"
|
||||
height="1.2em"
|
||||
width="1.2em"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M61,24.6h7.9l18.7,51.6h-7.7l-5.4-15.5H54.3l-5.6,15.5h-7.2L61,24.6z M72.6,55l-8-22.8L56.3,55H72.6z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M53.6,60.6c-10-4-16-9-22-14c0,0,1.3,1.3,0,0c-6,5-20,13-20,13l-4-6c8-5,10-6,19-13c-2.1-1.9-12-13-13-19h8 c4,9,10,14,10,14c10-8,10-19,10-19h8c0,0-1,13-12,24l0,0c5,5,10,9,19,13L53.6,60.6z M1.6,16.6h56v-8h-23v-7h-9v7h-24V16.6z"
|
||||
/>
|
||||
</svg>
|
||||
<select
|
||||
className="language-select"
|
||||
value={lang}
|
||||
onChange={(e) => {
|
||||
const newLang = e.target.value;
|
||||
let actualDest = window.location.pathname.replace(langPathRegex, '/');
|
||||
if (actualDest == '/') actualDest = `/introduction`;
|
||||
window.location.pathname = '/' + newLang + actualDest;
|
||||
}}
|
||||
>
|
||||
{Object.entries(KNOWN_LANGUAGES).map(([key, value]) => {
|
||||
return (
|
||||
<option value={value} key={value}>
|
||||
{key}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LanguageSelect;
|
||||
75
website/src/components/Header/Search.css
Normal file
75
website/src/components/Header/Search.css
Normal file
@ -0,0 +1,75 @@
|
||||
/** Style Algolia */
|
||||
:root {
|
||||
--docsearch-primary-color: var(--theme-accent);
|
||||
--docsearch-logo-color: var(--theme-text);
|
||||
}
|
||||
.search-input {
|
||||
flex-grow: 1;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0.33em 0.5;
|
||||
overflow: visible;
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
font-family: inherit;
|
||||
line-height: inherit;
|
||||
background-color: var(--theme-divider);
|
||||
border-color: var(--theme-divider);
|
||||
color: var(--theme-text-light);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-radius: 0.25rem;
|
||||
outline: 0;
|
||||
cursor: pointer;
|
||||
transition-timing-function: ease-out;
|
||||
transition-duration: 0.2s;
|
||||
transition-property: border-color, color;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.search-input:hover,
|
||||
.search-input:focus {
|
||||
color: var(--theme-text);
|
||||
border-color: var(--theme-text-light);
|
||||
}
|
||||
.search-input:hover::placeholder,
|
||||
.search-input:focus::placeholder {
|
||||
color: var(--theme-text-light);
|
||||
}
|
||||
.search-input::placeholder {
|
||||
color: var(--theme-text-light);
|
||||
}
|
||||
.search-hint {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 19px;
|
||||
padding: 3px 5px;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
letter-spacing: 0.125em;
|
||||
font-size: 13px;
|
||||
font-family: var(--font-mono);
|
||||
pointer-events: none;
|
||||
border-color: var(--theme-text-lighter);
|
||||
color: var(--theme-text-light);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-radius: 0.25rem;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
@media (min-width: 50em) {
|
||||
.search-hint {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ *\
|
||||
DocSearch (Algolia)
|
||||
\* ------------------------------------------------------------ */
|
||||
|
||||
.DocSearch-Modal .DocSearch-Hit a {
|
||||
box-shadow: none;
|
||||
border: 1px solid var(--theme-accent);
|
||||
}
|
||||
95
website/src/components/Header/Search.tsx
Normal file
95
website/src/components/Header/Search.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
/** @jsxImportSource react */
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import { ALGOLIA } from '../../config';
|
||||
import '@docsearch/css';
|
||||
import './Search.css';
|
||||
|
||||
import { createPortal } from 'react-dom';
|
||||
import * as docSearchReact from '@docsearch/react';
|
||||
|
||||
/** FIXME: This is still kinda nasty, but DocSearch is not ESM ready. */
|
||||
const DocSearchModal = docSearchReact.DocSearchModal || (docSearchReact as any).default.DocSearchModal;
|
||||
const useDocSearchKeyboardEvents =
|
||||
docSearchReact.useDocSearchKeyboardEvents || (docSearchReact as any).default.useDocSearchKeyboardEvents;
|
||||
|
||||
export default function Search() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const searchButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const [initialQuery, setInitialQuery] = useState('');
|
||||
|
||||
const onOpen = useCallback(() => {
|
||||
setIsOpen(true);
|
||||
}, [setIsOpen]);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
}, [setIsOpen]);
|
||||
|
||||
const onInput = useCallback(
|
||||
(e) => {
|
||||
setIsOpen(true);
|
||||
setInitialQuery(e.key);
|
||||
},
|
||||
[setIsOpen, setInitialQuery],
|
||||
);
|
||||
|
||||
useDocSearchKeyboardEvents({
|
||||
isOpen,
|
||||
onOpen,
|
||||
onClose,
|
||||
onInput,
|
||||
searchButtonRef,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<button type="button" ref={searchButtonRef} onClick={onOpen} className="rounded-md bg-slate-900 w-full px-2">
|
||||
<svg width="24" height="24" fill="none">
|
||||
<path
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
{/* <span>Search</span> */}
|
||||
|
||||
<span className="search-hint">
|
||||
<span className="sr-only">Press </span>
|
||||
|
||||
<kbd>/</kbd>
|
||||
|
||||
<span className="sr-only"> to search</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{isOpen &&
|
||||
createPortal(
|
||||
<DocSearchModal
|
||||
initialQuery={initialQuery}
|
||||
initialScrollY={window.scrollY}
|
||||
onClose={onClose}
|
||||
indexName={ALGOLIA.indexName}
|
||||
appId={ALGOLIA.appId}
|
||||
apiKey={ALGOLIA.apiKey}
|
||||
transformItems={(items) => {
|
||||
return items.map((item) => {
|
||||
// We transform the absolute URL into a relative URL to
|
||||
// work better on localhost, preview URLS.
|
||||
const a = document.createElement('a');
|
||||
a.href = item.url;
|
||||
const hash = a.hash === '#overview' ? '' : a.hash;
|
||||
return {
|
||||
...item,
|
||||
url: `${a.pathname}${hash}`,
|
||||
};
|
||||
});
|
||||
}}
|
||||
/>,
|
||||
document.body,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
44
website/src/components/Header/SidebarToggle.tsx
Normal file
44
website/src/components/Header/SidebarToggle.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
/** @jsxImportSource preact */
|
||||
import type { FunctionalComponent } from 'preact';
|
||||
import { useState, useEffect } from 'preact/hooks';
|
||||
|
||||
const MenuToggle: FunctionalComponent = () => {
|
||||
const [sidebarShown, setSidebarShown] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const body = document.querySelector('body')!;
|
||||
if (sidebarShown) {
|
||||
body.classList.add('mobile-sidebar-toggle');
|
||||
} else {
|
||||
body.classList.remove('mobile-sidebar-toggle');
|
||||
}
|
||||
}, [sidebarShown]);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={sidebarShown ? 'true' : 'false'}
|
||||
id="menu-toggle"
|
||||
onClick={() => setSidebarShown(!sidebarShown)}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
/>
|
||||
</svg>
|
||||
<span className="sr-only">Toggle sidebar</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default MenuToggle;
|
||||
26
website/src/components/Header/SkipToContent.astro
Normal file
26
website/src/components/Header/SkipToContent.astro
Normal file
@ -0,0 +1,26 @@
|
||||
---
|
||||
type Props = {};
|
||||
---
|
||||
|
||||
<a href="#article" class="sr-only focus:not-sr-only skiplink"><span>Skip to Content</span></a>
|
||||
|
||||
<style>
|
||||
.skiplink,
|
||||
.skiplink:focus,
|
||||
.skiplink:focus-visible {
|
||||
position: absolute;
|
||||
padding: 0.25em;
|
||||
font-size: larger;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 9;
|
||||
display: block;
|
||||
text-align: center;
|
||||
background-color: var(--theme-text-accent);
|
||||
color: var(--theme-bg);
|
||||
border-radius: 0.25em;
|
||||
outline: var(--theme-bg) solid 1px;
|
||||
outline-offset: 0;
|
||||
}
|
||||
</style>
|
||||
119
website/src/components/LeftSidebar/LeftSidebar.astro
Normal file
119
website/src/components/LeftSidebar/LeftSidebar.astro
Normal file
@ -0,0 +1,119 @@
|
||||
---
|
||||
// import { getLanguageFromURL } from '../../languages';
|
||||
import { SIDEBAR } from '../../config';
|
||||
|
||||
type Props = {
|
||||
currentPage: string;
|
||||
};
|
||||
|
||||
const { currentPage } = Astro.props as Props;
|
||||
const currentPageMatch = currentPage.endsWith('/')
|
||||
? currentPage.slice(1, -1)
|
||||
: currentPage.slice(1);
|
||||
const langCode = 'en'; // getLanguageFromURL(currentPage);
|
||||
const sidebar = SIDEBAR[langCode];
|
||||
---
|
||||
|
||||
<nav aria-labelledby="grid-left">
|
||||
<ul class="nav-groups">
|
||||
{
|
||||
Object.entries(sidebar).map(([header, children]) => (
|
||||
<li>
|
||||
<div class="nav-group">
|
||||
<h2>{header}</h2>
|
||||
<ul>
|
||||
{children.map((child) => {
|
||||
const url = Astro.site?.pathname + child.link;
|
||||
return (
|
||||
<li class="nav-link">
|
||||
<a href={url} aria-current={currentPageMatch === child.link ? 'page' : false}>
|
||||
{child.text}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<script is:inline>
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
var target = document.querySelector('[aria-current="page"]');
|
||||
if (target && target.offsetTop > window.innerHeight - 100) {
|
||||
document.querySelector('.nav-groups').scrollTop = target.offsetTop;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
nav {
|
||||
width: 100%;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.nav-groups {
|
||||
height: 100%;
|
||||
padding: 2rem 0;
|
||||
overflow-x: visible;
|
||||
overflow-y: auto;
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.nav-groups > li + li {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.nav-groups > :first-child {
|
||||
padding-top: var(--doc-padding);
|
||||
}
|
||||
|
||||
.nav-groups > :last-child {
|
||||
padding-bottom: 2rem;
|
||||
margin-bottom: var(--theme-navbar-height);
|
||||
}
|
||||
|
||||
.nav-group-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
padding: 0.1rem 1rem;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-link a {
|
||||
font-size: 1rem;
|
||||
margin: 1px;
|
||||
padding: 0.3rem 1rem;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-link a:hover,
|
||||
.nav-link a:focus {
|
||||
background-color: var(--theme-bg-hover);
|
||||
}
|
||||
|
||||
.nav-link a[aria-current='page'] {
|
||||
color: var(--theme-text-accent);
|
||||
background-color: var(--theme-bg-accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@media (min-width: 50em) {
|
||||
.nav-groups {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style is:global>
|
||||
:root.theme-dark .nav-link a[aria-current='page'] {
|
||||
color: hsla(var(--color-base-white), 100%, 1);
|
||||
}
|
||||
</style>
|
||||
54
website/src/components/PageContent/PageContent.astro
Normal file
54
website/src/components/PageContent/PageContent.astro
Normal file
@ -0,0 +1,54 @@
|
||||
---
|
||||
import type { Frontmatter } from '../../config';
|
||||
import MoreMenu from '../RightSidebar/MoreMenu.astro';
|
||||
import TableOfContents from '../RightSidebar/TableOfContents';
|
||||
import type { MarkdownHeading } from 'astro';
|
||||
|
||||
type Props = {
|
||||
frontmatter: Frontmatter;
|
||||
headings: MarkdownHeading[];
|
||||
githubEditUrl: string;
|
||||
};
|
||||
|
||||
const { frontmatter, headings, githubEditUrl } = Astro.props as Props;
|
||||
const title = frontmatter.title;
|
||||
---
|
||||
|
||||
<article id="article" class="content">
|
||||
<section class="main-section">
|
||||
<nav class="block sm:hidden">
|
||||
<TableOfContents client:media="(max-width: 50em)" headings={headings} />
|
||||
</nav>
|
||||
<div class="prose prose-invert max-w-full">
|
||||
<slot />
|
||||
</div>
|
||||
</section>
|
||||
<nav class="block sm:hidden">
|
||||
<MoreMenu editHref={githubEditUrl} />
|
||||
</nav>
|
||||
</article>
|
||||
|
||||
<style>
|
||||
.content {
|
||||
padding: 0;
|
||||
max-width: 75ch;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content > section {
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (min-width: 50em) {
|
||||
.sm\:hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
79
website/src/components/RightSidebar/MoreMenu.astro
Normal file
79
website/src/components/RightSidebar/MoreMenu.astro
Normal file
@ -0,0 +1,79 @@
|
||||
---
|
||||
import ThemeToggleButton from './ThemeToggleButton';
|
||||
import * as CONFIG from '../../config';
|
||||
|
||||
type Props = {
|
||||
editHref: string;
|
||||
};
|
||||
|
||||
const { editHref } = Astro.props as Props;
|
||||
const showMoreSection = CONFIG.COMMUNITY_INVITE_URL;
|
||||
---
|
||||
|
||||
{showMoreSection && <h2 class="heading">More</h2>}
|
||||
<ul>
|
||||
{
|
||||
editHref && (
|
||||
<li class={`header-link depth-2`}>
|
||||
<a class="edit-on-github" href={editHref} target="_blank">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
data-prefix="fas"
|
||||
data-icon="pen"
|
||||
class="svg-inline--fa fa-pen fa-w-16"
|
||||
role="img"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
height="1em"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M290.74 93.24l128.02 128.02-277.99 277.99-114.14 12.6C11.35 513.54-1.56 500.62.14 485.34l12.7-114.22 277.9-277.88zm207.2-19.06l-60.11-60.11c-18.75-18.75-49.16-18.75-67.91 0l-56.55 56.55 128.02 128.02 56.55-56.55c18.75-18.76 18.75-49.16 0-67.91z"
|
||||
/>
|
||||
</svg>
|
||||
<span>Edit this page</span>
|
||||
</a>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
{
|
||||
CONFIG.COMMUNITY_INVITE_URL && (
|
||||
<li class={`header-link depth-2`}>
|
||||
<a href={CONFIG.COMMUNITY_INVITE_URL} target="_blank">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
data-prefix="fas"
|
||||
data-icon="comment-alt"
|
||||
class="svg-inline--fa fa-comment-alt fa-w-16"
|
||||
role="img"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
height="1em"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M448 0H64C28.7 0 0 28.7 0 64v288c0 35.3 28.7 64 64 64h96v84c0 9.8 11.2 15.5 19.1 9.7L304 416h144c35.3 0 64-28.7 64-64V64c0-35.3-28.7-64-64-64z"
|
||||
/>
|
||||
</svg>
|
||||
<span>Join our community</span>
|
||||
</a>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
<div style="margin: 2rem 0; text-align: center;">
|
||||
<ThemeToggleButton client:visible />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.edit-on-github {
|
||||
text-decoration: none;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
font-size: 1rem;
|
||||
}
|
||||
</style>
|
||||
34
website/src/components/RightSidebar/RightSidebar.astro
Normal file
34
website/src/components/RightSidebar/RightSidebar.astro
Normal file
@ -0,0 +1,34 @@
|
||||
---
|
||||
import TableOfContents from './TableOfContents';
|
||||
import MoreMenu from './MoreMenu.astro';
|
||||
import type { MarkdownHeading } from 'astro';
|
||||
|
||||
type Props = {
|
||||
headings: MarkdownHeading[];
|
||||
githubEditUrl: string;
|
||||
};
|
||||
|
||||
const { headings, githubEditUrl } = Astro.props as Props;
|
||||
---
|
||||
|
||||
<nav class="sidebar-nav" aria-labelledby="grid-right">
|
||||
<div class="sidebar-nav-inner">
|
||||
<TableOfContents client:media="(min-width: 50em)" headings={headings} />
|
||||
<MoreMenu editHref={githubEditUrl} />
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
.sidebar-nav {
|
||||
width: 100%;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.sidebar-nav-inner {
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
padding-top: var(--doc-padding);
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
93
website/src/components/RightSidebar/TableOfContents.tsx
Normal file
93
website/src/components/RightSidebar/TableOfContents.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
import { unescape } from 'html-escaper';
|
||||
import type { MarkdownHeading } from 'astro';
|
||||
import type { FunctionalComponent } from 'preact';
|
||||
import { useState, useEffect, useRef } from 'preact/hooks';
|
||||
|
||||
type ItemOffsets = {
|
||||
id: string;
|
||||
topOffset: number;
|
||||
};
|
||||
|
||||
const TableOfContents: FunctionalComponent<{ headings: MarkdownHeading[] }> = ({
|
||||
headings = [],
|
||||
}) => {
|
||||
const toc = useRef<HTMLUListElement>();
|
||||
const onThisPageID = 'on-this-page-heading';
|
||||
const itemOffsets = useRef<ItemOffsets[]>([]);
|
||||
const [currentID, setCurrentID] = useState('overview');
|
||||
useEffect(() => {
|
||||
const getItemOffsets = () => {
|
||||
const titles = document.querySelectorAll('article :is(h1, h2, h3, h4)');
|
||||
itemOffsets.current = Array.from(titles).map((title) => ({
|
||||
id: title.id,
|
||||
topOffset: title.getBoundingClientRect().top + window.scrollY,
|
||||
}));
|
||||
};
|
||||
|
||||
getItemOffsets();
|
||||
window.addEventListener('resize', getItemOffsets);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', getItemOffsets);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!toc.current) return;
|
||||
|
||||
const setCurrent: IntersectionObserverCallback = (entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) {
|
||||
const { id } = entry.target;
|
||||
if (id === onThisPageID) continue;
|
||||
setCurrentID(entry.target.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const observerOptions: IntersectionObserverInit = {
|
||||
// Negative top margin accounts for `scroll-margin`.
|
||||
// Negative bottom margin means heading needs to be towards top of viewport to trigger intersection.
|
||||
rootMargin: '-100px 0% -66%',
|
||||
threshold: 1,
|
||||
};
|
||||
|
||||
const headingsObserver = new IntersectionObserver(setCurrent, observerOptions);
|
||||
|
||||
// Observe all the headings in the main page content.
|
||||
document.querySelectorAll('article :is(h1,h2,h3)').forEach((h) => headingsObserver.observe(h));
|
||||
|
||||
// Stop observing when the component is unmounted.
|
||||
return () => headingsObserver.disconnect();
|
||||
}, [toc.current]);
|
||||
|
||||
const onLinkClick = (e) => {
|
||||
setCurrentID(e.target.getAttribute('href').replace('#', ''));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 id={onThisPageID} className="heading">
|
||||
On this page
|
||||
</h2>
|
||||
<ul ref={toc}>
|
||||
{headings
|
||||
.filter(({ depth }) => depth > 1 && depth < 4)
|
||||
.map((heading) => (
|
||||
<li
|
||||
className={`header-link depth-${heading.depth} ${
|
||||
currentID === heading.slug ? 'current-header-link' : ''
|
||||
}`.trim()}
|
||||
>
|
||||
<a href={`#${heading.slug}`} onClick={onLinkClick}>
|
||||
{unescape(heading.text)}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableOfContents;
|
||||
37
website/src/components/RightSidebar/ThemeToggleButton.css
Normal file
37
website/src/components/RightSidebar/ThemeToggleButton.css
Normal file
@ -0,0 +1,37 @@
|
||||
.theme-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25em;
|
||||
padding: 0.33em 0.67em;
|
||||
border-radius: 99em;
|
||||
background-color: var(--theme-code-inline-bg);
|
||||
}
|
||||
|
||||
.theme-toggle > label:focus-within {
|
||||
outline: 2px solid transparent;
|
||||
box-shadow: 0 0 0 0.08em var(--theme-accent), 0 0 0 0.12em white;
|
||||
}
|
||||
|
||||
.theme-toggle > label {
|
||||
color: var(--theme-code-inline-text);
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.theme-toggle .checked {
|
||||
color: var(--theme-accent);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
input[name='theme-toggle'] {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
82
website/src/components/RightSidebar/ThemeToggleButton.tsx
Normal file
82
website/src/components/RightSidebar/ThemeToggleButton.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import type { FunctionalComponent } from 'preact';
|
||||
import { useState, useEffect } from 'preact/hooks';
|
||||
import './ThemeToggleButton.css';
|
||||
|
||||
const themes = ['light', 'dark'];
|
||||
|
||||
const icons = [
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>,
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
|
||||
</svg>,
|
||||
];
|
||||
|
||||
const ThemeToggle: FunctionalComponent = () => {
|
||||
const [theme, setTheme] = useState(() => {
|
||||
if (import.meta.env.SSR) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof localStorage !== undefined && localStorage.getItem('theme')) {
|
||||
return localStorage.getItem('theme');
|
||||
}
|
||||
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
return 'dark';
|
||||
}
|
||||
return 'light';
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
if (theme === 'light') {
|
||||
root.classList.remove('theme-dark');
|
||||
} else {
|
||||
root.classList.add('theme-dark');
|
||||
}
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<div className="theme-toggle">
|
||||
{themes.map((t, i) => {
|
||||
const icon = icons[i];
|
||||
const checked = t === theme;
|
||||
return (
|
||||
<label className={checked ? ' checked' : ''}>
|
||||
{icon}
|
||||
<input
|
||||
type="radio"
|
||||
name="theme-toggle"
|
||||
checked={checked}
|
||||
value={t}
|
||||
title={`Use ${t} theme`}
|
||||
aria-label={`Use ${t} theme`}
|
||||
onChange={() => {
|
||||
localStorage.setItem('theme', t);
|
||||
setTheme(t);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeToggle;
|
||||
6
website/src/components/strudel/JsDoc.astro
Normal file
6
website/src/components/strudel/JsDoc.astro
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
import { JsDoc } from './JsDoc';
|
||||
const { name, h } = Astro.props;
|
||||
---
|
||||
|
||||
<JsDoc name={name} h={h} client:only="react" />
|
||||
39
website/src/components/strudel/JsDoc.jsx
Normal file
39
website/src/components/strudel/JsDoc.jsx
Normal file
@ -0,0 +1,39 @@
|
||||
import jsdoc from '../../../../doc.json'; // doc.json is built with `npm run jsdoc-json`
|
||||
const docs = jsdoc.docs.reduce((acc, obj) => Object.assign(acc, { [obj.longname]: obj }), {});
|
||||
import { MiniRepl } from './MiniRepl';
|
||||
|
||||
export function JsDoc({ name, h = 3 }) {
|
||||
const item = docs[name];
|
||||
if (!item) {
|
||||
console.warn('Not found: ' + name);
|
||||
return <div />;
|
||||
}
|
||||
const CustomHeading = `h${h}`;
|
||||
const description = item.description.replaceAll(/\{@link ([a-zA-Z\.]+)?#?([a-zA-Z]*)\}/g, (_, a, b) => {
|
||||
// console.log(_, 'a', a, 'b', b);
|
||||
return `<a href="#${a.replaceAll('.', '').toLowerCase()}${b ? `-${b}` : ''}">${a}${b ? `#${b}` : ''}</a>`;
|
||||
});
|
||||
return (
|
||||
<>
|
||||
{!!h && <CustomHeading>{item.longname}</CustomHeading>}
|
||||
<div dangerouslySetInnerHTML={{ __html: description }} />
|
||||
<ul>
|
||||
{item.params?.map((param, i) => (
|
||||
<li key={i}>
|
||||
{param.name} ({param.type?.names?.join('|')}): {param.description?.replace(/(<([^>]+)>)/gi, '')}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{item.examples?.length ? (
|
||||
<div className="space-y-2">
|
||||
{item.examples?.map((example, k) => (
|
||||
<MiniRepl tune={example} key={k} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
7
website/src/components/strudel/MiniRepl.astro
Normal file
7
website/src/components/strudel/MiniRepl.astro
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
import { MiniRepl } from './MiniRepl';
|
||||
const { tune } = Astro.props;
|
||||
import '@strudel.cycles/react/dist/style.css';
|
||||
---
|
||||
|
||||
<MiniRepl tune={tune} client:only="react" />
|
||||
25
website/src/components/strudel/MiniRepl.jsx
Normal file
25
website/src/components/strudel/MiniRepl.jsx
Normal file
@ -0,0 +1,25 @@
|
||||
import { evalScope, controls } from '@strudel.cycles/core';
|
||||
import { MiniRepl as _MiniRepl } from '@strudel.cycles/react';
|
||||
import { samples } from '@strudel.cycles/webaudio';
|
||||
|
||||
fetch('https://strudel.tidalcycles.org/EmuSP12.json')
|
||||
.then((res) => res.json())
|
||||
.then((json) => samples(json, 'https://strudel.tidalcycles.org/EmuSP12/'));
|
||||
|
||||
evalScope(
|
||||
controls,
|
||||
import('@strudel.cycles/core'),
|
||||
// import('@strudel.cycles/tone'),
|
||||
import('@strudel.cycles/tonal'),
|
||||
import('@strudel.cycles/mini'),
|
||||
import('@strudel.cycles/midi'),
|
||||
import('@strudel.cycles/xen'),
|
||||
import('@strudel.cycles/webaudio'),
|
||||
import('@strudel.cycles/osc'),
|
||||
);
|
||||
|
||||
// prebake();
|
||||
|
||||
export function MiniRepl({ tune }) {
|
||||
return <_MiniRepl tune={tune} hideOutsideView={true} />;
|
||||
}
|
||||
950
website/src/components/tutorial.mdx
Normal file
950
website/src/components/tutorial.mdx
Normal file
@ -0,0 +1,950 @@
|
||||
---
|
||||
title: What is Strudel?
|
||||
description: Strudel Tutorial
|
||||
layout: ../layouts/MainLayout.astro
|
||||
---
|
||||
|
||||
# What is Strudel?
|
||||
|
||||
With Strudel, you can expressively write dynamic music pieces.
|
||||
It aims to be [Tidal Cycles](https://tidalcycles.org/) for JavaScript (started by the same author).
|
||||
|
||||
You don't need to know JavaScript or Tidal Cycles to make music with Strudel.
|
||||
|
||||
This interactive tutorial will guide you through the basics of Strudel.
|
||||
|
||||
The best place to actually make music with Strudel is the [Strudel REPL](https://strudel.tidalcycles.org/).
|
||||
|
||||
## Show me a Demo
|
||||
|
||||
To get a taste of what Strudel can do, check out this track:
|
||||
|
||||
<MiniRepl
|
||||
tune={`samples({
|
||||
bd: ['bd/BT0AADA.wav','bd/BT0AAD0.wav','bd/BT0A0DA.wav','bd/BT0A0D3.wav','bd/BT0A0D0.wav','bd/BT0A0A7.wav'],
|
||||
sd: ['sd/rytm-01-classic.wav','sd/rytm-00-hard.wav'],
|
||||
hh: ['hh27/000_hh27closedhh.wav','hh/000_hh3closedhh.wav'],
|
||||
}, 'github:tidalcycles/Dirt-Samples/master/');
|
||||
stack(
|
||||
s("bd,[~ <sd!3 sd(3,4,2)>],hh(3,4)") // drums
|
||||
.speed(perlin.range(.7,.9)) // random sample speed variation
|
||||
,"<a1 b1\*2 a1(3,8) e2>" // bassline
|
||||
.off(1/8,x=>x.add(12).degradeBy(.5)) // random octave jumps
|
||||
.add(perlin.range(0,.5)) // random pitch variation
|
||||
.superimpose(add(.05)) // add second, slightly detuned voice
|
||||
.n() // wrap in "n"
|
||||
.decay(.15).sustain(0) // make each note of equal length
|
||||
.s('sawtooth') // waveform
|
||||
.gain(.4) // turn down
|
||||
.cutoff(sine.slow(7).range(300,5000)) // automate cutoff
|
||||
,"<Am7!3 <Em7 E7b13 Em7 Ebm7b5>>".voicings('lefthand') // chords
|
||||
.superimpose(x=>x.add(.04)) // add second, slightly detuned voice
|
||||
.add(perlin.range(0,.5)) // random pitch variation
|
||||
.n() // wrap in "n"
|
||||
.s('sawtooth') // waveform
|
||||
.gain(.16) // turn down
|
||||
.cutoff(500) // fixed cutoff
|
||||
.attack(1) // slowly fade in
|
||||
)
|
||||
.slow(3/2)`}
|
||||
/>
|
||||
|
||||
## Disclaimer
|
||||
|
||||
- This project is still in its experimental state. In the future, parts of it might change significantly.
|
||||
- This tutorial is far from complete.
|
||||
|
||||
<br />
|
||||
|
||||
# Playing Pitches
|
||||
|
||||
Pitches are an essential building block for music. In Strudel, there are 3 different options to express a pitch:
|
||||
|
||||
- `note`: letter notation
|
||||
- `n`: number notation
|
||||
- `freq`: frequency notation
|
||||
|
||||
## note
|
||||
|
||||
Notes are notated with the note letter, followed by the octave number. You can notate flats with `b` and sharps with `#`.
|
||||
|
||||
<MiniRepl tune={`note("a3 c#4 e4 a4")`} />
|
||||
|
||||
By the way, you can edit the contents of the player, and press "update" to hear your change!
|
||||
You can also press "play" on the next player without needing to stop the last one.
|
||||
|
||||
## n
|
||||
|
||||
If you don't like notes, you can also use numbers with `n` instead:
|
||||
|
||||
<MiniRepl tune={`n("57 61 64 69")`} />
|
||||
|
||||
These numbers are interpreted as so called midi numbers, where adjacent whole numbers are 1 semitone apart.
|
||||
You could also write decimal numbers to get microtonal pitches:
|
||||
|
||||
<MiniRepl tune={`n("74.5 75 75.5 76")`} />
|
||||
|
||||
## freq
|
||||
|
||||
To get maximum freedom, you can also use `freq` to directly control the frequency:
|
||||
|
||||
<MiniRepl tune={`freq("220 275 330 440")`} />
|
||||
|
||||
In this example, we play A3 (220Hz), C#4 natural (275Hz), E4 (330Hz) and A4 (440Hz).
|
||||
|
||||
<br />
|
||||
|
||||
# Playing Sounds
|
||||
|
||||
Instead of pitches, we can also play sounds with `s`:
|
||||
|
||||
<MiniRepl tune={`s("bd hh sd hh")`} />
|
||||
|
||||
Similarly, we can also use `s` to change the sound of our pitches:
|
||||
|
||||
<MiniRepl tune={`note("a3 c#4 e4 a4").s("sawtooth")`} />
|
||||
|
||||
Try changing the sound to `square`, `triangle` or `sine`!
|
||||
|
||||
We will go into the defails of sounds and synths [later](http://localhost:3000/tutorial/#web-audio-output).
|
||||
|
||||
<br />
|
||||
|
||||
# Syntax
|
||||
|
||||
So far, we've seen the following syntax:
|
||||
|
||||
```
|
||||
xxx("foo").yyy("bar")
|
||||
```
|
||||
|
||||
Generally, `xxx` and `yyy` are called functions, while `foo` and `bar` are called function arguments.
|
||||
So far, we've used the functions to declare which aspect of the sound we want to control, and their arguments for the actual data.
|
||||
The `yyy` function is called a chained function, because it is appended with a dot.
|
||||
|
||||
Strudel makes heavy use of chained functions. Here is a more extreme example:
|
||||
|
||||
<MiniRepl
|
||||
tune={`note("a3 c#4 e4 a4")
|
||||
.s("sawtooth")
|
||||
.cutoff(500)
|
||||
//.delay(0.5)
|
||||
.room(0.5)`}
|
||||
/>
|
||||
|
||||
The `//` is a line comment, resulting in the `delay` function being ignored.
|
||||
It is a handy way to quickly turn stuff on and off. Try uncommenting this line by deleting `//`!
|
||||
|
||||
The good news is, that this covers 99% of the JavaScript syntax needed for Strudel!
|
||||
|
||||
Let's now look at the way we can express rhythms..
|
||||
|
||||
<br />
|
||||
|
||||
# Mini Notation
|
||||
|
||||
Similar to Tidal Cycles, Strudel has an embedded mini language that is designed to write rhythmic patterns in a short manner.
|
||||
Before diving deeper into the details, here is a flavor of how the mini language looks like:
|
||||
|
||||
<MiniRepl
|
||||
tune={`note(\`[
|
||||
[
|
||||
[e5 [b4 c5] d5 [c5 b4]]
|
||||
[a4 [a4 c5] e5 [d5 c5]]
|
||||
[b4 [~ c5] d5 e5]
|
||||
[c5 a4 a4 ~]
|
||||
[[~ d5] [~ f5] a5 [g5 f5]]
|
||||
[e5 [~ c5] e5 [d5 c5]]
|
||||
[b4 [b4 c5] d5 e5]
|
||||
[c5 a4 a4 ~]
|
||||
],[
|
||||
[[e2 e3]*4]
|
||||
[[a2 a3]*4]
|
||||
[[g#2 g#3]*2 [e2 e3]*2]
|
||||
[a2 a3 a2 a3 a2 a3 b1 c2]
|
||||
[[d2 d3]*4]
|
||||
[[c2 c3]*4]
|
||||
[[b1 b2]*2 [e2 e3]*2]
|
||||
[[a1 a2]*4]
|
||||
]
|
||||
]/16\`)`}
|
||||
/>
|
||||
|
||||
The snippet above is enclosed in backticks (`), which allows you to write multi-line strings.
|
||||
You can also use double quotes (") for single line mini notation.
|
||||
|
||||
## Sequences
|
||||
|
||||
We can play more notes by separating them with spaces:
|
||||
|
||||
<MiniRepl tune={`note("e5 b4 d5 c5")`} />
|
||||
|
||||
Here, those four notes are squashed into one cycle, so each note is a quarter second long.
|
||||
Try adding or removing notes and notice how the tempo changes!
|
||||
|
||||
## Division
|
||||
|
||||
We can slow the sequence down by enclosing it in brackets and dividing it by a number:
|
||||
|
||||
<MiniRepl tune={`note("[e5 b4 d5 c5]/2")`} />
|
||||
|
||||
The division by two means that the sequence will be played over the course of two cycles.
|
||||
You can also use decimal numbers for any tempo you like.
|
||||
|
||||
## Angle Brackets
|
||||
|
||||
Using angle brackets, we can define the sequence length based on the number of children:
|
||||
|
||||
<MiniRepl tune={`note("<e5 b4 d5 c5>")`} />
|
||||
|
||||
The above snippet is the same as:
|
||||
|
||||
<MiniRepl tune={`note("[e5 b4 d5 c5]/4")`} />
|
||||
|
||||
The advantage of the angle brackets, is that we can add more children without needing to change the number at the end.
|
||||
|
||||
## Multiplication
|
||||
|
||||
Contrary to division, a sequence can be sped up by multiplying it by a number:
|
||||
|
||||
<MiniRepl tune={`note("[e5 b4 d5 c5]*2")`} />
|
||||
|
||||
The multiplication by 2 here means that the sequence will play twice a cycle.
|
||||
|
||||
## Bracket Nesting
|
||||
|
||||
To create more interesting rhythms, you can nest sequences with brackets, like this:
|
||||
|
||||
<MiniRepl tune={`note("e5 [b4 c5] d5 [c5 b4]")`} />
|
||||
|
||||
## Rests
|
||||
|
||||
The "~" represents a rest:
|
||||
|
||||
<MiniRepl tune={`note("[b4 [~ c5] d5 e5]")`} />
|
||||
|
||||
## Parallel
|
||||
|
||||
Using commas, we can play chords:
|
||||
|
||||
<MiniRepl tune={`note("g3,b3,e4")`} />
|
||||
|
||||
To play multiple chords in a sequence, we have to wrap them in brackets:
|
||||
|
||||
<MiniRepl tune={`note("<[g3,b3,e4] [a3,c3,e4] [b3,d3,f#4] [b3,e4,g4]>")`} />
|
||||
|
||||
## Elongation
|
||||
|
||||
With the "@" symbol, we can specify temporal "weight" of a sequence child:
|
||||
|
||||
<MiniRepl tune={`note("<[g3,b3,e4]@2 [a3,c3,e4] [b3,d3,f#4]>")`} />
|
||||
|
||||
Here, the first chord has a weight of 2, making it twice the length of the other chords. The default weight is 1.
|
||||
|
||||
## Replication
|
||||
|
||||
Using "!" we can repeat without speeding up:
|
||||
|
||||
<MiniRepl tune={`note("<[g3,b3,e4]!2 [a3,c3,e4] [b3,d3,f#4]>")`} />
|
||||
|
||||
In essence, the `x!n` is like a shortcut for `[x*n]@n`.
|
||||
|
||||
## Euclidian
|
||||
|
||||
Using round brackets, we can create rhythmical sub-divisions based on three parameters: beats, segments and offset.
|
||||
The first parameter controls how may beats will be played.
|
||||
The second parameter controls the total amount of segments the beats will be distributed over.
|
||||
The third (optional) parameter controls the starting position for distributing the beats.
|
||||
One popular Euclidian rhythm (going by various names, such as "Pop Clave") is "(3,8,0)" or simply "(3,8)",
|
||||
resulting in a rhythmical structure of "x ~ ~ x ~ ~ x ~" (3 beats over 8 segments, starting on position 1).
|
||||
|
||||
<MiniRepl tune={`note("e5(2,8) b4(3,8) d5(2,8) c5(3,8)").slow(4)`} />
|
||||
|
||||
<br />
|
||||
|
||||
# Synths, Samples & Effects
|
||||
|
||||
Let's take a closer look at how we can control synths, sounds and effects.
|
||||
|
||||
## Synths
|
||||
|
||||
So far, all the mini notation examples all used the same sound, which is kind of boring.
|
||||
We can change the sound, using the `s` function:
|
||||
|
||||
<MiniRepl tune={`note("c2 <eb2 <g2 g1>>").s('sawtooth')`} />
|
||||
|
||||
Here, we are wrapping our notes inside `note` and set the sound using `s`, connected by a dot.
|
||||
|
||||
Those functions are only 2 of many ways to alter the properties, or _params_ of a sound.
|
||||
The power of patterns allows us to sequence any _param_ independently:
|
||||
|
||||
<MiniRepl tune={`note("c2 <eb2 <g2 g1>>").s("<sawtooth square triangle>")`} />
|
||||
|
||||
Now we not only pattern the notes, but the sound as well!
|
||||
`sawtooth` `square` and `triangle` are the basic waveforms available in `s`.
|
||||
|
||||
### Envelope
|
||||
|
||||
You can control the envelope of a synth using the `attack`, `decay`, `sustain` and `release` functions:
|
||||
|
||||
<MiniRepl
|
||||
tune={`note("c2 <eb2 <g2 g1>>").s('sawtooth')
|
||||
.attack(.1).decay(.1).sustain(.2).release(.1)`}
|
||||
/>
|
||||
|
||||
## Samples
|
||||
|
||||
Besides Synths, `s` can also play back samples:
|
||||
|
||||
<MiniRepl tune={`s("bd sd,hh*8,misc/2")`} />
|
||||
|
||||
To know which sounds are available, open the [default sample map](https://strudel.tidalcycles.org/EmuSP12.json)
|
||||
|
||||
### Custom Sample Maps
|
||||
|
||||
You can load your own sample map like this:
|
||||
|
||||
<MiniRepl
|
||||
tune={`samples({
|
||||
bd: 'bd/BT0AADA.wav',
|
||||
sd: 'sd/rytm-01-classic.wav',
|
||||
hh: 'hh27/000_hh27closedhh.wav',
|
||||
}, 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/');
|
||||
s("bd sd,hh*8")`}
|
||||
/>
|
||||
|
||||
The `samples` function takes an object that maps sound names to audio file paths.
|
||||
The second argument is the base URL that comes before each path. Make sure your base URL ends with a slash, while your sample paths do **not** begin with one.
|
||||
|
||||
Because github is a popular choice to dump samples, there is a shortcut for that:
|
||||
|
||||
<MiniRepl
|
||||
tune={`samples({
|
||||
bd: 'bd/BT0AADA.wav',
|
||||
sd: 'sd/rytm-01-classic.wav',
|
||||
hh: 'hh27/000_hh27closedhh.wav',
|
||||
}, 'github:tidalcycles/Dirt-Samples/master/');
|
||||
s("bd sd,hh*8")`}
|
||||
/>
|
||||
|
||||
The format is `github:user/repo/branch/`.
|
||||
|
||||
### Multiple Samples per Sound
|
||||
|
||||
It is also possible, to declare multiple files for one sound, using the array notation:
|
||||
|
||||
<MiniRepl
|
||||
tune={`samples({
|
||||
bd: ['bd/BT0AADA.wav','bd/BT0AAD0.wav'],
|
||||
sd: ['sd/rytm-01-classic.wav','sd/rytm-00-hard.wav'],
|
||||
hh: ['hh27/000_hh27closedhh.wav','hh/000_hh3closedhh.wav'],
|
||||
}, 'github:tidalcycles/Dirt-Samples/master/');
|
||||
s("<bd:0 bd:1>,~ <sd:0 sd:1>,[hh:0 hh:1]*2")`}
|
||||
/>
|
||||
|
||||
The `:0` `:1` etc. are the indices of the array.
|
||||
The sample number can also be set using `n`:
|
||||
|
||||
<MiniRepl
|
||||
tune={`samples({
|
||||
bd: ['bd/BT0AADA.wav','bd/BT0AAD0.wav'],
|
||||
sd: ['sd/rytm-01-classic.wav','sd/rytm-00-hard.wav'],
|
||||
hh: ['hh27/000_hh27closedhh.wav','hh/000_hh3closedhh.wav'],
|
||||
}, 'github:tidalcycles/Dirt-Samples/master/');
|
||||
s("bd,~ sd,hh*4").n("<0 1>")`}
|
||||
/>
|
||||
|
||||
### Pitched Sounds
|
||||
|
||||
For pitched sounds, you can use `note`, just like with synths:
|
||||
|
||||
<MiniRepl
|
||||
tune={`samples({
|
||||
'gtr': 'gtr/0001_cleanC.wav',
|
||||
}, 'github:tidalcycles/Dirt-Samples/master/');
|
||||
note("g3 [bb3 c4] <g4 f4 eb4 f3>@2").s('gtr').gain(.5)`}
|
||||
/>
|
||||
|
||||
Here, the guitar samples will overlap, because they always play till the end.
|
||||
If we want them to behave more like a synth, we can add `clip(1)`:
|
||||
|
||||
<MiniRepl
|
||||
tune={`samples({
|
||||
'gtr': 'gtr/0001_cleanC.wav',
|
||||
}, 'github:tidalcycles/Dirt-Samples/master/');
|
||||
note("g3 [bb3 c4] <g4 f4 eb4 f3>@2").s('gtr').clip(1)
|
||||
.gain(.5)`}
|
||||
/>
|
||||
|
||||
### Base Pitch
|
||||
|
||||
If we have 2 samples with different base pitches, we can make them in tune by specifying the pitch like this:
|
||||
|
||||
<MiniRepl
|
||||
tune={`samples({
|
||||
'gtr': 'gtr/0001_cleanC.wav',
|
||||
'moog': { 'g3': 'moog/005_Mighty%20Moog%20G3.wav' },
|
||||
}, 'github:tidalcycles/Dirt-Samples/master/');
|
||||
note("g3 [bb3 c4] <g4 f4 eb4 f3>@2").s("gtr,moog").clip(1)
|
||||
.gain(.5)`}
|
||||
/>
|
||||
|
||||
If a sample has no pitch set, `c3` is the default.
|
||||
|
||||
We can also declare different samples for different regions of the keyboard:
|
||||
|
||||
<MiniRepl
|
||||
tune={`samples({
|
||||
'moog': {
|
||||
'g2': 'moog/004_Mighty%20Moog%20G2.wav',
|
||||
'g3': 'moog/005_Mighty%20Moog%20G3.wav',
|
||||
'g4': 'moog/006_Mighty%20Moog%20G4.wav',
|
||||
}}, 'github:tidalcycles/Dirt-Samples/master/');
|
||||
note("g2!2 <bb2 c3>!2, <c4@3 [<eb4 bb3> g4 f4]>")
|
||||
.s('moog').clip(1)
|
||||
.gain(.5)`}
|
||||
/>
|
||||
|
||||
The sampler will always pick the closest matching sample for the current note!
|
||||
|
||||
## Sampler Effects
|
||||
|
||||
### Pattern.begin
|
||||
|
||||
<JsDoc name="Pattern.begin" h={0} />
|
||||
|
||||
### Pattern.end
|
||||
|
||||
<JsDoc name="Pattern.end" h={0} />
|
||||
|
||||
### Pattern.loopAt
|
||||
|
||||
<JsDoc name="Pattern.loopAt" h={0} />
|
||||
|
||||
### Pattern.chop
|
||||
|
||||
<JsDoc name="Pattern.chop" h={0} />
|
||||
|
||||
## Audio Effects
|
||||
|
||||
Wether you're using a synth or a sample, you can apply these effects:
|
||||
|
||||
### gain
|
||||
|
||||
<JsDoc name="gain" h={0} />
|
||||
|
||||
### velocity
|
||||
|
||||
<JsDoc name="velocity" h={0} />
|
||||
|
||||
### cutoff
|
||||
|
||||
<JsDoc name="cutoff" h={0} />
|
||||
|
||||
### resonance
|
||||
|
||||
<JsDoc name="resonance" h={0} />
|
||||
|
||||
### hcutoff
|
||||
|
||||
<JsDoc name="hcutoff" h={0} />
|
||||
|
||||
### hresonance
|
||||
|
||||
<JsDoc name="hresonance" h={0} />
|
||||
|
||||
### bandf
|
||||
|
||||
<JsDoc name="bandf" h={0} />
|
||||
|
||||
### bandq
|
||||
|
||||
<JsDoc name="bandq" h={0} />
|
||||
|
||||
### vowel
|
||||
|
||||
<JsDoc name="vowel" h={0} />
|
||||
|
||||
### pan
|
||||
|
||||
<JsDoc name="pan" h={0} />
|
||||
|
||||
### coarse
|
||||
|
||||
<JsDoc name="coarse" h={0} />
|
||||
|
||||
### shape
|
||||
|
||||
<JsDoc name="shape" h={0} />
|
||||
|
||||
### crush
|
||||
|
||||
<JsDoc name="crush" h={0} />
|
||||
|
||||
<br />
|
||||
|
||||
# JavaScript API
|
||||
|
||||
While the mini notation is powerful on its own, there is much more to discover.
|
||||
Internally, the mini notation will expand to use the actual functional JavaScript API.
|
||||
|
||||
For example, this Pattern in Mini Notation:
|
||||
|
||||
<MiniRepl tune={`note("c3 eb3 g3")`} />
|
||||
|
||||
is equivalent to this Pattern without Mini Notation:
|
||||
|
||||
<MiniRepl tune={`note(seq(c3, eb3, g3))`} />
|
||||
|
||||
Similarly, there is an equivalent function for every aspect of the mini notation.
|
||||
|
||||
Which representation to use is a matter of context. As a rule of thumb, you can think of the JavaScript API
|
||||
to fit better for the larger context, while mini notation is more practical for individiual rhythms.
|
||||
|
||||
## Limits of Mini Notation
|
||||
|
||||
While the Mini Notation is a powerful way to write rhythms shortly, it also has its limits. Take this example:
|
||||
|
||||
<MiniRepl
|
||||
tune={`stack(
|
||||
note("c2 eb2(3,8)").s('sawtooth').cutoff(800),
|
||||
s("bd,~ sd,hh*4")
|
||||
)`}
|
||||
/>
|
||||
|
||||
Here, we are using mini notation for the individual rhythms, while using the function `stack` to mix them.
|
||||
While stack is also available as `,` in mini notation, we cannot use it here, because we have different types of sounds.
|
||||
|
||||
## Notes
|
||||
|
||||
Notes are automatically available as variables:
|
||||
|
||||
<MiniRepl tune={`note(seq(d4, fs4, a4)) // note("d4 f#4 a4")`} />
|
||||
|
||||
An important difference to the mini notation:
|
||||
For sharp notes, the letter "s" is used instead of "#", because JavaScript does not support "#" in a variable name.
|
||||
|
||||
The above is the same as:
|
||||
|
||||
<MiniRepl tune={`note(seq('d4', 'f#4', 'a4'))`} />
|
||||
|
||||
Using strings, you can also use "#".
|
||||
|
||||
## Alternative Syntax
|
||||
|
||||
In the above example, we are nesting a function inside a function, which makes reading the parens a little more difficult.
|
||||
To avoid getting to many nested parens, there is an alternative syntax to add a type to a pattern:
|
||||
|
||||
<MiniRepl tune={`seq(d4, fs4, a4).note()`} />
|
||||
|
||||
You can use this with any function that declares a type (like `n`, `s`, `note`, `freq` etc), just make sure to leave the parens empty!
|
||||
|
||||
## Pattern Factories
|
||||
|
||||
The following functions will return a pattern.
|
||||
|
||||
|
||||
### cat
|
||||
|
||||
<JsDoc name="cat" h={0} />
|
||||
|
||||
### seq
|
||||
|
||||
<JsDoc name="seq" h={0} />
|
||||
|
||||
### stack
|
||||
|
||||
<JsDoc name="stack" h={0} />
|
||||
|
||||
### timeCat
|
||||
|
||||
<JsDoc name="timeCat" h={0} />
|
||||
|
||||
## Combining Patterns
|
||||
|
||||
You can freely mix JS patterns, mini patterns and values! For example, this pattern:
|
||||
|
||||
<MiniRepl
|
||||
tune={`cat(
|
||||
stack(g3,b3,e4),
|
||||
stack(a3,c3,e4),
|
||||
stack(b3,d3,fs4),
|
||||
stack(b3,e4,g4)
|
||||
).note()`}
|
||||
/>
|
||||
|
||||
...is equivalent to:
|
||||
|
||||
<MiniRepl
|
||||
tune={`cat(
|
||||
"g3,b3,e4",
|
||||
"a3,c3,e4",
|
||||
"b3,d3,f#4",
|
||||
"b3,e4,g4"
|
||||
).note()`}
|
||||
/>
|
||||
|
||||
... as well as:
|
||||
|
||||
<MiniRepl tune={`note("<[g3,b3,e4] [a3,c3,e4] [b3,d3,f#4] [b3,e4,g4]>")`} />
|
||||
|
||||
While mini notation is almost always shorter, it only has a handful of modifiers: \* / ! @.
|
||||
When using JS patterns, there is a lot more you can do.
|
||||
|
||||
## Time Modifiers
|
||||
|
||||
The following functions modify a pattern temporal structure in some way.
|
||||
|
||||
### Pattern.slow
|
||||
|
||||
<JsDoc name="Pattern.slow" h={0} />
|
||||
|
||||
### Pattern.fast
|
||||
|
||||
<JsDoc name="Pattern.fast" h={0} />
|
||||
|
||||
### Pattern.early
|
||||
|
||||
<JsDoc name="Pattern.early" h={0} />
|
||||
|
||||
### Pattern.late
|
||||
|
||||
<JsDoc name="Pattern.late" h={0} />
|
||||
|
||||
### Pattern.legato
|
||||
|
||||
<JsDoc name="Pattern.legato" h={0} />
|
||||
|
||||
### Pattern.struct
|
||||
|
||||
<JsDoc name="Pattern.struct" h={0} />
|
||||
|
||||
### Pattern.euclid
|
||||
|
||||
<JsDoc name="Pattern.euclid" h={0} />
|
||||
|
||||
### Pattern.euclidLegato
|
||||
|
||||
<JsDoc name="Pattern.euclidLegato" h={0} />
|
||||
|
||||
### Pattern.rev
|
||||
|
||||
<JsDoc name="Pattern.rev" h={0} />
|
||||
|
||||
### Pattern.iter
|
||||
|
||||
<JsDoc name="Pattern.iter" h={0} />
|
||||
|
||||
### Pattern.iterBack
|
||||
|
||||
<JsDoc name="Pattern.iterBack" h={0} />
|
||||
|
||||
## Conditional Modifiers
|
||||
|
||||
### Pattern.every
|
||||
|
||||
<JsDoc name="Pattern.every" h={0} />
|
||||
|
||||
### Pattern.when
|
||||
|
||||
<JsDoc name="Pattern.when" h={0} />
|
||||
|
||||
## Accumulation Modifiers
|
||||
|
||||
### Pattern.stack
|
||||
|
||||
<JsDoc name="Pattern.stack" h={0} />
|
||||
|
||||
### Pattern.superimpose
|
||||
|
||||
<JsDoc name="Pattern.superimpose" h={0} />
|
||||
|
||||
### Pattern.layer
|
||||
|
||||
<JsDoc name="Pattern.layer" h={0} />
|
||||
|
||||
### Pattern.off
|
||||
|
||||
<JsDoc name="Pattern.off" h={0} />
|
||||
|
||||
### Pattern.echo
|
||||
|
||||
<JsDoc name="Pattern.echo" h={0} />
|
||||
|
||||
### Pattern.echoWith
|
||||
|
||||
<JsDoc name="Pattern.echoWith" h={0} />
|
||||
|
||||
## Concat Modifiers
|
||||
|
||||
### Pattern.seq
|
||||
|
||||
<JsDoc name="Pattern.seq" h={0} />
|
||||
|
||||
### Pattern.cat
|
||||
|
||||
<JsDoc name="Pattern.cat" h={0} />
|
||||
|
||||
## Value Modifiers
|
||||
|
||||
### Pattern.add
|
||||
|
||||
<JsDoc name="Pattern.add" h={0} />
|
||||
|
||||
### Pattern.sub
|
||||
|
||||
<JsDoc name="Pattern.sub" h={0} />
|
||||
|
||||
### Pattern.mul
|
||||
|
||||
<JsDoc name="Pattern.mul" h={0} />
|
||||
|
||||
### Pattern.div
|
||||
|
||||
<JsDoc name="Pattern.div" h={0} />
|
||||
|
||||
### Pattern.round
|
||||
|
||||
<JsDoc name="Pattern.round" h={0} />
|
||||
|
||||
### Pattern.apply
|
||||
|
||||
<JsDoc name="Pattern.apply" h={0} />
|
||||
|
||||
### Pattern.range
|
||||
|
||||
<JsDoc name="Pattern.range" h={0} />
|
||||
|
||||
### Pattern.chunk
|
||||
|
||||
<JsDoc name="Pattern.chunk" h={0} />
|
||||
|
||||
### Pattern.chunkBack
|
||||
|
||||
<JsDoc name="Pattern.chunkBack" h={0} />
|
||||
|
||||
## Continuous Signals
|
||||
|
||||
Signals are patterns with continuous values, meaning they have theoretically infinite steps.
|
||||
They can provide streams of numbers that can be sampled at discrete points in time.
|
||||
|
||||
### saw
|
||||
|
||||
<JsDoc name="saw" h={0} />
|
||||
|
||||
### sine
|
||||
|
||||
<JsDoc name="sine" h={0} />
|
||||
|
||||
### cosine
|
||||
|
||||
<JsDoc name="cosine" h={0} />
|
||||
|
||||
### tri
|
||||
|
||||
<JsDoc name="tri" h={0} />
|
||||
|
||||
### square
|
||||
|
||||
<JsDoc name="square" h={0} />
|
||||
|
||||
### Ranges from -1 to 1
|
||||
|
||||
There is also `saw2`, `sine2`, `cosine2`, `tri2` and `square2` which have a range from -1 to 1!
|
||||
|
||||
### rand
|
||||
|
||||
<JsDoc name="rand" h={0} />
|
||||
|
||||
### perlin
|
||||
|
||||
<JsDoc name="perlin" h={0} />
|
||||
|
||||
### irand
|
||||
|
||||
<JsDoc name="irand" h={0} />
|
||||
|
||||
## Random Modifiers
|
||||
|
||||
These methods add random behavior to your Patterns.
|
||||
|
||||
### chooseCycles
|
||||
|
||||
<JsDoc name="chooseCycles" h={0} />
|
||||
|
||||
### Pattern.degradeBy
|
||||
|
||||
<JsDoc name="Pattern.degradeBy" h={0} />
|
||||
|
||||
### Pattern.degrade
|
||||
|
||||
<JsDoc name="Pattern.degrade" h={0} />
|
||||
|
||||
### Pattern.undegradeBy
|
||||
|
||||
<JsDoc name="Pattern.undegradeBy" h={0} />
|
||||
|
||||
### Pattern.sometimesBy
|
||||
|
||||
<JsDoc name="Pattern.sometimesBy" h={0} />
|
||||
|
||||
### Pattern.sometimes
|
||||
|
||||
<JsDoc name="Pattern.sometimes" h={0} />
|
||||
|
||||
### Pattern.someCyclesBy
|
||||
|
||||
<JsDoc name="Pattern.someCyclesBy" h={0} />
|
||||
|
||||
### Pattern.someCycles
|
||||
|
||||
<JsDoc name="Pattern.someCycles" h={0} />
|
||||
|
||||
### Pattern.often
|
||||
|
||||
<JsDoc name="Pattern.often" h={0} />
|
||||
|
||||
### Pattern.rarely
|
||||
|
||||
<JsDoc name="Pattern.rarely" h={0} />
|
||||
|
||||
### Pattern.almostNever
|
||||
|
||||
<JsDoc name="Pattern.almostNever" h={0} />
|
||||
|
||||
### Pattern.almostAlways
|
||||
|
||||
<JsDoc name="Pattern.almostAlways" h={0} />
|
||||
|
||||
### Pattern.never
|
||||
|
||||
<JsDoc name="Pattern.never" h={0} />
|
||||
|
||||
### Pattern.always
|
||||
|
||||
<JsDoc name="Pattern.always" h={0} />
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
# Tonal API
|
||||
|
||||
The Tonal API, uses [tonaljs](https://github.com/tonaljs/tonal) to provide helpers for musical operations.
|
||||
|
||||
### transpose(semitones)
|
||||
|
||||
Transposes all notes to the given number of semitones:
|
||||
|
||||
<MiniRepl tune={`"c2 c3".fast(2).transpose("<0 -2 5 3>".slow(2)).note()`} />
|
||||
|
||||
This method gets really exciting when we use it with a pattern as above.
|
||||
|
||||
Instead of numbers, scientific interval notation can be used as well:
|
||||
|
||||
<MiniRepl tune={`"c2 c3".fast(2).transpose("<1P -2M 4P 3m>".slow(2)).note()`} />
|
||||
|
||||
### scale(name)
|
||||
|
||||
Turns numbers into notes in the scale (zero indexed). Also sets scale for other scale operations, like scaleTranpose.
|
||||
|
||||
<MiniRepl
|
||||
tune={`"0 2 4 6 4 2"
|
||||
.scale(seq('C2 major', 'C2 minor').slow(2))
|
||||
.note()`}
|
||||
/>
|
||||
|
||||
Note that the scale root is octaved here. You can also omit the octave, then index zero will default to octave 3.
|
||||
|
||||
All the available scale names can be found [here](https://github.com/tonaljs/tonal/blob/main/packages/scale-type/data.ts).
|
||||
|
||||
### scaleTranspose(steps)
|
||||
|
||||
Transposes notes inside the scale by the number of steps:
|
||||
|
||||
<MiniRepl
|
||||
tune={`"-8 [2,4,6]"
|
||||
.scale('C4 bebop major')
|
||||
.scaleTranspose("<0 -1 -2 -3 -4 -5 -6 -4>")
|
||||
.note()`}
|
||||
/>
|
||||
|
||||
### voicings(range?)
|
||||
|
||||
Turns chord symbols into voicings, using the smoothest voice leading possible:
|
||||
|
||||
<MiniRepl tune={`stack("<C^7 A7 Dm7 G7>".voicings('lefthand'), "<C3 A2 D3 G2>").note()`} />
|
||||
|
||||
### rootNotes(octave = 2)
|
||||
|
||||
Turns chord symbols into root notes of chords in given octave.
|
||||
|
||||
<MiniRepl tune={`"<C^7 A7b13 Dm7 G7>".rootNotes(3).note()`} />
|
||||
|
||||
Together with layer, struct and voicings, this can be used to create a basic backing track:
|
||||
|
||||
<MiniRepl
|
||||
tune={`"<C^7 A7b13 Dm7 G7>".layer(
|
||||
x => x.voicings('lefthand').struct("~ x").note(),
|
||||
x => x.rootNotes(2).note().s('sawtooth').cutoff(800)
|
||||
)`}
|
||||
/>
|
||||
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
# MIDI API
|
||||
|
||||
Strudel also supports midi via [webmidi](https://npmjs.com/package/webmidi).
|
||||
|
||||
### midi(outputName?)
|
||||
|
||||
Either connect a midi device or use the IAC Driver (Mac) or Midi Through Port (Linux) for internal midi messages.
|
||||
If no outputName is given, it uses the first midi output it finds.
|
||||
|
||||
<MiniRepl
|
||||
tune={`stack("<C^7 A7 Dm7 G7>".voicings('lefthand'), "<C3 A2 D3 G2>")
|
||||
.midi()`}
|
||||
/>
|
||||
|
||||
In the console, you will see a log of the available MIDI devices as soon as you run the code, e.g. `Midi connected! Using "Midi Through Port-0".`
|
||||
|
||||
# Superdirt API
|
||||
|
||||
In mainline tidal, the actual sound is generated via Superdirt, which runs inside Supercollider.
|
||||
Strudel also supports using Superdirt as a backend, although it requires some developer tooling to run.
|
||||
|
||||
## Prequisites
|
||||
|
||||
Getting Superdirt to work with Strudel, you need to
|
||||
|
||||
1. install SuperCollider + sc3 plugins, see [Tidal Docs](https://tidalcycles.org/docs/) (Install Tidal) for more info.
|
||||
2. install [node.js](https://nodejs.org/en/)
|
||||
3. download [Strudel Repo](https://github.com/tidalcycles/strudel/) (or git clone, if you have git installed)
|
||||
4. run `npm i` in the strudel directory
|
||||
5. run `npm run osc` to start the osc server, which forwards OSC messages from Strudel REPL to SuperCollider
|
||||
|
||||
Now you're all set!
|
||||
|
||||
## Usage
|
||||
|
||||
1. Start SuperCollider, either using SuperCollider IDE or by running `sclang` in a terminal
|
||||
2. Open the [Strudel REPL](https://strudel.tidalcycles.org/#cygiYmQgc2QiKS5vc2MoKQ%3D%3D)
|
||||
|
||||
...or test it here:
|
||||
|
||||
<MiniRepl tune={`s("bd sd").osc()`} />
|
||||
|
||||
If you now hear sound, congratulations! If not, you can get help on the [#strudel channel in the TidalCycles discord](https://discord.com/invite/HGEdXmRkzT).
|
||||
|
||||
### Pattern.osc
|
||||
|
||||
<JsDoc name="Pattern.osc" h={0} />
|
||||
|
||||
## Superdirt Params
|
||||
|
||||
The following functions can be used with superdirt:
|
||||
|
||||
`s n note freq channel orbit cutoff resonance hcutoff hresonance bandf bandq djf vowel cut begin end loop fadeTime speed unitA gain amp accelerate crush coarse delay lock leslie lrate lsize pan panspan pansplay room size dry shape squiz waveloss attack decay octave detune tremolodepth`
|
||||
|
||||
Please refer to [Tidal Docs](https://tidalcycles.org/) for more info.
|
||||
55
website/src/config.ts
Normal file
55
website/src/config.ts
Normal file
@ -0,0 +1,55 @@
|
||||
export const SITE = {
|
||||
title: 'Strudel Docs',
|
||||
description: 'Documentation for the Strudel Live Coding Language',
|
||||
defaultLanguage: 'en_US',
|
||||
};
|
||||
|
||||
export const OPEN_GRAPH = {
|
||||
image: {
|
||||
src: 'https://github.com/withastro/astro/blob/main/assets/social/banner-minimal.png?raw=true',
|
||||
alt:
|
||||
'astro logo on a starry expanse of space,' +
|
||||
' with a purple saturn-like planet floating in the right foreground',
|
||||
},
|
||||
twitter: 'astrodotbuild',
|
||||
};
|
||||
|
||||
// This is the type of the frontmatter you put in the docs markdown files.
|
||||
export type Frontmatter = {
|
||||
title: string;
|
||||
description: string;
|
||||
layout: string;
|
||||
image?: { src: string; alt: string };
|
||||
dir?: 'ltr' | 'rtl';
|
||||
ogLocale?: string;
|
||||
lang?: string;
|
||||
};
|
||||
|
||||
export const KNOWN_LANGUAGES = {
|
||||
English: 'en',
|
||||
} as const;
|
||||
export const KNOWN_LANGUAGE_CODES = Object.values(KNOWN_LANGUAGES);
|
||||
|
||||
export const GITHUB_EDIT_URL = `https://github.com/withastro/astro/tree/main/examples/docs`;
|
||||
|
||||
export const COMMUNITY_INVITE_URL = `https://astro.build/chat`;
|
||||
|
||||
// See "Algolia" section of the README for more information.
|
||||
export const ALGOLIA = {
|
||||
indexName: 'XXXXXXXXXX',
|
||||
appId: 'XXXXXXXXXX',
|
||||
apiKey: 'XXXXXXXXXX',
|
||||
};
|
||||
|
||||
export type Sidebar = Record<
|
||||
typeof KNOWN_LANGUAGE_CODES[number],
|
||||
Record<string, { text: string; link: string }[]>
|
||||
>;
|
||||
export const SIDEBAR: Sidebar = {
|
||||
en: {
|
||||
'Learn': [
|
||||
{ text: 'Tutorial', link: 'tutorial' },
|
||||
],
|
||||
// 'Another Section': [{ text: 'Page 4', link: 'en/page-4' }],
|
||||
},
|
||||
};
|
||||
1
website/src/env.d.ts
vendored
Normal file
1
website/src/env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="astro/client" />
|
||||
10
website/src/languages.ts
Normal file
10
website/src/languages.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { KNOWN_LANGUAGES, KNOWN_LANGUAGE_CODES } from './config';
|
||||
export { KNOWN_LANGUAGES, KNOWN_LANGUAGE_CODES };
|
||||
|
||||
export const langPathRegex = /\/([a-z]{2}-?[A-Z]{0,2})\//;
|
||||
|
||||
export function getLanguageFromURL(pathname: string) {
|
||||
const langCodeMatch = pathname.match(langPathRegex);
|
||||
const langCode = langCodeMatch ? langCodeMatch[1] : 'en';
|
||||
return langCode as typeof KNOWN_LANGUAGE_CODES[number];
|
||||
}
|
||||
136
website/src/layouts/DefaultLayout.astro
Normal file
136
website/src/layouts/DefaultLayout.astro
Normal file
@ -0,0 +1,136 @@
|
||||
---
|
||||
import HeadCommon from '../components/HeadCommon.astro';
|
||||
import HeadSEO from '../components/HeadSEO.astro';
|
||||
import Header from '../components/Header/Header.astro';
|
||||
import PageContent from '../components/PageContent/PageContent.astro';
|
||||
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';
|
||||
|
||||
type Props = {
|
||||
frontmatter: CONFIG.Frontmatter;
|
||||
headings: MarkdownHeading[];
|
||||
};
|
||||
|
||||
const { frontmatter, headings } = Astro.props as Props;
|
||||
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
|
||||
const currentPage = Astro.url.pathname;
|
||||
const currentFile = `src/pages${currentPage.replace(/\/$/, '')}.md`;
|
||||
const githubEditUrl = `${CONFIG.GITHUB_EDIT_URL}/${currentFile}`;
|
||||
---
|
||||
|
||||
<html dir={frontmatter.dir ?? 'ltr'} lang={frontmatter.lang ?? 'en-us'} class="initial">
|
||||
<head>
|
||||
<HeadCommon />
|
||||
<HeadSEO frontmatter={frontmatter} canonicalUrl={canonicalURL} />
|
||||
<title>
|
||||
{frontmatter.title ? `${frontmatter.title} 🚀 ${CONFIG.SITE.title}` : CONFIG.SITE.title}
|
||||
</title>
|
||||
<style>
|
||||
body {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-rows: var(--theme-navbar-height) 1fr;
|
||||
--gutter: 0.5rem;
|
||||
--doc-padding: 2rem;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-template-columns: minmax(var(--gutter), 1fr) minmax(0, var(--max-width)) minmax(var(--gutter), 1fr);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.grid-sidebar {
|
||||
height: 100vh;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#grid-left {
|
||||
position: fixed;
|
||||
background-color: var(--theme-bg);
|
||||
z-index: 10;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#grid-main {
|
||||
padding: var(--doc-padding) var(--gutter);
|
||||
grid-column: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#grid-right {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 50em) {
|
||||
.layout {
|
||||
overflow: initial;
|
||||
grid-template-columns: 20rem minmax(0, var(--max-width));
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
#grid-left {
|
||||
display: flex;
|
||||
padding-left: 2rem;
|
||||
position: sticky;
|
||||
grid-column: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 72em) {
|
||||
.layout {
|
||||
grid-template-columns: 20rem minmax(0, var(--max-width)) 18rem;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#grid-right {
|
||||
grid-column: 3;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style is:global>
|
||||
.layout > * {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mobile-sidebar-toggle {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mobile-sidebar-toggle #grid-left {
|
||||
display: block;
|
||||
top: 2rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<Header currentPage={currentPage} />
|
||||
<main class="layout">
|
||||
<!-- <aside id="grid-left" class="grid-sidebar" title="Site Navigation">
|
||||
<LeftSidebar currentPage={currentPage} />
|
||||
</aside> -->
|
||||
<div id="grid-main">
|
||||
<PageContent frontmatter={frontmatter} headings={headings} githubEditUrl={githubEditUrl}>
|
||||
<slot />
|
||||
</PageContent>
|
||||
</div>
|
||||
<aside id="grid-right" class="grid-sidebar" title="Table of Contents">
|
||||
<RightSidebar headings={headings} githubEditUrl={githubEditUrl} />
|
||||
</aside>
|
||||
</main>
|
||||
<Footer path={currentFile} />
|
||||
</body>
|
||||
</html>
|
||||
51
website/src/layouts/MainLayout.astro
Normal file
51
website/src/layouts/MainLayout.astro
Normal file
@ -0,0 +1,51 @@
|
||||
---
|
||||
import HeadCommon from '../components/HeadCommon.astro';
|
||||
import HeadSEO from '../components/HeadSEO.astro';
|
||||
import Header from '../components/Header/Header.astro';
|
||||
import PageContent from '../components/PageContent/PageContent.astro';
|
||||
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';
|
||||
|
||||
type Props = {
|
||||
frontmatter: CONFIG.Frontmatter;
|
||||
headings: MarkdownHeading[];
|
||||
};
|
||||
|
||||
const { frontmatter, headings } = Astro.props as Props;
|
||||
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
|
||||
const currentPage = Astro.url.pathname;
|
||||
const currentFile = `src/pages${currentPage.replace(/\/$/, '')}.md`;
|
||||
const githubEditUrl = `${CONFIG.GITHUB_EDIT_URL}/${currentFile}`;
|
||||
---
|
||||
|
||||
<html dir={frontmatter.dir ?? 'ltr'} lang={frontmatter.lang ?? 'en-us'} class="initial">
|
||||
<head>
|
||||
<HeadCommon />
|
||||
<HeadSEO frontmatter={frontmatter} canonicalUrl={canonicalURL} />
|
||||
<title>
|
||||
{frontmatter.title ? `${frontmatter.title} 🚀 ${CONFIG.SITE.title}` : CONFIG.SITE.title}
|
||||
</title>
|
||||
</head>
|
||||
|
||||
<body class="h-screen">
|
||||
<div class="max-w-[1000px] m-auto h-full space-y-4 flex flex-col">
|
||||
<header class="max-w-full">
|
||||
<Header currentPage={currentPage} />
|
||||
</header>
|
||||
<main class="flex space-x-4 overflow-hidden relative">
|
||||
<main class="h-full grow overflow-auto px-2">
|
||||
<PageContent frontmatter={frontmatter} headings={headings} githubEditUrl={githubEditUrl}>
|
||||
<slot />
|
||||
</PageContent>
|
||||
</main>
|
||||
<aside class="w-2xl flex-none h-full overflow-auto px-4" title="Table of Contents">
|
||||
<RightSidebar headings={headings} githubEditUrl={githubEditUrl} />
|
||||
</aside>
|
||||
</main>
|
||||
<!-- <Footer path={currentFile} /> -->
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
7
website/src/pages/index.astro
Normal file
7
website/src/pages/index.astro
Normal file
@ -0,0 +1,7 @@
|
||||
<script is:inline>
|
||||
// Redirect your homepage to the first page of documentation.
|
||||
// If you have a landing page, remove this script and add it here!
|
||||
// window.location.pathname = `/en/introduction`;
|
||||
</script>
|
||||
|
||||
<h1>Hello</h1>
|
||||
7
website/src/pages/tutorial/index.astro
Normal file
7
website/src/pages/tutorial/index.astro
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
import { Content } from '../../components/tutorial.mdx';
|
||||
import MiniRepl from '../../components/strudel/MiniRepl.astro';
|
||||
import JsDoc from '../../components/strudel/JsDoc.astro';
|
||||
---
|
||||
|
||||
<Content components={{ MiniRepl, JsDoc }} />
|
||||
0
website/src/styles/index.css
Normal file
0
website/src/styles/index.css
Normal file
125
website/src/styles/theme.css
Normal file
125
website/src/styles/theme.css
Normal file
@ -0,0 +1,125 @@
|
||||
:root {
|
||||
--font-fallback: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif,
|
||||
Apple Color Emoji, Segoe UI Emoji;
|
||||
--font-body: system-ui, var(--font-fallback);
|
||||
--font-mono: 'IBM Plex Mono', Consolas, 'Andale Mono WT', 'Andale Mono', 'Lucida Console',
|
||||
'Lucida Sans Typewriter', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Liberation Mono',
|
||||
'Nimbus Mono L', Monaco, 'Courier New', Courier, monospace;
|
||||
|
||||
/*
|
||||
* Variables with --color-base prefix define
|
||||
* the hue, and saturation values to be used for
|
||||
* hsla colors.
|
||||
*
|
||||
* ex:
|
||||
*
|
||||
* --color-base-{color}: {hue}, {saturation};
|
||||
*
|
||||
*/
|
||||
|
||||
--color-base-white: 0, 0%;
|
||||
--color-base-black: 240, 100%;
|
||||
--color-base-gray: 215, 14%;
|
||||
--color-base-blue: 212, 100%;
|
||||
--color-base-blue-dark: 212, 72%;
|
||||
--color-base-green: 158, 79%;
|
||||
--color-base-orange: 22, 100%;
|
||||
--color-base-purple: 269, 79%;
|
||||
--color-base-red: 351, 100%;
|
||||
--color-base-yellow: 41, 100%;
|
||||
|
||||
/*
|
||||
* Color palettes are made using --color-base
|
||||
* variables, along with a lightness value to
|
||||
* define different variants.
|
||||
*
|
||||
*/
|
||||
|
||||
--color-gray-5: var(--color-base-gray), 5%;
|
||||
--color-gray-10: var(--color-base-gray), 10%;
|
||||
--color-gray-20: var(--color-base-gray), 20%;
|
||||
--color-gray-30: var(--color-base-gray), 30%;
|
||||
--color-gray-40: var(--color-base-gray), 40%;
|
||||
--color-gray-50: var(--color-base-gray), 50%;
|
||||
--color-gray-60: var(--color-base-gray), 60%;
|
||||
--color-gray-70: var(--color-base-gray), 70%;
|
||||
--color-gray-80: var(--color-base-gray), 80%;
|
||||
--color-gray-90: var(--color-base-gray), 90%;
|
||||
--color-gray-95: var(--color-base-gray), 95%;
|
||||
|
||||
--color-blue: var(--color-base-blue), 61%;
|
||||
--color-blue-dark: var(--color-base-blue-dark), 39%;
|
||||
--color-green: var(--color-base-green), 42%;
|
||||
--color-orange: var(--color-base-orange), 50%;
|
||||
--color-purple: var(--color-base-purple), 54%;
|
||||
--color-red: var(--color-base-red), 54%;
|
||||
--color-yellow: var(--color-base-yellow), 59%;
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--theme-accent: hsla(var(--color-blue), 1);
|
||||
--theme-text-accent: hsla(var(--color-blue), 1);
|
||||
--theme-accent-opacity: 0.15;
|
||||
--theme-divider: hsla(var(--color-gray-95), 1);
|
||||
--theme-text: hsla(var(--color-gray-10), 1);
|
||||
--theme-text-light: hsla(var(--color-gray-40), 1);
|
||||
/* @@@: not used anywhere */
|
||||
--theme-text-lighter: hsla(var(--color-gray-80), 1);
|
||||
--theme-bg: hsla(var(--color-base-white), 100%, 1);
|
||||
--theme-bg-hover: hsla(var(--color-gray-95), 1);
|
||||
--theme-bg-offset: hsla(var(--color-gray-90), 1);
|
||||
--theme-bg-accent: hsla(var(--color-blue), var(--theme-accent-opacity));
|
||||
--theme-code-inline-bg: hsla(var(--color-gray-95), 1);
|
||||
--theme-code-inline-text: var(--theme-text);
|
||||
--theme-code-bg: hsla(217, 19%, 27%, 1);
|
||||
--theme-code-text: hsla(var(--color-gray-95), 1);
|
||||
--theme-navbar-bg: hsla(var(--color-base-white), 100%, 1);
|
||||
--theme-navbar-height: 6rem;
|
||||
--theme-selection-color: hsla(var(--color-blue), 1);
|
||||
--theme-selection-bg: hsla(var(--color-blue), var(--theme-accent-opacity));
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--theme-bg);
|
||||
color: var(--theme-text);
|
||||
}
|
||||
|
||||
:root.theme-dark {
|
||||
color-scheme: dark;
|
||||
--theme-accent-opacity: 0.15;
|
||||
--theme-accent: hsla(var(--color-blue), 1);
|
||||
--theme-text-accent: hsla(var(--color-blue), 1);
|
||||
--theme-divider: hsla(var(--color-gray-10), 1);
|
||||
--theme-text: hsla(var(--color-gray-90), 1);
|
||||
--theme-text-light: hsla(var(--color-gray-80), 1);
|
||||
|
||||
/* @@@: not used anywhere */
|
||||
--theme-text-lighter: hsla(var(--color-gray-40), 1);
|
||||
--theme-bg: hsla(215, 28%, 17%, 1);
|
||||
--theme-bg-hover: hsla(var(--color-gray-40), 1);
|
||||
--theme-bg-offset: hsla(var(--color-gray-5), 1);
|
||||
--theme-code-inline-bg: hsla(var(--color-gray-10), 1);
|
||||
--theme-code-inline-text: hsla(var(--color-base-white), 100%, 1);
|
||||
--theme-code-bg: hsla(var(--color-gray-5), 1);
|
||||
--theme-code-text: hsla(var(--color-base-white), 100%, 1);
|
||||
--theme-navbar-bg: hsla(215, 28%, 17%, 1);
|
||||
--theme-selection-color: hsla(var(--color-base-white), 100%, 1);
|
||||
--theme-selection-bg: hsla(var(--color-purple), var(--theme-accent-opacity));
|
||||
|
||||
/* DocSearch [Algolia] */
|
||||
--docsearch-modal-background: var(--theme-bg);
|
||||
--docsearch-searchbox-focus-background: var(--theme-divider);
|
||||
--docsearch-footer-background: var(--theme-divider);
|
||||
--docsearch-text-color: var(--theme-text);
|
||||
--docsearch-hit-background: var(--theme-divider);
|
||||
--docsearch-hit-shadow: none;
|
||||
--docsearch-hit-color: var(--theme-text);
|
||||
--docsearch-footer-shadow: inset 0 2px 10px #000;
|
||||
--docsearch-modal-shadow: inset 0 0 8px #000;
|
||||
}
|
||||
|
||||
::selection {
|
||||
color: var(--theme-selection-color);
|
||||
background-color: var(--theme-selection-bg);
|
||||
}
|
||||
8
website/tailwind.config.cjs
Normal file
8
website/tailwind.config.cjs
Normal file
@ -0,0 +1,8 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [require('@tailwindcss/typography')],
|
||||
}
|
||||
8
website/tsconfig.json
Normal file
8
website/tsconfig.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"skipLibCheck": true,
|
||||
"jsxImportSource": "react"
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user