diff --git a/docs/_static/custom.css b/docs/_static/custom.css index 9dbd6d5..29247e0 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -1,6 +1,124 @@ +/* Pandas tables. Pulled from the Jupyter / nbsphinx CSS + Included with MyST-NB until removal in v1.4.0 (Mar 2026). +*/ +div.cell_output table { + border: none; + border-collapse: collapse; + border-spacing: 0; + color: var(--color-foreground-primary); + font-size: 1em; + table-layout: fixed; +} +div.cell_output thead { + border-bottom: 1px solid var(--color-table-border); + vertical-align: bottom; +} +div.cell_output tr, +div.cell_output th, +div.cell_output td { + text-align: right; + vertical-align: middle; + padding: 0.5em 0.5em; + line-height: normal; + white-space: normal; + max-width: none; + border: none; +} +div.cell_output th { + font-weight: bold; + /* background: var(--color-table-header-background); */ +} +div.cell_output tbody tr:nth-child(odd) { + background: var(--color-background-secondary); +} +div.cell_output tbody tr:hover { + background: var(--color-background-hover); +} + +/* ipywidgets dark mode support. + The widgets CSS uses --jp-* variables throughout, but Furo doesn't define them. + The embed bundle injects the --jp-* defaults in :root *after* page load, so + remapping them is unreliable. Instead we directly override the widget selectors + with Furo's --color-* variables, which already adapt to light/dark mode. + noUiSlider (used for sliders) has entirely hardcoded colors, so those are + also overridden here. +*/ + +/* Text / label colors */ +.jupyter-widgets, +.widget-label, .jupyter-widget-label, +.widget-label-basic, .jupyter-widget-label-basic, +.widget-readout, .jupyter-widget-readout { + color: var(--color-foreground-primary) !important; +} + +/* Input / textarea / select backgrounds */ +.jupyter-widgets input[type="text"], +.jupyter-widgets input[type="number"], +.jupyter-widgets input[type="password"], +.jupyter-widgets textarea, +.jupyter-widgets select { + background-color: var(--color-background-secondary) !important; + color: var(--color-foreground-primary) !important; + border-color: var(--color-background-border) !important; +} + +/* Checkboxes: use brand color for the check/tick instead of bright white/blue */ +.jupyter-widgets input[type="checkbox"] { + accent-color: var(--color-brand-primary) !important; +} + +/* Checkbox body: render native checkbox in dark style in dark mode */ +body[data-theme="dark"] .jupyter-widgets input[type="checkbox"] { + color-scheme: dark; +} +@media (prefers-color-scheme: dark) { + body:not([data-theme="light"]) .jupyter-widgets input[type="checkbox"] { + color-scheme: dark; + } +} + +/* Color picker: tone down the bright border */ +.jupyter-widgets input[type="color"] { + background-color: var(--color-background-secondary) !important; + border-color: var(--color-background-border) !important; +} + +/* Buttons */ +.jupyter-button { + background-color: var(--color-background-secondary) !important; + color: var(--color-foreground-primary) !important; + border-color: var(--color-background-border) !important; +} +.jupyter-button.mod-primary { + background-color: var(--color-brand-primary) !important; + color: white !important; +} + +/* noUiSlider track, handle, and active range */ +.widget-slider .noUi-target, +.jupyter-widget-slider .noUi-target { + background: var(--color-background-secondary) !important; + border-color: var(--color-background-border) !important; + box-shadow: none !important; +} +.widget-slider .noUi-handle, +.jupyter-widget-slider .noUi-handle { + background: var(--color-background-primary) !important; + border-color: var(--color-background-border) !important; + box-shadow: none !important; +} +.widget-slider .noUi-connect, +.jupyter-widget-slider .noUi-connect { + background: var(--color-brand-primary) !important; +} + +/* Misc. overrides */ + table.dataframe { font-size: 0.8em !important; } + figcaption p { font-size: 85%; line-height: 1.3; diff --git a/docs/changes.md b/docs/changes.md index 4b388de..dff3c09 100644 --- a/docs/changes.md +++ b/docs/changes.md @@ -1,5 +1,13 @@ # Release notes +## v0.1.3 (unreleased) + +* Improve styling of abcjs containers, pandas dataframes, and ipywidgets + in the docs ({pull}`103`). + You can now activate abcjs responsive mode in + {class}`~pyabc2.abcjs.widget.ABCJSWidget`, + but non-responsive is still the default. + ## v0.1.2 (2026-02-03) * Update Norbeck to the current 2026-01 version ({pull}`96`) diff --git a/pyabc2/abcjs/widget/__init__.py b/pyabc2/abcjs/widget/__init__.py index 9ddb999..9e47a90 100644 --- a/pyabc2/abcjs/widget/__init__.py +++ b/pyabc2/abcjs/widget/__init__.py @@ -82,6 +82,13 @@ class ABCJSWidget(anywidget.AnyWidget): 740, help="Width of the staff in pixels.", ).tag(sync=True) + responsive = traitlets.Bool( + False, + help=( + "Whether the rendering should be responsive to container width " + "(up to `staff_width` + some padding)." + ), + ).tag(sync=True) transpose = traitlets.Integer( 0, help="Visual transpose in half steps.", @@ -115,6 +122,11 @@ def interactive(abc: str = "", **kwargs) -> "ipywidgets.Widget": # pragma: no c description="Staff width (px)", **slider_kws, ) + responsive_cbox = ipw.Checkbox( + value=False, + description="Responsive", + indent=True, + ) line_thickness_slider = ipw.FloatSlider( min=-0.4, max=2, @@ -152,6 +164,7 @@ def interactive(abc: str = "", **kwargs) -> "ipywidgets.Widget": # pragma: no c ipw.link((w, "abc"), (input_box, "value")) ipw.link((w, "staff_width"), (width_slider, "value")) + ipw.link((w, "responsive"), (responsive_cbox, "value")) ipw.link((w, "scale"), (scale_slider, "value")) ipw.link((w, "line_thickness_increase"), (line_thickness_slider, "value")) ipw.link((w, "transpose"), (transpose_slider, "value")) @@ -196,6 +209,7 @@ def save(_): [ input_box, width_slider, + responsive_cbox, scale_slider, line_thickness_slider, transpose_slider, diff --git a/pyabc2/abcjs/widget/index.css b/pyabc2/abcjs/widget/index.css index 5a5f1bd..8e23b7d 100644 --- a/pyabc2/abcjs/widget/index.css +++ b/pyabc2/abcjs/widget/index.css @@ -1,12 +1,23 @@ -div.container.debug { +div.abcjs-widget-container { + max-width: var(--staff-max-width, none); + overflow-x: auto; +} + +div.abcjs-widget-container.debug { color: forestgreen; border: 1px solid forestgreen; } -div.music.debug { +div.abcjs-widget-container div.music { + /* ABCJS seems to set `overflow: hidden` on the music div, + which prevents the container's auto overflow from working. */ + overflow: visible !important; +} + +div.abcjs-widget-container div.music.debug { border: 1px dashed grey; } -div.container code { +div.abcjs-widget-container code { white-space: pre-wrap; } diff --git a/pyabc2/abcjs/widget/index.js b/pyabc2/abcjs/widget/index.js index 7ec5050..a8e909b 100644 --- a/pyabc2/abcjs/widget/index.js +++ b/pyabc2/abcjs/widget/index.js @@ -53,6 +53,7 @@ function render({ model, el }) { let showDebugInput = () => model.get('debug_input'); let showLogo = () => model.get('logo'); let staffwidth = () => model.get('staff_width'); + let doResize = () => model.get('responsive'); let visualTranspose = () => model.get('transpose'); let active_music_ids = model.get("_active_music_ids"); @@ -60,7 +61,7 @@ function render({ model, el }) { console.log(`first_load ${first_load}`); let container = el; - container.classList.add('container'); + container.classList.add('abcjs-widget-container'); if ((first_load || showLogo()) && !hide()) { let logo = document.createElement('img'); @@ -94,6 +95,12 @@ function render({ model, el }) { music.innerHTML = ''; }; + // Clear any inline styles ABCJS may have been set on the music div + // (e.g. height/position from responsive mode's ResizeObserver), + // which would otherwise persist across re-renders and cause + // phantom empty space when toggling responsive on/off. + music.removeAttribute('style'); + head.innerHTML = ''; if (showDebugInput() && !hide()) { let code = document.createElement('code'); @@ -113,6 +120,19 @@ function render({ model, el }) { if (showDebugBox()) {showDebug.push('box')}; if (showDebugGrid()) {showDebug.push('grid')}; + // Responsive setting + let responsive = doResize() ? "resize" : undefined; + // We drive max-width via a CSS custom property so ABCJS's async ResizeObserver + // (which may clear inline max-width) cannot override it. + // ABCJS docs say left and right padding in the SVG are 15 and 50, + // so we try to give enough space for that, but note that in practice + // the results are not identical to responsive-off. + if (doResize()) { + container.style.setProperty('--staff-max-width', (staffwidth() + 65) + 'px'); + } else { + container.style.removeProperty('--staff-max-width'); + } + // NOTE: doesn't work with `music_id` passed as target, // even though it should, still not sure why let tunes = ABCJS.renderAbc( @@ -121,6 +141,7 @@ function render({ model, el }) { { foregroundColor: foregroundColor(), lineThickness: lineThickness(), + responsive: responsive, scale: scale(), showDebug: showDebug, staffwidth: staffwidth(),