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 = {