spacing is three bands, not one gutter
Top Nav, Sections and Footer each get their own horizontal padding, plus the one space that band owns: the room under the rule, the gap between bands, the room above the footer. The preview marker now names its band, so a change moves one band and not all three. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+56
-16
@@ -575,38 +575,78 @@ export function paintMeasure(document: Document): () => void {
|
||||
}
|
||||
|
||||
/**
|
||||
* The site's gutter, applied to a rendered page before it is saved.
|
||||
* The site's spacing, applied to a rendered page before it is saved.
|
||||
*
|
||||
* ── Why this can be classes and not variables ──
|
||||
*
|
||||
* Colours and typefaces go into the frame as CSS custom properties, because that is what the page reads
|
||||
* them as. A gutter is Tailwind classes — `px-4 md:px-8` — because only a class can carry a breakpoint,
|
||||
* them as. Spacing is Tailwind classes — `px-4 md:px-8` — because only a class can carry a breakpoint,
|
||||
* and there is no variable to set instead.
|
||||
*
|
||||
* That works here for one reason: the classes are in the SITE's stylesheet, which the frame already
|
||||
* has. The site safelists every step (`@source inline` in styles.css), so any value this writes is one
|
||||
* the frame can already render. A class invented here would silently do nothing.
|
||||
*
|
||||
* ── Only gutter classes are touched ──
|
||||
* ── Named bands, and why that replaced one gutter ──
|
||||
*
|
||||
* The elements carrying `data-gutter` hold the gutter and nothing else that looks like it: a section's
|
||||
* own padding lands on the <section>, and the band padding on <header>. So removing everything matching
|
||||
* `px-N` from these three is safe, where doing it to an arbitrary element would not be.
|
||||
* Every marked element used to take the same `px-*`. Now the header, the bands and the footer each have
|
||||
* their own, so the marker carries which one it is — `data-gutter="header"`, `data-space="footer"` —
|
||||
* and this rewrites one band without touching the other two.
|
||||
*
|
||||
* ── Only the matching utility is touched ──
|
||||
*
|
||||
* Each target has a pattern naming exactly what this may remove: `px-*` from a gutter, `mb-*` from the
|
||||
* header, `space-y-*` from the bands. Everything else on the element survives, which is what makes it
|
||||
* safe to do this to a `<header>` carrying a border and a background as well.
|
||||
*
|
||||
* The one exception is a SECTION, whose own padding is also a `px-*` on the marked element — so while
|
||||
* the panel is open its padding reads as the site gutter. Harmless on the layout page, whose bands have
|
||||
* none, and that page is the only one this runs against.
|
||||
*/
|
||||
export function applyGutter(document: Document, classes: string): () => void {
|
||||
const targets = [...document.querySelectorAll<HTMLElement>('[data-gutter]')];
|
||||
const before = targets.map((element) => element.className);
|
||||
export type SpacingDraft = {
|
||||
header?: { paddingX?: Steps; marginBottom?: Steps };
|
||||
sections?: { paddingX?: Steps; gap?: Steps };
|
||||
footer?: { paddingX?: Steps; marginTop?: Steps };
|
||||
};
|
||||
|
||||
const GUTTER = /^(?:md:|lg:)?px-\d+$/;
|
||||
export type Steps = Partial<Record<'base' | 'md' | 'lg', number>>;
|
||||
|
||||
for (const element of targets) {
|
||||
const kept = element.className.split(/\s+/).filter((name) => name && !GUTTER.test(name));
|
||||
element.className = [...kept, ...classes.split(/\s+/).filter(Boolean)].join(' ');
|
||||
/** Mirrors `scale` in the site's layout-settings.ts. Same reason as themeProperties: a copy that shows
|
||||
* a change before it is saved. If the live preview and the saved page disagree, this one is wrong. */
|
||||
export const scaleClasses = (utility: string, steps: Steps | undefined): string => {
|
||||
if (!steps) return '';
|
||||
const prefix = { base: '', md: 'md:', lg: 'lg:' } as const;
|
||||
return (['base', 'md', 'lg'] as const)
|
||||
.filter((breakpoint) => typeof steps[breakpoint] === 'number')
|
||||
.map((breakpoint) => `${prefix[breakpoint]}${utility}-${steps[breakpoint]}`)
|
||||
.join(' ');
|
||||
};
|
||||
|
||||
/** One selector, the utility it owns, and the classes to put there. */
|
||||
const TARGETS = [
|
||||
{ selector: '[data-gutter="header"]', utility: 'px', read: (s: SpacingDraft) => s.header?.paddingX },
|
||||
{ selector: '[data-gutter="sections"]', utility: 'px', read: (s: SpacingDraft) => s.sections?.paddingX },
|
||||
{ selector: '[data-gutter="footer"]', utility: 'px', read: (s: SpacingDraft) => s.footer?.paddingX },
|
||||
{ selector: '[data-space="header"]', utility: 'mb', read: (s: SpacingDraft) => s.header?.marginBottom },
|
||||
{ selector: '[data-space="sections"]', utility: 'space-y', read: (s: SpacingDraft) => s.sections?.gap },
|
||||
{ selector: '[data-space="footer"]', utility: 'mt', read: (s: SpacingDraft) => s.footer?.marginTop },
|
||||
] as const;
|
||||
|
||||
export function applySpacing(document: Document, spacing: SpacingDraft): () => void {
|
||||
const touched: Array<[HTMLElement, string]> = [];
|
||||
|
||||
for (const target of TARGETS) {
|
||||
const pattern = new RegExp(`^(?:md:|lg:)?${target.utility}-\\d+$`);
|
||||
const classes = scaleClasses(target.utility, target.read(spacing)).split(/\s+/).filter(Boolean);
|
||||
|
||||
for (const element of document.querySelectorAll<HTMLElement>(target.selector)) {
|
||||
touched.push([element, element.className]);
|
||||
const kept = element.className.split(/\s+/).filter((name) => name && !pattern.test(name));
|
||||
element.className = [...kept, ...classes].join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
targets.forEach((element, index) => {
|
||||
element.className = before[index]!;
|
||||
});
|
||||
for (const [element, before] of touched) element.className = before;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user