@notiqs/fretboard 0.1.0

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/README.md ADDED
@@ -0,0 +1,301 @@
1
+ # @notiqs/fretboard
2
+
3
+ A customizable, zero-dependency React fretboard component for guitar, bass, and string instruments. Dark-themed by default, fully styleable via CSS variables and class overrides.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ npm install @notiqs/fretboard
9
+ ```
10
+
11
+ ```tsx
12
+ import { Fretboard } from '@notiqs/fretboard'
13
+ import '@notiqs/fretboard/styles.css'
14
+
15
+ // C major scale on standard guitar (partial)
16
+ const positions = [
17
+ { string: 0, fret: 0, note: 'E', isRoot: false },
18
+ { string: 0, fret: 1, note: 'F', isRoot: false },
19
+ { string: 0, fret: 3, note: 'G', isRoot: false },
20
+ { string: 1, fret: 0, note: 'B', isRoot: false },
21
+ { string: 1, fret: 1, note: 'C', isRoot: true },
22
+ // ...
23
+ ]
24
+
25
+ <Fretboard
26
+ positions={positions}
27
+ tuning={[64, 59, 55, 50, 45, 40]} // Standard guitar E-e (MIDI)
28
+ stringNames={['e', 'B', 'G', 'D', 'A', 'E']}
29
+ rootNote="C"
30
+ />
31
+ ```
32
+
33
+ ## Props Reference
34
+
35
+ | Prop | Type | Default | Description |
36
+ |------|------|---------|-------------|
37
+ | `positions` | `FretPosition[]` | *required* | Notes to display on the fretboard |
38
+ | `tuning` | `number[]` | *required* | MIDI note numbers per string, top to bottom |
39
+ | `stringNames` | `string[]` | *required* | Labels for each string |
40
+ | `maxFrets` | `number` | `12` | Number of frets to render |
41
+ | `startFret` | `number` | `0` | First fret to render (useful for position practice) |
42
+ | `displayMode` | `'notes' \| 'intervals'` | `'notes'` | Show note names or interval labels |
43
+ | `rootNote` | `string` | — | Root note for interval calculation and root highlighting |
44
+ | `showFretNumbers` | `boolean` | `true` | Show fret number row below the fretboard |
45
+ | `onNoteClick` | `(info: NoteClickInfo) => void` | — | Callback when a note marker is clicked |
46
+ | `scrollToFret` | `number` | — | Auto-scroll to center this fret in the viewport |
47
+ | `classNames` | `Partial<FretboardClassNames>` | — | Override classes for each selector |
48
+ | `styles` | `Partial<FretboardStyles>` | — | Override inline styles for each selector |
49
+
50
+ ### FretPosition
51
+
52
+ ```ts
53
+ interface FretPosition {
54
+ string: number // String index (0 = highest pitch string)
55
+ fret: number // Fret number (0 = open string)
56
+ note: string // Note name, e.g. "C", "F#"
57
+ isRoot?: boolean // Highlight as root note
58
+ category?: string // Color category (see Categories)
59
+ displayOverride?: string // Custom label (overrides note/interval)
60
+ }
61
+ ```
62
+
63
+ ### NoteClickInfo
64
+
65
+ ```ts
66
+ interface NoteClickInfo {
67
+ note: string
68
+ midi: number
69
+ stringIndex: number
70
+ fret: number
71
+ category?: string
72
+ }
73
+ ```
74
+
75
+ ## Selectors
76
+
77
+ Every element has a static class for easy CSS targeting. Use the `classNames` prop to append additional classes.
78
+
79
+ | Selector | Static class | Description |
80
+ |----------|-------------|-------------|
81
+ | `root` | `.notiqs-fretboard-root` | Outermost scrollable container |
82
+ | `stringRow` | `.notiqs-fretboard-stringRow` | One horizontal string row |
83
+ | `stringName` | `.notiqs-fretboard-stringName` | String label (E, B, G...) |
84
+ | `fretCell` | `.notiqs-fretboard-fretCell` | Individual fret cell |
85
+ | `noteMarker` | `.notiqs-fretboard-noteMarker` | The note circle/pill |
86
+ | `fretNumbers` | `.notiqs-fretboard-fretNumbers` | Fret number row container |
87
+ | `fretNumber` | `.notiqs-fretboard-fretNumber` | Individual fret number |
88
+
89
+ Fret cells also expose data attributes: `data-fret`, `data-open`, `data-has-note`.
90
+
91
+ > **String wire:** The horizontal wire on each string is rendered as a CSS pseudo-element on `fretCell`. Customize it via the `--notiqs-wire-color` CSS variable rather than a `classNames` selector.
92
+ Note markers expose: `data-category`, `data-root`.
93
+
94
+ ## CSS Variables
95
+
96
+ Override these on `.notiqs-fretboard-root` (or any ancestor) to theme the fretboard.
97
+
98
+ ### Layout
99
+
100
+ | Variable | Default | Description |
101
+ |----------|---------|-------------|
102
+ | `--notiqs-cell-width` | `60px` | Width of each fret cell |
103
+ | `--notiqs-cell-height` | `44px` | Height of each fret cell |
104
+ | `--notiqs-cell-gap` | `2px` | Gap between cells |
105
+ | `--notiqs-string-label-width` | `30px` | Width of string name column |
106
+ | `--notiqs-note-size` | `34px` | Note marker width & height |
107
+ | `--notiqs-note-radius` | `10px` | Note marker border radius |
108
+ | `--notiqs-note-font-size` | `0.8rem` | Note marker font size |
109
+
110
+ ### Colors
111
+
112
+ | Variable | Default | Description |
113
+ |----------|---------|-------------|
114
+ | `--notiqs-cell-bg` | `#16162a` | Fret cell background |
115
+ | `--notiqs-cell-bg-hover` | `#1e1e3a` | Fret cell hover background |
116
+ | `--notiqs-cell-bg-open` | `#0d0d12` | Open string cell background |
117
+ | `--notiqs-cell-border` | `#2a2a4a` | Fret line color |
118
+ | `--notiqs-cell-border-open` | `#3a3a5a` | Nut (open fret) border |
119
+ | `--notiqs-wire-color` | `linear-gradient(...)` | String wire gradient |
120
+ | `--notiqs-text` | `#e8e8f0` | Primary text color |
121
+ | `--notiqs-text-muted` | `#888` | Secondary text color |
122
+
123
+ ### Category Colors
124
+
125
+ Each category has three tokens: base, dark (gradient end), and glow.
126
+
127
+ | Category | Base | Dark | Glow |
128
+ |----------|------|------|------|
129
+ | `root` | `--notiqs-color-root` | `--notiqs-color-root-dark` | `--notiqs-color-root-glow` |
130
+ | `third` | `--notiqs-color-third` | `--notiqs-color-third-dark` | `--notiqs-color-third-glow` |
131
+ | `fifth` | `--notiqs-color-fifth` | `--notiqs-color-fifth-dark` | `--notiqs-color-fifth-glow` |
132
+ | `seventh` | `--notiqs-color-seventh` | `--notiqs-color-seventh-dark` | `--notiqs-color-seventh-glow` |
133
+ | `extension` | `--notiqs-color-extension` | `--notiqs-color-extension-dark` | `--notiqs-color-extension-glow` |
134
+ | `other` | `--notiqs-color-other` | `--notiqs-color-other-dark` | `--notiqs-color-other-glow` |
135
+
136
+ ## Theming
137
+
138
+ ### Light Theme Override
139
+
140
+ ```css
141
+ .notiqs-fretboard-root {
142
+ --notiqs-cell-bg: #f5f5f5;
143
+ --notiqs-cell-bg-hover: #e8e8e8;
144
+ --notiqs-cell-bg-open: #ffffff;
145
+ --notiqs-cell-border: #d0d0d0;
146
+ --notiqs-cell-border-open: #b0b0b0;
147
+ --notiqs-wire-color: linear-gradient(to right, #ccc 0%, #999 100%);
148
+ --notiqs-text: #1a1a1a;
149
+ --notiqs-text-muted: #666;
150
+ }
151
+ ```
152
+
153
+ ### classNames Usage
154
+
155
+ ```tsx
156
+ <Fretboard
157
+ positions={positions}
158
+ tuning={tuning}
159
+ stringNames={names}
160
+ classNames={{
161
+ root: 'my-fretboard',
162
+ noteMarker: 'my-note',
163
+ fretCell: 'my-cell',
164
+ }}
165
+ />
166
+ ```
167
+
168
+ ```css
169
+ .my-fretboard { border: 1px solid #333; border-radius: 8px; padding: 12px; }
170
+ .my-note { border-radius: 50%; } /* circular markers */
171
+ ```
172
+
173
+ ### Inline Styles
174
+
175
+ ```tsx
176
+ <Fretboard
177
+ positions={positions}
178
+ tuning={tuning}
179
+ stringNames={names}
180
+ styles={{
181
+ root: { maxHeight: 300, overflow: 'hidden' },
182
+ noteMarker: { borderRadius: '50%' },
183
+ }}
184
+ />
185
+ ```
186
+
187
+ ## Categories
188
+
189
+ The built-in stylesheet provides colors for these `category` values:
190
+
191
+ | Category | Color | Use case |
192
+ |----------|-------|----------|
193
+ | `root` | Violet | Root notes |
194
+ | `third` | Pink | Major/minor thirds |
195
+ | `fifth` | Teal | Perfect fifths |
196
+ | `seventh` | Gold | Sevenths |
197
+ | `extension` | Blue | 9ths, 11ths, 13ths |
198
+ | `other` | Gray | 2nds, 4ths, 6ths |
199
+ | `common` | Teal | Shared notes (scale comparison) |
200
+ | `scaleA` | Blue | Scale A unique notes |
201
+ | `scaleB` | Pink | Scale B unique notes |
202
+ | `chord` | Teal | Chord tones |
203
+ | `scale` | Blue | Non-chord scale notes |
204
+
205
+ ### Custom Categories
206
+
207
+ Add your own categories via `data-category` CSS selectors:
208
+
209
+ ```css
210
+ .notiqs-fretboard-noteMarker[data-category="myCustom"] {
211
+ background: linear-gradient(135deg, #ff6600 0%, #cc5200 100%);
212
+ box-shadow: 0 4px 12px rgba(255, 102, 0, 0.35);
213
+ }
214
+ ```
215
+
216
+ Then pass `category: 'myCustom'` in your `FretPosition` objects.
217
+
218
+ ## Recipes
219
+
220
+ ### Interval Mode
221
+
222
+ ```tsx
223
+ <Fretboard
224
+ positions={positions}
225
+ tuning={[64, 59, 55, 50, 45, 40]}
226
+ stringNames={['e', 'B', 'G', 'D', 'A', 'E']}
227
+ rootNote="C"
228
+ displayMode="intervals"
229
+ />
230
+ ```
231
+
232
+ Notes display as `R`, `b3`, `5`, `7`, etc. instead of note names.
233
+
234
+ ### Click Handling
235
+
236
+ ```tsx
237
+ function MyApp() {
238
+ const handleClick = (info: NoteClickInfo) => {
239
+ console.log(`Clicked ${info.note} (MIDI ${info.midi}) on string ${info.stringIndex}, fret ${info.fret}`)
240
+ // Play audio, check quiz answer, etc.
241
+ }
242
+
243
+ return (
244
+ <Fretboard
245
+ positions={positions}
246
+ tuning={tuning}
247
+ stringNames={names}
248
+ onNoteClick={handleClick}
249
+ />
250
+ )
251
+ }
252
+ ```
253
+
254
+ ### Bass Tuning
255
+
256
+ ```tsx
257
+ // 4-string bass: G D A E
258
+ <Fretboard positions={positions} tuning={[43, 38, 33, 28]} stringNames={['G', 'D', 'A', 'E']} rootNote="E" />
259
+
260
+ // 5-string bass: G D A E B
261
+ <Fretboard positions={positions} tuning={[43, 38, 33, 28, 23]} stringNames={['G', 'D', 'A', 'E', 'B']} rootNote="E" />
262
+ ```
263
+
264
+ ### Scroll to Fret / Position Practice
265
+
266
+ ```tsx
267
+ <Fretboard positions={positions} tuning={tuning} stringNames={names} scrollToFret={7} />
268
+ <Fretboard positions={positions} tuning={tuning} stringNames={names} startFret={5} maxFrets={8} />
269
+ ```
270
+
271
+ ### Display Override (Quiz Mode)
272
+
273
+ ```tsx
274
+ const quizPositions = [
275
+ { string: 2, fret: 5, note: 'C', isRoot: true, displayOverride: '?', category: 'challenge' },
276
+ ]
277
+ ```
278
+
279
+ ## Exports
280
+
281
+ ```ts
282
+ // Component
283
+ export { Fretboard } from '@notiqs/fretboard'
284
+
285
+ // Types
286
+ export type {
287
+ FretboardProps,
288
+ FretPosition,
289
+ NoteClickInfo,
290
+ FretboardClassNames,
291
+ FretboardStyles,
292
+ FretboardSelector,
293
+ } from '@notiqs/fretboard'
294
+
295
+ // Utilities
296
+ export { getNoteAtFret, getIntervalLabel, normalizeNote, midiToNoteName } from '@notiqs/fretboard'
297
+ ```
298
+
299
+ ## License
300
+
301
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,239 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ Fretboard: () => Fretboard,
24
+ getIntervalLabel: () => getIntervalLabel,
25
+ getNoteAtFret: () => getNoteAtFret,
26
+ midiToNoteName: () => midiToNoteName,
27
+ normalizeNote: () => normalizeNote
28
+ });
29
+ module.exports = __toCommonJS(index_exports);
30
+
31
+ // src/Fretboard.tsx
32
+ var import_react = require("react");
33
+
34
+ // src/utils.ts
35
+ var NOTE_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
36
+ var ENHARMONIC_MAP = {
37
+ "Cb": "B",
38
+ "Db": "C#",
39
+ "Eb": "D#",
40
+ "Fb": "E",
41
+ "Gb": "F#",
42
+ "Ab": "G#",
43
+ "Bb": "A#",
44
+ "E#": "F",
45
+ "B#": "C",
46
+ "C##": "D",
47
+ "D##": "E",
48
+ "E##": "F#",
49
+ "F##": "G",
50
+ "G##": "A",
51
+ "A##": "B",
52
+ "B##": "C#",
53
+ "Cbb": "A#",
54
+ "Dbb": "C",
55
+ "Ebb": "D",
56
+ "Fbb": "D#",
57
+ "Gbb": "F",
58
+ "Abb": "G",
59
+ "Bbb": "A"
60
+ };
61
+ var INTERVAL_LABELS = ["R", "\u266D2", "2", "\u266D3", "3", "4", "\u266D5", "5", "\u266D6", "6", "\u266D7", "7"];
62
+ function normalizeNote(note) {
63
+ const pitchClass = note.replace(/\d+$/, "");
64
+ return ENHARMONIC_MAP[pitchClass] || pitchClass;
65
+ }
66
+ function midiToNoteName(midi) {
67
+ return NOTE_NAMES[midi % 12];
68
+ }
69
+ function getNoteAtFret(stringMidi, fret) {
70
+ return midiToNoteName(stringMidi + fret);
71
+ }
72
+ function getIntervalLabel(note, rootNote) {
73
+ const noteNorm = normalizeNote(note);
74
+ const rootNorm = normalizeNote(rootNote);
75
+ const noteIndex = NOTE_NAMES.indexOf(noteNorm);
76
+ const rootIndex = NOTE_NAMES.indexOf(rootNorm);
77
+ if (noteIndex === -1 || rootIndex === -1) return note;
78
+ const semitones = (noteIndex - rootIndex + 12) % 12;
79
+ return INTERVAL_LABELS[semitones];
80
+ }
81
+
82
+ // src/Fretboard.tsx
83
+ var import_jsx_runtime = require("react/jsx-runtime");
84
+ var GLOW_COLORS = {
85
+ root: "var(--notiqs-color-root-glow)",
86
+ third: "var(--notiqs-color-third-glow)",
87
+ fifth: "var(--notiqs-color-fifth-glow)",
88
+ seventh: "var(--notiqs-color-seventh-glow)",
89
+ extension: "var(--notiqs-color-extension-glow)",
90
+ other: "var(--notiqs-color-other-glow)",
91
+ chord: "var(--notiqs-color-fifth-glow)",
92
+ common: "var(--notiqs-color-fifth-glow)",
93
+ scale: "var(--notiqs-color-extension-glow)",
94
+ scaleA: "var(--notiqs-color-extension-glow)",
95
+ scaleB: "var(--notiqs-color-third-glow)"
96
+ };
97
+ function getGlowColor(category, isRoot) {
98
+ if (isRoot) return GLOW_COLORS.root;
99
+ if (category && category in GLOW_COLORS) return GLOW_COLORS[category];
100
+ return GLOW_COLORS.root;
101
+ }
102
+ function cx(staticClass, consumerClass) {
103
+ return consumerClass ? `${staticClass} ${consumerClass}` : staticClass;
104
+ }
105
+ function Fretboard({
106
+ positions,
107
+ tuning,
108
+ stringNames,
109
+ maxFrets = 12,
110
+ startFret = 0,
111
+ displayMode = "notes",
112
+ rootNote,
113
+ showFretNumbers = true,
114
+ onNoteClick,
115
+ scrollToFret,
116
+ classNames,
117
+ styles
118
+ }) {
119
+ const rootRef = (0, import_react.useRef)(null);
120
+ (0, import_react.useEffect)(() => {
121
+ if (scrollToFret === void 0 || !rootRef.current) return;
122
+ const container = rootRef.current;
123
+ const cell = container.querySelector(
124
+ `.notiqs-fretboard-fretCell[data-fret="${scrollToFret}"]`
125
+ );
126
+ if (cell) {
127
+ const cellCenter = cell.offsetLeft + cell.offsetWidth / 2;
128
+ const containerWidth = container.clientWidth;
129
+ requestAnimationFrame(() => {
130
+ container.scrollTo({
131
+ left: Math.max(0, cellCenter - containerWidth / 2),
132
+ behavior: "smooth"
133
+ });
134
+ });
135
+ }
136
+ }, [scrollToFret]);
137
+ const getPositionAt = (stringIndex, fret) => {
138
+ return positions.find((p) => p.string === stringIndex && p.fret === fret);
139
+ };
140
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
141
+ "div",
142
+ {
143
+ ref: rootRef,
144
+ className: cx("notiqs-fretboard-root", classNames?.root),
145
+ style: styles?.root,
146
+ children: [
147
+ tuning.map((_, stringIndex) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
148
+ "div",
149
+ {
150
+ className: cx("notiqs-fretboard-stringRow", classNames?.stringRow),
151
+ style: styles?.stringRow,
152
+ children: [
153
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
154
+ "span",
155
+ {
156
+ className: cx("notiqs-fretboard-stringName", classNames?.stringName),
157
+ style: styles?.stringName,
158
+ children: stringNames[stringIndex]
159
+ }
160
+ ),
161
+ Array.from({ length: maxFrets - startFret + 1 }, (_2, i) => {
162
+ const fret = startFret + i;
163
+ const stringMidi = tuning[stringIndex];
164
+ const note = getNoteAtFret(stringMidi, fret);
165
+ const position = getPositionAt(stringIndex, fret);
166
+ const hasNote = !!position;
167
+ const isRoot = position?.isRoot ?? (!!rootNote && note === rootNote);
168
+ const midi = stringMidi + fret;
169
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
170
+ "div",
171
+ {
172
+ "data-fret": fret,
173
+ "data-open": fret === 0 || void 0,
174
+ "data-has-note": hasNote || void 0,
175
+ className: cx("notiqs-fretboard-fretCell", classNames?.fretCell),
176
+ style: {
177
+ cursor: hasNote && onNoteClick ? "pointer" : "default",
178
+ ...styles?.fretCell
179
+ },
180
+ onClick: () => {
181
+ if (hasNote && onNoteClick) {
182
+ onNoteClick({ note, midi, stringIndex, fret, category: position?.category });
183
+ }
184
+ },
185
+ children: hasNote && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
186
+ "span",
187
+ {
188
+ "data-category": position?.category || void 0,
189
+ "data-root": isRoot || void 0,
190
+ className: cx("notiqs-fretboard-noteMarker", classNames?.noteMarker),
191
+ style: {
192
+ "--note-glow-color": getGlowColor(position?.category, isRoot),
193
+ ...styles?.noteMarker
194
+ },
195
+ children: position?.displayOverride ?? (displayMode === "intervals" && rootNote ? getIntervalLabel(note, rootNote) : note)
196
+ }
197
+ )
198
+ },
199
+ fret
200
+ );
201
+ })
202
+ ]
203
+ },
204
+ stringIndex
205
+ )),
206
+ showFretNumbers && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
207
+ "div",
208
+ {
209
+ className: cx("notiqs-fretboard-fretNumbers", classNames?.fretNumbers),
210
+ style: styles?.fretNumbers,
211
+ children: [
212
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {}),
213
+ Array.from({ length: maxFrets - startFret + 1 }, (_, i) => {
214
+ const fret = startFret + i;
215
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
216
+ "span",
217
+ {
218
+ className: cx("notiqs-fretboard-fretNumber", classNames?.fretNumber),
219
+ style: styles?.fretNumber,
220
+ children: fret
221
+ },
222
+ fret
223
+ );
224
+ })
225
+ ]
226
+ }
227
+ )
228
+ ]
229
+ }
230
+ );
231
+ }
232
+ // Annotate the CommonJS export names for ESM import in node:
233
+ 0 && (module.exports = {
234
+ Fretboard,
235
+ getIntervalLabel,
236
+ getNoteAtFret,
237
+ midiToNoteName,
238
+ normalizeNote
239
+ });
@@ -0,0 +1,44 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { CSSProperties } from 'react';
3
+
4
+ interface FretPosition {
5
+ string: number;
6
+ fret: number;
7
+ note: string;
8
+ isRoot?: boolean;
9
+ category?: string;
10
+ displayOverride?: string;
11
+ }
12
+ interface NoteClickInfo {
13
+ note: string;
14
+ midi: number;
15
+ stringIndex: number;
16
+ fret: number;
17
+ category?: string;
18
+ }
19
+ type FretboardSelector = 'root' | 'stringRow' | 'stringName' | 'fretCell' | 'noteMarker' | 'fretNumbers' | 'fretNumber';
20
+ type FretboardClassNames = Record<FretboardSelector, string>;
21
+ type FretboardStyles = Record<FretboardSelector, CSSProperties>;
22
+ interface FretboardProps {
23
+ positions: FretPosition[];
24
+ tuning: number[];
25
+ stringNames: string[];
26
+ maxFrets?: number;
27
+ startFret?: number;
28
+ displayMode?: 'notes' | 'intervals';
29
+ rootNote?: string;
30
+ showFretNumbers?: boolean;
31
+ onNoteClick?: (info: NoteClickInfo) => void;
32
+ scrollToFret?: number;
33
+ classNames?: Partial<FretboardClassNames>;
34
+ styles?: Partial<FretboardStyles>;
35
+ }
36
+
37
+ declare function Fretboard({ positions, tuning, stringNames, maxFrets, startFret, displayMode, rootNote, showFretNumbers, onNoteClick, scrollToFret, classNames, styles, }: FretboardProps): react_jsx_runtime.JSX.Element;
38
+
39
+ declare function normalizeNote(note: string): string;
40
+ declare function midiToNoteName(midi: number): string;
41
+ declare function getNoteAtFret(stringMidi: number, fret: number): string;
42
+ declare function getIntervalLabel(note: string, rootNote: string): string;
43
+
44
+ export { type FretPosition, Fretboard, type FretboardClassNames, type FretboardProps, type FretboardSelector, type FretboardStyles, type NoteClickInfo, getIntervalLabel, getNoteAtFret, midiToNoteName, normalizeNote };
@@ -0,0 +1,44 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { CSSProperties } from 'react';
3
+
4
+ interface FretPosition {
5
+ string: number;
6
+ fret: number;
7
+ note: string;
8
+ isRoot?: boolean;
9
+ category?: string;
10
+ displayOverride?: string;
11
+ }
12
+ interface NoteClickInfo {
13
+ note: string;
14
+ midi: number;
15
+ stringIndex: number;
16
+ fret: number;
17
+ category?: string;
18
+ }
19
+ type FretboardSelector = 'root' | 'stringRow' | 'stringName' | 'fretCell' | 'noteMarker' | 'fretNumbers' | 'fretNumber';
20
+ type FretboardClassNames = Record<FretboardSelector, string>;
21
+ type FretboardStyles = Record<FretboardSelector, CSSProperties>;
22
+ interface FretboardProps {
23
+ positions: FretPosition[];
24
+ tuning: number[];
25
+ stringNames: string[];
26
+ maxFrets?: number;
27
+ startFret?: number;
28
+ displayMode?: 'notes' | 'intervals';
29
+ rootNote?: string;
30
+ showFretNumbers?: boolean;
31
+ onNoteClick?: (info: NoteClickInfo) => void;
32
+ scrollToFret?: number;
33
+ classNames?: Partial<FretboardClassNames>;
34
+ styles?: Partial<FretboardStyles>;
35
+ }
36
+
37
+ declare function Fretboard({ positions, tuning, stringNames, maxFrets, startFret, displayMode, rootNote, showFretNumbers, onNoteClick, scrollToFret, classNames, styles, }: FretboardProps): react_jsx_runtime.JSX.Element;
38
+
39
+ declare function normalizeNote(note: string): string;
40
+ declare function midiToNoteName(midi: number): string;
41
+ declare function getNoteAtFret(stringMidi: number, fret: number): string;
42
+ declare function getIntervalLabel(note: string, rootNote: string): string;
43
+
44
+ export { type FretPosition, Fretboard, type FretboardClassNames, type FretboardProps, type FretboardSelector, type FretboardStyles, type NoteClickInfo, getIntervalLabel, getNoteAtFret, midiToNoteName, normalizeNote };
package/dist/index.js ADDED
@@ -0,0 +1,208 @@
1
+ // src/Fretboard.tsx
2
+ import { useRef, useEffect } from "react";
3
+
4
+ // src/utils.ts
5
+ var NOTE_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
6
+ var ENHARMONIC_MAP = {
7
+ "Cb": "B",
8
+ "Db": "C#",
9
+ "Eb": "D#",
10
+ "Fb": "E",
11
+ "Gb": "F#",
12
+ "Ab": "G#",
13
+ "Bb": "A#",
14
+ "E#": "F",
15
+ "B#": "C",
16
+ "C##": "D",
17
+ "D##": "E",
18
+ "E##": "F#",
19
+ "F##": "G",
20
+ "G##": "A",
21
+ "A##": "B",
22
+ "B##": "C#",
23
+ "Cbb": "A#",
24
+ "Dbb": "C",
25
+ "Ebb": "D",
26
+ "Fbb": "D#",
27
+ "Gbb": "F",
28
+ "Abb": "G",
29
+ "Bbb": "A"
30
+ };
31
+ var INTERVAL_LABELS = ["R", "\u266D2", "2", "\u266D3", "3", "4", "\u266D5", "5", "\u266D6", "6", "\u266D7", "7"];
32
+ function normalizeNote(note) {
33
+ const pitchClass = note.replace(/\d+$/, "");
34
+ return ENHARMONIC_MAP[pitchClass] || pitchClass;
35
+ }
36
+ function midiToNoteName(midi) {
37
+ return NOTE_NAMES[midi % 12];
38
+ }
39
+ function getNoteAtFret(stringMidi, fret) {
40
+ return midiToNoteName(stringMidi + fret);
41
+ }
42
+ function getIntervalLabel(note, rootNote) {
43
+ const noteNorm = normalizeNote(note);
44
+ const rootNorm = normalizeNote(rootNote);
45
+ const noteIndex = NOTE_NAMES.indexOf(noteNorm);
46
+ const rootIndex = NOTE_NAMES.indexOf(rootNorm);
47
+ if (noteIndex === -1 || rootIndex === -1) return note;
48
+ const semitones = (noteIndex - rootIndex + 12) % 12;
49
+ return INTERVAL_LABELS[semitones];
50
+ }
51
+
52
+ // src/Fretboard.tsx
53
+ import { jsx, jsxs } from "react/jsx-runtime";
54
+ var GLOW_COLORS = {
55
+ root: "var(--notiqs-color-root-glow)",
56
+ third: "var(--notiqs-color-third-glow)",
57
+ fifth: "var(--notiqs-color-fifth-glow)",
58
+ seventh: "var(--notiqs-color-seventh-glow)",
59
+ extension: "var(--notiqs-color-extension-glow)",
60
+ other: "var(--notiqs-color-other-glow)",
61
+ chord: "var(--notiqs-color-fifth-glow)",
62
+ common: "var(--notiqs-color-fifth-glow)",
63
+ scale: "var(--notiqs-color-extension-glow)",
64
+ scaleA: "var(--notiqs-color-extension-glow)",
65
+ scaleB: "var(--notiqs-color-third-glow)"
66
+ };
67
+ function getGlowColor(category, isRoot) {
68
+ if (isRoot) return GLOW_COLORS.root;
69
+ if (category && category in GLOW_COLORS) return GLOW_COLORS[category];
70
+ return GLOW_COLORS.root;
71
+ }
72
+ function cx(staticClass, consumerClass) {
73
+ return consumerClass ? `${staticClass} ${consumerClass}` : staticClass;
74
+ }
75
+ function Fretboard({
76
+ positions,
77
+ tuning,
78
+ stringNames,
79
+ maxFrets = 12,
80
+ startFret = 0,
81
+ displayMode = "notes",
82
+ rootNote,
83
+ showFretNumbers = true,
84
+ onNoteClick,
85
+ scrollToFret,
86
+ classNames,
87
+ styles
88
+ }) {
89
+ const rootRef = useRef(null);
90
+ useEffect(() => {
91
+ if (scrollToFret === void 0 || !rootRef.current) return;
92
+ const container = rootRef.current;
93
+ const cell = container.querySelector(
94
+ `.notiqs-fretboard-fretCell[data-fret="${scrollToFret}"]`
95
+ );
96
+ if (cell) {
97
+ const cellCenter = cell.offsetLeft + cell.offsetWidth / 2;
98
+ const containerWidth = container.clientWidth;
99
+ requestAnimationFrame(() => {
100
+ container.scrollTo({
101
+ left: Math.max(0, cellCenter - containerWidth / 2),
102
+ behavior: "smooth"
103
+ });
104
+ });
105
+ }
106
+ }, [scrollToFret]);
107
+ const getPositionAt = (stringIndex, fret) => {
108
+ return positions.find((p) => p.string === stringIndex && p.fret === fret);
109
+ };
110
+ return /* @__PURE__ */ jsxs(
111
+ "div",
112
+ {
113
+ ref: rootRef,
114
+ className: cx("notiqs-fretboard-root", classNames?.root),
115
+ style: styles?.root,
116
+ children: [
117
+ tuning.map((_, stringIndex) => /* @__PURE__ */ jsxs(
118
+ "div",
119
+ {
120
+ className: cx("notiqs-fretboard-stringRow", classNames?.stringRow),
121
+ style: styles?.stringRow,
122
+ children: [
123
+ /* @__PURE__ */ jsx(
124
+ "span",
125
+ {
126
+ className: cx("notiqs-fretboard-stringName", classNames?.stringName),
127
+ style: styles?.stringName,
128
+ children: stringNames[stringIndex]
129
+ }
130
+ ),
131
+ Array.from({ length: maxFrets - startFret + 1 }, (_2, i) => {
132
+ const fret = startFret + i;
133
+ const stringMidi = tuning[stringIndex];
134
+ const note = getNoteAtFret(stringMidi, fret);
135
+ const position = getPositionAt(stringIndex, fret);
136
+ const hasNote = !!position;
137
+ const isRoot = position?.isRoot ?? (!!rootNote && note === rootNote);
138
+ const midi = stringMidi + fret;
139
+ return /* @__PURE__ */ jsx(
140
+ "div",
141
+ {
142
+ "data-fret": fret,
143
+ "data-open": fret === 0 || void 0,
144
+ "data-has-note": hasNote || void 0,
145
+ className: cx("notiqs-fretboard-fretCell", classNames?.fretCell),
146
+ style: {
147
+ cursor: hasNote && onNoteClick ? "pointer" : "default",
148
+ ...styles?.fretCell
149
+ },
150
+ onClick: () => {
151
+ if (hasNote && onNoteClick) {
152
+ onNoteClick({ note, midi, stringIndex, fret, category: position?.category });
153
+ }
154
+ },
155
+ children: hasNote && /* @__PURE__ */ jsx(
156
+ "span",
157
+ {
158
+ "data-category": position?.category || void 0,
159
+ "data-root": isRoot || void 0,
160
+ className: cx("notiqs-fretboard-noteMarker", classNames?.noteMarker),
161
+ style: {
162
+ "--note-glow-color": getGlowColor(position?.category, isRoot),
163
+ ...styles?.noteMarker
164
+ },
165
+ children: position?.displayOverride ?? (displayMode === "intervals" && rootNote ? getIntervalLabel(note, rootNote) : note)
166
+ }
167
+ )
168
+ },
169
+ fret
170
+ );
171
+ })
172
+ ]
173
+ },
174
+ stringIndex
175
+ )),
176
+ showFretNumbers && /* @__PURE__ */ jsxs(
177
+ "div",
178
+ {
179
+ className: cx("notiqs-fretboard-fretNumbers", classNames?.fretNumbers),
180
+ style: styles?.fretNumbers,
181
+ children: [
182
+ /* @__PURE__ */ jsx("span", {}),
183
+ Array.from({ length: maxFrets - startFret + 1 }, (_, i) => {
184
+ const fret = startFret + i;
185
+ return /* @__PURE__ */ jsx(
186
+ "span",
187
+ {
188
+ className: cx("notiqs-fretboard-fretNumber", classNames?.fretNumber),
189
+ style: styles?.fretNumber,
190
+ children: fret
191
+ },
192
+ fret
193
+ );
194
+ })
195
+ ]
196
+ }
197
+ )
198
+ ]
199
+ }
200
+ );
201
+ }
202
+ export {
203
+ Fretboard,
204
+ getIntervalLabel,
205
+ getNoteAtFret,
206
+ midiToNoteName,
207
+ normalizeNote
208
+ };
@@ -0,0 +1,202 @@
1
+ /* @notiqs/fretboard — Default Stylesheet */
2
+
3
+ .notiqs-fretboard-root {
4
+ --notiqs-cell-width: 60px;
5
+ --notiqs-cell-height: 44px;
6
+ --notiqs-cell-gap: 2px;
7
+ --notiqs-string-label-width: 30px;
8
+ --notiqs-note-size: 34px;
9
+ --notiqs-note-radius: 10px;
10
+ --notiqs-note-font-size: 0.8rem;
11
+
12
+ --notiqs-cell-bg: #16162a;
13
+ --notiqs-cell-bg-hover: #1e1e3a;
14
+ --notiqs-cell-bg-open: #0d0d12;
15
+ --notiqs-cell-border: #2a2a4a;
16
+ --notiqs-cell-border-open: #3a3a5a;
17
+ --notiqs-wire-color: linear-gradient(to right, #2a2a4a 0%, #3a3a5a 100%);
18
+ --notiqs-text: #e8e8f0;
19
+ --notiqs-text-muted: #888;
20
+
21
+ --notiqs-color-root: #9d6eff;
22
+ --notiqs-color-root-dark: #7c4ddb;
23
+ --notiqs-color-root-glow: rgba(157, 110, 255, 0.35);
24
+ --notiqs-color-third: #ff6eb4;
25
+ --notiqs-color-third-dark: #e8459a;
26
+ --notiqs-color-third-glow: rgba(255, 110, 180, 0.35);
27
+ --notiqs-color-fifth: #20c997;
28
+ --notiqs-color-fifth-dark: #2ba89a;
29
+ --notiqs-color-fifth-glow: rgba(32, 201, 151, 0.35);
30
+ --notiqs-color-seventh: #d4a030;
31
+ --notiqs-color-seventh-dark: #b88a28;
32
+ --notiqs-color-seventh-glow: rgba(212, 160, 48, 0.35);
33
+ --notiqs-color-extension: #5b9bd5;
34
+ --notiqs-color-extension-dark: #4a8fd4;
35
+ --notiqs-color-extension-glow: rgba(91, 155, 213, 0.35);
36
+ --notiqs-color-other: #3a3a5a;
37
+ --notiqs-color-other-dark: #2a2a4a;
38
+ --notiqs-color-other-glow: rgba(50, 50, 74, 0.3);
39
+ }
40
+
41
+ .notiqs-fretboard-root {
42
+ display: flex;
43
+ flex-direction: column;
44
+ gap: var(--notiqs-cell-gap);
45
+ overflow-x: auto;
46
+ padding-bottom: 1rem;
47
+ }
48
+
49
+ .notiqs-fretboard-stringRow {
50
+ display: flex;
51
+ align-items: center;
52
+ gap: var(--notiqs-cell-gap);
53
+ }
54
+
55
+ .notiqs-fretboard-stringName {
56
+ width: var(--notiqs-string-label-width);
57
+ text-align: center;
58
+ flex-shrink: 0;
59
+ font-weight: 700;
60
+ color: var(--notiqs-text-muted);
61
+ font-size: 0.875rem;
62
+ }
63
+
64
+ .notiqs-fretboard-fretCell {
65
+ width: var(--notiqs-cell-width);
66
+ height: var(--notiqs-cell-height);
67
+ display: flex;
68
+ align-items: center;
69
+ justify-content: center;
70
+ background: var(--notiqs-cell-bg);
71
+ border-right: 2px solid var(--notiqs-cell-border);
72
+ position: relative;
73
+ flex-shrink: 0;
74
+ transition: background-color 0.2s;
75
+ }
76
+
77
+ .notiqs-fretboard-fretCell:hover {
78
+ background: var(--notiqs-cell-bg-hover);
79
+ }
80
+
81
+ .notiqs-fretboard-fretCell[data-open="true"] {
82
+ background: var(--notiqs-cell-bg-open);
83
+ border-right: 4px solid var(--notiqs-cell-border-open);
84
+ }
85
+
86
+ /* String wire */
87
+ .notiqs-fretboard-fretCell::after {
88
+ content: '';
89
+ position: absolute;
90
+ top: 50%;
91
+ left: 0;
92
+ right: 0;
93
+ height: 2px;
94
+ background: var(--notiqs-wire-color);
95
+ transform: translateY(-50%);
96
+ z-index: 0;
97
+ border-radius: 1px;
98
+ }
99
+
100
+ /* Note Marker — default (root color) */
101
+ .notiqs-fretboard-noteMarker {
102
+ position: relative;
103
+ z-index: 1;
104
+ width: var(--notiqs-note-size);
105
+ height: var(--notiqs-note-size);
106
+ display: flex;
107
+ align-items: center;
108
+ justify-content: center;
109
+ background: linear-gradient(135deg, var(--notiqs-color-root) 0%, var(--notiqs-color-root-dark) 100%);
110
+ color: white;
111
+ border-radius: var(--notiqs-note-radius);
112
+ font-weight: 600;
113
+ font-size: var(--notiqs-note-font-size);
114
+ box-shadow: 0 4px 12px var(--notiqs-color-root-glow);
115
+ transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1),
116
+ box-shadow 0.25s ease, filter 0.2s ease;
117
+ }
118
+
119
+ .notiqs-fretboard-fretCell[data-has-note="true"]:hover .notiqs-fretboard-noteMarker {
120
+ transform: scale(1.25) translateY(-3px);
121
+ filter: brightness(1.2);
122
+ box-shadow:
123
+ 0 8px 24px var(--note-glow-color, var(--notiqs-color-root-glow)),
124
+ 0 4px 8px rgba(0,0,0,0.4),
125
+ 0 0 20px var(--note-glow-color, var(--notiqs-color-root-glow));
126
+ }
127
+
128
+ .notiqs-fretboard-fretCell[data-has-note="true"]:active .notiqs-fretboard-noteMarker {
129
+ transform: scale(1.15) translateY(-1px);
130
+ }
131
+
132
+ /* Category Colors */
133
+ .notiqs-fretboard-noteMarker[data-root="true"],
134
+ .notiqs-fretboard-noteMarker[data-category="root"] {
135
+ background: linear-gradient(135deg, var(--notiqs-color-root) 0%, var(--notiqs-color-root-dark) 100%);
136
+ box-shadow: 0 4px 12px var(--notiqs-color-root-glow);
137
+ }
138
+
139
+ .notiqs-fretboard-noteMarker[data-category="third"] {
140
+ background: linear-gradient(135deg, var(--notiqs-color-third) 0%, var(--notiqs-color-third-dark) 100%);
141
+ box-shadow: 0 4px 12px var(--notiqs-color-third-glow);
142
+ }
143
+
144
+ .notiqs-fretboard-noteMarker[data-category="fifth"],
145
+ .notiqs-fretboard-noteMarker[data-category="chord"],
146
+ .notiqs-fretboard-noteMarker[data-category="common"] {
147
+ background: linear-gradient(135deg, var(--notiqs-color-fifth) 0%, var(--notiqs-color-fifth-dark) 100%);
148
+ box-shadow: 0 4px 12px var(--notiqs-color-fifth-glow);
149
+ }
150
+
151
+ .notiqs-fretboard-noteMarker[data-category="seventh"] {
152
+ background: linear-gradient(135deg, var(--notiqs-color-seventh) 0%, var(--notiqs-color-seventh-dark) 100%);
153
+ box-shadow: 0 4px 12px var(--notiqs-color-seventh-glow);
154
+ }
155
+
156
+ .notiqs-fretboard-noteMarker[data-category="extension"],
157
+ .notiqs-fretboard-noteMarker[data-category="scale"],
158
+ .notiqs-fretboard-noteMarker[data-category="scaleA"] {
159
+ background: linear-gradient(135deg, var(--notiqs-color-extension) 0%, var(--notiqs-color-extension-dark) 100%);
160
+ box-shadow: 0 4px 12px var(--notiqs-color-extension-glow);
161
+ }
162
+
163
+ .notiqs-fretboard-noteMarker[data-category="scaleB"] {
164
+ background: linear-gradient(135deg, var(--notiqs-color-third) 0%, var(--notiqs-color-third-dark) 100%);
165
+ box-shadow: 0 4px 12px var(--notiqs-color-third-glow);
166
+ }
167
+
168
+ .notiqs-fretboard-noteMarker[data-category="other"] {
169
+ background: linear-gradient(135deg, var(--notiqs-color-other) 0%, var(--notiqs-color-other-dark) 100%);
170
+ box-shadow: 0 4px 12px var(--notiqs-color-other-glow);
171
+ }
172
+
173
+ /* Fret Numbers */
174
+ .notiqs-fretboard-fretNumbers {
175
+ display: flex;
176
+ gap: var(--notiqs-cell-gap);
177
+ margin-top: 0.5rem;
178
+ }
179
+
180
+ .notiqs-fretboard-fretNumbers > span:first-child {
181
+ width: var(--notiqs-string-label-width);
182
+ flex-shrink: 0;
183
+ }
184
+
185
+ .notiqs-fretboard-fretNumber {
186
+ width: var(--notiqs-cell-width);
187
+ text-align: center;
188
+ flex-shrink: 0;
189
+ font-size: 0.875rem;
190
+ color: var(--notiqs-text-muted);
191
+ }
192
+
193
+ /* Responsive */
194
+ @media (max-width: 768px) {
195
+ .notiqs-fretboard-root {
196
+ --notiqs-cell-width: 48px;
197
+ --notiqs-cell-height: 38px;
198
+ --notiqs-note-size: 28px;
199
+ --notiqs-note-radius: 8px;
200
+ --notiqs-note-font-size: 0.75rem;
201
+ }
202
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@notiqs/fretboard",
3
+ "version": "0.1.0",
4
+ "description": "A customizable React fretboard component for guitar, bass, and string instruments",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ },
15
+ "./styles.css": "./dist/styles.css"
16
+ },
17
+ "files": ["dist", "README.md"],
18
+ "scripts": {
19
+ "build": "tsup",
20
+ "dev": "tsup --watch"
21
+ },
22
+ "peerDependencies": {
23
+ "react": ">=18.0.0"
24
+ },
25
+ "devDependencies": {
26
+ "react": "^19.2.0",
27
+ "tsup": "^8.4.0",
28
+ "typescript": "~5.9.3"
29
+ },
30
+ "keywords": ["react", "fretboard", "guitar", "bass", "music", "music-theory", "visualization", "component"],
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/nahularenas/Notiqs",
35
+ "directory": "packages/fretboard"
36
+ }
37
+ }