@seed-design/lynx-react 0.1.1-alpha.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/lib/components/ActionButton/ActionButton.d.ts +24 -0
- package/lib/components/ActionButton/ActionButton.d.ts.map +1 -0
- package/lib/components/ActionButton/ActionButton.jsx +21 -0
- package/lib/components/ActionButton/index.d.ts +2 -0
- package/lib/components/ActionButton/index.d.ts.map +1 -0
- package/lib/components/ActionButton/index.js +1 -0
- package/lib/components/Box/Box.d.ts +10 -0
- package/lib/components/Box/Box.d.ts.map +1 -0
- package/lib/components/Box/Box.jsx +19 -0
- package/lib/components/Box/index.d.ts +2 -0
- package/lib/components/Box/index.d.ts.map +1 -0
- package/lib/components/Box/index.js +1 -0
- package/lib/components/Flex/Flex.d.ts +33 -0
- package/lib/components/Flex/Flex.d.ts.map +1 -0
- package/lib/components/Flex/Flex.jsx +7 -0
- package/lib/components/Flex/index.d.ts +2 -0
- package/lib/components/Flex/index.d.ts.map +1 -0
- package/lib/components/Flex/index.js +1 -0
- package/lib/components/List/List.d.ts +9 -0
- package/lib/components/List/List.d.ts.map +1 -0
- package/lib/components/List/List.jsx +17 -0
- package/lib/components/List/index.d.ts +2 -0
- package/lib/components/List/index.d.ts.map +1 -0
- package/lib/components/List/index.js +1 -0
- package/lib/components/ListItem/ListItem.d.ts +8 -0
- package/lib/components/ListItem/ListItem.d.ts.map +1 -0
- package/lib/components/ListItem/ListItem.jsx +11 -0
- package/lib/components/ListItem/index.d.ts +2 -0
- package/lib/components/ListItem/index.d.ts.map +1 -0
- package/lib/components/ListItem/index.js +1 -0
- package/lib/components/ProgressCircle/ProgressCircle.d.ts +28 -0
- package/lib/components/ProgressCircle/ProgressCircle.d.ts.map +1 -0
- package/lib/components/ProgressCircle/ProgressCircle.jsx +292 -0
- package/lib/components/ProgressCircle/index.d.ts +2 -0
- package/lib/components/ProgressCircle/index.d.ts.map +1 -0
- package/lib/components/ProgressCircle/index.js +1 -0
- package/lib/components/ScrollView/ScrollView.d.ts +9 -0
- package/lib/components/ScrollView/ScrollView.d.ts.map +1 -0
- package/lib/components/ScrollView/ScrollView.jsx +17 -0
- package/lib/components/ScrollView/index.d.ts +2 -0
- package/lib/components/ScrollView/index.d.ts.map +1 -0
- package/lib/components/ScrollView/index.js +1 -0
- package/lib/components/Stack/Stack.d.ts +6 -0
- package/lib/components/Stack/Stack.d.ts.map +1 -0
- package/lib/components/Stack/Stack.jsx +10 -0
- package/lib/components/Stack/index.d.ts +2 -0
- package/lib/components/Stack/index.d.ts.map +1 -0
- package/lib/components/Stack/index.js +1 -0
- package/lib/components/Text/Text.d.ts +30 -0
- package/lib/components/Text/Text.d.ts.map +1 -0
- package/lib/components/Text/Text.jsx +49 -0
- package/lib/components/Text/index.d.ts +2 -0
- package/lib/components/Text/index.d.ts.map +1 -0
- package/lib/components/Text/index.js +1 -0
- package/lib/get-seed-class-name.d.ts +23 -0
- package/lib/get-seed-class-name.d.ts.map +1 -0
- package/lib/get-seed-class-name.js +39 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +4 -0
- package/lib/utils/dynamic-style.d.ts +13 -0
- package/lib/utils/dynamic-style.d.ts.map +1 -0
- package/lib/utils/dynamic-style.js +26 -0
- package/lib/utils/styled.d.ts +120 -0
- package/lib/utils/styled.d.ts.map +1 -0
- package/lib/utils/styled.jsx +131 -0
- package/package.json +51 -0
- package/src/components/ActionButton/ActionButton.tsx +62 -0
- package/src/components/ActionButton/index.ts +1 -0
- package/src/components/ProgressCircle/ProgressCircle.tsx +409 -0
- package/src/components/ProgressCircle/index.ts +1 -0
- package/src/get-seed-class-name.ts +52 -0
- package/src/index.ts +4 -0
- package/src/utils/dynamic-style.ts +26 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
runOnMainThread,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useMainThreadRef,
|
|
8
|
+
} from "@lynx-js/react";
|
|
9
|
+
import type { MainThread } from "@lynx-js/types";
|
|
10
|
+
import clsx from "clsx";
|
|
11
|
+
import { progressCircle } from "@seed-design/lynx-css/recipes/progress-circle";
|
|
12
|
+
import type { ProgressCircleVariantProps } from "@seed-design/lynx-css/recipes/progress-circle";
|
|
13
|
+
|
|
14
|
+
type Tone = "neutral" | "brand" | "staticWhite";
|
|
15
|
+
type Size = "24" | "40";
|
|
16
|
+
|
|
17
|
+
interface ProgressCircleContextValue {
|
|
18
|
+
tone: Tone;
|
|
19
|
+
numSize: number;
|
|
20
|
+
isDeterminate: boolean;
|
|
21
|
+
progress: number;
|
|
22
|
+
classes: ReturnType<typeof progressCircle>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const ProgressCircleContext = createContext<ProgressCircleContextValue | null>(null);
|
|
26
|
+
|
|
27
|
+
function useProgressCircleCtx() {
|
|
28
|
+
const ctx = useContext(ProgressCircleContext);
|
|
29
|
+
if (!ctx) {
|
|
30
|
+
throw new Error("ProgressCircle compound components must be used within ProgressCircle.Root");
|
|
31
|
+
}
|
|
32
|
+
return ctx;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// --- Background-thread-only utilities (for initial render) ---
|
|
36
|
+
|
|
37
|
+
function computeRingGeometry(numSize: number) {
|
|
38
|
+
const halfSize = numSize / 2;
|
|
39
|
+
const innerR = 0.53 * Math.SQRT2 * halfSize;
|
|
40
|
+
const ringCenterR = (halfSize + innerR) / 2;
|
|
41
|
+
const capSize = halfSize - innerR;
|
|
42
|
+
return { halfSize, innerR, ringCenterR, capSize };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function bgPieClipPath(size: number, angleDeg: number): string | undefined {
|
|
46
|
+
if (angleDeg >= 360) return undefined;
|
|
47
|
+
if (angleDeg <= 0) return 'path("M 0 0 Z")';
|
|
48
|
+
const c = size / 2;
|
|
49
|
+
const r = c + 1;
|
|
50
|
+
const rad = (angleDeg * Math.PI) / 180;
|
|
51
|
+
const endX = c + r * Math.sin(rad);
|
|
52
|
+
const endY = c - r * Math.cos(rad);
|
|
53
|
+
const largeArc = angleDeg > 180 ? 1 : 0;
|
|
54
|
+
return `path("M ${c} ${c} L ${c} ${c - r} A ${r} ${r} 0 ${largeArc} 1 ${endX} ${endY} Z")`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// --- Main-thread-only utilities (for animations) ---
|
|
58
|
+
// Compiled into the main thread bundle. Must NOT be called from render code.
|
|
59
|
+
|
|
60
|
+
function cubicBezier(rawT: number, x1: number, y1: number, x2: number, y2: number): number {
|
|
61
|
+
"main thread";
|
|
62
|
+
const t = Math.max(0, Math.min(1, rawT));
|
|
63
|
+
const cx = 3 * x1;
|
|
64
|
+
const bx = 3 * (x2 - x1) - cx;
|
|
65
|
+
const ax = 1 - cx - bx;
|
|
66
|
+
let currentT = t;
|
|
67
|
+
for (let i = 0; i < 8; i++) {
|
|
68
|
+
const currentX = ((ax * currentT + bx) * currentT + cx) * currentT - t;
|
|
69
|
+
const currentDx = (3 * ax * currentT + 2 * bx) * currentT + cx;
|
|
70
|
+
if (Math.abs(currentDx) < 1e-6) break;
|
|
71
|
+
currentT = currentT - currentX / currentDx;
|
|
72
|
+
}
|
|
73
|
+
const cy = 3 * y1;
|
|
74
|
+
const by = 3 * (y2 - y1) - cy;
|
|
75
|
+
const ay = 1 - cy - by;
|
|
76
|
+
return ((ay * currentT + by) * currentT + cy) * currentT;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function pieClipPath(size: number, angleDeg: number): string | undefined {
|
|
80
|
+
"main thread";
|
|
81
|
+
if (angleDeg >= 360) return undefined;
|
|
82
|
+
if (angleDeg <= 0) return 'path("M 0 0 Z")';
|
|
83
|
+
const c = size / 2;
|
|
84
|
+
const r = c + 1;
|
|
85
|
+
const rad = (angleDeg * Math.PI) / 180;
|
|
86
|
+
const endX = c + r * Math.sin(rad);
|
|
87
|
+
const endY = c - r * Math.cos(rad);
|
|
88
|
+
const largeArc = angleDeg > 180 ? 1 : 0;
|
|
89
|
+
return `path("M ${c} ${c} L ${c} ${c - r} A ${r} ${r} 0 ${largeArc} 1 ${endX} ${endY} Z")`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function sampleIndeterminate(t: number) {
|
|
93
|
+
"main thread";
|
|
94
|
+
const containerEased = cubicBezier(t, 0.35, 0.25, 0.65, 0.75);
|
|
95
|
+
const headEased = cubicBezier(t, 0.35, 0, 0.65, 1);
|
|
96
|
+
const tailEased = cubicBezier(t, 0.35, 0, 0.65, 0.6);
|
|
97
|
+
|
|
98
|
+
const headLength = headEased <= 0.75 ? (headEased / 0.75) * 360 : 360;
|
|
99
|
+
const tailOffset = tailEased <= 1 / 3 ? 0 : ((tailEased - 1 / 3) / (2 / 3)) * 360;
|
|
100
|
+
const arcLength = Math.max(0, headLength - tailOffset);
|
|
101
|
+
const containerDeg = containerEased * 360 + tailOffset;
|
|
102
|
+
|
|
103
|
+
return { containerDeg, arcLength };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// --- Animation constants ---
|
|
107
|
+
|
|
108
|
+
const INDETERMINATE_DURATION = 1200;
|
|
109
|
+
const TRANSITION_DURATION = 300;
|
|
110
|
+
|
|
111
|
+
// --- Components ---
|
|
112
|
+
|
|
113
|
+
export interface RootProps extends ProgressCircleVariantProps {
|
|
114
|
+
minValue?: number;
|
|
115
|
+
maxValue?: number;
|
|
116
|
+
value?: number;
|
|
117
|
+
children?: React.ReactNode;
|
|
118
|
+
className?: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Lynx에서 SVG를 사용할 수 없어 CSS clip-path 기반 pie sector로 구현.
|
|
123
|
+
*
|
|
124
|
+
* **Known Issues:**
|
|
125
|
+
* - clip-path가 Lynx에서 animatable이 아니라 JS RAF로 매 프레임 SVG path 생성
|
|
126
|
+
* - Lynx main thread에서 모듈 레벨 Map 미지원으로 인스턴스 간 RAF 공유 불가
|
|
127
|
+
* - 다수 인스턴스 동시 렌더링 시 성능 저하 가능
|
|
128
|
+
*
|
|
129
|
+
* Lynx SVG + stroke-dasharray 지원 시 CSS-only 애니메이션으로 전환 예정.
|
|
130
|
+
*/
|
|
131
|
+
function Root(props: RootProps) {
|
|
132
|
+
const size: Size = (props.size as Size) ?? "40";
|
|
133
|
+
const tone: Tone = (props.tone as Tone) ?? "neutral";
|
|
134
|
+
const numSize = Number(size);
|
|
135
|
+
|
|
136
|
+
const isDeterminate =
|
|
137
|
+
props.minValue !== undefined && props.maxValue !== undefined && props.value !== undefined;
|
|
138
|
+
|
|
139
|
+
const range = (props.maxValue ?? 1) - (props.minValue ?? 0);
|
|
140
|
+
const progress = isDeterminate
|
|
141
|
+
? range === 0
|
|
142
|
+
? 0
|
|
143
|
+
: ((props.value ?? 0) - (props.minValue ?? 0)) / range
|
|
144
|
+
: 0;
|
|
145
|
+
|
|
146
|
+
const classes = progressCircle({ tone, size });
|
|
147
|
+
|
|
148
|
+
const ctx = useMemo(
|
|
149
|
+
() => ({ tone, numSize, isDeterminate, progress, classes }),
|
|
150
|
+
[tone, numSize, isDeterminate, progress, classes],
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<ProgressCircleContext.Provider value={ctx}>
|
|
155
|
+
<view
|
|
156
|
+
className={clsx(classes.root, props.className)}
|
|
157
|
+
style={{ width: `${numSize}px`, height: `${numSize}px` }}
|
|
158
|
+
>
|
|
159
|
+
{props.children}
|
|
160
|
+
</view>
|
|
161
|
+
</ProgressCircleContext.Provider>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function Range() {
|
|
166
|
+
const { numSize, isDeterminate, progress, classes } = useProgressCircleCtx();
|
|
167
|
+
|
|
168
|
+
if (!isDeterminate) {
|
|
169
|
+
return <IndeterminateRange numSize={numSize} classes={classes} />;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return <DeterminateRange numSize={numSize} progress={progress} classes={classes} />;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
type Classes = ReturnType<typeof progressCircle>;
|
|
176
|
+
|
|
177
|
+
function DeterminateRange({
|
|
178
|
+
numSize,
|
|
179
|
+
progress,
|
|
180
|
+
classes,
|
|
181
|
+
}: {
|
|
182
|
+
numSize: number;
|
|
183
|
+
progress: number;
|
|
184
|
+
classes: Classes;
|
|
185
|
+
}) {
|
|
186
|
+
const rangeRef = useMainThreadRef<MainThread.Element>(null);
|
|
187
|
+
const startCapRef = useMainThreadRef<MainThread.Element>(null);
|
|
188
|
+
const endCapRef = useMainThreadRef<MainThread.Element>(null);
|
|
189
|
+
const prevProgressRef = useMainThreadRef<number>(progress);
|
|
190
|
+
const cancelRef = useMainThreadRef<number>(0);
|
|
191
|
+
|
|
192
|
+
const { halfSize, ringCenterR, capSize } = computeRingGeometry(numSize);
|
|
193
|
+
|
|
194
|
+
const initialAngle = progress * 360;
|
|
195
|
+
const initialClipPath = bgPieClipPath(numSize, initialAngle);
|
|
196
|
+
const initialEndRad = (initialAngle * Math.PI) / 180;
|
|
197
|
+
|
|
198
|
+
function startAnimation(newProgress: number) {
|
|
199
|
+
"main thread";
|
|
200
|
+
|
|
201
|
+
if (cancelRef.current) {
|
|
202
|
+
cancelAnimationFrame(cancelRef.current);
|
|
203
|
+
cancelRef.current = 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const from = prevProgressRef.current ?? 0;
|
|
207
|
+
prevProgressRef.current = newProgress;
|
|
208
|
+
if (from === newProgress) return;
|
|
209
|
+
|
|
210
|
+
const rangeEl = rangeRef.current;
|
|
211
|
+
const endCapEl = endCapRef.current;
|
|
212
|
+
let startTs = 0;
|
|
213
|
+
|
|
214
|
+
function applyProgress(p: number): void {
|
|
215
|
+
const angleDeg = p * 360;
|
|
216
|
+
const cp = pieClipPath(numSize, angleDeg);
|
|
217
|
+
if (cp) {
|
|
218
|
+
rangeEl?.setStyleProperty("clip-path", cp);
|
|
219
|
+
} else {
|
|
220
|
+
rangeEl?.setStyleProperty("clip-path", "none");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const rad = (angleDeg * Math.PI) / 180;
|
|
224
|
+
endCapEl?.setStyleProperties({
|
|
225
|
+
left: `${halfSize + ringCenterR * Math.sin(rad) - capSize / 2}px`,
|
|
226
|
+
top: `${halfSize - ringCenterR * Math.cos(rad) - capSize / 2}px`,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function step(ts: number): void {
|
|
231
|
+
if (!startTs) startTs = Number(ts);
|
|
232
|
+
const elapsed = ts - startTs;
|
|
233
|
+
if (elapsed >= TRANSITION_DURATION) {
|
|
234
|
+
applyProgress(newProgress);
|
|
235
|
+
cancelRef.current = 0;
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const t = elapsed / TRANSITION_DURATION;
|
|
239
|
+
const eased = cubicBezier(t, 0, 0, 0.15, 1);
|
|
240
|
+
applyProgress(from + (newProgress - from) * eased);
|
|
241
|
+
cancelRef.current = requestAnimationFrame(step);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
applyProgress(from);
|
|
245
|
+
cancelRef.current = requestAnimationFrame(step);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function cancelAnimation() {
|
|
249
|
+
"main thread";
|
|
250
|
+
if (cancelRef.current) {
|
|
251
|
+
cancelAnimationFrame(cancelRef.current);
|
|
252
|
+
cancelRef.current = 0;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
runOnMainThread(startAnimation)(progress);
|
|
258
|
+
return () => {
|
|
259
|
+
runOnMainThread(cancelAnimation)();
|
|
260
|
+
};
|
|
261
|
+
}, [progress]);
|
|
262
|
+
|
|
263
|
+
return (
|
|
264
|
+
<>
|
|
265
|
+
<view
|
|
266
|
+
main-thread:ref={rangeRef}
|
|
267
|
+
className={classes.range}
|
|
268
|
+
style={{
|
|
269
|
+
position: "absolute",
|
|
270
|
+
width: `${numSize}px`,
|
|
271
|
+
height: `${numSize}px`,
|
|
272
|
+
borderRadius: "50%",
|
|
273
|
+
clipPath: initialClipPath,
|
|
274
|
+
}}
|
|
275
|
+
/>
|
|
276
|
+
<view
|
|
277
|
+
main-thread:ref={startCapRef}
|
|
278
|
+
className={classes.cap}
|
|
279
|
+
style={{
|
|
280
|
+
width: `${capSize}px`,
|
|
281
|
+
height: `${capSize}px`,
|
|
282
|
+
left: `${halfSize - capSize / 2}px`,
|
|
283
|
+
top: `${halfSize - ringCenterR - capSize / 2}px`,
|
|
284
|
+
}}
|
|
285
|
+
/>
|
|
286
|
+
<view
|
|
287
|
+
main-thread:ref={endCapRef}
|
|
288
|
+
className={classes.cap}
|
|
289
|
+
style={{
|
|
290
|
+
width: `${capSize}px`,
|
|
291
|
+
height: `${capSize}px`,
|
|
292
|
+
left: `${halfSize + ringCenterR * Math.sin(initialEndRad) - capSize / 2}px`,
|
|
293
|
+
top: `${halfSize - ringCenterR * Math.cos(initialEndRad) - capSize / 2}px`,
|
|
294
|
+
}}
|
|
295
|
+
/>
|
|
296
|
+
</>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function IndeterminateRange({ numSize, classes }: { numSize: number; classes: Classes }) {
|
|
301
|
+
const containerRef = useMainThreadRef<MainThread.Element>(null);
|
|
302
|
+
const rangeRef = useMainThreadRef<MainThread.Element>(null);
|
|
303
|
+
const headCapRef = useMainThreadRef<MainThread.Element>(null);
|
|
304
|
+
const tailCapRef = useMainThreadRef<MainThread.Element>(null);
|
|
305
|
+
const rafIdRef = useMainThreadRef<number>(0);
|
|
306
|
+
|
|
307
|
+
const { halfSize, ringCenterR, capSize } = computeRingGeometry(numSize);
|
|
308
|
+
|
|
309
|
+
function startLoop() {
|
|
310
|
+
"main thread";
|
|
311
|
+
|
|
312
|
+
const containerEl = containerRef.current;
|
|
313
|
+
const rangeEl = rangeRef.current;
|
|
314
|
+
const headCapEl = headCapRef.current;
|
|
315
|
+
let startTs = 0;
|
|
316
|
+
|
|
317
|
+
function tick(ts: number): void {
|
|
318
|
+
if (!startTs) startTs = Number(ts);
|
|
319
|
+
|
|
320
|
+
const elapsed = ts - startTs;
|
|
321
|
+
const t = (elapsed % INDETERMINATE_DURATION) / INDETERMINATE_DURATION;
|
|
322
|
+
const state = sampleIndeterminate(t);
|
|
323
|
+
|
|
324
|
+
containerEl?.setStyleProperty("transform", `rotate(${state.containerDeg}deg)`);
|
|
325
|
+
|
|
326
|
+
const clipPath = pieClipPath(numSize, state.arcLength);
|
|
327
|
+
if (clipPath) {
|
|
328
|
+
rangeEl?.setStyleProperty("clip-path", clipPath);
|
|
329
|
+
} else {
|
|
330
|
+
rangeEl?.setStyleProperty("clip-path", "none");
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const headRad = (state.arcLength * Math.PI) / 180;
|
|
334
|
+
headCapEl?.setStyleProperties({
|
|
335
|
+
left: `${halfSize + ringCenterR * Math.sin(headRad) - capSize / 2}px`,
|
|
336
|
+
top: `${halfSize - ringCenterR * Math.cos(headRad) - capSize / 2}px`,
|
|
337
|
+
});
|
|
338
|
+
rafIdRef.current = requestAnimationFrame(tick);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
rafIdRef.current = requestAnimationFrame(tick);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function stopLoop() {
|
|
345
|
+
"main thread";
|
|
346
|
+
if (rafIdRef.current) {
|
|
347
|
+
cancelAnimationFrame(rafIdRef.current);
|
|
348
|
+
rafIdRef.current = 0;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
useEffect(() => {
|
|
353
|
+
runOnMainThread(startLoop)();
|
|
354
|
+
return () => {
|
|
355
|
+
runOnMainThread(stopLoop)();
|
|
356
|
+
};
|
|
357
|
+
}, []);
|
|
358
|
+
|
|
359
|
+
const initialClipPath = 'path("M 0 0 Z")';
|
|
360
|
+
|
|
361
|
+
return (
|
|
362
|
+
<view
|
|
363
|
+
main-thread:ref={containerRef}
|
|
364
|
+
style={{
|
|
365
|
+
position: "absolute",
|
|
366
|
+
width: `${numSize}px`,
|
|
367
|
+
height: `${numSize}px`,
|
|
368
|
+
transform: "rotate(0deg)",
|
|
369
|
+
}}
|
|
370
|
+
>
|
|
371
|
+
<view
|
|
372
|
+
main-thread:ref={rangeRef}
|
|
373
|
+
className={classes.range}
|
|
374
|
+
style={{
|
|
375
|
+
position: "absolute",
|
|
376
|
+
width: `${numSize}px`,
|
|
377
|
+
height: `${numSize}px`,
|
|
378
|
+
borderRadius: "50%",
|
|
379
|
+
clipPath: initialClipPath,
|
|
380
|
+
}}
|
|
381
|
+
/>
|
|
382
|
+
<view
|
|
383
|
+
main-thread:ref={tailCapRef}
|
|
384
|
+
className={classes.cap}
|
|
385
|
+
style={{
|
|
386
|
+
width: `${capSize}px`,
|
|
387
|
+
height: `${capSize}px`,
|
|
388
|
+
left: `${halfSize - capSize / 2}px`,
|
|
389
|
+
top: `${halfSize - ringCenterR - capSize / 2}px`,
|
|
390
|
+
}}
|
|
391
|
+
/>
|
|
392
|
+
<view
|
|
393
|
+
main-thread:ref={headCapRef}
|
|
394
|
+
className={classes.cap}
|
|
395
|
+
style={{
|
|
396
|
+
width: `${capSize}px`,
|
|
397
|
+
height: `${capSize}px`,
|
|
398
|
+
left: `${halfSize - capSize / 2}px`,
|
|
399
|
+
top: `${halfSize - ringCenterR - capSize / 2}px`,
|
|
400
|
+
}}
|
|
401
|
+
/>
|
|
402
|
+
</view>
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export const ProgressCircle = {
|
|
407
|
+
Root,
|
|
408
|
+
Range,
|
|
409
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ProgressCircle, type RootProps as ProgressCircleProps } from "./ProgressCircle";
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Lynx 런타임 전역 변수 타입 선언
|
|
2
|
+
// 실제 런타임에서는 Lynx 엔진이 제공하며, 빌드 시에는 @lynx-js/types가 정의함
|
|
3
|
+
declare const lynx: {
|
|
4
|
+
__globalProps?: Record<string, unknown>;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
declare const SystemInfo: {
|
|
8
|
+
platform?: "Android" | "iOS" | "Harmony" | "windows" | "macOS";
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type ColorMode = "system" | "light-only" | "dark-only";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Lynx 앱의 root `<page>` 요소에 적용할 SEED Design className을 반환한다.
|
|
15
|
+
*
|
|
16
|
+
* 테마(dark/light)와 플랫폼(iOS/Android)을 자동 감지하여
|
|
17
|
+
* 렌더링 전에 올바른 CSS 변수가 적용되도록 한다.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```tsx
|
|
21
|
+
* import { getSeedClassName } from "@seed-design/lynx-react";
|
|
22
|
+
*
|
|
23
|
+
* root.render(
|
|
24
|
+
* <page className={getSeedClassName({ colorMode: "system" })}>
|
|
25
|
+
* <App />
|
|
26
|
+
* </page>
|
|
27
|
+
* );
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export function getSeedClassName(options?: { colorMode?: ColorMode }): string {
|
|
31
|
+
const { colorMode = "system" } = options ?? {};
|
|
32
|
+
|
|
33
|
+
// Theme — lynx.__globalProps.theme에서 시스템 테마 읽기
|
|
34
|
+
const systemTheme = lynx?.__globalProps?.["theme"] as string | undefined;
|
|
35
|
+
let themeClass: string;
|
|
36
|
+
if (colorMode === "light-only") {
|
|
37
|
+
themeClass = "seed-user-color-scheme-light";
|
|
38
|
+
} else if (colorMode === "dark-only") {
|
|
39
|
+
themeClass = "seed-user-color-scheme-dark";
|
|
40
|
+
} else {
|
|
41
|
+
themeClass =
|
|
42
|
+
systemTheme?.toLowerCase() === "dark"
|
|
43
|
+
? "seed-user-color-scheme-dark"
|
|
44
|
+
: "seed-user-color-scheme-light";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Platform — SystemInfo.platform에서 디바이스 플랫폼 읽기
|
|
48
|
+
const platform = SystemInfo?.platform;
|
|
49
|
+
const platformClass = platform === "iOS" ? "seed-platform-ios" : "seed-platform-android";
|
|
50
|
+
|
|
51
|
+
return `${themeClass} ${platformClass}`;
|
|
52
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { ActionButton, type ActionButtonProps } from "./components/ActionButton";
|
|
2
|
+
export { ProgressCircle, type ProgressCircleProps } from "./components/ProgressCircle";
|
|
3
|
+
export { dynamicStyle } from "./utils/dynamic-style";
|
|
4
|
+
export { getSeedClassName } from "./get-seed-class-name";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert camelCase CSS property names to kebab-case.
|
|
3
|
+
* CSS custom properties (starting with `--`) are left unchanged.
|
|
4
|
+
*/
|
|
5
|
+
function toKebabCase(str: string): string {
|
|
6
|
+
if (str.startsWith("--")) return str;
|
|
7
|
+
return str.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Convert a style object to a Lynx-compatible style string.
|
|
12
|
+
*
|
|
13
|
+
* Lynx compiles object-form `style` props into static CSS at build time,
|
|
14
|
+
* which drops dynamically-set CSS custom properties. Passing styles as a
|
|
15
|
+
* string literal preserves runtime values.
|
|
16
|
+
*
|
|
17
|
+
* Property names are converted from camelCase to kebab-case so that
|
|
18
|
+
* `React.CSSProperties` keys (e.g. `fontSize`) produce valid CSS
|
|
19
|
+
* (e.g. `font-size`).
|
|
20
|
+
*/
|
|
21
|
+
export function dynamicStyle(style: Record<string, unknown>): string {
|
|
22
|
+
return Object.entries(style)
|
|
23
|
+
.filter(([_, v]) => v != null)
|
|
24
|
+
.map(([k, v]) => `${toKebabCase(k)}: ${v}`)
|
|
25
|
+
.join("; ");
|
|
26
|
+
}
|