From 71332cc509e25c90bbe0960605560ab54f964697 Mon Sep 17 00:00:00 2001 From: tendtoyj Date: Wed, 6 May 2026 15:56:16 +0900 Subject: [PATCH 1/4] refactor(pds-core,pds-ui): centralize tv/twMergeConfig in pds-core MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the `tv` factory and `tw-merge-config` from pds-ui into pds-core so consumers don't have to fetch them as separate files when installing components via shadcn. `cn` is now extended with the same `twMergeConfig` and lives in pds-core as the single source of truth — PDS typography group + arbitrary color utilities no longer drop each other regardless of which `cn` is reached. The registry's `utils` item is dropped; every component item now lists `@fluxloop-ai/pds-core` as a dependency, so `npx shadcn add ` is self-contained and lands in a buildable state on the consumer side. Co-Authored-By: Claude Opus 4.7 (1M context) --- .changeset/move-tv-to-core.md | 10 + INTEGRATION.md | 10 + packages/pds-core/package.json | 11 +- packages/pds-core/src/index.ts | 2 + packages/pds-core/src/utils/cn.ts | 5 +- packages/{pds-ui => pds-core}/src/utils/tv.ts | 5 +- .../src/utils/tw-merge-config.ts | 0 packages/pds-core/tsup.config.ts | 2 + packages/pds-ui/package.json | 1 - packages/pds-ui/registry.json | 299 ++++++++++++------ .../pds-ui/src/components/action-card.tsx | 3 +- .../pds-ui/src/components/action-tile.tsx | 3 +- packages/pds-ui/src/components/app-shell.tsx | 3 +- packages/pds-ui/src/components/avatar.tsx | 3 +- packages/pds-ui/src/components/badge.tsx | 3 +- packages/pds-ui/src/components/button.tsx | 3 +- .../src/components/chat-assistant-message.tsx | 3 +- .../src/components/chat-attachment-chip.tsx | 3 +- .../pds-ui/src/components/chat-composer.tsx | 3 +- .../src/components/chat-loading-dots.tsx | 3 +- .../src/components/chat-process-trace.tsx | 3 +- .../src/components/chat-user-message.tsx | 3 +- packages/pds-ui/src/components/checkbox.tsx | 3 +- packages/pds-ui/src/components/chip.tsx | 3 +- packages/pds-ui/src/components/combobox.tsx | 3 +- packages/pds-ui/src/components/dialog.tsx | 3 +- .../pds-ui/src/components/dropdown-menu.tsx | 3 +- packages/pds-ui/src/components/form.tsx | 3 +- .../pds-ui/src/components/icon-button.tsx | 3 +- packages/pds-ui/src/components/icon.tsx | 3 +- packages/pds-ui/src/components/input.tsx | 3 +- .../src/components/internal/close-button.tsx | 3 +- .../pds-ui/src/components/page-container.tsx | 3 +- packages/pds-ui/src/components/panel.tsx | 3 +- packages/pds-ui/src/components/popover.tsx | 3 +- packages/pds-ui/src/components/progress.tsx | 3 +- .../pds-ui/src/components/radio-group.tsx | 3 +- .../src/components/removable-tab-bar.tsx | 3 +- .../pds-ui/src/components/scroll-area.tsx | 3 +- .../pds-ui/src/components/section-header.tsx | 3 +- .../src/components/segmented-control.tsx | 3 +- packages/pds-ui/src/components/select.tsx | 3 +- packages/pds-ui/src/components/separator.tsx | 3 +- .../pds-ui/src/components/sidebar-list.tsx | 3 +- .../pds-ui/src/components/sidebar-menu.tsx | 3 +- packages/pds-ui/src/components/spinner.tsx | 3 +- packages/pds-ui/src/components/switch.tsx | 3 +- packages/pds-ui/src/components/tabs.tsx | 3 +- .../pds-ui/src/components/text-button.tsx | 3 +- packages/pds-ui/src/components/toast.tsx | 3 +- packages/pds-ui/src/components/tooltip.tsx | 3 +- packages/pds-ui/src/index.ts | 1 - packages/pds-ui/src/utils/cn.ts | 9 - pnpm-lock.yaml | 3 + 54 files changed, 282 insertions(+), 199 deletions(-) create mode 100644 .changeset/move-tv-to-core.md rename packages/{pds-ui => pds-core}/src/utils/tv.ts (71%) rename packages/{pds-ui => pds-core}/src/utils/tw-merge-config.ts (100%) delete mode 100644 packages/pds-ui/src/utils/cn.ts 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 index ad624cb..afc5250 100644 --- a/INTEGRATION.md +++ b/INTEGRATION.md @@ -96,8 +96,18 @@ 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 한다 — (B) 단계에서 `pds-core` 를 이미 깔았다면 그대로 빌드된다. shadcn 의 `lib/utils.ts` 를 따로 만들 필요 없음(레지스트리에 `utils` 항목이 없고, `cn` 의 SSOT 는 `pds-core`). + ```tsx // 사용 import { Button } from "@/components/ui/button"; diff --git a/packages/pds-core/package.json b/packages/pds-core/package.json index a312e29..bd11cfd 100644 --- a/packages/pds-core/package.json +++ b/packages/pds-core/package.json @@ -25,6 +25,14 @@ "types": "./dist/utils/cn.d.ts", "import": "./dist/utils/cn.js" }, + "./utils/tv": { + "types": "./dist/utils/tv.d.ts", + "import": "./dist/utils/tv.js" + }, + "./utils/tw-merge-config": { + "types": "./dist/utils/tw-merge-config.d.ts", + "import": "./dist/utils/tw-merge-config.js" + }, "./tailwind-preset": { "types": "./dist/tailwind-preset.d.ts", "import": "./dist/tailwind-preset.js" @@ -55,7 +63,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/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..2ca7772 100644 --- a/packages/pds-ui/registry.json +++ b/packages/pds-ui/registry.json @@ -3,27 +3,15 @@ "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 +25,11 @@ "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 +43,10 @@ "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 +60,11 @@ "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 +79,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 +96,10 @@ "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 +114,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 +131,14 @@ "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 +152,14 @@ "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 +173,10 @@ "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 +190,14 @@ "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 +211,9 @@ "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 +227,9 @@ "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 +243,10 @@ "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 +260,10 @@ "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 +277,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 +295,10 @@ "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 +312,10 @@ "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 +329,10 @@ "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 +346,11 @@ "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 +364,10 @@ "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 +381,10 @@ "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 +399,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 +417,10 @@ "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 +434,11 @@ "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 +452,12 @@ "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 +471,9 @@ "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 +487,9 @@ "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 +503,9 @@ "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 +519,13 @@ "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 +539,13 @@ "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 +559,9 @@ "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 +575,12 @@ "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 +594,13 @@ "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 +627,15 @@ "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 +649,9 @@ "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 +665,10 @@ "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 +683,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 +700,15 @@ "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 +722,14 @@ "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 +743,10 @@ "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 +760,10 @@ "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..9584e65 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 From 4654a018e9774ff7eae1fbe057ee6bef7a45a101 Mon Sep 17 00:00:00 2001 From: tendtoyj Date: Wed, 6 May 2026 15:56:23 +0900 Subject: [PATCH 2/4] fix(docs): keep AppShell inside mocked window in preview pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AppShell pins its root to `h-dvh`, which made the side panel overflow the bottom of the mocked desktop window in every preview that embeds it — most visibly clipping the chat composer's tone/model accessory row in the chat-pattern preview. Override with `className="h-full"` so AppShell follows the parent container in embedded contexts. The chat-pattern outer wrapper is also pinned with explicit `100dvh` math so the mocked window itself doesn't depend on flex padding behaviour. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/docs/app/preview/app-shell/page.tsx | 2 +- apps/docs/app/preview/chat-pattern/page.tsx | 7 ++++--- apps/docs/app/preview/page-container/page.tsx | 2 +- apps/docs/app/preview/sidebar-pattern/page.tsx | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) 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 ( -
+
- + +
- +
- + Date: Thu, 7 May 2026 17:04:02 +0900 Subject: [PATCH 3/4] docs+ci: merge INTEGRATION into README, add pds-core verify-exports guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fold INTEGRATION.md into README.md so consumers find install/usage/update flow in one place. Drop INTEGRATION.md. - Add packages/pds-core/scripts/verify-exports.mjs and wire it into pds-core's `build` (after tsup) and `prepublishOnly` so a future src/index.ts re-export drop is caught before publish — closes the stale dist sourcing fragility behind move-tv-to-core. - Expand the apps/docs landing-page package table to credit pds-core for the cn/tv utility helpers it now exposes. Co-Authored-By: Claude Opus 4.7 (1M context) --- INTEGRATION.md | 229 ------------------ README.md | 231 +++++++++++++++++-- apps/docs/content/docs/index.mdx | 2 +- packages/pds-core/package.json | 6 +- packages/pds-core/scripts/verify-exports.mjs | 50 ++++ pnpm-lock.yaml | 1 + 6 files changed, 270 insertions(+), 249 deletions(-) delete mode 100644 INTEGRATION.md create mode 100644 packages/pds-core/scripts/verify-exports.mjs diff --git a/INTEGRATION.md b/INTEGRATION.md deleted file mode 100644 index afc5250..0000000 --- a/INTEGRATION.md +++ /dev/null @@ -1,229 +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 -``` - -설치 가능한 컴포넌트 전체 목록은 레지스트리 인덱스에서 확인 — 브라우저나 `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 한다 — (B) 단계에서 `pds-core` 를 이미 깔았다면 그대로 빌드된다. shadcn 의 `lib/utils.ts` 를 따로 만들 필요 없음(레지스트리에 `utils` 항목이 없고, `cn` 의 SSOT 는 `pds-core`). - -```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/content/docs/index.mdx b/apps/docs/content/docs/index.mdx index 3c5847d..d6c77e5 100644 --- a/apps/docs/content/docs/index.mdx +++ b/apps/docs/content/docs/index.mdx @@ -16,7 +16,7 @@ Pluto Design System(PDS)은 The Pluto 앱의 **단일 UI 소스**다. | 패키지 | 역할 | | --- | --- | -| `@fluxloop-ai/pds-core` | 토큰(@theme), Tailwind preset, Pretendard, motion | +| `@fluxloop-ai/pds-core` | 토큰(@theme), Tailwind preset, Pretendard, motion, utility helpers (`cn`/`tv`) | | `@fluxloop-ai/pds-ui` | Radix + Tailwind 컴포넌트 (shadcn 레지스트리) | | `@fluxloop-ai/pds-icons` | phosphor 래퍼 + PDS 아이콘 + 브랜드 아이콘 | | `@fluxloop-ai/pds-markdown` | chat 슬롯용 markdown 렌더러 | diff --git a/packages/pds-core/package.json b/packages/pds-core/package.json index bd11cfd..5b88b0b 100644 --- a/packages/pds-core/package.json +++ b/packages/pds-core/package.json @@ -44,10 +44,12 @@ "src/fonts" ], "scripts": { - "build": "tsup", + "build": "tsup && node scripts/verify-exports.mjs", "dev": "tsup --watch", "typecheck": "tsc --noEmit", - "clean": "rm -rf dist .turbo" + "clean": "rm -rf dist .turbo", + "verify-exports": "node scripts/verify-exports.mjs", + "prepublishOnly": "node scripts/verify-exports.mjs" }, "peerDependencies": { "framer-motion": ">=11", 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/pnpm-lock.yaml b/pnpm-lock.yaml index 9584e65..ce85b86 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2727,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==} From c4ee44ca4e95c5504a7e32f31328c43ab5b385a5 Mon Sep 17 00:00:00 2001 From: tendtoyj Date: Thu, 7 May 2026 17:08:13 +0900 Subject: [PATCH 4/4] lint: apply biome formatting to registry.json and chat-pattern preview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit biome check 가 multi-line dependencies array (registry.json) 와 한 줄 공백 (chat-pattern/page.tsx) 을 fix-on-write 로만 수용하던 상태. CI lint 실패해서 따라잡음. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/docs/app/preview/chat-pattern/page.tsx | 1 - packages/pds-ui/registry.json | 226 ++++---------------- 2 files changed, 47 insertions(+), 180 deletions(-) diff --git a/apps/docs/app/preview/chat-pattern/page.tsx b/apps/docs/app/preview/chat-pattern/page.tsx index 566c251..9e76708 100644 --- a/apps/docs/app/preview/chat-pattern/page.tsx +++ b/apps/docs/app/preview/chat-pattern/page.tsx @@ -284,7 +284,6 @@ export default function ChatPatternPreviewPage() { -