diff --git a/src/lib/components/score/ScoreCard.svelte b/src/lib/components/score/ScoreCard.svelte new file mode 100644 index 0000000..bc55396 --- /dev/null +++ b/src/lib/components/score/ScoreCard.svelte @@ -0,0 +1,30 @@ + + +
onClick(score.id)} + on:keydown={(e) => e.key === 'Enter' && onClick(score.id)} + tabindex="0" + role="button" +> + {score.title} +
+

{score.title}

+
+ {#if score.tags && score.tags.length > 0} + {#each score.tags as tag} + + {/each} + {:else} + No tags + {/if} +
+

Last modified: {score.lastModified}

+
+
diff --git a/src/lib/components/score/ScoreSearch.svelte b/src/lib/components/score/ScoreSearch.svelte new file mode 100644 index 0000000..22f1240 --- /dev/null +++ b/src/lib/components/score/ScoreSearch.svelte @@ -0,0 +1,25 @@ + + +
+ + +
diff --git a/src/lib/components/score/TagBadge.svelte b/src/lib/components/score/TagBadge.svelte new file mode 100644 index 0000000..b070357 --- /dev/null +++ b/src/lib/components/score/TagBadge.svelte @@ -0,0 +1,12 @@ + + + + {tag} + diff --git a/src/lib/components/score/TagFilter.svelte b/src/lib/components/score/TagFilter.svelte new file mode 100644 index 0000000..0177575 --- /dev/null +++ b/src/lib/components/score/TagFilter.svelte @@ -0,0 +1,33 @@ + + +
+ + + +
diff --git a/src/lib/components/shell/Header.svelte b/src/lib/components/shell/Header.svelte index e14b8eb..974259b 100644 --- a/src/lib/components/shell/Header.svelte +++ b/src/lib/components/shell/Header.svelte @@ -1,173 +1,217 @@ - - - Ars Antiqua Online - - - - -
- - {#if userName} -
isDropdownOpen = false}> - - {#if isDropdownOpen} - - {/if} -
- {/if} - -
-
+ + + Ars Antiqua Online + + + + +
+ + {#if userName} +
(isDropdownOpen = false)}> + + {#if isDropdownOpen} + + {/if} +
+ {/if} + +
+
{#if isOpen} - + {/if} \ No newline at end of file + .invert { + filter: invert(1); + } + diff --git a/src/lib/types.ts b/src/lib/types.ts new file mode 100644 index 0000000..de27f0f --- /dev/null +++ b/src/lib/types.ts @@ -0,0 +1,19 @@ +export interface Score { + id: string; + title: string; + composer: string | 'Anonymous'; + dateComposed: string; + genre: string; + source: string; + notation: string; + voices: number; + language: string; + instruments: string[]; + description: string; + transcribedBy: string; + transcriptionDate: string; + lastModified: string; + tags: string[]; + thumbnailUrl: string; + meiData: string; +}; \ No newline at end of file diff --git a/src/lib/utils/colorUtils.test.ts b/src/lib/utils/colorUtils.test.ts new file mode 100644 index 0000000..61cf605 --- /dev/null +++ b/src/lib/utils/colorUtils.test.ts @@ -0,0 +1,95 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { getTagColor, getContrastColor } from './colorUtils'; // Adjust the import path as needed + +describe('Color Utils', () => { + beforeEach(() => { + // Clear the color cache before each test + (getTagColor as any).colorCache = new Map(); + }); + + describe('getTagColor', () => { + it('should return a valid HSL color string', () => { + const color = getTagColor('test'); + expect(color).toMatch(/^hsl\(\d+,\s*\d+%,\s*\d+%\)$/); + }); + + it('should return the same color for the same tag', () => { + const color1 = getTagColor('sameTag'); + const color2 = getTagColor('sameTag'); + expect(color1).toBe(color2); + }); + + it('should return different colors for different tags', () => { + const color1 = getTagColor('tag1'); + const color2 = getTagColor('tag2'); + expect(color1).not.toBe(color2); + }); + + it('should handle empty string', () => { + const color = getTagColor(''); + expect(color).toMatch(/^hsl\(\d+,\s*\d+%,\s*\d+%\)$/); + }); + + it('should handle long strings', () => { + const longTag = 'a'.repeat(1000); + const color = getTagColor(longTag); + expect(color).toMatch(/^hsl\(\d+,\s*\d+%,\s*\d+%\)$/); + }); + + it('should generate colors within the specified ranges', () => { + const color = getTagColor('test'); + const [hue, saturation, lightness] = color.match(/\d+/g)!.map(Number); + expect(hue).toBeGreaterThanOrEqual(0); + expect(hue).toBeLessThan(360); + expect(saturation).toBeGreaterThanOrEqual(25); + expect(saturation).toBeLessThanOrEqual(45); + expect(lightness).toBeGreaterThanOrEqual(85); + expect(lightness).toBeLessThanOrEqual(95); + }); + }); + + describe('getContrastColor', () => { + it('should return dark gray for light background', () => { + const contrastColor = getContrastColor('hsl(0, 0%, 90%)'); + expect(contrastColor).toBe('#4A4A4A'); + }); + + it('should return light gray for dark background', () => { + const contrastColor = getContrastColor('hsl(0, 0%, 20%)'); + expect(contrastColor).toBe('#E0E0E0'); + }); + + it('should handle edge case of pure white', () => { + const contrastColor = getContrastColor('hsl(0, 0%, 100%)'); + expect(contrastColor).toBe('#4A4A4A'); + }); + + it('should handle edge case of pure black', () => { + const contrastColor = getContrastColor('hsl(0, 0%, 0%)'); + expect(contrastColor).toBe('#E0E0E0'); + }); + + it('should handle invalid HSL string gracefully', () => { + const contrastColor = getContrastColor('not a color'); + expect(contrastColor).toBe('#E0E0E0'); // Defaults to light gray for invalid input + }); + }); + + describe('Integration of getTagColor and getContrastColor', () => { + it('should generate consistent contrast colors for the same tag', () => { + const tagColor = getTagColor('testTag'); + const contrastColor1 = getContrastColor(tagColor); + const contrastColor2 = getContrastColor(tagColor); + expect(contrastColor1).toBe(contrastColor2); + }); + + it('should generate appropriate contrast colors for various tags', () => { + const tags = ['light', 'dark', 'medium', 'vibrant', 'muted']; + tags.forEach(tag => { + const tagColor = getTagColor(tag); + const contrastColor = getContrastColor(tagColor); + expect(contrastColor).toMatch(/^#[A-F0-9]{6}$/); + }); + }); + }); +}); \ No newline at end of file diff --git a/src/lib/utils/colorUtils.ts b/src/lib/utils/colorUtils.ts new file mode 100644 index 0000000..60703f1 --- /dev/null +++ b/src/lib/utils/colorUtils.ts @@ -0,0 +1,54 @@ +const colorCache = new Map(); + +export function getTagColor(tag: string): string { + if (colorCache.has(tag)) { + return colorCache.get(tag); + } + + let hash = 0; + for (let i = 0; i < tag.length; i++) { + hash = tag.charCodeAt(i) + ((hash << 5) - hash); + } + + const hue = hash % 360; + const saturation = 25 + (hash % 20); // Lower saturation for more muted colors + const lightness = 85 + (hash % 10); // Higher lightness for softer colors + const color = `hsl(${hue}, ${saturation}%, ${lightness}%)`; + + colorCache.set(tag, color); + return color; +} + +export function getContrastColor(bgColor: string): string { + // Convert HSL to RGB + const hslMatch = bgColor.match(/\d+/g); + const hsl = hslMatch ? hslMatch.map(Number) : [0, 0, 0]; + const h = hsl[0] / 360; + const s = hsl[1] / 100; + const l = hsl[2] / 100; + + let r, g, b; + if (s === 0) { + r = g = b = l; + } else { + const hue2rgb = (p: number, q: number, t: number): number => { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1 / 6) return p + (q - p) * 6 * t; + if (t < 1 / 2) return q; + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; + return p; + }; + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + r = hue2rgb(p, q, h + 1 / 3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1 / 3); + } + + // Calculate luminance + const luminance = 0.299 * r + 0.587 * g + 0.114 * b; + + // Return dark gray or light gray based on luminance + return luminance > 0.6 ? '#4A4A4A' : '#E0E0E0'; +} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index a4c368a..06c6919 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,65 +1,4 @@ - - - - Historical Music Notation Examples - - -
-

Historical Music Notation Examples

- -
-

- This page demonstrates various examples of historical music notation using the Music Encoding Initiative (MEI) format, rendered with the Verovio library. -

-
- - {#each meiFiles as file} -
-

{file.title}

-
- {#if loadingStatus[file.path]} -

Loading musical score data...

- {:else if errors[file.path]} -

{errors[file.path]}

- {:else} - - {/if} -
-
- {/each} - -
-

About Historical Music Notation

-

- Music notation has evolved significantly over the centuries. From the early neumes of medieval chant to the complex scores of the Baroque era, each period brought its own innovations and conventions to written music. The examples above showcase some of these historical styles, demonstrating the rich diversity of musical notation throughout Western music history. -

-
-
\ No newline at end of file + diff --git a/src/routes/browse/+page.svelte b/src/routes/browse/+page.svelte new file mode 100644 index 0000000..8e22355 --- /dev/null +++ b/src/routes/browse/+page.svelte @@ -0,0 +1,143 @@ + + +
+

Browse All Scores

+ +
+ + +
+ + {#if filteredScores.length > 0} +
+ {#each filteredScores as score (score.id)} + + {/each} +
+ {:else} +
+

No scores found matching your search criteria.

+
+ {/if} +
diff --git a/src/routes/score/[id]/+page.svelte b/src/routes/score/[id]/+page.svelte new file mode 100644 index 0000000..8ce9de2 --- /dev/null +++ b/src/routes/score/[id]/+page.svelte @@ -0,0 +1,237 @@ + + +
+
+
+
+
+

Score Details

+ +
+ {scoreDetails.title} + +
+

+ + Musical Details +

+
+
Genre
+
{scoreDetails.genre}
+
Voices
+
{scoreDetails.voices}
+
Instruments
+
{scoreDetails.instruments.join(', ')}
+
Notation
+
{scoreDetails.notation}
+
Language
+
{scoreDetails.language}
+
+
+ +
+

+ + Source Information +

+
+
Source
+
{scoreDetails.source}
+
+
+ +
+

+ + Transcription Details +

+
+
Transcribed By
+
{scoreDetails.transcribedBy}
+
Transcription Date
+
{scoreDetails.transcriptionDate}
+
Last Modified
+
{scoreDetails.lastModified}
+
+
+ +
+

Description

+

{scoreDetails.description}

+
+ +
+ {#if scoreDetails.tags && scoreDetails.tags.length > 0} + {#each scoreDetails.tags as tag} + + {/each} + {:else} + No tags + {/if} +
+
+
+ +
+
+ +
+
+

{scoreDetails.title}

+

+ By {scoreDetails.composer} • {scoreDetails.dateComposed} +

+
+ {#if !isDetailsVisible} + + {/if} +
+
+
+
+ +
+
+
+
+
diff --git a/vite.config.ts b/vite.config.ts index 1164d28..6316b1e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -32,6 +32,7 @@ export default defineConfig({ '**/index.ts', '**/lib/stores/**', // TODO: Fix coverage for stores '**/lib/score/verovioWorker.ts', // Worker files are not included in coverage + '**/lib/types.ts', // Types are not included in coverage ], include: ['src/**/*.ts', 'src/**/*.js'], },