Skip to content

Commit 2ce2820

Browse files
committed
updated web
1 parent 2a72d31 commit 2ce2820

File tree

2 files changed

+162
-203
lines changed

2 files changed

+162
-203
lines changed

websur/src/artifact-component.tsx

Lines changed: 124 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@ import { Download, FileText } from 'lucide-react';
66
import { SurParser, SurDocument, Note } from './lib/sur-parser';
77
import html2pdf from 'html2pdf.js';
88

9-
const UPPER_BAR = '\u0304'; // Macron above: S̄
10-
const LOWER_BAR = '\u0332'; // Macron below: S̲
11-
129
const DEFAULT_SUR = `%% CONFIG
1310
name: "Albela Sajan"
1411
raag: "bhoopali"
@@ -67,18 +64,14 @@ const processNote = (note: Note): {
6764
type: 'note' | 'lyrics' | 'special';
6865
display: string;
6966
} => {
70-
console.log('Processing note for display:', note);
71-
72-
// Handle special characters first
7367
if (note.isSpecial) {
7468
return {
7569
text: note.sur || '',
7670
type: 'special',
77-
display: note.sur === '-' ? '−' : '
71+
display: note.sur || ''
7872
};
7973
}
8074

81-
// Handle lyrics next (including bracketed lyrics)
8275
if (note.lyrics) {
8376
return {
8477
text: note.lyrics,
@@ -87,46 +80,67 @@ const processNote = (note: Note): {
8780
};
8881
}
8982

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+
9293
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
10497
};
10598
}
10699

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('');
115102
return {
116-
text: note.sur,
103+
text: display,
117104
type: 'note',
118-
display: display
105+
display
119106
};
120107
}
121108

122-
// Default case
123109
return {
124-
text: '',
125-
type: 'special',
126-
display: '−'
110+
text: renderNoteWithOctave(note),
111+
type: 'note',
112+
display: renderNoteWithOctave(note)
127113
};
128114
};
129115

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+
130144
const BeatGrid: React.FC<BeatGridProps> = ({ beats, totalBeats = 16, groupSize = 4 }) => {
131145
console.log('BeatGrid received beats:', beats);
132146
const groups = [];
@@ -143,100 +157,23 @@ const BeatGrid: React.FC<BeatGridProps> = ({ beats, totalBeats = 16, groupSize =
143157
groups.push(group);
144158
}
145159

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-
204160
return (
205161
<div className="grid grid-cols-4 gap-0 border border-gray-200 rounded-lg">
206162
{groups.map((group, groupIndex) => (
207163
<div key={groupIndex} className="border-r border-gray-200 last:border-r-0">
208164
<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}
237174
</div>
238-
);
239-
})}
175+
</div>
176+
))}
240177
</div>
241178
</div>
242179
))}
@@ -251,20 +188,14 @@ const parseSURFile = (content: string): SurDocument => {
251188

252189
const PDFExporter = ({ config, composition }) => {
253190
const generatePDF = () => {
254-
// Create a hidden div with formatted content
255191
const container = document.createElement('div');
256-
container.style.display = 'none';
192+
container.style.padding = '20px';
257193
document.body.appendChild(container);
258194

259195
// Add title and metadata
260-
const title = document.createElement('h1');
261-
title.textContent = config.name || 'Untitled';
262-
container.appendChild(title);
263-
264196
const metadata = document.createElement('div');
265197
metadata.innerHTML = `
266-
<p><strong>Raag:</strong> ${config.raag || ''}</p>
267-
<p><strong>Taal:</strong> ${config.taal || ''}</p>
198+
<h1>${config.name || ''}</h1>
268199
<p><strong>Tempo:</strong> ${config.tempo || ''}</p>
269200
<p><strong>Beats per Row:</strong> ${config.beats_per_row || ''}</p>
270201
`;
@@ -283,29 +214,78 @@ const PDFExporter = ({ config, composition }) => {
283214
const lineDiv = document.createElement('div');
284215
lineDiv.className = 'line';
285216

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+
});
290248

291-
lineDiv.textContent = beats;
292249
section.appendChild(lineDiv);
293250
});
294251

295252
container.appendChild(section);
296253
});
297254

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
300280
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' }
306286
};
307287

308-
html2pdf().from(element).set(opt).save().then(() => {
288+
html2pdf().from(container).set(opt).save().then(() => {
309289
document.body.removeChild(container);
310290
});
311291
};

0 commit comments

Comments
 (0)