diff --git a/app/index.css b/app/index.css index 68f96367..2e0e1592 100644 --- a/app/index.css +++ b/app/index.css @@ -51,6 +51,10 @@ a { text-decoration-color: hsl(var(--link-hue), 75%, 50%); } +.bg-link-hue { + background-color: hsl(var(--link-hue), 75%, 50%); +} + /* a:hover { text-decoration-color: gold; diff --git a/app/src/components/meme-selector.tsx b/app/src/components/meme-selector.tsx index bdc27fd1..63dbc666 100644 --- a/app/src/components/meme-selector.tsx +++ b/app/src/components/meme-selector.tsx @@ -163,7 +163,7 @@ export function MemeSelector(props: MemeSelectorProps): JSX.Element { return (
{/* Header with tabs */}
diff --git a/app/src/components/tutorial.tsx b/app/src/components/tutorial.tsx new file mode 100644 index 00000000..e3e5765f --- /dev/null +++ b/app/src/components/tutorial.tsx @@ -0,0 +1,140 @@ +import solid from "@kixelated/signals/solid"; +import { onCleanup, onMount, Show } from "solid-js"; +import type { JSX } from "solid-js/jsx-runtime"; +import Settings from "../settings"; + +export function Tutorial(): JSX.Element { + const step = solid(Settings.tutorial.step); + + const steps = [ + { + title: "Share Stuff", + description: + "Enable your microphone or webcam down here. Or share your screen, just make sure you close that tab first. You know the one.", + arrow: "bottom-left", + styles: { bottom: "5rem", left: "1rem" }, + }, + { + title: "Talk Stuff", + description: "Spam unfunny messages down here. There's also a dank meme selector.", + arrow: "bottom", + styles: { bottom: "5rem", left: "50%", transform: "translateX(-50%)" }, + }, + { + title: "Advanced Stuff", + description: "I guess.", + arrow: "bottom-right", + styles: { bottom: "5rem", right: "1rem" }, + }, + { + title: "Other Stuff", + description: "Favorite the room if you want to hang later. Or leave unannounced, cold.", + arrow: "top-right", + styles: { top: "5rem", right: "1rem" }, + }, + ]; + + const nextStep = () => { + Settings.tutorial.step.set(step() + 1); + }; + + const skipTutorial = () => { + Settings.tutorial.step.set(steps.length); + }; + + onMount(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === "Escape" && step() < steps.length) { + skipTutorial(); + } + }; + document.addEventListener("keydown", handleEscape); + onCleanup(() => document.removeEventListener("keydown", handleEscape)); + }); + + return ( + + {/* Backdrop */} + +
+ +

{steps[step()].description}

+ +
+
+ {steps.map((_, index) => ( +
+ ))} +
+ +
+ + +
+
+ + {/* Arrow */} +
+
+ + ); +} diff --git a/app/src/controls.tsx b/app/src/controls.tsx index 1c5d6da3..bbc8bcac 100644 --- a/app/src/controls.tsx +++ b/app/src/controls.tsx @@ -15,39 +15,236 @@ import { import type { JSX } from "solid-js/jsx-runtime"; import { MemeSelector } from "./components/meme-selector"; import Tooltip from "./components/tooltip"; +import { Tutorial } from "./components/tutorial"; import type { Room } from "./room"; import type { Canvas } from "./room/canvas"; import { Local } from "./room/local"; import { Sound } from "./room/sound"; import Settings, { Modal } from "./settings"; +import { isMobile } from "./util/mobile"; + +type MobileSection = "publish" | "chat" | "settings" | null; export function Controls(props: { room: Room; local: Local; canvas: Canvas }): JSX.Element { + const mobile = isMobile(); + const [expandedSection, setExpandedSection] = createSignal(null); + const tutorial = solid(Settings.tutorial.step); + + const toggleSection = (section: MobileSection) => { + setExpandedSection((prev) => (prev === section ? null : section)); + }; + + const collapseAll = () => { + setExpandedSection(null); + }; + + // Check if any publish source is active + const micActive = solid(props.local.camera.audio.root); + const cameraActive = solid(props.local.webcam.source); + const screenActive = solid(props.local.screen.source); + const anySourceActive = createMemo(() => !!micActive() || !!cameraActive() || !!screenActive()); + + // Close expanded sections when clicking outside (mobile only) + let publishRef: HTMLDivElement | undefined; + let chatRef: HTMLDivElement | undefined; + let settingsRef: HTMLDivElement | undefined; + + onMount(() => { + const handleClick = (e: MouseEvent) => { + if (!mobile() || !expandedSection()) return; + + const target = e.target as Node; + const expanded = expandedSection(); + + // Check if click is inside the expanded section + if (expanded === "publish" && publishRef?.contains(target)) return; + if (expanded === "settings" && settingsRef?.contains(target)) return; + + // Click is outside the expanded section, so collapse + collapseAll(); + }; + document.addEventListener("click", handleClick, true); // Use capture phase + onCleanup(() => document.removeEventListener("click", handleClick, true)); + }); + return ( -