@unikit-bridge/ui 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +99 -2
  3. package/docs/tokens.md +63 -0
  4. package/package.json +75 -4
  5. package/src/bridge/native.tsx +301 -0
  6. package/src/bridge/types.ts +174 -0
  7. package/src/bridge/uniwind-env.d.ts +1 -0
  8. package/src/bridge/web.tsx +247 -0
  9. package/src/components/accordion.tsx +231 -0
  10. package/src/components/alert.tsx +157 -0
  11. package/src/components/aspect-ratio.tsx +40 -0
  12. package/src/components/avatar.tsx +275 -0
  13. package/src/components/badge.tsx +65 -0
  14. package/src/components/breadcrumb.tsx +173 -0
  15. package/src/components/button-group.tsx +166 -0
  16. package/src/components/button.tsx +106 -0
  17. package/src/components/card.tsx +112 -0
  18. package/src/components/checkbox.tsx +94 -0
  19. package/src/components/collapsible.tsx +120 -0
  20. package/src/components/empty.tsx +160 -0
  21. package/src/components/field.tsx +349 -0
  22. package/src/components/input.tsx +39 -0
  23. package/src/components/item.tsx +243 -0
  24. package/src/components/kbd.tsx +62 -0
  25. package/src/components/label.tsx +33 -0
  26. package/src/components/pagination.tsx +204 -0
  27. package/src/components/progress.tsx +60 -0
  28. package/src/components/radio-group.tsx +148 -0
  29. package/src/components/separator.tsx +50 -0
  30. package/src/components/skeleton.tsx +36 -0
  31. package/src/components/switch.tsx +128 -0
  32. package/src/components/table.tsx +193 -0
  33. package/src/components/tabs.tsx +213 -0
  34. package/src/components/textarea.tsx +64 -0
  35. package/src/components/toggle-group.tsx +187 -0
  36. package/src/components/toggle.tsx +132 -0
  37. package/src/index.ts +321 -0
  38. package/src/lib/utils.ts +7 -0
  39. package/src/theme/index.ts +4 -0
  40. package/src/theme/presets.ts +187 -0
  41. package/src/theme/provider.tsx +82 -0
  42. package/src/theme/tokens.ts +75 -0
package/src/index.ts ADDED
@@ -0,0 +1,321 @@
1
+ // @unikit-bridge/ui 공개 API (T-6 완성)
2
+ export { cn } from "./lib/utils";
3
+
4
+ export {
5
+ Button,
6
+ buttonVariants,
7
+ buttonTextVariants,
8
+ type ButtonProps,
9
+ } from "./components/button";
10
+
11
+ export {
12
+ Card,
13
+ CardHeader,
14
+ CardTitle,
15
+ CardDescription,
16
+ CardContent,
17
+ CardFooter,
18
+ cardVariants,
19
+ cardHeaderVariants,
20
+ cardTitleVariants,
21
+ cardDescriptionVariants,
22
+ cardContentVariants,
23
+ cardFooterVariants,
24
+ type CardProps,
25
+ type CardHeaderProps,
26
+ type CardTitleProps,
27
+ type CardDescriptionProps,
28
+ type CardContentProps,
29
+ type CardFooterProps,
30
+ } from "./components/card";
31
+
32
+ export { Input, inputVariants, type InputProps } from "./components/input";
33
+
34
+ export { Label, labelVariants, type LabelProps } from "./components/label";
35
+
36
+ export {
37
+ Badge,
38
+ badgeVariants,
39
+ badgeTextVariants,
40
+ type BadgeProps,
41
+ } from "./components/badge";
42
+
43
+ // ── shadcn 카탈로그 1차 배치 23종(사이클 30, T-4 통합) ──────────────────────
44
+
45
+ export { Checkbox, type CheckboxProps } from "./components/checkbox";
46
+
47
+ export {
48
+ RadioGroup,
49
+ RadioGroupItem,
50
+ type RadioGroupProps,
51
+ type RadioGroupItemProps,
52
+ } from "./components/radio-group";
53
+
54
+ export {
55
+ Switch,
56
+ switchTrackVariants,
57
+ switchThumbVariants,
58
+ type SwitchProps,
59
+ } from "./components/switch";
60
+
61
+ export {
62
+ Toggle,
63
+ toggleVariants,
64
+ toggleTextVariants,
65
+ type ToggleProps,
66
+ } from "./components/toggle";
67
+
68
+ export {
69
+ ToggleGroup,
70
+ ToggleGroupItem,
71
+ type ToggleGroupProps,
72
+ type ToggleGroupItemProps,
73
+ } from "./components/toggle-group";
74
+
75
+ export {
76
+ Tabs,
77
+ TabsList,
78
+ TabsTrigger,
79
+ TabsContent,
80
+ tabsListVariants,
81
+ type TabsProps,
82
+ type TabsListProps,
83
+ type TabsTriggerProps,
84
+ type TabsContentProps,
85
+ } from "./components/tabs";
86
+
87
+ export {
88
+ Accordion,
89
+ AccordionItem,
90
+ AccordionTrigger,
91
+ AccordionContent,
92
+ type AccordionProps,
93
+ type AccordionItemProps,
94
+ type AccordionTriggerProps,
95
+ type AccordionContentProps,
96
+ } from "./components/accordion";
97
+
98
+ export {
99
+ Collapsible,
100
+ CollapsibleTrigger,
101
+ CollapsibleContent,
102
+ type CollapsibleProps,
103
+ type CollapsibleTriggerProps,
104
+ type CollapsibleContentProps,
105
+ } from "./components/collapsible";
106
+
107
+ export { Progress, type ProgressProps } from "./components/progress";
108
+
109
+ export { AspectRatio, type AspectRatioProps } from "./components/aspect-ratio";
110
+
111
+ export {
112
+ Skeleton,
113
+ skeletonVariants,
114
+ type SkeletonProps,
115
+ } from "./components/skeleton";
116
+
117
+ export {
118
+ Separator,
119
+ separatorVariants,
120
+ type SeparatorProps,
121
+ } from "./components/separator";
122
+
123
+ export {
124
+ Alert,
125
+ AlertTitle,
126
+ AlertDescription,
127
+ alertVariants,
128
+ alertGlyphVariants,
129
+ alertTitleVariants,
130
+ alertDescriptionVariants,
131
+ type AlertProps,
132
+ type AlertTitleProps,
133
+ type AlertDescriptionProps,
134
+ } from "./components/alert";
135
+
136
+ export {
137
+ Kbd,
138
+ KbdGroup,
139
+ kbdVariants,
140
+ kbdTextVariants,
141
+ kbdGroupVariants,
142
+ type KbdProps,
143
+ type KbdGroupProps,
144
+ } from "./components/kbd";
145
+
146
+ export {
147
+ Table,
148
+ TableHeader,
149
+ TableBody,
150
+ TableFooter,
151
+ TableRow,
152
+ TableHead,
153
+ TableCell,
154
+ TableCaption,
155
+ tableContainerVariants,
156
+ tableVariants,
157
+ tableHeaderVariants,
158
+ tableBodyVariants,
159
+ tableFooterVariants,
160
+ tableRowVariants,
161
+ tableHeadVariants,
162
+ tableHeadTextVariants,
163
+ tableCellVariants,
164
+ tableCellTextVariants,
165
+ tableCaptionVariants,
166
+ tableCaptionTextVariants,
167
+ type TableProps,
168
+ type TableHeaderProps,
169
+ type TableBodyProps,
170
+ type TableFooterProps,
171
+ type TableRowProps,
172
+ type TableHeadProps,
173
+ type TableCellProps,
174
+ type TableCaptionProps,
175
+ } from "./components/table";
176
+
177
+ export {
178
+ Breadcrumb,
179
+ BreadcrumbList,
180
+ BreadcrumbItem,
181
+ BreadcrumbLink,
182
+ BreadcrumbPage,
183
+ BreadcrumbSeparator,
184
+ BreadcrumbEllipsis,
185
+ breadcrumbListVariants,
186
+ breadcrumbItemVariants,
187
+ breadcrumbLinkVariants,
188
+ breadcrumbLinkTextVariants,
189
+ breadcrumbPageVariants,
190
+ breadcrumbSeparatorVariants,
191
+ breadcrumbSeparatorTextVariants,
192
+ breadcrumbEllipsisVariants,
193
+ breadcrumbEllipsisTextVariants,
194
+ type BreadcrumbProps,
195
+ type BreadcrumbListProps,
196
+ type BreadcrumbItemProps,
197
+ type BreadcrumbLinkProps,
198
+ type BreadcrumbPageProps,
199
+ type BreadcrumbSeparatorProps,
200
+ type BreadcrumbEllipsisProps,
201
+ } from "./components/breadcrumb";
202
+
203
+ export {
204
+ Pagination,
205
+ PaginationContent,
206
+ PaginationLink,
207
+ PaginationItem,
208
+ PaginationPrevious,
209
+ PaginationNext,
210
+ PaginationEllipsis,
211
+ paginationVariants,
212
+ paginationContentVariants,
213
+ paginationGlyphVariants,
214
+ paginationLabelVariants,
215
+ paginationEllipsisVariants,
216
+ paginationEllipsisTextVariants,
217
+ type PaginationProps,
218
+ type PaginationContentProps,
219
+ type PaginationLinkProps,
220
+ type PaginationItemProps,
221
+ type PaginationPreviousProps,
222
+ type PaginationNextProps,
223
+ type PaginationEllipsisProps,
224
+ } from "./components/pagination";
225
+
226
+ export {
227
+ Textarea,
228
+ textareaVariants,
229
+ type TextareaProps,
230
+ } from "./components/textarea";
231
+
232
+ export {
233
+ Avatar,
234
+ AvatarImage,
235
+ AvatarFallback,
236
+ AvatarBadge,
237
+ AvatarGroup,
238
+ AvatarGroupCount,
239
+ type AvatarSize,
240
+ type AvatarProps,
241
+ type AvatarImageProps,
242
+ type AvatarFallbackProps,
243
+ type AvatarBadgeProps,
244
+ type AvatarGroupProps,
245
+ type AvatarGroupCountProps,
246
+ } from "./components/avatar";
247
+
248
+ export {
249
+ Empty,
250
+ EmptyHeader,
251
+ EmptyTitle,
252
+ EmptyDescription,
253
+ EmptyContent,
254
+ EmptyMedia,
255
+ type EmptyProps,
256
+ type EmptyHeaderProps,
257
+ type EmptyTitleProps,
258
+ type EmptyDescriptionProps,
259
+ type EmptyContentProps,
260
+ type EmptyMediaProps,
261
+ } from "./components/empty";
262
+
263
+ export {
264
+ Field,
265
+ FieldLabel,
266
+ FieldDescription,
267
+ FieldError,
268
+ FieldGroup,
269
+ FieldLegend,
270
+ FieldSeparator,
271
+ FieldSet,
272
+ FieldContent,
273
+ FieldTitle,
274
+ type FieldSetProps,
275
+ type FieldLegendProps,
276
+ type FieldGroupProps,
277
+ type FieldProps,
278
+ type FieldContentProps,
279
+ type FieldLabelProps,
280
+ type FieldTitleProps,
281
+ type FieldDescriptionProps,
282
+ type FieldSeparatorProps,
283
+ type FieldErrorProps,
284
+ } from "./components/field";
285
+
286
+ export {
287
+ Item,
288
+ ItemMedia,
289
+ ItemContent,
290
+ ItemActions,
291
+ ItemGroup,
292
+ ItemSeparator,
293
+ ItemTitle,
294
+ ItemDescription,
295
+ ItemHeader,
296
+ ItemFooter,
297
+ type ItemGroupProps,
298
+ type ItemSeparatorProps,
299
+ type ItemProps,
300
+ type ItemMediaProps,
301
+ type ItemContentProps,
302
+ type ItemTitleProps,
303
+ type ItemDescriptionProps,
304
+ type ItemActionsProps,
305
+ type ItemHeaderProps,
306
+ type ItemFooterProps,
307
+ } from "./components/item";
308
+
309
+ export {
310
+ ButtonGroup,
311
+ ButtonGroupSeparator,
312
+ ButtonGroupText,
313
+ buttonGroupVariants,
314
+ type ButtonGroupProps,
315
+ type ButtonGroupTextProps,
316
+ type ButtonGroupSeparatorProps,
317
+ } from "./components/button-group";
318
+
319
+ // 테마(T-5가 packages/unikit/src/theme/ 을 병렬 생성) — 디렉터리 존재 여부와
320
+ // 무관하게 이 라인은 항상 유지한다(T-6 지시).
321
+ export * from "./theme";
@@ -0,0 +1,7 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ /** shadcn/ui 표준 cn 유틸 — clsx(조건 결합) + tailwind-merge(클래스 충돌 병합) */
5
+ export function cn(...inputs: ClassValue[]) {
6
+ return twMerge(clsx(inputs));
7
+ }
@@ -0,0 +1,4 @@
1
+ // UniKit 테마 공개 내보내기 — tokens(단일 출처) + Provider.
2
+ // src/index.ts(T-6 소유)의 `export * from "./theme";`가 이 파일을 가리킨다.
3
+ export * from "./tokens";
4
+ export * from "./provider";
@@ -0,0 +1,187 @@
1
+ /**
2
+ * UniKit 프리셋 테마 — 미리 준비된 디자인 세트 3종 (3차 사이클, T-17).
3
+ *
4
+ * 값의 원천: docs/sdd/22-design-plan.md §2.3(설계 확정값 표).
5
+ * 계산 재료는 Tailwind 공식 팔레트(hex) → HSL 채널(소수 1자리)이며,
6
+ * 색 선택 규칙(22-design-plan §2.2)은 다음과 같다:
7
+ * - 라이트 primary = «흰 전경과 WCAG 대비 ≥4.5:1을 만족하는 가장 밝은 주색 단계»
8
+ * → blue-600(5.17) / emerald-700(5.48) / amber-700(5.02)
9
+ * - 다크 primary = 주색-500
10
+ * - 다크 primary-foreground = 중립-950 (주색-950은 blue에서 4.00:1로 미달 — 중립-950은 3종 전부 ≥5.48)
11
+ * - 중립 계열 N = blue→slate, emerald→zinc, amber→stone (shadcn 관례)
12
+ * - destructive 쌍·radius = 기본 테마(tokens.ts)값 공통 — 다크 --destructive는
13
+ * 사이클 30 G4/G5 조정값(0 72% 65%, red-400 계) 반영(22-design-plan §2.3 수정 이력)
14
+ *
15
+ * 기본 테마(zinc)는 이 파일에 없다 — src/theme/tokens.ts의 lightTokens/darkTokens이
16
+ * 곧 기본 프리셋이다(값 사본 금지). registry 항목 이름 매핑은 generate-design.ts의
17
+ * THEME_ITEMS 상수가 담당한다.
18
+ *
19
+ * 주의: 값을 임의로 변경하면 안 된다 — 수정이 필요하면 설계 문서(22-design-plan §2.2·§2.3)를
20
+ * 먼저 고친다. audit:design가 registry 사본·대비 기준(≥4.5)과의 일치를 검사한다.
21
+ */
22
+ import type { UnikitThemeTokens } from "./tokens";
23
+
24
+ /** 프리셋 정의 — label·neutral·primaryStep은 카탈로그 표기용 메타(값 아님) */
25
+ export interface ThemePreset {
26
+ label: string;
27
+ /** 중립 계열 이름(slate/zinc/stone) */
28
+ neutral: string;
29
+ /** 카탈로그 표기용 라이트 primary 단계(예: "blue-600") */
30
+ primaryStep: string;
31
+ light: UnikitThemeTokens;
32
+ dark: UnikitThemeTokens;
33
+ }
34
+
35
+ /** 프리셋 3종 — 키 순서는 tokens.ts 선언 순서와 동일(멱등성 기준) */
36
+ export const themePresets = {
37
+ blue: {
38
+ label: "Blue",
39
+ neutral: "slate",
40
+ primaryStep: "blue-600",
41
+ light: {
42
+ "--background": "0 0% 100%",
43
+ "--foreground": "228.6 84% 4.9%",
44
+ "--card": "0 0% 100%",
45
+ "--card-foreground": "228.6 84% 4.9%",
46
+ "--popover": "0 0% 100%",
47
+ "--popover-foreground": "228.6 84% 4.9%",
48
+ "--primary": "221.2 83.2% 53.3%",
49
+ "--primary-foreground": "0 0% 100%",
50
+ "--secondary": "210 40% 96.1%",
51
+ "--secondary-foreground": "222.2 47.4% 11.2%",
52
+ "--muted": "210 40% 96.1%",
53
+ "--muted-foreground": "215.4 16.3% 46.9%",
54
+ "--accent": "210 40% 96.1%",
55
+ "--accent-foreground": "222.2 47.4% 11.2%",
56
+ "--destructive": "0 72.2% 50.6%",
57
+ "--destructive-foreground": "0 0% 98%",
58
+ "--border": "214.3 31.8% 91.4%",
59
+ "--input": "214.3 31.8% 91.4%",
60
+ "--ring": "221.2 83.2% 53.3%",
61
+ "--radius": "0.5rem",
62
+ },
63
+ dark: {
64
+ "--background": "228.6 84% 4.9%",
65
+ "--foreground": "210 40% 98%",
66
+ "--card": "228.6 84% 4.9%",
67
+ "--card-foreground": "210 40% 98%",
68
+ "--popover": "228.6 84% 4.9%",
69
+ "--popover-foreground": "210 40% 98%",
70
+ "--primary": "217.2 91.2% 59.8%",
71
+ "--primary-foreground": "228.6 84% 4.9%",
72
+ "--secondary": "217.2 32.6% 17.5%",
73
+ "--secondary-foreground": "210 40% 98%",
74
+ "--muted": "217.2 32.6% 17.5%",
75
+ "--muted-foreground": "215 20.2% 65.1%",
76
+ "--accent": "217.2 32.6% 17.5%",
77
+ "--accent-foreground": "210 40% 98%",
78
+ "--destructive": "0 72% 65%",
79
+ "--destructive-foreground": "0 0% 98%",
80
+ "--border": "217.2 32.6% 17.5%",
81
+ "--input": "217.2 32.6% 17.5%",
82
+ "--ring": "217.2 91.2% 59.8%",
83
+ "--radius": "0.5rem",
84
+ },
85
+ },
86
+ emerald: {
87
+ label: "Emerald",
88
+ neutral: "zinc",
89
+ primaryStep: "emerald-700",
90
+ light: {
91
+ "--background": "0 0% 100%",
92
+ "--foreground": "240 10% 3.9%",
93
+ "--card": "0 0% 100%",
94
+ "--card-foreground": "240 10% 3.9%",
95
+ "--popover": "0 0% 100%",
96
+ "--popover-foreground": "240 10% 3.9%",
97
+ "--primary": "162.9 93.5% 24.3%",
98
+ "--primary-foreground": "0 0% 100%",
99
+ "--secondary": "240 4.8% 95.9%",
100
+ "--secondary-foreground": "240 5.9% 10%",
101
+ "--muted": "240 4.8% 95.9%",
102
+ "--muted-foreground": "240 3.8% 46.1%",
103
+ "--accent": "240 4.8% 95.9%",
104
+ "--accent-foreground": "240 5.9% 10%",
105
+ "--destructive": "0 72.2% 50.6%",
106
+ "--destructive-foreground": "0 0% 98%",
107
+ "--border": "240 5.9% 90%",
108
+ "--input": "240 5.9% 90%",
109
+ "--ring": "162.9 93.5% 24.3%",
110
+ "--radius": "0.5rem",
111
+ },
112
+ dark: {
113
+ "--background": "240 10% 3.9%",
114
+ "--foreground": "0 0% 98%",
115
+ "--card": "240 10% 3.9%",
116
+ "--card-foreground": "0 0% 98%",
117
+ "--popover": "240 10% 3.9%",
118
+ "--popover-foreground": "0 0% 98%",
119
+ "--primary": "160.1 84.1% 39.4%",
120
+ "--primary-foreground": "240 10% 3.9%",
121
+ "--secondary": "240 3.7% 15.9%",
122
+ "--secondary-foreground": "0 0% 98%",
123
+ "--muted": "240 3.7% 15.9%",
124
+ "--muted-foreground": "240 5% 64.9%",
125
+ "--accent": "240 3.7% 15.9%",
126
+ "--accent-foreground": "0 0% 98%",
127
+ "--destructive": "0 72% 65%",
128
+ "--destructive-foreground": "0 0% 98%",
129
+ "--border": "240 3.7% 15.9%",
130
+ "--input": "240 3.7% 15.9%",
131
+ "--ring": "160.1 84.1% 39.4%",
132
+ "--radius": "0.5rem",
133
+ },
134
+ },
135
+ amber: {
136
+ label: "Amber",
137
+ neutral: "stone",
138
+ primaryStep: "amber-700",
139
+ light: {
140
+ "--background": "0 0% 100%",
141
+ "--foreground": "20 14.3% 4.1%",
142
+ "--card": "0 0% 100%",
143
+ "--card-foreground": "20 14.3% 4.1%",
144
+ "--popover": "0 0% 100%",
145
+ "--popover-foreground": "20 14.3% 4.1%",
146
+ "--primary": "26 90.5% 37.1%",
147
+ "--primary-foreground": "0 0% 100%",
148
+ "--secondary": "60 4.8% 95.9%",
149
+ "--secondary-foreground": "24 9.8% 10%",
150
+ "--muted": "60 4.8% 95.9%",
151
+ "--muted-foreground": "25 5.3% 44.7%",
152
+ "--accent": "60 4.8% 95.9%",
153
+ "--accent-foreground": "24 9.8% 10%",
154
+ "--destructive": "0 72.2% 50.6%",
155
+ "--destructive-foreground": "0 0% 98%",
156
+ "--border": "20 5.9% 90%",
157
+ "--input": "20 5.9% 90%",
158
+ "--ring": "26 90.5% 37.1%",
159
+ "--radius": "0.5rem",
160
+ },
161
+ dark: {
162
+ "--background": "20 14.3% 4.1%",
163
+ "--foreground": "60 9.1% 97.8%",
164
+ "--card": "20 14.3% 4.1%",
165
+ "--card-foreground": "60 9.1% 97.8%",
166
+ "--popover": "20 14.3% 4.1%",
167
+ "--popover-foreground": "60 9.1% 97.8%",
168
+ "--primary": "37.7 92.1% 50.2%",
169
+ "--primary-foreground": "20 14.3% 4.1%",
170
+ "--secondary": "12 6.5% 15.1%",
171
+ "--secondary-foreground": "60 9.1% 97.8%",
172
+ "--muted": "12 6.5% 15.1%",
173
+ "--muted-foreground": "24 5.4% 63.9%",
174
+ "--accent": "12 6.5% 15.1%",
175
+ "--accent-foreground": "60 9.1% 97.8%",
176
+ "--destructive": "0 72% 65%",
177
+ "--destructive-foreground": "0 0% 98%",
178
+ "--border": "12 6.5% 15.1%",
179
+ "--input": "12 6.5% 15.1%",
180
+ "--ring": "37.7 92.1% 50.2%",
181
+ "--radius": "0.5rem",
182
+ },
183
+ },
184
+ } as const satisfies Record<string, ThemePreset>;
185
+
186
+ /** 프리셋 키 집합("blue" | "emerald" | "amber") */
187
+ export type PresetKey = keyof typeof themePresets;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * UniKit 테마 Provider — colorScheme 상태를 보유하고 브리지를 통해
3
+ * 각 플랫폼의 스타일링 엔진에 반영한다.
4
+ *
5
+ * 메커니즘(엔진 교체: NativeWind → 브리지 구조):
6
+ * - 토큰 값은 이제 JS 주입(vars())이 아니라 소비자 앱의 global.css에 상주한다
7
+ * (기본값 = generate:design이 tokens.ts에서 생성).
8
+ * - 이 Provider는 스킴 상태만 보유하고, 변경 시 bridge.applyColorScheme을 호출한다:
9
+ * · native → uniwind Uniwind.setTheme(다크 변형·CSS 변수가 활성 테마로 전환)
10
+ * · web → 문서 루트 .dark 클래스 토글(표준 Tailwind dark 변형)
11
+ *
12
+ * 동작 규격(02-design §1.4): 즉시 전환, 영속화 없음, 기본값 라이트.
13
+ */
14
+ import {
15
+ createContext,
16
+ useContext,
17
+ useEffect,
18
+ useMemo,
19
+ useState,
20
+ type ReactNode,
21
+ } from "react";
22
+ import { applyColorScheme } from "@unikit-bridge/ui/primitives";
23
+
24
+ import type { UnikitColorScheme } from "./tokens";
25
+
26
+ export type UnikitThemeContextValue = {
27
+ /** 현재 색 스킴 */
28
+ colorScheme: UnikitColorScheme;
29
+ /** 스킴 직접 지정 */
30
+ setColorScheme: (colorScheme: UnikitColorScheme) => void;
31
+ /** 라이트 ↔ 다크 즉시 전환 */
32
+ toggleColorScheme: () => void;
33
+ };
34
+
35
+ const UnikitThemeContext = createContext<UnikitThemeContextValue | null>(null);
36
+
37
+ export type UnikitThemeProviderProps = {
38
+ children: ReactNode;
39
+ /** 초기 스킴(기본값 "light" — 02-design §1.4) */
40
+ defaultColorScheme?: UnikitColorScheme;
41
+ };
42
+
43
+ export function UnikitThemeProvider({
44
+ children,
45
+ defaultColorScheme = "light",
46
+ }: UnikitThemeProviderProps) {
47
+ const [colorScheme, setColorScheme] =
48
+ useState<UnikitColorScheme>(defaultColorScheme);
49
+
50
+ const value = useMemo<UnikitThemeContextValue>(
51
+ () => ({
52
+ colorScheme,
53
+ setColorScheme,
54
+ toggleColorScheme: () =>
55
+ setColorScheme((prev) => (prev === "light" ? "dark" : "light")),
56
+ }),
57
+ [colorScheme],
58
+ );
59
+
60
+ // 스킴 → 엔진 바인딩(마운트 시 기본값도 반영 — 네이티브에서 adaptive system
61
+ // 테마 대신 설계 기본값 라이트로 고정된다, 02-design §1.4).
62
+ useEffect(() => {
63
+ applyColorScheme(colorScheme);
64
+ }, [colorScheme]);
65
+
66
+ return (
67
+ <UnikitThemeContext.Provider value={value}>
68
+ {children}
69
+ </UnikitThemeContext.Provider>
70
+ );
71
+ }
72
+
73
+ /** 현재 테마 상태 접근 훅 — UnikitThemeProvider 내부에서만 사용할 수 있다 */
74
+ export function useUnikitTheme(): UnikitThemeContextValue {
75
+ const context = useContext(UnikitThemeContext);
76
+ if (context == null) {
77
+ throw new Error(
78
+ "useUnikitTheme는 UnikitThemeProvider 내부에서 사용해야 합니다.",
79
+ );
80
+ }
81
+ return context;
82
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * UniKit 디자인 토큰 — 라이트/다크 단일 출처(single source of truth).
3
+ *
4
+ * 값의 원천: docs/sdd/02-design.md §2.1(색 19종, HSL 채널값)·§2.2(반경).
5
+ * 값 형식은 TW3 `hsl(var(--token))` 매핑과 호환하는 HSL 채널 문자열("H S% L%")이며,
6
+ * `vars()` 주입(웹·네이티브 공통)과 apps/web/app/globals.css의 변수 블록이
7
+ * 이 객체를 그대로 복사한 값이다(완료 기준: 값 1:1 대조).
8
+ *
9
+ * 주의: 값을 임의로 변경하면 안 된다 — 수정이 필요하면 설계 문서(02-design)를 먼저 고친다.
10
+ * `--destructive`(라이트)는 shadcn 기본값이 아닌 red-600 계(02-design §2.1 주석, WCAG AA 확보).
11
+ * `--destructive`(다크)도 shadcn 원본 대비 red-400 계로 조정(사이클 30 G4 — 원본 값은
12
+ * 다크 배경 대비 1.99:1로 텍스트가 사실상 안 읽힘. 조정값 6.1:1).
13
+ * `--radius`는 스킴과 무관한 공통값(§2.2)이며 두 객체에 동일하게 포함한다.
14
+ */
15
+
16
+ export type UnikitColorScheme = "light" | "dark";
17
+
18
+ /** 라이트 스킴 토큰(기본값 — 02-design §1.4) */
19
+ export const lightTokens = {
20
+ "--background": "0 0% 100%",
21
+ "--foreground": "240 10% 3.9%",
22
+ "--card": "0 0% 100%",
23
+ "--card-foreground": "240 10% 3.9%",
24
+ "--popover": "0 0% 100%",
25
+ "--popover-foreground": "240 10% 3.9%",
26
+ "--primary": "240 5.9% 10%",
27
+ "--primary-foreground": "0 0% 98%",
28
+ "--secondary": "240 4.8% 95.9%",
29
+ "--secondary-foreground": "240 5.9% 10%",
30
+ "--muted": "240 4.8% 95.9%",
31
+ "--muted-foreground": "240 3.8% 46.1%",
32
+ "--accent": "240 4.8% 95.9%",
33
+ "--accent-foreground": "240 5.9% 10%",
34
+ "--destructive": "0 72.2% 50.6%",
35
+ "--destructive-foreground": "0 0% 98%",
36
+ "--border": "240 5.9% 90%",
37
+ "--input": "240 5.9% 90%",
38
+ "--ring": "240 5.9% 10%",
39
+ "--radius": "0.5rem",
40
+ } as const;
41
+
42
+ /** 다크 스킴 토큰 — satisfies로 라이트와 키 집합이 동일함을 컴파일 타임에 강제 */
43
+ export const darkTokens = {
44
+ "--background": "240 10% 3.9%",
45
+ "--foreground": "0 0% 98%",
46
+ "--card": "240 10% 3.9%",
47
+ "--card-foreground": "0 0% 98%",
48
+ "--popover": "240 10% 3.9%",
49
+ "--popover-foreground": "0 0% 98%",
50
+ "--primary": "0 0% 98%",
51
+ "--primary-foreground": "240 5.9% 10%",
52
+ "--secondary": "240 3.7% 15.9%",
53
+ "--secondary-foreground": "0 0% 98%",
54
+ "--muted": "240 3.7% 15.9%",
55
+ "--muted-foreground": "240 5% 64.9%",
56
+ "--accent": "240 3.7% 15.9%",
57
+ "--accent-foreground": "0 0% 98%",
58
+ "--destructive": "0 72% 65%",
59
+ "--destructive-foreground": "0 0% 98%",
60
+ "--border": "240 3.7% 15.9%",
61
+ "--input": "240 3.7% 15.9%",
62
+ "--ring": "240 4.9% 83.9%",
63
+ "--radius": "0.5rem",
64
+ } as const satisfies Record<keyof typeof lightTokens, string>;
65
+
66
+ /** 토큰 CSS 변수 이름(--background 등 색 19종 + --radius) */
67
+ export type UnikitThemeTokenName = keyof typeof lightTokens;
68
+
69
+ /** CSS 변수 이름 → 값("H S% L%" 문자열) 매핑. vars() 주입·globals.css 복사 양쪽에 쓴다. */
70
+ export type UnikitThemeTokens = Record<UnikitThemeTokenName, string>;
71
+
72
+ /** 스킴에 해당하는 토큰 객체를 반환한다 */
73
+ export function getThemeTokens(colorScheme: UnikitColorScheme): UnikitThemeTokens {
74
+ return colorScheme === "dark" ? darkTokens : lightTokens;
75
+ }