diff --git a/packages/core/src/__tests__/api.json b/packages/core/src/__tests__/api.json index c5dcde921..7e88e3e43 100644 --- a/packages/core/src/__tests__/api.json +++ b/packages/core/src/__tests__/api.json @@ -660,5 +660,28 @@ "@media (min-width: 768px){.md\\:rotate-3{--tw-rotate:3deg;transform:rotate(3deg);transform:translateX(var(--tw-translate-x,0)) translateY(var(--tw-translate-y,0)) rotate(var(--tw-rotate,0)) skewX(var(--tw-skew-x,0)) skewY(var(--tw-skew-y,0)) scaleX(var(--tw-scale-x,1)) scaleY(var(--tw-scale-y,1))}}", "@media (min-width: 768px){.md\\:hover\\:-rotate-6:hover{--tw-rotate:calc(6deg * -1);transform:rotate(calc(6deg * -1));transform:translateX(var(--tw-translate-x,0)) translateY(var(--tw-translate-y,0)) rotate(var(--tw-rotate,0)) skewX(var(--tw-skew-x,0)) skewY(var(--tw-skew-y,0)) scaleX(var(--tw-scale-x,1)) scaleY(var(--tw-scale-y,1))}}" ] + ], + "container": [ + "container", + [ + ".container{width:100%}", + "@media (min-width: 640px){.container{max-width:640px}}", + "@media (min-width: 768px){.container{max-width:768px}}", + "@media (min-width: 1024px){.container{max-width:1024px}}", + "@media (min-width: 1280px){.container{max-width:1280px}}", + "@media (min-width: 1536px){.container{max-width:1536px}}" + ] + ], + "md:(container mx-auto)": [ + "md:container md:mx-auto", + [ + "@media (min-width: 768px){.md\\:mx-auto{margin-left:auto;margin-right:auto}}", + "@media (min-width: 768px){.md\\:container{width:100%}}", + "@media (min-width: 768px){@media (min-width: 640px){.md\\:container{max-width:640px}}}", + "@media (min-width: 768px){@media (min-width: 768px){.md\\:container{max-width:768px}}}", + "@media (min-width: 768px){@media (min-width: 1024px){.md\\:container{max-width:1024px}}}", + "@media (min-width: 768px){@media (min-width: 1280px){.md\\:container{max-width:1280px}}}", + "@media (min-width: 768px){@media (min-width: 1536px){.md\\:container{max-width:1536px}}}" + ] ] } diff --git a/packages/core/src/__tests__/api.test.ts b/packages/core/src/__tests__/api.test.ts index 121036d53..e248c03e6 100644 --- a/packages/core/src/__tests__/api.test.ts +++ b/packages/core/src/__tests__/api.test.ts @@ -220,6 +220,90 @@ test('tw`bg-${"fuchsia"} rounded-${"xl"}`', () => { }) /* eslint-enable no-template-curly-in-string */ +test('container center', () => { + const { tw } = create({ + injector, + prefix: false, + preflight: false, + mode: strict, + theme: { + extend: { + container: { + center: true, + }, + }, + }, + }) + + expect(tw`container`).toBe('container') + expect(injector.target).toStrictEqual([ + '.container{width:100%;margin-right:auto;margin-left:auto}', + '@media (min-width: 640px){.container{max-width:640px}}', + '@media (min-width: 768px){.container{max-width:768px}}', + '@media (min-width: 1024px){.container{max-width:1024px}}', + '@media (min-width: 1280px){.container{max-width:1280px}}', + '@media (min-width: 1536px){.container{max-width:1536px}}', + ]) +}) + +test('container padding', () => { + const { tw } = create({ + injector, + prefix: false, + preflight: false, + mode: strict, + theme: { + extend: { + container: { + padding: '2rem', + }, + }, + }, + }) + + expect(tw`container`).toBe('container') + expect(injector.target).toStrictEqual([ + '.container{width:100%;padding-right:2rem;padding-left:2rem}', + '@media (min-width: 640px){.container{max-width:640px;padding-right:2rem;padding-left:2rem}}', + '@media (min-width: 768px){.container{max-width:768px;padding-right:2rem;padding-left:2rem}}', + '@media (min-width: 1024px){.container{max-width:1024px;padding-right:2rem;padding-left:2rem}}', + '@media (min-width: 1280px){.container{max-width:1280px;padding-right:2rem;padding-left:2rem}}', + '@media (min-width: 1536px){.container{max-width:1536px;padding-right:2rem;padding-left:2rem}}', + ]) +}) + +test('container padding per screeen', () => { + const { tw } = create({ + injector, + prefix: false, + preflight: false, + mode: strict, + theme: { + extend: { + container: { + padding: { + DEFAULT: '1rem', + sm: '2rem', + lg: '4rem', + xl: '5rem', + '2xl': '6rem', + }, + }, + }, + }, + }) + + expect(tw`container`).toBe('container') + expect(injector.target).toStrictEqual([ + '.container{width:100%;padding-right:1rem;padding-left:1rem}', + '@media (min-width: 640px){.container{max-width:640px;padding-right:2rem;padding-left:2rem}}', + '@media (min-width: 768px){.container{max-width:768px;padding-right:1rem;padding-left:1rem}}', + '@media (min-width: 1024px){.container{max-width:1024px;padding-right:4rem;padding-left:4rem}}', + '@media (min-width: 1280px){.container{max-width:1280px;padding-right:5rem;padding-left:5rem}}', + '@media (min-width: 1536px){.container{max-width:1536px;padding-right:6rem;padding-left:6rem}}', + ]) +}) + test('falsy arguments', () => { expect(tw(true, false, '', null, undefined, 0, Number.NaN)).toBe('') expect(tw('')).toBe('') diff --git a/packages/core/src/internal/presedence.ts b/packages/core/src/internal/presedence.ts index 76c8d7e25..320ab64e3 100644 --- a/packages/core/src/internal/presedence.ts +++ b/packages/core/src/internal/presedence.ts @@ -17,8 +17,12 @@ let match: RegExpExecArray | null // 1536px -> 9 // 36rem -> 3 // 96rem -> 9 -const responsivePrecedence = (css: string): number => - (match = /^(\d+(?:.\d+)?)(p)?/.exec(css)) ? +match[1] / (match[2] ? 15 : 1) / 10 : 0 +export const responsivePrecedence = (css: string): number => + (((match = /(?:^|min-width:\s*)(\d+(?:.\d+)?)(p)?/.exec(css)) + ? +match[1] / (match[2] ? 15 : 1) / 10 + : 0) & + 31) << + 22 // Colon and dash count of string (ascending): 0 -> 7 => 3 bits export const seperatorPrecedence = (string: string): number => { @@ -89,7 +93,7 @@ export const makeVariantPresedenceCalculator = ( // 1536px -> 9 // 36rem -> 3 // 96rem -> 9 - (responsivePrecedence(_) & 31) << 22 + responsivePrecedence(_) : // 1: dark mode flag variant === ':dark' ? 1 << 21 diff --git a/packages/core/src/internal/theme.ts b/packages/core/src/internal/theme.ts index fd33c8b7f..f10cd0830 100644 --- a/packages/core/src/internal/theme.ts +++ b/packages/core/src/internal/theme.ts @@ -3,7 +3,6 @@ import type { ThemeColor, ThemeConfiguration, ThemeResolver, - ThemeSectionRecord, ThemeSectionResolverContext, } from '@tw-in-js/types' @@ -76,14 +75,14 @@ export const makeThemeResolver = (config?: ThemeConfiguration): ThemeResolver => const deref = ( theme: undefined | Partial, section: keyof Theme, - ): ThemeSectionRecord | undefined => { + ): Record | undefined => { const base = theme && theme[section] const value = is.function(base) ? base(themeResolver, resolveContext) : base return value && section === 'colors' ? flattenColorPalette(value as Record) - : value + : (value as Record) } const resolve = (( diff --git a/packages/core/src/process/serialize.ts b/packages/core/src/process/serialize.ts index 032f2acf9..0de769763 100644 --- a/packages/core/src/process/serialize.ts +++ b/packages/core/src/process/serialize.ts @@ -4,6 +4,7 @@ import * as is from '../internal/is' import { join, includes, escape, hyphenate } from '../internal/util' import { + responsivePrecedence, descending, declarationPropertyPrecedence, declarationValuePrecedence, @@ -71,7 +72,7 @@ export const serialize = ( // Handling the `@font-face` where the // block doesn't need the brackets wrapped stringify([], key, 0, value) - } else { + } else if (key[1] === 'k') { // To prevent // "@keyframes spin{from{transform:rotate(0deg)}}" // "@keyframes spin{to{transform:rotate(360deg)}}" @@ -81,7 +82,7 @@ export const serialize = ( // => "@keyframes name{from{transform:rotate(0deg)}from{transform:rotate(0deg)}}" const currentSize = rules.length - stringify([], key[1] === 'k' ? '' : selector, presedence, value) + stringify([], '', 0, value) const waypoints = rules.splice(currentSize, rules.length - currentSize) @@ -96,6 +97,8 @@ export const serialize = ( // eslint-disable-next-line unicorn/no-reduce p: waypoints.reduce((sum, p) => sum + p.p, 0), }) + } else { + stringify(atRules.concat(key), selector, presedence | responsivePrecedence(key), value) } } else { // Call the serialize for this block diff --git a/packages/core/src/tailwind/plugins.ts b/packages/core/src/tailwind/plugins.ts index 5f123d68d..07be87249 100644 --- a/packages/core/src/tailwind/plugins.ts +++ b/packages/core/src/tailwind/plugins.ts @@ -8,6 +8,7 @@ import type { ThemeResolver, Context, Falsy, + ThemeContainer, } from '@tw-in-js/types' import * as is from '../internal/is' @@ -20,12 +21,12 @@ let _: undefined | string | CSSRules | CSSProperties | string[] | boolean | Fals let __: undefined | string | CSSProperties let $: undefined | string -const property = (property?: string) => ( +const property = (property: string) => ( params: string[], context: unknown, id: string, ): CSSRules => ({ - [property || id]: id + ((_ = join(params)) && '-' + _), + [property]: id + ((_ = join(params)) && '-' + _), }) const propertyValue = (property: string, separator?: string) => (params: string[]): CSSRules => ({ @@ -91,14 +92,12 @@ const withOpacityFallback = ( kind: string, color: string | undefined, ): CSSRules | undefined => - color - ? (_ = asRGBA(color, kind + '-opacity')) && _ !== color - ? { - [`--tw-${kind}-opacity`]: '1', - [property]: [color, _], - } - : { [property]: color } - : undefined + color && (_ = asRGBA(color, kind + '-opacity')) && _ !== color + ? { + [`--tw-${kind}-opacity`]: '1', + [property]: [color, _], + } + : { [property]: color } const reversableEdge = ( params: string[], @@ -865,5 +864,38 @@ export const corePlugins: Plugins = { ' ', ), }), + + container: (params, { theme }) => { + const { screens = theme('screens'), center, padding } = theme('container') as ThemeContainer + + const paddingFor = (screen: string): CSSRules => + (_ = padding && (is.string(padding) ? padding : padding[screen] || padding.DEFAULT)) + ? { + paddingRight: _, + paddingLeft: _, + } + : {} + + // eslint-disable-next-line unicorn/no-reduce + return Object.keys(screens).reduce( + (rules, screen) => { + if ((_ = screens[screen])) { + rules[`@media (min-width: ${_})`] = { + '&': { + 'max-width': _, + ...paddingFor(screen), + }, + } + } + + return rules + }, + { + width: '100%', + ...(center ? { marginRight: 'auto', marginLeft: 'auto' } : {}), + ...paddingFor('xs'), + } as CSSRules, + ) + }, } /* eslint-enable unicorn/prevent-abbreviations, no-return-assign, no-cond-assign, @typescript-eslint/consistent-type-assertions */ diff --git a/packages/core/src/tailwind/theme.ts b/packages/core/src/tailwind/theme.ts index d46189821..e9369502e 100644 --- a/packages/core/src/tailwind/theme.ts +++ b/packages/core/src/tailwind/theme.ts @@ -296,6 +296,7 @@ export const defaultTheme: Theme = { inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)', none: 'none', }, + container: {}, divideColor: (theme) => theme('borderColor'), divideOpacity: (theme) => theme('borderOpacity'), divideWidth: (theme) => theme('borderWidth'), @@ -514,6 +515,8 @@ export const defaultTheme: Theme = { // 90: '0.9', // 100: '1', 5: '0.05', + 25: '0.25', + 75: '0.75', 95: '0.95', }, order: { diff --git a/packages/types/src/theme.ts b/packages/types/src/theme.ts index 1d997b8cf..cbf513dc0 100644 --- a/packages/types/src/theme.ts +++ b/packages/types/src/theme.ts @@ -16,7 +16,9 @@ export interface ThemeResolver { export type Unwrap = T extends string[] ? string : T extends Record ? R : T -export type ThemeSectionType = T extends ThemeSection ? Unwrap : never +export type ThemeSectionType = T extends ThemeSection + ? Unwrap + : Exclude> export interface ThemeSectionResolverContext { /** @@ -40,6 +42,12 @@ export type ThemeSectionResolver = ( export type ThemeSection = ThemeSectionRecord | ThemeSectionResolver +export interface ThemeContainer { + screens?: Record + center?: boolean + padding?: string | Record +} + export type ThemeColor = string | Record export type ThemeFontSize = @@ -65,6 +73,7 @@ export interface Theme { borderRadius: ThemeSection borderWidth: ThemeSection boxShadow: ThemeSection + container: ThemeContainer | ThemeSectionResolver divideColor: ThemeSection divideOpacity: ThemeSection divideWidth: ThemeSection