Skip to content

Commit b95a579

Browse files
adewaleclaude
andcommitted
feat: Add centralized feature flags with Loop Ruler disabled by default
Introduces a centralized feature flags configuration system at src/config/features.ts for managing feature toggles across the app. Feature flags can be overridden via VITE_FEATURE_* environment variables. Current flags: - loopRuler: false (disabled while assessing UX impact) - mixerPanel: true - pitchOverview: true - effectsPanel: true - velocityLane: true - multiplayer: true Audit findings for Loop Ruler disable: - UI: LoopRuler component hidden, no way to set loops via UI - State: loopRegion still exists but resets to null on session load - Audio: Scheduler/timing still respects loopRegion if set via multiplayer - Visual: Steps outside loop still dimmed if loopRegion set via sync - Tests: All 3188 unit tests pass, no E2E tests for LoopRuler To re-enable: set VITE_FEATURE_LOOP_RULER=true in .env 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 5ce154e commit b95a579

File tree

2 files changed

+98
-7
lines changed

2 files changed

+98
-7
lines changed

app/src/components/StepSequencer.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { CursorOverlay } from './CursorOverlay';
1414
import { MixerPanel } from './MixerPanel';
1515
import { LoopRuler } from './LoopRuler';
1616
import { PitchOverview } from './PitchOverview';
17+
import { features } from '../config/features';
1718
import type { LoopRegion } from '../types';
1819
import { DEFAULT_STEP_COUNT } from '../types';
1920
import { detectMirrorDirection } from '../utils/patternOps';
@@ -551,13 +552,15 @@ export function StepSequencer() {
551552
</div>
552553

553554
{/* Phase 31G: Loop ruler above grid - set loop regions by dragging */}
554-
<LoopRuler
555-
totalSteps={longestTrackStepCount}
556-
loopRegion={state.loopRegion ?? null}
557-
onSetLoopRegion={isPublished ? () => {} : handleSetLoopRegion}
558-
currentStep={state.currentStep}
559-
isPlaying={state.isPlaying}
560-
/>
555+
{features.loopRuler && (
556+
<LoopRuler
557+
totalSteps={longestTrackStepCount}
558+
loopRegion={state.loopRegion ?? null}
559+
onSetLoopRegion={isPublished ? () => {} : handleSetLoopRegion}
560+
currentStep={state.currentStep}
561+
isPlaying={state.isPlaying}
562+
/>
563+
)}
561564

562565
{/* Phase 31A: Progress bar above grid - shows playback position */}
563566
<div

app/src/config/features.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/**
2+
* Centralized Feature Flags Configuration
3+
*
4+
* Controls which features are enabled/disabled across the application.
5+
* Flags can be overridden via environment variables (VITE_FEATURE_*).
6+
*
7+
* Usage:
8+
* import { features } from '../config/features';
9+
* if (features.loopRuler) { ... }
10+
*
11+
* Environment variable override:
12+
* VITE_FEATURE_LOOP_RULER=true // Enable loop ruler
13+
* VITE_FEATURE_LOOP_RULER=false // Disable loop ruler
14+
*
15+
* @see specs/FEATURES.md for feature documentation
16+
*/
17+
18+
/**
19+
* Parse a boolean environment variable
20+
* Returns defaultValue if not set, true if 'true'/'1', false otherwise
21+
*/
22+
function parseEnvBool(envVar: string | undefined, defaultValue: boolean): boolean {
23+
if (envVar === undefined || envVar === '') {
24+
return defaultValue;
25+
}
26+
return envVar === 'true' || envVar === '1';
27+
}
28+
29+
/**
30+
* Feature flags object - import this to check feature states
31+
*/
32+
export const features = {
33+
/**
34+
* Loop Ruler - UI for setting loop regions by dragging
35+
* Default: false (disabled while we assess UX impact)
36+
* The underlying loop region functionality remains in state/audio systems
37+
*/
38+
loopRuler: parseEnvBool(import.meta.env.VITE_FEATURE_LOOP_RULER, false),
39+
40+
/**
41+
* Mixer Panel - Volume/pan controls for tracks
42+
* Default: true (stable feature)
43+
*/
44+
mixerPanel: parseEnvBool(import.meta.env.VITE_FEATURE_MIXER_PANEL, true),
45+
46+
/**
47+
* Pitch Overview - Visual display of track pitches
48+
* Default: true (stable feature)
49+
*/
50+
pitchOverview: parseEnvBool(import.meta.env.VITE_FEATURE_PITCH_OVERVIEW, true),
51+
52+
/**
53+
* Effects Panel - Reverb, delay, etc.
54+
* Default: true (stable feature)
55+
*/
56+
effectsPanel: parseEnvBool(import.meta.env.VITE_FEATURE_EFFECTS_PANEL, true),
57+
58+
/**
59+
* Velocity Lane - Per-step velocity editing
60+
* Default: true (stable feature)
61+
*/
62+
velocityLane: parseEnvBool(import.meta.env.VITE_FEATURE_VELOCITY_LANE, true),
63+
64+
/**
65+
* Multiplayer - Real-time collaboration via WebSocket
66+
* Default: true (stable feature)
67+
*/
68+
multiplayer: parseEnvBool(import.meta.env.VITE_FEATURE_MULTIPLAYER, true),
69+
} as const;
70+
71+
/**
72+
* Type for feature flag keys
73+
*/
74+
export type FeatureFlag = keyof typeof features;
75+
76+
/**
77+
* Check if a feature is enabled
78+
*/
79+
export function isFeatureEnabled(flag: FeatureFlag): boolean {
80+
return features[flag];
81+
}
82+
83+
/**
84+
* Get all feature flags as an object (useful for debugging)
85+
*/
86+
export function getAllFeatureFlags(): Record<FeatureFlag, boolean> {
87+
return { ...features };
88+
}

0 commit comments

Comments
 (0)