otherplane 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.
Files changed (63) hide show
  1. package/CLAUDE.md +130 -0
  2. package/LICENSE +21 -0
  3. package/README.md +146 -0
  4. package/bin/otherplane.mjs +489 -0
  5. package/engine/eslint.config.mjs +25 -0
  6. package/engine/next.config.ts +43 -0
  7. package/engine/package-lock.json +6848 -0
  8. package/engine/package.json +36 -0
  9. package/engine/postcss.config.mjs +5 -0
  10. package/engine/src/app/LandingRedirect.tsx +15 -0
  11. package/engine/src/app/[room]/RoomViewer.tsx +413 -0
  12. package/engine/src/app/[room]/page.tsx +30 -0
  13. package/engine/src/app/favicon.ico +0 -0
  14. package/engine/src/app/layout.tsx +45 -0
  15. package/engine/src/app/page.tsx +11 -0
  16. package/engine/src/app/providers.tsx +22 -0
  17. package/engine/src/components/controls/MobileHud.tsx +25 -0
  18. package/engine/src/components/controls/PlayerController.tsx +170 -0
  19. package/engine/src/components/controls/TouchLookController.tsx +93 -0
  20. package/engine/src/components/controls/VirtualStick.tsx +153 -0
  21. package/engine/src/components/edit/EditCapture.tsx +182 -0
  22. package/engine/src/components/edit/EditorPanel.tsx +265 -0
  23. package/engine/src/components/edit/Markers.tsx +91 -0
  24. package/engine/src/components/hud/Button.tsx +228 -0
  25. package/engine/src/components/hud/ClickToPlay.tsx +13 -0
  26. package/engine/src/components/hud/ContentOverlay.tsx +44 -0
  27. package/engine/src/components/hud/NavHeader.module.css +24 -0
  28. package/engine/src/components/scene/Artifacts.tsx +85 -0
  29. package/engine/src/components/scene/Exits.tsx +92 -0
  30. package/engine/src/components/scene/PointerLockBridge.tsx +28 -0
  31. package/engine/src/components/scene/WorldScene.tsx +164 -0
  32. package/engine/src/components/spark/SparkLayer.tsx +112 -0
  33. package/engine/src/components/spark/SplatWorld.tsx +156 -0
  34. package/engine/src/config/audio.ts +11 -0
  35. package/engine/src/data/editApi.ts +73 -0
  36. package/engine/src/data/presets.ts +34 -0
  37. package/engine/src/data/room.ts +100 -0
  38. package/engine/src/data/site.ts +50 -0
  39. package/engine/src/data/universeconfig.ts +19 -0
  40. package/engine/src/icons/ArrowLeft.tsx +20 -0
  41. package/engine/src/icons/ChevronDown.tsx +23 -0
  42. package/engine/src/icons/ChevronLeft.tsx +22 -0
  43. package/engine/src/icons/Home.tsx +22 -0
  44. package/engine/src/icons/Spinner.module.css +13 -0
  45. package/engine/src/icons/Spinner.tsx +28 -0
  46. package/engine/src/icons/VolumeMax.tsx +21 -0
  47. package/engine/src/icons/VolumeX.tsx +22 -0
  48. package/engine/src/icons/icons.interface.ts +7 -0
  49. package/engine/src/icons/index.ts +27 -0
  50. package/engine/src/physics/RapierProvider.tsx +302 -0
  51. package/engine/src/physics/index.ts +2 -0
  52. package/engine/src/physics/types.ts +9 -0
  53. package/engine/src/providers/audio.tsx +215 -0
  54. package/engine/src/providers/edit.tsx +357 -0
  55. package/engine/src/providers/pointerLock.tsx +88 -0
  56. package/engine/src/styles/globals.css +88 -0
  57. package/engine/tailwind.config.js +184 -0
  58. package/engine/tsconfig.json +27 -0
  59. package/otherplane.config.example.json +6 -0
  60. package/package.json +56 -0
  61. package/schema/room.schema.json +77 -0
  62. package/scripts/gen_world.py +147 -0
  63. package/skill.md +94 -0
@@ -0,0 +1,73 @@
1
+ // src/data/editApi.ts
2
+ // Client for the edit-mode writer — the tiny local HTTP sidecar the `otherplane
3
+ // edit` CLI runs alongside `next dev` (see bin/otherplane.mjs). It is the ONLY
4
+ // thing that writes room.json: edit mode POSTs marked coordinates here and the
5
+ // CLI merges them into the PROJECT source (never the mirror) and re-syncs.
6
+ //
7
+ // This exists only in edit mode. A published static export has no writer and no
8
+ // edit UI, so none of this is ever reachable there.
9
+
10
+ import type { Entryway, Exit, Artifact, Vec3 } from '@/data/room';
11
+
12
+ // The CLI passes its port through NEXT_PUBLIC_EDIT_API; fall back to the default.
13
+ const API = process.env.NEXT_PUBLIC_EDIT_API || 'http://localhost:4400';
14
+
15
+ /** One room as the writer reports it for the exit-target dropdown. */
16
+ export type RoomSummary = {
17
+ slug: string;
18
+ display_name: string;
19
+ entryways: { id: string; pos: Vec3; yaw: number }[];
20
+ };
21
+
22
+ /** The coordinate data the editor owns and writes back (never asset URLs). */
23
+ export type Marks = {
24
+ entryways: Entryway[];
25
+ exits: Exit[];
26
+ artifacts: Artifact[];
27
+ };
28
+
29
+ async function req<T>(path: string, init?: RequestInit): Promise<T> {
30
+ const res = await fetch(`${API}${path}`, {
31
+ ...init,
32
+ headers: { 'Content-Type': 'application/json', ...(init?.headers ?? {}) },
33
+ });
34
+ const body = await res.json().catch(() => ({}));
35
+ if (!res.ok) throw new Error(body?.error || `${res.status} ${res.statusText}`);
36
+ return body as T;
37
+ }
38
+
39
+ /** List every room + its entryways, for wiring exits by menu. */
40
+ export async function fetchRooms(): Promise<RoomSummary[]> {
41
+ const { rooms } = await req<{ rooms: RoomSummary[] }>('/rooms');
42
+ return rooms;
43
+ }
44
+
45
+ /**
46
+ * Persist a room's marks (and optionally its calibration.scale). Asset URLs are
47
+ * untouched — the CLI merges only the keys we send.
48
+ */
49
+ export async function saveMarks(
50
+ slug: string,
51
+ marks: Marks,
52
+ calibration?: { scale: number },
53
+ ): Promise<void> {
54
+ const body = calibration ? { ...marks, calibration } : marks;
55
+ await req(`/rooms/${slug}`, { method: 'PUT', body: JSON.stringify(body) });
56
+ }
57
+
58
+ /** Merge fields into otherplane.config.json (e.g. the per-museum walk speed). */
59
+ export async function saveConfig(patch: Record<string, unknown>): Promise<void> {
60
+ await req('/config', { method: 'PUT', body: JSON.stringify(patch) });
61
+ }
62
+
63
+ /**
64
+ * Wire a two-way door by reusing each side's existing entryway position. Both
65
+ * entryways must already exist — the reciprocal exit sits at the entryway you
66
+ * point it at (you can't conjure the far room's coordinates from this one).
67
+ */
68
+ export async function linkDoor(
69
+ a: { slug: string; entryId: string },
70
+ b: { slug: string; entryId: string },
71
+ ): Promise<void> {
72
+ await req('/doors/link', { method: 'POST', body: JSON.stringify({ a, b }) });
73
+ }
@@ -0,0 +1,34 @@
1
+ // src/data/presets.ts
2
+ import type { Room } from '@/data/room';
3
+
4
+ // WorldDef is the viewer's internal per-room render shape, built from a Room
5
+ // (roomToWorldDef below). There is no hardcoded world library — the renderer is
6
+ // given one room at a time.
7
+ export type WorldDef = {
8
+ id: string;
9
+ name: string;
10
+ url: string; // .spz or .ply (Spark auto-detects)
11
+ colliderUrl?: string; // GLB collider mesh (Marble) for real wall collision
12
+ musicUrl: string;
13
+ position?: [number, number, number];
14
+ quaternion?: [number, number, number, number]; // x,y,z,w
15
+ scale?: number;
16
+ };
17
+
18
+ // All Marble splats share the same canonical transform: identity position, the
19
+ // Spark 180°-about-X flip, and the room's calibrated scale. Per-room
20
+ // rotation/offset is never needed since rooms never share space.
21
+ export function roomToWorldDef(id: string, room: Room, scaleOverride?: number): WorldDef {
22
+ return {
23
+ id,
24
+ name: room.display_name,
25
+ url: room.splat_url,
26
+ colliderUrl: room.collider_url,
27
+ musicUrl: room.music_url ?? '',
28
+ position: [0, 0, 0],
29
+ quaternion: [1, 0, 0, 0], // Spark convention: 180° about X
30
+ // Edit mode drives scale live off the room-scale slider; published builds use
31
+ // the room's saved calibration.
32
+ scale: scaleOverride ?? room.calibration.scale,
33
+ };
34
+ }
@@ -0,0 +1,100 @@
1
+ // src/data/room.ts
2
+ // The viewer's ONE primitive: a room. There is no "museum" object — a museum is
3
+ // just rooms linked by exits, exactly like the web is pages linked by hrefs.
4
+ //
5
+ // Each room lives at its own URL (the /r/[id] route, or any remote URL). The
6
+ // renderer is a deep module: "here is one room — its splat, its collider, its
7
+ // music, its entryways, its exits, its artifacts — render it and let me walk."
8
+ //
9
+ // Two distinct concepts that an old "door" used to conflate:
10
+ // • entryway — a named, addressable spot you ARRIVE at: { id, pos, yaw }. You
11
+ // spawn at an entryway, facing into the room. It's what a URL fragment points
12
+ // at (…/library#from-study). One entryway is the "default" (used when the URL
13
+ // has no fragment).
14
+ // • exit — a spot you walk up to and INTERACT with (gaze + E) to leave:
15
+ // { pos, radius, to }. `to` is a URL, normally "<room-url>#<entryway-id>"
16
+ // (relative within a museum, absolute across museums). Dead exits 404 quietly.
17
+ //
18
+ // artifacts are a third, separate thing: walk up, interact, open a web URL in an
19
+ // overlay — content in place, no transport.
20
+
21
+ export type Vec3 = [number, number, number];
22
+
23
+ /** A named spawn point: a position + the yaw you face when you arrive there. */
24
+ export type Entryway = { id: string; pos: Vec3; yaw: number };
25
+
26
+ /** A walk-up interactive link to another room (or any URL). */
27
+ export type Exit = { pos: Vec3; radius?: number; to: string };
28
+
29
+ /** Walk up + interact to open `url` in a fullscreen overlay. */
30
+ export type Artifact = { id?: string; pos: Vec3; radius: number; url: string };
31
+
32
+ export type Room = {
33
+ display_name: string;
34
+ splat_url: string;
35
+ collider_url: string;
36
+ music_url?: string | null;
37
+ pano_url?: string | null;
38
+ thumbnail_url?: string | null;
39
+ /** Marble reconstructs at arbitrary scale; this brings the room to meters. */
40
+ calibration: { scale: number };
41
+ entryways: Entryway[];
42
+ exits: Exit[];
43
+ artifacts: Artifact[];
44
+ };
45
+
46
+ /** The default entryway is the one named "default", else the first listed. */
47
+ export const DEFAULT_ENTRYWAY_ID = 'default';
48
+
49
+ /**
50
+ * Resolve which entryway to spawn at. `id` comes from the URL fragment
51
+ * (…/library#from-study → "from-study"); empty/unknown falls back to the
52
+ * default. Returns null only if the room declares no entryways at all (a
53
+ * freshly generated, not-yet-marked room — the renderer then best-effort
54
+ * searches for floor).
55
+ */
56
+ export function resolveEntryway(room: Room, id?: string | null): Entryway | null {
57
+ if (id) {
58
+ const match = room.entryways.find((e) => e.id === id);
59
+ if (match) return match;
60
+ }
61
+ return (
62
+ room.entryways.find((e) => e.id === DEFAULT_ENTRYWAY_ID) ??
63
+ room.entryways[0] ??
64
+ null
65
+ );
66
+ }
67
+
68
+ /** Read the entryway id from a URL hash like "#from-study" → "from-study". */
69
+ export function entrywayIdFromHash(hash: string): string | null {
70
+ const h = hash.replace(/^#/, '').trim();
71
+ return h.length ? h : null;
72
+ }
73
+
74
+ /**
75
+ * Load a room from its room.json URL. Asset URLs inside are resolved RELATIVE TO
76
+ * the room.json's own URL — so a room is a portable unit: assets can sit next to
77
+ * it (`./study.spz`), live on object storage (`https://r2.../study.spz`), or point
78
+ * at the World Labs CDN. The viewer never cares where the bytes are.
79
+ */
80
+ export async function loadRoom(url: string): Promise<Room> {
81
+ const res = await fetch(url);
82
+ if (!res.ok) {
83
+ throw new Error(`Failed to load room "${url}": ${res.status} ${res.statusText}`);
84
+ }
85
+ const room = (await res.json()) as Room;
86
+ if (!room.splat_url || !room.collider_url) {
87
+ throw new Error(`room "${url}" is missing splat_url/collider_url`);
88
+ }
89
+ const base = new URL(url, window.location.href);
90
+ const abs = (u: string | null | undefined) => (u ? new URL(u, base).href : u ?? null);
91
+ room.splat_url = abs(room.splat_url) as string;
92
+ room.collider_url = abs(room.collider_url) as string;
93
+ room.music_url = abs(room.music_url);
94
+ room.pano_url = abs(room.pano_url);
95
+ room.thumbnail_url = abs(room.thumbnail_url);
96
+ room.entryways ??= [];
97
+ room.exits ??= [];
98
+ room.artifacts ??= [];
99
+ return room;
100
+ }
@@ -0,0 +1,50 @@
1
+ // Deploy-level config, read from `otherplane.config.json` at the repo root.
2
+ //
3
+ // A museum is just rooms linked by exits — there is no global "museum" object and
4
+ // no start_room baked into the room model. But a single *deploy* still has to
5
+ // answer a few per-deploy questions: which room does the site root (/) land on,
6
+ // what is the page titled, is it served under a sub-path. Those choices belong to
7
+ // the project, NOT to the renderer source — so they live in the project's
8
+ // otherplane.config.json and the engine reads them here.
9
+ //
10
+ // Read at BUILD time and SERVER-SIDE only (it touches fs). Never import this from
11
+ // a 'use client' component, or the build will try to bundle fs for the browser.
12
+ import { readFileSync } from 'fs';
13
+ import { join } from 'path';
14
+
15
+ export type SiteConfig = {
16
+ /** Slug the site root (/) redirects to. Empty = no redirect. */
17
+ landingRoom: string;
18
+ /** Sub-path prefix for project-page hosting ("" = served at domain root). */
19
+ basePath: string;
20
+ /** Document <title> / metadata. */
21
+ siteTitle: string;
22
+ /** Per-museum walk speed (ships to every viewer). Editable in edit mode. */
23
+ moveSpeed: number;
24
+ };
25
+
26
+ const DEFAULTS: SiteConfig = { landingRoom: '', basePath: '', siteTitle: 'Otherplane', moveSpeed: 14 };
27
+
28
+ function projectDir(): string {
29
+ // The CLI passes the project root via OTHERPLANE_PROJECT. Falling back to the
30
+ // engine's parent keeps this repo's sibling layout working when run directly.
31
+ return process.env.OTHERPLANE_PROJECT || join(process.cwd(), '..');
32
+ }
33
+
34
+ function load(): SiteConfig {
35
+ let cfg = DEFAULTS;
36
+ try {
37
+ const raw = readFileSync(join(projectDir(), 'otherplane.config.json'), 'utf8');
38
+ cfg = { ...DEFAULTS, ...JSON.parse(raw) };
39
+ } catch {
40
+ cfg = DEFAULTS;
41
+ }
42
+ // `otherplane build --base /museum` overrides basePath for a one-off deploy.
43
+ if (typeof process.env.OTHERPLANE_BASE_PATH === 'string') {
44
+ cfg = { ...cfg, basePath: process.env.OTHERPLANE_BASE_PATH };
45
+ }
46
+ return cfg;
47
+ }
48
+
49
+ export const SITE: SiteConfig = load();
50
+ export const LANDING_ROOM = SITE.landingRoom;
@@ -0,0 +1,19 @@
1
+ export const CONFIG = {
2
+ // Physics
3
+ GRAVITY: { x: 0, y: -9.81, z: 0 },
4
+ ENVIRONMENT_RESTITUTION: 0.0,
5
+
6
+ // Player
7
+ PLAYER: {
8
+ RADIUS: 0.33,
9
+ HALF_HEIGHT: 0.55,
10
+ START: [0, 1.4, 0] as [number, number, number],
11
+ FRICTION: 0.9,
12
+ RESTI: 0.0,
13
+ // Low, so gravity actually accelerates a fall (real quadratic drop) instead
14
+ // of capping it at a floaty ~2.5 m/s terminal velocity. Horizontal velocity
15
+ // is set explicitly every frame in PlayerController, so it doesn't rely on
16
+ // damping to stop — walking stays snappy.
17
+ LINEAR_DAMPING: 0.1,
18
+ },
19
+ }
@@ -0,0 +1,20 @@
1
+ import React from "react";
2
+ import { IconProps, defaultClassName } from "./icons.interface";
3
+
4
+ export function ArrowLeftLine({ className }: IconProps) {
5
+ return (
6
+ <svg
7
+ width="24"
8
+ height="24"
9
+ viewBox="0 0 24 24"
10
+ fill="none"
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ className={className || defaultClassName}
13
+ >
14
+ <path d="M19 12H5M5 12L12 19M5 12L12 5"
15
+ strokeWidth="2"
16
+ strokeLinecap="round"
17
+ strokeLinejoin="round"/>
18
+ </svg>
19
+ );
20
+ }
@@ -0,0 +1,23 @@
1
+ import React from "react";
2
+ import { IconProps, defaultClassName } from "./icons.interface";
3
+
4
+ export function ChevronDownLine({ className }: IconProps) {
5
+ return (
6
+ <svg
7
+ width="24"
8
+ height="24"
9
+ viewBox="0 0 24 24"
10
+ fill="none"
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ className={className || defaultClassName}
13
+ >
14
+ <path
15
+ d="M6 9L12 15L18 9"
16
+ // stroke="black"
17
+ strokeWidth="2"
18
+ strokeLinecap="round"
19
+ strokeLinejoin="round"
20
+ />
21
+ </svg>
22
+ );
23
+ }
@@ -0,0 +1,22 @@
1
+ import React from "react";
2
+ import { IconProps, defaultStrokeClassName } from "./icons.interface"
3
+
4
+ export function ChevronLeftLine({ className }: IconProps) {
5
+ return (
6
+ <svg
7
+ width="24"
8
+ height="24"
9
+ viewBox="0 0 24 24"
10
+ fill="none"
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ className={className || defaultStrokeClassName}
13
+ >
14
+ <path
15
+ d="M15 18L9 12L15 6"
16
+ strokeWidth="2"
17
+ strokeLinecap="round"
18
+ strokeLinejoin="round"
19
+ />
20
+ </svg>
21
+ );
22
+ }
@@ -0,0 +1,22 @@
1
+ import React from "react";
2
+ import { IconProps, defaultStrokeClassName } from "./icons.interface";
3
+
4
+ export function HomeLine({ className }: IconProps) {
5
+ return (
6
+ <svg
7
+ width="24"
8
+ height="24"
9
+ viewBox="0 0 24 24"
10
+ fill="none"
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ className={className || defaultStrokeClassName}
13
+ >
14
+ <path d="M9 21V13.6C9 13.0399 9 12.7599 9.10899 12.546C9.20487 12.3578 9.35785 12.2049 9.54601 12.109C9.75992 12 10.0399 12 10.6 12H13.4C13.9601 12 14.2401 12 14.454 12.109C14.6422 12.2049 14.7951 12.3578 14.891 12.546C15 12.7599 15 13.0399 15 13.6V21M11.0177 2.764L4.23539 8.03912C3.78202 8.39175 3.55534 8.56806 3.39203 8.78886C3.24737 8.98444 3.1396 9.20478 3.07403 9.43905C3 9.70352 3 9.9907 3 10.5651V17.8C3 18.9201 3 19.4801 3.21799 19.908C3.40973 20.2843 3.71569 20.5903 4.09202 20.782C4.51984 21 5.07989 21 6.2 21H17.8C18.9201 21 19.4802 21 19.908 20.782C20.2843 20.5903 20.5903 20.2843 20.782 19.908C21 19.4801 21 18.9201 21 17.8V10.5651C21 9.9907 21 9.70352 20.926 9.43905C20.8604 9.20478 20.7526 8.98444 20.608 8.78886C20.4447 8.56806 20.218 8.39175 19.7646 8.03913L12.9823 2.764C12.631 2.49075 12.4553 2.35412 12.2613 2.3016C12.0902 2.25526 11.9098 2.25526 11.7387 2.3016C11.5447 2.35412 11.369 2.49075 11.0177 2.764Z"
15
+ strokeWidth="2"
16
+ strokeLinecap="round"
17
+ strokeLinejoin="round"
18
+ />
19
+ </svg>
20
+ );
21
+ }
22
+
@@ -0,0 +1,13 @@
1
+ @keyframes spin {
2
+ 0% {
3
+ transform: rotate(0deg);
4
+ }
5
+
6
+ 100% {
7
+ transform: rotate(360deg);
8
+ }
9
+ }
10
+
11
+ .spinner {
12
+ animation: spin 1s linear infinite;
13
+ }
@@ -0,0 +1,28 @@
1
+ import clsx from "clsx";
2
+ import { ComponentProps } from "react";
3
+ import styles from "./Spinner.module.css";
4
+
5
+ export interface Props extends ComponentProps<"svg"> {
6
+ size?: number;
7
+ }
8
+
9
+ export function Spinner({ size = 16, className, ...props }: Props) {
10
+ return (
11
+ <svg
12
+ width={size}
13
+ height={size}
14
+ viewBox="0 0 16 16"
15
+ fill="none"
16
+ xmlns="http://www.w3.org/2000/svg"
17
+ className={clsx(className, styles.spinner)}
18
+ {...props}
19
+ >
20
+ <path
21
+ d="M14 8a6 6 0 1 1-6-6"
22
+ stroke="currentColor"
23
+ strokeWidth="2"
24
+ vectorEffect="non-scaling-stroke"
25
+ />
26
+ </svg>
27
+ );
28
+ }
@@ -0,0 +1,21 @@
1
+ import React from "react";
2
+ import { IconProps, defaultStrokeClassName } from "./icons.interface";
3
+
4
+ export function VolumeMaxLine({ className }: IconProps) {
5
+ return (
6
+ <svg
7
+ width="24"
8
+ height="24"
9
+ viewBox="0 0 24 24"
10
+ fill="none"
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ className={className || defaultStrokeClassName}
13
+ >
14
+ <path d="M19.7479 5.00005C21.1652 6.97029 22 9.38768 22 12C22 14.6124 21.1652 17.0298 19.7479 19M15.7453 8.00005C16.5362 9.13388 17 10.5128 17 12C17 13.4873 16.5362 14.8662 15.7453 16M9.63432 4.36573L6.46863 7.53142C6.29568 7.70437 6.2092 7.79085 6.10828 7.85269C6.01881 7.90752 5.92127 7.94792 5.81923 7.97242C5.70414 8.00005 5.58185 8.00005 5.33726 8.00005H3.6C3.03995 8.00005 2.75992 8.00005 2.54601 8.10904C2.35785 8.20492 2.20487 8.3579 2.10899 8.54606C2 8.75997 2 9.04 2 9.60005V14.4C2 14.9601 2 15.2401 2.10899 15.454C2.20487 15.6422 2.35785 15.7952 2.54601 15.8911C2.75992 16 3.03995 16 3.6 16H5.33726C5.58185 16 5.70414 16 5.81923 16.0277C5.92127 16.0522 6.01881 16.0926 6.10828 16.1474C6.2092 16.2093 6.29568 16.2957 6.46863 16.4687L9.63431 19.6344C10.0627 20.0627 10.2769 20.2769 10.4608 20.2914C10.6203 20.304 10.7763 20.2394 10.8802 20.1177C11 19.9774 11 19.6745 11 19.0687V4.93142C11 4.3256 11 4.0227 10.8802 3.88243C10.7763 3.76073 10.6203 3.69614 10.4608 3.7087C10.2769 3.72317 10.0627 3.93736 9.63432 4.36573Z"
15
+ strokeWidth="2"
16
+ strokeLinecap="round"
17
+ strokeLinejoin="round"
18
+ />
19
+ </svg>
20
+ );
21
+ }
@@ -0,0 +1,22 @@
1
+ import React from "react";
2
+ import { IconProps, defaultStrokeClassName } from "./icons.interface";
3
+
4
+ export function VolumeXLine({ className }: IconProps) {
5
+ return (
6
+ <svg
7
+ width="24"
8
+ height="24"
9
+ viewBox="0 0 24 24"
10
+ fill="none"
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ className={className || defaultStrokeClassName}
13
+ >
14
+ <path
15
+ d="M22 9.00005L16 15M16 9.00005L22 15M9.63432 4.36573L6.46863 7.53142C6.29568 7.70437 6.2092 7.79085 6.10828 7.85269C6.01881 7.90752 5.92127 7.94792 5.81923 7.97242C5.70414 8.00005 5.58185 8.00005 5.33726 8.00005H3.6C3.03995 8.00005 2.75992 8.00005 2.54601 8.10904C2.35785 8.20492 2.20487 8.3579 2.10899 8.54606C2 8.75997 2 9.04 2 9.60005V14.4C2 14.9601 2 15.2401 2.10899 15.454C2.20487 15.6422 2.35785 15.7952 2.54601 15.8911C2.75992 16 3.03995 16 3.6 16H5.33726C5.58185 16 5.70414 16 5.81923 16.0277C5.92127 16.0522 6.01881 16.0926 6.10828 16.1474C6.2092 16.2093 6.29568 16.2957 6.46863 16.4687L9.63431 19.6344C10.0627 20.0627 10.2769 20.2769 10.4608 20.2914C10.6203 20.304 10.7763 20.2394 10.8802 20.1177C11 19.9774 11 19.6745 11 19.0687V4.93142C11 4.3256 11 4.0227 10.8802 3.88243C10.7763 3.76073 10.6203 3.69614 10.4608 3.7087C10.2769 3.72317 10.0627 3.93736 9.63432 4.36573Z"
16
+ strokeWidth="2"
17
+ strokeLinecap="round"
18
+ strokeLinejoin="round"
19
+ />
20
+ </svg>
21
+ );
22
+ }
@@ -0,0 +1,7 @@
1
+ export type IconProps = {
2
+ className?: string;
3
+ };
4
+
5
+ export const defaultStrokeClassName = "stroke-primary";
6
+ export const defaultFillClassName = "fill-primary";
7
+ export const defaultClassName = `${defaultStrokeClassName} ${defaultFillClassName}`;
@@ -0,0 +1,27 @@
1
+ export * from "./ChevronLeft";
2
+ export * from "./ChevronDown";
3
+ export * from "./ArrowLeft";
4
+ export * from "./Spinner"
5
+ export * from "./VolumeMax"
6
+ export * from "./VolumeX"
7
+ export * from "./Home"
8
+
9
+ export enum IconColor {
10
+ Primary = "primary",
11
+ Secondary = "secondary",
12
+ Tertiary = "tertiary",
13
+ InvertedPrimary = "inverted-primary"
14
+ }
15
+
16
+ export type IconProps = {
17
+ className?: string;
18
+ color?: IconColor;
19
+ };
20
+
21
+ export const defaultFillClass = "fill-primary";
22
+ export const defaultStrokeClass = "stroke-primary";
23
+
24
+ export const fillClass = (color: IconColor | undefined) =>
25
+ `fill-${color ? color : "primary"}`;
26
+ export const strokeClass = (color: IconColor | undefined) =>
27
+ `stroke-${color ? color : "primary"}`;