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
4 changes: 2 additions & 2 deletions components.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"utils": "@/utilities/cn",
"ui": "@/components/ui",
"lib": "@/lib",
"lib": "@/utilities",
"hooks": "@/hooks"
},
"menuColor": "default",
Expand Down
4 changes: 2 additions & 2 deletions cspell.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ export default defineConfig({
"src/midi/scales.ts",
],
dictionaryDefinitions: [
{ name: "project", path: "./.cspell/project.txt", addWords: true },
{ name: "music", path: "./.cspell/music.txt" },
{ name: "project", path: ".cspell/project.txt", addWords: true },
{ name: "music", path: ".cspell/music.txt" },
],
dictionaries: ["project", "music"],
overrides: [
Expand Down
38 changes: 26 additions & 12 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,25 @@

Modules in `src/` use lowercase, hyphen-separated filenames. `components/ui/` contains
shadcn Base UI primitives; `components/layout/` contains the header and footer.
`features/workspace/` separates components, dialogs, and runtime coordination.
`components/playground/` contains the interactive screen and its controls;
`components/dialogs/` contains the algorithm editor and MIDI export dialogs.
`sorting/` contains the engine and request handling, and
`sorting/algorithms/` only algorithm implementations. Generators, utilities,
MIDI support, and visualizations each have their own directory. The client entry,
worker entry, and vendor bridge stay at the top level. `@/` aliases `src/`.
`sorting/algorithms/` only algorithm implementations. Generators,
MIDI support, and visualizations each have their own directory. The worker lives in
`sorting/worker.mjs`; the Timbre adapter lives in `audio/timbre.mjs`. `@/` aliases `src/`.

`utilities/` contains small, stateless, domain-independent helpers: `cn.ts` combines
CSS classes; `random.ts`, `shuffle.ts`, and `swap.ts` handle numbers and arrays.
There is no separate `lib/` folder or catch-all `utils.ts` file. shadcn's
`utils` alias points to `@/utilities/cn` so newly generated components use the same helper.
Domain-specific code stays with its domain rather than accumulating in `utilities/`.

The sorting playground is the interactive data/sort screen, including its editor
and playback controls. React presentation lives under `components/`;
`controllers/` connects the sorting, audio, state, and visualization modules. Shared
settings and atoms remain in `state/`; component-local state stays in its component. A player
is one data or sort panel; the playground coordinates both. The older “workspace”
name did not describe a separate domain concept.

TypeScript is introduced incrementally alongside `.mjs` modules. Vite handles
bundling; `pnpm run typecheck` checks `.ts` and `.tsx` application modules separately.
Expand All @@ -18,30 +32,30 @@ one level above, separate from the implementations they register.

## UI

TanStack Start routes in `src/routes/` render Home, About, and API through the
shared document in `src/pages/`. Public URLs are `/audio-sort/`, `/audio-sort/about`, and `/audio-sort/api`;
TanStack Start routes in `src/routes/` contain Home, About, and API page content and use
the shared document in `src/components/layout/`. Public URLs are `/audio-sort/`, `/audio-sort/about`, and `/audio-sort/api`;
prerendering emits `index.html`, `about/index.html`, and `api/index.html`.
TanStack links provide client navigation and ordinary anchor fallbacks without
JavaScript. Audio and editor dependencies are dynamically imported by
Home's effect, never evaluated during server prerendering. About/API remain usable
without JavaScript. `src/client.tsx` hydrates the document.

[`browser-workspace.tsx`](../src/features/workspace/browser-workspace.tsx) owns the browser workspace lifecycle with an application-scoped
[`browser-playground.tsx`](../src/components/playground/browser-playground.tsx) owns the browser playground lifecycle with an application-scoped
vanilla Jotai store. React owns settings, tabs, transport controls, counters,
sliders, and dialogs. Shared buttons, links, and option controls use Tailwind classes.
`styles/globals.css` holds the shadcn theme and document defaults;
`styles/visualizations.css` styles D3-owned SVG children. There is no `site.css`.
The light theme uses sky accents and neutral surfaces. Workspace layout uses one
The light theme uses sky accents and neutral surfaces. Playground layout uses one
threshold (`lg`, 1024px): stacked below it, side-by-side above it. Chart heights
are fluid, bounded with `clamp()`, without height-specific media queries.

[`create-workspace.mjs`](../src/features/workspace/runtime/create-workspace.mjs) coordinates workers,
[`create-playground.mjs`](../src/controllers/create-playground.mjs) coordinates workers,
data generation, settings subscriptions, soundfont preloading, and player lifetime.
React reads its playback snapshots through `useSyncExternalStore`. Settings and
custom algorithms are read directly from Jotai; there is no mirrored settings cache.
Audio clocks and nodes remain outside React and Jotai.

[`create-workspace-player.mjs`](../src/features/workspace/runtime/create-workspace-player.mjs) owns
[`create-player.mjs`](../src/controllers/create-player.mjs) owns
the contents of its D3 SVG and delegates transport and synthesis to
`audio/create-transport.ts` and `audio/create-timbre-audio.mjs`.
React renders the surrounding controls and an empty SVG host, never chart children.
Expand All @@ -65,11 +79,11 @@ initialization does not depend on measuring thumb widths. MIDI export uses shadc
Cached-page suspension disconnects runtime effects, pauses audio, cancels workers
and pending resumes, and closes dialogs. Returning reconnects effects without
automatically playing. React continues to represent the same Jotai store.
Non-cached exits and Home effect cleanup unmount the workspace, dispose owned resources, and
Non-cached exits and Home effect cleanup unmount the playground, dispose owned resources, and
release native pointer listeners. Fresh runtime instances can reuse the store.
The shared AudioContext stays library-owned.

All third-party JavaScript uses package imports. `vendor.mjs` only re-exports the
All third-party JavaScript uses package imports. `audio/timbre.mjs` only re-exports the
pinned Timbre browser entry; there are no classic script tags or `public/js` files.
D3 imports remain scoped. Sample audio is fetched and decoded by first-party
modules; see [the audio boundary](audio-dependencies.md).
Expand Down
5 changes: 3 additions & 2 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ styles live in `src/styles/visualizations.css`.

Add primitives with `pnpm dlx shadcn@latest add <component>`. `components.json`
selects Base UI, the Nova preset, neutral base colors, and Lucide icons. Keep shared
primitives in `src/components/ui/` and workspace behavior in `src/features/workspace/`.
primitives in `src/components/ui/`, screen controls in `src/components/playground/`,
and dialogs in `src/components/dialogs/`. Non-React coordination lives in `src/controllers/`.
Use `lg:` for the application's stacked/side-by-side layout; avoid adding extra width tiers.

## Checks
Expand All @@ -59,7 +60,7 @@ TypeScript can infer imported JavaScript modules (`allowJs`), but `checkJs` stay
off until those modules are migrated. Worker payloads are still checked at runtime.

Vendor and generated files are excluded from linting and formatting. Oxfmt formats
source CSS and TSX alongside JavaScript. `src/pages/site-document.tsx` owns the
source CSS and TSX alongside JavaScript. `src/components/layout/site-document.tsx` owns the
shared document, header, and footer. Start generates `src/route-tree.gen.ts` from
`src/routes/`; commit that file but do not edit it manually.

Expand Down
24 changes: 12 additions & 12 deletions docs/migration-handoff.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@

## Completed UI migration

PR #47 migrated the whole workspace; PR #48 polished icons, hover states, and typography:
PR #47 migrated the whole playground; PR #48 polished icons, hover states, and typography:

- The workspace uses a reusable vanilla Jotai store.
- `ui/workspace.tsx` assembles settings, playback controls, and native dialogs.
- `ui/create-workspace.mjs` owns worker/data coordination and audio subscriptions.
- `ui/create-workspace-player.mjs` bridges the existing transport/audio modules and D3.
- The playground uses a reusable vanilla Jotai store.
- `components/playground/sorting-playground.tsx` assembles settings, playback controls, and dialogs.
- `controllers/create-playground.mjs` owns worker/data coordination and audio subscriptions.
- `controllers/create-player.mjs` bridges the existing transport/audio modules and D3.
- React owns controls; D3 owns SVG children; audio draws the waveform canvas.
- Settings/algorithm overrides stay in Jotai. Playback snapshots use `useSyncExternalStore`.
- Native ranges replace plugin sliders; native dialogs handle editing and MIDI export.
Expand All @@ -34,12 +34,12 @@ the old JSONP/MP3 extensions. No new license-output logic is included.
## Completed TanStack Start shell

Eleventy and Liquid are replaced by TanStack Start file routes and React pages.
The existing workspace, runtime, and visual design are retained.
The existing playground, runtime, and visual design are retained.

- `src/routes/` defines the three pages; Start generates `src/route-tree.gen.ts`.
- `src/pages/site-document.tsx` renders the shared header/footer and document.
- `src/pages/home.tsx` dynamically imports `src/features/workspace/browser-workspace.tsx` after hydration.
It renders the workspace within Start's React root; runtime lifecycle cleanup
- `src/components/layout/site-document.tsx` renders the shared header/footer and document.
- `src/routes/index.tsx` dynamically imports `src/components/playground/browser-playground.tsx` after hydration.
It renders the playground within Start's React root; runtime lifecycle cleanup
handles navigation away, cached pages, and remounts. Audio/editor modules never
execute during prerendering.
- Public routes are `/audio-sort/`, `/audio-sort/about`, and `/audio-sort/api`. Per the user's updated preference,
Expand All @@ -55,7 +55,7 @@ The existing workspace, runtime, and visual design are retained.
per-page output configuration. Link crawling stays disabled because it duplicates
base-prefixed URLs in the Pages build.
- Tests cover clean URLs/reloads, client navigation, no-JavaScript content,
hydration errors, lazy-load recovery, and the existing workspace regressions.
hydration errors, lazy-load recovery, and the existing playground regressions.
- Adding Vite's raw-import types exposed an existing `getFunctionBody` signature
mismatch; it now explicitly accepts the source strings it already handled.

Expand All @@ -66,10 +66,10 @@ or sorting APIs. It uses shadcn's Base UI Nova primitives, retains Lucide and th
system font, and maps the light theme to Tailwind sky/neutral colors.

- `src/components/ui/`: shared primitives; `components/layout/`: header/footer.
- `src/features/workspace/`: components, separate dialogs, and runtime coordination.
- `src/components/playground/` and `src/components/dialogs/`: controls and dialogs; `src/controllers/`: runtime coordination.
- `src/styles/globals.css`: theme/document defaults; `visualizations.css`: D3 state styles.
- `site.css` and the `tw:` prefix are removed. Buttons and links own their Tailwind classes.
- Two workspace layouts use one 1024px threshold; chart heights are fluid.
- Two playground layouts use one 1024px threshold; chart heights are fluid.
- Tab panels stay mounted for Ace/canvas lifetime. Slider thumbs use center alignment
to avoid hidden-panel measurement. Base UI handles dialog focus and dismissal.

Expand Down
10 changes: 5 additions & 5 deletions docs/modernization.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ JSONP/MP3/soundfont scripts are removed. The [audio dependency audit](audio-depe
records sample-host verification and the Timbre package compatibility checks.
Timbre now uses the pinned `14.11.25` browser entry with its Node-only dependencies
excluded. The obsolete local bundles, map, and Flash asset are removed.
Phase 6 now replaces the complete workspace in PR #47, not a sequence of islands.
Phase 6 now replaces the complete playground in PR #47, not a sequence of islands.
React owns all controls and dialogs, uses the existing Jotai store, and subscribes
to playback snapshots from a separate runtime. D3 and audio retain their owned
SVG/canvas hosts. The legacy controller, plugin sliders, Bootstrap CSS/JavaScript,
Expand All @@ -100,12 +100,12 @@ Public routes are `/audio-sort/`, `/audio-sort/about`, and `/audio-sort/api`, wi
Static prerendering emits directory index files into `dist/audio-sort/`;
build-time server files remain in `.tanstack/` and are not deployed. Development,
production, preview, and browser tests share the same base and build configuration.
Home imports the existing workspace after hydration; About/API need no audio or
editor runtime. Client navigation unmounts the workspace when leaving Home.
Home imports the existing playground after hydration; About/API need no audio or
editor runtime. Client navigation unmounts the playground when leaving Home.

The current UI refinement adds shadcn Base UI (Nova preset) before phase 8.
Shared primitives live in `components/ui/`; workspace components, dialogs, and
runtime modules live in `features/workspace/`. Tailwind component classes replace
Shared primitives live in `components/ui/`; playground components, dialogs, and
runtime modules live in `components/playground/`, `components/dialogs/`, and `controllers/`, respectively. Tailwind component classes replace
`site.css`, with sky/neutral theme tokens and a small D3 stylesheet. One `lg`
threshold separates stacked and side-by-side layouts; chart heights use `clamp()`.
Review the larger controls, dialog behavior, and laptop/mobile layouts before merging.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Envelope, EnvelopeKey } from "../../../state/envelope.ts";
import type { Envelope, EnvelopeKey } from "../state/envelope.ts";

export function formatEnvelopeValue(key: EnvelopeKey, value: number): string {
if (key === "s") return `${Math.round(value * 100)}%`;
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { Button } from "@/components/ui/button";
import { useEffect, useRef, useState } from "react";
import { useAtomValue } from "jotai";
import { settingsAtom } from "../../../state/settings.ts";
import { settingsAtom } from "../../state/settings.ts";
import {
algorithmCatalogAtom,
addAlgorithmAtom,
editAlgorithmAtom,
} from "../../../state/algorithm-overrides.ts";
import { algorithms } from "../../../sorting/algorithm-registry.mjs";
import { sources } from "../../../sorting/algorithm-sources.mjs";
import { getFunctionBody } from "../../../sorting/sort-requests.ts";
import type { createCodeEditor } from "../runtime/create-code-editor.mjs";
import type { Props } from "../runtime/workspace-types.ts";
import { WorkspaceDialog as Dialog } from "./workspace-dialog.tsx";
} from "../../state/algorithm-overrides.ts";
import { algorithms } from "../../sorting/algorithm-registry.mjs";
import { sources } from "../../sorting/algorithm-sources.mjs";
import { getFunctionBody } from "../../sorting/sort-requests.ts";
import type { createCodeEditor } from "./create-code-editor.mjs";
import type { Props } from "../../controllers/playground-types.ts";
import { PlayerDialog as Dialog } from "./player-dialog.tsx";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import { Input } from "@/components/ui/input";
export function AlgorithmDialog({
Expand All @@ -32,7 +32,7 @@ export function AlgorithmDialog({
useEffect(() => {
let cancelled = false;
let instance: ReturnType<typeof createCodeEditor> | undefined;
void import("../runtime/create-code-editor.mjs")
void import("./create-code-editor.mjs")
.then(({ createCodeEditor }) => {
if (cancelled) return;
instance = createCodeEditor(host.current!);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { NativeSelect } from "@/components/ui/native-select";
import { instruments } from "../../../midi/instruments.ts";
import { WorkspaceDialog as Dialog } from "./workspace-dialog.tsx";
import type { Props, PlayerId } from "../runtime/workspace-types.ts";
import { instruments } from "../../midi/instruments.ts";
import { PlayerDialog as Dialog } from "./player-dialog.tsx";
import type { Props, PlayerId } from "../../controllers/playground-types.ts";
export function MidiDialog({
runtime,
id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";

export function WorkspaceDialog({
export function PlayerDialog({
id,
title,
children,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { ReactNode } from "react";
import { HeadContent, Scripts } from "@tanstack/react-router";
import { Header } from "../components/layout/header.tsx";
import { Footer } from "../components/layout/footer.tsx";
import stylesheet from "../styles/globals.css?url";
import { Header } from "./header.tsx";
import { Footer } from "./footer.tsx";
import stylesheet from "../../styles/globals.css?url";

export function SiteDocument({ children }: { children: ReactNode }) {
return (
Expand Down
2 changes: 1 addition & 1 deletion src/components/option-button.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { cn } from "@/utilities/cn";
import type { ComponentProps } from "react";

export function OptionButton({ className, ...props }: ComponentProps<typeof Button>) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { useEffect, useState } from "react";
import { createStore } from "jotai/vanilla";
import { Workspace } from "./components/workspace.tsx";
import { createWorkspace } from "./runtime/create-workspace.mjs";
import { SortingPlayground } from "./sorting-playground.tsx";
import { createPlayground } from "../../controllers/create-playground.mjs";

const store = createStore();
type Runtime = ReturnType<typeof createWorkspace>;
type Runtime = ReturnType<typeof createPlayground>;

export function BrowserWorkspace() {
export function BrowserPlayground() {
const [runtime, setRuntime] = useState<Runtime | null>(null);
useEffect(() => {
let current: Runtime | null = null;
const mount = () => {
current = createWorkspace(store);
current = createPlayground(store);
setRuntime(current);
};
const onPageHide = (event: PageTransitionEvent) => {
Expand All @@ -36,5 +36,5 @@ export function BrowserWorkspace() {
current?.destroy();
};
}, []);
return runtime ? <Workspace runtime={runtime} /> : null;
return runtime ? <SortingPlayground runtime={runtime} /> : null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { Toggle } from "@/components/ui/toggle";
import { ValueSlider } from "@/components/value-slider";
import { useSyncExternalStore } from "react";
import { useAtomValue } from "jotai";
import { playbackPreferencesAtom } from "../../../state/playback-preferences.ts";
import type { Props, PlayerId } from "../runtime/workspace-types.ts";
import { playbackPreferencesAtom } from "../../state/playback-preferences.ts";
import type { Props, PlayerId } from "../../controllers/playground-types.ts";
import { FastForward, Rewind, SkipBack, SkipForward, Square, RotateCcw } from "lucide-react";
import { ControlIcon } from "../../../components/control-icon.tsx";
import { ControlIcon } from "../control-icon.tsx";

export function Counters({ runtime }: Props) {
const state = useSyncExternalStore(runtime.subscribe, () => runtime.getSnapshot().sort);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import type { ReactNode } from "react";
import { Download } from "lucide-react";
import { Button } from "@/components/ui/button";
import { ControlIcon } from "@/components/control-icon";
import type { PlayerId } from "../runtime/workspace-types";
import type { PlayerId } from "../../controllers/playground-types";

export function WorkspaceSection({
export function PlayerSection({
id,
title,
description,
Expand Down
Loading