ascii-tabs 0.0.0-stage → 0.10.3

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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +657 -2
  3. package/ascii-tabs.js +2046 -0
  4. package/package.json +51 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 libasoles
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,658 @@
1
- # Temporary Holding Version
1
+ # ascii-tabs
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A zero-dependency web component for editing and displaying guitar tablature, with plain ASCII import and export.
4
+
5
+ **▶ [Try the live demo](https://libasoles.github.io/ascii-tabs/)**: every feature, editable, in your browser.
6
+
7
+ ## Install
8
+
9
+ `ascii-tabs` is published on npm as a single ES module with no dependencies. It defines the `<ascii-tabs>` custom element when imported, so it works in any framework or in plain HTML.
10
+
11
+ ```sh
12
+ npm install ascii-tabs
13
+ ```
14
+
15
+ ```js
16
+ import 'ascii-tabs';
17
+ // or, to also use the helpers:
18
+ import { parse, format } from 'ascii-tabs';
19
+ ```
20
+
21
+ ```html
22
+ <ascii-tabs></ascii-tabs>
23
+ ```
24
+
25
+ No bundler? Load it from a CDN instead, see [Usage](#usage). Full documentation and a live, editable demo: **https://libasoles.github.io/ascii-tabs/**
26
+
27
+ ## See it in action
28
+
29
+ ### Edit and organize tabs
30
+
31
+ Click a string and type a fret. Navigate with the keyboard, select and move groups of notes, and copy tabs as ASCII. Each Sheet has copy and delete controls, with an add button below.
32
+
33
+ <a href="https://libasoles.github.io/ascii-tabs/">
34
+ <picture>
35
+ <source media="(prefers-color-scheme: dark)" srcset="docs/img/example-dark.png">
36
+ <img alt="An ascii-tabs editor showing the opening of Ode to Joy on strings 1 and 2, with copy, delete and add buttons" src="docs/img/example-light.png" width="760">
37
+ </picture>
38
+ </a>
39
+
40
+ ### Switch notation and spacing
41
+
42
+ Show frets, letter names or solfege, and adjust spacing with optional built-in controls. Interface language and string labels are configurable independently.
43
+
44
+ <img alt="Two rendered tabs: letter note names with notation and spacing controls, and solfege with Spanish controls" src="docs/img/feature-notation.png" width="760">
45
+
46
+ ### Style individual notes
47
+
48
+ Highlight a melody with per-note colors and bold text, and put notes in parentheses. The screenshot below uses structured note data in the same editor.
49
+
50
+ <img alt="Guitar tab with red and blue melody notes, bold and normal weight frets, and parenthesized notes" src="docs/img/feature-note-styles.png" width="760">
51
+
52
+ Design notes: [spec](docs/spec-v0.1.md), [glossary](GLOSSARY.md), [ADRs](docs/adr). `tablatura.html` is the original standalone editor this library grew from.
53
+
54
+ ## Usage
55
+
56
+ ```html
57
+ <script type="module" src="https://cdn.jsdelivr.net/gh/libasoles/ascii-tabs@0.10.3/ascii-tabs.js"></script>
58
+
59
+ <ascii-tabs></ascii-tabs>
60
+ ```
61
+
62
+ A bare `<ascii-tabs>` is editable: a hint, one Sheet per Tab with copy and delete buttons, and an add button. Click a string and type the fret number (0–24); move with the arrow keys, Enter and Tab. Press Cmd+Right Arrow or Alt+Right Arrow to copy all six strings of the current column into the next column, replacing its contents, and move the cursor there. Press Cmd+0 or Alt+0 to fill only the empty cells of the current column with zeros, keeping existing frets and the cursor in place.
63
+
64
+ Drag from an empty cell or a blank area of a Sheet to select notes inside a rectangle. Drag any selected note to move the group, keeping its relative columns and strings; occupied destination cells are replaced. Cmd+Right Arrow or Alt+Right Arrow duplicates the selected group immediately after its last column and selects the duplicate. Cmd/Ctrl+C or the Sheet's copy button copies the selection as ASCII; Cmd/Ctrl+V pastes ASCII at the current column, preserving string positions. Delete or Backspace removes the selected notes, and Escape clears the selection.
65
+
66
+ ## Initial value
67
+
68
+ Pass the content as data through the `value` property. This is the preferred way to fill the element: no parsing, nothing ambiguous, and it is the same shape `value` and `change` give back.
69
+
70
+ `value` is `(number|{ fret: number|null, color?: string, bold?: boolean, parenthesized?: boolean }|null)[][][]`: an array of Tabs, each an array of Columns, each Column 6 entries from String 1 to String 6, a Fret (0–24) or `null` for an empty String. A note with `parenthesized: true` is displayed and copied in parentheses; `fret: null` is allowed for an empty pair.
71
+
72
+ ```html
73
+ <ascii-tabs id="song"></ascii-tabs>
74
+ <script>
75
+ song.value = [
76
+ [ // one Tab
77
+ [null, null, 2, null, 0, null],
78
+ [null, null, null, 2, null, null],
79
+ [null, 3, null, null, null, null],
80
+ [null, null, 2, null, 0, null],
81
+ ],
82
+ ];
83
+ </script>
84
+ ```
85
+
86
+ The component renders that data as:
87
+
88
+ <img alt="Rendered editor for the initial value example, showing frets on strings 2 through 5 with copy, delete and add controls" src="docs/img/initial-value.png" width="760">
89
+
90
+ `value` can be set before or after the element upgrades. Read it back the same way; every edit fires `change`:
91
+
92
+ ```js
93
+ song.addEventListener('change', e => console.log(e.detail.value));
94
+ ```
95
+
96
+ `change` fires immediately when a cell's value changes, including each accepted digit
97
+ (desktop and mobile), Backspace, Delete, Escape, parentheses and bold toggles.
98
+ For a single-cell edit, `detail.note` identifies the edited cell and its new value:
99
+
100
+ ```js
101
+ song.addEventListener('change', ({ detail: { value, note } }) => {
102
+ save(value); // All Tabs, in the same format as song.value.
103
+ if (note) {
104
+ const { tabIndex, columnIndex, stringIndex, value: editedNote } = note;
105
+ preview(editedNote, { tabIndex, columnIndex, stringIndex });
106
+ }
107
+ });
108
+
109
+ song.addEventListener('cellblur', ({ detail: { value, note } }) => {
110
+ // Same payload when leaving a cell, including an untouched cell.
111
+ commit(value, note);
112
+ });
113
+ ```
114
+
115
+ Indices are zero-based; `stringIndex: 0` is String 1. `note.value` is a number,
116
+ a note object (including its styles), or `null` after clearing the cell. Both
117
+ `detail.value` and `note.value` are independent copies. Multi-cell operations
118
+ (paste, moves, duplication, selection edits) and Tab additions/removals emit
119
+ `change` with `note: null`. `cellblur` bubbles when keyboard or pointer navigation
120
+ moves to another cell, or when the editor loses focus; clicking the same cell
121
+ does not emit it. These are DOM events: register callbacks with `addEventListener`.
122
+
123
+ Setting `value` does not fire `change`. The component is uncontrolled by default; for the controlled pattern, keep the Tabs in your own state, update it on `change` and write `value` back when the state changes elsewhere:
124
+
125
+ ```js
126
+ let state = [[]];
127
+ song.value = state;
128
+ song.addEventListener('change', e => { state = e.detail.value; save(state); });
129
+ function load(next) { state = next; song.value = state; }
130
+ ```
131
+
132
+ ### Column annotations
133
+
134
+ Set `annotations` to an array per Tab of `{ column, text }` or `{ column, html, text? }` entries. `column` is
135
+ the zero-based logical Column index in `value`, independent of Spacing and Staff
136
+ breaks. No spaces are needed to align text:
137
+
138
+ ```js
139
+ song.labels = 'notes';
140
+ song.setDocument({
141
+ value: [[
142
+ [null, null, null, null, null, null],
143
+ [1, 3, 2, 0, null, null],
144
+ [1, 3, 2, 3, null, null],
145
+ [1, 3, 2, null, 0, null],
146
+ ]],
147
+ annotations: [[
148
+ { column: 0, text: 'RE MENOR' },
149
+ { column: 1, text: 'Dm*' },
150
+ { column: 2, text: 'Dm/F*' },
151
+ { column: 3, text: 'Dm/A' },
152
+ ]],
153
+ });
154
+ // Update text independently, keeping the editing cursor:
155
+ song.annotations = [[{ column: 1, text: 'Dm' }]];
156
+ ```
157
+
158
+ Changing the Spacing slider, notation, labels, font or available width keeps each
159
+ Annotation aligned with its Column, including when it moves to another Staff.
160
+ In ASCII layout the text starts at the Column's Fret position; with `columnLayout`
161
+ it starts at the supplied center. Long text wraps within the remaining Staff
162
+ width. Overlapping annotations occupy separate lanes above the Strings, and each
163
+ Staff reserves their full height. Explicit newlines are supported, duplicate
164
+ anchors stack. `text` displays HTML characters literally; `html` renders markup:
165
+
166
+ ```js
167
+ song.annotations = [[
168
+ { column: 1, html: '<strong>Dm</strong><sup>*</sup><br><small>Intro</small>' },
169
+ { column: 2, html: '<em>Dm/F</em>', text: 'Dm/F' },
170
+ ]];
171
+ ```
172
+
173
+ When both fields are supplied, `html` controls the display and `text` is the ASCII
174
+ fallback. Without a fallback, browser copy/export extracts text from HTML,
175
+ decoding entities and preserving `<br>` and common block breaks. `format()` outside
176
+ a browser requires `text` for HTML annotations and throws a `TypeError` if it is
177
+ missing. HTML is inserted as supplied; pass trusted or sanitized markup. Normal
178
+ flow HTML participates in collision measurement, including later size changes
179
+ such as loaded images. Arbitrary positioned content can escape that flow.
180
+
181
+ Column indices must be nonnegative integers within the corresponding Tab,
182
+ including explicitly supplied empty Columns. Invalid annotations throw a
183
+ `TypeError` before changing state. Empty display content is hidden; `null` or `[]` clears all
184
+ annotations. Assignment and retrieval copy every entry. Entries are sorted by
185
+ Column, preserving the order of duplicate anchors. Annotations on trailing empty
186
+ Columns keep those Columns in `value` and ASCII output.
187
+
188
+ Setting `value` preserves annotations and requires their anchors to remain valid.
189
+ Use `setDocument({ value, annotations, columnLayout })` to update them atomically;
190
+ omitting `annotations` from `setDocument` clears them. Properties can be assigned
191
+ before upgrade, with `value` supplied first. Annotation updates do not emit
192
+ `change`. Annotations are separate from note data and built-in note storage;
193
+ persist them in consumer state. Deleting a Tab removes its annotations. Editing,
194
+ dragging and duplicating Frets leave annotations attached to their Columns.
195
+
196
+ #### Annotation width
197
+
198
+ `annotation-fit` (property `annotationFit`) decides what an Annotation does to
199
+ the Columns around it, in editable and `readonly` Tabs alike:
200
+
201
+ | Value | Behavior |
202
+ | --- | --- |
203
+ | `widen` (default) | The Annotation's Column grows to hold its text plus one space, so the next Column starts after it, even when that Column has notes and no Annotation. |
204
+ | `overflow` | Columns keep the width of their Frets; long text crosses into the next Column. Overlapping Annotations still stack in separate lanes. |
205
+ | `stagger` | Annotations alternate between two rows, so each can span about two Columns. Only when two Annotations on the same row would touch does the gap between them grow. |
206
+
207
+ ```html
208
+ <ascii-tabs annotations-editable annotation-fit="stagger"></ascii-tabs>
209
+ ```
210
+
211
+ Widths use the text (or `text` fallback of an `html` Annotation), measured in
212
+ characters, and are recalculated whenever Annotations, Spacing, notation or the
213
+ available width change. Text longer than a Staff wraps inside it. The setting does
214
+ nothing with `columnLayout`, which keeps its explicit geometry. Copy
215
+ uses the same widths and rows, and a Column grows when an edit is committed. `format(tab, { annotations, annotationFit })` takes
216
+ the same option and defaults to `'overflow'`, so existing output does not change.
217
+
218
+ Whole-Tab copy includes annotations, calculating ASCII offsets from the selected
219
+ Spacing and notation. `format(tab, { annotations: [{ column: 1, text: 'Dm' }] })`
220
+ uses the same anchors; colliding text occupies separate lines. Copying selected
221
+ notes keeps the usual six-String ASCII. ASCII import/paste still accepts numeric
222
+ tablature; remove annotation lines before parsing copied text.
223
+
224
+ ### Editing annotations and titles
225
+
226
+ Enable either interaction independently (both default to off):
227
+
228
+ ```html
229
+ <ascii-tabs annotations-editable titles-editable></ascii-tabs>
230
+ ```
231
+
232
+ The matching properties are `annotationsEditable` and `titlesEditable` (booleans).
233
+ Hover a Column to reveal a short horizontal bar above its Strings; click in that
234
+ upper strip to write an annotation. Clicking the Strings still edits Frets. Click
235
+ an existing annotation to edit it. The first annotation at an anchor is edited
236
+ from its strip; stacked annotations can each be clicked separately.
237
+
238
+ Hover the empty space immediately above a Sheet's upper-left corner to reveal
239
+ the same indicator for its title. Titles sit outside the card. Both targets also
240
+ work by touch or keyboard: Tab to the target and press Enter or Space. The editor
241
+ has no surrounding border, just a thin bottom border. Enter or blur saves,
242
+ Escape cancels, and Shift+Enter inserts a newline. Saving empty text removes the
243
+ annotation or clears the title. Typed text is literal; editing an HTML annotation
244
+ replaces that entry with plain text only when its text changes. Cancelling or
245
+ saving unchanged text preserves the HTML.
246
+
247
+ `tabTitles` is an array of strings, one per Tab; missing entries are empty titles.
248
+ Assign `null` or `[]` to clear titles. Titles display even when editing is disabled
249
+ or `readonly` is set. `readonly` disables both interactions. `value` assignments
250
+ preserve titles; `setDocument({ value, annotations, columnLayout, tabTitles })`
251
+ updates the whole document atomically and clears titles when omitted. Removing a
252
+ Tab removes its title; adding a Tab starts with an empty title. Titles are display
253
+ metadata and are not included in ASCII copy/export.
254
+
255
+ ```js
256
+ song.tabTitles = ['Estudio en re menor'];
257
+ song.addEventListener('annotationchange', ({ detail }) => {
258
+ // { index: Tab index, column: Column index, annotations: all Tabs' annotations }
259
+ saveAnnotations(detail.annotations);
260
+ });
261
+ song.addEventListener('titlechange', ({ detail }) => {
262
+ // { index: Tab index, title: updated text, tabTitles: all Tabs' titles }
263
+ saveTitles(detail.tabTitles);
264
+ });
265
+ ```
266
+
267
+ These events bubble and fire only for user edits that change content. Programmatic
268
+ assignments are silent. They do not emit the component's note `change` event.
269
+ Persist annotations and titles in consumer state; built-in storage contains notes.
270
+ Annotating an empty visual Column makes it a logical Column in `value` so its
271
+ anchor stays valid. Programmatic updates to the document or metadata, disabling
272
+ editing, removing a Tab, and disconnecting cancel any open text editor.
273
+
274
+ ### Aligning with a score
275
+
276
+ Use external geometry when a score renderer owns horizontal spacing. Update the
277
+ content and geometry together, especially when the number of Columns changes:
278
+
279
+ ```js
280
+ song.setDocument({
281
+ value: [[
282
+ [5, 7, null, null, null, null],
283
+ [null, null, null, null, 0, null],
284
+ [12, null, null, null, null, null],
285
+ [null, null, null, null, null, null],
286
+ ]],
287
+ columnLayout: [{ width: 640, positions: [84.5, 132, 201.25, 290] }],
288
+ });
289
+ ```
290
+
291
+ `columnLayout` has one `{ width, positions }` entry per Tab. Each `positions[c]`
292
+ is the center of Column `c` in CSS pixels from the left edge of the String area
293
+ (`.ascii-tabs-area`). `width` excludes labels, Sheet padding and tools. Align that
294
+ area's origin with the score and translate score coordinates into CSS pixels.
295
+ Labels occupy their own gutter. Frets, note names and parentheses are centered
296
+ independently of their text width. Set `--ascii-tabs-fret-background` to match the
297
+ surface behind the Frets; it defaults to `--ascii-tabs-sheet`.
298
+
299
+ Widths must be finite and positive; positions must be finite, strictly increasing,
300
+ within `[0, width]`, and match the logical Column count, including trailing empty
301
+ Columns. A Tab with zero Columns uses `positions: []` and still shows six Strings.
302
+ Invalid geometry or mismatched content throws a descriptive `TypeError` before
303
+ changing state. Geometry is copied on assignment and retrieval.
304
+
305
+ Each Tab stays in one Staff, with horizontal scrolling when needed and no automatic
306
+ padding or wrapping. `value` and `change.detail.value` preserve all Columns.
307
+ Editing, mobile input, selection and dragging retain their logical indices.
308
+ Clicks and drop targets choose the nearest center within the String area; exact
309
+ ties choose the earlier Column. Moving or duplicating Frets within existing
310
+ Columns is allowed. Paste, duplication and group moves that exceed the available
311
+ Columns are rejected in full. Navigation stops at the first/last Column; deleting
312
+ Frets leaves their Columns in place. Add/delete Tab buttons are disabled, and
313
+ `addTab()`/`removeTab()` do nothing: structure belongs to the consumer.
314
+
315
+ Assign `song.columnLayout` to reposition existing Columns without resetting the
316
+ cursor or typing. Assigning `song.value` while geometry is active requires the same
317
+ Tab/Column counts; use `setDocument({ value, columnLayout })` to change both
318
+ atomically. Programmatic assignments do not emit `change`. Properties may also be
319
+ assigned before custom-element upgrade (supply both `value` and `columnLayout`).
320
+ Built-in storage is suspended while external geometry is active: it neither loads
321
+ nor overwrites the ASCII store. The consumer owns persistence.
322
+
323
+ ```js
324
+ song.addEventListener('layoutoverflow', ({ detail }) => {
325
+ // { index, width, collisions }
326
+ // Recalculate score spacing, then provide new positions and width.
327
+ console.log(detail);
328
+ });
329
+ ```
330
+
331
+ `layoutoverflow` bubbles after rendering when displayed Frets overlap on a String
332
+ or extend past the area's edges. Each collision has a zero-based `string`,
333
+ `columns` indices and an `overlap` in CSS pixels; edge collisions also have
334
+ `edge: 'left' | 'right'`. Identical reports are suppressed until geometry or
335
+ collisions change. Centers never move automatically. Increasing only `width`
336
+ does not separate existing centers: recalculate positions too.
337
+
338
+ Set `song.columnLayout = null` to restore ASCII layout, wrapping, storage and
339
+ editing. `setDocument({ value })` also selects ASCII layout. Copy still uses
340
+ `format()` and exports ordinary ASCII with the chosen Spacing and notation;
341
+ pixel positions and trailing empty Columns are not preserved in copied text.
342
+ Geometry is never stored inside `Tab[]`.
343
+
344
+ ### Per-note colors and bold
345
+
346
+ Use an object instead of a number to give a note its own CSS color:
347
+
348
+ ```js
349
+ song.value = [[
350
+ [{ fret: 3, color: '#e63946' }, null, null, null, null, null],
351
+ [5, { fret: 2, color: 'royalblue' }, null, null, null, null],
352
+ ]];
353
+ ```
354
+
355
+ Numbers still use the default color. Objects require a valid `fret` (0–24); omitted or invalid colors use the default. Colors are preserved in `value`, `change`, storage, edits, moves and duplicates. Returned objects are independent copies. Color applies only to the note, including while focused; string dashes keep their theme color. CSS variables such as `var(--melody-color)` work too.
356
+
357
+ Set `bold: true` for bold text or `bold: false` for normal weight. Omit `bold` to keep the existing default (bold). You can combine it with `color`:
358
+
359
+ ```js
360
+ song.value = [[
361
+ [{ fret: 3, bold: true, color: '#b91c1c' }, null, null, null, null, null],
362
+ [{ fret: 5, bold: false }, null, null, null, null, null],
363
+ ]];
364
+ ```
365
+
366
+ Press `b` to toggle each selected note's bold state, or the current cell's note when there is no selection. Empty cells stay empty. Notes without an explicit `bold` value start bold, so the first press makes them normal weight.
367
+
368
+ `bold` accepts booleans only and follows the note through edits, moves, duplication, `value`, `change` and storage. Copying as ASCII discards font weight.
369
+
370
+ Colors are available through structured data only. ASCII input has no color information, and copying or calling `format` exports plain text without colors.
371
+
372
+ Type `(` (Shift plus the opening-parenthesis key for your keyboard layout) in a cell to wrap its note in parentheses. An empty cell gets an empty pair with the caret inside, ready for a fret number. Repeating the shortcut keeps a single pair. Parentheses survive storage, ASCII copy/paste, and notation changes.
373
+
374
+ To erase a parenthesized note, press Delete/Supr: this clears the entire cell, including the parentheses. Backspace removes digits inside the parentheses and leaves `()` when the last digit is erased. Escape restores the cell's value before the current edit, so it also undoes parentheses just added. There is currently no shortcut to remove only the parentheses while keeping the note.
375
+
376
+ If all you have is ASCII text, put it in a `<pre>` instead (see below).
377
+
378
+ ## ASCII in and out
379
+
380
+ As an alternative to `value`, a `<pre>` per Tab becomes the initial value; it is ignored when `value` is set. Parsing is lenient: number labels (`1 `–`6 `) and note labels (`e|B|G|D|A|E|`), any number of dashes. Frets at the same horizontal position share a Column, and several Staves are joined into one Tab.
381
+
382
+ ```html
383
+ <ascii-tabs>
384
+ <pre>
385
+ 1 ---------
386
+ 2 ---------
387
+ 3 -2---2---
388
+ 4 ---------
389
+ 5 -0---0---
390
+ 6 ---------</pre>
391
+ <pre>
392
+ e|-0--3--|
393
+ B|-1--0--|
394
+ G|-0--0--|
395
+ D|-2--0--|
396
+ A|-3--2--|
397
+ E|-----3-|</pre>
398
+ </ascii-tabs>
399
+ ```
400
+
401
+ The copy button copies the same plain ASCII. `parse` and `format` are exported as pure functions:
402
+
403
+ ```js
404
+ import { parse, format } from 'https://cdn.jsdelivr.net/gh/libasoles/ascii-tabs@0.10.3/ascii-tabs.js';
405
+
406
+ const tab = parse('1 -3--5-\n2 ------\n3 ------\n4 ------\n5 ------\n6 ------'); // one Tab: Column[]
407
+ format(tab, { spacing: 2 }); // "1 -3--5-\n2 ------\n..."
408
+ ```
409
+
410
+ `format` writes number labels, Spacing 2 by default, widens a Column that holds a two-digit Fret on every String, and cuts each Staff right after its last Column. Malformed ASCII logs a console error and parses to an empty Tab.
411
+
412
+ ## Composition
413
+
414
+ Features are turned on by composing parts, not by boolean attributes ([ADR 0001](docs/adr/0001-composition-over-boolean-props.md)). With no part children you get the default composition: hint, a Sheet with copy and delete, and add (read-only: just a Sheet with copy). With any part child, only the parts you declare appear. Content inside a part replaces its default icon or text.
415
+
416
+ ```html
417
+ <ascii-tabs>
418
+ <ascii-tabs-hint></ascii-tabs-hint>
419
+ <ascii-tabs-sheet>
420
+ <ascii-tabs-copy></ascii-tabs-copy>
421
+ <ascii-tabs-delete></ascii-tabs-delete>
422
+ </ascii-tabs-sheet>
423
+ <ascii-tabs-add></ascii-tabs-add>
424
+ </ascii-tabs>
425
+ ```
426
+
427
+ ### Read-only
428
+
429
+ ```html
430
+ <ascii-tabs id="song" readonly></ascii-tabs>
431
+ <script>
432
+ song.value = [[[3, null, null, null, null, null]]];
433
+ </script>
434
+ ```
435
+
436
+ `readonly` disables editing. Hint, delete and add hide themselves even when declared; copy still works. Like the editable element, every Staff runs the full width of its Sheet.
437
+
438
+ Elements with no width (including those inside `hidden` or `display: none` panels) defer
439
+ Staffs and annotations until `ResizeObserver` detects a visible size. Properties and
440
+ getters remain available while hidden. Showing an unchanged panel at the same width
441
+ reuses its rendered content; changes made while hidden appear on reveal.
442
+
443
+ ### Copy, hint, add and delete
444
+
445
+ ```html
446
+ <!-- No copy button -->
447
+ <ascii-tabs><ascii-tabs-sheet></ascii-tabs-sheet></ascii-tabs>
448
+
449
+ <!-- Custom copy text, custom hint -->
450
+ <ascii-tabs>
451
+ <ascii-tabs-hint>Pick a string, then type a fret.</ascii-tabs-hint>
452
+ <ascii-tabs-sheet><ascii-tabs-copy>Copy ASCII</ascii-tabs-copy></ascii-tabs-sheet>
453
+ <ascii-tabs-add>+ New tab</ascii-tabs-add>
454
+ </ascii-tabs>
455
+ ```
456
+
457
+ Delete never asks for confirmation. With several Tabs it removes its Tab; with one Tab it clears it, and is disabled while that Tab is empty.
458
+
459
+ The delete button emits a cancelable `delete` event on `<ascii-tabs>` before it acts. This gives consumers three choices: do nothing to keep the default, add a listener for an extra effect, or cancel the event and take over the action. `event.detail.index` is the Tab targeted by the button.
460
+
461
+ ```js
462
+ // Keep the default and complement it (for example, analytics).
463
+ tabs.addEventListener('delete', event => {
464
+ console.log('Deleting tab', event.detail.index);
465
+ });
466
+
467
+ // Override the default—for an asynchronous confirmation, cancel first and
468
+ // invoke the public method only after confirmation succeeds.
469
+ tabs.addEventListener('delete', async event => {
470
+ event.preventDefault();
471
+ if (await confirmDeletion()) tabs.removeTab(event.detail.index);
472
+ });
473
+ ```
474
+
475
+ When one empty Tab remains, the default button is disabled because there is no Tab for it to remove. Add `enabled` to the delete part when the host needs to handle that click—for example, to remove a surrounding songbook block. The default behavior still does nothing in that state; cancel the event to replace it.
476
+
477
+ ```html
478
+ <ascii-tabs>
479
+ <ascii-tabs-sheet><ascii-tabs-delete enabled></ascii-tabs-delete></ascii-tabs-sheet>
480
+ </ascii-tabs>
481
+ ```
482
+
483
+ ### Custom controls
484
+
485
+ Skip the parts and call the root's methods:
486
+
487
+ ```html
488
+ <ascii-tabs id="tabs"></ascii-tabs>
489
+ <button onclick="tabs.copy(0)">Copy</button>
490
+ <button onclick="tabs.addTab()">Add</button>
491
+ <button onclick="tabs.removeTab(0)">Delete</button>
492
+ ```
493
+
494
+ `copy(index)`, `addTab()` and `removeTab(index)` behave like the built-in buttons, including the delete rules.
495
+
496
+ ## Spacing
497
+
498
+ Spacing is the number of dashes between two Columns' Frets: `spacing="N"` (1–6, clamped, default 2), also available as the `spacing` property. Add the optional slider part to let people change it; it is not in the default composition and also works in `readonly`. Spacing changes the rendered Tab and the copied text, and a Column with a two-digit Fret is still one character wider on every String.
499
+
500
+ ```html
501
+ <!-- Initial spacing, no slider -->
502
+ <ascii-tabs spacing="3"></ascii-tabs>
503
+
504
+ <!-- Slider from 1 to 6 (put text inside to replace its label) -->
505
+ <ascii-tabs>
506
+ <ascii-tabs-spacing></ascii-tabs-spacing>
507
+ <ascii-tabs-sheet><ascii-tabs-copy></ascii-tabs-copy></ascii-tabs-sheet>
508
+ </ascii-tabs>
509
+ ```
510
+
511
+ ## Note notation
512
+
513
+ `notation` changes the displayed and copied cell text: `frets` (default), `letters`
514
+ (`C D E F G A B`), or `solfege` (lowercase `do re mi fa sol la si`). Notes default to
515
+ standard guitar tuning, String 1 to 6: E B G D A E. For example, Fret 0 on
516
+ String 1 is `E` or `mi`, and Fret 3 is `G` or `sol`. Accidentals use sharps
517
+ (`F#`, `fa#`); octaves share the same name.
518
+
519
+ ```html
520
+ <ascii-tabs notation="letters"></ascii-tabs>
521
+ <ascii-tabs notation="solfege" lang="es"></ascii-tabs>
522
+
523
+ <!-- Optional selector, also available in read-only mode -->
524
+ <ascii-tabs notation="solfege" lang="es">
525
+ <ascii-tabs-notation></ascii-tabs-notation>
526
+ <ascii-tabs-spacing></ascii-tabs-spacing>
527
+ <ascii-tabs-hint></ascii-tabs-hint>
528
+ <ascii-tabs-sheet><ascii-tabs-copy></ascii-tabs-copy></ascii-tabs-sheet>
529
+ </ascii-tabs>
530
+ ```
531
+
532
+ Also available as the `notation` property and the `format(tab, { notation: 'solfege' })`
533
+ option. Every Column uses its longest displayed name (including `sol#`) on all six
534
+ Strings, keeping chords aligned and wrapping by the actual text width. Spacing
535
+ still adds the configured dashes around each Column.
536
+
537
+ Editing, `value`, storage and `change` keep numeric Frets (0–24). Type fret
538
+ numbers in every notation. `lang` controls interface text independently of note
539
+ notation, and `labels` still controls the String labels. The copy button exports
540
+ the selected notation. Import and paste accept numeric tablature only: note names
541
+ without octaves cannot identify the original Fret (for example, 0, 12 and 24).
542
+ Switch to `frets` before copying when you need to paste the result back.
543
+
544
+ ## Tuning
545
+
546
+ Set `tuning` to six open-string note names, in order from String 1 (highest)
547
+ to String 6 (lowest). It affects displayed and copied note names and
548
+ `labels="notes"`; Frets in `value` and storage stay unchanged.
549
+
550
+ ```html
551
+ <!-- Drop D: lower only String 6 from E to D -->
552
+ <ascii-tabs tuning="E B G D A D" notation="letters" labels="notes"></ascii-tabs>
553
+ ```
554
+
555
+ ```js
556
+ tabs.tuning = ['E', 'B', 'G', 'D', 'A', 'D'];
557
+ format(tab, { tuning: ['E', 'B', 'G', 'D', 'A', 'D'], notation: 'solfege' });
558
+ ```
559
+
560
+ The property and `format` option accept an array or a space-separated string.
561
+ The getter returns a fresh array. Names accept A–G (case insensitive), optionally
562
+ followed by `#` or `b`, without octave numbers. Flats are normalized to sharps.
563
+ Missing, invalid or incomplete tunings use standard tuning (`E B G D A E`).
564
+ Changing or removing the attribute updates the view immediately.
565
+
566
+ ## String labels
567
+
568
+ Strings are labelled `1`–`6` by default, on screen and in copied text. `labels="notes"` names them using `tuning` (standard tuning by default: `e B G D A E`, String 1 to 6). `labels="none"` hides the labels and uses their space for Columns. This also applies to copied text. The `labels` property accepts `"numbers"`, `"notes"`, or `"none"`; other values fall back to numbers. Parsing accepts all three styles whatever the attribute says.
569
+
570
+ ```html
571
+ <ascii-tabs></ascii-tabs> <!-- 1 -3--5- -->
572
+ <ascii-tabs labels="notes"></ascii-tabs> <!-- e|-3--5- -->
573
+ <ascii-tabs labels="none"></ascii-tabs> <!-- -3--5- -->
574
+ ```
575
+
576
+ ## Tool position
577
+
578
+ A Sheet's tools (copy, delete) stand in a column to the right of the Staves by default, and the Staves leave room for them. `tools="top"` lines them up in a row above the first String instead. Also available as the `tools` property.
579
+
580
+ ```html
581
+ <ascii-tabs></ascii-tabs> <!-- column beside the Staves -->
582
+ <ascii-tabs tools="top"></ascii-tabs> <!-- row above them -->
583
+ ```
584
+
585
+ ## Sheet variant
586
+
587
+ Sheets look like cards by default. `variant="flat"` removes their background, border, and padding so the Tab can blend into its surrounding layout. It is also available as the `variant` property.
588
+
589
+ In the flat variant, each Staff has a transparent background in both light and
590
+ dark themes. Override `--ascii-tabs-flat-background` to give it a custom surface.
591
+
592
+ ```html
593
+ <ascii-tabs variant="flat"></ascii-tabs>
594
+ ```
595
+
596
+ ## Themes
597
+
598
+ The element renders in the light DOM ([ADR 0002](docs/adr/0002-light-dom.md)) and paints only its Sheets, never the page background. Without `theme` it follows `prefers-color-scheme`; `theme="light"` or `theme="dark"` forces a palette. Every colour is a custom property you can override on the element or any ancestor:
599
+
600
+ | Property | Light | Dark |
601
+ |---|---|---|
602
+ | `--ascii-tabs-paper` | `#fdfaf5` | `#1b1a18` |
603
+ | `--ascii-tabs-sheet` | `#fff` | `#232220` |
604
+ | `--ascii-tabs-flat-background` | `transparent` | `transparent` |
605
+ | `--ascii-tabs-ink` | `#1a1a1a` | `#f1ede4` |
606
+ | `--ascii-tabs-dash` | `#6f6c66` | `#959189` |
607
+ | `--ascii-tabs-string` | `#b3ada1` | `#b3ada1` |
608
+ | `--ascii-tabs-line` | `#e7e2d6` | `#36342f` |
609
+ | `--ascii-tabs-hover` | `#f4efe4` | `#2c2a27` |
610
+ | `--ascii-tabs-focus` | `#f6e4df` | `#3a2a1d` |
611
+ | `--ascii-tabs-accent` | `#8b0000` | `#fb923c` |
612
+
613
+ `--ascii-tabs-font` and `--ascii-tabs-font-size` set the typography. The font
614
+ size defaults to `22px`, but its custom property is not set on the component,
615
+ so it can be inherited from an ancestor:
616
+
617
+ ```css
618
+ .compact-tabs { --ascii-tabs-font-size: 1em; }
619
+ ```
620
+
621
+ `--ascii-tabs-string` colours the visual guitar strings (the dashes). They are hidden from assistive technology because they are diagrammatic decoration; labels, Frets, hints, and controls continue to use the AA-contrast text colours.
622
+
623
+ ```html
624
+ <ascii-tabs></ascii-tabs> <!-- auto -->
625
+ <ascii-tabs theme="light"></ascii-tabs>
626
+ <ascii-tabs theme="dark"></ascii-tabs>
627
+ <ascii-tabs style="--ascii-tabs-accent: teal; --ascii-tabs-sheet: #f0fafa"></ascii-tabs>
628
+ ```
629
+
630
+ ## Text
631
+
632
+ Built-in text is English. `lang="es"` switches every built-in string to Spanish. The `messages` property takes a partial object that is merged over the active language; content you put inside a part still wins over `messages`.
633
+
634
+ ```html
635
+ <ascii-tabs></ascii-tabs> <!-- "Click a string and type the fret number." -->
636
+ <ascii-tabs lang="es"></ascii-tabs> <!-- "Hacé clic en una cuerda y escribí el número de traste." -->
637
+
638
+ <ascii-tabs id="tabs"></ascii-tabs>
639
+ <script>
640
+ tabs.messages = { hint: 'Tap a string, then type a fret.', copy: 'Copy as ASCII' };
641
+ </script>
642
+ ```
643
+
644
+ Keys: `hint`, `copy`, `copied`, `delete`, `add`, `fret`, `spacing`, `notation`, `frets`, `letters`, `solfege`.
645
+
646
+ ## Storage
647
+
648
+ Nothing is stored unless you add the storage part. `<ascii-tabs-storage key="…">` loads the Tabs from `localStorage` under that key on start and saves them on every change. Stored Tabs take precedence over an initial `value` and `<pre>` content; an empty or unreadable store falls back to them. Storage failures (private mode, quota) are ignored. Two instances with different keys don't interfere.
649
+
650
+ ```html
651
+ <ascii-tabs>
652
+ <ascii-tabs-storage key="my-tabs"></ascii-tabs-storage>
653
+ </ascii-tabs>
654
+ ```
655
+
656
+ A root whose only part child is the storage part still gets the default composition. The stored format is Tabs → Columns → 6 cells, each `""` or the Fret as text (as in the original editor); colored notes are stored as objects. Reading also accepts numbers and a single Tab.
657
+
658
+ MIT licensed.