Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@ vite.config.ts.timestamp-*
/flatpak/.flatpak-builder/
/flatpak/splitwave.deb
/flatpak/*.flatpak

# storybook
/storybook-static
38 changes: 38 additions & 0 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { StorybookConfig } from '@storybook/sveltekit';

const CSF_ADDON = '@storybook/addon-svelte-csf';
const KIT_MOCKS = '@storybook/sveltekit/internal/mocks';
// Deps that ship uncompiled Svelte (components, runes). The Svelte plugin on
// this Vite cannot hook esbuild pre-bundling, so they are served unbundled.
const RAW_SVELTE_DEPS = [CSF_ADDON, KIT_MOCKS];
const isRawSvelte = (d: string) => RAW_SVELTE_DEPS.some((p) => d.startsWith(p));

const config: StorybookConfig & { optimizeViteDeps?: (deps: string[]) => string[] } = {
stories: ['../src/**/*.stories.svelte'],
addons: [CSF_ADDON],
framework: '@storybook/sveltekit',
optimizeViteDeps: (deps) => deps.filter((d) => !isRawSvelte(d)),
viteFinal(config) {
config.plugins = [
...(config.plugins ?? []),
{
name: 'splitwave:unbundle-svelte-csf',
enforce: 'post',
config(c) {
const deps = c.optimizeDeps ?? {};
deps.include = (deps.include ?? []).filter((d) => !isRawSvelte(d));
deps.exclude = [
...(deps.exclude ?? []),
CSF_ADDON,
`${CSF_ADDON}/internal/create-runtime-stories`,
...['forms', 'navigation', 'stores', 'state.svelte.js'].map((m) => `${KIT_MOCKS}/app/${m}`)
];
c.optimizeDeps = deps;
}
}
];
return config;
}
};

export default config;
39 changes: 39 additions & 0 deletions .storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { Preview } from '@storybook/sveltekit';
import '../src/app.css';
import '@xyflow/svelte/dist/base.css';
import { DEFAULT_ENV, installMockBackend } from '../src/stories/harness/backend';

// Stories that never mount a node canvas still touch Tauri through shared stores.
installMockBackend(DEFAULT_ENV);
// The app shell never scrolls; story pages taller than the viewport must.
document.body.style.overflow = 'auto';

const preview: Preview = {
globalTypes: {
theme: {
description: 'Colour theme',
toolbar: {
title: 'Theme',
icon: 'mirror',
items: [
{ value: 'light', title: 'Light' },
{ value: 'dark', title: 'Dark' }
],
dynamicTitle: true
}
}
},
initialGlobals: { theme: 'dark' },
decorators: [
(story, context) => {
document.documentElement.classList.toggle('dark', context.globals.theme === 'dark');
return story();
}
],
parameters: {
layout: 'fullscreen',
controls: { expanded: true, sort: 'none' }
}
};

export default preview;
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Docs live in `docs/`, not here:
- [docs/CONCEPT.md](docs/CONCEPT.md) — engine architecture: RT audio path, pacing, effects, layout, frontend conventions, Rust quirks, platforms.
- [docs/RULES.md](docs/RULES.md) — universal rules for every change.
- [docs/FEATURES.md](docs/FEATURES.md) — how a new feature must look (categories, inline params, scope, per-platform behaviour).
- [docs/UI.md](docs/UI.md) — building UI: reuse Storybook components, stories required for every node and shared component.
- [CONTRIBUTING.md](CONTRIBUTING.md) — diff hygiene, per-OS testing, PR checklist.
- [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) — build and run.

Expand Down Expand Up @@ -34,4 +35,5 @@ Without these, one chunk takes ~16 ms and the worker stalls.
- Read the current code, not earlier explanations.
- RT path change → `cargo check`.
- Svelte change → `bun run check`.
- New node or shared UI component → reuse existing Storybook components first, add its stories under `src/stories/` in the same commit (see [docs/UI.md](docs/UI.md)).
- Rust `#[derive(TS)]` change → `bun run generate`, commit the generated files with the Rust change.
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ cargo check --manifest-path src-tauri/Cargo.toml
bun run format # Prettier + rustfmt
```

Both checks must pass, and `format` must leave the tree clean. If you changed a `#[derive(TS)]` type in Rust, run `bun run generate` and commit the regenerated files in `src/lib/modules/pipeline/generated/` together with the Rust change — never by hand, and never on their own.
Both checks must pass, and `format` must leave the tree clean. A new node or shared component ships with its Storybook stories in the same PR; see [docs/UI.md](docs/UI.md). If you changed a `#[derive(TS)]` type in Rust, run `bun run generate` and commit the regenerated files in `src/lib/modules/pipeline/generated/` together with the Rust change — never by hand, and never on their own.

Anything persisted to disk — pipeline JSON, `virtual-devices.json`, the macOS driver plist — has to keep loading for people upgrading. Add a `#[serde(default)]` or a versioned migration; do not silently change the meaning of an existing field.

Expand Down
270 changes: 269 additions & 1 deletion bun.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ bun run tauri dev
```bash
bun run check # svelte-check + tsc
bun run generate # regenerate TypeScript types from Rust (ts-rs)
bun run storybook # Storybook on :6006, nodes and shared components with mocked engine
cargo check --manifest-path src-tauri/Cargo.toml
bun run tauri build --bundles app # local .app build (macOS)
bun run tauri build --bundles appimage # local AppImage build (Linux)
Expand Down
3 changes: 2 additions & 1 deletion docs/FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,5 @@ A platform that cannot support the feature returns a real error from that backen
2. Confirm the category: input, effect, output, monitor, or network.
3. Sketch the node's inline parameters — every one must fit in the node.
4. Check which per-OS files the change reaches.
5. Split into smallest viable PRs. Each PR follows [CONTRIBUTING.md](../CONTRIBUTING.md).
5. Build the node from existing Storybook components and add its stories, per [UI.md](UI.md).
6. Split into smallest viable PRs. Each PR follows [CONTRIBUTING.md](../CONTRIBUTING.md).
3 changes: 2 additions & 1 deletion docs/RULES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# RULES.md

Universal rules that apply to every change in this repo. Concept and architecture background: [CONCEPT.md](CONCEPT.md). What a new feature must look like: [FEATURES.md](FEATURES.md). PR hygiene: [CONTRIBUTING.md](../CONTRIBUTING.md).
Universal rules that apply to every change in this repo. Concept and architecture background: [CONCEPT.md](CONCEPT.md). What a new feature must look like: [FEATURES.md](FEATURES.md). How UI is built: [UI.md](UI.md). PR hygiene: [CONTRIBUTING.md](../CONTRIBUTING.md).

## Sections

Expand Down Expand Up @@ -47,4 +47,5 @@ type(scope): subject
- Read the current code, not earlier explanations.
- RT path change → `cargo check`.
- Svelte change → `bun run check`.
- New node or shared component → add its stories in the same commit, see [UI.md](UI.md).
- Rust `#[derive(TS)]` change → `bun run generate`, commit the generated files with the Rust change.
48 changes: 48 additions & 0 deletions docs/UI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# UI.md

How node and app UI is built. Code conventions for Svelte: [CONCEPT.md](CONCEPT.md#frontend). What a node may contain: [FEATURES.md](FEATURES.md).

## Sections

- [Storybook is the catalogue](#storybook-is-the-catalogue)
- [Reuse before you build](#reuse-before-you-build)
- [Adding a node](#adding-a-node)
- [Adding a shared component](#adding-a-shared-component)
- [Mocked engine](#mocked-engine)

## Storybook is the catalogue

`bun run storybook` opens every node and every shared component on :6006, driven by a mocked engine. Start there before writing UI: it shows what already exists and how each piece looks in every state, in light and dark.

| Folder | Contents |
| ------------------------- | ---------------------------------------------------- |
| `src/stories/nodes/` | One file per node kind, plus `Nodes/Gallery` |
| `src/stories/forms/` | Inputs: combobox, toggle, slider, stepper, segmented |
| `src/stories/components/` | Display pieces: meter bar, signal bars, copy button |
| `src/stories/harness/` | Node canvas, mocked engine, fake signal, arg helpers |

## Reuse before you build

- Pick controls from Storybook first. A node that needs a dropdown uses `Combobox`, a parameter uses the effect `Slider`, an on/off uses `Toggle`, a small choice uses `SegmentedButtons`.
- If nothing fits, extend the existing component rather than cloning it into the node.
- A new visual element used, or likely to be used, in more than one place becomes a shared component, not markup inside a node.

## Adding a node

The node's stories land in the same commit as the node, in `src/stories/nodes/<kind>.stories.svelte`:

- Render it through `NodeCanvas` with `kind` and the story args.
- Expose every field of the node's data as a control (`dataArgs`), plus the signal (`signalArgs`), wiring (`wiringArgs`) and engine state (`envArgs`) the node reacts to.
- One named story per visual state: defaults, extremes, bypassed, unwired, multichannel, missing device or file, errors, per-platform differences.

## Adding a shared component

Its stories land in the same commit, in `src/stories/forms/` for inputs or `src/stories/components/` for everything else:

- Controls for every prop that changes how it looks.
- Controlled inputs go through `Controlled` so they respond to clicks, with changes logged in the Actions panel via `fn()`.
- One named story per state: empty, filled, disabled, compact size, overflow (long labels, long lists).

## Mocked engine

Stories never call the real Tauri backend. `harness/backend.ts` answers every command the UI sends, `harness/signal.ts` emits scope, meter, loudness, gain-reduction and transport events. When a node needs a new command or event, add it there with an `envArgs` / `signalArgs` control; do not stub it inside a single story.
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"tauri": "tauri",
"format": "prettier --write \"**/*.{ts,tsx,md,svelte,json}\" && cargo fmt --manifest-path src-tauri/Cargo.toml",
"generate": "cd src-tauri && cargo test"
"generate": "cd src-tauri && cargo test",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"license": "MIT",
"dependencies": {
Expand All @@ -37,13 +39,17 @@
"tailwindcss": "^4.3.0"
},
"devDependencies": {
"@storybook/addon-svelte-csf": "^5.1.3",
"@storybook/svelte": "^10.6.0",
"@storybook/sveltekit": "^10.6.0",
"@sveltejs/adapter-static": "^3.0.6",
"@sveltejs/kit": "^2.9.0",
"@sveltejs/vite-plugin-svelte": "^7.1.2",
"@tauri-apps/cli": "^2",
"@types/semver": "^7.8.0",
"bun-types": "^1.4.2",
"prettier": "3.8.3",
"storybook": "^10.6.0",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"typescript": "~5.6.2",
Expand Down
40 changes: 0 additions & 40 deletions src/routes/preview/+page.svelte

This file was deleted.

104 changes: 0 additions & 104 deletions src/routes/preview/_fake_signal.ts

This file was deleted.

18 changes: 18 additions & 0 deletions src/stories/components/copy_button.stories.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import CopyButton from '$lib/components/copy_button.svelte';

const { Story } = defineMeta({
title: 'Components/Copy Button',
component: CopyButton,
argTypes: { text: { control: 'text' }, label: { control: 'text' }, copiedLabel: { control: 'text' } },
args: { text: 'K7Q2XF', label: 'Copy', copiedLabel: 'Copied!' }
});
</script>

{#snippet template(args: Record<string, any>)}
<div class="p-8"><CopyButton {...args} text={args.text} /></div>
{/snippet}

<Story name="Default" {template} />
<Story name="Room code" args={{ label: 'Copy room code' }} {template} />
Loading
Loading