diff --git a/.changeset/move-tv-to-core.md b/.changeset/move-tv-to-core.md
new file mode 100644
index 0000000..508b647
--- /dev/null
+++ b/.changeset/move-tv-to-core.md
@@ -0,0 +1,10 @@
+---
+"@fluxloop-ai/pds-core": minor
+"@fluxloop-ai/pds-ui": minor
+---
+
+`tv` 팩토리와 `tw-merge-config` 를 `@fluxloop-ai/pds-core` 로 이동.
+
+- pds-core 가 `tv`, `VariantProps`, `twMergeConfig`, `PDS_TYPOGRAPHY_VARIANTS` 를 새로 export. `cn` 도 `extendTailwindMerge(twMergeConfig)` 로 강화돼 PDS typography 그룹(`text-body2` 등) 과 임의 색 클래스 충돌이 해소됨.
+- pds-ui 컴포넌트는 `../utils/cn`, `../utils/tv` 가 아니라 `@fluxloop-ai/pds-core` 에서 직접 import. 레지스트리 `utils` 항목은 제거됐고, 컴포넌트별 `dependencies` 에 `@fluxloop-ai/pds-core` 가 추가돼 `npx shadcn add` 한 번이면 추가 파일 fetch 없이 빌드된다.
+- 소비자 측: 기존에 `lib/utils.ts` 로 복사된 `cn` 을 쓰고 있었다면, `@fluxloop-ai/pds-core` 의 `cn` 으로 옮기거나 그대로 둬도 됨(중복일 뿐 동작 무방). 단 PDS typography 그룹 인지가 필요한 새 컴포넌트는 core 의 `cn`/`tv` 사용 권장.
diff --git a/INTEGRATION.md b/INTEGRATION.md
deleted file mode 100644
index ad624cb..0000000
--- a/INTEGRATION.md
+++ /dev/null
@@ -1,219 +0,0 @@
-# PDS 운영 모델 — 다른 레포에서 PDS를 어떻게 쓰고, 어떻게 업데이트하는가
-
-이 문서는 **소비자 레포 입장**에서 PDS를 도입·유지하는 방법, 그리고 **메인테이너 입장**에서 변경이 어떻게 흘러가는지를 설명한다.
-
-요약: PDS는 **하이브리드 배포**다. 패키지 종류에 따라 소비 방식과 업데이트 절차가 다르다.
-
----
-
-## 1. 두 가지 배포 채널
-
-| 패키지 | 배포 방식 | 소비자 입장 |
-|---|---|---|
-| `@fluxloop-ai/pds-core` | **npm 패키지** (GitHub Packages) | `pnpm add` 후 `import` |
-| `@fluxloop-ai/pds-icons` | **npm 패키지** (GitHub Packages) | `pnpm add` 후 `import` |
-| `@fluxloop-ai/pds-markdown` | **npm 패키지** (GitHub Packages) | `pnpm add` 후 `import` |
-| `@fluxloop-ai/pds-ui` | **shadcn 레지스트리** (소스 복사) | `npx shadcn add` 로 파일을 소비자 레포에 복붙 |
-
-### 왜 둘로 나눠 놨나
-
-- **core / icons / markdown** 은 한 번 정해지면 거의 안 바뀌는 인프라(토큰 · CSS · 아이콘 · 마크다운 렌더러) → 의존성으로 잠가두고 한꺼번에 업데이트하는 게 깔끔.
-- **ui** 는 컴포넌트 — 앱마다 살짝씩 다른 인터랙션 · 스타일 요구가 생긴다. shadcn 모델은 "복사해서 그 자리에서 고친다"가 핵심. 그래서 변형 자유도가 필요한 컴포넌트는 의존성으로 잠그지 않고 소스로 푼다.
-
----
-
-## 2. 소비자 레포가 PDS를 도입하는 절차
-
-### (A) 인증 — `.npmrc` 한 번 세팅
-
-GitHub Packages 는 비공개 레지스트리라 토큰이 필요. 소비자 레포에:
-
-```ini
-# .npmrc (소비자 레포 루트)
-@fluxloop-ai:registry=https://npm.pkg.github.com
-//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
-```
-
-각 개발자는 로컬 `~/.npmrc` 또는 환경변수 `GITHUB_TOKEN` 으로 자기 PAT(Personal Access Token, `read:packages` scope)를 주입. CI 에서는 `secrets.GITHUB_TOKEN` 을 그대로 쓰면 된다.
-
-### (B) core / icons / markdown 설치
-
-```bash
-pnpm add @fluxloop-ai/pds-core @fluxloop-ai/pds-icons @fluxloop-ai/pds-markdown
-```
-
-> `pds-markdown` 은 chat 메시지 렌더링이 필요한 앱만 설치하면 된다. chat 을 안 쓰는 도구라면 제외 가능.
-
-앱 루트 CSS 한 줄:
-
-```css
-/* app/globals.css 같은 데 */
-@import "@fluxloop-ai/pds-core/styles";
-```
-
-이걸 import 하면 토큰(`--pds-color-*`, `--pds-spacing-*`, `--pds-motion-*` 등) · Pretendard · 기본 스타일이 한 번에 들어온다.
-
-Tailwind 설정에 preset:
-
-```ts
-// tailwind.config.ts
-import { plutoPreset } from "@fluxloop-ai/pds-core/tailwind-preset";
-
-export default {
- presets: [plutoPreset],
- // ...
-};
-```
-
-아이콘:
-
-```tsx
-import { Plus, ArrowRight } from "@fluxloop-ai/pds-icons/icons";
-
-
-```
-
-브랜드 로고(AI 모델/벤더):
-
-```tsx
-import { Anthropic, OpenAI } from "@fluxloop-ai/pds-icons/brands";
-```
-
-Markdown 렌더러(chat 메시지 슬롯):
-
-```tsx
-import { renderMarkdown } from "@fluxloop-ai/pds-markdown";
-import "@fluxloop-ai/pds-markdown/styles";
-
-
-```
-
-### (C) ui 컴포넌트 설치 (필요한 것만)
-
-```bash
-npx shadcn add https://pluto-design-system-docs.vercel.app/r/button
-npx shadcn add https://pluto-design-system-docs.vercel.app/r/dialog
-npx shadcn add https://pluto-design-system-docs.vercel.app/r/chat-user-message
-```
-
-이게 하는 일: PDS 레포의 `packages/pds-ui/src/components/button.tsx` 를 **너의 레포 안의 `components/ui/button.tsx` 로 복사**. 의존성이 아니다. 이후 그 파일은 소비자 레포가 소유 — 마음대로 수정 가능.
-
-```tsx
-// 사용
-import { Button } from "@/components/ui/button";
-
-
-```
-
-### (D) 한 번 더 정리하면
-
-```
-소비자 레포
-├── .npmrc # 1회 세팅
-├── package.json
-│ └── deps: @fluxloop-ai/pds-core,
-│ @fluxloop-ai/pds-icons,
-│ @fluxloop-ai/pds-markdown ← 잠긴 의존성
-├── app/globals.css
-│ └── @import "@fluxloop-ai/pds-core/styles" ← 토큰 진입점
-├── components/ui/ ← shadcn 으로 복사된 PDS-UI 컴포넌트
-│ ├── button.tsx
-│ ├── dialog.tsx
-│ └── ...
-└── tailwind.config.ts
- └── presets: [plutoPreset]
-```
-
----
-
-## 3. 업데이트 — 두 채널이 다르게 동작
-
-### core / icons / markdown 업데이트 (자동 / 잠금)
-
-PDS 레포에 변경 → Changesets 가 버전 bump → publish → 소비자 레포에서:
-
-```bash
-pnpm update @fluxloop-ai/pds-core @fluxloop-ai/pds-icons @fluxloop-ai/pds-markdown
-```
-
-또는 `pnpm outdated` 로 확인. semver 따라 자동 호환. 토큰 값이 변하거나 새 토큰이 추가되면 그게 다음 빌드부터 반영된다. **소비자가 수정하지 않은 영역**이라 충돌 없음.
-
-### ui 업데이트 (수동 / 명시적)
-
-shadcn 모델은 **자동 업데이트가 없다**. 컴포넌트는 이미 소비자 레포의 일부니까 PDS 가 변경됐는지 모른다. 두 가지 길:
-
-1. **재실행해서 덮어쓰기** (소비자가 수정 안 했을 때)
- ```bash
- npx shadcn add https://pluto-design-system-docs.vercel.app/r/button --overwrite
- ```
- 해당 파일이 최신 PDS 소스로 교체된다.
-
-2. **수동 머지** (소비자가 컴포넌트를 수정해서 살짝 갈라졌을 때)
- - PDS 레포의 새 버전 소스를 보고 변경분만 골라 적용.
- - 또는 `git diff` 로 비교해서 cherry-pick.
-
-#### Tradeoff
-
-- **자유도**: 컴포넌트별로 변형 · 삭제 · 확장 가능 (앱마다 다른 요구 흡수).
-- **부담**: 업데이트가 명시적 → 누락되기 쉬움. PDS 변경 공지를 잘 받아야 한다.
-
-이 부담을 줄이려고 보통:
-- PDS 가 changelog (`CHANGELOG.md`) + GitHub Release 로 변경분을 명시적으로 공지.
-- 소비자 앱들은 주기적으로 (예: 스프린트 시작 때) 의도적인 "PDS sync" 시간을 따로 잡는다.
-
----
-
-## 4. 변경이 PDS 안에서 흘러가는 경로 (메인테이너 시점)
-
-```
-1. PDS 레포에서 컴포넌트 / 토큰 수정 (예: Button.tsx)
-2. PR 만들 때 `pnpm changeset` 으로 변경 종류 기록 (major/minor/patch)
-3. PR 머지 → main push
-4. release.yml 발동
- → Changesets 가 누적된 changeset 들을 모아 "Release PR" 자동 생성
- (이 PR 안에 version bump + CHANGELOG 업데이트가 들어있음)
-5. 메인테이너가 Release PR 검토 후 머지
-6. release.yml 다시 발동
- → core/icons/markdown 은 GitHub Packages 에 publish
- → ui 는 registry.json 이 갱신되어 shadcn 으로 항상 최신을 받게 됨
- → GitHub Release 자동 생성
-7. 소비자 레포들이 변경분을 본다
- → core/icons/markdown 은 `pnpm update` 로 받음
- → ui 는 필요한 컴포넌트만 `npx shadcn add ... --overwrite` 로 받음
-```
-
-**핵심**: PDS 메인테이너는 changeset 만 신경 쓰면 된다. 버전 매기기 · publish · 태그 · changelog 는 모두 워크플로우가 한다.
-
----
-
-## 5. 실무에서 자주 나오는 질문들
-
-**Q. 소비자가 PDS-UI 컴포넌트를 고쳐서 쓰고 싶을 때**
-→ 그게 바로 shadcn 모델의 의도. 복사된 파일은 그 레포 소유라 마음대로 수정. 단 PDS 자체에 반영하고 싶다면 PR 을 PDS 레포로 올려야 한다.
-
-**Q. 토큰 값이 바뀌었는데 컴포넌트 코드는 그대로일 때**
-→ core 만 업데이트하면 된다. 컴포넌트는 `var(--pds-*)` 만 참조하니까 자동으로 새 값 따라간다.
-
-**Q. PDS 가 0.x 면 잦은 breaking change 가 있을 텐데**
-→ 그래서 0.x 동안은 소비자 앱이 PDS 버전을 핀(`^0.1.0` 이 아닌 `~0.1.0` 또는 정확한 버전)으로 잠그는 것도 옵션. 1.0 이후엔 semver 로 안전. README 에 명시한 이유.
-
-**Q. 앱이 두 개 이상이면 동기화는 어떻게**
-→ 모든 앱이 같은 changelog 를 보고 같은 타이밍에 sync. PDS 가 메인 채널(GitHub Release / Slack 알림 등) 으로 공지하면 된다. 강제할 도구는 없음 (의도적). 앱별로 PDS 버전 다르게 가져갈 수 있는 게 단점이자 장점.
-
-**Q. Markdown 렌더러를 직접 만들고 싶다**
-→ `pds-markdown` 은 권장 구현체일 뿐, `ChatUserMessage` · `ChatAssistantMessage` 가 받는 `renderMarkdown?: (text: string) => ReactNode` slot 에 임의 렌더러를 끼워도 된다. streaming 친화 변형, 자체 syntax highlighter, Anthropic SDK helper 등 — 앱이 원하는 대로.
-
----
-
-## 6. 정리
-
-| 측면 | core / icons / markdown | ui |
-|---|---|---|
-| 배포 | GitHub Packages npm | shadcn 레지스트리 |
-| 설치 | `pnpm add` | `npx shadcn add ` |
-| 위치 | `node_modules/` | 소비자 레포의 `components/ui/` |
-| 수정 | ❌ (잠긴 의존성) | ✅ (소비자 소유) |
-| 업데이트 | `pnpm update` | `--overwrite` 재실행 / 수동 머지 |
-| semver | 따른다 | 따르지만 강제력 없음 (소스 복사라서) |
-
-도입 비용은 낮다 (`.npmrc` 한 번 + `pnpm add`). 운영 비용은 ui 채널의 명시적 sync 가 핵심 — 이걸 의식적으로 짜면 두 앱 이상에서도 PDS 가 깔끔하게 굴러간다.
diff --git a/README.md b/README.md
index fa1a280..fa98ac1 100644
--- a/README.md
+++ b/README.md
@@ -2,31 +2,46 @@
The Pluto 앱의 **단일 UI 소스**. 토큰 · 프리미티브 · AI 친화 컴포넌트가 하나의 체계로 묶인 모노레포.
-- **코드가 SSOT** — Figma는 역생성으로 유지
+- **코드가 SSOT** — Figma 는 역생성으로 유지
- **macOS-first, Windows-ready** — Electron 37+ 대상 최적화
- **AI 친화** — Claude / Cursor 가 읽고 수정하기 쉬운 구조
- **Stack** — Tailwind v4 + Radix UI + `tailwind-variants`, React 19
+---
+
## 상태
-**Pre-1.0.** 0.x 동안은 breaking change 가 자유롭게 들어옵니다. 1.0 이전까지는 마이너 단위 차이도 깨지는 변경을 포함할 수 있습니다. 마이너 릴리즈는 아래 표에 기록합니다.
+**Pre-1.0.** 0.x 동안은 breaking change 가 자유롭게 들어옵니다. 1.0 이전까지는 마이너 단위 차이도 깨지는 변경을 포함할 수 있습니다.
| 버전 | 날짜 | 요약 |
| --- | --- | --- |
| `0.1.0` | 2026-05-05 | 첫 공개 — 토큰 7 카테고리(color · typography · spacing · radius · shadow · z-index · motion), 컴포넌트 8 그룹(Primitives · Actions · Form · Layout · Navigation · Overlays · Feedback · AI/Agent Chat), `pds-markdown` chat 슬롯 렌더러 동봉 |
+---
+
## 패키지 구성
-| 패키지 | 역할 | 배포 |
-| --- | --- | --- |
-| [`@fluxloop-ai/pds-core`](packages/pds-core) | 토큰(`@theme`), Tailwind preset, Pretendard, motion | GitHub Packages (npm) |
-| [`@fluxloop-ai/pds-ui`](packages/pds-ui) | Radix + Tailwind 컴포넌트 (소스 노출) | shadcn 레지스트리 |
-| [`@fluxloop-ai/pds-icons`](packages/pds-icons) | phosphor 래퍼 + PDS 네이티브 아이콘 + 브랜드 아이콘 | GitHub Packages (npm) |
-| [`@fluxloop-ai/pds-markdown`](packages/pds-markdown) | chat 슬롯용 markdown 렌더러 (react-markdown + remark-gfm) | GitHub Packages (npm) |
+PDS 는 **하이브리드 배포** — 패키지마다 소비 방식이 다르다.
+
+| 패키지 | 역할 | 배포 채널 | 소비자 입장 |
+| --- | --- | --- | --- |
+| [`@fluxloop-ai/pds-core`](packages/pds-core) | 토큰(`@theme`), Tailwind preset, Pretendard, motion, utility helpers (`cn`/`tv`) | **npm** (GitHub Packages) | `pnpm add` 후 `import` |
+| [`@fluxloop-ai/pds-icons`](packages/pds-icons) | phosphor 래퍼 + PDS 네이티브 아이콘 + 브랜드 아이콘 | **npm** (GitHub Packages) | `pnpm add` 후 `import` |
+| [`@fluxloop-ai/pds-markdown`](packages/pds-markdown) | chat 슬롯용 markdown 렌더러 (react-markdown + remark-gfm) | **npm** (GitHub Packages) | `pnpm add` 후 `import` |
+| [`@fluxloop-ai/pds-ui`](packages/pds-ui) | Radix + Tailwind 컴포넌트 | **shadcn 레지스트리** (소스 복사) | `npx shadcn add` 로 파일을 소비자 레포에 복사 |
+
+### 왜 둘로 나눠 놨나
+
+- **core / icons / markdown** 은 한 번 정해지면 거의 안 바뀌는 인프라(토큰 · CSS · 아이콘 · 마크다운). 의존성으로 잠가두고 한꺼번에 업데이트하는 게 깔끔.
+- **ui** 는 컴포넌트 — 앱마다 살짝씩 다른 인터랙션 · 스타일 요구가 생긴다. shadcn 모델은 "복사해서 그 자리에서 고친다" 가 핵심. 변형 자유도가 필요한 컴포넌트는 의존성으로 잠그지 않고 소스로 푼다.
-## 설치 (앱 레포)
+---
-> 비공개 GitHub Packages 레지스트리에서 배포됩니다. 소비자 레포의 `.npmrc` 가 필요합니다.
+## 빠른 시작
+
+### 1. `.npmrc` 인증 (1회)
+
+GitHub Packages 는 비공개 레지스트리. 소비자 레포 루트에:
```ini
# .npmrc
@@ -34,21 +49,199 @@ The Pluto 앱의 **단일 UI 소스**. 토큰 · 프리미티브 · AI 친화
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
```
+각 개발자는 로컬 `~/.npmrc` 또는 환경변수 `GITHUB_TOKEN` 으로 자기 PAT(`read:packages` scope) 를 주입. CI 에서는 `secrets.GITHUB_TOKEN` 을 그대로 쓰면 된다.
+
+### 2. core / icons / markdown 설치
+
```bash
-# 토큰 · 아이콘 · markdown (npm 으로 설치)
pnpm add @fluxloop-ai/pds-core @fluxloop-ai/pds-icons @fluxloop-ai/pds-markdown
-
-# 컴포넌트 (shadcn 으로 소스 복사)
-npx shadcn add https://pluto-design-system-docs.vercel.app/r/button
```
-앱 루트 CSS 에 토큰 연결:
+> `pds-markdown` 은 chat 메시지 렌더링이 필요한 앱만 설치하면 된다.
+
+### 3. 토큰 + Tailwind preset 연결
+
+앱 루트 CSS 한 줄:
```css
+/* app/globals.css 같은 데 */
@import "@fluxloop-ai/pds-core/styles";
```
-> 소비자 레포에서 PDS 를 도입 · 유지하는 자세한 절차(인증 · 업데이트 흐름 · tradeoff · FAQ)는 [`INTEGRATION.md`](./INTEGRATION.md) 참조.
+이걸 import 하면 토큰(`--pds-color-*`, `--pds-spacing-*`, `--pds-motion-*` 등) · Pretendard · 기본 reset 이 한 번에 들어온다.
+
+Tailwind 설정에 preset:
+
+```ts
+// tailwind.config.ts
+import { plutoPreset } from "@fluxloop-ai/pds-core/tailwind-preset";
+
+export default {
+ presets: [plutoPreset],
+ // ...
+};
+```
+
+### 4. UI 컴포넌트 설치 (필요한 것만)
+
+```bash
+npx shadcn add https://pluto-design-system-docs.vercel.app/r/button
+npx shadcn add https://pluto-design-system-docs.vercel.app/r/dialog
+npx shadcn add https://pluto-design-system-docs.vercel.app/r/chat-user-message
+```
+
+설치 가능한 컴포넌트 전체 목록은 레지스트리 인덱스에서 확인 — 브라우저나 `curl` 로 한번 받아두면 된다:
+
+```
+https://pluto-design-system-docs.vercel.app/r/registry.json
+```
+
+(`/r/index.json` 아님 — shadcn 컨벤션상 인덱스 파일명은 `registry.json` 이다.)
+
+이게 하는 일: PDS 레포의 `packages/pds-ui/src/components/button.tsx` 를 **너의 레포 안의 `components/ui/button.tsx` 로 복사**. 의존성이 아니다. 이후 그 파일은 소비자 레포가 소유 — 마음대로 수정 가능.
+
+복사된 파일은 `@fluxloop-ai/pds-core` 에서 `cn` / `tv` / `VariantProps` 를 import 한다 — 위 3 단계에서 `pds-core` 를 이미 깔았다면 그대로 빌드된다. shadcn 의 `lib/utils.ts` 를 따로 만들 필요 없음 (레지스트리에 `utils` 항목이 없고, `cn` 의 SSOT 는 `pds-core`).
+
+---
+
+## 사용
+
+### 컴포넌트
+
+```tsx
+import { Button } from "@/components/ui/button";
+
+
+```
+
+### 아이콘
+
+```tsx
+import { Plus, ArrowRight } from "@fluxloop-ai/pds-icons/icons";
+
+
+```
+
+브랜드 로고(AI 모델/벤더):
+
+```tsx
+import { Anthropic, OpenAI } from "@fluxloop-ai/pds-icons/brands";
+```
+
+### Markdown 렌더러 (chat 메시지 슬롯)
+
+```tsx
+import { renderMarkdown } from "@fluxloop-ai/pds-markdown";
+import "@fluxloop-ai/pds-markdown/styles";
+
+
+```
+
+### Utility helpers (자체 컴포넌트 만들 때)
+
+PDS 와 동일한 `cn` / `tv` 헬퍼 사용 가능. `tv` 는 PDS typography variants(`text-body2` 등) 까지 인식하는 `tailwind-variants` 래퍼이고, `cn` 도 같은 config 를 공유한다.
+
+```tsx
+import { cn, tv, type VariantProps } from "@fluxloop-ai/pds-core";
+```
+
+소비자 레포에서 별도로 `tailwind-variants` 를 설치할 필요 없음 — `pds-core` 가 잠긴 의존으로 갖고 있다.
+
+### 소비자 레포 최종 구조
+
+```
+소비자 레포
+├── .npmrc # 1회 세팅
+├── package.json
+│ └── deps: @fluxloop-ai/pds-core,
+│ @fluxloop-ai/pds-icons,
+│ @fluxloop-ai/pds-markdown ← 잠긴 의존성
+├── app/globals.css
+│ └── @import "@fluxloop-ai/pds-core/styles" ← 토큰 진입점
+├── components/ui/ ← shadcn 으로 복사된 PDS-UI 컴포넌트
+│ ├── button.tsx
+│ ├── dialog.tsx
+│ └── ...
+└── tailwind.config.ts
+ └── presets: [plutoPreset]
+```
+
+---
+
+## 업데이트
+
+두 채널이 다르게 동작.
+
+### core / icons / markdown — 자동 / 잠금
+
+PDS 레포에 변경 → Changesets 가 버전 bump → publish → 소비자 레포에서:
+
+```bash
+pnpm update @fluxloop-ai/pds-core @fluxloop-ai/pds-icons @fluxloop-ai/pds-markdown
+```
+
+semver 따라 자동 호환. 토큰 값이 변하거나 새 토큰이 추가되면 다음 빌드부터 반영. **소비자가 수정하지 않는 영역**이라 충돌 없음.
+
+### ui — 수동 / 명시적
+
+shadcn 모델은 **자동 업데이트가 없다**. 컴포넌트는 이미 소비자 레포의 일부니까 PDS 가 변경됐는지 모른다. 두 가지 길:
+
+1. **재실행해서 덮어쓰기** (소비자가 수정 안 했을 때)
+ ```bash
+ npx shadcn add https://pluto-design-system-docs.vercel.app/r/button --overwrite
+ ```
+2. **수동 머지** (소비자가 컴포넌트를 수정해서 살짝 갈라졌을 때)
+ - PDS 레포의 새 버전 소스를 보고 변경분만 골라 적용.
+ - 또는 `git diff` 로 비교해서 cherry-pick.
+
+#### Tradeoff
+
+- **자유도**: 컴포넌트별 변형 · 삭제 · 확장 가능 (앱마다 다른 요구 흡수).
+- **부담**: 업데이트가 명시적 → 누락 위험. PDS 변경 공지(GitHub Release / `CHANGELOG.md` / Slack) 를 잘 받아야 함.
+
+부담을 줄이려고 보통 — 앱들은 주기적으로 (예: 스프린트 시작 때) 의도적인 "PDS sync" 시간을 따로 잡는다.
+
+---
+
+## FAQ
+
+**Q. UI 컴포넌트를 고쳐서 쓰고 싶다**
+→ 그게 바로 shadcn 모델의 의도. 복사된 파일은 그 레포 소유라 마음대로 수정. PDS 자체에 반영하고 싶다면 PDS 레포로 PR.
+
+**Q. 토큰 값만 바뀌고 컴포넌트 코드는 그대로일 때**
+→ `pds-core` 만 업데이트. 컴포넌트는 `var(--pds-*)` 만 참조하니 자동으로 새 값을 따라간다.
+
+**Q. 0.x 동안 잦은 breaking change 가 걱정된다**
+→ 0.x 동안은 정확한 버전(`~0.1.0` 또는 정확 버전) 으로 핀 권장. 1.0 이후엔 semver 안전.
+
+**Q. 앱이 두 개 이상이면 동기화는?**
+→ 모든 앱이 같은 changelog 를 보고 같은 타이밍에 sync. PDS 가 메인 채널(GitHub Release / Slack 알림 등) 로 공지하면 된다. 강제할 도구는 없음 (의도적). 앱별로 PDS 버전 다르게 가져갈 수 있는 게 단점이자 장점.
+
+**Q. Markdown 렌더러를 직접 만들고 싶다**
+→ `pds-markdown` 은 권장 구현체일 뿐. `ChatUserMessage` · `ChatAssistantMessage` 가 받는 `renderMarkdown?: (text: string) => ReactNode` slot 에 임의 렌더러를 끼워도 된다. streaming 친화 변형, 자체 syntax highlighter, Anthropic SDK helper 등 — 앱이 원하는 대로.
+
+---
+
+## 메인테이너 — 변경이 PDS 안에서 흘러가는 경로
+
+```
+1. PDS 레포에서 컴포넌트 / 토큰 수정 (예: Button.tsx)
+2. PR 만들 때 `pnpm changeset` 으로 변경 종류 기록 (major/minor/patch)
+3. PR 머지 → main push
+4. release.yml 발동
+ → Changesets 가 누적된 changeset 들을 모아 "Release PR" 자동 생성
+ (이 PR 안에 version bump + CHANGELOG 업데이트)
+5. 메인테이너가 Release PR 검토 후 머지
+6. release.yml 다시 발동
+ → core/icons/markdown 은 GitHub Packages 에 publish
+ → ui 는 registry.json 이 갱신되어 shadcn 으로 항상 최신을 받게 됨
+ → GitHub Release 자동 생성
+7. 소비자 레포들이 변경분을 수신 (위 "업데이트" 절차)
+```
+
+**핵심**: 메인테이너는 changeset 만 신경 쓰면 된다. 버전 매기기 · publish · 태그 · changelog 는 모두 워크플로우가 처리.
+
+---
## 문서 (카탈로그)
@@ -71,7 +264,9 @@ pnpm --filter docs dev
# → http://localhost:3000
```
-> 0.1.1 부터 Vercel preview 도메인(`https://pluto-design-system-docs.vercel.app`)에서 docs + shadcn registry 가 호스팅됩니다. 커스텀 도메인은 후속.
+> 0.1.1 부터 https://pluto-design-system-docs.vercel.app 에서 docs + shadcn registry 가 호스팅됩니다. 커스텀 도메인은 후속.
+
+---
## 개발
@@ -101,6 +296,8 @@ pnpm changeset
`major` / `minor` / `patch` 중 선택 → 짧은 요약 작성. 누적된 changeset 은 머지 시점에 Release PR 로 자동 묶여 다음 버전에 반영됩니다.
+---
+
## 라이선스
UNLICENSED — Fluxloop AI 내부 사용 전용. 외부 배포 · 재공개 금지.
diff --git a/apps/docs/app/preview/app-shell/page.tsx b/apps/docs/app/preview/app-shell/page.tsx
index 97d0138..71e4ddc 100644
--- a/apps/docs/app/preview/app-shell/page.tsx
+++ b/apps/docs/app/preview/app-shell/page.tsx
@@ -45,7 +45,7 @@ export default function AppShellPreviewPage() {
-
+
{/* 사이드바 토글 — 사이드바 열림/닫힘 무관하게 같은 자리 */}
t.tab);
return (
-
+
-
+
-
+
-
+ =11",
@@ -55,7 +65,8 @@
},
"dependencies": {
"clsx": "^2.1.1",
- "tailwind-merge": "^3.5.0"
+ "tailwind-merge": "^3.5.0",
+ "tailwind-variants": "^3.2.2"
},
"devDependencies": {
"framer-motion": "^11.11.0",
diff --git a/packages/pds-core/scripts/verify-exports.mjs b/packages/pds-core/scripts/verify-exports.mjs
new file mode 100644
index 0000000..c5a7734
--- /dev/null
+++ b/packages/pds-core/scripts/verify-exports.mjs
@@ -0,0 +1,50 @@
+#!/usr/bin/env node
+/**
+ * publish 직전 가드 — pds-core 가 노출해야 하는 named export 가 빠진 채
+ * 패키지가 나가는 사고를 막는다. 과거 dist 가 src 보다 더 많은 export 를
+ * 갖고 있었던(stale dist) 시기에 src/index.ts 에서 export 라인이 빠진 채
+ * 대량 publish 가 일어났던 사고에서 출발한 검증.
+ *
+ * dist/index.js 를 실제로 import 해서 named export 존재를 확인한다.
+ * 빠진 게 있으면 비제로 종료. CI 의 build / pre-publish 단계에서 호출.
+ */
+import { fileURLToPath } from "node:url";
+import path from "node:path";
+
+const here = path.dirname(fileURLToPath(import.meta.url));
+const distEntry = path.resolve(here, "../dist/index.js");
+
+const required = [
+ "cn",
+ "tv",
+ "twMergeConfig",
+ "PDS_TYPOGRAPHY_VARIANTS",
+ "pdsCardIn",
+ "pdsDuration",
+ "pdsEasing",
+ "pdsFadeCollapse",
+ "pdsStepIn",
+ "pdsStylesEntry",
+];
+
+let mod;
+try {
+ mod = await import(distEntry);
+} catch (err) {
+ console.error(`✖ pds-core dist 를 import 할 수 없음: ${distEntry}`);
+ console.error(` → tsup 빌드가 먼저 실행됐는지 확인.`);
+ console.error(err);
+ process.exit(1);
+}
+
+const missing = required.filter((name) => !(name in mod));
+if (missing.length) {
+ console.error("✖ pds-core export 검증 실패 — 다음 named export 가 누락:");
+ for (const name of missing) console.error(` - ${name}`);
+ console.error(
+ "\n → packages/pds-core/src/index.ts 에서 해당 심볼을 re-export 하고 빌드를 다시 돌려라.",
+ );
+ process.exit(1);
+}
+
+console.log(`✓ pds-core exports OK — ${required.length} named exports present`);
diff --git a/packages/pds-core/src/index.ts b/packages/pds-core/src/index.ts
index 86bbb65..200f043 100644
--- a/packages/pds-core/src/index.ts
+++ b/packages/pds-core/src/index.ts
@@ -7,3 +7,5 @@ export {
} from "./motion/variants.js";
export { pdsStylesEntry } from "./tailwind-preset.js";
export { cn } from "./utils/cn.js";
+export { tv, type VariantProps } from "./utils/tv.js";
+export { PDS_TYPOGRAPHY_VARIANTS, twMergeConfig } from "./utils/tw-merge-config.js";
diff --git a/packages/pds-core/src/utils/cn.ts b/packages/pds-core/src/utils/cn.ts
index a70ebb6..1ca5217 100644
--- a/packages/pds-core/src/utils/cn.ts
+++ b/packages/pds-core/src/utils/cn.ts
@@ -1,5 +1,8 @@
import { type ClassValue, clsx } from "clsx";
-import { twMerge } from "tailwind-merge";
+import { extendTailwindMerge } from "tailwind-merge";
+import { twMergeConfig } from "./tw-merge-config.js";
+
+const twMerge = extendTailwindMerge(twMergeConfig);
export function cn(...inputs: ClassValue[]): string {
return twMerge(clsx(inputs));
diff --git a/packages/pds-ui/src/utils/tv.ts b/packages/pds-core/src/utils/tv.ts
similarity index 71%
rename from packages/pds-ui/src/utils/tv.ts
rename to packages/pds-core/src/utils/tv.ts
index db34b76..339402f 100644
--- a/packages/pds-ui/src/utils/tv.ts
+++ b/packages/pds-core/src/utils/tv.ts
@@ -5,10 +5,11 @@
* 않게 한다.
*
* pds-ui 컴포넌트는 `tailwind-variants` 에서 직접 `tv` 를 가져오지 말고 이 모듈
- * 의 `tv` 를 사용해야 한다.
+ * 의 `tv` 를 사용해야 한다. 외부 소비자도 PDS typography group 을 인지하는
+ * `tv` 가 필요하면 `@fluxloop-ai/pds-core` 에서 가져오면 된다.
*/
import { createTV } from "tailwind-variants";
-import { twMergeConfig } from "./tw-merge-config";
+import { twMergeConfig } from "./tw-merge-config.js";
const tv = createTV({ twMergeConfig });
diff --git a/packages/pds-ui/src/utils/tw-merge-config.ts b/packages/pds-core/src/utils/tw-merge-config.ts
similarity index 100%
rename from packages/pds-ui/src/utils/tw-merge-config.ts
rename to packages/pds-core/src/utils/tw-merge-config.ts
diff --git a/packages/pds-core/tsup.config.ts b/packages/pds-core/tsup.config.ts
index 4164ec3..c39255f 100644
--- a/packages/pds-core/tsup.config.ts
+++ b/packages/pds-core/tsup.config.ts
@@ -5,6 +5,8 @@ export default defineConfig({
index: "src/index.ts",
"motion/variants": "src/motion/variants.ts",
"utils/cn": "src/utils/cn.ts",
+ "utils/tv": "src/utils/tv.ts",
+ "utils/tw-merge-config": "src/utils/tw-merge-config.ts",
"tailwind-preset": "src/tailwind-preset.ts",
},
format: ["esm"],
diff --git a/packages/pds-ui/package.json b/packages/pds-ui/package.json
index d58e51d..bae8173 100644
--- a/packages/pds-ui/package.json
+++ b/packages/pds-ui/package.json
@@ -10,7 +10,6 @@
".": "./src/index.ts",
"./components/*": "./src/components/*.tsx",
"./types": "./src/types/index.ts",
- "./utils/cn": "./src/utils/cn.ts",
"./registry.json": "./registry.json"
},
"files": [
diff --git a/packages/pds-ui/registry.json b/packages/pds-ui/registry.json
index a6663fb..b7c7132 100644
--- a/packages/pds-ui/registry.json
+++ b/packages/pds-ui/registry.json
@@ -3,27 +3,12 @@
"name": "pluto-design-system",
"homepage": "https://pluto-design-system-docs.vercel.app",
"items": [
- {
- "name": "utils",
- "type": "registry:lib",
- "title": "cn utility",
- "description": "clsx + tailwind-merge helper used by every PDS component.",
- "dependencies": ["clsx", "tailwind-merge"],
- "files": [
- {
- "path": "src/utils/cn.ts",
- "type": "registry:lib",
- "target": "lib/utils.ts"
- }
- ]
- },
{
"name": "separator",
"type": "registry:ui",
"title": "Separator",
"description": "Radix-based horizontal/vertical divider using PDS line tokens.",
- "dependencies": ["@radix-ui/react-separator", "tailwind-variants"],
- "registryDependencies": ["utils"],
+ "dependencies": ["@fluxloop-ai/pds-core", "@radix-ui/react-separator"],
"files": [
{
"path": "src/components/separator.tsx",
@@ -37,8 +22,7 @@
"type": "registry:ui",
"title": "Avatar",
"description": "Radix Avatar, 8 sizes, person/company/academy fallback icons from pds-icons.",
- "dependencies": ["@radix-ui/react-avatar", "@fluxloop-ai/pds-icons", "tailwind-variants"],
- "registryDependencies": ["utils"],
+ "dependencies": ["@fluxloop-ai/pds-core", "@radix-ui/react-avatar", "@fluxloop-ai/pds-icons"],
"files": [
{
"path": "src/components/avatar.tsx",
@@ -52,8 +36,7 @@
"type": "registry:ui",
"title": "Icon",
"description": "phosphor wrapper with PDS size/color tokens. Accepts custom SVG components.",
- "dependencies": ["@fluxloop-ai/pds-icons", "tailwind-variants"],
- "registryDependencies": ["utils"],
+ "dependencies": ["@fluxloop-ai/pds-core", "@fluxloop-ai/pds-icons"],
"files": [
{
"path": "src/components/icon.tsx",
@@ -67,8 +50,7 @@
"type": "registry:ui",
"title": "Button",
"description": "4 variants × 3 sizes, icon slots, loading state, asChild support.",
- "dependencies": ["@radix-ui/react-slot", "@fluxloop-ai/pds-icons", "tailwind-variants"],
- "registryDependencies": ["utils"],
+ "dependencies": ["@fluxloop-ai/pds-core", "@radix-ui/react-slot", "@fluxloop-ai/pds-icons"],
"files": [
{
"path": "src/components/button.tsx",
@@ -83,11 +65,10 @@
"title": "Input",
"description": "9-slot TextField with invalid/positive/disabled/readOnly + leading/trailing + reset.",
"dependencies": [
+ "@fluxloop-ai/pds-core",
"@radix-ui/react-compose-refs",
- "@fluxloop-ai/pds-icons",
- "tailwind-variants"
+ "@fluxloop-ai/pds-icons"
],
- "registryDependencies": ["utils"],
"files": [
{
"path": "src/components/input.tsx",
@@ -101,8 +82,7 @@
"type": "registry:ui",
"title": "Tooltip",
"description": "Radix Tooltip with size, shortcut slot, and mode(hover/always/click).",
- "dependencies": ["@radix-ui/react-tooltip", "tailwind-variants"],
- "registryDependencies": ["utils"],
+ "dependencies": ["@fluxloop-ai/pds-core", "@radix-ui/react-tooltip"],
"files": [
{
"path": "src/components/tooltip.tsx",
@@ -117,11 +97,10 @@
"title": "DropdownMenu",
"description": "Radix DropdownMenu with items/checkbox/radio/submenu/shortcut slot.",
"dependencies": [
+ "@fluxloop-ai/pds-core",
"@radix-ui/react-dropdown-menu",
- "@fluxloop-ai/pds-icons",
- "tailwind-variants"
+ "@fluxloop-ai/pds-icons"
],
- "registryDependencies": ["utils"],
"files": [
{
"path": "src/components/dropdown-menu.tsx",
@@ -135,8 +114,8 @@
"type": "registry:ui",
"title": "Dialog",
"description": "Radix Dialog — popup/full × sm/md/lg/xl × fixed/free. Navigation/Body/ActionArea slots.",
- "dependencies": ["@radix-ui/react-dialog", "@fluxloop-ai/pds-icons", "tailwind-variants"],
- "registryDependencies": ["utils", "close-button"],
+ "dependencies": ["@fluxloop-ai/pds-core", "@radix-ui/react-dialog", "@fluxloop-ai/pds-icons"],
+ "registryDependencies": ["close-button"],
"files": [
{
"path": "src/components/dialog.tsx",
@@ -150,8 +129,8 @@
"type": "registry:ui",
"title": "Toast",
"description": "Radix Toast with info/success/warning/error variants.",
- "dependencies": ["@radix-ui/react-toast", "@fluxloop-ai/pds-icons", "tailwind-variants"],
- "registryDependencies": ["utils", "close-button"],
+ "dependencies": ["@fluxloop-ai/pds-core", "@radix-ui/react-toast", "@fluxloop-ai/pds-icons"],
+ "registryDependencies": ["close-button"],
"files": [
{
"path": "src/components/toast.tsx",
@@ -165,8 +144,7 @@
"type": "registry:ui",
"title": "ScrollArea",
"description": "Radix ScrollArea with macOS-style thin scrollbar + hover expand.",
- "dependencies": ["@radix-ui/react-scroll-area", "tailwind-variants"],
- "registryDependencies": ["utils"],
+ "dependencies": ["@fluxloop-ai/pds-core", "@radix-ui/react-scroll-area"],
"files": [
{
"path": "src/components/scroll-area.tsx",
@@ -180,8 +158,12 @@
"type": "registry:ui",
"title": "Popover",
"description": "Radix Popover — sm/md/lg × normal/custom. Header/Body/ActionArea slots.",
- "dependencies": ["@radix-ui/react-popover", "@fluxloop-ai/pds-icons", "tailwind-variants"],
- "registryDependencies": ["utils", "close-button"],
+ "dependencies": [
+ "@fluxloop-ai/pds-core",
+ "@radix-ui/react-popover",
+ "@fluxloop-ai/pds-icons"
+ ],
+ "registryDependencies": ["close-button"],
"files": [
{
"path": "src/components/popover.tsx",
@@ -195,8 +177,7 @@
"type": "registry:ui",
"title": "Spinner",
"description": "Pure SVG loading indicator. sm/md/lg (16/20/28px). Respects prefers-reduced-motion.",
- "dependencies": ["tailwind-variants"],
- "registryDependencies": ["utils"],
+ "dependencies": ["@fluxloop-ai/pds-core"],
"files": [
{
"path": "src/components/spinner.tsx",
@@ -210,8 +191,7 @@
"type": "registry:ui",
"title": "Badge",
"description": "Status label — solid/outlined × xs/sm/md × 5 colors.",
- "dependencies": ["tailwind-variants"],
- "registryDependencies": ["utils"],
+ "dependencies": ["@fluxloop-ai/pds-core"],
"files": [
{
"path": "src/components/badge.tsx",
@@ -225,8 +205,7 @@
"type": "registry:ui",
"title": "Chip",
"description": "Compact action chip — solid/outlined × xsmall/small/medium/large with active toggle and leading/trailing slots.",
- "dependencies": ["@radix-ui/react-slot", "tailwind-variants"],
- "registryDependencies": ["utils"],
+ "dependencies": ["@fluxloop-ai/pds-core", "@radix-ui/react-slot"],
"files": [
{
"path": "src/components/chip.tsx",
@@ -240,8 +219,7 @@
"type": "registry:ui",
"title": "Progress",
"description": "Radix Progress — determinate + indeterminate, sm/md track.",
- "dependencies": ["@radix-ui/react-progress", "tailwind-variants"],
- "registryDependencies": ["utils"],
+ "dependencies": ["@fluxloop-ai/pds-core", "@radix-ui/react-progress"],
"files": [
{
"path": "src/components/progress.tsx",
@@ -255,8 +233,11 @@
"type": "registry:ui",
"title": "Checkbox",
"description": "Radix Checkbox — sm/md, checked/indeterminate/invalid/disabled states.",
- "dependencies": ["@radix-ui/react-checkbox", "@fluxloop-ai/pds-icons", "tailwind-variants"],
- "registryDependencies": ["utils"],
+ "dependencies": [
+ "@fluxloop-ai/pds-core",
+ "@radix-ui/react-checkbox",
+ "@fluxloop-ai/pds-icons"
+ ],
"files": [
{
"path": "src/components/checkbox.tsx",
@@ -270,8 +251,7 @@
"type": "registry:ui",
"title": "Switch",
"description": "Radix Switch — sm/md track, primary-normal on-state.",
- "dependencies": ["@radix-ui/react-switch", "tailwind-variants"],
- "registryDependencies": ["utils"],
+ "dependencies": ["@fluxloop-ai/pds-core", "@radix-ui/react-switch"],
"files": [
{
"path": "src/components/switch.tsx",
@@ -285,8 +265,7 @@
"type": "registry:ui",
"title": "RadioGroup",
"description": "Radix RadioGroup — sm/md, horizontal/vertical orientation, loop on by default.",
- "dependencies": ["@radix-ui/react-radio-group", "tailwind-variants"],
- "registryDependencies": ["utils"],
+ "dependencies": ["@fluxloop-ai/pds-core", "@radix-ui/react-radio-group"],
"files": [
{
"path": "src/components/radio-group.tsx",
@@ -300,8 +279,7 @@
"type": "registry:ui",
"title": "SegmentedControl",
"description": "Trackless single-select control for 2-3 inline options — sm/md, hug or fullWidth, snap selection.",
- "dependencies": ["@radix-ui/react-radio-group", "tailwind-variants"],
- "registryDependencies": ["utils"],
+ "dependencies": ["@fluxloop-ai/pds-core", "@radix-ui/react-radio-group"],
"files": [
{
"path": "src/components/segmented-control.tsx",
@@ -315,8 +293,7 @@
"type": "registry:ui",
"title": "Select",
"description": "Radix Select — sm/md/lg trigger with Input-aligned tokens, item-aligned/popper content.",
- "dependencies": ["@radix-ui/react-select", "@fluxloop-ai/pds-icons", "tailwind-variants"],
- "registryDependencies": ["utils"],
+ "dependencies": ["@fluxloop-ai/pds-core", "@radix-ui/react-select", "@fluxloop-ai/pds-icons"],
"files": [
{
"path": "src/components/select.tsx",
@@ -330,8 +307,7 @@
"type": "registry:ui",
"title": "Form",
"description": "FormField + FormLabel + FormControl + FormDescription + FormErrorMessage — useId-based id wiring via context.",
- "dependencies": ["@radix-ui/react-slot", "tailwind-variants"],
- "registryDependencies": ["utils"],
+ "dependencies": ["@fluxloop-ai/pds-core", "@radix-ui/react-slot"],
"files": [
{
"path": "src/components/form.tsx",
@@ -345,8 +321,7 @@
"type": "registry:ui",
"title": "Tabs",
"description": "Radix Tabs — sm/md/lg × hug/fill, horizontal/vertical, underline indicator.",
- "dependencies": ["@radix-ui/react-tabs", "tailwind-variants"],
- "registryDependencies": ["utils"],
+ "dependencies": ["@fluxloop-ai/pds-core", "@radix-ui/react-tabs"],
"files": [
{
"path": "src/components/tabs.tsx",
@@ -361,12 +336,11 @@
"title": "Combobox",
"description": "cmdk inside Radix Popover — searchable select wrapped behind PDS vocabulary.",
"dependencies": [
+ "@fluxloop-ai/pds-core",
"cmdk",
"@radix-ui/react-popover",
- "@fluxloop-ai/pds-icons",
- "tailwind-variants"
+ "@fluxloop-ai/pds-icons"
],
- "registryDependencies": ["utils"],
"files": [
{
"path": "src/components/combobox.tsx",
@@ -380,8 +354,7 @@
"type": "registry:ui",
"title": "IconButton",
"description": "Square icon-only Button — variant × size, asChild support.",
- "dependencies": ["@radix-ui/react-slot", "tailwind-variants"],
- "registryDependencies": ["utils"],
+ "dependencies": ["@fluxloop-ai/pds-core", "@radix-ui/react-slot"],
"files": [
{
"path": "src/components/icon-button.tsx",
@@ -395,8 +368,7 @@
"type": "registry:ui",
"title": "TextButton",
"description": "Inline link-like Button — variant × size, optional leading/trailing icon, loading state.",
- "dependencies": ["@radix-ui/react-slot", "@fluxloop-ai/pds-icons", "tailwind-variants"],
- "registryDependencies": ["utils"],
+ "dependencies": ["@fluxloop-ai/pds-core", "@radix-ui/react-slot", "@fluxloop-ai/pds-icons"],
"files": [
{
"path": "src/components/text-button.tsx",
@@ -410,8 +382,8 @@
"type": "registry:ui",
"title": "Panel",
"description": "Surface container with optional leading icon and PanelCell layout primitive.",
- "dependencies": ["tailwind-variants"],
- "registryDependencies": ["utils", "icon"],
+ "dependencies": ["@fluxloop-ai/pds-core"],
+ "registryDependencies": ["icon"],
"files": [
{
"path": "src/components/panel.tsx",
@@ -425,8 +397,7 @@
"type": "registry:ui",
"title": "PageContainer",
"description": "Padded page-level wrapper with size variants for content alignment.",
- "dependencies": ["tailwind-variants"],
- "registryDependencies": ["utils"],
+ "dependencies": ["@fluxloop-ai/pds-core"],
"files": [
{
"path": "src/components/page-container.tsx",
@@ -440,8 +411,7 @@
"type": "registry:ui",
"title": "SectionHeader",
"description": "Section heading with title/description and optional trailing slot.",
- "dependencies": ["tailwind-variants"],
- "registryDependencies": ["utils"],
+ "dependencies": ["@fluxloop-ai/pds-core"],
"files": [
{
"path": "src/components/section-header.tsx",
@@ -455,8 +425,7 @@
"type": "registry:ui",
"title": "RemovableTabBar",
"description": "Horizontal tab bar with per-tab close affordance.",
- "dependencies": ["tailwind-variants"],
- "registryDependencies": ["utils"],
+ "dependencies": ["@fluxloop-ai/pds-core"],
"files": [
{
"path": "src/components/removable-tab-bar.tsx",
@@ -470,8 +439,8 @@
"type": "registry:ui",
"title": "ActionCard",
"description": "Card-shaped action surface — leading icon, title, description, trailing slot.",
- "dependencies": ["@radix-ui/react-slot", "tailwind-variants"],
- "registryDependencies": ["utils", "icon"],
+ "dependencies": ["@fluxloop-ai/pds-core", "@radix-ui/react-slot"],
+ "registryDependencies": ["icon"],
"files": [
{
"path": "src/components/action-card.tsx",
@@ -485,8 +454,8 @@
"type": "registry:ui",
"title": "ActionTile",
"description": "Compact tile with header/leading/title/description/footer slots for action grids.",
- "dependencies": ["@radix-ui/react-slot", "tailwind-variants"],
- "registryDependencies": ["utils", "icon"],
+ "dependencies": ["@fluxloop-ai/pds-core", "@radix-ui/react-slot"],
+ "registryDependencies": ["icon"],
"files": [
{
"path": "src/components/action-tile.tsx",
@@ -500,8 +469,7 @@
"type": "registry:ui",
"title": "AppShell",
"description": "Root layout — sidebar, optional side panel, main column. Splitter and Electron titlebar slot.",
- "dependencies": ["tailwind-variants"],
- "registryDependencies": ["utils"],
+ "dependencies": ["@fluxloop-ai/pds-core"],
"files": [
{
"path": "src/components/app-shell.tsx",
@@ -515,8 +483,8 @@
"type": "registry:ui",
"title": "SidebarMenu",
"description": "Vertical icon-first menu list for app sidebars.",
- "dependencies": ["tailwind-variants"],
- "registryDependencies": ["utils", "icon"],
+ "dependencies": ["@fluxloop-ai/pds-core"],
+ "registryDependencies": ["icon"],
"files": [
{
"path": "src/components/sidebar-menu.tsx",
@@ -530,8 +498,8 @@
"type": "registry:ui",
"title": "SidebarList",
"description": "Sidebar list of items with leading icon, label, and trailing overflow menu.",
- "dependencies": ["@fluxloop-ai/pds-icons", "tailwind-variants"],
- "registryDependencies": ["utils", "icon"],
+ "dependencies": ["@fluxloop-ai/pds-core", "@fluxloop-ai/pds-icons"],
+ "registryDependencies": ["icon"],
"files": [
{
"path": "src/components/sidebar-list.tsx",
@@ -558,8 +526,8 @@
"type": "registry:ui",
"title": "ChatCopyButton (internal)",
"description": "Internal copy-to-clipboard button used by chat message components.",
- "dependencies": ["@fluxloop-ai/pds-icons", "tailwind-variants"],
- "registryDependencies": ["utils", "icon-button", "tooltip", "chat-types"],
+ "dependencies": ["@fluxloop-ai/pds-core", "@fluxloop-ai/pds-icons"],
+ "registryDependencies": ["icon-button", "tooltip", "chat-types"],
"files": [
{
"path": "src/components/internal/chat-copy-button.tsx",
@@ -573,8 +541,7 @@
"type": "registry:ui",
"title": "ChatLoadingDots",
"description": "Single-orb pulse loading indicator for in-progress assistant responses.",
- "dependencies": ["tailwind-variants"],
- "registryDependencies": ["utils"],
+ "dependencies": ["@fluxloop-ai/pds-core"],
"files": [
{
"path": "src/components/chat-loading-dots.tsx",
@@ -588,8 +555,7 @@
"type": "registry:ui",
"title": "ChatAttachmentChip",
"description": "Chip preview for chat composer attachments (file label, remove action).",
- "dependencies": ["@fluxloop-ai/pds-icons", "tailwind-variants"],
- "registryDependencies": ["utils"],
+ "dependencies": ["@fluxloop-ai/pds-core", "@fluxloop-ai/pds-icons"],
"files": [
{
"path": "src/components/chat-attachment-chip.tsx",
@@ -604,11 +570,10 @@
"title": "ChatComposer",
"description": "Chat input — auto-grow textarea with attachments and send action.",
"dependencies": [
+ "@fluxloop-ai/pds-core",
"@radix-ui/react-compose-refs",
- "@fluxloop-ai/pds-icons",
- "tailwind-variants"
+ "@fluxloop-ai/pds-icons"
],
- "registryDependencies": ["utils"],
"files": [
{
"path": "src/components/chat-composer.tsx",
@@ -622,8 +587,8 @@
"type": "registry:ui",
"title": "ChatUserMessage",
"description": "User chat bubble — supports text and attachment blocks, copy action.",
- "dependencies": ["@fluxloop-ai/pds-icons", "tailwind-variants"],
- "registryDependencies": ["utils", "tooltip", "chat-attachment-chip", "chat-copy-button"],
+ "dependencies": ["@fluxloop-ai/pds-core", "@fluxloop-ai/pds-icons"],
+ "registryDependencies": ["tooltip", "chat-attachment-chip", "chat-copy-button"],
"files": [
{
"path": "src/components/chat-user-message.tsx",
@@ -637,8 +602,8 @@
"type": "registry:ui",
"title": "ChatAssistantMessage",
"description": "Assistant response — plain text (no bubble), copy action, loading state.",
- "dependencies": ["tailwind-variants"],
- "registryDependencies": ["utils", "tooltip", "chat-loading-dots", "chat-copy-button"],
+ "dependencies": ["@fluxloop-ai/pds-core"],
+ "registryDependencies": ["tooltip", "chat-loading-dots", "chat-copy-button"],
"files": [
{
"path": "src/components/chat-assistant-message.tsx",
@@ -652,8 +617,7 @@
"type": "registry:ui",
"title": "ChatProcessTrace",
"description": "Collapsible trace UI for assistant tool/process steps.",
- "dependencies": ["@fluxloop-ai/pds-icons", "tailwind-variants"],
- "registryDependencies": ["utils"],
+ "dependencies": ["@fluxloop-ai/pds-core", "@fluxloop-ai/pds-icons"],
"files": [
{
"path": "src/components/chat-process-trace.tsx",
@@ -667,8 +631,7 @@
"type": "registry:ui",
"title": "CloseButton (internal)",
"description": "Internal close affordance shared by dialog/popover/toast.",
- "dependencies": ["@fluxloop-ai/pds-icons", "tailwind-variants"],
- "registryDependencies": ["utils"],
+ "dependencies": ["@fluxloop-ai/pds-core", "@fluxloop-ai/pds-icons"],
"files": [
{
"path": "src/components/internal/close-button.tsx",
diff --git a/packages/pds-ui/src/components/action-card.tsx b/packages/pds-ui/src/components/action-card.tsx
index 9d7e5a1..33689f0 100644
--- a/packages/pds-ui/src/components/action-card.tsx
+++ b/packages/pds-ui/src/components/action-card.tsx
@@ -1,10 +1,9 @@
"use client";
+import { cn, tv, type VariantProps } from "@fluxloop-ai/pds-core";
import type { PhosphorIcon } from "@fluxloop-ai/pds-icons/icons";
import { Slot } from "@radix-ui/react-slot";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv, type VariantProps } from "../utils/tv";
import { Icon } from "./icon";
/* ============================================================================
diff --git a/packages/pds-ui/src/components/action-tile.tsx b/packages/pds-ui/src/components/action-tile.tsx
index 527c5a1..983c410 100644
--- a/packages/pds-ui/src/components/action-tile.tsx
+++ b/packages/pds-ui/src/components/action-tile.tsx
@@ -1,10 +1,9 @@
"use client";
+import { cn, tv, type VariantProps } from "@fluxloop-ai/pds-core";
import type { PhosphorIcon } from "@fluxloop-ai/pds-icons/icons";
import { Slot } from "@radix-ui/react-slot";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv, type VariantProps } from "../utils/tv";
import { Icon } from "./icon";
/* ============================================================================
diff --git a/packages/pds-ui/src/components/app-shell.tsx b/packages/pds-ui/src/components/app-shell.tsx
index 9dd6940..a93ccc7 100644
--- a/packages/pds-ui/src/components/app-shell.tsx
+++ b/packages/pds-ui/src/components/app-shell.tsx
@@ -1,8 +1,7 @@
"use client";
+import { cn, tv } from "@fluxloop-ai/pds-core";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv } from "../utils/tv";
/* ============================================================================
* AppShell — 데스크탑 앱 셸 (3-패널 레이아웃)
diff --git a/packages/pds-ui/src/components/avatar.tsx b/packages/pds-ui/src/components/avatar.tsx
index 0a14aa1..27e2c70 100644
--- a/packages/pds-ui/src/components/avatar.tsx
+++ b/packages/pds-ui/src/components/avatar.tsx
@@ -1,10 +1,9 @@
"use client";
+import { cn, tv, type VariantProps } from "@fluxloop-ai/pds-core";
import { Buildings, GraduationCap, User } from "@fluxloop-ai/pds-icons/icons";
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv, type VariantProps } from "../utils/tv";
const avatar = tv({
slots: {
diff --git a/packages/pds-ui/src/components/badge.tsx b/packages/pds-ui/src/components/badge.tsx
index d52373f..7913dff 100644
--- a/packages/pds-ui/src/components/badge.tsx
+++ b/packages/pds-ui/src/components/badge.tsx
@@ -1,8 +1,7 @@
"use client";
+import { cn, tv, type VariantProps } from "@fluxloop-ai/pds-core";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv, type VariantProps } from "../utils/tv";
const ACCENT_FOREGROUND_VARS = {
red: "accent-foreground-red",
diff --git a/packages/pds-ui/src/components/button.tsx b/packages/pds-ui/src/components/button.tsx
index 8862f7b..6ba5871 100644
--- a/packages/pds-ui/src/components/button.tsx
+++ b/packages/pds-ui/src/components/button.tsx
@@ -1,10 +1,9 @@
"use client";
+import { cn, tv, type VariantProps } from "@fluxloop-ai/pds-core";
import { CircleNotch } from "@fluxloop-ai/pds-icons/icons";
import { Slot } from "@radix-ui/react-slot";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv, type VariantProps } from "../utils/tv";
const button = tv({
slots: {
diff --git a/packages/pds-ui/src/components/chat-assistant-message.tsx b/packages/pds-ui/src/components/chat-assistant-message.tsx
index 478dada..65699ef 100644
--- a/packages/pds-ui/src/components/chat-assistant-message.tsx
+++ b/packages/pds-ui/src/components/chat-assistant-message.tsx
@@ -1,8 +1,7 @@
"use client";
+import { cn, tv } from "@fluxloop-ai/pds-core";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv } from "../utils/tv";
import { ChatLoadingDots } from "./chat-loading-dots";
import { ChatCopyButton } from "./internal/chat-copy-button";
import { TooltipProvider } from "./tooltip";
diff --git a/packages/pds-ui/src/components/chat-attachment-chip.tsx b/packages/pds-ui/src/components/chat-attachment-chip.tsx
index a4bc038..ee81d4b 100644
--- a/packages/pds-ui/src/components/chat-attachment-chip.tsx
+++ b/packages/pds-ui/src/components/chat-attachment-chip.tsx
@@ -1,9 +1,8 @@
"use client";
+import { cn, tv } from "@fluxloop-ai/pds-core";
import { File, X } from "@fluxloop-ai/pds-icons/icons";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv } from "../utils/tv";
const chatAttachmentChip = tv({
slots: {
diff --git a/packages/pds-ui/src/components/chat-composer.tsx b/packages/pds-ui/src/components/chat-composer.tsx
index 2b7707a..4463546 100644
--- a/packages/pds-ui/src/components/chat-composer.tsx
+++ b/packages/pds-ui/src/components/chat-composer.tsx
@@ -1,10 +1,9 @@
"use client";
+import { cn, tv } from "@fluxloop-ai/pds-core";
import { ArrowUp } from "@fluxloop-ai/pds-icons/icons";
import { useComposedRefs } from "@radix-ui/react-compose-refs";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv } from "../utils/tv";
const chatComposer = tv({
slots: {
diff --git a/packages/pds-ui/src/components/chat-loading-dots.tsx b/packages/pds-ui/src/components/chat-loading-dots.tsx
index 98ec0ac..ed97bba 100644
--- a/packages/pds-ui/src/components/chat-loading-dots.tsx
+++ b/packages/pds-ui/src/components/chat-loading-dots.tsx
@@ -1,8 +1,7 @@
"use client";
+import { cn, tv } from "@fluxloop-ai/pds-core";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv } from "../utils/tv";
const chatLoadingDots = tv({
slots: {
diff --git a/packages/pds-ui/src/components/chat-process-trace.tsx b/packages/pds-ui/src/components/chat-process-trace.tsx
index dcc950d..9c01d04 100644
--- a/packages/pds-ui/src/components/chat-process-trace.tsx
+++ b/packages/pds-ui/src/components/chat-process-trace.tsx
@@ -1,10 +1,9 @@
"use client";
+import { cn, tv } from "@fluxloop-ai/pds-core";
import { CaretRight } from "@fluxloop-ai/pds-icons/icons";
import * as React from "react";
import type { TextBlock, ThinkingBlock, ToolResultBlock, ToolUseBlock } from "../types/chat";
-import { cn } from "../utils/cn";
-import { tv } from "../utils/tv";
export type ProcessTraceBlock = TextBlock | ThinkingBlock | ToolUseBlock | ToolResultBlock;
diff --git a/packages/pds-ui/src/components/chat-user-message.tsx b/packages/pds-ui/src/components/chat-user-message.tsx
index 36ed0f7..40a480e 100644
--- a/packages/pds-ui/src/components/chat-user-message.tsx
+++ b/packages/pds-ui/src/components/chat-user-message.tsx
@@ -1,10 +1,9 @@
"use client";
+import { cn, tv, type VariantProps } from "@fluxloop-ai/pds-core";
import { ImageBroken } from "@fluxloop-ai/pds-icons/icons";
import * as React from "react";
import type { ContentBlock, DocumentBlock, ImageBlock, TextBlock } from "../types/chat";
-import { cn } from "../utils/cn";
-import { tv, type VariantProps } from "../utils/tv";
import { ChatAttachmentChip } from "./chat-attachment-chip";
import { ChatCopyButton, extractCopyText } from "./internal/chat-copy-button";
import { TooltipProvider } from "./tooltip";
diff --git a/packages/pds-ui/src/components/checkbox.tsx b/packages/pds-ui/src/components/checkbox.tsx
index a225bd3..8233e8f 100644
--- a/packages/pds-ui/src/components/checkbox.tsx
+++ b/packages/pds-ui/src/components/checkbox.tsx
@@ -1,10 +1,9 @@
"use client";
+import { cn, tv, type VariantProps } from "@fluxloop-ai/pds-core";
import { Check, Minus } from "@fluxloop-ai/pds-icons/icons";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv, type VariantProps } from "../utils/tv";
const checkbox = tv({
slots: {
diff --git a/packages/pds-ui/src/components/chip.tsx b/packages/pds-ui/src/components/chip.tsx
index 999157f..7cd0563 100644
--- a/packages/pds-ui/src/components/chip.tsx
+++ b/packages/pds-ui/src/components/chip.tsx
@@ -1,9 +1,8 @@
"use client";
+import { cn, tv, type VariantProps } from "@fluxloop-ai/pds-core";
import { Slot } from "@radix-ui/react-slot";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv, type VariantProps } from "../utils/tv";
const chip = tv({
slots: {
diff --git a/packages/pds-ui/src/components/combobox.tsx b/packages/pds-ui/src/components/combobox.tsx
index 5576d00..d23e285 100644
--- a/packages/pds-ui/src/components/combobox.tsx
+++ b/packages/pds-ui/src/components/combobox.tsx
@@ -1,11 +1,10 @@
"use client";
+import { cn, tv, type VariantProps } from "@fluxloop-ai/pds-core";
import { CaretDown } from "@fluxloop-ai/pds-icons/icons";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { Command as CommandRoot } from "cmdk";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv, type VariantProps } from "../utils/tv";
const combobox = tv({
slots: {
diff --git a/packages/pds-ui/src/components/dialog.tsx b/packages/pds-ui/src/components/dialog.tsx
index da8336b..640ca8b 100644
--- a/packages/pds-ui/src/components/dialog.tsx
+++ b/packages/pds-ui/src/components/dialog.tsx
@@ -1,9 +1,8 @@
"use client";
+import { cn, tv, type VariantProps } from "@fluxloop-ai/pds-core";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv, type VariantProps } from "../utils/tv";
import { CloseButton } from "./internal/close-button";
const dialog = tv({
diff --git a/packages/pds-ui/src/components/dropdown-menu.tsx b/packages/pds-ui/src/components/dropdown-menu.tsx
index 686af5d..96a8389 100644
--- a/packages/pds-ui/src/components/dropdown-menu.tsx
+++ b/packages/pds-ui/src/components/dropdown-menu.tsx
@@ -1,10 +1,9 @@
"use client";
+import { cn, tv, type VariantProps } from "@fluxloop-ai/pds-core";
import { CaretRight, Check } from "@fluxloop-ai/pds-icons/icons";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv, type VariantProps } from "../utils/tv";
const menu = tv({
slots: {
diff --git a/packages/pds-ui/src/components/form.tsx b/packages/pds-ui/src/components/form.tsx
index 1daf6a4..3ccfc10 100644
--- a/packages/pds-ui/src/components/form.tsx
+++ b/packages/pds-ui/src/components/form.tsx
@@ -1,9 +1,8 @@
"use client";
+import { cn, tv, type VariantProps } from "@fluxloop-ai/pds-core";
import { Slot } from "@radix-ui/react-slot";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv, type VariantProps } from "../utils/tv";
type FormFieldContextValue = {
baseId: string;
diff --git a/packages/pds-ui/src/components/icon-button.tsx b/packages/pds-ui/src/components/icon-button.tsx
index 2788306..280ec85 100644
--- a/packages/pds-ui/src/components/icon-button.tsx
+++ b/packages/pds-ui/src/components/icon-button.tsx
@@ -1,9 +1,8 @@
"use client";
+import { cn, tv, type VariantProps } from "@fluxloop-ai/pds-core";
import { Slot } from "@radix-ui/react-slot";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv, type VariantProps } from "../utils/tv";
const iconButton = tv({
base: [
diff --git a/packages/pds-ui/src/components/icon.tsx b/packages/pds-ui/src/components/icon.tsx
index 4cb43f6..4a8815c 100644
--- a/packages/pds-ui/src/components/icon.tsx
+++ b/packages/pds-ui/src/components/icon.tsx
@@ -1,9 +1,8 @@
"use client";
+import { cn, tv, type VariantProps } from "@fluxloop-ai/pds-core";
import type { IconWeight, PhosphorIcon } from "@fluxloop-ai/pds-icons/icons";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv, type VariantProps } from "../utils/tv";
const icon = tv({
base: "inline-block shrink-0",
diff --git a/packages/pds-ui/src/components/input.tsx b/packages/pds-ui/src/components/input.tsx
index 89958b7..245156b 100644
--- a/packages/pds-ui/src/components/input.tsx
+++ b/packages/pds-ui/src/components/input.tsx
@@ -1,10 +1,9 @@
"use client";
+import { cn, tv, type VariantProps } from "@fluxloop-ai/pds-core";
import { Check, X, XCircle as XCircleFill } from "@fluxloop-ai/pds-icons/icons";
import { useComposedRefs } from "@radix-ui/react-compose-refs";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv, type VariantProps } from "../utils/tv";
const input = tv({
slots: {
diff --git a/packages/pds-ui/src/components/internal/close-button.tsx b/packages/pds-ui/src/components/internal/close-button.tsx
index 68f84de..661725c 100644
--- a/packages/pds-ui/src/components/internal/close-button.tsx
+++ b/packages/pds-ui/src/components/internal/close-button.tsx
@@ -1,9 +1,8 @@
"use client";
+import { cn, tv, type VariantProps } from "@fluxloop-ai/pds-core";
import { X } from "@fluxloop-ai/pds-icons/icons";
import * as React from "react";
-import { cn } from "../../utils/cn";
-import { tv, type VariantProps } from "../../utils/tv";
const closeButton = tv({
base: [
diff --git a/packages/pds-ui/src/components/page-container.tsx b/packages/pds-ui/src/components/page-container.tsx
index 8afd377..e380f0f 100644
--- a/packages/pds-ui/src/components/page-container.tsx
+++ b/packages/pds-ui/src/components/page-container.tsx
@@ -1,8 +1,7 @@
"use client";
+import { cn, tv, type VariantProps } from "@fluxloop-ai/pds-core";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv, type VariantProps } from "../utils/tv";
/* ============================================================================
* PageContainer — 페이지 본문 frame (max-width · gutter · padding)
diff --git a/packages/pds-ui/src/components/panel.tsx b/packages/pds-ui/src/components/panel.tsx
index 301253a..f6db7c1 100644
--- a/packages/pds-ui/src/components/panel.tsx
+++ b/packages/pds-ui/src/components/panel.tsx
@@ -1,9 +1,8 @@
"use client";
+import { cn, tv } from "@fluxloop-ai/pds-core";
import type { PhosphorIcon } from "@fluxloop-ai/pds-icons/icons";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv } from "../utils/tv";
import { Icon } from "./icon";
/* ============================================================================
diff --git a/packages/pds-ui/src/components/popover.tsx b/packages/pds-ui/src/components/popover.tsx
index d4d825a..22ea782 100644
--- a/packages/pds-ui/src/components/popover.tsx
+++ b/packages/pds-ui/src/components/popover.tsx
@@ -1,9 +1,8 @@
"use client";
+import { cn, tv, type VariantProps } from "@fluxloop-ai/pds-core";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv, type VariantProps } from "../utils/tv";
import { CloseButton } from "./internal/close-button";
const popover = tv({
diff --git a/packages/pds-ui/src/components/progress.tsx b/packages/pds-ui/src/components/progress.tsx
index 2f52089..3798d49 100644
--- a/packages/pds-ui/src/components/progress.tsx
+++ b/packages/pds-ui/src/components/progress.tsx
@@ -1,9 +1,8 @@
"use client";
+import { cn, tv, type VariantProps } from "@fluxloop-ai/pds-core";
import * as ProgressPrimitive from "@radix-ui/react-progress";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv, type VariantProps } from "../utils/tv";
const progress = tv({
slots: {
diff --git a/packages/pds-ui/src/components/radio-group.tsx b/packages/pds-ui/src/components/radio-group.tsx
index 4201553..cfeedc5 100644
--- a/packages/pds-ui/src/components/radio-group.tsx
+++ b/packages/pds-ui/src/components/radio-group.tsx
@@ -1,9 +1,8 @@
"use client";
+import { cn, tv, type VariantProps } from "@fluxloop-ai/pds-core";
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv, type VariantProps } from "../utils/tv";
const radio = tv({
slots: {
diff --git a/packages/pds-ui/src/components/removable-tab-bar.tsx b/packages/pds-ui/src/components/removable-tab-bar.tsx
index 15e9064..aff7ef2 100644
--- a/packages/pds-ui/src/components/removable-tab-bar.tsx
+++ b/packages/pds-ui/src/components/removable-tab-bar.tsx
@@ -1,8 +1,7 @@
"use client";
+import { cn, tv, type VariantProps } from "@fluxloop-ai/pds-core";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv, type VariantProps } from "../utils/tv";
const removableTabBar = tv({
slots: {
diff --git a/packages/pds-ui/src/components/scroll-area.tsx b/packages/pds-ui/src/components/scroll-area.tsx
index 2c8c500..897b4d1 100644
--- a/packages/pds-ui/src/components/scroll-area.tsx
+++ b/packages/pds-ui/src/components/scroll-area.tsx
@@ -1,9 +1,8 @@
"use client";
+import { cn, tv, type VariantProps } from "@fluxloop-ai/pds-core";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv, type VariantProps } from "../utils/tv";
const scrollArea = tv({
slots: {
diff --git a/packages/pds-ui/src/components/section-header.tsx b/packages/pds-ui/src/components/section-header.tsx
index 24dc8b8..f7dd30a 100644
--- a/packages/pds-ui/src/components/section-header.tsx
+++ b/packages/pds-ui/src/components/section-header.tsx
@@ -1,8 +1,7 @@
"use client";
+import { cn, tv, type VariantProps } from "@fluxloop-ai/pds-core";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv, type VariantProps } from "../utils/tv";
const sectionHeader = tv({
slots: {
diff --git a/packages/pds-ui/src/components/segmented-control.tsx b/packages/pds-ui/src/components/segmented-control.tsx
index 231f7c8..b206c0d 100644
--- a/packages/pds-ui/src/components/segmented-control.tsx
+++ b/packages/pds-ui/src/components/segmented-control.tsx
@@ -1,9 +1,8 @@
"use client";
+import { cn, tv, type VariantProps } from "@fluxloop-ai/pds-core";
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv, type VariantProps } from "../utils/tv";
const segmentedControl = tv({
slots: {
diff --git a/packages/pds-ui/src/components/select.tsx b/packages/pds-ui/src/components/select.tsx
index 22a2f40..7f038c0 100644
--- a/packages/pds-ui/src/components/select.tsx
+++ b/packages/pds-ui/src/components/select.tsx
@@ -1,10 +1,9 @@
"use client";
+import { cn, tv, type VariantProps } from "@fluxloop-ai/pds-core";
import { CaretDown, CaretUp, Check } from "@fluxloop-ai/pds-icons/icons";
import * as SelectPrimitive from "@radix-ui/react-select";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv, type VariantProps } from "../utils/tv";
const select = tv({
slots: {
diff --git a/packages/pds-ui/src/components/separator.tsx b/packages/pds-ui/src/components/separator.tsx
index 1cbc6f4..07804fd 100644
--- a/packages/pds-ui/src/components/separator.tsx
+++ b/packages/pds-ui/src/components/separator.tsx
@@ -1,9 +1,8 @@
"use client";
+import { cn, tv, type VariantProps } from "@fluxloop-ai/pds-core";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv, type VariantProps } from "../utils/tv";
const separator = tv({
base: "shrink-0 border-0",
diff --git a/packages/pds-ui/src/components/sidebar-list.tsx b/packages/pds-ui/src/components/sidebar-list.tsx
index 6f6955c..b2f1c33 100644
--- a/packages/pds-ui/src/components/sidebar-list.tsx
+++ b/packages/pds-ui/src/components/sidebar-list.tsx
@@ -1,9 +1,8 @@
"use client";
+import { cn, tv } from "@fluxloop-ai/pds-core";
import { DotsThree } from "@fluxloop-ai/pds-icons/icons";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv } from "../utils/tv";
import { Icon } from "./icon";
const sidebarList = tv({
diff --git a/packages/pds-ui/src/components/sidebar-menu.tsx b/packages/pds-ui/src/components/sidebar-menu.tsx
index e346595..8db8267 100644
--- a/packages/pds-ui/src/components/sidebar-menu.tsx
+++ b/packages/pds-ui/src/components/sidebar-menu.tsx
@@ -1,9 +1,8 @@
"use client";
+import { cn, tv } from "@fluxloop-ai/pds-core";
import type { PhosphorIcon } from "@fluxloop-ai/pds-icons/icons";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv } from "../utils/tv";
import { Icon } from "./icon";
const sidebarMenu = tv({
diff --git a/packages/pds-ui/src/components/spinner.tsx b/packages/pds-ui/src/components/spinner.tsx
index cb46794..7485334 100644
--- a/packages/pds-ui/src/components/spinner.tsx
+++ b/packages/pds-ui/src/components/spinner.tsx
@@ -1,8 +1,7 @@
"use client";
+import { cn, tv, type VariantProps } from "@fluxloop-ai/pds-core";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv, type VariantProps } from "../utils/tv";
const spinner = tv({
base: [
diff --git a/packages/pds-ui/src/components/switch.tsx b/packages/pds-ui/src/components/switch.tsx
index 96437b0..cb288e5 100644
--- a/packages/pds-ui/src/components/switch.tsx
+++ b/packages/pds-ui/src/components/switch.tsx
@@ -1,9 +1,8 @@
"use client";
+import { cn, tv, type VariantProps } from "@fluxloop-ai/pds-core";
import * as SwitchPrimitive from "@radix-ui/react-switch";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv, type VariantProps } from "../utils/tv";
const switchStyles = tv({
slots: {
diff --git a/packages/pds-ui/src/components/tabs.tsx b/packages/pds-ui/src/components/tabs.tsx
index 3c06536..b389e7c 100644
--- a/packages/pds-ui/src/components/tabs.tsx
+++ b/packages/pds-ui/src/components/tabs.tsx
@@ -1,9 +1,8 @@
"use client";
+import { cn, tv } from "@fluxloop-ai/pds-core";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv } from "../utils/tv";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip";
type TabsVariant = "segmented" | "label";
diff --git a/packages/pds-ui/src/components/text-button.tsx b/packages/pds-ui/src/components/text-button.tsx
index 4718bd7..4b73f07 100644
--- a/packages/pds-ui/src/components/text-button.tsx
+++ b/packages/pds-ui/src/components/text-button.tsx
@@ -1,10 +1,9 @@
"use client";
+import { cn, tv, type VariantProps } from "@fluxloop-ai/pds-core";
import { CircleNotch } from "@fluxloop-ai/pds-icons/icons";
import { Slot } from "@radix-ui/react-slot";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv, type VariantProps } from "../utils/tv";
const textButton = tv({
slots: {
diff --git a/packages/pds-ui/src/components/toast.tsx b/packages/pds-ui/src/components/toast.tsx
index ab8a0e4..9403e66 100644
--- a/packages/pds-ui/src/components/toast.tsx
+++ b/packages/pds-ui/src/components/toast.tsx
@@ -1,10 +1,9 @@
"use client";
+import { cn, tv, type VariantProps } from "@fluxloop-ai/pds-core";
import { Check, Info, Warning, WarningCircle } from "@fluxloop-ai/pds-icons/icons";
import * as ToastPrimitive from "@radix-ui/react-toast";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv, type VariantProps } from "../utils/tv";
import { Icon } from "./icon";
import { CloseButton } from "./internal/close-button";
diff --git a/packages/pds-ui/src/components/tooltip.tsx b/packages/pds-ui/src/components/tooltip.tsx
index 0569fee..f5d85e5 100644
--- a/packages/pds-ui/src/components/tooltip.tsx
+++ b/packages/pds-ui/src/components/tooltip.tsx
@@ -1,9 +1,8 @@
"use client";
+import { cn, tv, type VariantProps } from "@fluxloop-ai/pds-core";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import * as React from "react";
-import { cn } from "../utils/cn";
-import { tv, type VariantProps } from "../utils/tv";
const tooltip = tv({
slots: {
diff --git a/packages/pds-ui/src/index.ts b/packages/pds-ui/src/index.ts
index e644b3f..1e4dd95 100644
--- a/packages/pds-ui/src/index.ts
+++ b/packages/pds-ui/src/index.ts
@@ -252,7 +252,6 @@ export {
TooltipTrigger,
tooltip,
} from "./components/tooltip";
-export { cn } from "./utils/cn";
export type {
ScrollFadeMaskOptions,
UseScrollFadeOptions,
diff --git a/packages/pds-ui/src/utils/cn.ts b/packages/pds-ui/src/utils/cn.ts
deleted file mode 100644
index b1340a3..0000000
--- a/packages/pds-ui/src/utils/cn.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { type ClassValue, clsx } from "clsx";
-import { extendTailwindMerge } from "tailwind-merge";
-import { twMergeConfig } from "./tw-merge-config";
-
-const twMerge = extendTailwindMerge(twMergeConfig);
-
-export function cn(...inputs: ClassValue[]): string {
- return twMerge(clsx(inputs));
-}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 500dcdb..ce85b86 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -90,6 +90,9 @@ importers:
tailwind-merge:
specifier: ^3.5.0
version: 3.5.0
+ tailwind-variants:
+ specifier: ^3.2.2
+ version: 3.2.2(tailwind-merge@3.5.0)(tailwindcss@4.2.2)
devDependencies:
framer-motion:
specifier: ^11.11.0
@@ -2724,6 +2727,7 @@ packages:
'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
+ deprecated: Potential CWE-502 - Update to 1.3.1 or higher
'@upsetjs/venn.js@2.0.0':
resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==}