Repository files navigation

Harmony

Off you go!

# for good measure
npm run songs
npm run dev
# open: http://localhost:5173/demo/chord-search/

Generating the song dataset locally

Chord data lives in the sibling harmony-data repo and is not committed to this project. The app reads static/data/songs.json (gitignored).

To build or refresh the dataset from harmony-data:

npm run songs

This runs tasks/build-songs.js, which:

  • reads corrected Top-10 songs from ../harmony-data/songs/corrected/
  • joins Billboard flags and chart year from ../harmony-data/data/tracker.csv and billboard.csv
  • converts chord data into the format used by the chord-search matcher
  • writes static/data/songs.json (full corrected Top-10 corpus, used by all /demo/ pages)

Note: src/data/hand-corrected-songs.ts holds handCorrectedSongs; src/data/hand-reviewed-songs.ts holds problematicSongs and trickySongsToMatchCorrectly for reviewing song data before use in the UI. Hand corrections are applied at runtime when loading songs.json.

When harmony-data is updated (e.g. after pulling fresh scrapes):

cd ../harmony-data && git pull
cd ../harmony && npm run songs
npm run dev

The /demo/chord-search route fetches /data/songs.json at runtime. If the file is missing, run npm run songs first.

Coverage cache (IndexedDB)

All /demo/ pages that run the core-progression coverage algorithm (define-chord-progression, core-progressions, harmony-map, artists) cache the result in the browser's IndexedDB so the worker-pool computation only runs once.

The cache key is a SHA-256 fingerprint of:

  • Core progression matching fields — each progression's name, chordProgression, and scale (not group names, descriptions, or colors)
  • Song corpus — sorted songKey + section count + roman-token count per song, so re-running npm run songs or editing hand-corrected-songs.ts automatically invalidates the cache
  • Schema versionCOVERAGE_CACHE_SCHEMA_VERSION in coverageCacheKey.ts

When to bump COVERAGE_CACHE_SCHEMA_VERSION: if you change matching algorithm logic (files under progression-matching-logic/) in a way that would produce different SongCoverageEntry results for the same input data, increment the constant to force all browsers to recompute.

Naming map clusters

/harmony-map circles dense groups of songs and can label them (e.g. "doo wop", "axis"). Those names are shared, hand-curated data committed in src/data/named-clusters.ts — not browser storage — so everyone who pulls the repo sees the same names.

Each entry is { anchorSongKey, name }. A cluster is identified by one representative song, not by its exact membership — membership shifts a little every time the embedding re-runs, but as long as that one song is still grouped there, the name still resolves. This also means there should only ever be one entry per name (and per anchor song); naming a cluster from a newly highlighted song replaces its existing entry rather than adding a second one.

To name or rename a cluster:

  1. On /harmony-map, select a song in the cluster you want to name and hit highlight in the song inspector panel (a song must be highlighted — that's what becomes the anchor).
  2. Click an empty area inside the cluster's dashed circle. A text input appears; type the name and hit Enter.
  3. This updates the map live in your session, but a browser page can't write to files on disk, so it isn't saved yet. Open the devtools console — naming logs a paste-ready line, e.g.:
    Cluster named — add to src/data/named-clusters.ts to persist:
    { anchorSongKey: "jason-mraz__im-yours", name: "axis" }
    
  4. Copy that into the namedClusters array in src/data/named-clusters.ts (replacing the old entry if you're renaming/re-anchoring one that already exists), then commit and push as usual. Your collaborator picks it up on their next git pull.

MIDI input, classification, search

Quick start to use your keyboard

Designed for Reface CP MIDI input set to the keyboard's second octave on the octave slider (next to volume).

You can do common actions directly from the keyboard:

  • Clear search: hit lowest three notes
  • Toggle search on/off (ie so you can play chords without them scrambling your current results): highest 3 notes

Implementation details: harmony/src/chord-processing/README.md


Svelte Starter [default info from the pudding starter repo]

This starter template aims to quickly scaffold a SvelteKit project, designed around data-driven, visual stories at The Pudding.

Notes

  • Do not use or reproduce The Pudding logos or fonts without written permission.
  • Prettier Formatting: Disable any text editor Prettier extensions to take advantage of the built-in rules.

Features

  • ArchieML for micro-CMS powered by Google Docs and Sheets
  • Lucide Icons for simple/easy svg icons
  • Style Dictionary for CSS/JS style parity
  • Runed for svelte5 rune utilities
  • CSV, JSON, and SVG imports
  • SSR static-hosted builds by default

Quickstart

From Scratch

  • Click the green Use this template button above.
  • Alternatively: npx degit the-pudding/svelte-starter my-project

Pre-existing Project

  • clone the repo

Installation

  • In your local repo run pnpm install or npm install

Development

npm run dev

Change the script in package.json to "dev": "svelte-kit dev --host" to test on your local network on a different device.

Deploy

Check out the Makefile for specific tasks.

Staging (on Github)

npm run staging

Production (on AWS for pudding.cool)

npm run prodution

Manual

npm run build

This generates a directory called build with the statically rendered app.

Password-Protected

To create a password-protected build:

Make sure you have a .env file in your root with a value of PASSWORD=yourpassword

make protect

Then run either make github or make pudding.

Style

There are a few stylesheets included by default in src/styles. Refer to them in app.css, the place for applying global styles.

For variable parity in both CSS and JS, modify files in the properties folder using the Style Dictionary API.

Run npm run style to regenerate the style dictionary.

Some css utility classes in reset.css

  • .sr-only: makes content invisible available for screen reader
  • .text-outline: adds a psuedo stroke to text element

Custom Fonts

For locally hosted fonts, simply add the font to the static/assets folder and include a reference in src/styles/font.css, making sure the url starts with "assets/...".

Google Docs and Sheets

  • Create a Google Doc or Sheet
  • Click Share -> Advanced -> Change... -> Anyone with this link
  • In the address bar, grab the ID - eg. "...com/document/d/1IiA5a5iCjbjOYvZVgPcjGzMy5PyfCzpPF-LnQdCdFI0/edit"
  • paste in the ID above into google.config.js, and set the filepath to where you want the file saved
  • If you want to do a Google Sheet, be sure to include the gid value in the url as well

Running npm run gdoc at any point (even in new tab while server is running) will fetch the latest from all Docs and Sheets.

Structural Overview

Pages

The src/routes directory contains pages for your app. For a single-page app (most cases) you don't have to modify anything in here. +page.svelte represents the root page, think of it as the index.html file. It is prepopulated with a few things like metadata and font preloading. It also includes a reference to a blank slate component src/components/Index.svelte. This is the file you want to really start in for your app.

Embedding Data

For smaller datasets, it is often great to embed the data into the HTML file. If you want to use data as-is, you can use normal import syntax (e.g., import data from "$data/file.csv"). If you are working with data but you want to preserve the original or clean/parse just what you need to use in the browser to optimize the front-end payload, you can load it via +page.server.js, do some work on it, and return just what you need. This is passed automatically to +page.svelte and accessible in any component with getContext("data").

Pre-loaded helpers

Components

Located in src/components.

// UsageimportExamplefrom"$components/Example.svelte";
  • Footer.svelte: Pudding recirculation and social links.
  • Header.svelte: Pudding masthead.

Helper Components

Located in src/components/helpers.

// UsageimportExamplefrom"$components/helpers/Example.svelte";

Available

  • Scrolly.svelte: Scrollytelling.

Need to migrate

  • ButtonSet.svelte: Accessible button group inputs.
  • Chunk.svelte: Split text into smaller dom element chunks.
  • Countdown.svelte: Countdown timer text.
  • DarkModeToggle.svelte: A toggle button for dark mode.
  • Figure.svelte: A barebones chart figure component to handle slots.
  • MotionToggle.svelte: A toggle button to enable/disable front-end user motion preference.
  • Range.svelte: Customizable range slider.
  • ShareLink.svelte: Button to share link natively/copy to clipboard.
  • SortTable.svelte: Sortable semantic table with customizable props.
  • Slider.svelte (and Slider.Slide.svelte): A slider widget, especially useful for swipe/slide stories.
  • Tap.svelte: Edge-of-screen tapping library, designed to integrate with slider.
  • Tip.svelte: Button that links to Strip payment link.
  • Toggle.svelte: Accessible toggle inputs.

Headless Components

bits UI comes pre-installed. It is recommended to use these for any UI components.

Layercake Chart Components

Starter templates for various chart types to be used with LayerCake. Located in src/components/layercake.

Note: You must install the module layercake first.

// UsageimportExamplefrom"$components/layercake/Example.svelte";

Actions

Located in src/actions.

// Usageimportexamplefrom"$actions/action.js";
  • canTab.js: enable/disable tabbing on child elements.
  • checkOverlap.js: Label overlapping detection. Loops through selection of nodes and adds a class to the ones that are overlapping. Once one is hidden it ignores it.
  • focusTrap.js: Enable a keyboard focus trap for modals and menus.
  • keepWithinBox.js: Offsets and element left/right to stay within parent.
  • inView.js: detect when an element enters or exits the viewport.
  • resize.js: detect when an element is resized.

Runes

These are located in src/runes. You can put custom ones in src/runes/misc.js or create unique files for more complex ones.

import{example}from"$runes/misc/misc.js";
  • useWindowDimensions: returns an object { width, height } of the viewport dimensions. It is debounced for performance.
  • useClipboard: copy content to clipboard.
  • useFetcher: load async data from endpoints (local or external).
  • useWindowFocus: determine if the window is in focus or not.

For more preset runes, use runed which is preloaded.

Utils

Located in src/utils/.

// Usageimportexamplefrom"$utils/example.js";
  • checkScrollDir.js: Gets the user's scroll direction ("up" or "down")
  • csvDownload.js: Converts a flat array of data to CSV content ready to be used as an href value for download.
  • generateId.js: Generate an alphanumeric id.
  • loadCsv.js: Loads and parses a CSV file.
  • loadImage.js: Loads an image.
  • loadJson.js: Loads and parses a JSON file.
  • loadPixels.js: Loads the pixel data of an image via an offscreen canvas.
  • localStorage.js: Read and write to local storage.
  • mapToArray.js: Convenience function to convert a map to an array.
  • move.js: transform translate function shorthand.
  • transformSvg.js: Custom transition lets you apply an svg transform property with the in/out svelte transition. Parameters (with defaults):
  • translate.js: Convenience function for transform translate css.
  • urlParams.js: Get and set url parameters.

Tips

Image asset paths

For img tags, use relative paths:

<imgsrc="assets/demo/test.jpg" />

or use base if on a sub route:

<script>import{base}from"$app/paths";</script><imgsrc="{base}/assets/demo/test.jpg" />

For CSS background images, use absolute paths:

background:url("/assets/demo/test.jpg");

View example code in the preloaded demo.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

Harmony

Off you go!

# for good measure
npm run songs
npm run dev
# open: http://localhost:5173/demo/chord-search/

Generating the song dataset locally

Chord data lives in the sibling harmony-data repo and is not committed to this project. The app reads static/data/songs.json (gitignored).

To build or refresh the dataset from harmony-data:

npm run songs

This runs tasks/build-songs.js, which:

  • reads corrected Top-10 songs from ../harmony-data/songs/corrected/
  • joins Billboard flags and chart year from ../harmony-data/data/tracker.csv and billboard.csv
  • converts chord data into the format used by the chord-search matcher
  • writes static/data/songs.json (full corrected Top-10 corpus, used by all /demo/ pages)

Note: src/data/hand-corrected-songs.ts holds handCorrectedSongs; src/data/hand-reviewed-songs.ts holds problematicSongs and trickySongsToMatchCorrectly for reviewing song data before use in the UI. Hand corrections are applied at runtime when loading songs.json.

When harmony-data is updated (e.g. after pulling fresh scrapes):

cd ../harmony-data && git pull
cd ../harmony && npm run songs
npm run dev

The /demo/chord-search route fetches /data/songs.json at runtime. If the file is missing, run npm run songs first.

Coverage cache (IndexedDB)

All /demo/ pages that run the core-progression coverage algorithm (define-chord-progression, core-progressions, harmony-map, artists) cache the result in the browser's IndexedDB so the worker-pool computation only runs once.

The cache key is a SHA-256 fingerprint of:

  • Core progression matching fields — each progression's name, chordProgression, and scale (not group names, descriptions, or colors)
  • Song corpus — sorted songKey + section count + roman-token count per song, so re-running npm run songs or editing hand-corrected-songs.ts automatically invalidates the cache
  • Schema versionCOVERAGE_CACHE_SCHEMA_VERSION in coverageCacheKey.ts

When to bump COVERAGE_CACHE_SCHEMA_VERSION: if you change matching algorithm logic (files under progression-matching-logic/) in a way that would produce different SongCoverageEntry results for the same input data, increment the constant to force all browsers to recompute.

Naming map clusters

/harmony-map circles dense groups of songs and can label them (e.g. "doo wop", "axis"). Those names are shared, hand-curated data committed in src/data/named-clusters.ts — not browser storage — so everyone who pulls the repo sees the same names.

Each entry is { anchorSongKey, name }. A cluster is identified by one representative song, not by its exact membership — membership shifts a little every time the embedding re-runs, but as long as that one song is still grouped there, the name still resolves. This also means there should only ever be one entry per name (and per anchor song); naming a cluster from a newly highlighted song replaces its existing entry rather than adding a second one.

To name or rename a cluster:

  1. On /harmony-map, select a song in the cluster you want to name and hit highlight in the song inspector panel (a song must be highlighted — that's what becomes the anchor).
  2. Click an empty area inside the cluster's dashed circle. A text input appears; type the name and hit Enter.
  3. This updates the map live in your session, but a browser page can't write to files on disk, so it isn't saved yet. Open the devtools console — naming logs a paste-ready line, e.g.:
    Cluster named — add to src/data/named-clusters.ts to persist:
    { anchorSongKey: "jason-mraz__im-yours", name: "axis" }
    
  4. Copy that into the namedClusters array in src/data/named-clusters.ts (replacing the old entry if you're renaming/re-anchoring one that already exists), then commit and push as usual. Your collaborator picks it up on their next git pull.

MIDI input, classification, search

Quick start to use your keyboard

Designed for Reface CP MIDI input set to the keyboard's second octave on the octave slider (next to volume).

You can do common actions directly from the keyboard:

  • Clear search: hit lowest three notes
  • Toggle search on/off (ie so you can play chords without them scrambling your current results): highest 3 notes

Implementation details: harmony/src/chord-processing/README.md


Svelte Starter [default info from the pudding starter repo]

This starter template aims to quickly scaffold a SvelteKit project, designed around data-driven, visual stories at The Pudding.

Notes

  • Do not use or reproduce The Pudding logos or fonts without written permission.
  • Prettier Formatting: Disable any text editor Prettier extensions to take advantage of the built-in rules.

Features

  • ArchieML for micro-CMS powered by Google Docs and Sheets
  • Lucide Icons for simple/easy svg icons
  • Style Dictionary for CSS/JS style parity
  • Runed for svelte5 rune utilities
  • CSV, JSON, and SVG imports
  • SSR static-hosted builds by default

Quickstart

From Scratch

  • Click the green Use this template button above.
  • Alternatively: npx degit the-pudding/svelte-starter my-project

Pre-existing Project

  • clone the repo

Installation

  • In your local repo run pnpm install or npm install

Development

npm run dev

Change the script in package.json to "dev": "svelte-kit dev --host" to test on your local network on a different device.

Deploy

Check out the Makefile for specific tasks.

Staging (on Github)

npm run staging

Production (on AWS for pudding.cool)

npm run prodution

Manual

npm run build

This generates a directory called build with the statically rendered app.

Password-Protected

To create a password-protected build:

Make sure you have a .env file in your root with a value of PASSWORD=yourpassword

make protect

Then run either make github or make pudding.

Style

There are a few stylesheets included by default in src/styles. Refer to them in app.css, the place for applying global styles.

For variable parity in both CSS and JS, modify files in the properties folder using the Style Dictionary API.

Run npm run style to regenerate the style dictionary.

Some css utility classes in reset.css

  • .sr-only: makes content invisible available for screen reader
  • .text-outline: adds a psuedo stroke to text element

Custom Fonts

For locally hosted fonts, simply add the font to the static/assets folder and include a reference in src/styles/font.css, making sure the url starts with "assets/...".

Google Docs and Sheets

  • Create a Google Doc or Sheet
  • Click Share -> Advanced -> Change... -> Anyone with this link
  • In the address bar, grab the ID - eg. "...com/document/d/1IiA5a5iCjbjOYvZVgPcjGzMy5PyfCzpPF-LnQdCdFI0/edit"
  • paste in the ID above into google.config.js, and set the filepath to where you want the file saved
  • If you want to do a Google Sheet, be sure to include the gid value in the url as well

Running npm run gdoc at any point (even in new tab while server is running) will fetch the latest from all Docs and Sheets.

Structural Overview

Pages

The src/routes directory contains pages for your app. For a single-page app (most cases) you don't have to modify anything in here. +page.svelte represents the root page, think of it as the index.html file. It is prepopulated with a few things like metadata and font preloading. It also includes a reference to a blank slate component src/components/Index.svelte. This is the file you want to really start in for your app.

Embedding Data

For smaller datasets, it is often great to embed the data into the HTML file. If you want to use data as-is, you can use normal import syntax (e.g., import data from "$data/file.csv"). If you are working with data but you want to preserve the original or clean/parse just what you need to use in the browser to optimize the front-end payload, you can load it via +page.server.js, do some work on it, and return just what you need. This is passed automatically to +page.svelte and accessible in any component with getContext("data").

Pre-loaded helpers

Components

Located in src/components.

// UsageimportExamplefrom"$components/Example.svelte";
  • Footer.svelte: Pudding recirculation and social links.
  • Header.svelte: Pudding masthead.

Helper Components

Located in src/components/helpers.

// UsageimportExamplefrom"$components/helpers/Example.svelte";

Available

  • Scrolly.svelte: Scrollytelling.

Need to migrate

  • ButtonSet.svelte: Accessible button group inputs.
  • Chunk.svelte: Split text into smaller dom element chunks.
  • Countdown.svelte: Countdown timer text.
  • DarkModeToggle.svelte: A toggle button for dark mode.
  • Figure.svelte: A barebones chart figure component to handle slots.
  • MotionToggle.svelte: A toggle button to enable/disable front-end user motion preference.
  • Range.svelte: Customizable range slider.
  • ShareLink.svelte: Button to share link natively/copy to clipboard.
  • SortTable.svelte: Sortable semantic table with customizable props.
  • Slider.svelte (and Slider.Slide.svelte): A slider widget, especially useful for swipe/slide stories.
  • Tap.svelte: Edge-of-screen tapping library, designed to integrate with slider.
  • Tip.svelte: Button that links to Strip payment link.
  • Toggle.svelte: Accessible toggle inputs.

Headless Components

bits UI comes pre-installed. It is recommended to use these for any UI components.

Layercake Chart Components

Starter templates for various chart types to be used with LayerCake. Located in src/components/layercake.

Note: You must install the module layercake first.

// UsageimportExamplefrom"$components/layercake/Example.svelte";

Actions

Located in src/actions.

// Usageimportexamplefrom"$actions/action.js";
  • canTab.js: enable/disable tabbing on child elements.
  • checkOverlap.js: Label overlapping detection. Loops through selection of nodes and adds a class to the ones that are overlapping. Once one is hidden it ignores it.
  • focusTrap.js: Enable a keyboard focus trap for modals and menus.
  • keepWithinBox.js: Offsets and element left/right to stay within parent.
  • inView.js: detect when an element enters or exits the viewport.
  • resize.js: detect when an element is resized.

Runes

These are located in src/runes. You can put custom ones in src/runes/misc.js or create unique files for more complex ones.

import{example}from"$runes/misc/misc.js";
  • useWindowDimensions: returns an object { width, height } of the viewport dimensions. It is debounced for performance.
  • useClipboard: copy content to clipboard.
  • useFetcher: load async data from endpoints (local or external).
  • useWindowFocus: determine if the window is in focus or not.

For more preset runes, use runed which is preloaded.

Utils

Located in src/utils/.

// Usageimportexamplefrom"$utils/example.js";
  • checkScrollDir.js: Gets the user's scroll direction ("up" or "down")
  • csvDownload.js: Converts a flat array of data to CSV content ready to be used as an href value for download.
  • generateId.js: Generate an alphanumeric id.
  • loadCsv.js: Loads and parses a CSV file.
  • loadImage.js: Loads an image.
  • loadJson.js: Loads and parses a JSON file.
  • loadPixels.js: Loads the pixel data of an image via an offscreen canvas.
  • localStorage.js: Read and write to local storage.
  • mapToArray.js: Convenience function to convert a map to an array.
  • move.js: transform translate function shorthand.
  • transformSvg.js: Custom transition lets you apply an svg transform property with the in/out svelte transition. Parameters (with defaults):
  • translate.js: Convenience function for transform translate css.
  • urlParams.js: Get and set url parameters.

Tips

Image asset paths

For img tags, use relative paths:

<imgsrc="assets/demo/test.jpg" />

or use base if on a sub route:

<script>import{base}from"$app/paths";</script><imgsrc="{base}/assets/demo/test.jpg" />

For CSS background images, use absolute paths:

background:url("/assets/demo/test.jpg");

View example code in the preloaded demo.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Harmony

Off you go!

# for good measure
npm run songs
npm run dev
# open: http://localhost:5173/demo/chord-search/

Generating the song dataset locally

Chord data lives in the sibling harmony-data repo and is not committed to this project. The app reads static/data/songs.json (gitignored).

To build or refresh the dataset from harmony-data:

npm run songs

This runs tasks/build-songs.js, which:

  • reads corrected Top-10 songs from ../harmony-data/songs/corrected/
  • joins Billboard flags and chart year from ../harmony-data/data/tracker.csv and billboard.csv
  • converts chord data into the format used by the chord-search matcher
  • writes static/data/songs.json (full corrected Top-10 corpus, used by all /demo/ pages)

Note: src/data/hand-corrected-songs.ts holds handCorrectedSongs; src/data/hand-reviewed-songs.ts holds problematicSongs and trickySongsToMatchCorrectly for reviewing song data before use in the UI. Hand corrections are applied at runtime when loading songs.json.

When harmony-data is updated (e.g. after pulling fresh scrapes):

cd ../harmony-data && git pull
cd ../harmony && npm run songs
npm run dev

The /demo/chord-search route fetches /data/songs.json at runtime. If the file is missing, run npm run songs first.

Coverage cache (IndexedDB)

All /demo/ pages that run the core-progression coverage algorithm (define-chord-progression, core-progressions, harmony-map, artists) cache the result in the browser's IndexedDB so the worker-pool computation only runs once.

The cache key is a SHA-256 fingerprint of:

  • Core progression matching fields — each progression's name, chordProgression, and scale (not group names, descriptions, or colors)
  • Song corpus — sorted songKey + section count + roman-token count per song, so re-running npm run songs or editing hand-corrected-songs.ts automatically invalidates the cache
  • Schema versionCOVERAGE_CACHE_SCHEMA_VERSION in coverageCacheKey.ts

When to bump COVERAGE_CACHE_SCHEMA_VERSION: if you change matching algorithm logic (files under progression-matching-logic/) in a way that would produce different SongCoverageEntry results for the same input data, increment the constant to force all browsers to recompute.

Naming map clusters

/harmony-map circles dense groups of songs and can label them (e.g. "doo wop", "axis"). Those names are shared, hand-curated data committed in src/data/named-clusters.ts — not browser storage — so everyone who pulls the repo sees the same names.

Each entry is { anchorSongKey, name }. A cluster is identified by one representative song, not by its exact membership — membership shifts a little every time the embedding re-runs, but as long as that one song is still grouped there, the name still resolves. This also means there should only ever be one entry per name (and per anchor song); naming a cluster from a newly highlighted song replaces its existing entry rather than adding a second one.

To name or rename a cluster:

  1. On /harmony-map, select a song in the cluster you want to name and hit highlight in the song inspector panel (a song must be highlighted — that's what becomes the anchor).
  2. Click an empty area inside the cluster's dashed circle. A text input appears; type the name and hit Enter.
  3. This updates the map live in your session, but a browser page can't write to files on disk, so it isn't saved yet. Open the devtools console — naming logs a paste-ready line, e.g.:
    Cluster named — add to src/data/named-clusters.ts to persist:
    { anchorSongKey: "jason-mraz__im-yours", name: "axis" }
    
  4. Copy that into the namedClusters array in src/data/named-clusters.ts (replacing the old entry if you're renaming/re-anchoring one that already exists), then commit and push as usual. Your collaborator picks it up on their next git pull.

MIDI input, classification, search

Quick start to use your keyboard

Designed for Reface CP MIDI input set to the keyboard's second octave on the octave slider (next to volume).

You can do common actions directly from the keyboard:

  • Clear search: hit lowest three notes
  • Toggle search on/off (ie so you can play chords without them scrambling your current results): highest 3 notes

Implementation details: harmony/src/chord-processing/README.md


Svelte Starter [default info from the pudding starter repo]

This starter template aims to quickly scaffold a SvelteKit project, designed around data-driven, visual stories at The Pudding.

Notes

  • Do not use or reproduce The Pudding logos or fonts without written permission.
  • Prettier Formatting: Disable any text editor Prettier extensions to take advantage of the built-in rules.

Features

  • ArchieML for micro-CMS powered by Google Docs and Sheets
  • Lucide Icons for simple/easy svg icons
  • Style Dictionary for CSS/JS style parity
  • Runed for svelte5 rune utilities
  • CSV, JSON, and SVG imports
  • SSR static-hosted builds by default

Quickstart

From Scratch

  • Click the green Use this template button above.
  • Alternatively: npx degit the-pudding/svelte-starter my-project

Pre-existing Project

  • clone the repo

Installation

  • In your local repo run pnpm install or npm install

Development

npm run dev

Change the script in package.json to "dev": "svelte-kit dev --host" to test on your local network on a different device.

Deploy

Check out the Makefile for specific tasks.

Staging (on Github)

npm run staging

Production (on AWS for pudding.cool)

npm run prodution

Manual

npm run build

This generates a directory called build with the statically rendered app.

Password-Protected

To create a password-protected build:

Make sure you have a .env file in your root with a value of PASSWORD=yourpassword

make protect

Then run either make github or make pudding.

Style

There are a few stylesheets included by default in src/styles. Refer to them in app.css, the place for applying global styles.

For variable parity in both CSS and JS, modify files in the properties folder using the Style Dictionary API.

Run npm run style to regenerate the style dictionary.

Some css utility classes in reset.css

  • .sr-only: makes content invisible available for screen reader
  • .text-outline: adds a psuedo stroke to text element

Custom Fonts

For locally hosted fonts, simply add the font to the static/assets folder and include a reference in src/styles/font.css, making sure the url starts with "assets/...".

Google Docs and Sheets

  • Create a Google Doc or Sheet
  • Click Share -> Advanced -> Change... -> Anyone with this link
  • In the address bar, grab the ID - eg. "...com/document/d/1IiA5a5iCjbjOYvZVgPcjGzMy5PyfCzpPF-LnQdCdFI0/edit"
  • paste in the ID above into google.config.js, and set the filepath to where you want the file saved
  • If you want to do a Google Sheet, be sure to include the gid value in the url as well

Running npm run gdoc at any point (even in new tab while server is running) will fetch the latest from all Docs and Sheets.

Structural Overview

Pages

The src/routes directory contains pages for your app. For a single-page app (most cases) you don't have to modify anything in here. +page.svelte represents the root page, think of it as the index.html file. It is prepopulated with a few things like metadata and font preloading. It also includes a reference to a blank slate component src/components/Index.svelte. This is the file you want to really start in for your app.

Embedding Data

For smaller datasets, it is often great to embed the data into the HTML file. If you want to use data as-is, you can use normal import syntax (e.g., import data from "$data/file.csv"). If you are working with data but you want to preserve the original or clean/parse just what you need to use in the browser to optimize the front-end payload, you can load it via +page.server.js, do some work on it, and return just what you need. This is passed automatically to +page.svelte and accessible in any component with getContext("data").

Pre-loaded helpers

Components

Located in src/components.

// UsageimportExamplefrom"$components/Example.svelte";
  • Footer.svelte: Pudding recirculation and social links.
  • Header.svelte: Pudding masthead.

Helper Components

Located in src/components/helpers.

// UsageimportExamplefrom"$components/helpers/Example.svelte";

Available

  • Scrolly.svelte: Scrollytelling.

Need to migrate

  • ButtonSet.svelte: Accessible button group inputs.
  • Chunk.svelte: Split text into smaller dom element chunks.
  • Countdown.svelte: Countdown timer text.
  • DarkModeToggle.svelte: A toggle button for dark mode.
  • Figure.svelte: A barebones chart figure component to handle slots.
  • MotionToggle.svelte: A toggle button to enable/disable front-end user motion preference.
  • Range.svelte: Customizable range slider.
  • ShareLink.svelte: Button to share link natively/copy to clipboard.
  • SortTable.svelte: Sortable semantic table with customizable props.
  • Slider.svelte (and Slider.Slide.svelte): A slider widget, especially useful for swipe/slide stories.
  • Tap.svelte: Edge-of-screen tapping library, designed to integrate with slider.
  • Tip.svelte: Button that links to Strip payment link.
  • Toggle.svelte: Accessible toggle inputs.

Headless Components

bits UI comes pre-installed. It is recommended to use these for any UI components.

Layercake Chart Components

Starter templates for various chart types to be used with LayerCake. Located in src/components/layercake.

Note: You must install the module layercake first.

// UsageimportExamplefrom"$components/layercake/Example.svelte";

Actions

Located in src/actions.

// Usageimportexamplefrom"$actions/action.js";
  • canTab.js: enable/disable tabbing on child elements.
  • checkOverlap.js: Label overlapping detection. Loops through selection of nodes and adds a class to the ones that are overlapping. Once one is hidden it ignores it.
  • focusTrap.js: Enable a keyboard focus trap for modals and menus.
  • keepWithinBox.js: Offsets and element left/right to stay within parent.
  • inView.js: detect when an element enters or exits the viewport.
  • resize.js: detect when an element is resized.

Runes

These are located in src/runes. You can put custom ones in src/runes/misc.js or create unique files for more complex ones.

import{example}from"$runes/misc/misc.js";
  • useWindowDimensions: returns an object { width, height } of the viewport dimensions. It is debounced for performance.
  • useClipboard: copy content to clipboard.
  • useFetcher: load async data from endpoints (local or external).
  • useWindowFocus: determine if the window is in focus or not.

For more preset runes, use runed which is preloaded.

Utils

Located in src/utils/.

// Usageimportexamplefrom"$utils/example.js";
  • checkScrollDir.js: Gets the user's scroll direction ("up" or "down")
  • csvDownload.js: Converts a flat array of data to CSV content ready to be used as an href value for download.
  • generateId.js: Generate an alphanumeric id.
  • loadCsv.js: Loads and parses a CSV file.
  • loadImage.js: Loads an image.
  • loadJson.js: Loads and parses a JSON file.
  • loadPixels.js: Loads the pixel data of an image via an offscreen canvas.
  • localStorage.js: Read and write to local storage.
  • mapToArray.js: Convenience function to convert a map to an array.
  • move.js: transform translate function shorthand.
  • transformSvg.js: Custom transition lets you apply an svg transform property with the in/out svelte transition. Parameters (with defaults):
  • translate.js: Convenience function for transform translate css.
  • urlParams.js: Get and set url parameters.

Tips

Image asset paths

For img tags, use relative paths:

<imgsrc="assets/demo/test.jpg" />

or use base if on a sub route:

<script>import{base}from"$app/paths";</script><imgsrc="{base}/assets/demo/test.jpg" />

For CSS background images, use absolute paths:

background:url("/assets/demo/test.jpg");

View example code in the preloaded demo.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Harmony

Off you go!

# for good measure
npm run songs
npm run dev
# open: http://localhost:5173/demo/chord-search/

Generating the song dataset locally

Chord data lives in the sibling harmony-data repo and is not committed to this project. The app reads static/data/songs.json (gitignored).

To build or refresh the dataset from harmony-data:

npm run songs

This runs tasks/build-songs.js, which:

  • reads corrected Top-10 songs from ../harmony-data/songs/corrected/
  • joins Billboard flags and chart year from ../harmony-data/data/tracker.csv and billboard.csv
  • converts chord data into the format used by the chord-search matcher
  • writes static/data/songs.json (full corrected Top-10 corpus, used by all /demo/ pages)

Note: src/data/hand-corrected-songs.ts holds handCorrectedSongs; src/data/hand-reviewed-songs.ts holds problematicSongs and trickySongsToMatchCorrectly for reviewing song data before use in the UI. Hand corrections are applied at runtime when loading songs.json.

When harmony-data is updated (e.g. after pulling fresh scrapes):

cd ../harmony-data && git pull
cd ../harmony && npm run songs
npm run dev

The /demo/chord-search route fetches /data/songs.json at runtime. If the file is missing, run npm run songs first.

Coverage cache (IndexedDB)

All /demo/ pages that run the core-progression coverage algorithm (define-chord-progression, core-progressions, harmony-map, artists) cache the result in the browser's IndexedDB so the worker-pool computation only runs once.

The cache key is a SHA-256 fingerprint of:

  • Core progression matching fields — each progression's name, chordProgression, and scale (not group names, descriptions, or colors)
  • Song corpus — sorted songKey + section count + roman-token count per song, so re-running npm run songs or editing hand-corrected-songs.ts automatically invalidates the cache
  • Schema versionCOVERAGE_CACHE_SCHEMA_VERSION in coverageCacheKey.ts

When to bump COVERAGE_CACHE_SCHEMA_VERSION: if you change matching algorithm logic (files under progression-matching-logic/) in a way that would produce different SongCoverageEntry results for the same input data, increment the constant to force all browsers to recompute.

Naming map clusters

/harmony-map circles dense groups of songs and can label them (e.g. "doo wop", "axis"). Those names are shared, hand-curated data committed in src/data/named-clusters.ts — not browser storage — so everyone who pulls the repo sees the same names.

Each entry is { anchorSongKey, name }. A cluster is identified by one representative song, not by its exact membership — membership shifts a little every time the embedding re-runs, but as long as that one song is still grouped there, the name still resolves. This also means there should only ever be one entry per name (and per anchor song); naming a cluster from a newly highlighted song replaces its existing entry rather than adding a second one.

To name or rename a cluster:

  1. On /harmony-map, select a song in the cluster you want to name and hit highlight in the song inspector panel (a song must be highlighted — that's what becomes the anchor).
  2. Click an empty area inside the cluster's dashed circle. A text input appears; type the name and hit Enter.
  3. This updates the map live in your session, but a browser page can't write to files on disk, so it isn't saved yet. Open the devtools console — naming logs a paste-ready line, e.g.:
    Cluster named — add to src/data/named-clusters.ts to persist:
    { anchorSongKey: "jason-mraz__im-yours", name: "axis" }
    
  4. Copy that into the namedClusters array in src/data/named-clusters.ts (replacing the old entry if you're renaming/re-anchoring one that already exists), then commit and push as usual. Your collaborator picks it up on their next git pull.

MIDI input, classification, search

Quick start to use your keyboard

Designed for Reface CP MIDI input set to the keyboard's second octave on the octave slider (next to volume).

You can do common actions directly from the keyboard:

  • Clear search: hit lowest three notes
  • Toggle search on/off (ie so you can play chords without them scrambling your current results): highest 3 notes

Implementation details: harmony/src/chord-processing/README.md


Svelte Starter [default info from the pudding starter repo]

This starter template aims to quickly scaffold a SvelteKit project, designed around data-driven, visual stories at The Pudding.

Notes

  • Do not use or reproduce The Pudding logos or fonts without written permission.
  • Prettier Formatting: Disable any text editor Prettier extensions to take advantage of the built-in rules.

Features

  • ArchieML for micro-CMS powered by Google Docs and Sheets
  • Lucide Icons for simple/easy svg icons
  • Style Dictionary for CSS/JS style parity
  • Runed for svelte5 rune utilities
  • CSV, JSON, and SVG imports
  • SSR static-hosted builds by default

Quickstart

From Scratch

  • Click the green Use this template button above.
  • Alternatively: npx degit the-pudding/svelte-starter my-project

Pre-existing Project

  • clone the repo

Installation

  • In your local repo run pnpm install or npm install

Development

npm run dev

Change the script in package.json to "dev": "svelte-kit dev --host" to test on your local network on a different device.

Deploy

Check out the Makefile for specific tasks.

Staging (on Github)

npm run staging

Production (on AWS for pudding.cool)

npm run prodution

Manual

npm run build

This generates a directory called build with the statically rendered app.

Password-Protected

To create a password-protected build:

Make sure you have a .env file in your root with a value of PASSWORD=yourpassword

make protect

Then run either make github or make pudding.

Style

There are a few stylesheets included by default in src/styles. Refer to them in app.css, the place for applying global styles.

For variable parity in both CSS and JS, modify files in the properties folder using the Style Dictionary API.

Run npm run style to regenerate the style dictionary.

Some css utility classes in reset.css

  • .sr-only: makes content invisible available for screen reader
  • .text-outline: adds a psuedo stroke to text element

Custom Fonts

For locally hosted fonts, simply add the font to the static/assets folder and include a reference in src/styles/font.css, making sure the url starts with "assets/...".

Google Docs and Sheets

  • Create a Google Doc or Sheet
  • Click Share -> Advanced -> Change... -> Anyone with this link
  • In the address bar, grab the ID - eg. "...com/document/d/1IiA5a5iCjbjOYvZVgPcjGzMy5PyfCzpPF-LnQdCdFI0/edit"
  • paste in the ID above into google.config.js, and set the filepath to where you want the file saved
  • If you want to do a Google Sheet, be sure to include the gid value in the url as well

Running npm run gdoc at any point (even in new tab while server is running) will fetch the latest from all Docs and Sheets.

Structural Overview

Pages

The src/routes directory contains pages for your app. For a single-page app (most cases) you don't have to modify anything in here. +page.svelte represents the root page, think of it as the index.html file. It is prepopulated with a few things like metadata and font preloading. It also includes a reference to a blank slate component src/components/Index.svelte. This is the file you want to really start in for your app.

Embedding Data

For smaller datasets, it is often great to embed the data into the HTML file. If you want to use data as-is, you can use normal import syntax (e.g., import data from "$data/file.csv"). If you are working with data but you want to preserve the original or clean/parse just what you need to use in the browser to optimize the front-end payload, you can load it via +page.server.js, do some work on it, and return just what you need. This is passed automatically to +page.svelte and accessible in any component with getContext("data").

Pre-loaded helpers

Components

Located in src/components.

// UsageimportExamplefrom"$components/Example.svelte";
  • Footer.svelte: Pudding recirculation and social links.
  • Header.svelte: Pudding masthead.

Helper Components

Located in src/components/helpers.

// UsageimportExamplefrom"$components/helpers/Example.svelte";

Available

  • Scrolly.svelte: Scrollytelling.

Need to migrate

  • ButtonSet.svelte: Accessible button group inputs.
  • Chunk.svelte: Split text into smaller dom element chunks.
  • Countdown.svelte: Countdown timer text.
  • DarkModeToggle.svelte: A toggle button for dark mode.
  • Figure.svelte: A barebones chart figure component to handle slots.
  • MotionToggle.svelte: A toggle button to enable/disable front-end user motion preference.
  • Range.svelte: Customizable range slider.
  • ShareLink.svelte: Button to share link natively/copy to clipboard.
  • SortTable.svelte: Sortable semantic table with customizable props.
  • Slider.svelte (and Slider.Slide.svelte): A slider widget, especially useful for swipe/slide stories.
  • Tap.svelte: Edge-of-screen tapping library, designed to integrate with slider.
  • Tip.svelte: Button that links to Strip payment link.
  • Toggle.svelte: Accessible toggle inputs.

Headless Components

bits UI comes pre-installed. It is recommended to use these for any UI components.

Layercake Chart Components

Starter templates for various chart types to be used with LayerCake. Located in src/components/layercake.

Note: You must install the module layercake first.

// UsageimportExamplefrom"$components/layercake/Example.svelte";

Actions

Located in src/actions.

// Usageimportexamplefrom"$actions/action.js";
  • canTab.js: enable/disable tabbing on child elements.
  • checkOverlap.js: Label overlapping detection. Loops through selection of nodes and adds a class to the ones that are overlapping. Once one is hidden it ignores it.
  • focusTrap.js: Enable a keyboard focus trap for modals and menus.
  • keepWithinBox.js: Offsets and element left/right to stay within parent.
  • inView.js: detect when an element enters or exits the viewport.
  • resize.js: detect when an element is resized.

Runes

These are located in src/runes. You can put custom ones in src/runes/misc.js or create unique files for more complex ones.

import{example}from"$runes/misc/misc.js";
  • useWindowDimensions: returns an object { width, height } of the viewport dimensions. It is debounced for performance.
  • useClipboard: copy content to clipboard.
  • useFetcher: load async data from endpoints (local or external).
  • useWindowFocus: determine if the window is in focus or not.

For more preset runes, use runed which is preloaded.

Utils

Located in src/utils/.

// Usageimportexamplefrom"$utils/example.js";
  • checkScrollDir.js: Gets the user's scroll direction ("up" or "down")
  • csvDownload.js: Converts a flat array of data to CSV content ready to be used as an href value for download.
  • generateId.js: Generate an alphanumeric id.
  • loadCsv.js: Loads and parses a CSV file.
  • loadImage.js: Loads an image.
  • loadJson.js: Loads and parses a JSON file.
  • loadPixels.js: Loads the pixel data of an image via an offscreen canvas.
  • localStorage.js: Read and write to local storage.
  • mapToArray.js: Convenience function to convert a map to an array.
  • move.js: transform translate function shorthand.
  • transformSvg.js: Custom transition lets you apply an svg transform property with the in/out svelte transition. Parameters (with defaults):
  • translate.js: Convenience function for transform translate css.
  • urlParams.js: Get and set url parameters.

Tips

Image asset paths

For img tags, use relative paths:

<imgsrc="assets/demo/test.jpg" />

or use base if on a sub route:

<script>import{base}from"$app/paths";</script><imgsrc="{base}/assets/demo/test.jpg" />

For CSS background images, use absolute paths:

background:url("/assets/demo/test.jpg");

View example code in the preloaded demo.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

Harmony

Off you go!

# for good measure
npm run songs
npm run dev
# open: http://localhost:5173/demo/chord-search/

Generating the song dataset locally

Chord data lives in the sibling harmony-data repo and is not committed to this project. The app reads static/data/songs.json (gitignored).

To build or refresh the dataset from harmony-data:

npm run songs

This runs tasks/build-songs.js, which:

  • reads corrected Top-10 songs from ../harmony-data/songs/corrected/
  • joins Billboard flags and chart year from ../harmony-data/data/tracker.csv and billboard.csv
  • converts chord data into the format used by the chord-search matcher
  • writes static/data/songs.json (full corrected Top-10 corpus, used by all /demo/ pages)

Note: src/data/hand-corrected-songs.ts holds handCorrectedSongs; src/data/hand-reviewed-songs.ts holds problematicSongs and trickySongsToMatchCorrectly for reviewing song data before use in the UI. Hand corrections are applied at runtime when loading songs.json.

When harmony-data is updated (e.g. after pulling fresh scrapes):

cd ../harmony-data && git pull
cd ../harmony && npm run songs
npm run dev

The /demo/chord-search route fetches /data/songs.json at runtime. If the file is missing, run npm run songs first.

Coverage cache (IndexedDB)

All /demo/ pages that run the core-progression coverage algorithm (define-chord-progression, core-progressions, harmony-map, artists) cache the result in the browser's IndexedDB so the worker-pool computation only runs once.

The cache key is a SHA-256 fingerprint of:

  • Core progression matching fields — each progression's name, chordProgression, and scale (not group names, descriptions, or colors)
  • Song corpus — sorted songKey + section count + roman-token count per song, so re-running npm run songs or editing hand-corrected-songs.ts automatically invalidates the cache
  • Schema versionCOVERAGE_CACHE_SCHEMA_VERSION in coverageCacheKey.ts

When to bump COVERAGE_CACHE_SCHEMA_VERSION: if you change matching algorithm logic (files under progression-matching-logic/) in a way that would produce different SongCoverageEntry results for the same input data, increment the constant to force all browsers to recompute.

Naming map clusters

/harmony-map circles dense groups of songs and can label them (e.g. "doo wop", "axis"). Those names are shared, hand-curated data committed in src/data/named-clusters.ts — not browser storage — so everyone who pulls the repo sees the same names.

Each entry is { anchorSongKey, name }. A cluster is identified by one representative song, not by its exact membership — membership shifts a little every time the embedding re-runs, but as long as that one song is still grouped there, the name still resolves. This also means there should only ever be one entry per name (and per anchor song); naming a cluster from a newly highlighted song replaces its existing entry rather than adding a second one.

To name or rename a cluster:

  1. On /harmony-map, select a song in the cluster you want to name and hit highlight in the song inspector panel (a song must be highlighted — that's what becomes the anchor).
  2. Click an empty area inside the cluster's dashed circle. A text input appears; type the name and hit Enter.
  3. This updates the map live in your session, but a browser page can't write to files on disk, so it isn't saved yet. Open the devtools console — naming logs a paste-ready line, e.g.:
    Cluster named — add to src/data/named-clusters.ts to persist:
    { anchorSongKey: "jason-mraz__im-yours", name: "axis" }
    
  4. Copy that into the namedClusters array in src/data/named-clusters.ts (replacing the old entry if you're renaming/re-anchoring one that already exists), then commit and push as usual. Your collaborator picks it up on their next git pull.

MIDI input, classification, search

Quick start to use your keyboard

Designed for Reface CP MIDI input set to the keyboard's second octave on the octave slider (next to volume).

You can do common actions directly from the keyboard:

  • Clear search: hit lowest three notes
  • Toggle search on/off (ie so you can play chords without them scrambling your current results): highest 3 notes

Implementation details: harmony/src/chord-processing/README.md


Svelte Starter [default info from the pudding starter repo]

This starter template aims to quickly scaffold a SvelteKit project, designed around data-driven, visual stories at The Pudding.

Notes

  • Do not use or reproduce The Pudding logos or fonts without written permission.
  • Prettier Formatting: Disable any text editor Prettier extensions to take advantage of the built-in rules.

Features

  • ArchieML for micro-CMS powered by Google Docs and Sheets
  • Lucide Icons for simple/easy svg icons
  • Style Dictionary for CSS/JS style parity
  • Runed for svelte5 rune utilities
  • CSV, JSON, and SVG imports
  • SSR static-hosted builds by default

Quickstart

From Scratch

  • Click the green Use this template button above.
  • Alternatively: npx degit the-pudding/svelte-starter my-project

Pre-existing Project

  • clone the repo

Installation

  • In your local repo run pnpm install or npm install

Development

npm run dev

Change the script in package.json to "dev": "svelte-kit dev --host" to test on your local network on a different device.

Deploy

Check out the Makefile for specific tasks.

Staging (on Github)

npm run staging

Production (on AWS for pudding.cool)

npm run prodution

Manual

npm run build

This generates a directory called build with the statically rendered app.

Password-Protected

To create a password-protected build:

Make sure you have a .env file in your root with a value of PASSWORD=yourpassword

make protect

Then run either make github or make pudding.

Style

There are a few stylesheets included by default in src/styles. Refer to them in app.css, the place for applying global styles.

For variable parity in both CSS and JS, modify files in the properties folder using the Style Dictionary API.

Run npm run style to regenerate the style dictionary.

Some css utility classes in reset.css

  • .sr-only: makes content invisible available for screen reader
  • .text-outline: adds a psuedo stroke to text element

Custom Fonts

For locally hosted fonts, simply add the font to the static/assets folder and include a reference in src/styles/font.css, making sure the url starts with "assets/...".

Google Docs and Sheets

  • Create a Google Doc or Sheet
  • Click Share -> Advanced -> Change... -> Anyone with this link
  • In the address bar, grab the ID - eg. "...com/document/d/1IiA5a5iCjbjOYvZVgPcjGzMy5PyfCzpPF-LnQdCdFI0/edit"
  • paste in the ID above into google.config.js, and set the filepath to where you want the file saved
  • If you want to do a Google Sheet, be sure to include the gid value in the url as well

Running npm run gdoc at any point (even in new tab while server is running) will fetch the latest from all Docs and Sheets.

Structural Overview

Pages

The src/routes directory contains pages for your app. For a single-page app (most cases) you don't have to modify anything in here. +page.svelte represents the root page, think of it as the index.html file. It is prepopulated with a few things like metadata and font preloading. It also includes a reference to a blank slate component src/components/Index.svelte. This is the file you want to really start in for your app.

Embedding Data

For smaller datasets, it is often great to embed the data into the HTML file. If you want to use data as-is, you can use normal import syntax (e.g., import data from "$data/file.csv"). If you are working with data but you want to preserve the original or clean/parse just what you need to use in the browser to optimize the front-end payload, you can load it via +page.server.js, do some work on it, and return just what you need. This is passed automatically to +page.svelte and accessible in any component with getContext("data").

Pre-loaded helpers

Components

Located in src/components.

// UsageimportExamplefrom"$components/Example.svelte";
  • Footer.svelte: Pudding recirculation and social links.
  • Header.svelte: Pudding masthead.

Helper Components

Located in src/components/helpers.

// UsageimportExamplefrom"$components/helpers/Example.svelte";

Available

  • Scrolly.svelte: Scrollytelling.

Need to migrate

  • ButtonSet.svelte: Accessible button group inputs.
  • Chunk.svelte: Split text into smaller dom element chunks.
  • Countdown.svelte: Countdown timer text.
  • DarkModeToggle.svelte: A toggle button for dark mode.
  • Figure.svelte: A barebones chart figure component to handle slots.
  • MotionToggle.svelte: A toggle button to enable/disable front-end user motion preference.
  • Range.svelte: Customizable range slider.
  • ShareLink.svelte: Button to share link natively/copy to clipboard.
  • SortTable.svelte: Sortable semantic table with customizable props.
  • Slider.svelte (and Slider.Slide.svelte): A slider widget, especially useful for swipe/slide stories.
  • Tap.svelte: Edge-of-screen tapping library, designed to integrate with slider.
  • Tip.svelte: Button that links to Strip payment link.
  • Toggle.svelte: Accessible toggle inputs.

Headless Components

bits UI comes pre-installed. It is recommended to use these for any UI components.

Layercake Chart Components

Starter templates for various chart types to be used with LayerCake. Located in src/components/layercake.

Note: You must install the module layercake first.

// UsageimportExamplefrom"$components/layercake/Example.svelte";

Actions

Located in src/actions.

// Usageimportexamplefrom"$actions/action.js";
  • canTab.js: enable/disable tabbing on child elements.
  • checkOverlap.js: Label overlapping detection. Loops through selection of nodes and adds a class to the ones that are overlapping. Once one is hidden it ignores it.
  • focusTrap.js: Enable a keyboard focus trap for modals and menus.
  • keepWithinBox.js: Offsets and element left/right to stay within parent.
  • inView.js: detect when an element enters or exits the viewport.
  • resize.js: detect when an element is resized.

Runes

These are located in src/runes. You can put custom ones in src/runes/misc.js or create unique files for more complex ones.

import{example}from"$runes/misc/misc.js";
  • useWindowDimensions: returns an object { width, height } of the viewport dimensions. It is debounced for performance.
  • useClipboard: copy content to clipboard.
  • useFetcher: load async data from endpoints (local or external).
  • useWindowFocus: determine if the window is in focus or not.

For more preset runes, use runed which is preloaded.

Utils

Located in src/utils/.

// Usageimportexamplefrom"$utils/example.js";
  • checkScrollDir.js: Gets the user's scroll direction ("up" or "down")
  • csvDownload.js: Converts a flat array of data to CSV content ready to be used as an href value for download.
  • generateId.js: Generate an alphanumeric id.
  • loadCsv.js: Loads and parses a CSV file.
  • loadImage.js: Loads an image.
  • loadJson.js: Loads and parses a JSON file.
  • loadPixels.js: Loads the pixel data of an image via an offscreen canvas.
  • localStorage.js: Read and write to local storage.
  • mapToArray.js: Convenience function to convert a map to an array.
  • move.js: transform translate function shorthand.
  • transformSvg.js: Custom transition lets you apply an svg transform property with the in/out svelte transition. Parameters (with defaults):
  • translate.js: Convenience function for transform translate css.
  • urlParams.js: Get and set url parameters.

Tips

Image asset paths

For img tags, use relative paths:

<imgsrc="assets/demo/test.jpg" />

or use base if on a sub route:

<script>import{base}from"$app/paths";</script><imgsrc="{base}/assets/demo/test.jpg" />

For CSS background images, use absolute paths:

background:url("/assets/demo/test.jpg");

View example code in the preloaded demo.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Harmony

Off you go!

# for good measure
npm run songs
npm run dev
# open: http://localhost:5173/demo/chord-search/

Generating the song dataset locally

Chord data lives in the sibling harmony-data repo and is not committed to this project. The app reads static/data/songs.json (gitignored).

To build or refresh the dataset from harmony-data:

npm run songs

This runs tasks/build-songs.js, which:

  • reads corrected Top-10 songs from ../harmony-data/songs/corrected/
  • joins Billboard flags and chart year from ../harmony-data/data/tracker.csv and billboard.csv
  • converts chord data into the format used by the chord-search matcher
  • writes static/data/songs.json (full corrected Top-10 corpus, used by all /demo/ pages)

Note: src/data/hand-corrected-songs.ts holds handCorrectedSongs; src/data/hand-reviewed-songs.ts holds problematicSongs and trickySongsToMatchCorrectly for reviewing song data before use in the UI. Hand corrections are applied at runtime when loading songs.json.

When harmony-data is updated (e.g. after pulling fresh scrapes):

cd ../harmony-data && git pull
cd ../harmony && npm run songs
npm run dev

The /demo/chord-search route fetches /data/songs.json at runtime. If the file is missing, run npm run songs first.

Coverage cache (IndexedDB)

All /demo/ pages that run the core-progression coverage algorithm (define-chord-progression, core-progressions, harmony-map, artists) cache the result in the browser's IndexedDB so the worker-pool computation only runs once.

The cache key is a SHA-256 fingerprint of:

  • Core progression matching fields — each progression's name, chordProgression, and scale (not group names, descriptions, or colors)
  • Song corpus — sorted songKey + section count + roman-token count per song, so re-running npm run songs or editing hand-corrected-songs.ts automatically invalidates the cache
  • Schema versionCOVERAGE_CACHE_SCHEMA_VERSION in coverageCacheKey.ts

When to bump COVERAGE_CACHE_SCHEMA_VERSION: if you change matching algorithm logic (files under progression-matching-logic/) in a way that would produce different SongCoverageEntry results for the same input data, increment the constant to force all browsers to recompute.

Naming map clusters

/harmony-map circles dense groups of songs and can label them (e.g. "doo wop", "axis"). Those names are shared, hand-curated data committed in src/data/named-clusters.ts — not browser storage — so everyone who pulls the repo sees the same names.

Each entry is { anchorSongKey, name }. A cluster is identified by one representative song, not by its exact membership — membership shifts a little every time the embedding re-runs, but as long as that one song is still grouped there, the name still resolves. This also means there should only ever be one entry per name (and per anchor song); naming a cluster from a newly highlighted song replaces its existing entry rather than adding a second one.

To name or rename a cluster:

  1. On /harmony-map, select a song in the cluster you want to name and hit highlight in the song inspector panel (a song must be highlighted — that's what becomes the anchor).
  2. Click an empty area inside the cluster's dashed circle. A text input appears; type the name and hit Enter.
  3. This updates the map live in your session, but a browser page can't write to files on disk, so it isn't saved yet. Open the devtools console — naming logs a paste-ready line, e.g.:
    Cluster named — add to src/data/named-clusters.ts to persist:
    { anchorSongKey: "jason-mraz__im-yours", name: "axis" }
    
  4. Copy that into the namedClusters array in src/data/named-clusters.ts (replacing the old entry if you're renaming/re-anchoring one that already exists), then commit and push as usual. Your collaborator picks it up on their next git pull.

MIDI input, classification, search

Quick start to use your keyboard

Designed for Reface CP MIDI input set to the keyboard's second octave on the octave slider (next to volume).

You can do common actions directly from the keyboard:

  • Clear search: hit lowest three notes
  • Toggle search on/off (ie so you can play chords without them scrambling your current results): highest 3 notes

Implementation details: harmony/src/chord-processing/README.md


Svelte Starter [default info from the pudding starter repo]

This starter template aims to quickly scaffold a SvelteKit project, designed around data-driven, visual stories at The Pudding.

Notes

  • Do not use or reproduce The Pudding logos or fonts without written permission.
  • Prettier Formatting: Disable any text editor Prettier extensions to take advantage of the built-in rules.

Features

  • ArchieML for micro-CMS powered by Google Docs and Sheets
  • Lucide Icons for simple/easy svg icons
  • Style Dictionary for CSS/JS style parity
  • Runed for svelte5 rune utilities
  • CSV, JSON, and SVG imports
  • SSR static-hosted builds by default

Quickstart

From Scratch

  • Click the green Use this template button above.
  • Alternatively: npx degit the-pudding/svelte-starter my-project

Pre-existing Project

  • clone the repo

Installation

  • In your local repo run pnpm install or npm install

Development

npm run dev

Change the script in package.json to "dev": "svelte-kit dev --host" to test on your local network on a different device.

Deploy

Check out the Makefile for specific tasks.

Staging (on Github)

npm run staging

Production (on AWS for pudding.cool)

npm run prodution

Manual

npm run build

This generates a directory called build with the statically rendered app.

Password-Protected

To create a password-protected build:

Make sure you have a .env file in your root with a value of PASSWORD=yourpassword

make protect

Then run either make github or make pudding.

Style

There are a few stylesheets included by default in src/styles. Refer to them in app.css, the place for applying global styles.

For variable parity in both CSS and JS, modify files in the properties folder using the Style Dictionary API.

Run npm run style to regenerate the style dictionary.

Some css utility classes in reset.css

  • .sr-only: makes content invisible available for screen reader
  • .text-outline: adds a psuedo stroke to text element

Custom Fonts

For locally hosted fonts, simply add the font to the static/assets folder and include a reference in src/styles/font.css, making sure the url starts with "assets/...".

Google Docs and Sheets

  • Create a Google Doc or Sheet
  • Click Share -> Advanced -> Change... -> Anyone with this link
  • In the address bar, grab the ID - eg. "...com/document/d/1IiA5a5iCjbjOYvZVgPcjGzMy5PyfCzpPF-LnQdCdFI0/edit"
  • paste in the ID above into google.config.js, and set the filepath to where you want the file saved
  • If you want to do a Google Sheet, be sure to include the gid value in the url as well

Running npm run gdoc at any point (even in new tab while server is running) will fetch the latest from all Docs and Sheets.

Structural Overview

Pages

The src/routes directory contains pages for your app. For a single-page app (most cases) you don't have to modify anything in here. +page.svelte represents the root page, think of it as the index.html file. It is prepopulated with a few things like metadata and font preloading. It also includes a reference to a blank slate component src/components/Index.svelte. This is the file you want to really start in for your app.

Embedding Data

For smaller datasets, it is often great to embed the data into the HTML file. If you want to use data as-is, you can use normal import syntax (e.g., import data from "$data/file.csv"). If you are working with data but you want to preserve the original or clean/parse just what you need to use in the browser to optimize the front-end payload, you can load it via +page.server.js, do some work on it, and return just what you need. This is passed automatically to +page.svelte and accessible in any component with getContext("data").

Pre-loaded helpers

Components

Located in src/components.

// UsageimportExamplefrom"$components/Example.svelte";
  • Footer.svelte: Pudding recirculation and social links.
  • Header.svelte: Pudding masthead.

Helper Components

Located in src/components/helpers.

// UsageimportExamplefrom"$components/helpers/Example.svelte";

Available

  • Scrolly.svelte: Scrollytelling.

Need to migrate

  • ButtonSet.svelte: Accessible button group inputs.
  • Chunk.svelte: Split text into smaller dom element chunks.
  • Countdown.svelte: Countdown timer text.
  • DarkModeToggle.svelte: A toggle button for dark mode.
  • Figure.svelte: A barebones chart figure component to handle slots.
  • MotionToggle.svelte: A toggle button to enable/disable front-end user motion preference.
  • Range.svelte: Customizable range slider.
  • ShareLink.svelte: Button to share link natively/copy to clipboard.
  • SortTable.svelte: Sortable semantic table with customizable props.
  • Slider.svelte (and Slider.Slide.svelte): A slider widget, especially useful for swipe/slide stories.
  • Tap.svelte: Edge-of-screen tapping library, designed to integrate with slider.
  • Tip.svelte: Button that links to Strip payment link.
  • Toggle.svelte: Accessible toggle inputs.

Headless Components

bits UI comes pre-installed. It is recommended to use these for any UI components.

Layercake Chart Components

Starter templates for various chart types to be used with LayerCake. Located in src/components/layercake.

Note: You must install the module layercake first.

// UsageimportExamplefrom"$components/layercake/Example.svelte";

Actions

Located in src/actions.

// Usageimportexamplefrom"$actions/action.js";
  • canTab.js: enable/disable tabbing on child elements.
  • checkOverlap.js: Label overlapping detection. Loops through selection of nodes and adds a class to the ones that are overlapping. Once one is hidden it ignores it.
  • focusTrap.js: Enable a keyboard focus trap for modals and menus.
  • keepWithinBox.js: Offsets and element left/right to stay within parent.
  • inView.js: detect when an element enters or exits the viewport.
  • resize.js: detect when an element is resized.

Runes

These are located in src/runes. You can put custom ones in src/runes/misc.js or create unique files for more complex ones.

import{example}from"$runes/misc/misc.js";
  • useWindowDimensions: returns an object { width, height } of the viewport dimensions. It is debounced for performance.
  • useClipboard: copy content to clipboard.
  • useFetcher: load async data from endpoints (local or external).
  • useWindowFocus: determine if the window is in focus or not.

For more preset runes, use runed which is preloaded.

Utils

Located in src/utils/.

// Usageimportexamplefrom"$utils/example.js";
  • checkScrollDir.js: Gets the user's scroll direction ("up" or "down")
  • csvDownload.js: Converts a flat array of data to CSV content ready to be used as an href value for download.
  • generateId.js: Generate an alphanumeric id.
  • loadCsv.js: Loads and parses a CSV file.
  • loadImage.js: Loads an image.
  • loadJson.js: Loads and parses a JSON file.
  • loadPixels.js: Loads the pixel data of an image via an offscreen canvas.
  • localStorage.js: Read and write to local storage.
  • mapToArray.js: Convenience function to convert a map to an array.
  • move.js: transform translate function shorthand.
  • transformSvg.js: Custom transition lets you apply an svg transform property with the in/out svelte transition. Parameters (with defaults):
  • translate.js: Convenience function for transform translate css.
  • urlParams.js: Get and set url parameters.

Tips

Image asset paths

For img tags, use relative paths:

<imgsrc="assets/demo/test.jpg" />

or use base if on a sub route:

<script>import{base}from"$app/paths";</script><imgsrc="{base}/assets/demo/test.jpg" />

For CSS background images, use absolute paths:

background:url("/assets/demo/test.jpg");

View example code in the preloaded demo.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Harmony

Off you go!

# for good measure
npm run songs
npm run dev
# open: http://localhost:5173/demo/chord-search/

Generating the song dataset locally

Chord data lives in the sibling harmony-data repo and is not committed to this project. The app reads static/data/songs.json (gitignored).

To build or refresh the dataset from harmony-data:

npm run songs

This runs tasks/build-songs.js, which:

  • reads corrected Top-10 songs from ../harmony-data/songs/corrected/
  • joins Billboard flags and chart year from ../harmony-data/data/tracker.csv and billboard.csv
  • converts chord data into the format used by the chord-search matcher
  • writes static/data/songs.json (full corrected Top-10 corpus, used by all /demo/ pages)

Note: src/data/hand-corrected-songs.ts holds handCorrectedSongs; src/data/hand-reviewed-songs.ts holds problematicSongs and trickySongsToMatchCorrectly for reviewing song data before use in the UI. Hand corrections are applied at runtime when loading songs.json.

When harmony-data is updated (e.g. after pulling fresh scrapes):

cd ../harmony-data && git pull
cd ../harmony && npm run songs
npm run dev

The /demo/chord-search route fetches /data/songs.json at runtime. If the file is missing, run npm run songs first.

Coverage cache (IndexedDB)

All /demo/ pages that run the core-progression coverage algorithm (define-chord-progression, core-progressions, harmony-map, artists) cache the result in the browser's IndexedDB so the worker-pool computation only runs once.

The cache key is a SHA-256 fingerprint of:

  • Core progression matching fields — each progression's name, chordProgression, and scale (not group names, descriptions, or colors)
  • Song corpus — sorted songKey + section count + roman-token count per song, so re-running npm run songs or editing hand-corrected-songs.ts automatically invalidates the cache
  • Schema versionCOVERAGE_CACHE_SCHEMA_VERSION in coverageCacheKey.ts

When to bump COVERAGE_CACHE_SCHEMA_VERSION: if you change matching algorithm logic (files under progression-matching-logic/) in a way that would produce different SongCoverageEntry results for the same input data, increment the constant to force all browsers to recompute.

Naming map clusters

/harmony-map circles dense groups of songs and can label them (e.g. "doo wop", "axis"). Those names are shared, hand-curated data committed in src/data/named-clusters.ts — not browser storage — so everyone who pulls the repo sees the same names.

Each entry is { anchorSongKey, name }. A cluster is identified by one representative song, not by its exact membership — membership shifts a little every time the embedding re-runs, but as long as that one song is still grouped there, the name still resolves. This also means there should only ever be one entry per name (and per anchor song); naming a cluster from a newly highlighted song replaces its existing entry rather than adding a second one.

To name or rename a cluster:

  1. On /harmony-map, select a song in the cluster you want to name and hit highlight in the song inspector panel (a song must be highlighted — that's what becomes the anchor).
  2. Click an empty area inside the cluster's dashed circle. A text input appears; type the name and hit Enter.
  3. This updates the map live in your session, but a browser page can't write to files on disk, so it isn't saved yet. Open the devtools console — naming logs a paste-ready line, e.g.:
    Cluster named — add to src/data/named-clusters.ts to persist:
    { anchorSongKey: "jason-mraz__im-yours", name: "axis" }
    
  4. Copy that into the namedClusters array in src/data/named-clusters.ts (replacing the old entry if you're renaming/re-anchoring one that already exists), then commit and push as usual. Your collaborator picks it up on their next git pull.

MIDI input, classification, search

Quick start to use your keyboard

Designed for Reface CP MIDI input set to the keyboard's second octave on the octave slider (next to volume).

You can do common actions directly from the keyboard:

  • Clear search: hit lowest three notes
  • Toggle search on/off (ie so you can play chords without them scrambling your current results): highest 3 notes

Implementation details: harmony/src/chord-processing/README.md


Svelte Starter [default info from the pudding starter repo]

This starter template aims to quickly scaffold a SvelteKit project, designed around data-driven, visual stories at The Pudding.

Notes

  • Do not use or reproduce The Pudding logos or fonts without written permission.
  • Prettier Formatting: Disable any text editor Prettier extensions to take advantage of the built-in rules.

Features

  • ArchieML for micro-CMS powered by Google Docs and Sheets
  • Lucide Icons for simple/easy svg icons
  • Style Dictionary for CSS/JS style parity
  • Runed for svelte5 rune utilities
  • CSV, JSON, and SVG imports
  • SSR static-hosted builds by default

Quickstart

From Scratch

  • Click the green Use this template button above.
  • Alternatively: npx degit the-pudding/svelte-starter my-project

Pre-existing Project

  • clone the repo

Installation

  • In your local repo run pnpm install or npm install

Development

npm run dev

Change the script in package.json to "dev": "svelte-kit dev --host" to test on your local network on a different device.

Deploy

Check out the Makefile for specific tasks.

Staging (on Github)

npm run staging

Production (on AWS for pudding.cool)

npm run prodution

Manual

npm run build

This generates a directory called build with the statically rendered app.

Password-Protected

To create a password-protected build:

Make sure you have a .env file in your root with a value of PASSWORD=yourpassword

make protect

Then run either make github or make pudding.

Style

There are a few stylesheets included by default in src/styles. Refer to them in app.css, the place for applying global styles.

For variable parity in both CSS and JS, modify files in the properties folder using the Style Dictionary API.

Run npm run style to regenerate the style dictionary.

Some css utility classes in reset.css

  • .sr-only: makes content invisible available for screen reader
  • .text-outline: adds a psuedo stroke to text element

Custom Fonts

For locally hosted fonts, simply add the font to the static/assets folder and include a reference in src/styles/font.css, making sure the url starts with "assets/...".

Google Docs and Sheets

  • Create a Google Doc or Sheet
  • Click Share -> Advanced -> Change... -> Anyone with this link
  • In the address bar, grab the ID - eg. "...com/document/d/1IiA5a5iCjbjOYvZVgPcjGzMy5PyfCzpPF-LnQdCdFI0/edit"
  • paste in the ID above into google.config.js, and set the filepath to where you want the file saved
  • If you want to do a Google Sheet, be sure to include the gid value in the url as well

Running npm run gdoc at any point (even in new tab while server is running) will fetch the latest from all Docs and Sheets.

Structural Overview

Pages

The src/routes directory contains pages for your app. For a single-page app (most cases) you don't have to modify anything in here. +page.svelte represents the root page, think of it as the index.html file. It is prepopulated with a few things like metadata and font preloading. It also includes a reference to a blank slate component src/components/Index.svelte. This is the file you want to really start in for your app.

Embedding Data

For smaller datasets, it is often great to embed the data into the HTML file. If you want to use data as-is, you can use normal import syntax (e.g., import data from "$data/file.csv"). If you are working with data but you want to preserve the original or clean/parse just what you need to use in the browser to optimize the front-end payload, you can load it via +page.server.js, do some work on it, and return just what you need. This is passed automatically to +page.svelte and accessible in any component with getContext("data").

Pre-loaded helpers

Components

Located in src/components.

// UsageimportExamplefrom"$components/Example.svelte";
  • Footer.svelte: Pudding recirculation and social links.
  • Header.svelte: Pudding masthead.

Helper Components

Located in src/components/helpers.

// UsageimportExamplefrom"$components/helpers/Example.svelte";

Available

  • Scrolly.svelte: Scrollytelling.

Need to migrate

  • ButtonSet.svelte: Accessible button group inputs.
  • Chunk.svelte: Split text into smaller dom element chunks.
  • Countdown.svelte: Countdown timer text.
  • DarkModeToggle.svelte: A toggle button for dark mode.
  • Figure.svelte: A barebones chart figure component to handle slots.
  • MotionToggle.svelte: A toggle button to enable/disable front-end user motion preference.
  • Range.svelte: Customizable range slider.
  • ShareLink.svelte: Button to share link natively/copy to clipboard.
  • SortTable.svelte: Sortable semantic table with customizable props.
  • Slider.svelte (and Slider.Slide.svelte): A slider widget, especially useful for swipe/slide stories.
  • Tap.svelte: Edge-of-screen tapping library, designed to integrate with slider.
  • Tip.svelte: Button that links to Strip payment link.
  • Toggle.svelte: Accessible toggle inputs.

Headless Components

bits UI comes pre-installed. It is recommended to use these for any UI components.

Layercake Chart Components

Starter templates for various chart types to be used with LayerCake. Located in src/components/layercake.

Note: You must install the module layercake first.

// UsageimportExamplefrom"$components/layercake/Example.svelte";

Actions

Located in src/actions.

// Usageimportexamplefrom"$actions/action.js";
  • canTab.js: enable/disable tabbing on child elements.
  • checkOverlap.js: Label overlapping detection. Loops through selection of nodes and adds a class to the ones that are overlapping. Once one is hidden it ignores it.
  • focusTrap.js: Enable a keyboard focus trap for modals and menus.
  • keepWithinBox.js: Offsets and element left/right to stay within parent.
  • inView.js: detect when an element enters or exits the viewport.
  • resize.js: detect when an element is resized.

Runes

These are located in src/runes. You can put custom ones in src/runes/misc.js or create unique files for more complex ones.

import{example}from"$runes/misc/misc.js";
  • useWindowDimensions: returns an object { width, height } of the viewport dimensions. It is debounced for performance.
  • useClipboard: copy content to clipboard.
  • useFetcher: load async data from endpoints (local or external).
  • useWindowFocus: determine if the window is in focus or not.

For more preset runes, use runed which is preloaded.

Utils

Located in src/utils/.

// Usageimportexamplefrom"$utils/example.js";
  • checkScrollDir.js: Gets the user's scroll direction ("up" or "down")
  • csvDownload.js: Converts a flat array of data to CSV content ready to be used as an href value for download.
  • generateId.js: Generate an alphanumeric id.
  • loadCsv.js: Loads and parses a CSV file.
  • loadImage.js: Loads an image.
  • loadJson.js: Loads and parses a JSON file.
  • loadPixels.js: Loads the pixel data of an image via an offscreen canvas.
  • localStorage.js: Read and write to local storage.
  • mapToArray.js: Convenience function to convert a map to an array.
  • move.js: transform translate function shorthand.
  • transformSvg.js: Custom transition lets you apply an svg transform property with the in/out svelte transition. Parameters (with defaults):
  • translate.js: Convenience function for transform translate css.
  • urlParams.js: Get and set url parameters.

Tips

Image asset paths

For img tags, use relative paths:

<imgsrc="assets/demo/test.jpg" />

or use base if on a sub route:

<script>import{base}from"$app/paths";</script><imgsrc="{base}/assets/demo/test.jpg" />

For CSS background images, use absolute paths:

background:url("/assets/demo/test.jpg");

View example code in the preloaded demo.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

Harmony

Off you go!

# for good measure
npm run songs
npm run dev
# open: http://localhost:5173/demo/chord-search/

Generating the song dataset locally

Chord data lives in the sibling harmony-data repo and is not committed to this project. The app reads static/data/songs.json (gitignored).

To build or refresh the dataset from harmony-data:

npm run songs

This runs tasks/build-songs.js, which:

  • reads corrected Top-10 songs from ../harmony-data/songs/corrected/
  • joins Billboard flags and chart year from ../harmony-data/data/tracker.csv and billboard.csv
  • converts chord data into the format used by the chord-search matcher
  • writes static/data/songs.json (full corrected Top-10 corpus, used by all /demo/ pages)

Note: src/data/hand-corrected-songs.ts holds handCorrectedSongs; src/data/hand-reviewed-songs.ts holds problematicSongs and trickySongsToMatchCorrectly for reviewing song data before use in the UI. Hand corrections are applied at runtime when loading songs.json.

When harmony-data is updated (e.g. after pulling fresh scrapes):

cd ../harmony-data && git pull
cd ../harmony && npm run songs
npm run dev

The /demo/chord-search route fetches /data/songs.json at runtime. If the file is missing, run npm run songs first.

Coverage cache (IndexedDB)

All /demo/ pages that run the core-progression coverage algorithm (define-chord-progression, core-progressions, harmony-map, artists) cache the result in the browser's IndexedDB so the worker-pool computation only runs once.

The cache key is a SHA-256 fingerprint of:

  • Core progression matching fields — each progression's name, chordProgression, and scale (not group names, descriptions, or colors)
  • Song corpus — sorted songKey + section count + roman-token count per song, so re-running npm run songs or editing hand-corrected-songs.ts automatically invalidates the cache
  • Schema versionCOVERAGE_CACHE_SCHEMA_VERSION in coverageCacheKey.ts

When to bump COVERAGE_CACHE_SCHEMA_VERSION: if you change matching algorithm logic (files under progression-matching-logic/) in a way that would produce different SongCoverageEntry results for the same input data, increment the constant to force all browsers to recompute.

Naming map clusters

/harmony-map circles dense groups of songs and can label them (e.g. "doo wop", "axis"). Those names are shared, hand-curated data committed in src/data/named-clusters.ts — not browser storage — so everyone who pulls the repo sees the same names.

Each entry is { anchorSongKey, name }. A cluster is identified by one representative song, not by its exact membership — membership shifts a little every time the embedding re-runs, but as long as that one song is still grouped there, the name still resolves. This also means there should only ever be one entry per name (and per anchor song); naming a cluster from a newly highlighted song replaces its existing entry rather than adding a second one.

To name or rename a cluster:

  1. On /harmony-map, select a song in the cluster you want to name and hit highlight in the song inspector panel (a song must be highlighted — that's what becomes the anchor).
  2. Click an empty area inside the cluster's dashed circle. A text input appears; type the name and hit Enter.
  3. This updates the map live in your session, but a browser page can't write to files on disk, so it isn't saved yet. Open the devtools console — naming logs a paste-ready line, e.g.:
    Cluster named — add to src/data/named-clusters.ts to persist:
    { anchorSongKey: "jason-mraz__im-yours", name: "axis" }
    
  4. Copy that into the namedClusters array in src/data/named-clusters.ts (replacing the old entry if you're renaming/re-anchoring one that already exists), then commit and push as usual. Your collaborator picks it up on their next git pull.

MIDI input, classification, search

Quick start to use your keyboard

Designed for Reface CP MIDI input set to the keyboard's second octave on the octave slider (next to volume).

You can do common actions directly from the keyboard:

  • Clear search: hit lowest three notes
  • Toggle search on/off (ie so you can play chords without them scrambling your current results): highest 3 notes

Implementation details: harmony/src/chord-processing/README.md


Svelte Starter [default info from the pudding starter repo]

This starter template aims to quickly scaffold a SvelteKit project, designed around data-driven, visual stories at The Pudding.

Notes

  • Do not use or reproduce The Pudding logos or fonts without written permission.
  • Prettier Formatting: Disable any text editor Prettier extensions to take advantage of the built-in rules.

Features

  • ArchieML for micro-CMS powered by Google Docs and Sheets
  • Lucide Icons for simple/easy svg icons
  • Style Dictionary for CSS/JS style parity
  • Runed for svelte5 rune utilities
  • CSV, JSON, and SVG imports
  • SSR static-hosted builds by default

Quickstart

From Scratch

  • Click the green Use this template button above.
  • Alternatively: npx degit the-pudding/svelte-starter my-project

Pre-existing Project

  • clone the repo

Installation

  • In your local repo run pnpm install or npm install

Development

npm run dev

Change the script in package.json to "dev": "svelte-kit dev --host" to test on your local network on a different device.

Deploy

Check out the Makefile for specific tasks.

Staging (on Github)

npm run staging

Production (on AWS for pudding.cool)

npm run prodution

Manual

npm run build

This generates a directory called build with the statically rendered app.

Password-Protected

To create a password-protected build:

Make sure you have a .env file in your root with a value of PASSWORD=yourpassword

make protect

Then run either make github or make pudding.

Style

There are a few stylesheets included by default in src/styles. Refer to them in app.css, the place for applying global styles.

For variable parity in both CSS and JS, modify files in the properties folder using the Style Dictionary API.

Run npm run style to regenerate the style dictionary.

Some css utility classes in reset.css

  • .sr-only: makes content invisible available for screen reader
  • .text-outline: adds a psuedo stroke to text element

Custom Fonts

For locally hosted fonts, simply add the font to the static/assets folder and include a reference in src/styles/font.css, making sure the url starts with "assets/...".

Google Docs and Sheets

  • Create a Google Doc or Sheet
  • Click Share -> Advanced -> Change... -> Anyone with this link
  • In the address bar, grab the ID - eg. "...com/document/d/1IiA5a5iCjbjOYvZVgPcjGzMy5PyfCzpPF-LnQdCdFI0/edit"
  • paste in the ID above into google.config.js, and set the filepath to where you want the file saved
  • If you want to do a Google Sheet, be sure to include the gid value in the url as well

Running npm run gdoc at any point (even in new tab while server is running) will fetch the latest from all Docs and Sheets.

Structural Overview

Pages

The src/routes directory contains pages for your app. For a single-page app (most cases) you don't have to modify anything in here. +page.svelte represents the root page, think of it as the index.html file. It is prepopulated with a few things like metadata and font preloading. It also includes a reference to a blank slate component src/components/Index.svelte. This is the file you want to really start in for your app.

Embedding Data

For smaller datasets, it is often great to embed the data into the HTML file. If you want to use data as-is, you can use normal import syntax (e.g., import data from "$data/file.csv"). If you are working with data but you want to preserve the original or clean/parse just what you need to use in the browser to optimize the front-end payload, you can load it via +page.server.js, do some work on it, and return just what you need. This is passed automatically to +page.svelte and accessible in any component with getContext("data").

Pre-loaded helpers

Components

Located in src/components.

// UsageimportExamplefrom"$components/Example.svelte";
  • Footer.svelte: Pudding recirculation and social links.
  • Header.svelte: Pudding masthead.

Helper Components

Located in src/components/helpers.

// UsageimportExamplefrom"$components/helpers/Example.svelte";

Available

  • Scrolly.svelte: Scrollytelling.

Need to migrate

  • ButtonSet.svelte: Accessible button group inputs.
  • Chunk.svelte: Split text into smaller dom element chunks.
  • Countdown.svelte: Countdown timer text.
  • DarkModeToggle.svelte: A toggle button for dark mode.
  • Figure.svelte: A barebones chart figure component to handle slots.
  • MotionToggle.svelte: A toggle button to enable/disable front-end user motion preference.
  • Range.svelte: Customizable range slider.
  • ShareLink.svelte: Button to share link natively/copy to clipboard.
  • SortTable.svelte: Sortable semantic table with customizable props.
  • Slider.svelte (and Slider.Slide.svelte): A slider widget, especially useful for swipe/slide stories.
  • Tap.svelte: Edge-of-screen tapping library, designed to integrate with slider.
  • Tip.svelte: Button that links to Strip payment link.
  • Toggle.svelte: Accessible toggle inputs.

Headless Components

bits UI comes pre-installed. It is recommended to use these for any UI components.

Layercake Chart Components

Starter templates for various chart types to be used with LayerCake. Located in src/components/layercake.

Note: You must install the module layercake first.

// UsageimportExamplefrom"$components/layercake/Example.svelte";

Actions

Located in src/actions.

// Usageimportexamplefrom"$actions/action.js";
  • canTab.js: enable/disable tabbing on child elements.
  • checkOverlap.js: Label overlapping detection. Loops through selection of nodes and adds a class to the ones that are overlapping. Once one is hidden it ignores it.
  • focusTrap.js: Enable a keyboard focus trap for modals and menus.
  • keepWithinBox.js: Offsets and element left/right to stay within parent.
  • inView.js: detect when an element enters or exits the viewport.
  • resize.js: detect when an element is resized.

Runes

These are located in src/runes. You can put custom ones in src/runes/misc.js or create unique files for more complex ones.

import{example}from"$runes/misc/misc.js";
  • useWindowDimensions: returns an object { width, height } of the viewport dimensions. It is debounced for performance.
  • useClipboard: copy content to clipboard.
  • useFetcher: load async data from endpoints (local or external).
  • useWindowFocus: determine if the window is in focus or not.

For more preset runes, use runed which is preloaded.

Utils

Located in src/utils/.

// Usageimportexamplefrom"$utils/example.js";
  • checkScrollDir.js: Gets the user's scroll direction ("up" or "down")
  • csvDownload.js: Converts a flat array of data to CSV content ready to be used as an href value for download.
  • generateId.js: Generate an alphanumeric id.
  • loadCsv.js: Loads and parses a CSV file.
  • loadImage.js: Loads an image.
  • loadJson.js: Loads and parses a JSON file.
  • loadPixels.js: Loads the pixel data of an image via an offscreen canvas.
  • localStorage.js: Read and write to local storage.
  • mapToArray.js: Convenience function to convert a map to an array.
  • move.js: transform translate function shorthand.
  • transformSvg.js: Custom transition lets you apply an svg transform property with the in/out svelte transition. Parameters (with defaults):
  • translate.js: Convenience function for transform translate css.
  • urlParams.js: Get and set url parameters.

Tips

Image asset paths

For img tags, use relative paths:

<imgsrc="assets/demo/test.jpg" />

or use base if on a sub route:

<script>import{base}from"$app/paths";</script><imgsrc="{base}/assets/demo/test.jpg" />

For CSS background images, use absolute paths:

background:url("/assets/demo/test.jpg");

View example code in the preloaded demo.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages