@q9labsai/chalk-react-native 4.1.11 → 4.1.12
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/dist/android.js +3 -1
- package/dist/{chunk-MMBE2BVK.js → chunk-FVHH65FR.js} +648 -406
- package/dist/components/Entrance.d.ts.map +1 -1
- package/dist/components/logo/Logo.d.ts +11 -0
- package/dist/components/logo/Logo.d.ts.map +1 -0
- package/dist/components/native-space-view/SpaceTopBar.android.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/ios-pad.js +3 -1
- package/dist/ios-phone.js +3 -1
- package/dist/ios.js +3 -1
- package/dist/macos.js +3 -1
- package/package.json +4 -4
- package/src/components/Entrance.test.tsx +7 -0
- package/src/components/Entrance.tsx +2 -1
- package/src/components/logo/Logo.test.tsx +144 -0
- package/src/components/logo/Logo.tsx +309 -0
- package/src/components/native-space-view/SpaceTopBar.android.test.tsx +7 -0
- package/src/components/native-space-view/SpaceTopBar.android.tsx +2 -1
- package/src/index.ts +2 -0
- package/dist/components/LogoElements.d.ts +0 -6
- package/dist/components/LogoElements.d.ts.map +0 -1
- package/src/components/LogoElements.test.tsx +0 -60
- package/src/components/LogoElements.tsx +0 -119
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
2
|
+
import { AccessibilityInfo, Animated, Pressable } from "react-native";
|
|
3
|
+
import Svg, { Defs, Ellipse, G, LinearGradient, Path, Stop, Text as SvgText } from "react-native-svg";
|
|
4
|
+
|
|
5
|
+
const AnimatedG = Animated.createAnimatedComponent(G);
|
|
6
|
+
|
|
7
|
+
export type LogoMotion = "orbit-burst" | "none";
|
|
8
|
+
export type LogoVariant = "mark" | "wordmark";
|
|
9
|
+
|
|
10
|
+
export interface LogoProps {
|
|
11
|
+
readonly accessibilityLabel?: string | null;
|
|
12
|
+
readonly color?: string;
|
|
13
|
+
readonly height?: number;
|
|
14
|
+
readonly motion?: LogoMotion;
|
|
15
|
+
readonly variant?: LogoVariant;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface InteractionState {
|
|
19
|
+
readonly focus: boolean;
|
|
20
|
+
readonly hover: boolean;
|
|
21
|
+
readonly press: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface StickAnimationValues {
|
|
25
|
+
readonly rotation: Animated.Value;
|
|
26
|
+
readonly scaleX: Animated.Value;
|
|
27
|
+
readonly scaleY: Animated.Value;
|
|
28
|
+
readonly translateX: Animated.Value;
|
|
29
|
+
readonly translateY: Animated.Value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type StickAnimationValuesTuple = readonly [StickAnimationValues, StickAnimationValues, StickAnimationValues, StickAnimationValues];
|
|
33
|
+
|
|
34
|
+
interface StickMotion {
|
|
35
|
+
readonly burstRotation: number;
|
|
36
|
+
readonly burstScale: number;
|
|
37
|
+
readonly burstSettleRotation: number;
|
|
38
|
+
readonly burstSettleScale: number;
|
|
39
|
+
readonly burstSettleX: number;
|
|
40
|
+
readonly burstSettleY: number;
|
|
41
|
+
readonly burstX: number;
|
|
42
|
+
readonly burstY: number;
|
|
43
|
+
readonly orbitRotation: readonly [number, number, number, number];
|
|
44
|
+
readonly orbitX: readonly [number, number, number, number];
|
|
45
|
+
readonly orbitY: readonly [number, number, number, number];
|
|
46
|
+
readonly originX: number;
|
|
47
|
+
readonly originY: number;
|
|
48
|
+
readonly transform: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const STICK_MOTIONS: readonly [StickMotion, StickMotion, StickMotion, StickMotion] = [
|
|
52
|
+
{
|
|
53
|
+
transform: "rotate(-15 20 60)",
|
|
54
|
+
originX: 15,
|
|
55
|
+
originY: 42,
|
|
56
|
+
orbitRotation: [0, -5, 0, 0],
|
|
57
|
+
orbitX: [0, -4, 0, 0],
|
|
58
|
+
orbitY: [0, 3, 0, 0],
|
|
59
|
+
burstRotation: -8,
|
|
60
|
+
burstScale: 1.14,
|
|
61
|
+
burstSettleRotation: 2,
|
|
62
|
+
burstSettleScale: 0.98,
|
|
63
|
+
burstSettleX: -2,
|
|
64
|
+
burstSettleY: 2,
|
|
65
|
+
burstX: -14,
|
|
66
|
+
burstY: -9,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
transform: "rotate(-8 32 55)",
|
|
70
|
+
originX: 29,
|
|
71
|
+
originY: 38,
|
|
72
|
+
orbitRotation: [0, 4, 0, 0],
|
|
73
|
+
orbitX: [0, 3, 0, 0],
|
|
74
|
+
orbitY: [0, -4, 0, 0],
|
|
75
|
+
burstRotation: -4,
|
|
76
|
+
burstScale: 1.14,
|
|
77
|
+
burstSettleRotation: 1,
|
|
78
|
+
burstSettleScale: 0.98,
|
|
79
|
+
burstSettleX: 1,
|
|
80
|
+
burstSettleY: 1,
|
|
81
|
+
burstX: -4,
|
|
82
|
+
burstY: -14,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
transform: "rotate(12 60 30)",
|
|
86
|
+
originX: 42,
|
|
87
|
+
originY: 32,
|
|
88
|
+
orbitRotation: [0, 5, 0, 0],
|
|
89
|
+
orbitX: [0, 5, 0, 0],
|
|
90
|
+
orbitY: [0, 2, 0, 0],
|
|
91
|
+
burstRotation: 8,
|
|
92
|
+
burstScale: 1.16,
|
|
93
|
+
burstSettleRotation: -2,
|
|
94
|
+
burstSettleScale: 0.98,
|
|
95
|
+
burstSettleX: 2,
|
|
96
|
+
burstSettleY: 1,
|
|
97
|
+
burstX: 14,
|
|
98
|
+
burstY: -6,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
transform: "rotate(5 55 50)",
|
|
102
|
+
originX: 55,
|
|
103
|
+
originY: 50,
|
|
104
|
+
orbitRotation: [0, -4, 0, 0],
|
|
105
|
+
orbitX: [0, -3, 0, 0],
|
|
106
|
+
orbitY: [0, -3, 0, 0],
|
|
107
|
+
burstRotation: 7,
|
|
108
|
+
burstScale: 1.13,
|
|
109
|
+
burstSettleRotation: -1,
|
|
110
|
+
burstSettleScale: 0.98,
|
|
111
|
+
burstSettleX: 1,
|
|
112
|
+
burstSettleY: -1,
|
|
113
|
+
burstX: 11,
|
|
114
|
+
burstY: 12,
|
|
115
|
+
},
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
const ORBIT_DURATION = 4800;
|
|
119
|
+
const BURST_DURATION = 1100;
|
|
120
|
+
const DEFAULT_COLOR = "#1A332B";
|
|
121
|
+
const DEFAULT_HEIGHT = 32;
|
|
122
|
+
const DEFAULT_MOTION: LogoMotion = "orbit-burst";
|
|
123
|
+
const DEFAULT_VARIANT: LogoVariant = "wordmark";
|
|
124
|
+
const WORDMARK_VIEW_BOX = "0 0 200 80";
|
|
125
|
+
const MARK_VIEW_BOX = "0 0 68 80";
|
|
126
|
+
|
|
127
|
+
function createStickAnimationValues(): StickAnimationValuesTuple {
|
|
128
|
+
return [
|
|
129
|
+
{ rotation: new Animated.Value(0), scaleX: new Animated.Value(1), scaleY: new Animated.Value(1), translateX: new Animated.Value(0), translateY: new Animated.Value(0) },
|
|
130
|
+
{ rotation: new Animated.Value(0), scaleX: new Animated.Value(1), scaleY: new Animated.Value(1), translateX: new Animated.Value(0), translateY: new Animated.Value(0) },
|
|
131
|
+
{ rotation: new Animated.Value(0), scaleX: new Animated.Value(1), scaleY: new Animated.Value(1), translateX: new Animated.Value(0), translateY: new Animated.Value(0) },
|
|
132
|
+
{ rotation: new Animated.Value(0), scaleX: new Animated.Value(1), scaleY: new Animated.Value(1), translateX: new Animated.Value(0), translateY: new Animated.Value(0) },
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function createOrbitAnimation(stick: StickAnimationValues, motion: StickMotion, index: number): Animated.CompositeAnimation {
|
|
137
|
+
const keyframes = [
|
|
138
|
+
Animated.parallel([
|
|
139
|
+
Animated.timing(stick.rotation, { duration: ORBIT_DURATION / 2, toValue: motion.orbitRotation[1], useNativeDriver: true }),
|
|
140
|
+
Animated.timing(stick.translateX, { duration: ORBIT_DURATION / 2, toValue: motion.orbitX[1], useNativeDriver: true }),
|
|
141
|
+
Animated.timing(stick.translateY, { duration: ORBIT_DURATION / 2, toValue: motion.orbitY[1], useNativeDriver: true }),
|
|
142
|
+
]),
|
|
143
|
+
Animated.parallel([
|
|
144
|
+
Animated.timing(stick.rotation, { duration: ORBIT_DURATION / 2, toValue: motion.orbitRotation[2], useNativeDriver: true }),
|
|
145
|
+
Animated.timing(stick.translateX, { duration: ORBIT_DURATION / 2, toValue: motion.orbitX[2], useNativeDriver: true }),
|
|
146
|
+
Animated.timing(stick.translateY, { duration: ORBIT_DURATION / 2, toValue: motion.orbitY[2], useNativeDriver: true }),
|
|
147
|
+
]),
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
return Animated.sequence([Animated.delay(index * 90), Animated.loop(Animated.sequence(keyframes))]);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function createBurstAnimation(stick: StickAnimationValues, motion: StickMotion): Animated.CompositeAnimation {
|
|
154
|
+
return Animated.loop(
|
|
155
|
+
Animated.sequence([
|
|
156
|
+
Animated.parallel([
|
|
157
|
+
Animated.timing(stick.rotation, { duration: BURST_DURATION * 0.24, toValue: motion.burstRotation, useNativeDriver: true }),
|
|
158
|
+
Animated.timing(stick.scaleX, { duration: BURST_DURATION * 0.24, toValue: motion.burstScale, useNativeDriver: true }),
|
|
159
|
+
Animated.timing(stick.scaleY, { duration: BURST_DURATION * 0.24, toValue: motion.burstScale, useNativeDriver: true }),
|
|
160
|
+
Animated.timing(stick.translateX, { duration: BURST_DURATION * 0.24, toValue: motion.burstX, useNativeDriver: true }),
|
|
161
|
+
Animated.timing(stick.translateY, { duration: BURST_DURATION * 0.24, toValue: motion.burstY, useNativeDriver: true }),
|
|
162
|
+
]),
|
|
163
|
+
Animated.parallel([
|
|
164
|
+
Animated.timing(stick.rotation, { duration: BURST_DURATION * 0.25, toValue: motion.burstSettleRotation, useNativeDriver: true }),
|
|
165
|
+
Animated.timing(stick.scaleX, { duration: BURST_DURATION * 0.25, toValue: motion.burstSettleScale, useNativeDriver: true }),
|
|
166
|
+
Animated.timing(stick.scaleY, { duration: BURST_DURATION * 0.25, toValue: motion.burstSettleScale, useNativeDriver: true }),
|
|
167
|
+
Animated.timing(stick.translateX, { duration: BURST_DURATION * 0.25, toValue: motion.burstSettleX, useNativeDriver: true }),
|
|
168
|
+
Animated.timing(stick.translateY, { duration: BURST_DURATION * 0.25, toValue: motion.burstSettleY, useNativeDriver: true }),
|
|
169
|
+
]),
|
|
170
|
+
Animated.parallel([
|
|
171
|
+
Animated.timing(stick.rotation, { duration: BURST_DURATION * 0.33, toValue: 0, useNativeDriver: true }),
|
|
172
|
+
Animated.timing(stick.scaleX, { duration: BURST_DURATION * 0.33, toValue: 1, useNativeDriver: true }),
|
|
173
|
+
Animated.timing(stick.scaleY, { duration: BURST_DURATION * 0.33, toValue: 1, useNativeDriver: true }),
|
|
174
|
+
Animated.timing(stick.translateX, { duration: BURST_DURATION * 0.33, toValue: 0, useNativeDriver: true }),
|
|
175
|
+
Animated.timing(stick.translateY, { duration: BURST_DURATION * 0.33, toValue: 0, useNativeDriver: true }),
|
|
176
|
+
]),
|
|
177
|
+
]),
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function resetStickAnimationValues(values: readonly StickAnimationValues[]): void {
|
|
182
|
+
for (const stick of values) {
|
|
183
|
+
stick.rotation.stopAnimation();
|
|
184
|
+
stick.scaleX.stopAnimation();
|
|
185
|
+
stick.scaleY.stopAnimation();
|
|
186
|
+
stick.translateX.stopAnimation();
|
|
187
|
+
stick.translateY.stopAnimation();
|
|
188
|
+
stick.rotation.setValue(0);
|
|
189
|
+
stick.scaleX.setValue(1);
|
|
190
|
+
stick.scaleY.setValue(1);
|
|
191
|
+
stick.translateX.setValue(0);
|
|
192
|
+
stick.translateY.setValue(0);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function Logo({ accessibilityLabel = "Chalk", color = DEFAULT_COLOR, height = DEFAULT_HEIGHT, motion = DEFAULT_MOTION, variant = DEFAULT_VARIANT }: LogoProps): React.JSX.Element {
|
|
197
|
+
const [reducedMotion, setReducedMotion] = useState<boolean | null>(null);
|
|
198
|
+
const [interaction, setInteraction] = useState<InteractionState>({ focus: false, hover: false, press: false });
|
|
199
|
+
const idPrefix = `chalk-logo-${useId().replace(/[^a-zA-Z0-9_-]/gu, "")}`;
|
|
200
|
+
const animationValues = useRef<StickAnimationValuesTuple | null>(null);
|
|
201
|
+
if (animationValues.current === null) {
|
|
202
|
+
animationValues.current = createStickAnimationValues();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const sticks = animationValues.current;
|
|
206
|
+
const interactionActive = interaction.focus || interaction.hover || interaction.press;
|
|
207
|
+
const animate = motion === "orbit-burst" && reducedMotion === false;
|
|
208
|
+
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
let cancelled = false;
|
|
211
|
+
const subscription = AccessibilityInfo.addEventListener("reduceMotionChanged", setReducedMotion);
|
|
212
|
+
|
|
213
|
+
void AccessibilityInfo.isReduceMotionEnabled().then((enabled) => {
|
|
214
|
+
if (!cancelled) {
|
|
215
|
+
setReducedMotion(enabled);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
return () => {
|
|
220
|
+
cancelled = true;
|
|
221
|
+
subscription.remove();
|
|
222
|
+
};
|
|
223
|
+
}, []);
|
|
224
|
+
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
if (!animate) {
|
|
227
|
+
resetStickAnimationValues(sticks);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const animations = interactionActive
|
|
232
|
+
? [createBurstAnimation(sticks[0], STICK_MOTIONS[0]), createBurstAnimation(sticks[1], STICK_MOTIONS[1]), createBurstAnimation(sticks[2], STICK_MOTIONS[2]), createBurstAnimation(sticks[3], STICK_MOTIONS[3])]
|
|
233
|
+
: [createOrbitAnimation(sticks[0], STICK_MOTIONS[0], 0), createOrbitAnimation(sticks[1], STICK_MOTIONS[1], 1), createOrbitAnimation(sticks[2], STICK_MOTIONS[2], 2), createOrbitAnimation(sticks[3], STICK_MOTIONS[3], 3)];
|
|
234
|
+
const animation = Animated.parallel(animations);
|
|
235
|
+
animation.start();
|
|
236
|
+
|
|
237
|
+
return () => {
|
|
238
|
+
animation.stop();
|
|
239
|
+
};
|
|
240
|
+
}, [animate, interactionActive, sticks]);
|
|
241
|
+
|
|
242
|
+
const setInteractionFlag = (key: keyof InteractionState, value: boolean): void => {
|
|
243
|
+
setInteraction((current) => (current[key] === value ? current : { ...current, [key]: value }));
|
|
244
|
+
};
|
|
245
|
+
const width = variant === "wordmark" ? height * 2.5 : height * (68 / 80);
|
|
246
|
+
const viewBox = variant === "wordmark" ? WORDMARK_VIEW_BOX : MARK_VIEW_BOX;
|
|
247
|
+
const accessible = accessibilityLabel !== null;
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
<Pressable
|
|
251
|
+
accessible={false}
|
|
252
|
+
onBlur={() => setInteractionFlag("focus", false)}
|
|
253
|
+
onFocus={() => setInteractionFlag("focus", true)}
|
|
254
|
+
onHoverIn={() => setInteractionFlag("hover", true)}
|
|
255
|
+
onHoverOut={() => setInteractionFlag("hover", false)}
|
|
256
|
+
onPressIn={() => setInteractionFlag("press", true)}
|
|
257
|
+
onPressOut={() => setInteractionFlag("press", false)}
|
|
258
|
+
>
|
|
259
|
+
<Svg accessibilityLabel={accessible ? (accessibilityLabel ?? undefined) : undefined} accessible={accessible} color={color} fill="none" focusable={false} height={height} viewBox={viewBox} width={width}>
|
|
260
|
+
<Defs>
|
|
261
|
+
<LinearGradient id={`${idPrefix}-green`} x1="0%" x2="100%" y1="0%" y2="0%">
|
|
262
|
+
<Stop offset="0%" stopColor="#C5E5C0" />
|
|
263
|
+
<Stop offset="100%" stopColor="#80B879" />
|
|
264
|
+
</LinearGradient>
|
|
265
|
+
<LinearGradient id={`${idPrefix}-yellow`} x1="0%" x2="100%" y1="0%" y2="0%">
|
|
266
|
+
<Stop offset="0%" stopColor="#FCEAB3" />
|
|
267
|
+
<Stop offset="100%" stopColor="#D9B641" />
|
|
268
|
+
</LinearGradient>
|
|
269
|
+
<LinearGradient id={`${idPrefix}-blue`} x1="0%" x2="100%" y1="0%" y2="0%">
|
|
270
|
+
<Stop offset="0%" stopColor="#B2E0F0" />
|
|
271
|
+
<Stop offset="100%" stopColor="#55AAC9" />
|
|
272
|
+
</LinearGradient>
|
|
273
|
+
<LinearGradient id={`${idPrefix}-pink`} x1="0%" x2="100%" y1="0%" y2="0%">
|
|
274
|
+
<Stop offset="0%" stopColor="#F8CACA" />
|
|
275
|
+
<Stop offset="100%" stopColor="#D67B7B" />
|
|
276
|
+
</LinearGradient>
|
|
277
|
+
</Defs>
|
|
278
|
+
|
|
279
|
+
<G transform="translate(2, 2) scale(0.95)">
|
|
280
|
+
<AnimatedG originX={STICK_MOTIONS[0].originX} originY={STICK_MOTIONS[0].originY} rotation={sticks[0].rotation} scaleX={sticks[0].scaleX} scaleY={sticks[0].scaleY} transform={STICK_MOTIONS[0].transform} translateX={sticks[0].translateX} translateY={sticks[0].translateY}>
|
|
281
|
+
<Path d="M9,20 L21,20 L22,63 A7,7 0 0 1 8,63 Z" fill={`url(#${idPrefix}-green)`} />
|
|
282
|
+
<Ellipse cx="15" cy="20" fill="#D8ECD4" rx="6" ry="2.5" />
|
|
283
|
+
</AnimatedG>
|
|
284
|
+
|
|
285
|
+
<AnimatedG originX={STICK_MOTIONS[1].originX} originY={STICK_MOTIONS[1].originY} rotation={sticks[1].rotation} scaleX={sticks[1].scaleX} scaleY={sticks[1].scaleY} transform={STICK_MOTIONS[1].transform} translateX={sticks[1].translateX} translateY={sticks[1].translateY}>
|
|
286
|
+
<Path d="M23,15 L35,15 L36,60 A7,7 0 0 1 22,60 Z" fill={`url(#${idPrefix}-yellow)`} />
|
|
287
|
+
<Ellipse cx="29" cy="15" fill="#FEF3D1" rx="6" ry="2.5" />
|
|
288
|
+
</AnimatedG>
|
|
289
|
+
|
|
290
|
+
<AnimatedG originX={STICK_MOTIONS[2].originX} originY={STICK_MOTIONS[2].originY} rotation={sticks[2].rotation} scaleX={sticks[2].scaleX} scaleY={sticks[2].scaleY} transform={STICK_MOTIONS[2].transform} translateX={sticks[2].translateX} translateY={sticks[2].translateY}>
|
|
291
|
+
<Path d="M36,8 L48,8 L49,56 A7,7 0 0 1 35,56 Z" fill={`url(#${idPrefix}-blue)`} />
|
|
292
|
+
<Ellipse cx="42" cy="8" fill="#D0EDF8" rx="6" ry="2.5" />
|
|
293
|
+
</AnimatedG>
|
|
294
|
+
|
|
295
|
+
<AnimatedG originX={STICK_MOTIONS[3].originX} originY={STICK_MOTIONS[3].originY} rotation={sticks[3].rotation} scaleX={sticks[3].scaleX} scaleY={sticks[3].scaleY} transform={STICK_MOTIONS[3].transform} translateX={sticks[3].translateX} translateY={sticks[3].translateY}>
|
|
296
|
+
<Path d="M49,70 L61,70 L62,29 A7,7 0 0 0 48,29 Z" fill={`url(#${idPrefix}-pink)`} />
|
|
297
|
+
<Ellipse cx="55" cy="70" fill="#FBE4E4" rx="6" ry="2.5" />
|
|
298
|
+
</AnimatedG>
|
|
299
|
+
</G>
|
|
300
|
+
|
|
301
|
+
{variant === "wordmark" ? (
|
|
302
|
+
<SvgText fill="currentColor" fontFamily="system-ui, -apple-system, BlinkMacSystemFont, sans-serif" fontSize="38" fontWeight="600" letterSpacing="-0.02em" x="90" y="52">
|
|
303
|
+
chalk
|
|
304
|
+
</SvgText>
|
|
305
|
+
) : null}
|
|
306
|
+
</Svg>
|
|
307
|
+
</Pressable>
|
|
308
|
+
);
|
|
309
|
+
}
|
|
@@ -2,6 +2,7 @@ import { describe, expect, it, vi } from "vitest";
|
|
|
2
2
|
|
|
3
3
|
vi.mock("@hugeicons/core-free-icons/dist/esm/UserGroupIcon", () => ({ default: "UserGroupIcon" }));
|
|
4
4
|
vi.mock("@hugeicons/react-native", () => ({ HugeiconsIcon: "HugeiconsIcon" }));
|
|
5
|
+
vi.mock("../logo/Logo", () => ({ Logo: "Logo" }));
|
|
5
6
|
vi.mock("react-native", () => ({
|
|
6
7
|
Image: "Image",
|
|
7
8
|
Platform: { OS: "android" },
|
|
@@ -24,6 +25,12 @@ describe("SpaceTopBarAndroid", () => {
|
|
|
24
25
|
expect(images[0]).toMatchObject({ props: { accessibilityLabel: "Chalk", source: { uri: "https://cdn.example.test/logo.png" } } });
|
|
25
26
|
expect(findText(tree)).toEqual(expect.arrayContaining(["Design space", "1:02", "4"]));
|
|
26
27
|
});
|
|
28
|
+
|
|
29
|
+
it("uses the SDK mark when no custom logo URL is supplied", () => {
|
|
30
|
+
const tree = SpaceTopBarAndroid({ spaceName: "Design space", participantCount: 4, formattedDuration: "1:02" });
|
|
31
|
+
|
|
32
|
+
expect(findElements(tree, "Logo")).toMatchObject([{ props: { height: 24, variant: "mark" } }]);
|
|
33
|
+
});
|
|
27
34
|
});
|
|
28
35
|
|
|
29
36
|
function findElements(value: unknown, type: string): { readonly props?: Record<string, unknown> }[] {
|
|
@@ -3,6 +3,7 @@ import { HugeiconsIcon } from "@hugeicons/react-native";
|
|
|
3
3
|
import { Image, Platform, StyleSheet, Text, View } from "react-native";
|
|
4
4
|
import { Theme } from "../../ui/theme";
|
|
5
5
|
import { useNativeTheme } from "../../ui/native-theme";
|
|
6
|
+
import { Logo } from "../logo/Logo";
|
|
6
7
|
|
|
7
8
|
export interface SpaceTopBarProps {
|
|
8
9
|
spaceName: string;
|
|
@@ -16,7 +17,7 @@ export function SpaceTopBarAndroid({ spaceName, participantCount, formattedDurat
|
|
|
16
17
|
return (
|
|
17
18
|
<View style={[styles.topBar, { backgroundColor: theme.colors.darkCanvas, borderColor: theme.colors.border }]}>
|
|
18
19
|
<View style={styles.topBarLeft}>
|
|
19
|
-
{logoUrl ? <Image accessibilityLabel="Chalk" source={{ uri: logoUrl }} style={styles.logo} /> :
|
|
20
|
+
{logoUrl ? <Image accessibilityLabel="Chalk" source={{ uri: logoUrl }} style={styles.logo} /> : <Logo height={24} variant="mark" />}
|
|
20
21
|
<View style={[styles.connectionDot, { backgroundColor: theme.colors.success }]} />
|
|
21
22
|
<Text style={[styles.topBarSpaceName, { color: theme.colors.foreground }]} numberOfLines={1}>
|
|
22
23
|
{spaceName}
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,8 @@ export { Chalk } from "./components/Chalk";
|
|
|
2
2
|
export type { ChalkFeatures, ChalkProps, ChalkTheme, ChalkThemeTokens, SpaceLayout } from "./components/Chalk";
|
|
3
3
|
export { Entrance } from "./components/Entrance";
|
|
4
4
|
export type { EntranceProps, EntranceSettings } from "./components/Entrance";
|
|
5
|
+
export { Logo } from "./components/logo/Logo";
|
|
6
|
+
export type { LogoMotion, LogoProps, LogoVariant } from "./components/logo/Logo";
|
|
5
7
|
export { ChalkProvider, useSpaceClient } from "./context/space-client-context";
|
|
6
8
|
export type { ChalkProviderProps } from "./context/space-client-context";
|
|
7
9
|
export { useCan, useChat, useConnection, useMedia, useParticipants, useReactions, useSelf, useWhiteboard } from "./hooks/space-hooks";
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"LogoElements.d.ts","sourceRoot":"","sources":["../../src/components/LogoElements.tsx"],"names":[],"mappings":"AASA,UAAU,iBAAiB;IACzB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,YAAY,CAAC,EAAE,IAAS,EAAE,EAAE,iBAAiB,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAyGhF"}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
// @vitest-environment happy-dom
|
|
2
|
-
import type { ReactElement, ReactNode } from "react";
|
|
3
|
-
import { describe, expect, it, vi } from "vitest";
|
|
4
|
-
|
|
5
|
-
import { renderHook } from "../test-support/test-renderer";
|
|
6
|
-
|
|
7
|
-
vi.mock("react-native", () => {
|
|
8
|
-
class AnimatedValue {
|
|
9
|
-
interpolate(input: unknown): { readonly input: unknown } {
|
|
10
|
-
return { input };
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
return {
|
|
15
|
-
Animated: {
|
|
16
|
-
Value: AnimatedValue,
|
|
17
|
-
createAnimatedComponent: (component: unknown) => component,
|
|
18
|
-
},
|
|
19
|
-
};
|
|
20
|
-
});
|
|
21
|
-
vi.mock("react-native-svg", () => ({ Circle: "Circle", Ellipse: "Ellipse", G: "G", Path: "Path", Rect: "Rect", default: "Svg" }));
|
|
22
|
-
vi.mock("./native-animation-controller", () => ({ createAnimationRefController: () => () => undefined }));
|
|
23
|
-
|
|
24
|
-
import Svg from "react-native-svg";
|
|
25
|
-
import { LogoElements } from "./LogoElements";
|
|
26
|
-
|
|
27
|
-
describe("LogoElements", () => {
|
|
28
|
-
it("uses the requested size and keeps the complete canonical mark composition", () => {
|
|
29
|
-
const { result } = renderHook(() => LogoElements({ size: 96 }));
|
|
30
|
-
const tree = result.current;
|
|
31
|
-
|
|
32
|
-
expect(tree.type).toBe(Svg);
|
|
33
|
-
expect(tree.props).toMatchObject({ height: 96, viewBox: "0 0 64 64", width: 96 });
|
|
34
|
-
expect(findElements(tree, "Path")).toHaveLength(1);
|
|
35
|
-
expect(findElements(tree, "Circle")).toHaveLength(3);
|
|
36
|
-
expect(findElements(tree, "Rect")).toHaveLength(4);
|
|
37
|
-
expect(findElements(tree, "Ellipse")).toHaveLength(4);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("preserves the brand palette across each logo bar and particle", () => {
|
|
41
|
-
const { result } = renderHook(() => LogoElements({}));
|
|
42
|
-
const tree = result.current;
|
|
43
|
-
const fills = findElements(tree, "Rect").map((element) => element.props.fill);
|
|
44
|
-
const particleFills = findElements(tree, "Circle").map((element) => element.props.fill);
|
|
45
|
-
|
|
46
|
-
expect(fills).toEqual(["#A8D5A2", "#F5D76E", "#7EC8E3", "#F0A0A0"]);
|
|
47
|
-
expect(particleFills).toEqual(["#A8D5A2", "#F5D76E", "#7EC8E3"]);
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
function findElements(node: ReactNode, type: string): ReactElement<{ readonly fill?: string }>[] {
|
|
52
|
-
if (!isElement(node)) return [];
|
|
53
|
-
const matches = node.type === type ? [node] : [];
|
|
54
|
-
const children = node.props.children;
|
|
55
|
-
return matches.concat((Array.isArray(children) ? children : [children]).flatMap((child) => findElements(child, type)));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function isElement(node: ReactNode): node is ReactElement<{ readonly children?: ReactNode; readonly fill?: string }> {
|
|
59
|
-
return typeof node === "object" && node !== null && "type" in node && "props" in node;
|
|
60
|
-
}
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import { useRef } from "react";
|
|
2
|
-
import { Animated } from "react-native";
|
|
3
|
-
import Svg, { Circle, Ellipse, G, Path, Rect } from "react-native-svg";
|
|
4
|
-
import { Theme } from "../ui/theme";
|
|
5
|
-
import { createAnimationRefController, type AnimationRefCallback } from "./native-animation-controller";
|
|
6
|
-
|
|
7
|
-
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
|
|
8
|
-
const AnimatedPath = Animated.createAnimatedComponent(Path);
|
|
9
|
-
|
|
10
|
-
interface LogoElementsProps {
|
|
11
|
-
size?: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function LogoElements({ size = 64 }: LogoElementsProps): React.JSX.Element {
|
|
15
|
-
const particle1 = useRef(new Animated.Value(0)).current;
|
|
16
|
-
const particle2 = useRef(new Animated.Value(0)).current;
|
|
17
|
-
const particle3 = useRef(new Animated.Value(0)).current;
|
|
18
|
-
const arcProgress = useRef(new Animated.Value(0)).current;
|
|
19
|
-
|
|
20
|
-
const animationRef = useRef<AnimationRefCallback<unknown> | null>(null);
|
|
21
|
-
const attachAnimations =
|
|
22
|
-
animationRef.current ??
|
|
23
|
-
(animationRef.current = createAnimationRefController<unknown>(() => {
|
|
24
|
-
const createParticleAnimation = (animation: Animated.Value, delay: number) =>
|
|
25
|
-
Animated.loop(
|
|
26
|
-
Animated.sequence([
|
|
27
|
-
Animated.delay(delay),
|
|
28
|
-
Animated.timing(animation, {
|
|
29
|
-
toValue: 1,
|
|
30
|
-
duration: 2500,
|
|
31
|
-
useNativeDriver: true,
|
|
32
|
-
}),
|
|
33
|
-
]),
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
const particleAnimations = [createParticleAnimation(particle1, 0), createParticleAnimation(particle2, 800), createParticleAnimation(particle3, 1600)];
|
|
37
|
-
const arcAnimation = Animated.loop(
|
|
38
|
-
Animated.sequence([
|
|
39
|
-
Animated.timing(arcProgress, {
|
|
40
|
-
toValue: 1,
|
|
41
|
-
duration: 3500,
|
|
42
|
-
useNativeDriver: true,
|
|
43
|
-
}),
|
|
44
|
-
Animated.delay(2500),
|
|
45
|
-
]),
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
return [...particleAnimations, arcAnimation];
|
|
49
|
-
}));
|
|
50
|
-
|
|
51
|
-
const createParticleProps = (animation: Animated.Value, xOffset: number) => ({
|
|
52
|
-
opacity: animation.interpolate({
|
|
53
|
-
inputRange: [0, 0.2, 0.8, 1],
|
|
54
|
-
outputRange: [0, 0.5, 0.5, 0],
|
|
55
|
-
}),
|
|
56
|
-
transform: [
|
|
57
|
-
{
|
|
58
|
-
translateY: animation.interpolate({
|
|
59
|
-
inputRange: [0, 1],
|
|
60
|
-
outputRange: [0, -20],
|
|
61
|
-
}),
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
translateX: animation.interpolate({
|
|
65
|
-
inputRange: [0, 1],
|
|
66
|
-
outputRange: [0, xOffset],
|
|
67
|
-
}),
|
|
68
|
-
},
|
|
69
|
-
],
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
return (
|
|
73
|
-
<Svg ref={attachAnimations} width={size} height={size} viewBox="0 0 64 64" fill="none">
|
|
74
|
-
<AnimatedPath
|
|
75
|
-
d="M 12 16 Q 32 -2 52 16"
|
|
76
|
-
stroke={Theme.logo.arc}
|
|
77
|
-
strokeWidth="1.5"
|
|
78
|
-
strokeLinecap="round"
|
|
79
|
-
fill="none"
|
|
80
|
-
opacity={arcProgress.interpolate({
|
|
81
|
-
inputRange: [0, 0.2, 0.7, 1],
|
|
82
|
-
outputRange: [0, 1, 1, 0],
|
|
83
|
-
})}
|
|
84
|
-
transform={[
|
|
85
|
-
{
|
|
86
|
-
translateY: arcProgress.interpolate({
|
|
87
|
-
inputRange: [0, 1],
|
|
88
|
-
outputRange: [4, -4],
|
|
89
|
-
}),
|
|
90
|
-
},
|
|
91
|
-
]}
|
|
92
|
-
/>
|
|
93
|
-
|
|
94
|
-
<AnimatedCircle cx="16" cy="12" r="1.5" fill={Theme.logo.mint} {...createParticleProps(particle1, -8)} />
|
|
95
|
-
<AnimatedCircle cx="28" cy="8" r="2" fill={Theme.logo.gold} {...createParticleProps(particle2, 4)} />
|
|
96
|
-
<AnimatedCircle cx="40" cy="6" r="1.5" fill={Theme.logo.sky} {...createParticleProps(particle3, 8)} />
|
|
97
|
-
|
|
98
|
-
<G transform="rotate(-20 16 48)">
|
|
99
|
-
<Rect x="8" y="16" width="12" height="40" rx="6" fill={Theme.logo.mint} />
|
|
100
|
-
<Ellipse cx="14" cy="16" rx="6" ry="3.5" fill={Theme.logo.mintDark} />
|
|
101
|
-
</G>
|
|
102
|
-
|
|
103
|
-
<G transform="rotate(-5 24 44)">
|
|
104
|
-
<Rect x="18" y="12" width="12" height="44" rx="6" fill={Theme.logo.gold} />
|
|
105
|
-
<Ellipse cx="24" cy="12" rx="6" ry="3.5" fill={Theme.logo.goldDark} />
|
|
106
|
-
</G>
|
|
107
|
-
|
|
108
|
-
<G transform="rotate(25 44 20)">
|
|
109
|
-
<Rect x="28" y="4" width="12" height="42" rx="6" fill={Theme.logo.sky} />
|
|
110
|
-
<Ellipse cx="34" cy="4" rx="6" ry="3.5" fill={Theme.logo.skyDark} />
|
|
111
|
-
</G>
|
|
112
|
-
|
|
113
|
-
<G transform="rotate(10 44 40)">
|
|
114
|
-
<Rect x="38" y="18" width="12" height="38" rx="6" fill={Theme.logo.coral} />
|
|
115
|
-
<Ellipse cx="44" cy="56" rx="6" ry="3.5" fill={Theme.logo.coralDark} />
|
|
116
|
-
</G>
|
|
117
|
-
</Svg>
|
|
118
|
-
);
|
|
119
|
-
}
|