Skip to content

Commit 41bb375

Browse files
committed
feat: add custom spacing support to layout positioning utilities
Support custom spacing values for all positioning utilities: - top-*, right-*, bottom-*, left-* - start-*, end-* (RTL-aware) - inset-*, inset-x-*, inset-y-*, inset-s-*, inset-e-* Custom spacing merges with defaults and can override preset values.
1 parent 8b75010 commit 41bb375

File tree

3 files changed

+117
-17
lines changed

3 files changed

+117
-17
lines changed

src/parser/index.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export type CustomTheme = {
2121
colors?: Record<string, string>;
2222
fontFamily?: Record<string, string>;
2323
fontSize?: Record<string, number>;
24+
spacing?: Record<string, number>;
2425
};
2526

2627
/**
@@ -50,17 +51,17 @@ export function parseClassName(className: string, customTheme?: CustomTheme): St
5051
export function parseClass(cls: string, customTheme?: CustomTheme): StyleObject {
5152
// Try each parser in order
5253
// Note: parseBorder must come before parseColor to avoid border-[3px] being parsed as a color
53-
// parseBorder, parseColor and parseTypography get custom theme
54+
// Parsers receive relevant custom theme properties
5455
const parsers: Array<(cls: string) => StyleObject | null> = [
55-
parseSpacing,
56+
(cls: string) => parseSpacing(cls, customTheme?.spacing),
5657
(cls: string) => parseBorder(cls, customTheme?.colors),
5758
(cls: string) => parseColor(cls, customTheme?.colors),
58-
parseLayout,
59+
(cls: string) => parseLayout(cls, customTheme?.spacing),
5960
(cls: string) => parseTypography(cls, customTheme?.fontFamily, customTheme?.fontSize),
60-
parseSizing,
61+
(cls: string) => parseSizing(cls, customTheme?.spacing),
6162
parseShadow,
6263
parseAspectRatio,
63-
parseTransform,
64+
(cls: string) => parseTransform(cls, customTheme?.spacing),
6465
];
6566

6667
for (const parser of parsers) {

src/parser/layout.test.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,3 +719,97 @@ describe("parseLayout - logical inset (RTL-aware)", () => {
719719
expect(parseLayout("inset-e-[20%]")).toEqual({ end: "20%" });
720720
});
721721
});
722+
723+
describe("parseLayout - custom spacing", () => {
724+
const customSpacing = {
725+
xs: 4,
726+
sm: 8,
727+
md: 16,
728+
lg: 32,
729+
xl: 64,
730+
"4": 20, // Override default (16)
731+
};
732+
733+
it("should support custom spacing values for top/right/bottom/left", () => {
734+
expect(parseLayout("top-xs", customSpacing)).toEqual({ top: 4 });
735+
expect(parseLayout("top-sm", customSpacing)).toEqual({ top: 8 });
736+
expect(parseLayout("top-md", customSpacing)).toEqual({ top: 16 });
737+
expect(parseLayout("top-lg", customSpacing)).toEqual({ top: 32 });
738+
expect(parseLayout("top-xl", customSpacing)).toEqual({ top: 64 });
739+
740+
expect(parseLayout("right-xs", customSpacing)).toEqual({ right: 4 });
741+
expect(parseLayout("bottom-sm", customSpacing)).toEqual({ bottom: 8 });
742+
expect(parseLayout("left-md", customSpacing)).toEqual({ left: 16 });
743+
});
744+
745+
it("should override default spacing values", () => {
746+
// Without custom spacing, top-4 is 16
747+
expect(parseLayout("top-4")).toEqual({ top: 16 });
748+
749+
// With custom spacing, top-4 is 20 (overridden)
750+
expect(parseLayout("top-4", customSpacing)).toEqual({ top: 20 });
751+
});
752+
753+
it("should merge custom spacing with defaults", () => {
754+
// Custom spacing should merge with defaults
755+
// top-0 should still work (from defaults)
756+
expect(parseLayout("top-0", customSpacing)).toEqual({ top: 0 });
757+
758+
// top-8 should still work (from defaults, not overridden)
759+
expect(parseLayout("top-8", customSpacing)).toEqual({ top: 32 });
760+
761+
// top-xs should work (from custom)
762+
expect(parseLayout("top-xs", customSpacing)).toEqual({ top: 4 });
763+
});
764+
765+
it("should support custom spacing for start/end (RTL-aware)", () => {
766+
expect(parseLayout("start-xs", customSpacing)).toEqual({ start: 4 });
767+
expect(parseLayout("end-lg", customSpacing)).toEqual({ end: 32 });
768+
});
769+
770+
it("should support custom spacing for inset utilities", () => {
771+
expect(parseLayout("inset-xs", customSpacing)).toEqual({
772+
top: 4,
773+
right: 4,
774+
bottom: 4,
775+
left: 4,
776+
});
777+
778+
expect(parseLayout("inset-x-sm", customSpacing)).toEqual({
779+
left: 8,
780+
right: 8,
781+
});
782+
783+
expect(parseLayout("inset-y-md", customSpacing)).toEqual({
784+
top: 16,
785+
bottom: 16,
786+
});
787+
});
788+
789+
it("should support custom spacing for inset-s/inset-e", () => {
790+
expect(parseLayout("inset-s-lg", customSpacing)).toEqual({ start: 32 });
791+
expect(parseLayout("inset-e-xl", customSpacing)).toEqual({ end: 64 });
792+
});
793+
794+
it("should support negative values with custom spacing for start/end", () => {
795+
// Note: -top-*, -left-*, -right-*, -bottom-* negative prefixes are not supported
796+
// Use arbitrary values like top-[-10px] for negative positioning
797+
expect(parseLayout("-start-sm", customSpacing)).toEqual({ start: -8 });
798+
expect(parseLayout("-end-md", customSpacing)).toEqual({ end: -16 });
799+
});
800+
801+
it("should still support arbitrary values with custom spacing", () => {
802+
expect(parseLayout("top-[100px]", customSpacing)).toEqual({ top: 100 });
803+
expect(parseLayout("inset-[50%]", customSpacing)).toEqual({
804+
top: "50%",
805+
right: "50%",
806+
bottom: "50%",
807+
left: "50%",
808+
});
809+
});
810+
811+
it("should handle non-layout classes as null", () => {
812+
expect(parseLayout("text-xl", customSpacing)).toBeNull();
813+
expect(parseLayout("bg-blue-500", customSpacing)).toBeNull();
814+
});
815+
});

src/parser/layout.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,13 @@ export const INSET_SCALE: Record<string, number> = {
237237

238238
/**
239239
* Parse layout classes
240+
* @param cls - The class name to parse
241+
* @param customSpacing - Optional custom spacing values from tailwind.config (for inset utilities)
240242
*/
241-
export function parseLayout(cls: string): StyleObject | null {
243+
export function parseLayout(cls: string, customSpacing?: Record<string, number>): StyleObject | null {
244+
// Merge custom spacing with defaults for inset utilities
245+
const insetMap = customSpacing ? { ...INSET_SCALE, ...customSpacing } : INSET_SCALE;
246+
242247
// Z-index: z-0, z-10, z-20, z-[999], etc.
243248
if (cls.startsWith("z-")) {
244249
const zKey = cls.substring(2);
@@ -270,7 +275,7 @@ export function parseLayout(cls: string): StyleObject | null {
270275
return { top: arbitraryTop };
271276
}
272277

273-
const topValue = INSET_SCALE[topKey];
278+
const topValue = insetMap[topKey];
274279
if (topValue !== undefined) {
275280
return { top: topValue };
276281
}
@@ -291,7 +296,7 @@ export function parseLayout(cls: string): StyleObject | null {
291296
return { right: arbitraryRight };
292297
}
293298

294-
const rightValue = INSET_SCALE[rightKey];
299+
const rightValue = insetMap[rightKey];
295300
if (rightValue !== undefined) {
296301
return { right: rightValue };
297302
}
@@ -312,7 +317,7 @@ export function parseLayout(cls: string): StyleObject | null {
312317
return { bottom: arbitraryBottom };
313318
}
314319

315-
const bottomValue = INSET_SCALE[bottomKey];
320+
const bottomValue = insetMap[bottomKey];
316321
if (bottomValue !== undefined) {
317322
return { bottom: bottomValue };
318323
}
@@ -333,7 +338,7 @@ export function parseLayout(cls: string): StyleObject | null {
333338
return { left: arbitraryLeft };
334339
}
335340

336-
const leftValue = INSET_SCALE[leftKey];
341+
const leftValue = insetMap[leftKey];
337342
if (leftValue !== undefined) {
338343
return { left: leftValue };
339344
}
@@ -364,7 +369,7 @@ export function parseLayout(cls: string): StyleObject | null {
364369
return { start: arbitraryStart };
365370
}
366371

367-
const startValue = INSET_SCALE[startKey];
372+
const startValue = insetMap[startKey];
368373
if (startValue !== undefined) {
369374
return { start: isNegative ? -startValue : startValue };
370375
}
@@ -395,7 +400,7 @@ export function parseLayout(cls: string): StyleObject | null {
395400
return { end: arbitraryEnd };
396401
}
397402

398-
const endValue = INSET_SCALE[endKey];
403+
const endValue = insetMap[endKey];
399404
if (endValue !== undefined) {
400405
return { end: isNegative ? -endValue : endValue };
401406
}
@@ -411,7 +416,7 @@ export function parseLayout(cls: string): StyleObject | null {
411416
return { left: arbitraryInset, right: arbitraryInset };
412417
}
413418

414-
const insetValue = INSET_SCALE[insetKey];
419+
const insetValue = insetMap[insetKey];
415420
if (insetValue !== undefined) {
416421
return { left: insetValue, right: insetValue };
417422
}
@@ -427,7 +432,7 @@ export function parseLayout(cls: string): StyleObject | null {
427432
return { top: arbitraryInset, bottom: arbitraryInset };
428433
}
429434

430-
const insetValue = INSET_SCALE[insetKey];
435+
const insetValue = insetMap[insetKey];
431436
if (insetValue !== undefined) {
432437
return { top: insetValue, bottom: insetValue };
433438
}
@@ -443,7 +448,7 @@ export function parseLayout(cls: string): StyleObject | null {
443448
return { start: arbitraryInset };
444449
}
445450

446-
const insetValue = INSET_SCALE[insetKey];
451+
const insetValue = insetMap[insetKey];
447452
if (insetValue !== undefined) {
448453
return { start: insetValue };
449454
}
@@ -459,7 +464,7 @@ export function parseLayout(cls: string): StyleObject | null {
459464
return { end: arbitraryInset };
460465
}
461466

462-
const insetValue = INSET_SCALE[insetKey];
467+
const insetValue = insetMap[insetKey];
463468
if (insetValue !== undefined) {
464469
return { end: insetValue };
465470
}
@@ -475,7 +480,7 @@ export function parseLayout(cls: string): StyleObject | null {
475480
return { top: arbitraryInset, right: arbitraryInset, bottom: arbitraryInset, left: arbitraryInset };
476481
}
477482

478-
const insetValue = INSET_SCALE[insetKey];
483+
const insetValue = insetMap[insetKey];
479484
if (insetValue !== undefined) {
480485
return { top: insetValue, right: insetValue, bottom: insetValue, left: insetValue };
481486
}

0 commit comments

Comments
 (0)