@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 +301 -0
- package/dist/index.cjs +239 -0
- package/dist/index.d.cts +44 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +208 -0
- package/dist/styles.css +202 -0
- package/package.json +37 -0
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
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
};
|
package/dist/styles.css
ADDED
|
@@ -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
|
+
}
|