├── docs ├── recipes │ ├── README.md │ ├── migrate-from │ │ ├── twin-macro.md │ │ └── tailwindcss.md │ └── use-with │ │ ├── gatsby.md │ │ ├── vue.md │ │ ├── svelte.md │ │ ├── nextjs.md │ │ ├── wmr.md │ │ ├── preact.md │ │ ├── lit-element.md │ │ ├── react.md │ │ └── web-components.md ├── advanced │ ├── README.md │ ├── ssr.md │ └── contributing.md ├── getting-started │ ├── best-practices.md │ ├── modules.md │ ├── customize-the-theme.md │ ├── browser-support.md │ ├── installation.md │ ├── README.md │ ├── styling-with-twind.md │ └── using-the-shim.md ├── release-notes │ └── README.md ├── README.md └── faqs │ └── README.md ├── example ├── favicon.ico ├── robots.txt ├── test.js ├── umd.html ├── snowpack.config.js ├── index.html ├── logo.svg ├── index.js └── shim.html ├── src ├── twind │ ├── default.ts │ ├── variants.ts │ ├── apply.ts │ ├── sheets.ts │ ├── modes.ts │ ├── translate.ts │ ├── inject.ts │ ├── prefix.ts │ ├── decorate.ts │ ├── helpers.ts │ ├── instance.ts │ └── directive.ts ├── types │ ├── index.ts │ ├── util.ts │ ├── css.ts │ └── theme.ts ├── __fixtures__ │ ├── env.js │ ├── process-plugins.d.ts │ ├── dom-env.ts │ └── process-plugins.js ├── __tests__ │ ├── ssr-no-op.test.ts │ ├── tailwind-compat.test.ts │ ├── sheets.test.ts │ ├── node.test.ts │ ├── variants.test.ts │ ├── dom.test.ts │ ├── prefix.test.ts │ ├── setup.test.ts │ ├── theme.test.ts │ ├── dark-mode.test.ts │ ├── hash.test.ts │ ├── mode.test.ts │ ├── plugins.test.ts │ └── preflight.test.ts ├── index.ts ├── internal │ ├── dom.ts │ └── util.ts ├── colors │ ├── README.md │ ├── colors.test.ts │ └── index.ts ├── sheets │ ├── virtual.test.ts │ ├── dom.test.ts │ ├── index.ts │ └── README.md ├── server │ ├── README.md │ ├── index.ts │ └── async-sheet.test.ts ├── shim │ ├── index.ts │ ├── server │ │ ├── README.md │ │ └── index.ts │ └── README.md └── observe │ ├── index.ts │ └── README.md ├── .gitattributes ├── benchmarks ├── package.json ├── README.md └── css.ts ├── .editorconfig ├── LICENSE ├── .gitignore ├── .github └── workflows │ ├── website.yml │ └── ci.yml ├── tsconfig.json └── package.json /docs/recipes/README.md: -------------------------------------------------------------------------------- 1 | To be written... 2 | -------------------------------------------------------------------------------- /docs/recipes/migrate-from/twin-macro.md: -------------------------------------------------------------------------------- 1 | To be written... @twind/macro 2 | -------------------------------------------------------------------------------- /docs/recipes/migrate-from/tailwindcss.md: -------------------------------------------------------------------------------- 1 | To be written... Use the shim. 2 | -------------------------------------------------------------------------------- /example/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sync/twind/main/example/favicon.ico -------------------------------------------------------------------------------- /example/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/twind/default.ts: -------------------------------------------------------------------------------- 1 | import { create } from './instance' 2 | 3 | export const { tw, setup } = create() 4 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './css' 2 | export * from './theme' 3 | export * from './twind' 4 | export * from './util' 5 | -------------------------------------------------------------------------------- /src/types/util.ts: -------------------------------------------------------------------------------- 1 | export type Falsy = '' | 0 | -0 | false | null | undefined | void 2 | 3 | export type MaybeArray = T | readonly T[] 4 | -------------------------------------------------------------------------------- /docs/advanced/README.md: -------------------------------------------------------------------------------- 1 | Advanced - Style: Explanation; understanding, explain, discursive explanation 2 | 3 |
4 | 5 | Continue to {@page Setup} 6 | -------------------------------------------------------------------------------- /docs/getting-started/best-practices.md: -------------------------------------------------------------------------------- 1 | use template literal, shared directive as inline plugins for cacheability 2 | 3 |
4 | 5 | Continue to {@page Quicksheet} 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behavior to always use LF line endings 2 | * text=auto eol=lf 3 | 4 | # Denote all files that are truly binary and should not be modified. 5 | *.ico binary 6 | -------------------------------------------------------------------------------- /docs/release-notes/README.md: -------------------------------------------------------------------------------- 1 | > 💡 Please refer to [releases](https://github.com/tw-in-js/twind/releases) for now. We aim to provide detailed release information here in the future. 2 | 3 | To be written... 4 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | > This folder contains the documentation for the upcoming release. For the current documentation take a look at https://twind.dev/docs. 2 | 3 | If you find any incorrect or missing documentation then please [open an issue](https://github.com/tw-in-js/twind/issues) for discussion. 4 | -------------------------------------------------------------------------------- /src/__fixtures__/env.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | require('esbuild-register') 4 | 5 | // Node's error-message stack size is limited at 10, but it's pretty useful 6 | // to see more than that when a test fails. 7 | Error.stackTraceLimit = 100 8 | 9 | process.env.NODE_ENV = 'test' 10 | -------------------------------------------------------------------------------- /src/__fixtures__/process-plugins.d.ts: -------------------------------------------------------------------------------- 1 | export declare function processPlugins(): { 2 | screens: Record 3 | variants: string[] 4 | directives: Record }> 5 | darkMode: false | 'media' | 'class' 6 | prefix: string 7 | separator: string 8 | } 9 | -------------------------------------------------------------------------------- /src/__tests__/ssr-no-op.test.ts: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu' 2 | import * as assert from 'uvu/assert' 3 | 4 | import { tw } from '..' 5 | 6 | const test = suite('ssr') 7 | 8 | test('uses the no-op sheet by default', () => { 9 | assert.is(tw('group flex text-center md:text-left'), 'group flex text-center md:text-left') 10 | }) 11 | 12 | test.run() 13 | -------------------------------------------------------------------------------- /src/types/css.ts: -------------------------------------------------------------------------------- 1 | import type * as CSS from 'csstype' 2 | 3 | export interface CSSCustomProperties { 4 | '--tw-rotate'?: string 5 | '--tw-gradient-stops'?: string 6 | } 7 | 8 | export interface CSSProperties 9 | extends CSS.PropertiesFallback, 10 | CSS.PropertiesHyphenFallback, 11 | CSSCustomProperties {} 12 | -------------------------------------------------------------------------------- /benchmarks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "benchmarks", 4 | "scripts": { 5 | "bench": "node -r esm -r esbuild-register" 6 | }, 7 | "dependencies": { 8 | "@emotion/css": "^11.0.0", 9 | "@types/benchmark": "^2.1.0", 10 | "benchmark": "^2.1.4", 11 | "goober": "^2.0.30", 12 | "otion": "^0.6.2", 13 | "styled-components": "^5.2.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | # All files should use 5 | # - tabs unless specified otherwise 6 | # - unix-style newlines with a newline ending every file 7 | [*] 8 | indent_style = space 9 | indent_size = 2 10 | end_of_line = lf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * [[include:src/README.md]] 3 | * 4 | * @packageDocumentation 5 | * @module twind 6 | */ 7 | 8 | export * from './twind/apply' 9 | export * from './twind/default' 10 | export * from './twind/directive' 11 | export * from './twind/instance' 12 | export * from './twind/modes' 13 | export * from './twind/prefix' 14 | export * from './twind/sheets' 15 | export { theme } from './twind/theme' 16 | export { cyrb32 as hash } from './internal/util' 17 | 18 | export * from './types' 19 | -------------------------------------------------------------------------------- /docs/faqs/README.md: -------------------------------------------------------------------------------- 1 | > 💡 Didn't found an answer? Try [GitHub discussions](https://github.com/tw-in-js/twind/discussions), our [issues tracker](https://github.com/tw-in-js/twind/issues) or ask the community in our [discord channel](https://discord.com/invite/2aP5NkszvD). 2 | 3 |
How to inject global styles? 4 | 5 | To be written... use preflight. 6 | 7 |
8 | 9 |
How to access theme values? 10 | 11 | To be written... use `tw.theme`. 12 | 13 |
14 | -------------------------------------------------------------------------------- /example/test.js: -------------------------------------------------------------------------------- 1 | import '../src/__tests__/api.test' 2 | import '../src/__tests__/apply.test' 3 | import '../src/__tests__/dark-mode.test' 4 | import '../src/__tests__/hash.test' 5 | import '../src/__tests__/mode.test' 6 | import '../src/__tests__/plugins.test' 7 | import '../src/__tests__/prefix.test' 8 | import '../src/__tests__/preflight.test' 9 | import '../src/__tests__/theme.test' 10 | import '../src/colors/colors.test' 11 | import '../src/css/css.test' 12 | import '../src/sheets/virtual.test' 13 | import '../src/shim/server/shim.test' 14 | -------------------------------------------------------------------------------- /src/twind/variants.ts: -------------------------------------------------------------------------------- 1 | export const coreVariants: Record = { 2 | dark: '@media (prefers-color-scheme:dark)', 3 | sticky: '@supports ((position: -webkit-sticky) or (position:sticky))', 4 | 'motion-reduce': '@media (prefers-reduced-motion:reduce)', 5 | 'motion-safe': '@media (prefers-reduced-motion:no-preference)', 6 | first: '&:first-child', 7 | last: '&:last-child', 8 | even: '&:nth-child(2n)', 9 | odd: '&:nth-child(odd)', 10 | children: '&>*', 11 | siblings: '&~*', 12 | sibling: '&+*', 13 | override: '&&', 14 | } 15 | -------------------------------------------------------------------------------- /src/twind/apply.ts: -------------------------------------------------------------------------------- 1 | import type { Token, Directive, CSSRules, Context } from '../types' 2 | 3 | import { parse } from './parse' 4 | import { directive } from './directive' 5 | 6 | export interface Apply { 7 | (strings: TemplateStringsArray, ...interpolations: Token[]): Directive 8 | 9 | (...tokens: Token[]): Directive 10 | } 11 | 12 | const applyFactory = (tokens: unknown[], { css }: Context) => css(parse(tokens)) 13 | 14 | export const apply: Apply = (...tokens: unknown[]): Directive => 15 | directive(applyFactory, tokens) 16 | -------------------------------------------------------------------------------- /example/umd.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |

This is Twind!

13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /example/snowpack.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import("snowpack").SnowpackUserConfig } */ 2 | 3 | module.exports = { 4 | mount: { 5 | '../example': '/', 6 | '../src': '/_src_', 7 | '../dist': '/_dist_', 8 | }, 9 | packageOptions: { 10 | knownEntrypoints: ['uvu', 'uvu/assert', 'snoop'], 11 | external: ['dlv', 'tailwindcss', 'jsdom', 'async_hooks', 'node:async_hooks'], 12 | }, 13 | alias: { 14 | twind: '../src', 15 | }, 16 | routes: [ 17 | // Prevent type only exports (eg empty modules) to break snowpack 18 | { 19 | src: '/_src_/types/index.js', 20 | dest: (request, response) => { 21 | response.writeHead(200, { 'Content-Type': 'application/javascript' }) 22 | response.end('') 23 | }, 24 | }, 25 | ], 26 | } 27 | -------------------------------------------------------------------------------- /src/__tests__/tailwind-compat.test.ts: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu' 2 | import * as assert from 'uvu/assert' 3 | 4 | import { tw, setup, strict } from '..' 5 | 6 | // Tailwind only supports Node.JS >=12.13.0 7 | // use feature detection 8 | if (Object.entries && [].flatMap) { 9 | const test = suite('tailwind compat') 10 | 11 | setup({ mode: strict }) 12 | 13 | test('all tailwind directives are available', async () => { 14 | const { processPlugins } = await import('../__fixtures__/process-plugins') 15 | 16 | const { directives } = processPlugins() 17 | 18 | for (const directive of Object.keys(directives)) { 19 | try { 20 | assert.ok(tw(directive)) 21 | } catch (error) { 22 | console.warn(directive, directives[directive]) 23 | throw error 24 | } 25 | } 26 | }) 27 | 28 | test.run() 29 | } 30 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Twind 8 | 9 | 10 | 11 | 12 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/getting-started/modules.md: -------------------------------------------------------------------------------- 1 | Below is a list of Twind's modules and a summary of what it’s for. 2 | 3 | - {@link twind} - {@page Styling with Twind | tw} and {@page Setup | setup} 4 | - {@link twind/colors} - the Tailwind v2 [extended color palette](https://tailwindcss.com/docs/customizing-colors#color-palette-reference) 5 | - {@link twind/css} - how to apply custom css 6 | - {@link twind/observe} - the base for {@page Using the Shim | twind/shim} which can be used standalone 7 | - {@link twind/server} - how to extract the generated css on the server 8 | - {@link twind/sheets} - several additional sheet implementations that can be used with [setup({ sheet })](https://twind.dev/docs/handbook/advanced/setup.html#sheet). 9 | - {@link twind/shim} - allows to copy-paste tailwind examples 10 | - {@link twind/shim/server} - generate CSS from static HTML 11 | 12 |
13 | 14 | Continue to {@page Advanced} 15 | -------------------------------------------------------------------------------- /src/internal/dom.ts: -------------------------------------------------------------------------------- 1 | export const STYLE_ELEMENT_ID = '__twind' as const 2 | 3 | declare global { 4 | interface Window { 5 | [STYLE_ELEMENT_ID]?: HTMLStyleElement 6 | } 7 | } 8 | 9 | export const getStyleElement = (nonce?: string): HTMLStyleElement => { 10 | // Hydrate existing style element if available 11 | // self[id] - every element with an id is available through the global object 12 | // eslint-disable-next-line no-restricted-globals 13 | let element = self[STYLE_ELEMENT_ID] 14 | 15 | if (!element) { 16 | // Create a new one otherwise 17 | element = document.head.appendChild(document.createElement('style')) 18 | 19 | element.id = STYLE_ELEMENT_ID 20 | nonce && (element.nonce = nonce) 21 | 22 | // Avoid Edge bug where empty style elements doesn't create sheets 23 | element.appendChild(document.createTextNode('')) 24 | } 25 | 26 | return element 27 | } 28 | -------------------------------------------------------------------------------- /docs/recipes/use-with/gatsby.md: -------------------------------------------------------------------------------- 1 | ## Gatsby 2 | 3 | > ❗ This has not been tested yet. 4 | 5 | ```js 6 | /* gatsby-ssr.js */ 7 | 8 | const { setup } = require('twind') 9 | const { asyncVirtualSheet, getStyleTagProperties } = require('twind/server') 10 | 11 | const sheet = asyncVirtualSheet() 12 | 13 | setup({ ...sharedOptions, sheet }) 14 | 15 | exports.wrapPageElement = ({ element }) => { 16 | sheet.reset() 17 | 18 | return element 19 | } 20 | 21 | exports.onRenderBody = ({ setHeadComponents, pathname }) => { 22 | const { id, textContent } = getStyleTagProperties(sheet) 23 | 24 | const styleProps = { 25 | id, 26 | dangerouslySetInnerHTML: { 27 | __html: textContent, 28 | }, 29 | } 30 | 31 | setHeadComponents([ 32 | React.createElement('style', { 33 | id, 34 | dangerouslySetInnerHTML: { 35 | __html: textContent, 36 | }, 37 | }), 38 | ]) 39 | } 40 | ``` 41 | -------------------------------------------------------------------------------- /src/twind/sheets.ts: -------------------------------------------------------------------------------- 1 | // Based on https://github.com/kripod/otion/blob/main/packages/otion/src/injectors.ts 2 | // License MIT 3 | import type { SheetConfig, Sheet } from '../types' 4 | import { noop } from '../internal/util' 5 | import { getStyleElement } from '../internal/dom' 6 | 7 | /** 8 | * Creates an sheet which inserts style rules through the CSS Object Model. 9 | */ 10 | export const cssomSheet = ({ 11 | nonce, 12 | target = getStyleElement(nonce).sheet as CSSStyleSheet, 13 | }: SheetConfig = {}): Sheet => { 14 | const offset = target.cssRules.length 15 | 16 | return { 17 | target, 18 | insert: (rule, index) => target.insertRule(rule, offset + index), 19 | } 20 | } 21 | 22 | /** 23 | * An sheet placeholder which performs no operations. Useful for avoiding errors in a non-browser environment. 24 | */ 25 | export const voidSheet = (): Sheet => ({ 26 | target: null, 27 | insert: noop, 28 | }) 29 | -------------------------------------------------------------------------------- /src/__tests__/sheets.test.ts: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu' 2 | import * as assert from 'uvu/assert' 3 | 4 | import * as DOM from '../__fixtures__/dom-env' 5 | 6 | import { voidSheet, cssomSheet } from '..' 7 | 8 | const test = DOM.configure(suite('sheets')) 9 | 10 | test('voidSheet', () => { 11 | const sheet = voidSheet() 12 | 13 | assert.is(sheet.insert('', 0), undefined) 14 | }) 15 | 16 | test('cssomSheet', () => { 17 | const sheet = cssomSheet() 18 | 19 | assert.is(sheet.insert('html{padding:0}', 0), 0) 20 | assert.is(sheet.insert('body{margin:0}', 1), 1) 21 | assert.is(sheet.insert('*{margin:0}', 1), 1) 22 | 23 | const style = document.querySelector('#__twind') as HTMLStyleElement 24 | 25 | assert.is(style.tagName, 'STYLE') 26 | assert.is(style.nonce, '') 27 | 28 | assert.equal( 29 | [...(style.sheet?.cssRules || [])].map((rule) => rule.cssText), 30 | ['html {padding: 0;}', '* {margin: 0;}', 'body {margin: 0;}'], 31 | ) 32 | }) 33 | 34 | test.run() 35 | -------------------------------------------------------------------------------- /docs/recipes/use-with/vue.md: -------------------------------------------------------------------------------- 1 | ## Vue 2 | 3 | To be written... 4 | 5 | ## Server Side Rendering 6 | 7 | ```js 8 | // createBundleRenderer works the same 9 | import { createRenderer } from 'vue-server-renderer' 10 | 11 | import { setup } from 'twind' 12 | import { asyncVirtualSheet, getStyleTag } from 'twind/server' 13 | 14 | import { createApp } from './app' 15 | 16 | const sheet = asyncVirtualSheet() 17 | 18 | setup({ ...sharedOptions, sheet }) 19 | 20 | const renderer = createRenderer({ 21 | /* options */ 22 | }) 23 | 24 | async function ssr() { 25 | // 1. Reset the sheet for a new rendering 26 | sheet.reset() 27 | 28 | // 2. Render the app 29 | const body = await renderer.renderToString(createApp()) 30 | 31 | // 3. Create the style tag with all generated CSS rules 32 | const styleTag = getStyleTag(sheet) 33 | 34 | // 4. Generate the response html 35 | return ` 36 | 37 | ${styleTag} 38 | ${body} 39 | 40 | ` 41 | } 42 | ``` 43 | -------------------------------------------------------------------------------- /src/__tests__/node.test.ts: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu' 2 | import * as assert from 'uvu/assert' 3 | 4 | import type { Instance } from '..' 5 | import type { VirtualSheet } from '../sheets/index' 6 | 7 | import { virtualSheet } from '../sheets/index' 8 | import { create, noprefix, strict } from '..' 9 | 10 | const test = suite<{ 11 | sheet: VirtualSheet 12 | instance: Instance 13 | }>('node') 14 | 15 | test.before.each((context) => { 16 | context.sheet = virtualSheet() 17 | context.instance = create({ 18 | sheet: context.sheet, 19 | mode: strict, 20 | prefix: noprefix, 21 | hash: true, 22 | preflight: false, 23 | }) 24 | }) 25 | 26 | test.after.each(({ sheet }) => { 27 | sheet.reset() 28 | }) 29 | 30 | test('class names are hashed', ({ instance, sheet }) => { 31 | assert.is(instance.tw('group flex pt-4 text-center'), 'tw-1bk5mm5 tw-bibj42 tw-aysv3w tw-10s9vuy') 32 | assert.equal(sheet.target, [ 33 | '.tw-bibj42{display:flex}', 34 | '.tw-aysv3w{padding-top:1rem}', 35 | '.tw-10s9vuy{text-align:center}', 36 | ]) 37 | }) 38 | 39 | test.run() 40 | -------------------------------------------------------------------------------- /example/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 [these people](https://github.com/tw-in-js/twind/graphs/contributors) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .build 2 | build 3 | node_modules 4 | web_modules 5 | .DS_Store 6 | 7 | .cache 8 | dist 9 | website 10 | __generated__ 11 | tsconfig.dist*.json 12 | 13 | benchmarks/*.min.mjs 14 | 15 | # Svelte Component type defs 16 | *.svelte.tsx 17 | __svelte-jsx.d.ts 18 | __svelte-shims.d.ts 19 | 20 | # Logs 21 | logs 22 | *.log 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | lerna-debug.log* 27 | 28 | # Diagnostic reports (https://nodejs.org/api/report.html) 29 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 30 | 31 | # Runtime data 32 | pids 33 | *.pid 34 | *.seed 35 | *.pid.lock 36 | 37 | # Directory for instrumented libs generated by jscoverage/JSCover 38 | lib-cov 39 | 40 | # Coverage directory used by tools like istanbul 41 | coverage/ 42 | *.lcov 43 | 44 | # nyc test coverage 45 | .nyc_output 46 | 47 | # Dependency directories 48 | node_modules/ 49 | jspm_packages/ 50 | 51 | # TypeScript v1 declaration files 52 | typings/ 53 | 54 | # TypeScript cache 55 | *.tsbuildinfo 56 | 57 | # Optional npm cache directory 58 | .npm 59 | 60 | # Optional eslint cache 61 | .eslintcache 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | -------------------------------------------------------------------------------- /src/__tests__/variants.test.ts: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu' 2 | import * as assert from 'uvu/assert' 3 | 4 | import type { Instance, Configuration } from '../types' 5 | import type { VirtualSheet } from '../sheets/index' 6 | 7 | import { virtualSheet } from '../sheets/index' 8 | import { create, strict } from '../index' 9 | 10 | const test = suite<{ 11 | sheet: VirtualSheet 12 | setup: (config?: Configuration) => Instance 13 | }>('variants') 14 | 15 | test.before.each((context) => { 16 | context.sheet = virtualSheet() 17 | context.setup = (config?: Configuration): Instance => 18 | create({ sheet: context.sheet, mode: strict, preflight: false, prefix: false, ...config }) 19 | }) 20 | 21 | test('custom variants', ({ setup, sheet }) => { 22 | const { tw } = setup({ 23 | variants: { 24 | hocus: '&:hover,&:focus', 25 | }, 26 | }) 27 | 28 | assert.is(tw`text-blue(500 hocus:700)`, 'text-blue-500 hocus:text-blue-700') 29 | assert.equal(sheet.target, [ 30 | '.text-blue-500{--tw-text-opacity:1;color:#3b82f6;color:rgba(59,130,246,var(--tw-text-opacity))}', 31 | '.hocus\\:text-blue-700:hover,.hocus\\:text-blue-700:focus{--tw-text-opacity:1;color:#1d4ed8;color:rgba(29,78,216,var(--tw-text-opacity))}', 32 | ]) 33 | }) 34 | 35 | test.run() 36 | -------------------------------------------------------------------------------- /docs/recipes/use-with/svelte.md: -------------------------------------------------------------------------------- 1 | ## [Svelte](https://svelte.dev/) 2 | 3 | ```html 4 | 7 | 8 |
9 |

This is Twind!

10 |
11 | ``` 12 | 13 | > 🚀 [live and interactive demo](https://svelte.dev/repl/f0026dd2e9a44beaa14839d65117b852?version=3) 14 | 15 | ## Server Side Rendering 16 | 17 | ```js 18 | import { setup } from 'twind' 19 | import { virtualSheet, getStyleTag } from 'twind/sheets' 20 | 21 | import App from './app.svelte' 22 | 23 | const sheet = virtualSheet() 24 | 25 | setup({ ...sharedOptions, sheet }) 26 | 27 | function ssr() { 28 | // 1. Reset the sheet for a new rendering 29 | sheet.reset() 30 | 31 | // 2. Render the app 32 | const { head = '', html, css } = App.render({ 33 | /* options */ 34 | }) 35 | 36 | if (css && css.code) { 37 | head += `` 38 | } 39 | 40 | // 3. Create the style tag with all generated CSS rules 41 | head += getStyleTag(sheet) 42 | 43 | // 4. Generate the response html 44 | return ` 45 | 46 | ${head} 47 | ${html} 48 | 49 | ` 50 | } 51 | ``` 52 | -------------------------------------------------------------------------------- /src/colors/README.md: -------------------------------------------------------------------------------- 1 | [![Documentation](https://flat.badgen.net/badge/icon/Documentation?icon=awesome&label)](https://twind.dev/docs/modules/twind_colors.html) 2 | [![Github](https://flat.badgen.net/badge/icon/tw-in-js%2Ftwind%2Fsrc%2Fcolors?icon=github&label)](https://github.com/tw-in-js/twind/tree/main/src/colors) 3 | [![Module Size](https://flat.badgen.net/badgesize/brotli/https:/unpkg.com/twind/colors/colors.js?icon=jsdelivr&label&color=blue&cache=10800)](https://unpkg.com/twind/colors/colors.js 'brotli module size') 4 | [![Typescript](https://flat.badgen.net/badge/icon/included?icon=typescript&label)](https://unpkg.com/browse/twind/colors/colors.d.ts) 5 | 6 | `twind/colors` exposes all [Taiwlind v2 colors](https://tailwindcss.com/docs/customizing-colors#color-palette-reference) as named exports. 7 | 8 | ```js 9 | import * as colors from 'twind/colors' 10 | 11 | setup({ 12 | theme: { 13 | colors: { 14 | // Build your palette here 15 | gray: colors.trueGray, 16 | red: colors.red, 17 | blue: colors.lightBlue, 18 | yellow: colors.amber, 19 | }, 20 | }, 21 | }) 22 | ``` 23 | 24 | To extend the existing color palette use `theme.extend`: 25 | 26 | ```js 27 | import * as colors from 'twind/colors' 28 | 29 | setup({ 30 | theme: { 31 | extend: { 32 | colors, 33 | }, 34 | }, 35 | }) 36 | ``` 37 | -------------------------------------------------------------------------------- /src/twind/modes.ts: -------------------------------------------------------------------------------- 1 | import type { Mode } from '../types' 2 | 3 | import { join, noop } from '../internal/util' 4 | 5 | export const mode = (report: (message: string) => void): Mode => ({ 6 | unknown(section, key = [], optional, context) { 7 | if (!optional) { 8 | // TODO hint about possible values, did you mean ... 9 | this.report({ id: 'UNKNOWN_THEME_VALUE', key: join([section, ...key], '.') }, context) 10 | } 11 | }, 12 | 13 | report({ id, ...info }) { 14 | const message = `[${id}] ${JSON.stringify(info)}` 15 | // Generate a stacktrace that starts at callee site 16 | const stack = (new Error(message).stack || message).split('at ') 17 | 18 | // Drop all frames until we hit the first `tw` or `setup` call 19 | // We are using splice(1, 1) to keep the message header - this includes line break and "at " indentation 20 | for ( 21 | let frame: string | undefined; 22 | (frame = stack.splice(1, 1)[0]) && !/(^|\.)(tw|setup) /.test(frame); 23 | 24 | ) { 25 | /* no-op */ 26 | } 27 | 28 | // Put it back together 29 | return report(stack.join('at ')) 30 | }, 31 | }) 32 | 33 | export const warn = mode((message) => console.warn(message)) 34 | 35 | export const strict = mode((message) => { 36 | throw new Error(message) 37 | }) 38 | 39 | export const silent = mode(noop) 40 | -------------------------------------------------------------------------------- /src/sheets/virtual.test.ts: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu' 2 | import * as assert from 'uvu/assert' 3 | 4 | import { getStyleTag, virtualSheet } from './index' 5 | 6 | const test = suite('virtualSheet') 7 | 8 | test('insert', () => { 9 | const sheet = virtualSheet() 10 | 11 | sheet.insert('html{margin:0}', 0) 12 | assert.equal(sheet.target, ['html{margin:0}']) 13 | 14 | sheet.insert('body{margin:0}', 0) 15 | assert.equal(sheet.target, ['body{margin:0}', 'html{margin:0}']) 16 | 17 | sheet.insert('*{margin:0}', 1) 18 | assert.equal(sheet.target, ['body{margin:0}', '*{margin:0}', 'html{margin:0}']) 19 | }) 20 | 21 | test('reset', () => { 22 | const sheet = virtualSheet() 23 | 24 | sheet.insert('html{margin:0}', 0) 25 | assert.equal(sheet.target, ['html{margin:0}']) 26 | const snapshot = sheet.reset() 27 | assert.equal(sheet.target, []) 28 | 29 | sheet.insert('body{margin:0}', 0) 30 | assert.equal(sheet.target, ['body{margin:0}']) 31 | 32 | sheet.reset(snapshot) 33 | assert.equal(sheet.target, ['html{margin:0}']) 34 | }) 35 | 36 | test('getStyleTag', () => { 37 | const sheet = virtualSheet() 38 | 39 | sheet.insert('html{margin:0}', 0) 40 | sheet.insert('body{margin:0}', 1) 41 | 42 | assert.is(getStyleTag(sheet), '') 43 | 44 | assert.is( 45 | getStyleTag(sheet, { nonce: '123456' }), 46 | '', 47 | ) 48 | }) 49 | test.run() 50 | -------------------------------------------------------------------------------- /.github/workflows/website.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy Website 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build-and-deploy: 10 | runs-on: ubuntu-20.04 11 | steps: 12 | - name: Checkout 🛎️ 13 | uses: actions/checkout@v2 14 | with: 15 | # for the deployment to work correctly 16 | persist-credentials: false 17 | 18 | - name: Install 19 | uses: bahmutov/npm-install@v1 20 | 21 | - name: Build 🔧 22 | run: | 23 | mkdir website 24 | yarn typedoc 25 | 26 | sed 's% src="/_src_/index.js"% src="https://cdn.skypack.dev/twind"%g;s% src="/_src_/shim/index.js"% src="https://cdn.skypack.dev/twind/shim"%g' < example/landing.html > website/index.html 27 | 28 | echo '' > website/docs/modules.html 29 | 30 | mkdir website/api 31 | echo '' > website/api/index.html 32 | 33 | - name: Deploy 🚀 34 | uses: JamesIves/github-pages-deploy-action@3.7.1 35 | with: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | BASE_BRANCH: main 38 | BRANCH: gh-pages # The branch the action should deploy to. 39 | FOLDER: website # The folder the action should deploy. 40 | CLEAN: true # Automatically remove deleted files from the deploy branch 41 | CLEAN_EXCLUDE: '["CNAME", ".nojekyll"]' 42 | -------------------------------------------------------------------------------- /src/server/README.md: -------------------------------------------------------------------------------- 1 | This module provides asynchronous Node.JS **only** Static Extraction a.k.a. Server Side Rendering (SSR). In most cases the {@link twind/sheets | synchronous SSR} should be preferred. Please refer to the {@page Extract Styles aka SSR | SSR guide} for more details. 2 | 3 | > ❗ This is an experimental feature and only supported for Node.JS >=12. Use with care and please [report any issue](https://github.com/tw-in-js/twind/issues/new) you find. 4 | > Consider using the synchronous API when ever possible due to the relatively expensive nature of the [promise introspection API](https://docs.google.com/document/d/1rda3yKGHimKIhg5YeoAmCOtyURgsbTH_qaYR79FELlk/edit) provided by V8. 5 | > Async server side rendering is implemented using [async_hooks](https://nodejs.org/docs/latest-v14.x/api/async_hooks.html). Callback-based APIs and event emitters may not work or need special handling. 6 | 7 | ```js 8 | import { setup } from 'twind' 9 | import { asyncVirtualSheet, getStyleTag } from 'twind/server' 10 | 11 | const sheet = asyncVirtualSheet() 12 | 13 | setup({ ...sharedOptions, sheet }) 14 | 15 | async function ssr() { 16 | // 1. Reset the sheet for a new rendering 17 | sheet.reset() 18 | 19 | // 2. Render the app 20 | const body = await renderTheApp() 21 | 22 | // 3. Create the style tag with all generated CSS rules 23 | const styleTag = getStyleTag(sheet) 24 | 25 | // 4. Generate the response html 26 | return ` 27 | 28 | ${styleTag} 29 | ${body} 30 | 31 | ` 32 | } 33 | ``` 34 | -------------------------------------------------------------------------------- /src/sheets/dom.test.ts: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu' 2 | import * as assert from 'uvu/assert' 3 | 4 | import * as DOM from '../__fixtures__/dom-env' 5 | 6 | import { create, strict } from '../index' 7 | import { domSheet } from './index' 8 | 9 | const test = DOM.configure(suite('domSheet')) 10 | 11 | test('injects in to a style sheet element', () => { 12 | const nonce = Math.random().toString(36) 13 | 14 | const { tw, setup } = create() 15 | 16 | assert.not.ok(document.querySelector('#__twind')) 17 | 18 | setup({ sheet: domSheet({ nonce }), mode: strict, preflight: false }) 19 | 20 | const style = document.querySelector('#__twind') as HTMLStyleElement 21 | 22 | assert.is(style.tagName, 'STYLE') 23 | assert.is(style.nonce, nonce) 24 | 25 | assert.is(tw('group flex text-center md:text-left'), 'group flex text-center md:text-left') 26 | 27 | assert.is( 28 | style.textContent, 29 | [ 30 | '.flex{display:flex}', 31 | '.text-center{text-align:center}', 32 | '@media (min-width:768px){.md\\:text-left{text-align:left}}', 33 | ].join(''), 34 | ) 35 | 36 | // re-use existing stylesheet 37 | const { tw: tw2 } = create({ sheet: domSheet({ nonce }), mode: strict, preflight: false }) 38 | 39 | assert.is(tw2('font-bold'), 'font-bold') 40 | 41 | assert.is(document.querySelectorAll('#__twind').length, 1) 42 | 43 | assert.is( 44 | style.textContent, 45 | [ 46 | '.flex{display:flex}', 47 | '.text-center{text-align:center}', 48 | '@media (min-width:768px){.md\\:text-left{text-align:left}}', 49 | '.font-bold{font-weight:700}', 50 | ].join(''), 51 | ) 52 | }) 53 | 54 | test.run() 55 | -------------------------------------------------------------------------------- /src/twind/translate.ts: -------------------------------------------------------------------------------- 1 | import type { Context, CSSRules, Plugins, Rule, Falsy, InlineDirective } from '../types' 2 | 3 | import { join, tail } from '../internal/util' 4 | 5 | export const translate = ( 6 | plugins: Plugins, 7 | context: Context, 8 | ): ((rule: Rule, isTranslating?: boolean) => InlineDirective | CSSRules | string | Falsy) => ( 9 | rule, 10 | isTranslating, 11 | ) => { 12 | // If this is a inline directive - called it right away 13 | if (typeof rule.d === 'function') { 14 | return rule.d(context) 15 | } 16 | 17 | const parameters = rule.d.split('-') 18 | 19 | // Bail early for already hashed class names 20 | // Only if there are no variants and no negation 21 | // If there are variants or negation unknown directive will be reported 22 | if (!isTranslating && parameters[0] === 'tw' && rule.$ === rule.d) { 23 | return rule.$ 24 | } 25 | 26 | // Try to find a plugin to handle this directive 27 | // example 'bg-gradient-to-r' 28 | // 1. 'bg-gradient-to-r' -> parameters=['bg-gradient-to-r'] 29 | // 2. 'bg-gradient-to' -> parameters=['bg-gradient-to', 'r'] 30 | // 4. 'bg-gradient' -> parameters=['bg-gradient', 'to', 'r'] 31 | // 5. 'bg' -> parameters=['bg', 'gradient', 'to', 'r'] 32 | for (let index = parameters.length; index; index--) { 33 | const id = join(parameters.slice(0, index)) 34 | 35 | const plugin = plugins[id] 36 | 37 | if (plugin) { 38 | return typeof plugin === 'function' 39 | ? plugin(tail(parameters, index), context, id) 40 | : typeof plugin === 'string' 41 | ? context[isTranslating ? 'css' : 'tw'](plugin) 42 | : plugin 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/twind/inject.ts: -------------------------------------------------------------------------------- 1 | import type { Context, Sheet, Mode, SheetInit } from '../types' 2 | 3 | import { sortedInsertionIndex } from '../internal/util' 4 | 5 | import type { RuleWithPresedence } from './serialize' 6 | 7 | // Insert css rules using presedence to find the correct position within the sheet 8 | export const inject = ( 9 | sheet: Sheet, 10 | mode: Mode, 11 | init: SheetInit, 12 | context: Context, 13 | ): ((rule: RuleWithPresedence) => void) => { 14 | // An array of presedence by index within the sheet 15 | // always sorted 16 | let sortedPrecedences: number[] 17 | init((value = []) => (sortedPrecedences = value)) 18 | 19 | // Cache for already inserted css rules 20 | // to prevent double insertions 21 | let insertedRules: Set 22 | init>((value = new Set()) => (insertedRules = value)) 23 | 24 | return ({ r: css, p: presedence }) => { 25 | // If not already inserted 26 | if (!insertedRules.has(css)) { 27 | // Mark rule as inserted 28 | insertedRules.add(css) 29 | 30 | // Find the correct position 31 | const index = sortedInsertionIndex(sortedPrecedences, presedence) 32 | 33 | try { 34 | // Insert 35 | sheet.insert(css, index) 36 | 37 | // Update sorted index 38 | sortedPrecedences.splice(index, 0, presedence) 39 | } catch (error) { 40 | // Some thrown error a because of specific pseudo classes 41 | // let filter them to prevent unnecessary warnings 42 | // ::-moz-focus-inner 43 | // :-moz-focusring 44 | if (!/:-[mwo]/.test(css)) { 45 | mode.report({ id: 'INJECT_CSS_ERROR', css, error: error as Error }, context) 46 | } 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /benchmarks/README.md: -------------------------------------------------------------------------------- 1 | # Benchmarks 2 | 3 | > Run `yarn install` 4 | 5 | The following benchmarks are available: 6 | 7 | - `yarn bench css`: benchmark [`css()` function](https://emotion.sh/docs/@emotion/css) 8 | 9 | ``` 10 | # Object Styles 11 | twind (static): tw-11wt74w 12 | twind (dynamic): tw-11wt74w 13 | twind (css): tw-1fygejv 14 | otion: _1r5gb7q _17egndr _1cw4hmo _9gfcjl _vdgfbm _1qsuvl4 _1y5pc60 _1osl5h8 _807wit _sv3kep _ol9ofu _eeqxny _1uim63f _1aq9rcw _twccl2 _jwtbbb 15 | goober: go1683933151 16 | emotion: css-nr9dlk 17 | twind (static) x 4,157,870 ops/sec ±0.61% (91 runs sampled) 18 | twind (dynamic) x 27,569 ops/sec ±0.33% (94 runs sampled) 19 | twind (css) x 203,990 ops/sec ±0.32% (94 runs sampled) 20 | otion@0.6.2 x 53,592 ops/sec ±0.85% (96 runs sampled) 21 | goober@2.0.30 x 842,430 ops/sec ±1.10% (88 runs sampled) 22 | emotion@11.1.3 x 162,460 ops/sec ±0.75% (90 runs sampled) 23 | 24 | Fastest is: twind (static) 25 | 26 | # Template Literal Styles 27 | twind (tw): inline-block rounded py-2 my-2 mx-4 w-44 bg-transparent text-white border-2 border-solid border-white hover:text-black focus:border-2 focus:border-dashed focus:border-black text-sm md:text-base lg:text-lg 28 | twind (apply): tw-1m1idym 29 | twind (css): tw-1fygejv 30 | goober: go3227344850 31 | emotion: css-du3o4a 32 | twind (static) x 3,651,611 ops/sec ±0.61% (93 runs sampled) 33 | twind (tw) x 400,438 ops/sec ±0.35% (84 runs sampled) 34 | twind (apply) x 342,725 ops/sec ±0.37% (96 runs sampled) 35 | twind (css) x 270,020 ops/sec ±0.53% (95 runs sampled) 36 | goober@2.0.30 x 632,419 ops/sec ±0.59% (95 runs sampled) 37 | emotion@11.1.3 x 229,990 ops/sec ±0.17% (99 runs sampled) 38 | 39 | Fastest is: twind (static) 40 | ``` 41 | -------------------------------------------------------------------------------- /src/twind/prefix.ts: -------------------------------------------------------------------------------- 1 | import type { Prefixer } from '../types' 2 | 3 | import { cssPropertyAlias, cssPropertyPrefixFlags, cssValuePrefixFlags } from 'style-vendorizer' 4 | 5 | export const noprefix: Prefixer = (property: string, value: string, important?: boolean): string => 6 | `${property}:${value}${important ? ' !important' : ''}` 7 | 8 | export const autoprefix: Prefixer = ( 9 | property: string, 10 | value: string, 11 | important?: boolean, 12 | ): string => { 13 | let cssText = '' 14 | 15 | // Resolve aliases, e.g. `gap` -> `grid-gap` 16 | const propertyAlias = cssPropertyAlias(property) 17 | if (propertyAlias) cssText += `${noprefix(propertyAlias, value, important)};` 18 | 19 | // Prefix properties, e.g. `backdrop-filter` -> `-webkit-backdrop-filter` 20 | let flags = cssPropertyPrefixFlags(property) 21 | if (flags & 0b001) cssText += `-webkit-${noprefix(property, value, important)};` 22 | if (flags & 0b010) cssText += `-moz-${noprefix(property, value, important)};` 23 | if (flags & 0b100) cssText += `-ms-${noprefix(property, value, important)};` 24 | 25 | // Prefix values, e.g. `position: "sticky"` -> `position: "-webkit-sticky"` 26 | // Notice that flags don't overlap and property prefixing isn't needed here 27 | flags = cssValuePrefixFlags(property, value) 28 | if (flags & 0b001) cssText += `${noprefix(property, `-webkit-${value}`, important)};` 29 | if (flags & 0b010) cssText += `${noprefix(property, `-moz-${value}`, important)};` 30 | if (flags & 0b100) cssText += `${noprefix(property, `-ms-${value}`, important)};` 31 | 32 | /* Include the standardized declaration last */ 33 | /* https://css-tricks.com/ordering-css3-properties/ */ 34 | cssText += noprefix(property, value, important) 35 | 36 | return cssText 37 | } 38 | -------------------------------------------------------------------------------- /src/twind/decorate.ts: -------------------------------------------------------------------------------- 1 | import type { Context, CSSRules, Rule, DarkMode, ThemeScreenValue } from '../types' 2 | 3 | import { tail, escape, buildMediaQuery } from '../internal/util' 4 | 5 | let _: RegExpExecArray | null | readonly ThemeScreenValue[] | string 6 | 7 | export const GROUP_RE = /^:(group(?:(?!-focus).+?)*)-(.+)$/ 8 | 9 | // Wraps a CSS rule object with variant at-rules and pseudo classes 10 | // { '.selector': {...} } 11 | // => { '&:hover': { '.selector': {...} } } 12 | // => { '@media (mind-width: ...)': { '&:hover': { '.selector': {...} } } } 13 | export const decorate = ( 14 | darkMode: DarkMode, 15 | variants: Record, 16 | { theme, tag }: Context, 17 | ): ((translation: CSSRules, rule: Rule) => CSSRules) => { 18 | // Select the wrapper for a variant 19 | const applyVariant = (translation: CSSRules, variant: string): CSSRules => { 20 | // Check responsive 21 | if ((_ = theme('screens', tail(variant), ''))) { 22 | return { [buildMediaQuery(_)]: translation } 23 | } 24 | 25 | // Dark mode 26 | if (variant === ':dark' && darkMode === 'class') { 27 | return { [`.dark &`]: translation } 28 | } 29 | 30 | // Groups classes like: group-focus and group-hover 31 | // these need to add a marker selector with the pseudo class 32 | // => '.group:focus .group-focus:selector' 33 | if ((_ = GROUP_RE.exec(variant))) { 34 | return { [`.${escape(tag((_ as RegExpExecArray)[1]))}:${_[2]} &`]: translation } 35 | } 36 | 37 | // Check other well known variants 38 | // and fallback to pseudo class or element 39 | return { [variants[tail(variant)] || '&' + variant]: translation } 40 | } 41 | 42 | // Apply variants depth-first 43 | return (translation, rule) => rule.v.reduceRight(applyVariant, translation) 44 | } 45 | -------------------------------------------------------------------------------- /src/__tests__/dom.test.ts: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu' 2 | import * as assert from 'uvu/assert' 3 | 4 | import * as DOM from '../__fixtures__/dom-env' 5 | 6 | import { create, strict } from '..' 7 | 8 | const test = DOM.configure(suite('dom')) 9 | 10 | test('injects in to a style sheet element', () => { 11 | const nonce = Math.random().toString(36) 12 | 13 | const { tw, setup } = create() 14 | 15 | assert.not.ok(document.querySelector('#__twind')) 16 | 17 | setup({ nonce, mode: strict, preflight: false }) 18 | 19 | const style = document.querySelector('#__twind') as HTMLStyleElement 20 | 21 | assert.is(style.tagName, 'STYLE') 22 | assert.is(style.nonce, nonce) 23 | 24 | assert.is(tw('group flex text-center md:text-left'), 'group flex text-center md:text-left') 25 | 26 | assert.equal( 27 | [...(style.sheet?.cssRules || [])].map((rule) => rule.cssText), 28 | [ 29 | '.flex {display: flex;}', 30 | '.text-center {text-align: center;}', 31 | '@media (min-width:768px) {.md\\:text-left {text-align: left;}}', 32 | ], 33 | ) 34 | 35 | // re-use existing stylesheet 36 | const { tw: tw2 } = create({ mode: strict, preflight: false }) 37 | 38 | assert.is(tw2('group flex text-center md:text-left'), 'group flex text-center md:text-left') 39 | 40 | assert.is(document.querySelectorAll('#__twind').length, 1) 41 | 42 | assert.equal( 43 | [...(style.sheet?.cssRules || [])].map((rule) => rule.cssText), 44 | [ 45 | '.flex {display: flex;}', 46 | '.text-center {text-align: center;}', 47 | '@media (min-width:768px) {.md\\:text-left {text-align: left;}}', 48 | '.flex {display: flex;}', 49 | '.text-center {text-align: center;}', 50 | '@media (min-width:768px) {.md\\:text-left {text-align: left;}}', 51 | ], 52 | ) 53 | }) 54 | 55 | test.run() 56 | -------------------------------------------------------------------------------- /src/__tests__/prefix.test.ts: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu' 2 | import * as assert from 'uvu/assert' 3 | 4 | import type { Instance } from '../types' 5 | import type { VirtualSheet } from '../sheets/index' 6 | 7 | import { virtualSheet } from '../sheets/index' 8 | import { create, strict } from '../index' 9 | 10 | const test = suite<{ 11 | sheet: VirtualSheet 12 | instance: Instance 13 | }>('prefix') 14 | 15 | test.before((context) => { 16 | context.sheet = virtualSheet() 17 | context.instance = create({ 18 | sheet: context.sheet, 19 | mode: strict, 20 | preflight: false, 21 | plugins: { 22 | 'scroll-snap': (parts) => { 23 | return { 'scroll-snap-type': parts[0] } 24 | }, 25 | }, 26 | }) 27 | }) 28 | 29 | test.after.each(({ sheet }) => { 30 | sheet.reset() 31 | }) 32 | 33 | test('add prefix', ({ sheet, instance }) => { 34 | assert.is( 35 | instance.tw('sticky scroll-snap-x appearance-menulist-button'), 36 | 'sticky scroll-snap-x appearance-menulist-button', 37 | ) 38 | assert.equal(sheet.target, [ 39 | '.sticky{position:-webkit-sticky;position:sticky}', 40 | '.appearance-menulist-button{-webkit-appearance:menulist-button;-moz-appearance:menulist-button;appearance:menulist-button}', 41 | '.scroll-snap-x{scroll-snap-type:x}', 42 | ]) 43 | }) 44 | 45 | test('add prefix with important', ({ sheet, instance }) => { 46 | assert.is( 47 | instance.tw('sticky! scroll-snap-x! appearance-menulist-button!'), 48 | 'sticky! scroll-snap-x! appearance-menulist-button!', 49 | ) 50 | assert.equal(sheet.target, [ 51 | '.sticky\\!{position:-webkit-sticky !important;position:sticky !important}', 52 | '.appearance-menulist-button\\!{-webkit-appearance:menulist-button !important;-moz-appearance:menulist-button !important;appearance:menulist-button !important}', 53 | '.scroll-snap-x\\!{scroll-snap-type:x !important}', 54 | ]) 55 | }) 56 | 57 | test.run() 58 | -------------------------------------------------------------------------------- /docs/recipes/use-with/nextjs.md: -------------------------------------------------------------------------------- 1 | ## [Next.js](https://nextjs.org/) 2 | 3 | ```js 4 | import { tw } from 'twind' 5 | 6 | export default function IndexPage() { 7 | return ( 8 |
9 |

10 | This is Twind! 11 |

12 |
13 | ) 14 | } 15 | ``` 16 | 17 | ## Server Side Rendering 18 | 19 | > 💡 The [tw-in-js/example-next](https://github.com/tw-in-js/example-next) repository uses this setup. 20 | 21 | ```js 22 | /* twind.config.js */ 23 | export default { 24 | /* Shared config */ 25 | } 26 | ``` 27 | 28 | ```js 29 | /* pages/_app.js */ 30 | import App from 'next/app' 31 | 32 | import { setup } from 'twind' 33 | import twindConfig from '../twind.config' 34 | 35 | if (typeof window !== 'undefined') { 36 | setup(twindConfig) 37 | } 38 | 39 | export default App 40 | ``` 41 | 42 | ```js 43 | /* pages/_document.js */ 44 | 45 | import Document from 'next/document' 46 | import * as React from 'react' 47 | 48 | import { setup } from 'twind' 49 | import { asyncVirtualSheet, getStyleTagProperties } from 'twind/server' 50 | 51 | import twindConfig from '../twind.config' 52 | 53 | const sheet = asyncVirtualSheet() 54 | 55 | setup({ ...twindConfig, sheet }) 56 | 57 | export default class MyDocument extends Document { 58 | static async getInitialProps(ctx) { 59 | sheet.reset() 60 | 61 | const initialProps = await Document.getInitialProps(ctx) 62 | 63 | const { id, textContent } = getStyleTagProperties(sheet) 64 | 65 | const styleProps = { 66 | id, 67 | key: id, 68 | dangerouslySetInnerHTML: { 69 | __html: textContent, 70 | }, 71 | } 72 | 73 | return { 74 | ...initialProps, 75 | styles: [...initialProps.styles, React.createElement('style', styleProps)], 76 | } 77 | } 78 | } 79 | ``` 80 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/tsconfig", 3 | "include": ["src"], 4 | "typedocOptions": { 5 | "entryPoints": [ 6 | "./src/index.ts", 7 | "./src/colors/index.ts", 8 | "./src/css/index.ts", 9 | "./src/observe/index.ts", 10 | "./src/server/index.ts", 11 | "./src/sheets/index.ts", 12 | "./src/shim/index.ts", 13 | "./src/shim/server/index.ts" 14 | ], 15 | "out": "./website/docs", 16 | "includeVersion": true, 17 | "readme": "./README.md", 18 | "excludePrivate": true, 19 | "excludeInternal": true, 20 | "includes": "./", 21 | "theme": "pages-plugin", 22 | "pages": "./docs/pages.json" 23 | }, 24 | "compilerOptions": { 25 | // During development for Node v10.8 support 26 | "target": "es2018", 27 | "module": "ESNext", 28 | "lib": ["esnext", "dom", "dom.iterable"], 29 | // ensure that nobody can accidentally use this config for a build 30 | "noEmit": true, 31 | "declaration": true, 32 | "declarationMap": true, 33 | "noEmitOnError": true, 34 | "noErrorTruncation": true, 35 | // Enforce using `import type` instead of `import` for Types 36 | "importsNotUsedAsValues": "error", 37 | "allowJs": true, 38 | // Generate inline sourcemaps 39 | "sourceMap": false, 40 | "inlineSourceMap": true, 41 | "inlineSources": true, 42 | // Search under node_modules for non-relative imports. 43 | "moduleResolution": "node", 44 | // Enable strictest settings like strictNullChecks & noImplicitAny. 45 | "strict": true, 46 | // Disallow features that require cross-file information for emit. 47 | "isolatedModules": true, 48 | "allowSyntheticDefaultImports": true, 49 | // Import non-ES modules as default imports. 50 | "esModuleInterop": true, 51 | // Allow to import json files 52 | "resolveJsonModule": true, 53 | "skipLibCheck": true, 54 | "forceConsistentCasingInFileNames": true 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/twind/helpers.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/consistent-type-assertions */ 2 | import type { CSSRules } from '../types' 3 | 4 | import { joinTruthy } from '../internal/util' 5 | 6 | const positions = (resolve: (position: string) => undefined | string[] | void) => ( 7 | value: string | string[] | undefined, 8 | position: string, 9 | prefix?: string, 10 | suffix?: string, 11 | ): CSSRules | undefined => { 12 | if (value) { 13 | const properties = position && resolve(position) 14 | 15 | if (properties && properties.length > 0) { 16 | return properties.reduce((declarations, property) => { 17 | declarations[joinTruthy([prefix, property, suffix])] = value 18 | return declarations 19 | }, {} as CSSRules) 20 | } 21 | } 22 | } 23 | 24 | export const corners = positions( 25 | (key) => 26 | (({ 27 | t: ['top-left', 'top-right'], 28 | r: ['top-right', 'bottom-right'], 29 | b: ['bottom-left', 'bottom-right'], 30 | l: ['bottom-left', 'top-left'], 31 | tl: ['top-left'], 32 | tr: ['top-right'], 33 | bl: ['bottom-left'], 34 | br: ['bottom-right'], 35 | } as Record)[key]), 36 | ) 37 | 38 | export const expandEdges = (key: string): string[] | undefined => { 39 | const parts = (({ x: 'lr', y: 'tb' } as Record)[key] || key || '') 40 | .split('') 41 | .sort() 42 | 43 | for (let index = parts.length; index--; ) { 44 | if ( 45 | !(parts[index] = ({ 46 | t: 'top', 47 | r: 'right', 48 | b: 'bottom', 49 | l: 'left', 50 | } as Record)[parts[index]]) 51 | ) 52 | return 53 | } 54 | 55 | if (parts.length) return parts 56 | } 57 | 58 | // Support several edges like 'tr' 59 | // 'x' and 'y' can not be combined with others because size 'xl' 60 | // Every char must be a edge position 61 | // Sort to have consistent declaration ordering 62 | export const edges = positions(expandEdges) 63 | /* eslint-enable @typescript-eslint/consistent-type-assertions */ 64 | -------------------------------------------------------------------------------- /src/twind/instance.ts: -------------------------------------------------------------------------------- 1 | import type { Configuration, Context, Instance } from '../types' 2 | 3 | import { configure } from './configure' 4 | 5 | export const create = (config?: Configuration): Instance => { 6 | // We are using lazy variables to trigger setup either 7 | // on first `setup` or `tw` call 8 | // 9 | // This allows to provide one-time lazy configuration 10 | // 11 | // These variables are not named `tw` and `setup` 12 | // as we use `tw` and `setup` to find the callee site 13 | // during stacktrace generation 14 | // This allows the error stacktrace to start at the call site. 15 | 16 | // Used by `tw` 17 | let process = (tokens: unknown[]): string => { 18 | // eslint-disable-next-line @typescript-eslint/no-use-before-define 19 | init() 20 | return process(tokens) 21 | } 22 | 23 | // Used by `setup` 24 | let init = (config?: Configuration): void => { 25 | // Replace implementation with configured ones 26 | // `process`: the real one 27 | // `init`: invokes `mode.report` with `LATE_SETUP_CALL` 28 | // eslint-disable-next-line @typescript-eslint/no-extra-semi 29 | ;({ process, init } = configure(config)) 30 | } 31 | 32 | // If we got a config, start right away 33 | if (config) init(config) 34 | 35 | let context: Context 36 | const fromContext = (key: Key) => (): Context[Key] => { 37 | if (!context) { 38 | process([ 39 | (_: Context) => { 40 | context = _ 41 | return '' 42 | }, 43 | ]) 44 | } 45 | 46 | return context[key] 47 | } 48 | 49 | // The instance methods delegate to the lazy ones. 50 | // This ensures that after setup we use the configured 51 | // `process` and `setup` fails. 52 | return { 53 | tw: Object.defineProperties((...tokens: unknown[]) => process(tokens), { 54 | theme: { 55 | get: fromContext('theme'), 56 | }, 57 | // css: { 58 | // get: fromContext('css'), 59 | // }, 60 | // tag: { 61 | // get: fromContext('tag'), 62 | // }, 63 | }), 64 | 65 | setup: (config) => init(config), 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /docs/getting-started/customize-the-theme.md: -------------------------------------------------------------------------------- 1 | Applying a new theme or extending the default is probably the most common customization. For maximum compatibility and ease of adoption, theming in Twind works exactly the same as [theming in Tailwind](https://tailwindcss.com/docs/theme). 2 | 3 | 4 | 5 | 6 | - [Colors](#colors) 7 | - [Referencing other values](#referencing-other-values) 8 | 9 | 10 | 11 | Here is an example of overriding and extending values in the theme: 12 | 13 | ```js 14 | import { setup } from 'twind' 15 | 16 | setup({ 17 | theme: { 18 | fontFamily: { 19 | sans: ['Helvetica', 'sans-serif'], 20 | serif: ['Times', 'serif'], 21 | }, 22 | extend: { 23 | spacing: { 24 | 128: '32rem', 25 | 144: '36rem', 26 | }, 27 | }, 28 | }, 29 | }) 30 | ``` 31 | 32 | ## Colors 33 | 34 | The Tailwind v2 [extended color palette](https://tailwindcss.com/docs/customizing-colors#color-palette-reference) is available as {@link twind/colors}: 35 | 36 | ```js 37 | import * as colors from 'twind/colors' 38 | 39 | setup({ 40 | theme: { 41 | colors: { 42 | // Build your palette here 43 | gray: colors.trueGray, 44 | red: colors.red, 45 | blue: colors.lightBlue, 46 | yellow: colors.amber, 47 | }, 48 | }, 49 | }) 50 | ``` 51 | 52 | To extend the existing color palette use `theme.extend`: 53 | 54 | ```js 55 | import * as colors from 'twind/colors' 56 | 57 | setup({ 58 | theme: { 59 | extend: { 60 | colors, 61 | }, 62 | }, 63 | }) 64 | ``` 65 | 66 | ## Referencing other values 67 | 68 | If you need to reference another value in your theme, you can do so by providing a closure instead of a static value. The closure will receive a `theme()` function that you can use to look up other values in your theme. 69 | 70 | ```js 71 | setup({ 72 | theme: { 73 | fill: (theme) => theme('colors'), 74 | }, 75 | }) 76 | ``` 77 | 78 |
79 | 80 | Continue to {@page Tailwind Extensions} 81 | -------------------------------------------------------------------------------- /src/colors/colors.test.ts: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu' 2 | import * as assert from 'uvu/assert' 3 | 4 | import { virtualSheet } from '../sheets/index' 5 | 6 | import { create, strict } from '../index' 7 | import * as colors from './index' 8 | import type { ThemeResolver, TW } from '../types' 9 | 10 | const test = suite('twind/colors') 11 | 12 | test('new colors are available', () => { 13 | const sheet = virtualSheet() 14 | const { tw } = create({ 15 | sheet, 16 | mode: strict, 17 | preflight: false, 18 | prefix: false, 19 | theme: { extend: { colors } }, 20 | }) 21 | 22 | assert.is(tw('text-rose-200'), 'text-rose-200') 23 | assert.equal(sheet.target, [ 24 | '.text-rose-200{--tw-text-opacity:1;color:#fecdd3;color:rgba(254,205,211,var(--tw-text-opacity))}', 25 | ]) 26 | }) 27 | 28 | const getTheme = (tw: TW): ThemeResolver => { 29 | let theme: ThemeResolver 30 | 31 | tw((context) => { 32 | theme = context.theme 33 | return '' 34 | }) 35 | 36 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 37 | return theme! 38 | } 39 | 40 | test('default theme colors match tailwind v2 config', () => { 41 | const { tw } = create({ mode: strict, prefix: false }) 42 | 43 | const theme = getTheme(tw) 44 | 45 | assert.equal( 46 | { 47 | // https://github.com/tailwindlabs/tailwindcss/blob/master/stubs/defaultConfig.stub.js#L19 48 | black: colors.black, 49 | white: colors.white, 50 | gray: colors.coolGray, 51 | red: colors.red, 52 | yellow: colors.amber, 53 | green: colors.emerald, 54 | blue: colors.blue, 55 | indigo: colors.indigo, 56 | purple: colors.violet, 57 | pink: colors.pink, 58 | }, 59 | { 60 | black: theme('colors', 'black'), 61 | white: theme('colors', 'white'), 62 | gray: theme('colors', 'gray'), 63 | red: theme('colors', 'red'), 64 | yellow: theme('colors', 'yellow'), 65 | green: theme('colors', 'green'), 66 | blue: theme('colors', 'blue'), 67 | indigo: theme('colors', 'indigo'), 68 | purple: theme('colors', 'purple'), 69 | pink: theme('colors', 'pink'), 70 | }, 71 | ) 72 | }) 73 | 74 | test.run() 75 | -------------------------------------------------------------------------------- /src/__tests__/setup.test.ts: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu' 2 | import * as assert from 'uvu/assert' 3 | 4 | import { virtualSheet } from '../sheets/index' 5 | 6 | import { create, strict } from '../index' 7 | 8 | const test = suite('setup') 9 | 10 | test('theme extend section callback', () => { 11 | const sheet = virtualSheet() 12 | const { tw } = create({ 13 | sheet, 14 | mode: strict, 15 | preflight: false, 16 | prefix: false, 17 | theme: { 18 | extend: { 19 | backgroundImage: (theme) => ({ 20 | // Use a own gradient 21 | 'gradient-radial': `radial-gradient(${theme('colors.blue.500')}, ${theme( 22 | 'colors.red.500', 23 | )});`, 24 | // Integrate with gradient colors stops (from-*, via-*, to-*) 25 | 'gradient-15': 26 | 'linear-gradient(.15turn, var(--tw-gradient-stops,var(--tw-gradient-from,transparent),var(--tw-gradient-to,transparent)))', 27 | }), 28 | }, 29 | }, 30 | }) 31 | 32 | assert.is(tw`bg-gradient-radial`, 'bg-gradient-radial') 33 | assert.equal(sheet.target, [ 34 | '.bg-gradient-radial{background-image:radial-gradient(#3b82f6, #ef4444);}', 35 | ]) 36 | sheet.reset() 37 | 38 | assert.is( 39 | tw`bg-gradient-15 from-green-400 to-blue-500`, 40 | 'bg-gradient-15 from-green-400 to-blue-500', 41 | ) 42 | assert.equal(sheet.target, [ 43 | '.from-green-400{--tw-gradient-from:#34d399;--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to,rgba(52,211,153,0))}', 44 | '.bg-gradient-15{background-image:linear-gradient(.15turn, var(--tw-gradient-stops,var(--tw-gradient-from,transparent),var(--tw-gradient-to,transparent)))}', 45 | '.to-blue-500{--tw-gradient-to:#3b82f6}', 46 | ]) 47 | sheet.reset() 48 | }) 49 | 50 | test('theme extend value callback', () => { 51 | const sheet = virtualSheet() 52 | const { tw } = create({ 53 | sheet, 54 | mode: strict, 55 | preflight: false, 56 | prefix: false, 57 | theme: { 58 | fill: (theme) => theme('colors'), 59 | }, 60 | }) 61 | 62 | assert.is(tw`fill-red-500`, 'fill-red-500') 63 | assert.equal(sheet.target, ['.fill-red-500{fill:#ef4444}']) 64 | }) 65 | 66 | test.run() 67 | -------------------------------------------------------------------------------- /docs/recipes/use-with/wmr.md: -------------------------------------------------------------------------------- 1 | ## [WMR](https://github.com/preactjs/wmr) 2 | 3 | > 💡 The [tw-in-js/example-wmr](https://github.com/tw-in-js/example-wmr) repository uses this setup. 4 | 5 | First we need to add `@rollup/plugin-json` to the dependencies. 6 | 7 | ```sh 8 | npm install -D @rollup/plugin-json 9 | ``` 10 | 11 | Next we create or modify the following files: 12 | 13 | **wmr.config.mjs** 14 | 15 | ```js 16 | /** @param {import('wmr').Options} config */ 17 | export default async function (config) { 18 | if (config.mode === 'build') { 19 | const { default: json } = await import('@rollup/plugin-json') 20 | config.plugins.push(json()) 21 | } 22 | } 23 | ``` 24 | 25 | **public/twind.config.js** 26 | 27 | ```js 28 | export default { 29 | /* Shared config */ 30 | } 31 | ``` 32 | 33 | **public/index.js** 34 | 35 | ```js 36 | import hydrate from 'preact-iso/hydrate' 37 | 38 | import { setup } from 'twind' 39 | // Or if you are using twind/shim 40 | // import { setup } from 'twind/shim' 41 | 42 | import twindConfig from './twind.config' 43 | 44 | if (typeof window !== 'undefined') { 45 | setup(twindConfig) 46 | } 47 | 48 | export function App() { 49 | /* Your app */ 50 | } 51 | 52 | hydrate() 53 | 54 | export async function prerender(data) { 55 | const { default: prerender } = await import('./prerender') 56 | 57 | return prerender() 58 | // Or if you are using twind/shim 59 | // return prerender(, { shim: true }) 60 | } 61 | ``` 62 | 63 | **public/prerender.js** 64 | 65 | ```js 66 | import prerender from 'preact-iso/prerender' 67 | 68 | import { setup } from 'twind' 69 | import { asyncVirtualSheet, getStyleTagProperties, shim } from 'twind/server' 70 | 71 | import twindConfig from './twind.config' 72 | 73 | const sheet = asyncVirtualSheet() 74 | 75 | setup({ ...twindConfig, sheet }) 76 | 77 | export default async (app, options = {}) => { 78 | sheet.reset() 79 | 80 | const result = await prerender(app) 81 | 82 | if (options.shim) { 83 | result.html = shim(result.html) 84 | } 85 | 86 | const { id, textContent } = getStyleTagProperties(sheet) 87 | 88 | result.html = `${result.html}` 89 | 90 | return result 91 | } 92 | ``` 93 | -------------------------------------------------------------------------------- /src/server/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | // ^^^^ This comment is need to prevent browser bundles of this file 3 | 4 | /** 5 | * [[include:src/server/README.md]] 6 | * 7 | * @packageDocumentation 8 | * @module twind/server 9 | */ 10 | 11 | import { executionAsyncId, createHook } from 'async_hooks' 12 | 13 | import type { Sheet, SheetInit } from '../types' 14 | import { virtualSheet } from '../sheets/index' 15 | 16 | export type { Storage, StyleTagProperties, StyleTagSheet, VirtualSheet } from '../sheets/index' 17 | export { virtualSheet, getStyleTag, getStyleTagProperties } from '../sheets/index' 18 | export { shim } from '../shim/server/index' 19 | 20 | export interface AsyncVirtualSheet extends Sheet { 21 | readonly target: readonly string[] 22 | init: SheetInit 23 | reset: () => void 24 | enable: () => void 25 | disable: () => void 26 | } 27 | 28 | interface Snapshot { 29 | state: unknown[] | undefined 30 | } 31 | 32 | export const asyncVirtualSheet = (): AsyncVirtualSheet => { 33 | const sheet = virtualSheet() 34 | const initial = sheet.reset() 35 | 36 | const store = new Map() 37 | 38 | const asyncHook = createHook({ 39 | init(asyncId, type, triggerAsyncId) { 40 | const snapshot = store.get(triggerAsyncId) 41 | 42 | if (snapshot) { 43 | store.set(asyncId, snapshot) 44 | } 45 | }, 46 | 47 | before(asyncId) { 48 | const snapshot = store.get(asyncId) 49 | 50 | if (snapshot) { 51 | sheet.reset(snapshot.state) 52 | } 53 | }, 54 | 55 | after(asyncId) { 56 | const snapshot = store.get(asyncId) 57 | 58 | if (snapshot) { 59 | snapshot.state = sheet.reset(initial) 60 | } 61 | }, 62 | 63 | destroy(asyncId) { 64 | store.delete(asyncId) 65 | }, 66 | }).enable() 67 | 68 | return { 69 | get target() { 70 | return sheet.target 71 | }, 72 | insert: sheet.insert, 73 | init: sheet.init, 74 | reset: () => { 75 | const asyncId = executionAsyncId() 76 | 77 | const snapshot = store.get(asyncId) 78 | 79 | if (snapshot) { 80 | snapshot.state = undefined 81 | } else { 82 | store.set(asyncId, { state: undefined }) 83 | } 84 | 85 | sheet.reset() 86 | }, 87 | enable: () => asyncHook.enable(), 88 | disable: () => asyncHook.disable(), 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /docs/recipes/use-with/preact.md: -------------------------------------------------------------------------------- 1 | ## [Preact](https://preactjs.com/) 2 | 3 | ```js 4 | import { h, render } from 'preact' 5 | 6 | import { tw } from 'twind' 7 | 8 | render( 9 |
10 |

This is Twind!

11 |
, 12 | document.body, 13 | ) 14 | ``` 15 | 16 | ## htm/preact - [Preact](https://preactjs.com/) with [htm](https://github.com/developit/htm) 17 | 18 | ```js 19 | import { render } from 'preact' 20 | import { html } from 'htm/preact' 21 | 22 | import { tw } from 'twind' 23 | 24 | render( 25 | html` 26 |
27 |

28 | This is Twind! 29 |

30 |
31 | `, 32 | document.body, 33 | ) 34 | ``` 35 | 36 | > 🚀 [live and interactive demo](https://esm.codes/#aW1wb3J0IHsgaCwgcmVuZGVyIH0gZnJvbSAnaHR0cHM6Ly9jZG4uc2t5cGFjay5kZXYvcHJlYWN0JwppbXBvcnQgaHRtIGZyb20gJ2h0dHBzOi8vY2RuLnNreXBhY2suZGV2L2h0bScKCmltcG9ydCB7IHR3IH0gZnJvbSAnaHR0cHM6Ly9jZG4uc2t5cGFjay5kZXYvdHdpbmQnCgpjb25zdCBodG1sID0gaHRtLmJpbmQoaCkKCnJlbmRlcigKICBodG1sYAogICAgPG1haW4gY2xhc3NOYW1lPSIke3R3YGgtc2NyZWVuIGJnLXB1cnBsZS00MDAgZmxleCBpdGVtcy1jZW50ZXIganVzdGlmeS1jZW50ZXJgfSI+CiAgICAgIDxoMSBjbGFzc05hbWU9IiR7dHdgZm9udC1ib2xkIHRleHQoY2VudGVyIDV4bCB3aGl0ZSBzbTpncmF5LTgwMCBtZDpwaW5rLTcwMClgfSI+CiAgICAgICAgVGhpcyBpcyBUd2luZCEKICAgICAgPC9oMT4KICAgIDwvbWFpbj4KICBgLAogIGRvY3VtZW50LmJvZHksCikK) 37 | 38 | ## Server Side Rendering 39 | 40 | ```js 41 | import renderToString from 'preact-render-to-string' 42 | 43 | import { setup } from 'twind' 44 | import { virtualSheet, getStyleTag } from 'twind/sheets' 45 | 46 | import App from './app' 47 | 48 | const sheet = virtualSheet() 49 | 50 | setup({ ...sharedOptions, sheet }) 51 | 52 | function ssr() { 53 | // 1. Reset the sheet for a new rendering 54 | sheet.reset() 55 | 56 | // 2. Render the app 57 | const body = renderToString() 58 | 59 | // 3. Create the style tag with all generated CSS rules 60 | const styleTag = getStyleTag(sheet) 61 | 62 | // 4. Generate the response html 63 | return ` 64 | 65 | ${styleTag} 66 | ${body} 67 | 68 | ` 69 | } 70 | ``` 71 | -------------------------------------------------------------------------------- /docs/recipes/use-with/lit-element.md: -------------------------------------------------------------------------------- 1 | ## [LitElement](https://lit-element.polymer-project.org/) 2 | 3 | > ❗ This example is using [Constructable Stylesheet Objects](https://wicg.github.io/construct-stylesheets/) and `DocumentOrShadowRoot.adoptedStyleSheets` which have [limited browser support](https://caniuse.com/mdn-api_documentorshadowroot_adoptedstylesheets) at the moment (December 2020). 4 | 5 | ```js 6 | import { LitElement, html } from 'lit-element' 7 | import { create, cssomSheet } from 'twind' 8 | 9 | // 1. Create separate CSSStyleSheet 10 | const sheet = cssomSheet({ target: new CSSStyleSheet() }) 11 | 12 | // 2. Use that to create an own twind instance 13 | const { tw } = create({ sheet }) 14 | 15 | class TwindElement extends LitElement { 16 | // 3. Apply the same style to each instance of this component 17 | static styles = [sheet.target] 18 | 19 | render() { 20 | // 4. Use "own" tw function 21 | return html` 22 |
23 |

24 | This is Twind! 25 |

26 |
27 | ` 28 | } 29 | } 30 | 31 | customElements.define('twind-element', TwindElement) 32 | 33 | document.body.innerHTML = '' 34 | ``` 35 | 36 | > 🚀 [live and interactive demo](https://esm.codes/#aW1wb3J0IHsgTGl0RWxlbWVudCwgaHRtbCB9IGZyb20gJ2h0dHBzOi8vY2RuLnNreXBhY2suZGV2L2xpdC1lbGVtZW50JwppbXBvcnQgeyBjcmVhdGUsIGNzc29tU2hlZXQgfSBmcm9tICdodHRwczovL2Nkbi5za3lwYWNrLmRldi90d2luZCcKCmNvbnN0IHNoZWV0ID0gY3Nzb21TaGVldCh7IHRhcmdldDogbmV3IENTU1N0eWxlU2hlZXQoKSB9KQoKY29uc3QgeyB0dyB9ID0gY3JlYXRlKHsgc2hlZXQgfSkKCmNsYXNzIFR3aW5kRWxlbWVudCBleHRlbmRzIExpdEVsZW1lbnQgewogIGNyZWF0ZVJlbmRlclJvb3QoKSB7CiAgICBjb25zdCBzaGFkb3cgPSBzdXBlci5jcmVhdGVSZW5kZXJSb290KCkKICAgIHNoYWRvdy5hZG9wdGVkU3R5bGVTaGVldHMgPSBbc2hlZXQudGFyZ2V0XQogICAgcmV0dXJuIHNoYWRvdwogIH0KCiAgcmVuZGVyKCkgewogICAgcmV0dXJuIGh0bWxgCiAgICAgIDxtYWluIGNsYXNzPSIke3R3YGgtc2NyZWVuIGJnLXB1cnBsZS00MDAgZmxleCBpdGVtcy1jZW50ZXIganVzdGlmeS1jZW50ZXJgfSI+CiAgICAgICAgPGgxIGNsYXNzPSIke3R3YGZvbnQtYm9sZCB0ZXh0KGNlbnRlciA1eGwgd2hpdGUgc206Z3JheS04MDAgbWQ6cGluay03MDApYH0iPgogICAgICAgICAgVGhpcyBpcyBUd2luZCEKICAgICAgICA8L2gxPgogICAgICA8L21haW4+CiAgICBgCiAgfQp9CgpjdXN0b21FbGVtZW50cy5kZWZpbmUoJ3R3aW5kLWVsZW1lbnQnLCBUd2luZEVsZW1lbnQpOwoKZG9jdW1lbnQuYm9keS5pbm5lckhUTUwgPSAnPHR3aW5kLWVsZW1lbnQ+PC90d2luZC1lbGVtZW50PicK) 37 | -------------------------------------------------------------------------------- /src/twind/directive.ts: -------------------------------------------------------------------------------- 1 | import type { Context, Directive, MaybeThunk } from '../types' 2 | 3 | import { ensureMaxSize, evalThunk } from '../internal/util' 4 | 5 | let isFunctionFree: boolean 6 | const detectFunction = (key: string, value: unknown): unknown => { 7 | if (typeof value === 'function') { 8 | isFunctionFree = false 9 | } 10 | 11 | return value 12 | } 13 | 14 | const stringify = (data: unknown): string | false => { 15 | isFunctionFree = true 16 | 17 | const key = JSON.stringify(data, detectFunction) 18 | 19 | return isFunctionFree && key 20 | } 21 | 22 | /* eslint-disable @typescript-eslint/no-explicit-any */ 23 | const cacheByFactory = new WeakMap< 24 | (data: any, context: Context) => any, 25 | Map> 26 | >() 27 | /* eslint-enable @typescript-eslint/no-explicit-any */ 28 | 29 | /** 30 | * Returns an optimized and cached function for use with `tw`. 31 | * 32 | * `tw` caches rules based on the function identity. This helper caches 33 | * the function based on the data. 34 | * 35 | * @param factory to use when the directive is invoked 36 | * @param data to use 37 | */ 38 | export const directive = ( 39 | factory: (data: Data, context: Context) => MaybeThunk, 40 | data: Data, 41 | ): Directive => { 42 | const key = stringify(data) 43 | 44 | let directive: Directive | undefined 45 | 46 | if (key) { 47 | // eslint-disable-next-line no-var 48 | var cache = cacheByFactory.get(factory) 49 | 50 | if (!cache) { 51 | cacheByFactory.set(factory, (cache = new Map())) 52 | } 53 | 54 | directive = cache.get(key) 55 | } 56 | 57 | if (!directive) { 58 | directive = Object.defineProperty( 59 | (params: string[] | Context, context: Context): T => { 60 | context = Array.isArray(params) ? context : params 61 | return evalThunk(factory(data, context), context) 62 | }, 63 | 'toJSON', 64 | { 65 | // Allow twind to generate a unique id for this directive 66 | // twind uses JSON.stringify which returns undefined for functions like this directive 67 | // providing a toJSON function allows to include this directive in the id generation 68 | value: () => key || data, 69 | }, 70 | ) 71 | 72 | if (cache) { 73 | cache.set(key as string, directive as Directive) 74 | 75 | // Ensure the cache does not grow unlimited 76 | ensureMaxSize(cache, 10000) 77 | } 78 | } 79 | 80 | return directive as Directive 81 | } 82 | -------------------------------------------------------------------------------- /src/__fixtures__/dom-env.ts: -------------------------------------------------------------------------------- 1 | import type { Test } from 'uvu' 2 | 3 | import { JSDOM, VirtualConsole } from 'jsdom' 4 | 5 | const GLOBAL_PROPERTIES = ['window', 'self', 'document', 'navigator', 'getComputedStyle'] as const 6 | 7 | const kDefaultView = Symbol('kDefaultView') 8 | 9 | export function configure(test: T, options?: SetupOptions & ResetOptions): T { 10 | test.before(() => { 11 | setup(options) 12 | }) 13 | 14 | test.before.each(() => reset(options)) 15 | 16 | test.after(destroy) 17 | 18 | return test 19 | } 20 | 21 | export interface SetupOptions { 22 | html?: string 23 | url?: string 24 | console?: Console 25 | } 26 | 27 | export function setup({ 28 | html = '', 29 | url = 'http://localhost', 30 | console = global.console, 31 | }: SetupOptions = {}): JSDOM { 32 | const dom = new JSDOM(html, { 33 | pretendToBeVisual: true, 34 | runScripts: 'dangerously', 35 | url, 36 | virtualConsole: new VirtualConsole().sendTo(console), 37 | }) 38 | 39 | const { defaultView } = dom.window.document 40 | if (!defaultView) { 41 | throw new TypeError('JSDOM did not return a Window object') 42 | } 43 | 44 | Object.defineProperty(global, kDefaultView, { 45 | configurable: true, 46 | get: () => defaultView, 47 | }) 48 | 49 | defaultView.addEventListener('error', (event) => { 50 | process.emit('uncaughtException', event.error) 51 | }) 52 | 53 | GLOBAL_PROPERTIES.forEach((property) => 54 | Object.defineProperty(global, property, { 55 | enumerable: true, 56 | configurable: true, 57 | get: () => defaultView[property], 58 | }), 59 | ) 60 | 61 | return dom 62 | } 63 | 64 | export interface ResetOptions { 65 | title?: string 66 | head?: string 67 | body?: string 68 | } 69 | 70 | export function reset({ title = '', head = '', body = '' }: ResetOptions = {}): void { 71 | document.title = title 72 | document.head.innerHTML = head 73 | document.body.innerHTML = body 74 | } 75 | 76 | export function destroy(): void { 77 | GLOBAL_PROPERTIES.forEach((property) => Reflect.deleteProperty(global, property)) 78 | 79 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 80 | const defaultView = (global as any)[kDefaultView] as WindowProxy & typeof globalThis 81 | 82 | if (defaultView) { 83 | // Dispose "document" to prevent "load" event from triggering. 84 | Object.defineProperty(defaultView, 'document', { value: null }) 85 | 86 | defaultView.close() 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/shim/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * [[include:src/shim/README.md]] 3 | * 4 | * @packageDocumentation 5 | * @module twind/shim 6 | */ 7 | 8 | import type { Configuration } from '../types' 9 | 10 | import { setup as setupTW } from '../index' 11 | import { createObserver } from '../observe/index' 12 | 13 | /** 14 | * Options for {@link setup}. 15 | */ 16 | export interface ShimConfiguration extends Configuration { 17 | /** 18 | * The root element to shim (default: `document.documentElement`). 19 | */ 20 | target?: HTMLElement 21 | } 22 | 23 | if (typeof document !== 'undefined' && typeof addEventListener === 'function') { 24 | // eslint-disable-next-line no-var 25 | var onload = () => { 26 | const script = document.querySelector('script[type="twind-config"]') 27 | 28 | setup(script ? JSON.parse(script.innerHTML) : {}) 29 | } 30 | 31 | if (document.readyState === 'loading') { 32 | // Loading hasn't finished yet 33 | addEventListener('DOMContentLoaded', onload) 34 | } else { 35 | // `DOMContentLoaded` has already fired 36 | // invoke on next tick to allow other setup methods to run 37 | // eslint-disable-next-line no-var 38 | var timeoutRef = setTimeout(onload) 39 | } 40 | } 41 | 42 | const observer = createObserver() 43 | 44 | /** 45 | * Stop shimming/observing all nodes. 46 | */ 47 | export const disconnect = (): void => { 48 | if (onload) { 49 | // Removing the callbacks ensures that the setup is called only once 50 | // either programmatically from userland or by DOMContentLoaded/setTimeout 51 | removeEventListener('DOMContentLoaded', onload) 52 | clearTimeout(timeoutRef) 53 | } 54 | 55 | observer.disconnect() 56 | } 57 | 58 | /** 59 | * Configure the default {@link tw} and starts {@link observe | observing} the 60 | * {@link ShimConfiguration.target | target element} (default: `document.documentElement`). 61 | * 62 | * You do not need to call this method. As an alternativ you can provide a 63 | * `` element within the document. 64 | * The content must be valid JSON and all {@link twind.setup | twind setup options} 65 | * (including hash) are supported. 66 | */ 67 | export const setup = ({ 68 | target = document.documentElement, 69 | ...config 70 | }: ShimConfiguration = {}): void => { 71 | if (Object.keys(config).length) { 72 | setupTW(config) 73 | } 74 | 75 | // Remove event listeners 76 | disconnect() 77 | 78 | observer.observe(target) 79 | 80 | target.hidden = false 81 | } 82 | -------------------------------------------------------------------------------- /docs/recipes/use-with/react.md: -------------------------------------------------------------------------------- 1 | ## [React](https://reactjs.org/) 2 | 3 | ```js 4 | import ReactDOM from 'react-dom' 5 | import * as React from 'react' 6 | 7 | import { tw } from 'twind' 8 | 9 | ReactDOM.render( 10 |
11 |

This is Twind!

12 |
, 13 | document.body, 14 | ) 15 | ``` 16 | 17 | ### htm/react - [React](https://reactjs.org/) with [htm](https://github.com/developit/htm) 18 | 19 | ```js 20 | import ReactDOM from 'react-dom' 21 | import { html } from 'htm/react' 22 | 23 | import { tw } from 'twind' 24 | 25 | ReactDOM.render( 26 | html` 27 |
28 |

29 | This is Twind! 30 |

31 |
32 | `, 33 | document.body, 34 | ) 35 | ``` 36 | 37 | > 🚀 [live and interactive demo](https://esm.codes/#aW1wb3J0IHsgcmVuZGVyIH0gZnJvbSAnaHR0cHM6Ly9jZG4uc2t5cGFjay5kZXYvcmVhY3QtZG9tJwppbXBvcnQgKiBhcyBSZWFjdCBmcm9tICdodHRwczovL2Nkbi5za3lwYWNrLmRldi9yZWFjdCcKaW1wb3J0IGh0bSBmcm9tICdodHRwczovL2Nkbi5za3lwYWNrLmRldi9odG0nCgppbXBvcnQgeyB0dyB9IGZyb20gJ2h0dHBzOi8vY2RuLnNreXBhY2suZGV2L3R3aW5kJwoKY29uc3QgaHRtbCA9IGh0bS5iaW5kKFJlYWN0LmNyZWF0ZUVsZW1lbnQpCgpyZW5kZXIoCiAgaHRtbGAKICAgIDxtYWluIGNsYXNzTmFtZT0iJHt0d2BoLXNjcmVlbiBiZy1wdXJwbGUtNDAwIGZsZXggaXRlbXMtY2VudGVyIGp1c3RpZnktY2VudGVyYH0iPgogICAgICA8aDEgY2xhc3NOYW1lPSIke3R3YGZvbnQtYm9sZCB0ZXh0KGNlbnRlciA1eGwgd2hpdGUgc206Z3JheS04MDAgbWQ6cGluay03MDApYH0iPgogICAgICAgIFRoaXMgaXMgVHdpbmQhCiAgICAgIDwvaDE+CiAgICA8L21haW4+CiAgYCwKICBkb2N1bWVudC5ib2R5LAopCg==) 38 | 39 | ## Server Side Rendering 40 | 41 | ```js 42 | import { renderToString } from 'react-dom/server' 43 | 44 | import { setup } from 'twind' 45 | import { virtualSheet, getStyleTag } from 'twind/sheets' 46 | 47 | import App from './app' 48 | 49 | const sheet = virtualSheet() 50 | 51 | setup({ ...sharedOptions, sheet }) 52 | 53 | function ssr() { 54 | // 1. Reset the sheet for a new rendering 55 | sheet.reset() 56 | 57 | // 2. Render the app 58 | const body = renderToString() 59 | 60 | // 3. Create the style tag with all generated CSS rules 61 | const styleTag = getStyleTag(sheet) 62 | 63 | // 4. Generate the response html 64 | return ` 65 | 66 | ${styleTag} 67 | ${body} 68 | 69 | ` 70 | } 71 | ``` 72 | -------------------------------------------------------------------------------- /docs/getting-started/browser-support.md: -------------------------------------------------------------------------------- 1 | The library will currently run in [all browsers](https://browserslist.dev/?q=PjAlLCBub3QgQ2hyb21lIDwzNiwgbm90IEVkZ2UgPDEyLCBub3QgRmlyZWZveCA8MjAsIG5vdCBPcGVyYSA8MjUsIG5vdCBTYWZhcmkgPDgsIG5vdCBpT1MgPDgsIG5vdCBPcGVyYU1vYmlsZSA8PSAxMi4xLCBub3QgaWUgPD0gMTEsIG5vdCBJRV9Nb2IgPD0gMTE%3D) that support [Math.imul](https://caniuse.com/mdn-javascript_builtins_math_imul), [Map](https://caniuse.com/mdn-javascript_builtins_map), [Set](https://caniuse.com/mdn-javascript_builtins_set) and [WeakMap](https://caniuse.com/mdn-javascript_builtins_weakmap) (eg Chrome >=36, Edge >=12, Firefox >=20, Opera >=25, Safari >=8, iOS >=8). Additionally all LTS versions of Node.js are supported. 2 | 3 | Some new tailwind features use [CSS Variables (Custom Properties)](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties) which are [**not** available in legacy browsers](https://caniuse.com/css-variables) (Chrome <49, IE, Edge <16, Firefox <31, Opera <36, Safari <9.1, iOS <9.3). For IE 11 you can try the [CSS Variables Polyfill](https://github.com/nuxodin/ie11CustomProperties). 4 | 5 | We included fallbacks for the following directives which mimic [Tailwind v1](https://v1.tailwindcss.com/) behavior: 6 | 7 | - Color Opacity 8 | - [border-opacity-\*](https://tailwindcss.com/docs/border-opacity) 9 | - [bg-opacity-\*](https://tailwindcss.com/docs/background-opacity) 10 | - [text-opacity-\*](https://tailwindcss.com/docs/text-opacity) 11 | - [placeholder-opacity-\*](https://tailwindcss.com/docs/placeholder-opacity) 12 | - Reversing Children Order 13 | - [divide-\*-reverse](https://tailwindcss.com/docs/divide-width#reversing-children-order) 14 | - [space-\*-reverse](https://tailwindcss.com/docs/space#reversing-children-order) 15 | - `rotate`, `scale` , `skew` and `translate` can only be used alone 16 | 17 | > `rotate-45` works but when using `rotate-45 scale-150` only one of both is applied. In that case you must use `transform`: `transform rotate-45 scale-150` 18 | 19 | Some directive only work with CSS Variables and are not supported in legacy browsers: 20 | 21 | - [Ring](https://tailwindcss.com/docs/ring-width) 22 | 23 | ## Supporting IE11 and obsolete platforms 24 | 25 | This library uses features like destructuring assignment and const/let declarations and doesn't ship with ES5 transpiled sources. If you aim to support browsers like IE11 and below → make sure you configure your transpiler/bundler to include your `node_modules`. 26 | 27 | Additionally you need to provide a [polyfill](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/imul#Polyfill) for `Math.imul`. IE 11 already supports `Map`, `Set` and `WeakMap` - no polyfills needed for these. 28 | 29 |
30 | 31 | Continue to {@page Modules Summary} 32 | -------------------------------------------------------------------------------- /docs/recipes/use-with/web-components.md: -------------------------------------------------------------------------------- 1 | ## Web Components - [Custom Elements](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements) and [Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM) 2 | 3 | This example shows how Custom Element can have its styles separated without having the side effect of polluting the root document's styles. 4 | 5 | > ❗ This example is using [Constructable Stylesheet Objects](https://wicg.github.io/construct-stylesheets/) and `DocumentOrShadowRoot.adoptedStyleSheets` which have [limited browser support](https://caniuse.com/mdn-api_documentorshadowroot_adoptedstylesheets) at the moment (December 2020). The [Constructible style sheets polyfill](https://github.com/calebdwilliams/construct-style-sheets) offers a solution for all modern browsers and IE 11. 6 | 7 | ```js 8 | import { create, cssomSheet } from 'twind' 9 | 10 | // 1. Create separate CSSStyleSheet 11 | const sheet = cssomSheet({ target: new CSSStyleSheet() }) 12 | 13 | // 2. Use that to create an own twind instance 14 | const { tw } = create({ sheet }) 15 | 16 | class TwindElement extends HTMLElement { 17 | constructor() { 18 | super() 19 | 20 | const shadow = this.attachShadow({ mode: 'open' }) 21 | 22 | // 3. Apply the same style to each instance of this component 23 | shadow.adoptedStyleSheets = [sheet.target] 24 | 25 | // 4. Use "own" tw function 26 | shadow.innerHTML = ` 27 |
28 |

29 | This is Twind! 30 |

31 |
32 | ` 33 | } 34 | } 35 | 36 | customElements.define('twind-element', TwindElement) 37 | 38 | document.body.innerHTML = '' 39 | ``` 40 | 41 | > 🚀 [live and interactive demo](https://esm.codes/#aW1wb3J0IHsgY3JlYXRlLCBjc3NvbVNoZWV0IH0gZnJvbSAnaHR0cHM6Ly9jZG4uc2t5cGFjay5kZXYvdHdpbmQnCgpjb25zdCBzaGVldCA9IGNzc29tU2hlZXQoeyB0YXJnZXQ6IG5ldyBDU1NTdHlsZVNoZWV0KCkgfSkKCmNvbnN0IHsgdHcgfSA9IGNyZWF0ZSh7IHNoZWV0IH0pCgpjbGFzcyBUd2luZEVsZW1lbnQgZXh0ZW5kcyBIVE1MRWxlbWVudCB7CiAgY29uc3RydWN0b3IoKSB7CiAgICBzdXBlcigpCgogICAgY29uc3Qgc2hhZG93ID0gdGhpcy5hdHRhY2hTaGFkb3coeyBtb2RlOiAnb3BlbicgfSkKCiAgICBzaGFkb3cuYWRvcHRlZFN0eWxlU2hlZXRzID0gW3NoZWV0LnRhcmdldF0KCiAgICBzaGFkb3cuaW5uZXJIVE1MID0gYAogICAgICA8bWFpbiBjbGFzcz0iJHt0d2BoLXNjcmVlbiBiZy1wdXJwbGUtNDAwIGZsZXggaXRlbXMtY2VudGVyIGp1c3RpZnktY2VudGVyYH0iPgogICAgICAgIDxoMSBjbGFzcz0iJHt0d2Bmb250LWJvbGQgdGV4dChjZW50ZXIgNXhsIHdoaXRlIHNtOmdyYXktODAwIG1kOnBpbmstNzAwKWB9Ij4KICAgICAgICAgIFRoaXMgaXMgVHdpbmQhCiAgICAgICAgPC9oMT4KICAgICAgPC9tYWluPgogICAgYAogIH0KfQoKY3VzdG9tRWxlbWVudHMuZGVmaW5lKCd0d2luZC1lbGVtZW50JywgVHdpbmRFbGVtZW50KQoKZG9jdW1lbnQuYm9keS5pbm5lckhUTUwgPSAnPHR3aW5kLWVsZW1lbnQ+PC90d2luZC1lbGVtZW50PicK) 42 | -------------------------------------------------------------------------------- /src/__tests__/theme.test.ts: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu' 2 | import * as assert from 'uvu/assert' 3 | 4 | import { makeThemeResolver } from '../twind/theme' 5 | 6 | const test = suite('theme') 7 | 8 | test('no custom theme', () => { 9 | const theme = makeThemeResolver() 10 | 11 | assert.is(theme('borderColor', []), '#e5e7eb') 12 | }) 13 | 14 | test('default color', () => { 15 | const theme = makeThemeResolver({ 16 | extend: { 17 | colors: { 18 | gray: { 19 | DEFAULT: '#aaa', 20 | }, 21 | }, 22 | }, 23 | }) 24 | 25 | assert.is(theme('borderColor', 'gray'), '#aaa') 26 | }) 27 | 28 | test('custom color', () => { 29 | const theme = makeThemeResolver({ 30 | extend: { 31 | colors: { 32 | gray: { 33 | custom: '#aaa', 34 | }, 35 | }, 36 | }, 37 | }) 38 | 39 | assert.is(theme('borderColor.gray.custom'), '#aaa') 40 | assert.is(theme('borderColor', 'gray.custom'), '#aaa') 41 | assert.is(theme('borderColor', 'gray-custom'), '#aaa') 42 | assert.is(theme('borderColor', ['gray', 'custom']), '#aaa') 43 | 44 | assert.is(theme('borderColor.gray.300'), '#d1d5db') 45 | assert.is(theme('borderColor', 'gray.300'), '#d1d5db') 46 | assert.is(theme('borderColor', 'gray-300'), '#d1d5db') 47 | assert.is(theme('borderColor', ['gray', '300']), '#d1d5db') 48 | 49 | assert.is(theme('borderColor.gray.special', '#bbb'), '#bbb') 50 | assert.is(theme('borderColor', 'gray.special', '#bbb'), '#bbb') 51 | assert.is(theme('borderColor', 'gray-special', '#bbb'), '#bbb') 52 | assert.is(theme('borderColor', ['gray', 'special'], '#bbb'), '#bbb') 53 | }) 54 | 55 | test('deep custom color', () => { 56 | const theme = makeThemeResolver({ 57 | extend: { 58 | colors: { 59 | gray: { 60 | a: { 61 | b: { 62 | c: { 63 | d: { 64 | e: { 65 | f: '#abcdef', 66 | }, 67 | }, 68 | }, 69 | }, 70 | }, 71 | }, 72 | }, 73 | }, 74 | }) 75 | 76 | assert.is(theme('colors', 'gray.a.b.c.d.e.f'), '#abcdef') 77 | assert.is(theme('colors', 'gray-a-b-c-d-e-f'), '#abcdef') 78 | assert.is(theme('colors', ['gray', 'a', 'b', 'c', 'd', 'e', 'f']), '#abcdef') 79 | 80 | assert.is(theme('colors', 'gray.300'), '#d1d5db') 81 | assert.is(theme('colors', 'gray-300'), '#d1d5db') 82 | assert.is(theme('colors', ['gray', '300']), '#d1d5db') 83 | }) 84 | 85 | test('negative is available and no-op', () => { 86 | const theme = makeThemeResolver({ 87 | extend: { 88 | spacing: (theme, { negative }) => ({ 89 | ...negative({ xs: '1rem' }), 90 | }), 91 | }, 92 | }) 93 | 94 | assert.is(theme('spacing', 'xs'), undefined) 95 | assert.is(theme('spacing', '-xs'), undefined) 96 | 97 | assert.is(theme('spacing', 'px'), '1px') 98 | }) 99 | 100 | test.run() 101 | -------------------------------------------------------------------------------- /docs/getting-started/installation.md: -------------------------------------------------------------------------------- 1 | It is possible to install and thus use Twind in a multitude of different ways. We expose various {@page Modules Summary | different bundles} – from the latest ESM syntax to UMD bundles – with the aim of accommodating for as many dev setups as possible. This said, for the smallest size and fastest performance we recommend you use the ESM bundles. 2 | 3 | > 💡 Although compatible with traditional bundlers no build step is required to use Twind. 4 | 5 | 6 | 7 | 8 | - [Importing as a local dependency](#importing-as-a-local-dependency) 9 | - [Importing as a remote dependency](#importing-as-a-remote-dependency) 10 | 11 | 12 | 13 | ## Importing as a local dependency 14 | 15 | Most build tools rely on packages to be installed locally on the machine they are running on. Usually these packages are available on and installed via npm. Twind is no different in this regard. 16 | 17 | 1. Run the following command in your terminal, from your project root: 18 | 19 | ```sh 20 | npm i twind 21 | ``` 22 | 23 | 2. Then go ahead and import the module into your application using the bare module specifier: 24 | 25 | ```js 26 | import { tw } from 'twind' 27 | ``` 28 | 29 | Assuming you have your bundler configured correctly then you should now be able to use the module. 30 | 31 | ## Importing as a remote dependency 32 | 33 | Given that nearly all [browsers support es modules](https://caniuse.com/es6-module) now, sometimes it is desirable to import a module straight from from a CDN such as [skypack](https://skypack.dev/), [unpkg](https://unpkg.com/) or [jspm](https://jspm.dev/). 34 | 35 | 1. Add the following line to a javascript file referenced by a script tag with `type="module"` like below: 36 | 37 | ```html 38 | 41 | ``` 42 | 43 | Assuming you have an internet connection then you should now be able to use the module. 44 | 45 | > 🚀 [live and interactive demo](https://esm.codes/#aW1wb3J0IHsgdHcgfSBmcm9tICdodHRwczovL2Nkbi5za3lwYWNrLmRldi90d2luZCcKCmRvY3VtZW50LmJvZHkuaW5uZXJIVE1MID0gYAogIDxtYWluIGNsYXNzPSIke3R3YGgtc2NyZWVuIGJnLXB1cnBsZS00MDAgZmxleCBpdGVtcy1jZW50ZXIganVzdGlmeS1jZW50ZXJgfSI+CiAgICA8aDEgY2xhc3M9IiR7dHdgZm9udC1ib2xkIHRleHQoY2VudGVyIDV4bCB3aGl0ZSBzbTpncmF5LTgwMCBtZDpwaW5rLTcwMClgfSI+CiAgICAgIFRoaXMgaXMgVHdpbmQhCiAgICA8L2gxPgogIDwvbWFpbj4KYA==) 46 | 47 |
How to support legacy browser with the UMD bundles (Click to expand) 48 | 49 | > 💡 You may need to provide certain {@page Browser Support | polyfills} depending on your target browser. 50 | 51 | ```html 52 | 53 | 56 | ``` 57 | 58 |
59 | 60 |
61 | 62 | Continue to {@page Styling with Twind} 63 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | import './test' 3 | 4 | import { tw, setup } from 'twind' 5 | import { domSheet } from 'twind/sheets' 6 | import { animation } from 'twind/css' 7 | 8 | setup({ sheet: domSheet() }) 9 | 10 | const bounce = animation('1s ease infinite', { 11 | 'from, 20%, 53%, 80%, to': { 12 | transform: 'translate3d(0,0,0)', 13 | }, 14 | '40%, 43%': { 15 | transform: 'translate3d(0, -30px, 0)', 16 | }, 17 | '70%': { 18 | transform: 'translate3d(0, -15px, 0)', 19 | }, 20 | '90%': { 21 | transform: 'translate3d(0, -4px, 0)', 22 | }, 23 | }) 24 | 25 | const style = { 26 | main: tw` 27 | text-green-500 28 | bg-current 29 | xxx 30 | font-sans 31 | w-full 32 | min-h-screen 33 | flex 34 | items-center 35 | justify-center 36 | transition 37 | duration-1000 38 | hover:( 39 | sm:(bg-blue-600 text-blue-500) 40 | md:(text-purple-700 bg-purple-500) 41 | ) 42 | `, 43 | card: tw` 44 | bg-white 45 | max-w-sm 46 | rounded-2xl 47 | overflow-hidden 48 | shadow-2xl 49 | border(current 8) 50 | rotate( 51 | -3 hover:6 52 | sm:(0 hover:3) 53 | md:(3 hover:-6) 54 | ) 55 | `, 56 | tag: tw` 57 | inline-block 58 | bg-gray-200 59 | hover:( 60 | bg-gray-100 61 | text-gray-800 62 | shadow-lg 63 | ${bounce} 64 | ) 65 | border(& gray-400) 66 | rounded-full 67 | px-2 68 | py-1 69 | text(sm gray-600) 70 | font-semibold 71 | cursor-pointer 72 | `, 73 | } 74 | 75 | document.body.innerHTML = ` 76 |
77 |
78 | Sunset in the mountains 82 |
83 |
84 |

The Coldest Sunset In The World

85 |
86 |
87 |

88 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Voluptatibus quia, nulla! 89 | Maiores et perferendis eaque, exercitationem praesentium nihil. 90 |

91 |
92 |
93 | 96 | 99 |
100 |
101 |
102 |
#photography
103 |
#travel
104 |
#winter
105 |
106 |
107 |
108 | ` 109 | -------------------------------------------------------------------------------- /src/shim/server/README.md: -------------------------------------------------------------------------------- 1 | [![Documentation](https://flat.badgen.net/badge/icon/Documentation?icon=awesome&label)](https://twind.dev/docs/modules/twind_shim_server.html) 2 | [![Github](https://flat.badgen.net/badge/icon/tw-in-js%2Ftwind%2Fsrc%2Fshim%2Fserver?icon=github&label)](https://github.com/tw-in-js/twind/tree/main/src/shim/server) 3 | [![Module Size](https://flat.badgen.net/badgesize/brotli/https:/unpkg.com/twind/shim/server/server.js?icon=jsdelivr&label&color=blue&cache=10800)](https://unpkg.com/twind/shim/server/server.js 'brotli module size') 4 | [![Typescript](https://flat.badgen.net/badge/icon/included?icon=typescript&label)](https://unpkg.com/browse/twind/shim/server/server.d.ts) 5 | 6 | For static HTML processing (usually to provide SSR support for your javascript-powered web apps), `twind/shim/server` exports a dedicated {@link shim} function that accepts HTML string as input and will: 7 | 8 | 1. parse the markup and process element classes with either the {@link twind.tw | default/global tw} instance or a {@link ShimOptions.tw | custom} instance 9 | 2. populate the provided sheet with the generated rules 10 | 3. output the HTML string with the final element classes 11 | 12 | All Twind syntax features like {@page Thinking in Groups | grouping} are supported within class attributes. 13 | 14 | The {@link shim} function also accepts an optional 2nd argument that can be a {@link twind.create | custom} `tw` instance or an {@link ShimOptions | options object} (including `tw` instance). 15 | 16 | ```js 17 | import { create } from 'twind' 18 | import { shim, virtualSheet, getStyleTag } from 'twind/shim/server' 19 | 20 | const sheet = virtualSheet() 21 | 22 | const { tw } = create({ ...sharedOptions, sheet }) 23 | 24 | sheet.reset() 25 | 26 | const markup = shim(htmlString, { 27 | tw, // defaults to default `tw` instance 28 | }) 29 | 30 | const styleTag = getStyleTag(sheet) 31 | ``` 32 | 33 |
Asynchronous SSR 34 | 35 | > ❗ This is an experimental feature. Use with care and please [report any issue](https://github.com/tw-in-js/twind/issues/new) you find. 36 | > Consider using the synchronous API when ever possible due to the relatively expensive nature of the [promise introspection API](https://docs.google.com/document/d/1rda3yKGHimKIhg5YeoAmCOtyURgsbTH_qaYR79FELlk/edit) provided by V8. 37 | > Async server side rendering is implemented using [async_hooks](https://nodejs.org/docs/latest-v14.x/api/async_hooks.html). Callback-based APIs and event emitters may not work or need special handling. 38 | 39 | ```js 40 | import { setup } from 'twind' 41 | import { asyncVirtualSheet, getStyleTagProperties, shim } from 'twind/server' 42 | 43 | const sheet = asyncVirtualSheet() 44 | 45 | setup({ ...sharedOptions, sheet }) 46 | 47 | async function ssr() { 48 | // 1. Reset the sheet for a new rendering 49 | sheet.reset() 50 | 51 | // 2. Render the app to an html string and handle class attributes 52 | const body = shim(await renderTheApp()) 53 | 54 | // 3. Create the style tag with all generated CSS rules 55 | const styleTag = getStyleTag(sheet) 56 | 57 | // 4. Generate the response html 58 | return ` 59 | 60 | ${styleTag} 61 | ${body} 62 | 63 | ` 64 | } 65 | ``` 66 | 67 |
68 | -------------------------------------------------------------------------------- /docs/advanced/ssr.md: -------------------------------------------------------------------------------- 1 | # Static Extraction a.k.a. Server Side Rendering 2 | 3 | Twind supports static extraction a.k.a. Server Side Rendering (SSR) out of the box. 4 | 5 | 6 | 7 | 8 | - [Synchronous SSR](#synchronous-ssr) 9 | - [Asynchronous SSR](#asynchronous-ssr) 10 | - [Streaming SSR](#streaming-ssr) 11 | 12 | 13 | 14 | ## Synchronous SSR 15 | 16 | The following example assumes your app is using the `tw` named export from `twind` 17 | but the same logic can be applied to custom instances. 18 | 19 | ```js 20 | import { setup } from 'twind' 21 | import { virtualSheet, getStyleTag } from 'twind/sheets' 22 | 23 | const sheet = virtualSheet() 24 | 25 | setup({ ...sharedOptions, sheet }) 26 | 27 | function ssr() { 28 | // 1. Reset the sheet for a new rendering 29 | sheet.reset() 30 | 31 | // 2. Render the app 32 | const body = renderTheApp() 33 | 34 | // 3. Create the style tag with all generated CSS rules 35 | const styleTag = getStyleTag(sheet) 36 | 37 | // 4. Generate the response html 38 | return ` 39 | 40 | ${styleTag} 41 | ${body} 42 | 43 | ` 44 | } 45 | ``` 46 | 47 | In order to prevent harmful code injection on the web, a [Content Security Policy (CSP)](https://developer.mozilla.org/docs/Web/HTTP/CSP) may be put in place. During server-side rendering, a cryptographic nonce (number used once) may be embedded when generating a page on demand: 48 | 49 | ```js 50 | // ... other code is the same as before ... 51 | 52 | // Usage with webpack: https://webpack.js.org/guides/csp/ 53 | const styleTag = getStyleTag(sheet, { nonce: __webpack_nonce__ }) 54 | ``` 55 | 56 | ## Asynchronous SSR 57 | 58 | > ❗ Please note the `twind/server` bundle is Node.JS only. 59 | > ❗ This is an experimental feature and only supported for Node.JS >=12. Use with care and please [report any issue](https://github.com/tw-in-js/twind/issues/new) you find. 60 | > Consider using the synchronous API when ever possible due to the relatively expensive nature of the [promise introspection API](https://docs.google.com/document/d/1rda3yKGHimKIhg5YeoAmCOtyURgsbTH_qaYR79FELlk/edit) provided by V8. 61 | > Async server side rendering is implemented using [async_hooks](https://nodejs.org/docs/latest-v14.x/api/async_hooks.html). Callback-based APIs and event emitters may not work or need special handling. 62 | 63 | ```js 64 | import { setup } from 'twind' 65 | import { asyncVirtualSheet, getStyleTag } from 'twind/server' 66 | 67 | const sheet = asyncVirtualSheet() 68 | 69 | setup({ ...sharedOptions, sheet }) 70 | 71 | async function ssr() { 72 | // 1. Reset the sheet for a new rendering 73 | sheet.reset() 74 | 75 | // 2. Render the app 76 | const body = await renderTheApp() 77 | 78 | // 3. Create the style tag with all generated CSS rules 79 | const styleTag = getStyleTag(sheet) 80 | 81 | // 4. Generate the response html 82 | return ` 83 | 84 | ${styleTag} 85 | ${body} 86 | 87 | ` 88 | } 89 | ``` 90 | 91 | ## Streaming SSR 92 | 93 | > Supporting ReactDOM.renderToNodeStream and Vue.renderToStream is still on the roadmap... 94 | -------------------------------------------------------------------------------- /src/server/async-sheet.test.ts: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu' 2 | import * as assert from 'uvu/assert' 3 | 4 | import { promisify } from 'util' 5 | 6 | import type { AsyncVirtualSheet } from '.' 7 | import { asyncVirtualSheet, getStyleTagProperties } from '.' 8 | import { create } from '..' 9 | 10 | const delay = promisify(setImmediate) 11 | 12 | // We are using async_hooks and they are currently not working as expected on Node.JS v10 13 | if (Number(process.versions.node.split('.')[0]) >= 12) { 14 | const test = suite<{ 15 | sheet: AsyncVirtualSheet 16 | }>('asyncVirtualSheet') 17 | 18 | test.before((context) => { 19 | context.sheet = asyncVirtualSheet() 20 | }) 21 | 22 | test('concurrent sheets', async ({ sheet }) => { 23 | const [first, second] = await Promise.all([ 24 | delay().then(async () => { 25 | sheet.reset() 26 | 27 | sheet.insert('html{margin:0}', 0) 28 | assert.equal(sheet.target, ['html{margin:0}']) 29 | 30 | await delay() 31 | 32 | sheet.insert('body{margin:0}', 0) 33 | assert.equal(sheet.target, ['body{margin:0}', 'html{margin:0}']) 34 | 35 | return getStyleTagProperties(sheet) 36 | }), 37 | 38 | delay().then(async () => { 39 | sheet.reset() 40 | 41 | sheet.insert('m0{margin:0}', 0) 42 | assert.equal(sheet.target, ['m0{margin:0}']) 43 | 44 | await delay() 45 | 46 | sheet.insert('p1{padding:1}', 1) 47 | assert.equal(sheet.target, ['m0{margin:0}', 'p1{padding:1}']) 48 | 49 | return getStyleTagProperties(sheet) 50 | }), 51 | ]) 52 | 53 | assert.equal(first, { 54 | id: '__twind', 55 | textContent: 'body{margin:0}html{margin:0}', 56 | }) 57 | assert.equal(second, { 58 | id: '__twind', 59 | textContent: 'm0{margin:0}p1{padding:1}', 60 | }) 61 | }) 62 | 63 | test('includes preflight', async ({ sheet }) => { 64 | const { tw } = create({ sheet }) 65 | 66 | const [first, second] = await Promise.all([ 67 | Promise.resolve().then(async () => { 68 | sheet.reset() 69 | 70 | assert.is(tw`text-center font-bold`, 'text-center font-bold') 71 | 72 | await delay() 73 | 74 | assert.is(tw`text-xl`, 'text-xl') 75 | 76 | return getStyleTagProperties(sheet) 77 | }), 78 | 79 | Promise.resolve().then(async () => { 80 | sheet.reset() 81 | 82 | assert.is(tw`italic`, 'italic') 83 | 84 | await delay() 85 | 86 | assert.is(tw`underline`, 'underline') 87 | 88 | return getStyleTagProperties(sheet) 89 | }), 90 | ]) 91 | 92 | assert.match(first.textContent, 'ol,ul{list-style:none}') 93 | assert.match(first.textContent, '.text-center{text-align:center}.font-bold{font-weight:700}') 94 | assert.not.match( 95 | first.textContent, 96 | '.italic{font-style:italic}.underline{-webkit-text-decoration:underline;text-decoration:underline}', 97 | ) 98 | 99 | assert.match(second.textContent, 'ol,ul{list-style:none}') 100 | assert.match( 101 | second.textContent, 102 | '.italic{font-style:italic}.underline{-webkit-text-decoration:underline;text-decoration:underline}', 103 | ) 104 | assert.not.match( 105 | second.textContent, 106 | '.text-center{text-align:center}.font-bold{font-weight:700}', 107 | ) 108 | }) 109 | 110 | test.run() 111 | } 112 | -------------------------------------------------------------------------------- /src/shim/server/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | // ^^^^ This comment is need to prevent browser bundles of this file 3 | 4 | /** 5 | * [[include:src/shim/server/README.md]] 6 | * 7 | * @packageDocumentation 8 | * @module twind/shim/server 9 | */ 10 | 11 | import type { TW } from '../../types' 12 | import * as htmlparser2 from 'htmlparser2' 13 | import { tw as defaultTW } from '../../index' 14 | 15 | // htmlparser2 has no esm bundle => 16 | // a little dance to work around different cjs loaders 17 | const { Tokenizer } = 18 | ((htmlparser2 as unknown) as { default: typeof import('htmlparser2') }).default || htmlparser2 19 | 20 | /** 21 | * Options for {@link shim}. 22 | */ 23 | export interface ShimOptions { 24 | /** 25 | * Custom {@link twind.tw | tw} instance to use (default: {@link twind.tw}). 26 | */ 27 | tw?: TW 28 | } 29 | 30 | export { virtualSheet, getStyleTag, getStyleTagProperties } from '../../sheets/index' 31 | 32 | const noop = () => undefined 33 | 34 | /** 35 | * Shim the passed html. 36 | * 37 | * 1. tokenize the markup and process element classes with either the 38 | * {@link twind.tw | default/global tw} instance or a {@link ShimOptions.tw | custom} instance 39 | * 2. populate the provided sheet with the generated rules 40 | * 3. output the HTML markup with the final element classes 41 | 42 | * @param markup the html to shim 43 | * @param options to use 44 | * @return the HTML markup with the final element classes 45 | */ 46 | export const shim = (markup: string, options: TW | ShimOptions = {}): string => { 47 | const { tw = defaultTW } = typeof options === 'function' ? { tw: options } : options 48 | 49 | let lastAttribName = '' 50 | let lastChunkStart = 0 51 | const chunks: string[] = [] 52 | 53 | const tokenizer = new Tokenizer( 54 | { 55 | decodeEntities: false, 56 | xmlMode: false, 57 | }, 58 | { 59 | onattribend: noop, 60 | onattribdata: (value) => { 61 | if (lastAttribName === 'class') { 62 | const currentIndex = tokenizer.getAbsoluteIndex() 63 | const startIndex = currentIndex - value.length 64 | const parsedClassNames = tw(value) 65 | 66 | // We only need to shift things around if we need to actually change the markup 67 | if (parsedClassNames !== value) { 68 | // We've hit another mutation boundary 69 | chunks.push(markup.slice(lastChunkStart, startIndex)) 70 | chunks.push(parsedClassNames) 71 | lastChunkStart = currentIndex 72 | } 73 | } 74 | // This may not be strictly necessary 75 | lastAttribName = '' 76 | }, 77 | onattribname: (name) => { 78 | lastAttribName = name 79 | }, 80 | oncdata: noop, 81 | onclosetag: noop, 82 | oncomment: noop, 83 | ondeclaration: noop, 84 | onend: noop, 85 | onerror: noop, 86 | onopentagend: noop, 87 | onopentagname: noop, 88 | onprocessinginstruction: noop, 89 | onselfclosingtag: noop, 90 | ontext: noop, 91 | }, 92 | ) 93 | 94 | tokenizer.end(markup) 95 | 96 | // Avoid unnecessary array operations and string concatenation if we never 97 | // needed to slice and dice things. 98 | if (!chunks.length) { 99 | return markup 100 | } 101 | 102 | // Combine the current set of chunks with the tail-end of the input 103 | return chunks.join('') + markup.slice(lastChunkStart || 0, markup.length) 104 | } 105 | -------------------------------------------------------------------------------- /src/__tests__/dark-mode.test.ts: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu' 2 | import * as assert from 'uvu/assert' 3 | 4 | import type { Instance } from '../types' 5 | import type { VirtualSheet } from '../sheets/index' 6 | 7 | import { virtualSheet } from '../sheets/index' 8 | import { create, strict } from '../index' 9 | 10 | const test = suite<{ 11 | sheet: VirtualSheet 12 | instance: Instance 13 | }>('dark mode') 14 | 15 | test.before((context) => { 16 | context.sheet = virtualSheet() 17 | context.instance = create({ 18 | sheet: context.sheet, 19 | mode: strict, 20 | preflight: false, 21 | theme: { extend: { colors: { '#111': '#111', '#222': '#222', '#333': '#333' } } }, 22 | }) 23 | }) 24 | 25 | test.after.each(({ sheet }) => { 26 | sheet.reset() 27 | }) 28 | 29 | test('default to dark mode media strategy', ({ instance, sheet }) => { 30 | assert.is(instance.tw('text-white dark:text-black'), 'text-white dark:text-black') 31 | assert.equal(sheet.target, [ 32 | '.text-white{--tw-text-opacity:1;color:#fff;color:rgba(255,255,255,var(--tw-text-opacity))}', 33 | '@media (prefers-color-scheme:dark){.dark\\:text-black{--tw-text-opacity:1;color:#000;color:rgba(0,0,0,var(--tw-text-opacity))}}', 34 | ]) 35 | }) 36 | 37 | test('dark mode class strategy', ({ instance, sheet }) => { 38 | instance = create() 39 | 40 | instance.setup({ 41 | darkMode: 'class', 42 | sheet: sheet, 43 | preflight: false, 44 | mode: strict, 45 | theme: { extend: { colors: { '#111': '#111', '#222': '#222', '#333': '#333' } } }, 46 | }) 47 | 48 | assert.is(instance.tw('text-white dark:text-black'), 'text-white dark:text-black') 49 | assert.equal(sheet.target, [ 50 | '.text-white{--tw-text-opacity:1;color:#fff;color:rgba(255,255,255,var(--tw-text-opacity))}', 51 | '.dark .dark\\:text-black{--tw-text-opacity:1;color:#000;color:rgba(0,0,0,var(--tw-text-opacity))}', 52 | ]) 53 | }) 54 | 55 | test('stacking with screens', ({ instance, sheet }) => { 56 | assert.is( 57 | instance.tw('text-#111 dark:text-#222 lg:text-black lg:dark:text-white'), 58 | 'text-#111 dark:text-#222 lg:text-black lg:dark:text-white', 59 | ) 60 | assert.equal(sheet.target, [ 61 | '.text-\\#111{--tw-text-opacity:1;color:#111;color:rgba(17,17,17,var(--tw-text-opacity))}', 62 | '@media (min-width:1024px){.lg\\:text-black{--tw-text-opacity:1;color:#000;color:rgba(0,0,0,var(--tw-text-opacity))}}', 63 | '@media (prefers-color-scheme:dark){.dark\\:text-\\#222{--tw-text-opacity:1;color:#222;color:rgba(34,34,34,var(--tw-text-opacity))}}', 64 | '@media (min-width:1024px){@media (prefers-color-scheme:dark){.lg\\:dark\\:text-white{--tw-text-opacity:1;color:#fff;color:rgba(255,255,255,var(--tw-text-opacity))}}}', 65 | ]) 66 | }) 67 | 68 | test('stacking with other variants', ({ instance, sheet }) => { 69 | assert.is( 70 | instance.tw('text-#111 hover:text-#222 lg:dark:text-black lg:dark:hover:text-white'), 71 | 'text-#111 hover:text-#222 lg:dark:text-black lg:dark:hover:text-white', 72 | ) 73 | assert.equal(sheet.target, [ 74 | '.text-\\#111{--tw-text-opacity:1;color:#111;color:rgba(17,17,17,var(--tw-text-opacity))}', 75 | '.hover\\:text-\\#222:hover{--tw-text-opacity:1;color:#222;color:rgba(34,34,34,var(--tw-text-opacity))}', 76 | '@media (min-width:1024px){@media (prefers-color-scheme:dark){.lg\\:dark\\:text-black{--tw-text-opacity:1;color:#000;color:rgba(0,0,0,var(--tw-text-opacity))}}}', 77 | '@media (min-width:1024px){@media (prefers-color-scheme:dark){.lg\\:dark\\:hover\\:text-white:hover{--tw-text-opacity:1;color:#fff;color:rgba(255,255,255,var(--tw-text-opacity))}}}', 78 | ]) 79 | }) 80 | 81 | test.run() 82 | -------------------------------------------------------------------------------- /src/sheets/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * [[include:src/sheets/README.md]] 3 | * 4 | * @packageDocumentation 5 | * @module twind/sheets 6 | */ 7 | 8 | import type { SheetConfig, Sheet, SheetInit, SheetInitCallback } from '../types' 9 | import { getStyleElement, STYLE_ELEMENT_ID } from '../internal/dom' 10 | 11 | /** 12 | * Creates an sheet which inserts style rules through the Document Object Model. 13 | */ 14 | export const domSheet = ({ 15 | nonce, 16 | target = getStyleElement(nonce), 17 | }: SheetConfig = {}): Sheet => { 18 | const offset = target.childNodes.length 19 | 20 | return { 21 | target, 22 | insert: (rule, index) => 23 | target.insertBefore(document.createTextNode(rule), target.childNodes[offset + index]), 24 | } 25 | } 26 | 27 | /** 28 | * Allows to reset and snaphot the current state of an sheet and 29 | * in extension the internal mutable state (caches, ...) of `tw`. 30 | */ 31 | export interface Storage { 32 | /** 33 | * Register a function that should be called to create a new state. 34 | */ 35 | init: SheetInit 36 | 37 | /** 38 | * Creates a snapshot of the current state, invokes all init callbacks to create a fresh state 39 | * and returns the snaphot. 40 | */ 41 | reset: (snapshot?: unknown[] | undefined) => unknown[] 42 | } 43 | 44 | const createStorage = (): Storage => { 45 | const callbacks: SheetInitCallback[] = [] 46 | let state: unknown[] = [] 47 | 48 | const invoke = (callback: SheetInitCallback, index: number): T => 49 | (state[index] = callback(state[index] as T)) 50 | 51 | return { 52 | init: (callback) => invoke(callback, callbacks.push(callback as SheetInitCallback) - 1), 53 | reset: (snapshot = []) => { 54 | // eslint-disable-next-line @typescript-eslint/no-extra-semi 55 | ;[snapshot, state] = [state, snapshot] 56 | callbacks.forEach(invoke) 57 | return snapshot 58 | }, 59 | } 60 | } 61 | 62 | /** 63 | * A sheet that collects styles into an array. 64 | */ 65 | export interface VirtualSheet extends Sheet, Storage { 66 | init: SheetInit 67 | } 68 | 69 | /** 70 | * Creates an sheet which collects style rules into an array. 71 | */ 72 | export const virtualSheet = (): VirtualSheet => { 73 | const storage = createStorage() 74 | 75 | let target: string[] 76 | storage.init((value = []) => (target = value)) 77 | 78 | return { 79 | ...storage, 80 | get target() { 81 | return [...target] 82 | }, 83 | insert: (rule, index) => target.splice(index, 0, rule), 84 | } 85 | } 86 | 87 | export interface StyleTagProperties { 88 | id: string 89 | textContent: string 90 | } 91 | 92 | export interface HasTarget { 93 | readonly target: readonly string[] 94 | } 95 | 96 | export type StyleTagSheet = HasTarget | readonly string[] 97 | 98 | /** 99 | * Transforms css rules into `` 119 | } 120 | -------------------------------------------------------------------------------- /src/__tests__/hash.test.ts: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu' 2 | import * as assert from 'uvu/assert' 3 | 4 | import type { Instance } from '../types' 5 | import type { VirtualSheet } from '../sheets/index' 6 | 7 | import { virtualSheet } from '../sheets/index' 8 | 9 | import { create, strict } from '../index' 10 | import { apply, css } from '../css' 11 | 12 | const test = suite<{ 13 | sheet: VirtualSheet 14 | tw: Instance['tw'] 15 | }>('hash') 16 | 17 | test.before((context) => { 18 | context.sheet = virtualSheet() 19 | const { tw } = create({ 20 | sheet: context.sheet, 21 | mode: strict, 22 | preflight: false, 23 | hash: true, 24 | prefix: false, 25 | theme: { extend: { colors: { primary: '#0d3880', '#0d3880': '#0d3880' } } }, 26 | }) 27 | context.tw = tw 28 | }) 29 | 30 | test.after.each(({ sheet }) => { 31 | sheet.reset() 32 | }) 33 | 34 | test('class names are hashed', ({ tw, sheet }) => { 35 | assert.is(tw('group flex pt-4 text-center'), 'tw-1bk5mm5 tw-bibj42 tw-aysv3w tw-10s9vuy') 36 | assert.equal(sheet.target, [ 37 | '.tw-bibj42{display:flex}', 38 | '.tw-aysv3w{padding-top:1rem}', 39 | '.tw-10s9vuy{text-align:center}', 40 | ]) 41 | }) 42 | 43 | test('keyframes are hashed', ({ tw, sheet }) => { 44 | assert.is(tw('animate-pulse'), 'tw-j3t2kc') 45 | assert.equal(sheet.target, [ 46 | '@keyframes tw-sktrkv{0%,100%{opacity:1}50%{opacity:.5}}', 47 | '.tw-j3t2kc{animation:tw-sktrkv 2s cubic-bezier(0.4, 0, 0.6, 1) infinite}', 48 | ]) 49 | }) 50 | 51 | test('accept already hashed rules', ({ tw, sheet }) => { 52 | assert.is(tw('tw-1bk5mm5 tw-bibj42'), 'tw-1bk5mm5 tw-bibj42') 53 | assert.equal(sheet.target, []) 54 | }) 55 | 56 | test('already hashed rule can not have variants', ({ tw }) => { 57 | assert.throws(() => { 58 | tw('md:tw-bibj42') 59 | }, /UNKNOWN_DIRECTIVE/) 60 | }) 61 | 62 | test('already hashed rule can be negated', ({ tw }) => { 63 | assert.throws(() => { 64 | tw('-tw-bibj42') 65 | }, /UNKNOWN_DIRECTIVE/) 66 | }) 67 | 68 | test('transform', ({ tw, sheet }) => { 69 | assert.is(tw('transform'), 'tw-dmb5fl') 70 | assert.equal(sheet.target, [ 71 | '.tw-dmb5fl{--tw-1e4pbj4:0;--tw-142admc:0;--tw-9ouawy:0;--tw-wnlb2r:0;--tw-o4ir2d:0;--tw-vkgkf8:1;--tw-1lff04g:1;transform:translateX(var(--tw-1e4pbj4,0)) translateY(var(--tw-142admc,0)) rotate(var(--tw-9ouawy,0)) skewX(var(--tw-wnlb2r,0)) skewY(var(--tw-o4ir2d,0)) scaleX(var(--tw-vkgkf8,1)) scaleY(var(--tw-1lff04g,1))}', 72 | ]) 73 | }) 74 | 75 | test('scale', ({ tw, sheet }) => { 76 | assert.is(tw('scale-90'), 'tw-aytitt') 77 | assert.equal(sheet.target, [ 78 | '.tw-aytitt{--tw-vkgkf8:0.9;--tw-1lff04g:0.9;transform:scale(0.9);transform:translateX(var(--tw-1e4pbj4,0)) translateY(var(--tw-142admc,0)) rotate(var(--tw-9ouawy,0)) skewX(var(--tw-wnlb2r,0)) skewY(var(--tw-o4ir2d,0)) scaleX(var(--tw-vkgkf8,1)) scaleY(var(--tw-1lff04g,1))}', 79 | ]) 80 | }) 81 | 82 | test('bg-gradient-to-r from-purple-500', ({ tw, sheet }) => { 83 | assert.is(tw('bg-gradient-to-r from-purple-500'), 'tw-59jif0 tw-182i7wd') 84 | assert.equal(sheet.target, [ 85 | '.tw-182i7wd{--tw-sc6ze8:#8b5cf6;--tw-wt1r4o:var(--tw-sc6ze8),var(--tw-z5bexf,rgba(139,92,246,0))}', 86 | '.tw-59jif0{background-image:linear-gradient(to right,var(--tw-wt1r4o))}', 87 | ]) 88 | }) 89 | 90 | test('different variant produce different hashes', ({ tw, sheet }) => { 91 | assert.is(tw('sm:text-center lg:text-center'), 'tw-1s56i2w tw-wg34pa') 92 | assert.equal(sheet.target, [ 93 | '@media (min-width:640px){.tw-1s56i2w{text-align:center}}', 94 | '@media (min-width:1024px){.tw-wg34pa{text-align:center}}', 95 | ]) 96 | }) 97 | 98 | test('same style in different layers has different hash', ({ tw, sheet }) => { 99 | assert.is(tw`w-0 ${css({ width: '0px' })} ${apply(`w-0`)}`, 'tw-1x8w1q0 tw-1r1ybee tw-a7wz74') 100 | assert.equal(sheet.target, [ 101 | // apply(`w-0`) 102 | '.tw-a7wz74{width:0px}', 103 | // w-0 104 | '.tw-1x8w1q0{width:0px}', 105 | // css({ width: '0px' }) 106 | '.tw-1r1ybee{width:0px}', 107 | ]) 108 | }) 109 | 110 | test.run() 111 | -------------------------------------------------------------------------------- /src/__tests__/mode.test.ts: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu' 2 | import * as assert from 'uvu/assert' 3 | import { snoop } from 'snoop' 4 | 5 | import { virtualSheet } from '../sheets/index' 6 | import { create, strict, silent, mode } from '../index' 7 | 8 | const test = suite('mode') 9 | 10 | const noop: typeof console.warn = () => { 11 | /* no-op */ 12 | } 13 | 14 | test('mode warn (default)', () => { 15 | const consoleWarn = console.warn 16 | 17 | try { 18 | const { tw } = create() 19 | 20 | const warn = snoop(noop) 21 | console.warn = warn.fn 22 | 23 | assert.is(tw('unknown-directive'), 'unknown-directive') 24 | assert.is(warn.callCount, 1) 25 | assert.match(warn.lastCall.arguments[0], /UNKNOWN_DIRECTIVE/) 26 | 27 | assert.is(tw('rounded-t-xxx'), 'rounded-t-xxx') 28 | assert.is(warn.callCount, 2) 29 | assert.match(warn.lastCall.arguments[0], /UNKNOWN_THEME_VALUE/) 30 | 31 | assert.is(tw('gap'), 'gap') 32 | assert.is(warn.callCount, 3) 33 | assert.match(warn.lastCall.arguments[0], /UNKNOWN_THEME_VALUE/) 34 | } finally { 35 | console.warn = consoleWarn 36 | } 37 | }) 38 | 39 | test('mode silent', () => { 40 | const consoleWarn = console.warn 41 | 42 | try { 43 | const { tw } = create({ mode: silent }) 44 | 45 | const warn = snoop(noop) 46 | console.warn = warn.fn 47 | 48 | assert.is(tw('unknown-directive'), 'unknown-directive') 49 | assert.is(warn.callCount, 0) 50 | 51 | assert.is(tw('rounded-t-xxx'), 'rounded-t-xxx') 52 | assert.is(warn.callCount, 0) 53 | } finally { 54 | console.warn = consoleWarn 55 | } 56 | }) 57 | 58 | test('mode strict', () => { 59 | const instance = create({ 60 | sheet: virtualSheet(), 61 | mode: strict, 62 | }) 63 | 64 | assert.throws(() => instance.tw('unknown-directive'), /UNKNOWN_DIRECTIVE/) 65 | }) 66 | 67 | test('ignore vendor specific pseudo classes errors', () => { 68 | const sheet = virtualSheet() 69 | const warn = snoop(noop) 70 | 71 | const calls: [string, number][] = [] 72 | 73 | sheet.insert = (rule, index) => { 74 | calls.push([rule, index]) 75 | 76 | if (rule.includes(':-moz')) { 77 | throw new Error( 78 | `Failed to execute 'insertRule' on 'CSSStyleSheet': Failed to parse the rule '${rule}'.`, 79 | ) 80 | } 81 | } 82 | 83 | const instance = create({ 84 | sheet: sheet, 85 | mode: mode(warn.fn), 86 | preflight() { 87 | return { 88 | '::-moz-focus-inner': { borderStyle: 'none' }, 89 | ':-moz-focusring': { outline: '1px dotted ButtonText' }, 90 | } 91 | }, 92 | }) 93 | 94 | assert.is(instance.tw('underline text-center'), 'underline text-center') 95 | 96 | assert.equal(calls, [ 97 | ['::-moz-focus-inner{border-style:none}', 0], 98 | [':-moz-focusring{outline:1px dotted ButtonText}', 0], 99 | ['.underline{-webkit-text-decoration:underline;text-decoration:underline}', 0], 100 | ['.text-center{text-align:center}', 1], 101 | ]) 102 | 103 | assert.is(warn.callCount, 0) 104 | }) 105 | 106 | test('propagate other errors to warn', () => { 107 | const sheet = virtualSheet() 108 | const warn = snoop(noop) 109 | 110 | const calls: [string, number][] = [] 111 | 112 | sheet.insert = (rule, index) => { 113 | calls.push([rule, index]) 114 | 115 | if (rule.includes('invalid-web')) { 116 | throw new Error( 117 | `Failed to execute 'insertRule' on 'CSSStyleSheet': Failed to parse the rule '${rule}'.`, 118 | ) 119 | } 120 | } 121 | 122 | const instance = create({ 123 | sheet: sheet, 124 | mode: mode(warn.fn), 125 | preflight() { 126 | return { '.invalid-web': { color: 'blue' } } 127 | }, 128 | }) 129 | 130 | assert.is(instance.tw('underline'), 'underline') 131 | 132 | assert.equal(calls, [ 133 | ['.invalid-web{color:blue}', 0], 134 | ['.underline{-webkit-text-decoration:underline;text-decoration:underline}', 0], 135 | ]) 136 | 137 | assert.is(warn.callCount, 1) 138 | assert.match(warn.lastCall.arguments[0], /INJECT_CSS_ERROR/) 139 | }) 140 | 141 | test.run() 142 | -------------------------------------------------------------------------------- /docs/advanced/contributing.md: -------------------------------------------------------------------------------- 1 | If you have made it this far then thanks for checking out Twind. We hoped you have enjoyed learning about Tailwind-in-JS and would love to hear what you think! 2 | 3 | Please show your appreciation by sharing the project on twitter or by starring it on GitHub. 4 | 5 | If you have any implementation specific feedback or ideas then please [create an issue](https://github.com/tw-in-js/twind) for discussion. 6 | 7 | 8 | 9 | 10 | - [Local development](#local-development) 11 | - [Ideas for the future](#ideas-for-the-future) 12 | - [Packages / Modules](#packages--modules) 13 | - [Ideas](#ideas) 14 | - [Theme](#theme) 15 | - [Plugins](#plugins) 16 | - [Links](#links) 17 | 18 | 19 | 20 | ## Local development 21 | 22 | If you would like to try your hand at fixing a bug or adding a new feature then first clone the repository and cd into the project directory. 23 | 24 | > Ensure you run at least Node v12. 25 | 26 | Then run `yarn install` followed by any of the following commands: 27 | 28 | - `yarn start`: Start example 29 | - `yarn build`: Build the package 30 | - `yarn test`: Run test suite 31 | - `yarn test:coverage`: Run test suite with coverage 32 | - `yarn test:watch`: Run test suite in watch mode 33 | - `yarn format`: Ensure consistent code style 34 | - `yarn lint`: Run eslint 35 | - `yarn lint:fix`: Run eslint fix 36 | - `yarn release`: To publish the package 37 | 38 | ## Ideas for the future 39 | 40 | Below is an outline of some todos, packages, plugins and ideas that we would like to explore or implement given enough time and resources. If you would like to help us at all then please create an issue around any of the following points so that we can discuss the idea further. 41 | 42 | ## Packages / Modules 43 | 44 | - twind/styled - like styled components (https://github.com/cristianbote/goober/tree/master/benchmarks) 45 | - twind/legacy - for IE11 with reset and polyfills (Math.imul, CSS.escape) 46 | - add note in docs about umd bundles and how to use them 47 | - twind/play - play mode 48 | - @twind/forms 49 | - @twind/eslint-plugin - nice to have 50 | - @twind/prettier-plugin - nice to have 51 | - @twind/extensions 52 | - border gradients: https://t.co/W7YVS7f0Jp 53 | - scroll snap: https://t.co/7xqvpFQ9Qu 54 | - "on" colors: generate matching contrast color and use that one as text/background color 55 | - Floating labels: https://t.co/g5TMqIBh4b?ssr=true 56 | - Stretched link: https://v5.getbootstrap.com/docs/5.0/helpers/stretched-link/ 57 | - and others (see Plugins below) 58 | 59 | ## Ideas 60 | 61 | - convert css to twind 62 | - https://github.com/miklosme/css-to-tailwind 63 | - https://github.com/ritz078/transform/pull/263 64 | - https://transform-git-fork-jlarky-convert-to-twind.ritz078.now.sh/css-to-twind 65 | - minifier for template-literals 66 | - apply grouping where possible 67 | - react-native support 68 | - presets: https://tailwindcss.com/docs/presets 69 | - benchmark using https://github.com/A-gambit/CSS-IN-JS-Benchmarks 70 | - size comparison: build same page with other libs and compare size 71 | - adapter for standard tailwindcss plugins 72 | 73 | ### Theme 74 | 75 | - live theme updates 76 | - track used theme values and re-translate 77 | 78 | ### Plugins 79 | 80 | - plugin api like tailwind: `setup({ plugins: [typography] })` 81 | - `flex-gap-*`: https://github.com/tailwindlabs/tailwindcss/discussions/2316 82 | - https://www.npmjs.com/package/@savvywombat/tailwindcss-grid-areas 83 | - https://www.npmjs.com/package/tailwindcss-ripple 84 | - https://github.com/tailwindlabs/tailwindcss-aspect-ratio 85 | - https://github.com/innocenzi/tailwindcss-scroll-snap 86 | - https://github.com/aerni/tailwindcss-rfs 87 | - https://github.com/jhta/tailwindcss-truncate-multiline# 88 | - https://github.com/opdavies/tailwindcss-plugin-skip-link 89 | - https://github.com/bradlc/tailwindcss-type 90 | 91 | ## Links 92 | 93 | - https://github.com/aniftyco/awesome-tailwindcss 94 | - https://nerdcave.com/tailwind-cheat-sheet 95 | -------------------------------------------------------------------------------- /docs/getting-started/README.md: -------------------------------------------------------------------------------- 1 | > 🙋 If you find any incorrect or missing documentation then please [open an issue](https://github.com/tw-in-js/twind/issues) for discussion. 2 | 3 | This is the user manual for the `twind` library. A small compiler that turns Tailwind short hand into CSS rules at run, build or serve time. If you have used Tailwind and/or a CSS-in-JS solution before then most of the API will feel very familiar. 4 | 5 | We are confident that feature parity with Tailwind V2 has been achieved. We recommend you refer the Tailwind documentation site for anything non Twind implementation specific; information around directives, variants, theming etc. 6 | 7 | > 📚 **[Tailwind Documentation](https://tailwindcss.com)** 8 | 9 | Despite being very flexible and powerful, it was our intention to keep the surface API as minimal as possible. We appreciate that Twind is likely to be used by developers & designers alike and so we try provide sensible defaults out of the box, with little to no need for {@page Setup | customization}. 10 | 11 | 12 | 13 | 14 | - [Quickstart](#quickstart) 15 | - [twind/shim](#twindshim) 16 | 17 | 18 | 19 | ## Quickstart 20 | 21 | > 💡 Note that examples are given in vanilla JS but the module is compatible with all popular frameworks 22 | 23 | Getting started with the library requires no configuration, build step or even {@page Installation | installation} if you use [skypack](https://skypack.dev/), [unpkg](https://unpkg.com/) or [jspm](https://jspm.dev/) – see the {@page Installation} guide for more information. 24 | 25 | ```js 26 | import { tw } from 'https://cdn.skypack.dev/twind' 27 | 28 | document.body.innerHTML = ` 29 |
30 |

This is Tailwind in JS!

31 |
32 | ` 33 | ``` 34 | 35 | > 🚀 [live and interactive demo](https://esm.codes/#aW1wb3J0IHsgdHcgfSBmcm9tICdodHRwczovL2Nkbi5za3lwYWNrLmRldi90d2luZCcKCmRvY3VtZW50LmJvZHkuaW5uZXJIVE1MID0gYAogIDxtYWluIGNsYXNzPSIke3R3YGgtc2NyZWVuIGJnLXB1cnBsZS00MDAgZmxleCBpdGVtcy1jZW50ZXIganVzdGlmeS1jZW50ZXJgfSI+CiAgICA8aDEgY2xhc3M9IiR7dHdgZm9udC1ib2xkIHRleHQoY2VudGVyIDV4bCB3aGl0ZSBzbTpncmF5LTgwMCBtZDpwaW5rLTcwMClgfSI+CiAgICAgIFRoaXMgaXMgVHdpbmQhCiAgICA8L2gxPgogIDwvbWFpbj4KYA==) 36 | 37 | Using the exported {@page Styling with Twind | tw} function results in the compilation of the rules like `bg-black text-white` and `text-xl` exactly as specified in the [Tailwind documentation](https://tailwincss.com/docs). For convenience the default [tailwind theme](https://github.com/tailwindlabs/tailwindcss/blob/v1/stubs/defaultConfig.stub.js) is used along with the preflight [base styles](https://tailwindcss.com/docs/preflight) if neither are {@page Setup | provided by the developer}. 38 | 39 | ## twind/shim 40 | 41 | > 💡 Seamless integration with existing Tailwind HTML. This feature can be used together with your favorite framework without any additional setup. 42 | 43 | The {@page Using the Shim | twind/shim} module allows to use the `class` attribute for tailwind rules. If such a rule is detected the corresponding CSS rule is created and injected into the stylesheet. _No need for `tw`_ but it can be used on the same page as well (see example below). 44 | 45 | ```html 46 | 47 | 48 | 49 | 50 | 51 | 52 |
53 |

This is Twind!

54 |
55 | 56 | 57 | ``` 58 | 59 | > 🚀 [live and interactive shim demo](https://esm.codes/#aW1wb3J0ICdodHRwczovL2Nkbi5za3lwYWNrLmRldi90d2luZC9zaGltJwoKZG9jdW1lbnQuYm9keS5pbm5lckhUTUwgPSBgCiAgPG1haW4gY2xhc3M9Imgtc2NyZWVuIGJnLXB1cnBsZS00MDAgZmxleCBpdGVtcy1jZW50ZXIganVzdGlmeS1jZW50ZXIiPgogICAgPGgxIGNsYXNzPSJmb250LWJvbGQgdGV4dChjZW50ZXIgNXhsIHdoaXRlIHNtOmdyYXktODAwIG1kOnBpbmstNzAwKSI+CiAgICAgIFRoaXMgaXMgVHdpbmQhCiAgICA8L2gxPgogIDwvbWFpbj4KYA==) 60 | 61 |
62 | 63 | Continue to {@page Installation} 64 | -------------------------------------------------------------------------------- /src/observe/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * [[include:src/observe/README.md]] 3 | * 4 | * @packageDocumentation 5 | * @module twind/observe 6 | */ 7 | 8 | import type { TW } from '../types' 9 | import { tw as defaultTW } from '../index' 10 | import { ensureMaxSize } from '../internal/util' 11 | 12 | /** 13 | * Options for {@link createObserver}. 14 | */ 15 | export interface ShimConfiguration { 16 | /** 17 | * Custom {@link twind.tw | tw} instance to use (default: {@link twind.tw}). 18 | */ 19 | tw?: TW 20 | } 21 | 22 | /** Provides the ability to watch for changes being made to the DOM tree. */ 23 | export interface TwindObserver { 24 | /** 25 | * Stops observer from observing any mutations. 26 | */ 27 | disconnect(): TwindObserver 28 | 29 | /** 30 | * Observe an additional element. 31 | */ 32 | observe(target: Node): TwindObserver 33 | } 34 | 35 | const caches = new WeakMap>() 36 | 37 | const getCache = (tw: TW): Map => { 38 | let rulesToClassCache = caches.get(tw) 39 | 40 | if (!rulesToClassCache) { 41 | rulesToClassCache = new Map() 42 | caches.set(tw, rulesToClassCache) 43 | } 44 | 45 | return rulesToClassCache 46 | } 47 | 48 | /** 49 | * Creates a new {@link TwindObserver}. 50 | * 51 | * @param options to use 52 | */ 53 | export const createObserver = ({ tw = defaultTW }: ShimConfiguration = {}): TwindObserver => { 54 | const rulesToClassCache = getCache(tw) 55 | 56 | const handleMutation = ({ target, addedNodes }: MinimalMutationRecord): void => { 57 | // Not using target.classList.value (not supported in all browsers) or target.class (this is an SVGAnimatedString for svg) 58 | const rules = (target as Element).getAttribute?.('class') 59 | 60 | if (rules) { 61 | let className = rulesToClassCache.get(rules) 62 | 63 | if (!className) { 64 | className = tw(rules) 65 | 66 | // Remember the generated class name 67 | rulesToClassCache.set(rules, className) 68 | 69 | // Ensure the cache does not grow unlimited 70 | ensureMaxSize(rulesToClassCache, 10000) 71 | } 72 | 73 | if (rules !== className) { 74 | // Not using `target.className = ...` as that is read-only for SVGElements 75 | // eslint-disable-next-line @typescript-eslint/no-extra-semi 76 | ;(target as Element).setAttribute('class', className) 77 | } 78 | } 79 | 80 | for (let index = addedNodes.length; index--; ) { 81 | const node = addedNodes[index] 82 | 83 | handleMutations([ 84 | { 85 | target: node, 86 | addedNodes: (node as Element).children || [], 87 | }, 88 | ]) 89 | } 90 | } 91 | 92 | const handleMutations = (mutations: MinimalMutationRecord[]): void => 93 | mutations.forEach(handleMutation) 94 | 95 | if (typeof MutationObserver === 'function') { 96 | const observer = new MutationObserver(handleMutations) 97 | 98 | return { 99 | observe(target) { 100 | handleMutations([{ target, addedNodes: [target] }]) 101 | 102 | observer.observe(target, { 103 | attributes: true, 104 | attributeFilter: ['class'], 105 | subtree: true, 106 | childList: true, 107 | }) 108 | 109 | return this 110 | }, 111 | 112 | disconnect() { 113 | observer.disconnect() 114 | return this 115 | }, 116 | } 117 | } 118 | 119 | // Non-browser-like environment – return a no-op implementation 120 | return { 121 | observe() { 122 | return this 123 | }, 124 | 125 | disconnect() { 126 | return this 127 | }, 128 | } 129 | } 130 | 131 | /** 132 | * Creates a new {@link TwindObserver} and {@link TwindObserver.observe | start observing} the passed target element. 133 | * @param this to bind 134 | * @param target to shim 135 | * @param config to use 136 | */ 137 | export function observe( 138 | this: ShimConfiguration | undefined | void, 139 | target: Node, 140 | config: ShimConfiguration | undefined | void = this, 141 | ): TwindObserver { 142 | return createObserver(config as ShimConfiguration | undefined).observe(target) 143 | } 144 | 145 | /** 146 | * Simplified MutationRecord which allows use to pass an 147 | * ArrayLike (compatible with Array and NodeList) `addedNodes` and 148 | * omit other properties we are not interested in. 149 | */ 150 | interface MinimalMutationRecord { 151 | readonly addedNodes: ArrayLike 152 | readonly target: Node 153 | } 154 | -------------------------------------------------------------------------------- /src/__fixtures__/process-plugins.js: -------------------------------------------------------------------------------- 1 | import dlv from 'dlv' 2 | 3 | import corePlugins from 'tailwindcss/lib/corePlugins.js' 4 | import resolveConfig from 'tailwindcss/lib/util/resolveConfig.js' 5 | import defaultConfig from 'tailwindcss/stubs/defaultConfig.stub.js' 6 | import transformThemeValue from 'tailwindcss/lib/util/transformThemeValue.js' 7 | 8 | export function processPlugins() { 9 | const config = resolveConfig([defaultConfig]) 10 | 11 | const plugins = [...corePlugins(config), ...dlv(config, 'plugins', [])] 12 | 13 | const { 14 | theme: { screens }, 15 | variantOrder: variants, 16 | darkMode, 17 | prefix, 18 | separator, 19 | } = config 20 | 21 | const applyConfiguredPrefix = (selector) => selector 22 | 23 | const getConfigValue = (path, defaultValue) => (path ? dlv(config, path, defaultValue) : config) 24 | 25 | const utilities = {} 26 | 27 | plugins.forEach((plugin) => { 28 | if (plugin.__isOptionsFunction) { 29 | plugin = plugin() 30 | } 31 | 32 | const handler = typeof plugin === 'function' ? plugin : dlv(plugin, 'handler', () => {}) 33 | 34 | handler({ 35 | // Postcss, 36 | config: getConfigValue, 37 | 38 | theme: (path, defaultValue) => { 39 | const [pathRoot, ...subPaths] = path.split('.') 40 | 41 | const value = getConfigValue(['theme', pathRoot, ...subPaths], defaultValue) 42 | 43 | return transformThemeValue(pathRoot)(value) 44 | }, 45 | 46 | corePlugins: (path) => { 47 | if (Array.isArray(config.corePlugins)) { 48 | return config.corePlugins.includes(path) 49 | } 50 | 51 | return getConfigValue(`corePlugins.${path}`, true) 52 | }, 53 | 54 | variants: (path, defaultValue) => { 55 | if (Array.isArray(config.variants)) { 56 | return config.variants 57 | } 58 | 59 | return getConfigValue(`variants.${path}`, defaultValue) 60 | }, 61 | 62 | // No escaping of class names as we use theme as directives 63 | e: (className) => className, 64 | 65 | prefix: applyConfiguredPrefix, 66 | 67 | addUtilities: (newUtilities) => { 68 | // => const defaultOptions = { variants: [], respectPrefix: true, respectImportant: true } 69 | 70 | // options = Array.isArray(options) 71 | // ? { ...defaultOptions, variants: options } 72 | // : { ...defaultOptions, ...options } 73 | 74 | // .directive => CSS rule 75 | Object.assign(utilities, ...(Array.isArray(newUtilities) ? newUtilities : [newUtilities])) 76 | 77 | // =>pluginUtilities.push( 78 | // wrapWithLayer(wrapWithVariants(styles.nodes, options.variants), 'utilities'), 79 | // ) 80 | }, 81 | 82 | addComponents: (newComponents /*, options */) => { 83 | // => const defaultOptions = { variants: [], respectPrefix: true } 84 | // options = Array.isArray(options) 85 | // ? { ...defaultOptions, variants: options } 86 | // : { ...defaultOptions, ...options } 87 | // pluginComponents.push( 88 | // wrapWithLayer(wrapWithVariants(styles.nodes, options.variants), 'components'), 89 | // ) 90 | Object.assign( 91 | utilities, 92 | ...(Array.isArray(newComponents) ? newComponents : [newComponents]), 93 | ) 94 | }, 95 | 96 | addBase: (/* baseStyles */) => { 97 | // => pluginBaseStyles.push(wrapWithLayer(parseStyles(baseStyles), 'base')) 98 | }, 99 | 100 | addVariant: (/* name, generator, options = {} */) => { 101 | // =>pluginVariantGenerators[name] = generateVariantFunction(generator, options) 102 | }, 103 | }) 104 | }) 105 | 106 | const directives = { 107 | group: { selector: '.group', properties: {} }, 108 | } 109 | 110 | for (const selector of Object.keys(utilities)) { 111 | // '@keyframes spin' 112 | if (selector[0] === '@') { 113 | continue 114 | } 115 | 116 | // '.ordinal, .slashed-zero, .lining-nums, .oldstyle-nums' 117 | if (selector.includes(',')) { 118 | continue 119 | } 120 | 121 | // '.placeholder-black::placeholder' 122 | // '.divide-pink-50 > :not([hidden]) ~ :not([hidden])' 123 | const [, directive] = /^\.([^\s:]+)/.exec(selector) || [] 124 | 125 | if (directive) { 126 | // Unescape 127 | // '.w-4\\/5' 128 | // '.space-x-2\\.5' 129 | directives[directive.replace(/\\/g, '')] = { 130 | selector, 131 | properties: utilities[selector], 132 | } 133 | } 134 | } 135 | 136 | return { 137 | screens, 138 | variants, 139 | directives, 140 | darkMode, 141 | prefix, 142 | separator, 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /docs/getting-started/styling-with-twind.md: -------------------------------------------------------------------------------- 1 | > 💡 If you are unfamiliar with the Tailwind CSS shorthand syntax please read the [Tailwind documentation](https://tailwindcss.com/docs) about [Utility-First](https://tailwindcss.com/docs/utility-first), [Responsive Design](https://tailwindcss.com/docs/responsive-design) and [Hover, Focus, & Other States](https://tailwindcss.com/docs/hover-focus-and-other-states). 2 | 3 | ```js 4 | import { tw } from 'twind' 5 | 6 | document.body.innerHTML = ` 7 |
8 |

This is Twind!

9 |
10 | ` 11 | ``` 12 | 13 | > 🚀 [live and interactive demo](https://esm.codes/#aW1wb3J0IHsgdHcgfSBmcm9tICdodHRwczovL2Nkbi5za3lwYWNrLmRldi90d2luZCcKCmRvY3VtZW50LmJvZHkuaW5uZXJIVE1MID0gYAogIDxtYWluIGNsYXNzPSIke3R3YGgtc2NyZWVuIGJnLXB1cnBsZS00MDAgZmxleCBpdGVtcy1jZW50ZXIganVzdGlmeS1jZW50ZXJgfSI+CiAgICA8aDEgY2xhc3M9IiR7dHdgZm9udC1ib2xkIHRleHQoY2VudGVyIDV4bCB3aGl0ZSBzbTpncmF5LTgwMCBtZDpwaW5rLTcwMClgfSI+VGhpcyBpcyBUd2luZCE8L2gxPgogIDwvbWFpbj4KYA==) 14 | 15 | 16 | 17 | 18 | - [The tw function](#the-tw-function) 19 | - [The apply function](#the-apply-function) 20 | 21 | 22 | 23 | ## The tw function 24 | 25 | Calling the {@link twind.tw | tw} function results in the shorthand rules to be interpreted, normalized and compiled into CSS rules which get added to a stylesheet in the head of the document. The function will return a string consisting of all the class names that were processed. These class names can then be applied to the element itself much like any other CSS-in-JS library. 26 | 27 | ```js 28 | tw`bg-gray-200 rounded` 29 | //=> bg-gray-200 rounded 30 | tw`bg-gray-200 ${false && 'rounded'}` 31 | //=> bg-gray-200 32 | ``` 33 | 34 |
Show me more examples 35 | 36 | ```js 37 | tw`bg-gray-200 ${[false && 'rounded', 'block']}` 38 | //=> bg-gray-200 block 39 | tw`bg-gray-200 ${{ rounded: false, underline: isTrue() }}` 40 | //=> bg-gray-200 underline 41 | tw`bg-${randomColor()}` 42 | //=> bg-blue-500 43 | tw`hover:${({ tw }) => tw`underline`}` 44 | //=> hover:underline 45 | tw`bg-${'fuchsia'}) sm:${'underline'} lg:${false && 'line-through'} text-${[ 46 | 'underline', 47 | 'center', 48 | ]} rounded-${{ lg: false, xl: true }})` 49 | // => bg-fuchsia sm:underline text-underline text-center rounded-xl 50 | 51 | tw`text-${'gray'}-100 bg-${'red'}(600 hover:700 ${'focus'}:800)` 52 | // => text-gray-100 bg-red-600 hover:bg-red-700 focus:bg-red-800 53 | ``` 54 | 55 |
56 | 57 | It is possible to invoke the {@link twind.tw | tw} function in a multitude of different ways. It can take any number of arguments, each of which can be an Object, Array, Boolean, Number, String or Template Literal. This ability is inspired heavily by the [clsx](https://npmjs.com/clsx) library by [Luke Edwards](https://github.com/lukeed). 58 | 59 | > Note any falsey values are always discarded as well as standalone boolean and number values 60 | 61 | ```js 62 | // Strings 63 | tw('bg-gray-200', true && 'rounded', 'underline') 64 | //=> bg-gray-200 rounded underline 65 | 66 | // Objects 67 | tw({ 'bg-gray-200': true, rounded: false, underline: isTrue() }) 68 | //=> bg-gray-200 underline 69 | 70 | // Arrays 71 | tw(['bg-gray-200', 0, false, 'rounded']) 72 | //=> bg-gray-200 rounded 73 | 74 | // Mixed 75 | tw('bg-gray-200', [ 76 | 1 && 'rounded', 77 | { underline: false, 'text-black': null }, 78 | ['text-lg', ['shadow-lg']], 79 | ]) 80 | //=> bg-gray-200 rounded text-lg shadow-lg 81 | ``` 82 | 83 |
Show me more examples 84 | 85 | ```js 86 | tw({ 'bg-gray-200': true }, { rounded: false }, null, { underline: true }) 87 | //=> bg-gray-200 underline 88 | 89 | tw({ hover: ['bg-red-500', 'p-3'] }, 'm-1') 90 | // => hover:bg-red-500 hover:p-3 m-1 91 | 92 | tw(['bg-gray-200'], ['', 0, false, 'rounded'], [['underline']]) 93 | //=> bg-gray-200 rounded underline 94 | 95 | tw({ 96 | sm: ['hover:rounded', 'active:rounded-full'], 97 | md: { rounded: true, hover: 'bg-white' }, 98 | lg: { 99 | 'rounded-full': true, 100 | hover: 'bg-white text-black active:(underline shadow)', 101 | }, 102 | }) 103 | // sm:hover:rounded sm:active:rounded-full md:rounded md:hover:bg-white lg:rounded-full lg:hover:bg-white lg:hover:text-black lg:hover:active:underline lg:hover:active:shadow 104 | ``` 105 | 106 |
107 | 108 | ## The apply function 109 | 110 | To be documented... 111 | 112 |
113 | 114 | Continue to {@page Thinking in Groups} 115 | -------------------------------------------------------------------------------- /example/shim.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Twind - Auto Shim 8 | 9 | 14 | 15 | 16 |
17 |
18 |
21 |
22 |
23 |
24 |
25 |

26 | An advanced online playground for Tailwind CSS, including support for things like: 27 |

28 |
    29 |
  • 30 | 31 | 36 | 41 | 42 | 43 |

    44 | Customizing your 45 | tailwind.config.js file 46 |

    47 |
  • 48 |
  • 49 | 50 | 55 | 60 | 61 | 62 |

    63 | Extracting twes with 64 | @apply 65 |

    66 |
  • 67 |
  • 68 | 69 | 74 | 79 | 80 | 81 |

    Code completion with instant preview

    82 |
  • 83 |
84 |

85 | Perfect for learning how the framework works, prototyping a new idea, or creating 86 | a demo to share online. 87 |

88 |
89 |
90 |

Want to dig deeper into Tailwind?

91 |

92 | 93 | Read the docs → 94 | 95 |

96 |
97 |
98 |
99 |
100 |
101 |
102 | 103 | 104 | -------------------------------------------------------------------------------- /src/internal/util.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Context, 3 | Hasher, 4 | Falsy, 5 | MaybeThunk, 6 | CSSRules, 7 | ThemeScreen, 8 | ThemeScreenValue, 9 | } from '../types' 10 | 11 | interface Includes { 12 | (value: string, search: string): boolean 13 | (value: readonly T[], search: T): boolean 14 | } 15 | 16 | export const includes: Includes = (value: string | readonly unknown[], search: unknown) => 17 | // eslint-disable-next-line no-implicit-coercion 18 | !!~(value as string).indexOf(search as string) 19 | 20 | export const join = (parts: readonly string[], separator = '-'): string => parts.join(separator) 21 | 22 | export const joinTruthy = (parts: readonly (string | Falsy)[], separator?: string): string => 23 | join(parts.filter(Boolean) as string[], separator) 24 | 25 | export const tail = (array: T, startIndex = 1): T => 26 | array.slice(startIndex) as T 27 | 28 | export const identity = (value: T): T => value 29 | 30 | export const noop = (): void => { 31 | /* no-op */ 32 | } 33 | 34 | export const capitalize = (value: string): string => value[0].toUpperCase() + tail(value) 35 | 36 | export const hyphenate = (value: string): string => value.replace(/[A-Z]/g, '-$&').toLowerCase() 37 | 38 | export const evalThunk = (value: MaybeThunk, context: Context): T => { 39 | while (typeof value === 'function') { 40 | value = (value as (context: Context) => T)(context) 41 | } 42 | 43 | return value 44 | } 45 | 46 | export const ensureMaxSize = (map: Map, max: number): void => { 47 | // Ensure the cache does not grow unlimited 48 | if (map.size > max) { 49 | map.delete(map.keys().next().value) 50 | } 51 | } 52 | 53 | export const merge = (target: CSSRules, source: CSSRules, context: Context): CSSRules => 54 | source 55 | ? Object.keys(source).reduce((target, key) => { 56 | const value = evalThunk(source[key], context) 57 | 58 | if (value && typeof value === 'object' && !Array.isArray(value)) { 59 | target[key] = merge((target[key] || {}) as CSSRules, value as CSSRules, context) 60 | } else { 61 | // hyphenate target key only if key is property like (\w-) 62 | target[hyphenate(key)] = value 63 | } 64 | 65 | return target 66 | }, target) 67 | : target 68 | 69 | export const escape = 70 | (typeof CSS !== 'undefined' && CSS.escape) || 71 | // Simplified: escaping only special characters 72 | // Needed for NodeJS and Edge <79 (https://caniuse.com/mdn-api_css_escape) 73 | ((className: string): string => { 74 | const firstCodeUnit = className.charCodeAt(0) 75 | let firstChar = '' 76 | 77 | // If the character is the first character and is in the range [0-9] (2xl, ...) 78 | if (firstCodeUnit >= 0x0030 && firstCodeUnit <= 0x0039) { 79 | // https://drafts.csswg.org/cssom/#escape-a-character-as-code-point 80 | firstChar = '\\' + firstCodeUnit.toString(16) + ' ' 81 | className = tail(className) 82 | } 83 | 84 | // Simplifed escape testing only for chars that we know happen to be in tailwind directives 85 | return firstChar + className.replace(/[!./:#]/g, '\\$&') 86 | }) 87 | 88 | export const buildMediaQuery = (screen: ThemeScreen): string => { 89 | if (!Array.isArray(screen)) { 90 | screen = [screen as ThemeScreenValue] 91 | } 92 | 93 | return ( 94 | '@media ' + 95 | join( 96 | (screen as ThemeScreenValue[]).map((screen) => { 97 | if (typeof screen === 'string') { 98 | screen = { min: screen } 99 | } 100 | 101 | return ( 102 | (screen as { raw?: string }).raw || 103 | join( 104 | Object.keys(screen).map( 105 | (feature) => `(${feature}-width:${(screen as Record)[feature]})`, 106 | ), 107 | ' and ', 108 | ) 109 | ) 110 | }), 111 | ',', 112 | ) 113 | ) 114 | } 115 | 116 | // Based on https://stackoverflow.com/a/52171480 117 | export const cyrb32: Hasher = (value: string): string => { 118 | let h = 9 119 | 120 | for (let index = value.length; index--; ) { 121 | h = Math.imul(h ^ value.charCodeAt(index), 0x5f356495) 122 | } 123 | 124 | return 'tw-' + ((h ^ (h >>> 9)) >>> 0).toString(36) 125 | } 126 | 127 | /** 128 | * Find the array index of where to add an element to keep it sorted. 129 | * 130 | * @returns The insertion index 131 | */ 132 | export const sortedInsertionIndex = (array: readonly number[], element: number): number => { 133 | let high = array.length 134 | 135 | // Theres only one option then 136 | if (high === 0) return 0 137 | 138 | // Find position by binary search 139 | for (let low = 0; low < high; ) { 140 | const pivot = (high + low) >> 1 141 | 142 | // Less-Then-Equal to add new equal element after all existing equal elements (stable sort) 143 | if (array[pivot] <= element) { 144 | low = pivot + 1 145 | } else { 146 | high = pivot 147 | } 148 | } 149 | 150 | return high 151 | } 152 | -------------------------------------------------------------------------------- /src/__tests__/plugins.test.ts: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu' 2 | import * as assert from 'uvu/assert' 3 | 4 | import type { Instance, Configuration } from '../types' 5 | import type { VirtualSheet } from '../sheets/index' 6 | 7 | import { virtualSheet } from '../sheets/index' 8 | import { css, theme, apply } from '../css/index' 9 | import { create, strict } from '../index' 10 | 11 | const test = suite<{ 12 | sheet: VirtualSheet 13 | setup: (config?: Configuration) => Instance 14 | }>('plugins') 15 | 16 | test.before.each((context) => { 17 | context.sheet = virtualSheet() 18 | context.setup = (config?: Configuration): Instance => 19 | create({ sheet: context.sheet, mode: strict, preflight: false, prefix: false, ...config }) 20 | }) 21 | 22 | test('value can be a token string', ({ setup, sheet }) => { 23 | const { tw } = setup({ 24 | plugins: { 25 | card: 'max-w-md mx-auto bg-white rounded-xl shadow-md overflow-hidden md:max-w-2xl', 26 | }, 27 | }) 28 | 29 | assert.is( 30 | tw('mx-auto card my-4'), 31 | 'mx-auto max-w-md mx-auto bg-white rounded-xl shadow-md overflow-hidden md:max-w-2xl my-4', 32 | ) 33 | assert.equal(sheet.target, [ 34 | '*{--tw-shadow:0 0 transparent}', 35 | '.mx-auto{margin-left:auto;margin-right:auto}', 36 | '.bg-white{--tw-bg-opacity:1;background-color:#fff;background-color:rgba(255,255,255,var(--tw-bg-opacity))}', 37 | '.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);box-shadow:0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);box-shadow:var(--tw-ring-offset-shadow,0 0 transparent),var(--tw-ring-shadow,0 0 transparent),var(--tw-shadow)}', 38 | '.my-4{margin-bottom:1rem;margin-top:1rem}', 39 | '.overflow-hidden{overflow:hidden}', 40 | '.max-w-md{max-width:28rem}', 41 | '.rounded-xl{border-radius:0.75rem}', 42 | '@media (min-width:768px){.md\\:max-w-2xl{max-width:42rem}}', 43 | ]) 44 | }) 45 | 46 | test('plugin can return new tokens to parse using `tw`', ({ setup }) => { 47 | const { tw } = setup({ 48 | plugins: { 49 | btn(args, { theme, tw }) { 50 | if (args[0]) { 51 | const color = theme('colors', args[0] + '-500', '') 52 | 53 | if (color) { 54 | return tw`hover:bg(${args[0] + '-500'}) underline)` 55 | } 56 | } else { 57 | return tw('font-bold py-2 px-4 rounded') 58 | } 59 | }, 60 | }, 61 | }) 62 | 63 | assert.is(tw('btn'), 'font-bold py-2 px-4 rounded') 64 | assert.is(tw('btn-purple cursor-not-allowed'), 'hover:bg-purple-500 underline cursor-not-allowed') 65 | assert.is( 66 | tw('btn cursor-not-allowed btn-purple transition'), 67 | 'font-bold py-2 px-4 rounded cursor-not-allowed hover:bg-purple-500 underline transition', 68 | ) 69 | assert.is( 70 | tw('btn sm:focus:btn-purple transition'), 71 | 'font-bold py-2 px-4 rounded sm:focus:hover:bg-purple-500 sm:focus:underline transition', 72 | ) 73 | 74 | assert.throws(() => tw('btn-unknown-color'), /UNKNOWN_DIRECTIVE/) 75 | }) 76 | 77 | test('value can be a css object', ({ setup, sheet }) => { 78 | const { tw } = setup({ 79 | plugins: { 80 | amazing: { color: 'blue', fontSize: '99px' }, 81 | }, 82 | }) 83 | 84 | assert.is(tw('amazing'), 'amazing') 85 | assert.equal(sheet.target, ['.amazing{color:blue;font-size:99px}']) 86 | }) 87 | 88 | test('value can be an CSS directive', ({ setup, sheet }) => { 89 | const { tw } = setup({ 90 | plugins: { 91 | amazing: css` 92 | color: red; 93 | font-size: ${theme('spacing.96')}; 94 | `, 95 | }, 96 | }) 97 | 98 | assert.is(tw('amazing'), 'amazing') 99 | assert.equal(sheet.target, ['.amazing{color:red;font-size:24rem}']) 100 | }) 101 | 102 | test('value can return an CSS directive', ({ setup, sheet }) => { 103 | const { tw } = setup({ 104 | plugins: { 105 | amazing: (params) => css` 106 | color: ${theme('colors', params[0] + '-500')}; 107 | font-size: ${theme('spacing.96')}; 108 | `, 109 | }, 110 | }) 111 | 112 | assert.is(tw('amazing-blue'), 'amazing-blue') 113 | assert.equal(sheet.target, ['.amazing-blue{color:#3b82f6;font-size:24rem}']) 114 | }) 115 | 116 | test('value can be an apply directive', ({ setup, sheet }) => { 117 | const { tw } = setup({ 118 | plugins: { 119 | amazing: apply`text(red-500 7xl)`, 120 | }, 121 | }) 122 | 123 | assert.is(tw('amazing'), 'amazing') 124 | assert.equal(sheet.target, [ 125 | '.amazing{--tw-text-opacity:1;color:#ef4444;color:rgba(239,68,68,var(--tw-text-opacity));font-size:4.5rem;line-height:1}', 126 | ]) 127 | }) 128 | 129 | test('value can be an apply directive', ({ setup, sheet }) => { 130 | const { tw } = setup({ 131 | plugins: { 132 | amazing: (params) => apply`text(${params[0]}-500 7xl)`, 133 | }, 134 | }) 135 | 136 | assert.is(tw('amazing-blue'), 'amazing-blue') 137 | assert.equal(sheet.target, [ 138 | '.amazing-blue{--tw-text-opacity:1;color:#3b82f6;color:rgba(59,130,246,var(--tw-text-opacity));font-size:4.5rem;line-height:1}', 139 | ]) 140 | }) 141 | 142 | test.run() 143 | -------------------------------------------------------------------------------- /src/sheets/README.md: -------------------------------------------------------------------------------- 1 | [![Documentation](https://flat.badgen.net/badge/icon/Documentation?icon=awesome&label)](https://twind.dev/docs/modules/twind_sheets.html) 2 | [![Github](https://flat.badgen.net/badge/icon/tw-in-js%2Ftwind%2Fsrc%2Fsheets?icon=github&label)](https://github.com/tw-in-js/twind/tree/main/src/sheets) 3 | [![Module Size](https://flat.badgen.net/badgesize/brotli/https:/unpkg.com/twind/sheets/sheets.js?icon=jsdelivr&label&color=blue&cache=10800)](https://unpkg.com/twind/sheets/sheets.js 'brotli module size') 4 | [![Typescript](https://flat.badgen.net/badge/icon/included?icon=typescript&label)](https://unpkg.com/browse/twind/sheets/sheets.d.ts) 5 | 6 | This module provides [virtualSheet](#virtual-sheet) and [domSheet](#dom-sheet) which can be used with {@link twind.setup | setup}({ {@link twind.Configuration.sheet | sheet} }). 7 | 8 | 9 | 10 | 11 | - [Virtual Sheet](#virtual-sheet) 12 | - [Using for Static Extraction a.k.a. Server Side Rendering (SSR)](#using-for-static-extraction-aka-server-side-rendering-ssr) 13 | - [Using in tests](#using-in-tests) 14 | - [DOM Sheet](#dom-sheet) 15 | - [Custom Sheet Implementation](#custom-sheet-implementation) 16 | 17 | 18 | 19 | ## Virtual Sheet 20 | 21 | The virtual sheet collects style rules into an array. This is most useful during testing and {@page Extract Styles aka SSR | server side rendering (SSR)}. 22 | 23 | Additionally it provides an API to reset the current internal state of its `tw` function. 24 | 25 | ### Using for Static Extraction a.k.a. Server Side Rendering (SSR) 26 | 27 | > 💡 You can find detailed instructions and example in the {@page Extract Styles aka SSR | Server Side Rendering (SSR) guide}. 28 | 29 | The following example assumes your app is using the `tw` named export from `twind` 30 | but the same logic can be applied to custom instances. 31 | 32 | ```js 33 | import { setup } from 'twind' 34 | import { virtualSheet, getStyleTag } from 'twind/sheets' 35 | 36 | const sheet = virtualSheet() 37 | 38 | setup({ ...sharedOptions, sheet }) 39 | 40 | function ssr() { 41 | // 1. Reset the sheet for a new rendering 42 | sheet.reset() 43 | 44 | // 2. Render the app 45 | const body = renderTheApp() 46 | 47 | // 3. Create the style tag with all generated CSS rules 48 | const styleTag = getStyleTag(sheet) 49 | 50 | // 4. Generate the response html 51 | return ` 52 | 53 | ${styleTag} 54 | ${body} 55 | 56 | ` 57 | } 58 | ``` 59 | 60 | ### Using in tests 61 | 62 | > 💡 The example below uses [uvu](https://github.com/lukeed/uvu). Please adjust the test code to your testing framework. 63 | 64 | ```js 65 | import { suite } from 'uvu' 66 | import * as assert from 'uvu/assert' 67 | 68 | import { create } from 'twind' 69 | import { virtualSheet } from 'twind/sheets' 70 | 71 | const test = suite('using virtual sheet') 72 | 73 | // Setup code to be run once before all tests 74 | test.before((context) => { 75 | context.sheet = virtualSheet() 76 | 77 | const instance = create({ 78 | sheet: context.sheet, 79 | // Fail tests on unknown rules or theme values 80 | mode: 'strict', 81 | // Prevent preflight rules to be added into sheet 82 | preflight: false, 83 | // Do not prefix properties and values 84 | prefix: false, 85 | }) 86 | 87 | context.tw = instance.tw 88 | }) 89 | 90 | // Clear the state before each test 91 | test.before.each(({ sheet }) => { 92 | sheet.reset() 93 | }) 94 | 95 | test('render one rule', ({ tw, sheet }) => { 96 | assert.is(tw`text(center)`, 'text-center') 97 | assert.equal(sheet.target, '.text-center{text-align:center}') 98 | }) 99 | ``` 100 | 101 | ## DOM Sheet 102 | 103 | This sheet uses DOM Text nodes to insert the CSS rules into the stylesheet. Using DOM manipulation makes this way slower than the {@link twind.cssomSheet | default sheet} but allows to see the generated CSS in to DOM. Most modern browser display CSS rules from the speedy default sheet using their CSS inspector. 104 | 105 | > 💡 In production it is advised to use {@link twind.cssomSheet | speedy default sheet}. 106 | 107 | If the `domSheet` is passed no `target` it looks for an style element with the id `__twind`. If no such element is found it will create one and append it to the `document.head`. 108 | 109 | ```js 110 | import { setup } from 'twind' 111 | import { domSheet } from 'twind/sheets' 112 | 113 | setup({ ...sharedOptions, sheet: domSheet() }) 114 | ``` 115 | 116 | ## Custom Sheet Implementation 117 | 118 | In case the builtin sheet implementations do not solve your use case, you can create your own: 119 | 120 | ```js 121 | import { setup } from 'twind' 122 | 123 | const customSheet = (target = []) => ({ 124 | target, 125 | insert: (rule, index) => { 126 | // rule: the CSS rule to insert 127 | // index: the rule's position 128 | target.splice(index, 0, rule) 129 | }, 130 | }) 131 | 132 | setup({ ...sharedOptions, sheet: customSheet() }) 133 | ``` 134 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twind", 3 | "version": "0.15.1", 4 | "description": "compiles tailwind like shorthand syntax into css at runtime", 5 | "// mark as private to prevent accidental publish - use 'yarn release'": "", 6 | "private": true, 7 | "keywords": [ 8 | "tailwind", 9 | "tw-in-js", 10 | "tailwind-in-js" 11 | ], 12 | "homepage": "https://twind.dev", 13 | "bugs": "https://github.com/tw-in-js/twind/issues", 14 | "repository": "github:tw-in-js/twind", 15 | "license": "MIT", 16 | "contributors": [ 17 | "Luke Jackson (lukejacksonn.github.io)", 18 | "Sascha Tandel (https://github.com/sastan)" 19 | ], 20 | "engines": { 21 | "node": ">=10.13" 22 | }, 23 | "// The 'module', 'unpkg' and 'types' fields are added by distilt": "", 24 | "main": "src/index.ts", 25 | "// Each entry is expanded into several bundles (module, script, types, require, node, and default)": "", 26 | "exports": { 27 | ".": "./src/index.ts", 28 | "./colors": "./src/colors/index.ts", 29 | "./css": "./src/css/index.ts", 30 | "./observe": "./src/observe/index.ts", 31 | "./shim": "./src/shim/index.ts", 32 | "./shim/server": "./src/shim/server/index.ts", 33 | "./server": "./src/server/index.ts", 34 | "./sheets": "./src/sheets/index.ts", 35 | "./package.json": "./package.json" 36 | }, 37 | "// These are relative from within the dist/ folder": "", 38 | "sideEffects": [ 39 | "./shim/shim.js" 40 | ], 41 | "size-limit": [ 42 | { 43 | "path": "dist/twind.js", 44 | "gzip": true, 45 | "limit": "12.5kb" 46 | } 47 | ], 48 | "// These are ONLY bundled (eg included) in the umd builds": "", 49 | "bundledDependencies": [ 50 | "style-vendorizer" 51 | ], 52 | "dependencies": { 53 | "csstype": "^3.0.5", 54 | "htmlparser2": "^6.0.0", 55 | "style-vendorizer": "^2.0.0" 56 | }, 57 | "peerDependencies": { 58 | "typescript": "^4.1.0" 59 | }, 60 | "peerDependenciesMeta": { 61 | "typescript": { 62 | "optional": true 63 | } 64 | }, 65 | "devDependencies": { 66 | "@size-limit/file": "^4.9.1", 67 | "@types/jsdom": "^16.2.5", 68 | "@typescript-eslint/eslint-plugin": "^4.9.1", 69 | "@typescript-eslint/parser": "^4.9.1", 70 | "c8": "^7.3.5", 71 | "distilt": "^0.9.4", 72 | "dlv": "^1.1.3", 73 | "doctoc": "^2.0.0", 74 | "esbuild": "^0.8.31", 75 | "esbuild-register": "^2.0.0", 76 | "eslint": "^7.15.0", 77 | "eslint-config-prettier": "^7.0.0", 78 | "eslint-plugin-prettier": "^3.2.0", 79 | "esm": "^3.2.25", 80 | "husky": "^4.3.8", 81 | "jsdom": "^16.4.0", 82 | "lint-staged": ">=10", 83 | "prettier": "^2.0.5", 84 | "size-limit": "^4.9.1", 85 | "snoop": "^1.0.2", 86 | "snowpack": "^3.0.11", 87 | "tailwindcss": "^2.0.1", 88 | "typedoc": "^0.20.24", 89 | "typedoc-plugin-pages": "npm:@sastan/typedoc-plugin-pages@0.0.1", 90 | "typedoc-plugin-param-names": "^2.0.0", 91 | "typescript": "^4.1.3", 92 | "uvu": "^0.5.1", 93 | "watchlist": "^0.2.3" 94 | }, 95 | "// Some packages require Node v12, we support v10 => mark them as optional": "", 96 | "optionalDependencies": { 97 | "tailwindcss": "^2.0.1", 98 | "typedoc-plugin-param-names": "^2.0.0" 99 | }, 100 | "scripts": { 101 | "build": "distilt", 102 | "preformat": "doctoc --update-only --notitle --maxlevel 3 docs src", 103 | "format": "prettier --write --ignore-path .gitignore .", 104 | "lint": "eslint --ext .js,.ts --ignore-path .gitignore .", 105 | "lint:fix": "yarn lint -- --fix", 106 | "release": "npx np --contents dist", 107 | "start": "snowpack --config example/snowpack.config.js dev", 108 | "test": "uvu -r esm -r ./src/__fixtures__/env.js", 109 | "test:coverage": "c8 --src src -x '**/__fixtures__/**' -x '**/__tests__/**' -x '**/*.test.ts' -x 'src/types/**' --all -r lcov -r text yarn test", 110 | "test:watch": "watchlist src -- yarn test", 111 | "version": "yarn build", 112 | "typedoc": "typedoc" 113 | }, 114 | "prettier": { 115 | "printWidth": 100, 116 | "semi": false, 117 | "singleQuote": true, 118 | "trailingComma": "all", 119 | "bracketSpacing": true 120 | }, 121 | "eslintConfig": { 122 | "root": true, 123 | "parserOptions": { 124 | "ecmaVersion": 2020, 125 | "sourceType": "module" 126 | }, 127 | "env": { 128 | "es6": true, 129 | "shared-node-browser": true 130 | }, 131 | "extends": [ 132 | "eslint:recommended", 133 | "plugin:prettier/recommended" 134 | ], 135 | "overrides": [ 136 | { 137 | "files": [ 138 | "snowpack.config.js" 139 | ], 140 | "env": { 141 | "node": true 142 | }, 143 | "parserOptions": { 144 | "ecmaVersion": 2019, 145 | "sourceType": "script" 146 | } 147 | }, 148 | { 149 | "files": [ 150 | "**/*.ts" 151 | ], 152 | "parser": "@typescript-eslint/parser", 153 | "plugins": [ 154 | "@typescript-eslint" 155 | ], 156 | "extends": [ 157 | "plugin:@typescript-eslint/recommended" 158 | ] 159 | } 160 | ] 161 | }, 162 | "husky": { 163 | "hooks": { 164 | "pre-commit": "lint-staged" 165 | } 166 | }, 167 | "lint-staged": { 168 | "docs/**/*.md": "doctoc --update-only --notitle --maxlevel 3", 169 | "*.{js,jsx,cjs,mjs,ts,tsx}": "eslint --cache --fix", 170 | "*.{json,js,jsx,cjs,mjs,ts,tsx,md,html,css,yml,yaml}": "prettier --write" 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /docs/getting-started/using-the-shim.md: -------------------------------------------------------------------------------- 1 | > Seamless integration with existing Tailwind HTML. This feature can be used together with your favorite framework without any additional setup. 2 | 3 | The {@link twind/shim} module allows for the use of the `class` attribute for tailwind rules. If such a rule is detected, the corresponding CSS rule is created and injected into the stylesheet dynamically. The default {@link twind/shim} export is intended for [client-side](#client-side-dynamic-extraction) usage and, without configuration, utilizes the default/global `tw` instance. For [server-side](#server-side-static-extraction) usage, {@link twind/shim/server} exports a dedicated {@link twind/shim/server.shim | shim} function that will parse and update a static HTML string while collecting the style rules into a sheet for further usage in your respective framework. 4 | 5 | > 💡 `twind/shim` can be used together with `tw` and every framework as it detects `class` changes. All Twind syntax features like {@page Thinking in Groups | grouping} are supported. 6 | 7 | 8 | 9 | 10 | - [Dynamic Extraction](#dynamic-extraction) 11 | - [Static Extraction](#static-extraction) 12 | 13 | 14 | 15 | ## Dynamic Extraction 16 | 17 | For runtime processing of your javascript-assisted HTML documents, simply include the {@link twind/shim} module and watch the magic happen. 18 | 19 | ```html 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 |

This is Twind!

28 |
29 | 30 | 31 | ``` 32 | 33 | > 🚀 [live and interactive shim demo](https://esm.codes/#aW1wb3J0ICdodHRwczovL2Nkbi5za3lwYWNrLmRldi90d2luZC9zaGltJwoKZG9jdW1lbnQuYm9keS5pbm5lckhUTUwgPSBgCiAgPG1haW4gY2xhc3M9Imgtc2NyZWVuIGJnLXB1cnBsZS00MDAgZmxleCBpdGVtcy1jZW50ZXIganVzdGlmeS1jZW50ZXIiPgogICAgPGgxIGNsYXNzPSJmb250LWJvbGQgdGV4dChjZW50ZXIgNXhsIHdoaXRlIHNtOmdyYXktODAwIG1kOnBpbmstNzAwKSI+CiAgICAgIFRoaXMgaXMgVHdpbmQhCiAgICA8L2gxPgogIDwvbWFpbj4KYA==) 34 | 35 | To prevent FOUC (_flash of unstyled content_) it is advised to set the `hidden` attribute on the target element. {@link twind/shim} will remove it once all styles have been generated. 36 | 37 | ```html 38 | 39 | 40 | 41 | 42 | ``` 43 | 44 |
How can I use twind/shim from javascript (Click to expand) 45 | 46 | > Internally {@link twind/shim} uses {@link twind/observe} which may be useful for advanced use cases. 47 | 48 | ```js 49 | import 'twind/shim' 50 | ``` 51 | 52 | ```js 53 | import { setup, disconnect } from 'twind/shim' 54 | ``` 55 | 56 |
57 | 58 |
How to support legacy browser with the UMD bundles (Click to expand) 59 | 60 | > You may need to provide certain [polyfills](./browser-support.md) depending on your target browser. 61 | 62 | ```html 63 | 64 | 65 | 66 | ``` 67 | 68 |
69 | 70 |
Implementation Details (Click to expand) 71 | 72 | {@link twind/shim} starts {@link twind/observe | observing} class attributes changes right after the [DOM content has been loaded](https://developer.mozilla.org/en-US/docs/Web/API/Document/DOMContentLoaded_event). For further details see {@link twind/observe | twind/observe}. 73 | 74 |
75 | 76 | ## Static Extraction 77 | 78 | If you wish to remove Twind's runtime overhead or you're interested in using Twind in a universal or "isomorphic" web app, {@link wind/shim/server} exports the dedicated {@link wind/shim/server.shim} function for performant processing of static HTML. 79 | 80 | > 💡 You'll find more details and examples in the {@page Extract Styles aka SSR} guide. 81 | 82 | ```js 83 | import { setup } from 'twind' 84 | import { virtualSheet, getStyleTag, shim } from 'twind/shim/server' 85 | 86 | const sheet = virtualSheet() 87 | 88 | setup({ ...sharedOptions, sheet }) 89 | 90 | function ssr() { 91 | // 1. Reset the sheet for a new rendering 92 | sheet.reset() 93 | 94 | // 2. Render the app to an html string and handle class attributes 95 | const body = shim(renderTheApp()) 96 | 97 | // 3. Create the style tag with all generated CSS rules 98 | const styleTag = getStyleTag(sheet) 99 | 100 | // 4. Generate the response html 101 | return ` 102 | 103 | ${styleTag} 104 | ${body} 105 | 106 | ` 107 | } 108 | ``` 109 | 110 | In order to prevent harmful code injection on the web, a [Content Security Policy (CSP)](https://developer.mozilla.org/docs/Web/HTTP/CSP) may be put in place. During server-side rendering, a cryptographic nonce (number used once) may be embedded when generating a page on demand: 111 | 112 | ```js 113 | // ... other code is the same as before ... 114 | 115 | // Usage with webpack: https://webpack.js.org/guides/csp/ 116 | const styleTag = getStyleTag(sheet, { nonce: __webpack_nonce__ }) 117 | ``` 118 | 119 |
120 | 121 | Continue to {@page Customize the Theme} 122 | -------------------------------------------------------------------------------- /src/__tests__/preflight.test.ts: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu' 2 | import * as assert from 'uvu/assert' 3 | 4 | import { virtualSheet } from '../sheets/index' 5 | import { create, strict } from '../index' 6 | 7 | const test = suite('preflight') 8 | 9 | test('add preflight styles', () => { 10 | const sheet = virtualSheet() 11 | 12 | const { tw } = create({ sheet, mode: strict }) 13 | 14 | // Ensure utitilities are added after base styles 15 | assert.is(tw`text-center`, 'text-center') 16 | 17 | assert.equal(sheet.target, [ 18 | 'button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;margin:0;padding:0;line-height:inherit;color:inherit}', 19 | 'sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}', 20 | 'html{line-height:1.5;-webkit-text-size-adjust:100%;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"}', 21 | 'table{text-indent:0;border-color:inherit;border-collapse:collapse}', 22 | 'hr{height:0;color:inherit;border-top-width:1px}', 23 | 'input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}', 24 | '::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}', 25 | 'button{background-color:transparent;background-image:none}', 26 | 'body{font-family:inherit;line-height:inherit}', 27 | '*,::before,::after{box-sizing:border-box;border:0 solid #e5e7eb}', 28 | 'h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}', 29 | 'a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}', 30 | '::-moz-focus-inner{border-style:none;padding:0}', 31 | '[type="search"]{-webkit-appearance:textfield;outline-offset:-2px}', 32 | 'pre,code,kbd,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}', 33 | 'img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}', 34 | 'img,video{max-width:100%;height:auto}', 35 | 'body,blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre,fieldset,ol,ul{margin:0}', 36 | 'button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}', 37 | 'fieldset,ol,ul,legend{padding:0}', 38 | 'textarea{resize:vertical}', 39 | 'button,[role="button"]{cursor:pointer}', 40 | ':-moz-focusring{outline:1px dotted ButtonText}', 41 | '::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}', 42 | 'summary{display:list-item}', 43 | ':root{-moz-tab-size:4;tab-size:4}', 44 | 'ol,ul{list-style:none}', 45 | 'img{border-style:solid}', 46 | 'button,select{text-transform:none}', 47 | ':-moz-ui-invalid{box-shadow:none}', 48 | 'progress{vertical-align:baseline}', 49 | 'abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}', 50 | 'b,strong{font-weight:bolder}', 51 | 'sub{bottom:-0.25em}', 52 | 'sup{top:-0.5em}', 53 | 'button,[type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}', 54 | '::-webkit-search-decoration{-webkit-appearance:none}', 55 | '.text-center{text-align:center}', 56 | ]) 57 | }) 58 | 59 | test('add preflight styles with custom theme', () => { 60 | const sheet = virtualSheet() 61 | create({ 62 | sheet, 63 | theme: { 64 | extend: { 65 | fontFamily: { sans: 'ui-sans-serif', mono: 'ui-monospace' }, 66 | borderColor: { DEFAULT: '#222' }, 67 | placeholderColor: { DEFAULT: '#333' }, 68 | }, 69 | }, 70 | }) 71 | 72 | assert.ok( 73 | sheet.target.includes( 74 | 'html{line-height:1.5;-webkit-text-size-adjust:100%;font-family:ui-sans-serif}', 75 | ), 76 | ) 77 | assert.ok(sheet.target.includes('*,::before,::after{box-sizing:border-box;border:0 solid #222}')) 78 | assert.ok(sheet.target.includes('input::placeholder,textarea::placeholder{opacity:1;color:#333}')) 79 | assert.ok(sheet.target.includes('pre,code,kbd,samp{font-family:ui-monospace;font-size:1em}')) 80 | }) 81 | 82 | test('add preflight styles with theme missing some values', () => { 83 | const sheet = virtualSheet() 84 | create({ 85 | sheet, 86 | theme: { 87 | fontFamily: { sans: 'ui-sans-serif', mono: 'ui-monospace' }, 88 | colors: {}, 89 | borderColor: {}, 90 | placeholderColor: {}, 91 | }, 92 | }) 93 | 94 | assert.ok( 95 | sheet.target.includes('*,::before,::after{box-sizing:border-box;border:0 solid currentColor}'), 96 | ) 97 | assert.ok( 98 | sheet.target.includes('input::placeholder,textarea::placeholder{opacity:1;color:#a1a1aa}'), 99 | ) 100 | }) 101 | 102 | test('use custom preflight styles', () => { 103 | const sheet = virtualSheet() 104 | create({ 105 | sheet, 106 | preflight: (css) => ({ html: css.html }), 107 | }) 108 | 109 | assert.equal(sheet.target, [ 110 | 'html{line-height:1.5;-webkit-text-size-adjust:100%;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"}', 111 | ]) 112 | }) 113 | 114 | test('use custom preflight with fallback to built-in', () => { 115 | const sheet = virtualSheet() 116 | create({ 117 | sheet, 118 | preflight: () => { 119 | /* no-op */ 120 | }, 121 | }) 122 | 123 | assert.is(sheet.target.length, 37) 124 | }) 125 | 126 | test('use custom preflight JSON style', () => { 127 | const sheet = virtualSheet() 128 | create({ 129 | sheet, 130 | preflight: { 131 | '@font-face': { 132 | 'font-family': 'Baloo', 133 | src: 'url(./Baloo-Regular.ttf)', 134 | }, 135 | }, 136 | }) 137 | 138 | assert.is(sheet.target.length, 38) 139 | assert.ok(sheet.target.includes('@font-face{font-family:Baloo;src:url(./Baloo-Regular.ttf)}')) 140 | }) 141 | 142 | test.run() 143 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | # Push events on main branch 7 | - main 8 | paths-ignore: 9 | # Do not run for docs changes 10 | - 'docs/**' 11 | - '**/*.md' 12 | 13 | pull_request: 14 | paths-ignore: 15 | # Do not run for docs changes 16 | - 'docs/**' 17 | - '**/*.md' 18 | 19 | jobs: 20 | build: 21 | name: Node ${{ matrix.node }} on ${{ matrix.os }} 22 | runs-on: ${{ matrix.os }} 23 | strategy: 24 | matrix: 25 | node: ['10.x', '12.x', '14.x'] 26 | os: [ubuntu-20.04, macos-10.15, windows-2019] 27 | 28 | steps: 29 | - name: Checkout 🛎️ 30 | uses: actions/checkout@v2 31 | 32 | - name: Use Node ${{ matrix.node }} 33 | uses: actions/setup-node@v1 34 | with: 35 | node-version: ${{ matrix.node }} 36 | 37 | - name: Install 🔧 38 | uses: bahmutov/npm-install@v1 39 | 40 | - name: Lint 41 | run: yarn run lint 42 | 43 | - name: Test 44 | run: yarn run test:coverage 45 | 46 | - name: Build 47 | # Not running on Node 10.x as it does not have support for 'fs/promises' 48 | # Not running on windows as it hangs (don't know why) 49 | if: ${{ matrix.node != '10.x' && !startsWith(matrix.os, 'windows') }} 50 | run: yarn run build 51 | 52 | - name: Coveralls 53 | uses: coverallsapp/github-action@master 54 | with: 55 | github-token: ${{ secrets.GITHUB_TOKEN }} 56 | flag-name: build-${{ matrix.node }}-${{ matrix.os }} 57 | parallel: true 58 | 59 | publish-pr: 60 | name: Publish to Github Packages 61 | # Run only for PRs originated from same repo 62 | if: ${{ github.event.pull_request && github.repository == github.event.pull_request.head.repo.full_name }} 63 | needs: build 64 | runs-on: ubuntu-20.04 65 | env: 66 | NPM_PACKAGE_NAME: twind 67 | steps: 68 | - name: Checkout 🛎️ 69 | uses: actions/checkout@v2 70 | 71 | - name: Use Node v14.x 72 | uses: actions/setup-node@v1 73 | with: 74 | node-version: '14' 75 | 76 | - name: Install 🔧 77 | uses: bahmutov/npm-install@v1 78 | 79 | - name: Build 80 | run: yarn run build 81 | 82 | - uses: actions/setup-node@v1 83 | with: 84 | registry-url: 'https://npm.pkg.github.com' 85 | 86 | - name: Publish 87 | env: 88 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 89 | run: | 90 | sed -i -r 's|("name": ")[^"]+(")|\1@${{ github.repository }}\2|;s|("version": ")[^"]+(")|\10.${{ github.event.number }}.${{ github.run_number }}\2|' dist/package.json 91 | 92 | echo '# THIS IS THE [PR ${{ github.event.number }}](${{ github.event.pull_request.html_url }}) PREVIEW PACKAGE' > dist/README.md 93 | echo "" >> dist/README.md 94 | echo '> Official releases are _only_ available on [registry.npmjs.org](https://www.npmjs.com/package/${{ env.NPM_PACKAGE_NAME }}) as `${{ env.NPM_PACKAGE_NAME }}`.' >> dist/README.md 95 | echo "" >> dist/README.md 96 | echo "---" >> dist/README.md 97 | echo "" >> dist/README.md 98 | cat README.md >> dist/README.md 99 | 100 | npm publish --tag "pr${{ github.event.number }}" --access public dist 101 | 102 | - name: Comment 103 | uses: mshick/add-pr-comment@v1 104 | with: 105 | repo-token: ${{ secrets.GITHUB_TOKEN }} 106 | repo-token-user-login: 'github-actions[bot]' # The user.login for temporary GitHub tokens 107 | allow-repeats: false # This is the default 108 | message: | 109 | ## Try the Preview Package 110 | 111 | > Official releases are **only** available on [registry.npmjs.org](https://www.npmjs.com/package/${{ env.NPM_PACKAGE_NAME }}) as `${{ env.NPM_PACKAGE_NAME }}`. 112 | 113 | This PR has been published to [npm.pkg.github.com](https://github.com/orgs/${{ github.repository_owner }}/packages?repo_name=${{ env.NPM_PACKAGE_NAME }}) as `@${{ github.repository }}@pr${{ github.event.number }}`. 114 | 115 | **Install/Update** 116 | 117 |
Configure your NPM client (click to expand) 118 | 119 | Adjust you `.npmrc` to use `https://npm.pkg.github.com` for `@${{ github.repository_owner }}` 120 | 121 | ``` 122 | @${{ github.repository_owner }}:registry=https://npm.pkg.github.com 123 | ``` 124 | 125 | Using the command line: 126 | 127 | ```sh 128 | npm config set @${{ github.repository_owner }}:registry https://npm.pkg.github.com --global 129 | ``` 130 | 131 |
132 | 133 | ```sh 134 | # For npm 135 | npm install --force ${{ env.NPM_PACKAGE_NAME }}@npm:@${{ github.repository }}@pr${{ github.event.number }} 136 | 137 | # For yarn - upgrade implies install 138 | yarn upgrade ${{ env.NPM_PACKAGE_NAME }}@npm:@${{ github.repository }}@pr${{ github.event.number }} 139 | ``` 140 | 141 | size-pr: 142 | name: Size Limit 143 | runs-on: ubuntu-20.04 144 | # Run only for PRs 145 | if: ${{ github.event.pull_request }} 146 | needs: build 147 | env: 148 | CI_JOB_NUMBER: 1 149 | steps: 150 | - name: Checkout 🛎️ 151 | uses: actions/checkout@v2 152 | 153 | - name: Use Node v14.x 154 | uses: actions/setup-node@v1 155 | with: 156 | node-version: '14' 157 | 158 | - name: Install 🔧 159 | uses: bahmutov/npm-install@v1 160 | 161 | - name: size-limit 162 | uses: andresz1/size-limit-action@v1 163 | with: 164 | github_token: ${{ secrets.GITHUB_TOKEN }} 165 | skip_step: install 166 | 167 | coveralls: 168 | name: Collect Coverage 169 | needs: build 170 | runs-on: ubuntu-20.04 171 | steps: 172 | - name: Coveralls Finished 173 | uses: coverallsapp/github-action@master 174 | with: 175 | github-token: ${{ secrets.github_token }} 176 | parallel-finished: true 177 | -------------------------------------------------------------------------------- /src/types/theme.ts: -------------------------------------------------------------------------------- 1 | import type { CSSProperties } from './css' 2 | import type { Context } from './twind' 3 | import type { MaybeArray } from './util' 4 | 5 | export interface ThemeResolver { 6 |
(section: Section): Record> 7 | 8 |
(keypath: `${Section}.${string}`): 9 | | ThemeSectionType 10 | | undefined 11 | 12 |
( 13 | keypath: `${Section}.${string}`, 14 | defaultValue: NonNullable>, 15 | ): NonNullable> 16 | 17 |
(section: Section, key: string | string[]): 18 | | ThemeSectionType 19 | | undefined 20 | 21 |
( 22 | section: Section, 23 | key: string | string[], 24 | defaultValue: NonNullable>, 25 | ): NonNullable> 26 | } 27 | 28 | export interface ThemeHelper { 29 |
(section: Section): ( 30 | context: Context, 31 | ) => Record> 32 | 33 |
(keypath: `${Section}.${string}`): ( 34 | context: Context, 35 | ) => ThemeSectionType | undefined 36 | 37 |
( 38 | keypath: `${Section}.${string}`, 39 | defaultValue: NonNullable>, 40 | ): (context: Context) => NonNullable> 41 | 42 |
(section: Section, key: string | string[]): ( 43 | context: Context, 44 | ) => ThemeSectionType | undefined 45 | 46 |
( 47 | section: Section, 48 | key: string | string[], 49 | defaultValue: NonNullable>, 50 | ): (context: Context) => NonNullable> 51 | } 52 | 53 | export type Unwrap = T extends string[] ? string : T extends Record ? R : T 54 | 55 | export type ThemeSectionType = T extends ThemeSection 56 | ? Unwrap 57 | : Exclude> 58 | 59 | export interface ThemeSectionResolverContext { 60 | /** 61 | * No-op function as negated values are automatically infered and do _not_ not to be in the theme. 62 | */ 63 | readonly negative: ( 64 | records: Record, 65 | ) => Record 66 | 67 | readonly breakpoints: ( 68 | records: Record, 69 | ) => Record 70 | } 71 | 72 | export type ThemeSectionRecord = Record 73 | 74 | export type ThemeSectionResolver = ( 75 | theme: ThemeResolver, 76 | context: ThemeSectionResolverContext, 77 | ) => ThemeSectionRecord 78 | 79 | export type ThemeSection = ThemeSectionRecord | ThemeSectionResolver 80 | 81 | export interface ThemeContainer { 82 | screens?: Record 83 | center?: boolean 84 | padding?: string | Record 85 | } 86 | 87 | export type ThemeScreenValue = 88 | | string 89 | | { raw: string } 90 | | { min: string; max?: string } 91 | | { min?: string; max: string } 92 | 93 | export type ThemeScreen = MaybeArray 94 | 95 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 96 | export interface ThemeColorObject extends Record { 97 | /* empty */ 98 | } 99 | 100 | export type ThemeColor = string | ThemeColorObject 101 | 102 | export type ThemeFontSize = 103 | | string 104 | | [size: string, lineHeight: string] 105 | | [size: string, options: { lineHeight?: string; letterSpacing?: string }] 106 | 107 | export type ThemeOutline = [outline: string, offset: string] 108 | 109 | export interface Theme { 110 | colors: ThemeSection 111 | spacing: ThemeSection 112 | durations: ThemeSection 113 | 114 | screens: ThemeSection 115 | 116 | animation: ThemeSection 117 | backgroundColor: ThemeSection 118 | backgroundImage: ThemeSection 119 | backgroundOpacity: ThemeSection 120 | borderColor: ThemeSection 121 | borderOpacity: ThemeSection 122 | borderRadius: ThemeSection 123 | borderWidth: ThemeSection 124 | boxShadow: ThemeSection 125 | container: ThemeContainer | ThemeSectionResolver 126 | divideColor: ThemeSection 127 | divideOpacity: ThemeSection 128 | divideWidth: ThemeSection 129 | fill: ThemeSection 130 | flex: ThemeSection 131 | fontFamily: ThemeSection 132 | fontSize: ThemeSection 133 | fontWeight: ThemeSection 134 | gap: ThemeSection 135 | gradientColorStops: ThemeSection 136 | height: ThemeSection 137 | inset: ThemeSection 138 | keyframes: ThemeSection> 139 | letterSpacing: ThemeSection 140 | lineHeight: ThemeSection 141 | margin: ThemeSection 142 | maxHeight: ThemeSection 143 | maxWidth: ThemeSection 144 | minHeight: ThemeSection 145 | minWidth: ThemeSection 146 | opacity: ThemeSection 147 | order: ThemeSection 148 | outline: ThemeSection 149 | padding: ThemeSection 150 | placeholderColor: ThemeSection 151 | placeholderOpacity: ThemeSection 152 | ringColor: ThemeSection 153 | ringOffsetColor: ThemeSection 154 | ringOffsetWidth: ThemeSection 155 | ringOpacity: ThemeSection 156 | ringWidth: ThemeSection 157 | rotate: ThemeSection 158 | scale: ThemeSection 159 | skew: ThemeSection 160 | space: ThemeSection 161 | stroke: ThemeSection 162 | strokeWidth: ThemeSection 163 | textColor: ThemeSection 164 | textOpacity: ThemeSection 165 | transitionDelay: ThemeSection 166 | transitionDuration: ThemeSection 167 | transitionProperty: ThemeSection 168 | transitionTimingFunction: ThemeSection 169 | translate: ThemeSection 170 | width: ThemeSection 171 | zIndex: ThemeSection 172 | } 173 | -------------------------------------------------------------------------------- /benchmarks/css.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | import Benchmark from 'benchmark' 3 | 4 | import { tw, apply } from '../src' 5 | import { css } from '../src/css' 6 | 7 | import { css as otion } from 'otion' 8 | import { version as otionVersion } from 'otion/package.json' 9 | 10 | import { css as goober } from 'goober' 11 | import { version as gooberVersion } from 'goober/package.json' 12 | 13 | import { css as emotion } from '@emotion/css' 14 | import { version as emotionVersion } from '@emotion/css/package.json' 15 | 16 | // Run the benchmarks 17 | ;(async function run() { 18 | await objectStyles() 19 | await templateLiteralStyles() 20 | })().catch((error) => { 21 | console.error(error) 22 | process.exit(1) 23 | }) 24 | 25 | function objectStyles(): Promise { 26 | const color = 'black' 27 | 28 | const styles = () => ({ 29 | display: 'inline-block', 30 | borderRadius: '3px', 31 | padding: '0.5rem 0', 32 | margin: '0.5rem 1rem', 33 | width: '11rem', 34 | background: 'transparent', 35 | color: 'white', 36 | border: '2px solid white', 37 | 38 | '&:hover': { 39 | color, 40 | }, 41 | 42 | '&:focus': { 43 | border: '2px dashed black', 44 | }, 45 | 46 | fontSize: '0.875rem', 47 | lineHeight: '1.25rem', 48 | 49 | '@media (min-width: 768px)': { 50 | fontSize: '1rem', 51 | lineHeight: '1.5rem', 52 | }, 53 | 54 | '@media (min-width: 1280px)': { 55 | fontSize: '1.125rem', 56 | lineHeight: '1.75rem', 57 | }, 58 | }) 59 | 60 | console.log('# Object Styles') 61 | console.log('twind (static):', tw(styles)) 62 | console.log( 63 | 'twind (dynamic):', 64 | tw(() => styles()), 65 | ) 66 | console.log('twind (css):', tw(css(styles()))) 67 | console.log('otion:', otion(styles())) 68 | console.log('goober:', goober(styles())) 69 | console.log('emotion:', emotion(styles())) 70 | 71 | return new Promise((resolve, reject) => { 72 | new Benchmark.Suite('Object Styles') 73 | .add('twind (static)', () => tw(styles)) 74 | .add('twind (dynamic)', () => tw(() => styles())) 75 | .add('twind (css)', () => tw(css(styles()))) 76 | .add(`otion@${otionVersion}`, () => otion(styles())) 77 | .add(`goober@${gooberVersion}`, () => goober(styles())) 78 | .add(`emotion@${emotionVersion}`, () => emotion(styles())) 79 | .on('error', reject) 80 | .on('cycle', function (event) { 81 | console.log(String(event.target)) 82 | }) 83 | .on('complete', function () { 84 | const fastest = this.filter('fastest').map('name')[0] 85 | console.log('') 86 | console.log('Fastest is: ' + fastest) 87 | console.log('') 88 | resolve() 89 | }) 90 | .run() 91 | }) 92 | } 93 | 94 | function templateLiteralStyles(): Promise { 95 | const color = 'black' 96 | 97 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 98 | const styles = (impl: (template: TemplateStringsArray, ...args: any[]) => any): any => impl` 99 | display: inline-block; 100 | border-radius: 3px; 101 | padding: 0.5rem 0; 102 | margin: 0.5rem 1rem; 103 | width: 11rem; 104 | background: transparent; 105 | color: white; 106 | border: 2px solid white; 107 | &:hover { 108 | color: ${color}; 109 | } 110 | &:focus { 111 | border: 2px dashed black; 112 | } 113 | 114 | font-size: 0.875rem; 115 | line-height: 1.25rem; 116 | 117 | @media (min-width: 768px) { 118 | font-size: 1rem; 119 | line-height: 1.5rem; 120 | } 121 | 122 | @media (min-width: 1280px) { 123 | font-size: 1.125rem; 124 | line-height: 1.75rem; 125 | } 126 | ` 127 | 128 | console.log('# Template Literal Styles') 129 | console.log( 130 | 'twind (tw):', 131 | tw` 132 | inline-block 133 | rounded 134 | py-2 135 | my-2 mx-4 136 | w-44 137 | bg-transparent 138 | text-white 139 | border(2 solid white) 140 | hover:text-${color} 141 | focus:border(2 dashed black) 142 | text(sm md:base lg:lg) 143 | `, 144 | ) 145 | 146 | const applied = apply` 147 | inline-block 148 | rounded 149 | py-2 150 | my-2 mx-4 151 | w-44 152 | bg-transparent 153 | text-white 154 | border(2 solid white) 155 | hover:text-${color} 156 | focus:border(2 dashed black) 157 | text(sm md:base lg:lg) 158 | ` 159 | 160 | console.log('twind (apply):', tw(applied)) 161 | 162 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 163 | const twind = (...args: any[]): string => tw(css(...args)) 164 | 165 | console.log('twind (css):', styles(twind)) 166 | console.log('goober:', styles(goober)) 167 | console.log('emotion:', styles(emotion)) 168 | 169 | return new Promise((resolve, reject) => { 170 | new Benchmark.Suite('Template Literal') 171 | .add('twind (static)', () => tw(applied)) 172 | .add( 173 | 'twind (tw)', 174 | () => 175 | tw` 176 | inline-block 177 | rounded 178 | py-2 179 | my-2 mx-4 180 | w-44 181 | bg-transparent 182 | text-white 183 | border(2 solid white) 184 | hover:text-${color} 185 | focus:border(2 dashed black) 186 | text(sm md:base lg:lg) 187 | `, 188 | ) 189 | .add('twind (apply)', () => 190 | tw(apply` 191 | inline-block 192 | rounded 193 | py-2 194 | my-2 mx-4 195 | w-44 196 | bg-transparent 197 | text-white 198 | border(2 solid white) 199 | hover:text-${color} 200 | focus:border(2 dashed black) 201 | text(sm md:base lg:lg) 202 | `), 203 | ) 204 | .add(`twind (css)`, () => styles(twind)) 205 | .add(`goober@${gooberVersion}`, () => styles(goober)) 206 | .add(`emotion@${emotionVersion}`, () => styles(emotion)) 207 | .on('error', reject) 208 | .on('cycle', function (event) { 209 | console.log(String(event.target)) 210 | }) 211 | .on('complete', function () { 212 | const fastest = this.filter('fastest').map('name')[0] 213 | console.log('') 214 | console.log('Fastest is: ' + fastest) 215 | console.log('') 216 | resolve() 217 | }) 218 | .run() 219 | }) 220 | } 221 | -------------------------------------------------------------------------------- /src/colors/index.ts: -------------------------------------------------------------------------------- 1 | // Source from https://github.com/tailwindlabs/tailwindcss/blob/master/colors.js 2 | // License: MIT 3 | 4 | /** 5 | * [[include:src/colors/README.md]] 6 | * 7 | * @packageDocumentation 8 | * @module twind/colors 9 | */ 10 | 11 | import type { ThemeColor } from '../types' 12 | 13 | export const black = '#000' 14 | export const white = '#fff' 15 | 16 | export const rose: ThemeColor = { 17 | 50: '#fff1f2', 18 | 100: '#ffe4e6', 19 | 200: '#fecdd3', 20 | 300: '#fda4af', 21 | 400: '#fb7185', 22 | 500: '#f43f5e', 23 | 600: '#e11d48', 24 | 700: '#be123c', 25 | 800: '#9f1239', 26 | 900: '#881337', 27 | } 28 | 29 | export const pink: ThemeColor = { 30 | 50: '#fdf2f8', 31 | 100: '#fce7f3', 32 | 200: '#fbcfe8', 33 | 300: '#f9a8d4', 34 | 400: '#f472b6', 35 | 500: '#ec4899', 36 | 600: '#db2777', 37 | 700: '#be185d', 38 | 800: '#9d174d', 39 | 900: '#831843', 40 | } 41 | 42 | export const fuchsia: ThemeColor = { 43 | 50: '#fdf4ff', 44 | 100: '#fae8ff', 45 | 200: '#f5d0fe', 46 | 300: '#f0abfc', 47 | 400: '#e879f9', 48 | 500: '#d946ef', 49 | 600: '#c026d3', 50 | 700: '#a21caf', 51 | 800: '#86198f', 52 | 900: '#701a75', 53 | } 54 | 55 | export const purple: ThemeColor = { 56 | 50: '#faf5ff', 57 | 100: '#f3e8ff', 58 | 200: '#e9d5ff', 59 | 300: '#d8b4fe', 60 | 400: '#c084fc', 61 | 500: '#a855f7', 62 | 600: '#9333ea', 63 | 700: '#7e22ce', 64 | 800: '#6b21a8', 65 | 900: '#581c87', 66 | } 67 | 68 | export const violet: ThemeColor = { 69 | 50: '#f5f3ff', 70 | 100: '#ede9fe', 71 | 200: '#ddd6fe', 72 | 300: '#c4b5fd', 73 | 400: '#a78bfa', 74 | 500: '#8b5cf6', 75 | 600: '#7c3aed', 76 | 700: '#6d28d9', 77 | 800: '#5b21b6', 78 | 900: '#4c1d95', 79 | } 80 | 81 | export const indigo: ThemeColor = { 82 | 50: '#eef2ff', 83 | 100: '#e0e7ff', 84 | 200: '#c7d2fe', 85 | 300: '#a5b4fc', 86 | 400: '#818cf8', 87 | 500: '#6366f1', 88 | 600: '#4f46e5', 89 | 700: '#4338ca', 90 | 800: '#3730a3', 91 | 900: '#312e81', 92 | } 93 | 94 | export const blue: ThemeColor = { 95 | 50: '#eff6ff', 96 | 100: '#dbeafe', 97 | 200: '#bfdbfe', 98 | 300: '#93c5fd', 99 | 400: '#60a5fa', 100 | 500: '#3b82f6', 101 | 600: '#2563eb', 102 | 700: '#1d4ed8', 103 | 800: '#1e40af', 104 | 900: '#1e3a8a', 105 | } 106 | 107 | export const lightBlue: ThemeColor = { 108 | 50: '#f0f9ff', 109 | 100: '#e0f2fe', 110 | 200: '#bae6fd', 111 | 300: '#7dd3fc', 112 | 400: '#38bdf8', 113 | 500: '#0ea5e9', 114 | 600: '#0284c7', 115 | 700: '#0369a1', 116 | 800: '#075985', 117 | 900: '#0c4a6e', 118 | } 119 | 120 | export const cyan: ThemeColor = { 121 | 50: '#ecfeff', 122 | 100: '#cffafe', 123 | 200: '#a5f3fc', 124 | 300: '#67e8f9', 125 | 400: '#22d3ee', 126 | 500: '#06b6d4', 127 | 600: '#0891b2', 128 | 700: '#0e7490', 129 | 800: '#155e75', 130 | 900: '#164e63', 131 | } 132 | 133 | export const teal: ThemeColor = { 134 | 50: '#f0fdfa', 135 | 100: '#ccfbf1', 136 | 200: '#99f6e4', 137 | 300: '#5eead4', 138 | 400: '#2dd4bf', 139 | 500: '#14b8a6', 140 | 600: '#0d9488', 141 | 700: '#0f766e', 142 | 800: '#115e59', 143 | 900: '#134e4a', 144 | } 145 | 146 | export const emerald: ThemeColor = { 147 | 50: '#ecfdf5', 148 | 100: '#d1fae5', 149 | 200: '#a7f3d0', 150 | 300: '#6ee7b7', 151 | 400: '#34d399', 152 | 500: '#10b981', 153 | 600: '#059669', 154 | 700: '#047857', 155 | 800: '#065f46', 156 | 900: '#064e3b', 157 | } 158 | 159 | export const green: ThemeColor = { 160 | 50: '#f0fdf4', 161 | 100: '#dcfce7', 162 | 200: '#bbf7d0', 163 | 300: '#86efac', 164 | 400: '#4ade80', 165 | 500: '#22c55e', 166 | 600: '#16a34a', 167 | 700: '#15803d', 168 | 800: '#166534', 169 | 900: '#14532d', 170 | } 171 | 172 | export const lime: ThemeColor = { 173 | 50: '#f7fee7', 174 | 100: '#ecfccb', 175 | 200: '#d9f99d', 176 | 300: '#bef264', 177 | 400: '#a3e635', 178 | 500: '#84cc16', 179 | 600: '#65a30d', 180 | 700: '#4d7c0f', 181 | 800: '#3f6212', 182 | 900: '#365314', 183 | } 184 | 185 | export const yellow: ThemeColor = { 186 | 50: '#fefce8', 187 | 100: '#fef9c3', 188 | 200: '#fef08a', 189 | 300: '#fde047', 190 | 400: '#facc15', 191 | 500: '#eab308', 192 | 600: '#ca8a04', 193 | 700: '#a16207', 194 | 800: '#854d0e', 195 | 900: '#713f12', 196 | } 197 | 198 | export const amber: ThemeColor = { 199 | 50: '#fffbeb', 200 | 100: '#fef3c7', 201 | 200: '#fde68a', 202 | 300: '#fcd34d', 203 | 400: '#fbbf24', 204 | 500: '#f59e0b', 205 | 600: '#d97706', 206 | 700: '#b45309', 207 | 800: '#92400e', 208 | 900: '#78350f', 209 | } 210 | 211 | export const orange: ThemeColor = { 212 | 50: '#fff7ed', 213 | 100: '#ffedd5', 214 | 200: '#fed7aa', 215 | 300: '#fdba74', 216 | 400: '#fb923c', 217 | 500: '#f97316', 218 | 600: '#ea580c', 219 | 700: '#c2410c', 220 | 800: '#9a3412', 221 | 900: '#7c2d12', 222 | } 223 | 224 | export const red: ThemeColor = { 225 | 50: '#fef2f2', 226 | 100: '#fee2e2', 227 | 200: '#fecaca', 228 | 300: '#fca5a5', 229 | 400: '#f87171', 230 | 500: '#ef4444', 231 | 600: '#dc2626', 232 | 700: '#b91c1c', 233 | 800: '#991b1b', 234 | 900: '#7f1d1d', 235 | } 236 | 237 | export const warmGray: ThemeColor = { 238 | 50: '#fafaf9', 239 | 100: '#f5f5f4', 240 | 200: '#e7e5e4', 241 | 300: '#d6d3d1', 242 | 400: '#a8a29e', 243 | 500: '#78716c', 244 | 600: '#57534e', 245 | 700: '#44403c', 246 | 800: '#292524', 247 | 900: '#1c1917', 248 | } 249 | 250 | export const trueGray: ThemeColor = { 251 | 50: '#fafafa', 252 | 100: '#f5f5f5', 253 | 200: '#e5e5e5', 254 | 300: '#d4d4d4', 255 | 400: '#a3a3a3', 256 | 500: '#737373', 257 | 600: '#525252', 258 | 700: '#404040', 259 | 800: '#262626', 260 | 900: '#171717', 261 | } 262 | 263 | export const gray: ThemeColor = { 264 | 50: '#fafafa', 265 | 100: '#f4f4f5', 266 | 200: '#e4e4e7', 267 | 300: '#d4d4d8', 268 | 400: '#a1a1aa', 269 | 500: '#71717a', 270 | 600: '#52525b', 271 | 700: '#3f3f46', 272 | 800: '#27272a', 273 | 900: '#18181b', 274 | } 275 | 276 | export const coolGray: ThemeColor = { 277 | 50: '#f9fafb', 278 | 100: '#f3f4f6', 279 | 200: '#e5e7eb', 280 | 300: '#d1d5db', 281 | 400: '#9ca3af', 282 | 500: '#6b7280', 283 | 600: '#4b5563', 284 | 700: '#374151', 285 | 800: '#1f2937', 286 | 900: '#111827', 287 | } 288 | 289 | export const blueGray: ThemeColor = { 290 | 50: '#f8fafc', 291 | 100: '#f1f5f9', 292 | 200: '#e2e8f0', 293 | 300: '#cbd5e1', 294 | 400: '#94a3b8', 295 | 500: '#64748b', 296 | 600: '#475569', 297 | 700: '#334155', 298 | 800: '#1e293b', 299 | 900: '#0f172a', 300 | } 301 | -------------------------------------------------------------------------------- /src/shim/README.md: -------------------------------------------------------------------------------- 1 | [![Documentation](https://flat.badgen.net/badge/icon/Documentation?icon=awesome&label)](https://twind.dev/docs/modules/twind_shim.html) 2 | [![Github](https://flat.badgen.net/badge/icon/tw-in-js%2Ftwind%2Fsrc%2Fshim?icon=github&label)](https://github.com/tw-in-js/twind/tree/main/src/shim) 3 | [![Module Size](https://flat.badgen.net/badgesize/brotli/https:/unpkg.com/twind/shim/shim.js?icon=jsdelivr&label&color=blue&cache=10800)](https://unpkg.com/twind/shim/shim.js 'brotli module size') 4 | [![Typescript](https://flat.badgen.net/badge/icon/included?icon=typescript&label)](https://unpkg.com/browse/twind/shim/shim.d.ts) 5 | 6 | > Allows to copy-paste tailwind examples. This feature can be used together with your favorite framework without any additional setup. 7 | 8 | The `twind/shim` module allows for the use of the `class` attribute for tailwind rules. If such a rule is detected, the corresponding CSS rule is created and injected into the stylesheet dynamically. `twind/shim` is intended for client-side usage and, without configuration, utilizes the default/global {@link twind.tw | tw instance}. For server-side usage, {@link twind/shim/server} exports a dedicated {@link twind/shim/server.shim | shim function} that will parse and update a static HTML string while collecting the style rules into a sheet for further usage in your respective framework. 9 | 10 | There is _no need for `tw`_ but it can be used on the same elements as well. All Twind syntax features like {@page Thinking in Groups | grouping} are supported within class attributes. See [example/shim.html](https://github.com/tw-in-js/twind/blob/main/example/shim.html) for a full example. 11 | 12 | 13 | 14 | 15 | - [Usage](#usage) 16 | - [Customize `tw` instance](#customize-tw-instance) 17 | - [Prevent FOUC (_flash of unstyled content_)](#prevent-fouc-_flash-of-unstyled-content_) 18 | - [FAQ](#faq) 19 | 20 | 21 | 22 | ## Usage 23 | 24 | For runtime processing of your javascript-assisted HTML documents, simply include the `twind/shim` module and watch the magic happen. 25 | 26 | ```html 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 |

This is Twind!

35 |
36 | 37 | 38 | ``` 39 | 40 | > 🚀 [live and interactive shim demo](https://esm.codes/#aW1wb3J0ICdodHRwczovL2Nkbi5za3lwYWNrLmRldi90d2luZC9zaGltJwoKZG9jdW1lbnQuYm9keS5pbm5lckhUTUwgPSBgCiAgPG1haW4gY2xhc3M9Imgtc2NyZWVuIGJnLXB1cnBsZS00MDAgZmxleCBpdGVtcy1jZW50ZXIganVzdGlmeS1jZW50ZXIiPgogICAgPGgxIGNsYXNzPSJmb250LWJvbGQgdGV4dChjZW50ZXIgNXhsIHdoaXRlIHNtOmdyYXktODAwIG1kOnBpbmstNzAwKSI+CiAgICAgIFRoaXMgaXMgVHdpbmQhCiAgICA8L2gxPgogIDwvbWFpbj4KYA==) 41 | 42 | The `twind/shim` module utilizes the {@link twind/observe} module internally, but it provides its own {@link setup} function for customizing the used {@link tw} instance and setting the target node to be shimmed. It also provides a {@link disconnect} function to stop shimming/observing all nodes. 43 | 44 | ```js 45 | import { setup, disconnect } from 'twind/shim' 46 | 47 | setup({ 48 | // node element to shim/observe (default: document.documentElement) 49 | target: document.querySelector('#__twind'), 50 | 51 | // All other setup options are supported 52 | }) 53 | 54 | // stop shimming/observing all nodes 55 | disconnect() 56 | ``` 57 | 58 | ## Customize `tw` instance 59 | 60 | You can provide a `` within the document. The content must be valid JSON and all {@link twind.setup | twind setup options} (including {@link twind.Configuration.hash | hash}) are supported. 61 | 62 | ```html 63 | 64 | 65 | 66 | 67 | 72 | 73 | 74 |
75 |

76 | This is Twind! 77 |

78 |
79 | 80 | 81 | ``` 82 | 83 | Alternatively the following works: 84 | 85 | ```js 86 | import { setup } from "https://cdn.skypack.dev/twind/shim" 87 | 88 | setup({ 89 | target: document.body, // Default document.documentElement (eg html) 90 | ... // All other twind setup options are supported 91 | }) 92 | ``` 93 | 94 | It is possible to mix `twind/shim` with {@link tw}: 95 | 96 | ```js 97 | import 'twind/shim' 98 | import { tw } from 'twind' 99 | 100 | const styles = { 101 | center: tw`flex items-center justify-center`, 102 | } 103 | 104 | document.body.innerHTML = ` 105 |
106 |

107 | This is Twind! 108 |

109 |
110 | ` 111 | ``` 112 | 113 | ## Prevent FOUC (_flash of unstyled content_) 114 | 115 | To prevent FOUC (_flash of unstyled content_) it is advised to set the `hidden` attribute on the target element. `twind/shim` will remove it once all styles have been generated. 116 | 117 | ```html 118 | 119 | 120 | 121 | 122 | ``` 123 | 124 | ## FAQ 125 | 126 | > You can click on each question to reveal the answer. 127 | 128 |
How can I use twind/shim from javascript? 129 | 130 | > Internally `twind/shim` uses {@link twind/observe} which may be useful on its own for advanced use cases. 131 | 132 | ```js 133 | import 'twind/shim' 134 | ``` 135 | 136 | ```js 137 | import { setup, disconnect } from 'twind/shim' 138 | ``` 139 | 140 |
141 | 142 |
How to support legacy browser with the UMD bundles? 143 | 144 | > You may need to provide certain [polyfills](./browser-support.md) depending on your target browser. 145 | 146 | ```html 147 | 148 | 149 | 150 | ``` 151 | 152 |
153 | 154 |
How does the shim work? 155 | 156 | `twind/shim` starts {@link twind/observe.observer | observing} class attributes changes right after the [DOM content has been loaded](https://developer.mozilla.org/en-US/docs/Web/API/Document/DOMContentLoaded_event). For further details see {@link twind/observe}. 157 | 158 |
159 | -------------------------------------------------------------------------------- /src/observe/README.md: -------------------------------------------------------------------------------- 1 | [![Documentation](https://flat.badgen.net/badge/icon/Documentation?icon=awesome&label)](https://twind.dev/docs/modules/twind_observe.html) 2 | [![Github](https://flat.badgen.net/badge/icon/tw-in-js%2Ftwind%2Fsrc%2Fobserve?icon=github&label)](https://github.com/tw-in-js/twind/tree/main/src/observe) 3 | [![Module Size](https://flat.badgen.net/badgesize/brotli/https:/unpkg.com/twind/observe/observe.js?icon=jsdelivr&label&color=blue&cache=10800)](https://unpkg.com/twind/observe/observe.js 'brotli module size') 4 | [![Typescript](https://flat.badgen.net/badge/icon/included?icon=typescript&label)](https://unpkg.com/browse/twind/observe/observe.d.ts) 5 | 6 | > Allows to copy-paste tailwind examples. This feature can be used together with your favorite framework without any additional setup. 7 | 8 | The `twind/observe` modules allows to use the `class` attribute for tailwind rules. If such a rule is detected the corresponding CSS rule is created and injected into the stylesheet. _No need for `tw`_ but it can be used on the same elements as well. 9 | 10 | > This is meant for advanced use cases. Most of the time you may want to use {@link twind/shim}. 11 | 12 | 13 | 14 | 15 | - [Usage](#usage) 16 | - [Customization](#customization) 17 | - [API](#api) 18 | - [Example](#example) 19 | - [Implementation Details](#implementation-details) 20 | 21 | 22 | 23 | ## Usage 24 | 25 | ```js 26 | import { observe } from 'twind/observe' 27 | 28 | observe(document.body) 29 | 30 | document.body.innerHTML = ` 31 |
32 |

33 | This is Twind! 34 |

35 |
36 | ` 37 | ``` 38 | 39 | > 🚀 [live and interactive shim demo](https://esm.codes/#aW1wb3J0IHsgb2JzZXJ2ZSB9IGZyb20gJ2h0dHBzOi8vY2RuLnNreXBhY2suZGV2L3R3aW5kL29ic2VydmUnCgpvYnNlcnZlKGRvY3VtZW50LmJvZHkpCgpkb2N1bWVudC5ib2R5LmlubmVySFRNTCA9IGAKICA8bWFpbiBjbGFzcz0iaC1zY3JlZW4gYmctcHVycGxlLTQwMCBmbGV4IGl0ZW1zLWNlbnRlciBqdXN0aWZ5LWNlbnRlciI+CiAgICA8aDEgY2xhc3M9ImZvbnQtYm9sZCB0ZXh0KGNlbnRlciA1eGwgd2hpdGUgc206Z3JheS04MDAgbWQ6cGluay03MDApIj4KICAgICAgVGhpcyBpcyBUd2luZCEKICAgIDwvaDE+CiAgPC9tYWluPgpg) 40 | 41 | All Twind syntax features like [grouping](https://github.com/tw-in-js/twind/blob/main/docs/grouping.md) are supported within class attributes 42 | 43 | > If you want to simplify the instantiation and automatically observe take look at {@link twind/shim}. 44 | 45 | ## Customization 46 | 47 | `twind/observe` uses the {@link twind.tw default/global `tw`} instance if not configured otherwise. You can provide a custom instance in several ways: 48 | 49 | ```js 50 | import { create } from 'twind' 51 | import { observe, createObserver } from 'twind/observe' 52 | 53 | // Create a custom instance 54 | const instance = create(/* ... */) 55 | 56 | // 1. As second parameter 57 | observe(document.body, instance) 58 | 59 | // 2. As this context 60 | observe.call(instance, document.body) 61 | observe.bind(instance)(document.body) 62 | 63 | // 3. Use the factory 64 | createObserver(instance).observe(document.body) 65 | ``` 66 | 67 | ## API 68 | 69 | ```js 70 | import { createObserver, observe } from 'twind/observe' 71 | 72 | const observer = createObserver(/* custom instance */) 73 | 74 | // Or to start observing an element right away 75 | // const observer = observe(node, /* custom instance */) 76 | 77 | // Start observing a node; can be called several times with different nodes 78 | observer.observe(node) 79 | 80 | // Stop observing all nodes 81 | observer.disconnect() 82 | ``` 83 | 84 | ## Example 85 | 86 | This example shows how a custom observer instance can be used to shim a web component. 87 | 88 | > ❗ This example is using [Constructable Stylesheet Objects](https://wicg.github.io/construct-stylesheets/) and `DocumentOrShadowRoot.adoptedStyleSheets` which have [limited browser support](https://caniuse.com/mdn-api_documentorshadowroot_adoptedstylesheets) at the moment (December 2020). 89 | 90 | ```js 91 | import { LitElement, html } from 'lit-element' 92 | import { create, cssomSheet } from 'twind' 93 | import { createObserver } from 'twind/observe' 94 | 95 | // 1. Create separate CSSStyleSheet 96 | const sheet = cssomSheet({ target: new CSSStyleSheet() }) 97 | 98 | // 2. Use that to create an own twind instance 99 | const instance = create({ sheet }) 100 | 101 | class TwindElement extends LitElement { 102 | // 3. Apply the same style to each instance of this component 103 | static styles = [sheet.target] 104 | 105 | // 4. Start observing class attributes changes 106 | connectedCallback() { 107 | super.connectedCallback() 108 | this._observer = createObserver(instance).observe(this.renderRoot) 109 | } 110 | 111 | // 5. Stop observing class attributes changes 112 | disconnectedCallback() { 113 | super.disconnectedCallback() 114 | this._observer.disconnect() 115 | } 116 | 117 | render() { 118 | // 5. Use tailwind rules in class attributes 119 | return html` 120 |
121 |

This is Twind!

122 |
123 | ` 124 | } 125 | } 126 | 127 | customElements.define('twind-element', TwindElement) 128 | 129 | document.body.innerHTML = '' 130 | ``` 131 | 132 | > 🚀 [live and interactive demo](https://esm.codes/#aW1wb3J0IHsgTGl0RWxlbWVudCwgaHRtbCB9IGZyb20gJ2h0dHBzOi8vY2RuLnNreXBhY2suZGV2L2xpdC1lbGVtZW50JwppbXBvcnQgeyBjcmVhdGUsIGNzc29tU2hlZXQgfSBmcm9tICdodHRwczovL2Nkbi5za3lwYWNrLmRldi90d2luZCcKaW1wb3J0IHsgY3JlYXRlT2JzZXJ2ZXIgfSBmcm9tICdodHRwczovL2Nkbi5za3lwYWNrLmRldi90d2luZC9vYnNlcnZlJwoKY29uc3Qgc2hlZXQgPSBjc3NvbVNoZWV0KHsgdGFyZ2V0OiBuZXcgQ1NTU3R5bGVTaGVldCgpIH0pCgpjb25zdCB7IHR3IH0gPSBjcmVhdGUoeyBzaGVldCB9KQoKY2xhc3MgVHdpbmRFbGVtZW50IGV4dGVuZHMgTGl0RWxlbWVudCB7CiAgY3JlYXRlUmVuZGVyUm9vdCgpIHsKICAgIGNvbnN0IHNoYWRvdyA9IHN1cGVyLmNyZWF0ZVJlbmRlclJvb3QoKQogICAgc2hhZG93LmFkb3B0ZWRTdHlsZVNoZWV0cyA9IFtzaGVldC50YXJnZXRdCiAgICByZXR1cm4gc2hhZG93CiAgfQoKICBjb25uZWN0ZWRDYWxsYmFjaygpIHsKICAgIHRoaXMuX29ic2VydmVyID0gY3JlYXRlT2JzZXJ2ZXIoaW5zdGFuY2UpLm9ic2VydmUodGhpcy5yZW5kZXJSb290KQogIH0KCiAgZGlzY29ubmVjdGVkQ2FsbGJhY2soKSB7CiAgICB0aGlzLl9vYnNlcnZlci5kaXNjb25uZWN0KCkKICB9CgogIHJlbmRlcigpIHsKICAgIHJldHVybiBodG1sYAogICAgICA8bWFpbiBjbGFzcz0iaC1zY3JlZW4gYmctcHVycGxlLTQwMCBmbGV4IGl0ZW1zLWNlbnRlciBqdXN0aWZ5LWNlbnRlciI+CiAgICAgICAgPGgxIGNsYXNzPSJmb250LWJvbGQgdGV4dChjZW50ZXIgNXhsIHdoaXRlIHNtOmdyYXktODAwIG1kOnBpbmstNzAwKSI+CiAgICAgICAgICBUaGlzIGlzIFR3aW5kIQogICAgICAgIDwvaDE+CiAgICAgIDwvbWFpbj4KICAgIGAKICB9Cn0KCmN1c3RvbUVsZW1lbnRzLmRlZmluZSgndHdpbmQtZWxlbWVudCcsIFR3aW5kRWxlbWVudCk7Cgpkb2N1bWVudC5ib2R5LmlubmVySFRNTCA9ICc8dHdpbmQtZWxlbWVudD48L3R3aW5kLWVsZW1lbnQ+Jwo=) 133 | 134 | ## Implementation Details 135 | 136 | This uses a [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) to detect changed class attributes or added DOM nodes. On detection the class attribute is parsed and translated by twind to inject the required classes into the stylesheet and the class attribute is updated to reflect the added CSS class names that may have been hashed. 137 | --------------------------------------------------------------------------------