Skip to content

comneed/lyra

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

100 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Lyra Design System

LyraλŠ” OpenAI Apps SDK λ””μžμΈ κ°€μ΄λ“œλΌμΈμ„ μ€€μˆ˜ν•˜λ©°, μ ‘κ·Όμ„±κ³Ό μ‚¬μš©μ„±μ„ μ΅œμš°μ„ μœΌλ‘œ ν•˜λŠ” ν˜„λŒ€μ μΈ React 기반 λ””μžμΈ μ‹œμŠ€ν…œμž…λ‹ˆλ‹€. Base UI Componentsλ₯Ό 기반으둜 κ΅¬μΆ•λ˜μ—ˆμœΌλ©°, 체계적인 λ””μžμΈ 토큰과 μž¬μ‚¬μš© κ°€λŠ₯ν•œ μ»΄ν¬λ„ŒνŠΈλ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€.

OpenAI Apps SDK μ€€μˆ˜: LyraλŠ” OpenAI의 λ””μžμΈ κ°€μ΄λ“œλΌμΈμ„ 따라 μΌκ΄€λ˜κ³  μ ‘κ·Ό κ°€λŠ₯ν•œ μ‚¬μš©μž κ²½ν—˜μ„ μ œκ³΅ν•©λ‹ˆλ‹€. 단색 λ°°κ²½, gradient λ―Έμ‚¬μš©, λͺ…ν™•ν•œ 계측 ꡬ쑰, μ΅œμ†Œν•œμ˜ μ•‘μ…˜ μ œν•œ λ“± OpenAI의 λ””μžμΈ 철학을 λ°˜μ˜ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

✨ μ£Όμš” νŠΉμ§•

  • πŸ€– OpenAI Apps SDK μ€€μˆ˜: OpenAI λ””μžμΈ κ°€μ΄λ“œλΌμΈμ„ λ”°λ₯Έ μΌκ΄€λœ UX
  • 🎨 체계적인 λ””μžμΈ 토큰: Style Dictionary 기반 토큰 μ‹œμŠ€ν…œμœΌλ‘œ μΌκ΄€λœ λ””μžμΈ μ–Έμ–΄ 제곡
  • ♿️ μ ‘κ·Όμ„± μš°μ„ : Base UI Components 기반의 WCAG 2.1 AA μ€€μˆ˜ μ»΄ν¬λ„ŒνŠΈ
  • πŸ“± λ°˜μ‘ν˜• λ””μžμΈ: Polaris λ°©μ‹μ˜ λ―Έλ””μ–΄ 쿼리 μ‹œμŠ€ν…œμœΌλ‘œ λͺ¨λ“  λ””λ°”μ΄μŠ€ 지원
  • 🎭 CSS Modules: μŠ€νƒ€μΌ 좩돌 μ—†λŠ” μ•ˆμ „ν•œ μŠ€μ½”ν”„ μŠ€νƒ€μΌλ§
  • πŸ§ͺ μ™„μ „ν•œ ν…ŒμŠ€νŠΈ: Vitest 기반 μœ λ‹› ν…ŒμŠ€νŠΈ 및 Storybook μΈν„°λž™μ…˜ ν…ŒμŠ€νŠΈ
  • πŸ“š ν’λΆ€ν•œ λ¬Έμ„œν™”: Storybook으둜 μž‘μ„±λœ μΈν„°λž™ν‹°λΈŒ μ»΄ν¬λ„ŒνŠΈ λ¬Έμ„œ
  • πŸ”§ TypeScript: μ™„λ²½ν•œ νƒ€μž… μ •μ˜ 제곡
  • πŸš€ λͺ¨λ…Έλ ˆν¬ ꡬ쑰: Turborepo 기반 κ³ μ„±λŠ₯ λΉŒλ“œ μ‹œμŠ€ν…œ

πŸ—οΈ λͺ¨λ…Έλ ˆν¬ ꡬ쑰

lyra/
β”œβ”€β”€ apps/
β”‚   └── web/           # μ›Ή μ• ν”Œλ¦¬μΌ€μ΄μ…˜ (Vite)
β”œβ”€β”€ packages/
β”‚   β”œβ”€β”€ design-tokens/ # λ””μžμΈ 토큰 μ‹œμŠ€ν…œ
β”‚   β”œβ”€β”€ ui/            # UI μ»΄ν¬λ„ŒνŠΈ 라이브러리 (Storybook 포함)
β”‚   β”œβ”€β”€ eslint-config/ # ESLint 곡유 μ„€μ •
β”‚   └── typescript-config/ # TypeScript 곡유 μ„€μ •
└── docs/              # ν”„λ‘œμ νŠΈ λ¬Έμ„œ

πŸš€ λΉ λ₯Έ μ‹œμž‘

ν•„μˆ˜ μš”κ΅¬μ‚¬ν•­

  • Node.js 18.x 이상
  • pnpm 10.x 이상

μ„€μΉ˜

# μ €μž₯μ†Œ 클둠
git clone https://github.com/YuJM/lyra.git
cd lyra

# μ˜μ‘΄μ„± μ„€μΉ˜
pnpm install

개발 μ„œλ²„ μ‹€ν–‰

# λͺ¨λ“  νŒ¨ν‚€μ§€λ₯Ό watch λͺ¨λ“œλ‘œ μ‹€ν–‰
pnpm dev

# Storybook λ¬Έμ„œ μ„œλ²„ μ‹€ν–‰ (localhost:6006)
pnpm dev --filter=@lyra/ui

λΉŒλ“œ

# λͺ¨λ“  νŒ¨ν‚€μ§€ λΉŒλ“œ
pnpm build

# νŠΉμ • νŒ¨ν‚€μ§€λ§Œ λΉŒλ“œ
pnpm build --filter=@lyra/ui

πŸ“¦ νŒ¨ν‚€μ§€ 상세

@lyra/design-tokens

λ””μžμΈ μ‹œμŠ€ν…œμ˜ 핡심 토큰을 κ΄€λ¦¬ν•˜λŠ” νŒ¨ν‚€μ§€μž…λ‹ˆλ‹€.

μ œκ³΅ν•˜λŠ” 토큰:

  • 색상 (Color primitives)
  • νƒ€μ΄ν¬κ·Έλž˜ν”Ό (Font family, size, weight, line height)
  • 간격 (Spacing scale)
  • 브레이크포인트 (Responsive breakpoints)
  • 그림자 (Shadow tokens)
  • ν…Œλ‘λ¦¬ (Border radius, width)
  • μ• λ‹ˆλ©”μ΄μ…˜ (Duration, easing)
  • Z-index (Layering system)

기술 μŠ€νƒ:

  • Style Dictionary (토큰 λ³€ν™˜)
  • DTCG 포맷 지원
  • CSS, JavaScript, JSON ν˜•μ‹ 좜λ ₯
  • Polaris 방식 λ―Έλ””μ–΄ 쿼리 μžλ™ 생성

μ‚¬μš©λ²•:

import '@lyra/design-tokens/css';

// CSS λ³€μˆ˜λ‘œ μ‚¬μš©
.element {
  color: var(--color-blue-600);
  padding: var(--spacing-4);
  font-size: var(--font-size-base);
}

@lyra/ui

Base UI Components 기반의 μ ‘κ·Όμ„± μš°μ„  React μ»΄ν¬λ„ŒνŠΈ λΌμ΄λΈŒλŸ¬λ¦¬μž…λ‹ˆλ‹€.

제곡 μ»΄ν¬λ„ŒνŠΈ:

Form Components

  • Button: λ‹€μ–‘ν•œ variantλ₯Ό μ§€μ›ν•˜λŠ” λ²„νŠΌ
  • Checkbox: 단일/κ·Έλ£Ή μ²΄ν¬λ°•μŠ€
  • Radio: λΌλ””μ˜€ λ²„νŠΌ 및 κ·Έλ£Ή
  • Switch: ν† κΈ€ μŠ€μœ„μΉ˜
  • Field: 폼 ν•„λ“œ ꡬ성 μš”μ†Œ (Label, Control, Description, Error)
  • Select: λ“œλ‘­λ‹€μš΄ 선택 μ»΄ν¬λ„ŒνŠΈ

Overlay Components

  • Dialog: λͺ¨λ‹¬ λ‹€μ΄μ–Όλ‘œκ·Έ
  • Tooltip: 툴팁

기술 μŠ€νƒ:

  • React 19
  • Base UI Components
  • CSS Modules + PostCSS
  • Rollup (λΉŒλ“œ μ‹œμŠ€ν…œ)
  • Vitest (ν…ŒμŠ€νŒ…)
  • Storybook (λ¬Έμ„œν™”)

μ‚¬μš©λ²•:

import { Button, Field, Select } from '@lyra/ui';
import '@lyra/ui/styles';

function App() {
  return (
    <>
      <Button variant="primary">제좜</Button>

      <Field.Root>
        <Field.Label>이메일</Field.Label>
        <Field.Control type="email" />
        <Field.Description>λ‘œκ·ΈμΈμ— μ‚¬μš©ν•  μ΄λ©”μΌμž…λ‹ˆλ‹€</Field.Description>
      </Field.Root>

      <Select.Root>
        <Select.Trigger>
          <Select.Value placeholder="μ„ νƒν•˜μ„Έμš”" />
        </Select.Trigger>
        <Select.Portal>
          <Select.Popup>
            <Select.Item value="1">μ˜΅μ…˜ 1</Select.Item>
            <Select.Item value="2">μ˜΅μ…˜ 2</Select.Item>
          </Select.Popup>
        </Select.Portal>
      </Select.Root>
    </>
  );
}

πŸ› οΈ 개발 κ°€μ΄λ“œ

λͺ…λ Ήμ–΄

개발

pnpm dev                    # λͺ¨λ“  νŒ¨ν‚€μ§€λ₯Ό watch λͺ¨λ“œλ‘œ μ‹€ν–‰
pnpm dev --filter=@lyra/ui  # νŠΉμ • νŒ¨ν‚€μ§€λ§Œ μ‹€ν–‰

λΉŒλ“œ

pnpm build                     # λͺ¨λ“  νŒ¨ν‚€μ§€ λΉŒλ“œ
pnpm build --filter=@lyra/ui   # UI νŒ¨ν‚€μ§€ 및 Storybook λΉŒλ“œ

ν…ŒμŠ€νŠΈ

pnpm test                   # λͺ¨λ“  ν…ŒμŠ€νŠΈ μ‹€ν–‰
pnpm test --filter=@lyra/ui # UI νŒ¨ν‚€μ§€ ν…ŒμŠ€νŠΈλ§Œ μ‹€ν–‰
pnpm test:watch             # Watch λͺ¨λ“œλ‘œ ν…ŒμŠ€νŠΈ

λ¦°νŒ…

pnpm lint                   # λͺ¨λ“  νŒ¨ν‚€μ§€ λ¦°νŒ…
pnpm lint:fix               # 린트 μ—λŸ¬ μžλ™ μˆ˜μ •

클린업

pnpm clean                  # node_modules 및 λΉŒλ“œ κ²°κ³Όλ¬Ό μ‚­μ œ

μƒˆ μ»΄ν¬λ„ŒνŠΈ μΆ”κ°€

  1. μ»΄ν¬λ„ŒνŠΈ 파일 생성
// packages/ui/src/components/my-component/my-component.tsx
import * as BaseUI from '@base-ui-components/react/MyComponent';
import styles from './my-component.module.css';

export function MyComponent({ children, ...props }) {
  return (
    <BaseUI.Root {...props} className={styles.root}>
      {children}
    </BaseUI.Root>
  );
}
  1. μŠ€νƒ€μΌ μž‘μ„±
/* packages/ui/src/components/my-component/my-component.module.css */
.root {
  padding: var(--spacing-4);
  background: var(--color-bg-surface-default);
}
  1. ν…ŒμŠ€νŠΈ μž‘μ„±
// packages/ui/src/components/my-component/my-component.test.tsx
import { render, screen } from '@testing-library/react';
import { MyComponent } from './my-component';

describe('MyComponent', () => {
  it('renders children', () => {
    render(<MyComponent>Test</MyComponent>);
    expect(screen.getByText('Test')).toBeInTheDocument();
  });
});
  1. Storybook μŠ€ν† λ¦¬ μΆ”κ°€
// packages/ui/src/stories/components/my-component/my-component.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { MyComponent } from '../../../components/my-component/my-component';

const meta = {
  title: "MyComponent",
  component: MyComponent,
  tags: ["autodocs"],
} satisfies Meta<typeof MyComponent>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {
  args: {
    children: "Hello World",
  },
};
  1. export μΆ”κ°€
// packages/ui/src/index.tsx
export { MyComponent } from './components/my-component/my-component';

🎨 λ””μžμΈ 토큰 μ‚¬μš© κ°€μ΄λ“œ

CSSμ—μ„œ μ‚¬μš©

.button {
  /* 색상 토큰 */
  color: var(--color-text-primary);
  background: var(--color-bg-primary-default);
  border-color: var(--color-border-default);

  /* 간격 토큰 */
  padding: var(--spacing-2) var(--spacing-4);
  margin: var(--spacing-4);

  /* νƒ€μ΄ν¬κ·Έλž˜ν”Ό 토큰 */
  font-family: var(--font-family-sans);
  font-size: var(--font-size-base);
  font-weight: var(--font-weight-medium);
  line-height: var(--line-height-normal);

  /* ν…Œλ‘λ¦¬ 토큰 */
  border-radius: var(--border-radius-md);

  /* μ• λ‹ˆλ©”μ΄μ…˜ 토큰 */
  transition-duration: var(--duration-fast);
  transition-timing-function: var(--easing-ease-in-out);
}

λ°˜μ‘ν˜• λ―Έλ””μ–΄ 쿼리

.container {
  width: 100%;
}

/* 640px μ΄ν•˜ (λͺ¨λ°”일) */
@media (--sm-down) {
  .container {
    padding: var(--spacing-2);
  }
}

/* 640px 이상 (νƒœλΈ”λ¦Ώ+) */
@media (--sm-up) {
  .container {
    padding: var(--spacing-4);
  }
}

/* 640px ~ 768px (νƒœλΈ”λ¦Ώλ§Œ) */
@media (--sm-only) {
  .container {
    padding: var(--spacing-3);
  }
}

πŸ§ͺ ν…ŒμŠ€νŒ…

μœ λ‹› ν…ŒμŠ€νŠΈ (Vitest)

# λͺ¨λ“  ν…ŒμŠ€νŠΈ μ‹€ν–‰
pnpm test

# Watch λͺ¨λ“œ
pnpm test:watch

# 컀버리지 리포트
pnpm test:coverage

Storybook μΈν„°λž™μ…˜ ν…ŒμŠ€νŠΈ

import { expect, userEvent, within } from '@storybook/test';

export const InteractionTest: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    const button = canvas.getByRole('button');

    await userEvent.click(button);
    await expect(button).toHaveAttribute('aria-pressed', 'true');
  },
};

πŸ“– λ¬Έμ„œν™”

Storybook μ‹€ν–‰

# 개발 λͺ¨λ“œ
pnpm dev --filter=@lyra/ui

# λΉŒλ“œ
pnpm build --filter=@lyra/ui

# Storybook만 μ‹€ν–‰
cd packages/ui && pnpm storybook

Storybook은 http://localhost:6006 μ—μ„œ μ‹€ν–‰λ©λ‹ˆλ‹€.

πŸ”§ 기술 μŠ€νƒ

μ½”μ–΄

  • React 19: UI 라이브러리
  • TypeScript: νƒ€μž… μ•ˆμ •μ„±
  • Base UI Components: μ ‘κ·Όμ„± μš°μ„  ν—€λ“œλ¦¬μŠ€ μ»΄ν¬λ„ŒνŠΈ

λΉŒλ“œ 도ꡬ

  • Turborepo: λͺ¨λ…Έλ ˆν¬ λΉŒλ“œ μ‹œμŠ€ν…œ
  • pnpm: νŒ¨ν‚€μ§€ λ§€λ‹ˆμ €
  • Rollup: UI νŒ¨ν‚€μ§€ λ²ˆλ“€λŸ¬
  • Vite: 개발 μ„œλ²„ 및 λΉŒλ“œ 도ꡬ

μŠ€νƒ€μΌλ§

  • CSS Modules: μŠ€μ½”ν”„ μŠ€νƒ€μΌλ§
  • PostCSS: CSS λ³€ν™˜
    • postcss-nesting
    • postcss-custom-media
    • postcss-mixins
    • postcss-global-data
  • Style Dictionary: λ””μžμΈ 토큰 λ³€ν™˜

ν…ŒμŠ€νŒ… & λ¬Έμ„œν™”

  • Vitest: μœ λ‹› ν…ŒμŠ€νŠΈ ν”„λ ˆμž„μ›Œν¬
  • Testing Library: React μ»΄ν¬λ„ŒνŠΈ ν…ŒμŠ€νŒ…
  • Storybook: μ»΄ν¬λ„ŒνŠΈ λ¬Έμ„œν™” 및 μΈν„°λž™μ…˜ ν…ŒμŠ€νŠΈ
  • Chromatic: μ‹œκ°μ  νšŒκ·€ ν…ŒμŠ€νŠΈ

μ½”λ“œ ν’ˆμ§ˆ

  • ESLint: μ½”λ“œ λ¦°νŒ…
  • TypeScript: 정적 νƒ€μž… 검사
  • Changesets: 버전 관리 및 μ²΄μΈμ§€λ‘œκ·Έ

πŸ“ 버전 관리

이 ν”„λ‘œμ νŠΈλŠ” Changesetsλ₯Ό μ‚¬μš©ν•˜μ—¬ 버전을 κ΄€λ¦¬ν•©λ‹ˆλ‹€.

체인지셋 생성

pnpm changeset
  1. λ³€κ²½λœ νŒ¨ν‚€μ§€ 선택
  2. 버전 λ²”ν”„ νƒ€μž… 선택 (major/minor/patch)
  3. λ³€κ²½ 사항 μš”μ•½ μž‘μ„±

버전 μ—…λ°μ΄νŠΈ

pnpm changeset version

νΌλΈ”λ¦¬μ‹œ

pnpm release

🀝 κΈ°μ—¬ν•˜κΈ°

μ΄μŠˆμ™€ ν’€ λ¦¬ν€˜μŠ€νŠΈλŠ” μ–Έμ œλ‚˜ ν™˜μ˜ν•©λ‹ˆλ‹€!

  1. Fork the repository
  2. Create your feature branch (git checkout -b feat/amazing-feature)
  3. Commit your changes (git commit -m 'feat: add amazing feature')
  4. Push to the branch (git push origin feat/amazing-feature)
  5. Open a Pull Request

πŸ“„ λΌμ΄μ„ μŠ€

MIT

πŸ”— 링크

About

design system for openai apps

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •