Skip to content

Commit

Permalink
Implement bezier-tokens package (#1685)
Browse files Browse the repository at this point in the history
<!--
  How to write a good PR title:
- Follow [the Conventional Commits
specification](https://www.conventionalcommits.org/en/v1.0.0/).
  - Give as much context as necessary and as little as possible
  - Prefix it with [WIP] while it’s a work in progress
-->

## Self Checklist

- [x] I wrote a PR title in **English** and added an appropriate
**label** to the PR.
- [x] I wrote the commit message in **English** and to follow [**the
Conventional Commits
specification**](https://www.conventionalcommits.org/en/v1.0.0/).
- [x] I [added the
**changeset**](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md)
about the changes that needed to be released. (or didn't have to)
- [x] I wrote or updated **documentation** related to the changes. (or
didn't have to)
- [x] I wrote or updated **tests** related to the changes. (or didn't
have to)
- [x] I tested the changes in various browsers. (or didn't have to)
  - Windows: Chrome, Edge, (Optional) Firefox
  - macOS: Chrome, Edge, Safari, (Optional) Firefox

## Related Issue
<!-- Please link to issue if one exists -->

Fixes #994 

## Summary
<!-- Please brief explanation of the changes made -->

베지어 디자인 시스템의 디자인 토큰 패키지인 bezier-tokens 패키지를 추가합니다.

> **디자인 토큰이 무엇인가요?**
> 
> Design tokens are a methodology for expressing design decisions in a
platform-agnostic way so that they can be shared across different
disciplines, tools, and technologies. They help establish a common
vocabulary across organisations. (from w3c dtcg)
> 
> - https://design-tokens.github.io/community-group/format/
> - https://m3.material.io/foundations/design-tokens/overview

## Details
<!-- Please elaborate description of the changes -->

### Note

- 여러 디자인 토큰 변환 라이브러리를 리서치해보았습니다. 사용자의 규모와 향후 업데이트 로드맵, 커스터마이즈 가능 범위 등을
살펴보았을 때, Style dictionary가 가장 적절하다고 생각하여 선택하였습니다.
- 디자인 토큰을 피그마에서 연동하기에는 현상황에서 어려웠습니다. 현재 피그마 Variables가 오픈베타여서 타이포그래피 등의
토큰 등을 지원하고 있지 않는 상황입니다. 또한 피그마 Variables의 등장으로 Token Studio같은 서드파티 플러그인을
사용하지 않기로 팀 내부에서 결정했기 때문에, 피그마 Variables의 스펙이 언제든지 추가되거나 변할 수 있다는 뜻입니다.
따라서 지금 피그마-소스 코드 변환기를 구현하는 건 시기상조라고 생각했습니다.
- 현재 작업중인 새로운 디자인 시스템에 토큰을 적용하지 않고, 기존의(프로덕션) 레거시 디자인 토큰을 적용했습니다. 정확히는
현재 bezier-react의 Foundation들을 디자인 토큰으로 분해했습니다(= 피그마에는 토큰으로 분류되지 않은 경우도
있습니다). 토큰 적용 & 정적 스타일링 방식으로 변경 -> 새로운 디자인 토큰 적용으로 단계를 나누어가기 위해서입니다.

### Build step

빌드는 간략하게 다음의 과정으로 이루어집니다.

1. JSON(Design token)을 cjs/esm/css 로 변환합니다.
2. 변환된 cjs/esm 의 엔트리포인트(index.js)를 만듭니다.
3. 타입스크립트 컴파일러를 통해 변환된 js 파일로부터 타입 선언을 만듭니다.

- **향후 1번의 변환 과정에 iOS, Android용 스타일 변환기, JSON 변환기 등을 추가할 수 있습니다.**
- 1번의 변환 과정은 글로벌 토큰(기존의 팔레트, 레디우스 등)과 시맨틱 토큰(라이트/다크 테마)이 별개로 이루어집니다.
라이트/다크 테마를 함께 빌드하게 되면 키가 충돌했다는 메세지와 함께 빌드 에러가 발생합니다. themeable같은 속성을 사용할
수도 있으나, JSON에 style-dictionary 라이브러리에 종속적인 속성을 포함시키고 싶지 않았습니다. 토큰은 더
순수하게 두는 게 나중을 위하여 좋다고 판단했습니다.
- Composite token(예: 타이포그래피)를 지원하지 않습니다. 현재 공식적으로 지원하지 않는 스펙이며, 현상황에서는
개별 토큰들을 bezier-react(그 외 각 플랫폼 디자인 시스템)에서 조합해도 큰 무리가 없다고 판단했습니다.

#### File tree

```md
dist
 ┣ cjs
 ┃ ┣ darkTheme.js
 ┃ ┣ global.js
 ┃ ┣ index.js
 ┃ ┗ lightTheme.js
 ┣ css
 ┃ ┣ dark-theme.css
 ┃ ┣ global.css
 ┃ ┗ light-theme.css
 ┣ esm
 ┃ ┣ darkTheme.mjs
 ┃ ┣ global.mjs
 ┃ ┣ index.mjs
 ┃ ┗ lightTheme.mjs
 ┗ types
 ┃ ┣ cjs
 ┃ ┃ ┣ darkTheme.d.ts
 ┃ ┃ ┣ darkTheme.d.ts.map
 ┃ ┃ ┣ global.d.ts
 ┃ ┃ ┣ global.d.ts.map
 ┃ ┃ ┣ index.d.ts
 ┃ ┃ ┣ index.d.ts.map
 ┃ ┃ ┣ lightTheme.d.ts
 ┃ ┃ ┗ lightTheme.d.ts.map
 ┃ ┗ esm
 ┃ ┃ ┣ darkTheme.d.mts
 ┃ ┃ ┣ darkTheme.d.mts.map
 ┃ ┃ ┣ global.d.mts
 ┃ ┃ ┣ global.d.mts.map
 ┃ ┃ ┣ index.d.mts
 ┃ ┃ ┣ index.d.mts.map
 ┃ ┃ ┣ lightTheme.d.mts
 ┃ ┃ ┗ lightTheme.d.mts.map
```

### Next

- 이 패키지의 js, css를 가지고 bezier-react의 스타일 시스템, 테마 기능을 구성하게 됩니다. (#1690)
- 이 패키지의 토큰에 더해 bezier-react의 constants(disabled 0.4, z-index), 타이포그래피
등을 bezier-react에서 추가, 확장하여 최종적으로 사용자 애플리케이션에 제공하는 방향으로 구현하고자 합니다. (#1495
에서 작업)

### Breaking change? (Yes/No)
<!-- If Yes, please describe the impact and migration path for users -->

No

## References
<!-- Please list any other resources or points the reviewer should be
aware of -->

- https://amzn.github.io/style-dictionary
- https://dbanks.design/blog/dark-mode-with-style-dictionary/
- amzn/style-dictionary#848 : Composite token
관련 이슈
  • Loading branch information
sungik-choi authored Nov 22, 2023
1 parent a265aff commit 576616a
Show file tree
Hide file tree
Showing 24 changed files with 1,875 additions and 8 deletions.
5 changes: 5 additions & 0 deletions .changeset/rare-coats-sing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@channel.io/bezier-tokens": minor
---

First release
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"jest-environment-jsdom": "^29.6.4",
"npm-run-all": "^4.1.5",
"stylelint": "^13.13.1",
"ts-node": "^10.9.1",
"turbo": "^1.10.13",
"typescript": "^4.9.5",
"typescript-plugin-css-modules": "^5.0.1"
Expand Down
2 changes: 1 addition & 1 deletion packages/bezier-figma-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Plugins > Development > Import plugin from manifest

## Contributing

See [contribution guide](../../CONTRIBUTING.md).
See [contribution guide](https://github.com/channel-io/bezier-react/wiki/Contribute).

## Maintainers

Expand Down
2 changes: 1 addition & 1 deletion packages/bezier-icons/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ console.log(isBezierIcon(<svg />)) // false

## Contributing

See [contribution guide](../../CONTRIBUTING.md).
See [contribution guide](https://github.com/channel-io/bezier-react/wiki/Contribute).

## Maintainers

Expand Down
2 changes: 1 addition & 1 deletion packages/bezier-react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ root.render(

## Contributing

See [contribution guide](../../CONTRIBUTING.md).
See [contribution guide](https://github.com/channel-io/bezier-react/wiki/Contribute).

## Maintainers

Expand Down
2 changes: 2 additions & 0 deletions packages/bezier-tokens/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
dist
26 changes: 26 additions & 0 deletions packages/bezier-tokens/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module.exports = {
root: true,
extends: ['bezier'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.eslint.json',
},
rules: {
'no-restricted-imports': 'off',
'sort-imports': [
'error',
{
ignoreDeclarationSort: true,
},
],
'import/order': [
'error',
{
'newlines-between': 'always',
alphabetize: { order: 'asc' },
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
},
],
'@typescript-eslint/naming-convention': 'off',
},
}
44 changes: 44 additions & 0 deletions packages/bezier-tokens/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Bezier Tokens

Bezier Tokens is a design tokens library that implements Bezier design system.

## Installation

```bash
npm i -D @channel.io/bezier-tokens
```

## Usage

### JavaScript

You can access and use values by token group.

```ts
import { tokens } from '@channel.io/bezier-tokens'

console.log(tokens.global['blue-300']) // "#..."
console.log(tokens.lightTheme['bg-black-dark']) // "#..."
```

### CSS

Provide all design tokens as CSS variables. If you want to apply dark theme tokens, add the `data-bezier-theme="dark"` attribute to the parent element. The default is light theme tokens, which can also be applied by adding the `data-bezier-theme="light"` attribute to the parent element.

```ts
import '@channel.io/bezier-tokens/css/global.css'
import '@channel.io/bezier-tokens/css/light-theme.css'
import '@channel.io/bezier-tokens/css/dark-theme.css'

div {
background: var(--bg-black-dark);
}
```

## Contributing

See [contribution guide](https://github.com/channel-io/bezier-react/wiki/Contribute).

## Maintainers

This package is mainly contributed by Channel Corp. Although feel free to contribution, or raise concerns!
56 changes: 56 additions & 0 deletions packages/bezier-tokens/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
"name": "@channel.io/bezier-tokens",
"version": "0.1.0",
"description": "Design tokens for Bezier design system.",
"repository": {
"type": "git",
"url": "https://github.com/channel-io/bezier-react",
"directory": "packages/bezier-tokens"
},
"main": "dist/cjs/index.js",
"module": "dist/esm/index.mjs",
"types": "dist/types/cjs/index.d.ts",
"exports": {
".": {
"require": {
"types": "./dist/types/cjs/index.d.ts",
"default": "./dist/cjs/index.js"
},
"import": {
"types": "./dist/types/esm/index.d.mts",
"default": "./dist/esm/index.mjs"
}
},
"./css/*": "./dist/css/*"
},
"sideEffects": [
"**/*.css"
],
"files": [
"dist"
],
"scripts": {
"build": "run-s clean:build build:tokens build:js-index build:types",
"build:tokens": "ts-node-esm scripts/build-tokens.ts",
"build:js-index": "ts-node-esm scripts/build-js-index.ts",
"build:types": "tsc -p tsconfig.build.json",
"lint": "TIMING=1 eslint --cache .",
"typecheck": "tsc --noEmit",
"clean": "run-s 'clean:*'",
"clean:build": "rm -rf dist",
"clean:cache": "rm -rf node_modules .turbo .eslintcache"
},
"keywords": [
"channel",
"design",
"tokens",
"design tokens"
],
"author": "Channel Corp.",
"license": "Apache-2.0",
"devDependencies": {
"eslint-config-bezier": "workspace:*",
"style-dictionary": "^3.9.0",
"tsconfig": "workspace:*"
}
}
73 changes: 73 additions & 0 deletions packages/bezier-tokens/scripts/build-js-index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import fs from 'fs'
import path from 'path'

interface BuildJsIndexFileOptions {
buildPath: string
isCjs: boolean
}

const getFileExtensionByModuleSystem = (isCjs: boolean) =>
(isCjs ? '.js' : '.mjs')

function buildJsIndexFile({ buildPath, isCjs }: BuildJsIndexFileOptions) {
const fileExtension = getFileExtensionByModuleSystem(isCjs)
const indexFile = `index${fileExtension}`
let exportStatements = ''

if (!fs.existsSync(buildPath)) {
// eslint-disable-next-line no-console
console.log(`Directory not found: ${buildPath}`)
return
}

const files = fs.readdirSync(buildPath)
// eslint-disable-next-line no-console
console.log(`Reading files in ${buildPath}:`, files)

files.forEach((file) => {
if (file.endsWith(fileExtension) && file !== indexFile) {
const moduleName = file.replace(fileExtension, '')
if (!isCjs) {
exportStatements += `import ${moduleName} from './${file}';\n`
}
}
})

if (isCjs) {
exportStatements += 'exports.tokens = Object.freeze({\n'
} else {
exportStatements += '\nexport const tokens = Object.freeze({\n'
}

files.forEach((file) => {
if (file.endsWith(fileExtension) && file !== indexFile) {
const moduleName = file.replace(fileExtension, '')
if (isCjs) {
exportStatements += ` ${moduleName}: require('./${moduleName}'),\n`
} else {
exportStatements += ` ${moduleName},\n`
}
}
})

exportStatements += '});\n'

fs.writeFileSync(path.join(buildPath, indexFile), exportStatements)
// eslint-disable-next-line no-console
console.log(`✅ Created ${indexFile} in ${buildPath}`)
}

function main() {
[
{
buildPath: 'dist/cjs',
isCjs: true,
},
{
buildPath: 'dist/esm',
isCjs: false,
},
].forEach(buildJsIndexFile)
}

main()
108 changes: 108 additions & 0 deletions packages/bezier-tokens/scripts/build-tokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import StyleDictionary, { type Config } from 'style-dictionary'

import {
customJsCjs,
customJsEsm,
} from './lib/format'
import { customFontPxToRem } from './lib/transform'
import { toCamelCase } from './lib/utils'

const TokenBuilder = StyleDictionary.registerTransform(customFontPxToRem)
.registerFormat(customJsCjs)
.registerFormat(customJsEsm)

const COMMON_WEB_TRANSFORMS = [
'attribute/cti',
'name/cti/kebab',
'size/rem',
'color/css',
customFontPxToRem.name,
]

interface DefineConfigOptions {
source: string[]
destination: string
options?: {
cssSelector: string
}
}

function defineConfig({
source,
destination,
options,
}: DefineConfigOptions): Config {
return {
source,
platforms: {
'js/cjs': {
transforms: COMMON_WEB_TRANSFORMS,
buildPath: 'dist/cjs/',
files: [
{
destination: `${toCamelCase(destination)}.js`,
format: customJsCjs.name,
filter: (token) => token.filePath.includes(destination),
},
],
},
'js/esm': {
transforms: COMMON_WEB_TRANSFORMS,
buildPath: 'dist/esm/',
files: [
{
destination: `${toCamelCase(destination)}.mjs`,
format: customJsEsm.name,
filter: (token) => token.filePath.includes(destination),
},
],
},
css: {
transforms: COMMON_WEB_TRANSFORMS,
basePxFontSize: 10,
buildPath: 'dist/css/',
files: [
{
destination: `${destination}.css`,
format: 'css/variables',
filter: (token) => token.filePath.includes(destination),
options: {
selector: options?.cssSelector,
outputReferences: true,
},
},
],
},
},
}
}

function main() {
[
TokenBuilder.extend(
defineConfig({
source: ['src/global/*.json'],
destination: 'global',
options: { cssSelector: ':where(:root, :host)' },
}),
),
TokenBuilder.extend(
defineConfig({
source: ['src/global/*.json', 'src/semantic/light-theme/*.json'],
destination: 'light-theme',
options: {
cssSelector: ':where(:root, :host), [data-bezier-theme="light"]',
},
}),
),
TokenBuilder.extend(
defineConfig({
source: ['src/global/*.json', 'src/semantic/dark-theme/*.json'],
destination: 'dark-theme',
options: { cssSelector: '[data-bezier-theme="dark"]' },
}),
),
].forEach((builder) => builder.buildAllPlatforms())
}

main()
41 changes: 41 additions & 0 deletions packages/bezier-tokens/scripts/lib/format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {
type Format,
type Named,
formatHelpers,
} from 'style-dictionary'

type CustomFormat = Named<Format>

const { fileHeader } = formatHelpers

export const customJsCjs: CustomFormat = {
name: 'custom/js/cjs',
formatter({ dictionary, file }) {
return (
`${fileHeader({ file })
}module.exports = {` +
`\n${
dictionary.allTokens
.map((token) => ` "${token.name}": ${JSON.stringify(token.value)},`)
.join('\n')
}\n` +
'}'
)
},
}

export const customJsEsm: CustomFormat = {
name: 'custom/js/esm',
formatter({ dictionary, file }) {
return (
`${fileHeader({ file })
}export default {` +
`\n${
dictionary.allTokens
.map((token) => ` "${token.name}": ${JSON.stringify(token.value)},`)
.join('\n')
}\n` +
'}'
)
},
}
Loading

0 comments on commit 576616a

Please sign in to comment.