diff --git a/example/babel.config.js b/example/babel.config.js index 21cd9cb..c4995b5 100644 --- a/example/babel.config.js +++ b/example/babel.config.js @@ -1,17 +1,43 @@ const path = require('path'); -const { getConfig } = require('react-native-builder-bob/babel-config'); const pkg = require('../packages/ui/package.json'); -const root = path.resolve(__dirname, '../packages/ui'); +const libraryRoot = path.resolve(__dirname, '../packages/ui'); +const tokensRoot = path.resolve(__dirname, '../packages/tokens'); +const librarySource = path.join(libraryRoot, 'src'); module.exports = function (api) { api.cache(true); - return getConfig( - { - presets: ['babel-preset-expo'], - plugins: ['react-native-reanimated/plugin'], - }, - { root, pkg } - ); + return { + presets: ['babel-preset-expo'], + plugins: ['react-native-reanimated/plugin'], + overrides: [ + { + exclude: /\/node_modules\//, + plugins: [ + [ + require.resolve('babel-plugin-module-resolver'), + { + extensions: ['.tsx', '.ts', '.jsx', '.js', '.json'], + alias: { + 'react-native-circuit-ui': path.join(libraryRoot, pkg.source), + '@circuit-ui/tokens': path.join(tokensRoot, 'src/index.ts'), + }, + }, + ], + ], + }, + { + include: librarySource, + presets: [ + [ + require.resolve('react-native-builder-bob/babel-preset'), + { + supportsStaticESM: true, + }, + ], + ], + }, + ], + }; }; diff --git a/example/metro.config.js b/example/metro.config.js index a6f7a47..654bb5a 100644 --- a/example/metro.config.js +++ b/example/metro.config.js @@ -23,13 +23,38 @@ config.server = { port: 8083, }; +const defaultResolveRequest = config.resolver?.resolveRequest; + // Allow Metro to resolve workspace packages hoisted to root node_modules config.resolver = { ...config.resolver, + extraNodeModules: { + ...(config.resolver.extraNodeModules ?? {}), + 'react-native-circuit-ui': path.resolve(repoRoot, 'packages/ui/src'), + '@circuit-ui/tokens': path.resolve(repoRoot, 'packages/tokens/src'), + }, nodeModulesPaths: [ path.resolve(repoRoot, 'node_modules'), path.resolve(__dirname, 'node_modules'), ], + resolveRequest(context, moduleName, platform) { + if (moduleName === 'react-native-circuit-ui') { + return { + type: 'sourceFile', + filePath: path.resolve(repoRoot, 'packages/ui/src/index.tsx'), + }; + } + + if (moduleName === '@circuit-ui/tokens') { + return { + type: 'sourceFile', + filePath: path.resolve(repoRoot, 'packages/tokens/src/index.ts'), + }; + } + + const resolver = defaultResolveRequest ?? context.resolveRequest; + return resolver(context, moduleName, platform); + }, }; // Watch packages/tokens so hot reload works when token values change diff --git a/example/package.json b/example/package.json index 082c7b3..7a736a6 100644 --- a/example/package.json +++ b/example/package.json @@ -22,7 +22,8 @@ "react-native-gesture-handler": "~2.20.2", "react-native-reanimated": "~3.16.7", "react-native-svg": "^15.11.2", - "react-native-web": "~0.19.13" + "react-native-web": "~0.19.13", + "zustand": "^5.0.0" }, "devDependencies": { "@babel/core": "^7.20.0", diff --git a/example/src/FeaturesShowcase.tsx b/example/src/FeaturesShowcase.tsx index e0b2566..ded31fe 100644 --- a/example/src/FeaturesShowcase.tsx +++ b/example/src/FeaturesShowcase.tsx @@ -12,15 +12,15 @@ import { } from 'react-native-circuit-ui'; // Feature imports -import { DashboardTabBar } from '../../src/features/dashboard'; -import type { DashboardTab } from '../../src/features/dashboard'; -import { WelcomeView } from '../../src/features/welcome'; -import { ProfileCard, SignInView } from '../../src/features/account'; -import { FeaturedCard } from '../../src/features/discover'; -import { TrophiesView } from '../../src/features/trophies'; -import type { Trophy } from '../../src/features/trophies'; -import { MyCircuitsView } from '../../src/features/circuits'; -import type { MyCircuitsState } from '../../src/features/circuits'; +import { DashboardTabBar } from '../../packages/ui/src/features/dashboard'; +import type { DashboardTab } from '../../packages/ui/src/features/dashboard'; +import { WelcomeView } from '../../packages/ui/src/features/welcome'; +import { ProfileCard, SignInView } from '../../packages/ui/src/features/account'; +import { FeaturedCard } from '../../packages/ui/src/features/discover'; +import { TrophiesView } from '../../packages/ui/src/features/trophies'; +import type { Trophy } from '../../packages/ui/src/features/trophies'; +import { MyCircuitsView } from '../../packages/ui/src/features/circuits'; +import type { MyCircuitsState } from '../../packages/ui/src/features/circuits'; const Section = ({ title, diff --git a/example/src/PlaygroundShowcase.tsx b/example/src/PlaygroundShowcase.tsx index be9e435..2e264ab 100644 --- a/example/src/PlaygroundShowcase.tsx +++ b/example/src/PlaygroundShowcase.tsx @@ -10,20 +10,20 @@ import { create } from 'zustand'; import { SongToolbar, SongMixerTabBar, -} from '../../src/features/playground/components/SongView'; -import { TrackView } from '../../src/features/playground/components/TrackView'; -import { MixerView } from '../../src/features/playground/components/Mixer'; -import { DrumPadsView } from '../../src/features/playground/components/DrumPads'; -import { PianoKeyboard } from '../../src/features/playground/components/PianoKeyboard'; -import { SongSectionsView } from '../../src/features/playground/components/Sections'; -import { SoundBankView } from '../../src/features/playground/components/SoundBank'; -import { PlaygroundsDashboard } from '../../src/features/playground/components/PlaygroundsDashboard'; -import { AddTrackMenu } from '../../src/features/playground/components/Toolbar'; -import { OnboardingSheet } from '../../src/features/playground/components/Onboarding'; -import { ClipEditorView } from '../../src/features/playground/components/ClipEditor'; -import { BottomPanel } from '../../src/features/playground/components/BottomPanel'; -import { SongStoreProvider } from '../../src/features/playground/stores/playgroundStore'; -import type { SongStore } from '../../src/features/playground/stores/playgroundStore'; +} from '../../packages/ui/src/features/playground/components/SongView'; +import { TrackView } from '../../packages/ui/src/features/playground/components/TrackView'; +import { MixerView } from '../../packages/ui/src/features/playground/components/Mixer'; +import { DrumPadsView } from '../../packages/ui/src/features/playground/components/DrumPads'; +import { PianoKeyboard } from '../../packages/ui/src/features/playground/components/PianoKeyboard'; +import { SongSectionsView } from '../../packages/ui/src/features/playground/components/Sections'; +import { SoundBankView } from '../../packages/ui/src/features/playground/components/SoundBank'; +import { PlaygroundsDashboard } from '../../packages/ui/src/features/playground/components/PlaygroundsDashboard'; +import { AddTrackMenu } from '../../packages/ui/src/features/playground/components/Toolbar'; +import { OnboardingSheet } from '../../packages/ui/src/features/playground/components/Onboarding'; +import { ClipEditorView } from '../../packages/ui/src/features/playground/components/ClipEditor'; +import { BottomPanel } from '../../packages/ui/src/features/playground/components/BottomPanel'; +import { SongStoreProvider } from '../../packages/ui/src/features/playground/stores/playgroundStore'; +import type { SongStore } from '../../packages/ui/src/features/playground/stores/playgroundStore'; // Mock data import { @@ -31,7 +31,7 @@ import { createMockPlaygroundsList, createMockDrumClip, createDrumSamples, -} from '../../src/features/playground/mocks'; +} from '../../packages/ui/src/features/playground/mocks'; // Create a mock store for showcase components that use useSongContext const noop = () => {}; diff --git a/package.json b/package.json index 0551629..a238a7a 100644 --- a/package.json +++ b/package.json @@ -8,53 +8,18 @@ "packageManager": "yarn@3.6.1", "scripts": { "build": "yarn workspaces foreach -A run prepare", + "prepare": "yarn workspace @circuit-ui/tokens prepare && yarn workspace @circuit-ui/core prepare", "test": "yarn workspace @circuit-ui/core test:unit", "test:visual": "yarn workspace @circuit-ui/core test:visual", "test:unit": "yarn workspace @circuit-ui/core test:unit", "test:screenshots": "./scripts/capture-screenshots.sh", "typecheck": "yarn workspaces foreach -A run typecheck", - "lint": "eslint \"packages/*/src/**/*.{js,ts,tsx}\"", - "example": "yarn workspace @circuit-ui-example" + "lint": "yarn workspace @circuit-ui/core lint", + "example": "yarn workspace react-native-circuit-ui-example" }, "commitlint": { "extends": [ "@commitlint/config-conventional" ] - }, - "eslintConfig": { - "root": true, - "extends": [ - "@react-native", - "prettier" - ], - "rules": { - "react/react-in-jsx-scope": "off", - "prettier/prettier": [ - "error", - { - "quoteProps": "consistent", - "singleQuote": true, - "tabWidth": 2, - "trailingComma": "es5", - "useTabs": false - } - ], - "react-native/no-inline-styles": "off", - "no-bitwise": "off", - "no-void": "off", - "no-shadow": "off", - "@typescript-eslint/no-shadow": "off" - } - }, - "eslintIgnore": [ - "node_modules/", - "packages/*/lib/" - ], - "prettier": { - "quoteProps": "consistent", - "singleQuote": true, - "tabWidth": 2, - "trailingComma": "es5", - "useTabs": false } } diff --git a/packages/ui/package.json b/packages/ui/package.json index 6c7c716..4642a5f 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -323,5 +323,41 @@ "infile": "CHANGELOG.md" } } + }, + "eslintConfig": { + "root": true, + "extends": [ + "@react-native", + "prettier" + ], + "rules": { + "react/react-in-jsx-scope": "off", + "prettier/prettier": [ + "error", + { + "quoteProps": "consistent", + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "useTabs": false + } + ], + "react-native/no-inline-styles": "off", + "no-bitwise": "off", + "no-void": "off", + "no-shadow": "off", + "@typescript-eslint/no-shadow": "off" + } + }, + "eslintIgnore": [ + "node_modules/", + "packages/*/lib/" + ], + "prettier": { + "quoteProps": "consistent", + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "useTabs": false } } diff --git a/packages/ui/src/components/FeedbackView/FeedbackView.tsx b/packages/ui/src/components/FeedbackView/FeedbackView.tsx index 0f1b1e6..cff6c9e 100644 --- a/packages/ui/src/components/FeedbackView/FeedbackView.tsx +++ b/packages/ui/src/components/FeedbackView/FeedbackView.tsx @@ -7,7 +7,13 @@ * iOS and Android so both platforms stay at parity. */ import { memo, useState } from 'react'; -import { View, ScrollView, TextInput, Pressable, StyleSheet } from 'react-native'; +import { + View, + ScrollView, + TextInput, + Pressable, + StyleSheet, +} from 'react-native'; import { Text } from '../Text'; import { Button } from '../Button'; import { Icon, Icons } from '../SFSymbol'; @@ -16,7 +22,17 @@ import { makeSpacing } from '../../theme/spacing'; /** Same emoji set as the native FeedbackView, same order. */ export const FEEDBACK_EMOJIS = [ - 'πŸ₯΅', 'πŸ˜’', '🀨', '😎', '🀩', '🀘', 'πŸŽ‰', '🀯', 'πŸ”₯', 'πŸš€', 'πŸ’‘', + 'πŸ₯΅', + 'πŸ˜’', + '🀨', + '😎', + '🀩', + '🀘', + 'πŸŽ‰', + '🀯', + 'πŸ”₯', + 'πŸš€', + 'πŸ’‘', ] as const; export interface FeedbackViewProps { @@ -44,14 +60,18 @@ export const FeedbackView = memo(function FeedbackView({ const toggleEmoji = (emoji: string) => setSelected((prev) => - prev.includes(emoji) ? prev.filter((e) => e !== emoji) : [...prev, emoji], + prev.includes(emoji) ? prev.filter((e) => e !== emoji) : [...prev, emoji] ); // Allow sending once there's at least an emoji or some text. const canSend = selected.length > 0 || feedback.trim().length > 0; return ( - + {/* Header */} @@ -67,9 +87,16 @@ export const FeedbackView = memo(function FeedbackView({ - + {/* Emoji multi-select */} - + Pick as many as you like {/* 5-column grid, matching the native FeedbackView (LazyVGrid). */} @@ -88,7 +115,13 @@ export const FeedbackView = memo(function FeedbackView({ isSelected && { backgroundColor: colors.mcWhite4 }, ]} > - {emoji} + + {emoji} + ); @@ -97,8 +130,12 @@ export const FeedbackView = memo(function FeedbackView({ {/* Free text */} - Anything we could do better? - Every bit of feedback helps! + + Anything we could do better? + + + Every bit of feedback helps! + @@ -124,8 +164,15 @@ export const FeedbackView = memo(function FeedbackView({ style={styles.sendButton} /> {onEmailDirectly && ( - - Prefer to email us directly? + + + Prefer to email us directly? + )} diff --git a/packages/ui/src/components/Hint/WithHint.tsx b/packages/ui/src/components/Hint/WithHint.tsx index 59ebc46..0aab240 100644 --- a/packages/ui/src/components/Hint/WithHint.tsx +++ b/packages/ui/src/components/Hint/WithHint.tsx @@ -48,7 +48,7 @@ export const WithHint = memo(function WithHint({ if (dismissTimer.current) clearTimeout(dismissTimer.current); dismissTimer.current = setTimeout( () => setTooltipVisible(false), - TOOLTIP_DISMISS_MS, + TOOLTIP_DISMISS_MS ); } else { setTooltipVisible(false); @@ -66,7 +66,10 @@ export const WithHint = memo(function WithHint({ {children} {active && ( - + )} @@ -76,7 +79,9 @@ export const WithHint = memo(function WithHint({ pointerEvents="none" style={[ styles.tooltipLayer, - tooltipPlacement === 'below' ? styles.tooltipBelow : styles.tooltipAbove, + tooltipPlacement === 'below' + ? styles.tooltipBelow + : styles.tooltipAbove, ]} > {tooltipPlacement === 'below' && ( @@ -88,7 +93,11 @@ export const WithHint = memo(function WithHint({ { backgroundColor: tooltipBg, borderColor: tooltipBorder }, ]} > - + {tip} diff --git a/packages/ui/src/components/NotePrecisionPanel/NotePrecisionPanel.tsx b/packages/ui/src/components/NotePrecisionPanel/NotePrecisionPanel.tsx index cf53194..48767dc 100644 --- a/packages/ui/src/components/NotePrecisionPanel/NotePrecisionPanel.tsx +++ b/packages/ui/src/components/NotePrecisionPanel/NotePrecisionPanel.tsx @@ -78,7 +78,8 @@ export const NotePrecisionPanel = memo(function NotePrecisionPanel({ // Computed here so CanvasKit is ready when matchFont runs (avoids web crash) const velFont = useMemo( - () => matchFont({ fontFamily: 'monospace', fontSize: 8, fontWeight: '600' }), + () => + matchFont({ fontFamily: 'monospace', fontSize: 8, fontWeight: '600' }), [] ); diff --git a/packages/ui/src/components/PianoRoll/SkiaPianoRollGrid.tsx b/packages/ui/src/components/PianoRoll/SkiaPianoRollGrid.tsx index 2502f5d..194fa09 100644 --- a/packages/ui/src/components/PianoRoll/SkiaPianoRollGrid.tsx +++ b/packages/ui/src/components/PianoRoll/SkiaPianoRollGrid.tsx @@ -44,7 +44,11 @@ import { scheduleOnRN } from 'react-native-worklets'; import { Text } from '../Text'; import { Icon, Icons } from '../SFSymbol'; import { useTheme, hexToRgba } from '../../theme'; -import type { ClipNote, InstrumentType, Sample } from '../../features/playground/types'; +import type { + ClipNote, + InstrumentType, + Sample, +} from '../../features/playground/types'; const LABEL_COL_WIDTH = 60; const DEFAULT_MELODIC_MIN_PITCH = 48; @@ -127,7 +131,8 @@ export const SkiaPianoRollGrid = memo(function SkiaPianoRollGrid({ // Computed here (not module scope) so CanvasKit is ready when matchFont runs const noteFont = useMemo( - () => matchFont({ fontFamily: 'monospace', fontSize: 9, fontWeight: '600' }), + () => + matchFont({ fontFamily: 'monospace', fontSize: 9, fontWeight: '600' }), [] ); diff --git a/packages/ui/src/components/PianoRoll/SkiaPianoRollGrid.web.tsx b/packages/ui/src/components/PianoRoll/SkiaPianoRollGrid.web.tsx index de916b6..306d56c 100644 --- a/packages/ui/src/components/PianoRoll/SkiaPianoRollGrid.web.tsx +++ b/packages/ui/src/components/PianoRoll/SkiaPianoRollGrid.web.tsx @@ -19,7 +19,11 @@ import { import { Text } from '../Text'; import { Icon, Icons } from '../SFSymbol'; import { useTheme, hexToRgba } from '../../theme'; -import type { ClipNote, InstrumentType, Sample } from '../../features/playground/types'; +import type { + ClipNote, + InstrumentType, + Sample, +} from '../../features/playground/types'; const LABEL_COL_WIDTH = 60; const DEFAULT_MELODIC_MIN_PITCH = 48; @@ -56,7 +60,11 @@ export interface SkiaPianoRollGridProps { melodicMinPitch?: number; onNotePress?: (index: number) => void; onNoteResize?: (index: number, newDuration: number) => void; - onNoteMove?: (index: number, newPosition: number, newNoteNumber: number) => void; + onNoteMove?: ( + index: number, + newPosition: number, + newNoteNumber: number + ) => void; onGridTap?: (noteNumber: number, position: number) => void; onPitchLabelTap?: (pitch: number) => void; onToggleExpand?: () => void; @@ -115,7 +123,11 @@ export const SkiaPianoRollGrid = memo(function SkiaPianoRollGrid({ const el = containerRef.current; if (!el) return; const handleWheel = (ev: WheelEvent) => { - const wheel = ev as WheelEvent & { ctrlKey?: boolean; metaKey?: boolean; deltaY?: number }; + const wheel = ev as WheelEvent & { + ctrlKey?: boolean; + metaKey?: boolean; + deltaY?: number; + }; if (!wheel.ctrlKey && !wheel.metaKey) return; ev.preventDefault(); const delta = (wheel.deltaY ?? 0) > 0 ? -0.25 : 0.25; @@ -173,7 +185,11 @@ export const SkiaPianoRollGrid = memo(function SkiaPianoRollGrid({ ); return ( - + {/* Pitch labels */} diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index 568d7f4..e316253 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -39,7 +39,13 @@ export type { ScoreIndicatorProps } from './ScoreIndicator'; export { HintBubble } from './HintBubble'; export type { HintBubbleProps } from './HintBubble'; -export { WithHint, HintProvider, HintContext, useHintContext, HintIDs } from './Hint'; +export { + WithHint, + HintProvider, + HintContext, + useHintContext, + HintIDs, +} from './Hint'; export type { WithHintProps, HintContextValue } from './Hint'; export { DurationLabel } from './DurationLabel'; diff --git a/packages/ui/src/features/playground/components/ClipEditor/ClipEditorView.tsx b/packages/ui/src/features/playground/components/ClipEditor/ClipEditorView.tsx index 0e83f4b..ec350d3 100644 --- a/packages/ui/src/features/playground/components/ClipEditor/ClipEditorView.tsx +++ b/packages/ui/src/features/playground/components/ClipEditor/ClipEditorView.tsx @@ -985,41 +985,41 @@ export const ClipEditorView = memo(function ClipEditorView({ callbacks?.onNoteDelete?.(idx)} - onNoteResize={(idx, newDuration) => - callbacks?.onNoteResize?.(idx, newDuration) - } - onNoteMove={(idx, newPos, newNote) => - callbacks?.onNoteMove?.(idx, newPos, newNote) - } - onGridTap={(noteNumber, position) => { - callbacks?.onNoteAdd?.({ - noteNumber, - velocity: 100, - position, - duration: 0.25, - }); - }} - onPitchLabelTap={(pitch) => { - setSelectedPitchIndex( - selectedPitchIndex === pitch ? null : pitch - ); - }} - onToggleExpand={() => setIsExpanded(!isExpanded)} - onZoomIn={() => setZoom(Math.min(zoom + 0.25, 3))} - onZoomOut={() => setZoom(Math.max(zoom - 0.25, 1))} - onZoomChange={(z) => setZoom(Math.max(1, Math.min(3, z)))} - showNoteLabels={showPianoNoteNames} - /> + notes={clip.notes} + samples={samplesList} + instrumentType={instrumentType} + trackColor={trackColor} + lengthInBeats={clip.activeLengthInBars * 4} + zoomLevel={zoom} + isExpanded={isExpanded} + selectedPitchIndex={selectedPitchIndex} + melodicMinPitch={melodicMinPitch} + onNotePress={(idx) => callbacks?.onNoteDelete?.(idx)} + onNoteResize={(idx, newDuration) => + callbacks?.onNoteResize?.(idx, newDuration) + } + onNoteMove={(idx, newPos, newNote) => + callbacks?.onNoteMove?.(idx, newPos, newNote) + } + onGridTap={(noteNumber, position) => { + callbacks?.onNoteAdd?.({ + noteNumber, + velocity: 100, + position, + duration: 0.25, + }); + }} + onPitchLabelTap={(pitch) => { + setSelectedPitchIndex( + selectedPitchIndex === pitch ? null : pitch + ); + }} + onToggleExpand={() => setIsExpanded(!isExpanded)} + onZoomIn={() => setZoom(Math.min(zoom + 0.25, 3))} + onZoomOut={() => setZoom(Math.max(zoom - 0.25, 1))} + onZoomChange={(z) => setZoom(Math.max(1, Math.min(3, z)))} + showNoteLabels={showPianoNoteNames} + /> diff --git a/packages/ui/src/features/playground/components/ClipEditor/ClipSettingsModal.tsx b/packages/ui/src/features/playground/components/ClipEditor/ClipSettingsModal.tsx index b20351b..532b073 100644 --- a/packages/ui/src/features/playground/components/ClipEditor/ClipSettingsModal.tsx +++ b/packages/ui/src/features/playground/components/ClipEditor/ClipSettingsModal.tsx @@ -74,7 +74,9 @@ export const ClipSettingsModal = memo(function ClipSettingsModal({ {/* Tempo β€” label, value and slider grouped so the divider sits below the whole control, not between the label and its slider. */} - + Tempo diff --git a/packages/ui/src/features/playground/components/DrumPads/DrumPadsView.web.tsx b/packages/ui/src/features/playground/components/DrumPads/DrumPadsView.web.tsx index 5015420..0227972 100644 --- a/packages/ui/src/features/playground/components/DrumPads/DrumPadsView.web.tsx +++ b/packages/ui/src/features/playground/components/DrumPads/DrumPadsView.web.tsx @@ -25,10 +25,22 @@ function visualToSample(visualIndex: number): number { // QWERTY β†’ visual grid index (row-major, top-left = 0) const DRUM_KEY_MAP: Record = { - q: 0, w: 1, e: 2, r: 3, - a: 4, s: 5, d: 6, f: 7, - z: 8, x: 9, c: 10, v: 11, - '1': 12, '2': 13, '3': 14, '4': 15, + 'q': 0, + 'w': 1, + 'e': 2, + 'r': 3, + 'a': 4, + 's': 5, + 'd': 6, + 'f': 7, + 'z': 8, + 'x': 9, + 'c': 10, + 'v': 11, + '1': 12, + '2': 13, + '3': 14, + '4': 15, }; function isTypingTarget(): boolean { @@ -105,7 +117,7 @@ export const DrumPadsView = memo(function DrumPadsView({ return () => { window.removeEventListener('keydown', onDown); window.removeEventListener('keyup', onUp); - held.forEach(key => { + held.forEach((key) => { const visualIdx = DRUM_KEY_MAP[key]; if (visualIdx !== undefined) handleNativeRelease(visualIdx); }); diff --git a/packages/ui/src/features/playground/components/ExportAudio/ExportAudioView.tsx b/packages/ui/src/features/playground/components/ExportAudio/ExportAudioView.tsx index 86addfa..f82de41 100644 --- a/packages/ui/src/features/playground/components/ExportAudio/ExportAudioView.tsx +++ b/packages/ui/src/features/playground/components/ExportAudio/ExportAudioView.tsx @@ -54,8 +54,7 @@ export interface ExportAudioViewProps { const READY_COPY = 'Ready to export'; const EMPTY_COPY = 'Nothing to export'; -const DISABLED_REASON = - 'Disabled until the playground contains notes or audio'; +const DISABLED_REASON = 'Disabled until the playground contains notes or audio'; export const ExportAudioView = memo(function ExportAudioView({ visible, @@ -78,7 +77,8 @@ export const ExportAudioView = memo(function ExportAudioView({ const exportDisabled = !canExport || isExporting; const gradientId = coverId ?? songId ?? 'export-cover'; - const hasName = typeof playgroundName === 'string' && playgroundName.length > 0; + const hasName = + typeof playgroundName === 'string' && playgroundName.length > 0; const handleExport = async () => { if (exportDisabled) return; diff --git a/packages/ui/src/features/playground/components/ExportAudio/__tests__/ExportAudioView.test.tsx b/packages/ui/src/features/playground/components/ExportAudio/__tests__/ExportAudioView.test.tsx index b28d9ec..8448c7b 100644 --- a/packages/ui/src/features/playground/components/ExportAudio/__tests__/ExportAudioView.test.tsx +++ b/packages/ui/src/features/playground/components/ExportAudio/__tests__/ExportAudioView.test.tsx @@ -94,7 +94,10 @@ describe('ExportAudioView', () => { id: 1, type: 'audio', clips: [ - createMockClip({ notes: [], audioFileReference: 'recordings/take-1.wav' }), + createMockClip({ + notes: [], + audioFileReference: 'recordings/take-1.wav', + }), ], }); const store = createTestStore({ tracks: [audioTrack] } as any); diff --git a/packages/ui/src/features/playground/components/PianoKeyboard/PianoKeyboard.web.tsx b/packages/ui/src/features/playground/components/PianoKeyboard/PianoKeyboard.web.tsx index d23e5b9..129c60a 100644 --- a/packages/ui/src/features/playground/components/PianoKeyboard/PianoKeyboard.web.tsx +++ b/packages/ui/src/features/playground/components/PianoKeyboard/PianoKeyboard.web.tsx @@ -15,11 +15,22 @@ const SQUARE_SHARPS = new Set([8, 20]); const HAS_LEFT = new Set([1, 6, 13, 18]); const HAS_RIGHT = new Set([3, 10, 15, 22]); const NOTE_NAMES: Record = { - 0: 'C', 2: 'D', 4: 'E', 5: 'F', 7: 'G', 9: 'A', 11: 'B', + 0: 'C', + 2: 'D', + 4: 'E', + 5: 'F', + 7: 'G', + 9: 'A', + 11: 'B', }; const NOTE_COLORS: Record = { - 0: palette.mcOrange, 2: palette.mcBlue, 4: palette.mcGreen, - 5: palette.mcPink, 7: palette.mcPurple, 9: palette.mcYellow, 11: palette.mcBlue, + 0: palette.mcOrange, + 2: palette.mcBlue, + 4: palette.mcGreen, + 5: palette.mcPink, + 7: palette.mcPurple, + 9: palette.mcYellow, + 11: palette.mcBlue, }; const SHARP_H = 56; @@ -27,8 +38,24 @@ const SHARP_H = 56; // Lower octave: A W S E D F T G Y H U J // Upper octave: K O L P ; ' const QWERTY_KEY_MAP: Record = { - a: 0, w: 1, s: 2, e: 3, d: 4, f: 5, t: 6, g: 7, y: 8, h: 9, u: 10, j: 11, - k: 12, o: 13, l: 14, p: 15, ';': 16, "'": 17, + 'a': 0, + 'w': 1, + 's': 2, + 'e': 3, + 'd': 4, + 'f': 5, + 't': 6, + 'g': 7, + 'y': 8, + 'h': 9, + 'u': 10, + 'j': 11, + 'k': 12, + 'o': 13, + 'l': 14, + 'p': 15, + ';': 16, + "'": 17, }; function isTypingTarget(): boolean { @@ -61,18 +88,34 @@ export const PianoKeyboard = memo(function PianoKeyboard({ const [pressed, setPressed] = useState>(new Set()); const totalKeys = numberOfOctaves * 12; - const keys = useMemo(() => Array.from({ length: totalKeys }, (_, i) => i), [totalKeys]); - const octaves = useMemo(() => Array.from({ length: numberOfOctaves }, (_, i) => i), [numberOfOctaves]); + const keys = useMemo( + () => Array.from({ length: totalKeys }, (_, i) => i), + [totalKeys] + ); + const octaves = useMemo( + () => Array.from({ length: numberOfOctaves }, (_, i) => i), + [numberOfOctaves] + ); - const handlePress = useCallback((k: number) => { - setPressed(prev => new Set(prev).add(k)); - onNoteOn?.(k); - }, [onNoteOn]); + const handlePress = useCallback( + (k: number) => { + setPressed((prev) => new Set(prev).add(k)); + onNoteOn?.(k); + }, + [onNoteOn] + ); - const handleRelease = useCallback((k: number) => { - setPressed(prev => { const s = new Set(prev); s.delete(k); return s; }); - onNoteOff?.(k); - }, [onNoteOff]); + const handleRelease = useCallback( + (k: number) => { + setPressed((prev) => { + const s = new Set(prev); + s.delete(k); + return s; + }); + onNoteOff?.(k); + }, + [onNoteOff] + ); // QWERTY keyboard input useEffect(() => { @@ -103,7 +146,7 @@ export const PianoKeyboard = memo(function PianoKeyboard({ window.removeEventListener('keydown', onDown); window.removeEventListener('keyup', onUp); // Release any held keys on unmount to prevent stuck notes - held.forEach(key => { + held.forEach((key) => { const idx = QWERTY_KEY_MAP[key]; if (idx !== undefined) handleRelease(idx); }); @@ -112,8 +155,12 @@ export const PianoKeyboard = memo(function PianoKeyboard({ const renderOctave = (octave: number) => { const start = octave * 12; - const sk = keys.filter(k => k >= start && k < start + 12 && ALL_SHARP.has(k)); - const nk = keys.filter(k => k >= start && k < start + 12 && !ALL_SHARP.has(k)); + const sk = keys.filter( + (k) => k >= start && k < start + 12 && ALL_SHARP.has(k) + ); + const nk = keys.filter( + (k) => k >= start && k < start + 12 && !ALL_SHARP.has(k) + ); return ( @@ -128,7 +175,11 @@ export const PianoKeyboard = memo(function PianoKeyboard({ style={[ styles.sharpKey, sq ? styles.sharpSq : styles.sharpRect, - { backgroundColor: active ? highlightColor : 'rgba(247,247,247,0.8)' }, + { + backgroundColor: active + ? highlightColor + : 'rgba(247,247,247,0.8)', + }, ]} onPressIn={() => handlePress(k)} onPressOut={() => handleRelease(k)} @@ -153,14 +204,25 @@ export const PianoKeyboard = memo(function PianoKeyboard({ key={k} style={[ styles.natKey, - { backgroundColor: active ? highlightColor : 'rgba(247,247,247,0.8)' }, + { + backgroundColor: active + ? highlightColor + : 'rgba(247,247,247,0.8)', + }, ]} onPressIn={() => handlePress(k)} onPressOut={() => handleRelease(k)} > {showNoteNames && NOTE_NAMES[s] && NOTE_COLORS[s] && ( - - + + {NOTE_NAMES[s]} @@ -191,10 +253,25 @@ const styles = StyleSheet.create({ octave: { flex: 1 }, sharpRow: { flexDirection: 'row', height: SHARP_H }, sharpKey: { borderWidth: 1, borderColor: palette.mcBlack }, - sharpRect: { flex: 1, justifyContent: 'center', alignItems: 'center', paddingHorizontal: 12 }, + sharpRect: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + paddingHorizontal: 12, + }, sharpSq: { minWidth: 56, justifyContent: 'center', alignItems: 'center' }, - sharpContent: { flexDirection: 'row', alignItems: 'center', flex: 1, justifyContent: 'center' }, - circle: { width: 34, height: 34, borderRadius: 17, backgroundColor: palette.mcBlack }, + sharpContent: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, + justifyContent: 'center', + }, + circle: { + width: 34, + height: 34, + borderRadius: 17, + backgroundColor: palette.mcBlack, + }, spacer: { flex: 1 }, natRow: { flexDirection: 'row', flex: 1 }, natKey: { @@ -205,6 +282,12 @@ const styles = StyleSheet.create({ alignItems: 'center', paddingBottom: 4, }, - badge: { width: 20, height: 20, borderRadius: 4, justifyContent: 'center', alignItems: 'center' }, + badge: { + width: 20, + height: 20, + borderRadius: 4, + justifyContent: 'center', + alignItems: 'center', + }, badgeTxt: { fontSize: 10 }, }); diff --git a/packages/ui/src/theme/typography.ts b/packages/ui/src/theme/typography.ts index 0de0e02..f7c9f08 100644 --- a/packages/ui/src/theme/typography.ts +++ b/packages/ui/src/theme/typography.ts @@ -11,11 +11,7 @@ */ import type { TextStyle } from 'react-native'; import { Platform } from 'react-native'; -import { - fontSize, - fontWeight, - lineHeight, -} from '@circuit-ui/tokens'; +import { fontSize, fontWeight, lineHeight } from '@circuit-ui/tokens'; import type { FontWeight } from '@circuit-ui/tokens'; export { fontSize, fontWeight, lineHeight }; diff --git a/yarn.lock b/yarn.lock index 333e552..69a37fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12781,6 +12781,7 @@ __metadata: react-native-reanimated: ~3.16.7 react-native-svg: ^15.11.2 react-native-web: ~0.19.13 + zustand: ^5.0.0 languageName: unknown linkType: soft