Pinceauを少しさわってみた
Nuxt ThemesやDoucsで利用されているPinceau。
Docusをカスタマイズするために、もう少しちゃんと理解しようと、
少し触ってみたときの備忘録(´ω`)
Pinceau
デザイントークンのライブラリ。
こんな感じでtokens.config
を用意すると、
// tokens.config.ts import { defineTheme } from 'pinceau' defineTheme({ // Media queries media: { mobile: '(min-width: 320px)', tablet: '(min-width: 768px)', desktop: '(min-width: 1280px)' }, // Some Design tokens color: { red: { 1: '#FCDFDA', 2: '#F48E7C', 3: '#ED4D31', 4: '#A0240E', 5: '#390D05', }, }, primary: { 1: '{color.red.1}', 2: '{color.red.2}' }, space: { 1: '0.25rem', 2: '0.5rem', 3: '0.75rem', 4: '1rem' }, // Utils properties utils: { px: (value: PropertyValue<'padding'>) => ({ paddingLeft: value, paddingRight: value }), py: (value: PropertyValue<'padding'>) => ({ paddingTop: value, paddingBottom: value }) } });
こんな感じ($dt()
)、Vueのコンポーネント内からアクセスできる。
<style lang="postcss"> button { background-color: $dt('red.1'); @desktop { padding: $dt('space.2') $dt('space.4'); } } </style>
それぞれCSS変数になっているので、
<template>
内で上書きすることもできる。
<template> <MyButton :css="{ '--button-primary': '{color.red.200}' }" /> </template>
css()
を使うと、objectをcssに変換できるので、
props
とかも使える感じ。
<style lang="ts"> css({ '.my-button': { px: '{space.4}', backgroundColor: (props) => `{color.${props.color}.4}`, } }) </style> <script setup> defineProps({ color: computedStyle<keyof PinceauTheme['color']>('green'), ...variants }) </script>
ビルド時にCSSだけでなく、型定義なども出力してくれるので、
これらのデザイントークンをタブ補完できるようになっている。
pinceau/nuxtモジュール
Nuxtモジュールも用意されているので導入も簡単。
// nuxt.config.ts defineNuxtConfig({ modules: ['pinceau/nuxt'], pinceau: { ...options } })
ビルド時に生成するファイル
ビルドをするとtokens.config.ts
から、
以下のファイルを生成する。
# Docusの雛形の場合 $ tree .nuxt/pinceau/ --dirsfirst .nuxt/pinceau/ ├── theme │ └── index.css ... CSS変数 ├── index.ts ... マージ/正規化されたtokens.config ├── utils.ts ... tokens.configのutils部分 ├── definitions.ts ... VSCode拡張機能用 └── schema.ts ... Nuxt Studio用。index.tsのJSON Scheme?
それぞれの冒頭の数行はこんな感じ。
theme/index.css
@media { :root { --pinceau-mq: initial; --docus-loadingBar-gradientColorStop3: #0047e1; --docus-loadingBar-gradientColorStop2: #34cdfe;
index.ts
export const theme = { "media": { "xs": { "value": "(min-width: 475px)", "variable": "var(--media-xs)", "raw": "(min-width: 475px)" }, "sm": { "value": "(min-width: 640px)", "variable": "var(--media-sm)", "raw": "(min-width: 640px)" },
utils.ts
import { PinceauTheme, PropertyValue } from 'pinceau' export const my = (value: PropertyValue<'margin'>) => { return { marginTop: value, marginBottom: value, } }
Multi layer theming
こんな感じで設定すると、
複数のtokens.config.ts
をマージしてくれるっぽい。
{ configLayers: [ // `string` './my-themes/basic/', // `ConfigLayer` with `tokens` { tokens: { color: { primary: 'red' } } }, // `ConfigLayer` with `cwd` { cwd: './my-themes/figma', configFileName: 'figma.config' } ] }
Docusの場合は、layerになってる感じ。
一番下のレイヤーから、
- @nuxt-themes/tokensのtokens.config.ts
- 基本的な設定
media
/color
/width
/height
/shadow
radii
/size
/space
/borderWidth
/opacity
font
/fontWeight
/fontSize
/letterSpacing
/lead
text
/ease
- @nuxt-themes/typographyのtokens.config.ts
@nuxt-themes/typography
用の設定typography.xxx
/prose.xxx
の定義
- @nuxt-themes/docusのtokens.config.ts
@nuxt-themes/docus
の設定docus.xxx
+color
/shadow
/typography
の拡張
- @nuxt-themes/elementsのtokens.config.ts(リポジトリは非公開っぽい?)
@nuxt-themes/elements
の設定elements.xxx
+color
/space
の拡張
- appのtokens.config.ts
- 自分でカスタマイズする部分
最終的にどんなtokens.config
になるかは、
生成ファイルのtheme/index.css
、index.ts
、utils.ts
をみるとよさそう。
StyleDictionaryで生成しているよう
ソースをのぞいてみると、Style Dictionaryを使って、
各ファイルを生成しているよう。
import type { Core as Instance } from 'style-dictionary-esm' import StyleDictionary from 'style-dictionary-esm' export async function generateTheme( tokens: any, definitions: any, // ... ): Promise<ThemeGenerationOutput> { let styleDictionary: Instance = StyleDictionary // Files created by Pinceau const files = [ { destination: 'theme/index.css', // Has to be named `css` to be recognized as CSS output format: 'css', }, { destination: 'index.ts', format: 'pinceau/typescript', }, { destination: 'utils.ts', format: 'pinceau/utils', }, ] // ... }
READMEにも、
This package takes a lot of inspiration from these amazing projects:
と書かれているので、tokens.config.ts
の中身も、
style-dictionaryの形式に従っているっぽい?
仮説というか妄想
最終的にはTailwindも併用したい。
Tailwindには便利なUtilが揃っているので、
トライアンドエラーで開発するには便利。
Issueにあがっているけど、
もしかしたらTailwindのtokens.config.tsが用意されるかもしれない
また、style-dictionary → TailwindのThemeへの変換はあるっぽいので、
うまく使うといいかも?
tokens.config.ts
を起点に、Style Dictionaryで、
- TailwindのTheme
- Flutter用の.dart
みたいなこともできそう?
ビルド中のhook
オプションを見ると、configとビルド結果を扱えるよう。
import fs from "node:fs"; export default defineNuxtConfig({ extends: '@nuxt-themes/docus', pinceau: { // - After all your configLayers has been merged together // - Before generateTheme is called to generate all of your theme outputs // Before generateTheme is called to generate all of your theme outputs configResolved: (config: LoadConfigResult<PinceauTheme>) => { fs.writeFileSync("./.nuxt/pinceau-config-resolved.json", JSON.stringify(config, null, 2)); }, // A function that will be called when your theme is built. // That is useful if you want to listen to the Pinceau outputs. configBuilt: (config: ThemeGenerationOutput) => { fs.writeFileSync("./.nuxt/pinceau-config-built.json", JSON.stringify(config, null, 2)); } } })
型定義はこんな感じ。
// GeneratedPinceauTheme = 生成されたindex.tsのtheme export type PinceauTheme = GeneratedPinceauTheme /** * A configuration data once loaded by Pinceau. */ export interface LoadConfigResult<T = any> { // マージされたtokens.config.ts config: T // configから生成されたdefinisions.tsの中身 definitions: { [key: string]: any } // configLayersの元のパス sources: string[] } export interface ThemeGenerationOutput { buildPath: string tokens: any outputs: Record<string, any> }
generateTheme()
が呼ばれている箇所をみるとこんな感じ。
message('CONFIG_RESOLVED', [resolvedConfig]) // ... const builtTheme = await generateTheme(resolvedConfig.config, resolvedConfig.definitions, options) if (options?.configBuilt) { options.configBuilt(builtTheme) }
configResolved
...generateTheme()=style-dictionary
の引数で使われるconfigBuilt
...generateTheme()=style-dictionary
の結果
それを踏まえて、もう一度generateTheme
をみてみる。
import StyleDictionary from 'style-dictionary-esm' export async function generateTheme( tokens: any, // ... ): Promise<ThemeGenerationOutput> { let styleDictionary: Instance = StyleDictionary // ... 略 // registerFormatとかregisterTransformとか // StyleDictionaryの設定 // ... 略 // Responsive tokens const mqKeys = ['dark', 'light', ...Object.keys(tokens?.media || {})] // schema.ts const schema = await resolveUntypedSchema({ tokensConfig: tokens }) // ... 略 // styleDictionary = styleDictionary.extend({ // tokens: normalizeConfig(tokens, mqKeys, true), // ...略 }) try { result = await new Promise<ThemeGenerationOutput>( (resolve) => { styleDictionary.registerAction({ name: 'done', do: ({ tokens }) => { // configBuiltに渡される引数 resolve tokens: flattenTokens(tokens), outputs, buildPath, }) }, undo: () => {}, }) styleDictionary.buildAllPlatforms() }, ) } // ... return result }
直接ではなく、カスタムformatやtransformがあるけど、
なんとか行けそうな感じ。
ざっくり、それぞれの結果をファイルに吐き出すようにして、
import fs from "node:fs"; export default defineNuxtConfig({ extends: '@nuxt-themes/docus', pinceau: { configResolved: (config: LoadConfigResult<PinceauTheme>) => { fs.writeFileSync("./.nuxt/pinceau-config-resolved-config.json", JSON.stringify(config.config, null, 2)); }, configBuilt: (config: ThemeGenerationOutput) => { fs.writeFileSync("./.nuxt/pinceau-config-built-tokens.json", JSON.stringify(config.tokens, null, 2)); } } })
自前でstyle-dictionaryを実行するスクリプトを用意。
// scripts/build-sd.ts import type { Core as Instance } from 'style-dictionary-esm' import StyleDictionary from 'style-dictionary-esm'; import builtTokens from "../.nuxt/pinceau-config-built-tokens.json"; // **************************** // * MAIN // **************************** async function main() { let styleDictionary: Instance = StyleDictionary styleDictionary = styleDictionary.extend({ tokens: builtTokens, platforms: { css: { transformGroup: "css", buildPath: ".sd-build/css/", files: [ { destination: "_variables.css", format: "css/variables", }, ], }, }, }) styleDictionary.buildAllPlatforms(); } main().then();
実行してみるとこんな感じ。
$ pnpm jiti scripts/build-sd.ts css ✔︎ .sd-build/css/_variables.css $ head -n 20 .sd-build/css/_variables.css :root { --media-xs: (min-width: 475px); --media-sm: (min-width: 640px); --media-md: (min-width: 768px); --media-lg: (min-width: 1024px); --media-xl: (min-width: 1280px); --media-2xl: (min-width: 1536px); --media-rm: (prefers-reduced-motion: reduce); --media-landscape: only screen and (orientation: landscape); --media-portrait: only screen and (orientation: portrait); --color-white: #ffffff; --color-black: #0b0a0a; --color-gray-50: #fbfbfb; --color-gray-100: #f6f5f4; --color-gray-200: #ecebe8;
それっぽい何かは出せるっぽい。
もう少しgenerateTheme
のカスタムtransformやformatを移植すると、
他のプラットフォーム用のファイルもtokens.config.tsから生成できそうかも。
以上!! 便利だけど、いろんなのを考えると難しいね(*´ω`*)