diff --git a/.changeset/field-group-targeted-selectors.md b/.changeset/field-group-targeted-selectors.md new file mode 100644 index 00000000..73fccef9 --- /dev/null +++ b/.changeset/field-group-targeted-selectors.md @@ -0,0 +1,5 @@ +--- +"@styleframe/theme": patch +--- + +Fix field-group horizontal/vertical border joining to target known control classes (`.input`, `.textarea`, `.select`, `.button`, `.dropdown`) instead of all children (`*`), using `:where`/`:is` for correct specificity. diff --git a/theme/src/recipes/field-group/useFieldGroupRecipe.test.ts b/theme/src/recipes/field-group/useFieldGroupRecipe.test.ts index e88884d5..9c4d13c6 100644 --- a/theme/src/recipes/field-group/useFieldGroupRecipe.test.ts +++ b/theme/src/recipes/field-group/useFieldGroupRecipe.test.ts @@ -144,6 +144,9 @@ describe("useFieldGroupRecipe", () => { }); describe("seam selectors", () => { + const C = `:where(.input, .textarea, .select, .button, .dropdown)`; + const K = `:is(.input, .textarea, .select, .button, .dropdown)`; + it("flattens horizontal seams and grows fields", () => { const s = createInstance(); useFieldGroupRecipe(s); @@ -152,10 +155,10 @@ describe("useFieldGroupRecipe", () => { expect(horizontal).toBeDefined(); expect( - findChildRule(horizontal, "& > *:not(:last-child)")?.declarations, + findChildRule(horizontal, `& > ${C}:has(~ ${K})`)?.declarations, ).toMatchObject({ borderRightWidth: "0" }); expect( - findChildRule(horizontal, "& > *:not(:first-child)")?.declarations, + findChildRule(horizontal, `& > ${C} ~ ${K}`)?.declarations, ).toMatchObject({ borderTopLeftRadius: "0" }); for (const query of ["& > .input", "& > .select", "& > .textarea"]) { @@ -174,10 +177,10 @@ describe("useFieldGroupRecipe", () => { expect(vertical).toBeDefined(); expect( - findChildRule(vertical, "& > *:not(:last-child)")?.declarations, + findChildRule(vertical, `& > ${C}:has(~ ${K})`)?.declarations, ).toMatchObject({ borderBottomWidth: "0" }); expect( - findChildRule(vertical, "& > *:not(:first-child)")?.declarations, + findChildRule(vertical, `& > ${C} ~ ${K}`)?.declarations, ).toMatchObject({ borderTopLeftRadius: "0" }); }); diff --git a/theme/src/recipes/field-group/useFieldGroupRecipe.ts b/theme/src/recipes/field-group/useFieldGroupRecipe.ts index 53a3f5af..3ffe7a0c 100644 --- a/theme/src/recipes/field-group/useFieldGroupRecipe.ts +++ b/theme/src/recipes/field-group/useFieldGroupRecipe.ts @@ -62,14 +62,26 @@ export const useFieldGroupRecipe = createUseRecipe( (s) => { const { selector } = s; + const fieldGroupChildren = [ + ".input", + ".textarea", + ".select", + ".button", + ".dropdown", + ]; + const C = `:where(${fieldGroupChildren.join(", ")})`; // subject anchor, 0 specificity + const K = `:is(${fieldGroupChildren.join(", ")})`; // adjacency key, (0,1,0) specificity + // Horizontal: join controls side-by-side and let fields take the slack. selector(".field-group.-horizontal", { - "& > *:not(:last-child)": { + // non-last control (has a following control) → merge right edge into the next + [`& > ${C}:has(~ ${K})`]: { borderTopRightRadius: "0", borderBottomRightRadius: "0", borderRightWidth: "0", }, - "& > *:not(:first-child)": { + // non-first control (preceded by a control) → square the left edge + [`& > ${C} ~ ${K}`]: { borderTopLeftRadius: "0", borderBottomLeftRadius: "0", }, @@ -80,12 +92,12 @@ export const useFieldGroupRecipe = createUseRecipe( // Vertical: join controls top-to-bottom. selector(".field-group.-vertical", { - "& > *:not(:last-child)": { + [`& > ${C}:has(~ ${K})`]: { borderBottomLeftRadius: "0", borderBottomRightRadius: "0", borderBottomWidth: "0", }, - "& > *:not(:first-child)": { + [`& > ${C} ~ ${K}`]: { borderTopLeftRadius: "0", borderTopRightRadius: "0", },