From a2f7e349a0ebf9d14b0de694ffcecc1bcc258603 Mon Sep 17 00:00:00 2001
From: Adam <7889445+DMNerd@users.noreply.github.com>
Date: Sun, 17 May 2026 16:55:34 +0200
Subject: [PATCH 1/4] Add scoped source import aliases
---
docs/project-structure.md | 32 +++
knip.json | 2 +-
src/app/App.jsx | 243 +++++-------------
src/app/hooks/useAppOrchestration.js | 14 +-
src/app/hooks/useAppPanelModels.js | 6 +-
src/app/main.jsx | 4 +-
src/app/providers/ToastProvider.jsx | 40 +++
.../Layout => app/shell}/AppLayout.jsx | 0
src/app/shell/AppPanels.jsx | 52 ++++
src/{components/UI => app/shell}/StageHud.jsx | 2 +-
.../StageHudContainer.jsx | 10 +-
src/app/shell/StageShell.jsx | 95 +++++++
src/{lib => domain}/meta/meta.ts | 4 +-
.../presets/neckFilterModes.ts | 2 +-
src/{lib => domain}/presets/presetState.ts | 4 +-
src/{lib => domain}/presets/presets.ts | 2 +-
src/{lib => domain}/theory/capoChords.ts | 0
src/{lib => domain}/theory/chords.ts | 0
src/{lib => domain}/theory/fretboardShapes.ts | 0
src/{lib => domain}/theory/notation.ts | 0
src/{lib => domain}/theory/scales.ts | 0
src/{lib => domain}/theory/shapeSystems.ts | 2 +-
src/{lib => domain}/theory/tuning.ts | 0
.../display/components}/DisplayControls.jsx | 18 +-
.../display}/hooks/useDisplayState.js | 4 +-
src/{ => features/display}/hooks/useTheme.js | 2 +-
src/features/display/index.js | 2 +
.../display/store}/useDisplayPrefsStore.js | 10 +-
.../display/store}/useThemeStore.js | 4 +-
.../export/components}/ExportControls.jsx | 12 +-
.../components}/TuningPackEditorModal.jsx | 14 +-
.../components}/TuningPackManagerModal.jsx | 8 +-
.../CustomTuningModalsContainer.jsx | 6 +-
.../containers/ExportPanelContainer.jsx | 4 +-
.../hooks/useExportCustomTuningDomain.js | 6 +-
src/features/export/index.js | 23 ++
.../export/model}/importPipeline.ts | 0
.../export/model}/scales.ts | 0
.../export/model}/schema.ts | 4 +-
.../export/model}/tuningIO.ts | 2 +-
.../export/model}/tuningPackNormalization.js | 8 +-
.../export/model}/tuningPackSearch.js | 0
.../fretboard/components}/Fretboard.jsx | 28 +-
.../fretboard}/hooks/useFretboardLayout.js | 2 +-
.../fretboard}/hooks/useInlays.js | 0
.../fretboard}/hooks/useLabels.js | 0
.../fretboard}/hooks/usePitchMapping.js | 2 +-
src/features/fretboard/index.js | 3 +
.../fretboard/model}/renderFilters.js | 0
.../components}/InstrumentControls.jsx | 20 +-
.../instrument/components}/PresetPicker.jsx | 4 +-
.../containers/InstrumentPanelContainer.jsx | 4 +-
.../instrument}/hooks/useCapo.js | 4 +-
.../instrument}/hooks/useCustomTuningPacks.js | 6 +-
.../instrument}/hooks/useDrawFrets.js | 4 +-
.../instrument}/hooks/useInstrumentConfig.js | 14 +-
.../instrument}/hooks/useInstrumentDomain.js | 14 +-
.../instrument}/hooks/useMergedPresets.js | 10 +-
.../instrument}/hooks/usePresetBuilder.js | 8 +-
.../instrument}/hooks/useStringsChange.js | 0
.../instrument}/hooks/useTuningIO.js | 16 +-
src/features/instrument/index.js | 2 +
.../store}/useInstrumentCoreStore.js | 12 +-
.../store}/useInstrumentWorkflowStore.js | 8 +-
.../practice/components}/BeatIndicator.jsx | 0
.../components}/MetronomeControls.jsx | 8 +-
.../containers/PracticePanelContainer.jsx | 4 +-
.../containers/usePracticePanelState.js | 10 +-
.../practice}/hooks/useMetronomeEngine.js | 2 +-
.../practice}/hooks/usePracticeActions.js | 0
.../hooks/usePracticeMetronomeDomain.js | 8 +-
src/features/practice/index.js | 17 ++
.../store}/useMetronomeEngineStore.js | 0
.../practice/store}/useMetronomePrefsStore.js | 10 +-
.../share/components}/ShareConfigModal.jsx | 6 +-
.../share/components}/ShareQrCode.jsx | 0
.../share}/hooks/useUrlShareHydration.js | 2 +-
src/features/share/index.js | 3 +
.../share/model}/shareCodec.ts | 10 +-
.../share/model}/shareConfigModalModel.ts | 6 +-
.../share/model}/shareLimits.ts | 0
.../share/model}/shareSchema.ts | 0
.../share/model}/shareScopes.ts | 0
.../share/model}/shareState.js | 2 +-
.../theory/components}/ChordControls.jsx | 18 +-
.../theory/components}/ChordTypePicker.jsx | 6 +-
.../theory/components}/ScaleControls.jsx | 10 +-
.../theory/components}/ScalePicker.jsx | 2 +-
.../containers/TheoryPanelContainer.jsx | 8 +-
.../theory}/hooks/resetMusicalState.js | 2 +-
.../theory}/hooks/useAccidentalRespell.js | 0
.../theory}/hooks/useRandomScale.js | 4 +-
.../theory}/hooks/useScaleAndChord.js | 0
.../theory}/hooks/useSystemNoteNames.js | 2 +-
.../theory}/hooks/useTheoryDomain.js | 14 +-
src/features/theory/index.js | 10 +
.../theory/model}/chordCapoDisplay.js | 0
.../theory/model}/theoryPanelModel.js | 0
.../theory/store}/useTheoryStore.js | 8 +-
src/{lib => shared}/config/appDefaults.ts | 2 +-
src/{ => shared}/hooks/hotkeyHandler.js | 2 +-
src/{ => shared}/hooks/hotkeyUtils.js | 0
src/{ => shared}/hooks/hotkeys.types.d.ts | 0
src/{ => shared}/hooks/hotkeysTable.js | 6 +-
src/{ => shared}/hooks/useCombobox.js | 0
src/{ => shared}/hooks/useConfirm.js | 2 +-
src/{ => shared}/hooks/useFilteredOptions.js | 0
src/{ => shared}/hooks/useHotkeys.js | 10 +-
src/{ => shared}/hooks/useNumberField.js | 2 +-
src/{ => shared}/hooks/useResets.js | 6 +-
src/{ => shared}/hooks/useThrottledTrigger.js | 0
.../hooks/validatedStorageUtils.js | 0
.../lib}/applyValueOrUpdaterOnDraft.ts | 0
.../lib/controlModels.js} | 2 +-
src/{utils => shared/lib}/degreeColors.ts | 0
.../lib}/domainReturnBuilders.js | 0
src/{utils => shared/lib}/fretLabels.ts | 0
src/{utils => shared/lib}/makeImmerSetters.ts | 0
src/{utils => shared/lib}/math.ts | 0
src/{utils => shared/lib}/memo.ts | 0
.../lib}/normalizeStringList.ts | 0
src/{utils => shared/lib}/object.ts | 0
src/{utils => shared/lib}/ordinals.ts | 0
.../lib}/panelContracts.js | 0
src/{utils => shared/lib}/random.ts | 0
src/{stores => shared/lib}/resetAllStores.js | 16 +-
src/{utils => shared/lib}/shapeColors.ts | 0
src/{ => shared}/lib/storage/scopedStorage.ts | 2 +-
src/{ => shared}/lib/storage/storageKeys.ts | 0
src/{ => shared}/lib/storage/windowScope.ts | 0
src/{utils => shared/lib}/svgDelegation.js | 0
src/{utils => shared/lib}/textFit.js | 0
src/{utils => shared/lib}/toast.ts | 0
.../combobox => shared/ui}/BaseCombobox.jsx | 8 +-
.../UI => shared/ui}/ConfirmDialog.jsx | 0
.../UI => shared/ui}/ErrorFallback.jsx | 6 +-
.../ui}/FloatingListbox.jsx | 0
.../UI => shared/ui}/HotkeysCheatsheet.jsx | 0
.../UI/modals => shared/ui}/ModalFrame.jsx | 0
.../UI => shared/ui}/NumberField.jsx | 2 +-
.../UI => shared/ui}/PanelHeader.jsx | 0
.../UI => shared/ui}/SafeLazyModal.jsx | 2 +-
.../UI => shared/ui}/SafeSection.jsx | 2 +-
src/{components/UI => shared/ui}/Section.jsx | 0
.../UI => shared/ui}/SegmentedRadioGroup.jsx | 0
.../UI => shared/ui}/ToggleSwitch.jsx | 0
.../UI => shared/ui}/errorFallbackReset.js | 2 +-
src/shared/ui/index.js | 2 +
.../{ => app}/appDomainHooks.contract.test.js | 4 +-
.../presets}/neckFilterModes.test.js | 4 +-
src/tests/{ => domain/theory}/chords.test.js | 2 +-
.../theory}/fretboardShapes.test.ts | 2 +-
.../{ => domain/theory}/notation.test.js | 2 +-
.../{ => domain/theory}/shapeSystems.test.ts | 2 +-
src/tests/{ => domain/theory}/tuning.test.js | 2 +-
.../export}/importPipeline.test.ts | 2 +-
.../export}/tuningPackEditorModal.test.js | 2 +-
.../fretboardLayoutMemoization.test.js | 4 +-
.../hiddenFretsMetaAndRender.test.js | 12 +-
.../fretboard}/pitchMapping.test.js | 4 +-
.../instrument}/presetBuilder.test.js | 2 +-
.../instrument}/storeMigrationParity.test.js | 91 ++++---
.../storeSelectorStability.test.js | 21 +-
.../instrument}/tuningIO.test.js | 10 +-
.../practice}/useMetronomeEngine.test.js | 2 +-
.../{ => features/share}/shareCodec.test.ts | 2 +-
.../{ => features/share}/shareLimits.test.ts | 2 +-
.../share}/shareStateAdapter.test.js | 2 +-
.../share}/useUrlShareHydration.test.js | 4 +-
.../theory}/chordCapoDisplay.test.js | 2 +-
.../{ => features/theory}/noteNaming.test.js | 2 +-
.../theory}/theoryControlModel.test.js | 6 +-
.../theory}/useRandomScale.test.js | 2 +-
.../applyValueOrUpdaterOnDraft.test.ts | 2 +-
src/tests/{ => shared}/degreeColors.test.js | 2 +-
src/tests/{ => shared}/fretLabels.test.js | 2 +-
src/tests/{ => shared}/numberField.test.js | 2 +-
.../segmentedRadioGroup.test.jsx} | 2 +-
src/tests/{ => shared}/useHotkeys.test.js | 6 +-
.../{ => shared}/useValidatedStorage.test.js | 4 +-
tsconfig.json | 7 +-
vite.config.js | 9 +-
182 files changed, 730 insertions(+), 544 deletions(-)
create mode 100644 docs/project-structure.md
create mode 100644 src/app/providers/ToastProvider.jsx
rename src/{components/Layout => app/shell}/AppLayout.jsx (100%)
create mode 100644 src/app/shell/AppPanels.jsx
rename src/{components/UI => app/shell}/StageHud.jsx (97%)
rename src/app/{containers => shell}/StageHudContainer.jsx (89%)
create mode 100644 src/app/shell/StageShell.jsx
rename src/{lib => domain}/meta/meta.ts (97%)
rename src/{lib => domain}/presets/neckFilterModes.ts (99%)
rename src/{lib => domain}/presets/presetState.ts (72%)
rename src/{lib => domain}/presets/presets.ts (99%)
rename src/{lib => domain}/theory/capoChords.ts (100%)
rename src/{lib => domain}/theory/chords.ts (100%)
rename src/{lib => domain}/theory/fretboardShapes.ts (100%)
rename src/{lib => domain}/theory/notation.ts (100%)
rename src/{lib => domain}/theory/scales.ts (100%)
rename src/{lib => domain}/theory/shapeSystems.ts (99%)
rename src/{lib => domain}/theory/tuning.ts (100%)
rename src/{components/UI/controls => features/display/components}/DisplayControls.jsx (94%)
rename src/{ => features/display}/hooks/useDisplayState.js (94%)
rename src/{ => features/display}/hooks/useTheme.js (94%)
create mode 100644 src/features/display/index.js
rename src/{stores => features/display/store}/useDisplayPrefsStore.js (85%)
rename src/{stores => features/display/store}/useThemeStore.js (82%)
rename src/{components/UI/controls => features/export/components}/ExportControls.jsx (94%)
rename src/{components/UI/modals => features/export/components}/TuningPackEditorModal.jsx (98%)
rename src/{components/UI/modals => features/export/components}/TuningPackManagerModal.jsx (97%)
rename src/{app => features/export}/containers/CustomTuningModalsContainer.jsx (86%)
rename src/{app => features/export}/containers/ExportPanelContainer.jsx (86%)
rename src/{app => features/export}/hooks/useExportCustomTuningDomain.js (94%)
create mode 100644 src/features/export/index.js
rename src/{lib/export => features/export/model}/importPipeline.ts (100%)
rename src/{lib/export => features/export/model}/scales.ts (100%)
rename src/{lib/export => features/export/model}/schema.ts (94%)
rename src/{lib/export => features/export/model}/tuningIO.ts (98%)
rename src/{components/UI/modals => features/export/model}/tuningPackNormalization.js (95%)
rename src/{components/UI/modals => features/export/model}/tuningPackSearch.js (100%)
rename src/{components/Fretboard => features/fretboard/components}/Fretboard.jsx (98%)
rename src/{ => features/fretboard}/hooks/useFretboardLayout.js (97%)
rename src/{ => features/fretboard}/hooks/useInlays.js (100%)
rename src/{ => features/fretboard}/hooks/useLabels.js (100%)
rename src/{ => features/fretboard}/hooks/usePitchMapping.js (96%)
create mode 100644 src/features/fretboard/index.js
rename src/{components/Fretboard => features/fretboard/model}/renderFilters.js (100%)
rename src/{components/UI/controls => features/instrument/components}/InstrumentControls.jsx (93%)
rename src/{components/UI/combobox => features/instrument/components}/PresetPicker.jsx (96%)
rename src/{app => features/instrument}/containers/InstrumentPanelContainer.jsx (80%)
rename src/{ => features/instrument}/hooks/useCapo.js (95%)
rename src/{ => features/instrument}/hooks/useCustomTuningPacks.js (98%)
rename src/{ => features/instrument}/hooks/useDrawFrets.js (91%)
rename src/{ => features/instrument}/hooks/useInstrumentConfig.js (94%)
rename src/{app => features/instrument}/hooks/useInstrumentDomain.js (91%)
rename src/{ => features/instrument}/hooks/useMergedPresets.js (96%)
rename src/{ => features/instrument}/hooks/usePresetBuilder.js (93%)
rename src/{ => features/instrument}/hooks/useStringsChange.js (100%)
rename src/{ => features/instrument}/hooks/useTuningIO.js (96%)
create mode 100644 src/features/instrument/index.js
rename src/{stores => features/instrument/store}/useInstrumentCoreStore.js (97%)
rename src/{stores => features/instrument/store}/useInstrumentWorkflowStore.js (95%)
rename src/{components/UI => features/practice/components}/BeatIndicator.jsx (100%)
rename src/{components/UI/controls => features/practice/components}/MetronomeControls.jsx (97%)
rename src/{app => features/practice}/containers/PracticePanelContainer.jsx (81%)
rename src/{app => features/practice}/containers/usePracticePanelState.js (96%)
rename src/{ => features/practice}/hooks/useMetronomeEngine.js (99%)
rename src/{ => features/practice}/hooks/usePracticeActions.js (100%)
rename src/{app => features/practice}/hooks/usePracticeMetronomeDomain.js (86%)
create mode 100644 src/features/practice/index.js
rename src/{stores => features/practice/store}/useMetronomeEngineStore.js (100%)
rename src/{stores => features/practice/store}/useMetronomePrefsStore.js (93%)
rename src/{components/UI/modals => features/share/components}/ShareConfigModal.jsx (95%)
rename src/{components/UI/qr => features/share/components}/ShareQrCode.jsx (100%)
rename src/{app => features/share}/hooks/useUrlShareHydration.js (99%)
create mode 100644 src/features/share/index.js
rename src/{lib/url => features/share/model}/shareCodec.ts (97%)
rename src/{components/UI/modals => features/share/model}/shareConfigModalModel.ts (76%)
rename src/{lib/url => features/share/model}/shareLimits.ts (100%)
rename src/{lib/url => features/share/model}/shareSchema.ts (100%)
rename src/{lib/url => features/share/model}/shareScopes.ts (100%)
rename src/{app/adapters => features/share/model}/shareState.js (98%)
rename src/{components/UI/controls => features/theory/components}/ChordControls.jsx (96%)
rename src/{components/UI/combobox => features/theory/components}/ChordTypePicker.jsx (96%)
rename src/{components/UI/controls => features/theory/components}/ScaleControls.jsx (94%)
rename src/{components/UI/combobox => features/theory/components}/ScalePicker.jsx (98%)
rename src/{app => features/theory}/containers/TheoryPanelContainer.jsx (91%)
rename src/{ => features/theory}/hooks/resetMusicalState.js (95%)
rename src/{ => features/theory}/hooks/useAccidentalRespell.js (100%)
rename src/{ => features/theory}/hooks/useRandomScale.js (93%)
rename src/{ => features/theory}/hooks/useScaleAndChord.js (100%)
rename src/{ => features/theory}/hooks/useSystemNoteNames.js (90%)
rename src/{app => features/theory}/hooks/useTheoryDomain.js (94%)
create mode 100644 src/features/theory/index.js
rename src/{components/UI/controls => features/theory/model}/chordCapoDisplay.js (100%)
rename src/{app/containers => features/theory/model}/theoryPanelModel.js (100%)
rename src/{stores => features/theory/store}/useTheoryStore.js (96%)
rename src/{lib => shared}/config/appDefaults.ts (97%)
rename src/{ => shared}/hooks/hotkeyHandler.js (92%)
rename src/{ => shared}/hooks/hotkeyUtils.js (100%)
rename src/{ => shared}/hooks/hotkeys.types.d.ts (100%)
rename src/{ => shared}/hooks/hotkeysTable.js (97%)
rename src/{ => shared}/hooks/useCombobox.js (100%)
rename src/{ => shared}/hooks/useConfirm.js (95%)
rename src/{ => shared}/hooks/useFilteredOptions.js (100%)
rename src/{ => shared}/hooks/useHotkeys.js (87%)
rename src/{ => shared}/hooks/useNumberField.js (97%)
rename src/{ => shared}/hooks/useResets.js (94%)
rename src/{ => shared}/hooks/useThrottledTrigger.js (100%)
rename src/{ => shared}/hooks/validatedStorageUtils.js (100%)
rename src/{utils => shared/lib}/applyValueOrUpdaterOnDraft.ts (100%)
rename src/{app/adapters/controls.js => shared/lib/controlModels.js} (99%)
rename src/{utils => shared/lib}/degreeColors.ts (100%)
rename src/{app/hooks => shared/lib}/domainReturnBuilders.js (100%)
rename src/{utils => shared/lib}/fretLabels.ts (100%)
rename src/{utils => shared/lib}/makeImmerSetters.ts (100%)
rename src/{utils => shared/lib}/math.ts (100%)
rename src/{utils => shared/lib}/memo.ts (100%)
rename src/{utils => shared/lib}/normalizeStringList.ts (100%)
rename src/{utils => shared/lib}/object.ts (100%)
rename src/{utils => shared/lib}/ordinals.ts (100%)
rename src/{app/contracts => shared/lib}/panelContracts.js (100%)
rename src/{utils => shared/lib}/random.ts (100%)
rename src/{stores => shared/lib}/resetAllStores.js (69%)
rename src/{utils => shared/lib}/shapeColors.ts (100%)
rename src/{ => shared}/lib/storage/scopedStorage.ts (97%)
rename src/{ => shared}/lib/storage/storageKeys.ts (100%)
rename src/{ => shared}/lib/storage/windowScope.ts (100%)
rename src/{utils => shared/lib}/svgDelegation.js (100%)
rename src/{utils => shared/lib}/textFit.js (100%)
rename src/{utils => shared/lib}/toast.ts (100%)
rename src/{components/UI/combobox => shared/ui}/BaseCombobox.jsx (98%)
rename src/{components/UI => shared/ui}/ConfirmDialog.jsx (100%)
rename src/{components/UI => shared/ui}/ErrorFallback.jsx (96%)
rename src/{components/UI/combobox => shared/ui}/FloatingListbox.jsx (100%)
rename src/{components/UI => shared/ui}/HotkeysCheatsheet.jsx (100%)
rename src/{components/UI/modals => shared/ui}/ModalFrame.jsx (100%)
rename src/{components/UI => shared/ui}/NumberField.jsx (94%)
rename src/{components/UI => shared/ui}/PanelHeader.jsx (100%)
rename src/{components/UI => shared/ui}/SafeLazyModal.jsx (88%)
rename src/{components/UI => shared/ui}/SafeSection.jsx (84%)
rename src/{components/UI => shared/ui}/Section.jsx (100%)
rename src/{components/UI => shared/ui}/SegmentedRadioGroup.jsx (100%)
rename src/{components/UI => shared/ui}/ToggleSwitch.jsx (100%)
rename src/{components/UI => shared/ui}/errorFallbackReset.js (90%)
create mode 100644 src/shared/ui/index.js
rename src/tests/{ => app}/appDomainHooks.contract.test.js (96%)
rename src/tests/{ => domain/presets}/neckFilterModes.test.js (98%)
rename src/tests/{ => domain/theory}/chords.test.js (92%)
rename src/tests/{ => domain/theory}/fretboardShapes.test.ts (99%)
rename src/tests/{ => domain/theory}/notation.test.js (98%)
rename src/tests/{ => domain/theory}/shapeSystems.test.ts (99%)
rename src/tests/{ => domain/theory}/tuning.test.js (98%)
rename src/tests/{ => features/export}/importPipeline.test.ts (98%)
rename src/tests/{ => features/export}/tuningPackEditorModal.test.js (98%)
rename src/tests/{ => features/fretboard}/fretboardLayoutMemoization.test.js (92%)
rename src/tests/{ => features/fretboard}/hiddenFretsMetaAndRender.test.js (97%)
rename src/tests/{ => features/fretboard}/pitchMapping.test.js (91%)
rename src/tests/{ => features/instrument}/presetBuilder.test.js (97%)
rename src/tests/{ => features/instrument}/storeMigrationParity.test.js (89%)
rename src/tests/{ => features/instrument}/storeSelectorStability.test.js (85%)
rename src/tests/{ => features/instrument}/tuningIO.test.js (93%)
rename src/tests/{ => features/practice}/useMetronomeEngine.test.js (95%)
rename src/tests/{ => features/share}/shareCodec.test.ts (99%)
rename src/tests/{ => features/share}/shareLimits.test.ts (96%)
rename src/tests/{ => features/share}/shareStateAdapter.test.js (98%)
rename src/tests/{ => features/share}/useUrlShareHydration.test.js (97%)
rename src/tests/{ => features/theory}/chordCapoDisplay.test.js (94%)
rename src/tests/{ => features/theory}/noteNaming.test.js (98%)
rename src/tests/{ => features/theory}/theoryControlModel.test.js (97%)
rename src/tests/{ => features/theory}/useRandomScale.test.js (97%)
rename src/tests/{ => shared}/applyValueOrUpdaterOnDraft.test.ts (94%)
rename src/tests/{ => shared}/degreeColors.test.js (89%)
rename src/tests/{ => shared}/fretLabels.test.js (99%)
rename src/tests/{ => shared}/numberField.test.js (96%)
rename src/tests/{SegmentedRadioGroup.test.jsx => shared/segmentedRadioGroup.test.jsx} (91%)
rename src/tests/{ => shared}/useHotkeys.test.js (97%)
rename src/tests/{ => shared}/useValidatedStorage.test.js (93%)
diff --git a/docs/project-structure.md b/docs/project-structure.md
new file mode 100644
index 0000000..2c3c824
--- /dev/null
+++ b/docs/project-structure.md
@@ -0,0 +1,32 @@
+# Project Structure
+
+The source tree is organized by product feature/domain rather than by technical type.
+
+## Top-level source areas
+
+- `src/app/` contains application composition only: bootstrapping, global providers, app-level orchestration hooks, and shell/layout components.
+- `src/features/*/` contains feature-owned UI, hooks, models, containers, and stores. Public feature APIs are exposed through each feature's `index.js` barrel.
+- `src/shared/` contains reusable feature-agnostic UI primitives, hooks, libraries, and configuration.
+- `src/domain/` contains pure domain data and music-theory/preset/meta modules that do not depend on React app wiring.
+- `src/tests/` mirrors this organization with `features/`, `domain/`, `shared/`, and `app/` folders.
+
+## Import aliases
+
+Use scoped aliases for ownership boundaries:
+
+- `@app/*` for app composition, providers, and shell code.
+- `@features/*` for feature public barrels and intentional feature-internal imports.
+- `@shared/*` for feature-agnostic UI, hooks, helpers, and config.
+- `@domain/*` for pure domain modules.
+- `@styles/*` for global styles.
+- `@/*` remains available as a compatibility fallback, but new imports should prefer the scoped aliases above.
+
+## Import boundary rules
+
+- `src/features/*` may import from `src/shared/*` and `src/domain/*`.
+- `src/app/*` may import from feature public entry points such as `@features/instrument` instead of feature internals where possible.
+- Feature internals must not import from `src/app/*`.
+- Feature internals should avoid importing another feature's internal files; use the other feature's `index.js` when a dependency is intentional.
+- `src/shared/*` should remain feature-agnostic. If a shared helper must coordinate app-wide stores, keep the dependency isolated and document it in review.
+
+These boundaries are enforced during code review rather than by an ESLint boundary plugin.
diff --git a/knip.json b/knip.json
index e8a17d8..a9b8fb8 100644
--- a/knip.json
+++ b/knip.json
@@ -1,5 +1,5 @@
{
- "entry": ["index.html", "src/tests/**/*.test.js"],
+ "entry": ["index.html", "src/tests/**/*.test.{js,jsx,ts,tsx}"],
"project": ["src/**/*.{js,jsx,ts,tsx}"],
"ignoreDependencies": ["lightningcss"]
}
diff --git a/src/app/App.jsx b/src/app/App.jsx
index 9c8d04a..5bdc214 100644
--- a/src/app/App.jsx
+++ b/src/app/App.jsx
@@ -1,55 +1,41 @@
-import { useCallback, useMemo, useRef } from "react";
-import clsx from "clsx";
-import {
- FiCheckCircle,
- FiAlertTriangle,
- FiInfo,
- FiLoader,
-} from "react-icons/fi";
-import { Toaster, ToastBar } from "react-hot-toast";
-
-import { downloadPNG, downloadSVG, printFretboard } from "@/lib/export/scales";
-import Fretboard from "@/components/Fretboard/Fretboard";
-import StageHudContainer from "@/app/containers/StageHudContainer";
-
-import { TUNINGS } from "@/lib/theory/tuning";
-import { ALL_SCALES } from "@/lib/theory/scales";
-import { PRESET_TUNING_META } from "@/lib/presets/presets";
+import { useMemo, useRef } from "react";
+import { useDisplayState } from "@features/display";
import {
- STR_MIN,
- STR_MAX,
- FRETS_MIN,
+ downloadPNG,
+ downloadSVG,
+ printFretboard,
+ useExportCustomTuningDomain,
+} from "@features/export";
+import { useInstrumentDomain } from "@features/instrument";
+import { usePracticeMetronomeDomain } from "@features/practice";
+import { useUrlShareHydration } from "@features/share";
+import { useTheoryDomain } from "@features/theory";
+import { PanelHeader } from "@shared/ui";
+import { useConfirm } from "@shared/hooks/useConfirm";
+import { TUNINGS } from "@domain/theory/tuning";
+import { ALL_SCALES } from "@domain/theory/scales";
+import { PRESET_TUNING_META } from "@domain/presets/presets";
+import { DEFAULT_TUNINGS, PRESET_TUNINGS } from "@domain/presets/presetState";
+import {
+ DISPLAY_DEFAULTS,
FRETS_MAX,
+ FRETS_MIN,
getFactoryFrets,
- SYSTEM_DEFAULT,
- ROOT_DEFAULT,
- DISPLAY_DEFAULTS,
METRONOME_DEFAULTS,
+ ROOT_DEFAULT,
SCALE_DEFAULT,
-} from "@/lib/config/appDefaults";
-
-import { DEFAULT_TUNINGS, PRESET_TUNINGS } from "@/lib/presets/presetState";
-
-import PanelHeader from "@/components/UI/PanelHeader";
-import SafeSection from "@/components/UI/SafeSection";
-import DisplayControls from "@/components/UI/controls/DisplayControls";
+ STR_MAX,
+ STR_MIN,
+ SYSTEM_DEFAULT,
+} from "@shared/config/appDefaults";
-import { useConfirm } from "@/hooks/useConfirm";
-import { useDisplayState } from "@/hooks/useDisplayState";
-import AppLayout from "@/components/Layout/AppLayout";
-import InstrumentPanelContainer from "@/app/containers/InstrumentPanelContainer";
-import TheoryPanelContainer from "@/app/containers/TheoryPanelContainer";
-import PracticePanelContainer from "@/app/containers/PracticePanelContainer";
-import ExportPanelContainer from "@/app/containers/ExportPanelContainer";
-import CustomTuningModalsContainer from "@/app/containers/CustomTuningModalsContainer";
-import { useTheoryDomain } from "@/app/hooks/useTheoryDomain";
-import { useInstrumentDomain } from "@/app/hooks/useInstrumentDomain";
-import { usePracticeMetronomeDomain } from "@/app/hooks/usePracticeMetronomeDomain";
-import { useExportCustomTuningDomain } from "@/app/hooks/useExportCustomTuningDomain";
-import { useAppOrchestration } from "@/app/hooks/useAppOrchestration";
-import { useAppPanelModels } from "@/app/hooks/useAppPanelModels";
-import { useUrlShareHydration } from "@/app/hooks/useUrlShareHydration";
+import AppLayout from "@app/shell/AppLayout";
+import { AppModals, AppPanels } from "@app/shell/AppPanels";
+import StageShell from "@app/shell/StageShell";
+import ToastProvider from "@app/providers/ToastProvider";
+import { useAppOrchestration } from "@app/hooks/useAppOrchestration";
+import { useAppPanelModels } from "@app/hooks/useAppPanelModels";
export default function App() {
const boardRef = useRef(null);
@@ -94,22 +80,8 @@ export default function App() {
noteNaming: displayPrefs.noteNaming,
});
const { instrumentState, instrumentDerived, capo } = instrumentDomain;
- const { strings, tuning, stringMeta, boardMeta } = instrumentState;
+ const { tuning, stringMeta, boardMeta } = instrumentState;
const { drawFrets } = instrumentDerived;
- const { capoFret, toggleCapoAt, effectiveStringMeta } = capo;
-
- const {
- show,
- showOpen,
- showFretNums,
- dotSize,
- lefty,
- openOnlyInScale,
- colorByDegree,
- colorByShape,
- accidental,
- microLabelStyle,
- } = displayPrefs;
const practiceDomain = usePracticeMetronomeDomain({
metronomeDefaults: METRONOME_DEFAULTS,
@@ -175,7 +147,7 @@ export default function App() {
themeMode,
root: theoryDomain.system.root,
scale: theoryDomain.scale.scale,
- accidental,
+ accidental: displayPrefs.accidental,
noteNaming: displayPrefs.noteNaming,
strings: instrumentState.strings,
systemId: theoryDomain.system.systemId,
@@ -193,91 +165,38 @@ export default function App() {
});
const header = ;
- const showPracticeHud = orchestration.showPracticeHud;
-
- const { handleSelectNote: handleTheorySelectNote } = theoryDomain.handlers;
-
- const handleSelectNote = useCallback(
- (pc, providedName, event) => {
- handleTheorySelectNote(pc, providedName, event, {
- capoFret,
- });
- },
- [capoFret, handleTheorySelectNote],
- );
-
const stage = (
-
-
toggleFs()}
- >
- resetAll({ confirm: true })}
- showPracticeHud={showPracticeHud}
- />
-
-
-
-
-
+
);
const controls = useMemo(
() => (
- <>
-
-
-
- {
- resetDisplay();
- }}
- >
-
-
-
- >
+
),
[
instrumentPanel,
@@ -292,45 +211,7 @@ export default function App() {
],
);
- const modals = (
-
- );
-
- const toaster = (
-
- {(t) => {
- const icon =
- t.type === "success" ? (
-
- ) : t.type === "error" ? (
-
- ) : t.type === "loading" ? (
-
- ) : (
-
- );
- return (
-
- {({ message, action }) => (
-
-
{icon}
-
{message}
- {action}
-
- )}
-
- );
- }}
-
- );
+ const modals = ;
return (
}
/>
);
}
diff --git a/src/app/hooks/useAppOrchestration.js b/src/app/hooks/useAppOrchestration.js
index 491e49e..382d39e 100644
--- a/src/app/hooks/useAppOrchestration.js
+++ b/src/app/hooks/useAppOrchestration.js
@@ -1,19 +1,19 @@
import { createElement, useCallback } from "react";
import { toast } from "react-hot-toast";
-import HotkeysCheatsheet from "@/components/UI/HotkeysCheatsheet";
-import { LABEL_VALUES } from "@/hooks/useLabels";
-import { useAccidentalRespell } from "@/hooks/useAccidentalRespell";
-import { useHotkeys } from "@/hooks/useHotkeys";
-import { useResets } from "@/hooks/useResets";
+import HotkeysCheatsheet from "@shared/ui/HotkeysCheatsheet";
+import { LABEL_VALUES } from "@features/fretboard";
+import { useAccidentalRespell } from "@features/theory";
+import { useHotkeys } from "@shared/hooks/useHotkeys";
+import { useResets } from "@shared/hooks/useResets";
import {
CAPO_DEFAULT,
FRETS_MAX,
FRETS_MIN,
STR_MAX,
STR_MIN,
-} from "@/lib/config/appDefaults";
+} from "@shared/config/appDefaults";
-/** @typedef {import("@/app/hooks/interfaces").AppOrchestrationInput} AppOrchestrationInput */
+/** @typedef {import("@app/hooks/interfaces").AppOrchestrationInput} AppOrchestrationInput */
function validateOrchestrationInputsDev({
displayPrefs,
diff --git a/src/app/hooks/useAppPanelModels.js b/src/app/hooks/useAppPanelModels.js
index aa06d02..9b8039c 100644
--- a/src/app/hooks/useAppPanelModels.js
+++ b/src/app/hooks/useAppPanelModels.js
@@ -2,13 +2,13 @@ import { useMemo } from "react";
import {
buildDisplayControlModel,
buildTheoryControlModel,
-} from "@/app/adapters/controls";
-import { buildRawShareState } from "@/app/adapters/shareState";
+} from "@shared/lib/controlModels";
+import { buildRawShareState } from "@features/share";
import {
CHORD_DEFAULT,
ROOT_DEFAULT,
SCALE_DEFAULT,
-} from "@/lib/config/appDefaults";
+} from "@shared/config/appDefaults";
export function useAppPanelModels({
theoryDomain,
diff --git a/src/app/main.jsx b/src/app/main.jsx
index 0c23902..326b5ad 100644
--- a/src/app/main.jsx
+++ b/src/app/main.jsx
@@ -2,8 +2,8 @@ import { StrictMode } from "react";
import ReactDOM from "react-dom/client";
import { ErrorBoundary } from "react-error-boundary";
import App from "./App.jsx";
-import "@/styles/index.css";
-import ErrorFallback from "@/components/UI/ErrorFallback";
+import "@styles/index.css";
+import ErrorFallback from "@shared/ui/ErrorFallback";
ReactDOM.createRoot(document.getElementById("root")).render(
diff --git a/src/app/providers/ToastProvider.jsx b/src/app/providers/ToastProvider.jsx
new file mode 100644
index 0000000..8f46ce6
--- /dev/null
+++ b/src/app/providers/ToastProvider.jsx
@@ -0,0 +1,40 @@
+import { FiAlertTriangle, FiCheckCircle, FiInfo, FiLoader } from "react-icons/fi";
+import { Toaster, ToastBar } from "react-hot-toast";
+
+export default function ToastProvider() {
+ return (
+
+ {(t) => {
+ const icon =
+ t.type === "success" ? (
+
+ ) : t.type === "error" ? (
+
+ ) : t.type === "loading" ? (
+
+ ) : (
+
+ );
+ return (
+
+ {({ message, action }) => (
+
+
{icon}
+
{message}
+ {action}
+
+ )}
+
+ );
+ }}
+
+ );
+}
diff --git a/src/components/Layout/AppLayout.jsx b/src/app/shell/AppLayout.jsx
similarity index 100%
rename from src/components/Layout/AppLayout.jsx
rename to src/app/shell/AppLayout.jsx
diff --git a/src/app/shell/AppPanels.jsx b/src/app/shell/AppPanels.jsx
new file mode 100644
index 0000000..78f696c
--- /dev/null
+++ b/src/app/shell/AppPanels.jsx
@@ -0,0 +1,52 @@
+import { InstrumentPanelContainer } from "@features/instrument";
+import { TheoryPanelContainer } from "@features/theory";
+import { PracticePanelContainer } from "@features/practice";
+import { DisplayControls } from "@features/display";
+import {
+ CustomTuningModalsContainer,
+ ExportPanelContainer,
+} from "@features/export";
+import { SafeSection } from "@shared/ui";
+
+export function AppPanels({
+ instrumentPanel,
+ instrumentControlModel,
+ theoryPanel,
+ practicePanel,
+ metronomeControlModel,
+ displayPrefs,
+ resetDisplay,
+ displayControlModel,
+ exportPanel,
+}) {
+ return (
+ <>
+
+
+
+ {
+ resetDisplay();
+ }}
+ >
+
+
+
+ >
+ );
+}
+
+export function AppModals({ modalPanel }) {
+ return ;
+}
diff --git a/src/components/UI/StageHud.jsx b/src/app/shell/StageHud.jsx
similarity index 97%
rename from src/components/UI/StageHud.jsx
rename to src/app/shell/StageHud.jsx
index c74b142..6e93492 100644
--- a/src/components/UI/StageHud.jsx
+++ b/src/app/shell/StageHud.jsx
@@ -1,6 +1,6 @@
import clsx from "clsx";
import { FiMaximize, FiMinimize, FiRotateCcw } from "react-icons/fi";
-import BeatIndicator from "@/components/UI/BeatIndicator";
+import { BeatIndicator } from "@features/practice";
function StageHud({
isFs,
diff --git a/src/app/containers/StageHudContainer.jsx b/src/app/shell/StageHudContainer.jsx
similarity index 89%
rename from src/app/containers/StageHudContainer.jsx
rename to src/app/shell/StageHudContainer.jsx
index d9eacd1..d2fe625 100644
--- a/src/app/containers/StageHudContainer.jsx
+++ b/src/app/shell/StageHudContainer.jsx
@@ -1,12 +1,10 @@
-import StageHud from "@/components/UI/StageHud";
+import StageHud from "@app/shell/StageHud";
import {
+ selectMetronomePrefs,
useMetronomePlaybackStatus,
- useMetronomeTickCursor,
-} from "@/hooks/useMetronomeEngine";
-import {
useMetronomePrefsStore,
- selectMetronomePrefs,
-} from "@/stores/useMetronomePrefsStore";
+ useMetronomeTickCursor,
+} from "@features/practice";
export default function StageHudContainer({
isFs,
diff --git a/src/app/shell/StageShell.jsx b/src/app/shell/StageShell.jsx
new file mode 100644
index 0000000..7de589c
--- /dev/null
+++ b/src/app/shell/StageShell.jsx
@@ -0,0 +1,95 @@
+import { useCallback } from "react";
+import clsx from "clsx";
+
+import StageHudContainer from "@app/shell/StageHudContainer";
+import { Fretboard } from "@features/fretboard";
+import { SafeSection } from "@shared/ui";
+
+export default function StageShell({
+ boardRef,
+ stageRef,
+ isFs,
+ toggleFs,
+ resetAll,
+ showPracticeHud,
+ displayPrefs,
+ theoryDomain,
+ theoryPanel,
+ instrumentState,
+ drawFrets,
+ boardMeta,
+ capo,
+ onResetCapo,
+}) {
+ const { strings, tuning } = instrumentState;
+ const { capoFret, toggleCapoAt, effectiveStringMeta } = capo;
+ const { handleSelectNote: handleTheorySelectNote } = theoryDomain.handlers;
+ const {
+ show,
+ showOpen,
+ showFretNums,
+ dotSize,
+ lefty,
+ openOnlyInScale,
+ colorByDegree,
+ colorByShape,
+ accidental,
+ microLabelStyle,
+ noteNaming,
+ } = displayPrefs;
+
+ const handleSelectNote = useCallback(
+ (pc, providedName, event) => {
+ handleTheorySelectNote(pc, providedName, event, {
+ capoFret,
+ });
+ },
+ [capoFret, handleTheorySelectNote],
+ );
+
+ return (
+
+
toggleFs()}
+ >
+ resetAll({ confirm: true })}
+ showPracticeHud={showPracticeHud}
+ />
+
+
+
+
+
+ );
+}
diff --git a/src/lib/meta/meta.ts b/src/domain/meta/meta.ts
similarity index 97%
rename from src/lib/meta/meta.ts
rename to src/domain/meta/meta.ts
index 6293884..6923f65 100644
--- a/src/lib/meta/meta.ts
+++ b/src/domain/meta/meta.ts
@@ -1,5 +1,5 @@
-import { isPlainObject } from "@/utils/object";
-import { isNeckFilterMode } from "@/lib/presets/neckFilterModes";
+import { isPlainObject } from "@shared/lib/object";
+import { isNeckFilterMode } from "@domain/presets/neckFilterModes";
export type StringMeta = {
index: number;
diff --git a/src/lib/presets/neckFilterModes.ts b/src/domain/presets/neckFilterModes.ts
similarity index 99%
rename from src/lib/presets/neckFilterModes.ts
rename to src/domain/presets/neckFilterModes.ts
index d3fafbb..eff9cbc 100644
--- a/src/lib/presets/neckFilterModes.ts
+++ b/src/domain/presets/neckFilterModes.ts
@@ -1,4 +1,4 @@
-import { isPlainObject } from "@/utils/object";
+import { isPlainObject } from "@shared/lib/object";
export const KG_NECK_HIDDEN_FRETS = Object.freeze([1, 5, 11, 15, 19, 23]);
export const NECK_FILTER_MODES = Object.freeze({
diff --git a/src/lib/presets/presetState.ts b/src/domain/presets/presetState.ts
similarity index 72%
rename from src/lib/presets/presetState.ts
rename to src/domain/presets/presetState.ts
index 8a9118a..454aec1 100644
--- a/src/lib/presets/presetState.ts
+++ b/src/domain/presets/presetState.ts
@@ -1,8 +1,8 @@
-import { TUNINGS } from "@/lib/theory/tuning";
+import { TUNINGS } from "@domain/theory/tuning";
import {
systemsFromTuningMap,
buildPresetStateForSystems,
-} from "@/lib/presets/presets";
+} from "@domain/presets/presets";
const SYSTEMS = systemsFromTuningMap(TUNINGS);
export const { PRESET_TUNINGS, DEFAULT_TUNINGS, DEFAULT_PRESET_NAME } =
diff --git a/src/lib/presets/presets.ts b/src/domain/presets/presets.ts
similarity index 99%
rename from src/lib/presets/presets.ts
rename to src/domain/presets/presets.ts
index 4cb826d..7faab70 100644
--- a/src/lib/presets/presets.ts
+++ b/src/domain/presets/presets.ts
@@ -1,5 +1,5 @@
// Top of file (added import)
-import type { TuningPresetMeta } from "@/lib/meta/meta";
+import type { TuningPresetMeta } from "@domain/meta/meta";
/* =========================
Core shared constants
diff --git a/src/lib/theory/capoChords.ts b/src/domain/theory/capoChords.ts
similarity index 100%
rename from src/lib/theory/capoChords.ts
rename to src/domain/theory/capoChords.ts
diff --git a/src/lib/theory/chords.ts b/src/domain/theory/chords.ts
similarity index 100%
rename from src/lib/theory/chords.ts
rename to src/domain/theory/chords.ts
diff --git a/src/lib/theory/fretboardShapes.ts b/src/domain/theory/fretboardShapes.ts
similarity index 100%
rename from src/lib/theory/fretboardShapes.ts
rename to src/domain/theory/fretboardShapes.ts
diff --git a/src/lib/theory/notation.ts b/src/domain/theory/notation.ts
similarity index 100%
rename from src/lib/theory/notation.ts
rename to src/domain/theory/notation.ts
diff --git a/src/lib/theory/scales.ts b/src/domain/theory/scales.ts
similarity index 100%
rename from src/lib/theory/scales.ts
rename to src/domain/theory/scales.ts
diff --git a/src/lib/theory/shapeSystems.ts b/src/domain/theory/shapeSystems.ts
similarity index 99%
rename from src/lib/theory/shapeSystems.ts
rename to src/domain/theory/shapeSystems.ts
index bfce8a2..24c03b0 100644
--- a/src/lib/theory/shapeSystems.ts
+++ b/src/domain/theory/shapeSystems.ts
@@ -14,7 +14,7 @@ import {
type ShapeNote,
type ShapeOccurrence,
summarizeShape,
-} from "@/lib/theory/fretboardShapes";
+} from "@domain/theory/fretboardShapes";
export type NotesPerStringConstraint =
| { kind: "exact"; value: number; includeEmptyStrings?: boolean }
diff --git a/src/lib/theory/tuning.ts b/src/domain/theory/tuning.ts
similarity index 100%
rename from src/lib/theory/tuning.ts
rename to src/domain/theory/tuning.ts
diff --git a/src/components/UI/controls/DisplayControls.jsx b/src/features/display/components/DisplayControls.jsx
similarity index 94%
rename from src/components/UI/controls/DisplayControls.jsx
rename to src/features/display/components/DisplayControls.jsx
index 57a6dae..fe2c2e6 100644
--- a/src/components/UI/controls/DisplayControls.jsx
+++ b/src/features/display/components/DisplayControls.jsx
@@ -1,15 +1,15 @@
import { useId } from "react";
import clsx from "clsx";
-import Section from "@/components/UI/Section";
-import { LABEL_OPTIONS } from "@/hooks/useLabels";
-import { MICRO_LABEL_STYLES } from "@/utils/fretLabels";
-import { getDegreeColor } from "@/utils/degreeColors";
-import { getShapeColor } from "@/utils/shapeColors";
+import Section from "@shared/ui/Section";
+import { LABEL_OPTIONS } from "@features/fretboard";
+import { MICRO_LABEL_STYLES } from "@shared/lib/fretLabels";
+import { getDegreeColor } from "@shared/lib/degreeColors";
+import { getShapeColor } from "@shared/lib/shapeColors";
import { FiInfo } from "react-icons/fi";
-import { memoWithShallowPick } from "@/utils/memo";
-import { DOT_SIZE_MAX, DOT_SIZE_MIN } from "@/lib/config/appDefaults";
-import ToggleSwitch from "@/components/UI/ToggleSwitch";
-import SegmentedRadioGroup from "@/components/UI/SegmentedRadioGroup";
+import { memoWithShallowPick } from "@shared/lib/memo";
+import { DOT_SIZE_MAX, DOT_SIZE_MIN } from "@shared/config/appDefaults";
+import ToggleSwitch from "@shared/ui/ToggleSwitch";
+import SegmentedRadioGroup from "@shared/ui/SegmentedRadioGroup";
function DegreeLegend({ k = 7 }) {
if (!Number.isFinite(k) || k < 1) return null;
diff --git a/src/hooks/useDisplayState.js b/src/features/display/hooks/useDisplayState.js
similarity index 94%
rename from src/hooks/useDisplayState.js
rename to src/features/display/hooks/useDisplayState.js
index a4e0027..0323e8e 100644
--- a/src/hooks/useDisplayState.js
+++ b/src/features/display/hooks/useDisplayState.js
@@ -1,7 +1,7 @@
import { useEffect, useMemo, useRef } from "react";
import { useFullscreen, useToggle } from "react-use";
import { useShallow } from "zustand/react/shallow";
-import { useTheme } from "@/hooks/useTheme";
+import { useTheme } from "@features/display/hooks/useTheme";
import {
useDisplayPrefsStore,
selectDisplayHydrateWithDefaults,
@@ -9,7 +9,7 @@ import {
selectDisplaySetPrefs,
selectDisplayResetPrefs,
selectDisplaySetters,
-} from "@/stores/useDisplayPrefsStore";
+} from "@features/display/store/useDisplayPrefsStore";
export function useDisplayState(defaults) {
const {
diff --git a/src/hooks/useTheme.js b/src/features/display/hooks/useTheme.js
similarity index 94%
rename from src/hooks/useTheme.js
rename to src/features/display/hooks/useTheme.js
index 9b25674..11870f9 100644
--- a/src/hooks/useTheme.js
+++ b/src/features/display/hooks/useTheme.js
@@ -6,7 +6,7 @@ import {
useThemeStore,
selectTheme,
selectSetTheme,
-} from "@/stores/useThemeStore";
+} from "@features/display/store/useThemeStore";
export function useTheme() {
const { theme, setTheme } = useThemeStore(
diff --git a/src/features/display/index.js b/src/features/display/index.js
new file mode 100644
index 0000000..01f7972
--- /dev/null
+++ b/src/features/display/index.js
@@ -0,0 +1,2 @@
+export { default as DisplayControls } from "./components/DisplayControls";
+export { useDisplayState } from "./hooks/useDisplayState";
diff --git a/src/stores/useDisplayPrefsStore.js b/src/features/display/store/useDisplayPrefsStore.js
similarity index 85%
rename from src/stores/useDisplayPrefsStore.js
rename to src/features/display/store/useDisplayPrefsStore.js
index 151c0e5..962985f 100644
--- a/src/stores/useDisplayPrefsStore.js
+++ b/src/features/display/store/useDisplayPrefsStore.js
@@ -2,11 +2,11 @@ import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";
-import { STORAGE_KEYS } from "@/lib/storage/storageKeys";
-import { createGlobalStorage } from "@/lib/storage/scopedStorage";
-import { DISPLAY_DEFAULTS } from "@/lib/config/appDefaults";
-import { makeImmerSetters } from "@/utils/makeImmerSetters";
-import { applyValueOrUpdaterOnDraft } from "@/utils/applyValueOrUpdaterOnDraft";
+import { STORAGE_KEYS } from "@shared/lib/storage/storageKeys";
+import { createGlobalStorage } from "@shared/lib/storage/scopedStorage";
+import { DISPLAY_DEFAULTS } from "@shared/config/appDefaults";
+import { makeImmerSetters } from "@shared/lib/makeImmerSetters";
+import { applyValueOrUpdaterOnDraft } from "@shared/lib/applyValueOrUpdaterOnDraft";
const SETTER_KEYS = [
"show",
diff --git a/src/stores/useThemeStore.js b/src/features/display/store/useThemeStore.js
similarity index 82%
rename from src/stores/useThemeStore.js
rename to src/features/display/store/useThemeStore.js
index 476c876..41d8fc5 100644
--- a/src/stores/useThemeStore.js
+++ b/src/features/display/store/useThemeStore.js
@@ -1,8 +1,8 @@
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
-import { STORAGE_KEYS } from "@/lib/storage/storageKeys";
-import { createGlobalStorage } from "@/lib/storage/scopedStorage";
+import { STORAGE_KEYS } from "@shared/lib/storage/storageKeys";
+import { createGlobalStorage } from "@shared/lib/storage/scopedStorage";
export const useThemeStore = create(
persist(
diff --git a/src/components/UI/controls/ExportControls.jsx b/src/features/export/components/ExportControls.jsx
similarity index 94%
rename from src/components/UI/controls/ExportControls.jsx
rename to src/features/export/components/ExportControls.jsx
index 0cccb70..d95de5d 100644
--- a/src/components/UI/controls/ExportControls.jsx
+++ b/src/features/export/components/ExportControls.jsx
@@ -1,16 +1,16 @@
import { useRef, useMemo, useState } from "react";
import clsx from "clsx";
import { toast } from "react-hot-toast";
-import Section from "@/components/UI/Section";
-import { withToastPromise } from "@/utils/toast";
-import { memoWithKeys } from "@/utils/memo";
-import { PNG_EXPORT_SCALE, EXPORT_PADDING } from "@/lib/export/scales";
+import Section from "@shared/ui/Section";
+import { withToastPromise } from "@shared/lib/toast";
+import { memoWithKeys } from "@shared/lib/memo";
+import { PNG_EXPORT_SCALE, EXPORT_PADDING } from "@features/export/model/scales";
import {
getImportPipelineErrorMessage,
IMPORT_PIPELINE_ERROR_CODES,
runImportFilePipeline,
-} from "@/lib/export/importPipeline";
-import ShareConfigModal from "@/components/UI/modals/ShareConfigModal";
+} from "@features/export/model/importPipeline";
+import { ShareConfigModal } from "@features/share";
function ExportControls({
boardRef,
diff --git a/src/components/UI/modals/TuningPackEditorModal.jsx b/src/features/export/components/TuningPackEditorModal.jsx
similarity index 98%
rename from src/components/UI/modals/TuningPackEditorModal.jsx
rename to src/features/export/components/TuningPackEditorModal.jsx
index 4ca74eb..73ef99c 100644
--- a/src/components/UI/modals/TuningPackEditorModal.jsx
+++ b/src/features/export/components/TuningPackEditorModal.jsx
@@ -7,10 +7,10 @@ import {
useToggle,
useWindowSize,
} from "react-use";
-import { parseTuningPack } from "@/lib/export/schema";
-import { useConfirm } from "@/hooks/useConfirm";
+import { parseTuningPack } from "@features/export/model/schema";
+import { useConfirm } from "@shared/hooks/useConfirm";
import { toast } from "react-hot-toast";
-import { memoWithShallowPick } from "@/utils/memo";
+import { memoWithShallowPick } from "@shared/lib/memo";
import {
FiPlus,
FiEdit2,
@@ -22,15 +22,15 @@ import {
FiAlertTriangle,
FiRefreshCcw,
} from "react-icons/fi";
-import ModalFrame from "@/components/UI/modals/ModalFrame";
-import { STR_MAX, STR_MIN } from "@/lib/config/appDefaults";
-import { SPELLING_MARKER_DISPLAY } from "@/lib/theory/notation";
+import ModalFrame from "@shared/ui/ModalFrame";
+import { STR_MAX, STR_MIN } from "@shared/config/appDefaults";
+import { SPELLING_MARKER_DISPLAY } from "@domain/theory/notation";
import {
buildNoteOptionsForPack,
ensurePack,
buildTemplatePack,
togglePackSpelling,
-} from "@/components/UI/modals/tuningPackNormalization";
+} from "@features/export/model/tuningPackNormalization";
function clonePack(pack) {
if (!pack) return null;
diff --git a/src/components/UI/modals/TuningPackManagerModal.jsx b/src/features/export/components/TuningPackManagerModal.jsx
similarity index 97%
rename from src/components/UI/modals/TuningPackManagerModal.jsx
rename to src/features/export/components/TuningPackManagerModal.jsx
index 9e8ecaf..6039bc5 100644
--- a/src/components/UI/modals/TuningPackManagerModal.jsx
+++ b/src/features/export/components/TuningPackManagerModal.jsx
@@ -1,14 +1,14 @@
import { useCallback, useMemo, useState } from "react";
import { FiEdit2, FiTrash2 } from "react-icons/fi";
import { useLatest, useWindowSize } from "react-use";
-import { findSystemByEdo, getSystemLabel } from "@/lib/theory/tuning";
-import { memoWithShallowPick } from "@/utils/memo";
-import ModalFrame from "@/components/UI/modals/ModalFrame";
+import { findSystemByEdo, getSystemLabel } from "@domain/theory/tuning";
+import { memoWithShallowPick } from "@shared/lib/memo";
+import ModalFrame from "@shared/ui/ModalFrame";
import {
formatStringsCount,
normalizePack,
packMatchesQuery,
-} from "@/components/UI/modals/tuningPackSearch";
+} from "@features/export/model/tuningPackSearch";
function TuningPackManagerModal({
isOpen,
diff --git a/src/app/containers/CustomTuningModalsContainer.jsx b/src/features/export/containers/CustomTuningModalsContainer.jsx
similarity index 86%
rename from src/app/containers/CustomTuningModalsContainer.jsx
rename to src/features/export/containers/CustomTuningModalsContainer.jsx
index eaeb37d..ba7d365 100644
--- a/src/app/containers/CustomTuningModalsContainer.jsx
+++ b/src/features/export/containers/CustomTuningModalsContainer.jsx
@@ -1,11 +1,11 @@
import { lazy } from "react";
-import SafeLazyModal from "@/components/UI/SafeLazyModal";
+import SafeLazyModal from "@shared/ui/SafeLazyModal";
const TuningPackEditorModal = lazy(
- () => import("@/components/UI/modals/TuningPackEditorModal"),
+ () => import("@features/export/components/TuningPackEditorModal"),
);
const TuningPackManagerModal = lazy(
- () => import("@/components/UI/modals/TuningPackManagerModal"),
+ () => import("@features/export/components/TuningPackManagerModal"),
);
export default function CustomTuningModalsContainer({
diff --git a/src/app/containers/ExportPanelContainer.jsx b/src/features/export/containers/ExportPanelContainer.jsx
similarity index 86%
rename from src/app/containers/ExportPanelContainer.jsx
rename to src/features/export/containers/ExportPanelContainer.jsx
index 727de52..f71d016 100644
--- a/src/app/containers/ExportPanelContainer.jsx
+++ b/src/features/export/containers/ExportPanelContainer.jsx
@@ -1,8 +1,8 @@
import React from "react";
import { ErrorBoundary } from "react-error-boundary";
-import ErrorFallback from "@/components/UI/ErrorFallback";
-import ExportControls from "@/components/UI/controls/ExportControls";
+import ErrorFallback from "@shared/ui/ErrorFallback";
+import ExportControls from "@features/export/components/ExportControls";
export default function ExportPanelContainer({
fileBase,
diff --git a/src/app/hooks/useExportCustomTuningDomain.js b/src/features/export/hooks/useExportCustomTuningDomain.js
similarity index 94%
rename from src/app/hooks/useExportCustomTuningDomain.js
rename to src/features/export/hooks/useExportCustomTuningDomain.js
index 58d13d9..8194c24 100644
--- a/src/app/hooks/useExportCustomTuningDomain.js
+++ b/src/features/export/hooks/useExportCustomTuningDomain.js
@@ -1,7 +1,7 @@
import { useCallback, useMemo } from "react";
-import { slug } from "@/lib/export/scales";
-import { PANEL_CONTRACTS } from "@/app/contracts/panelContracts";
-import { buildExportCustomTuningDomainReturn } from "@/app/hooks/domainReturnBuilders";
+import { slug } from "@features/export/model/scales";
+import { PANEL_CONTRACTS } from "@shared/lib/panelContracts";
+import { buildExportCustomTuningDomainReturn } from "@shared/lib/domainReturnBuilders";
export function useExportCustomTuningDomain({
boardRef,
diff --git a/src/features/export/index.js b/src/features/export/index.js
new file mode 100644
index 0000000..028c55b
--- /dev/null
+++ b/src/features/export/index.js
@@ -0,0 +1,23 @@
+export { default as ExportPanelContainer } from "./containers/ExportPanelContainer";
+export { default as CustomTuningModalsContainer } from "./containers/CustomTuningModalsContainer";
+export { useExportCustomTuningDomain } from "./hooks/useExportCustomTuningDomain";
+export {
+ downloadPNG,
+ downloadSVG,
+ EXPORT_PADDING,
+ PNG_EXPORT_SCALE,
+ printFretboard,
+ slug,
+} from "./model/scales";
+export {
+ buildTuningPack,
+ downloadJsonFile,
+ ensurePackHasId,
+ normalizePackName,
+ removePackByIdentifier,
+} from "./model/tuningIO";
+export {
+ parseTuningPack,
+ stripVersionField,
+ TuningPackArraySchema,
+} from "./model/schema";
diff --git a/src/lib/export/importPipeline.ts b/src/features/export/model/importPipeline.ts
similarity index 100%
rename from src/lib/export/importPipeline.ts
rename to src/features/export/model/importPipeline.ts
diff --git a/src/lib/export/scales.ts b/src/features/export/model/scales.ts
similarity index 100%
rename from src/lib/export/scales.ts
rename to src/features/export/model/scales.ts
diff --git a/src/lib/export/schema.ts b/src/features/export/model/schema.ts
similarity index 94%
rename from src/lib/export/schema.ts
rename to src/features/export/model/schema.ts
index 4da6e83..a29b6c1 100644
--- a/src/lib/export/schema.ts
+++ b/src/features/export/model/schema.ts
@@ -1,6 +1,6 @@
import * as v from "valibot";
-import { STR_MAX, STR_MIN } from "@/lib/config/appDefaults";
-import { normalizeSpellingHint } from "@/lib/theory/notation";
+import { STR_MAX, STR_MIN } from "@shared/config/appDefaults";
+import { normalizeSpellingHint } from "@domain/theory/notation";
export const TuningStringSchema = v.pipe(
v.object({
diff --git a/src/lib/export/tuningIO.ts b/src/features/export/model/tuningIO.ts
similarity index 98%
rename from src/lib/export/tuningIO.ts
rename to src/features/export/model/tuningIO.ts
index 74df2e2..9b12cbb 100644
--- a/src/lib/export/tuningIO.ts
+++ b/src/features/export/model/tuningIO.ts
@@ -80,7 +80,7 @@ export function downloadTuningPack(pack: TuningPack, filename?: string) {
downloadJsonFile(pack, `${filename || pack.name}.tuning.json`);
}
-import { isPlainObject } from "@/utils/object";
+import { isPlainObject } from "@shared/lib/object";
export interface PackMeta {
id?: string | null;
diff --git a/src/components/UI/modals/tuningPackNormalization.js b/src/features/export/model/tuningPackNormalization.js
similarity index 95%
rename from src/components/UI/modals/tuningPackNormalization.js
rename to src/features/export/model/tuningPackNormalization.js
index f866699..cd278bf 100644
--- a/src/components/UI/modals/tuningPackNormalization.js
+++ b/src/features/export/model/tuningPackNormalization.js
@@ -1,16 +1,16 @@
-import { isPlainObject } from "@/utils/object";
-import { STR_MAX, STR_MIN } from "@/lib/config/appDefaults";
+import { isPlainObject } from "@shared/lib/object";
+import { STR_MAX, STR_MIN } from "@shared/config/appDefaults";
import {
isGermanicSpellingMarker,
normalizeSpellingHint,
renderNoteName,
-} from "@/lib/theory/notation";
+} from "@domain/theory/notation";
import {
TUNINGS,
findSystemByEdo,
getSystemLabel,
nameFallback,
-} from "@/lib/theory/tuning";
+} from "@domain/theory/tuning";
const TEMPLATE_STRINGS = [
{ label: "String 1", note: "E4" },
diff --git a/src/components/UI/modals/tuningPackSearch.js b/src/features/export/model/tuningPackSearch.js
similarity index 100%
rename from src/components/UI/modals/tuningPackSearch.js
rename to src/features/export/model/tuningPackSearch.js
diff --git a/src/components/Fretboard/Fretboard.jsx b/src/features/fretboard/components/Fretboard.jsx
similarity index 98%
rename from src/components/Fretboard/Fretboard.jsx
rename to src/features/fretboard/components/Fretboard.jsx
index 48468b4..b69b386 100644
--- a/src/components/Fretboard/Fretboard.jsx
+++ b/src/features/fretboard/components/Fretboard.jsx
@@ -8,34 +8,34 @@ import {
useRef,
} from "react";
import clsx from "clsx";
-import { useFretboardLayout } from "@/hooks/useFretboardLayout";
-import { useSystemNoteNames } from "@/hooks/useSystemNoteNames";
-import { useScaleAndChord } from "@/hooks/useScaleAndChord";
-import { useInlays } from "@/hooks/useInlays";
-import { useLabels } from "@/hooks/useLabels";
-import { getDegreeColor } from "@/utils/degreeColors";
-import { buildFretLabel, MICRO_LABEL_STYLES } from "@/utils/fretLabels";
+import { useFretboardLayout } from "@features/fretboard/hooks/useFretboardLayout";
+import { useSystemNoteNames } from "@features/theory/hooks/useSystemNoteNames";
+import { useScaleAndChord } from "@features/theory/hooks/useScaleAndChord";
+import { useInlays } from "@features/fretboard/hooks/useInlays";
+import { useLabels } from "@features/fretboard/hooks/useLabels";
+import { getDegreeColor } from "@shared/lib/degreeColors";
+import { buildFretLabel, MICRO_LABEL_STYLES } from "@shared/lib/fretLabels";
import {
arrayRefAndLengthEqual,
objectRefAndKeyEqual,
setRefAndSizeEqual,
-} from "@/utils/memo";
-import { createTextFit } from "@/utils/textFit";
+} from "@shared/lib/memo";
+import { createTextFit } from "@shared/lib/textFit";
import {
maybePreventContextMenu,
parseDatasetNumber,
resolveClosestDatasetElement,
-} from "@/utils/svgDelegation";
-import { toStringMetaMap } from "@/lib/meta/meta";
+} from "@shared/lib/svgDelegation";
+import { toStringMetaMap } from "@domain/meta/meta";
import {
normalizeHiddenFrets,
isHiddenFret,
buildRenderedFretIndices,
resolveVisibleCapoFret,
reconcileCapoState,
-} from "@/components/Fretboard/renderFilters";
-import { findDistinctWindowShapeOccurrences } from "@/lib/theory/fretboardShapes";
-import { getShapeColor } from "@/utils/shapeColors";
+} from "@features/fretboard/model/renderFilters";
+import { findDistinctWindowShapeOccurrences } from "@domain/theory/fretboardShapes";
+import { getShapeColor } from "@shared/lib/shapeColors";
const ROOT_NOTE_RADIUS_MULTIPLIER = 1.1;
const CHORD_NOTE_RADIUS_MULTIPLIER = 1.05;
diff --git a/src/hooks/useFretboardLayout.js b/src/features/fretboard/hooks/useFretboardLayout.js
similarity index 97%
rename from src/hooks/useFretboardLayout.js
rename to src/features/fretboard/hooks/useFretboardLayout.js
index b6fe33c..f82a078 100644
--- a/src/hooks/useFretboardLayout.js
+++ b/src/features/fretboard/hooks/useFretboardLayout.js
@@ -1,5 +1,5 @@
import { useMemo } from "react";
-import { toStringMetaMap } from "@/lib/meta/meta";
+import { toStringMetaMap } from "@domain/meta/meta";
/**
* Geometry/layout for the fretboard SVG.
diff --git a/src/hooks/useInlays.js b/src/features/fretboard/hooks/useInlays.js
similarity index 100%
rename from src/hooks/useInlays.js
rename to src/features/fretboard/hooks/useInlays.js
diff --git a/src/hooks/useLabels.js b/src/features/fretboard/hooks/useLabels.js
similarity index 100%
rename from src/hooks/useLabels.js
rename to src/features/fretboard/hooks/useLabels.js
diff --git a/src/hooks/usePitchMapping.js b/src/features/fretboard/hooks/usePitchMapping.js
similarity index 96%
rename from src/hooks/usePitchMapping.js
rename to src/features/fretboard/hooks/usePitchMapping.js
index de49cee..aa3c806 100644
--- a/src/hooks/usePitchMapping.js
+++ b/src/features/fretboard/hooks/usePitchMapping.js
@@ -1,5 +1,5 @@
import { useMemo, useCallback } from "react";
-import { buildNoteAliases, renderNoteName } from "@/lib/theory/notation";
+import { buildNoteAliases, renderNoteName } from "@domain/theory/notation";
function getDisplayAccidentals(accidental) {
if (accidental === "both") return ["sharp", "flat"];
diff --git a/src/features/fretboard/index.js b/src/features/fretboard/index.js
new file mode 100644
index 0000000..5a2ff2a
--- /dev/null
+++ b/src/features/fretboard/index.js
@@ -0,0 +1,3 @@
+export { default as Fretboard } from "./components/Fretboard";
+export { LABEL_OPTIONS, LABEL_VALUES, useLabels } from "./hooks/useLabels";
+export { usePitchMapping } from "./hooks/usePitchMapping";
diff --git a/src/components/Fretboard/renderFilters.js b/src/features/fretboard/model/renderFilters.js
similarity index 100%
rename from src/components/Fretboard/renderFilters.js
rename to src/features/fretboard/model/renderFilters.js
diff --git a/src/components/UI/controls/InstrumentControls.jsx b/src/features/instrument/components/InstrumentControls.jsx
similarity index 93%
rename from src/components/UI/controls/InstrumentControls.jsx
rename to src/features/instrument/components/InstrumentControls.jsx
index bdaa764..139ad0d 100644
--- a/src/components/UI/controls/InstrumentControls.jsx
+++ b/src/features/instrument/components/InstrumentControls.jsx
@@ -1,22 +1,22 @@
import clsx from "clsx";
-import Section from "@/components/UI/Section";
-import PresetPicker from "@/components/UI/combobox/PresetPicker";
+import Section from "@shared/ui/Section";
+import PresetPicker from "@features/instrument/components/PresetPicker";
import {
STR_MIN,
STR_MAX,
FRETS_MIN,
FRETS_MAX,
-} from "@/lib/config/appDefaults";
-import { withToastPromise } from "@/utils/toast";
-import { memoWithShallowPick } from "@/utils/memo";
-import NumberField from "@/components/UI/NumberField";
-import SegmentedRadioGroup from "@/components/UI/SegmentedRadioGroup";
-import { renderNoteName } from "@/lib/theory/notation";
-import { normalizeIntlNoteName } from "@/lib/theory/notation";
+} from "@shared/config/appDefaults";
+import { withToastPromise } from "@shared/lib/toast";
+import { memoWithShallowPick } from "@shared/lib/memo";
+import NumberField from "@shared/ui/NumberField";
+import SegmentedRadioGroup from "@shared/ui/SegmentedRadioGroup";
+import { renderNoteName } from "@domain/theory/notation";
+import { normalizeIntlNoteName } from "@domain/theory/notation";
import {
coerceNeckFilterMode,
getNeckFilterOptions,
-} from "@/lib/presets/neckFilterModes";
+} from "@domain/presets/neckFilterModes";
function InstrumentControls({ state, actions, meta }) {
const { strings, frets, tuning, systemId, selectedPreset, neckFilterMode } =
state;
diff --git a/src/components/UI/combobox/PresetPicker.jsx b/src/features/instrument/components/PresetPicker.jsx
similarity index 96%
rename from src/components/UI/combobox/PresetPicker.jsx
rename to src/features/instrument/components/PresetPicker.jsx
index a8ca515..788598a 100644
--- a/src/components/UI/combobox/PresetPicker.jsx
+++ b/src/features/instrument/components/PresetPicker.jsx
@@ -1,7 +1,7 @@
import { useCallback, useMemo } from "react";
import clsx from "clsx";
-import { normalizeStringList } from "@/utils/normalizeStringList";
-import BaseCombobox from "@/components/UI/combobox/BaseCombobox";
+import { normalizeStringList } from "@shared/lib/normalizeStringList";
+import BaseCombobox from "@shared/ui/BaseCombobox";
function toBadges({ name, customPresetSet, presetMetaMap }) {
const badges = [];
diff --git a/src/app/containers/InstrumentPanelContainer.jsx b/src/features/instrument/containers/InstrumentPanelContainer.jsx
similarity index 80%
rename from src/app/containers/InstrumentPanelContainer.jsx
rename to src/features/instrument/containers/InstrumentPanelContainer.jsx
index 838b7ee..f2cfc6f 100644
--- a/src/app/containers/InstrumentPanelContainer.jsx
+++ b/src/features/instrument/containers/InstrumentPanelContainer.jsx
@@ -1,8 +1,8 @@
import React from "react";
import { ErrorBoundary } from "react-error-boundary";
-import ErrorFallback from "@/components/UI/ErrorFallback";
-import InstrumentControls from "@/components/UI/controls/InstrumentControls";
+import ErrorFallback from "@shared/ui/ErrorFallback";
+import InstrumentControls from "@features/instrument/components/InstrumentControls";
export default function InstrumentPanelContainer({
state,
diff --git a/src/hooks/useCapo.js b/src/features/instrument/hooks/useCapo.js
similarity index 95%
rename from src/hooks/useCapo.js
rename to src/features/instrument/hooks/useCapo.js
index c285fea..39b26ff 100644
--- a/src/hooks/useCapo.js
+++ b/src/features/instrument/hooks/useCapo.js
@@ -1,6 +1,6 @@
import { useState, useMemo, useCallback } from "react";
-import { CAPO_DEFAULT } from "@/lib/config/appDefaults";
-import { toStringMetaMap } from "@/lib/meta/meta";
+import { CAPO_DEFAULT } from "@shared/config/appDefaults";
+import { toStringMetaMap } from "@domain/meta/meta";
/**
* Manages capo state and derives per-string metadata that respects the capo.
diff --git a/src/hooks/useCustomTuningPacks.js b/src/features/instrument/hooks/useCustomTuningPacks.js
similarity index 98%
rename from src/hooks/useCustomTuningPacks.js
rename to src/features/instrument/hooks/useCustomTuningPacks.js
index 79b3114..c838997 100644
--- a/src/hooks/useCustomTuningPacks.js
+++ b/src/features/instrument/hooks/useCustomTuningPacks.js
@@ -1,15 +1,15 @@
import { useCallback, useEffect } from "react";
import { useLatest, useMountedState } from "react-use";
import { useShallow } from "zustand/react/shallow";
-import { slug } from "@/lib/export/scales";
-import { withToastPromise } from "@/utils/toast";
+import { slug } from "@features/export";
+import { withToastPromise } from "@shared/lib/toast";
import {
useInstrumentWorkflowStore,
selectInstrumentWorkflowActions,
selectWorkflowEditorState,
selectWorkflowManagerOpen,
selectWorkflowPendingPresetName,
-} from "@/stores/useInstrumentWorkflowStore";
+} from "@features/instrument/store/useInstrumentWorkflowStore";
function resolvePackLabel(target) {
if (target && typeof target === "object") {
diff --git a/src/hooks/useDrawFrets.js b/src/features/instrument/hooks/useDrawFrets.js
similarity index 91%
rename from src/hooks/useDrawFrets.js
rename to src/features/instrument/hooks/useDrawFrets.js
index b1d88c3..bb56a9b 100644
--- a/src/hooks/useDrawFrets.js
+++ b/src/features/instrument/hooks/useDrawFrets.js
@@ -1,7 +1,7 @@
import { useMemo, useEffect } from "react";
import { usePrevious } from "react-use";
-import { FRETS_MIN, FRETS_MAX } from "@/lib/config/appDefaults";
-import { clamp } from "@/utils/math";
+import { FRETS_MIN, FRETS_MAX } from "@shared/config/appDefaults";
+import { clamp } from "@shared/lib/math";
/**
* Normalize drawn fret count across temperaments.
diff --git a/src/hooks/useInstrumentConfig.js b/src/features/instrument/hooks/useInstrumentConfig.js
similarity index 94%
rename from src/hooks/useInstrumentConfig.js
rename to src/features/instrument/hooks/useInstrumentConfig.js
index 3bef261..0c86f30 100644
--- a/src/hooks/useInstrumentConfig.js
+++ b/src/features/instrument/hooks/useInstrumentConfig.js
@@ -1,12 +1,12 @@
import { useCallback, useEffect, useMemo } from "react";
import { usePrevious } from "react-use";
import { useShallow } from "zustand/react/shallow";
-import { useDrawFrets } from "@/hooks/useDrawFrets";
-import { useCapo } from "@/hooks/useCapo";
-import { useStringsChange } from "@/hooks/useStringsChange";
-import { usePresetBuilder } from "@/hooks/usePresetBuilder";
-import { normalizePresetMeta } from "@/lib/meta/meta";
-import { isPlainObject } from "@/utils/object";
+import { useDrawFrets } from "@features/instrument/hooks/useDrawFrets";
+import { useCapo } from "@features/instrument/hooks/useCapo";
+import { useStringsChange } from "@features/instrument/hooks/useStringsChange";
+import { usePresetBuilder } from "@features/instrument/hooks/usePresetBuilder";
+import { normalizePresetMeta } from "@domain/meta/meta";
+import { isPlainObject } from "@shared/lib/object";
import {
useInstrumentCoreStore,
selectInstrumentCoreActions,
@@ -19,7 +19,7 @@ import {
selectInstrumentStringMeta,
selectInstrumentStrings,
selectInstrumentTuning,
-} from "@/stores/useInstrumentCoreStore";
+} from "@features/instrument/store/useInstrumentCoreStore";
const selectInstrumentConfigStore = (state) => ({
strings: selectInstrumentStrings(state),
diff --git a/src/app/hooks/useInstrumentDomain.js b/src/features/instrument/hooks/useInstrumentDomain.js
similarity index 91%
rename from src/app/hooks/useInstrumentDomain.js
rename to src/features/instrument/hooks/useInstrumentDomain.js
index 69a5521..6944357 100644
--- a/src/app/hooks/useInstrumentDomain.js
+++ b/src/features/instrument/hooks/useInstrumentDomain.js
@@ -1,15 +1,15 @@
import { useCallback, useMemo } from "react";
-import { buildInstrumentControlModel } from "@/app/adapters/controls";
-import { PANEL_CONTRACTS } from "@/app/contracts/panelContracts";
-import { useCustomTuningPacks } from "@/hooks/useCustomTuningPacks";
+import { buildInstrumentControlModel } from "@shared/lib/controlModels";
+import { PANEL_CONTRACTS } from "@shared/lib/panelContracts";
+import { useCustomTuningPacks } from "@features/instrument/hooks/useCustomTuningPacks";
import {
useInstrumentCapoSlice,
useInstrumentConfig,
useInstrumentFretsSlice,
-} from "@/hooks/useInstrumentConfig";
-import { useMergedPresets } from "@/hooks/useMergedPresets";
-import { useTuningIO } from "@/hooks/useTuningIO";
-import { buildInstrumentDomainReturn } from "@/app/hooks/domainReturnBuilders";
+} from "@features/instrument/hooks/useInstrumentConfig";
+import { useMergedPresets } from "@features/instrument/hooks/useMergedPresets";
+import { useTuningIO } from "@features/instrument/hooks/useTuningIO";
+import { buildInstrumentDomainReturn } from "@shared/lib/domainReturnBuilders";
export function useInstrumentDomain({
system,
diff --git a/src/hooks/useMergedPresets.js b/src/features/instrument/hooks/useMergedPresets.js
similarity index 96%
rename from src/hooks/useMergedPresets.js
rename to src/features/instrument/hooks/useMergedPresets.js
index ffae2e4..5b59d41 100644
--- a/src/hooks/useMergedPresets.js
+++ b/src/features/instrument/hooks/useMergedPresets.js
@@ -6,21 +6,21 @@ import {
useLatest,
useMountedState,
} from "react-use";
-import { normalizePresetMeta } from "@/lib/meta/meta";
+import { normalizePresetMeta } from "@domain/meta/meta";
import {
applyNeckFilterModeToBoardMeta,
NECK_FILTER_MODES,
resolvePresetNeckFilterMode,
resolveNeckFilterModeIntentFromBoardMeta,
-} from "@/lib/presets/neckFilterModes";
-import { isPlainObject } from "@/utils/object";
-import { coerceAnyTuning, usePresetBuilder } from "@/hooks/usePresetBuilder";
+} from "@domain/presets/neckFilterModes";
+import { isPlainObject } from "@shared/lib/object";
+import { coerceAnyTuning, usePresetBuilder } from "@features/instrument/hooks/usePresetBuilder";
import {
useInstrumentWorkflowStore,
selectInstrumentWorkflowActions,
selectWorkflowQueuedPresetName,
selectWorkflowSelectedPreset,
-} from "@/stores/useInstrumentWorkflowStore";
+} from "@features/instrument/store/useInstrumentWorkflowStore";
function areTuningsEqual(a, b) {
if (!Array.isArray(a) || !Array.isArray(b)) return false;
diff --git a/src/hooks/usePresetBuilder.js b/src/features/instrument/hooks/usePresetBuilder.js
similarity index 93%
rename from src/hooks/usePresetBuilder.js
rename to src/features/instrument/hooks/usePresetBuilder.js
index 4eb0e7e..48c75f1 100644
--- a/src/hooks/usePresetBuilder.js
+++ b/src/features/instrument/hooks/usePresetBuilder.js
@@ -1,8 +1,8 @@
import { useMemo } from "react";
-import { normalizePresetMeta } from "@/lib/meta/meta";
-import { normalizeIntlNoteName } from "@/lib/theory/notation";
-import { isGermanicSpellingMarker } from "@/lib/theory/notation";
-import { isPlainObject } from "@/utils/object";
+import { normalizePresetMeta } from "@domain/meta/meta";
+import { normalizeIntlNoteName } from "@domain/theory/notation";
+import { isGermanicSpellingMarker } from "@domain/theory/notation";
+import { isPlainObject } from "@shared/lib/object";
function isGermanicPresetSpelling(value) {
if (!isPlainObject(value)) return false;
diff --git a/src/hooks/useStringsChange.js b/src/features/instrument/hooks/useStringsChange.js
similarity index 100%
rename from src/hooks/useStringsChange.js
rename to src/features/instrument/hooks/useStringsChange.js
diff --git a/src/hooks/useTuningIO.js b/src/features/instrument/hooks/useTuningIO.js
similarity index 96%
rename from src/hooks/useTuningIO.js
rename to src/features/instrument/hooks/useTuningIO.js
index b0978e8..b27b889 100644
--- a/src/hooks/useTuningIO.js
+++ b/src/features/instrument/hooks/useTuningIO.js
@@ -1,27 +1,27 @@
import { useCallback, useEffect, useRef } from "react";
import { useLatest } from "react-use";
import { useShallow } from "zustand/react/shallow";
-import { ordinal } from "@/utils/ordinals";
+import { ordinal } from "@shared/lib/ordinals";
import * as v from "valibot";
import {
parseTuningPack,
stripVersionField,
TuningPackArraySchema,
-} from "@/lib/export/schema";
-import { buildTuningPack, downloadJsonFile } from "@/lib/export/tuningIO";
-import { withToastPromise } from "@/utils/toast";
-import { isPlainObject } from "@/utils/object";
+} from "@features/export";
+import { buildTuningPack, downloadJsonFile } from "@features/export";
+import { withToastPromise } from "@shared/lib/toast";
+import { isPlainObject } from "@shared/lib/object";
import {
ensurePackHasId,
normalizePackName,
removePackByIdentifier,
-} from "@/lib/export/tuningIO";
+} from "@features/export";
import {
useInstrumentWorkflowStore,
selectInstrumentWorkflowActions,
selectWorkflowCustomTunings,
-} from "@/stores/useInstrumentWorkflowStore";
-import { sanitizeBoardMetaForModeStorage } from "@/lib/presets/neckFilterModes";
+} from "@features/instrument/store/useInstrumentWorkflowStore";
+import { sanitizeBoardMetaForModeStorage } from "@domain/presets/neckFilterModes";
function ensureUniqueName(desiredName, takenNames) {
const base = normalizePackName(desiredName);
diff --git a/src/features/instrument/index.js b/src/features/instrument/index.js
new file mode 100644
index 0000000..d421e4e
--- /dev/null
+++ b/src/features/instrument/index.js
@@ -0,0 +1,2 @@
+export { default as InstrumentPanelContainer } from "./containers/InstrumentPanelContainer";
+export { useInstrumentDomain } from "./hooks/useInstrumentDomain";
diff --git a/src/stores/useInstrumentCoreStore.js b/src/features/instrument/store/useInstrumentCoreStore.js
similarity index 97%
rename from src/stores/useInstrumentCoreStore.js
rename to src/features/instrument/store/useInstrumentCoreStore.js
index 25c5e92..5d9c1c6 100644
--- a/src/stores/useInstrumentCoreStore.js
+++ b/src/features/instrument/store/useInstrumentCoreStore.js
@@ -9,15 +9,15 @@ import {
STR_MAX,
FRETS_MIN,
FRETS_MAX,
-} from "@/lib/config/appDefaults";
-import { STORAGE_KEYS } from "@/lib/storage/storageKeys";
-import { createScopedStorage } from "@/lib/storage/scopedStorage";
-import { clamp } from "@/utils/math";
-import { applyValueOrUpdaterOnDraft } from "@/utils/applyValueOrUpdaterOnDraft";
+} from "@shared/config/appDefaults";
+import { STORAGE_KEYS } from "@shared/lib/storage/storageKeys";
+import { createScopedStorage } from "@shared/lib/storage/scopedStorage";
+import { clamp } from "@shared/lib/math";
+import { applyValueOrUpdaterOnDraft } from "@shared/lib/applyValueOrUpdaterOnDraft";
import {
coerceNeckFilterMode,
NECK_FILTER_MODES,
-} from "@/lib/presets/neckFilterModes";
+} from "@domain/presets/neckFilterModes";
let lastSerializedGlobalDefaultTuningMap = null;
diff --git a/src/stores/useInstrumentWorkflowStore.js b/src/features/instrument/store/useInstrumentWorkflowStore.js
similarity index 95%
rename from src/stores/useInstrumentWorkflowStore.js
rename to src/features/instrument/store/useInstrumentWorkflowStore.js
index cf2fe08..cbe134e 100644
--- a/src/stores/useInstrumentWorkflowStore.js
+++ b/src/features/instrument/store/useInstrumentWorkflowStore.js
@@ -2,10 +2,10 @@ import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";
-import { STORAGE_KEYS } from "@/lib/storage/storageKeys";
-import { createGlobalStorage } from "@/lib/storage/scopedStorage";
-import { makeImmerSetters } from "@/utils/makeImmerSetters";
-import { applyValueOrUpdaterOnDraft } from "@/utils/applyValueOrUpdaterOnDraft";
+import { STORAGE_KEYS } from "@shared/lib/storage/storageKeys";
+import { createGlobalStorage } from "@shared/lib/storage/scopedStorage";
+import { makeImmerSetters } from "@shared/lib/makeImmerSetters";
+import { applyValueOrUpdaterOnDraft } from "@shared/lib/applyValueOrUpdaterOnDraft";
function readLegacyCustomTunings() {
if (typeof globalThis.localStorage === "undefined") return null;
diff --git a/src/components/UI/BeatIndicator.jsx b/src/features/practice/components/BeatIndicator.jsx
similarity index 100%
rename from src/components/UI/BeatIndicator.jsx
rename to src/features/practice/components/BeatIndicator.jsx
diff --git a/src/components/UI/controls/MetronomeControls.jsx b/src/features/practice/components/MetronomeControls.jsx
similarity index 97%
rename from src/components/UI/controls/MetronomeControls.jsx
rename to src/features/practice/components/MetronomeControls.jsx
index 303ad08..6e20b6c 100644
--- a/src/components/UI/controls/MetronomeControls.jsx
+++ b/src/features/practice/components/MetronomeControls.jsx
@@ -1,8 +1,8 @@
import clsx from "clsx";
-import Section from "@/components/UI/Section";
-import { memoWithShallowPick } from "@/utils/memo";
-import ToggleSwitch from "@/components/UI/ToggleSwitch";
-import NumberField from "@/components/UI/NumberField";
+import Section from "@shared/ui/Section";
+import { memoWithShallowPick } from "@shared/lib/memo";
+import ToggleSwitch from "@shared/ui/ToggleSwitch";
+import NumberField from "@shared/ui/NumberField";
const TIME_SIGNATURES = ["2/4", "3/4", "4/4", "5/4", "6/8", "7/8"];
const SUBDIVISIONS = ["Quarter", "Eighth", "Triplet", "Sixteenth"];
diff --git a/src/app/containers/PracticePanelContainer.jsx b/src/features/practice/containers/PracticePanelContainer.jsx
similarity index 81%
rename from src/app/containers/PracticePanelContainer.jsx
rename to src/features/practice/containers/PracticePanelContainer.jsx
index bcb3b30..dc3b3df 100644
--- a/src/app/containers/PracticePanelContainer.jsx
+++ b/src/features/practice/containers/PracticePanelContainer.jsx
@@ -1,8 +1,8 @@
import React from "react";
import { ErrorBoundary } from "react-error-boundary";
-import ErrorFallback from "@/components/UI/ErrorFallback";
-import MetronomeControls from "@/components/UI/controls/MetronomeControls";
+import ErrorFallback from "@shared/ui/ErrorFallback";
+import MetronomeControls from "@features/practice/components/MetronomeControls";
export default function PracticePanelContainer({
metronome,
diff --git a/src/app/containers/usePracticePanelState.js b/src/features/practice/containers/usePracticePanelState.js
similarity index 96%
rename from src/app/containers/usePracticePanelState.js
rename to src/features/practice/containers/usePracticePanelState.js
index 05cba71..e02d0d2 100644
--- a/src/app/containers/usePracticePanelState.js
+++ b/src/features/practice/containers/usePracticePanelState.js
@@ -2,12 +2,12 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "react-hot-toast";
import { useShallow } from "zustand/react/shallow";
-import { usePracticeActions } from "@/hooks/usePracticeActions";
-import { useMetronomePlayback } from "@/hooks/useMetronomeEngine";
+import { usePracticeActions } from "@features/practice/hooks/usePracticeActions";
+import { useMetronomePlayback } from "@features/practice/hooks/useMetronomeEngine";
import {
useRandomScale,
formatRandomizedScaleAnnouncement,
-} from "@/hooks/useRandomScale";
+} from "@features/theory";
import {
useMetronomePrefsStore,
selectMetronomeHydrateWithDefaults,
@@ -16,8 +16,8 @@ import {
selectMetronomeSetPrefs,
selectMetronomeSetRandomizeMode,
selectMetronomeSetters,
-} from "@/stores/useMetronomePrefsStore";
-import { useMetronomeEngineStore } from "@/stores/useMetronomeEngineStore";
+} from "@features/practice/store/useMetronomePrefsStore";
+import { useMetronomeEngineStore } from "@features/practice/store/useMetronomeEngineStore";
export default function usePracticePanelState({
metronomeDefaults,
diff --git a/src/hooks/useMetronomeEngine.js b/src/features/practice/hooks/useMetronomeEngine.js
similarity index 99%
rename from src/hooks/useMetronomeEngine.js
rename to src/features/practice/hooks/useMetronomeEngine.js
index 5012c57..ed59afd 100644
--- a/src/hooks/useMetronomeEngine.js
+++ b/src/features/practice/hooks/useMetronomeEngine.js
@@ -5,7 +5,7 @@ import {
selectMetronomeEngineActions,
selectMetronomeEnginePlaybackState,
selectMetronomeEngineCursorState,
-} from "@/stores/useMetronomeEngineStore";
+} from "@features/practice/store/useMetronomeEngineStore";
const LOOKAHEAD_MS = 25;
const SCHEDULE_AHEAD_SEC = 0.1;
diff --git a/src/hooks/usePracticeActions.js b/src/features/practice/hooks/usePracticeActions.js
similarity index 100%
rename from src/hooks/usePracticeActions.js
rename to src/features/practice/hooks/usePracticeActions.js
diff --git a/src/app/hooks/usePracticeMetronomeDomain.js b/src/features/practice/hooks/usePracticeMetronomeDomain.js
similarity index 86%
rename from src/app/hooks/usePracticeMetronomeDomain.js
rename to src/features/practice/hooks/usePracticeMetronomeDomain.js
index 45db716..60bdd1e 100644
--- a/src/app/hooks/usePracticeMetronomeDomain.js
+++ b/src/features/practice/hooks/usePracticeMetronomeDomain.js
@@ -1,8 +1,8 @@
import { useMemo } from "react";
-import { buildMetronomeControlModel } from "@/app/adapters/controls";
-import { PANEL_CONTRACTS } from "@/app/contracts/panelContracts";
-import usePracticePanelState from "@/app/containers/usePracticePanelState";
-import { buildPracticeMetronomeDomainReturn } from "@/app/hooks/domainReturnBuilders";
+import { buildMetronomeControlModel } from "@shared/lib/controlModels";
+import { PANEL_CONTRACTS } from "@shared/lib/panelContracts";
+import usePracticePanelState from "@features/practice/containers/usePracticePanelState";
+import { buildPracticeMetronomeDomainReturn } from "@shared/lib/domainReturnBuilders";
export function usePracticeMetronomeDomain({
metronomeDefaults,
diff --git a/src/features/practice/index.js b/src/features/practice/index.js
new file mode 100644
index 0000000..4357ee0
--- /dev/null
+++ b/src/features/practice/index.js
@@ -0,0 +1,17 @@
+export { default as BeatIndicator } from "./components/BeatIndicator";
+export { default as PracticePanelContainer } from "./containers/PracticePanelContainer";
+export { usePracticeMetronomeDomain } from "./hooks/usePracticeMetronomeDomain";
+export {
+ useMetronomePlayback,
+ useMetronomePlaybackStatus,
+ useMetronomeTickCursor,
+} from "./hooks/useMetronomeEngine";
+export {
+ selectMetronomeHydrateWithDefaults,
+ selectMetronomePrefs,
+ selectMetronomeRandomizeMode,
+ selectMetronomeSetPrefs,
+ selectMetronomeSetRandomizeMode,
+ selectMetronomeSetters,
+ useMetronomePrefsStore,
+} from "./store/useMetronomePrefsStore";
diff --git a/src/stores/useMetronomeEngineStore.js b/src/features/practice/store/useMetronomeEngineStore.js
similarity index 100%
rename from src/stores/useMetronomeEngineStore.js
rename to src/features/practice/store/useMetronomeEngineStore.js
diff --git a/src/stores/useMetronomePrefsStore.js b/src/features/practice/store/useMetronomePrefsStore.js
similarity index 93%
rename from src/stores/useMetronomePrefsStore.js
rename to src/features/practice/store/useMetronomePrefsStore.js
index 73ee342..812f5be 100644
--- a/src/stores/useMetronomePrefsStore.js
+++ b/src/features/practice/store/useMetronomePrefsStore.js
@@ -2,11 +2,11 @@ import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";
import { immer } from "zustand/middleware/immer";
-import { METRONOME_DEFAULTS } from "@/lib/config/appDefaults";
-import { STORAGE_KEYS } from "@/lib/storage/storageKeys";
-import { createGlobalStorage } from "@/lib/storage/scopedStorage";
-import { makeImmerSetters } from "@/utils/makeImmerSetters";
-import { applyValueOrUpdaterOnDraft } from "@/utils/applyValueOrUpdaterOnDraft";
+import { METRONOME_DEFAULTS } from "@shared/config/appDefaults";
+import { STORAGE_KEYS } from "@shared/lib/storage/storageKeys";
+import { createGlobalStorage } from "@shared/lib/storage/scopedStorage";
+import { makeImmerSetters } from "@shared/lib/makeImmerSetters";
+import { applyValueOrUpdaterOnDraft } from "@shared/lib/applyValueOrUpdaterOnDraft";
const SETTER_KEYS = [
"bpm",
diff --git a/src/components/UI/modals/ShareConfigModal.jsx b/src/features/share/components/ShareConfigModal.jsx
similarity index 95%
rename from src/components/UI/modals/ShareConfigModal.jsx
rename to src/features/share/components/ShareConfigModal.jsx
index 45a940b..502d0ff 100644
--- a/src/components/UI/modals/ShareConfigModal.jsx
+++ b/src/features/share/components/ShareConfigModal.jsx
@@ -1,8 +1,8 @@
import { toast } from "react-hot-toast";
-import ModalFrame from "@/components/UI/modals/ModalFrame";
-import { buildShareConfigModalModel } from "@/components/UI/modals/shareConfigModalModel";
-import ShareQrCode from "@/components/UI/qr/ShareQrCode";
+import ModalFrame from "@shared/ui/ModalFrame";
+import { buildShareConfigModalModel } from "@features/share/model/shareConfigModalModel";
+import ShareQrCode from "@features/share/components/ShareQrCode";
async function copyTextWithFallback(text) {
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
diff --git a/src/components/UI/qr/ShareQrCode.jsx b/src/features/share/components/ShareQrCode.jsx
similarity index 100%
rename from src/components/UI/qr/ShareQrCode.jsx
rename to src/features/share/components/ShareQrCode.jsx
diff --git a/src/app/hooks/useUrlShareHydration.js b/src/features/share/hooks/useUrlShareHydration.js
similarity index 99%
rename from src/app/hooks/useUrlShareHydration.js
rename to src/features/share/hooks/useUrlShareHydration.js
index d98e7a4..22245f5 100644
--- a/src/app/hooks/useUrlShareHydration.js
+++ b/src/features/share/hooks/useUrlShareHydration.js
@@ -4,7 +4,7 @@ import { toast } from "react-hot-toast";
import {
parseSharePayload,
resolveInstrumentHydrationValues,
-} from "@/lib/url/shareCodec";
+} from "@features/share/model/shareCodec";
function hasAnyValues(payload) {
return !!(
diff --git a/src/features/share/index.js b/src/features/share/index.js
new file mode 100644
index 0000000..2a3ea2c
--- /dev/null
+++ b/src/features/share/index.js
@@ -0,0 +1,3 @@
+export { default as ShareConfigModal } from "./components/ShareConfigModal";
+export { useUrlShareHydration } from "./hooks/useUrlShareHydration";
+export { buildRawShareState } from "./model/shareState";
diff --git a/src/lib/url/shareCodec.ts b/src/features/share/model/shareCodec.ts
similarity index 97%
rename from src/lib/url/shareCodec.ts
rename to src/features/share/model/shareCodec.ts
index f25ab79..a8ba793 100644
--- a/src/lib/url/shareCodec.ts
+++ b/src/features/share/model/shareCodec.ts
@@ -6,15 +6,15 @@ import {
STR_MAX,
STR_MIN,
SYSTEM_DEFAULT,
-} from "@/lib/config/appDefaults";
-import { TUNINGS } from "@/lib/theory/tuning";
-import { SHARE_FIELD_SELECTORS } from "@/lib/url/shareScopes";
-import { SHARE_QUERY_KEYS, SHARE_SCHEMA_VERSION } from "@/lib/url/shareSchema";
+} from "@shared/config/appDefaults";
+import { TUNINGS } from "@domain/theory/tuning";
+import { SHARE_FIELD_SELECTORS } from "@features/share/model/shareScopes";
+import { SHARE_QUERY_KEYS, SHARE_SCHEMA_VERSION } from "@features/share/model/shareSchema";
import {
coerceNeckFilterMode,
isNeckFilterMode,
NECK_FILTER_MODES,
-} from "@/lib/presets/neckFilterModes";
+} from "@domain/presets/neckFilterModes";
type ShareValues = Partial<{
systemId: string;
diff --git a/src/components/UI/modals/shareConfigModalModel.ts b/src/features/share/model/shareConfigModalModel.ts
similarity index 76%
rename from src/components/UI/modals/shareConfigModalModel.ts
rename to src/features/share/model/shareConfigModalModel.ts
index ba3c437..5bf242e 100644
--- a/src/components/UI/modals/shareConfigModalModel.ts
+++ b/src/features/share/model/shareConfigModalModel.ts
@@ -1,6 +1,6 @@
-import { serializeShareState } from "@/app/adapters/shareState";
-import { buildSharePayload, serializeSharePayload } from "@/lib/url/shareCodec";
-import { evaluateShareUrlSize } from "@/lib/url/shareLimits";
+import { serializeShareState } from "@features/share/model/shareState";
+import { buildSharePayload, serializeSharePayload } from "@features/share/model/shareCodec";
+import { evaluateShareUrlSize } from "@features/share/model/shareLimits";
export function buildShareConfigModalModel({
isOpen,
diff --git a/src/lib/url/shareLimits.ts b/src/features/share/model/shareLimits.ts
similarity index 100%
rename from src/lib/url/shareLimits.ts
rename to src/features/share/model/shareLimits.ts
diff --git a/src/lib/url/shareSchema.ts b/src/features/share/model/shareSchema.ts
similarity index 100%
rename from src/lib/url/shareSchema.ts
rename to src/features/share/model/shareSchema.ts
diff --git a/src/lib/url/shareScopes.ts b/src/features/share/model/shareScopes.ts
similarity index 100%
rename from src/lib/url/shareScopes.ts
rename to src/features/share/model/shareScopes.ts
diff --git a/src/app/adapters/shareState.js b/src/features/share/model/shareState.js
similarity index 98%
rename from src/app/adapters/shareState.js
rename to src/features/share/model/shareState.js
index f7c221e..d29cfd7 100644
--- a/src/app/adapters/shareState.js
+++ b/src/features/share/model/shareState.js
@@ -1,7 +1,7 @@
import {
coerceNeckFilterMode,
NECK_FILTER_MODES,
-} from "@/lib/presets/neckFilterModes";
+} from "@domain/presets/neckFilterModes";
function toPlainSerializable(value) {
if (value === null) return null;
diff --git a/src/components/UI/controls/ChordControls.jsx b/src/features/theory/components/ChordControls.jsx
similarity index 96%
rename from src/components/UI/controls/ChordControls.jsx
rename to src/features/theory/components/ChordControls.jsx
index 079afef..a5cb5bf 100644
--- a/src/components/UI/controls/ChordControls.jsx
+++ b/src/features/theory/components/ChordControls.jsx
@@ -1,23 +1,23 @@
import { memo, useId, useMemo } from "react";
import clsx from "clsx";
-import Section from "@/components/UI/Section";
+import Section from "@shared/ui/Section";
import {
CHORD_TYPES,
CHORD_LABELS,
STANDARD_CHORD_TYPES,
-} from "@/lib/theory/chords";
+} from "@domain/theory/chords";
import { FiRotateCcw } from "react-icons/fi";
import {
arrayRefAndLengthEqual,
objectRefAndKeyEqual,
setRefAndSizeEqual,
-} from "@/utils/memo";
-import { useScaleAndChord } from "@/hooks/useScaleAndChord";
-import { ROOT_DEFAULT, CHORD_DEFAULT } from "@/lib/config/appDefaults";
-import ChordTypePicker from "@/components/UI/combobox/ChordTypePicker";
-import SegmentedRadioGroup from "@/components/UI/SegmentedRadioGroup";
-import ToggleSwitch from "@/components/UI/ToggleSwitch";
-import { buildCapoChordDisplay } from "@/components/UI/controls/chordCapoDisplay";
+} from "@shared/lib/memo";
+import { useScaleAndChord } from "@features/theory/hooks/useScaleAndChord";
+import { ROOT_DEFAULT, CHORD_DEFAULT } from "@shared/config/appDefaults";
+import ChordTypePicker from "@features/theory/components/ChordTypePicker";
+import SegmentedRadioGroup from "@shared/ui/SegmentedRadioGroup";
+import ToggleSwitch from "@shared/ui/ToggleSwitch";
+import { buildCapoChordDisplay } from "@features/theory/model/chordCapoDisplay";
function ChordControls({ state, actions, meta }) {
const {
diff --git a/src/components/UI/combobox/ChordTypePicker.jsx b/src/features/theory/components/ChordTypePicker.jsx
similarity index 96%
rename from src/components/UI/combobox/ChordTypePicker.jsx
rename to src/features/theory/components/ChordTypePicker.jsx
index 8839cc0..2ed7ba1 100644
--- a/src/components/UI/combobox/ChordTypePicker.jsx
+++ b/src/features/theory/components/ChordTypePicker.jsx
@@ -1,8 +1,8 @@
import { Fragment, useCallback, useMemo } from "react";
import clsx from "clsx";
-import { CHORD_LABELS, isMicrotonalChordType } from "@/lib/theory/chords";
-import { normalizeStringList } from "@/utils/normalizeStringList";
-import BaseCombobox from "@/components/UI/combobox/BaseCombobox";
+import { CHORD_LABELS, isMicrotonalChordType } from "@domain/theory/chords";
+import { normalizeStringList } from "@shared/lib/normalizeStringList";
+import BaseCombobox from "@shared/ui/BaseCombobox";
const SECTION_LABELS = {
standard: "Standard triads & sevenths",
diff --git a/src/components/UI/controls/ScaleControls.jsx b/src/features/theory/components/ScaleControls.jsx
similarity index 94%
rename from src/components/UI/controls/ScaleControls.jsx
rename to src/features/theory/components/ScaleControls.jsx
index c01f4b8..28cf4a9 100644
--- a/src/components/UI/controls/ScaleControls.jsx
+++ b/src/features/theory/components/ScaleControls.jsx
@@ -1,11 +1,11 @@
import { useId, useMemo } from "react";
import clsx from "clsx";
import { FiShuffle, FiRotateCcw } from "react-icons/fi";
-import Section from "@/components/UI/Section";
-import { memoWithKeys } from "@/utils/memo";
-import ScalePicker from "@/components/UI/combobox/ScalePicker";
-import { RANDOMIZE_MODES } from "@/hooks/useRandomScale";
-import SegmentedRadioGroup from "@/components/UI/SegmentedRadioGroup";
+import Section from "@shared/ui/Section";
+import { memoWithKeys } from "@shared/lib/memo";
+import ScalePicker from "@features/theory/components/ScalePicker";
+import { RANDOMIZE_MODES } from "@features/theory/hooks/useRandomScale";
+import SegmentedRadioGroup from "@shared/ui/SegmentedRadioGroup";
function ScaleControls({ state, actions, meta }) {
const {
diff --git a/src/components/UI/combobox/ScalePicker.jsx b/src/features/theory/components/ScalePicker.jsx
similarity index 98%
rename from src/components/UI/combobox/ScalePicker.jsx
rename to src/features/theory/components/ScalePicker.jsx
index bfbde97..983c620 100644
--- a/src/components/UI/combobox/ScalePicker.jsx
+++ b/src/features/theory/components/ScalePicker.jsx
@@ -1,4 +1,4 @@
-import BaseCombobox from "@/components/UI/combobox/BaseCombobox";
+import BaseCombobox from "@shared/ui/BaseCombobox";
import clsx from "clsx";
import { useCallback } from "react";
diff --git a/src/app/containers/TheoryPanelContainer.jsx b/src/features/theory/containers/TheoryPanelContainer.jsx
similarity index 91%
rename from src/app/containers/TheoryPanelContainer.jsx
rename to src/features/theory/containers/TheoryPanelContainer.jsx
index b5b73f2..e758392 100644
--- a/src/app/containers/TheoryPanelContainer.jsx
+++ b/src/features/theory/containers/TheoryPanelContainer.jsx
@@ -1,9 +1,9 @@
import { ErrorBoundary } from "react-error-boundary";
-import ErrorFallback from "@/components/UI/ErrorFallback";
-import ScaleControls from "@/components/UI/controls/ScaleControls";
-import ChordControls from "@/components/UI/controls/ChordControls";
-import { buildChordFit } from "@/app/containers/theoryPanelModel";
+import ErrorFallback from "@shared/ui/ErrorFallback";
+import ScaleControls from "@features/theory/components/ScaleControls";
+import ChordControls from "@features/theory/components/ChordControls";
+import { buildChordFit } from "@features/theory/model/theoryPanelModel";
export default function TheoryPanelContainer({ controlModel, reset }) {
const state = controlModel?.state ?? {};
diff --git a/src/hooks/resetMusicalState.js b/src/features/theory/hooks/resetMusicalState.js
similarity index 95%
rename from src/hooks/resetMusicalState.js
rename to src/features/theory/hooks/resetMusicalState.js
index c47b1f8..ddb8ea2 100644
--- a/src/hooks/resetMusicalState.js
+++ b/src/features/theory/hooks/resetMusicalState.js
@@ -2,7 +2,7 @@ import {
ROOT_DEFAULT,
SCALE_DEFAULT,
CHORD_DEFAULT,
-} from "@/lib/config/appDefaults";
+} from "@shared/config/appDefaults";
export function resetMusicalStateFromRefs(current) {
// Dependency note: stop/reset metronome before changing musical state
diff --git a/src/hooks/useAccidentalRespell.js b/src/features/theory/hooks/useAccidentalRespell.js
similarity index 100%
rename from src/hooks/useAccidentalRespell.js
rename to src/features/theory/hooks/useAccidentalRespell.js
diff --git a/src/hooks/useRandomScale.js b/src/features/theory/hooks/useRandomScale.js
similarity index 93%
rename from src/hooks/useRandomScale.js
rename to src/features/theory/hooks/useRandomScale.js
index fa4c0d9..392faa0 100644
--- a/src/hooks/useRandomScale.js
+++ b/src/features/theory/hooks/useRandomScale.js
@@ -1,6 +1,6 @@
import { useCallback } from "react";
-import { pickRandomScale } from "@/utils/random";
-import { useThrottledTrigger } from "@/hooks/useThrottledTrigger";
+import { pickRandomScale } from "@shared/lib/random";
+import { useThrottledTrigger } from "@shared/hooks/useThrottledTrigger";
export const RANDOMIZE_MODES = {
Both: "both",
diff --git a/src/hooks/useScaleAndChord.js b/src/features/theory/hooks/useScaleAndChord.js
similarity index 100%
rename from src/hooks/useScaleAndChord.js
rename to src/features/theory/hooks/useScaleAndChord.js
diff --git a/src/hooks/useSystemNoteNames.js b/src/features/theory/hooks/useSystemNoteNames.js
similarity index 90%
rename from src/hooks/useSystemNoteNames.js
rename to src/features/theory/hooks/useSystemNoteNames.js
index 7e080cc..052af9a 100644
--- a/src/hooks/useSystemNoteNames.js
+++ b/src/features/theory/hooks/useSystemNoteNames.js
@@ -1,5 +1,5 @@
import { useMemo } from "react";
-import { usePitchMapping } from "@/hooks/usePitchMapping";
+import { usePitchMapping } from "@features/fretboard";
/**
* Provides pc<->name mapping & the system's full name list.
diff --git a/src/app/hooks/useTheoryDomain.js b/src/features/theory/hooks/useTheoryDomain.js
similarity index 94%
rename from src/app/hooks/useTheoryDomain.js
rename to src/features/theory/hooks/useTheoryDomain.js
index 2f8e470..de6233e 100644
--- a/src/app/hooks/useTheoryDomain.js
+++ b/src/features/theory/hooks/useTheoryDomain.js
@@ -1,16 +1,16 @@
import { useCallback, useEffect, useMemo } from "react";
import { useShallow } from "zustand/react/shallow";
-import { buildBaselineScalesForSystem } from "@/lib/theory/scales";
+import { buildBaselineScalesForSystem } from "@domain/theory/scales";
import {
buildChordPCsFromPc,
isMicrotonalChordType,
-} from "@/lib/theory/chords";
-import { CHORD_DEFAULT, ROOT_DEFAULT } from "@/lib/config/appDefaults";
-import { resolveCapoRelativeChordRootPc } from "@/lib/theory/capoChords";
+} from "@domain/theory/chords";
+import { CHORD_DEFAULT, ROOT_DEFAULT } from "@shared/config/appDefaults";
+import { resolveCapoRelativeChordRootPc } from "@domain/theory/capoChords";
-import { useSystemNoteNames } from "@/hooks/useSystemNoteNames";
-import { buildTheoryDomainReturn } from "@/app/hooks/domainReturnBuilders";
+import { useSystemNoteNames } from "@features/theory/hooks/useSystemNoteNames";
+import { buildTheoryDomainReturn } from "@shared/lib/domainReturnBuilders";
import {
useTheoryStore,
selectTheoryActions,
@@ -23,7 +23,7 @@ import {
selectTheoryScale,
selectTheoryShowChord,
selectTheorySystemId,
-} from "@/stores/useTheoryStore";
+} from "@features/theory/store/useTheoryStore";
const selectTheoryDomainStore = (state) => ({
systemId: selectTheorySystemId(state),
diff --git a/src/features/theory/index.js b/src/features/theory/index.js
new file mode 100644
index 0000000..2dec000
--- /dev/null
+++ b/src/features/theory/index.js
@@ -0,0 +1,10 @@
+export { default as TheoryPanelContainer } from "./containers/TheoryPanelContainer";
+export { useTheoryDomain } from "./hooks/useTheoryDomain";
+export { useScaleAndChord } from "./hooks/useScaleAndChord";
+export {
+ formatRandomizedScaleAnnouncement,
+ RANDOMIZE_MODES,
+ useRandomScale,
+} from "./hooks/useRandomScale";
+export { useSystemNoteNames } from "./hooks/useSystemNoteNames";
+export { useAccidentalRespell } from "./hooks/useAccidentalRespell";
diff --git a/src/components/UI/controls/chordCapoDisplay.js b/src/features/theory/model/chordCapoDisplay.js
similarity index 100%
rename from src/components/UI/controls/chordCapoDisplay.js
rename to src/features/theory/model/chordCapoDisplay.js
diff --git a/src/app/containers/theoryPanelModel.js b/src/features/theory/model/theoryPanelModel.js
similarity index 100%
rename from src/app/containers/theoryPanelModel.js
rename to src/features/theory/model/theoryPanelModel.js
diff --git a/src/stores/useTheoryStore.js b/src/features/theory/store/useTheoryStore.js
similarity index 96%
rename from src/stores/useTheoryStore.js
rename to src/features/theory/store/useTheoryStore.js
index ffc1c41..686f519 100644
--- a/src/stores/useTheoryStore.js
+++ b/src/features/theory/store/useTheoryStore.js
@@ -7,10 +7,10 @@ import {
ROOT_DEFAULT,
SCALE_DEFAULT,
SYSTEM_DEFAULT,
-} from "@/lib/config/appDefaults";
-import { STORAGE_KEYS } from "@/lib/storage/storageKeys";
-import { createGlobalStorage } from "@/lib/storage/scopedStorage";
-import { makeImmerSetters } from "@/utils/makeImmerSetters";
+} from "@shared/config/appDefaults";
+import { STORAGE_KEYS } from "@shared/lib/storage/storageKeys";
+import { createGlobalStorage } from "@shared/lib/storage/scopedStorage";
+import { makeImmerSetters } from "@shared/lib/makeImmerSetters";
function isNonEmptyString(value) {
return typeof value === "string" && value.trim().length > 0;
diff --git a/src/lib/config/appDefaults.ts b/src/shared/config/appDefaults.ts
similarity index 97%
rename from src/lib/config/appDefaults.ts
rename to src/shared/config/appDefaults.ts
index 49ae0ec..fb016a2 100644
--- a/src/lib/config/appDefaults.ts
+++ b/src/shared/config/appDefaults.ts
@@ -1,4 +1,4 @@
-import { clamp } from "@/utils/math";
+import { clamp } from "@shared/lib/math";
/* =========================
Instrument bounds & factories
diff --git a/src/hooks/hotkeyHandler.js b/src/shared/hooks/hotkeyHandler.js
similarity index 92%
rename from src/hooks/hotkeyHandler.js
rename to src/shared/hooks/hotkeyHandler.js
index eec5f03..9f95e50 100644
--- a/src/hooks/hotkeyHandler.js
+++ b/src/shared/hooks/hotkeyHandler.js
@@ -1,5 +1,5 @@
import { isHotkey } from "is-hotkey";
-import { isTypingTarget, toHotkeyCombo } from "@/hooks/hotkeyUtils";
+import { isTypingTarget, toHotkeyCombo } from "@shared/hooks/hotkeyUtils";
function isDialogInteractionTarget(target) {
if (!target || typeof target.closest !== "function") return false;
diff --git a/src/hooks/hotkeyUtils.js b/src/shared/hooks/hotkeyUtils.js
similarity index 100%
rename from src/hooks/hotkeyUtils.js
rename to src/shared/hooks/hotkeyUtils.js
diff --git a/src/hooks/hotkeys.types.d.ts b/src/shared/hooks/hotkeys.types.d.ts
similarity index 100%
rename from src/hooks/hotkeys.types.d.ts
rename to src/shared/hooks/hotkeys.types.d.ts
diff --git a/src/hooks/hotkeysTable.js b/src/shared/hooks/hotkeysTable.js
similarity index 97%
rename from src/hooks/hotkeysTable.js
rename to src/shared/hooks/hotkeysTable.js
index c24d66b..4c29748 100644
--- a/src/hooks/hotkeysTable.js
+++ b/src/shared/hooks/hotkeysTable.js
@@ -1,7 +1,7 @@
-import { clamp } from "@/utils/math";
-import { DOT_SIZE_DEFAULT } from "@/lib/config/appDefaults";
+import { clamp } from "@shared/lib/math";
+import { DOT_SIZE_DEFAULT } from "@shared/config/appDefaults";
-/** @typedef {import("@/hooks/hotkeys.types").HotkeysLiveRef} HotkeysLiveRef */
+/** @typedef {import("@shared/hooks/hotkeys.types").HotkeysLiveRef} HotkeysLiveRef */
/** @param {HotkeysLiveRef} liveRef */
export function buildShortcutTableFromRefs(liveRef) {
diff --git a/src/hooks/useCombobox.js b/src/shared/hooks/useCombobox.js
similarity index 100%
rename from src/hooks/useCombobox.js
rename to src/shared/hooks/useCombobox.js
diff --git a/src/hooks/useConfirm.js b/src/shared/hooks/useConfirm.js
similarity index 95%
rename from src/hooks/useConfirm.js
rename to src/shared/hooks/useConfirm.js
index 312a874..83d9d11 100644
--- a/src/hooks/useConfirm.js
+++ b/src/shared/hooks/useConfirm.js
@@ -1,6 +1,6 @@
import { createElement } from "react";
import { toast } from "react-hot-toast";
-import ConfirmDialog from "@/components/UI/ConfirmDialog";
+import ConfirmDialog from "@shared/ui/ConfirmDialog";
export function useConfirm() {
function confirm({
diff --git a/src/hooks/useFilteredOptions.js b/src/shared/hooks/useFilteredOptions.js
similarity index 100%
rename from src/hooks/useFilteredOptions.js
rename to src/shared/hooks/useFilteredOptions.js
diff --git a/src/hooks/useHotkeys.js b/src/shared/hooks/useHotkeys.js
similarity index 87%
rename from src/hooks/useHotkeys.js
rename to src/shared/hooks/useHotkeys.js
index de15424..e20b0aa 100644
--- a/src/hooks/useHotkeys.js
+++ b/src/shared/hooks/useHotkeys.js
@@ -7,12 +7,12 @@ import {
STR_MIN,
DOT_SIZE_MAX,
DOT_SIZE_MIN,
-} from "@/lib/config/appDefaults";
-import { createShortcutHandler } from "@/hooks/hotkeyHandler";
-import { buildShortcutTableFromRefs } from "@/hooks/hotkeysTable";
+} from "@shared/config/appDefaults";
+import { createShortcutHandler } from "@shared/hooks/hotkeyHandler";
+import { buildShortcutTableFromRefs } from "@shared/hooks/hotkeysTable";
-/** @typedef {import("@/hooks/hotkeys.types").HotkeysLiveState} HotkeysLiveState */
-/** @typedef {import("@/hooks/hotkeys.types").HotkeysLiveRef} HotkeysLiveRef */
+/** @typedef {import("@shared/hooks/hotkeys.types").HotkeysLiveState} HotkeysLiveState */
+/** @typedef {import("@shared/hooks/hotkeys.types").HotkeysLiveRef} HotkeysLiveRef */
export function useHotkeys(options) {
const {
diff --git a/src/hooks/useNumberField.js b/src/shared/hooks/useNumberField.js
similarity index 97%
rename from src/hooks/useNumberField.js
rename to src/shared/hooks/useNumberField.js
index 615a239..bd3680d 100644
--- a/src/hooks/useNumberField.js
+++ b/src/shared/hooks/useNumberField.js
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useState } from "react";
-import { clamp } from "@/utils/math";
+import { clamp } from "@shared/lib/math";
export function commitNumberField({
rawOverride,
diff --git a/src/hooks/useResets.js b/src/shared/hooks/useResets.js
similarity index 94%
rename from src/hooks/useResets.js
rename to src/shared/hooks/useResets.js
index 36f4f39..f1ec3c7 100644
--- a/src/hooks/useResets.js
+++ b/src/shared/hooks/useResets.js
@@ -5,10 +5,10 @@ import {
DISPLAY_DEFAULTS,
SYSTEM_DEFAULT,
getFactoryFrets,
-} from "@/lib/config/appDefaults";
+} from "@shared/config/appDefaults";
import { useLatest } from "react-use";
-import { resetAllStores } from "@/stores/resetAllStores";
-import { resetMusicalStateFromRefs } from "@/hooks/resetMusicalState";
+import { resetAllStores } from "@shared/lib/resetAllStores";
+import { resetMusicalStateFromRefs } from "@features/theory/hooks/resetMusicalState";
export function useResets({
system,
diff --git a/src/hooks/useThrottledTrigger.js b/src/shared/hooks/useThrottledTrigger.js
similarity index 100%
rename from src/hooks/useThrottledTrigger.js
rename to src/shared/hooks/useThrottledTrigger.js
diff --git a/src/hooks/validatedStorageUtils.js b/src/shared/hooks/validatedStorageUtils.js
similarity index 100%
rename from src/hooks/validatedStorageUtils.js
rename to src/shared/hooks/validatedStorageUtils.js
diff --git a/src/utils/applyValueOrUpdaterOnDraft.ts b/src/shared/lib/applyValueOrUpdaterOnDraft.ts
similarity index 100%
rename from src/utils/applyValueOrUpdaterOnDraft.ts
rename to src/shared/lib/applyValueOrUpdaterOnDraft.ts
diff --git a/src/app/adapters/controls.js b/src/shared/lib/controlModels.js
similarity index 99%
rename from src/app/adapters/controls.js
rename to src/shared/lib/controlModels.js
index 4d95e9f..7d85553 100644
--- a/src/app/adapters/controls.js
+++ b/src/shared/lib/controlModels.js
@@ -2,7 +2,7 @@ import {
getEffectiveCapoPitchOffset,
transposeCapoRelativeChordRootPc,
transposePitchClassSet,
-} from "@/lib/theory/capoChords";
+} from "@domain/theory/capoChords";
const METRONOME_TIME_SIGNATURES = ["2/4", "3/4", "4/4", "5/4", "6/8", "7/8"];
const METRONOME_SUBDIVISIONS = ["Quarter", "Eighth", "Triplet", "Sixteenth"];
diff --git a/src/utils/degreeColors.ts b/src/shared/lib/degreeColors.ts
similarity index 100%
rename from src/utils/degreeColors.ts
rename to src/shared/lib/degreeColors.ts
diff --git a/src/app/hooks/domainReturnBuilders.js b/src/shared/lib/domainReturnBuilders.js
similarity index 100%
rename from src/app/hooks/domainReturnBuilders.js
rename to src/shared/lib/domainReturnBuilders.js
diff --git a/src/utils/fretLabels.ts b/src/shared/lib/fretLabels.ts
similarity index 100%
rename from src/utils/fretLabels.ts
rename to src/shared/lib/fretLabels.ts
diff --git a/src/utils/makeImmerSetters.ts b/src/shared/lib/makeImmerSetters.ts
similarity index 100%
rename from src/utils/makeImmerSetters.ts
rename to src/shared/lib/makeImmerSetters.ts
diff --git a/src/utils/math.ts b/src/shared/lib/math.ts
similarity index 100%
rename from src/utils/math.ts
rename to src/shared/lib/math.ts
diff --git a/src/utils/memo.ts b/src/shared/lib/memo.ts
similarity index 100%
rename from src/utils/memo.ts
rename to src/shared/lib/memo.ts
diff --git a/src/utils/normalizeStringList.ts b/src/shared/lib/normalizeStringList.ts
similarity index 100%
rename from src/utils/normalizeStringList.ts
rename to src/shared/lib/normalizeStringList.ts
diff --git a/src/utils/object.ts b/src/shared/lib/object.ts
similarity index 100%
rename from src/utils/object.ts
rename to src/shared/lib/object.ts
diff --git a/src/utils/ordinals.ts b/src/shared/lib/ordinals.ts
similarity index 100%
rename from src/utils/ordinals.ts
rename to src/shared/lib/ordinals.ts
diff --git a/src/app/contracts/panelContracts.js b/src/shared/lib/panelContracts.js
similarity index 100%
rename from src/app/contracts/panelContracts.js
rename to src/shared/lib/panelContracts.js
diff --git a/src/utils/random.ts b/src/shared/lib/random.ts
similarity index 100%
rename from src/utils/random.ts
rename to src/shared/lib/random.ts
diff --git a/src/stores/resetAllStores.js b/src/shared/lib/resetAllStores.js
similarity index 69%
rename from src/stores/resetAllStores.js
rename to src/shared/lib/resetAllStores.js
index c07ef69..882d7e8 100644
--- a/src/stores/resetAllStores.js
+++ b/src/shared/lib/resetAllStores.js
@@ -1,12 +1,12 @@
-import { STORAGE_KEYS } from "@/lib/storage/storageKeys";
+import { STORAGE_KEYS } from "@shared/lib/storage/storageKeys";
-import { useDisplayPrefsStore } from "@/stores/useDisplayPrefsStore";
-import { useMetronomePrefsStore } from "@/stores/useMetronomePrefsStore";
-import { useMetronomeEngineStore } from "@/stores/useMetronomeEngineStore";
-import { useInstrumentCoreStore } from "@/stores/useInstrumentCoreStore";
-import { useInstrumentWorkflowStore } from "@/stores/useInstrumentWorkflowStore";
-import { useTheoryStore } from "@/stores/useTheoryStore";
-import { useThemeStore } from "@/stores/useThemeStore";
+import { useDisplayPrefsStore } from "@features/display/store/useDisplayPrefsStore";
+import { useMetronomePrefsStore } from "@features/practice/store/useMetronomePrefsStore";
+import { useMetronomeEngineStore } from "@features/practice/store/useMetronomeEngineStore";
+import { useInstrumentCoreStore } from "@features/instrument/store/useInstrumentCoreStore";
+import { useInstrumentWorkflowStore } from "@features/instrument/store/useInstrumentWorkflowStore";
+import { useTheoryStore } from "@features/theory/store/useTheoryStore";
+import { useThemeStore } from "@features/display/store/useThemeStore";
const NON_PERSISTED_APP_KEYS = [
// Global non-store key intentionally kept outside zustand persist.
diff --git a/src/utils/shapeColors.ts b/src/shared/lib/shapeColors.ts
similarity index 100%
rename from src/utils/shapeColors.ts
rename to src/shared/lib/shapeColors.ts
diff --git a/src/lib/storage/scopedStorage.ts b/src/shared/lib/storage/scopedStorage.ts
similarity index 97%
rename from src/lib/storage/scopedStorage.ts
rename to src/shared/lib/storage/scopedStorage.ts
index 981d563..9251011 100644
--- a/src/lib/storage/scopedStorage.ts
+++ b/src/shared/lib/storage/scopedStorage.ts
@@ -1,4 +1,4 @@
-import { scopeKey } from "@/lib/storage/windowScope";
+import { scopeKey } from "@shared/lib/storage/windowScope";
function getLocalStorage() {
if (typeof globalThis.localStorage === "undefined") {
diff --git a/src/lib/storage/storageKeys.ts b/src/shared/lib/storage/storageKeys.ts
similarity index 100%
rename from src/lib/storage/storageKeys.ts
rename to src/shared/lib/storage/storageKeys.ts
diff --git a/src/lib/storage/windowScope.ts b/src/shared/lib/storage/windowScope.ts
similarity index 100%
rename from src/lib/storage/windowScope.ts
rename to src/shared/lib/storage/windowScope.ts
diff --git a/src/utils/svgDelegation.js b/src/shared/lib/svgDelegation.js
similarity index 100%
rename from src/utils/svgDelegation.js
rename to src/shared/lib/svgDelegation.js
diff --git a/src/utils/textFit.js b/src/shared/lib/textFit.js
similarity index 100%
rename from src/utils/textFit.js
rename to src/shared/lib/textFit.js
diff --git a/src/utils/toast.ts b/src/shared/lib/toast.ts
similarity index 100%
rename from src/utils/toast.ts
rename to src/shared/lib/toast.ts
diff --git a/src/components/UI/combobox/BaseCombobox.jsx b/src/shared/ui/BaseCombobox.jsx
similarity index 98%
rename from src/components/UI/combobox/BaseCombobox.jsx
rename to src/shared/ui/BaseCombobox.jsx
index c5d418e..a97b776 100644
--- a/src/components/UI/combobox/BaseCombobox.jsx
+++ b/src/shared/ui/BaseCombobox.jsx
@@ -1,10 +1,10 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useVirtualizer } from "@tanstack/react-virtual";
import clsx from "clsx";
-import FloatingListbox from "@/components/UI/combobox/FloatingListbox";
-import useCombobox from "@/hooks/useCombobox";
-import useFilteredOptions from "@/hooks/useFilteredOptions";
-import { createTextFit } from "@/utils/textFit";
+import FloatingListbox from "@shared/ui/FloatingListbox";
+import useCombobox from "@shared/hooks/useCombobox";
+import useFilteredOptions from "@shared/hooks/useFilteredOptions";
+import { createTextFit } from "@shared/lib/textFit";
const DEFAULT_VIRTUALIZATION_THRESHOLD = 100;
const DEFAULT_OPTION_HEIGHT = 40;
diff --git a/src/components/UI/ConfirmDialog.jsx b/src/shared/ui/ConfirmDialog.jsx
similarity index 100%
rename from src/components/UI/ConfirmDialog.jsx
rename to src/shared/ui/ConfirmDialog.jsx
diff --git a/src/components/UI/ErrorFallback.jsx b/src/shared/ui/ErrorFallback.jsx
similarity index 96%
rename from src/components/UI/ErrorFallback.jsx
rename to src/shared/ui/ErrorFallback.jsx
index e1e2e8d..f0cc7a1 100644
--- a/src/components/UI/ErrorFallback.jsx
+++ b/src/shared/ui/ErrorFallback.jsx
@@ -1,6 +1,6 @@
import React, { useMemo } from "react";
import { toast } from "react-hot-toast";
-import { withToastPromise } from "@/utils/toast";
+import { withToastPromise } from "@shared/lib/toast";
import { useCopyToClipboard, useToggle } from "react-use";
import {
FiAlertTriangle,
@@ -12,8 +12,8 @@ import {
FiExternalLink,
FiTrash2,
} from "react-icons/fi";
-import { useConfirm } from "@/hooks/useConfirm";
-import { performFactoryReset } from "@/components/UI/errorFallbackReset";
+import { useConfirm } from "@shared/hooks/useConfirm";
+import { performFactoryReset } from "@shared/ui/errorFallbackReset";
export default function ErrorFallback({
error,
diff --git a/src/components/UI/combobox/FloatingListbox.jsx b/src/shared/ui/FloatingListbox.jsx
similarity index 100%
rename from src/components/UI/combobox/FloatingListbox.jsx
rename to src/shared/ui/FloatingListbox.jsx
diff --git a/src/components/UI/HotkeysCheatsheet.jsx b/src/shared/ui/HotkeysCheatsheet.jsx
similarity index 100%
rename from src/components/UI/HotkeysCheatsheet.jsx
rename to src/shared/ui/HotkeysCheatsheet.jsx
diff --git a/src/components/UI/modals/ModalFrame.jsx b/src/shared/ui/ModalFrame.jsx
similarity index 100%
rename from src/components/UI/modals/ModalFrame.jsx
rename to src/shared/ui/ModalFrame.jsx
diff --git a/src/components/UI/NumberField.jsx b/src/shared/ui/NumberField.jsx
similarity index 94%
rename from src/components/UI/NumberField.jsx
rename to src/shared/ui/NumberField.jsx
index 20a954b..d93fe97 100644
--- a/src/components/UI/NumberField.jsx
+++ b/src/shared/ui/NumberField.jsx
@@ -1,5 +1,5 @@
import clsx from "clsx";
-import { useNumberField } from "@/hooks/useNumberField";
+import { useNumberField } from "@shared/hooks/useNumberField";
function NumberField({
id,
diff --git a/src/components/UI/PanelHeader.jsx b/src/shared/ui/PanelHeader.jsx
similarity index 100%
rename from src/components/UI/PanelHeader.jsx
rename to src/shared/ui/PanelHeader.jsx
diff --git a/src/components/UI/SafeLazyModal.jsx b/src/shared/ui/SafeLazyModal.jsx
similarity index 88%
rename from src/components/UI/SafeLazyModal.jsx
rename to src/shared/ui/SafeLazyModal.jsx
index f7c5bf0..7c7301f 100644
--- a/src/components/UI/SafeLazyModal.jsx
+++ b/src/shared/ui/SafeLazyModal.jsx
@@ -1,6 +1,6 @@
import { Suspense } from "react";
-import SafeSection from "@/components/UI/SafeSection";
+import SafeSection from "@shared/ui/SafeSection";
export default function SafeLazyModal({ isOpen, resetKeys, label, children }) {
if (!isOpen) return null;
diff --git a/src/components/UI/SafeSection.jsx b/src/shared/ui/SafeSection.jsx
similarity index 84%
rename from src/components/UI/SafeSection.jsx
rename to src/shared/ui/SafeSection.jsx
index 883dcb2..544cda1 100644
--- a/src/components/UI/SafeSection.jsx
+++ b/src/shared/ui/SafeSection.jsx
@@ -1,7 +1,7 @@
import React from "react";
import { ErrorBoundary } from "react-error-boundary";
-import ErrorFallback from "@/components/UI/ErrorFallback";
+import ErrorFallback from "@shared/ui/ErrorFallback";
export default function SafeSection({ children, resetKeys, onReset }) {
return (
diff --git a/src/components/UI/Section.jsx b/src/shared/ui/Section.jsx
similarity index 100%
rename from src/components/UI/Section.jsx
rename to src/shared/ui/Section.jsx
diff --git a/src/components/UI/SegmentedRadioGroup.jsx b/src/shared/ui/SegmentedRadioGroup.jsx
similarity index 100%
rename from src/components/UI/SegmentedRadioGroup.jsx
rename to src/shared/ui/SegmentedRadioGroup.jsx
diff --git a/src/components/UI/ToggleSwitch.jsx b/src/shared/ui/ToggleSwitch.jsx
similarity index 100%
rename from src/components/UI/ToggleSwitch.jsx
rename to src/shared/ui/ToggleSwitch.jsx
diff --git a/src/components/UI/errorFallbackReset.js b/src/shared/ui/errorFallbackReset.js
similarity index 90%
rename from src/components/UI/errorFallbackReset.js
rename to src/shared/ui/errorFallbackReset.js
index 6292339..c6fb33d 100644
--- a/src/components/UI/errorFallbackReset.js
+++ b/src/shared/ui/errorFallbackReset.js
@@ -1,4 +1,4 @@
-import { resetAllStores } from "@/stores/resetAllStores";
+import { resetAllStores } from "@shared/lib/resetAllStores";
export async function performFactoryReset({
confirm,
diff --git a/src/shared/ui/index.js b/src/shared/ui/index.js
new file mode 100644
index 0000000..dba4be6
--- /dev/null
+++ b/src/shared/ui/index.js
@@ -0,0 +1,2 @@
+export { default as PanelHeader } from "./PanelHeader";
+export { default as SafeSection } from "./SafeSection";
diff --git a/src/tests/appDomainHooks.contract.test.js b/src/tests/app/appDomainHooks.contract.test.js
similarity index 96%
rename from src/tests/appDomainHooks.contract.test.js
rename to src/tests/app/appDomainHooks.contract.test.js
index 4536959..b1be72a 100644
--- a/src/tests/appDomainHooks.contract.test.js
+++ b/src/tests/app/appDomainHooks.contract.test.js
@@ -6,13 +6,13 @@ import {
INSTRUMENT_DOMAIN_RETURN_KEYS,
PRACTICE_DOMAIN_RETURN_KEYS,
EXPORT_CUSTOM_DOMAIN_RETURN_KEYS,
-} from "@/app/hooks/contracts";
+} from "@app/hooks/contracts";
import {
buildTheoryDomainReturn,
buildInstrumentDomainReturn,
buildPracticeMetronomeDomainReturn,
buildExportCustomTuningDomainReturn,
-} from "@/app/hooks/domainReturnBuilders";
+} from "@shared/lib/domainReturnBuilders";
function sortedKeys(value) {
return Object.keys(value).sort();
diff --git a/src/tests/neckFilterModes.test.js b/src/tests/domain/presets/neckFilterModes.test.js
similarity index 98%
rename from src/tests/neckFilterModes.test.js
rename to src/tests/domain/presets/neckFilterModes.test.js
index ca7fc04..0db5a5c 100644
--- a/src/tests/neckFilterModes.test.js
+++ b/src/tests/domain/presets/neckFilterModes.test.js
@@ -16,8 +16,8 @@ import {
resolveNeckFilterModeIntentFromBoardMeta,
resolvePresetNeckFilterMode,
shouldApplyNeckFilterMode,
-} from "@/lib/presets/neckFilterModes";
-import { normalizePresetMeta } from "@/lib/meta/meta";
+} from "@domain/presets/neckFilterModes";
+import { normalizePresetMeta } from "@domain/meta/meta";
test("KG filter applies to 24-EDO 6-string non-fretless boards", () => {
const input = { notePlacement: "betweenFrets" };
diff --git a/src/tests/chords.test.js b/src/tests/domain/theory/chords.test.js
similarity index 92%
rename from src/tests/chords.test.js
rename to src/tests/domain/theory/chords.test.js
index 474f8bc..6de5249 100644
--- a/src/tests/chords.test.js
+++ b/src/tests/domain/theory/chords.test.js
@@ -1,7 +1,7 @@
import test from "node:test";
import assert from "node:assert/strict";
-import { buildChordPCsFromPc, degreeForStep } from "@/lib/theory/chords";
+import { buildChordPCsFromPc, degreeForStep } from "@domain/theory/chords";
const sort = (arr) => [...arr].sort((a, b) => a - b);
diff --git a/src/tests/fretboardShapes.test.ts b/src/tests/domain/theory/fretboardShapes.test.ts
similarity index 99%
rename from src/tests/fretboardShapes.test.ts
rename to src/tests/domain/theory/fretboardShapes.test.ts
index e349532..45e9db4 100644
--- a/src/tests/fretboardShapes.test.ts
+++ b/src/tests/domain/theory/fretboardShapes.test.ts
@@ -16,7 +16,7 @@ import {
propagateFretClass,
propagateFretToRange,
toRelativePitchClasses,
-} from "@/lib/theory/fretboardShapes";
+} from "@domain/theory/fretboardShapes";
void test("mod normalizes negatives to [0, n-1]", () => {
assert.equal(mod(-1, 12), 11);
diff --git a/src/tests/notation.test.js b/src/tests/domain/theory/notation.test.js
similarity index 98%
rename from src/tests/notation.test.js
rename to src/tests/domain/theory/notation.test.js
index 5cd8798..a5731a1 100644
--- a/src/tests/notation.test.js
+++ b/src/tests/domain/theory/notation.test.js
@@ -6,7 +6,7 @@ import {
normalizeSpellingMarker,
normalizeIntlNoteName,
resolveSpellingMarker,
-} from "@/lib/theory/notation";
+} from "@domain/theory/notation";
test("every configured spelling alias resolves to a canonical marker", () => {
for (const [alias, canonical] of Object.entries(SPELLING_MARKER_ALIASES)) {
diff --git a/src/tests/shapeSystems.test.ts b/src/tests/domain/theory/shapeSystems.test.ts
similarity index 99%
rename from src/tests/shapeSystems.test.ts
rename to src/tests/domain/theory/shapeSystems.test.ts
index 696dc61..8d57ecf 100644
--- a/src/tests/shapeSystems.test.ts
+++ b/src/tests/domain/theory/shapeSystems.test.ts
@@ -14,7 +14,7 @@ import {
shapeDegreeCoverage,
shapeHasContiguousStrings,
shapeMatchesNotesPerString,
-} from "@/lib/theory/shapeSystems";
+} from "@domain/theory/shapeSystems";
void test("notes-per-string analysis and constraints are generic", () => {
const shape = [
diff --git a/src/tests/tuning.test.js b/src/tests/domain/theory/tuning.test.js
similarity index 98%
rename from src/tests/tuning.test.js
rename to src/tests/domain/theory/tuning.test.js
index 9f04a87..174c89a 100644
--- a/src/tests/tuning.test.js
+++ b/src/tests/domain/theory/tuning.test.js
@@ -9,7 +9,7 @@ import {
stepToPc,
centsFromNearest,
nameFallback,
-} from "@/lib/theory/tuning";
+} from "@domain/theory/tuning";
function makeSystem(divisions) {
return {
diff --git a/src/tests/importPipeline.test.ts b/src/tests/features/export/importPipeline.test.ts
similarity index 98%
rename from src/tests/importPipeline.test.ts
rename to src/tests/features/export/importPipeline.test.ts
index ad72824..316ad7a 100644
--- a/src/tests/importPipeline.test.ts
+++ b/src/tests/features/export/importPipeline.test.ts
@@ -6,7 +6,7 @@ import {
IMPORT_PIPELINE_ERROR_CODES,
IMPORT_PIPELINE_MAX_FILE_SIZE_BYTES,
runImportFilePipeline,
-} from "@/lib/export/importPipeline";
+} from "@features/export/model/importPipeline";
void test("runImportFilePipeline rejects files with unsupported metadata", async () => {
const file = new File(["{}"], "tunings.txt", { type: "text/plain" });
diff --git a/src/tests/tuningPackEditorModal.test.js b/src/tests/features/export/tuningPackEditorModal.test.js
similarity index 98%
rename from src/tests/tuningPackEditorModal.test.js
rename to src/tests/features/export/tuningPackEditorModal.test.js
index 35eb8a6..5c7406f 100644
--- a/src/tests/tuningPackEditorModal.test.js
+++ b/src/tests/features/export/tuningPackEditorModal.test.js
@@ -5,7 +5,7 @@ import {
ensurePack,
buildTemplatePack,
togglePackSpelling,
-} from "@/components/UI/modals/tuningPackNormalization";
+} from "@features/export/model/tuningPackNormalization";
test("ensurePack preserves a valid top-level spelling field", () => {
const result = ensurePack({
diff --git a/src/tests/fretboardLayoutMemoization.test.js b/src/tests/features/fretboard/fretboardLayoutMemoization.test.js
similarity index 92%
rename from src/tests/fretboardLayoutMemoization.test.js
rename to src/tests/features/fretboard/fretboardLayoutMemoization.test.js
index b6b2d2e..1d6f36c 100644
--- a/src/tests/fretboardLayoutMemoization.test.js
+++ b/src/tests/features/fretboard/fretboardLayoutMemoization.test.js
@@ -4,7 +4,7 @@ import fs from "node:fs";
import path from "node:path";
import React from "react";
import { renderToStaticMarkup } from "react-dom/server";
-import { useFretboardLayout } from "@/hooks/useFretboardLayout";
+import { useFretboardLayout } from "@features/fretboard/hooks/useFretboardLayout";
function LayoutProbe(props) {
const layout = useFretboardLayout(props);
@@ -50,7 +50,7 @@ test("layout values update immediately when stringMeta changes", () => {
});
test("useFretboardLayout derives metaByIndex during render via useMemo", () => {
- const hookPath = path.resolve("src/hooks/useFretboardLayout.js");
+ const hookPath = path.resolve("src/features/fretboard/hooks/useFretboardLayout.js");
const source = fs.readFileSync(hookPath, "utf8");
assert.match(
diff --git a/src/tests/hiddenFretsMetaAndRender.test.js b/src/tests/features/fretboard/hiddenFretsMetaAndRender.test.js
similarity index 97%
rename from src/tests/hiddenFretsMetaAndRender.test.js
rename to src/tests/features/fretboard/hiddenFretsMetaAndRender.test.js
index 638f7d4..f9e35cf 100644
--- a/src/tests/hiddenFretsMetaAndRender.test.js
+++ b/src/tests/features/fretboard/hiddenFretsMetaAndRender.test.js
@@ -2,17 +2,17 @@ import test from "node:test";
import assert from "node:assert/strict";
import React from "react";
import { renderToStaticMarkup } from "react-dom/server";
-import Fretboard from "@/components/Fretboard/Fretboard";
-import { PRESET_TUNING_META } from "@/lib/presets/presets";
-import { normalizePresetMeta } from "@/lib/meta/meta";
-import { TUNINGS, findSystemByEdo } from "@/lib/theory/tuning";
+import Fretboard from "@features/fretboard/components/Fretboard";
+import { PRESET_TUNING_META } from "@domain/presets/presets";
+import { normalizePresetMeta } from "@domain/meta/meta";
+import { TUNINGS, findSystemByEdo } from "@domain/theory/tuning";
import {
normalizeHiddenFrets,
buildRenderedFretIndices,
resolveVisibleCapoFret,
reconcileCapoState,
-} from "@/components/Fretboard/renderFilters";
-import { buildFretLabel } from "@/utils/fretLabels";
+} from "@features/fretboard/model/renderFilters";
+import { buildFretLabel } from "@shared/lib/fretLabels";
test("King Gizzard 24-TET preset meta includes hidden fret board config", () => {
const meta = PRESET_TUNING_META["24-TET"]?.[6]?.["King Gizzard (C#F#C#F#BE)"];
diff --git a/src/tests/pitchMapping.test.js b/src/tests/features/fretboard/pitchMapping.test.js
similarity index 91%
rename from src/tests/pitchMapping.test.js
rename to src/tests/features/fretboard/pitchMapping.test.js
index 0fbf073..4f7ca17 100644
--- a/src/tests/pitchMapping.test.js
+++ b/src/tests/features/fretboard/pitchMapping.test.js
@@ -1,11 +1,11 @@
import test from "node:test";
import assert from "node:assert/strict";
-import { TUNINGS } from "@/lib/theory/tuning";
+import { TUNINGS } from "@domain/theory/tuning";
import {
buildNameToPcMap,
nameForPcWithDisplayAccidentals,
-} from "@/hooks/usePitchMapping";
+} from "@features/fretboard/hooks/usePitchMapping";
test("german naming keeps B/H pitch classes distinct", () => {
const map = buildNameToPcMap(TUNINGS["12-TET"], "german", "flat");
diff --git a/src/tests/presetBuilder.test.js b/src/tests/features/instrument/presetBuilder.test.js
similarity index 97%
rename from src/tests/presetBuilder.test.js
rename to src/tests/features/instrument/presetBuilder.test.js
index 4912d4b..196c97c 100644
--- a/src/tests/presetBuilder.test.js
+++ b/src/tests/features/instrument/presetBuilder.test.js
@@ -1,6 +1,6 @@
import test from "node:test";
import assert from "node:assert/strict";
-import { coerceAnyTuning } from "@/hooks/usePresetBuilder";
+import { coerceAnyTuning } from "@features/instrument/hooks/usePresetBuilder";
test("coerceAnyTuning translates german B/H note names to intl when requested", () => {
const pack = {
diff --git a/src/tests/storeMigrationParity.test.js b/src/tests/features/instrument/storeMigrationParity.test.js
similarity index 89%
rename from src/tests/storeMigrationParity.test.js
rename to src/tests/features/instrument/storeMigrationParity.test.js
index 00601e1..1b14fb2 100644
--- a/src/tests/storeMigrationParity.test.js
+++ b/src/tests/features/instrument/storeMigrationParity.test.js
@@ -1,8 +1,8 @@
import test from "node:test";
import assert from "node:assert/strict";
-import { STORAGE_KEYS } from "@/lib/storage/storageKeys";
-import { scopeKey } from "@/lib/storage/windowScope";
+import { STORAGE_KEYS } from "@shared/lib/storage/storageKeys";
+import { scopeKey } from "@shared/lib/storage/windowScope";
class MemoryStorage {
constructor() {
@@ -48,9 +48,16 @@ function setActiveWindowId(windowId) {
globalThis.__TV_WINDOW_ID__ = windowId;
}
-async function importFresh(relativePath) {
- const url = new URL(relativePath, import.meta.url);
- return import(`${url.href}?t=${Date.now()}-${Math.random()}`);
+async function importFresh(specifier) {
+ const cacheKey = `${Date.now()}-${Math.random()}`;
+ const isAliasedSpecifier =
+ /^@(app|features|shared|domain|styles)\//.test(specifier) ||
+ specifier.startsWith("@/");
+ if (isAliasedSpecifier) {
+ return import(`${specifier}?t=${cacheKey}`);
+ }
+ const url = new URL(specifier, import.meta.url);
+ return import(`${url.href}?t=${cacheKey}`);
}
function readStoredJson(key) {
@@ -75,7 +82,7 @@ test("legacy metronome prefs shape hydrates into normalized prefs store shape",
);
const { useMetronomePrefsStore } = await importFresh(
- "../stores/useMetronomePrefsStore.js",
+ "@features/practice/store/useMetronomePrefsStore.js",
);
await useMetronomePrefsStore.persist.rehydrate();
@@ -111,7 +118,7 @@ test("metronome store falls back for invalid persisted randomizeMode", async ()
);
const { useMetronomePrefsStore } = await importFresh(
- "../stores/useMetronomePrefsStore.js",
+ "@features/practice/store/useMetronomePrefsStore.js",
);
await useMetronomePrefsStore.persist.rehydrate();
@@ -125,7 +132,7 @@ test("legacy theory keys hydrate into new theory store and clear old keys", asyn
storage.setItem(STORAGE_KEYS.SYSTEM_ID, "24-TET");
storage.setItem(STORAGE_KEYS.ROOT, "D");
- const { useTheoryStore } = await importFresh("../stores/useTheoryStore.js");
+ const { useTheoryStore } = await importFresh("@features/theory/store/useTheoryStore.js");
await useTheoryStore.persist.rehydrate();
const state = useTheoryStore.getState();
@@ -148,7 +155,7 @@ test("theory store prefers valid persisted payload over legacy keys", async () =
storage.setItem(STORAGE_KEYS.SYSTEM_ID, "24-TET");
storage.setItem(STORAGE_KEYS.ROOT, "D");
- const { useTheoryStore } = await importFresh("../stores/useTheoryStore.js");
+ const { useTheoryStore } = await importFresh("@features/theory/store/useTheoryStore.js");
await useTheoryStore.persist.rehydrate();
const state = useTheoryStore.getState();
@@ -162,9 +169,9 @@ test("theory store prefers valid persisted payload over legacy keys", async () =
test("musical reset clears capo-relative chord mode through theory reset", async () => {
storage.clear();
- const { useTheoryStore } = await importFresh("../stores/useTheoryStore.js");
+ const { useTheoryStore } = await importFresh("@features/theory/store/useTheoryStore.js");
const { resetMusicalStateFromRefs } = await importFresh(
- "../hooks/resetMusicalState.js",
+ "@features/theory/hooks/resetMusicalState.js",
);
useTheoryStore.getState().setChordCapoRelative(true);
@@ -179,7 +186,7 @@ test("musical reset clears capo-relative chord mode through theory reset", async
test("musical reset fallback clears capo-relative chord mode", async () => {
const { resetMusicalStateFromRefs } = await importFresh(
- "../hooks/resetMusicalState.js",
+ "@features/theory/hooks/resetMusicalState.js",
);
const calls = [];
@@ -207,7 +214,7 @@ test("legacy custom tuning payload array remains compatible in workflow store",
storage.setItem(STORAGE_KEYS.CUSTOM_TUNINGS, JSON.stringify(legacyPayload));
const { useInstrumentWorkflowStore } = await importFresh(
- "../stores/useInstrumentWorkflowStore.js",
+ "@features/instrument/store/useInstrumentWorkflowStore.js",
);
await useInstrumentWorkflowStore.persist.rehydrate();
@@ -229,7 +236,7 @@ test("instrument core migration clamps strings/frets and reset action restores f
storage.setItem(STORAGE_KEYS.FRETS, "1");
const { useInstrumentCoreStore } = await importFresh(
- "../stores/useInstrumentCoreStore.js",
+ "@features/instrument/store/useInstrumentCoreStore.js",
);
await useInstrumentCoreStore.persist.rehydrate();
@@ -271,7 +278,7 @@ test("instrument core keeps global default tunings while using persisted strings
);
const { useInstrumentCoreStore } = await importFresh(
- "../stores/useInstrumentCoreStore.js",
+ "@features/instrument/store/useInstrumentCoreStore.js",
);
await useInstrumentCoreStore.persist.rehydrate();
@@ -290,7 +297,7 @@ test("instrument core keeps global default tunings while using persisted strings
test("instrument core store preserves tuning/stringMeta/boardMeta mutation semantics", async () => {
storage.clear();
const { useInstrumentCoreStore } = await importFresh(
- "../stores/useInstrumentCoreStore.js",
+ "@features/instrument/store/useInstrumentCoreStore.js",
);
useInstrumentCoreStore.setState({
@@ -330,7 +337,7 @@ test("instrument core store preserves tuning/stringMeta/boardMeta mutation seman
test("instrument core avoids redundant global default tuning writes for no-op updates", async () => {
storage.clear();
const { useInstrumentCoreStore } = await importFresh(
- "../stores/useInstrumentCoreStore.js",
+ "@features/instrument/store/useInstrumentCoreStore.js",
);
useInstrumentCoreStore.getState().updateUserDefaultTuningMap(() => {});
@@ -348,7 +355,7 @@ test("instrument core avoids redundant global default tuning writes for no-op up
test("instrument core setTuning updater recovers from invalid tuning shape", async () => {
storage.clear();
const { useInstrumentCoreStore } = await importFresh(
- "../stores/useInstrumentCoreStore.js",
+ "@features/instrument/store/useInstrumentCoreStore.js",
);
useInstrumentCoreStore.setState({ tuning: null });
@@ -375,7 +382,7 @@ test("instrument core migration defaults neckFilterMode to none when mode is abs
);
const { useInstrumentCoreStore } = await importFresh(
- "../stores/useInstrumentCoreStore.js",
+ "@features/instrument/store/useInstrumentCoreStore.js",
);
await useInstrumentCoreStore.persist.rehydrate();
const state = useInstrumentCoreStore.getState();
@@ -398,7 +405,7 @@ test("instrument core migration keeps explicit canonical neckFilterMode", async
);
const { useInstrumentCoreStore } = await importFresh(
- "../stores/useInstrumentCoreStore.js",
+ "@features/instrument/store/useInstrumentCoreStore.js",
);
await useInstrumentCoreStore.persist.rehydrate();
const state = useInstrumentCoreStore.getState();
@@ -421,7 +428,7 @@ test("instrument core migration preserves explicit fretless neck filter mode", a
);
const { useInstrumentCoreStore } = await importFresh(
- "../stores/useInstrumentCoreStore.js",
+ "@features/instrument/store/useInstrumentCoreStore.js",
);
await useInstrumentCoreStore.persist.rehydrate();
const state = useInstrumentCoreStore.getState();
@@ -432,7 +439,7 @@ test("instrument core migration preserves explicit fretless neck filter mode", a
test("instrument core setNeckFilterMode updates canonical mode", async () => {
storage.clear();
const { useInstrumentCoreStore } = await importFresh(
- "../stores/useInstrumentCoreStore.js",
+ "@features/instrument/store/useInstrumentCoreStore.js",
);
useInstrumentCoreStore.getState().setNeckFilterMode("kg");
@@ -456,7 +463,7 @@ test("display prefs store hydrates via globalThis localStorage adapter", async (
);
const { useDisplayPrefsStore } = await importFresh(
- "../stores/useDisplayPrefsStore.js",
+ "@features/display/store/useDisplayPrefsStore.js",
);
await useDisplayPrefsStore.persist.rehydrate();
@@ -476,7 +483,7 @@ test("global stores migrate scoped payloads back to unscoped keys", async () =>
}),
);
- const { useThemeStore } = await importFresh("../stores/useThemeStore.js");
+ const { useThemeStore } = await importFresh("@features/display/store/useThemeStore.js");
await useThemeStore.persist.rehydrate();
const unscopedPersisted = readStoredJson(STORAGE_KEYS.THEME);
@@ -489,13 +496,13 @@ test("global stores migrate scoped payloads back to unscoped keys", async () =>
test("value-or-updater setters preserve direct assignment and updater semantics", async () => {
storage.clear();
const { useDisplayPrefsStore } = await importFresh(
- "../stores/useDisplayPrefsStore.js",
+ "@features/display/store/useDisplayPrefsStore.js",
);
const { useMetronomePrefsStore } = await importFresh(
- "../stores/useMetronomePrefsStore.js",
+ "@features/practice/store/useMetronomePrefsStore.js",
);
const { useInstrumentCoreStore } = await importFresh(
- "../stores/useInstrumentCoreStore.js",
+ "@features/instrument/store/useInstrumentCoreStore.js",
);
useDisplayPrefsStore.getState().setPrefs({ accidental: "flat", dotSize: 20 });
@@ -535,10 +542,10 @@ test("value-or-updater setters preserve direct assignment and updater semantics"
test("metronome prefs setters and engine reset semantics remain distinct", async () => {
storage.clear();
const { useMetronomePrefsStore } = await importFresh(
- "../stores/useMetronomePrefsStore.js",
+ "@features/practice/store/useMetronomePrefsStore.js",
);
const { useMetronomeEngineStore } = await importFresh(
- "../stores/useMetronomeEngineStore.js",
+ "@features/practice/store/useMetronomeEngineStore.js",
);
useMetronomePrefsStore.getState().setters.setBpm(132);
@@ -579,9 +586,9 @@ test("metronome prefs setters and engine reset semantics remain distinct", async
test("theory and workflow action names and behaviors remain stable", async () => {
storage.clear();
- const { useTheoryStore } = await importFresh("../stores/useTheoryStore.js");
+ const { useTheoryStore } = await importFresh("@features/theory/store/useTheoryStore.js");
const { useInstrumentWorkflowStore } = await importFresh(
- "../stores/useInstrumentWorkflowStore.js",
+ "@features/instrument/store/useInstrumentWorkflowStore.js",
);
const theoryActions = useTheoryStore.getState();
@@ -659,19 +666,19 @@ test("resetAllStores restores defaults and clears only app-owned keys", async ()
JSON.stringify({ "12-TET:6": ["E", "A", "D", "G", "B", "E"] }),
);
- const { resetAllStores } = await importFresh("../stores/resetAllStores.js");
+ const { resetAllStores } = await importFresh("@shared/lib/resetAllStores.js");
const { useDisplayPrefsStore } =
- await import("../stores/useDisplayPrefsStore.js");
+ await import("@features/display/store/useDisplayPrefsStore.js");
const { useMetronomePrefsStore } =
- await import("../stores/useMetronomePrefsStore.js");
+ await import("@features/practice/store/useMetronomePrefsStore.js");
const { useInstrumentCoreStore } =
- await import("../stores/useInstrumentCoreStore.js");
+ await import("@features/instrument/store/useInstrumentCoreStore.js");
const { useInstrumentWorkflowStore } =
- await import("../stores/useInstrumentWorkflowStore.js");
+ await import("@features/instrument/store/useInstrumentWorkflowStore.js");
const { useMetronomeEngineStore } =
- await import("../stores/useMetronomeEngineStore.js");
- const { useTheoryStore } = await import("../stores/useTheoryStore.js");
- const { useThemeStore } = await import("../stores/useThemeStore.js");
+ await import("@features/practice/store/useMetronomeEngineStore.js");
+ const { useTheoryStore } = await import("@features/theory/store/useTheoryStore.js");
+ const { useThemeStore } = await import("@features/display/store/useThemeStore.js");
useDisplayPrefsStore.getState().setPrefs({ accidental: "flat", dotSize: 20 });
useMetronomePrefsStore.getState().setPrefs({ bpm: 132, timeSig: "5/4" });
@@ -743,7 +750,7 @@ test("resetAllStores only clears instrument scoped keys for the active window",
storage.setItem(STORAGE_KEYS.DISPLAY_PREFS, JSON.stringify({ state: {} }));
storage.setItem(STORAGE_KEYS.CUSTOM_TUNINGS, JSON.stringify({ state: [] }));
- const { resetAllStores } = await importFresh("../stores/resetAllStores.js");
+ const { resetAllStores } = await importFresh("@shared/lib/resetAllStores.js");
resetAllStores();
assert.equal(
@@ -760,9 +767,9 @@ test("resetAllStores only clears instrument scoped keys for the active window",
test("generated immer setters preserve non-target keys on full-store drafts", async () => {
storage.clear();
- const { useTheoryStore } = await importFresh("../stores/useTheoryStore.js");
+ const { useTheoryStore } = await importFresh("@features/theory/store/useTheoryStore.js");
const { useInstrumentWorkflowStore } = await importFresh(
- "../stores/useInstrumentWorkflowStore.js",
+ "@features/instrument/store/useInstrumentWorkflowStore.js",
);
useTheoryStore.setState({ extraTheoryKey: "keep-me" });
diff --git a/src/tests/storeSelectorStability.test.js b/src/tests/features/instrument/storeSelectorStability.test.js
similarity index 85%
rename from src/tests/storeSelectorStability.test.js
rename to src/tests/features/instrument/storeSelectorStability.test.js
index 2381454..73e6012 100644
--- a/src/tests/storeSelectorStability.test.js
+++ b/src/tests/features/instrument/storeSelectorStability.test.js
@@ -25,9 +25,16 @@ class MemoryStorage {
globalThis.localStorage = new MemoryStorage();
-async function importFresh(relativePath) {
- const url = new URL(relativePath, import.meta.url);
- return import(`${url.href}?t=${Date.now()}-${Math.random()}`);
+async function importFresh(specifier) {
+ const cacheKey = `${Date.now()}-${Math.random()}`;
+ const isAliasedSpecifier =
+ /^@(app|features|shared|domain|styles)\//.test(specifier) ||
+ specifier.startsWith("@/");
+ if (isAliasedSpecifier) {
+ return import(`${specifier}?t=${cacheKey}`);
+ }
+ const url = new URL(specifier, import.meta.url);
+ return import(`${url.href}?t=${cacheKey}`);
}
function trackSelectorChanges(store, selector) {
@@ -49,7 +56,7 @@ function trackSelectorChanges(store, selector) {
test("metronome HUD selector ignores unrelated engine updates", async () => {
const { useMetronomeEngineStore, selectMetronomeCurrentBeat } =
- await importFresh("../stores/useMetronomeEngineStore.js");
+ await importFresh("@features/practice/store/useMetronomeEngineStore.js");
useMetronomeEngineStore.setState({
isPlaying: false,
currentBeat: 1,
@@ -79,7 +86,7 @@ test("metronome cursor updates beat/bar atomically", async () => {
useMetronomeEngineStore,
selectMetronomeCurrentBeat,
selectMetronomeCurrentBar,
- } = await importFresh("../stores/useMetronomeEngineStore.js");
+ } = await importFresh("@features/practice/store/useMetronomeEngineStore.js");
useMetronomeEngineStore.setState({
isPlaying: false,
@@ -113,7 +120,7 @@ test("metronome cursor updates beat/bar atomically", async () => {
test("display controls selector only changes when selected pref changes", async () => {
const { useDisplayPrefsStore } = await importFresh(
- "../stores/useDisplayPrefsStore.js",
+ "@features/display/store/useDisplayPrefsStore.js",
);
useDisplayPrefsStore.setState((state) => ({
...state,
@@ -143,7 +150,7 @@ test("display controls selector only changes when selected pref changes", async
test("preset picker selector ignores workflow modal/editor updates", async () => {
const { useInstrumentWorkflowStore, selectWorkflowSelectedPreset } =
- await importFresh("../stores/useInstrumentWorkflowStore.js");
+ await importFresh("@features/instrument/store/useInstrumentWorkflowStore.js");
useInstrumentWorkflowStore.setState({
customTunings: [],
selectedPreset: "Factory default",
diff --git a/src/tests/tuningIO.test.js b/src/tests/features/instrument/tuningIO.test.js
similarity index 93%
rename from src/tests/tuningIO.test.js
rename to src/tests/features/instrument/tuningIO.test.js
index 29b22fc..a72864e 100644
--- a/src/tests/tuningIO.test.js
+++ b/src/tests/features/instrument/tuningIO.test.js
@@ -1,11 +1,11 @@
import test from "node:test";
import assert from "node:assert/strict";
-import { STR_MAX, STR_MIN } from "@/lib/config/appDefaults";
-import { parseTuningPack } from "@/lib/export/schema";
-import { removePackByIdentifier } from "@/lib/export/tuningIO";
-import { coerceAnyTuning } from "@/hooks/usePresetBuilder";
-import { sanitizeBoardMetaForModeStorage } from "@/lib/presets/neckFilterModes";
+import { STR_MAX, STR_MIN } from "@shared/config/appDefaults";
+import { parseTuningPack } from "@features/export/model/schema";
+import { removePackByIdentifier } from "@features/export/model/tuningIO";
+import { coerceAnyTuning } from "@features/instrument/hooks/usePresetBuilder";
+import { sanitizeBoardMetaForModeStorage } from "@domain/presets/neckFilterModes";
const basePack = {
name: "Example Tuning",
diff --git a/src/tests/useMetronomeEngine.test.js b/src/tests/features/practice/useMetronomeEngine.test.js
similarity index 95%
rename from src/tests/useMetronomeEngine.test.js
rename to src/tests/features/practice/useMetronomeEngine.test.js
index 2d16040..8c83b0d 100644
--- a/src/tests/useMetronomeEngine.test.js
+++ b/src/tests/features/practice/useMetronomeEngine.test.js
@@ -1,7 +1,7 @@
import test from "node:test";
import assert from "node:assert/strict";
-import { scheduleBeatUiUpdateWithAudioClock } from "../hooks/useMetronomeEngine.js";
+import { scheduleBeatUiUpdateWithAudioClock } from "@features/practice/hooks/useMetronomeEngine.js";
test("scheduleBeatUiUpdateWithAudioClock derives delay from audio clock", () => {
const calls = [];
diff --git a/src/tests/shareCodec.test.ts b/src/tests/features/share/shareCodec.test.ts
similarity index 99%
rename from src/tests/shareCodec.test.ts
rename to src/tests/features/share/shareCodec.test.ts
index 77c3045..af7870b 100644
--- a/src/tests/shareCodec.test.ts
+++ b/src/tests/features/share/shareCodec.test.ts
@@ -8,7 +8,7 @@ import {
parseSharePayload,
resolveInstrumentHydrationValues,
serializeSharePayload,
-} from "@/lib/url/shareCodec";
+} from "@features/share/model/shareCodec";
void test("buildSharePayload only keeps instrument scope + systemId", () => {
const payload = buildSharePayload({
diff --git a/src/tests/shareLimits.test.ts b/src/tests/features/share/shareLimits.test.ts
similarity index 96%
rename from src/tests/shareLimits.test.ts
rename to src/tests/features/share/shareLimits.test.ts
index 6e9b5f3..5e92077 100644
--- a/src/tests/shareLimits.test.ts
+++ b/src/tests/features/share/shareLimits.test.ts
@@ -5,7 +5,7 @@ import {
evaluateShareUrlSize,
SHARE_URL_QR_HARD_LIMIT_THRESHOLD,
SHARE_URL_WARNING_THRESHOLD,
-} from "@/lib/url/shareLimits";
+} from "@features/share/model/shareLimits";
void test("evaluateShareUrlSize returns healthy status under warning threshold", () => {
const value = evaluateShareUrlSize("x".repeat(SHARE_URL_WARNING_THRESHOLD));
diff --git a/src/tests/shareStateAdapter.test.js b/src/tests/features/share/shareStateAdapter.test.js
similarity index 98%
rename from src/tests/shareStateAdapter.test.js
rename to src/tests/features/share/shareStateAdapter.test.js
index 7103378..57dba41 100644
--- a/src/tests/shareStateAdapter.test.js
+++ b/src/tests/features/share/shareStateAdapter.test.js
@@ -4,7 +4,7 @@ import assert from "node:assert/strict";
import {
buildRawShareState,
serializeShareState,
-} from "@/app/adapters/shareState";
+} from "@features/share/model/shareState";
void test("buildRawShareState emits only codec-consumed raw share shape", () => {
const customTunings = [{ name: "Drop D", tuning: ["D2", "A2"] }];
diff --git a/src/tests/useUrlShareHydration.test.js b/src/tests/features/share/useUrlShareHydration.test.js
similarity index 97%
rename from src/tests/useUrlShareHydration.test.js
rename to src/tests/features/share/useUrlShareHydration.test.js
index 90f12d6..8a99bff 100644
--- a/src/tests/useUrlShareHydration.test.js
+++ b/src/tests/features/share/useUrlShareHydration.test.js
@@ -6,11 +6,11 @@ import {
clearUrlSearchParams,
evaluateUrlShareNoticeState,
shouldApplyUrlHydration,
-} from "@/app/hooks/useUrlShareHydration";
+} from "@features/share/hooks/useUrlShareHydration";
import {
parseSharePayload,
resolveInstrumentHydrationValues,
-} from "@/lib/url/shareCodec";
+} from "@features/share/model/shareCodec";
void test("areShareDomainsHydrated requires theory + instrument hydration only", () => {
assert.equal(
diff --git a/src/tests/chordCapoDisplay.test.js b/src/tests/features/theory/chordCapoDisplay.test.js
similarity index 94%
rename from src/tests/chordCapoDisplay.test.js
rename to src/tests/features/theory/chordCapoDisplay.test.js
index 6f94b92..695d96f 100644
--- a/src/tests/chordCapoDisplay.test.js
+++ b/src/tests/features/theory/chordCapoDisplay.test.js
@@ -1,7 +1,7 @@
import test from "node:test";
import assert from "node:assert/strict";
-import { buildCapoChordDisplay } from "@/components/UI/controls/chordCapoDisplay";
+import { buildCapoChordDisplay } from "@features/theory/model/chordCapoDisplay";
test("capo chord display describes shape-to-sounding mapping", () => {
const display = buildCapoChordDisplay({
diff --git a/src/tests/noteNaming.test.js b/src/tests/features/theory/noteNaming.test.js
similarity index 98%
rename from src/tests/noteNaming.test.js
rename to src/tests/features/theory/noteNaming.test.js
index cc1b800..a0f99e7 100644
--- a/src/tests/noteNaming.test.js
+++ b/src/tests/features/theory/noteNaming.test.js
@@ -6,7 +6,7 @@ import {
germanToEnglishNoteName,
renderNoteName,
buildNoteAliases,
-} from "@/lib/theory/notation";
+} from "@domain/theory/notation";
test("german note naming converts B and H correctly", () => {
assert.equal(toGermanNoteName("Bb"), "B");
diff --git a/src/tests/theoryControlModel.test.js b/src/tests/features/theory/theoryControlModel.test.js
similarity index 97%
rename from src/tests/theoryControlModel.test.js
rename to src/tests/features/theory/theoryControlModel.test.js
index a92ac3c..6940504 100644
--- a/src/tests/theoryControlModel.test.js
+++ b/src/tests/features/theory/theoryControlModel.test.js
@@ -1,9 +1,9 @@
import test from "node:test";
import assert from "node:assert/strict";
-import { buildTheoryControlModel } from "@/app/adapters/controls";
-import { resolveCapoRelativeChordRootPc } from "@/lib/theory/capoChords";
-import { buildChordFit } from "@/app/containers/theoryPanelModel";
+import { buildTheoryControlModel } from "@shared/lib/controlModels";
+import { resolveCapoRelativeChordRootPc } from "@domain/theory/capoChords";
+import { buildChordFit } from "@features/theory/model/theoryPanelModel";
test("buildTheoryControlModel carries canonical chord PC set names", () => {
const chordTonePcs = new Set([0, 4, 7]);
diff --git a/src/tests/useRandomScale.test.js b/src/tests/features/theory/useRandomScale.test.js
similarity index 97%
rename from src/tests/useRandomScale.test.js
rename to src/tests/features/theory/useRandomScale.test.js
index 250a69e..0491261 100644
--- a/src/tests/useRandomScale.test.js
+++ b/src/tests/features/theory/useRandomScale.test.js
@@ -5,7 +5,7 @@ import {
RANDOMIZE_MODES,
applyRandomizedScale,
formatRandomizedScaleAnnouncement,
-} from "@/hooks/useRandomScale";
+} from "@features/theory/hooks/useRandomScale";
test("applyRandomizedScale updates state by randomize mode", () => {
const result = { root: "D", scale: "Dorian" };
diff --git a/src/tests/applyValueOrUpdaterOnDraft.test.ts b/src/tests/shared/applyValueOrUpdaterOnDraft.test.ts
similarity index 94%
rename from src/tests/applyValueOrUpdaterOnDraft.test.ts
rename to src/tests/shared/applyValueOrUpdaterOnDraft.test.ts
index f46db98..36d8189 100644
--- a/src/tests/applyValueOrUpdaterOnDraft.test.ts
+++ b/src/tests/shared/applyValueOrUpdaterOnDraft.test.ts
@@ -1,7 +1,7 @@
import test from "node:test";
import assert from "node:assert/strict";
-import { applyValueOrUpdaterOnDraft } from "@/utils/applyValueOrUpdaterOnDraft";
+import { applyValueOrUpdaterOnDraft } from "@shared/lib/applyValueOrUpdaterOnDraft";
void test("direct value assignment updates draftContainer[key]", () => {
const container = {
diff --git a/src/tests/degreeColors.test.js b/src/tests/shared/degreeColors.test.js
similarity index 89%
rename from src/tests/degreeColors.test.js
rename to src/tests/shared/degreeColors.test.js
index f7a35eb..20e8d8f 100644
--- a/src/tests/degreeColors.test.js
+++ b/src/tests/shared/degreeColors.test.js
@@ -1,7 +1,7 @@
import test from "node:test";
import assert from "node:assert/strict";
-import { buildDegreePalette, getDegreeColor } from "@/utils/degreeColors";
+import { buildDegreePalette, getDegreeColor } from "@shared/lib/degreeColors";
test("buildDegreePalette assigns root and evenly spaced hues", () => {
const palette = buildDegreePalette(3, {
diff --git a/src/tests/fretLabels.test.js b/src/tests/shared/fretLabels.test.js
similarity index 99%
rename from src/tests/fretLabels.test.js
rename to src/tests/shared/fretLabels.test.js
index 6faa453..cf02d6e 100644
--- a/src/tests/fretLabels.test.js
+++ b/src/tests/shared/fretLabels.test.js
@@ -5,7 +5,7 @@ import {
buildFretLabel,
MICRO_LABEL_STYLES,
sampleLabels,
-} from "@/utils/fretLabels";
+} from "@shared/lib/fretLabels";
// ───────────────── 12‑TET (baseline) ─────────────────
test("12-TET integers only across styles", () => {
diff --git a/src/tests/numberField.test.js b/src/tests/shared/numberField.test.js
similarity index 96%
rename from src/tests/numberField.test.js
rename to src/tests/shared/numberField.test.js
index f961fc2..5327a57 100644
--- a/src/tests/numberField.test.js
+++ b/src/tests/shared/numberField.test.js
@@ -1,6 +1,6 @@
import test from "node:test";
import assert from "node:assert/strict";
-import { commitNumberField } from "@/hooks/useNumberField";
+import { commitNumberField } from "@shared/hooks/useNumberField";
test("commitNumberField submits valid in-range values", () => {
let error = "";
diff --git a/src/tests/SegmentedRadioGroup.test.jsx b/src/tests/shared/segmentedRadioGroup.test.jsx
similarity index 91%
rename from src/tests/SegmentedRadioGroup.test.jsx
rename to src/tests/shared/segmentedRadioGroup.test.jsx
index f244e2f..76541a4 100644
--- a/src/tests/SegmentedRadioGroup.test.jsx
+++ b/src/tests/shared/segmentedRadioGroup.test.jsx
@@ -2,7 +2,7 @@ import test from "node:test";
import assert from "node:assert/strict";
import { renderToStaticMarkup } from "react-dom/server";
-import SegmentedRadioGroup from "@/components/UI/SegmentedRadioGroup";
+import SegmentedRadioGroup from "@shared/ui/SegmentedRadioGroup";
test("segmented group emits disabled styling hooks for disabled options", () => {
const markup = renderToStaticMarkup(
diff --git a/src/tests/useHotkeys.test.js b/src/tests/shared/useHotkeys.test.js
similarity index 97%
rename from src/tests/useHotkeys.test.js
rename to src/tests/shared/useHotkeys.test.js
index df09e84..1ac540c 100644
--- a/src/tests/useHotkeys.test.js
+++ b/src/tests/shared/useHotkeys.test.js
@@ -3,9 +3,9 @@ import assert from "node:assert/strict";
import { isHotkey } from "is-hotkey";
-import { toHotkeyCombo } from "@/hooks/hotkeyUtils";
-import { createShortcutHandler } from "@/hooks/hotkeyHandler";
-import { buildShortcutTableFromRefs } from "@/hooks/hotkeysTable";
+import { toHotkeyCombo } from "@shared/hooks/hotkeyUtils";
+import { createShortcutHandler } from "@shared/hooks/hotkeyHandler";
+import { buildShortcutTableFromRefs } from "@shared/hooks/hotkeysTable";
const KEY_CODES = {
" ": 32,
diff --git a/src/tests/useValidatedStorage.test.js b/src/tests/shared/useValidatedStorage.test.js
similarity index 93%
rename from src/tests/useValidatedStorage.test.js
rename to src/tests/shared/useValidatedStorage.test.js
index 29737ce..861e390 100644
--- a/src/tests/useValidatedStorage.test.js
+++ b/src/tests/shared/useValidatedStorage.test.js
@@ -4,8 +4,8 @@ import assert from "node:assert/strict";
import {
coerceWithFallback,
resolveNextValue,
-} from "@/hooks/validatedStorageUtils";
-import { clamp } from "@/utils/math";
+} from "@shared/hooks/validatedStorageUtils";
+import { clamp } from "@shared/lib/math";
const numberInRange = (min, max, fallback) => (value) => {
if (typeof value === "number" && Number.isFinite(value)) {
diff --git a/tsconfig.json b/tsconfig.json
index 26cc6d1..0db322b 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -20,7 +20,12 @@
"forceConsistentCasingInFileNames": true,
"paths": {
- "@/*": ["./src/*"]
+ "@/*": ["./src/*"],
+ "@app/*": ["./src/app/*"],
+ "@features/*": ["./src/features/*"],
+ "@shared/*": ["./src/shared/*"],
+ "@domain/*": ["./src/domain/*"],
+ "@styles/*": ["./src/styles/*"]
},
"types": ["react", "react-dom", "node"]
diff --git a/vite.config.js b/vite.config.js
index 3d3258e..d282a02 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -164,7 +164,14 @@ export default defineConfig(({ command, mode }) => {
},
resolve: {
- alias: [{ find: "@", replacement: resolve(__dirname, "src") }],
+ alias: [
+ { find: "@app", replacement: resolve(__dirname, "src/app") },
+ { find: "@features", replacement: resolve(__dirname, "src/features") },
+ { find: "@shared", replacement: resolve(__dirname, "src/shared") },
+ { find: "@domain", replacement: resolve(__dirname, "src/domain") },
+ { find: "@styles", replacement: resolve(__dirname, "src/styles") },
+ { find: "@", replacement: resolve(__dirname, "src") },
+ ],
},
};
});
From 7543f3eeeb228676f25148b2e37d5485d3893561 Mon Sep 17 00:00:00 2001
From: Adam <7889445+DMNerd@users.noreply.github.com>
Date: Sun, 17 May 2026 17:33:33 +0200
Subject: [PATCH 2/4] Fix Fretboard hook lint warnings
---
src/features/fretboard/components/Fretboard.jsx | 13 ++++---------
1 file changed, 4 insertions(+), 9 deletions(-)
diff --git a/src/features/fretboard/components/Fretboard.jsx b/src/features/fretboard/components/Fretboard.jsx
index b69b386..2aa276a 100644
--- a/src/features/fretboard/components/Fretboard.jsx
+++ b/src/features/fretboard/components/Fretboard.jsx
@@ -413,20 +413,14 @@ const Fretboard = forwardRef(function Fretboard(
}),
[microLabelStyle, accidental],
);
+ const typographyCacheScope = `${microLabelStyle}:${system.divisions}:${width}:${frets}:${strings}:${effectiveDotSize}:${notePlacementMode}`;
const typographyCaches = useMemo(
() => ({
+ scope: typographyCacheScope,
fitByConfig: new Map(),
widthByTextStyle: new Map(),
}),
- [
- microLabelStyle,
- system.divisions,
- width,
- frets,
- strings,
- effectiveDotSize,
- notePlacementMode,
- ],
+ [typographyCacheScope],
);
const fitLabelCached = useCallback(
@@ -936,6 +930,7 @@ const Fretboard = forwardRef(function Fretboard(
fitLabelCached,
measureWidthCached,
safeCapoFret,
+ wireX,
]);
const resolveNotePcFromTarget = useCallback((target) => {
From c3092a5e05645bdbb099bc030998512c44828cac Mon Sep 17 00:00:00 2001
From: Adam <7889445+DMNerd@users.noreply.github.com>
Date: Sun, 17 May 2026 17:48:50 +0200
Subject: [PATCH 3/4] Fix lint formatting after refactor
---
src/app/providers/ToastProvider.jsx | 7 ++++-
.../export/components/ExportControls.jsx | 5 +++-
.../instrument/hooks/useMergedPresets.js | 5 +++-
src/features/share/model/shareCodec.ts | 5 +++-
.../share/model/shareConfigModalModel.ts | 5 +++-
.../fretboardLayoutMemoization.test.js | 4 ++-
.../instrument/storeMigrationParity.test.js | 30 ++++++++++++++-----
.../instrument/storeSelectorStability.test.js | 4 ++-
8 files changed, 50 insertions(+), 15 deletions(-)
diff --git a/src/app/providers/ToastProvider.jsx b/src/app/providers/ToastProvider.jsx
index 8f46ce6..b0634af 100644
--- a/src/app/providers/ToastProvider.jsx
+++ b/src/app/providers/ToastProvider.jsx
@@ -1,4 +1,9 @@
-import { FiAlertTriangle, FiCheckCircle, FiInfo, FiLoader } from "react-icons/fi";
+import {
+ FiAlertTriangle,
+ FiCheckCircle,
+ FiInfo,
+ FiLoader,
+} from "react-icons/fi";
import { Toaster, ToastBar } from "react-hot-toast";
export default function ToastProvider() {
diff --git a/src/features/export/components/ExportControls.jsx b/src/features/export/components/ExportControls.jsx
index d95de5d..267554e 100644
--- a/src/features/export/components/ExportControls.jsx
+++ b/src/features/export/components/ExportControls.jsx
@@ -4,7 +4,10 @@ import { toast } from "react-hot-toast";
import Section from "@shared/ui/Section";
import { withToastPromise } from "@shared/lib/toast";
import { memoWithKeys } from "@shared/lib/memo";
-import { PNG_EXPORT_SCALE, EXPORT_PADDING } from "@features/export/model/scales";
+import {
+ PNG_EXPORT_SCALE,
+ EXPORT_PADDING,
+} from "@features/export/model/scales";
import {
getImportPipelineErrorMessage,
IMPORT_PIPELINE_ERROR_CODES,
diff --git a/src/features/instrument/hooks/useMergedPresets.js b/src/features/instrument/hooks/useMergedPresets.js
index 5b59d41..17bad3c 100644
--- a/src/features/instrument/hooks/useMergedPresets.js
+++ b/src/features/instrument/hooks/useMergedPresets.js
@@ -14,7 +14,10 @@ import {
resolveNeckFilterModeIntentFromBoardMeta,
} from "@domain/presets/neckFilterModes";
import { isPlainObject } from "@shared/lib/object";
-import { coerceAnyTuning, usePresetBuilder } from "@features/instrument/hooks/usePresetBuilder";
+import {
+ coerceAnyTuning,
+ usePresetBuilder,
+} from "@features/instrument/hooks/usePresetBuilder";
import {
useInstrumentWorkflowStore,
selectInstrumentWorkflowActions,
diff --git a/src/features/share/model/shareCodec.ts b/src/features/share/model/shareCodec.ts
index a8ba793..c8ca04d 100644
--- a/src/features/share/model/shareCodec.ts
+++ b/src/features/share/model/shareCodec.ts
@@ -9,7 +9,10 @@ import {
} from "@shared/config/appDefaults";
import { TUNINGS } from "@domain/theory/tuning";
import { SHARE_FIELD_SELECTORS } from "@features/share/model/shareScopes";
-import { SHARE_QUERY_KEYS, SHARE_SCHEMA_VERSION } from "@features/share/model/shareSchema";
+import {
+ SHARE_QUERY_KEYS,
+ SHARE_SCHEMA_VERSION,
+} from "@features/share/model/shareSchema";
import {
coerceNeckFilterMode,
isNeckFilterMode,
diff --git a/src/features/share/model/shareConfigModalModel.ts b/src/features/share/model/shareConfigModalModel.ts
index 5bf242e..8aa90ab 100644
--- a/src/features/share/model/shareConfigModalModel.ts
+++ b/src/features/share/model/shareConfigModalModel.ts
@@ -1,5 +1,8 @@
import { serializeShareState } from "@features/share/model/shareState";
-import { buildSharePayload, serializeSharePayload } from "@features/share/model/shareCodec";
+import {
+ buildSharePayload,
+ serializeSharePayload,
+} from "@features/share/model/shareCodec";
import { evaluateShareUrlSize } from "@features/share/model/shareLimits";
export function buildShareConfigModalModel({
diff --git a/src/tests/features/fretboard/fretboardLayoutMemoization.test.js b/src/tests/features/fretboard/fretboardLayoutMemoization.test.js
index 1d6f36c..9bb969f 100644
--- a/src/tests/features/fretboard/fretboardLayoutMemoization.test.js
+++ b/src/tests/features/fretboard/fretboardLayoutMemoization.test.js
@@ -50,7 +50,9 @@ test("layout values update immediately when stringMeta changes", () => {
});
test("useFretboardLayout derives metaByIndex during render via useMemo", () => {
- const hookPath = path.resolve("src/features/fretboard/hooks/useFretboardLayout.js");
+ const hookPath = path.resolve(
+ "src/features/fretboard/hooks/useFretboardLayout.js",
+ );
const source = fs.readFileSync(hookPath, "utf8");
assert.match(
diff --git a/src/tests/features/instrument/storeMigrationParity.test.js b/src/tests/features/instrument/storeMigrationParity.test.js
index 1b14fb2..238c6df 100644
--- a/src/tests/features/instrument/storeMigrationParity.test.js
+++ b/src/tests/features/instrument/storeMigrationParity.test.js
@@ -132,7 +132,9 @@ test("legacy theory keys hydrate into new theory store and clear old keys", asyn
storage.setItem(STORAGE_KEYS.SYSTEM_ID, "24-TET");
storage.setItem(STORAGE_KEYS.ROOT, "D");
- const { useTheoryStore } = await importFresh("@features/theory/store/useTheoryStore.js");
+ const { useTheoryStore } = await importFresh(
+ "@features/theory/store/useTheoryStore.js",
+ );
await useTheoryStore.persist.rehydrate();
const state = useTheoryStore.getState();
@@ -155,7 +157,9 @@ test("theory store prefers valid persisted payload over legacy keys", async () =
storage.setItem(STORAGE_KEYS.SYSTEM_ID, "24-TET");
storage.setItem(STORAGE_KEYS.ROOT, "D");
- const { useTheoryStore } = await importFresh("@features/theory/store/useTheoryStore.js");
+ const { useTheoryStore } = await importFresh(
+ "@features/theory/store/useTheoryStore.js",
+ );
await useTheoryStore.persist.rehydrate();
const state = useTheoryStore.getState();
@@ -169,7 +173,9 @@ test("theory store prefers valid persisted payload over legacy keys", async () =
test("musical reset clears capo-relative chord mode through theory reset", async () => {
storage.clear();
- const { useTheoryStore } = await importFresh("@features/theory/store/useTheoryStore.js");
+ const { useTheoryStore } = await importFresh(
+ "@features/theory/store/useTheoryStore.js",
+ );
const { resetMusicalStateFromRefs } = await importFresh(
"@features/theory/hooks/resetMusicalState.js",
);
@@ -483,7 +489,9 @@ test("global stores migrate scoped payloads back to unscoped keys", async () =>
}),
);
- const { useThemeStore } = await importFresh("@features/display/store/useThemeStore.js");
+ const { useThemeStore } = await importFresh(
+ "@features/display/store/useThemeStore.js",
+ );
await useThemeStore.persist.rehydrate();
const unscopedPersisted = readStoredJson(STORAGE_KEYS.THEME);
@@ -586,7 +594,9 @@ test("metronome prefs setters and engine reset semantics remain distinct", async
test("theory and workflow action names and behaviors remain stable", async () => {
storage.clear();
- const { useTheoryStore } = await importFresh("@features/theory/store/useTheoryStore.js");
+ const { useTheoryStore } = await importFresh(
+ "@features/theory/store/useTheoryStore.js",
+ );
const { useInstrumentWorkflowStore } = await importFresh(
"@features/instrument/store/useInstrumentWorkflowStore.js",
);
@@ -677,8 +687,10 @@ test("resetAllStores restores defaults and clears only app-owned keys", async ()
await import("@features/instrument/store/useInstrumentWorkflowStore.js");
const { useMetronomeEngineStore } =
await import("@features/practice/store/useMetronomeEngineStore.js");
- const { useTheoryStore } = await import("@features/theory/store/useTheoryStore.js");
- const { useThemeStore } = await import("@features/display/store/useThemeStore.js");
+ const { useTheoryStore } =
+ await import("@features/theory/store/useTheoryStore.js");
+ const { useThemeStore } =
+ await import("@features/display/store/useThemeStore.js");
useDisplayPrefsStore.getState().setPrefs({ accidental: "flat", dotSize: 20 });
useMetronomePrefsStore.getState().setPrefs({ bpm: 132, timeSig: "5/4" });
@@ -767,7 +779,9 @@ test("resetAllStores only clears instrument scoped keys for the active window",
test("generated immer setters preserve non-target keys on full-store drafts", async () => {
storage.clear();
- const { useTheoryStore } = await importFresh("@features/theory/store/useTheoryStore.js");
+ const { useTheoryStore } = await importFresh(
+ "@features/theory/store/useTheoryStore.js",
+ );
const { useInstrumentWorkflowStore } = await importFresh(
"@features/instrument/store/useInstrumentWorkflowStore.js",
);
diff --git a/src/tests/features/instrument/storeSelectorStability.test.js b/src/tests/features/instrument/storeSelectorStability.test.js
index 73e6012..bb272cd 100644
--- a/src/tests/features/instrument/storeSelectorStability.test.js
+++ b/src/tests/features/instrument/storeSelectorStability.test.js
@@ -150,7 +150,9 @@ test("display controls selector only changes when selected pref changes", async
test("preset picker selector ignores workflow modal/editor updates", async () => {
const { useInstrumentWorkflowStore, selectWorkflowSelectedPreset } =
- await importFresh("@features/instrument/store/useInstrumentWorkflowStore.js");
+ await importFresh(
+ "@features/instrument/store/useInstrumentWorkflowStore.js",
+ );
useInstrumentWorkflowStore.setState({
customTunings: [],
selectedPreset: "Factory default",
From ad41ad8c3065917128217e6a9b8025c24bf89b4a Mon Sep 17 00:00:00 2001
From: Adam <7889445+DMNerd@users.noreply.github.com>
Date: Sun, 17 May 2026 17:54:27 +0200
Subject: [PATCH 4/4] Remove unnecessary domain type assertions
---
src/domain/meta/meta.ts | 8 ++++----
src/domain/presets/neckFilterModes.ts | 4 ++--
src/domain/presets/presets.ts | 2 +-
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/src/domain/meta/meta.ts b/src/domain/meta/meta.ts
index 6923f65..b6bb844 100644
--- a/src/domain/meta/meta.ts
+++ b/src/domain/meta/meta.ts
@@ -180,7 +180,9 @@ export function normalizePresetMeta(
return null;
}
- const result: Record = {};
+ const result: TuningPresetMeta & {
+ stringMeta?: NormalizedStringMeta | null;
+ } = {};
if (stringMeta) {
result.stringMeta = stringMeta;
}
@@ -188,7 +190,5 @@ export function normalizePresetMeta(
result.board = board;
}
- return result as TuningPresetMeta & {
- stringMeta?: NormalizedStringMeta | null;
- };
+ return result;
}
diff --git a/src/domain/presets/neckFilterModes.ts b/src/domain/presets/neckFilterModes.ts
index eff9cbc..bb78f00 100644
--- a/src/domain/presets/neckFilterModes.ts
+++ b/src/domain/presets/neckFilterModes.ts
@@ -75,7 +75,7 @@ export function stripFretlessStyle(
boardMeta: unknown,
): Record | null {
if (!isPlainObject(boardMeta)) return null;
- const next = { ...boardMeta } as Record;
+ const next = { ...boardMeta };
if (
next.fretStyle === FRETLESS_BOARD_META.fretStyle &&
next.notePlacement === FRETLESS_BOARD_META.notePlacement
@@ -272,7 +272,7 @@ export function sanitizeBoardMetaForModeStorage(
): Record | null {
if (!isPlainObject(boardMeta)) return null;
- const normalized = { ...boardMeta } as Record;
+ const normalized = { ...boardMeta };
if (normalized.neckFilterMode === NECK_FILTER_MODES.FRETLESS) {
if (normalized.fretStyle === FRETLESS_BOARD_META.fretStyle) {
delete normalized.fretStyle;
diff --git a/src/domain/presets/presets.ts b/src/domain/presets/presets.ts
index 7faab70..91faf58 100644
--- a/src/domain/presets/presets.ts
+++ b/src/domain/presets/presets.ts
@@ -414,7 +414,7 @@ function buildPresetMetaMap(
}
}
- return freezeDeep(out) as PresetMetaMap;
+ return freezeDeep(out);
}
const FRETLESS_BOARD_META: TuningPresetMeta = {