Skip to content

Commit 8795062

Browse files
committed
feat: add negative prefix support for positioning utilities
Add support for negative prefixes on top, right, bottom, and left positioning utilities (e.g., -top-4, -right-2, -bottom-8, -left-1). This brings these utilities in line with start/end positioning which already supported negative prefixes. Works with standard spacing scale, fractional values, arbitrary values, and custom spacing from tailwind config. Examples: - -top-4 → { top: -16 } - -right-0.5 → { right: -2 } - -bottom-[20px] → { bottom: -20 } - -left-[50%] → { left: "-50%" }
1 parent b935b80 commit 8795062

File tree

2 files changed

+98
-23
lines changed

2 files changed

+98
-23
lines changed

src/parser/layout.test.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,39 @@ describe("parseLayout - positioning utilities", () => {
521521
expect(parseLayout("left-[12.5%]")).toEqual({ left: "12.5%" });
522522
expect(parseLayout("left-[-20px]")).toEqual({ left: -20 });
523523
});
524+
525+
it("should parse negative positioning prefixes", () => {
526+
// Standard spacing scale
527+
expect(parseLayout("-top-1")).toEqual({ top: -4 });
528+
expect(parseLayout("-top-4")).toEqual({ top: -16 });
529+
expect(parseLayout("-right-1")).toEqual({ right: -4 });
530+
expect(parseLayout("-right-2")).toEqual({ right: -8 });
531+
expect(parseLayout("-bottom-4")).toEqual({ bottom: -16 });
532+
expect(parseLayout("-bottom-8")).toEqual({ bottom: -32 });
533+
expect(parseLayout("-left-2")).toEqual({ left: -8 });
534+
expect(parseLayout("-left-4")).toEqual({ left: -16 });
535+
});
536+
537+
it("should parse negative positioning with fractional values", () => {
538+
expect(parseLayout("-top-0.5")).toEqual({ top: -2 });
539+
expect(parseLayout("-right-1.5")).toEqual({ right: -6 });
540+
expect(parseLayout("-bottom-2.5")).toEqual({ bottom: -10 });
541+
expect(parseLayout("-left-3.5")).toEqual({ left: -14 });
542+
});
543+
544+
it("should parse negative positioning with arbitrary values", () => {
545+
expect(parseLayout("-top-[10px]")).toEqual({ top: -10 });
546+
expect(parseLayout("-right-[20px]")).toEqual({ right: -20 });
547+
expect(parseLayout("-bottom-[30]")).toEqual({ bottom: -30 });
548+
expect(parseLayout("-left-[40px]")).toEqual({ left: -40 });
549+
});
550+
551+
it("should parse negative positioning with arbitrary percentages", () => {
552+
expect(parseLayout("-top-[10%]")).toEqual({ top: "-10%" });
553+
expect(parseLayout("-right-[25%]")).toEqual({ right: "-25%" });
554+
expect(parseLayout("-bottom-[50%]")).toEqual({ bottom: "-50%" });
555+
expect(parseLayout("-left-[100%]")).toEqual({ left: "-100%" });
556+
});
524557
});
525558

526559
describe("parseLayout - inset utilities", () => {
@@ -791,9 +824,11 @@ describe("parseLayout - custom spacing", () => {
791824
expect(parseLayout("inset-e-xl", customSpacing)).toEqual({ end: 64 });
792825
});
793826

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
827+
it("should support negative values with custom spacing for positioning", () => {
828+
expect(parseLayout("-top-sm", customSpacing)).toEqual({ top: -8 });
829+
expect(parseLayout("-right-md", customSpacing)).toEqual({ right: -16 });
830+
expect(parseLayout("-bottom-lg", customSpacing)).toEqual({ bottom: -32 });
831+
expect(parseLayout("-left-xl", customSpacing)).toEqual({ left: -64 });
797832
expect(parseLayout("-start-sm", customSpacing)).toEqual({ start: -8 });
798833
expect(parseLayout("-end-md", customSpacing)).toEqual({ end: -16 });
799834
});

src/parser/layout.ts

Lines changed: 60 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -260,87 +260,127 @@ export function parseLayout(cls: string, customSpacing?: Record<string, number>)
260260
}
261261
}
262262

263-
// Top positioning: top-0, top-4, top-[10px], top-[50%], etc.
264-
if (cls.startsWith("top-")) {
265-
const topKey = cls.substring(4);
263+
// Top positioning: top-0, top-4, top-[10px], top-[50%], -top-4, etc.
264+
const topMatch = cls.match(/^(-?)top-(.+)$/);
265+
if (topMatch) {
266+
const [, negPrefix, topKey] = topMatch;
267+
const isNegative = negPrefix === "-";
266268

267269
// Auto value - return empty object (no-op, removes the property)
268270
if (topKey === "auto") {
269271
return {};
270272
}
271273

272-
// Arbitrary values: top-[123px], top-[50%], top-[-10px]
274+
// Arbitrary values: top-[123px], top-[50%], -top-[10px]
273275
const arbitraryTop = parseArbitraryInset(topKey);
274276
if (arbitraryTop !== null) {
277+
if (typeof arbitraryTop === "number") {
278+
return { top: isNegative ? -arbitraryTop : arbitraryTop };
279+
}
280+
// Percentage values with negative prefix
281+
if (isNegative && arbitraryTop.endsWith("%")) {
282+
const numValue = parseFloat(arbitraryTop);
283+
return { top: `${-numValue}%` };
284+
}
275285
return { top: arbitraryTop };
276286
}
277287

278288
const topValue = insetMap[topKey];
279289
if (topValue !== undefined) {
280-
return { top: topValue };
290+
return { top: isNegative ? -topValue : topValue };
281291
}
282292
}
283293

284-
// Right positioning: right-0, right-4, right-[10px], right-[50%], etc.
285-
if (cls.startsWith("right-")) {
286-
const rightKey = cls.substring(6);
294+
// Right positioning: right-0, right-4, right-[10px], right-[50%], -right-4, etc.
295+
const rightMatch = cls.match(/^(-?)right-(.+)$/);
296+
if (rightMatch) {
297+
const [, negPrefix, rightKey] = rightMatch;
298+
const isNegative = negPrefix === "-";
287299

288300
// Auto value - return empty object (no-op, removes the property)
289301
if (rightKey === "auto") {
290302
return {};
291303
}
292304

293-
// Arbitrary values: right-[123px], right-[50%], right-[-10px]
305+
// Arbitrary values: right-[123px], right-[50%], -right-[10px]
294306
const arbitraryRight = parseArbitraryInset(rightKey);
295307
if (arbitraryRight !== null) {
308+
if (typeof arbitraryRight === "number") {
309+
return { right: isNegative ? -arbitraryRight : arbitraryRight };
310+
}
311+
// Percentage values with negative prefix
312+
if (isNegative && arbitraryRight.endsWith("%")) {
313+
const numValue = parseFloat(arbitraryRight);
314+
return { right: `${-numValue}%` };
315+
}
296316
return { right: arbitraryRight };
297317
}
298318

299319
const rightValue = insetMap[rightKey];
300320
if (rightValue !== undefined) {
301-
return { right: rightValue };
321+
return { right: isNegative ? -rightValue : rightValue };
302322
}
303323
}
304324

305-
// Bottom positioning: bottom-0, bottom-4, bottom-[10px], bottom-[50%], etc.
306-
if (cls.startsWith("bottom-")) {
307-
const bottomKey = cls.substring(7);
325+
// Bottom positioning: bottom-0, bottom-4, bottom-[10px], bottom-[50%], -bottom-4, etc.
326+
const bottomMatch = cls.match(/^(-?)bottom-(.+)$/);
327+
if (bottomMatch) {
328+
const [, negPrefix, bottomKey] = bottomMatch;
329+
const isNegative = negPrefix === "-";
308330

309331
// Auto value - return empty object (no-op, removes the property)
310332
if (bottomKey === "auto") {
311333
return {};
312334
}
313335

314-
// Arbitrary values: bottom-[123px], bottom-[50%], bottom-[-10px]
336+
// Arbitrary values: bottom-[123px], bottom-[50%], -bottom-[10px]
315337
const arbitraryBottom = parseArbitraryInset(bottomKey);
316338
if (arbitraryBottom !== null) {
339+
if (typeof arbitraryBottom === "number") {
340+
return { bottom: isNegative ? -arbitraryBottom : arbitraryBottom };
341+
}
342+
// Percentage values with negative prefix
343+
if (isNegative && arbitraryBottom.endsWith("%")) {
344+
const numValue = parseFloat(arbitraryBottom);
345+
return { bottom: `${-numValue}%` };
346+
}
317347
return { bottom: arbitraryBottom };
318348
}
319349

320350
const bottomValue = insetMap[bottomKey];
321351
if (bottomValue !== undefined) {
322-
return { bottom: bottomValue };
352+
return { bottom: isNegative ? -bottomValue : bottomValue };
323353
}
324354
}
325355

326-
// Left positioning: left-0, left-4, left-[10px], left-[50%], etc.
327-
if (cls.startsWith("left-")) {
328-
const leftKey = cls.substring(5);
356+
// Left positioning: left-0, left-4, left-[10px], left-[50%], -left-4, etc.
357+
const leftMatch = cls.match(/^(-?)left-(.+)$/);
358+
if (leftMatch) {
359+
const [, negPrefix, leftKey] = leftMatch;
360+
const isNegative = negPrefix === "-";
329361

330362
// Auto value - return empty object (no-op, removes the property)
331363
if (leftKey === "auto") {
332364
return {};
333365
}
334366

335-
// Arbitrary values: left-[123px], left-[50%], left-[-10px]
367+
// Arbitrary values: left-[123px], left-[50%], -left-[10px]
336368
const arbitraryLeft = parseArbitraryInset(leftKey);
337369
if (arbitraryLeft !== null) {
370+
if (typeof arbitraryLeft === "number") {
371+
return { left: isNegative ? -arbitraryLeft : arbitraryLeft };
372+
}
373+
// Percentage values with negative prefix
374+
if (isNegative && arbitraryLeft.endsWith("%")) {
375+
const numValue = parseFloat(arbitraryLeft);
376+
return { left: `${-numValue}%` };
377+
}
338378
return { left: arbitraryLeft };
339379
}
340380

341381
const leftValue = insetMap[leftKey];
342382
if (leftValue !== undefined) {
343-
return { left: leftValue };
383+
return { left: isNegative ? -leftValue : leftValue };
344384
}
345385
}
346386

0 commit comments

Comments
 (0)