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