tooning-color-token
v1.0.0
Published
๐จ ํฌ๋ ๋์์ธ ์์คํ ์์ ํ ํฐ ํจํค์ง - ์ํฐํ๋ผ์ด์ฆ๊ธ ํ์ฅ์ฑ๊ณผ ์ฑ๋ฅ์ ๊ฐ์ถ ๋ธ๋๋๋ณ ์์ ์์คํ
Maintainers
Readme
@tooning/color-token
๐จ ํฌ๋ ๋์์ธ ์์คํ ์์ ํ ํฐ ํจํค์ง
์ํฐํ๋ผ์ด์ฆ๊ธ ํ์ฅ์ฑ๊ณผ ์ฑ๋ฅ์ ๊ฐ์ถ ํฌ๋ ์๋น์ค๋ณ ๋ธ๋๋ ์ปฌ๋ฌ ํ ํฐ ์์คํ ์ ๋๋ค.
โจ ์ฃผ์ ํน์ง
- ๐ฏ ๋ธ๋๋๋ณ ์์ ์์คํ : 6๊ฐ ํฌ๋ ์๋น์ค (main, character, chat, magic, editor, board)
- ๐ง ํ์ ์์ ์ฑ: ์์ ํ TypeScript ์ง์
- โก ์ฑ๋ฅ ์ต์ ํ: ์บ์ฑ ๋ฐ ๋ฉ๋ชจ์ด์ ์ด์ ์ง์
- ๐ ํ ๋ง ์์คํ : ๋คํฌ ๋ชจ๋, ๊ณ ๋๋น ๋ชจ๋ ํ์ฅ ์ค๋น
- ๐ฑ ์ ๊ทผ์ฑ: WCAG ๊ฐ์ด๋๋ผ์ธ ์ค์
- ๐ ๏ธ ๊ฐ๋ฐ์ ์นํ์ : ํ๋ถํ JSDoc ๋ฌธ์ํ
์ค์น
npm install @tooning/color-token๐ ๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ
๋ธ๋๋๋ณ ์์ ํจ์ (๊ถ์ฅ)
import { getBrandButton, getText, getBorder, getBrandIcon } from '@tooning/color-token';
// ๋ธ๋๋๋ณ ๋ฒํผ ์์
const mainButton = getBrandButton('main', 'primary'); // '#776EFF'
const hoverButton = getBrandButton('main', 'primary', 'hover'); // 'rgba(119, 110, 255, 0.08)'
const characterBtn = getBrandButton('character', 'secondary'); // '#ffffff'
// ๋ธ๋๋๋ณ ํ
์คํธ ์์
const highlightText = getText('chat', 'highlight'); // '#00C1B7'
const primaryText = getText('magic', 'primary'); // '#242424'
const errorText = getText('editor', 'error'); // '#EB5757'
// ๋ธ๋๋๋ณ ํ
๋๋ฆฌ ์์
const activeBorder = getBorder('board', 'activated'); // '#4766FF'
const defaultBorder = getBorder('main', 'subtle'); // '#E0E0E0'
// ๋ธ๋๋๋ณ ์์ด์ฝ ์์
const brandIcon = getBrandIcon('character', 'highlight'); // '#2CA06E'
const disabledIcon = getBrandIcon('magic', 'disabled'); // '#C2C2C2'๊ตฌ์กฐํ๋ ์์ ํ ํฐ ์ ๊ทผ
import { colorTokens } from '@tooning/color-token';
// ๋ ์ด์ด ์์
const background = colorTokens.layer.background; // '#ffffff'
const surface = colorTokens.layer.surface01; // '#F9F9F9'
const errorLayer = colorTokens.layer.error; // '#FDF2F2'
// ํ
์คํธ ์์
const primaryText = colorTokens.text.primary; // '#242424'
const secondaryText = colorTokens.text.secondary; // '#595959'
const mutedText = colorTokens.text.muted; // '#969696'
// ๋ฒํผ ์ํ
const buttonStates = colorTokens.button.states;
// { base: 'base', hover: 'hover', pressed: 'pressed', ... }๋ธ๋๋ ํ์ ์ ์
import type { BrandType, ButtonLevel, ButtonState } from '@tooning/color-token';
// ์ง์๋๋ ๋ธ๋๋
type Brand = 'main' | 'character' | 'chat' | 'magic' | 'editor' | 'board';
// ๋ฒํผ ๋ ๋ฒจ
type Level = 'primary' | 'secondary' | 'tertiary';
// ๋ฒํผ ์ํ
type State = 'base' | 'hover' | 'pressed' | 'activated' | 'disabled';๐จ ๋ธ๋๋๋ณ ์์ ๊ฐ์ด๋
๊ฐ ๋ธ๋๋ ์ปฌ๋ฌ
| ๋ธ๋๋ | ์์ | ์ฉ๋ |
|--------|------|------|
| ๐ฃ main | #776EFF | ๋ฉ์ธ ์๋น์ค, ๊ธฐ๋ณธ ๋ธ๋๋ฉ |
| ๐ข character | #2CA06E | ์บ๋ฆญํฐ, ์๋ฐํ ๊ด๋ จ |
| ๐ฆ chat | #00C1B7 | ์ฑํ
, ์ปค๋ฎค๋์ผ์ด์
|
| ๐ magic | #FF7700 | ๋งค์ง, ํน์ ํจ๊ณผ |
| ๐ฉท editor | #FF2778 | ์๋ํฐ, ์ฐฝ์ ๋๊ตฌ |
| ๐ต board | #4766FF | ๋ณด๋, ํ์
๊ธฐ๋ฅ |
์ฌ์ฉ ์์
// ์๋น์ค๋ณ CTA ๋ฒํผ
<Button color={getBrandButton('main', 'primary')}>๋ฉ์ธ ์๋น์ค</Button>
<Button color={getBrandButton('character', 'primary')}>์บ๋ฆญํฐ ์์ฑ</Button>
<Button color={getBrandButton('chat', 'primary')}>์ฑํ
์์</Button>
<Button color={getBrandButton('magic', 'primary')}>๋งค์ง ์ ์ฉ</Button>
<Button color={getBrandButton('editor', 'primary')}>์๋ํฐ ์ด๊ธฐ</Button>
<Button color={getBrandButton('board', 'primary')}>๋ณด๋ ์์ฑ</Button>
// ํธ๋ฒ ์ํ
<Button
color={getBrandButton('main', 'primary')}
hoverColor={getBrandButton('main', 'primary', 'hover')}
>
ํธ๋ฒ ํจ๊ณผ
</Button>๐ ๏ธ ๊ณ ๊ธ ๊ธฐ๋ฅ
์ฑ๋ฅ ์ต์ ํ (์บ์ฑ)
import { getBrandButtonCached, memoizedUtils, cacheUtils } from '@tooning/color-token';
// ์บ์๋ ์์ ํจ์ (๋ฐ๋ณต ํธ์ถ ์ ์ฑ๋ฅ ํฅ์)
const cachedColor = getBrandButtonCached('main', 'primary', 'hover');
// ๋ฉ๋ชจ์ด์ ์ด์
๋ ์ ํธ๋ฆฌํฐ ํจ์๋ค
const rgbaColor = memoizedUtils.withOpacity('#776EFF', 0.5);
const rgbValues = memoizedUtils.hexToRgb('#776EFF');
const luminance = memoizedUtils.getLuminance('#776EFF');
// ์บ์ ๊ด๋ฆฌ
console.log(cacheUtils.getStats()); // { colorCacheSize: 42 }
cacheUtils.clearAll(); // ๋ชจ๋ ์บ์ ์ด๊ธฐํ์์ ์ ํธ๋ฆฌํฐ ํจ์
import {
isValidHexColor,
hexToRgb,
rgbToHex,
getLuminance,
isLightColor,
getContrastColor
} from '@tooning/color-token';
// ์์ ์ ํจ์ฑ ๊ฒ์ฌ
isValidHexColor('#776EFF'); // true
isValidHexColor('#invalid'); // false
// ์์ ๋ณํ
hexToRgb('#776EFF'); // { r: 119, g: 110, b: 255 }
rgbToHex(119, 110, 255); // '#776eff'
// ๋ฐ๊ธฐ ๊ณ์ฐ (0-255)
getLuminance('#776EFF'); // ์ฝ 128
getLuminance('#ffffff'); // 255
getLuminance('#000000'); // 0
// ๋ฐ๊ธฐ ํ๋จ
isLightColor('#776EFF'); // false
isLightColor('#ffffff'); // true
// ์ ๊ทผ์ฑ์ ์ํ ๋๋น ์์
getContrastColor('#776EFF'); // '#ffffff' (์ด๋์ด ๋ฐฐ๊ฒฝ์ ๋ฐ์ ํ
์คํธ)
getContrastColor('#ffffff'); // '#000000' (๋ฐ์ ๋ฐฐ๊ฒฝ์ ์ด๋์ด ํ
์คํธ)ํฌ๋ช ๋ ์ ์ฉ
import { withOpacity, withOpacityLevel, opacityLevels } from '@tooning/color-token';
// ์ง์ ํฌ๋ช
๋ ์ง์
withOpacity('#776EFF', 0.5); // 'rgba(119, 110, 255, 0.5)'
// ๋ฏธ๋ฆฌ ์ ์๋ ํฌ๋ช
๋ ๋ ๋ฒจ ์ฌ์ฉ
withOpacityLevel('#776EFF', 'light'); // 'rgba(119, 110, 255, 0.08)'
withOpacityLevel('#776EFF', 'medium'); // 'rgba(119, 110, 255, 0.12)'
withOpacityLevel('#776EFF', 'strong'); // 'rgba(119, 110, 255, 0.16)'
// ์ฌ์ฉ ๊ฐ๋ฅํ ํฌ๋ช
๋ ๋ ๋ฒจ
console.log(opacityLevels);
// {
// subtle: 0.04, // 4%
// light: 0.08, // 8%
// medium: 0.12, // 12%
// strong: 0.16, // 16%
// bold: 0.24 // 24%
// }๐ ํ ๋ง ์์คํ
ํ ๋ง ๊ด๋ฆฌ
import { themeManager, lightTheme, ThemeConfig } from '@tooning/color-token';
// ํ์ฌ ํ
๋ง ํ์ธ
const currentTheme = themeManager.getCurrentTheme();
console.log(currentTheme.name); // 'light'
// ํ
๋ง ๋ณ๊ฒฝ ๋ฆฌ์ค๋
themeManager.addThemeListener((theme) => {
console.log(`ํ
๋ง๊ฐ ${theme.name}์ผ๋ก ๋ณ๊ฒฝ๋์์ต๋๋ค`);
// UI ์
๋ฐ์ดํธ ๋ก์ง
});
// ์ปค์คํ
ํ
๋ง ์์ฑ (ํฅํ ๋คํฌ ๋ชจ๋ ๋ฑ)
const customTheme: ThemeConfig = {
name: 'custom',
colors: { /* ์ปค์คํ
์์ */ },
config: { /* ์ปค์คํ
์ค์ */ }
};
// ํ
๋ง ์ ์ฉ
themeManager.setTheme(customTheme);๐ฆ Raw ์์ ํ๋ ํธ ์ ๊ทผ
import { brandPrimary, grayscale, brandSubcategory } from '@tooning/color-token';
// ๋ธ๋๋ ๊ธฐ๋ณธ ์์ ํ๋ ํธ
console.log(brandPrimary.main[500]); // '#776EFF'
console.log(brandPrimary.character[500]); // '#2CA06E'
console.log(brandPrimary.chat[500]); // '#00C1B7'
// ๊ทธ๋ ์ด์ค์ผ์ผ
console.log(grayscale[0]); // '#ffffff' (ํฐ์)
console.log(grayscale[500]); // '#969696' (์ค๊ฐ ํ์)
console.log(grayscale[900]); // '#242424' (์ด๋์ด ํ์)
// ์๋ธ์นดํ
๊ณ ๋ฆฌ ์์
console.log(brandSubcategory.error[500]); // '#EB5757' (์๋ฌ)
console.log(brandSubcategory.info[500]); // '#5676FF' (์ ๋ณด)๐ฏ ์ปดํฌ๋ํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ํตํฉ
Angular ์์
// brand-button.component.ts
import { Component, Input, HostListener } from '@angular/core';
import { getBrandButton, getText } from '@tooning/color-token';
import type { BrandType } from '@tooning/color-token';
@Component({
selector: 'app-brand-button',
template: `
<button
[style.background-color]="backgroundColor"
[style.color]="textColor"
[style.border]="borderStyle"
[style.padding]="'12px 24px'"
[style.border-radius]="'8px'"
[style.cursor]="'pointer'"
[style.transition]="'all 0.2s ease'"
>
<ng-content></ng-content>
</button>
`
})
export class BrandButtonComponent {
@Input() brand: BrandType = 'main';
backgroundColor: string = '';
textColor: string = '';
borderStyle: string = '';
ngOnInit() {
this.updateColors();
}
ngOnChanges() {
this.updateColors();
}
private updateColors() {
this.backgroundColor = getBrandButton(this.brand, 'primary');
this.textColor = getText(this.brand, 'inverse');
this.borderStyle = `1px solid ${getBrandButton(this.brand, 'primary')}`;
}
@HostListener('mouseenter')
onMouseEnter() {
this.backgroundColor = getBrandButton(this.brand, 'primary', 'hover');
}
@HostListener('mouseleave')
onMouseLeave() {
this.backgroundColor = getBrandButton(this.brand, 'primary');
}
@HostListener('mousedown')
onMouseDown() {
this.backgroundColor = getBrandButton(this.brand, 'primary', 'pressed');
}
@HostListener('mouseup')
onMouseUp() {
this.backgroundColor = getBrandButton(this.brand, 'primary');
}
}<!-- ์ฌ์ฉ๋ฒ -->
<app-brand-button brand="main">๋ฉ์ธ ์๋น์ค</app-brand-button>
<app-brand-button brand="character">์บ๋ฆญํฐ ์์ฑ</app-brand-button>
<app-brand-button brand="chat">์ฑํ
์์</app-brand-button>
<app-brand-button brand="magic">๋งค์ง ์ ์ฉ</app-brand-button>
<app-brand-button brand="editor">์๋ํฐ ์ด๊ธฐ</app-brand-button>
<app-brand-button brand="board">๋ณด๋ ์์ฑ</app-brand-button>Angular Service ํจํด
// color-token.service.ts
import { Injectable } from '@angular/core';
import {
getBrandButton,
getText,
getBorder,
getBrandIcon,
colorTokens,
type BrandType,
type ButtonLevel,
type ButtonState
} from '@tooning/color-token';
@Injectable({
providedIn: 'root'
})
export class ColorTokenService {
// ๋ธ๋๋๋ณ ๋ฒํผ ์์
getBrandButtonColor(
brand: BrandType,
level: ButtonLevel = 'primary',
state: ButtonState = 'base'
): string {
return getBrandButton(brand, level, state);
}
// ๋ธ๋๋๋ณ ํ
์คํธ ์์
getBrandTextColor(brand: BrandType, type: string = 'primary'): string {
return getText(brand, type as any);
}
// ๊ณตํต ์์ ํ ํฐ
getLayerColor(type: keyof typeof colorTokens.layer): string {
return colorTokens.layer[type];
}
getTextColor(type: keyof typeof colorTokens.text): string {
return colorTokens.text[type];
}
// ํ
๋ง๋ณ ์์ ์ธํธ ๋ฐํ
getBrandColorSet(brand: BrandType) {
return {
primary: this.getBrandButtonColor(brand, 'primary'),
primaryHover: this.getBrandButtonColor(brand, 'primary', 'hover'),
primaryPressed: this.getBrandButtonColor(brand, 'primary', 'pressed'),
secondary: this.getBrandButtonColor(brand, 'secondary'),
text: this.getBrandTextColor(brand, 'primary'),
textHighlight: this.getBrandTextColor(brand, 'highlight'),
};
}
}Angular Directive ํจํด
// brand-color.directive.ts
import { Directive, Input, ElementRef, OnInit, OnChanges } from '@angular/core';
import { getBrandButton } from '@tooning/color-token';
import type { BrandType, ButtonLevel, ButtonState } from '@tooning/color-token';
@Directive({
selector: '[appBrandColor]'
})
export class BrandColorDirective implements OnInit, OnChanges {
@Input() appBrandColor: BrandType = 'main';
@Input() level: ButtonLevel = 'primary';
@Input() state: ButtonState = 'base';
@Input() property: 'background-color' | 'color' | 'border-color' = 'background-color';
constructor(private el: ElementRef) {}
ngOnInit() {
this.applyColor();
}
ngOnChanges() {
this.applyColor();
}
private applyColor() {
const color = getBrandButton(this.appBrandColor, this.level, this.state);
this.el.nativeElement.style[this.property] = color;
}
}<!-- Directive ์ฌ์ฉ๋ฒ -->
<button appBrandColor="main" level="primary" property="background-color">
๋ฉ์ธ ๋ฒํผ
</button>
<div appBrandColor="character" level="primary" property="border-color"
style="border: 2px solid; padding: 16px;">
์บ๋ฆญํฐ ํ
๋ง ๋ฐ์ค
</div>
<span appBrandColor="chat" level="primary" property="color">
์ฑํ
๋ธ๋๋ ํ
์คํธ
</span>Angular Pipe ํจํด
// brand-color.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
import { getBrandButton, getText, getBorder, getBrandIcon } from '@tooning/color-token';
import type { BrandType, ButtonLevel, ButtonState } from '@tooning/color-token';
@Pipe({
name: 'brandColor',
pure: true
})
export class BrandColorPipe implements PipeTransform {
transform(
brand: BrandType,
type: 'button' | 'text' | 'border' | 'icon' = 'button',
level: ButtonLevel = 'primary',
state: ButtonState = 'base'
): string {
switch (type) {
case 'button':
return getBrandButton(brand, level, state);
case 'text':
return getText(brand, level as any);
case 'border':
return getBorder(brand, level as any);
case 'icon':
return getBrandIcon(brand, level as any);
default:
return getBrandButton(brand, level, state);
}
}
}<!-- Pipe ์ฌ์ฉ๋ฒ -->
<button [style.background-color]="'main' | brandColor:'button':'primary"
[style.color]="'main' | brandColor:'text':'inverse'"
(mouseenter)="hoverColor = ('main' | brandColor:'button':'primary':'hover')"
(mouseleave)="hoverColor = null"
[style.background-color]="hoverColor || ('main' | brandColor:'button':'primary')">
๋ฉ์ธ ๋ฒํผ
</button>
<!-- ๋ค์ํ ๋ธ๋๋ ์ ์ฉ -->
<div class="brand-showcase">
<button [style.background-color]="'character' | brandColor:'button':'primary'">
์บ๋ฆญํฐ ๋ฒํผ
</button>
<span [style.color]="'chat' | brandColor:'text':'highlight'">
์ฑํ
ํ
์คํธ
</span>
<div [style.border-color]="'magic' | brandColor:'border':'highlight'"
style="border: 2px solid; padding: 8px;">
๋งค์ง ๋ฐ์ค
</div>
</div>Angular Module ์ค์
// color-token.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BrandButtonComponent } from './components/brand-button.component';
import { BrandColorDirective } from './directives/brand-color.directive';
import { BrandColorPipe } from './pipes/brand-color.pipe';
import { ColorTokenService } from './services/color-token.service';
@NgModule({
declarations: [
BrandButtonComponent,
BrandColorDirective,
BrandColorPipe
],
imports: [
CommonModule
],
providers: [
ColorTokenService
],
exports: [
BrandButtonComponent,
BrandColorDirective,
BrandColorPipe
]
})
export class ColorTokenModule { }
## ๐ ์ฑ๋ฅ ๋ฐ ์ต์ ํ
### ์บ์ฑ ์ ๋ต
- **์์ ๊ณ์ฐ ์บ์ฑ**: ๋์ผํ ๋งค๊ฐ๋ณ์๋ก ํธ์ถ๋ ์์ ํจ์ ๊ฒฐ๊ณผ๋ฅผ ์บ์
- **๋ฉ๋ชจ์ด์ ์ด์
**: ๋ณต์กํ ์์ ๋ณํ ํจ์๋ค์ ๊ฒฐ๊ณผ๋ฅผ ๋ฉ๋ชจ๋ฆฌ์ ์ ์ฅ
- **LRU ์บ์**: ์ต๋ 1000๊ฐ ํญ๋ชฉ๊น์ง ์บ์, ์ค๋๋ ํญ๋ชฉ ์๋ ์ ๊ฑฐ
### ๋ฒ๋ค ํฌ๊ธฐ ์ต์ ํ
```typescript
// ํ์ํ ํจ์๋ง import (tree shaking ์ง์)
import { getBrandButton } from '@tooning/color-token';
// ์ ์ฒด import ๋์ ๊ฐ๋ณ import ๊ถ์ฅ
import { getBrandButton, getText, getBorder } from '@tooning/color-token';๐ง ์ค์ ๋ฐ ์ปค์คํฐ๋ง์ด์ง
์์ ์์คํ ์ค์ ์ ๊ทผ
import { colorSystemConfig } from '@tooning/color-token';
// ๊ธฐ๋ณธ ์์ ์ค์ ํ์ธ
console.log(colorSystemConfig.defaultColors.disabled); // '#E0E0E0'
console.log(colorSystemConfig.defaultColors.error); // '#EB5757'
// ๊ทธ๋ ์ด์ค์ผ์ผ ๋งคํ ํ์ธ
console.log(colorSystemConfig.grayscaleMap.textPrimary); // '#242424'
console.log(colorSystemConfig.grayscaleMap.textSecondary); // '#595959'๐งช ํ ์คํธ ๋ฐ ๋๋ฒ๊น
์์ ์ ํจ์ฑ ๊ฒ์ฌ
import { isValidHexColor } from '@tooning/color-token';
// ๊ฐ๋ฐ ํ๊ฒฝ์์ ์์ ์ ํจ์ฑ ๊ฒ์ฌ
const validateColors = (colors: string[]) => {
colors.forEach(color => {
if (!isValidHexColor(color)) {
console.warn(`Invalid color detected: ${color}`);
}
});
};์บ์ ๋ชจ๋ํฐ๋ง
import { cacheUtils } from '@tooning/color-token';
// ๊ฐ๋ฐ ๋๊ตฌ์์ ์บ์ ์ํ ๋ชจ๋ํฐ๋ง
setInterval(() => {
console.log('Cache stats:', cacheUtils.getStats());
}, 5000);๐ ํ์ ์ ์
// ์ฃผ์ ํ์
๋ค
export type BrandType = 'main' | 'character' | 'chat' | 'magic' | 'editor' | 'board';
export type ButtonLevel = 'primary' | 'secondary' | 'tertiary';
export type ButtonState = 'base' | 'hover' | 'pressed' | 'activated' | 'disabled';
export type BorderType = 'subtle' | 'strong' | 'highlight' | 'disabled' | 'activated' | 'error';
export type TextType = 'primary' | 'secondary' | 'muted' | 'inverse' | 'highlight' | 'error' | 'disabled' | 'activated';
export type IconType = 'primary' | 'secondary' | 'tertiary' | 'inverse' | 'highlight' | 'error' | 'disabled' | 'activated';
export type OpacityLevel = 'subtle' | 'light' | 'medium' | 'strong' | 'bold';
export type ColorValue = string; // hex ๋๋ rgba ํํ๐ ๋ง์ด๊ทธ๋ ์ด์ ๊ฐ์ด๋
v1.x์์ v2.x๋ก
// ์ด์ ๋ฒ์ (deprecated)
import { serviceBrands } from '@tooning/color-token';
const color = serviceBrands.main.primary[500];
// ์ ๋ฒ์ (๊ถ์ฅ)
import { getBrandButton } from '@tooning/color-token';
const color = getBrandButton('main', 'primary');๐ค ๊ธฐ์ฌํ๊ธฐ
- ์ด์ ์์ฑ ๋๋ ๊ธฐ์กด ์ด์ ํ์ธ
- ๋ธ๋์น ์์ฑ:
git checkout -b feature/์๊ธฐ๋ฅ - ๋ณ๊ฒฝ์ฌํญ ์ปค๋ฐ:
git commit -m '์ ๊ธฐ๋ฅ ์ถ๊ฐ' - ๋ธ๋์น ํธ์:
git push origin feature/์๊ธฐ๋ฅ - Pull Request ์์ฑ
๐ ๋ผ์ด์ผ์ค
MIT License - ์์ธํ ๋ด์ฉ์ LICENSE ํ์ผ์ ์ฐธ์กฐํ์ธ์.
