From 293d0e530e4095b34275d4ff81885dbd1d440c1d Mon Sep 17 00:00:00 2001 From: Sascha Tandel Date: Thu, 25 Mar 2021 19:52:48 +0100 Subject: [PATCH 1/2] fix: mark function calls as pure to allow tree-shaking --- src/twind/default.ts | 2 +- src/twind/serialize.ts | 73 +++++++++++++++++++++++++++--------------- src/twind/theme.ts | 68 +++++++++++++++++++-------------------- 3 files changed, 83 insertions(+), 60 deletions(-) diff --git a/src/twind/default.ts b/src/twind/default.ts index d15bb69e3..87191a71f 100644 --- a/src/twind/default.ts +++ b/src/twind/default.ts @@ -1,3 +1,3 @@ import { create } from './instance' -export const { tw, setup } = create() +export const { tw, setup } = /*#__PURE__*/ create() diff --git a/src/twind/serialize.ts b/src/twind/serialize.ts index 4ead68da7..47e899478 100644 --- a/src/twind/serialize.ts +++ b/src/twind/serialize.ts @@ -25,32 +25,55 @@ export interface RuleWithPresedence { const stringifyBlock = (body: string, selector: string): string => selector + '{' + body + '}' +// Not using const enums as they get transpiled to a lot of code +// /** +// * Determines the default order of styles. +// * +// * For example: screens have a higher presedence (eg override) utilities +// */ +// const enum Layer { +// /** +// * The preflight styles and any base styles registered by plugins. +// */ +// base = 0, + +// /** +// * Component classes and any component classes registered by plugins. +// */ +// components = 1, + +// /** +// * Utility classes and any utility classes registered by plugins. +// */ +// utilities = 2, + +// /** +// * Inline directives +// */ +// css = 3, +// } + /** - * Determines the default order of styles. - * - * For example: screens have a higher presedence (eg override) utilities + * The preflight styles and any base styles registered by plugins. */ -const enum Layer { - /** - * The preflight styles and any base styles registered by plugins. - */ - base = 0, - - /** - * Component classes and any component classes registered by plugins. - */ - components = 1, - - /** - * Utility classes and any utility classes registered by plugins. - */ - utilities = 2, - - /** - * Inline directives - */ - css = 3, -} +export type LayerBase = 0 + +/** + * Component classes and any component classes registered by plugins. + */ +export type LayerComponents = 1 + +/** + * Utility classes and any utility classes registered by plugins. + */ +export type LayerUtilities = 2 + +/** + * Inline directives + */ +export type LayerCss = 3 + +export type Layer = LayerBase | LayerComponents | LayerUtilities | LayerCss export const serialize = ( prefix: Prefixer, @@ -270,7 +293,7 @@ export const serialize = ( const variantPresedence = makeVariantPresedenceCalculator(theme, variants) - return (css, className, rule, layer = Layer.base) => { + return (css, className, rule, layer = 0 /* Layerbase */) => { // Initial presedence based on layer (base = 0, components = 1, utilities = 2, css = 3) layer <<= 28 diff --git a/src/twind/theme.ts b/src/twind/theme.ts index 7a31230f9..a2916863d 100644 --- a/src/twind/theme.ts +++ b/src/twind/theme.ts @@ -90,7 +90,7 @@ const themeFactory = (args: Parameters, { theme }: Context) => th export const theme = ((...args: Parameters): ReturnType => directive(themeFactory, args)) as ThemeHelper -const defaultTheme: Partial = { +const defaultTheme: Partial = /*#__PURE__*/ { screens: { sm: '640px', md: '768px', @@ -224,7 +224,7 @@ const defaultTheme: Partial = { spacing: { px: '1px', 0: '0px', - ...linear(4, 'rem', 4, 0.5, 0.5), + .../*#__PURE__*/ linear(4, 'rem', 4, 0.5, 0.5), // 0.5: '0.125rem', // 1: '0.25rem', // 1.5: '0.375rem', @@ -233,7 +233,7 @@ const defaultTheme: Partial = { // 3: '0.75rem', // 3.5: '0.875rem', // 4: '1rem', - ...linear(12, 'rem', 4, 5), + .../*#__PURE__*/ linear(12, 'rem', 4, 5), // 5: '1.25rem', // 6: '1.5rem', // 7: '1.75rem', @@ -243,7 +243,7 @@ const defaultTheme: Partial = { // 11: '2.75rem', // 12: '3rem', 14: '3.5rem', - ...linear(64, 'rem', 4, 16, 4), + .../*#__PURE__*/ linear(64, 'rem', 4, 16, 4), // 16: '4rem', // 20: '5rem', // 24: '6rem', @@ -281,7 +281,7 @@ const defaultTheme: Partial = { bounce: 'bounce 1s infinite', }, - backgroundColor: alias('colors'), + backgroundColor: /*#__PURE__*/ alias('colors'), backgroundImage: { none: 'none', // These are built-in @@ -294,7 +294,7 @@ const defaultTheme: Partial = { // 'gradient-to-l': 'linear-gradient(to left, var(--tw-gradient-stops))', // 'gradient-to-tl': 'linear-gradient(to top left, var(--tw-gradient-stops))', }, - backgroundOpacity: alias('opacity'), + backgroundOpacity: /*#__PURE__*/ alias('opacity'), // backgroundPosition: { // // The following are already handled by the plugin: // // center, right, left, bottom, top @@ -309,7 +309,7 @@ const defaultTheme: Partial = { ...theme('colors'), DEFAULT: theme('colors.gray.200', 'currentColor'), }), - borderOpacity: alias('opacity'), + borderOpacity: /*#__PURE__*/ alias('opacity'), borderRadius: { none: '0px', sm: '0.125rem', @@ -324,7 +324,7 @@ const defaultTheme: Partial = { }, borderWidth: { DEFAULT: '1px', - ...exponential(8, 'px'), + .../*#__PURE__*/ exponential(8, 'px'), // 0: '0px', // 2: '2px', // 4: '4px', @@ -344,9 +344,9 @@ const defaultTheme: Partial = { // cursor: { // // Default values are handled by plugin // }, - divideColor: alias('borderColor'), - divideOpacity: alias('borderOpacity'), - divideWidth: alias('borderWidth'), + divideColor: /*#__PURE__*/ alias('borderColor'), + divideOpacity: /*#__PURE__*/ alias('borderOpacity'), + divideWidth: /*#__PURE__*/ alias('borderWidth'), fill: { current: 'currentColor' }, flex: { 1: '1 1 0%', @@ -437,8 +437,8 @@ const defaultTheme: Partial = { auto: 'auto', 'span-full': '1 / -1', }, - gap: alias('spacing'), - gradientColorStops: alias('colors'), + gap: /*#__PURE__*/ alias('spacing'), + gradientColorStops: /*#__PURE__*/ alias('colors'), height: (theme) => ({ auto: 'auto', ...theme('spacing'), @@ -526,7 +526,7 @@ const defaultTheme: Partial = { normal: '1.5', relaxed: '1.625', loose: '2', - ...linear(10, 'rem', 4, 3), + .../*#__PURE__*/ linear(10, 'rem', 4, 3), // 3: '.75rem', // 4: '1rem', // 5: '1.25rem', @@ -583,7 +583,7 @@ const defaultTheme: Partial = { // // The plugins joins all arguments by default // }, opacity: { - ...linear(100, '', 100, 0, 10), + .../*#__PURE__*/ linear(100, '', 100, 0, 10), // 0: '0', // 10: '0.1', // 20: '0.2', @@ -603,7 +603,7 @@ const defaultTheme: Partial = { first: '-9999', last: '9999', none: '0', - ...linear(12, '', 1, 1), + .../*#__PURE__*/ linear(12, '', 1, 1), // 1: '1', // 2: '2', // 3: '3', @@ -622,15 +622,15 @@ const defaultTheme: Partial = { white: ['2px dotted white', '2px'], black: ['2px dotted black', '2px'], }, - padding: alias('spacing'), - placeholderColor: alias('colors'), - placeholderOpacity: alias('opacity'), + padding: /*#__PURE__*/ alias('spacing'), + placeholderColor: /*#__PURE__*/ alias('colors'), + placeholderOpacity: /*#__PURE__*/ alias('opacity'), ringColor: (theme) => ({ DEFAULT: theme('colors.blue.500', '#3b82f6'), ...theme('colors'), }), - ringOffsetColor: alias('colors'), - ringOffsetWidth: exponential(8, 'px'), + ringOffsetColor: /*#__PURE__*/ alias('colors'), + ringOffsetWidth: /*#__PURE__*/ exponential(8, 'px'), // 0: '0px', // 1: '1px', // 2: '2px', @@ -642,7 +642,7 @@ const defaultTheme: Partial = { }), ringWidth: { DEFAULT: '3px', - ...exponential(8, 'px'), + .../*#__PURE__*/ exponential(8, 'px'), // 0: '0px', // 1: '1px', // 2: '2px', @@ -650,15 +650,15 @@ const defaultTheme: Partial = { // 8: '8px', }, rotate: { - ...exponential(2, 'deg'), + .../*#__PURE__*/ exponential(2, 'deg'), // 0: '0deg', // 1: '1deg', // 2: '2deg', - ...exponential(12, 'deg', 3), + .../*#__PURE__*/ exponential(12, 'deg', 3), // 3: '3deg', // 6: '6deg', // 12: '12deg', - ...exponential(180, 'deg', 45), + .../*#__PURE__*/ exponential(180, 'deg', 45), // 45: '45deg', // 90: '90deg', // 180: '180deg', @@ -667,7 +667,7 @@ const defaultTheme: Partial = { 0: '0', 50: '.5', 75: '.75', - ...linear(110, '', 100, 90, 5), + .../*#__PURE__*/ linear(110, '', 100, 90, 5), // 90: '.9', // 95: '.95', // 100: '1', @@ -677,25 +677,25 @@ const defaultTheme: Partial = { 150: '1.5', }, skew: { - ...exponential(2, 'deg'), + .../*#__PURE__*/ exponential(2, 'deg'), // 0: '0deg', // 1: '1deg', // 2: '2deg', - ...exponential(12, 'deg', 3), + .../*#__PURE__*/ exponential(12, 'deg', 3), // 3: '3deg', // 6: '6deg', // 12: '12deg', }, - space: alias('spacing'), + space: /*#__PURE__*/ alias('spacing'), stroke: { current: 'currentColor', }, - strokeWidth: linear(2), + strokeWidth: /*#__PURE__*/ linear(2), // 0: '0', // 1: '1', // 2: '2',, - textColor: alias('colors'), - textOpacity: alias('opacity'), + textColor: /*#__PURE__*/ alias('colors'), + textOpacity: /*#__PURE__*/ alias('opacity'), // transformOrigin: { // // The following are already handled by the plugin: // // center, right, left, bottom, top @@ -705,7 +705,7 @@ const defaultTheme: Partial = { DEFAULT: '150ms', ...theme('durations'), }), - transitionDelay: alias('durations'), + transitionDelay: /*#__PURE__*/ alias('durations'), transitionProperty: { none: 'none', all: 'all', @@ -773,7 +773,7 @@ const defaultTheme: Partial = { }), zIndex: { auto: 'auto', - ...linear(50, '', 1, 0, 10), + .../*#__PURE__*/ linear(50, '', 1, 0, 10), // 0: '0', // 10: '10', // 20: '20', From 4f67b014953daa7f8be18490d1746e653abc1429 Mon Sep 17 00:00:00 2001 From: Sascha Tandel Date: Thu, 25 Mar 2021 19:54:43 +0100 Subject: [PATCH 2/2] feat: allow style component to be used as plugin --- src/style/index.ts | 35 +++++++++++++++++++++-- src/style/style.test.ts | 62 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/src/style/index.ts b/src/style/index.ts index d0264366d..0090462af 100644 --- a/src/style/index.ts +++ b/src/style/index.ts @@ -98,7 +98,7 @@ export interface Style { * ``` *
*/ - (props?: StyleProps): Directive + (props?: string | string[] | StyleProps): Directive /** * CSS Selector associated with the current component. @@ -180,6 +180,21 @@ const buildMediaRule = (key: string, value: undefined | StyleToken): CSSRules => [key[0] == '@' ? key : `@screen ${key}`]: typeof value == 'string' ? apply(value) : value, }) +/** + * Convert parts array into valid props for a twind/style function + * - Kebab Case (tailwind like): x-foo=bar-camelCase + * - Snake Case: x-foo=bar_camelCase & x-foo=bar_camel-case + * - BEM modifer: x-foo=bar--camelCase & x-foo=bar--camel-case + * - URL like: x-foo=bar&camelCase & x-foo=bar&camel-case + * e.g. foo=bar--baz --> { foo: 'bar', 'baz': true } + */ +const makeProps = (parts: string[]): Record => + parts.reduce((props, part) => { + const [key, value = true] = part.split('=', 2) + props[key] = value + return props + }, {} as Record) + const createStyle = ( config: StyleConfig = {}, base?: Style, @@ -191,7 +206,15 @@ const createStyle = ( const selector = (base || '') + '.' + id return Object.defineProperties( - (allProps?: StyleProps): Directive => { + (allProps?: string | string[] | StyleProps): Directive => { + if (typeof allProps == 'string') { + allProps = allProps.split('-') + } + + if (Array.isArray(allProps)) { + allProps = makeProps(allProps) as StyleProps + } + const { tw, css, class: localClass, className: localClassName, ...props } = { ...defaults, ...allProps, @@ -269,6 +292,14 @@ const createStyle = ( selector: { value: selector, }, + // ['size=sm', 'size=md', 'outline', 'outline=false'] + // ['size=sm', 'size=sm-outline', 'size=sm-outline=false'] + // ['size=md', 'size=md-outline', 'size=md-outline=false'] + // tokens: { + // value: Object.keys(variants).map((key) => + // Object.keys((variants as Record>)[key]), + // ), + // }, }, ) } diff --git a/src/style/style.test.ts b/src/style/style.test.ts index ac0ebc645..c397588ec 100644 --- a/src/style/style.test.ts +++ b/src/style/style.test.ts @@ -675,4 +675,66 @@ test('is added to component layer', ({ tw, sheet }) => { ]) }) +test('component can be used as plugin', ({ sheet }) => { + const button = style({ + base: ` + px-3 py-1 rounded-md + text-white bg-gray-500 hover:bg-gray-400 + `, + variants: { + variant: { + primary: 'bg-blue-500 hover:bg-blue-400', + warn: 'bg-red-500 hover:bg-red-400', + }, + outline: { true: `border-2 border-black` }, + }, + }) + + const { tw } = create({ + sheet, + mode: strict, + preflight: false, + prefix: false, + plugins: { + btn: button, + }, + }) + + assert.equal(sheet.target, []) + + assert.is(button.className, 'tw-1fg076q') + assert.is('' + button, '.tw-1fg076q') + assert.is(button.selector, '.tw-1fg076q') + + assert.is(tw`btn`, 'btn tw-1fg076q') + assert.equal(sheet.target, [ + '.btn{padding-left:0.75rem;padding-right:0.75rem;padding-bottom:0.25rem;padding-top:0.25rem;border-radius:0.375rem;--tw-text-opacity:1;color:#fff;color:rgba(255,255,255,var(--tw-text-opacity));--tw-bg-opacity:1;background-color:#6b7280;background-color:rgba(107,114,128,var(--tw-bg-opacity))}', + '.btn:hover{--tw-bg-opacity:1;background-color:#9ca3af;background-color:rgba(156,163,175,var(--tw-bg-opacity))}', + ]) + + sheet.reset() + + assert.is(tw`btn-outline`, 'btn-outline tw-1fg076q') + assert.equal(sheet.target, [ + '.btn-outline{padding-left:0.75rem;padding-right:0.75rem;padding-bottom:0.25rem;padding-top:0.25rem;border-radius:0.375rem;--tw-text-opacity:1;color:#fff;color:rgba(255,255,255,var(--tw-text-opacity));--tw-bg-opacity:1;background-color:#6b7280;background-color:rgba(107,114,128,var(--tw-bg-opacity));border-width:2px;--tw-border-opacity:1;border-color:#000;border-color:rgba(0,0,0,var(--tw-border-opacity))}', + '.btn-outline:hover{--tw-bg-opacity:1;background-color:#9ca3af;background-color:rgba(156,163,175,var(--tw-bg-opacity))}', + ]) + + sheet.reset() + + assert.is(tw`btn-variant=primary`, 'btn-variant=primary tw-1fg076q') + assert.equal(sheet.target, [ + '.btn-variant\\=primary{padding-left:0.75rem;padding-right:0.75rem;padding-bottom:0.25rem;padding-top:0.25rem;border-radius:0.375rem;--tw-text-opacity:1;color:#fff;color:rgba(255,255,255,var(--tw-text-opacity));--tw-bg-opacity:1;background-color:#3b82f6;background-color:rgba(59,130,246,var(--tw-bg-opacity))}', + '.btn-variant\\=primary:hover{--tw-bg-opacity:1;background-color:#60a5fa;background-color:rgba(96,165,250,var(--tw-bg-opacity))}', + ]) + + sheet.reset() + + assert.is(tw`btn-variant=warn-outline`, 'btn-variant=warn-outline tw-1fg076q') + assert.equal(sheet.target, [ + '.btn-variant\\=warn-outline{padding-left:0.75rem;padding-right:0.75rem;padding-bottom:0.25rem;padding-top:0.25rem;border-radius:0.375rem;--tw-text-opacity:1;color:#fff;color:rgba(255,255,255,var(--tw-text-opacity));--tw-bg-opacity:1;background-color:#ef4444;background-color:rgba(239,68,68,var(--tw-bg-opacity));border-width:2px;--tw-border-opacity:1;border-color:#000;border-color:rgba(0,0,0,var(--tw-border-opacity))}', + '.btn-variant\\=warn-outline:hover{--tw-bg-opacity:1;background-color:#f87171;background-color:rgba(248,113,113,var(--tw-bg-opacity))}', + ]) +}) + test.run()