@q9labsai/chalk-react-native 4.1.10 → 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.
@@ -1 +1 @@
1
- {"version":3,"file":"Entrance.d.ts","sourceRoot":"","sources":["../../src/components/Entrance.tsx"],"names":[],"mappings":"AAGA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IACrC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,gBAAgB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACvE,CAAC;AAEF,gFAAgF;AAChF,wBAAgB,QAAQ,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,kBAAkB,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAe,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,aAAa,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAczJ"}
1
+ {"version":3,"file":"Entrance.d.ts","sourceRoot":"","sources":["../../src/components/Entrance.tsx"],"names":[],"mappings":"AAIA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IACrC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,gBAAgB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACvE,CAAC;AAEF,gFAAgF;AAChF,wBAAgB,QAAQ,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,kBAAkB,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAe,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,aAAa,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAczJ"}
@@ -0,0 +1,11 @@
1
+ export type LogoMotion = "orbit-burst" | "none";
2
+ export type LogoVariant = "mark" | "wordmark";
3
+ export interface LogoProps {
4
+ readonly accessibilityLabel?: string | null;
5
+ readonly color?: string;
6
+ readonly height?: number;
7
+ readonly motion?: LogoMotion;
8
+ readonly variant?: LogoVariant;
9
+ }
10
+ export declare function Logo({ accessibilityLabel, color, height, motion, variant }: LogoProps): React.JSX.Element;
11
+ //# sourceMappingURL=Logo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Logo.d.ts","sourceRoot":"","sources":["../../../src/components/logo/Logo.tsx"],"names":[],"mappings":"AAMA,MAAM,MAAM,UAAU,GAAG,aAAa,GAAG,MAAM,CAAC;AAChD,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,UAAU,CAAC;AAE9C,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC;IAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,WAAW,CAAC;CAChC;AAoLD,wBAAgB,IAAI,CAAC,EAAE,kBAA4B,EAAE,KAAqB,EAAE,MAAuB,EAAE,MAAuB,EAAE,OAAyB,EAAE,EAAE,SAAS,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAiHvL"}
@@ -1 +1 @@
1
- {"version":3,"file":"SpaceTopBar.android.d.ts","sourceRoot":"","sources":["../../../src/components/native-space-view/SpaceTopBar.android.tsx"],"names":[],"mappings":"AAMA,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,EAAE,MAAM,CAAC;IACzB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,kBAAkB,CAAC,EAAE,SAAS,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,OAAO,EAAE,EAAE,gBAAgB,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAoBnI;AAED,OAAO,EAAE,kBAAkB,IAAI,WAAW,EAAE,CAAC"}
1
+ {"version":3,"file":"SpaceTopBar.android.d.ts","sourceRoot":"","sources":["../../../src/components/native-space-view/SpaceTopBar.android.tsx"],"names":[],"mappings":"AAOA,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,EAAE,MAAM,CAAC;IACzB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,kBAAkB,CAAC,EAAE,SAAS,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,OAAO,EAAE,EAAE,gBAAgB,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAoBnI;AAED,OAAO,EAAE,kBAAkB,IAAI,WAAW,EAAE,CAAC"}
package/dist/index.d.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 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,UAAU,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAC/G,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,YAAY,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC7E,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAC/E,YAAY,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACzE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,eAAe,EAAE,YAAY,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACtI,OAAO,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,EAAE,0BAA0B,EAAE,4BAA4B,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AAC3G,YAAY,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AACvE,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAC5H,YAAY,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,iBAAiB,EAAE,eAAe,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,UAAU,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAC/G,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,YAAY,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC7E,OAAO,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAC9C,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACjF,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAC/E,YAAY,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACzE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,eAAe,EAAE,YAAY,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACtI,OAAO,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,EAAE,0BAA0B,EAAE,4BAA4B,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AAC3G,YAAY,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AACvE,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAC5H,YAAY,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,iBAAiB,EAAE,eAAe,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ import {
2
2
  Chalk,
3
3
  ChalkProvider,
4
4
  Entrance,
5
+ Logo,
5
6
  buildChalkInviteLink,
6
7
  useCan,
7
8
  useChat,
@@ -12,7 +13,7 @@ import {
12
13
  useSelf,
13
14
  useSpaceClient,
14
15
  useWhiteboard
15
- } from "./chunk-MMBE2BVK.js";
16
+ } from "./chunk-FVHH65FR.js";
16
17
  import {
17
18
  THEME_PALETTES,
18
19
  THEME_TEXTURES,
@@ -34,6 +35,7 @@ export {
34
35
  Chalk,
35
36
  ChalkProvider,
36
37
  Entrance,
38
+ Logo,
37
39
  THEME_PALETTES,
38
40
  THEME_TEXTURES,
39
41
  Theme,
package/dist/ios-pad.js CHANGED
@@ -2,6 +2,7 @@ import {
2
2
  Chalk,
3
3
  ChalkProvider,
4
4
  Entrance,
5
+ Logo,
5
6
  buildChalkInviteLink,
6
7
  useCan,
7
8
  useChat,
@@ -12,7 +13,7 @@ import {
12
13
  useSelf,
13
14
  useSpaceClient,
14
15
  useWhiteboard
15
- } from "./chunk-MMBE2BVK.js";
16
+ } from "./chunk-FVHH65FR.js";
16
17
  import {
17
18
  THEME_PALETTES,
18
19
  THEME_TEXTURES,
@@ -34,6 +35,7 @@ export {
34
35
  Chalk,
35
36
  ChalkProvider,
36
37
  Entrance,
38
+ Logo,
37
39
  THEME_PALETTES,
38
40
  THEME_TEXTURES,
39
41
  Theme,
package/dist/ios-phone.js CHANGED
@@ -2,6 +2,7 @@ import {
2
2
  Chalk,
3
3
  ChalkProvider,
4
4
  Entrance,
5
+ Logo,
5
6
  buildChalkInviteLink,
6
7
  useCan,
7
8
  useChat,
@@ -12,7 +13,7 @@ import {
12
13
  useSelf,
13
14
  useSpaceClient,
14
15
  useWhiteboard
15
- } from "./chunk-MMBE2BVK.js";
16
+ } from "./chunk-FVHH65FR.js";
16
17
  import {
17
18
  THEME_PALETTES,
18
19
  THEME_TEXTURES,
@@ -34,6 +35,7 @@ export {
34
35
  Chalk,
35
36
  ChalkProvider,
36
37
  Entrance,
38
+ Logo,
37
39
  THEME_PALETTES,
38
40
  THEME_TEXTURES,
39
41
  Theme,
package/dist/ios.js CHANGED
@@ -2,6 +2,7 @@ import {
2
2
  Chalk,
3
3
  ChalkProvider,
4
4
  Entrance,
5
+ Logo,
5
6
  buildChalkInviteLink,
6
7
  useCan,
7
8
  useChat,
@@ -12,7 +13,7 @@ import {
12
13
  useSelf,
13
14
  useSpaceClient,
14
15
  useWhiteboard
15
- } from "./chunk-MMBE2BVK.js";
16
+ } from "./chunk-FVHH65FR.js";
16
17
  import {
17
18
  THEME_PALETTES,
18
19
  THEME_TEXTURES,
@@ -34,6 +35,7 @@ export {
34
35
  Chalk,
35
36
  ChalkProvider,
36
37
  Entrance,
38
+ Logo,
37
39
  THEME_PALETTES,
38
40
  THEME_TEXTURES,
39
41
  Theme,
package/dist/macos.js CHANGED
@@ -2,6 +2,7 @@ import {
2
2
  Chalk,
3
3
  ChalkProvider,
4
4
  Entrance,
5
+ Logo,
5
6
  buildChalkInviteLink,
6
7
  useCan,
7
8
  useChat,
@@ -12,7 +13,7 @@ import {
12
13
  useSelf,
13
14
  useSpaceClient,
14
15
  useWhiteboard
15
- } from "./chunk-MMBE2BVK.js";
16
+ } from "./chunk-FVHH65FR.js";
16
17
  import {
17
18
  THEME_PALETTES,
18
19
  THEME_TEXTURES,
@@ -34,6 +35,7 @@ export {
34
35
  Chalk,
35
36
  ChalkProvider,
36
37
  Entrance,
38
+ Logo,
37
39
  THEME_PALETTES,
38
40
  THEME_TEXTURES,
39
41
  Theme,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@q9labsai/chalk-react-native",
3
- "version": "4.1.10",
3
+ "version": "4.1.12",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -150,9 +150,9 @@
150
150
  "registry": "https://registry.npmjs.org/"
151
151
  },
152
152
  "dependencies": {
153
- "@q9labsai/chalk-client": "^4.1.10",
154
- "@q9labsai/chalk-whiteboard": "^4.1.10",
155
- "@q9labsai/facehash": "^4.1.10"
153
+ "@q9labsai/chalk-client": "^4.1.12",
154
+ "@q9labsai/chalk-whiteboard": "^4.1.12",
155
+ "@q9labsai/facehash": "^4.1.12"
156
156
  },
157
157
  "devDependencies": {
158
158
  "@types/node": "^26.2.0",
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
2
 
3
3
  vi.mock("./EntranceView", () => ({ EntranceView: "EntranceView" }));
4
+ vi.mock("./logo/Logo", () => ({ Logo: "Logo" }));
4
5
  vi.mock("react-native", () => ({ Image: "Image" }));
5
6
 
6
7
  import { Entrance } from "./Entrance";
@@ -16,4 +17,10 @@ describe("Entrance", () => {
16
17
 
17
18
  expect(element.props.onCancel).toBe(onCancel);
18
19
  });
20
+
21
+ it("uses the SDK wordmark when no custom logo URL is supplied", () => {
22
+ const element = Entrance({ onJoin: vi.fn(), spaceName: "Space" });
23
+
24
+ expect(element.props.logo).toMatchObject({ type: "Logo", props: { height: 32, variant: "wordmark" } });
25
+ });
19
26
  });
@@ -1,5 +1,6 @@
1
1
  import { EntranceView } from "./EntranceView";
2
2
  import { Image } from "react-native";
3
+ import { Logo } from "./logo/Logo";
3
4
 
4
5
  export type EntranceDefaults = {
5
6
  readonly microphone?: boolean;
@@ -31,7 +32,7 @@ export function Entrance({ spaceName, logoUrl, defaultDisplayName, defaults, err
31
32
  initialAudioEnabled={defaults?.microphone ?? true}
32
33
  initialVideoEnabled={defaults?.camera ?? true}
33
34
  joinDisabled={joining}
34
- logo={logoUrl ? <Image accessibilityLabel="Chalk" source={{ uri: logoUrl }} style={{ height: 32, width: 120 }} /> : undefined}
35
+ logo={logoUrl ? <Image accessibilityLabel="Chalk" source={{ uri: logoUrl }} style={{ height: 32, width: 120 }} /> : <Logo height={32} variant="wordmark" />}
35
36
  onCancel={onCancel}
36
37
  onJoin={({ displayName, microphoneEnabled, cameraEnabled }) => void onJoin({ displayName, microphone: microphoneEnabled, camera: cameraEnabled })}
37
38
  spaceName={spaceName}
@@ -0,0 +1,144 @@
1
+ // @vitest-environment happy-dom
2
+ import type { ReactElement, ReactNode } from "react";
3
+ import { beforeEach, describe, expect, it, vi } from "vitest";
4
+
5
+ import { act, renderHook } from "../../test-support/test-renderer";
6
+
7
+ const mocks = vi.hoisted(() => {
8
+ const animationStops: ReturnType<typeof vi.fn>[] = [];
9
+ const createAnimation = (config?: unknown) => {
10
+ const stop = vi.fn();
11
+ animationStops.push(stop);
12
+ return { config, start: vi.fn(), stop };
13
+ };
14
+ class AnimatedValue {
15
+ readonly initial: number;
16
+ readonly setValue = vi.fn();
17
+ readonly stopAnimation = vi.fn();
18
+ constructor(initial: number) {
19
+ this.initial = initial;
20
+ }
21
+ interpolate(input: unknown): { readonly input: unknown } {
22
+ return { input };
23
+ }
24
+ }
25
+
26
+ return {
27
+ accessibilityInfo: {
28
+ addEventListener: vi.fn(() => ({ remove: vi.fn() })),
29
+ isReduceMotionEnabled: vi.fn(() => Promise.resolve(false)),
30
+ },
31
+ animationStops,
32
+ animation: {
33
+ delay: vi.fn((duration: number) => createAnimation({ duration })),
34
+ loop: vi.fn((animation: unknown) => createAnimation({ animation })),
35
+ parallel: vi.fn((animations: readonly unknown[]) => createAnimation({ animations })),
36
+ sequence: vi.fn((animations: readonly unknown[]) => createAnimation({ animations })),
37
+ timing: vi.fn((_value: unknown, config: unknown) => createAnimation(config)),
38
+ Value: AnimatedValue,
39
+ },
40
+ };
41
+ });
42
+
43
+ vi.mock("react-native", () => ({ AccessibilityInfo: mocks.accessibilityInfo, Animated: { ...mocks.animation, createAnimatedComponent: (component: unknown) => component }, Pressable: "Pressable" }));
44
+ vi.mock("react-native-svg", () => ({ Defs: "Defs", Ellipse: "Ellipse", G: "G", LinearGradient: "LinearGradient", Path: "Path", Stop: "Stop", Text: "SvgText", default: "Svg" }));
45
+
46
+ import { Logo } from "./Logo";
47
+
48
+ describe("Logo", () => {
49
+ beforeEach(() => {
50
+ vi.clearAllMocks();
51
+ mocks.animationStops.length = 0;
52
+ mocks.accessibilityInfo.isReduceMotionEnabled.mockResolvedValue(false);
53
+ });
54
+
55
+ it("uses the canonical mark geometry and keeps the wordmark static", () => {
56
+ const { result } = renderHook(() => Logo({ accessibilityLabel: "Brand", color: "#123456", height: 96 }));
57
+ const pressable = result.current;
58
+ const svg = findElement(pressable, "Svg");
59
+
60
+ expect(svg).toMatchObject({ props: { accessibilityLabel: "Brand", accessible: true, color: "#123456", height: 96, viewBox: "0 0 200 80", width: 240 } });
61
+ expect(findElements(svg, "Path")).toHaveLength(4);
62
+ expect(findElements(svg, "Ellipse")).toHaveLength(4);
63
+ expect(findElements(svg, "SvgText")).toMatchObject([{ props: { fill: "currentColor", children: "chalk" } }]);
64
+ expect(findElements(svg, "G")).toHaveLength(5);
65
+ expect(findElements(svg, "SvgText")[0].props).not.toHaveProperty("rotation");
66
+ });
67
+
68
+ it("renders the mark fallback without adding an announced button", () => {
69
+ const { result } = renderHook(() => Logo({ accessibilityLabel: null, height: 24, motion: "none", variant: "mark" }));
70
+ const pressable = result.current;
71
+ const svg = findElement(pressable, "Svg");
72
+
73
+ expect(pressable.props).toMatchObject({ accessible: false });
74
+ expect(svg).toMatchObject({ props: { accessible: false, height: 24, viewBox: "0 0 68 80", width: 20.4 } });
75
+ expect(findElements(svg, "SvgText")).toHaveLength(0);
76
+ });
77
+
78
+ it("uses orbit by default and switches only the sticks to burst while active", async () => {
79
+ const { result } = renderHook(() => Logo({}));
80
+ await act(async () => await Promise.resolve());
81
+
82
+ const initialTimingCount = mocks.animation.timing.mock.calls.length;
83
+ act(() => {
84
+ result.current.props.onHoverIn();
85
+ });
86
+
87
+ expect(mocks.animation.timing.mock.calls.length).toBeGreaterThan(initialTimingCount);
88
+ expect(mocks.animation.timing.mock.calls.slice(initialTimingCount).map(([, config]) => (config as { readonly toValue: number }).toValue)).toEqual(expect.arrayContaining([-14, -9, -4, -14, 14, -6, 11, 12, 1.14, 1.16, 1.13]));
89
+ expect(findElements(result.current, "SvgText")[0].props).not.toHaveProperty("rotation");
90
+
91
+ act(() => {
92
+ result.current.props.onHoverOut();
93
+ });
94
+ expect(mocks.animation.timing.mock.calls.length).toBeGreaterThan(initialTimingCount + 12);
95
+ });
96
+
97
+ it("does not start animations when reduced motion is enabled and removes the listener", async () => {
98
+ mocks.accessibilityInfo.isReduceMotionEnabled.mockResolvedValue(true);
99
+ const { result, unmount } = renderHook(() => Logo({}));
100
+ await act(async () => await Promise.resolve());
101
+
102
+ expect(mocks.animation.timing).not.toHaveBeenCalled();
103
+ unmount();
104
+ expect(mocks.accessibilityInfo.addEventListener.mock.results[0]?.value.remove).toHaveBeenCalledTimes(1);
105
+ expect(findElements(result.current, "Svg")).toHaveLength(1);
106
+ });
107
+
108
+ it("stops the active orbit when the logo unmounts", async () => {
109
+ const { unmount } = renderHook(() => Logo({}));
110
+ await act(async () => await Promise.resolve());
111
+
112
+ unmount();
113
+
114
+ expect(mocks.animationStops.some((stop) => stop.mock.calls.length > 0)).toBe(true);
115
+ });
116
+ });
117
+
118
+ function findElement(node: ReactNode, type: string): ReactElement<{ readonly children?: ReactNode; readonly [key: string]: unknown }> {
119
+ if (isElement(node) && node.type === type) return node;
120
+ if (!isElement(node)) throw new Error(`Expected ${type}`);
121
+ const children = Array.isArray(node.props.children) ? node.props.children : [node.props.children];
122
+ for (const child of children) {
123
+ if (isElement(child) && child.type === type) return child;
124
+ if (isElement(child)) {
125
+ try {
126
+ return findElement(child, type);
127
+ } catch {
128
+ continue;
129
+ }
130
+ }
131
+ }
132
+ throw new Error(`Expected ${type}`);
133
+ }
134
+
135
+ function findElements(node: ReactNode, type: string): ReactElement<{ readonly children?: ReactNode; readonly [key: string]: unknown }>[] {
136
+ if (!isElement(node)) return [];
137
+ const matches = node.type === type ? [node] : [];
138
+ const children = Array.isArray(node.props.children) ? node.props.children : [node.props.children];
139
+ return matches.concat(children.flatMap((child) => findElements(child, type)));
140
+ }
141
+
142
+ function isElement(node: ReactNode): node is ReactElement<{ readonly children?: ReactNode; readonly [key: string]: unknown }> {
143
+ return typeof node === "object" && node !== null && "type" in node && "props" in node;
144
+ }