Skip to content

Tahul/pinceau

main
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Code

Files

Permalink
Failed to load latest commit information.

πŸ–Œ pinceau

NPM version

A CSS-in-TS framework built to feel like a native Vue feature.

🎨 Start painting β€’ 🚧 Documentation

🚨 Warning

Pinceau is still under heavy development, if you are missing some parts of the documentation, please open an issue and describe your problem. I'll be happy to help.

βš™οΈ Install

npm i pinceau
Nuxt
// nuxt.config.js
export default defineNuxtConfig({
  modules: [
    'pinceau/nuxt',
  ],
  pinceau: {
    ...PinceauOptions
  }
})

Example: playground/

This module only works with Nuxt 3.

Vite
// vite.config.ts
import Pinceau from 'pinceau/vite'

export default defineConfig({
  plugins: [
    Pinceau(PinceauOptions),
  ],
})

Example: playground/

PinceauOptions
export interface PinceauOptions {
  /**
   * The root directory of your project.
   *
   * @default process.cwd()
   */
  cwd?: string
  /**
   * The path of your configuration file.
   */
  configLayers?: ConfigOrPaths
  /**
   * The name of the style configuration file
   *
   * @default 'pinceau.config'
   */
  configFileName?: string
  /**
   * A callback called each time your config gets resolved.
   */
  configResolved?: (config: any) => void
  /**
   * The directry in which you store your design tokens.
   *
   * @default 'tokens'
   */
  tokensDir?: string
  /**
   * The directory in which you want to output the built version of your configuration.
   */
  outputDir?: string
  /**
   * Imports the default CSS reset in the project.
   *
   * @default true
   */
  preflight?: boolean
  /**
   * Excluded transform paths.
   */
  excludes?: string[]
  /**
   * Included transform paths.
   */
  includes?: string[]
  /**
   * Toggles color .{dark|light} global classes.
   *
   * If set to class, all @dark and @light clauses will also be generated
   * with .{dark|light} classes on <html> tag as a parent selector.
   *
   * @default 'class'
   */
  colorSchemeMode?: 'media' | 'class'
}
Volar

If you want to have all autocomplete and TypeScript powered features, you need to setup Volar in your IDE.

That also means these features sadly won't work in the StackBlitz playground, unless they provide support for it at some point.

Once Volar enabled, add the Pinceau plugin to your tsconfig.json:

{
  "vueCompilerOptions": {
    "plugins": ["pinceau/volar"]
  }
}

Once enabled, be sure to restart your TypeScript server, and enjoy autocompletion!

🎨 Configure

The configuration file is made to help you injecting all the Design Tokens you want into your app as CSS variables, JS references, or any other format you need or want.

pinceau.config.ts

You can decide to follow the suggested theme definition keys, but you also are free to define whatever design tokens you want, with the structure of your choice.

As an example, this configuration helped us at NuxtLabs to create a sync between Figma Tokens and our component library.

πŸ’‘ Configuration example
import { defineTheme } from 'pinceau'

export default defineTheme({
  media: {
    sm: '(min-width: 640px)',
    md: '(min-width: 768px)',
    lg: '(min-width: 1024px)',
    xl: '(min-width: 1280px)',
    xxl: '(min-width: 1536px)',
  },
  color: {
    primary: {
      50: {
        initial: '{color.orange.50}',
        dark: '{color.orange.900}'
      },
    },
    orange: {
      50: '#ffe9d9',
      100: '#ffd3b3',
      200: '#ffbd8d',
      300: '#ffa666',
      400: '#ff9040',
      500: '#ff7a1a',
      600: '#e15e00',
      700: '#a94700',
      800: '#702f00',
      900: '#381800',
    }
  }
})
🎨 Output example
:root {
  --color-primary-50: #ffe9d9;
  --color-orange-50: #ffe9d9;
  --color-orange-100: #ffd3b3;
  --color-orange-200: #ffbd8d;
  --color-orange-300: #ffa666;
  --color-orange-400: #ff9040;
  --color-orange-500: #ff7a1a;
  --color-orange-600: #e15e00;
  --color-orange-700: #a94700;
  --color-orange-800: #702f00;
  --color-orange-900: #381800;
}

html.dark :root {
  --color-primary-50: #381800;
}

Out of your configuration file, Pinceau will generate multiple output targets which will provide:

  • Type completion in css() function for mapped theme tokens
  • Token paths completion with globally available $dt() helper
  • Globally injected CSS variables
  • A lot more for you to discover, and for me to document ✨

This is powered by style-dictionary and runs on style-dictionary-esm.

πŸ–Œ Paint

Pinceau styling API is made to feel like a native Vue API.

To do so, it fully takes advantages of the Vue components <style> tags, and tries to give it even more super powers.

css()

Pinceau enables lang="ts" as a valid attribute for <style> or <style scoped> tags.

That enables the usage of another internal API, css().

<style lang="ts">
css({
  div: {
    color: '{color.primary.900}',
    backgroundColor: '{color.orange.50}'
  }
})
</style>

The css() function has mutliple features:

  • Based on @stitches/stringify

  • Autocomplete CSS properties from csstype

  • Autocomplete CSS values from theme design tokens

    πŸ’‘ Example
    <style lang="ts">
    css({
      color: '{*}', < Will autocomplete from all 'color' keys,
      padding: '{*}', < Will autocomplete from 'space' keys
    })
    </style>

    That works with all nested keys in theme keys, as long as the values are a valid Design Token.

    You also get autocompletion from all browser values from MDN thanks to csstype.

  • Autocomplete root keys from your <template> elements

    πŸ’‘ Example
    <template>
      <div>
        <button>
          Hello World!
        </button>
      </div>
    </template>
    
    <style lang="ts">
    css({
      '*' < Will autocomplete 'div' | 'button'
    })
    </style>
  • Supports postcss-nested syntax

    πŸ’‘ Example
    <style lang="ts">
    css({
      '.phone': {
        '&_title': {
          width: '500px',
          'body.is-variant &': {
            color: 'purple'
          }
        }
      }
    })
    </style>
  • Supports postcss-custom-properties syntax

    πŸ’‘ Example
    <style lang="ts">
    css({
      '.phone': {
        '--custom-property': '{color.primary.500}'
      }
    })
    </style>
  • Supports all custom features from Vue <style> attributes

    πŸ’‘ Example
    <script setup>
    defineProps({
      color: {
        type: String,
        required: false,
        default: $dt('color.primary.500')
      }
    })
    
    const myRef = ref()
    </script>
    
    <style lang="ts">
    css({
      '.phone': {
        '&:deep(...)': {},
        '&:slotted(...)': {},
        '&:global(...)': {},
        '--custom-property': 'v-bind(myRef)',
        color: 'v-bind(color)'
        // ^ This is autocompleted and shows type errors
      }
    })
    </style>

    These features get exact same autocomplete and type-safety as the would in a regular <style> tag.

    Including :deep() , :slotted(), :global() and v-bind.

  • Supports @dark & @light helpers

    πŸ’‘ Example
    <style lang="ts">
    css({
      '.block': {
        color: 'black',
        backgroundColor: 'white',
        '@light': {
          color: 'grey'
        },
        '@dark': {
          backgroundColor: 'black'
        }
      }
    })
    </style>
  • Supports @nuxtjs/color-mode

    πŸ’‘ Example
    <style lang="ts">
    css({
      '@sepia': {
        color: 'black'
      }
    })
    </style>

    This has same API as @dark or @light, but uses color modes defined in @nuxtjs/color-mode

  • Supports @mq helper

    πŸ’‘ Example
    • Configuration
    import { defineTheme } from 'pinceau'
    
    export default defineTheme({
      media: {
        sm: { value: '(min-width: 640px)' },
        md: { value: '(min-width: 768px)' },
        lg: { value: '(min-width: 1024px)' },
        xl: { value: '(min-width: 1280px)' },
        xxl: { value: '(min-width: 1536px)' },
        rm: { value: '(prefers-reduced-motion: reduce)' },
      },
    })
    • Component
    <style lang="ts">
    css({
      '.block': {
        width: '100%'
        '@lg': {
          width: '50%'
        },
      }
    })
    </style>
    
    <style lang="postcss">
    .block {
      width: 100%;
    
      @lg {
        width: 50%;
      }
    }
    </style>

    You can add as many screen you need, they will all get autocompleted.

  • Supports computed styles πŸ’‘ new

    πŸ’‘ Example

    This syntax only works with <script setup lang="ts"> for now.

    It is planned to support all <script> syntaxes soon.

    <script setup lang="ts">
    import type { PropType } from 'vue'
    // You must specify a key for props when using Variants
    const props = defineProps({
      color: {
        type: String as PropType<ThemeKey<'color'>>
      }
    })
    </script>
    
    <template>
      <div class="block" />
    </template>
    
    <style lang="ts">
    css({
      '.block': {
        backgroundColor: (props) => `{color.${props.color}`,
      }
    })
    </style>
  • Supports variants system πŸ’‘ new

    πŸ’‘ Example

    This syntax only works with <script setup lang="ts"> for now.

    It is planned to support all <script> syntaxes soon.

    Define your component variants at root of css():

    <script setup lang="ts">
    // You must specify a key for props when using Variants
    const props = defineProps({
      // This part is optional, it provides typings
      ...variants
    })
    </script>
    
    <template>
      <div class="block" :class="[$pinceau]" />
    </template>
    
    <style lang="ts">
    css({
      '.block': {
        backgroundColor: '{color.primary.500}',
      },
      variants: {
        transparent: {
          true: {
            backgroundColor: 'transparent'
          },
          options: {
            default: true
          }
        },
        shadows: {
          soft: {
            boxShadow: '{shadows.sm}'
          },
          smooth: {
            boxShadow: '{shadows.lg}',
            border: '2px solid {color.primary.500}'
          },
          heavy: {
            boxShadow: '{shadows.xl}',
            border: '4px solid {color.primary.800}'
          },
          options: {
            default: {
              sm: 'soft',
              lg: 'smooth',
              xl: 'heavy'
            }
          }
        }
      }
    })
    </style>

    Profit from automatically generated props and typings, and compatibility with all your media queries:

    <template>
      <Block :transparent="{ md: true, xl: false }" shadow="{ sm: 'soft', xl: 'smooth' }" />
    </template>

$dt()

$dt stands for $designToken.

In addition to css(), $dt() comes with everything you need to use your Design Tokens outside of <style>.

<script setup>
defineProps({
  color: {
    type: String,
    required: false,
    default: $dt('color.primary')
  }
})

const orangeVariable = $dt('color.orange.500')
</script>

<template>
  <div :style="{ backgroundColor: color }">
    {{ $dt('color.primary') }}
  </div>
</template>

<style lang="postcss">
div {
  color: $dt('color.orange.900')
}
</style>

$dt() helper will autocomplete all theme keys anywhere in your app.

Options

$dt() supports options as a second argument, allowing you not only to grab CSS variables from your token, but the whole definition from a token, or a tree of token.

/**
 * The key that will be unwrapped from the design token object.
 * 
 * Supports `nested.key.syntax`.
 * 
 * Can be set to `undefined` to return the whole design token object.
 * 
 * @default 'attributes.variable'
 */
key?: string
/**
 * Toggle deep flattening of the design token object to the requested key.
 *
 * If you query an token path containing mutliple design tokens and want a flat \`key: value\` object.
 * 
 * @default false
 */
flatten?: boolean
const allColors = $dt('color', { flatten: false, key: undefined })
const orangeColor = $dt('color.orange', { key: undefined })

This is considered as advanced usage and these options might be subject to changes.

Accessing CSS variables via $dt('token.path') should stay the same.

🚨 Warning

Please note that $dt() acts as a macro, like defineProps or defineEmits.

It will be replaced when your component gets transformed by Vite by the static value of the token.

That means the only valid value for $dt() is a plain string, not a reference to a string.

<script setup>
const test = $dt('color.primary.500') // βœ… Valid

const ref = ref('color.primary.500')
const refTest = $dt(ref.value) // 🚨 Invalid
</script>

πŸš€ More to come

Pinceau is currently in ⚑️ active ⚑️ development.

There is plenty of features to come, including:

  • Proper test suite

    • Configuration side relies on style-dictionary-esm which is heavily tested
    • css() core API relies on [@stitches/stringify] which also has tests, as this package maintains local implementation of it, I'll add some more tests to it.
    • All CSS transforms applied on .vue components are built with testing in mind
  • Util properties

    πŸ’‘ Example
    // Theme config
    defineTheme({
      color: {
        primary: {
          light: {
            value: '#B6465F'
          },
          dark: {
            value: '#4D1C26'
          }
        }
      },
      utils: {
        surface: (value: ThemeKeys<'color'>) => ({
          backgroundColor: `{color.${value}.dark}`,
          borderColor: `{color.${value}.light}`
        })
      }
    })
    
    // Component <style lang="ts">
    css({
      surface: 'primary'
    })
  • Configuring your own transforms, formats and actions

  • 🌐 Documentation website

  • 🎨 Online playground

  • Maybe yours? I'm happy to provide guidance and reviews on any PR!

  • And even more! πŸš€

πŸ’– Credits

This package takes inspiration in a lot of other projects, such as style-dictionary, Stitches, vanilla-extract, unocss.

License

MIT License Β© 2022-PRESENT YaΓ«l GUILLOUX


β€œAll you need to paint is a few tools, a little instruction, and a vision in your mind.” β€’ Bob Ross

About

πŸ‘¨β€πŸŽ¨ Incrementally adoptable CSS toolbox

Resources

License

Stars

Watchers

Forks

Sponsor this project

 

Packages

No packages published

Languages