diff --git a/components.json b/components.json index 42acebc..e569f26 100644 --- a/components.json +++ b/components.json @@ -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", diff --git a/cspell.config.ts b/cspell.config.ts index 2d34690..a5d7d2e 100644 --- a/cspell.config.ts +++ b/cspell.config.ts @@ -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: [ diff --git a/docs/architecture.md b/docs/architecture.md index d61a65c..0e6f393 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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. @@ -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. @@ -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). diff --git a/docs/development.md b/docs/development.md index 51c5dc9..1b8dd60 100644 --- a/docs/development.md +++ b/docs/development.md @@ -42,7 +42,8 @@ styles live in `src/styles/visualizations.css`. Add primitives with `pnpm dlx shadcn@latest add `. `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 @@ -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. diff --git a/docs/migration-handoff.md b/docs/migration-handoff.md index 212135f..74a209e 100644 --- a/docs/migration-handoff.md +++ b/docs/migration-handoff.md @@ -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. @@ -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, @@ -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. @@ -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. diff --git a/docs/modernization.md b/docs/modernization.md index a24d4a0..9e61512 100644 --- a/docs/modernization.md +++ b/docs/modernization.md @@ -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, @@ -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. diff --git a/src/features/workspace/runtime/envelope-diagram.ts b/src/audio/envelope-diagram.ts similarity index 96% rename from src/features/workspace/runtime/envelope-diagram.ts rename to src/audio/envelope-diagram.ts index 92e97f7..be4acd6 100644 --- a/src/features/workspace/runtime/envelope-diagram.ts +++ b/src/audio/envelope-diagram.ts @@ -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)}%`; diff --git a/src/features/workspace/runtime/string-preview.ts b/src/audio/string-preview.ts similarity index 100% rename from src/features/workspace/runtime/string-preview.ts rename to src/audio/string-preview.ts diff --git a/src/vendor.mjs b/src/audio/timbre.mjs similarity index 100% rename from src/vendor.mjs rename to src/audio/timbre.mjs diff --git a/src/features/workspace/dialogs/algorithm-dialog.tsx b/src/components/dialogs/algorithm-dialog.tsx similarity index 90% rename from src/features/workspace/dialogs/algorithm-dialog.tsx rename to src/components/dialogs/algorithm-dialog.tsx index 084d3be..f78d439 100644 --- a/src/features/workspace/dialogs/algorithm-dialog.tsx +++ b/src/components/dialogs/algorithm-dialog.tsx @@ -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({ @@ -32,7 +32,7 @@ export function AlgorithmDialog({ useEffect(() => { let cancelled = false; let instance: ReturnType | undefined; - void import("../runtime/create-code-editor.mjs") + void import("./create-code-editor.mjs") .then(({ createCodeEditor }) => { if (cancelled) return; instance = createCodeEditor(host.current!); diff --git a/src/features/workspace/runtime/create-code-editor.mjs b/src/components/dialogs/create-code-editor.mjs similarity index 100% rename from src/features/workspace/runtime/create-code-editor.mjs rename to src/components/dialogs/create-code-editor.mjs diff --git a/src/features/workspace/dialogs/midi-dialog.tsx b/src/components/dialogs/midi-dialog.tsx similarity index 92% rename from src/features/workspace/dialogs/midi-dialog.tsx rename to src/components/dialogs/midi-dialog.tsx index f30cbb9..ec0160d 100644 --- a/src/features/workspace/dialogs/midi-dialog.tsx +++ b/src/components/dialogs/midi-dialog.tsx @@ -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, diff --git a/src/features/workspace/dialogs/workspace-dialog.tsx b/src/components/dialogs/player-dialog.tsx similarity index 97% rename from src/features/workspace/dialogs/workspace-dialog.tsx rename to src/components/dialogs/player-dialog.tsx index 69fecc9..7f30890 100644 --- a/src/features/workspace/dialogs/workspace-dialog.tsx +++ b/src/components/dialogs/player-dialog.tsx @@ -8,7 +8,7 @@ import { } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; -export function WorkspaceDialog({ +export function PlayerDialog({ id, title, children, diff --git a/src/pages/site-document.tsx b/src/components/layout/site-document.tsx similarity index 85% rename from src/pages/site-document.tsx rename to src/components/layout/site-document.tsx index 13a4054..e8c49a7 100644 --- a/src/pages/site-document.tsx +++ b/src/components/layout/site-document.tsx @@ -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 ( diff --git a/src/components/option-button.tsx b/src/components/option-button.tsx index 15f94e0..d97db85 100644 --- a/src/components/option-button.tsx +++ b/src/components/option-button.tsx @@ -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) { diff --git a/src/features/workspace/browser-workspace.tsx b/src/components/playground/browser-playground.tsx similarity index 74% rename from src/features/workspace/browser-workspace.tsx rename to src/components/playground/browser-playground.tsx index c4278d3..442d548 100644 --- a/src/features/workspace/browser-workspace.tsx +++ b/src/components/playground/browser-playground.tsx @@ -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; +type Runtime = ReturnType; -export function BrowserWorkspace() { +export function BrowserPlayground() { const [runtime, setRuntime] = useState(null); useEffect(() => { let current: Runtime | null = null; const mount = () => { - current = createWorkspace(store); + current = createPlayground(store); setRuntime(current); }; const onPageHide = (event: PageTransitionEvent) => { @@ -36,5 +36,5 @@ export function BrowserWorkspace() { current?.destroy(); }; }, []); - return runtime ? : null; + return runtime ? : null; } diff --git a/src/features/workspace/components/filtered-options.tsx b/src/components/playground/filtered-options.tsx similarity index 100% rename from src/features/workspace/components/filtered-options.tsx rename to src/components/playground/filtered-options.tsx diff --git a/src/features/workspace/components/playback-controls.tsx b/src/components/playground/playback-controls.tsx similarity index 94% rename from src/features/workspace/components/playback-controls.tsx rename to src/components/playground/playback-controls.tsx index 8a26514..1842319 100644 --- a/src/features/workspace/components/playback-controls.tsx +++ b/src/components/playground/playback-controls.tsx @@ -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); diff --git a/src/features/workspace/components/workspace-section.tsx b/src/components/playground/player-section.tsx similarity index 90% rename from src/features/workspace/components/workspace-section.tsx rename to src/components/playground/player-section.tsx index 9697754..f0ba8b2 100644 --- a/src/features/workspace/components/workspace-section.tsx +++ b/src/components/playground/player-section.tsx @@ -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, diff --git a/src/features/workspace/components/setting-range.tsx b/src/components/playground/setting-range.tsx similarity index 100% rename from src/features/workspace/components/setting-range.tsx rename to src/components/playground/setting-range.tsx diff --git a/src/features/workspace/components/settings-controls.tsx b/src/components/playground/settings-controls.tsx similarity index 96% rename from src/features/workspace/components/settings-controls.tsx rename to src/components/playground/settings-controls.tsx index 4f6ad16..88862a0 100644 --- a/src/features/workspace/components/settings-controls.tsx +++ b/src/components/playground/settings-controls.tsx @@ -4,11 +4,11 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { useState } from "react"; import { useAtomValue, useSetAtom } from "jotai"; -import { settingsAtom, updateSettingAtom, defaults } from "../../../state/settings.ts"; -import { scales } from "../../../midi/scales.ts"; -import { instruments } from "../../../midi/instruments.ts"; +import { settingsAtom, updateSettingAtom, defaults } from "../../state/settings.ts"; +import { scales } from "../../midi/scales.ts"; +import { instruments } from "../../midi/instruments.ts"; import { WaveformControls } from "./waveform-controls.tsx"; -import type { Props } from "../runtime/workspace-types.ts"; +import type { Props } from "../../controllers/playground-types.ts"; const scaleOptions = Object.entries(scales) .sort( ([, a], [, b]) => diff --git a/src/features/workspace/components/sort-sidebar.tsx b/src/components/playground/sort-sidebar.tsx similarity index 98% rename from src/features/workspace/components/sort-sidebar.tsx rename to src/components/playground/sort-sidebar.tsx index 76f08bd..e03ad5d 100644 --- a/src/features/workspace/components/sort-sidebar.tsx +++ b/src/components/playground/sort-sidebar.tsx @@ -8,7 +8,7 @@ import { OptionButton } from "@/components/option-button"; import { ControlIcon } from "@/components/control-icon"; import { Info, CirclePlus, ChartNoAxesColumnIncreasing, List } from "lucide-react"; import { Counters } from "./playback-controls"; -import type { Props } from "../runtime/workspace-types"; +import type { Props } from "../../controllers/playground-types"; export function SortSidebar({ runtime, onDialog: setModal, diff --git a/src/features/workspace/components/workspace.tsx b/src/components/playground/sorting-playground.tsx similarity index 93% rename from src/features/workspace/components/workspace.tsx rename to src/components/playground/sorting-playground.tsx index 978da58..b36bebc 100644 --- a/src/features/workspace/components/workspace.tsx +++ b/src/components/playground/sorting-playground.tsx @@ -1,4 +1,4 @@ -import { WorkspaceSection } from "./workspace-section"; +import { PlayerSection } from "./player-section"; import { SortSidebar } from "./sort-sidebar"; import { Button } from "@/components/ui/button"; import { useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react"; @@ -6,9 +6,9 @@ import { Settings } from "./settings-controls.tsx"; import { Transport, Scrubber } from "./playback-controls.tsx"; import { AlgorithmDialog } from "../dialogs/algorithm-dialog.tsx"; import { MidiDialog } from "../dialogs/midi-dialog.tsx"; -import type { Props, PlayerId } from "../runtime/workspace-types.ts"; +import type { Props, PlayerId } from "../../controllers/playground-types.ts"; type Modal = "sort" | "add-algorithm" | "midi-export" | null; -export function Workspace({ runtime }: Props) { +export function SortingPlayground({ runtime }: Props) { const error = useSyncExternalStore(runtime.subscribe, () => runtime.getSnapshot().error); const suspended = useSyncExternalStore(runtime.subscribe, () => runtime.getSnapshot().suspended); const base = useRef(null); @@ -33,7 +33,7 @@ export function Workspace({ runtime }: Props) { const close = () => setModal(null); return ( <> - - - + - + {!suspended && modal === "sort" && ( )} diff --git a/src/features/workspace/components/waveform-controls.tsx b/src/components/playground/waveform-controls.tsx similarity index 93% rename from src/features/workspace/components/waveform-controls.tsx rename to src/components/playground/waveform-controls.tsx index 4410f74..bd8becf 100644 --- a/src/features/workspace/components/waveform-controls.tsx +++ b/src/components/playground/waveform-controls.tsx @@ -4,10 +4,10 @@ import { useLayoutEffect, useRef } from "react"; import { Field } from "@base-ui/react/field"; import { useAtomValue, useSetAtom } from "jotai"; import type { createStore } from "jotai/vanilla"; -import { settingsAtom, updateSettingAtom } from "../../../state/settings.ts"; -import { envelopeAtom, updateEnvelopeAtom, type EnvelopeKey } from "../../../state/envelope.ts"; -import { waveformDefaults, type WaveformId } from "../../../state/waveforms.ts"; -import { drawEnvelopeDiagram, formatEnvelopeValue } from "../runtime/envelope-diagram.ts"; +import { settingsAtom, updateSettingAtom } from "../../state/settings.ts"; +import { envelopeAtom, updateEnvelopeAtom, type EnvelopeKey } from "../../state/envelope.ts"; +import { waveformDefaults, type WaveformId } from "../../state/waveforms.ts"; +import { drawEnvelopeDiagram, formatEnvelopeValue } from "../../audio/envelope-diagram.ts"; type Store = ReturnType; const controls: ReadonlyArray<{ diff --git a/src/components/text-link.tsx b/src/components/text-link.tsx index 7236a63..2fa7c20 100644 --- a/src/components/text-link.tsx +++ b/src/components/text-link.tsx @@ -1,6 +1,6 @@ import { createLink } from "@tanstack/react-router"; import type { ComponentProps } from "react"; -import { cn } from "@/lib/utils"; +import { cn } from "@/utilities/cn"; export function TextLink({ className, ...props }: ComponentProps<"a">) { return ( diff --git a/src/features/workspace/runtime/connect-audio-settings.ts b/src/controllers/connect-audio-settings.ts similarity index 92% rename from src/features/workspace/runtime/connect-audio-settings.ts rename to src/controllers/connect-audio-settings.ts index a826e64..c5a4a32 100644 --- a/src/features/workspace/runtime/connect-audio-settings.ts +++ b/src/controllers/connect-audio-settings.ts @@ -1,6 +1,6 @@ import type { createStore } from "jotai/vanilla"; -import { settingsAtom, type Settings } from "../../../state/settings.ts"; -import { selectedWaveformAtom, type Waveform } from "../../../state/waveforms.ts"; +import { settingsAtom, type Settings } from "../state/settings.ts"; +import { selectedWaveformAtom, type Waveform } from "../state/waveforms.ts"; type Effects = { render: (settings: Settings, waveform: Waveform) => void; diff --git a/src/features/workspace/runtime/connect-playback-settings.ts b/src/controllers/connect-playback-settings.ts similarity index 88% rename from src/features/workspace/runtime/connect-playback-settings.ts rename to src/controllers/connect-playback-settings.ts index 6a00663..05a6c03 100644 --- a/src/features/workspace/runtime/connect-playback-settings.ts +++ b/src/controllers/connect-playback-settings.ts @@ -1,7 +1,7 @@ import type { createStore } from "jotai/vanilla"; -import { settingsAtom } from "../../../state/settings.ts"; -import { waveformDefaults } from "../../../state/waveforms.ts"; -import { playbackPreferencesAtom } from "../../../state/playback-preferences.ts"; +import { settingsAtom } from "../state/settings.ts"; +import { waveformDefaults } from "../state/waveforms.ts"; +import { playbackPreferencesAtom } from "../state/playback-preferences.ts"; type Store = ReturnType; type Preferences = ReturnType; diff --git a/src/features/workspace/runtime/connect-sort-settings.ts b/src/controllers/connect-sort-settings.ts similarity index 92% rename from src/features/workspace/runtime/connect-sort-settings.ts rename to src/controllers/connect-sort-settings.ts index ddadb08..f382227 100644 --- a/src/features/workspace/runtime/connect-sort-settings.ts +++ b/src/controllers/connect-sort-settings.ts @@ -1,6 +1,6 @@ import type { createStore } from "jotai/vanilla"; -import { settingsAtom } from "../../../state/settings.ts"; -import { algorithmCatalogAtom } from "../../../state/algorithm-overrides.ts"; +import { settingsAtom } from "../state/settings.ts"; +import { algorithmCatalogAtom } from "../state/algorithm-overrides.ts"; type Catalog = ReturnType; type Effects = { diff --git a/src/features/workspace/runtime/create-helpers.mjs b/src/controllers/create-helpers.mjs similarity index 91% rename from src/features/workspace/runtime/create-helpers.mjs rename to src/controllers/create-helpers.mjs index 3d113fa..9fc8693 100644 --- a/src/features/workspace/runtime/create-helpers.mjs +++ b/src/controllers/create-helpers.mjs @@ -1,4 +1,4 @@ -import { scales } from "../../../midi/scales.ts"; +import { scales } from "../midi/scales.ts"; export function createHelpers(settings, dependencies = { scales }) { return { diff --git a/src/features/workspace/runtime/create-workspace-player.mjs b/src/controllers/create-player.mjs similarity index 89% rename from src/features/workspace/runtime/create-workspace-player.mjs rename to src/controllers/create-player.mjs index 913b69f..86ca5e9 100644 --- a/src/features/workspace/runtime/create-workspace-player.mjs +++ b/src/controllers/create-player.mjs @@ -1,13 +1,13 @@ -import { createTimbreAudio } from "../../../audio/create-timbre-audio.mjs"; -import { createTransport } from "../../../audio/create-transport.ts"; -import { timbre } from "../../../vendor.mjs"; +import { createTimbreAudio } from "../audio/create-timbre-audio.mjs"; +import { createTransport } from "../audio/create-transport.ts"; +import { timbre } from "../audio/timbre.mjs"; import { select } from "d3-selection"; -import { visualizations } from "../../../visualizations/visualization-registry.mjs"; -import { createMidiBytes } from "../../../midi/create-midi-bytes.mjs"; -import { drawStringPreview } from "./string-preview.ts"; +import { visualizations } from "../visualizations/visualization-registry.mjs"; +import { createMidiBytes } from "../midi/create-midi-bytes.mjs"; +import { drawStringPreview } from "../audio/string-preview.ts"; // Owns only the D3 SVG contents and audio resources. React owns all controls. -export function createWorkspacePlayer({ +export function createPlayer({ svg, settings, getMidiNumber, diff --git a/src/features/workspace/runtime/create-workspace.mjs b/src/controllers/create-playground.mjs similarity index 89% rename from src/features/workspace/runtime/create-workspace.mjs rename to src/controllers/create-playground.mjs index c770a0c..efec220 100644 --- a/src/features/workspace/runtime/create-workspace.mjs +++ b/src/controllers/create-playground.mjs @@ -2,19 +2,19 @@ import { createStore } from "jotai/vanilla"; import { min, max } from "d3-array"; import { scaleLinear } from "d3-scale"; import { saveAs } from "file-saver"; -import { timbre } from "../../../vendor.mjs"; -import { createTimbreSoundfont } from "../../../audio/create-timbre-soundfont.mjs"; +import { timbre } from "../audio/timbre.mjs"; +import { createTimbreSoundfont } from "../audio/create-timbre-soundfont.mjs"; import { createHelpers } from "./create-helpers.mjs"; -import { createWorkspacePlayer } from "./create-workspace-player.mjs"; +import { createPlayer } from "./create-player.mjs"; import { connectAudioSettings } from "./connect-audio-settings.ts"; import { connectPlaybackSettings } from "./connect-playback-settings.ts"; import { connectSortSettings } from "./connect-sort-settings.ts"; -import { generators } from "../../../generators/generator-registry.ts"; -import { defaults, settingsAtom, updateSettingAtom } from "../../../state/settings.ts"; -import { selectedWaveformAtom } from "../../../state/waveforms.ts"; -import { algorithmCatalogAtom } from "../../../state/algorithm-overrides.ts"; -import { playbackPreferencesAtom, toggleLoopAtom } from "../../../state/playback-preferences.ts"; -import { createSortRequest, runSortRequest } from "../../../sorting/sort-requests.ts"; +import { generators } from "../generators/generator-registry.ts"; +import { defaults, settingsAtom, updateSettingAtom } from "../state/settings.ts"; +import { selectedWaveformAtom } from "../state/waveforms.ts"; +import { algorithmCatalogAtom } from "../state/algorithm-overrides.ts"; +import { playbackPreferencesAtom, toggleLoopAtom } from "../state/playback-preferences.ts"; +import { createSortRequest, runSortRequest } from "../sorting/sort-requests.ts"; const emptyPlayer = { position: 0, @@ -26,7 +26,7 @@ const emptyPlayer = { playing: false, }; -export function createWorkspace(store = createStore()) { +export function createPlayground(store = createStore()) { let snapshot = { base: emptyPlayer, sort: emptyPlayer, error: "", suspended: false }; const listeners = new Set(); const publish = (update) => { @@ -129,7 +129,7 @@ export function createWorkspace(store = createStore()) { } return; } - worker = new Worker(new URL("../../../worker.mjs", import.meta.url), { type: "module" }); + worker = new Worker(new URL("../sorting/worker.mjs", import.meta.url), { type: "module" }); const key = workerKey; worker.addEventListener("message", (event) => accept(event.data)); worker.addEventListener("error", (event) => accept({ key, error: event.message })); @@ -205,13 +205,13 @@ export function createWorkspace(store = createStore()) { }, getSnapshot: () => snapshot, mount(elements) { - if (destroyed) throw new Error("Cannot mount a destroyed workspace"); + if (destroyed) throw new Error("Cannot mount a destroyed playground"); canvas = elements.canvas; soundfont = createTimbreSoundfont(timbre); players = {}; try { for (const id of ["base", "sort"]) { - players[id] = createWorkspacePlayer({ + players[id] = createPlayer({ svg: elements[id], settings, getMidiNumber: helper.getMidiNumber, diff --git a/src/controllers/playground-types.ts b/src/controllers/playground-types.ts new file mode 100644 index 0000000..526a14c --- /dev/null +++ b/src/controllers/playground-types.ts @@ -0,0 +1,3 @@ +import type { createPlayground } from "./create-playground.mjs"; +export type PlayerId = "base" | "sort"; +export type Props = { runtime: ReturnType }; diff --git a/src/features/workspace/runtime/workspace-types.ts b/src/features/workspace/runtime/workspace-types.ts deleted file mode 100644 index 07af59d..0000000 --- a/src/features/workspace/runtime/workspace-types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type { createWorkspace } from "./create-workspace.mjs"; -export type PlayerId = "base" | "sort"; -export type Props = { runtime: ReturnType }; diff --git a/src/pages/about.tsx b/src/pages/about.tsx deleted file mode 100644 index 15ada1d..0000000 --- a/src/pages/about.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { buttonVariants } from "@/components/ui/button"; -import { TextLink } from "@/components/text-link"; -import { Link } from "@tanstack/react-router"; - -export function About() { - return ( -
-
-

Audio Sort

-

This app was created as a way to "hear" what sorting algorithms sound like.

-

- To accomplish this, it uses a long list of{" "} - libraries - , and some of the more recent browser features like{" "} - - Web Workers - - . -

-

- I plan on adding a few more features, and getting a longer list of default sorting - algorithms to choose from. You can currently tweak the existing algorithms, (or add your - own) to hear what small changes sound like. -

-

- If you have feature requests (or find a bug), please{" "} - - log an issue at Github - - . You can also leave{" "} - - comments on the Project Page - -

-

- - Algorithm API - -     - - Start Sorting - -

-
-
- ); -} diff --git a/src/pages/api.tsx b/src/pages/api.tsx deleted file mode 100644 index 2df44e5..0000000 --- a/src/pages/api.tsx +++ /dev/null @@ -1,185 +0,0 @@ -import { AppLink as Link } from "@/components/text-link"; - -export function Api() { - return ( -
-

Algorithm API

- -

- You can add/edit your own javascript algorithm by clicking the "Add Algorithm" button at the - bottom-left side of the main page. -

- -

- When writing your algorithm, you have access to the AS (Audio Sort) global - object. The methods on that object are described in the table below. The AS{" "} - object gives you access to the current array, allows you to set visualization markers, and - lets you "play" certain array elements. -

- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
MethodDescriptionMarker ColorMarker Level
AS.length()Gets the length of the array.N/AN/A
AS.size()Gets the length of the array.N/AN/A
AS.get(index) - Gets the sortItem object with the given array index. This sortItem can be passed to - other AS method calls. - N/AN/A
AS.play(item1, ..., itemN) - Will play the given items. Items can be indexes or array items returned via AS.get() - calls. - N/AN/A
AS.mark(item1, ..., itemN) - Will mark the given items. Items can be indexes or array items returned via AS.get() - calls. - -
 
-
1
AS.lt(itemOne, itemTwo) - Returns true if itemOne is less than itemTwo. Item can be an array index, or an - array item (returned via an AS.get() call). - -
 
-
2
AS.lte(itemOne, itemTwo) - Returns true if itemOne is less than or equal to itemTwo. Item can be an array - index, or an array item (returned via an AS.get() call). - -
 
-
2
AS.gt(itemOne, itemTwo) - Returns true if itemOne is greater than itemTwo. Item can be an array index, or an - array item (returned via an AS.get() call). - -
 
-
2
AS.gte(itemOne, itemTwo) - Returns true if itemOne is greater than or equal to itemTwo. Item can be an array - index, or an array item (returned via an AS.get() call). - -
 
-
2
AS.eq(itemOne, itemTwo) - Returns true if itemOne is equal to itemTwo. Item can be an array index, or an array - item (returned via an AS.get() call). - -
 
-
2
AS.neq(itemOne, itemTwo) - Returns true if itemOne is not equal to itemTwo. Item can be an array index, or an - array item (returned via an AS.get() call). - -
 
-
2
AS.swap(itemOne, itemTwo)Will swap the positions of the two items. -
 
-
3
N/A - Items that were swapped in the last iteration, will show up below the "swapped" - items. They appear as green circles, while items that are getting ready to be - swapped show up as yellow circles. - -
 
-
4
AS.highlight(item1, ..., itemN) - Will highlight the given items. Highlighted items stay highlighted until the next - AS.highlight() call, or until AS.clearHighlight() is called. - -
 
-
5
AS.clearHighlight()Clears the currently highlighted items.N/AN/A
-
-
- ); -} diff --git a/src/pages/home.tsx b/src/pages/home.tsx deleted file mode 100644 index 22cb839..0000000 --- a/src/pages/home.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { useEffect, useState } from "react"; -import type { ComponentType } from "react"; - -export function Home() { - const [Workspace, setWorkspace] = useState(null); - const [failed, setFailed] = useState(false); - useEffect(() => { - let cancelled = false; - // Audio/editor code is browser-only; never evaluate it during prerendering. - void import("../features/workspace/browser-workspace.tsx") - .then(({ BrowserWorkspace }) => { - if (!cancelled) setWorkspace(() => BrowserWorkspace); - }) - .catch(() => { - if (!cancelled) setFailed(true); - }); - return () => { - cancelled = true; - }; - }, []); - - return ( - <> -
{Workspace && }
- {failed && ( -

- Unable to load the workspace. Please reload the page to try again. -

- )} - - - ); -} diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index b8233d0..0a36680 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -1,4 +1,4 @@ import { createRootRoute } from "@tanstack/react-router"; -import { SiteDocument } from "../pages/site-document"; +import { SiteDocument } from "../components/layout/site-document"; export const Route = createRootRoute({ shellComponent: SiteDocument }); diff --git a/src/routes/about.tsx b/src/routes/about.tsx index e389876..2b42383 100644 --- a/src/routes/about.tsx +++ b/src/routes/about.tsx @@ -1,4 +1,56 @@ import { createFileRoute } from "@tanstack/react-router"; -import { About } from "../pages/about"; +import { buttonVariants } from "@/components/ui/button"; +import { TextLink } from "@/components/text-link"; +import { Link } from "@tanstack/react-router"; + +function About() { + return ( +
+
+

Audio Sort

+

This app was created as a way to "hear" what sorting algorithms sound like.

+

+ To accomplish this, it uses a long list of{" "} + libraries + , and some of the more recent browser features like{" "} + + Web Workers + + . +

+

+ I plan on adding a few more features, and getting a longer list of default sorting + algorithms to choose from. You can currently tweak the existing algorithms, (or add your + own) to hear what small changes sound like. +

+

+ If you have feature requests (or find a bug), please{" "} + + log an issue at Github + + . You can also leave{" "} + + comments on the Project Page + +

+

+ + Algorithm API + +     + + Start Sorting + +

+
+
+ ); +} export const Route = createFileRoute("/about")({ component: About }); diff --git a/src/routes/api.tsx b/src/routes/api.tsx index ccaa2c3..7fd037a 100644 --- a/src/routes/api.tsx +++ b/src/routes/api.tsx @@ -1,4 +1,188 @@ import { createFileRoute } from "@tanstack/react-router"; -import { Api } from "../pages/api"; +import { AppLink as Link } from "@/components/text-link"; + +function Api() { + return ( +
+

Algorithm API

+ +

+ You can add/edit your own javascript algorithm by clicking the "Add Algorithm" button at the + bottom-left side of the main page. +

+ +

+ When writing your algorithm, you have access to the AS (Audio Sort) global + object. The methods on that object are described in the table below. The AS{" "} + object gives you access to the current array, allows you to set visualization markers, and + lets you "play" certain array elements. +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodDescriptionMarker ColorMarker Level
AS.length()Gets the length of the array.N/AN/A
AS.size()Gets the length of the array.N/AN/A
AS.get(index) + Gets the sortItem object with the given array index. This sortItem can be passed to + other AS method calls. + N/AN/A
AS.play(item1, ..., itemN) + Will play the given items. Items can be indexes or array items returned via AS.get() + calls. + N/AN/A
AS.mark(item1, ..., itemN) + Will mark the given items. Items can be indexes or array items returned via AS.get() + calls. + +
 
+
1
AS.lt(itemOne, itemTwo) + Returns true if itemOne is less than itemTwo. Item can be an array index, or an + array item (returned via an AS.get() call). + +
 
+
2
AS.lte(itemOne, itemTwo) + Returns true if itemOne is less than or equal to itemTwo. Item can be an array + index, or an array item (returned via an AS.get() call). + +
 
+
2
AS.gt(itemOne, itemTwo) + Returns true if itemOne is greater than itemTwo. Item can be an array index, or an + array item (returned via an AS.get() call). + +
 
+
2
AS.gte(itemOne, itemTwo) + Returns true if itemOne is greater than or equal to itemTwo. Item can be an array + index, or an array item (returned via an AS.get() call). + +
 
+
2
AS.eq(itemOne, itemTwo) + Returns true if itemOne is equal to itemTwo. Item can be an array index, or an array + item (returned via an AS.get() call). + +
 
+
2
AS.neq(itemOne, itemTwo) + Returns true if itemOne is not equal to itemTwo. Item can be an array index, or an + array item (returned via an AS.get() call). + +
 
+
2
AS.swap(itemOne, itemTwo)Will swap the positions of the two items. +
 
+
3
N/A + Items that were swapped in the last iteration, will show up below the "swapped" + items. They appear as green circles, while items that are getting ready to be + swapped show up as yellow circles. + +
 
+
4
AS.highlight(item1, ..., itemN) + Will highlight the given items. Highlighted items stay highlighted until the next + AS.highlight() call, or until AS.clearHighlight() is called. + +
 
+
5
AS.clearHighlight()Clears the currently highlighted items.N/AN/A
+
+
+ ); +} export const Route = createFileRoute("/api")({ component: Api }); diff --git a/src/routes/index.tsx b/src/routes/index.tsx index f35eb35..c64885b 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,4 +1,40 @@ import { createFileRoute } from "@tanstack/react-router"; -import { Home } from "../pages/home"; +import { useEffect, useState } from "react"; +import type { ComponentType } from "react"; + +function Home() { + const [Playground, setPlayground] = useState(null); + const [failed, setFailed] = useState(false); + useEffect(() => { + let cancelled = false; + // Audio/editor code is browser-only; never evaluate it during prerendering. + void import("../components/playground/browser-playground.tsx") + .then(({ BrowserPlayground }) => { + if (!cancelled) setPlayground(() => BrowserPlayground); + }) + .catch(() => { + if (!cancelled) setFailed(true); + }); + return () => { + cancelled = true; + }; + }, []); + + return ( + <> +
{Playground && }
+ {failed && ( +

+ Unable to load the playground. Please reload the page to try again. +

+ )} + + + ); +} export const Route = createFileRoute("/")({ component: Home }); diff --git a/src/worker.mjs b/src/sorting/worker.mjs similarity index 59% rename from src/worker.mjs rename to src/sorting/worker.mjs index e06733c..d70a88c 100644 --- a/src/worker.mjs +++ b/src/sorting/worker.mjs @@ -1,4 +1,4 @@ -import { handleSortRequest } from "./sorting/sort-requests.ts"; +import { handleSortRequest } from "./sort-requests.ts"; globalThis.onmessage = ({ data }) => { globalThis.postMessage(handleSortRequest(data)); diff --git a/src/lib/utils.ts b/src/utilities/cn.ts similarity index 100% rename from src/lib/utils.ts rename to src/utilities/cn.ts diff --git a/test/audio-settings-connection.test.mjs b/test/audio-settings-connection.test.mjs index 53a9db0..be45ce6 100644 --- a/test/audio-settings-connection.test.mjs +++ b/test/audio-settings-connection.test.mjs @@ -1,6 +1,6 @@ import { expect, test, vi } from "vitest"; import { createStore } from "jotai/vanilla"; -import { connectAudioSettings } from "../src/features/workspace/runtime/connect-audio-settings.ts"; +import { connectAudioSettings } from "../src/controllers/connect-audio-settings.ts"; import { updateSettingAtom } from "../src/state/settings.ts"; import { updateEnvelopeAtom } from "../src/state/envelope.ts"; diff --git a/test/browser/site.spec.mjs b/test/browser/site.spec.mjs index fa1ebac..3ce62f8 100644 --- a/test/browser/site.spec.mjs +++ b/test/browser/site.spec.mjs @@ -317,7 +317,7 @@ test("built UI loads and algorithm IDs execute in the bundled worker", async ({ await page.goto("./"); const sortWorker = await workerReady; await expect(page.locator("#wrapper > #header")).toHaveCount(1); - await expect(page.locator("#workspace > #base-section")).toHaveCount(1); + await expect(page.locator("#playground > #base-section")).toHaveCount(1); await expect(page.locator("body > #footer")).toHaveCount(1); const workerURL = sortWorker.url(); expect(await sortWorker.evaluate(() => Object.hasOwn(globalThis, "AS"))).toBe(false); @@ -722,7 +722,7 @@ test("teardown clears owned resources and repeated remounts do not duplicate UI await page.evaluate(() => globalThis.sortWorkers.every((worker) => worker.wasTerminated)), ).toBe(true); expect(await page.evaluate(() => typeof globalThis.jQuery)).toBe("undefined"); - await expect(page.locator("#workspace > *")).toHaveCount(0); + await expect(page.locator("#playground > *")).toHaveCount(0); await page.evaluate(() => globalThis.dispatchEvent(new globalThis.PageTransitionEvent("pageshow", { persisted: true })), ); @@ -993,7 +993,7 @@ test("the page scrolls only when its content exceeds the viewport", async ({ pag } }); -test("charts size fluidly with two workspace layouts", async ({ page }) => { +test("charts size fluidly with two playground layouts", async ({ page }) => { await page.goto("./"); for (const [width, height, chartHeight] of [ [1440, 900, "216px"], @@ -1109,7 +1109,7 @@ test("clean routes hydrate, navigate, and reload without a server", async ({ page.on("console", (message) => { if (message.type() === "error") errors.push(message.text()); }); - for (const path of ["", "?view=sort#workspace"]) { + for (const path of ["", "?view=sort#playground"]) { const url = new URL(path, baseURL).href; expect((await page.goto(url)).status()).toBe(200); await expect(page.locator("#base-svg rect")).toHaveCount(12); @@ -1124,7 +1124,7 @@ test("clean routes hydrate, navigate, and reload without a server", async ({ await expect(page).toHaveURL(new URL("about", baseURL).href); await expect(page.locator("#about")).toBeVisible(); await page.waitForLoadState("networkidle"); - expect(await page.locator("#workspace").count()).toBe(0); + expect(await page.locator("#playground").count()).toBe(0); expect(await page.evaluate(() => globalThis.navigationWitness)).toBe(true); expect( await page.evaluate(() => globalThis.sortWorkers.every((worker) => worker.wasTerminated)), @@ -1163,11 +1163,11 @@ test("prerendered pages and navigation remain readable without JavaScript", asyn } }); -test("a failed workspace download shows a recoverable error", async ({ page }) => { - await page.route(/\/assets\/browser-workspace-[^/]+\.js$/, (route) => route.abort()); +test("a failed playground download shows a recoverable error", async ({ page }) => { + await page.route(/\/assets\/browser-playground-[^/]+\.js$/, (route) => route.abort()); await page.goto("./"); - await expect(page.getByRole("alert")).toContainText("Unable to load the workspace"); - await page.unroute(/\/assets\/browser-workspace-[^/]+\.js$/); + await expect(page.getByRole("alert")).toContainText("Unable to load the playground"); + await page.unroute(/\/assets\/browser-playground-[^/]+\.js$/); await page.reload(); await expect(page.locator("#base-svg rect")).toHaveCount(12); await expect(page.getByRole("alert")).toHaveCount(0); diff --git a/test/playback-settings-connection.test.mjs b/test/playback-settings-connection.test.mjs index 5e3c00c..5e3d262 100644 --- a/test/playback-settings-connection.test.mjs +++ b/test/playback-settings-connection.test.mjs @@ -1,6 +1,6 @@ import { expect, test, vi } from "vitest"; import { createStore } from "jotai/vanilla"; -import { connectPlaybackSettings } from "../src/features/workspace/runtime/connect-playback-settings.ts"; +import { connectPlaybackSettings } from "../src/controllers/connect-playback-settings.ts"; import { updateSettingAtom } from "../src/state/settings.ts"; import { toggleAutoPlayAtom, toggleLoopAtom } from "../src/state/playback-preferences.ts"; diff --git a/test/scales.test.mjs b/test/scales.test.mjs index de5da23..9def83d 100644 --- a/test/scales.test.mjs +++ b/test/scales.test.mjs @@ -1,7 +1,7 @@ import { createHash } from "node:crypto"; import { expect, test } from "vitest"; import { scales } from "../src/midi/scales.ts"; -import { createHelpers } from "../src/features/workspace/runtime/create-helpers.mjs"; +import { createHelpers } from "../src/controllers/create-helpers.mjs"; test("preserves all scale data extracted from the bundled subcollider 0.1.0", () => { // Baseline computed from ScaleInfo.names()/at() in the original bundle. diff --git a/test/settings.test.mjs b/test/settings.test.mjs index d1cbdc6..bffd0c7 100644 --- a/test/settings.test.mjs +++ b/test/settings.test.mjs @@ -1,7 +1,7 @@ import { expect, test, vi } from "vitest"; import { createStore } from "jotai/vanilla"; import { defaults, settingsAtom, updateSettingAtom } from "../src/state/settings.ts"; -import { createWorkspace } from "../src/features/workspace/runtime/create-workspace.mjs"; +import { createPlayground } from "../src/controllers/create-playground.mjs"; test("settings retain all nine existing defaults", () => { const store = createStore(); @@ -41,8 +41,8 @@ test("stores are isolated, snapshots immutable, and updates notify only on chang test("runtime audio settings getters read the current store, including falsy values and fallbacks", () => { const store = createStore(); - const controller = createWorkspace(store).settings; - const independent = createWorkspace().settings; + const controller = createPlayground(store).settings; + const independent = createPlayground().settings; for (const [key, value] of Object.entries({ volume: 0, tempo: 120, diff --git a/test/sort-settings-connection.test.mjs b/test/sort-settings-connection.test.mjs index 0350b9c..a5ae55f 100644 --- a/test/sort-settings-connection.test.mjs +++ b/test/sort-settings-connection.test.mjs @@ -1,6 +1,6 @@ import { expect, test, vi } from "vitest"; import { createStore } from "jotai/vanilla"; -import { connectSortSettings } from "../src/features/workspace/runtime/connect-sort-settings.ts"; +import { connectSortSettings } from "../src/controllers/connect-sort-settings.ts"; import { updateSettingAtom } from "../src/state/settings.ts"; import { addAlgorithmAtom, editAlgorithmAtom } from "../src/state/algorithm-overrides.ts"; diff --git a/test/string-preview.test.mjs b/test/string-preview.test.mjs index c25e961..635f56e 100644 --- a/test/string-preview.test.mjs +++ b/test/string-preview.test.mjs @@ -1,5 +1,5 @@ import { expect, test } from "vitest"; -import { getStringPreviewSamples } from "../src/features/workspace/runtime/string-preview.ts"; +import { getStringPreviewSamples } from "../src/audio/string-preview.ts"; test("string illustration is deterministic, bounded, oscillating, and decaying", () => { const samples = getStringPreviewSamples(); diff --git a/test/ui.test.mjs b/test/ui.test.mjs index c16b069..b2095bd 100644 --- a/test/ui.test.mjs +++ b/test/ui.test.mjs @@ -1,17 +1,17 @@ import { expect, test } from "vitest"; -import { createWorkspace } from "../src/features/workspace/runtime/create-workspace.mjs"; -import { createHelpers } from "../src/features/workspace/runtime/create-helpers.mjs"; -import { createWorkspacePlayer } from "../src/features/workspace/runtime/create-workspace-player.mjs"; +import { createPlayground } from "../src/controllers/create-playground.mjs"; +import { createHelpers } from "../src/controllers/create-helpers.mjs"; +import { createPlayer } from "../src/controllers/create-player.mjs"; import { visualizations } from "../src/visualizations/visualization-registry.mjs"; test("UI modules import without DOM initialization or first-party globals", () => { for (const name of ["A", "visualization"]) expect(Object.hasOwn(globalThis, name)).toBe(false); - const first = createWorkspace(); - const second = createWorkspace(); + const first = createPlayground(); + const second = createPlayground(); expect(first).not.toBe(second); expect(typeof first.mount).toBe("function"); expect(first.settings.getSelected("unknown", "fallback")).toBe("fallback"); - expect(typeof createWorkspacePlayer).toBe("function"); + expect(typeof createPlayer).toBe("function"); expect(Object.keys(visualizations).sort()).toEqual(["bar", "flat"]); for (const name of ["A", "visualization"]) expect(Object.hasOwn(globalThis, name)).toBe(false); }); @@ -33,10 +33,10 @@ test("MIDI helper reads current settings from its injected controller", () => { expect(helpers.getMidiNumber(4)).toBe(72); }); -test("destroying an unmounted workspace is safe and prevents reuse", () => { - const controller = createWorkspace(); +test("destroying an unmounted playground is safe and prevents reuse", () => { + const controller = createPlayground(); controller.destroy(); controller.destroy(); controller.resume(); - expect(() => controller.mount({})).toThrow("destroyed workspace"); + expect(() => controller.mount({})).toThrow("destroyed playground"); }); diff --git a/test/waveforms.test.mjs b/test/waveforms.test.mjs index 872b284..27146e5 100644 --- a/test/waveforms.test.mjs +++ b/test/waveforms.test.mjs @@ -3,11 +3,8 @@ import { createStore } from "jotai/vanilla"; import { waveformDefaults, selectedWaveformAtom } from "../src/state/waveforms.ts"; import { envelopeDefaults, envelopeAtom, updateEnvelopeAtom } from "../src/state/envelope.ts"; import { updateSettingAtom } from "../src/state/settings.ts"; -import { createWorkspace } from "../src/features/workspace/runtime/create-workspace.mjs"; -import { - getEnvelopePoints, - formatEnvelopeValue, -} from "../src/features/workspace/runtime/envelope-diagram.ts"; +import { createPlayground } from "../src/controllers/create-playground.mjs"; +import { getEnvelopePoints, formatEnvelopeValue } from "../src/audio/envelope-diagram.ts"; test("all eight generators use the shared defaults, including string", () => { const store = createStore(); @@ -69,7 +66,7 @@ test.each([ test("controller getters combine the current generator with the shared envelope", () => { const store = createStore(); - const controller = createWorkspace(store).settings; + const controller = createPlayground(store).settings; store.set(updateEnvelopeAtom, { key: "a", value: 100 }); store.set(updateSettingAtom, { key: "waveform", value: "sin" }); expect(controller.getSelectedWaveformInfo()).toMatchObject({ gen: "OscGen", a: 100 });