@unikit-bridge/ui 0.0.0-stage → 0.1.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.
- package/LICENSE +21 -0
- package/README.md +99 -2
- package/docs/tokens.md +63 -0
- package/package.json +75 -4
- package/src/bridge/native.tsx +170 -0
- package/src/bridge/types.ts +96 -0
- package/src/bridge/uniwind-env.d.ts +1 -0
- package/src/bridge/web.tsx +166 -0
- package/src/components/badge.tsx +65 -0
- package/src/components/button.tsx +106 -0
- package/src/components/card.tsx +112 -0
- package/src/components/input.tsx +39 -0
- package/src/components/label.tsx +33 -0
- package/src/index.ts +45 -0
- package/src/lib/utils.ts +7 -0
- package/src/theme/index.ts +4 -0
- package/src/theme/presets.ts +186 -0
- package/src/theme/provider.tsx +82 -0
- package/src/theme/tokens.ts +73 -0
|
@@ -0,0 +1,186 @@
|
|
|
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)값 공통
|
|
13
|
+
*
|
|
14
|
+
* 기본 테마(zinc)는 이 파일에 없다 — src/theme/tokens.ts의 lightTokens/darkTokens이
|
|
15
|
+
* 곧 기본 프리셋이다(값 사본 금지). registry 항목 이름 매핑은 generate-design.ts의
|
|
16
|
+
* THEME_ITEMS 상수가 담당한다.
|
|
17
|
+
*
|
|
18
|
+
* 주의: 값을 임의로 변경하면 안 된다 — 수정이 필요하면 설계 문서(22-design-plan §2.2·§2.3)를
|
|
19
|
+
* 먼저 고친다. audit:design가 registry 사본·대비 기준(≥4.5)과의 일치를 검사한다.
|
|
20
|
+
*/
|
|
21
|
+
import type { UnikitThemeTokens } from "./tokens";
|
|
22
|
+
|
|
23
|
+
/** 프리셋 정의 — label·neutral·primaryStep은 카탈로그 표기용 메타(값 아님) */
|
|
24
|
+
export interface ThemePreset {
|
|
25
|
+
label: string;
|
|
26
|
+
/** 중립 계열 이름(slate/zinc/stone) */
|
|
27
|
+
neutral: string;
|
|
28
|
+
/** 카탈로그 표기용 라이트 primary 단계(예: "blue-600") */
|
|
29
|
+
primaryStep: string;
|
|
30
|
+
light: UnikitThemeTokens;
|
|
31
|
+
dark: UnikitThemeTokens;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 프리셋 3종 — 키 순서는 tokens.ts 선언 순서와 동일(멱등성 기준) */
|
|
35
|
+
export const themePresets = {
|
|
36
|
+
blue: {
|
|
37
|
+
label: "Blue",
|
|
38
|
+
neutral: "slate",
|
|
39
|
+
primaryStep: "blue-600",
|
|
40
|
+
light: {
|
|
41
|
+
"--background": "0 0% 100%",
|
|
42
|
+
"--foreground": "228.6 84% 4.9%",
|
|
43
|
+
"--card": "0 0% 100%",
|
|
44
|
+
"--card-foreground": "228.6 84% 4.9%",
|
|
45
|
+
"--popover": "0 0% 100%",
|
|
46
|
+
"--popover-foreground": "228.6 84% 4.9%",
|
|
47
|
+
"--primary": "221.2 83.2% 53.3%",
|
|
48
|
+
"--primary-foreground": "0 0% 100%",
|
|
49
|
+
"--secondary": "210 40% 96.1%",
|
|
50
|
+
"--secondary-foreground": "222.2 47.4% 11.2%",
|
|
51
|
+
"--muted": "210 40% 96.1%",
|
|
52
|
+
"--muted-foreground": "215.4 16.3% 46.9%",
|
|
53
|
+
"--accent": "210 40% 96.1%",
|
|
54
|
+
"--accent-foreground": "222.2 47.4% 11.2%",
|
|
55
|
+
"--destructive": "0 72.2% 50.6%",
|
|
56
|
+
"--destructive-foreground": "0 0% 98%",
|
|
57
|
+
"--border": "214.3 31.8% 91.4%",
|
|
58
|
+
"--input": "214.3 31.8% 91.4%",
|
|
59
|
+
"--ring": "221.2 83.2% 53.3%",
|
|
60
|
+
"--radius": "0.5rem",
|
|
61
|
+
},
|
|
62
|
+
dark: {
|
|
63
|
+
"--background": "228.6 84% 4.9%",
|
|
64
|
+
"--foreground": "210 40% 98%",
|
|
65
|
+
"--card": "228.6 84% 4.9%",
|
|
66
|
+
"--card-foreground": "210 40% 98%",
|
|
67
|
+
"--popover": "228.6 84% 4.9%",
|
|
68
|
+
"--popover-foreground": "210 40% 98%",
|
|
69
|
+
"--primary": "217.2 91.2% 59.8%",
|
|
70
|
+
"--primary-foreground": "228.6 84% 4.9%",
|
|
71
|
+
"--secondary": "217.2 32.6% 17.5%",
|
|
72
|
+
"--secondary-foreground": "210 40% 98%",
|
|
73
|
+
"--muted": "217.2 32.6% 17.5%",
|
|
74
|
+
"--muted-foreground": "215 20.2% 65.1%",
|
|
75
|
+
"--accent": "217.2 32.6% 17.5%",
|
|
76
|
+
"--accent-foreground": "210 40% 98%",
|
|
77
|
+
"--destructive": "0 62.8% 30.6%",
|
|
78
|
+
"--destructive-foreground": "0 0% 98%",
|
|
79
|
+
"--border": "217.2 32.6% 17.5%",
|
|
80
|
+
"--input": "217.2 32.6% 17.5%",
|
|
81
|
+
"--ring": "217.2 91.2% 59.8%",
|
|
82
|
+
"--radius": "0.5rem",
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
emerald: {
|
|
86
|
+
label: "Emerald",
|
|
87
|
+
neutral: "zinc",
|
|
88
|
+
primaryStep: "emerald-700",
|
|
89
|
+
light: {
|
|
90
|
+
"--background": "0 0% 100%",
|
|
91
|
+
"--foreground": "240 10% 3.9%",
|
|
92
|
+
"--card": "0 0% 100%",
|
|
93
|
+
"--card-foreground": "240 10% 3.9%",
|
|
94
|
+
"--popover": "0 0% 100%",
|
|
95
|
+
"--popover-foreground": "240 10% 3.9%",
|
|
96
|
+
"--primary": "162.9 93.5% 24.3%",
|
|
97
|
+
"--primary-foreground": "0 0% 100%",
|
|
98
|
+
"--secondary": "240 4.8% 95.9%",
|
|
99
|
+
"--secondary-foreground": "240 5.9% 10%",
|
|
100
|
+
"--muted": "240 4.8% 95.9%",
|
|
101
|
+
"--muted-foreground": "240 3.8% 46.1%",
|
|
102
|
+
"--accent": "240 4.8% 95.9%",
|
|
103
|
+
"--accent-foreground": "240 5.9% 10%",
|
|
104
|
+
"--destructive": "0 72.2% 50.6%",
|
|
105
|
+
"--destructive-foreground": "0 0% 98%",
|
|
106
|
+
"--border": "240 5.9% 90%",
|
|
107
|
+
"--input": "240 5.9% 90%",
|
|
108
|
+
"--ring": "162.9 93.5% 24.3%",
|
|
109
|
+
"--radius": "0.5rem",
|
|
110
|
+
},
|
|
111
|
+
dark: {
|
|
112
|
+
"--background": "240 10% 3.9%",
|
|
113
|
+
"--foreground": "0 0% 98%",
|
|
114
|
+
"--card": "240 10% 3.9%",
|
|
115
|
+
"--card-foreground": "0 0% 98%",
|
|
116
|
+
"--popover": "240 10% 3.9%",
|
|
117
|
+
"--popover-foreground": "0 0% 98%",
|
|
118
|
+
"--primary": "160.1 84.1% 39.4%",
|
|
119
|
+
"--primary-foreground": "240 10% 3.9%",
|
|
120
|
+
"--secondary": "240 3.7% 15.9%",
|
|
121
|
+
"--secondary-foreground": "0 0% 98%",
|
|
122
|
+
"--muted": "240 3.7% 15.9%",
|
|
123
|
+
"--muted-foreground": "240 5% 64.9%",
|
|
124
|
+
"--accent": "240 3.7% 15.9%",
|
|
125
|
+
"--accent-foreground": "0 0% 98%",
|
|
126
|
+
"--destructive": "0 62.8% 30.6%",
|
|
127
|
+
"--destructive-foreground": "0 0% 98%",
|
|
128
|
+
"--border": "240 3.7% 15.9%",
|
|
129
|
+
"--input": "240 3.7% 15.9%",
|
|
130
|
+
"--ring": "160.1 84.1% 39.4%",
|
|
131
|
+
"--radius": "0.5rem",
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
amber: {
|
|
135
|
+
label: "Amber",
|
|
136
|
+
neutral: "stone",
|
|
137
|
+
primaryStep: "amber-700",
|
|
138
|
+
light: {
|
|
139
|
+
"--background": "0 0% 100%",
|
|
140
|
+
"--foreground": "20 14.3% 4.1%",
|
|
141
|
+
"--card": "0 0% 100%",
|
|
142
|
+
"--card-foreground": "20 14.3% 4.1%",
|
|
143
|
+
"--popover": "0 0% 100%",
|
|
144
|
+
"--popover-foreground": "20 14.3% 4.1%",
|
|
145
|
+
"--primary": "26 90.5% 37.1%",
|
|
146
|
+
"--primary-foreground": "0 0% 100%",
|
|
147
|
+
"--secondary": "60 4.8% 95.9%",
|
|
148
|
+
"--secondary-foreground": "24 9.8% 10%",
|
|
149
|
+
"--muted": "60 4.8% 95.9%",
|
|
150
|
+
"--muted-foreground": "25 5.3% 44.7%",
|
|
151
|
+
"--accent": "60 4.8% 95.9%",
|
|
152
|
+
"--accent-foreground": "24 9.8% 10%",
|
|
153
|
+
"--destructive": "0 72.2% 50.6%",
|
|
154
|
+
"--destructive-foreground": "0 0% 98%",
|
|
155
|
+
"--border": "20 5.9% 90%",
|
|
156
|
+
"--input": "20 5.9% 90%",
|
|
157
|
+
"--ring": "26 90.5% 37.1%",
|
|
158
|
+
"--radius": "0.5rem",
|
|
159
|
+
},
|
|
160
|
+
dark: {
|
|
161
|
+
"--background": "20 14.3% 4.1%",
|
|
162
|
+
"--foreground": "60 9.1% 97.8%",
|
|
163
|
+
"--card": "20 14.3% 4.1%",
|
|
164
|
+
"--card-foreground": "60 9.1% 97.8%",
|
|
165
|
+
"--popover": "20 14.3% 4.1%",
|
|
166
|
+
"--popover-foreground": "60 9.1% 97.8%",
|
|
167
|
+
"--primary": "37.7 92.1% 50.2%",
|
|
168
|
+
"--primary-foreground": "20 14.3% 4.1%",
|
|
169
|
+
"--secondary": "12 6.5% 15.1%",
|
|
170
|
+
"--secondary-foreground": "60 9.1% 97.8%",
|
|
171
|
+
"--muted": "12 6.5% 15.1%",
|
|
172
|
+
"--muted-foreground": "24 5.4% 63.9%",
|
|
173
|
+
"--accent": "12 6.5% 15.1%",
|
|
174
|
+
"--accent-foreground": "60 9.1% 97.8%",
|
|
175
|
+
"--destructive": "0 62.8% 30.6%",
|
|
176
|
+
"--destructive-foreground": "0 0% 98%",
|
|
177
|
+
"--border": "12 6.5% 15.1%",
|
|
178
|
+
"--input": "12 6.5% 15.1%",
|
|
179
|
+
"--ring": "37.7 92.1% 50.2%",
|
|
180
|
+
"--radius": "0.5rem",
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
} as const satisfies Record<string, ThemePreset>;
|
|
184
|
+
|
|
185
|
+
/** 프리셋 키 집합("blue" | "emerald" | "amber") */
|
|
186
|
+
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,73 @@
|
|
|
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
|
+
* `--radius`는 스킴과 무관한 공통값(§2.2)이며 두 객체에 동일하게 포함한다.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type UnikitColorScheme = "light" | "dark";
|
|
15
|
+
|
|
16
|
+
/** 라이트 스킴 토큰(기본값 — 02-design §1.4) */
|
|
17
|
+
export const lightTokens = {
|
|
18
|
+
"--background": "0 0% 100%",
|
|
19
|
+
"--foreground": "240 10% 3.9%",
|
|
20
|
+
"--card": "0 0% 100%",
|
|
21
|
+
"--card-foreground": "240 10% 3.9%",
|
|
22
|
+
"--popover": "0 0% 100%",
|
|
23
|
+
"--popover-foreground": "240 10% 3.9%",
|
|
24
|
+
"--primary": "240 5.9% 10%",
|
|
25
|
+
"--primary-foreground": "0 0% 98%",
|
|
26
|
+
"--secondary": "240 4.8% 95.9%",
|
|
27
|
+
"--secondary-foreground": "240 5.9% 10%",
|
|
28
|
+
"--muted": "240 4.8% 95.9%",
|
|
29
|
+
"--muted-foreground": "240 3.8% 46.1%",
|
|
30
|
+
"--accent": "240 4.8% 95.9%",
|
|
31
|
+
"--accent-foreground": "240 5.9% 10%",
|
|
32
|
+
"--destructive": "0 72.2% 50.6%",
|
|
33
|
+
"--destructive-foreground": "0 0% 98%",
|
|
34
|
+
"--border": "240 5.9% 90%",
|
|
35
|
+
"--input": "240 5.9% 90%",
|
|
36
|
+
"--ring": "240 5.9% 10%",
|
|
37
|
+
"--radius": "0.5rem",
|
|
38
|
+
} as const;
|
|
39
|
+
|
|
40
|
+
/** 다크 스킴 토큰 — satisfies로 라이트와 키 집합이 동일함을 컴파일 타임에 강제 */
|
|
41
|
+
export const darkTokens = {
|
|
42
|
+
"--background": "240 10% 3.9%",
|
|
43
|
+
"--foreground": "0 0% 98%",
|
|
44
|
+
"--card": "240 10% 3.9%",
|
|
45
|
+
"--card-foreground": "0 0% 98%",
|
|
46
|
+
"--popover": "240 10% 3.9%",
|
|
47
|
+
"--popover-foreground": "0 0% 98%",
|
|
48
|
+
"--primary": "0 0% 98%",
|
|
49
|
+
"--primary-foreground": "240 5.9% 10%",
|
|
50
|
+
"--secondary": "240 3.7% 15.9%",
|
|
51
|
+
"--secondary-foreground": "0 0% 98%",
|
|
52
|
+
"--muted": "240 3.7% 15.9%",
|
|
53
|
+
"--muted-foreground": "240 5% 64.9%",
|
|
54
|
+
"--accent": "240 3.7% 15.9%",
|
|
55
|
+
"--accent-foreground": "0 0% 98%",
|
|
56
|
+
"--destructive": "0 62.8% 30.6%",
|
|
57
|
+
"--destructive-foreground": "0 0% 98%",
|
|
58
|
+
"--border": "240 3.7% 15.9%",
|
|
59
|
+
"--input": "240 3.7% 15.9%",
|
|
60
|
+
"--ring": "240 4.9% 83.9%",
|
|
61
|
+
"--radius": "0.5rem",
|
|
62
|
+
} as const satisfies Record<keyof typeof lightTokens, string>;
|
|
63
|
+
|
|
64
|
+
/** 토큰 CSS 변수 이름(--background 등 색 19종 + --radius) */
|
|
65
|
+
export type UnikitThemeTokenName = keyof typeof lightTokens;
|
|
66
|
+
|
|
67
|
+
/** CSS 변수 이름 → 값("H S% L%" 문자열) 매핑. vars() 주입·globals.css 복사 양쪽에 쓴다. */
|
|
68
|
+
export type UnikitThemeTokens = Record<UnikitThemeTokenName, string>;
|
|
69
|
+
|
|
70
|
+
/** 스킴에 해당하는 토큰 객체를 반환한다 */
|
|
71
|
+
export function getThemeTokens(colorScheme: UnikitColorScheme): UnikitThemeTokens {
|
|
72
|
+
return colorScheme === "dark" ? darkTokens : lightTokens;
|
|
73
|
+
}
|