"use client" import * as React from "react" import { cn } from "../../lib/utils" /** * `Spacer` is replacement for `@wordpress/components`' `` - the largest single * WordPress dependency in the plugin at **1,720 live usages across 196 files**, bigger than * dropdown-menu + disabled + tab-panel combined. * * IT DOES NOT DO WHAT IT LOOKS LIKE, AND THAT IS THE ENTIRE REASON THIS FILE EXISTS. * `` does **not** render `margin: 24px`. WP's hook destructures * `marginBottom = 2` (`@wordpress/components/build/spacer/hook.js:26`), so the bottom rule is ALWAYS * emitted, and it is composed AFTER the shorthand: * * margin -> marginY -> marginX -> marginTop -> marginBottom -> marginLeft -> marginRight * * the real geometry of our dominant call site is **24px top/left/right and 8px bottom**. * A naive Tailwind conversion (`m-6`, `my-6`) would be wrong on all 1,720 sites, silently tripling * every bottom gap in the clinical forms, and no test in this repo would have caught it. That is the * "load-bearing styling lost in a refactor" failure exists to prevent - so this is an * ADAPTER that reproduces the geometry exactly, and adopting it is a one-line import swap. * * The second trap: the two prop shapes in our codebase mean different things. `space()` returns a * CSS-valid value verbatim but multiplies a bare number by a 4px grid base, so `margin={'24px'}` is * 24px while `margin="8"` is **32px**. Both shapes are live in the tree; do not "tidy" one into the * other. * * Why four longhands rather than a shorthand plus an override * The rendered geometry is identical, but resolving to longhands removes any dependence on CSSOM * shorthand-override semantics agreeing between jsdom and Chrome, and it puts the defaulting rule in * `resolveSpacerMargins`, a pure function that is exhaustively tested. See `__tests__/spacer.test.tsx`. * * Children are deliberately NOT accepted. All 1,720 call sites are self-closing, and typing them * out means passing children is a compile error rather than content that silently disappears. * Padding props are likewise not reproduced: nothing uses them, and `padding` is not a valid div * attribute, so reaching for one is a type error rather than a silently ignored prop. */ const GRID_BASE = "4px" export type SpaceValue = number | string | undefined /** WP's `isDefined` - `null` is as good as absent, and must emit no declaration. */ const isDefined = (value: T): value is Exclude => typeof value !== "undefined" && value !== null /** * Reproduces `@wordpress/components/build/utils/space.js` verbatim. * * - `undefined` -> `undefined` (no declaration) * - falsy (`0`, `''`) -> `'0'` - short-circuits BEFORE the grid multiplication * - a value CSS accepts, or a NaN one -> returned verbatim (`'24px'`, `'auto'`) * - anything else numeric -> `calc(4px * n)` - so `margin="8"` is 32px * * The `CSS.supports` probe is WP's, kept so browser behaviour is identical. jsdom does not implement * it, but the `Number.isNaN` arm reaches the same answer for every value shape in our tree. */ export function space(value: SpaceValue): string | undefined { if (typeof value === "undefined") { return undefined } // Handle empty strings; if it's the number 0 this still works. if (!value) { return "0" } const asInt = typeof value === "number" ? value : Number(value) if ( (typeof window !== "undefined" && window.CSS?.supports?.("margin", value.toString())) || Number.isNaN(asInt) ) { return value.toString() } return `calc(${GRID_BASE} * ${value})` } export interface SpacerMarginProps { margin?: SpaceValue marginX?: SpaceValue marginY?: SpaceValue marginTop?: SpaceValue /** Defaults to `2` (8px) exactly as WP does, and overrides `margin`'s bottom edge. */ marginBottom?: SpaceValue marginLeft?: SpaceValue marginRight?: SpaceValue } export interface ResolvedSpacerMargins { marginTop: string | undefined marginRight: string | undefined marginBottom: string | undefined marginLeft: string | undefined } /** * Collapses WP's seven margin props into the four edges they actually resolve to, applying its * composition order. `marginBottom` carries WP's `= 2` default, which is why the bottom edge is * almost never what the caller's `margin` says. */ export function resolveSpacerMargins({ margin, marginX, marginY, marginTop, marginBottom = 2, marginLeft, marginRight, }: SpacerMarginProps): ResolvedSpacerMargins { const edge = (...candidates: SpaceValue[]): string | undefined => { const winner = candidates.find(isDefined) return isDefined(winner) ? space(winner) : undefined } return { marginTop: edge(marginTop, marginY, margin), marginRight: edge(marginRight, marginX, margin), marginBottom: edge(marginBottom), marginLeft: edge(marginLeft, marginX, margin), } } type SpacerProps = Omit, "children"> & SpacerMarginProps function Spacer({ margin, marginX, marginY, marginTop, marginBottom, marginLeft, marginRight, className, style, ...props }: SpacerProps) { const margins = resolveSpacerMargins({ margin, marginX, marginY, marginTop, marginBottom, marginLeft, marginRight, }) return (
) } export { Spacer }