├── src
├── index.ts
├── utilities
│ ├── __tests__
│ │ ├── fixtures
│ │ │ ├── 1-no-config
│ │ │ │ └── .gitkeep
│ │ │ ├── 5-invalid-custom-config
│ │ │ │ └── .gitkeep
│ │ │ ├── 4-custom-config
│ │ │ │ └── foo.json
│ │ │ ├── 3-generate-css-config
│ │ │ │ └── generate-css.config.json
│ │ │ └── 2-package-json-config
│ │ │ │ └── package.json
│ │ └── read-config-async.ts
│ ├── create-css
│ │ ├── declarations
│ │ │ ├── outline.ts
│ │ │ ├── cursor.ts
│ │ │ ├── flex-wrap.ts
│ │ │ ├── flex.ts
│ │ │ ├── position.ts
│ │ │ ├── text-decoration.ts
│ │ │ ├── select.ts
│ │ │ ├── text-align.ts
│ │ │ ├── display.ts
│ │ │ ├── text-transform.ts
│ │ │ ├── align-items.ts
│ │ │ ├── justify-content.ts
│ │ │ └── index.ts
│ │ ├── map-selector-to-declaration.ts
│ │ ├── compute-color-value-factory.ts
│ │ ├── create-css.ts
│ │ ├── plugins
│ │ │ ├── color.ts
│ │ │ ├── index.ts
│ │ │ ├── line-height.ts
│ │ │ ├── border-color.ts
│ │ │ ├── letter-spacing.ts
│ │ │ ├── background-color.ts
│ │ │ ├── position.ts
│ │ │ ├── text-style.ts
│ │ │ ├── font.ts
│ │ │ ├── padding.ts
│ │ │ ├── border-width.ts
│ │ │ ├── width-and-height.ts
│ │ │ ├── margin.ts
│ │ │ └── border-radius.ts
│ │ ├── __tests__
│ │ │ ├── create-css-declaration-block
│ │ │ │ ├── declarations
│ │ │ │ │ ├── position
│ │ │ │ │ │ ├── top
│ │ │ │ │ │ │ ├── negative-top-position.ts
│ │ │ │ │ │ │ └── positive-top-position.ts
│ │ │ │ │ │ ├── left
│ │ │ │ │ │ │ ├── negative-left-position.ts
│ │ │ │ │ │ │ └── positive-left-position.ts
│ │ │ │ │ │ ├── right
│ │ │ │ │ │ │ ├── negative-right-position.ts
│ │ │ │ │ │ │ └── positive-right-position.ts
│ │ │ │ │ │ └── bottom
│ │ │ │ │ │ │ ├── negative-bottom-position.ts
│ │ │ │ │ │ │ └── positive-bottom-position.ts
│ │ │ │ │ ├── font-weight.ts
│ │ │ │ │ ├── font-size.ts
│ │ │ │ │ ├── text-style.ts
│ │ │ │ │ ├── color.ts
│ │ │ │ │ ├── font-family.ts
│ │ │ │ │ ├── line-height.ts
│ │ │ │ │ ├── letter-spacing.ts
│ │ │ │ │ ├── width-and-height
│ │ │ │ │ │ ├── width
│ │ │ │ │ │ │ ├── width.ts
│ │ │ │ │ │ │ ├── max-width.ts
│ │ │ │ │ │ │ └── min-width.ts
│ │ │ │ │ │ └── height
│ │ │ │ │ │ │ ├── height.ts
│ │ │ │ │ │ │ ├── max-height.ts
│ │ │ │ │ │ │ └── min-height.ts
│ │ │ │ │ ├── background-color.ts
│ │ │ │ │ ├── border-color.ts
│ │ │ │ │ ├── margin
│ │ │ │ │ │ ├── positive-margin.ts
│ │ │ │ │ │ └── negative-margin.ts
│ │ │ │ │ ├── padding.ts
│ │ │ │ │ ├── border-width.ts
│ │ │ │ │ └── border-radius.ts
│ │ │ │ └── create-css-declaration-block.ts
│ │ │ ├── parse-class-name.ts
│ │ │ └── compute-numeric-value-factory.ts
│ │ ├── parse-class-name.ts
│ │ ├── create-css-declaration-block.ts
│ │ ├── group-css-declaration-blocks-by-breakpoint.ts
│ │ └── compute-numeric-value-factory.ts
│ ├── extract-class-names-async
│ │ ├── extract-class-names-from-js.ts
│ │ ├── extract-class-names-from-html.ts
│ │ ├── extract-class-names-from-string-factory.ts
│ │ ├── extract-class-names-async.ts
│ │ └── __tests__
│ │ │ ├── extract-class-names-from-html.ts
│ │ │ └── extract-class-names-from-js.ts
│ ├── log.ts
│ ├── create-base-font-size-css.ts
│ ├── read-config-async.ts
│ └── stringify-css.ts
├── build.ts
├── watch.ts
├── css
│ └── reset.css
├── cli.ts
├── types.ts
└── generate-css.ts
├── .gitignore
├── example
├── example.html
└── generate-css.config.json
├── .github
└── workflows
│ └── build.yml
├── tsconfig.json
├── scripts
├── print-theme-keys.ts
└── create-css-docs.ts
├── LICENSE.md
├── package.json
├── README.md
└── docs
└── css.md
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './generate-css'
2 |
--------------------------------------------------------------------------------
/src/utilities/__tests__/fixtures/1-no-config/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/utilities/__tests__/fixtures/5-invalid-custom-config/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/utilities/create-css/declarations/outline.ts:
--------------------------------------------------------------------------------
1 | export const outline = {
2 | 'outline-none': {
3 | outline: 'none'
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *.log
3 | *.tsbuildinfo
4 | .nyc_output/
5 | coverage/
6 | example/style.css
7 | lib/
8 | node_modules/
9 | sandbox/
10 |
--------------------------------------------------------------------------------
/example/example.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/utilities/__tests__/fixtures/4-custom-config/foo.json:
--------------------------------------------------------------------------------
1 | {
2 | "reset": false,
3 | "theme": {
4 | "color": {
5 | "default": "#ffffff"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/utilities/__tests__/fixtures/3-generate-css-config/generate-css.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "reset": false,
3 | "theme": {
4 | "color": {
5 | "default": "#ffffff"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/utilities/create-css/declarations/cursor.ts:
--------------------------------------------------------------------------------
1 | export const cursor = {
2 | 'cursor-default': {
3 | cursor: 'default'
4 | },
5 | 'cursor-pointer': {
6 | cursor: 'pointer'
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/utilities/__tests__/fixtures/2-package-json-config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "generate-css": {
3 | "reset": false,
4 | "theme": {
5 | "color": {
6 | "default": "#ffffff"
7 | }
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/utilities/create-css/declarations/flex-wrap.ts:
--------------------------------------------------------------------------------
1 | export const flexWrap = {
2 | 'flex-nowrap': {
3 | 'flex-wrap': 'nowrap'
4 | },
5 | 'flex-wrap': {
6 | 'flex-wrap': 'wrap'
7 | },
8 | 'flex-wrap-reverse': {
9 | 'flex-wrap': 'wrap-reverse'
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/utilities/create-css/declarations/flex.ts:
--------------------------------------------------------------------------------
1 | export const flex = {
2 | 'flex-1': {
3 | flex: '1 1 0%'
4 | },
5 | 'flex-auto': {
6 | flex: '1 1 auto'
7 | },
8 | 'flex-initial': {
9 | flex: '0 1 auto'
10 | },
11 | 'flex-none': {
12 | flex: 'none'
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/utilities/create-css/declarations/position.ts:
--------------------------------------------------------------------------------
1 | export const position = {
2 | absolute: {
3 | position: 'absolute'
4 | },
5 | fixed: {
6 | position: 'fixed'
7 | },
8 | relative: {
9 | position: 'relative'
10 | },
11 | static: {
12 | position: 'static'
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/utilities/create-css/declarations/text-decoration.ts:
--------------------------------------------------------------------------------
1 | export const textDecoration = {
2 | 'line-through': {
3 | 'text-decoration': 'line-through'
4 | },
5 | 'no-underline': {
6 | 'text-decoration': 'none'
7 | },
8 | 'underline': {
9 | 'text-decoration': 'underline'
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/utilities/create-css/declarations/select.ts:
--------------------------------------------------------------------------------
1 | export const select = {
2 | 'select-all': {
3 | 'user-select': 'all'
4 | },
5 | 'select-auto': {
6 | 'user-select': 'auto'
7 | },
8 | 'select-none': {
9 | 'user-select': 'none'
10 | },
11 | 'select-text': {
12 | 'user-select': 'text'
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/utilities/create-css/declarations/text-align.ts:
--------------------------------------------------------------------------------
1 | export const textAlign = {
2 | 'text-center': {
3 | 'text-align': 'center'
4 | },
5 | 'text-justify': {
6 | 'text-align': 'justify'
7 | },
8 | 'text-left': {
9 | 'text-align': 'left'
10 | },
11 | 'text-right': {
12 | 'text-align': 'right'
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/utilities/create-css/declarations/display.ts:
--------------------------------------------------------------------------------
1 | export const display = {
2 | 'block': {
3 | display: 'block'
4 | },
5 | 'flex': {
6 | display: 'flex'
7 | },
8 | 'hidden': {
9 | display: 'none'
10 | },
11 | 'inline': {
12 | display: 'inline'
13 | },
14 | 'inline-flex': {
15 | display: 'inline-flex'
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/utilities/extract-class-names-async/extract-class-names-from-js.ts:
--------------------------------------------------------------------------------
1 | import { extractClassNamesFromStringFactory } from './extract-class-names-from-string-factory'
2 |
3 | const regex = /(?:const|var) \w*class_?(?:names?)? ?= ?(['"])((?:(?!\1).)+)\1/gi
4 |
5 | export const extractClassNamesFromJs = extractClassNamesFromStringFactory(
6 | regex,
7 | 2
8 | )
9 |
--------------------------------------------------------------------------------
/src/utilities/create-css/declarations/text-transform.ts:
--------------------------------------------------------------------------------
1 | export const textTransform = {
2 | 'caps': {
3 | 'text-transform': 'capitalize'
4 | },
5 | 'lowercase': {
6 | 'text-transform': 'lowercase'
7 | },
8 | 'normal-case': {
9 | 'text-transform': 'none'
10 | },
11 | 'uppercase': {
12 | 'text-transform': 'uppercase'
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/utilities/extract-class-names-async/extract-class-names-from-html.ts:
--------------------------------------------------------------------------------
1 | import { extractClassNamesFromStringFactory } from './extract-class-names-from-string-factory'
2 |
3 | const regex = /class=(['"])((?:(?!\1).)+)\1/gi // https://stackoverflow.com/a/8057827
4 |
5 | export const extractClassNamesFromHtml = extractClassNamesFromStringFactory(
6 | regex,
7 | 2
8 | )
9 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build
2 |
3 | on: push
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v2
10 | - uses: actions/setup-node@v1
11 | with:
12 | node-version: 12
13 | - run: yarn install --frozen-lockfile
14 | - run: yarn run lint
15 | - run: yarn run fix
16 | - run: yarn run test
17 |
--------------------------------------------------------------------------------
/src/utilities/create-css/map-selector-to-declaration.ts:
--------------------------------------------------------------------------------
1 | import { Declarations } from '../../types'
2 | import { declarations } from './declarations'
3 |
4 | export function mapSelectorToDeclaration(
5 | selector: string
6 | ): null | Declarations {
7 | const declaration = declarations[selector]
8 | if (typeof declaration === 'undefined') {
9 | return null
10 | }
11 | return declaration
12 | }
13 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "declaration": true,
4 | "declarationMap": true,
5 | "module": "commonjs",
6 | "moduleResolution": "node",
7 | "outDir": "./lib",
8 | "removeComments": true,
9 | "rootDir": "./src",
10 | "strict": true,
11 | "target": "es2020",
12 | "typeRoots": ["./node_modules/@types"]
13 | },
14 | "include": ["./src/**/*.ts"]
15 | }
16 |
--------------------------------------------------------------------------------
/src/utilities/create-css/declarations/align-items.ts:
--------------------------------------------------------------------------------
1 | export const alignItems = {
2 | 'items-baseline': {
3 | 'align-items': 'baseline'
4 | },
5 | 'items-center': {
6 | 'align-items': 'center'
7 | },
8 | 'items-end': {
9 | 'align-items': 'flex-end'
10 | },
11 | 'items-start': {
12 | 'align-items': 'flex-start'
13 | },
14 | 'items-stretch': {
15 | 'align-items': 'stretch'
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/example/generate-css.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "reset": true,
3 | "theme": {
4 | "baseFontSize": {
5 | "default": "16px"
6 | },
7 | "baseSpace": "1rem",
8 | "color": {
9 | "black": "#000",
10 | "blue": "#00f",
11 | "white": "#fff"
12 | },
13 | "fontFamily": {
14 | "default": "Helvetica, Arial, sans-serif"
15 | },
16 | "fontWeight": {
17 | "bold": "bolder"
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/utilities/log.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | import * as kleur from 'kleur'
4 |
5 | function error(message: string): void {
6 | console.log(`${kleur.red().bold('error')} ${message}`)
7 | }
8 |
9 | function info(message: string): void {
10 | console.log(`${kleur.blue().bold('info')} ${message}`)
11 | }
12 |
13 | function success(message: string): void {
14 | console.log(`${kleur.green().bold('success')} ${message}`)
15 | }
16 |
17 | export const log = {
18 | error,
19 | info,
20 | success
21 | }
22 |
--------------------------------------------------------------------------------
/src/utilities/create-css/declarations/justify-content.ts:
--------------------------------------------------------------------------------
1 | export const justifyContent = {
2 | 'justify-around': {
3 | 'justify-content': 'space-around'
4 | },
5 | 'justify-between': {
6 | 'justify-content': 'space-between'
7 | },
8 | 'justify-center': {
9 | 'justify-content': 'center'
10 | },
11 | 'justify-end': {
12 | 'justify-content': 'flex-end'
13 | },
14 | 'justify-evenly': {
15 | 'justify-content': 'space-evenly'
16 | },
17 | 'justify-start': {
18 | 'justify-content': 'flex-start'
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/build.ts:
--------------------------------------------------------------------------------
1 | import { generateCssAsync } from './generate-css'
2 | import { CliOptions } from './types'
3 | import { log } from './utilities/log'
4 | import { readConfigAsync } from './utilities/read-config-async'
5 |
6 | export async function build(cliOptions: CliOptions): Promise {
7 | try {
8 | const config = await readConfigAsync(cliOptions)
9 | if (config.outputPath !== null) {
10 | log.info('Generating CSS...')
11 | }
12 | await generateCssAsync(config)
13 | if (config.outputPath !== null) {
14 | log.success('Done')
15 | }
16 | } catch (error) {
17 | log.error(error.message)
18 | process.exit(1)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/utilities/extract-class-names-async/extract-class-names-from-string-factory.ts:
--------------------------------------------------------------------------------
1 | const consecutiveSpaceRegex = / +/
2 |
3 | export function extractClassNamesFromStringFactory(
4 | regex: RegExp,
5 | matchesIndex: number
6 | ): (string: string) => Array {
7 | return function (string: string): Array {
8 | const iterator = string.matchAll(regex)
9 | const result: { [key: string]: boolean } = {}
10 | for (const matches of iterator) {
11 | const classNames = matches[matchesIndex]
12 | .trim()
13 | .split(consecutiveSpaceRegex)
14 | for (const className of classNames) {
15 | result[className] = true
16 | }
17 | }
18 | return Object.keys(result).sort()
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/utilities/create-css/compute-color-value-factory.ts:
--------------------------------------------------------------------------------
1 | import { Theme, ThemeItem, ThemeKeys } from '../../types'
2 |
3 | export function computeColorValueFactory(
4 | theme: Theme
5 | ): (value: undefined | string, themeKeys: Array) => null | string {
6 | return function (
7 | value = 'default',
8 | themeKeys: Array
9 | ): null | string {
10 | for (const themeKey of themeKeys) {
11 | if (
12 | typeof themeKey !== 'undefined' &&
13 | typeof theme[themeKey] !== 'undefined'
14 | ) {
15 | const result = (theme[themeKey] as ThemeItem)[value]
16 | if (typeof result !== 'undefined') {
17 | return result
18 | }
19 | }
20 | }
21 | return null
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/utilities/create-css/create-css.ts:
--------------------------------------------------------------------------------
1 | import { CssDeclarationBlock, CssDeclarationBlocks, Theme } from '../../types'
2 | import { createCssDeclarationBlock } from './create-css-declaration-block'
3 | import { groupCssDeclarationBlocksByBreakpoint } from './group-css-declaration-blocks-by-breakpoint'
4 |
5 | export function createCss(
6 | classNames: Array,
7 | theme: Theme
8 | ): Array {
9 | const result: Array = []
10 | for (const className of classNames) {
11 | const cssDeclarationBlock = createCssDeclarationBlock(className, theme)
12 | if (cssDeclarationBlock === null) {
13 | continue
14 | }
15 | result.push(cssDeclarationBlock)
16 | }
17 | return groupCssDeclarationBlocksByBreakpoint(result, theme.breakpoint)
18 | }
19 |
--------------------------------------------------------------------------------
/src/utilities/create-css/plugins/color.ts:
--------------------------------------------------------------------------------
1 | /*
2 | color
3 |
4 | `.color` | `color: ${theme.color.default};`
5 | `.color-${key}` | `color: ${theme.color[key]};`
6 | */
7 |
8 | import { Plugin, ThemeKeys } from '../../../types'
9 |
10 | export const color: Plugin = {
11 | createDeclarations: function ({
12 | matches,
13 | computeColorValue
14 | }: {
15 | matches: RegExpMatchArray
16 | computeColorValue: (
17 | value: string,
18 | themeKeys: Array
19 | ) => null | string
20 | }): { [property: string]: string } {
21 | const color = computeColorValue(matches[1], ['color'])
22 | if (color === null) {
23 | throw new Error(`Invalid color: ${matches[1]}`)
24 | }
25 | return {
26 | color: `${color}`
27 | }
28 | },
29 | regex: /^color(?:-(.+))?$/
30 | }
31 |
--------------------------------------------------------------------------------
/src/utilities/create-css/plugins/index.ts:
--------------------------------------------------------------------------------
1 | import { backgroundColor } from './background-color'
2 | import { borderColor } from './border-color'
3 | import { borderRadius } from './border-radius'
4 | import { borderWidth } from './border-width'
5 | import { color } from './color'
6 | import { font } from './font'
7 | import { letterSpacing } from './letter-spacing'
8 | import { lineHeight } from './line-height'
9 | import { margin } from './margin'
10 | import { padding } from './padding'
11 | import { position } from './position'
12 | import { textStyle } from './text-style'
13 | import { widthAndHeight } from './width-and-height'
14 |
15 | export const plugins = [
16 | backgroundColor,
17 | borderColor,
18 | borderWidth,
19 | borderRadius,
20 | color,
21 | font,
22 | letterSpacing,
23 | lineHeight,
24 | margin,
25 | padding,
26 | position,
27 | textStyle,
28 | widthAndHeight
29 | ]
30 |
--------------------------------------------------------------------------------
/src/utilities/create-css/__tests__/create-css-declaration-block/declarations/position/top/negative-top-position.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'tap'
2 |
3 | import { createCssDeclarationBlock } from '../../../../../create-css-declaration-block'
4 |
5 | test('custom top position defined in `theme.space`', function (t) {
6 | t.plan(1)
7 | t.deepEqual(createCssDeclarationBlock('-top-sm', { space: { sm: '4px' } }), {
8 | breakpoint: null,
9 | className: '-top-sm',
10 | declarations: {
11 | top: '-4px'
12 | },
13 | pseudoClass: null,
14 | selector: '-top-sm'
15 | })
16 | })
17 |
18 | test('pixel top position', function (t) {
19 | t.plan(1)
20 | t.deepEqual(createCssDeclarationBlock('-top-2px', {}), {
21 | breakpoint: null,
22 | className: '-top-2px',
23 | declarations: {
24 | top: '-2px'
25 | },
26 | pseudoClass: null,
27 | selector: '-top-2px'
28 | })
29 | })
30 |
--------------------------------------------------------------------------------
/src/utilities/create-css/plugins/line-height.ts:
--------------------------------------------------------------------------------
1 | /*
2 | line-height
3 |
4 | `.leading` | `line-height: ${theme.lineHeight.default};`
5 | `.leading-${key}` | `line-height: ${theme.lineHeight[key]};`
6 | */
7 |
8 | import { Plugin, ThemeKeys } from '../../../types'
9 |
10 | export const lineHeight: Plugin = {
11 | createDeclarations: function ({
12 | computeNumericValue,
13 | matches
14 | }: {
15 | computeNumericValue: (
16 | value: string,
17 | themeKeys: Array
18 | ) => null | string
19 | matches: RegExpMatchArray
20 | }): { [property: string]: string } {
21 | const lineHeight = computeNumericValue(matches[1], ['lineHeight'])
22 | if (lineHeight === null) {
23 | throw new Error(`Invalid line-height: ${matches[1]}`)
24 | }
25 | return {
26 | 'line-height': `${lineHeight}`
27 | }
28 | },
29 | regex: /^(?:leading|line-height)(?:-(.+))?$/
30 | }
31 |
--------------------------------------------------------------------------------
/src/utilities/create-css/__tests__/create-css-declaration-block/declarations/position/left/negative-left-position.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'tap'
2 |
3 | import { createCssDeclarationBlock } from '../../../../../create-css-declaration-block'
4 |
5 | test('custom left position defined in `theme.space`', function (t) {
6 | t.plan(1)
7 | t.deepEqual(createCssDeclarationBlock('-left-sm', { space: { sm: '4px' } }), {
8 | breakpoint: null,
9 | className: '-left-sm',
10 | declarations: {
11 | left: '-4px'
12 | },
13 | pseudoClass: null,
14 | selector: '-left-sm'
15 | })
16 | })
17 |
18 | test('pixel left position', function (t) {
19 | t.plan(1)
20 | t.deepEqual(createCssDeclarationBlock('-left-2px', {}), {
21 | breakpoint: null,
22 | className: '-left-2px',
23 | declarations: {
24 | left: '-2px'
25 | },
26 | pseudoClass: null,
27 | selector: '-left-2px'
28 | })
29 | })
30 |
--------------------------------------------------------------------------------
/src/watch.ts:
--------------------------------------------------------------------------------
1 | import * as chokidar from 'chokidar'
2 |
3 | import { build } from './build'
4 | import { CliOptions } from './types'
5 | import { log } from './utilities/log'
6 |
7 | export function watch(cliOptions: CliOptions): void {
8 | const files = [cliOptions.sourceFilesPattern]
9 | if (typeof cliOptions.configFilePath === 'string') {
10 | files.push(cliOptions.configFilePath)
11 | } else {
12 | files.push('package.json')
13 | }
14 | if (cliOptions.prependCssFilesPattern !== null) {
15 | files.push(cliOptions.prependCssFilesPattern)
16 | }
17 | if (cliOptions.appendCssFilesPattern !== null) {
18 | files.push(cliOptions.appendCssFilesPattern)
19 | }
20 | const watcher = chokidar.watch(files)
21 | async function onChangeAsync() {
22 | await build(cliOptions)
23 | log.info('Watching...')
24 | }
25 | watcher.on('ready', onChangeAsync)
26 | watcher.on('change', onChangeAsync)
27 | }
28 |
--------------------------------------------------------------------------------
/scripts/print-theme-keys.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs-extra'
2 | import * as path from 'path'
3 |
4 | const themeTypeRegex = /export type Theme = {([\S\s]*?)}/
5 | const keysToOmit = ['baseSpace', 'baseFontSize', 'breakpoint']
6 |
7 | async function main() {
8 | const file = path.join(path.resolve(__dirname, '..'), 'src', 'types.ts')
9 | const content = await fs.readFile(file, 'utf8')
10 | const match = content.match(themeTypeRegex)
11 | if (match === null) {
12 | throw new Error('`Theme` type not found')
13 | }
14 | const lines = match[1].trim().split('\n')
15 | const result = []
16 | for (const line of lines) {
17 | const split = line.trim().split('?:')
18 | if (keysToOmit.indexOf(split[0]) === -1) {
19 | result.push(split[0])
20 | }
21 | }
22 | result.sort()
23 | for (const key of result) {
24 | console.log(`- \`theme.${key}\``) // eslint-disable-line no-console
25 | }
26 | }
27 | main()
28 |
--------------------------------------------------------------------------------
/src/utilities/extract-class-names-async/extract-class-names-async.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs-extra'
2 | import * as globby from 'globby'
3 |
4 | import { extractClassNamesFromHtml } from './extract-class-names-from-html'
5 | import { extractClassNamesFromJs } from './extract-class-names-from-js'
6 |
7 | const jsFileRegex = /.[jt]sx?$/
8 |
9 | export async function extractClassNamesAsync(
10 | pattern: string
11 | ): Promise> {
12 | const files = await globby(pattern)
13 | const result: { [key: string]: boolean } = {}
14 | for (const file of files) {
15 | const string = await fs.readFile(file, 'utf8')
16 | const classNames =
17 | jsFileRegex.test(file) === true
18 | ? extractClassNamesFromJs(string)
19 | : extractClassNamesFromHtml(string)
20 | for (const className of classNames) {
21 | result[className] = true
22 | }
23 | }
24 | return Object.keys(result).sort()
25 | }
26 |
--------------------------------------------------------------------------------
/src/utilities/create-css/declarations/index.ts:
--------------------------------------------------------------------------------
1 | import { Declarations } from '../../../types'
2 | import { alignItems } from './align-items'
3 | import { cursor } from './cursor'
4 | import { display } from './display'
5 | import { flex } from './flex'
6 | import { flexWrap } from './flex-wrap'
7 | import { justifyContent } from './justify-content'
8 | import { outline } from './outline'
9 | import { position } from './position'
10 | import { select } from './select'
11 | import { textAlign } from './text-align'
12 | import { textDecoration } from './text-decoration'
13 | import { textTransform } from './text-transform'
14 |
15 | export const declarations: { [selector: string]: Declarations } = {
16 | ...alignItems,
17 | ...cursor,
18 | ...display,
19 | ...flex,
20 | ...flexWrap,
21 | ...outline,
22 | ...justifyContent,
23 | ...position,
24 | ...select,
25 | ...textAlign,
26 | ...textDecoration,
27 | ...textTransform
28 | }
29 |
--------------------------------------------------------------------------------
/src/utilities/create-css/plugins/border-color.ts:
--------------------------------------------------------------------------------
1 | /*
2 | border-color
3 |
4 | `.border` | `border-color: ${theme.borderColor.default || theme.color.default};`
5 | `.border-${key}` | `border-color: ${theme.borderColor[key] || theme.color[key]};`
6 | */
7 |
8 | import { Plugin, ThemeKeys } from '../../../types'
9 |
10 | export const borderColor: Plugin = {
11 | createDeclarations: function ({
12 | matches,
13 | computeColorValue
14 | }: {
15 | matches: RegExpMatchArray
16 | computeColorValue: (
17 | value: string,
18 | themeKeys: Array
19 | ) => null | string
20 | }): { [property: string]: string } {
21 | const borderColor = computeColorValue(matches[1], ['borderColor', 'color'])
22 | if (borderColor === null) {
23 | throw new Error(`Invalid border color: ${matches[1]}`)
24 | }
25 | return {
26 | 'border-color': `${borderColor}`
27 | }
28 | },
29 | regex: /^border(?:-(.+))?$/
30 | }
31 |
--------------------------------------------------------------------------------
/src/utilities/create-css/plugins/letter-spacing.ts:
--------------------------------------------------------------------------------
1 | /*
2 | letter-spacing
3 |
4 | `.kerning` | `letter-spacing: ${theme.letterSpacing.default};`
5 | `.kerning-${key}` | `letter-spacing: ${theme.letterSpacing[key]};`
6 | */
7 |
8 | import { Plugin, ThemeKeys } from '../../../types'
9 |
10 | export const letterSpacing: Plugin = {
11 | createDeclarations: function ({
12 | computeNumericValue,
13 | matches
14 | }: {
15 | computeNumericValue: (
16 | value: string,
17 | themeKeys: Array
18 | ) => null | string
19 | matches: RegExpMatchArray
20 | }): { [property: string]: string } {
21 | const letterSpacing = computeNumericValue(matches[1], ['letterSpacing'])
22 | if (letterSpacing === null) {
23 | throw new Error(`Invalid letter-spacing: ${matches[1]}`)
24 | }
25 | return {
26 | 'letter-spacing': `${letterSpacing}`
27 | }
28 | },
29 | regex: /^(?:kerning|letter-spacing|tracking)(?:-(.+))?$/
30 | }
31 |
--------------------------------------------------------------------------------
/src/utilities/create-css/__tests__/create-css-declaration-block/declarations/font-weight.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'tap'
2 |
3 | import { createCssDeclarationBlock } from '../../../create-css-declaration-block'
4 |
5 | test('font-weight not defined in `theme`', function (t) {
6 | t.plan(2)
7 | t.throw(function () {
8 | createCssDeclarationBlock('font-bold', {})
9 | })
10 | t.throw(function () {
11 | createCssDeclarationBlock('font-bold', {
12 | fontWeight: {}
13 | })
14 | })
15 | })
16 |
17 | test('custom font weight defined in `theme.fontWeight`', function (t) {
18 | t.plan(1)
19 | t.deepEqual(
20 | createCssDeclarationBlock('font-bold', {
21 | fontWeight: {
22 | bold: '600'
23 | }
24 | }),
25 | {
26 | breakpoint: null,
27 | className: 'font-bold',
28 | declarations: {
29 | 'font-weight': '600'
30 | },
31 | pseudoClass: null,
32 | selector: 'font-bold'
33 | }
34 | )
35 | })
36 |
--------------------------------------------------------------------------------
/src/utilities/create-css/__tests__/create-css-declaration-block/declarations/position/right/negative-right-position.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'tap'
2 |
3 | import { createCssDeclarationBlock } from '../../../../../create-css-declaration-block'
4 |
5 | test('custom right position defined in `theme.space`', function (t) {
6 | t.plan(1)
7 | t.deepEqual(
8 | createCssDeclarationBlock('-right-sm', { space: { sm: '4px' } }),
9 | {
10 | breakpoint: null,
11 | className: '-right-sm',
12 | declarations: {
13 | right: '-4px'
14 | },
15 | pseudoClass: null,
16 | selector: '-right-sm'
17 | }
18 | )
19 | })
20 |
21 | test('pixel right position', function (t) {
22 | t.plan(1)
23 | t.deepEqual(createCssDeclarationBlock('-right-2px', {}), {
24 | breakpoint: null,
25 | className: '-right-2px',
26 | declarations: {
27 | right: '-2px'
28 | },
29 | pseudoClass: null,
30 | selector: '-right-2px'
31 | })
32 | })
33 |
--------------------------------------------------------------------------------
/src/utilities/create-css/__tests__/create-css-declaration-block/declarations/position/bottom/negative-bottom-position.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'tap'
2 |
3 | import { createCssDeclarationBlock } from '../../../../../create-css-declaration-block'
4 |
5 | test('custom bottom position defined in `theme.space`', function (t) {
6 | t.plan(1)
7 | t.deepEqual(
8 | createCssDeclarationBlock('-bottom-sm', { space: { sm: '4px' } }),
9 | {
10 | breakpoint: null,
11 | className: '-bottom-sm',
12 | declarations: {
13 | bottom: '-4px'
14 | },
15 | pseudoClass: null,
16 | selector: '-bottom-sm'
17 | }
18 | )
19 | })
20 |
21 | test('pixel bottom position', function (t) {
22 | t.plan(1)
23 | t.deepEqual(createCssDeclarationBlock('-bottom-2px', {}), {
24 | breakpoint: null,
25 | className: '-bottom-2px',
26 | declarations: {
27 | bottom: '-2px'
28 | },
29 | pseudoClass: null,
30 | selector: '-bottom-2px'
31 | })
32 | })
33 |
--------------------------------------------------------------------------------
/src/utilities/create-css/plugins/background-color.ts:
--------------------------------------------------------------------------------
1 | /*
2 | background-color
3 |
4 | `.bg` | `background-color: ${theme.backgroundColor.default || theme.color.default};`
5 | `.bg-${key}` | `background-color: ${theme.backgroundColor[key] || theme.color[key]};`
6 | */
7 |
8 | import { Plugin, ThemeKeys } from '../../../types'
9 |
10 | export const backgroundColor: Plugin = {
11 | createDeclarations: function ({
12 | matches,
13 | computeColorValue
14 | }: {
15 | matches: RegExpMatchArray
16 | computeColorValue: (
17 | value: string,
18 | themeKeys: Array
19 | ) => null | string
20 | }): { [property: string]: string } {
21 | const backgroundColor = computeColorValue(matches[1], [
22 | 'backgroundColor',
23 | 'color'
24 | ])
25 | if (backgroundColor === null) {
26 | throw new Error(`Invalid background color: ${matches[1]}`)
27 | }
28 | return {
29 | 'background-color': `${backgroundColor}`
30 | }
31 | },
32 | regex: /^bg(?:-(.+))?$/
33 | }
34 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) Yuan Qing Lim
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 |
--------------------------------------------------------------------------------
/src/utilities/create-base-font-size-css.ts:
--------------------------------------------------------------------------------
1 | import { Config } from '../types'
2 |
3 | export function createBaseFontSizeCss(config: Config): string {
4 | if (typeof config.theme.baseFontSize === 'undefined') {
5 | return ''
6 | }
7 | const result: Array = []
8 | for (const breakpoint of Object.keys(config.theme.baseFontSize)) {
9 | const fontSize = config.theme.baseFontSize[breakpoint]
10 | if (breakpoint === 'default') {
11 | result.push(createHtmlFontSizeCss(fontSize))
12 | continue
13 | }
14 | if (typeof config.theme.breakpoint === 'undefined') {
15 | throw new Error('`theme.breakpoint` not defined in configuration')
16 | }
17 | if (typeof config.theme.breakpoint[breakpoint] === 'undefined') {
18 | throw new Error(
19 | `Breakpoint not defined in \`theme.breakpoint\`: ${breakpoint}`
20 | )
21 | }
22 | result.push(
23 | `@media (min-width: ${
24 | config.theme.breakpoint[breakpoint]
25 | }) { ${createHtmlFontSizeCss(fontSize)} }`
26 | )
27 | }
28 | return result.join('')
29 | }
30 |
31 | function createHtmlFontSizeCss(fontSize: string): string {
32 | return `html { font-size: ${fontSize}; }`
33 | }
34 |
--------------------------------------------------------------------------------
/src/utilities/create-css/__tests__/create-css-declaration-block/declarations/position/top/positive-top-position.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'tap'
2 |
3 | import { createCssDeclarationBlock } from '../../../../../create-css-declaration-block'
4 |
5 | test('default top position', function (t) {
6 | t.plan(1)
7 | t.deepEqual(createCssDeclarationBlock('top', {}), {
8 | breakpoint: null,
9 | className: 'top',
10 | declarations: {
11 | top: '0'
12 | },
13 | pseudoClass: null,
14 | selector: 'top'
15 | })
16 | })
17 |
18 | test('custom top position defined in `theme.space`', function (t) {
19 | t.plan(1)
20 | t.deepEqual(createCssDeclarationBlock('top-sm', { space: { sm: '4px' } }), {
21 | breakpoint: null,
22 | className: 'top-sm',
23 | declarations: {
24 | top: '4px'
25 | },
26 | pseudoClass: null,
27 | selector: 'top-sm'
28 | })
29 | })
30 |
31 | test('pixel top position', function (t) {
32 | t.plan(1)
33 | t.deepEqual(createCssDeclarationBlock('top-2px', {}), {
34 | breakpoint: null,
35 | className: 'top-2px',
36 | declarations: {
37 | top: '2px'
38 | },
39 | pseudoClass: null,
40 | selector: 'top-2px'
41 | })
42 | })
43 |
--------------------------------------------------------------------------------
/src/utilities/create-css/__tests__/create-css-declaration-block/declarations/position/left/positive-left-position.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'tap'
2 |
3 | import { createCssDeclarationBlock } from '../../../../../create-css-declaration-block'
4 |
5 | test('default left position', function (t) {
6 | t.plan(1)
7 | t.deepEqual(createCssDeclarationBlock('left', {}), {
8 | breakpoint: null,
9 | className: 'left',
10 | declarations: {
11 | left: '0'
12 | },
13 | pseudoClass: null,
14 | selector: 'left'
15 | })
16 | })
17 |
18 | test('custom left position defined in `theme.space`', function (t) {
19 | t.plan(1)
20 | t.deepEqual(createCssDeclarationBlock('left-sm', { space: { sm: '4px' } }), {
21 | breakpoint: null,
22 | className: 'left-sm',
23 | declarations: {
24 | left: '4px'
25 | },
26 | pseudoClass: null,
27 | selector: 'left-sm'
28 | })
29 | })
30 |
31 | test('pixel left position', function (t) {
32 | t.plan(1)
33 | t.deepEqual(createCssDeclarationBlock('left-2px', {}), {
34 | breakpoint: null,
35 | className: 'left-2px',
36 | declarations: {
37 | left: '2px'
38 | },
39 | pseudoClass: null,
40 | selector: 'left-2px'
41 | })
42 | })
43 |
--------------------------------------------------------------------------------
/src/utilities/create-css/__tests__/create-css-declaration-block/declarations/position/right/positive-right-position.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'tap'
2 |
3 | import { createCssDeclarationBlock } from '../../../../../create-css-declaration-block'
4 |
5 | test('default right position', function (t) {
6 | t.plan(1)
7 | t.deepEqual(createCssDeclarationBlock('right', {}), {
8 | breakpoint: null,
9 | className: 'right',
10 | declarations: {
11 | right: '0'
12 | },
13 | pseudoClass: null,
14 | selector: 'right'
15 | })
16 | })
17 |
18 | test('custom right position defined in `theme.space`', function (t) {
19 | t.plan(1)
20 | t.deepEqual(createCssDeclarationBlock('right-sm', { space: { sm: '4px' } }), {
21 | breakpoint: null,
22 | className: 'right-sm',
23 | declarations: {
24 | right: '4px'
25 | },
26 | pseudoClass: null,
27 | selector: 'right-sm'
28 | })
29 | })
30 |
31 | test('pixel right position', function (t) {
32 | t.plan(1)
33 | t.deepEqual(createCssDeclarationBlock('right-2px', {}), {
34 | breakpoint: null,
35 | className: 'right-2px',
36 | declarations: {
37 | right: '2px'
38 | },
39 | pseudoClass: null,
40 | selector: 'right-2px'
41 | })
42 | })
43 |
--------------------------------------------------------------------------------
/src/utilities/create-css/__tests__/create-css-declaration-block/declarations/font-size.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'tap'
2 |
3 | import { createCssDeclarationBlock } from '../../../create-css-declaration-block'
4 |
5 | test('font size not defined in `theme`', function (t) {
6 | t.plan(2)
7 | t.throw(function () {
8 | createCssDeclarationBlock('font-sm', {})
9 | })
10 | t.throw(function () {
11 | createCssDeclarationBlock('font-sm', {
12 | fontSize: {}
13 | })
14 | })
15 | })
16 |
17 | test('custom font size defined in `theme.fontSize`', function (t) {
18 | t.plan(1)
19 | t.deepEqual(
20 | createCssDeclarationBlock('font-sm', {
21 | fontSize: {
22 | sm: '1.0rem'
23 | }
24 | }),
25 | {
26 | breakpoint: null,
27 | className: 'font-sm',
28 | declarations: {
29 | 'font-size': '1.0rem'
30 | },
31 | pseudoClass: null,
32 | selector: 'font-sm'
33 | }
34 | )
35 | })
36 |
37 | test('pixel font size', function (t) {
38 | t.plan(1)
39 | t.deepEqual(createCssDeclarationBlock('font-16px', {}), {
40 | breakpoint: null,
41 | className: 'font-16px',
42 | declarations: {
43 | 'font-size': '16px'
44 | },
45 | pseudoClass: null,
46 | selector: 'font-16px'
47 | })
48 | })
49 |
--------------------------------------------------------------------------------
/src/utilities/create-css/__tests__/create-css-declaration-block/declarations/position/bottom/positive-bottom-position.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'tap'
2 |
3 | import { createCssDeclarationBlock } from '../../../../../create-css-declaration-block'
4 |
5 | test('default bottom position', function (t) {
6 | t.plan(1)
7 | t.deepEqual(createCssDeclarationBlock('bottom', {}), {
8 | breakpoint: null,
9 | className: 'bottom',
10 | declarations: {
11 | bottom: '0'
12 | },
13 | pseudoClass: null,
14 | selector: 'bottom'
15 | })
16 | })
17 |
18 | test('custom bottom position defined in `theme.space`', function (t) {
19 | t.plan(1)
20 | t.deepEqual(
21 | createCssDeclarationBlock('bottom-sm', { space: { sm: '4px' } }),
22 | {
23 | breakpoint: null,
24 | className: 'bottom-sm',
25 | declarations: {
26 | bottom: '4px'
27 | },
28 | pseudoClass: null,
29 | selector: 'bottom-sm'
30 | }
31 | )
32 | })
33 |
34 | test('pixel bottom position', function (t) {
35 | t.plan(1)
36 | t.deepEqual(createCssDeclarationBlock('bottom-2px', {}), {
37 | breakpoint: null,
38 | className: 'bottom-2px',
39 | declarations: {
40 | bottom: '2px'
41 | },
42 | pseudoClass: null,
43 | selector: 'bottom-2px'
44 | })
45 | })
46 |
--------------------------------------------------------------------------------
/src/css/reset.css:
--------------------------------------------------------------------------------
1 | *,
2 | ::before,
3 | ::after {
4 | box-sizing: border-box;
5 | border-color: currentColor;
6 | border-style: solid;
7 | border-width: 0;
8 | }
9 |
10 | audio,
11 | canvas,
12 | embed,
13 | iframe,
14 | img,
15 | object,
16 | svg,
17 | video {
18 | display: block;
19 | }
20 |
21 | blockquote,
22 | dd,
23 | dl,
24 | fieldset,
25 | figure,
26 | h1,
27 | h2,
28 | h3,
29 | h4,
30 | h5,
31 | h6,
32 | hr,
33 | ol,
34 | p,
35 | pre,
36 | ul {
37 | margin: 0;
38 | }
39 |
40 | a {
41 | color: inherit;
42 | text-decoration: inherit;
43 | }
44 |
45 | button,
46 | input,
47 | optgroup,
48 | select,
49 | textarea {
50 | padding: 0;
51 | color: inherit;
52 | line-height: inherit;
53 | }
54 |
55 | button {
56 | background-color: transparent;
57 | background-image: none;
58 | }
59 |
60 | button:focus {
61 | outline: 1px dotted;
62 | outline: 5px auto -webkit-focus-ring-color;
63 | }
64 |
65 | fieldset {
66 | padding: 0;
67 | }
68 |
69 | h1,
70 | h2,
71 | h3,
72 | h4,
73 | h5,
74 | h6 {
75 | font-weight: inherit;
76 | font-size: inherit;
77 | }
78 |
79 | hr {
80 | border-top-width: 1px;
81 | }
82 |
83 | img {
84 | border-style: solid;
85 | }
86 |
87 | ol,
88 | ul {
89 | padding: 0;
90 | list-style: none;
91 | }
92 |
93 | table {
94 | border-collapse: collapse;
95 | }
96 |
--------------------------------------------------------------------------------
/src/utilities/create-css/parse-class-name.ts:
--------------------------------------------------------------------------------
1 | import { ParsedClassName } from '../../types'
2 |
3 | const classRegex = /^(?:([^@]+)@)?([^:]+)(?::(.+))?$/
4 |
5 | export function parseClassName(
6 | className: string,
7 | breakpoints: Array
8 | ): ParsedClassName {
9 | const matches = className.match(classRegex)
10 | if (matches === null) {
11 | throw new Error(`Invalid class name: ${className}`)
12 | }
13 | const breakpoint = typeof matches[1] === 'undefined' ? null : matches[1]
14 | if (breakpoint !== null && breakpoints.indexOf(breakpoint) === -1) {
15 | throw new Error(`Invalid breakpoint: ${breakpoint}`)
16 | }
17 | if (typeof matches[3] === 'undefined') {
18 | return {
19 | breakpoint,
20 | pseudoClass: null,
21 | selector: matches[2]
22 | }
23 | }
24 | return {
25 | breakpoint,
26 | pseudoClass: parsePseudoClass(matches[2]),
27 | selector: matches[3]
28 | }
29 | }
30 |
31 | const groupPseudoClassRegex = /^group-(.+)$/
32 |
33 | function parsePseudoClass(
34 | pseudoClass: string
35 | ): {
36 | value: string
37 | isParent: boolean
38 | } {
39 | const matches = pseudoClass.match(groupPseudoClassRegex)
40 | if (matches === null) {
41 | return {
42 | isParent: false,
43 | value: pseudoClass
44 | }
45 | }
46 | return {
47 | isParent: true,
48 | value: matches[1]
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/utilities/create-css/__tests__/create-css-declaration-block/declarations/text-style.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'tap'
2 |
3 | import { createCssDeclarationBlock } from '../../../create-css-declaration-block'
4 |
5 | test('text style not defined in `theme`', function (t) {
6 | t.plan(2)
7 | t.throw(function () {
8 | createCssDeclarationBlock('text-sm', {})
9 | })
10 | t.throw(function () {
11 | createCssDeclarationBlock('text-sm', {
12 | fontSize: {},
13 | fontWeight: {},
14 | letterSpacing: {},
15 | lineHeight: {}
16 | })
17 | })
18 | })
19 |
20 | test('text style with font size, font weight, letter-spacing and line-height defined in `theme`', function (t) {
21 | t.plan(1)
22 | t.deepEqual(
23 | createCssDeclarationBlock('text-sm', {
24 | fontSize: {
25 | sm: '1.0rem'
26 | },
27 | fontWeight: {
28 | sm: '400'
29 | },
30 | letterSpacing: {
31 | sm: '0.025em'
32 | },
33 | lineHeight: {
34 | sm: '1.25rem'
35 | }
36 | }),
37 | {
38 | breakpoint: null,
39 | className: 'text-sm',
40 | declarations: {
41 | 'font-size': '1.0rem',
42 | 'font-weight': '400',
43 | 'letter-spacing': '0.025em',
44 | 'line-height': '1.25rem'
45 | },
46 | pseudoClass: null,
47 | selector: 'text-sm'
48 | }
49 | )
50 | })
51 |
--------------------------------------------------------------------------------
/src/utilities/create-css/__tests__/create-css-declaration-block/declarations/color.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'tap'
2 |
3 | import { createCssDeclarationBlock } from '../../../create-css-declaration-block'
4 |
5 | test('color not defined in `theme`', function (t) {
6 | t.plan(3)
7 | t.throw(function () {
8 | createCssDeclarationBlock('color', {})
9 | })
10 | t.throw(function () {
11 | createCssDeclarationBlock('color-black', {})
12 | })
13 | t.throw(function () {
14 | createCssDeclarationBlock('color-black', {
15 | color: {}
16 | })
17 | })
18 | })
19 |
20 | test('default color defined in `theme.color`', function (t) {
21 | t.plan(1)
22 | t.deepEqual(
23 | createCssDeclarationBlock('color', {
24 | color: {
25 | default: '#ffffff'
26 | }
27 | }),
28 | {
29 | breakpoint: null,
30 | className: 'color',
31 | declarations: {
32 | color: '#ffffff'
33 | },
34 | pseudoClass: null,
35 | selector: 'color'
36 | }
37 | )
38 | })
39 |
40 | test('custom color defined in `theme.color`', function (t) {
41 | t.plan(1)
42 | t.deepEqual(
43 | createCssDeclarationBlock('color-black', {
44 | color: {
45 | black: '#000000'
46 | }
47 | }),
48 | {
49 | breakpoint: null,
50 | className: 'color-black',
51 | declarations: {
52 | color: '#000000'
53 | },
54 | pseudoClass: null,
55 | selector: 'color-black'
56 | }
57 | )
58 | })
59 |
--------------------------------------------------------------------------------
/src/utilities/create-css/plugins/position.ts:
--------------------------------------------------------------------------------
1 | /*
2 | top, right, bottom, left
3 |
4 | - `value` = `theme.space[key]` || `resolveNumericValue(key)`
5 |
6 | `.top` | `top: 0;`
7 | `.top-${key}` | `top: ${value};`
8 | `.-top-${key}` | `top: -${value};`
9 | `.right` | `right: 0;`
10 | `.right-${key}` | `right: ${value};`
11 | `.-right-${key}` | `right: -${value};`
12 | `.bottom` | `bottom: 0;`
13 | `.bottom-${key}` | `bottom: ${value};`
14 | `.-bottom-${key}` | `bottom: -${value};`
15 | `.left` | `left: 0;`
16 | `.left-${key}` | `left: ${value};`
17 | `.-left-${key}` | `left: -${value};`
18 | */
19 |
20 | import { Plugin, ThemeKeys } from '../../../types'
21 |
22 | export const position: Plugin = {
23 | createDeclarations: function ({
24 | matches,
25 | computeNumericValue
26 | }: {
27 | matches: RegExpMatchArray
28 | computeNumericValue: (
29 | value: string,
30 | themeKeys: Array
31 | ) => null | string
32 | }): { [property: string]: string } {
33 | const property = matches[2]
34 | const value = computeNumericValue(
35 | `${matches[1] === '-' ? '-' : ''}${
36 | typeof matches[3] === 'undefined' ? '0' : matches[3]
37 | }`,
38 | ['space']
39 | )
40 | if (value === null) {
41 | throw new Error(
42 | `Invalid ${
43 | matches[1] === '-' ? 'negative ' : ''
44 | }${property} position: ${matches[3]}`
45 | )
46 | }
47 | return {
48 | [property]: value
49 | }
50 | },
51 | regex: /^(-?)(top|right|bottom|left)?(?:-(.+))?$/
52 | }
53 |
--------------------------------------------------------------------------------
/src/utilities/create-css/__tests__/create-css-declaration-block/declarations/font-family.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'tap'
2 |
3 | import { createCssDeclarationBlock } from '../../../create-css-declaration-block'
4 |
5 | test('font-family not defined in `theme`', function (t) {
6 | t.plan(3)
7 | t.throw(function () {
8 | createCssDeclarationBlock('font', {})
9 | })
10 | t.throw(function () {
11 | createCssDeclarationBlock('font', {
12 | fontFamily: {}
13 | })
14 | })
15 | t.throw(function () {
16 | createCssDeclarationBlock('font-serif', {
17 | fontFamily: {}
18 | })
19 | })
20 | })
21 |
22 | test('default font defined in `theme.fontFamily`', function (t) {
23 | t.plan(1)
24 | t.deepEqual(
25 | createCssDeclarationBlock('font', {
26 | fontFamily: {
27 | default: 'sans-serif'
28 | }
29 | }),
30 | {
31 | breakpoint: null,
32 | className: 'font',
33 | declarations: {
34 | 'font-family': 'sans-serif'
35 | },
36 | pseudoClass: null,
37 | selector: 'font'
38 | }
39 | )
40 | })
41 |
42 | test('custom font defined in `theme.font`', function (t) {
43 | t.plan(1)
44 | t.deepEqual(
45 | createCssDeclarationBlock('font-serif', {
46 | fontFamily: {
47 | serif: 'serif'
48 | }
49 | }),
50 | {
51 | breakpoint: null,
52 | className: 'font-serif',
53 | declarations: {
54 | 'font-family': 'serif'
55 | },
56 | pseudoClass: null,
57 | selector: 'font-serif'
58 | }
59 | )
60 | })
61 |
--------------------------------------------------------------------------------
/src/utilities/create-css/create-css-declaration-block.ts:
--------------------------------------------------------------------------------
1 | import { CssDeclarationBlock, Theme } from '../../types'
2 | import { computeColorValueFactory } from './compute-color-value-factory'
3 | import { computeNumericValueFactory } from './compute-numeric-value-factory'
4 | import { mapSelectorToDeclaration } from './map-selector-to-declaration'
5 | import { parseClassName } from './parse-class-name'
6 | import { plugins } from './plugins'
7 |
8 | export function createCssDeclarationBlock(
9 | className: string,
10 | theme: Theme
11 | ): null | CssDeclarationBlock {
12 | const { breakpoint, pseudoClass, selector } = parseClassName(
13 | className,
14 | typeof theme.breakpoint === 'undefined' ? [] : Object.keys(theme.breakpoint)
15 | )
16 | const declarations = mapSelectorToDeclaration(selector)
17 | if (declarations !== null) {
18 | return {
19 | breakpoint,
20 | className,
21 | declarations,
22 | pseudoClass,
23 | selector
24 | }
25 | }
26 | const computeNumericValue = computeNumericValueFactory(theme)
27 | const computeColorValue = computeColorValueFactory(theme)
28 | let matches
29 | for (const plugin of plugins) {
30 | matches = selector.match(plugin.regex)
31 | if (matches !== null) {
32 | return {
33 | breakpoint,
34 | className,
35 | declarations: plugin.createDeclarations({
36 | computeColorValue,
37 | computeNumericValue,
38 | matches,
39 | theme
40 | }),
41 | pseudoClass,
42 | selector
43 | }
44 | }
45 | }
46 | return null
47 | }
48 |
--------------------------------------------------------------------------------
/src/utilities/create-css/__tests__/create-css-declaration-block/declarations/line-height.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'tap'
2 |
3 | import { createCssDeclarationBlock } from '../../../create-css-declaration-block'
4 |
5 | test('line-height not defined in `theme`', function (t) {
6 | t.plan(2)
7 | t.throw(function () {
8 | createCssDeclarationBlock('leading', {})
9 | })
10 | t.throw(function () {
11 | createCssDeclarationBlock('leading', { lineHeight: {} })
12 | })
13 | })
14 |
15 | test('default line-height defined in `theme.lineHeight`', function (t) {
16 | t.plan(1)
17 | t.deepEqual(
18 | createCssDeclarationBlock('leading', {
19 | lineHeight: {
20 | default: '1.5rem'
21 | }
22 | }),
23 | {
24 | breakpoint: null,
25 | className: 'leading',
26 | declarations: {
27 | 'line-height': '1.5rem'
28 | },
29 | pseudoClass: null,
30 | selector: 'leading'
31 | }
32 | )
33 | })
34 |
35 | test('custom line-height defined in `theme.lineHeight`', function (t) {
36 | t.plan(1)
37 | t.deepEqual(
38 | createCssDeclarationBlock('leading-sm', { lineHeight: { sm: '1.25rem' } }),
39 | {
40 | breakpoint: null,
41 | className: 'leading-sm',
42 | declarations: {
43 | 'line-height': '1.25rem'
44 | },
45 | pseudoClass: null,
46 | selector: 'leading-sm'
47 | }
48 | )
49 | })
50 |
51 | test('pixel line-height', function (t) {
52 | t.plan(1)
53 | t.deepEqual(createCssDeclarationBlock('leading-16px', {}), {
54 | breakpoint: null,
55 | className: 'leading-16px',
56 | declarations: {
57 | 'line-height': '16px'
58 | },
59 | pseudoClass: null,
60 | selector: 'leading-16px'
61 | })
62 | })
63 |
--------------------------------------------------------------------------------
/src/utilities/extract-class-names-async/__tests__/extract-class-names-from-html.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'tap'
2 |
3 | import { extractClassNamesFromHtml } from '../extract-class-names-from-html'
4 |
5 | test('empty string', function (t) {
6 | t.plan(1)
7 | t.deepEqual(extractClassNamesFromHtml(''), [])
8 | })
9 |
10 | test('no class attributes', function (t) {
11 | t.plan(1)
12 | t.deepEqual(extractClassNamesFromHtml(''), [])
13 | })
14 |
15 | test('empty class attribute', function (t) {
16 | t.plan(1)
17 | t.deepEqual(extractClassNamesFromHtml(''), [])
18 | })
19 |
20 | test('single class', function (t) {
21 | t.plan(1)
22 | t.deepEqual(extractClassNamesFromHtml(''), ['flex'])
23 | })
24 |
25 | test('multiple classes', function (t) {
26 | t.plan(1)
27 | t.deepEqual(extractClassNamesFromHtml(''), [
28 | 'bg-black',
29 | 'flex'
30 | ])
31 | })
32 |
33 | test('multiple matches', function (t) {
34 | t.plan(1)
35 | t.deepEqual(
36 | extractClassNamesFromHtml(
37 | ''
38 | ),
39 | ['bg-black', 'color-red', 'flex']
40 | )
41 | })
42 |
43 | test('single-quoted attributes', function (t) {
44 | t.plan(1)
45 | t.deepEqual(
46 | extractClassNamesFromHtml(
47 | ""
48 | ),
49 | ['bg-black', 'color-red', 'flex']
50 | )
51 | })
52 |
53 | test('leading and trailing consecutive spaces', function (t) {
54 | t.plan(1)
55 | t.deepEqual(
56 | extractClassNamesFromHtml(
57 | ""
58 | ),
59 | ['bg-black', 'color-red', 'flex']
60 | )
61 | })
62 |
--------------------------------------------------------------------------------
/src/utilities/create-css/__tests__/create-css-declaration-block/declarations/letter-spacing.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'tap'
2 |
3 | import { createCssDeclarationBlock } from '../../../create-css-declaration-block'
4 |
5 | test('letter-spacing not defined in `theme`', function (t) {
6 | t.plan(2)
7 | t.throw(function () {
8 | createCssDeclarationBlock('kerning', {})
9 | })
10 | t.throw(function () {
11 | createCssDeclarationBlock('kerning', { letterSpacing: {} })
12 | })
13 | })
14 |
15 | test('default letter-spacing defined in `theme.letterSpacing`', function (t) {
16 | t.plan(1)
17 | t.deepEqual(
18 | createCssDeclarationBlock('kerning', {
19 | letterSpacing: {
20 | default: '0.05em'
21 | }
22 | }),
23 | {
24 | breakpoint: null,
25 | className: 'kerning',
26 | declarations: {
27 | 'letter-spacing': '0.05em'
28 | },
29 | pseudoClass: null,
30 | selector: 'kerning'
31 | }
32 | )
33 | })
34 |
35 | test('custom letter-spacing defined in `theme.letterSpacing`', function (t) {
36 | t.plan(1)
37 | t.deepEqual(
38 | createCssDeclarationBlock('kerning-sm', {
39 | letterSpacing: { sm: '0.025em' }
40 | }),
41 | {
42 | breakpoint: null,
43 | className: 'kerning-sm',
44 | declarations: {
45 | 'letter-spacing': '0.025em'
46 | },
47 | pseudoClass: null,
48 | selector: 'kerning-sm'
49 | }
50 | )
51 | })
52 |
53 | test('pixel letter-spacing', function (t) {
54 | t.plan(1)
55 | t.deepEqual(createCssDeclarationBlock('kerning-1px', {}), {
56 | breakpoint: null,
57 | className: 'kerning-1px',
58 | declarations: {
59 | 'letter-spacing': '1px'
60 | },
61 | pseudoClass: null,
62 | selector: 'kerning-1px'
63 | })
64 | })
65 |
--------------------------------------------------------------------------------
/src/utilities/create-css/__tests__/create-css-declaration-block/declarations/width-and-height/width/width.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'tap'
2 |
3 | import { createCssDeclarationBlock } from '../../../../../create-css-declaration-block'
4 |
5 | test('width not defined in `theme`', function (t) {
6 | t.plan(2)
7 | t.throw(function () {
8 | createCssDeclarationBlock('w', {})
9 | })
10 | t.throw(function () {
11 | createCssDeclarationBlock('w', { width: {} })
12 | })
13 | })
14 |
15 | test('default width defined in `theme.width`', function (t) {
16 | t.plan(1)
17 | t.deepEqual(
18 | createCssDeclarationBlock('w', {
19 | width: {
20 | default: '8px'
21 | }
22 | }),
23 | {
24 | breakpoint: null,
25 | className: 'w',
26 | declarations: {
27 | width: '8px'
28 | },
29 | pseudoClass: null,
30 | selector: 'w'
31 | }
32 | )
33 | })
34 |
35 | test('custom width defined in `theme.width`', function (t) {
36 | t.plan(1)
37 | t.deepEqual(createCssDeclarationBlock('w-sm', { width: { sm: '4px' } }), {
38 | breakpoint: null,
39 | className: 'w-sm',
40 | declarations: {
41 | width: '4px'
42 | },
43 | pseudoClass: null,
44 | selector: 'w-sm'
45 | })
46 | })
47 |
48 | test('pixel width', function (t) {
49 | t.plan(1)
50 | t.deepEqual(createCssDeclarationBlock('w-2px', {}), {
51 | breakpoint: null,
52 | className: 'w-2px',
53 | declarations: {
54 | width: '2px'
55 | },
56 | pseudoClass: null,
57 | selector: 'w-2px'
58 | })
59 | })
60 |
61 | test('screen width', function (t) {
62 | t.plan(1)
63 | t.deepEqual(createCssDeclarationBlock('w-screen', {}), {
64 | breakpoint: null,
65 | className: 'w-screen',
66 | declarations: {
67 | width: '100vw'
68 | },
69 | pseudoClass: null,
70 | selector: 'w-screen'
71 | })
72 | })
73 |
--------------------------------------------------------------------------------
/src/utilities/create-css/__tests__/create-css-declaration-block/declarations/width-and-height/height/height.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'tap'
2 |
3 | import { createCssDeclarationBlock } from '../../../../../create-css-declaration-block'
4 |
5 | test('height not defined in `theme`', function (t) {
6 | t.plan(2)
7 | t.throw(function () {
8 | createCssDeclarationBlock('h', {})
9 | })
10 | t.throw(function () {
11 | createCssDeclarationBlock('h', { height: {} })
12 | })
13 | })
14 |
15 | test('default height defined in `theme.height`', function (t) {
16 | t.plan(1)
17 | t.deepEqual(
18 | createCssDeclarationBlock('h', {
19 | height: {
20 | default: '8px'
21 | }
22 | }),
23 | {
24 | breakpoint: null,
25 | className: 'h',
26 | declarations: {
27 | height: '8px'
28 | },
29 | pseudoClass: null,
30 | selector: 'h'
31 | }
32 | )
33 | })
34 |
35 | test('custom height defined in `theme.height`', function (t) {
36 | t.plan(1)
37 | t.deepEqual(createCssDeclarationBlock('h-sm', { height: { sm: '4px' } }), {
38 | breakpoint: null,
39 | className: 'h-sm',
40 | declarations: {
41 | height: '4px'
42 | },
43 | pseudoClass: null,
44 | selector: 'h-sm'
45 | })
46 | })
47 |
48 | test('pixel height', function (t) {
49 | t.plan(1)
50 | t.deepEqual(createCssDeclarationBlock('h-2px', {}), {
51 | breakpoint: null,
52 | className: 'h-2px',
53 | declarations: {
54 | height: '2px'
55 | },
56 | pseudoClass: null,
57 | selector: 'h-2px'
58 | })
59 | })
60 |
61 | test('screen height', function (t) {
62 | t.plan(1)
63 | t.deepEqual(createCssDeclarationBlock('h-screen', {}), {
64 | breakpoint: null,
65 | className: 'h-screen',
66 | declarations: {
67 | height: '100vh'
68 | },
69 | pseudoClass: null,
70 | selector: 'h-screen'
71 | })
72 | })
73 |
--------------------------------------------------------------------------------
/src/utilities/create-css/plugins/text-style.ts:
--------------------------------------------------------------------------------
1 | /*
2 | text style
3 |
4 | `.text` | `font-size: ${theme.fontSize.default};`
`font-weight: ${theme.fontWeight.default};`
`letter-spacing: ${theme.letterSpacing.default};`
`line-height: ${theme.lineHeight.default};`
5 | `.text-${key}` | `font-size: ${theme.fontSize[key]};`
`font-weight: ${theme.fontWeight[key]};`
`letter-spacing: ${theme.letterSpacing[key]};`
`line-height: ${theme.lineHeight[key]};`
6 | */
7 |
8 | import { Plugin, Theme } from '../../../types'
9 |
10 | export const textStyle: Plugin = {
11 | createDeclarations: function ({
12 | matches,
13 | theme
14 | }: {
15 | matches: RegExpMatchArray
16 | theme: Theme
17 | }): { [property: string]: string } {
18 | const result: { [property: string]: string } = {}
19 | const key = typeof matches[1] === 'undefined' ? 'default' : matches[1]
20 | if (typeof theme.fontSize !== 'undefined') {
21 | const fontSize = theme.fontSize[key]
22 | if (typeof fontSize !== 'undefined') {
23 | result['font-size'] = fontSize
24 | }
25 | }
26 | if (typeof theme.fontWeight !== 'undefined') {
27 | const fontWeight = theme.fontWeight[key]
28 | if (typeof fontWeight !== 'undefined') {
29 | result['font-weight'] = fontWeight
30 | }
31 | }
32 | if (typeof theme.letterSpacing !== 'undefined') {
33 | const letterSpacing = theme.letterSpacing[key]
34 | if (typeof letterSpacing !== 'undefined') {
35 | result['letter-spacing'] = letterSpacing
36 | }
37 | }
38 | if (typeof theme.lineHeight !== 'undefined') {
39 | const lineHeight = theme.lineHeight[key]
40 | if (typeof lineHeight !== 'undefined') {
41 | result['line-height'] = lineHeight
42 | }
43 | }
44 | if (Object.keys(result).length === 0) {
45 | throw new Error('Invalid text style')
46 | }
47 | return result
48 | },
49 | regex: /^text(?:-(.+))?$/
50 | }
51 |
--------------------------------------------------------------------------------
/src/cli.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import * as sade from 'sade'
3 |
4 | import { build } from './build'
5 | import { watch } from './watch'
6 |
7 | sade('generate-css ', true)
8 | .describe(
9 | 'Dynamically generate functional CSS classes from HTML and JavaScript source files'
10 | )
11 | .option(
12 | '-a, --append',
13 | 'Glob pattern for CSS files to append to the generated CSS file'
14 | )
15 | .option('-c, --config', 'Path to a `generate-css` configuration file')
16 | .option('-m, --minify', 'Whether to minify the generated CSS file', false)
17 | .option('-o, --output', 'Path to write the generated CSS file')
18 | .option(
19 | '-p, --prepend',
20 | 'Glob pattern for CSS files to prepend to the generated CSS file'
21 | )
22 | .option(
23 | '-w, --watch',
24 | 'Whether to automatically generate a CSS file on changes to the source files',
25 | false
26 | )
27 | .example('--append reset.css')
28 | .example('--minify')
29 | .example('--output style.css')
30 | .example('--prepend custom.css')
31 | .example('--watch')
32 | .action(async function (
33 | pattern: string,
34 | options: {
35 | append: string
36 | config: string
37 | minify: boolean
38 | output: string
39 | prepend: string
40 | watch: boolean
41 | }
42 | ) {
43 | const cliOptions = {
44 | appendCssFilesPattern:
45 | typeof options.append === 'undefined' ? null : options.append,
46 | configFilePath:
47 | typeof options.config === 'undefined' ? null : options.config,
48 | minify: options.minify,
49 | outputPath: typeof options.output === 'undefined' ? null : options.output,
50 | prependCssFilesPattern:
51 | typeof options.prepend === 'undefined' ? null : options.prepend,
52 | sourceFilesPattern: pattern
53 | }
54 | if (options.watch === true) {
55 | watch(cliOptions)
56 | return
57 | }
58 | await build(cliOptions)
59 | })
60 | .parse(process.argv)
61 |
--------------------------------------------------------------------------------
/src/utilities/create-css/__tests__/create-css-declaration-block/declarations/width-and-height/width/max-width.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'tap'
2 |
3 | import { createCssDeclarationBlock } from '../../../../../create-css-declaration-block'
4 |
5 | test('max-width not defined in `theme`', function (t) {
6 | t.plan(2)
7 | t.throw(function () {
8 | createCssDeclarationBlock('maxw', {})
9 | })
10 | t.throw(function () {
11 | createCssDeclarationBlock('maxw', { maxWidth: {} })
12 | })
13 | })
14 |
15 | test('default max-width defined in `theme.maxWidth`', function (t) {
16 | t.plan(1)
17 | t.deepEqual(
18 | createCssDeclarationBlock('maxw', {
19 | maxWidth: {
20 | default: '100%'
21 | }
22 | }),
23 | {
24 | breakpoint: null,
25 | className: 'maxw',
26 | declarations: {
27 | 'max-width': '100%'
28 | },
29 | pseudoClass: null,
30 | selector: 'maxw'
31 | }
32 | )
33 | })
34 |
35 | test('custom max-width defined in `theme.maxWidth`', function (t) {
36 | t.plan(1)
37 | t.deepEqual(
38 | createCssDeclarationBlock('maxw-sm', { maxWidth: { sm: '320px' } }),
39 | {
40 | breakpoint: null,
41 | className: 'maxw-sm',
42 | declarations: {
43 | 'max-width': '320px'
44 | },
45 | pseudoClass: null,
46 | selector: 'maxw-sm'
47 | }
48 | )
49 | })
50 |
51 | test('pixel max-width', function (t) {
52 | t.plan(1)
53 | t.deepEqual(createCssDeclarationBlock('maxw-320px', {}), {
54 | breakpoint: null,
55 | className: 'maxw-320px',
56 | declarations: {
57 | 'max-width': '320px'
58 | },
59 | pseudoClass: null,
60 | selector: 'maxw-320px'
61 | })
62 | })
63 |
64 | test('screen max-width', function (t) {
65 | t.plan(1)
66 | t.deepEqual(createCssDeclarationBlock('maxw-screen', {}), {
67 | breakpoint: null,
68 | className: 'maxw-screen',
69 | declarations: {
70 | 'max-width': '100vw'
71 | },
72 | pseudoClass: null,
73 | selector: 'maxw-screen'
74 | })
75 | })
76 |
--------------------------------------------------------------------------------
/src/utilities/create-css/__tests__/create-css-declaration-block/declarations/width-and-height/width/min-width.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'tap'
2 |
3 | import { createCssDeclarationBlock } from '../../../../../create-css-declaration-block'
4 |
5 | test('min-width not defined in `theme`', function (t) {
6 | t.plan(2)
7 | t.throw(function () {
8 | createCssDeclarationBlock('minw', {})
9 | })
10 | t.throw(function () {
11 | createCssDeclarationBlock('minw', { minWidth: {} })
12 | })
13 | })
14 |
15 | test('default min-width defined in `theme.minWidth`', function (t) {
16 | t.plan(1)
17 | t.deepEqual(
18 | createCssDeclarationBlock('minw', {
19 | minWidth: {
20 | default: '100%'
21 | }
22 | }),
23 | {
24 | breakpoint: null,
25 | className: 'minw',
26 | declarations: {
27 | 'min-width': '100%'
28 | },
29 | pseudoClass: null,
30 | selector: 'minw'
31 | }
32 | )
33 | })
34 |
35 | test('custom min-width defined in `theme.minWidth`', function (t) {
36 | t.plan(1)
37 | t.deepEqual(
38 | createCssDeclarationBlock('minw-sm', { minWidth: { sm: '320px' } }),
39 | {
40 | breakpoint: null,
41 | className: 'minw-sm',
42 | declarations: {
43 | 'min-width': '320px'
44 | },
45 | pseudoClass: null,
46 | selector: 'minw-sm'
47 | }
48 | )
49 | })
50 |
51 | test('pixel min-width', function (t) {
52 | t.plan(1)
53 | t.deepEqual(createCssDeclarationBlock('minw-320px', {}), {
54 | breakpoint: null,
55 | className: 'minw-320px',
56 | declarations: {
57 | 'min-width': '320px'
58 | },
59 | pseudoClass: null,
60 | selector: 'minw-320px'
61 | })
62 | })
63 |
64 | test('screen min-width', function (t) {
65 | t.plan(1)
66 | t.deepEqual(createCssDeclarationBlock('minw-screen', {}), {
67 | breakpoint: null,
68 | className: 'minw-screen',
69 | declarations: {
70 | 'min-width': '100vw'
71 | },
72 | pseudoClass: null,
73 | selector: 'minw-screen'
74 | })
75 | })
76 |
--------------------------------------------------------------------------------
/src/utilities/create-css/__tests__/create-css-declaration-block/declarations/width-and-height/height/max-height.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'tap'
2 |
3 | import { createCssDeclarationBlock } from '../../../../../create-css-declaration-block'
4 |
5 | test('max-height not defined in `theme`', function (t) {
6 | t.plan(2)
7 | t.throw(function () {
8 | createCssDeclarationBlock('maxh', {})
9 | })
10 | t.throw(function () {
11 | createCssDeclarationBlock('maxh', { maxHeight: {} })
12 | })
13 | })
14 |
15 | test('default max-height defined in `theme.maxHeight`', function (t) {
16 | t.plan(1)
17 | t.deepEqual(
18 | createCssDeclarationBlock('maxh', {
19 | maxHeight: {
20 | default: '100%'
21 | }
22 | }),
23 | {
24 | breakpoint: null,
25 | className: 'maxh',
26 | declarations: {
27 | 'max-height': '100%'
28 | },
29 | pseudoClass: null,
30 | selector: 'maxh'
31 | }
32 | )
33 | })
34 |
35 | test('custom max-height defined in `theme.maxHeight`', function (t) {
36 | t.plan(1)
37 | t.deepEqual(
38 | createCssDeclarationBlock('maxh-sm', { maxHeight: { sm: '320px' } }),
39 | {
40 | breakpoint: null,
41 | className: 'maxh-sm',
42 | declarations: {
43 | 'max-height': '320px'
44 | },
45 | pseudoClass: null,
46 | selector: 'maxh-sm'
47 | }
48 | )
49 | })
50 |
51 | test('pixel max-height', function (t) {
52 | t.plan(1)
53 | t.deepEqual(createCssDeclarationBlock('maxh-320px', {}), {
54 | breakpoint: null,
55 | className: 'maxh-320px',
56 | declarations: {
57 | 'max-height': '320px'
58 | },
59 | pseudoClass: null,
60 | selector: 'maxh-320px'
61 | })
62 | })
63 |
64 | test('screen max-height', function (t) {
65 | t.plan(1)
66 | t.deepEqual(createCssDeclarationBlock('maxh-screen', {}), {
67 | breakpoint: null,
68 | className: 'maxh-screen',
69 | declarations: {
70 | 'max-height': '100vh'
71 | },
72 | pseudoClass: null,
73 | selector: 'maxh-screen'
74 | })
75 | })
76 |
--------------------------------------------------------------------------------
/src/utilities/create-css/__tests__/create-css-declaration-block/declarations/width-and-height/height/min-height.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'tap'
2 |
3 | import { createCssDeclarationBlock } from '../../../../../create-css-declaration-block'
4 |
5 | test('min-height not defined in `theme`', function (t) {
6 | t.plan(2)
7 | t.throw(function () {
8 | createCssDeclarationBlock('minh', {})
9 | })
10 | t.throw(function () {
11 | createCssDeclarationBlock('minh', { minHeight: {} })
12 | })
13 | })
14 |
15 | test('default min-height defined in `theme.minHeight`', function (t) {
16 | t.plan(1)
17 | t.deepEqual(
18 | createCssDeclarationBlock('minh', {
19 | minHeight: {
20 | default: '100%'
21 | }
22 | }),
23 | {
24 | breakpoint: null,
25 | className: 'minh',
26 | declarations: {
27 | 'min-height': '100%'
28 | },
29 | pseudoClass: null,
30 | selector: 'minh'
31 | }
32 | )
33 | })
34 |
35 | test('custom min-height defined in `theme.minHeight`', function (t) {
36 | t.plan(1)
37 | t.deepEqual(
38 | createCssDeclarationBlock('minh-sm', { minHeight: { sm: '320px' } }),
39 | {
40 | breakpoint: null,
41 | className: 'minh-sm',
42 | declarations: {
43 | 'min-height': '320px'
44 | },
45 | pseudoClass: null,
46 | selector: 'minh-sm'
47 | }
48 | )
49 | })
50 |
51 | test('pixel min-height', function (t) {
52 | t.plan(1)
53 | t.deepEqual(createCssDeclarationBlock('minh-320px', {}), {
54 | breakpoint: null,
55 | className: 'minh-320px',
56 | declarations: {
57 | 'min-height': '320px'
58 | },
59 | pseudoClass: null,
60 | selector: 'minh-320px'
61 | })
62 | })
63 |
64 | test('screen min-height', function (t) {
65 | t.plan(1)
66 | t.deepEqual(createCssDeclarationBlock('minh-screen', {}), {
67 | breakpoint: null,
68 | className: 'minh-screen',
69 | declarations: {
70 | 'min-height': '100vh'
71 | },
72 | pseudoClass: null,
73 | selector: 'minh-screen'
74 | })
75 | })
76 |
--------------------------------------------------------------------------------
/src/utilities/create-css/__tests__/parse-class-name.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'tap'
2 |
3 | import { parseClassName } from '../parse-class-name'
4 |
5 | test('invalid class name', function (t) {
6 | t.plan(2)
7 | t.throw(function () {
8 | parseClassName('', [])
9 | })
10 | t.throw(function () {
11 | parseClassName(':', [])
12 | })
13 | })
14 |
15 | test('plain selector', function (t) {
16 | t.plan(1)
17 | t.deepEqual(parseClassName('block', []), {
18 | breakpoint: null,
19 | pseudoClass: null,
20 | selector: 'block'
21 | })
22 | })
23 |
24 | test('breakpoint not defined', function (t) {
25 | t.plan(1)
26 | t.throw(function () {
27 | parseClassName('sm@block', [])
28 | })
29 | })
30 |
31 | test('breakpoint defined', function (t) {
32 | t.plan(1)
33 | t.deepEqual(parseClassName('sm@block', ['sm']), {
34 | breakpoint: 'sm',
35 | pseudoClass: null,
36 | selector: 'block'
37 | })
38 | })
39 |
40 | test('pseudo-class', function (t) {
41 | t.plan(1)
42 | t.deepEqual(parseClassName('hover:block', []), {
43 | breakpoint: null,
44 | pseudoClass: {
45 | isParent: false,
46 | value: 'hover'
47 | },
48 | selector: 'block'
49 | })
50 | })
51 |
52 | test('group pseudo-class', function (t) {
53 | t.plan(1)
54 | t.deepEqual(parseClassName('group-hover:block', []), {
55 | breakpoint: null,
56 | pseudoClass: {
57 | isParent: true,
58 | value: 'hover'
59 | },
60 | selector: 'block'
61 | })
62 | })
63 |
64 | test('valid breakpoint and pseudo-class', function (t) {
65 | t.plan(1)
66 | t.deepEqual(parseClassName('sm@hover:block', ['sm']), {
67 | breakpoint: 'sm',
68 | pseudoClass: {
69 | isParent: false,
70 | value: 'hover'
71 | },
72 | selector: 'block'
73 | })
74 | })
75 |
76 | test('valid breakpoint and group pseudo-class', function (t) {
77 | t.plan(1)
78 | t.deepEqual(parseClassName('sm@group-hover:block', ['sm']), {
79 | breakpoint: 'sm',
80 | pseudoClass: {
81 | isParent: true,
82 | value: 'hover'
83 | },
84 | selector: 'block'
85 | })
86 | })
87 |
--------------------------------------------------------------------------------
/src/utilities/create-css/plugins/font.ts:
--------------------------------------------------------------------------------
1 | /*
2 | font-family
3 |
4 | `.font` | `font-family: ${theme.fontFamily.default};`
5 | `.font-${key}` | `font-family: ${theme.fontFamily[key]};`
6 | ---
7 | font-size
8 |
9 | `.font-${key}` | `font-size: ${theme.fontSize[key]};`
10 | ---
11 | font-weight
12 |
13 | `.font-${key}` | `font-weight: ${theme.fontWeight[key]};`
14 | */
15 |
16 | import { Plugin, Theme, ThemeKeys } from '../../../types'
17 |
18 | export const font: Plugin = {
19 | createDeclarations: function ({
20 | computeNumericValue,
21 | matches,
22 | theme
23 | }: {
24 | computeNumericValue: (
25 | value: string,
26 | themeKeys: Array
27 | ) => null | string
28 | matches: RegExpMatchArray
29 | theme: Theme
30 | }): { [property: string]: string } {
31 | if (typeof matches[1] === 'undefined') {
32 | if (typeof theme.fontFamily === 'undefined') {
33 | throw new Error('`theme.fontFamily` not defined in configuration')
34 | }
35 | const fontFamily = theme.fontFamily.default
36 | if (typeof fontFamily === 'undefined') {
37 | throw new Error(
38 | '`theme.fontFamily.default` not defined in configuration'
39 | )
40 | }
41 | return {
42 | 'font-family': `${fontFamily}`
43 | }
44 | }
45 | if (typeof theme.fontFamily !== 'undefined') {
46 | const fontFamily = theme.fontFamily[matches[1]]
47 | if (typeof fontFamily !== 'undefined') {
48 | return {
49 | 'font-family': `${fontFamily}`
50 | }
51 | }
52 | }
53 | if (typeof theme.fontWeight !== 'undefined') {
54 | const fontWeight = theme.fontWeight[matches[1]]
55 | if (typeof fontWeight !== 'undefined') {
56 | return {
57 | 'font-weight': `${fontWeight}`
58 | }
59 | }
60 | }
61 | const fontSize = computeNumericValue(matches[1], ['fontSize'])
62 | if (fontSize === null) {
63 | throw new Error(`Invalid font size: ${matches[1]}`)
64 | }
65 | return {
66 | 'font-size': `${fontSize}`
67 | }
68 | },
69 | regex: /^font(?:-(.+))?$/
70 | }
71 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | export interface CliOptions {
2 | appendCssFilesPattern: null | string
3 | configFilePath: null | string
4 | minify: boolean
5 | outputPath: null | string
6 | prependCssFilesPattern: null | string
7 | sourceFilesPattern: string
8 | }
9 |
10 | export interface Config extends CliOptions {
11 | reset: boolean
12 | theme: Theme
13 | }
14 |
15 | export type Theme = {
16 | backgroundColor?: ThemeItem
17 | baseFontSize?: ThemeItem
18 | baseSpace?: string
19 | borderColor?: ThemeItem
20 | borderRadius?: ThemeItem
21 | borderWidth?: ThemeItem
22 | breakpoint?: ThemeItem
23 | color?: ThemeItem
24 | fontFamily?: ThemeItem
25 | fontSize?: ThemeItem
26 | fontWeight?: ThemeItem
27 | height?: ThemeItem
28 | letterSpacing?: ThemeItem
29 | lineHeight?: ThemeItem
30 | margin?: ThemeItem
31 | maxHeight?: ThemeItem
32 | minHeight?: ThemeItem
33 | maxWidth?: ThemeItem
34 | minWidth?: ThemeItem
35 | padding?: ThemeItem
36 | space?: ThemeItem
37 | width?: ThemeItem
38 | }
39 |
40 | export type ThemeItem = {
41 | [key: string]: string
42 | }
43 |
44 | export type ThemeKeys = Exclude
45 |
46 | export interface ParsedClassName {
47 | breakpoint: null | string
48 | pseudoClass: null | PseudoClass
49 | selector: string
50 | }
51 |
52 | export type PseudoClass = {
53 | isParent: boolean
54 | value: string
55 | }
56 |
57 | export type CssDeclarationBlocks = {
58 | breakpoint: null | string
59 | cssDeclarationBlocks: Array
60 | }
61 | export interface CssDeclarationBlock extends ParsedClassName {
62 | className: string
63 | declarations: Declarations
64 | }
65 | export type Declarations = { [property: string]: string }
66 |
67 | export type Plugin = {
68 | regex: RegExp
69 | createDeclarations: (options: {
70 | computeColorValue: (
71 | value: undefined | string,
72 | themeKeys: Array
73 | ) => null | string
74 | computeNumericValue: (
75 | value: undefined | string,
76 | themeKeys: Array
77 | ) => null | string
78 | matches: RegExpMatchArray
79 | theme: Theme
80 | }) => { [property: string]: string }
81 | }
82 |
--------------------------------------------------------------------------------
/src/utilities/read-config-async.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs-extra'
2 | import * as path from 'path'
3 |
4 | import { CliOptions, Config, Theme } from '../types'
5 |
6 | const CONFIG_FILE_NAME = 'generate-css.config.json'
7 | const PACKAGE_JSON_KEY = 'generate-css'
8 |
9 | const defaultConfig = {
10 | reset: true,
11 | theme: {}
12 | }
13 |
14 | export async function readConfigAsync(cliOptions: CliOptions): Promise {
15 | const configFilePath = cliOptions.configFilePath
16 | if (typeof configFilePath === 'string') {
17 | if ((await fs.pathExists(configFilePath)) === false) {
18 | throw new Error(`Configuration file not found: ${configFilePath}`)
19 | }
20 | const config = await readGenerateCssConfig(configFilePath)
21 | return {
22 | ...defaultConfig,
23 | ...cliOptions,
24 | ...config
25 | }
26 | }
27 | const packageJsonConfig = await readPackageJsonConfig()
28 | if (packageJsonConfig !== null) {
29 | return {
30 | ...defaultConfig,
31 | ...cliOptions,
32 | ...packageJsonConfig
33 | }
34 | }
35 | const defaultConfigFilePath = path.join(process.cwd(), CONFIG_FILE_NAME)
36 | if ((await fs.pathExists(defaultConfigFilePath)) === true) {
37 | const config = await readGenerateCssConfig(defaultConfigFilePath)
38 | return {
39 | ...defaultConfig,
40 | ...cliOptions,
41 | ...config
42 | }
43 | }
44 | return {
45 | ...defaultConfig,
46 | ...cliOptions
47 | }
48 | }
49 |
50 | async function readPackageJsonConfig(): Promise {
54 | const packageJsonFilePath = path.join(process.cwd(), 'package.json')
55 | if ((await fs.pathExists(packageJsonFilePath)) === false) {
56 | return null
57 | }
58 | const packageJson = JSON.parse(await fs.readFile(packageJsonFilePath, 'utf8'))
59 | const config = packageJson[PACKAGE_JSON_KEY]
60 | if (typeof config === 'undefined') {
61 | return null
62 | }
63 | return config
64 | }
65 |
66 | async function readGenerateCssConfig(
67 | filePath: string
68 | ): Promise<{
69 | reset: boolean
70 | theme: Theme
71 | }> {
72 | return JSON.parse(await fs.readFile(filePath, 'utf8'))
73 | }
74 |
--------------------------------------------------------------------------------
/src/utilities/create-css/plugins/padding.ts:
--------------------------------------------------------------------------------
1 | /*
2 | padding
3 |
4 | defaultValue = theme.padding.default || theme.space.default
5 | value = theme.padding[key] || theme.space[key] || computeNumericValue(key)
6 |
7 | `.p` | `padding: ${defaultValue};`
8 | `.px` | `padding-left: ${defaultValue};`
`padding-right: ${defaultValue};`
9 | `.py` | `padding-top: ${defaultValue};`
`padding-bottom: ${defaultValue};`
10 | `.pt` | `padding-top: ${defaultValue};`
11 | `.pr` | `padding-right: ${defaultValue};`
12 | `.pb` | `padding-bottom: ${defaultValue};`
13 | `.pl` | `padding-left: ${defaultValue};`
14 | `.px-${key}` | `padding-left: ${value};`
`padding-right: ${value};`
15 | `.py-${key}` | `padding-top: ${value};`
`padding-bottom: ${value};`
16 | `.pt-${key}` | `padding-top: ${value};`
17 | `.pr-${key}` | `padding-right: ${value};`
18 | `.pb-${key}` | `padding-bottom: ${value};`
19 | `.pl-${key}` | `padding-left: ${value};`
20 | */
21 |
22 | import { Plugin, ThemeKeys } from '../../../types'
23 |
24 | export const padding: Plugin = {
25 | createDeclarations: function ({
26 | matches,
27 | computeNumericValue
28 | }: {
29 | matches: RegExpMatchArray
30 | computeNumericValue: (
31 | value: string,
32 | themeKeys: Array
33 | ) => null | string
34 | }): { [property: string]: string } {
35 | const value = computeNumericValue(matches[2], ['padding', 'space'])
36 | if (value === null) {
37 | throw new Error(`Invalid padding: ${matches[2]}`)
38 | }
39 | switch (matches[1]) {
40 | case 'x': {
41 | return {
42 | 'padding-left': value,
43 | 'padding-right': value
44 | }
45 | }
46 | case 'y': {
47 | return {
48 | 'padding-bottom': value,
49 | 'padding-top': value
50 | }
51 | }
52 | case 't': {
53 | return {
54 | 'padding-top': value
55 | }
56 | }
57 | case 'r': {
58 | return {
59 | 'padding-right': value
60 | }
61 | }
62 | case 'b': {
63 | return {
64 | 'padding-bottom': value
65 | }
66 | }
67 | case 'l': {
68 | return {
69 | 'padding-left': value
70 | }
71 | }
72 | }
73 | return {
74 | padding: value
75 | }
76 | },
77 | regex: /^p([xytrbl])?(?:-(.+))?$/
78 | }
79 |
--------------------------------------------------------------------------------
/src/utilities/stringify-css.ts:
--------------------------------------------------------------------------------
1 | import { Config, CssDeclarationBlock, CssDeclarationBlocks } from '../types'
2 |
3 | export function stringifyCss(
4 | css: Array,
5 | config: Config
6 | ): string {
7 | const result = []
8 | for (const { breakpoint, cssDeclarationBlocks } of css) {
9 | if (breakpoint !== null) {
10 | if (typeof config.theme.breakpoint === 'undefined') {
11 | throw new Error('`theme.breakpoint` not defined in configuration')
12 | }
13 | if (typeof config.theme.breakpoint[breakpoint] === 'undefined') {
14 | throw new Error(
15 | `Breakpoint not defined in \`theme.breakpoint\`: ${breakpoint}`
16 | )
17 | }
18 | result.push(
19 | `@media (min-width: ${config.theme.breakpoint[breakpoint]}) {`
20 | )
21 | }
22 | result.push(stringifyCssDeclarationBlocks(cssDeclarationBlocks))
23 | if (breakpoint !== null) {
24 | result.push('}')
25 | }
26 | }
27 | return result.join('')
28 | }
29 |
30 | function stringifyCssDeclarationBlocks(
31 | cssDeclarationBlocks: Array
32 | ): string {
33 | const result = []
34 | for (const { className, declarations, pseudoClass } of cssDeclarationBlocks) {
35 | if (pseudoClass !== null && pseudoClass.isParent === true) {
36 | result.push(`.group${stringifyPseudoClass(pseudoClass.value)} `)
37 | }
38 | result.push('.')
39 | result.push(escapeSpecialCharacters(className))
40 | if (pseudoClass !== null && pseudoClass.isParent === false) {
41 | result.push(stringifyPseudoClass(pseudoClass.value))
42 | }
43 | result.push(`{${stringifyDeclarations(declarations)}}`)
44 | }
45 | return result.join('')
46 | }
47 |
48 | const specialCharactersRegex = /[/:@%]/g
49 |
50 | function escapeSpecialCharacters(string: string) {
51 | return string.replace(specialCharactersRegex, function (match: string) {
52 | return `\\${match}`
53 | })
54 | }
55 |
56 | function stringifyPseudoClass(pseudoClass: string): string {
57 | if (pseudoClass === 'selection') {
58 | return ` ::${pseudoClass}`
59 | }
60 | if (pseudoClass === 'placeholder') {
61 | return `::${pseudoClass}`
62 | }
63 | return `:${pseudoClass}`
64 | }
65 |
66 | function stringifyDeclarations(declarations: { [property: string]: string }) {
67 | const result = []
68 | for (const property of Object.keys(declarations)) {
69 | result.push(`${property}:${declarations[property]}`)
70 | }
71 | return result.join(';')
72 | }
73 |
--------------------------------------------------------------------------------
/src/utilities/create-css/plugins/border-width.ts:
--------------------------------------------------------------------------------
1 | /*
2 | border-width
3 |
4 | - `defaultValue` = `theme.borderWidth.default`
5 | - `value` = `theme.borderWidth[key]` || `resolveNumericValue(key)`
6 |
7 | `.b` | `border-width: ${defaultValue};`
8 | `.bx` | `border-left-width: ${defaultValue};`
`border-right-width: ${defaultValue};`
9 | `.by` | `border-top-width: ${defaultValue};`
`border-bottom-width: ${defaultValue};`
10 | `.bt` | `border-top-width: ${defaultValue};`
11 | `.br` | `border-right-width: ${defaultValue};`
12 | `.bb` | `border-bottom-width: ${defaultValue};`
13 | `.bl` | `border-left-width: ${defaultValue};`
14 | `.b-${key}` | `border-width: ${value};`
15 | `.bx-${key}` | `border-left-width: ${value};`
`border-right-width: ${value};`
16 | `.by-${key}` | `border-top-width: ${value};`
`border-bottom-width: ${value};`
17 | `.bt-${key}` | `border-top-width: ${value};`
18 | `.br-${key}` | `border-right-width: ${value};`
19 | `.bb-${key}` | `border-bottom-width: ${value};`
20 | `.bl-${key}` | `border-left-width: ${value};`
21 | */
22 |
23 | import { Plugin, ThemeKeys } from '../../../types'
24 |
25 | export const borderWidth: Plugin = {
26 | createDeclarations: function ({
27 | matches,
28 | computeNumericValue
29 | }: {
30 | matches: RegExpMatchArray
31 | computeNumericValue: (
32 | value: string,
33 | themeKeys: Array
34 | ) => null | string
35 | }): { [property: string]: string } {
36 | const borderWidth = computeNumericValue(matches[2], ['borderWidth'])
37 | if (borderWidth === null) {
38 | throw new Error(`Invalid border width: ${matches[2]}`)
39 | }
40 | switch (matches[1]) {
41 | case 'x': {
42 | return {
43 | 'border-left-width': borderWidth,
44 | 'border-right-width': borderWidth
45 | }
46 | }
47 | case 'y': {
48 | return {
49 | 'border-bottom-width': borderWidth,
50 | 'border-top-width': borderWidth
51 | }
52 | }
53 | case 't': {
54 | return {
55 | 'border-top-width': borderWidth
56 | }
57 | }
58 | case 'r': {
59 | return {
60 | 'border-right-width': borderWidth
61 | }
62 | }
63 | case 'b': {
64 | return {
65 | 'border-bottom-width': borderWidth
66 | }
67 | }
68 | case 'l': {
69 | return {
70 | 'border-left-width': borderWidth
71 | }
72 | }
73 | }
74 | return {
75 | 'border-width': borderWidth
76 | }
77 | },
78 | regex: /^b([xytrbl])?(?:-(.+))?$/
79 | }
80 |
--------------------------------------------------------------------------------
/src/utilities/create-css/plugins/width-and-height.ts:
--------------------------------------------------------------------------------
1 | /*
2 | height
3 |
4 | `.h` | `height: ${theme.height.default};`
5 | `.h-screen` | `height: 100vh;`
6 | `.h-${key}` | `height: ${theme.height[key] || computeNumericValue(key)};`
7 | `.minh` | `min-height: ${theme.height.default};`
8 | `.minh-screen` | `min-height: 100vh;`
9 | `.minh-${key}` | `min-height: ${theme.height[key] || computeNumericValue(key)};`
10 | `.maxh` | `max-height: ${theme.height.default};`
11 | `.maxh-screen` | `max-height: 100vh;`
12 | `.maxh-${key}` | `max-height: ${theme.height[key] || computeNumericValue(key)};`
13 | ---
14 | width
15 |
16 | `.w` | `width: ${theme.width.default};`
17 | `.w-screen` | `width: 100vw;`
18 | `.w-${key}` | `width: ${theme.width[key] || computeNumericValue(key)};`
19 | `.minw` | `min-width: ${theme.width.default};`
20 | `.minw-screen` | `min-width: 100vw;`
21 | `.minw-${key}` | `min-width: ${theme.width[key] || computeNumericValue(key)};`
22 | `.maxw` | `max-width: ${theme.width.default};`
23 | `.maxw-screen` | `max-width: 100vw;`
24 | `.maxw-${key}` | `max-width: ${theme.width[key] || computeNumericValue(key)};`
25 | */
26 |
27 | import { Plugin, ThemeKeys } from '../../../types'
28 |
29 | export const widthAndHeight: Plugin = {
30 | createDeclarations: function ({
31 | matches,
32 | computeNumericValue
33 | }: {
34 | matches: RegExpMatchArray
35 | computeNumericValue: (
36 | value: string,
37 | themeKeys: Array
38 | ) => null | string
39 | }): { [property: string]: string } {
40 | const prefix = (typeof matches[1] === 'undefined' ? '' : matches[1]) as
41 | | ''
42 | | 'min'
43 | | 'max'
44 | const suffix = matches[2] === 'w' ? 'width' : 'height'
45 | const property = prefix === '' ? suffix : `${prefix}-${suffix}`
46 | if (matches[3] === 'screen') {
47 | return {
48 | [property]: suffix === 'width' ? '100vw' : '100vh'
49 | }
50 | }
51 | const value = computeNumericValue(
52 | matches[3],
53 | resolveProperty(prefix, suffix)
54 | )
55 | if (value === null) {
56 | throw new Error(`Invalid ${property}: ${matches[3]}`)
57 | }
58 | return {
59 | [property]: value
60 | }
61 | },
62 | regex: /^(max|min)?([wh])(?:-(.+))?$/
63 | }
64 |
65 | function resolveProperty(
66 | prefix: '' | 'min' | 'max',
67 | suffix: 'width' | 'height'
68 | ): Array<
69 | 'width' | 'minWidth' | 'maxHeight' | 'height' | 'minHeight' | 'maxWidth'
70 | > {
71 | if (prefix === '') {
72 | return [suffix]
73 | }
74 | if (suffix === 'width') {
75 | return [suffix, prefix === 'min' ? 'minWidth' : 'maxWidth']
76 | }
77 | return [suffix, prefix === 'min' ? 'minHeight' : 'maxHeight']
78 | }
79 |
--------------------------------------------------------------------------------
/src/utilities/create-css/__tests__/create-css-declaration-block/declarations/background-color.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'tap'
2 |
3 | import { createCssDeclarationBlock } from '../../../create-css-declaration-block'
4 |
5 | test('background color not defined in `theme`', function (t) {
6 | t.plan(4)
7 | t.throw(function () {
8 | createCssDeclarationBlock('bg', {})
9 | })
10 | t.throw(function () {
11 | createCssDeclarationBlock('bg-black', {})
12 | })
13 | t.throw(function () {
14 | createCssDeclarationBlock('bg-black', {
15 | backgroundColor: {}
16 | })
17 | })
18 | t.throw(function () {
19 | createCssDeclarationBlock('bg-black', {
20 | color: {}
21 | })
22 | })
23 | })
24 |
25 | test('default background color defined in `theme.backgroundColor`', function (t) {
26 | t.plan(1)
27 | t.deepEqual(
28 | createCssDeclarationBlock('bg', {
29 | backgroundColor: {
30 | default: '#ffffff'
31 | }
32 | }),
33 | {
34 | breakpoint: null,
35 | className: 'bg',
36 | declarations: {
37 | 'background-color': '#ffffff'
38 | },
39 | pseudoClass: null,
40 | selector: 'bg'
41 | }
42 | )
43 | })
44 |
45 | test('default background color defined in `theme.color`', function (t) {
46 | t.plan(1)
47 | t.deepEqual(
48 | createCssDeclarationBlock('bg', {
49 | color: {
50 | default: '#ffffff'
51 | }
52 | }),
53 | {
54 | breakpoint: null,
55 | className: 'bg',
56 | declarations: {
57 | 'background-color': '#ffffff'
58 | },
59 | pseudoClass: null,
60 | selector: 'bg'
61 | }
62 | )
63 | })
64 |
65 | test('custom background color defined in `theme.backgroundColor`', function (t) {
66 | t.plan(1)
67 | t.deepEqual(
68 | createCssDeclarationBlock('bg-black', {
69 | backgroundColor: {
70 | black: '#000000'
71 | }
72 | }),
73 | {
74 | breakpoint: null,
75 | className: 'bg-black',
76 | declarations: {
77 | 'background-color': '#000000'
78 | },
79 | pseudoClass: null,
80 | selector: 'bg-black'
81 | }
82 | )
83 | })
84 |
85 | test('custom background color defined in `theme.color`', function (t) {
86 | t.plan(1)
87 | t.deepEqual(
88 | createCssDeclarationBlock('bg-black', {
89 | color: {
90 | black: '#000000'
91 | }
92 | }),
93 | {
94 | breakpoint: null,
95 | className: 'bg-black',
96 | declarations: {
97 | 'background-color': '#000000'
98 | },
99 | pseudoClass: null,
100 | selector: 'bg-black'
101 | }
102 | )
103 | })
104 |
--------------------------------------------------------------------------------
/src/utilities/create-css/__tests__/create-css-declaration-block/declarations/border-color.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'tap'
2 |
3 | import { createCssDeclarationBlock } from '../../../create-css-declaration-block'
4 |
5 | test('border color not defined in `theme`', function (t) {
6 | t.plan(4)
7 | t.throw(function () {
8 | createCssDeclarationBlock('border', {})
9 | })
10 | t.throw(function () {
11 | createCssDeclarationBlock('border-black', {})
12 | })
13 | t.throw(function () {
14 | createCssDeclarationBlock('border-black', {
15 | borderColor: {}
16 | })
17 | })
18 | t.throw(function () {
19 | createCssDeclarationBlock('border-black', {
20 | color: {}
21 | })
22 | })
23 | })
24 |
25 | test('default border color defined in `theme.borderColor`', function (t) {
26 | t.plan(1)
27 | t.deepEqual(
28 | createCssDeclarationBlock('border', {
29 | borderColor: {
30 | default: '#ffffff'
31 | }
32 | }),
33 | {
34 | breakpoint: null,
35 | className: 'border',
36 | declarations: {
37 | 'border-color': '#ffffff'
38 | },
39 | pseudoClass: null,
40 | selector: 'border'
41 | }
42 | )
43 | })
44 |
45 | test('default border color defined in `theme.color`', function (t) {
46 | t.plan(1)
47 | t.deepEqual(
48 | createCssDeclarationBlock('border', {
49 | color: {
50 | default: '#ffffff'
51 | }
52 | }),
53 | {
54 | breakpoint: null,
55 | className: 'border',
56 | declarations: {
57 | 'border-color': '#ffffff'
58 | },
59 | pseudoClass: null,
60 | selector: 'border'
61 | }
62 | )
63 | })
64 |
65 | test('custom border color defined in `theme.borderColor`', function (t) {
66 | t.plan(1)
67 | t.deepEqual(
68 | createCssDeclarationBlock('border-black', {
69 | borderColor: {
70 | black: '#000000'
71 | }
72 | }),
73 | {
74 | breakpoint: null,
75 | className: 'border-black',
76 | declarations: {
77 | 'border-color': '#000000'
78 | },
79 | pseudoClass: null,
80 | selector: 'border-black'
81 | }
82 | )
83 | })
84 |
85 | test('custom border color defined in `theme.color`', function (t) {
86 | t.plan(1)
87 | t.deepEqual(
88 | createCssDeclarationBlock('border-black', {
89 | color: {
90 | black: '#000000'
91 | }
92 | }),
93 | {
94 | breakpoint: null,
95 | className: 'border-black',
96 | declarations: {
97 | 'border-color': '#000000'
98 | },
99 | pseudoClass: null,
100 | selector: 'border-black'
101 | }
102 | )
103 | })
104 |
--------------------------------------------------------------------------------
/src/generate-css.ts:
--------------------------------------------------------------------------------
1 | import * as csso from 'csso'
2 | import * as findUp from 'find-up'
3 | import * as fs from 'fs-extra'
4 | import * as globby from 'globby'
5 | import * as path from 'path'
6 | import * as prettier from 'prettier'
7 |
8 | import { Config } from './types'
9 | import { createBaseFontSizeCss } from './utilities/create-base-font-size-css'
10 | import { createCss } from './utilities/create-css/create-css'
11 | import { extractClassNamesAsync } from './utilities/extract-class-names-async/extract-class-names-async'
12 | import { stringifyCss } from './utilities/stringify-css'
13 |
14 | export async function generateCssAsync(config: Config): Promise {
15 | const classNames = await extractClassNamesAsync(config.sourceFilesPattern)
16 | const generatedCss = stringifyCss(createCss(classNames, config.theme), config)
17 | const prependCss =
18 | config.prependCssFilesPattern === null
19 | ? ''
20 | : await readFilesAsync(config.prependCssFilesPattern)
21 | const resetCss = config.reset === true ? await readResetCssFilesAsync() : ''
22 | const baseFontSizeCss =
23 | typeof config.theme.baseFontSize === 'undefined'
24 | ? ''
25 | : createBaseFontSizeCss(config)
26 | const appendCss =
27 | config.appendCssFilesPattern === null
28 | ? ''
29 | : await readFilesAsync(config.appendCssFilesPattern)
30 | const css = formatCss(
31 | [prependCss, resetCss, baseFontSizeCss, generatedCss, appendCss].join(''),
32 | config.minify
33 | )
34 | if (config.outputPath === null) {
35 | console.log(css) // eslint-disable-line no-console
36 | return
37 | }
38 | await fs.outputFile(config.outputPath, css)
39 | }
40 |
41 | async function readResetCssFilesAsync(): Promise {
42 | const normalizeCssPath = await findUp(
43 | path.join('node_modules', 'normalize.css', 'normalize.css')
44 | )
45 | if (typeof normalizeCssPath === 'undefined') {
46 | throw new Error('Cannot find normalize.css')
47 | }
48 | const resetCssPath = path.join(__dirname, 'css', 'reset.css')
49 | return [
50 | await fs.readFile(normalizeCssPath, 'utf8'),
51 | await fs.readFile(resetCssPath, 'utf8')
52 | ].join('')
53 | }
54 |
55 | async function readFilesAsync(pattern: string): Promise {
56 | const files = await globby(pattern)
57 | if (files.length === 0) {
58 | throw new Error(`No files matched by pattern: ${pattern}`)
59 | }
60 | const result = []
61 | for (const file of files) {
62 | result.push(await fs.readFile(file, 'utf8'))
63 | }
64 | return result.join('')
65 | }
66 |
67 | function formatCss(css: string, minify: boolean) {
68 | if (minify === true) {
69 | return csso.minify(css, { comments: false, forceMediaMerge: true }).css
70 | }
71 | return prettier.format(css, { parser: 'css' })
72 | }
73 |
--------------------------------------------------------------------------------
/src/utilities/create-css/group-css-declaration-blocks-by-breakpoint.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CssDeclarationBlock,
3 | CssDeclarationBlocks,
4 | ThemeItem
5 | } from '../../types'
6 |
7 | export function groupCssDeclarationBlocksByBreakpoint(
8 | cssDeclarationBlocks: Array,
9 | themeBreakpoints?: ThemeItem
10 | ): Array {
11 | if (cssDeclarationBlocks.length === 0) {
12 | return []
13 | }
14 | const base = []
15 | const breakpoints: { [breakpoint: string]: Array } = {}
16 | for (const cssDeclarationBlock of cssDeclarationBlocks) {
17 | const breakpoint = cssDeclarationBlock.breakpoint
18 | if (breakpoint === null) {
19 | base.push(cssDeclarationBlock)
20 | continue
21 | }
22 | if (typeof breakpoints[breakpoint] === 'undefined') {
23 | breakpoints[breakpoint] = []
24 | }
25 | breakpoints[breakpoint].push(cssDeclarationBlock)
26 | }
27 | const result: Array = []
28 | result.push({
29 | breakpoint: null,
30 | cssDeclarationBlocks: base
31 | })
32 | for (const breakpoint of Object.keys(breakpoints)) {
33 | result.push({
34 | breakpoint,
35 | cssDeclarationBlocks: breakpoints[breakpoint]
36 | })
37 | }
38 | return sortCss(result, themeBreakpoints)
39 | }
40 |
41 | function sortCss(
42 | css: Array,
43 | themeBreakpoints?: ThemeItem
44 | ) {
45 | return css
46 | .slice()
47 | .sort(function (a, b) {
48 | if (a.breakpoint === null) {
49 | return -1
50 | }
51 | if (b.breakpoint === null) {
52 | return 1
53 | }
54 | if (typeof themeBreakpoints !== 'undefined') {
55 | const aBreakpointWidth = themeBreakpoints[a.breakpoint]
56 | const bBreakpointWidth = themeBreakpoints[b.breakpoint]
57 | if (
58 | typeof aBreakpointWidth !== 'undefined' &&
59 | typeof bBreakpointWidth !== 'undefined'
60 | ) {
61 | return parseInt(aBreakpointWidth, 10) - parseInt(bBreakpointWidth, 10)
62 | }
63 | }
64 | return a.breakpoint.localeCompare(b.breakpoint)
65 | })
66 | .map(function ({ breakpoint, cssDeclarationBlocks }: CssDeclarationBlocks) {
67 | return {
68 | breakpoint,
69 | cssDeclarationBlocks: cssDeclarationBlocks
70 | .slice()
71 | .sort(function (x, y): number {
72 | if (x.selector !== y.selector) {
73 | return x.selector.localeCompare(y.selector)
74 | }
75 | if (x.pseudoClass === null) {
76 | return -1
77 | }
78 | if (y.pseudoClass === null) {
79 | return 1
80 | }
81 | return x.pseudoClass.value.localeCompare(y.pseudoClass.value)
82 | })
83 | }
84 | })
85 | }
86 |
--------------------------------------------------------------------------------
/src/utilities/create-css/compute-numeric-value-factory.ts:
--------------------------------------------------------------------------------
1 | import { Theme, ThemeItem, ThemeKeys } from '../../types'
2 |
3 | const minusPrefixRegex = /^(-?)(.+)$/
4 | const pixelValueRegex = /^\d+px$/
5 | const fractionRegex = /^(\d+)\/(\d+)$/
6 | const integerRegex = /^\d+$/
7 |
8 | export function computeNumericValueFactory(
9 | theme: Theme
10 | ): (value: undefined | string, themeKeys: Array) => null | string {
11 | const parsedSpace = parseSpace(theme.baseSpace)
12 | return function (
13 | value = 'default',
14 | themeKeys: Array
15 | ): null | string {
16 | let matches
17 | matches = value.match(minusPrefixRegex) as RegExpMatchArray
18 | const prefix = matches[1] === '-' ? '-' : ''
19 | const parsedValue = matches[2]
20 | for (const themeKey of themeKeys) {
21 | if (
22 | typeof themeKey !== 'undefined' &&
23 | typeof theme[themeKey] !== 'undefined'
24 | ) {
25 | const result = (theme[themeKey] as ThemeItem)[parsedValue]
26 | if (typeof result !== 'undefined') {
27 | return `${prefix}${result}`
28 | }
29 | }
30 | }
31 | switch (parsedValue) {
32 | case 'auto': {
33 | return 'auto'
34 | }
35 | case 'full': {
36 | return `${prefix}100%`
37 | }
38 | case 'px': {
39 | return `${prefix}1px`
40 | }
41 | }
42 | matches = parsedValue.match(pixelValueRegex)
43 | if (matches !== null) {
44 | if (matches[0] === '0px') {
45 | return '0'
46 | }
47 | return `${prefix}${matches[0]}`
48 | }
49 | matches = parsedValue.match(fractionRegex)
50 | if (matches !== null) {
51 | return `${prefix}${formatNumber(
52 | (parseFloat(matches[1]) / parseFloat(matches[2])) * 100
53 | )}%`
54 | }
55 | matches = parsedValue.match(integerRegex)
56 | if (matches !== null) {
57 | if (matches[0] === '0') {
58 | return '0'
59 | }
60 | if (parsedSpace === null) {
61 | throw new Error('`theme.space` not defined in configuration')
62 | }
63 | return `${prefix}${parseFloat(matches[0]) * parsedSpace.value}${
64 | parsedSpace.unit
65 | }`
66 | }
67 | return null
68 | }
69 | }
70 |
71 | const valueAndUnitRegex = /((?:\d*\.)?\d+) ?([A-Za-z]+)/
72 |
73 | function parseSpace(space?: string): null | { value: number; unit: string } {
74 | if (typeof space === 'undefined') {
75 | return null
76 | }
77 | const matches = space.match(valueAndUnitRegex)
78 | if (matches === null) {
79 | throw new Error(`Invalid space: ${space}`)
80 | }
81 | return {
82 | unit: matches[2],
83 | value: parseFloat(matches[1])
84 | }
85 | }
86 |
87 | const zeroesSuffixRegex = /0+$/
88 | const dotSuffixRegex = /\.$/
89 |
90 | function formatNumber(number: number): string {
91 | return number
92 | .toFixed(6)
93 | .replace(zeroesSuffixRegex, '')
94 | .replace(dotSuffixRegex, '')
95 | }
96 |
--------------------------------------------------------------------------------
/src/utilities/extract-class-names-async/__tests__/extract-class-names-from-js.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'tap'
2 |
3 | import { extractClassNamesFromJs } from '../extract-class-names-from-js'
4 |
5 | test('empty string', function (t) {
6 | t.plan(1)
7 | t.deepEqual(extractClassNamesFromJs(''), [])
8 | })
9 |
10 | test('does not match constants without a "CLASS" suffix', function (t) {
11 | t.plan(2)
12 | t.deepEqual(extractClassNamesFromJs('const HIDDEN = "hidden"'), [])
13 | t.deepEqual(extractClassNamesFromJs("const HIDDEN = 'hidden'"), [])
14 | })
15 |
16 | test('matches string constants with a "CLASS" suffix containing a single class', function (t) {
17 | t.plan(5)
18 | t.deepEqual(extractClassNamesFromJs('const DIV_CLASS = "flex"'), ['flex'])
19 | t.deepEqual(extractClassNamesFromJs('const DIV_CLASSNAME = "flex"'), ['flex'])
20 | t.deepEqual(extractClassNamesFromJs('const DIV_CLASSNAMES = "flex"'), [
21 | 'flex'
22 | ])
23 | t.deepEqual(extractClassNamesFromJs('const DIV_CLASS_NAME = "flex"'), [
24 | 'flex'
25 | ])
26 | t.deepEqual(extractClassNamesFromJs('const DIV_CLASS_NAMES = "flex"'), [
27 | 'flex'
28 | ])
29 | })
30 |
31 | test('matches string constants with a "CLASS" suffix containing multiple classes', function (t) {
32 | t.plan(5)
33 | t.deepEqual(extractClassNamesFromJs('const DIV_CLASS = "flex bg-black"'), [
34 | 'bg-black',
35 | 'flex'
36 | ])
37 | t.deepEqual(
38 | extractClassNamesFromJs('const DIV_CLASSNAME = "flex bg-black"'),
39 | ['bg-black', 'flex']
40 | )
41 | t.deepEqual(
42 | extractClassNamesFromJs('const DIV_CLASSNAMES = "flex bg-black"'),
43 | ['bg-black', 'flex']
44 | )
45 | t.deepEqual(
46 | extractClassNamesFromJs('const DIV_CLASS_NAME = "flex bg-black"'),
47 | ['bg-black', 'flex']
48 | )
49 | t.deepEqual(
50 | extractClassNamesFromJs('const DIV_CLASS_NAMES = "flex bg-black"'),
51 | ['bg-black', 'flex']
52 | )
53 | })
54 |
55 | test('multiple matches', function (t) {
56 | t.plan(1)
57 | const js = `
58 | const DIV_CLASS = "flex bg-black"
59 | const H1_CLASS = "color-red"
60 | `
61 | t.deepEqual(extractClassNamesFromJs(js), ['bg-black', 'color-red', 'flex'])
62 | })
63 |
64 | test('var', function (t) {
65 | t.plan(1)
66 | t.deepEqual(extractClassNamesFromJs('var DIV_CLASS = "flex bg-black"'), [
67 | 'bg-black',
68 | 'flex'
69 | ])
70 | })
71 |
72 | test('single-quoted strings', function (t) {
73 | t.plan(1)
74 | t.deepEqual(extractClassNamesFromJs("var DIV_CLASS = 'flex bg-black'"), [
75 | 'bg-black',
76 | 'flex'
77 | ])
78 | })
79 |
80 | test('lowercase', function (t) {
81 | t.plan(1)
82 | t.deepEqual(extractClassNamesFromJs("const div_class = 'flex bg-black'"), [
83 | 'bg-black',
84 | 'flex'
85 | ])
86 | })
87 |
88 | test('leading and trailing consecutive spaces', function (t) {
89 | t.plan(1)
90 | t.deepEqual(
91 | extractClassNamesFromJs("const div_class = ' flex bg-black '"),
92 | ['bg-black', 'flex']
93 | )
94 | })
95 |
--------------------------------------------------------------------------------
/src/utilities/__tests__/read-config-async.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs-extra'
2 | import * as path from 'path'
3 | import { test } from 'tap'
4 |
5 | import { readConfigAsync } from '../read-config-async'
6 |
7 | const defaultCliOptions = {
8 | appendCssFilesPattern: null,
9 | minify: false,
10 | outputPath: null,
11 | prependCssFilesPattern: null,
12 | sourceFilesPattern: '*'
13 | }
14 |
15 | test('no config', async function (t) {
16 | t.plan(3)
17 | process.chdir(path.join(__dirname, 'fixtures', '1-no-config'))
18 | t.false(await fs.pathExists('package.json'))
19 | t.false(await fs.pathExists('generate-css.config.json'))
20 | const config = await readConfigAsync({
21 | ...defaultCliOptions,
22 | configFilePath: null
23 | })
24 | t.deepEqual(config, {
25 | ...config,
26 | reset: true,
27 | theme: {}
28 | })
29 | })
30 |
31 | test('package.json config', async function (t) {
32 | t.plan(3)
33 | process.chdir(path.join(__dirname, 'fixtures', '2-package-json-config'))
34 | t.true(await fs.pathExists('package.json'))
35 | t.false(await fs.pathExists('generate-css.config.json'))
36 | const config = await readConfigAsync({
37 | ...defaultCliOptions,
38 | configFilePath: null
39 | })
40 | t.deepEqual(config, {
41 | ...config,
42 | reset: false,
43 | theme: {
44 | color: {
45 | default: '#ffffff'
46 | }
47 | }
48 | })
49 | })
50 |
51 | test('generate-css.config', async function (t) {
52 | t.plan(3)
53 | process.chdir(path.join(__dirname, 'fixtures', '3-generate-css-config'))
54 | t.false(await fs.pathExists('package.json'))
55 | t.true(await fs.pathExists('generate-css.config.json'))
56 | const config = await readConfigAsync({
57 | ...defaultCliOptions,
58 | configFilePath: null
59 | })
60 | t.deepEqual(config, {
61 | ...config,
62 | reset: false,
63 | theme: {
64 | color: {
65 | default: '#ffffff'
66 | }
67 | }
68 | })
69 | })
70 |
71 | test('custom config', async function (t) {
72 | t.plan(4)
73 | process.chdir(path.join(__dirname, 'fixtures', '4-custom-config'))
74 | t.false(await fs.pathExists('package.json'))
75 | t.false(await fs.pathExists('generate-css.config.json'))
76 | t.true(await fs.pathExists('foo.json'))
77 | const config = await readConfigAsync({
78 | ...defaultCliOptions,
79 | configFilePath: 'foo.json'
80 | })
81 | t.deepEqual(config, {
82 | ...config,
83 | reset: false,
84 | theme: {
85 | color: {
86 | default: '#ffffff'
87 | }
88 | }
89 | })
90 | })
91 |
92 | test('invalid custom config', async function (t) {
93 | t.plan(4)
94 | process.chdir(path.join(__dirname, 'fixtures', '5-invalid-custom-config'))
95 | t.false(await fs.pathExists('package.json'))
96 | t.false(await fs.pathExists('generate-css.config.json'))
97 | t.false(await fs.pathExists('foo.json'))
98 | try {
99 | await readConfigAsync({
100 | ...defaultCliOptions,
101 | configFilePath: 'foo.json'
102 | })
103 | t.fail()
104 | } catch {
105 | t.pass()
106 | }
107 | })
108 |
--------------------------------------------------------------------------------
/src/utilities/create-css/plugins/margin.ts:
--------------------------------------------------------------------------------
1 | /*
2 | margin
3 |
4 | - `defaultValue` = `theme.margin.default` || `theme.space.default`
5 | - `value` = `theme.margin[key]` || `theme.space[key]` || `resolveNumericValue(key)`
6 |
7 | `.m` | `margin: ${defaultValue};`
8 | `.mx` | `margin-left: ${defaultValue};`
`margin-right: ${defaultValue};`
9 | `.my` | `margin-top: ${defaultValue};`
`margin-bottom: ${defaultValue};`
10 | `.mt` | `margin-top: ${defaultValue};`
11 | `.mr` | `margin-right: ${defaultValue};`
12 | `.mb` | `margin-bottom: ${defaultValue};`
13 | `.ml` | `margin-left: ${defaultValue};`
14 | `.mx-${key}` | `margin-left: ${value};`
`margin-right: ${value};`
15 | `.my-${key}` | `margin-top: ${value};`
`margin-bottom: ${value};`
16 | `.mt-${key}` | `margin-top: ${value};`
17 | `.mr-${key}` | `margin-right: ${value};`
18 | `.mb-${key}` | `margin-bottom: ${value};`
19 | `.ml-${key}` | `margin-left: ${value};`
20 | `.-m` | `margin: -${defaultValue};`
21 | `.-mx` | `margin-left: -${defaultValue};`
`margin-right: -${defaultValue};`
22 | `.-my` | `margin-top: -${defaultValue};`
`margin-bottom: -${defaultValue};`
23 | `.-mt` | `margin-top: -${defaultValue};`
24 | `.-mr` | `margin-right: -${defaultValue};`
25 | `.-mb` | `margin-bottom: -${defaultValue};`
26 | `.-ml` | `margin-left: -${defaultValue};`
27 | `.-mx-${key}` | `margin-left: -${value};`
`margin-right: -${value};`
28 | `.-my-${key}` | `margin-top: -${value};`
`margin-bottom: -${value};`
29 | `.-mt-${key}` | `margin-top: -${value};`
30 | `.-mr-${key}` | `margin-right: -${value};`
31 | `.-mb-${key}` | `margin-bottom: -${value};`
32 | `.-ml-${key}` | `margin-left: -${value};`
33 | */
34 |
35 | import { Plugin, ThemeKeys } from '../../../types'
36 |
37 | export const margin: Plugin = {
38 | createDeclarations: function ({
39 | matches,
40 | computeNumericValue
41 | }: {
42 | matches: RegExpMatchArray
43 | computeNumericValue: (
44 | value: string,
45 | themeKeys: Array
46 | ) => null | string
47 | }): { [property: string]: string } {
48 | const value = computeNumericValue(
49 | `${matches[1] === '-' ? '-' : ''}${
50 | typeof matches[3] === 'undefined' ? 'default' : matches[3]
51 | }`,
52 | ['margin', 'space']
53 | )
54 | if (value === null) {
55 | throw new Error(
56 | `Invalid ${matches[1] === '-' ? 'negative ' : ''}margin: ${matches[3]}`
57 | )
58 | }
59 | switch (matches[2]) {
60 | case 'x': {
61 | return {
62 | 'margin-left': value,
63 | 'margin-right': value
64 | }
65 | }
66 | case 'y': {
67 | return {
68 | 'margin-bottom': value,
69 | 'margin-top': value
70 | }
71 | }
72 | case 't': {
73 | return {
74 | 'margin-top': value
75 | }
76 | }
77 | case 'r': {
78 | return {
79 | 'margin-right': value
80 | }
81 | }
82 | case 'b': {
83 | return {
84 | 'margin-bottom': value
85 | }
86 | }
87 | case 'l': {
88 | return {
89 | 'margin-left': value
90 | }
91 | }
92 | }
93 | return {
94 | margin: value
95 | }
96 | },
97 | regex: /^(-?)m([xytrbl])?(?:-(.+))?$/
98 | }
99 |
--------------------------------------------------------------------------------
/src/utilities/create-css/__tests__/create-css-declaration-block/create-css-declaration-block.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'tap'
2 |
3 | import { createCssDeclarationBlock } from '../../create-css-declaration-block'
4 |
5 | test('invalid class name', function (t) {
6 | t.plan(2)
7 | t.throw(function () {
8 | createCssDeclarationBlock('', {})
9 | })
10 | t.throw(function () {
11 | createCssDeclarationBlock(':', {})
12 | })
13 | })
14 |
15 | test('unrecognized class name', function (t) {
16 | t.plan(1)
17 | t.equal(createCssDeclarationBlock('foo', {}), null)
18 | })
19 |
20 | test('plain selector', function (t) {
21 | t.plan(1)
22 | t.deepEqual(createCssDeclarationBlock('block', {}), {
23 | breakpoint: null,
24 | className: 'block',
25 | declarations: {
26 | display: 'block'
27 | },
28 | pseudoClass: null,
29 | selector: 'block'
30 | })
31 | })
32 |
33 | test('breakpoint not defined', function (t) {
34 | t.plan(1)
35 | t.throw(function () {
36 | createCssDeclarationBlock('sm@block', {})
37 | })
38 | })
39 |
40 | test('breakpoint defined', function (t) {
41 | t.plan(1)
42 | t.deepEqual(
43 | createCssDeclarationBlock('sm@block', {
44 | breakpoint: {
45 | sm: '320px'
46 | }
47 | }),
48 | {
49 | breakpoint: 'sm',
50 | className: 'sm@block',
51 | declarations: {
52 | display: 'block'
53 | },
54 | pseudoClass: null,
55 | selector: 'block'
56 | }
57 | )
58 | })
59 |
60 | test('pseudo-class', function (t) {
61 | t.plan(1)
62 | t.deepEqual(createCssDeclarationBlock('hover:block', {}), {
63 | breakpoint: null,
64 | className: 'hover:block',
65 | declarations: {
66 | display: 'block'
67 | },
68 | pseudoClass: { isParent: false, value: 'hover' },
69 | selector: 'block'
70 | })
71 | })
72 |
73 | test('group pseudo-class', function (t) {
74 | t.plan(1)
75 | t.deepEqual(createCssDeclarationBlock('group-hover:block', {}), {
76 | breakpoint: null,
77 | className: 'group-hover:block',
78 | declarations: {
79 | display: 'block'
80 | },
81 | pseudoClass: { isParent: true, value: 'hover' },
82 | selector: 'block'
83 | })
84 | })
85 |
86 | test('valid breakpoint and pseudo-class', function (t) {
87 | t.plan(1)
88 | t.deepEqual(
89 | createCssDeclarationBlock('sm@hover:block', {
90 | breakpoint: {
91 | sm: '320px'
92 | }
93 | }),
94 | {
95 | breakpoint: 'sm',
96 | className: 'sm@hover:block',
97 | declarations: {
98 | display: 'block'
99 | },
100 | pseudoClass: { isParent: false, value: 'hover' },
101 | selector: 'block'
102 | }
103 | )
104 | })
105 |
106 | test('valid breakpoint and group pseudo-class', function (t) {
107 | t.plan(1)
108 | t.deepEqual(
109 | createCssDeclarationBlock('sm@group-hover:block', {
110 | breakpoint: {
111 | sm: '320px'
112 | }
113 | }),
114 | {
115 | breakpoint: 'sm',
116 | className: 'sm@group-hover:block',
117 | declarations: {
118 | display: 'block'
119 | },
120 | pseudoClass: { isParent: true, value: 'hover' },
121 | selector: 'block'
122 | }
123 | )
124 | })
125 |
--------------------------------------------------------------------------------
/scripts/create-css-docs.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs-extra'
2 | import * as globby from 'globby'
3 | import * as path from 'path'
4 |
5 | async function main(): Promise {
6 | const data = [
7 | ...(await readDeclarationsAsync()),
8 | ...(await readPluginsAsync())
9 | ].sort(function (x, y) {
10 | return x.name.localeCompare(y.name)
11 | })
12 | const result = []
13 | result.push('# Functional CSS classes')
14 | for (const { classes, name, value } of data) {
15 | const section = []
16 | section.push(`## ${name}\n\n`)
17 | if (value !== null) {
18 | section.push(`${value}\n\n`)
19 | }
20 | section.push('Class name | CSS rules\n')
21 | section.push(':--|:--\n')
22 | section.push(`${classes.replace(/\|\|/g, '\\|\\|')}`)
23 | result.push(section.join(''))
24 | }
25 | const file = path.join(path.resolve(__dirname, '..'), 'docs', 'css.md')
26 | await fs.outputFile(file, `${result.join('\n\n')}\n`)
27 | }
28 | main()
29 |
30 | async function readDeclarationsAsync(): Promise> {
31 | const directory = path.join(
32 | path.resolve(__dirname, '..'),
33 | 'src',
34 | 'utilities',
35 | 'create-css',
36 | 'declarations'
37 | )
38 | const files = await globby([
39 | path.join(directory, '*.ts'),
40 | `!${path.join(directory, 'index.ts')}`
41 | ])
42 | const result: Array = []
43 | for (const file of files) {
44 | const name = path.basename(file, '.ts')
45 | const classes = Object.values(require(file))[0]
46 | result.push({
47 | classes: stringifyClasses(classes),
48 | name,
49 | value: null
50 | })
51 | }
52 | return result
53 | }
54 |
55 | function stringifyClasses(classes: any): string {
56 | const result = []
57 | for (const className of Object.keys(classes)) {
58 | result.push(
59 | `\`.${className}\` | ${stringifyDeclarations(classes[className])}`
60 | )
61 | }
62 | return result.join('\n')
63 | }
64 |
65 | function stringifyDeclarations(declarations: any): string {
66 | const result = []
67 | for (const property of Object.keys(declarations)) {
68 | result.push(`\`${property}: ${declarations[property]};\``)
69 | }
70 | return result.join('
')
71 | }
72 |
73 | async function readPluginsAsync(): Promise> {
74 | const directory = path.join(
75 | path.resolve(__dirname, '..'),
76 | 'src',
77 | 'utilities',
78 | 'create-css',
79 | 'plugins'
80 | )
81 | const files = await globby([
82 | path.join(directory, '*.ts'),
83 | `!${path.join(directory, 'index.ts')}`
84 | ])
85 | const result: Array = []
86 | for (const file of files) {
87 | const string = await fs.readFile(file, 'utf8')
88 | if (string.indexOf('/*\n') !== 0) {
89 | continue
90 | }
91 | const comment = string.slice(3, string.indexOf('\n*/'))
92 | for (const section of comment.split('\n---\n')) {
93 | const split = section.split('\n\n')
94 | const name = split[0]
95 | if (split.length === 2) {
96 | result.push({
97 | classes: split[1],
98 | name,
99 | value: null
100 | })
101 | continue
102 | }
103 | result.push({
104 | classes: split[2],
105 | name,
106 | value: split[1]
107 | })
108 | }
109 | }
110 | return result
111 | }
112 |
--------------------------------------------------------------------------------
/src/utilities/create-css/__tests__/create-css-declaration-block/declarations/margin/positive-margin.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'tap'
2 |
3 | import { createCssDeclarationBlock } from '../../../../create-css-declaration-block'
4 |
5 | test('margin not defined in `theme`', function (t) {
6 | t.plan(2)
7 | t.throw(function () {
8 | createCssDeclarationBlock('m', {})
9 | })
10 | t.throw(function () {
11 | createCssDeclarationBlock('m', { margin: {} })
12 | })
13 | })
14 |
15 | test('default margin defined in `theme.margin`', function (t) {
16 | t.plan(1)
17 | t.deepEqual(
18 | createCssDeclarationBlock('m', {
19 | margin: {
20 | default: '8px'
21 | }
22 | }),
23 | {
24 | breakpoint: null,
25 | className: 'm',
26 | declarations: {
27 | margin: '8px'
28 | },
29 | pseudoClass: null,
30 | selector: 'm'
31 | }
32 | )
33 | })
34 |
35 | test('custom margin defined in `theme.margin`', function (t) {
36 | t.plan(1)
37 | t.deepEqual(createCssDeclarationBlock('m-sm', { margin: { sm: '4px' } }), {
38 | breakpoint: null,
39 | className: 'm-sm',
40 | declarations: {
41 | margin: '4px'
42 | },
43 | pseudoClass: null,
44 | selector: 'm-sm'
45 | })
46 | })
47 |
48 | test('pixel margin', function (t) {
49 | t.plan(1)
50 | t.deepEqual(createCssDeclarationBlock('m-2px', {}), {
51 | breakpoint: null,
52 | className: 'm-2px',
53 | declarations: {
54 | margin: '2px'
55 | },
56 | pseudoClass: null,
57 | selector: 'm-2px'
58 | })
59 | })
60 |
61 | test('two sides only', function (t) {
62 | t.plan(2)
63 | t.deepEqual(
64 | createCssDeclarationBlock('mx', {
65 | margin: {
66 | default: '8px'
67 | }
68 | }),
69 | {
70 | breakpoint: null,
71 | className: 'mx',
72 | declarations: {
73 | 'margin-left': '8px',
74 | 'margin-right': '8px'
75 | },
76 | pseudoClass: null,
77 | selector: 'mx'
78 | }
79 | )
80 | t.deepEqual(
81 | createCssDeclarationBlock('my-sm', {
82 | margin: {
83 | sm: '4px'
84 | }
85 | }),
86 | {
87 | breakpoint: null,
88 | className: 'my-sm',
89 | declarations: {
90 | 'margin-bottom': '4px',
91 | 'margin-top': '4px'
92 | },
93 | pseudoClass: null,
94 | selector: 'my-sm'
95 | }
96 | )
97 | })
98 |
99 | test('one side only', function (t) {
100 | t.plan(4)
101 | t.deepEqual(createCssDeclarationBlock('mt-2px', {}), {
102 | breakpoint: null,
103 | className: 'mt-2px',
104 | declarations: {
105 | 'margin-top': '2px'
106 | },
107 | pseudoClass: null,
108 | selector: 'mt-2px'
109 | })
110 | t.deepEqual(
111 | createCssDeclarationBlock('mr', {
112 | margin: { default: '8px' }
113 | }),
114 | {
115 | breakpoint: null,
116 | className: 'mr',
117 | declarations: {
118 | 'margin-right': '8px'
119 | },
120 | pseudoClass: null,
121 | selector: 'mr'
122 | }
123 | )
124 | t.deepEqual(createCssDeclarationBlock('mb-sm', { margin: { sm: '4px' } }), {
125 | breakpoint: null,
126 | className: 'mb-sm',
127 | declarations: {
128 | 'margin-bottom': '4px'
129 | },
130 | pseudoClass: null,
131 | selector: 'mb-sm'
132 | })
133 | t.deepEqual(createCssDeclarationBlock('ml-2px', {}), {
134 | breakpoint: null,
135 | className: 'ml-2px',
136 | declarations: {
137 | 'margin-left': '2px'
138 | },
139 | pseudoClass: null,
140 | selector: 'ml-2px'
141 | })
142 | })
143 |
--------------------------------------------------------------------------------
/src/utilities/create-css/__tests__/create-css-declaration-block/declarations/padding.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'tap'
2 |
3 | import { createCssDeclarationBlock } from '../../../create-css-declaration-block'
4 |
5 | test('padding not defined in `theme`', function (t) {
6 | t.plan(2)
7 | t.throw(function () {
8 | createCssDeclarationBlock('p', {})
9 | })
10 | t.throw(function () {
11 | createCssDeclarationBlock('p', { padding: {} })
12 | })
13 | })
14 |
15 | test('default padding defined in `theme.padding`', function (t) {
16 | t.plan(1)
17 | t.deepEqual(
18 | createCssDeclarationBlock('p', {
19 | padding: {
20 | default: '8px'
21 | }
22 | }),
23 | {
24 | breakpoint: null,
25 | className: 'p',
26 | declarations: {
27 | padding: '8px'
28 | },
29 | pseudoClass: null,
30 | selector: 'p'
31 | }
32 | )
33 | })
34 |
35 | test('custom padding defined in `theme.padding`', function (t) {
36 | t.plan(1)
37 | t.deepEqual(createCssDeclarationBlock('p-sm', { padding: { sm: '4px' } }), {
38 | breakpoint: null,
39 | className: 'p-sm',
40 | declarations: {
41 | padding: '4px'
42 | },
43 | pseudoClass: null,
44 | selector: 'p-sm'
45 | })
46 | })
47 |
48 | test('pixel padding', function (t) {
49 | t.plan(1)
50 | t.deepEqual(createCssDeclarationBlock('p-2px', {}), {
51 | breakpoint: null,
52 | className: 'p-2px',
53 | declarations: {
54 | padding: '2px'
55 | },
56 | pseudoClass: null,
57 | selector: 'p-2px'
58 | })
59 | })
60 |
61 | test('two sides only', function (t) {
62 | t.plan(2)
63 | t.deepEqual(
64 | createCssDeclarationBlock('px', {
65 | padding: {
66 | default: '8px'
67 | }
68 | }),
69 | {
70 | breakpoint: null,
71 | className: 'px',
72 | declarations: {
73 | 'padding-left': '8px',
74 | 'padding-right': '8px'
75 | },
76 | pseudoClass: null,
77 | selector: 'px'
78 | }
79 | )
80 | t.deepEqual(
81 | createCssDeclarationBlock('py-sm', {
82 | padding: {
83 | sm: '4px'
84 | }
85 | }),
86 | {
87 | breakpoint: null,
88 | className: 'py-sm',
89 | declarations: {
90 | 'padding-bottom': '4px',
91 | 'padding-top': '4px'
92 | },
93 | pseudoClass: null,
94 | selector: 'py-sm'
95 | }
96 | )
97 | })
98 |
99 | test('one side only', function (t) {
100 | t.plan(4)
101 | t.deepEqual(createCssDeclarationBlock('pt-2px', {}), {
102 | breakpoint: null,
103 | className: 'pt-2px',
104 | declarations: {
105 | 'padding-top': '2px'
106 | },
107 | pseudoClass: null,
108 | selector: 'pt-2px'
109 | })
110 | t.deepEqual(
111 | createCssDeclarationBlock('pr', {
112 | padding: { default: '8px' }
113 | }),
114 | {
115 | breakpoint: null,
116 | className: 'pr',
117 | declarations: {
118 | 'padding-right': '8px'
119 | },
120 | pseudoClass: null,
121 | selector: 'pr'
122 | }
123 | )
124 | t.deepEqual(createCssDeclarationBlock('pb-sm', { padding: { sm: '4px' } }), {
125 | breakpoint: null,
126 | className: 'pb-sm',
127 | declarations: {
128 | 'padding-bottom': '4px'
129 | },
130 | pseudoClass: null,
131 | selector: 'pb-sm'
132 | })
133 | t.deepEqual(createCssDeclarationBlock('pl-2px', {}), {
134 | breakpoint: null,
135 | className: 'pl-2px',
136 | declarations: {
137 | 'padding-left': '2px'
138 | },
139 | pseudoClass: null,
140 | selector: 'pl-2px'
141 | })
142 | })
143 |
--------------------------------------------------------------------------------
/src/utilities/create-css/__tests__/create-css-declaration-block/declarations/margin/negative-margin.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'tap'
2 |
3 | import { createCssDeclarationBlock } from '../../../../create-css-declaration-block'
4 |
5 | test('margin not defined in `theme`', function (t) {
6 | t.plan(2)
7 | t.throw(function () {
8 | createCssDeclarationBlock('-m', {})
9 | })
10 | t.throw(function () {
11 | createCssDeclarationBlock('-m', { margin: {} })
12 | })
13 | })
14 |
15 | test('default margin defined in `theme.margin`', function (t) {
16 | t.plan(1)
17 | t.deepEqual(
18 | createCssDeclarationBlock('-m', {
19 | margin: {
20 | default: '8px'
21 | }
22 | }),
23 | {
24 | breakpoint: null,
25 | className: '-m',
26 | declarations: {
27 | margin: '-8px'
28 | },
29 | pseudoClass: null,
30 | selector: '-m'
31 | }
32 | )
33 | })
34 |
35 | test('custom margin defined in `theme.margin`', function (t) {
36 | t.plan(1)
37 | t.deepEqual(createCssDeclarationBlock('-m-sm', { margin: { sm: '4px' } }), {
38 | breakpoint: null,
39 | className: '-m-sm',
40 | declarations: {
41 | margin: '-4px'
42 | },
43 | pseudoClass: null,
44 | selector: '-m-sm'
45 | })
46 | })
47 |
48 | test('pixel margin', function (t) {
49 | t.plan(1)
50 | t.deepEqual(createCssDeclarationBlock('-m-2px', {}), {
51 | breakpoint: null,
52 | className: '-m-2px',
53 | declarations: {
54 | margin: '-2px'
55 | },
56 | pseudoClass: null,
57 | selector: '-m-2px'
58 | })
59 | })
60 |
61 | test('two sides only', function (t) {
62 | t.plan(2)
63 | t.deepEqual(
64 | createCssDeclarationBlock('-mx', {
65 | margin: {
66 | default: '8px'
67 | }
68 | }),
69 | {
70 | breakpoint: null,
71 | className: '-mx',
72 | declarations: {
73 | 'margin-left': '-8px',
74 | 'margin-right': '-8px'
75 | },
76 | pseudoClass: null,
77 | selector: '-mx'
78 | }
79 | )
80 | t.deepEqual(
81 | createCssDeclarationBlock('-my-sm', {
82 | margin: {
83 | sm: '4px'
84 | }
85 | }),
86 | {
87 | breakpoint: null,
88 | className: '-my-sm',
89 | declarations: {
90 | 'margin-bottom': '-4px',
91 | 'margin-top': '-4px'
92 | },
93 | pseudoClass: null,
94 | selector: '-my-sm'
95 | }
96 | )
97 | })
98 |
99 | test('one side only', function (t) {
100 | t.plan(4)
101 | t.deepEqual(createCssDeclarationBlock('-mt-2px', {}), {
102 | breakpoint: null,
103 | className: '-mt-2px',
104 | declarations: {
105 | 'margin-top': '-2px'
106 | },
107 | pseudoClass: null,
108 | selector: '-mt-2px'
109 | })
110 | t.deepEqual(
111 | createCssDeclarationBlock('-mr', {
112 | margin: { default: '8px' }
113 | }),
114 | {
115 | breakpoint: null,
116 | className: '-mr',
117 | declarations: {
118 | 'margin-right': '-8px'
119 | },
120 | pseudoClass: null,
121 | selector: '-mr'
122 | }
123 | )
124 | t.deepEqual(createCssDeclarationBlock('-mb-sm', { margin: { sm: '4px' } }), {
125 | breakpoint: null,
126 | className: '-mb-sm',
127 | declarations: {
128 | 'margin-bottom': '-4px'
129 | },
130 | pseudoClass: null,
131 | selector: '-mb-sm'
132 | })
133 | t.deepEqual(createCssDeclarationBlock('-ml-2px', {}), {
134 | breakpoint: null,
135 | className: '-ml-2px',
136 | declarations: {
137 | 'margin-left': '-2px'
138 | },
139 | pseudoClass: null,
140 | selector: '-ml-2px'
141 | })
142 | })
143 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "generate-css",
3 | "version": "0.0.10",
4 | "description": "Dynamically generate functional CSS classes from HTML and JavaScript source files",
5 | "keywords": [
6 | "css",
7 | "dynamic-css",
8 | "functional-css",
9 | "generate-css",
10 | "style"
11 | ],
12 | "license": "MIT",
13 | "author": "Yuan Qing Lim",
14 | "repository": {
15 | "type": "git",
16 | "url": "git://github.com/yuanqing/generate-css.git"
17 | },
18 | "files": [
19 | "lib",
20 | "docs",
21 | "example"
22 | ],
23 | "bin": {
24 | "generate-css": "lib/cli.js"
25 | },
26 | "main": "lib/index.js",
27 | "scripts": {
28 | "build": "yarn run clean && concurrently --raw 'yarn run build-css' 'yarn run build-ts'",
29 | "build-css": "cpy 'css/**/*.css' '../lib' --cwd src --parents",
30 | "build-ts": "tsc",
31 | "clean": "rimraf '*.log' .nyc_output coverage example/style.css lib",
32 | "docs": "markdown-interpolate README.md && ts-node scripts/create-css-docs.ts",
33 | "fix": "concurrently --raw 'yarn run fix-css' 'yarn run fix-json' 'yarn run fix-ts'",
34 | "fix-css": "stylelint --fix 'src/**/*.css'",
35 | "fix-json": "prettier --loglevel error --write '*.json'",
36 | "fix-ts": "eslint --fix '{scripts,src}/**/*.ts'",
37 | "lint": "concurrently --raw 'yarn run lint-css' 'yarn run lint-ts'",
38 | "lint-css": "stylelint 'src/**/*.css'",
39 | "lint-ts": "eslint '{scripts,src}/**/*.ts'",
40 | "prepublishOnly": "yarn run clean && yarn run docs && yarn run build",
41 | "reset": "yarn run clean && rimraf node_modules yarn.lock && yarn install",
42 | "start": "ts-node src/cli.ts 'example/*.html' --config example/generate-css.config.json --output example/style.css",
43 | "test": "yarn run clean && tap 'src/**/__tests__/**/*.ts' --coverage-report html --coverage-report text --jobs-auto --no-browser --reporter terse",
44 | "watch": "yarn run clean && concurrently --raw 'yarn run watch-css' 'yarn run watch-ts'",
45 | "watch-css": "chokidar 'src/css/**/*.css' --command 'yarn run build-css' --initial --silent",
46 | "watch-ts": "tsc --preserveWatchOutput --watch"
47 | },
48 | "dependencies": {
49 | "chokidar": "^3.4.3",
50 | "csso": "^4.2.0",
51 | "find-up": "^5.0.0",
52 | "fs-extra": "^9.0.1",
53 | "globby": "^11.0.1",
54 | "kleur": "^4.1.3",
55 | "normalize.css": "^8.0.1",
56 | "prettier": "^2.2.1",
57 | "sade": "^1.7.4"
58 | },
59 | "devDependencies": {
60 | "@types/csso": "^3.5.1",
61 | "@types/fs-extra": "^9.0.6",
62 | "@types/node": "^14.14.16",
63 | "@types/npmlog": "^4.1.2",
64 | "@types/prettier": "^2.1.6",
65 | "@types/sade": "^1.7.2",
66 | "@types/tap": "^14.10.1",
67 | "chokidar-cli": "^2.1.0",
68 | "concurrently": "^5.3.0",
69 | "cpy-cli": "^3.1.1",
70 | "eslint": "^7.16.0",
71 | "eslint-config-yuanqing": "^0.0.4",
72 | "husky": "^4.3.6",
73 | "lint-staged": "^10.5.3",
74 | "markdown-interpolate": "^0.0.7",
75 | "prettier": "^2.2.1",
76 | "rimraf": "^3.0.2",
77 | "stylelint": "^13.8.0",
78 | "stylelint-config-yuanqing": "^0.0.1",
79 | "tap": "^14.11.0",
80 | "ts-node": "^9.1.1",
81 | "typescript": "^4.1.3"
82 | },
83 | "eslintConfig": {
84 | "extends": "eslint-config-yuanqing"
85 | },
86 | "husky": {
87 | "hooks": {
88 | "pre-commit": "lint-staged"
89 | }
90 | },
91 | "lint-staged": {
92 | "*.css": [
93 | "stylelint"
94 | ],
95 | "*.ts": [
96 | "eslint"
97 | ]
98 | },
99 | "prettier": "eslint-config-yuanqing/prettier",
100 | "stylelint": {
101 | "extends": "stylelint-config-yuanqing"
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/utilities/create-css/__tests__/create-css-declaration-block/declarations/border-width.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'tap'
2 |
3 | import { createCssDeclarationBlock } from '../../../create-css-declaration-block'
4 |
5 | test('border width not defined in `theme`', function (t) {
6 | t.plan(2)
7 | t.throw(function () {
8 | createCssDeclarationBlock('b', {})
9 | })
10 | t.throw(function () {
11 | createCssDeclarationBlock('b', { borderWidth: {} })
12 | })
13 | })
14 |
15 | test('default border width defined in `theme.borderWidth`', function (t) {
16 | t.plan(1)
17 | t.deepEqual(
18 | createCssDeclarationBlock('b', {
19 | borderWidth: {
20 | default: '8px'
21 | }
22 | }),
23 | {
24 | breakpoint: null,
25 | className: 'b',
26 | declarations: {
27 | 'border-width': '8px'
28 | },
29 | pseudoClass: null,
30 | selector: 'b'
31 | }
32 | )
33 | })
34 |
35 | test('custom border width defined in `theme.borderWidth`', function (t) {
36 | t.plan(1)
37 | t.deepEqual(
38 | createCssDeclarationBlock('b-sm', { borderWidth: { sm: '4px' } }),
39 | {
40 | breakpoint: null,
41 | className: 'b-sm',
42 | declarations: {
43 | 'border-width': '4px'
44 | },
45 | pseudoClass: null,
46 | selector: 'b-sm'
47 | }
48 | )
49 | })
50 |
51 | test('pixel border width', function (t) {
52 | t.plan(1)
53 | t.deepEqual(createCssDeclarationBlock('b-2px', {}), {
54 | breakpoint: null,
55 | className: 'b-2px',
56 | declarations: {
57 | 'border-width': '2px'
58 | },
59 | pseudoClass: null,
60 | selector: 'b-2px'
61 | })
62 | })
63 |
64 | test('two sides only', function (t) {
65 | t.plan(2)
66 | t.deepEqual(
67 | createCssDeclarationBlock('bx', {
68 | borderWidth: {
69 | default: '8px'
70 | }
71 | }),
72 | {
73 | breakpoint: null,
74 | className: 'bx',
75 | declarations: {
76 | 'border-left-width': '8px',
77 | 'border-right-width': '8px'
78 | },
79 | pseudoClass: null,
80 | selector: 'bx'
81 | }
82 | )
83 | t.deepEqual(
84 | createCssDeclarationBlock('by-sm', {
85 | borderWidth: {
86 | sm: '4px'
87 | }
88 | }),
89 | {
90 | breakpoint: null,
91 | className: 'by-sm',
92 | declarations: {
93 | 'border-bottom-width': '4px',
94 | 'border-top-width': '4px'
95 | },
96 | pseudoClass: null,
97 | selector: 'by-sm'
98 | }
99 | )
100 | })
101 |
102 | test('one side only', function (t) {
103 | t.plan(4)
104 | t.deepEqual(createCssDeclarationBlock('bt-2px', {}), {
105 | breakpoint: null,
106 | className: 'bt-2px',
107 | declarations: {
108 | 'border-top-width': '2px'
109 | },
110 | pseudoClass: null,
111 | selector: 'bt-2px'
112 | })
113 | t.deepEqual(
114 | createCssDeclarationBlock('br', {
115 | borderWidth: { default: '8px' }
116 | }),
117 | {
118 | breakpoint: null,
119 | className: 'br',
120 | declarations: {
121 | 'border-right-width': '8px'
122 | },
123 | pseudoClass: null,
124 | selector: 'br'
125 | }
126 | )
127 | t.deepEqual(
128 | createCssDeclarationBlock('bb-sm', { borderWidth: { sm: '4px' } }),
129 | {
130 | breakpoint: null,
131 | className: 'bb-sm',
132 | declarations: {
133 | 'border-bottom-width': '4px'
134 | },
135 | pseudoClass: null,
136 | selector: 'bb-sm'
137 | }
138 | )
139 | t.deepEqual(createCssDeclarationBlock('bl-2px', {}), {
140 | breakpoint: null,
141 | className: 'bl-2px',
142 | declarations: {
143 | 'border-left-width': '2px'
144 | },
145 | pseudoClass: null,
146 | selector: 'bl-2px'
147 | })
148 | })
149 |
--------------------------------------------------------------------------------
/src/utilities/create-css/__tests__/compute-numeric-value-factory.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'tap'
2 |
3 | import { computeNumericValueFactory } from '../compute-numeric-value-factory'
4 |
5 | test('invalid `theme.space`', function (t) {
6 | t.plan(1)
7 | t.throw(function () {
8 | computeNumericValueFactory({ baseSpace: '' })
9 | })
10 | })
11 |
12 | test('invalid `value`', function (t) {
13 | t.plan(2)
14 | const computeNumericValue = computeNumericValueFactory({
15 | baseSpace: '0.5rem'
16 | })
17 | t.equal(computeNumericValue('foo', []), null)
18 | t.equal(computeNumericValue('-foo', []), null)
19 | })
20 |
21 | test('theme `value`, single `themeKey`', function (t) {
22 | t.plan(4)
23 | const computeNumericValue = computeNumericValueFactory({
24 | breakpoint: { sm: '320px' },
25 | padding: { sm: '8px' }
26 | })
27 | t.equal(computeNumericValue('sm', ['breakpoint']), '320px')
28 | t.equal(computeNumericValue('-sm', ['breakpoint']), '-320px')
29 | t.equal(computeNumericValue('sm', ['padding']), '8px')
30 | t.equal(computeNumericValue('-sm', ['padding']), '-8px')
31 | })
32 |
33 | test('theme `value`, multiple `themeKey`', function (t) {
34 | t.plan(4)
35 | const computeNumericValue = computeNumericValueFactory({
36 | breakpoint: { sm: '320px' },
37 | padding: { sm: '8px' }
38 | })
39 | t.equal(computeNumericValue('sm', ['breakpoint', 'padding']), '320px')
40 | t.equal(computeNumericValue('-sm', ['breakpoint', 'padding']), '-320px')
41 | t.equal(computeNumericValue('sm', ['padding', 'breakpoint']), '8px')
42 | t.equal(computeNumericValue('-sm', ['padding', 'breakpoint']), '-8px')
43 | })
44 |
45 | test('auto `value`', function (t) {
46 | t.plan(1)
47 | const computeNumericValue = computeNumericValueFactory({
48 | baseSpace: '0.5rem'
49 | })
50 | t.equal(computeNumericValue('auto', []), 'auto')
51 | })
52 |
53 | test('full `value`', function (t) {
54 | t.plan(2)
55 | const computeNumericValue = computeNumericValueFactory({
56 | baseSpace: '0.5rem'
57 | })
58 | t.equal(computeNumericValue('full', []), '100%')
59 | t.equal(computeNumericValue('-full', []), '-100%')
60 | })
61 |
62 | test('pixel `value`', function (t) {
63 | t.plan(6)
64 | const computeNumericValue = computeNumericValueFactory({
65 | baseSpace: '0.5rem'
66 | })
67 | t.equal(computeNumericValue('0px', []), '0')
68 | t.equal(computeNumericValue('-0px', []), '0')
69 | t.equal(computeNumericValue('1px', []), '1px')
70 | t.equal(computeNumericValue('-1px', []), '-1px')
71 | t.equal(computeNumericValue('px', []), '1px')
72 | t.equal(computeNumericValue('-px', []), '-1px')
73 | })
74 |
75 | test('fraction `value`', function (t) {
76 | t.plan(6)
77 | const computeNumericValue = computeNumericValueFactory({
78 | baseSpace: '0.5rem'
79 | })
80 | t.equal(computeNumericValue('1/2', []), '50%')
81 | t.equal(computeNumericValue('-1/2', []), '-50%')
82 | t.equal(computeNumericValue('1/3', []), '33.333333%')
83 | t.equal(computeNumericValue('-1/3', []), '-33.333333%')
84 | t.equal(computeNumericValue('2/3', []), '66.666667%')
85 | t.equal(computeNumericValue('-2/3', []), '-66.666667%')
86 | })
87 |
88 | test('numeric `value`, with `theme.space` not defined', function (t) {
89 | t.plan(2)
90 | const computeNumericValue = computeNumericValueFactory({})
91 | t.throw(function () {
92 | computeNumericValue('1', [])
93 | })
94 | t.throw(function () {
95 | computeNumericValue('-1', [])
96 | })
97 | })
98 |
99 | test('numeric `value`, with `theme.space` defined', function (t) {
100 | t.plan(6)
101 | const computeNumericValue = computeNumericValueFactory({
102 | baseSpace: '0.5rem'
103 | })
104 | t.equal(computeNumericValue('0', []), '0')
105 | t.equal(computeNumericValue('-0', []), '0')
106 | t.equal(computeNumericValue('1', []), '0.5rem')
107 | t.equal(computeNumericValue('-1', []), '-0.5rem')
108 | t.equal(computeNumericValue('3', []), '1.5rem')
109 | t.equal(computeNumericValue('-3', []), '-1.5rem')
110 | })
111 |
--------------------------------------------------------------------------------
/src/utilities/create-css/plugins/border-radius.ts:
--------------------------------------------------------------------------------
1 | /*
2 | border-radius
3 |
4 | - `defaultValue` = `theme.borderRadius.default`
5 | - `value` = `theme.borderRadius[key]` || `resolveNumericValue(key)`
6 |
7 | `.rounded` | `border-radius: ${defaultValue};`
8 | `.rounded-t` | `border-top-left-radius: ${defaultValue};`
`border-top-right-radius: ${defaultValue};`
9 | `.rounded-r` | `border-top-right-radius: ${defaultValue};`
`border-bottom-right-radius: ${defaultValue};`
10 | `.rounded-b` | `border-bottom-left-radius: ${defaultValue};`
`border-bottom-right-radius: ${defaultValue};`
11 | `.rounded-l` | `border-top-left-radius: ${defaultValue};`
`border-bottom-left-radius: ${defaultValue};`
12 | `.rounded-tl` | `border-top-left-radius: ${defaultValue};`
13 | `.rounded-tr` | `border-top-right-radius: ${defaultValue};`
14 | `.rounded-bl` | `border-bottom-left-radius: ${defaultValue};`
15 | `.rounded-br` | `border-bottom-right-radius: ${defaultValue};`
16 | `.rounded-full` | `border-radius: 9999px;`
17 | `.rounded-t-full` | `border-top-left-radius: 9999px;`
`border-top-right-radius: 9999px;`
18 | `.rounded-r-full` | `border-top-right-radius: 9999px;`
`border-bottom-right-radius: 9999px;`
19 | `.rounded-b-full` | `border-bottom-left-radius: 9999px;`
`border-bottom-right-radius: 9999px;`
20 | `.rounded-l-full` | `border-top-left-radius: 9999px;`
`border-bottom-left-radius: 9999px;`
21 | `.rounded-tl-full` | `border-top-left-radius: 9999px;`
22 | `.rounded-tr-full` | `border-top-right-radius: 9999px;`
23 | `.rounded-bl-full` | `border-bottom-left-radius: 9999px;`
24 | `.rounded-br-full` | `border-bottom-right-radius: 9999px;`
25 | `.rounded-${key}` | `border-radius: ${value};`
26 | `.rounded-t-${key}` | `border-top-left-radius: ${value};`
`border-top-right-radius: ${value};`
27 | `.rounded-r-${key}` | `border-top-right-radius: ${value};`
`border-bottom-right-radius: ${value};`
28 | `.rounded-b-${key}` | `border-bottom-left-radius: ${value};`
`border-bottom-right-radius: ${value};`
29 | `.rounded-l-${key}` | `border-top-left-radius: ${value};`
`border-bottom-left-radius: ${value};`
30 | `.rounded-tl-${key}` | `border-top-left-radius: ${value};`
31 | `.rounded-tr-${key}` | `border-top-right-radius: ${value};`
32 | `.rounded-bl-${key}` | `border-bottom-left-radius: ${value};`
33 | `.rounded-br-${key}` | `border-bottom-right-radius: ${value};`
34 | */
35 |
36 | import { Plugin, ThemeKeys } from '../../../types'
37 |
38 | export const borderRadius: Plugin = {
39 | createDeclarations: function ({
40 | matches,
41 | computeNumericValue
42 | }: {
43 | matches: RegExpMatchArray
44 | computeNumericValue: (
45 | value: string,
46 | themeKeys: Array
47 | ) => null | string
48 | }): { [property: string]: string } {
49 | const borderRadius =
50 | matches[2] === 'full'
51 | ? '9999px'
52 | : computeNumericValue(matches[2], ['borderRadius'])
53 | if (borderRadius === null) {
54 | throw new Error(`Invalid border radius: ${matches[2]}`)
55 | }
56 | switch (matches[1]) {
57 | case 't': {
58 | return {
59 | 'border-top-left-radius': borderRadius,
60 | 'border-top-right-radius': borderRadius
61 | }
62 | }
63 | case 'r': {
64 | return {
65 | 'border-bottom-right-radius': borderRadius,
66 | 'border-top-right-radius': borderRadius
67 | }
68 | }
69 | case 'b': {
70 | return {
71 | 'border-bottom-left-radius': borderRadius,
72 | 'border-bottom-right-radius': borderRadius
73 | }
74 | }
75 | case 'l': {
76 | return {
77 | 'border-bottom-left-radius': borderRadius,
78 | 'border-top-left-radius': borderRadius
79 | }
80 | }
81 | case 'tl': {
82 | return {
83 | 'border-top-left-radius': borderRadius
84 | }
85 | }
86 | case 'tr': {
87 | return {
88 | 'border-top-right-radius': borderRadius
89 | }
90 | }
91 | case 'bl': {
92 | return {
93 | 'border-bottom-left-radius': borderRadius
94 | }
95 | }
96 | case 'br': {
97 | return {
98 | 'border-bottom-right-radius': borderRadius
99 | }
100 | }
101 | }
102 | return {
103 | 'border-radius': borderRadius
104 | }
105 | },
106 | regex: /^rounded(?:-([trbl]|[tb][lr]))?(?:-(.+))?$/
107 | }
108 |
--------------------------------------------------------------------------------
/src/utilities/create-css/__tests__/create-css-declaration-block/declarations/border-radius.ts:
--------------------------------------------------------------------------------
1 | import { test } from 'tap'
2 |
3 | import { createCssDeclarationBlock } from '../../../create-css-declaration-block'
4 |
5 | test('border radius not defined in `theme`', function (t) {
6 | t.plan(2)
7 | t.throw(function () {
8 | createCssDeclarationBlock('rounded', {})
9 | })
10 | t.throw(function () {
11 | createCssDeclarationBlock('rounded', { borderRadius: {} })
12 | })
13 | })
14 |
15 | test('full border radius', function (t) {
16 | t.plan(1)
17 | t.deepEqual(createCssDeclarationBlock('rounded-full', {}), {
18 | breakpoint: null,
19 | className: 'rounded-full',
20 | declarations: {
21 | 'border-radius': '9999px'
22 | },
23 | pseudoClass: null,
24 | selector: 'rounded-full'
25 | })
26 | })
27 |
28 | test('default border radius defined in `theme.borderRadius`', function (t) {
29 | t.plan(1)
30 | t.deepEqual(
31 | createCssDeclarationBlock('rounded', {
32 | borderRadius: {
33 | default: '8px'
34 | }
35 | }),
36 | {
37 | breakpoint: null,
38 | className: 'rounded',
39 | declarations: {
40 | 'border-radius': '8px'
41 | },
42 | pseudoClass: null,
43 | selector: 'rounded'
44 | }
45 | )
46 | })
47 |
48 | test('custom border radius defined in `theme.borderRadius`', function (t) {
49 | t.plan(1)
50 | t.deepEqual(
51 | createCssDeclarationBlock('rounded-sm', { borderRadius: { sm: '4px' } }),
52 | {
53 | breakpoint: null,
54 | className: 'rounded-sm',
55 | declarations: {
56 | 'border-radius': '4px'
57 | },
58 | pseudoClass: null,
59 | selector: 'rounded-sm'
60 | }
61 | )
62 | })
63 |
64 | test('pixel border radius', function (t) {
65 | t.plan(1)
66 | t.deepEqual(createCssDeclarationBlock('rounded-2px', {}), {
67 | breakpoint: null,
68 | className: 'rounded-2px',
69 | declarations: {
70 | 'border-radius': '2px'
71 | },
72 | pseudoClass: null,
73 | selector: 'rounded-2px'
74 | })
75 | })
76 |
77 | test('two corners only', function (t) {
78 | t.plan(4)
79 | t.deepEqual(createCssDeclarationBlock('rounded-t-full', {}), {
80 | breakpoint: null,
81 | className: 'rounded-t-full',
82 | declarations: {
83 | 'border-top-left-radius': '9999px',
84 | 'border-top-right-radius': '9999px'
85 | },
86 | pseudoClass: null,
87 | selector: 'rounded-t-full'
88 | })
89 | t.deepEqual(
90 | createCssDeclarationBlock('rounded-r', {
91 | borderRadius: { default: '8px' }
92 | }),
93 | {
94 | breakpoint: null,
95 | className: 'rounded-r',
96 | declarations: {
97 | 'border-bottom-right-radius': '8px',
98 | 'border-top-right-radius': '8px'
99 | },
100 | pseudoClass: null,
101 | selector: 'rounded-r'
102 | }
103 | )
104 | t.deepEqual(
105 | createCssDeclarationBlock('rounded-b-sm', { borderRadius: { sm: '4px' } }),
106 | {
107 | breakpoint: null,
108 | className: 'rounded-b-sm',
109 | declarations: {
110 | 'border-bottom-left-radius': '4px',
111 | 'border-bottom-right-radius': '4px'
112 | },
113 | pseudoClass: null,
114 | selector: 'rounded-b-sm'
115 | }
116 | )
117 | t.deepEqual(createCssDeclarationBlock('rounded-l-2px', {}), {
118 | breakpoint: null,
119 | className: 'rounded-l-2px',
120 | declarations: {
121 | 'border-bottom-left-radius': '2px',
122 | 'border-top-left-radius': '2px'
123 | },
124 | pseudoClass: null,
125 | selector: 'rounded-l-2px'
126 | })
127 | })
128 |
129 | test('one corner only', function (t) {
130 | t.plan(4)
131 | t.deepEqual(createCssDeclarationBlock('rounded-tl-full', {}), {
132 | breakpoint: null,
133 | className: 'rounded-tl-full',
134 | declarations: {
135 | 'border-top-left-radius': '9999px'
136 | },
137 | pseudoClass: null,
138 | selector: 'rounded-tl-full'
139 | })
140 | t.deepEqual(
141 | createCssDeclarationBlock('rounded-tr', {
142 | borderRadius: { default: '8px' }
143 | }),
144 | {
145 | breakpoint: null,
146 | className: 'rounded-tr',
147 | declarations: {
148 | 'border-top-right-radius': '8px'
149 | },
150 | pseudoClass: null,
151 | selector: 'rounded-tr'
152 | }
153 | )
154 | t.deepEqual(
155 | createCssDeclarationBlock('rounded-bl-sm', { borderRadius: { sm: '4px' } }),
156 | {
157 | breakpoint: null,
158 | className: 'rounded-bl-sm',
159 | declarations: {
160 | 'border-bottom-left-radius': '4px'
161 | },
162 | pseudoClass: null,
163 | selector: 'rounded-bl-sm'
164 | }
165 | )
166 | t.deepEqual(createCssDeclarationBlock('rounded-br-2px', {}), {
167 | breakpoint: null,
168 | className: 'rounded-br-2px',
169 | declarations: {
170 | 'border-bottom-right-radius': '2px'
171 | },
172 | pseudoClass: null,
173 | selector: 'rounded-br-2px'
174 | })
175 | })
176 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Generate CSS [](https://www.npmjs.com/package/generate-css) [](https://github.com/yuanqing/generate-css/actions?query=workflow%3Abuild) 
2 |
3 | > Dynamically generate functional CSS classes from HTML and JavaScript source files
4 |
5 | ## Features
6 |
7 | - Style your HTML using functional CSS classes, with support for applying styles specific to a pseudo-class (eg. `hover:bg-black`, `parent-hover:bg-black`) and/or specific to a breakpoint (eg. `sm@bg-black`)
8 | - Guarantees zero unused CSS; CSS classes are only included in the generated CSS file if they are actually used
9 |
10 | ## Example
11 |
12 | Given the following `example.html` file:
13 |
14 |
15 | ```html
16 |
17 |
18 | ```
19 |
20 |
21 | …and the following `generate-css.config.json` configuration file:
22 |
23 |
24 | ```json
25 | {
26 | "reset": true,
27 | "theme": {
28 | "baseFontSize": {
29 | "default": "16px"
30 | },
31 | "baseSpace": "1rem",
32 | "color": {
33 | "black": "#000",
34 | "blue": "#00f",
35 | "white": "#fff"
36 | },
37 | "fontFamily": {
38 | "default": "Helvetica, Arial, sans-serif"
39 | },
40 | "fontWeight": {
41 | "bold": "bolder"
42 | }
43 | }
44 | }
45 | ```
46 |
47 |
48 | Do:
49 |
50 | ```
51 | $ npx generate-css example.html --output style.css
52 | ```
53 |
54 | This will generate the following `style.css` file (with the opening reset rules omitted):
55 |
56 | ```
57 | // ...reset rules...
58 |
59 | html {
60 | font-size: 16px;
61 | }
62 | .hover\:bg-black:hover {
63 | background-color: #000;
64 | }
65 | .bg-blue {
66 | background-color: #00f;
67 | }
68 | .color-white {
69 | color: #fff;
70 | }
71 | .font {
72 | font-family: Helvetica, Arial, sans-serif;
73 | }
74 | .font-bold {
75 | font-weight: bolder;
76 | }
77 | .px-2 {
78 | padding-right: 2rem;
79 | padding-left: 2rem;
80 | }
81 | .py-1 {
82 | padding-top: 1rem;
83 | padding-bottom: 1rem;
84 | }
85 | .rounded-full {
86 | border-radius: 9999px;
87 | }
88 | ```
89 |
90 | See that:
91 |
92 | - With `theme.baseFontSize.default` set to `16px`, `font-size: 16px;` is applied on `html`.
93 | - With `theme.baseSpace` set to `1rem`, the padding value of `.px-2` is `2rem` (ie. `1rem` × `2`), and the padding value of `.py-1` is `1rem` (ie. `1rem` × `2`).
94 |
95 | ## Usage
96 |
97 | ### Functional CSS classes
98 |
99 | See the [full list of functional CSS classes](/docs/css.md#readme) currently supported by Generate CSS.
100 |
101 | There are two “types” of functional CSS classes:
102 |
103 | #### Classes *without* a `${key}`
104 |
105 | For these classes, the value used in the generated CSS would generally be resolved from `theme[propertyName].default`.
106 |
107 | #### Classes *with* a `${key}`
108 |
109 | For these classes, the value used in the generated CSS would generally be resolved from `theme[propertyName][key]`.
110 |
111 | For certain CSS classes, if `theme[propertyName][key]` is `undefined`, the value used in the generated CSS might then be resolved through the following mapping:
112 |
113 | `key` | `resolveNumericValue(key)` | Example
114 | :--|:--|:--
115 | `auto` | `auto` | `w-auto` → `width: auto;`
116 | `full` | `100%` | `w-full` → `width: 100%;`
117 | `px` | `1px` | `w-px` → `width: 1px;`
118 | `([0-9]+)px` | `($1)px` | `w-8px` → `width: 8px;`
119 | `([0-9]+)/([0-9]+)` | `($1 / $2 * 100)%` | `w-2/3` → `width: 66.666667%;`
120 | `([0-9]+)` | `theme.baseSpace` × `($1)` | `w-2` → `width: 2rem;`
(assuming `theme.baseSpace` is set to `1rem`)
121 |
122 | ### Pseudo-classes
123 |
124 | To apply a style on an element for a particular pseudo-class state only, add the pseudo-class keyword followed by a `:` character (eg. `hover:`) *before* the functional CSS class name.
125 |
126 | For example, using the class `hover:bg-black` in your HTML would result in the following CSS being generated:
127 |
128 | ```
129 | .hover\:bg-black:hover {
130 | background-color: #000;
131 | }
132 | ```
133 |
134 | ### Parent pseudo-classes
135 |
136 | To apply a style on an element for a particular *parent* pseudo-class state only, add the special parent pseudo-class keyword followed by a `:` character (eg. `parent-hover:`) *before* the functional CSS class name.
137 |
138 | For example, using the class `parent-hover:bg-black` in your HTML would result in the following CSS being generated:
139 |
140 | ```
141 | .parent:hover .parent-hover\:bg-black {
142 | background-color: #000;
143 | }
144 | ```
145 |
146 | ### Breakpoints
147 |
148 | Breakpoints must first be defined under the `theme.breakpoint` key in `generate-css.config.json`:
149 |
150 | ```json
151 | {
152 | "theme": {
153 | "breakpoint": {
154 | "sm": "540px",
155 | "md": "960px"
156 | }
157 | }
158 | }
159 | ```
160 |
161 | To apply a style on an element for a particular breakpoint and higher (the media-query is a `min-width` on the particular breakpoint), add the name of the breakpoint followed by an `@` character (eg. `sm@`) *before* the functional CSS class name.
162 |
163 | For example, using the class `sm@bg-black` in your HTML would result in the following CSS being generated:
164 |
165 | ```
166 | @media (min-width: 540px) {
167 | .sm\@bg-black {
168 | background-color: #000;
169 | }
170 | }
171 | ```
172 |
173 | ### Configuration
174 |
175 | Configure Generate CSS via a `generate-css.config.json` file or a `generate-css` key in your `package.json`.
176 |
177 | #### `"reset"`
178 |
179 | (*`boolean`*)
180 |
181 | Whether to prepend reset rules from [`normalize.css`](https://github.com/necolas/normalize.css/blob/master/normalize.css) and Generate CSS’s [`reset.css` file](https://github.com/yuanqing/generate-css/blob/master/src/css/reset.css) to the generated CSS file. Defaults to `true`.
182 |
183 | #### `"theme"`
184 |
185 | (*`object`*)
186 |
187 | All keys are optional.
188 |
189 | - `theme.baseSpace` (*`string`*) — The base space unit used to resolve functional class names with a `${key}`, eg. `w-2` → `width: 2rem;` (assuming `theme.baseSpace` is set to `1rem`)
190 | - `theme.baseFontSize` (*`object`*) — A mapping of breakpoint names to the base `font-size` to be applied on the `html` element for that breakpoint. `theme.baseFontSize.default` is the base `font-size` applied on the `html` element.
191 | - `theme.breakpoint` (*`object`*) — A mapping of breakpoint names to screen widths.
192 |
193 | All other keys are objects that map `${keys}` referenced in your functional CSS classes to their corresponding values:
194 |
195 |
196 | - `theme.backgroundColor`
197 | - `theme.borderColor`
198 | - `theme.borderRadius`
199 | - `theme.borderWidth`
200 | - `theme.color`
201 | - `theme.fontFamily`
202 | - `theme.fontSize`
203 | - `theme.fontWeight`
204 | - `theme.height`
205 | - `theme.letterSpacing`
206 | - `theme.lineHeight`
207 | - `theme.margin`
208 | - `theme.maxHeight`
209 | - `theme.maxWidth`
210 | - `theme.minHeight`
211 | - `theme.minWidth`
212 | - `theme.padding`
213 | - `theme.space`
214 | - `theme.width`
215 |
216 |
217 | ## CLI
218 |
219 |
220 | ```
221 |
222 | Description
223 | Dynamically generate functional CSS classes from HTML and JavaScript source files
224 |
225 | Usage
226 | $ generate-css [options]
227 |
228 | Options
229 | -a, --append Glob pattern for CSS files to append to the generated CSS file
230 | -c, --config Path to a `generate-css` configuration file
231 | -m, --minify Whether to minify the generated CSS file (default false)
232 | -o, --output Path to write the generated CSS file
233 | -p, --prepend Glob pattern for CSS files to prepend to the generated CSS file
234 | -w, --watch Whether to automatically generate a CSS file on changes to the source files (default false)
235 | -v, --version Displays current version
236 | -h, --help Displays this message
237 |
238 | Examples
239 | $ generate-css --append reset.css
240 | $ generate-css --minify
241 | $ generate-css --output style.css
242 | $ generate-css --prepend custom.css
243 | $ generate-css --watch
244 |
245 | ```
246 |
247 |
248 | ## Prior art
249 |
250 | - The functional CSS class naming convention used in Generate CSS is heavily inspired by [Tailwind](https://tailwindcss.com/) and [Tachyons](https://tachyons.io/).
251 |
252 | ## License
253 |
254 | [MIT](/LICENSE.md)
255 |
--------------------------------------------------------------------------------
/docs/css.md:
--------------------------------------------------------------------------------
1 | # Functional CSS classes
2 |
3 | ## align-items
4 |
5 | Class name | CSS rules
6 | :--|:--
7 | `.items-baseline` | `align-items: baseline;`
8 | `.items-center` | `align-items: center;`
9 | `.items-end` | `align-items: flex-end;`
10 | `.items-start` | `align-items: flex-start;`
11 | `.items-stretch` | `align-items: stretch;`
12 |
13 | ## background-color
14 |
15 | Class name | CSS rules
16 | :--|:--
17 | `.bg` | `background-color: ${theme.backgroundColor.default \|\| theme.color.default};`
18 | `.bg-${key}` | `background-color: ${theme.backgroundColor[key] \|\| theme.color[key]};`
19 |
20 | ## border-color
21 |
22 | Class name | CSS rules
23 | :--|:--
24 | `.border` | `border-color: ${theme.borderColor.default \|\| theme.color.default};`
25 | `.border-${key}` | `border-color: ${theme.borderColor[key] \|\| theme.color[key]};`
26 |
27 | ## border-radius
28 |
29 | - `defaultValue` = `theme.borderRadius.default`
30 | - `value` = `theme.borderRadius[key]` || `resolveNumericValue(key)`
31 |
32 | Class name | CSS rules
33 | :--|:--
34 | `.rounded` | `border-radius: ${defaultValue};`
35 | `.rounded-t` | `border-top-left-radius: ${defaultValue};`
`border-top-right-radius: ${defaultValue};`
36 | `.rounded-r` | `border-top-right-radius: ${defaultValue};`
`border-bottom-right-radius: ${defaultValue};`
37 | `.rounded-b` | `border-bottom-left-radius: ${defaultValue};`
`border-bottom-right-radius: ${defaultValue};`
38 | `.rounded-l` | `border-top-left-radius: ${defaultValue};`
`border-bottom-left-radius: ${defaultValue};`
39 | `.rounded-tl` | `border-top-left-radius: ${defaultValue};`
40 | `.rounded-tr` | `border-top-right-radius: ${defaultValue};`
41 | `.rounded-bl` | `border-bottom-left-radius: ${defaultValue};`
42 | `.rounded-br` | `border-bottom-right-radius: ${defaultValue};`
43 | `.rounded-full` | `border-radius: 9999px;`
44 | `.rounded-t-full` | `border-top-left-radius: 9999px;`
`border-top-right-radius: 9999px;`
45 | `.rounded-r-full` | `border-top-right-radius: 9999px;`
`border-bottom-right-radius: 9999px;`
46 | `.rounded-b-full` | `border-bottom-left-radius: 9999px;`
`border-bottom-right-radius: 9999px;`
47 | `.rounded-l-full` | `border-top-left-radius: 9999px;`
`border-bottom-left-radius: 9999px;`
48 | `.rounded-tl-full` | `border-top-left-radius: 9999px;`
49 | `.rounded-tr-full` | `border-top-right-radius: 9999px;`
50 | `.rounded-bl-full` | `border-bottom-left-radius: 9999px;`
51 | `.rounded-br-full` | `border-bottom-right-radius: 9999px;`
52 | `.rounded-${key}` | `border-radius: ${value};`
53 | `.rounded-t-${key}` | `border-top-left-radius: ${value};`
`border-top-right-radius: ${value};`
54 | `.rounded-r-${key}` | `border-top-right-radius: ${value};`
`border-bottom-right-radius: ${value};`
55 | `.rounded-b-${key}` | `border-bottom-left-radius: ${value};`
`border-bottom-right-radius: ${value};`
56 | `.rounded-l-${key}` | `border-top-left-radius: ${value};`
`border-bottom-left-radius: ${value};`
57 | `.rounded-tl-${key}` | `border-top-left-radius: ${value};`
58 | `.rounded-tr-${key}` | `border-top-right-radius: ${value};`
59 | `.rounded-bl-${key}` | `border-bottom-left-radius: ${value};`
60 | `.rounded-br-${key}` | `border-bottom-right-radius: ${value};`
61 |
62 | ## border-width
63 |
64 | - `defaultValue` = `theme.borderWidth.default`
65 | - `value` = `theme.borderWidth[key]` || `resolveNumericValue(key)`
66 |
67 | Class name | CSS rules
68 | :--|:--
69 | `.b` | `border-width: ${defaultValue};`
70 | `.bx` | `border-left-width: ${defaultValue};`
`border-right-width: ${defaultValue};`
71 | `.by` | `border-top-width: ${defaultValue};`
`border-bottom-width: ${defaultValue};`
72 | `.bt` | `border-top-width: ${defaultValue};`
73 | `.br` | `border-right-width: ${defaultValue};`
74 | `.bb` | `border-bottom-width: ${defaultValue};`
75 | `.bl` | `border-left-width: ${defaultValue};`
76 | `.b-${key}` | `border-width: ${value};`
77 | `.bx-${key}` | `border-left-width: ${value};`
`border-right-width: ${value};`
78 | `.by-${key}` | `border-top-width: ${value};`
`border-bottom-width: ${value};`
79 | `.bt-${key}` | `border-top-width: ${value};`
80 | `.br-${key}` | `border-right-width: ${value};`
81 | `.bb-${key}` | `border-bottom-width: ${value};`
82 | `.bl-${key}` | `border-left-width: ${value};`
83 |
84 | ## color
85 |
86 | Class name | CSS rules
87 | :--|:--
88 | `.color` | `color: ${theme.color.default};`
89 | `.color-${key}` | `color: ${theme.color[key]};`
90 |
91 | ## cursor
92 |
93 | Class name | CSS rules
94 | :--|:--
95 | `.cursor-default` | `cursor: default;`
96 | `.cursor-pointer` | `cursor: pointer;`
97 |
98 | ## display
99 |
100 | Class name | CSS rules
101 | :--|:--
102 | `.block` | `display: block;`
103 | `.flex` | `display: flex;`
104 | `.hidden` | `display: none;`
105 | `.inline-flex` | `display: inline-flex;`
106 |
107 | ## flex
108 |
109 | Class name | CSS rules
110 | :--|:--
111 | `.flex-1` | `flex: 1 1 0%;`
112 | `.flex-auto` | `flex: 1 1 auto;`
113 | `.flex-initial` | `flex: 0 1 auto;`
114 | `.flex-none` | `flex: none;`
115 |
116 | ## flex-wrap
117 |
118 | Class name | CSS rules
119 | :--|:--
120 | `.flex-nowrap` | `flex-wrap: nowrap;`
121 | `.flex-wrap` | `flex-wrap: wrap;`
122 | `.flex-wrap-reverse` | `flex-wrap: wrap-reverse;`
123 |
124 | ## font-family
125 |
126 | Class name | CSS rules
127 | :--|:--
128 | `.font` | `font-family: ${theme.fontFamily.default};`
129 | `.font-${key}` | `font-family: ${theme.fontFamily[key]};`
130 |
131 | ## font-size
132 |
133 | Class name | CSS rules
134 | :--|:--
135 | `.font-${key}` | `font-size: ${theme.fontSize[key]};`
136 |
137 | ## font-weight
138 |
139 | Class name | CSS rules
140 | :--|:--
141 | `.font-${key}` | `font-weight: ${theme.fontWeight[key]};`
142 |
143 | ## height
144 |
145 | Class name | CSS rules
146 | :--|:--
147 | `.h` | `height: ${theme.height.default};`
148 | `.h-screen` | `height: 100vh;`
149 | `.h-${key}` | `height: ${theme.height[key] \|\| computeNumericValue(key)};`
150 | `.minh` | `min-height: ${theme.height.default};`
151 | `.minh-screen` | `min-height: 100vh;`
152 | `.minh-${key}` | `min-height: ${theme.height[key] \|\| computeNumericValue(key)};`
153 | `.maxh` | `max-height: ${theme.height.default};`
154 | `.maxh-screen` | `max-height: 100vh;`
155 | `.maxh-${key}` | `max-height: ${theme.height[key] \|\| computeNumericValue(key)};`
156 |
157 | ## justify-content
158 |
159 | Class name | CSS rules
160 | :--|:--
161 | `.justify-around` | `justify-content: space-around;`
162 | `.justify-between` | `justify-content: space-between;`
163 | `.justify-center` | `justify-content: center;`
164 | `.justify-end` | `justify-content: flex-end;`
165 | `.justify-evenly` | `justify-content: space-evenly;`
166 | `.justify-start` | `justify-content: flex-start;`
167 |
168 | ## letter-spacing
169 |
170 | Class name | CSS rules
171 | :--|:--
172 | `.kerning` | `letter-spacing: ${theme.letterSpacing.default};`
173 | `.kerning-${key}` | `letter-spacing: ${theme.letterSpacing[key]};`
174 |
175 | ## line-height
176 |
177 | Class name | CSS rules
178 | :--|:--
179 | `.leading` | `line-height: ${theme.lineHeight.default};`
180 | `.leading-${key}` | `line-height: ${theme.lineHeight[key]};`
181 |
182 | ## margin
183 |
184 | - `defaultValue` = `theme.margin.default` || `theme.space.default`
185 | - `value` = `theme.margin[key]` || `theme.space[key]` || `resolveNumericValue(key)`
186 |
187 | Class name | CSS rules
188 | :--|:--
189 | `.m` | `margin: ${defaultValue};`
190 | `.mx` | `margin-left: ${defaultValue};`
`margin-right: ${defaultValue};`
191 | `.my` | `margin-top: ${defaultValue};`
`margin-bottom: ${defaultValue};`
192 | `.mt` | `margin-top: ${defaultValue};`
193 | `.mr` | `margin-right: ${defaultValue};`
194 | `.mb` | `margin-bottom: ${defaultValue};`
195 | `.ml` | `margin-left: ${defaultValue};`
196 | `.mx-${key}` | `margin-left: ${value};`
`margin-right: ${value};`
197 | `.my-${key}` | `margin-top: ${value};`
`margin-bottom: ${value};`
198 | `.mt-${key}` | `margin-top: ${value};`
199 | `.mr-${key}` | `margin-right: ${value};`
200 | `.mb-${key}` | `margin-bottom: ${value};`
201 | `.ml-${key}` | `margin-left: ${value};`
202 | `.-m` | `margin: -${defaultValue};`
203 | `.-mx` | `margin-left: -${defaultValue};`
`margin-right: -${defaultValue};`
204 | `.-my` | `margin-top: -${defaultValue};`
`margin-bottom: -${defaultValue};`
205 | `.-mt` | `margin-top: -${defaultValue};`
206 | `.-mr` | `margin-right: -${defaultValue};`
207 | `.-mb` | `margin-bottom: -${defaultValue};`
208 | `.-ml` | `margin-left: -${defaultValue};`
209 | `.-mx-${key}` | `margin-left: -${value};`
`margin-right: -${value};`
210 | `.-my-${key}` | `margin-top: -${value};`
`margin-bottom: -${value};`
211 | `.-mt-${key}` | `margin-top: -${value};`
212 | `.-mr-${key}` | `margin-right: -${value};`
213 | `.-mb-${key}` | `margin-bottom: -${value};`
214 | `.-ml-${key}` | `margin-left: -${value};`
215 |
216 | ## outline
217 |
218 | Class name | CSS rules
219 | :--|:--
220 | `.outline-none` | `outline: none;`
221 |
222 | ## padding
223 |
224 | defaultValue = theme.padding.default || theme.space.default
225 | value = theme.padding[key] || theme.space[key] || computeNumericValue(key)
226 |
227 | Class name | CSS rules
228 | :--|:--
229 | `.p` | `padding: ${defaultValue};`
230 | `.px` | `padding-left: ${defaultValue};`
`padding-right: ${defaultValue};`
231 | `.py` | `padding-top: ${defaultValue};`
`padding-bottom: ${defaultValue};`
232 | `.pt` | `padding-top: ${defaultValue};`
233 | `.pr` | `padding-right: ${defaultValue};`
234 | `.pb` | `padding-bottom: ${defaultValue};`
235 | `.pl` | `padding-left: ${defaultValue};`
236 | `.px-${key}` | `padding-left: ${value};`
`padding-right: ${value};`
237 | `.py-${key}` | `padding-top: ${value};`
`padding-bottom: ${value};`
238 | `.pt-${key}` | `padding-top: ${value};`
239 | `.pr-${key}` | `padding-right: ${value};`
240 | `.pb-${key}` | `padding-bottom: ${value};`
241 | `.pl-${key}` | `padding-left: ${value};`
242 |
243 | ## position
244 |
245 | Class name | CSS rules
246 | :--|:--
247 | `.absolute` | `position: absolute;`
248 | `.fixed` | `position: fixed;`
249 | `.relative` | `position: relative;`
250 | `.static` | `position: static;`
251 |
252 | ## select
253 |
254 | Class name | CSS rules
255 | :--|:--
256 | `.select-all` | `user-select: all;`
257 | `.select-auto` | `user-select: auto;`
258 | `.select-none` | `user-select: none;`
259 | `.select-text` | `user-select: text;`
260 |
261 | ## text style
262 |
263 | Class name | CSS rules
264 | :--|:--
265 | `.text` | `font-size: ${theme.fontSize.default};`
`font-weight: ${theme.fontWeight.default};`
`letter-spacing: ${theme.letterSpacing.default};`
`line-height: ${theme.lineHeight.default};`
266 | `.text-${key}` | `font-size: ${theme.fontSize[key]};`
`font-weight: ${theme.fontWeight[key]};`
`letter-spacing: ${theme.letterSpacing[key]};`
`line-height: ${theme.lineHeight[key]};`
267 |
268 | ## text-align
269 |
270 | Class name | CSS rules
271 | :--|:--
272 | `.text-center` | `text-align: center;`
273 | `.text-justify` | `text-align: justify;`
274 | `.text-left` | `text-align: left;`
275 | `.text-right` | `text-align: right;`
276 |
277 | ## text-decoration
278 |
279 | Class name | CSS rules
280 | :--|:--
281 | `.line-through` | `text-decoration: line-through;`
282 | `.no-underline` | `text-decoration: none;`
283 | `.underline` | `text-decoration: underline;`
284 |
285 | ## text-transform
286 |
287 | Class name | CSS rules
288 | :--|:--
289 | `.caps` | `text-transform: capitalize;`
290 | `.lowercase` | `text-transform: lowercase;`
291 | `.normal-case` | `text-transform: none;`
292 | `.uppercase` | `text-transform: uppercase;`
293 |
294 | ## top, right, bottom, left
295 |
296 | - `value` = `theme.space[key]` || `resolveNumericValue(key)`
297 |
298 | Class name | CSS rules
299 | :--|:--
300 | `.top` | `top: 0;`
301 | `.top-${key}` | `top: ${value};`
302 | `.-top-${key}` | `top: -${value};`
303 | `.right` | `right: 0;`
304 | `.right-${key}` | `right: ${value};`
305 | `.-right-${key}` | `right: -${value};`
306 | `.bottom` | `bottom: 0;`
307 | `.bottom-${key}` | `bottom: ${value};`
308 | `.-bottom-${key}` | `bottom: -${value};`
309 | `.left` | `left: 0;`
310 | `.left-${key}` | `left: ${value};`
311 | `.-left-${key}` | `left: -${value};`
312 |
313 | ## width
314 |
315 | Class name | CSS rules
316 | :--|:--
317 | `.w` | `width: ${theme.width.default};`
318 | `.w-screen` | `width: 100vw;`
319 | `.w-${key}` | `width: ${theme.width[key] \|\| computeNumericValue(key)};`
320 | `.minw` | `min-width: ${theme.width.default};`
321 | `.minw-screen` | `min-width: 100vw;`
322 | `.minw-${key}` | `min-width: ${theme.width[key] \|\| computeNumericValue(key)};`
323 | `.maxw` | `max-width: ${theme.width.default};`
324 | `.maxw-screen` | `max-width: 100vw;`
325 | `.maxw-${key}` | `max-width: ${theme.width[key] \|\| computeNumericValue(key)};`
326 |
--------------------------------------------------------------------------------