diff --git a/apps/protspace/notebooks/ProtSpace_Preparation.ipynb b/apps/protspace/notebooks/ProtSpace_Preparation.ipynb index bd78ebae..d61b47a9 100644 --- a/apps/protspace/notebooks/ProtSpace_Preparation.ipynb +++ b/apps/protspace/notebooks/ProtSpace_Preparation.ipynb @@ -531,7 +531,8 @@ " \"border\": \"1px solid #666\",\n", " \"padding\": \"6px 10px\",\n", " \"margin\": \"2px\",\n", - " \"flex\": \"1 1 220px\",\n", + " \"flex\": \"1 1 300px\",\n", + " \"min_width\": \"300px\",\n", " \"overflow\": \"hidden\",\n", "}\n", "\n", diff --git a/apps/protspace/tests/test_notebook_layout.py b/apps/protspace/tests/test_notebook_layout.py new file mode 100644 index 00000000..539df684 --- /dev/null +++ b/apps/protspace/tests/test_notebook_layout.py @@ -0,0 +1,58 @@ +import json +import warnings +from pathlib import Path + +NOTEBOOK_PATH = ( + Path(__file__).resolve().parents[1] / "notebooks" / "ProtSpace_Preparation.ipynb" +) +GENERATE_CELL_TITLE = "# @title 3. Generate & Download" +MIN_PARAMETER_GROUP_BASIS_PX = 300 + + +def _generate_cell_source() -> str: + notebook = json.loads(NOTEBOOK_PATH.read_text()) + for cell in notebook["cells"]: + source_text = "".join(cell.get("source", [])) + if source_text.startswith(GENERATE_CELL_TITLE): + return source_text + raise AssertionError("Generate & Download cell not found") + + +def test_parameter_groups_wrap_before_slider_tracks_collapse(): + displayed_widgets = [] + namespace = {"display": displayed_widgets.append} + + source = _generate_cell_source() + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", + message=r"Passing unrecognized arguments to super\(Layout\).*", + category=DeprecationWarning, + ) + exec(compile(source, str(NOTEBOOK_PATH), "exec"), namespace) + + parameter_groups = namespace["param_groups"] + assert displayed_widgets, "Generate cell did not render its widget tree" + assert parameter_groups, "Generate cell did not create parameter groups" + + param_grid = namespace["param_grid"] + # Match the flex-wrap token exactly: a substring test would accept "nowrap". + flex_flow = param_grid.layout.flex_flow or "" + assert "wrap" in flex_flow.split(), ( + f"Parameter grid must wrap (flex_flow={flex_flow!r}); " + "a nowrap row lets the cards shrink past their tracks" + ) + + for group, _methods in parameter_groups: + assert group.layout.overflow == "hidden" + flex_basis = group.layout.flex.split()[-1] + assert flex_basis.endswith("px") + basis_px = int(flex_basis.removesuffix("px")) + assert basis_px >= MIN_PARAMETER_GROUP_BASIS_PX + + min_width = group.layout.min_width or "" + assert min_width.endswith("px") + min_width_px = int(min_width.removesuffix("px")) + assert min_width_px >= basis_px, ( + "A lone parameter group must not shrink below its responsive flex basis" + ) diff --git a/openspec/changes/fix-colab-slider-responsive/.openspec.yaml b/openspec/changes/fix-colab-slider-responsive/.openspec.yaml new file mode 100644 index 00000000..5849c2db --- /dev/null +++ b/openspec/changes/fix-colab-slider-responsive/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-01 diff --git a/openspec/changes/fix-colab-slider-responsive/design.md b/openspec/changes/fix-colab-slider-responsive/design.md new file mode 100644 index 00000000..8c0865c2 --- /dev/null +++ b/openspec/changes/fix-colab-slider-responsive/design.md @@ -0,0 +1,41 @@ +## Context + +The Generate cell renders each dimensionality-reduction parameter group as an `ipywidgets.VBox` inside a wrapping `HBox`. Each group currently declares `flex: 1 1 220px`, while a rendered slider's description, value readout, and internal spacing consume about 228 px before any track length is available. With UMAP and t-SNE enabled, three groups therefore remain on one row at widths where their tracks collapse to nearly zero. + +The layout lives inside the committed notebook JSON, and the fix must work in Colab without adding new CSS, JavaScript, or dependencies. + +## Goals / Non-Goals + +**Goals:** + +- Wrap parameter groups before their slider tracks become unusable. +- Preserve the existing controls, values, callbacks, and ordinary desktop layout. +- Add a focused automated guard and repeatable rendered-browser verification. + +**Non-Goals:** + +- Redesigning other notebook sections or controls. +- Changing dimensionality-reduction defaults or processing behavior. +- Adding a general notebook testing framework or external dependency. + +## Decisions + +1. Increase the parameter-group flex basis from 220 px to 300 px. Rendered measurements show a consistent 228 px card-to-track difference (for example, a 345 px card has a 117 px track), so a 300 px card preserves roughly 72 px of track before the flex container wraps. A larger basis would wrap earlier but consume unnecessary vertical space; changing slider label widths would alter the established alignment and would not address every label/readout combination. +2. Set the parameter-group minimum width to the same 300 px reserve. `flex-basis` is only a preferred size while `flex-shrink: 1`; without an explicit floor, a lone card can still shrink below the responsive reserve. The parent `HBox` already uses ipywidgets' scrolling box behavior, so narrower panes can scroll to the full-width control instead of clipping a collapsed track. +3. Keep `flex-flow: row wrap` and the existing card-level `overflow: hidden`. Wrapping is already the intended responsive mechanism, and the explicit minimum width removes the residual shrink path without adding breakpoint CSS to the notebook. +4. Guard the responsive contract with a focused Python test that executes the committed notebook cell and asserts both the 300 px flex basis and its matching minimum-width floor. Rendered browser verification at desktop and compressed widths supplies the end-to-end evidence that the structural contract produces usable tracks. + +## Risks / Trade-offs + +- [Risk] Two cards may wrap sooner in unusually narrow notebook panes. → This is the intended trade-off: added vertical space preserves accurate slider interaction. +- [Risk] A pane narrower than one parameter group needs horizontal scrolling. → Preserve the 300 px control width and rely on the parent ipywidgets box's existing overflow behavior rather than collapsing the slider. +- [Risk] Widget implementations can change their fixed internal chrome. → Keep a margin above the measured 228 px and verify actual track geometry in a real browser. +- [Risk] Notebook editors can reserialize unrelated JSON. → Make the source edit surgically and review the notebook diff before committing. + +## Migration Plan + +No migration is required. The notebook-only layout value is applied the next time the Generate cell runs; reverting the one-line basis change restores the previous behavior. + +## Open Questions + +None. diff --git a/openspec/changes/fix-colab-slider-responsive/proposal.md b/openspec/changes/fix-colab-slider-responsive/proposal.md new file mode 100644 index 00000000..2da21ef9 --- /dev/null +++ b/openspec/changes/fix-colab-slider-responsive/proposal.md @@ -0,0 +1,25 @@ +## Why + +The preparation notebook keeps three dimensionality-reduction parameter cards on one row after their slider tracks have run out of usable width. Opening Colab's terminal or otherwise narrowing the notebook can therefore collapse each track to nearly zero pixels, making the controls difficult or impossible to use. + +## What Changes + +- Make parameter cards wrap before their slider tracks collapse under a compressed notebook viewport. +- Add a focused regression check for the minimum responsive card basis used by the preparation notebook. +- Verify the current-main reproduction at desktop and compressed widths with rendered `ipywidgets` controls. + +## Capabilities + +### New Capabilities + +- `colab-preparation-controls`: Responsive layout requirements for dimensionality-reduction parameter controls in the Colab preparation notebook. + +### Modified Capabilities + +None. + +## Impact + +- `apps/protspace/notebooks/ProtSpace_Preparation.ipynb` +- Focused Python regression coverage for the notebook layout contract +- No API, dependency, bundle-format, or application-runtime changes diff --git a/openspec/changes/fix-colab-slider-responsive/specs/colab-preparation-controls/spec.md b/openspec/changes/fix-colab-slider-responsive/specs/colab-preparation-controls/spec.md new file mode 100644 index 00000000..0b025da0 --- /dev/null +++ b/openspec/changes/fix-colab-slider-responsive/specs/colab-preparation-controls/spec.md @@ -0,0 +1,23 @@ +## ADDED Requirements + +### Requirement: Parameter sliders remain usable in compressed notebook layouts + +The preparation notebook SHALL lay out dimensionality-reduction parameter groups with a wrapping flex basis and minimum width of at least 300 px so a group's slider description and value readout cannot collapse its interactive track before or after the group wraps. + +#### Scenario: Terminal compresses the notebook content area + +- **WHEN** three parameter groups are visible and the notebook content area becomes too narrow to allocate at least 300 px to each group +- **THEN** the groups wrap onto another row +- **AND** each visible slider retains a usable horizontal track instead of collapsing to its thumb + +#### Scenario: Pane becomes narrower than one parameter group + +- **WHEN** the notebook content area becomes narrower than 300 px +- **THEN** each parameter group retains its minimum width +- **AND** the surrounding widget area can scroll horizontally instead of shrinking the slider track below its usable reserve + +#### Scenario: Ordinary desktop notebook width + +- **WHEN** the notebook content area can allocate at least 300 px to each of three visible groups +- **THEN** the groups remain on one row +- **AND** existing slider labels, values, and interactions remain unchanged diff --git a/openspec/changes/fix-colab-slider-responsive/tasks.md b/openspec/changes/fix-colab-slider-responsive/tasks.md new file mode 100644 index 00000000..6d42e51f --- /dev/null +++ b/openspec/changes/fix-colab-slider-responsive/tasks.md @@ -0,0 +1,20 @@ +## 1. Regression Coverage + +- [x] 1.1 Add a focused test that requires the preparation notebook's parameter groups to reserve at least 300 px before wrapping +- [x] 1.2 Run the focused test against the current 220 px basis and record the expected failure + +## 2. Responsive Fix + +- [x] 2.1 Increase only the parameter-group flex basis to the specified responsive minimum +- [x] 2.2 Rerun the focused test and record the passing result + +## 3. Verification + +- [x] 3.1 Reproduce the three-card notebook layout at desktop and compressed viewports and confirm usable slider tracks after wrapping +- [x] 3.2 Validate the OpenSpec change and run the repository-mandated `pnpm precommit` gate + +## 4. Review Follow-up + +- [x] 4.1 Add a regression assertion that a lone parameter group cannot shrink below the responsive reserve +- [x] 4.2 Set each parameter group's minimum width to the 300 px flex basis +- [x] 4.3 Run focused Python checks, validate the amended OpenSpec change, and rerun `pnpm precommit`