personal memory agent
0

Configure Feed

Select the types of activity you want to include in your feed.

fix(convey): stop mobile facet pills shrinking so the row can scroll

On mobile, the facet-pill row squeezed pills to fit instead of scrolling.

The prior fix 09d1436c9 made .facet-bar .facet-pills-container compute overflow-x: auto under the 768px breakpoint, but .facet-pill still carried flex-shrink: 1. The pills compressed to fit, so the row never overflowed and overflow-x: auto had nothing to scroll.

Under the same breakpoint, .facet-bar .facet-pills-container .facet-pill { flex-shrink: 0 } keeps pills at content width, and justify-content: flex-start on the container makes the row scroll from the first pill.

The cascade mechanisms differ by rule. flex-shrink: 0 wins by specificity: (0,3,0) vs the unconditional .facet-pill at (0,1,0); source order runs against it because the media block is earlier in the file. justify-content: flex-start wins by source order: it has equal (0,2,0) specificity to the base rule and is placed after it.

Base rules are overridden, never edited in place. .facet-pill .label needs no override once the parent pill stops shrinking.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

+175
+8
solstone/convey/static/app.css
··· 667 667 .facet-bar .facet-pills-container { 668 668 overflow-x: auto; 669 669 -webkit-overflow-scrolling: touch; 670 + justify-content: flex-start; /* Beats the base center by source order */ 671 + } 672 + 673 + /* Mobile: pills keep their content width so the row overflows and scrolls 674 + instead of compressing. Wins the unconditional .facet-pill flex-shrink: 1 675 + below by specificity, since that rule is later in source. */ 676 + .facet-bar .facet-pills-container .facet-pill { 677 + flex-shrink: 0; 670 678 } 671 679 } 672 680
+167
tests/test_convey_facet_pill.py
··· 21 21 subprocess.run([node, "-e", script], check=True, text=True) 22 22 23 23 24 + def _class_specificity(selector: str) -> int: 25 + return selector.count(".") 26 + 27 + 28 + def _rule_block(css: str, selector: str, start: int = 0) -> tuple[int, str]: 29 + index = css.find(f"{selector} {{", start) 30 + assert index != -1, f"{selector} rule was not found" 31 + close = css.find("}", index) 32 + assert close != -1, f"{selector} rule is not closed" 33 + return index, css[index : close + 1] 34 + 35 + 24 36 def _render_facet_chooser_script(body: str) -> str: 25 37 fn = _function_body( 26 38 Path("solstone/convey/static/app.js").read_text(encoding="utf-8"), ··· 150 162 assert base_close != -1, "base facet-pills container rule is not closed" 151 163 assert base_index < css.find("overflow-x: clip", base_index) < base_close 152 164 assert base_index < mobile_index 165 + 166 + 167 + def test_mobile_facet_pills_block_prevents_pill_compression_below_content_width(): 168 + css = Path("solstone/convey/static/app.css").read_text(encoding="utf-8") 169 + 170 + mobile_anchor = ( 171 + "@media (max-width: 768px) {\n" 172 + " .facet-bar .facet-pills-container {\n" 173 + " overflow-x: auto;" 174 + ) 175 + mobile_index = css.find(mobile_anchor) 176 + assert mobile_index != -1, "mobile facet-pills block was not found" 177 + 178 + depth = 0 179 + mobile_close = -1 180 + for index in range(mobile_index, len(css)): 181 + char = css[index] 182 + if char == "{": 183 + depth += 1 184 + elif char == "}": 185 + depth -= 1 186 + if depth == 0: 187 + mobile_close = index 188 + break 189 + 190 + assert mobile_close != -1, "mobile facet-pills block is not closed" 191 + mobile_block = css[mobile_index : mobile_close + 1] 192 + _, container_rule = _rule_block(mobile_block, ".facet-bar .facet-pills-container") 193 + _, pill_rule = _rule_block( 194 + mobile_block, ".facet-bar .facet-pills-container .facet-pill" 195 + ) 196 + 197 + assert "overflow-x: auto;" in container_rule 198 + assert "justify-content: flex-start;" in container_rule 199 + assert "flex-shrink: 0;" in pill_rule 200 + 201 + 202 + def test_mobile_facet_pill_flex_shrink_override_wins_by_specificity(): 203 + css = Path("solstone/convey/static/app.css").read_text(encoding="utf-8") 204 + 205 + mobile_selector = ".facet-bar .facet-pills-container .facet-pill" 206 + competing_selector = ".facet-pill" 207 + mobile_index, mobile_rule = _rule_block(css, mobile_selector) 208 + competing_anchor = f"\n{competing_selector} {{\n" 209 + competing_index = css.find(competing_anchor) 210 + assert competing_index != -1, "unconditional .facet-pill rule was not found" 211 + competing_close = css.find("}", competing_index) 212 + assert competing_close != -1, "unconditional .facet-pill rule is not closed" 213 + competing_rule = css[competing_index + 1 : competing_close + 1] 214 + 215 + assert mobile_selector.endswith(competing_selector) 216 + assert _class_specificity(mobile_selector) == 3 217 + assert _class_specificity(competing_selector) == 1 218 + assert _class_specificity(mobile_selector) > _class_specificity(competing_selector) 219 + assert mobile_index < competing_index, ( 220 + "source order runs against the mobile flex-shrink override; specificity carries it" 221 + ) 222 + assert "flex-shrink: 0;" in mobile_rule 223 + assert "flex-shrink: 1;" in competing_rule 224 + 225 + 226 + def test_mobile_facet_container_justify_override_wins_by_source_order(): 227 + css = Path("solstone/convey/static/app.css").read_text(encoding="utf-8") 228 + 229 + selector = ".facet-bar .facet-pills-container" 230 + base_index, base_rule = _rule_block(css, selector) 231 + mobile_index, mobile_rule = _rule_block(css, selector, base_index + 1) 232 + base_justify_index = css.find("justify-content: center;", base_index) 233 + mobile_justify_index = css.find("justify-content: flex-start;", mobile_index) 234 + 235 + assert _class_specificity(selector) == 2 236 + assert "justify-content: center;" in base_rule 237 + assert "justify-content: flex-start;" in mobile_rule 238 + assert base_index < base_justify_index < mobile_index 239 + assert mobile_index < mobile_justify_index 240 + assert mobile_justify_index > base_justify_index 241 + 242 + 243 + def test_base_facet_layout_rules_remain_unchanged(): 244 + css = Path("solstone/convey/static/app.css").read_text(encoding="utf-8") 245 + 246 + base_container = ( 247 + ".facet-bar .facet-pills-container {\n" 248 + " flex: 1;\n" 249 + " display: flex;\n" 250 + " gap: 10px;\n" 251 + " align-items: center;\n" 252 + " justify-content: center;\n" 253 + " overflow-x: clip;\n" 254 + " overflow-y: visible;\n" 255 + " min-width: 0; /* Allow container to shrink below content size */\n" 256 + "}" 257 + ) 258 + base_pill = ( 259 + ".facet-pill {\n" 260 + " appearance: none;\n" 261 + " font-family: inherit;\n" 262 + " color: inherit;\n" 263 + " line-height: inherit;\n" 264 + " display: flex;\n" 265 + " align-items: center;\n" 266 + " padding: 8px 16px;\n" 267 + " border-radius: 20px;\n" 268 + " background: var(--pill-bg-rest, #f5f5f5);\n" 269 + " border: 1px solid var(--facet-border, #e5e0db);\n" 270 + " cursor: pointer;\n" 271 + " font-size: 15px;\n" 272 + " font-weight: 500;\n" 273 + " transition: transform 0.2s ease, opacity 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease, background 0.2s ease, color 0.2s ease;\n" 274 + " user-select: none;\n" 275 + " position: relative;\n" 276 + " min-width: 0; /* Allow pill to shrink below content size */\n" 277 + " flex-shrink: 1; /* Allow shrinking when space is tight */\n" 278 + "}" 279 + ) 280 + 281 + assert base_container in css 282 + assert base_pill in css 283 + 284 + 285 + def test_facet_pill_label_ellipsis_rule_remains_unchanged(): 286 + css = Path("solstone/convey/static/app.css").read_text(encoding="utf-8") 287 + 288 + label_rule = ( 289 + ".facet-pill .label {\n" 290 + " overflow: hidden;\n" 291 + " text-overflow: ellipsis;\n" 292 + " white-space: nowrap;\n" 293 + " min-width: 0; /* Can shrink to zero width */\n" 294 + " flex-shrink: 1;\n" 295 + " user-select: none;\n" 296 + " pointer-events: none;\n" 297 + "}" 298 + ) 299 + 300 + assert label_rule in css 301 + 302 + 303 + def test_mobile_flex_shrink_override_is_scoped_to_rendered_facet_row(): 304 + css = Path("solstone/convey/static/app.css").read_text(encoding="utf-8") 305 + app_js = Path("solstone/convey/static/app.js").read_text(encoding="utf-8") 306 + fn = _function_body(app_js, "renderFacetChooser") 307 + 308 + override_selector = ".facet-bar .facet-pills-container .facet-pill" 309 + class_assignment = "pill.className = 'facet-pill';" 310 + append_call = "facetPillsContainer.appendChild(pill);" 311 + class_index = fn.find(class_assignment) 312 + append_index = fn.find(append_call) 313 + 314 + assert f" {override_selector} {{\n flex-shrink: 0;" in css 315 + assert override_selector.startswith(".facet-bar .facet-pills-container ") 316 + assert app_js.count(class_assignment) == 1 317 + assert class_index != -1, "renderFacetChooser does not create facet pills" 318 + assert append_index != -1, "renderFacetChooser does not append facet pills" 319 + assert class_index < append_index 153 320 154 321 155 322 def test_selected_pill_defers_to_css_for_contrast_safe_treatment():