@barmark/render 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,41 @@ package uses [semantic versioning](https://semver.org/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.2.0] - 2026-11-01
10
+
11
+ Easier to read, and the bar numbers easier to scan. Every size below was
12
+ chosen on barmark.dev. The chart looks different, so check pages that
13
+ style it. Requires `barmark` 0.2.0 or later.
14
+
15
+ ### Changed
16
+
17
+ - **Chords are 16px by default** (`--barmark-size` was `1.25rem`, now `1rem`),
18
+ and no label is under 12px: the meta line, headings, bar numbers, lyrics,
19
+ repeat counts, triplet marks and diagnostics are all larger in proportion.
20
+ - **Bar numbers sit flush left** in a fixed gutter, on their row's chord
21
+ baseline. Section headings, lyrics, note lines and mid-chart changes
22
+ start at the first bar line, so a `3/4` never sits among the numbers.
23
+ - **Chords start at the left** of their share of the bar instead of centred,
24
+ and no bar is narrower than a bar holding D7sus4.
25
+ - **A change written mid-chart names itself** and marks where it takes
26
+ effect: "→ Time 3/4 · Tempo ♩ = 120", with a rule, not a lone "3/4".
27
+ - **`###` note lines are italic** at weight 400 (Edwin has no bold, so 600 was
28
+ faked), and lyrics and note lines use Georgia, which has a true italic.
29
+ - **A single ringed note** has room inside its ring and sits on the chord
30
+ baseline (it was 1px low).
31
+
32
+ ### Added
33
+
34
+ - **A `text` property on `<barmark-chart>`** for live editing: set it and the
35
+ chart redraws, batched with attribute changes. While it is a string it
36
+ wins over `src` (no fetch) and the script child; `null` returns to them.
37
+ Text set before the element is defined is kept.
38
+ - **`--barmark-text-font` and `--barmark-label-font`**: the fonts for lyrics
39
+ and note lines, and for the meta line, mid-chart changes, headings and
40
+ diagnostics.
41
+ - **`--barmark-directive` and `--barmark-note`**: the colours of mid-chart
42
+ changes and of note lines.
43
+
9
44
  ## [0.1.0] - 2026-10-31
10
45
 
11
46
  The first release: barmark charts as an evenly spaced bar grid, as HTML
@@ -64,5 +99,6 @@ that needs no script or as the `<barmark-chart>` web component. Requires
64
99
  zoom without sideways scrolling, the element renders and re-renders, and
65
100
  a screen reader hears "bar 9: D". Run them with `bun run test:browser`.
66
101
 
67
- [Unreleased]: https://github.com/kevinpeckham/barmark/compare/render-v0.1.0...HEAD
102
+ [Unreleased]: https://github.com/kevinpeckham/barmark/compare/render-v0.2.0...HEAD
103
+ [0.2.0]: https://github.com/kevinpeckham/barmark/compare/render-v0.1.0...render-v0.2.0
68
104
  [0.1.0]: https://github.com/kevinpeckham/barmark/releases/tag/render-v0.1.0
package/README.md CHANGED
@@ -43,12 +43,25 @@ themselves.
43
43
  </barmark-chart>
44
44
  ```
45
45
 
46
- The element renders from its `src` URL, or from a child
46
+ The element renders from its `text` property, its `src` URL, or a child
47
47
  `<script type="text/barmark">` (indentation is removed), and renders again
48
- whenever either changes; it fetches a `src` once, and renders once
49
- however many attributes change together. A chart rendered on the server
50
- by `renderChart` keeps its music until it has a source of its own, and
51
- still follows `theme` and `bar-numbers`.
48
+ whenever one changes; it fetches a `src` once, and renders once however
49
+ many properties and attributes change together. A chart rendered on the
50
+ server by `renderChart` keeps its music until it has a source of its own,
51
+ and still follows `theme` and `bar-numbers`.
52
+
53
+ For a live editor, set `text`:
54
+
55
+ ```js
56
+ const chart = document.querySelector("barmark-chart");
57
+ editor.addEventListener("input", () => { chart.text = editor.value; });
58
+ ```
59
+
60
+ While `text` is a string it wins over `src` and the script child, and no
61
+ fetch happens; set it to `null` to go back to them. Any string is safe:
62
+ the chart escapes it, and problems show as diagnostics, never errors.
63
+ There is no `text` attribute: chart text belongs in the property or the
64
+ script child.
52
65
 
53
66
  The element registers the font itself, from the `fonts/` folder beside its
54
67
  own module. If your bundler moves that file, register it yourself before
@@ -64,6 +77,7 @@ the element loads, with `<style data-barmark-font>` holding
64
77
  | `rehearsalLetters` | `rehearsal-letters` (`rehearsalLetters`) | `false` | boxed A, B, C… on each section as played |
65
78
  | `diagnostics` | | `false` | list the chart's errors and warnings below it |
66
79
  | `fontUrl` | | none | emit Edwin's `@font-face` rule before the chart |
80
+ | (the `text` argument) | (`text`) | `null` | the chart to draw; wins over `src` and the script child |
67
81
 
68
82
  ## Styling on purpose
69
83
 
@@ -71,18 +85,23 @@ Set these custom properties on the element or anything above it:
71
85
 
72
86
  | Property | What it sets |
73
87
  |---|---|
74
- | `--barmark-font` | the chart's typeface |
75
- | `--barmark-size` | the base size everything scales from (default `1.25rem`) |
88
+ | `--barmark-font` | the chords' typeface |
89
+ | `--barmark-text-font` | lyrics and `###` note lines (default Georgia) |
90
+ | `--barmark-label-font` | the meta line, mid-chart changes, headings and diagnostics (default the system sans) |
91
+ | `--barmark-size` | the chord size everything scales from (default `1rem`) |
76
92
  | `--barmark-ink` | chords and text |
77
93
  | `--barmark-paper` | the background |
78
94
  | `--barmark-muted` | holds, bar numbers, braces, meta lines |
79
95
  | `--barmark-rule` | bar lines |
80
96
  | `--barmark-accent` | errors |
97
+ | `--barmark-directive` | mid-chart changes (default the muted colour) |
98
+ | `--barmark-note` | `###` note lines (default the ink colour) |
81
99
 
82
100
  ```css
83
101
  barmark-chart {
84
- --barmark-size: 1rem;
102
+ --barmark-size: 0.9rem;
85
103
  --barmark-paper: transparent;
104
+ --barmark-text-font: "Noto Serif", Georgia, serif;
86
105
  }
87
106
  barmark-chart::part(heading) {
88
107
  color: teal;
@@ -121,6 +140,12 @@ another serif.
121
140
 
122
141
  ## What it draws
123
142
 
143
+ Bar numbers sit in a gutter at the left edge; section headings, lyrics,
144
+ note lines and mid-chart changes start at the first bar line. Chords start
145
+ at the left of their share of the bar. A change written mid-chart names
146
+ itself: `time: 3/4, tempo: 120` draws as "→ Time 3/4 · Tempo ♩ = 120".
147
+ At the default size chords are 16px and no label is under 12px.
148
+
124
149
  Chords appear in their canonical form whatever the author typed (`Cø`
125
150
  shows as Cm⁷(♭5)). `%` is drawn as the bar it copies; a replayed section
126
151
  is drawn in full with its own bar numbers, and its own lyrics replace the
package/dist/element.d.ts CHANGED
@@ -4,6 +4,13 @@ type Attributes = {
4
4
  hasAttribute(name: string): boolean;
5
5
  };
6
6
  export declare function optionsFromAttributes(el: Attributes): RenderOptions;
7
+ export type ChartSource = "text" | "src" | "script" | "none";
8
+ /** What a chart draws: its `text` while that is a string, else `src`, else its script child. */
9
+ export declare function chartSource(from: {
10
+ text: string | null;
11
+ src: string;
12
+ script: boolean;
13
+ }): ChartSource;
7
14
  /** Chart text from a script element: blank edges and common indentation removed. */
8
15
  export declare function inlineChartText(text: string): string;
9
16
  export {};
package/dist/element.js CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
- * `<barmark-chart>`: renders itself from a `src` URL or from a child
3
- * `<script type="text/barmark">`, and again whenever either changes. A
4
- * shadow root rendered on the server is reused. Importing this module
5
- * registers the element (once) and the font (once per document).
2
+ * `<barmark-chart>`: renders itself from its `text` property, a `src` URL
3
+ * or a child `<script type="text/barmark">`, and again whenever one
4
+ * changes. A shadow root rendered on the server is reused. Importing this
5
+ * module registers the element (once) and the font (once per document).
6
6
  */
7
7
  import { FONT_FAMILY, fontFaceCss } from "./font.js";
8
8
  import { loadErrorContent, shadowContent, THEMES } from "./html.js";
@@ -14,6 +14,14 @@ export function optionsFromAttributes(el) {
14
14
  rehearsalLetters: el.hasAttribute("rehearsal-letters"),
15
15
  };
16
16
  }
17
+ /** What a chart draws: its `text` while that is a string, else `src`, else its script child. */
18
+ export function chartSource(from) {
19
+ if (from.text !== null)
20
+ return "text";
21
+ if (from.src)
22
+ return "src";
23
+ return from.script ? "script" : "none";
24
+ }
17
25
  /** Chart text from a script element: blank edges and common indentation removed. */
18
26
  export function inlineChartText(text) {
19
27
  const lines = text.replace(/^\s*\n|\n\s*$/g, "").split("\n");
@@ -43,7 +51,14 @@ function defineElement() {
43
51
  #scheduled = false;
44
52
  /** The text fetched for `src`, kept so a change of options does not fetch again. */
45
53
  #fetched = null;
54
+ #text = null;
46
55
  connectedCallback() {
56
+ // Text set before the element was defined landed on the instance: route it through the setter.
57
+ if (Object.hasOwn(this, "text")) {
58
+ const text = this.text ?? null;
59
+ delete this.text;
60
+ this.text = text;
61
+ }
47
62
  registerFont(this.ownerDocument);
48
63
  this.#observer.observe(this, { childList: true, subtree: true, characterData: true });
49
64
  this.#schedule();
@@ -55,6 +70,15 @@ function defineElement() {
55
70
  if (this.isConnected)
56
71
  this.#schedule();
57
72
  }
73
+ /** Chart text to draw; while it is a string it wins over `src` and a script child. */
74
+ get text() {
75
+ return this.#text;
76
+ }
77
+ set text(v) {
78
+ this.#text = v === null || v === undefined ? null : String(v);
79
+ if (this.isConnected)
80
+ this.#schedule();
81
+ }
58
82
  get src() {
59
83
  return this.getAttribute("src") ?? "";
60
84
  }
@@ -95,27 +119,30 @@ function defineElement() {
95
119
  async #render() {
96
120
  const script = this.querySelector('script[type="text/barmark"]');
97
121
  const options = optionsFromAttributes(this);
122
+ const source = chartSource({ text: this.#text, src: this.src, script: script !== null });
98
123
  // With no source of its own, a server-rendered chart keeps its music and takes the options it can.
99
- if (!this.src && !script)
124
+ if (source === "none")
100
125
  return this.#restyle(options);
101
126
  const render = ++this.#renders;
102
- const content = await this.#content(script, options);
127
+ const content = await this.#content(source, script, options);
103
128
  if (render !== this.#renders)
104
129
  return;
105
130
  (this.shadowRoot ?? this.attachShadow({ mode: "open" })).innerHTML = content;
106
131
  }
107
- async #content(script, options) {
108
- if (!this.src)
132
+ async #content(source, script, options) {
133
+ if (source === "text")
134
+ return shadowContent(this.#text ?? "", options);
135
+ if (source !== "src")
109
136
  return shadowContent(inlineChartText(script?.textContent ?? ""), options);
110
137
  try {
111
- return shadowContent(await this.#text(this.src), options);
138
+ return shadowContent(await this.#fetch(this.src), options);
112
139
  }
113
140
  catch (e) {
114
141
  this.#fetched = null;
115
142
  return loadErrorContent(`Could not load ${this.src}: ${e.message}`, options);
116
143
  }
117
144
  }
118
- #text(src) {
145
+ #fetch(src) {
119
146
  if (this.#fetched?.src !== src)
120
147
  this.#fetched = { src, text: fetchText(src) };
121
148
  return this.#fetched.text;
package/dist/html.js CHANGED
@@ -3,7 +3,7 @@ import { escapeHtml } from "./escape.js";
3
3
  import { fontFaceCss } from "./font.js";
4
4
  import { layoutChart } from "./model.js";
5
5
  import { speakBar } from "./speech.js";
6
- import { FIT_STEPS, stylesheet } from "./styles.js";
6
+ import { FIT_STEPS, MIN_BAR, stylesheet } from "./styles.js";
7
7
  export function renderChart(text, options = {}) {
8
8
  return `${fontStyle(options)}<barmark-chart><template shadowrootmode="open">${shadowContent(text, options)}</template></barmark-chart>`;
9
9
  }
@@ -32,16 +32,16 @@ function headerHtml(model) {
32
32
  return "";
33
33
  return `<header class="bm-header">${title}${meta ? `<p class="bm-meta" part="meta">${meta}</p>` : ""}</header>`;
34
34
  }
35
- /** "D major · 4/4 · ♩ = 145 · swing", escaped. */
36
- function metaText(d) {
35
+ /** "D major · 4/4 · ♩ = 145 · swing", escaped; labelled, "Key D major · Time 4/4 · …". */
36
+ function metaText(d, labelled = false) {
37
37
  return [
38
- d.key ? `${glyphs(d.key.tonic)} ${d.key.mode}` : "",
39
- d.time ? `${d.time.n}/${d.time.d}` : "",
40
- d.tempo ? `♩ = ${d.tempo}` : "",
41
- d.feel ?? "",
38
+ ["Key", d.key ? `${glyphs(d.key.tonic)} ${d.key.mode}` : ""],
39
+ ["Time", d.time ? `${d.time.n}/${d.time.d}` : ""],
40
+ ["Tempo", d.tempo ? `♩ = ${d.tempo}` : ""],
41
+ ["Feel", d.feel ?? ""],
42
42
  ]
43
- .filter(Boolean)
44
- .map(escapeHtml)
43
+ .filter(([, value]) => value)
44
+ .map(([label, value]) => escapeHtml(labelled ? `${label} ${value}` : (value ?? "")))
45
45
  .join(" · ");
46
46
  }
47
47
  function blockHtml(block, settings) {
@@ -66,8 +66,8 @@ function fitWidth(block) {
66
66
  }
67
67
  /** Em per bar: padding, plus each slot's text and the gap after it. */
68
68
  const cellWidth = (cell) => cell.kind === "fermata"
69
- ? 2.6
70
- : Math.max(2.6, 1.5 + (cell.meter ? 0.8 : 0) + cell.slots.reduce((w, s) => w + slotWidth(s), 0));
69
+ ? MIN_BAR
70
+ : Math.max(MIN_BAR, 1.5 + (cell.meter ? 0.8 : 0) + cell.slots.reduce((w, s) => w + slotWidth(s), 0));
71
71
  /** Roughly: 0.65em a baseline character, 0.4em a superscript one, 0.6em between slots. */
72
72
  function slotWidth(slot) {
73
73
  if (slot.kind === "group")
@@ -104,7 +104,8 @@ function itemHtml(item, columns, settings) {
104
104
  case "text":
105
105
  return `<p class="bm-text${item.note ? " bm-note" : ""}" part="text">${escapeHtml(item.text)}</p>`;
106
106
  case "directive":
107
- return `<p class="bm-directive" part="meta">${metaText(item.set)}</p>`;
107
+ // Mid-chart, a change names itself: "→ Time 3/4", never a lone "3/4".
108
+ return `<p class="bm-directive" part="meta"><span class="bm-change" aria-hidden="true">→</span><span>${metaText(item.set, true)}</span></p>`;
108
109
  case "error":
109
110
  return errorHtml(item.source, item.messages);
110
111
  }
package/dist/styles.d.ts CHANGED
@@ -1,8 +1,4 @@
1
- /**
2
- * The chart's styles. Everything is sized in em from `--barmark-size`, so
3
- * text size and zoom scale the chart; colours come from the palettes below
4
- * unless the page sets the `--barmark-*` custom properties.
5
- */
1
+ export declare const MIN_BAR = 4.68;
6
2
  export type Palette = {
7
3
  ink: string;
8
4
  paper: string;
package/dist/styles.js CHANGED
@@ -1,4 +1,41 @@
1
1
  import { FONT_FAMILY } from "./font.js";
2
+ /**
3
+ * The chart's styles. Everything is sized in em from `--barmark-size`, so
4
+ * text size and zoom scale the chart; colours come from the palettes below
5
+ * unless the page sets the `--barmark-*` custom properties.
6
+ */
7
+ /**
8
+ * Text sizes as fractions of the chord size, `--barmark-size` (1rem unless
9
+ * the page sets it). Chosen on barmark.dev, 2026-11-01: at the default size
10
+ * nothing a reader needs is under 12px. The superscript is a fraction of its
11
+ * chord and the octave of its note name; everything else of the chord size.
12
+ */
13
+ const SIZES = {
14
+ title: 1.16,
15
+ meta: 1,
16
+ directive: 0.97,
17
+ heading: 0.77,
18
+ text: 0.9,
19
+ noChord: 1.03,
20
+ noteNames: 0.9,
21
+ superscript: 0.84,
22
+ octave: 0.79,
23
+ tuplet: 0.84,
24
+ repeat: 0.97,
25
+ diagnostics: 0.97,
26
+ barNumber: 0.84,
27
+ meter: 0.77,
28
+ cut: 0.77,
29
+ };
30
+ /** Lengths, in chord sizes: the bar-number gutter, the narrowest bar (a D7sus4 bar), the gap under a heading. */
31
+ const GUTTER = 2.06;
32
+ export const MIN_BAR = 4.68;
33
+ const HEADING_GAP = 0.645;
34
+ /** The single note's ring, in its own em: padding, and the border it spills past. */
35
+ const RING = { y: 0.21, x: 0.5, border: 0.07 };
36
+ const em = (n) => `${Number(n.toFixed(3))}em`;
37
+ /** A length in chord sizes, in the em of text at `size`. */
38
+ const at = (size, length) => em(length / size);
2
39
  /** Both pass WCAG AA on their paper (tests/contrast.test.ts). */
3
40
  export const PALETTES = {
4
41
  light: { ink: "#1d1d1f", paper: "#fffdf8", muted: "#6a6052", rule: "#4a4a4a", accent: "#9a4a12" },
@@ -10,12 +47,17 @@ const colours = (p) => Object.keys(p)
10
47
  .map((k) => `--bm-${k}:var(--barmark-${k},${p[k]});`)
11
48
  .join("");
12
49
  const SANS = `system-ui,-apple-system,"Segoe UI",sans-serif`;
50
+ /** The meta line, directives, headings and diagnostics. */
51
+ const LABEL = `var(--barmark-label-font,${SANS})`;
52
+ /** Lyrics and note lines: a serif every system has, with a true italic (Edwin has none). */
53
+ const TEXT = "var(--barmark-text-font,Georgia,serif)";
54
+ const CHART_FONT = `var(--barmark-font,"${FONT_FAMILY}","Edwin",Georgia,serif)`;
13
55
  const css = `
14
56
  :host{all:initial;display:block}
15
57
  .bm-chart{${colours(PALETTES.light)}
16
58
  container:bm/inline-size;
17
- font-family:var(--barmark-font,"${FONT_FAMILY}","Edwin",Georgia,serif);
18
- font-size:var(--barmark-size,1.25rem);
59
+ font-family:${CHART_FONT};
60
+ font-size:var(--barmark-size,1rem);
19
61
  line-height:1.3;color:var(--bm-ink);background:var(--bm-paper);
20
62
  padding:1em 1.2em;box-sizing:border-box;text-align:start}
21
63
  .bm-chart *{box-sizing:border-box}
@@ -24,45 +66,50 @@ const css = `
24
66
  @media print{.bm-chart[data-theme]{${colours(PALETTES.light)}color-scheme:light}}
25
67
  .bm-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
26
68
  .bm-header{margin-bottom:.8em}
27
- .bm-title{font-size:1.5em;font-weight:normal;margin:0 0 .15em}
28
- .bm-meta,.bm-directive{font-family:${SANS};font-size:.62em;color:var(--bm-muted);margin:0}
29
- .bm-directive{grid-column:1/-1;margin:.9em 0 .3em}
69
+ .bm-title{font-size:${em(SIZES.title)};font-weight:normal;margin:0 0 .15em}
70
+ .bm-meta{font-family:${LABEL};font-size:${em(SIZES.meta)};color:var(--bm-muted);margin:0}
71
+ .bm-directive{grid-column:1/-1;display:flex;align-items:baseline;gap:.4em;margin:.9em 0 .3em ${at(SIZES.directive, GUTTER)};font-family:${LABEL};font-size:${em(SIZES.directive)};color:var(--barmark-directive,var(--bm-muted))}
72
+ .bm-directive::after{content:"";flex:1;align-self:center;margin-left:.4em;border-top:.06em solid var(--bm-rule);opacity:.6}
30
73
  .bm-section{margin-top:.9em}
31
- .bm-heading{display:flex;align-items:center;gap:.5em;margin:0 0 .4em;font:600 .6em/1 ${SANS};letter-spacing:.08em;text-transform:uppercase;color:var(--bm-ink)}
74
+ .bm-heading{display:flex;align-items:center;gap:.5em;margin:0 0 ${at(SIZES.heading, HEADING_GAP)} ${at(SIZES.heading, GUTTER)};font:600 ${em(SIZES.heading)}/1 ${LABEL};letter-spacing:.08em;text-transform:uppercase;color:var(--bm-ink)}
32
75
  .bm-rehearsal{border:.12em solid currentColor;padding:.15em .35em;border-radius:.15em;letter-spacing:0}
33
76
  .bm-times{font:italic 1.2em/1 Georgia,serif;text-transform:none;letter-spacing:0;color:var(--bm-muted)}
34
- .bm-grid{display:grid;grid-template-columns:auto repeat(var(--bm-cols),auto) auto;width:max-content;max-width:100%}
77
+ .bm-grid{display:grid;grid-template-columns:${em(GUTTER)} repeat(var(--bm-cols),auto) auto;width:max-content;max-width:100%}
35
78
  .bm-chart[data-bar-numbers="off"] .bm-grid{grid-template-columns:repeat(var(--bm-cols),auto) auto}
36
79
  .bm-row{display:contents}
37
- .bm-bar-number{font:.5em/1 ${SANS};color:var(--bm-muted);padding:1.6em .8em 0 0;text-align:end}
38
- .bm-bar{position:relative;display:flex;align-items:baseline;min-width:2.6em;min-height:2.2em;padding:.65em .7em .25em;border-right:.075em solid var(--bm-rule)}
80
+ .bm-bar-number{display:flex;flex:none;align-items:baseline;width:${at(SIZES.barNumber, GUTTER)};padding-top:${at(SIZES.barNumber, 0.65)};font:${em(SIZES.barNumber)}/1 ${SANS};color:var(--bm-muted)}
81
+ .bm-bar-number::before{content:"\\200b";font:${em(1 / SIZES.barNumber)}/1.3 ${CHART_FONT}}
82
+ .bm-chart[data-bar-numbers="off"] :is(.bm-heading,.bm-text,.bm-directive){margin-left:0}
83
+ .bm-bar{position:relative;display:flex;align-items:baseline;min-width:${em(MIN_BAR)};min-height:2.2em;padding:.65em .7em .25em;border-right:.075em solid var(--bm-rule)}
39
84
  .bm-row>div.bm-bar:first-of-type{border-left:.075em solid var(--bm-rule)}
40
85
  .bm-row>div.bm-bar[data-repeat-start]{border-left:.25em double var(--bm-rule)}
41
86
  .bm-row>div.bm-bar[data-repeat-end]{border-right:.25em double var(--bm-rule)}
42
- .bm-bar-body{display:flex;flex:1;align-items:baseline;justify-content:space-around;gap:.6em}
87
+ .bm-bar-body{display:flex;flex:1;align-items:baseline;gap:.6em}
88
+ .bm-bar-body>:not(.bm-meter,.bm-cut){flex:1 1 0;min-width:max-content}
43
89
  .bm-repeat{display:flex;align-items:center;padding-left:.4em;white-space:nowrap}
44
- .bm-repeat .bm-times{font-size:.75em}
90
+ .bm-repeat .bm-times{font-size:${em(SIZES.repeat)}}
45
91
  .bm-chord{white-space:nowrap}
46
- .bm-chord sup{font-size:.62em;vertical-align:.55em;line-height:0}
47
- .bm-chord sub{font-size:.6em;line-height:0}
92
+ .bm-chord sup{font-size:${em(SIZES.superscript)};vertical-align:.55em;line-height:0}
93
+ .bm-chord sub{font-size:${em(SIZES.octave)};line-height:0}
48
94
  .bm-hold,.bm-unknown,.bm-brace{color:var(--bm-muted)}
49
- .bm-nc{font-size:.72em;letter-spacing:.06em}
95
+ .bm-nc{font-size:${em(SIZES.noChord)};letter-spacing:.06em}
50
96
  .bm-silence{display:inline-block;width:.9em;height:.32em;background:var(--bm-ink);vertical-align:.25em}
51
- .bm-ring{display:inline-block;min-width:1.4em;padding:0 .3em;border:.07em solid currentColor;border-radius:1em;text-align:center}
97
+ .bm-ring,.bm-notes{font-size:${em(SIZES.noteNames)}}
98
+ .bm-ring{display:inline-block;min-width:1.4em;padding:${em(RING.y)} ${em(RING.x)};margin:${em(-(RING.y + RING.border))} 0;border:${em(RING.border)} solid currentColor;border-radius:1em;text-align:center}
52
99
  .bm-notes{display:inline-flex;gap:.3em;align-items:baseline}
53
100
  .bm-notes u{text-decoration-thickness:.08em;text-underline-offset:.15em}
54
101
  .bm-group{position:relative;display:inline-flex;gap:.45em;align-items:baseline}
55
102
  .bm-group::before{content:"";position:absolute;left:0;right:0;top:-.45em;height:.3em;border:.06em solid var(--bm-muted);border-bottom:none}
56
- .bm-tuplet{position:absolute;left:50%;top:-1.25em;transform:translateX(-50%);font:italic 600 .55em/1 Georgia,serif;color:var(--bm-muted);background:var(--bm-paper);padding:0 .3em}
57
- .bm-meter{display:inline-flex;flex-direction:column;align-items:center;align-self:center;font:700 .5em/.95 ${SANS};color:var(--bm-muted)}
103
+ .bm-tuplet{position:absolute;left:50%;top:-1.25em;transform:translateX(-50%);font:italic 600 ${em(SIZES.tuplet)}/1 Georgia,serif;color:var(--bm-muted);background:var(--bm-paper);padding:0 .3em}
104
+ .bm-meter{display:inline-flex;flex-direction:column;align-items:center;align-self:center;font:700 ${em(SIZES.meter)}/.95 ${SANS};color:var(--bm-muted)}
58
105
  .bm-fermata-glyph{display:block;color:var(--bm-ink)}
59
106
  .bm-bar[data-cut]{outline:.08em dashed var(--bm-muted);outline-offset:-.25em}
60
- .bm-cut{position:absolute;top:.3em;right:.45em;font:.42em/1 ${SANS};color:var(--bm-muted)}
61
- .bm-text{grid-column:1/-1;margin:.2em 0 .5em;font-size:.8em}
62
- .bm-note{font-size:.7em;font-weight:600}
107
+ .bm-cut{position:absolute;top:.3em;right:.45em;font:${em(SIZES.cut)}/1 ${SANS};color:var(--bm-muted)}
108
+ .bm-text{grid-column:1/-1;margin:.2em 0 .5em ${at(SIZES.text, GUTTER)};font-family:${TEXT};font-size:${em(SIZES.text)}}
109
+ .bm-note{font-style:italic;color:var(--barmark-note,var(--bm-ink))}
63
110
  .bm-error{grid-column:1/-1;margin:.3em 0}
64
111
  .bm-error code{font:.7em/1.4 ui-monospace,Menlo,Consolas,monospace;text-decoration:underline wavy var(--bm-accent);text-decoration-thickness:.08em;text-underline-offset:.25em;white-space:pre-wrap}
65
- .bm-diagnostics{margin:1em 0 0;padding-left:1.2em;font:.55em/1.5 ${SANS};color:var(--bm-muted)}
112
+ .bm-diagnostics{margin:1em 0 0;padding-left:1.2em;font:${em(SIZES.diagnostics)}/1.5 ${LABEL};color:var(--bm-muted)}
66
113
  .bm-diagnostics .bm-error{color:var(--bm-accent)}
67
114
  @media (forced-colors:active){
68
115
  .bm-bar,.bm-row>div.bm-bar:first-of-type,.bm-row>div.bm-bar[data-repeat-start],.bm-row>div.bm-bar[data-repeat-end]{border-color:CanvasText}
@@ -72,7 +119,7 @@ const css = `
72
119
  }
73
120
  ${FIT_STEPS.map((w) => {
74
121
  const g = `.bm-grid[data-fit="${w}"]`;
75
- return `@container bm (max-width:${w}em){${g}{display:block}${g}>.bm-row{display:flex;flex-wrap:wrap}${g} .bm-empty{display:none}${g} .bm-bar{flex:0 1 auto;max-width:100%;border-left:.075em solid var(--bm-rule);margin-left:-.075em}${g} :is(.bm-bar-body,.bm-group,.bm-notes){flex-wrap:wrap}}`;
122
+ return `@container bm (max-width:${w}em){${g}{display:block}${g}>.bm-row{display:flex;flex-wrap:wrap}${g} .bm-empty{display:none}${g} .bm-bar{flex:0 1 auto;max-width:100%;border-left:.075em solid var(--bm-rule);margin-left:-.075em}${g} :is(.bm-bar-body,.bm-group,.bm-notes){flex-wrap:wrap}${g} .bm-bar-body>*{flex:1 1 auto;min-width:auto}}`;
76
123
  }).join("\n")}
77
124
  `;
78
125
  /** Collapse the readable source above: whitespace around punctuation and between rules. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barmark/render",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Render barmark chord charts as a clean, accessible bar grid: HTML with Declarative Shadow DOM, or the <barmark-chart> web component.",
5
5
  "type": "module",
6
6
  "exports": {