Skip to content

Commit

Permalink
add injectGlobal which allows to add CSS to the base layer
Browse files Browse the repository at this point in the history
  • Loading branch information
sastan committed Jan 25, 2022
1 parent 90da3bb commit 1800dec
Show file tree
Hide file tree
Showing 8 changed files with 197 additions and 49 deletions.
5 changes: 5 additions & 0 deletions .changeset/heavy-schools-film.md
@@ -0,0 +1,5 @@
---
'twind': patch
---

add `injectGlobal` which allows to add CSS to the base layer
4 changes: 3 additions & 1 deletion README.md
Expand Up @@ -256,7 +256,6 @@ We have created a few [examples](https://github.com/tw-in-js/twind/tree/next/exa

## TODO

- `inject` for global styles (CSS string and objects) — always base layer
- rewrite https://github.com/TanStack/tanstack.com
- support `is(:hover,:focus-visible):underline`?
- style: should it pass `class` and `className` through? alternatives: string concat, `cx`
Expand All @@ -272,6 +271,9 @@ We have created a few [examples](https://github.com/tw-in-js/twind/tree/next/exa
- auto support dark mode in theme helpers (`<section>.dark.<key>` or `dark.<section>.<key>`)
- @twind/preset-\* from tailwind core
- @twind/react
- Global: https://emotion.sh/docs/globals
- createGlobalStyles: https://goober.js.org/api/createGlobalStyles
- createGlobalStyle: https://styled-components.com/docs/api#createglobalstyle
- @twind/completions — provide autocompletion for classNames
- a package to make it easy to create lightweight versions of presets (like https://lodash.com/custom-builds)
- postcss plugin like tailwindcss for SSR
Expand Down
51 changes: 3 additions & 48 deletions packages/twind/src/css.ts
@@ -1,14 +1,11 @@
import type { CSSObject, Falsey } from './types'
import type { CSSObject, CSSValue } from './types'

import { register } from './internal/registry'
import { serialize } from './internal/serialize'
import { hash } from './utils'
import { Layer } from './internal/precedence'
import { interleave } from './internal/interleave'
import { removeComments } from './internal/parse'
import { merge } from './internal/merge'

export type CSSValue = string | number | bigint | Falsey
import { astish } from './internal/astish'

export function css(strings: TemplateStringsArray, ...interpolations: readonly CSSValue[]): string

Expand All @@ -18,17 +15,7 @@ export function css(
strings: CSSObject | string | TemplateStringsArray,
...interpolations: readonly CSSValue[]
): string {
const ast = Array.isArray(strings)
? astish(
interleave(strings as TemplateStringsArray, interpolations, (interpolation) =>
interpolation != null && typeof interpolation != 'boolean'
? (interpolation as unknown as string)
: '',
),
)
: typeof strings == 'string'
? astish(strings)
: [strings as CSSObject]
const ast = astish(strings, interpolations)

const className = (ast.find((o) => o.label)?.label || 'css') + hash(JSON.stringify(ast))

Expand All @@ -39,35 +26,3 @@ export function css(
),
)
}

// Based on https://github.com/cristianbote/goober/blob/master/src/core/astish.js
const newRule = / *(?:(?:([\u0080-\uFFFF\w-%@]+) *:? *([^{;]+?);|([^;}{]*?) *{)|(}))/g

/**
* Convert a css style string into a object
*/
function astish(css: string): CSSObject[] {
css = removeComments(css)

const rules: CSSObject[] = []
const tree: string[] = []
let block: RegExpExecArray | null

while ((block = newRule.exec(css))) {
// Remove the current entry
if (block[4]) tree.pop()

if (block[3]) {
// new nested
tree.push(block[3])
} else if (!block[4]) {
rules.push(
tree.reduceRight((css, block) => ({ [block]: css }), {
[block[1]]: block[2],
} as CSSObject),
)
}
}

return rules
}
1 change: 1 addition & 0 deletions packages/twind/src/index.ts
Expand Up @@ -11,6 +11,7 @@ export * from './css'
export * from './cx'
export * from './define-config'
export * from './extract'
export * from './inject-global'
export * from './inline'
export * from './nested'
export * from './observe'
Expand Down
35 changes: 35 additions & 0 deletions packages/twind/src/inject-global.ts
@@ -0,0 +1,35 @@
import type { CSSObject, CSSValue } from './types'

import { tw as tw$ } from './runtime'
import { astish } from './internal/astish'
import { css } from './css'

/**
* Injects styles into the global scope and is useful for applications such as gloabl styles, CSS resets or font faces.
*
* It **does not** return a class name, but adds the styles within the base layer to the stylesheet directly.
*/
export function injectGlobal(
this: ((tokens: string) => string) | undefined | void,
style: CSSObject | string,
): void

export function injectGlobal(
this: ((tokens: string) => string) | undefined | void,
strings: TemplateStringsArray,
...interpolations: readonly CSSValue[]
): void

export function injectGlobal(
this: ((tokens: string) => string) | undefined | void,
strings: CSSObject | string | TemplateStringsArray,
...interpolations: readonly CSSValue[]
): void {
const tw = typeof this == 'function' ? this : tw$

tw(
css({
'@layer base': astish(strings, interpolations),
} as CSSObject),
)
}
52 changes: 52 additions & 0 deletions packages/twind/src/internal/astish.ts
@@ -0,0 +1,52 @@
import type { CSSObject, CSSValue } from '../types'
import { interleave } from './interleave'
import { removeComments } from './parse'

export function astish(
strings: CSSObject | string | TemplateStringsArray,
interpolations: readonly CSSValue[],
): CSSObject[] {
return Array.isArray(strings)
? astish$(
interleave(strings as TemplateStringsArray, interpolations, (interpolation) =>
interpolation != null && typeof interpolation != 'boolean'
? (interpolation as unknown as string)
: '',
),
)
: typeof strings == 'string'
? astish$(strings)
: [strings as CSSObject]
}

// Based on https://github.com/cristianbote/goober/blob/master/src/core/astish.js
const newRule = / *(?:(?:([\u0080-\uFFFF\w-%@]+) *:? *([^{;]+?);|([^;}{]*?) *{)|(}))/g

/**
* Convert a css style string into a object
*/
function astish$(css: string): CSSObject[] {
css = removeComments(css)

const rules: CSSObject[] = []
const tree: string[] = []
let block: RegExpExecArray | null

while ((block = newRule.exec(css))) {
// Remove the current entry
if (block[4]) tree.pop()

if (block[3]) {
// new nested
tree.push(block[3])
} else if (!block[4]) {
rules.push(
tree.reduceRight((css, block) => ({ [block]: css }), {
[block[1]]: block[2],
} as CSSObject),
)
}
}

return rules
}
96 changes: 96 additions & 0 deletions packages/twind/src/tests/inject-global.test.ts
@@ -0,0 +1,96 @@
import { assert, test, afterEach } from 'vitest'

import presetTailwind from '@twind/preset-tailwind'
import { setup, tw, twind, virtual, injectGlobal } from '..'

setup({
presets: [presetTailwind({ enablePreflight: false })],
})

afterEach(() => tw.clear())

test('inject global styles', () => {
tw('underline')

injectGlobal`
* {
box-sizing: border-box;
}
@font-face {
font-family: 'Patrick Hand SC';
font-style: normal;
font-weight: 400;
src: local('Patrick Hand SC'),
local('PatrickHandSC-Regular'),
url(https://fonts.gstatic.com/s/patrickhandsc/v4/OYFWCgfCR-7uHIovjUZXsZ71Uis0Qeb9Gqo8IZV7ckE.woff2)
format('woff2');
unicode-range: U+0100-024f, U+1-1eff,
U+20a0-20ab, U+20ad-20cf, U+2c60-2c7f,
U+A720-A7FF;
}
`

tw('shadow')

assert.deepEqual(tw.target, [
"@font-face{font-family:'Patrick Hand SC';font-style:normal;font-weight:400;src:local('Patrick Hand SC'), local('PatrickHandSC-Regular'), url(https: format('woff2');unicode-range:U+0100-024f, U+1-1eff, U+20a0-20ab, U+20ad-20cf, U+2c60-2c7f, U+A720-A7FF}",
'*{--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000}',
'*{box-sizing:border-box}',
'.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,0.1), 0 1px 2px -1px rgba(0,0,0,0.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}',
'.underline{text-decoration:underline}',
])
})

test('inject global styles using custom tw', () => {
const tw$ = twind(
{
presets: [presetTailwind({ enablePreflight: false })],
},
virtual(),
)

tw$('underline')

const inject = injectGlobal.bind(tw$)

inject`
:root > body {
@apply bg-white;
border: 3px solid red;
@apply text-black;
}
html,
body,
#__next {
@apply: h-screen w-screen p-0 m-0 overflow-x-hidden overflow-y-auto;
font-size: 14px;
}
* {
scrollbar-color: theme(colors.gray.500);
&::-webkit-scrollbar,
& scrollbar {
width: 1rem;
height: 1rem;
}
}
`

tw$('shadow')

assert.deepEqual(tw$.target, [
'*{--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000}',
':root > body{--tw-bg-opacity:1;background-color:rgba(255,255,255,var(--tw-bg-opacity));border:3px solid red;--tw-text-opacity:1;color:rgba(0,0,0,var(--tw-text-opacity))}',
'html,body,#__next{height:100vh;width:100vw;padding:0px;margin:0px;overflow-x:hidden;overflow-y:auto;font-size:14px}',
'*{scrollbar-color:#6b7280}',
'*::-webkit-scrollbar,* scrollbar{width:1rem;height:1rem}',
'.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,0.1), 0 1px 2px -1px rgba(0,0,0,0.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}',
'.underline{text-decoration:underline}',
])

assert.lengthOf(tw.target as string[], 0)
})
2 changes: 2 additions & 0 deletions packages/twind/src/types.ts
Expand Up @@ -42,6 +42,8 @@ export type CSSBase = BaseProperties & CSSNested

export type CSSObject = CSSProperties & CSSBase

export type CSSValue = string | number | bigint | Falsey

export type Preflight = CSSBase | string

export interface TwindRule {
Expand Down

0 comments on commit 1800dec

Please sign in to comment.