@@ -6,9 +6,6 @@ import { Download, FileText } from 'lucide-react';
6
6
import { SurParser , SurDocument , Note } from './lib/sur-parser' ;
7
7
import html2pdf from 'html2pdf.js' ;
8
8
9
- const UPPER_BAR = '\u0304' ; // Macron above: S̄
10
- const LOWER_BAR = '\u0332' ; // Macron below: S̲
11
-
12
9
const DEFAULT_SUR = `%% CONFIG
13
10
name: "Albela Sajan"
14
11
raag: "bhoopali"
@@ -67,18 +64,14 @@ const processNote = (note: Note): {
67
64
type : 'note' | 'lyrics' | 'special' ;
68
65
display : string ;
69
66
} => {
70
- console . log ( 'Processing note for display:' , note ) ;
71
-
72
- // Handle special characters first
73
67
if ( note . isSpecial ) {
74
68
return {
75
69
text : note . sur || '' ,
76
70
type : 'special' ,
77
- display : note . sur === '-' ? '−' : '· '
71
+ display : note . sur || ' '
78
72
} ;
79
73
}
80
74
81
- // Handle lyrics next (including bracketed lyrics)
82
75
if ( note . lyrics ) {
83
76
return {
84
77
text : note . lyrics ,
@@ -87,46 +80,67 @@ const processNote = (note: Note): {
87
80
} ;
88
81
}
89
82
90
- // Handle compound notes (only valid SUR notes)
91
- if ( note . compound ) {
83
+ if ( note . mixed ) {
84
+ const display = note . mixed . map ( part => {
85
+ if ( part . isSpecial ) return part . sur ;
86
+ if ( part . lyrics ) return part . lyrics ;
87
+ if ( part . compound ) {
88
+ return part . compound . map ( n => renderNoteWithOctave ( n ) ) . join ( '' ) ;
89
+ }
90
+ return renderNoteWithOctave ( part ) ;
91
+ } ) . join ( '' ) ;
92
+
92
93
return {
93
- text : note . compound . map ( n => n . sur ) . join ( '' ) ,
94
- type : 'note' ,
95
- display : note . compound . map ( n => {
96
- let display = n . sur || '' ;
97
- if ( n . octave === 'upper' ) {
98
- display = `${ display } ${ UPPER_BAR } ` ;
99
- } else if ( n . octave === 'lower' ) {
100
- display = `${ display } ${ LOWER_BAR } ` ;
101
- }
102
- return display ;
103
- } ) . join ( '' )
94
+ text : display ,
95
+ type : note . mixed . some ( part => part . lyrics ) ? 'lyrics' : 'note' ,
96
+ display
104
97
} ;
105
98
}
106
99
107
- // Handle single SUR notes
108
- if ( note . sur ) {
109
- let display = note . sur ;
110
- if ( note . octave === 'upper' ) {
111
- display = `${ display } ${ UPPER_BAR } ` ;
112
- } else if ( note . octave === 'lower' ) {
113
- display = `${ display } ${ LOWER_BAR } ` ;
114
- }
100
+ if ( note . compound ) {
101
+ const display = note . compound . map ( n => renderNoteWithOctave ( n ) ) . join ( '' ) ;
115
102
return {
116
- text : note . sur ,
103
+ text : display ,
117
104
type : 'note' ,
118
- display : display
105
+ display
119
106
} ;
120
107
}
121
108
122
- // Default case
123
109
return {
124
- text : '' ,
125
- type : 'special ' ,
126
- display : '−'
110
+ text : renderNoteWithOctave ( note ) ,
111
+ type : 'note ' ,
112
+ display : renderNoteWithOctave ( note )
127
113
} ;
128
114
} ;
129
115
116
+ const renderNote = ( beat : Note | number , beatIndex : number ) => {
117
+ if ( typeof beat === 'number' ) {
118
+ return < span className = "text-gray-500" > { beat } </ span > ;
119
+ }
120
+
121
+ const processed = processNote ( beat ) ;
122
+ return (
123
+ < span className = {
124
+ processed . type === 'lyrics' ? 'text-blue-600' :
125
+ processed . type === 'special' ? 'text-gray-400' :
126
+ 'text-black'
127
+ } >
128
+ { processed . display }
129
+ </ span >
130
+ ) ;
131
+ } ;
132
+
133
+ // Unicode combining characters for octave markers
134
+ const UPPER_BAR = '\u0305' ; // Combining overline
135
+ const LOWER_BAR = '\u0332' ; // Combining underline
136
+
137
+ const renderNoteWithOctave = ( note : { sur ?: string ; octave ?: 'upper' | 'middle' | 'lower' } ) => {
138
+ if ( ! note . sur ) return '' ;
139
+ if ( note . octave === 'upper' ) return `${ note . sur } ${ UPPER_BAR } ` ;
140
+ if ( note . octave === 'lower' ) return `${ note . sur } ${ LOWER_BAR } ` ;
141
+ return note . sur ;
142
+ } ;
143
+
130
144
const BeatGrid : React . FC < BeatGridProps > = ( { beats, totalBeats = 16 , groupSize = 4 } ) => {
131
145
console . log ( 'BeatGrid received beats:' , beats ) ;
132
146
const groups = [ ] ;
@@ -143,100 +157,23 @@ const BeatGrid: React.FC<BeatGridProps> = ({ beats, totalBeats = 16, groupSize =
143
157
groups . push ( group ) ;
144
158
}
145
159
146
- const renderNote = ( note : Note , index : number ) => {
147
- if ( 'lyrics' in note ) {
148
- return < span key = { index } style = { { color : 'blue' } } > { note . lyrics } </ span > ;
149
- }
150
-
151
- if ( note . isSpecial ) {
152
- return < span key = { index } style = { { color : 'gray' } } >
153
- { note . sur === '-' ? '−' : '·' }
154
- </ span > ;
155
- }
156
-
157
- const renderSingleNote = ( sur : string , octave ?: 'upper' | 'middle' | 'lower' ) => {
158
- if ( octave === 'upper' ) {
159
- return `${ sur } ${ UPPER_BAR } ` ;
160
- } else if ( octave === 'lower' ) {
161
- return `${ sur } ${ LOWER_BAR } ` ;
162
- }
163
- return sur ;
164
- } ;
165
-
166
- if ( 'mixed' in note ) {
167
- return (
168
- < span key = { index } >
169
- { note . mixed . map ( ( part , i ) => (
170
- < span key = { i } >
171
- { 'lyrics' in part ? (
172
- < span style = { { color : 'blue' } } > { part . lyrics } </ span >
173
- ) : 'compound' in part ? (
174
- < span style = { { color : 'black' } } >
175
- { part . compound . map ( ( n , j ) => renderSingleNote ( n . sur , n . octave ) ) . join ( '' ) }
176
- </ span >
177
- ) : (
178
- < span style = { { color : 'black' } } >
179
- { renderSingleNote ( part . sur || '' , part . octave ) }
180
- </ span >
181
- ) }
182
- { i < note . mixed . length - 1 ? ' ' : '' }
183
- </ span >
184
- ) ) }
185
- </ span >
186
- ) ;
187
- }
188
-
189
- if ( 'compound' in note ) {
190
- return (
191
- < span key = { index } style = { { color : 'black' } } >
192
- { note . compound . map ( ( n , i ) => renderSingleNote ( n . sur , n . octave ) ) . join ( '' ) }
193
- </ span >
194
- ) ;
195
- }
196
-
197
- return (
198
- < span key = { index } style = { { color : 'black' } } >
199
- { renderSingleNote ( note . sur || '' , note . octave ) }
200
- </ span >
201
- ) ;
202
- } ;
203
-
204
160
return (
205
161
< div className = "grid grid-cols-4 gap-0 border border-gray-200 rounded-lg" >
206
162
{ groups . map ( ( group , groupIndex ) => (
207
163
< div key = { groupIndex } className = "border-r border-gray-200 last:border-r-0" >
208
164
< div className = "grid grid-cols-4" >
209
- { group . map ( ( beat , beatIndex ) => {
210
- const key = `${ groupIndex } -${ beatIndex } ` ;
211
- if ( typeof beat === 'number' ) {
212
- return (
213
- < div key = { key } className = "text-center p-1 text-gray-500 text-sm border-r border-gray-100 last:border-r-0" >
214
- { beat }
215
- </ div >
216
- ) ;
217
- }
218
-
219
- const processedBeat = processNote ( beat ) ;
220
- console . log ( 'Processed beat for display:' , processedBeat ) ;
221
-
222
- return (
223
- < div
224
- key = { key }
225
- className = "text-center p-1 border-r border-gray-100 last:border-r-0 relative group"
226
- title = { `Beat ${ groupIndex * 4 + beatIndex + 1 } ` }
227
- >
228
- < div className = { `text-sm ${
229
- processedBeat . type === 'lyrics' ? 'text-blue-600' :
230
- 'text-black'
231
- } `} >
232
- { renderNote ( beat , beatIndex ) }
233
- </ div >
234
- < div className = "absolute -top-8 left-1/2 transform -translate-x-1/2 bg-black text-white text-xs px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none" >
235
- Beat { groupIndex * 4 + beatIndex + 1 }
236
- </ div >
165
+ { group . map ( ( beat , beatIndex ) => (
166
+ < div
167
+ key = { `${ groupIndex } -${ beatIndex } ` }
168
+ className = "text-center p-1 border-r border-gray-100 last:border-r-0 relative group"
169
+ title = { `Beat ${ groupIndex * 4 + beatIndex + 1 } ` }
170
+ >
171
+ { renderNote ( beat , beatIndex ) }
172
+ < div className = "absolute -top-8 left-1/2 transform -translate-x-1/2 bg-black text-white text-xs px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none" >
173
+ Beat { groupIndex * 4 + beatIndex + 1 }
237
174
</ div >
238
- ) ;
239
- } ) }
175
+ </ div >
176
+ ) ) }
240
177
</ div >
241
178
</ div >
242
179
) ) }
@@ -251,20 +188,14 @@ const parseSURFile = (content: string): SurDocument => {
251
188
252
189
const PDFExporter = ( { config, composition } ) => {
253
190
const generatePDF = ( ) => {
254
- // Create a hidden div with formatted content
255
191
const container = document . createElement ( 'div' ) ;
256
- container . style . display = 'none ' ;
192
+ container . style . padding = '20px ' ;
257
193
document . body . appendChild ( container ) ;
258
194
259
195
// Add title and metadata
260
- const title = document . createElement ( 'h1' ) ;
261
- title . textContent = config . name || 'Untitled' ;
262
- container . appendChild ( title ) ;
263
-
264
196
const metadata = document . createElement ( 'div' ) ;
265
197
metadata . innerHTML = `
266
- <p><strong>Raag:</strong> ${ config . raag || '' } </p>
267
- <p><strong>Taal:</strong> ${ config . taal || '' } </p>
198
+ <h1>${ config . name || '' } </h1>
268
199
<p><strong>Tempo:</strong> ${ config . tempo || '' } </p>
269
200
<p><strong>Beats per Row:</strong> ${ config . beats_per_row || '' } </p>
270
201
` ;
@@ -283,29 +214,78 @@ const PDFExporter = ({ config, composition }) => {
283
214
const lineDiv = document . createElement ( 'div' ) ;
284
215
lineDiv . className = 'line' ;
285
216
286
- const beats = line . beats . map ( note => {
287
- if ( typeof note === 'number' ) return note . toString ( ) ;
288
- return note . sur || note . lyrics || '-' ;
289
- } ) . join ( ' ' ) ;
217
+ line . beats . forEach ( beat => {
218
+ const beatSpan = document . createElement ( 'span' ) ;
219
+
220
+ if ( typeof beat === 'number' ) {
221
+ beatSpan . textContent = beat . toString ( ) ;
222
+ beatSpan . className = 'text-gray-500' ;
223
+ } else if ( beat . isSpecial ) {
224
+ beatSpan . textContent = beat . sur ;
225
+ beatSpan . className = 'text-gray-400' ;
226
+ } else if ( beat . lyrics ) {
227
+ beatSpan . textContent = beat . lyrics ;
228
+ beatSpan . className = 'text-blue-600' ;
229
+ } else if ( beat . mixed ) {
230
+ beatSpan . textContent = beat . mixed . map ( part => {
231
+ if ( part . isSpecial ) return part . sur ;
232
+ if ( part . lyrics ) return part . lyrics ;
233
+ if ( part . compound ) {
234
+ return part . compound . map ( n => renderNoteWithOctave ( n ) ) . join ( '' ) ;
235
+ }
236
+ return renderNoteWithOctave ( part ) ;
237
+ } ) . join ( '' ) ;
238
+ } else if ( beat . compound ) {
239
+ beatSpan . textContent = beat . compound . map ( note =>
240
+ renderNoteWithOctave ( note )
241
+ ) . join ( '' ) ;
242
+ } else {
243
+ beatSpan . textContent = renderNoteWithOctave ( beat ) ;
244
+ }
245
+
246
+ lineDiv . appendChild ( beatSpan ) ;
247
+ } ) ;
290
248
291
- lineDiv . textContent = beats ;
292
249
section . appendChild ( lineDiv ) ;
293
250
} ) ;
294
251
295
252
container . appendChild ( section ) ;
296
253
} ) ;
297
254
298
- // Use html2pdf to generate PDF
299
- const element = container ;
255
+ // Add CSS styles
256
+ const style = document . createElement ( 'style' ) ;
257
+ style . textContent = `
258
+ .section { margin-bottom: 24px; }
259
+ .line {
260
+ display: grid;
261
+ grid-template-columns: repeat(16, 1fr);
262
+ border-left: 1px solid #e5e7eb;
263
+ border-right: 1px solid #e5e7eb;
264
+ }
265
+ .line > span {
266
+ padding: 4px;
267
+ border-right: 1px solid #e5e7eb;
268
+ text-align: center;
269
+ }
270
+ .line > span:last-child {
271
+ border-right: none;
272
+ }
273
+ .text-blue-600 { color: #2563eb; }
274
+ .text-gray-500 { color: #6b7280; }
275
+ .text-gray-400 { color: #9ca3af; }
276
+ ` ;
277
+ container . appendChild ( style ) ;
278
+
279
+ // Generate PDF
300
280
const opt = {
301
- margin : 1 ,
302
- filename : `${ config . name || 'composition' } .pdf` ,
303
- image : { type : 'jpeg' , quality : 0.98 } ,
304
- html2canvas : { scale : 2 } ,
305
- jsPDF : { unit : 'in' , format : 'letter' , orientation : 'portrait' }
281
+ margin : 1 ,
282
+ filename : `${ config . name || 'composition' } .pdf` ,
283
+ image : { type : 'jpeg' , quality : 0.98 } ,
284
+ html2canvas : { scale : 2 } ,
285
+ jsPDF : { unit : 'in' , format : 'letter' , orientation : 'portrait' }
306
286
} ;
307
287
308
- html2pdf ( ) . from ( element ) . set ( opt ) . save ( ) . then ( ( ) => {
288
+ html2pdf ( ) . from ( container ) . set ( opt ) . save ( ) . then ( ( ) => {
309
289
document . body . removeChild ( container ) ;
310
290
} ) ;
311
291
} ;
0 commit comments