Repository navigation
fix(form): keep a field's own margin-bottom regardless of stylesheet load order - #19412
Conversation
…load order form/Container.scss gave every direct child margin-bottom: 10px and a field's own rule (margin-bottom: 5px on .neo-textfield, 2.5rem on .neo-file-upload-field) tied it at the same specificity, so whichever stylesheet the browser inserted last won. Neo inserts a class's stylesheet the first time an instance of it is created, so the same form rendered with different spacing depending on which component the app happened to create first. Wrap the container's base rule in :where(), which contributes zero specificity, so a field's own margin always wins. The last-child rule is untouched and keeps its (0,2,0) specificity. Generated by Claude Code.
neo-opus-ada
left a comment
There was a problem hiding this comment.
PR Micro-Review
Opening: Thank you, @Saadanjum0. Two things stand out: you proved the bug red-first on dev by reverting the SCSS and rebuilding the theme, and you flagged the screenshot item honestly instead of guessing. If you'd like to see more of the engine, set up the Engine from a clone (three commands), then open apps/workstation and drag a tab out of the browser window (why that works). Your agent needs neither our Knowledge Base nor our Memory Core: neo-agent-skills is on npm.
Class: contained. One selector change that #19230 prescribed, plus its component spec. The PR introduces no concept beyond what the ticket already decided.
Verdict: Request Changes
Glance: At head 2793c1dd, the SCSS is the ticket's decided fix:
:where(.neo-form-container) > *contributes (0,0,0), so.neo-textfieldand.neo-file-upload-fieldat (0,1,0) win in any load order.- The untouched last-child rule keeps (0,2,0), and the one-line why is there.
- CI is green (32/32), and no file outside the two the ticket names changed.
- Your note on the 11 forms is enough for the screenshot item: a static read can't tell creation order, and the maintainers' post-merge check covers it.
I found one problem, in the spec. It reads getComputedStyle as soon as an element exists, but Neo doesn't wait for a class's stylesheet before it renders. insertThemeFiles asks the main thread to add the <link> and only counts the pending loads (countLoadingThemeFiles in src/worker/App.mjs). So a read can land before a stylesheet has loaded. In the second test that cuts the wrong way: on dev, the form's fields can read 5px from the already-loaded Text.css before Container.css arrives and turns them into 10px. The test that should catch the bug could then pass on the bug.
Findings:
-
Wait for the stylesheets before reading any margin. Add a helper and call it after each
createNeoInstance(...)and itswaitForSelector(...), before the firstmarginBottomOf(...):async function stylesheetsLoaded(page, names) { await page.waitForFunction(names => { const links = [...document.querySelectorAll('link[rel="stylesheet"]')]; return names.every(name => links.some(link => link.href.includes(name))) && links.every(link => link.sheet) }, names) } // e.g. in both tests, once the form exists: await stylesheetsLoaded(page, ['/form/Container.css', '/form/field/Text.css', '/form/field/FileUpload.css']);
A
<link>has itssheetonly once it has loaded, and theme files arrive ascss/<folder>/<class path>.css(Stylesheet.addThemeFilesinsrc/main/addon/Stylesheet.mjs). A file that never loads makes the wait fail loudly instead of reading defaults. Then rerun your red check: ondev, the second test should still read10px. -
Origin Session ID: 5267f5db-e1d4-4297-8570-4981234133aa
⚖️ Ada · @neo-opus-ada · Claude Opus 5.5 · Claude Code
[review-budget-managed]
- outcome: within-budget
- ordinary-limit: 1
- activation-issue: 15257
- activation-pr: 15307
- activated-at: 2026-07-16T20:54:31Z
getComputedStyle() could run before a field's or the container's theme stylesheet had finished loading, since insertThemeFiles() only tells the main thread to add the <link> and does not wait for it to load. On dev, that let the second test read 5px from an already-loaded Text.css before Container.css arrived, which could pass even if the margin-bottom fix itself regressed. Add stylesheetsLoaded(), which waits for every named theme file's <link> to be present with a resolved .sheet, and call it before each test's first margin read.
|
Good catch, thank you — pushed f20f78f. Added Full |
neo-opus-ada
left a comment
There was a problem hiding this comment.
PR Micro-Review — Round 2
Opening: Thank you, @Saadanjum0. You turned the finding around in one commit, and the comment above the helper explains the race to the next reader.
Class: contained. One test-only commit on top of the reviewed SCSS fix.
Verdict: Approve
Glance: At head f20f78f1, the one finding from round 1 is resolved:
stylesheetsLoaded()waits untilContainer.css,Text.cssandFileUpload.csseach have a<link>and every stylesheet<link>has itssheet. Both tests call it before the firstmarginBottomOf(...).- Both tests create the full form, so all three stylesheets always get a
<link>and the wait resolves. A stylesheet that never loads keeps itssheetatnull, and the wait times out loudly instead of reading defaults. - The wait only moves the read until after the stylesheets have loaded. On
dev, the second test now reads the cascade's final10pxand fails, where before it could read5pxearly and pass on the bug. Your red-first check from round 1 still holds. - CI: lint, guard, the unit timezone jobs and the baseline checks are green. The components, e2e-engine, unit and build-all jobs were still running when I reviewed (the operator restarted them).
Findings: none.
- Origin Session ID: 4095e966-9503-44c4-a759-5e7293dc3daa
⚖️ Ada · @neo-opus-ada · Claude Opus 5.5 · Claude Code
…drop three stale claims (#19431) (#19432) Lines for #19403 and #19405 (the merged-cycle gap), #19412 (a first Engine PR) and #19426 (the Dock Layouts card). The claim-reply line gets its live bound after #19414. Line 190 follows #19408. The package chapter states that the package ships no browser bundles and a workspace builds them (#18823), and the DevIndex and site-deploy sentences no longer cite the closed #19047.
|
This pull request was merged before Neo.mjs kept a standing for its contributors. It does now, counted in merged pull requests, so here is yours. Standing: Contributor (1 merged). 4 more reach Returning Contributor. Your badge, if you would like to show it, is at https://neomjs.com/badges/saadanjum0-160885369/ with three forms to choose from. The card's line: Next, if you want one: #20926 (also See it run: the engine you changed runs live in the browser: https://neomjs.com/examples This note comes once. From here on a comment arrives with each merge. To be left out of the records, open an issue in neomjs/pages. |
Please make sure to read the Contributing Guidelines:
https://github.com/neomjs/neo/blob/dev/CONTRIBUTING.md
Does this PR resolve an issue? (Required)
Resolves #19230
(@Saadanjum01, who claimed the issue, is my other account. The fork lives under this one, so the PR comes from here.)
What kind of change does this PR introduce? (check at least one)
Does this PR introduce a breaking change? (check one)
The PR fulfills this requirement:
devbranch, not themainbranchOther information:
Problem.
form/Container.scssgives every direct childmargin-bottom: 10pxthrough.neo-form-container > *. Two field roots set their own, different bottom margin at the samespecificity:
.neo-textfield(5px, inherited by Number/Date/Time/etc.) and.neo-file-upload-field(2.5rem, reserved for its error message). Since both rules tie on specificity, whichever stylesheet
the browser inserted last won. Neo inserts a class's stylesheet the first time an instance of that
class is created (
insertThemeFiles()insrc/worker/App.mjs), so the same form rendered with 5px or10px of spacing depending on which component the app happened to create first — not on anything the
form's own markup controls.
Fix. Wrap the container's base rule in
:where(), which contributes zero specificity, so a field'sown margin always wins:
.neo-form-container { padding: 20px; - - > * { - margin-bottom: 10px; - - &:last-child { - margin-bottom: 0; - } - } } + +// A field's own margin wins; the container spaces only the children that set none. +:where(.neo-form-container) > * { + margin-bottom: 10px; +} + +.neo-form-container > *:last-child { + margin-bottom: 0; +}The last-child rule is untouched and keeps its
(0,2,0)specificity, as called out in the issue.Test.
test/playwright/component/form/Container.spec.mjsbuilds the same three-child form (TextField,FileUpload field, TextField) twice:
stylesheets insert before the form's does).
Both assert the same
getComputedStyle(...).marginBottom:5pxfor the first TextField, the uploadfield's own
2.5rem(computed in px) for the upload field, and0pxfor the last child.Before this fix, the second test fails on
dev(first TextField reads10px, reproducing the issue);the first test happens to pass by accident of creation order. I verified this by temporarily reverting
the SCSS change and rebuilding the dev theme:
With the fix restored, both pass, and so does the rest of the
formsuite (37/37):Scope note on the "Done when" list's screenshot item. 11 files under
examples/andapps/importform/Container.mjs:examples/layout/form/MainContainer.mjs,examples/component/toast/MainContainer.mjs,examples/component/process/realWorldExample/MainContainer.mjs,examples/container/fragment/MainContainer.mjs,apps/form/view/FormPageContainer.mjs,apps/form/view/FormContainer.mjs,apps/legit/view/AddFileDialog.mjs,apps/realworld2/view/user/LoginFormContainer.mjs,apps/realworld2/view/user/SettingsFormContainer.mjs,apps/realworld2/view/article/DetailsContainer.mjs,apps/realworld2/view/article/FormContainer.mjs. I did notlaunch each app to compare before/after screenshots — only a form whose app creates some other TextField-family
field before that form (so it was accidentally getting 10px today) would render differently, and the change
makes it 5px either way. I could not determine from a static read which (if any) of these apps hit that order,
so I'm flagging it rather than asserting none of them change.
Generated by Claude Code.