riffscore 1.0.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Joseph Kotvas
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # RiffScore
2
+
3
+ **RiffScore** is a self-hostable, embeddable sheet music editor for React.
4
+
5
+ Unlike commercial platforms that require users to leave your site or pay subscription fees, RiffScore allows you to embed interactive, editable scores directly into your application.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install riffscore
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```tsx
16
+ import { RiffScore } from 'riffscore';
17
+
18
+ function App() {
19
+ return <RiffScore />;
20
+ }
21
+ ```
22
+
23
+ That's it! RiffScore renders a fully interactive grand staff editor with sensible defaults.
24
+
25
+ ### With Configuration
26
+
27
+ ```tsx
28
+ <RiffScore config={{
29
+ score: {
30
+ staff: 'treble', // 'grand' | 'treble' | 'bass'
31
+ measureCount: 4,
32
+ keySignature: 'G'
33
+ }
34
+ }} />
35
+ ```
36
+
37
+ ### Read-Only Mode
38
+
39
+ ```tsx
40
+ <RiffScore config={{
41
+ ui: { showToolbar: false },
42
+ interaction: { isEnabled: false }
43
+ }} />
44
+ ```
45
+
46
+ See the [Configuration Guide](./docs/CONFIGURATION.md) for all available options.
47
+
48
+ ---
49
+
50
+ ## Documentation
51
+
52
+ | Guide | Description |
53
+ |-------|-------------|
54
+ | 📖 [Configuration](./docs/CONFIGURATION.md) | Complete API reference for config options |
55
+ | 📘 [Architecture](./docs/ARCHITECTURE.md) | Technical reference for developers |
56
+ | 🎨 [Interaction Design](./docs/INTERACTION.md) | Guide to the intuitive editing behavior |
57
+ | 🤝 [Contributing](./docs/CONTRIBUTING.md) | How to set up and contribute to the project |
58
+
59
+ ---
60
+
61
+ ## Features
62
+
63
+ * **Self-Hostable**: No external dependencies or platform lock-in.
64
+ * **Embeddable**: Drop it into any React application.
65
+ * **Configurable**: Full control over UI, interactions, and score content.
66
+ * **SMuFL Compliance**: Beautiful engraving using the [Bravura](https://github.com/steinbergmedia/bravura) font.
67
+ * **Interactive**: Full editing capabilities right in the browser.
68
+ * **Music Engine**: Powered by [Tonal.js](https://github.com/tonaljs/tonal) for music theory logic and [Tone.js](https://tonejs.github.io/) for accurate browser-based playback.
69
+
70
+ ---
71
+
72
+ ## Repository Structure
73
+
74
+ ```
75
+ riffscore/
76
+ ├── src/ ← Library source
77
+ ├── demo/ ← Next.js demo app
78
+ ├── dist/ ← Built library (ESM/CJS/DTS)
79
+ └── tsup.config.ts
80
+ ```
81
+
82
+ ### Development
83
+
84
+ ```bash
85
+ # Install dependencies
86
+ npm install
87
+ cd demo && npm install
88
+
89
+ # Build library
90
+ npm run build
91
+
92
+ # Run demo
93
+ npm run demo:dev
94
+ ```
95
+
96
+ ---
97
+
98
+ ## Coming Soon
99
+
100
+ * **Imperative API**: Programmatically control the score (e.g., `score.addNote(...)`)
101
+ * **Chord Symbols**: Input and playback for lead sheets
102
+ * **Import**: MusicXML import
@@ -0,0 +1,341 @@
1
+ import * as react from 'react';
2
+ import react__default, { ReactNode } from 'react';
3
+ import * as react_jsx_runtime from 'react/jsx-runtime';
4
+
5
+ declare const THEMES: {
6
+ readonly DARK: {
7
+ accent: "#1DA59C";
8
+ background: "#1e293b";
9
+ panelBackground: "rgba(30, 41, 59, 0.8)";
10
+ text: "#e2e8f0";
11
+ secondaryText: "hsla(215, 20%, 65%, 1.00)";
12
+ border: "rgba(255, 255, 255, 0.1)";
13
+ buttonBackground: "rgba(30, 41, 59, 0.8)";
14
+ buttonHoverBackground: "hsla(218, 33%, 28%, 1.00)";
15
+ score: {
16
+ line: "hsla(215, 16%, 47%, 1.00)";
17
+ note: "#e2e8f0";
18
+ fill: "#e2e8f0";
19
+ };
20
+ };
21
+ readonly COOL: {
22
+ accent: "#22d3ee";
23
+ background: "#0f172a";
24
+ panelBackground: "rgba(15, 23, 42, 0.8)";
25
+ text: "#bfdbfe";
26
+ secondaryText: "#60a5fa";
27
+ border: "rgba(255, 255, 255, 0.1)";
28
+ buttonBackground: "rgba(15, 23, 42, 0.8)";
29
+ buttonHoverBackground: "#1e3a8a";
30
+ score: {
31
+ line: "#60a5fa";
32
+ note: "#bfdbfe";
33
+ fill: "#bfdbfe";
34
+ };
35
+ };
36
+ readonly WARM: {
37
+ accent: "#fb923c";
38
+ background: "#1c1917";
39
+ panelBackground: "rgba(28, 25, 23, 0.8)";
40
+ text: "#e7e5e4";
41
+ secondaryText: "#a8a29e";
42
+ border: "rgba(255, 255, 255, 0.1)";
43
+ buttonBackground: "rgba(28, 25, 23, 0.8)";
44
+ buttonHoverBackground: "#292524";
45
+ score: {
46
+ line: "#78716c";
47
+ note: "#e7e5e4";
48
+ fill: "#e7e5e4";
49
+ };
50
+ };
51
+ readonly LIGHT: {
52
+ accent: "#1DA59C";
53
+ background: string;
54
+ panelBackground: string;
55
+ text: "#1e293b";
56
+ secondaryText: "hsla(215, 16%, 47%, 1.00)";
57
+ border: string;
58
+ buttonBackground: string;
59
+ buttonHoverBackground: string;
60
+ score: {
61
+ line: "hsla(215, 20%, 65%, 1.00)";
62
+ note: string;
63
+ fill: string;
64
+ };
65
+ };
66
+ };
67
+ type ThemeName = keyof typeof THEMES;
68
+ interface Theme {
69
+ accent: string;
70
+ background: string;
71
+ panelBackground: string;
72
+ text: string;
73
+ secondaryText: string;
74
+ border: string;
75
+ buttonBackground: string;
76
+ buttonHoverBackground: string;
77
+ score: {
78
+ line: string;
79
+ note: string;
80
+ fill: string;
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Type definitions for the Sheet Music Editor
86
+ *
87
+ * This file defines the data model for scores, staves, measures, events, and notes.
88
+ * The model supports multiple staves for Grand Staff rendering.
89
+ */
90
+ interface Note {
91
+ id: string | number;
92
+ pitch: string | null;
93
+ accidental?: 'sharp' | 'flat' | 'natural' | null;
94
+ tied?: boolean;
95
+ isRest?: boolean;
96
+ }
97
+ interface ScoreEvent {
98
+ id: string | number;
99
+ duration: string;
100
+ dotted: boolean;
101
+ notes: Note[];
102
+ isRest?: boolean;
103
+ tuplet?: {
104
+ ratio: [number, number];
105
+ groupSize: number;
106
+ position: number;
107
+ baseDuration?: string;
108
+ id?: string;
109
+ };
110
+ }
111
+ interface Measure {
112
+ id: string | number;
113
+ events: ScoreEvent[];
114
+ isPickup?: boolean;
115
+ }
116
+ interface Staff {
117
+ id: string | number;
118
+ clef: 'treble' | 'bass' | 'grand';
119
+ keySignature: string;
120
+ measures: Measure[];
121
+ }
122
+ interface Score {
123
+ title: string;
124
+ timeSignature: string;
125
+ keySignature: string;
126
+ bpm: number;
127
+ staves: Staff[];
128
+ }
129
+ /**
130
+ * Represents the current selection state in the editor.
131
+ * Supports Grand Staff by tracking which staff is selected.
132
+ */
133
+ interface Selection {
134
+ staffIndex: number;
135
+ measureIndex: number | null;
136
+ eventId: string | number | null;
137
+ noteId: string | number | null;
138
+ selectedNotes: Array<{
139
+ staffIndex: number;
140
+ measureIndex: number;
141
+ eventId: string | number;
142
+ noteId: string | number | null;
143
+ }>;
144
+ anchor?: {
145
+ staffIndex: number;
146
+ measureIndex: number;
147
+ eventId: string | number;
148
+ noteId: string | number | null;
149
+ } | null;
150
+ }
151
+ /**
152
+ * Utility type for allowing partial nested objects
153
+ */
154
+ type DeepPartial<T> = {
155
+ [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
156
+ };
157
+ /**
158
+ * Staff template options for score generation
159
+ */
160
+ type StaffTemplate = 'grand' | 'treble' | 'bass';
161
+ /**
162
+ * Configuration interface for RiffScore component.
163
+ * Supports two modes:
164
+ * - Generator Mode: Pass `staff` + `measureCount` to create blank scores
165
+ * - Render Mode: Pass `staves` array to load existing compositions
166
+ */
167
+ /**
168
+ * Configuration interface for RiffScore component.
169
+ * Supports two modes:
170
+ * - Generator Mode: Pass `staff` + `measureCount` to create blank scores
171
+ * - Render Mode: Pass `staves` array to load existing compositions
172
+ */
173
+ interface RiffScoreConfig {
174
+ ui: {
175
+ showToolbar: boolean;
176
+ scale: number;
177
+ theme?: ThemeName;
178
+ };
179
+ interaction: {
180
+ isEnabled: boolean;
181
+ enableKeyboard: boolean;
182
+ enablePlayback: boolean;
183
+ };
184
+ score: {
185
+ title: string;
186
+ bpm: number;
187
+ timeSignature: string;
188
+ keySignature: string;
189
+ staff?: StaffTemplate;
190
+ measureCount?: number;
191
+ staves?: Staff[];
192
+ };
193
+ }
194
+
195
+ /**
196
+ * RiffScore Component
197
+ *
198
+ * Configurable React component for rendering and interacting with musical scores.
199
+ * Supports two modes:
200
+ * - Generator Mode: Create blank scores from templates (staff + measureCount)
201
+ * - Render Mode: Load compositions from staves array
202
+ */
203
+
204
+ interface RiffScoreProps {
205
+ config?: DeepPartial<RiffScoreConfig>;
206
+ }
207
+ /**
208
+ * RiffScore - Configurable Music Notation Editor
209
+ *
210
+ * @example
211
+ * // Generator Mode - Create blank grand staff with 4 measures
212
+ * <RiffScore config={{ score: { staff: 'grand', measureCount: 4 } }} />
213
+ *
214
+ * @example
215
+ * // Render Mode - Load existing composition
216
+ * <RiffScore config={{ score: { staves: myStaves } }} />
217
+ *
218
+ * @example
219
+ * // Disable all interaction (read-only display)
220
+ * <RiffScore config={{ interaction: { isEnabled: false } }} />
221
+ */
222
+ declare const RiffScore: react__default.FC<RiffScoreProps>;
223
+
224
+ interface ScoreEditorContentProps {
225
+ scale?: number;
226
+ label?: string;
227
+ showToolbar?: boolean;
228
+ enableKeyboard?: boolean;
229
+ enablePlayback?: boolean;
230
+ }
231
+ declare const ScoreEditorContent: ({ scale, label, showToolbar, enableKeyboard, enablePlayback, }: ScoreEditorContentProps) => react_jsx_runtime.JSX.Element;
232
+ declare const ScoreEditor: ({ scale, label, initialData, }: {
233
+ scale?: number;
234
+ label?: string;
235
+ initialData?: any;
236
+ }) => react_jsx_runtime.JSX.Element;
237
+
238
+ interface ThemeContextType {
239
+ theme: Theme;
240
+ themeName: ThemeName;
241
+ setTheme: (name: ThemeName) => void;
242
+ zoom: number;
243
+ setZoom: (zoom: number) => void;
244
+ }
245
+ declare const ThemeProvider: react__default.FC<{
246
+ children: react__default.ReactNode;
247
+ initialTheme?: ThemeName;
248
+ }>;
249
+ declare const useTheme: () => ThemeContextType;
250
+
251
+ /**
252
+ * Input mode for entry - determines whether canvas clicks create notes or rests.
253
+ */
254
+ type InputMode = 'NOTE' | 'REST';
255
+
256
+ interface Command {
257
+ type: string;
258
+ execute(score: Score): Score;
259
+ undo(score: Score): Score;
260
+ }
261
+
262
+ type EditorState = 'SELECTION_READY' | 'ENTRY_READY' | 'IDLE';
263
+
264
+ /**
265
+ * Main score logic orchestrator hook.
266
+ * Composes focused hooks for measure, note, modifier, and navigation actions.
267
+ */
268
+ declare const useScoreLogic: (initialScore: any) => {
269
+ score: Score;
270
+ selection: Selection;
271
+ editorState: EditorState;
272
+ selectedDurations: string[];
273
+ selectedDots: boolean[];
274
+ selectedTies: boolean[];
275
+ selectedAccidentals: string[];
276
+ setSelection: react.Dispatch<react.SetStateAction<Selection>>;
277
+ previewNote: any;
278
+ setPreviewNote: react.Dispatch<any>;
279
+ history: Command[];
280
+ redoStack: Command[];
281
+ undo: () => void;
282
+ redo: () => void;
283
+ dispatch: (command: Command) => void;
284
+ activeDuration: string;
285
+ setActiveDuration: react.Dispatch<react.SetStateAction<string>>;
286
+ isDotted: boolean;
287
+ setIsDotted: react.Dispatch<react.SetStateAction<boolean>>;
288
+ activeAccidental: "sharp" | "flat" | "natural" | null;
289
+ activeTie: boolean;
290
+ inputMode: InputMode;
291
+ setInputMode: react.Dispatch<react.SetStateAction<InputMode>>;
292
+ toggleInputMode: () => void;
293
+ handleTimeSignatureChange: (newSig: string) => void;
294
+ handleKeySignatureChange: (newKey: string) => void;
295
+ addMeasure: () => void;
296
+ removeMeasure: () => void;
297
+ togglePickup: () => void;
298
+ setGrandStaff: () => void;
299
+ handleMeasureHover: (measureIndex: number | null, hit: any, pitch: string, staffIndex?: number) => void;
300
+ addNoteToMeasure: (measureIndex: number, newNote: any, shouldAutoAdvance?: boolean, placementOverride?: any) => void;
301
+ addChordToMeasure: (measureIndex: number, notes: any[], duration: string, dotted: boolean) => void;
302
+ deleteSelected: () => void;
303
+ handleNoteSelection: (measureIndex: number, eventId: string | number, noteId: string | number | null, staffIndex?: number, isMulti?: boolean, selectAllInEvent?: boolean, isShift?: boolean) => void;
304
+ handleDurationChange: (newDuration: string) => void;
305
+ handleDotToggle: () => void;
306
+ handleAccidentalToggle: (type: "flat" | "natural" | "sharp" | null) => void;
307
+ handleTieToggle: () => void;
308
+ currentQuantsPerMeasure: number;
309
+ scoreRef: react.RefObject<Score>;
310
+ checkDurationValidity: (targetDuration: string) => boolean;
311
+ checkDotValidity: () => boolean;
312
+ updateNotePitch: (measureIndex: number, eventId: string | number, noteId: string | number, newPitch: string) => void;
313
+ applyTuplet: (ratio: [number, number], groupSize: number) => boolean;
314
+ removeTuplet: () => boolean;
315
+ canApplyTuplet: (groupSize: number) => boolean;
316
+ activeTupletRatio: [number, number] | null;
317
+ transposeSelection: (direction: string, isShift: boolean) => void;
318
+ moveSelection: (direction: string, isShift: boolean) => void;
319
+ switchStaff: (direction: "up" | "down") => void;
320
+ focusScore: () => void;
321
+ };
322
+
323
+ type ScoreContextType = ReturnType<typeof useScoreLogic> & {
324
+ pendingClefChange: {
325
+ targetClef: 'treble' | 'bass';
326
+ } | null;
327
+ setPendingClefChange: react__default.Dispatch<react__default.SetStateAction<{
328
+ targetClef: 'treble' | 'bass';
329
+ } | null>>;
330
+ handleClefChange: (val: string) => void;
331
+ };
332
+ declare const useScoreContext: () => ScoreContextType;
333
+ interface ScoreProviderProps {
334
+ children: ReactNode;
335
+ initialScore?: any;
336
+ }
337
+ declare const ScoreProvider: react__default.FC<ScoreProviderProps>;
338
+
339
+ declare const ConfigMenu: () => react_jsx_runtime.JSX.Element;
340
+
341
+ export { ConfigMenu, type Measure, type Note, RiffScore, type RiffScoreConfig, type Score, ScoreEditor, ScoreEditorContent, type ScoreEvent, ScoreProvider, type Selection, type Staff, ThemeProvider, useScoreContext, useTheme };