feat: french kid-friendly room entry

Replace the English Name + Room ID form with two big, no-keyboard
buttons aimed at a 6-year-old:

- "Construire tout seul" joins a unique solo room (solo-<random>).
- "Construire a plusieurs" joins a fixed shared room ("lisael") so two
  kids who tap it land together without typing anything.
- An auto name ("Constructeur NNN") keeps self.name non-empty for the
  people list.
- A small "Salle perso" link reveals an optional room field for adults.

Also francise the visible chrome: Creer / Modifier modes, the rotate
toggle, the empty-room people label, and the help dialog (which no
longer auto-opens so the builder is playable immediately).
This commit is contained in:
L'électron rare
2026-06-13 18:43:34 +02:00
parent 17a6012423
commit ca1c5749cc
7 changed files with 224 additions and 219 deletions
+25 -29
View File
@@ -7,8 +7,10 @@ import * as Dialog from "@radix-ui/react-dialog";
import { Cross2Icon, InfoCircledIcon } from "@radix-ui/react-icons";
import "./dialogbox.css";
// The help dialog stays CLOSED by default so the builder is immediately
// playable for a child (no overlay to dismiss). The "?" button reopens it.
export const DialogBox = () => (
<Dialog.Root defaultOpen={true}>
<Dialog.Root defaultOpen={false}>
<Dialog.Trigger asChild>
<button
style={{
@@ -19,6 +21,7 @@ export const DialogBox = () => (
zIndex: "1000000000",
}}
className="Button violet"
aria-label="Aide"
>
<InfoCircledIcon color="black" transform="scale(2)" />
</button>
@@ -26,45 +29,38 @@ export const DialogBox = () => (
<Dialog.Portal>
<Dialog.Overlay className="DialogOverlay" />
<Dialog.Content className="DialogContent">
<Dialog.Title className="DialogTitle">Manual</Dialog.Title>
<Dialog.Title className="DialogTitle">Comment jouer</Dialog.Title>
<Dialog.Description className="DialogDescription" asChild>
<ul>
<li>Use left side panel to change the properties of brick</li>
<li>
You change the pivot point of bricks using{" "}
Choisis une <strong>forme</strong> et une <strong>couleur</strong>{" "}
en haut, puis clique pour poser une pièce.
</li>
<li>
En haut, <strong>🧱 Créer</strong> pose des pièces,{" "}
<strong> Modifier</strong> sert à les choisir, les déplacer ou
les effacer.
</li>
<li>
Pour effacer : passe en <strong> Modifier</strong>, clique une
pièce (ou maintiens <span className="key">Maj</span> pour en
choisir plusieurs) puis appuie sur{" "}
<span className="key">Suppr</span>.
</li>
<li>
Tu peux décaler le point de pose avec les touches{" "}
<span className="key">A</span> <span className="key">S</span>{" "}
<span className="key">W</span> and <span className="key">D</span>{" "}
keys or You can use <strong>anchorX</strong> and{" "}
<strong>anchorZ</strong> option from the control panel
<span className="key">W</span> <span className="key">D</span>.
</li>
<li>
To delete brick, select <strong>Edit</strong> option in control
panel then select brick by clicking on brick or hold{" "}
<span className="key">Shift</span> to select multiple bricks and
use
<span className="key">Delete</span> to delete selected bricks
</li>
<li>
Top bar has two options. create Mode which let&apos;s you create
new bricks while Edit mode let&apos;s you edit existing bricks by
selecting
</li>
<li>
You can also use <span className="key">CTRL</span> +{" "}
<span className="key">Z</span> for undo,{" "}
<span className="key">CTRL</span> + <span className="key">R</span>{" "}
for redo.
</li>
<li>
Bottom bar has (from left) first button to undo, second to redo,
third to delete selected and forth button displays list of all
users in room
En bas : les deux flèches annulent ou refont, la poubelle efface
la sélection, et le dernier bouton montre qui joue avec toi.
</li>
</ul>
</Dialog.Description>
<Dialog.Close asChild>
<button className="IconButton" aria-label="Close">
<button className="IconButton" aria-label="Fermer">
<Cross2Icon />
</button>
</Dialog.Close>
+59 -83
View File
@@ -1,98 +1,74 @@
/* eslint-disable react/prop-types */
import * as Form from "@radix-ui/react-form";
import "./styles.css";
import { useRef } from "react";
import { useRef, useState } from "react";
import { useStore } from "../../../store";
import { uID } from "../../../utils";
const JoinRoomForm = ({ setRoomId }) => {
const form = useRef();
// Fixed default room so two people who click "à plusieurs" land together
// without typing anything.
const SHARED_ROOM = "lisael";
const name = useRef();
const room = useRef();
function autoName() {
return `Constructeur ${Math.floor(Math.random() * 900) + 100}`;
}
export const JoinRoomScreen = ({ setRoomId }) => {
const setSelfName = useStore((state) => state.setSelfName);
const [showPerso, setShowPerso] = useState(false);
const persoRoom = useRef("");
const submit = (e) => {
e.preventDefault();
setSelfName(name.current);
setRoomId(room.current);
const join = (roomId) => {
setSelfName(autoName());
setRoomId(roomId);
};
const playSolo = () => join(`solo-${uID(6)}`);
const playTogether = () => join(SHARED_ROOM);
const joinPerso = () => {
const r = (persoRoom.current || "").trim();
join(r ? `perso-${r}` : `solo-${uID(6)}`);
};
return (
<Form.Root ref={form} className="FormRoot" onSubmit={submit}>
<Form.Field className="FormField" name="email">
<div
style={{
display: "flex",
alignItems: "baseline",
justifyContent: "space-between",
}}
>
<Form.Label className="FormLabel">Name</Form.Label>
<Form.Message className="FormMessage" match="valueMissing">
Please enter your Name
</Form.Message>
</div>
<Form.Control asChild>
<input
className="Input"
type="text"
required
onChange={(e) => {
name.current = e.target.value;
}}
/>
</Form.Control>
</Form.Field>
<Form.Field className="FormField" name="question">
<div
style={{
display: "flex",
alignItems: "baseline",
justifyContent: "space-between",
}}
>
<Form.Label className="FormLabel">Room ID</Form.Label>
<Form.Message className="FormMessage" match="valueMissing">
Please enter a room ID
</Form.Message>
</div>
<Form.Control asChild>
<input
className="Input"
type="text"
required
onChange={(e) => {
room.current = e.target.value;
}}
/>
</Form.Control>
</Form.Field>
<Form.Submit asChild>
<button className="FormButton" style={{ marginTop: 10 }}>
Enter In Room
</button>
</Form.Submit>
</Form.Root>
);
};
<div className="JoinScreen">
<div className="JoinCard">
<h1 className="JoinTitle">🧱 Blocs Pro</h1>
<p className="JoinSubtitle">On construit ?</p>
export const JoinRoomScreen = ({ setRoomId }) => {
return (
<div
style={{
width: "100vw",
height: "100vh",
boxSizing: "border-box",
margin: 0,
padding: 0,
background: "black",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<JoinRoomForm setRoomId={setRoomId} />
<button className="BigButton solo" onClick={playSolo}>
<span className="BigButtonEmoji">👷</span>
Construire tout seul
</button>
<button className="BigButton together" onClick={playTogether}>
<span className="BigButtonEmoji">👫</span>
Construire à plusieurs
</button>
<button
className="PersoToggle"
onClick={() => setShowPerso((v) => !v)}
>
{showPerso ? "▾ Cacher" : "▸ Salle perso (pour les grands)"}
</button>
{showPerso && (
<div className="PersoBox">
<input
className="PersoInput"
type="text"
placeholder="Nom de la salle"
onChange={(e) => {
persoRoom.current = e.target.value;
}}
/>
<button className="PersoGo" onClick={joinPerso}>
Entrer
</button>
</div>
)}
</div>
</div>
);
};
+131 -86
View File
@@ -1,95 +1,140 @@
@import "@radix-ui/colors/black-alpha.css";
@import "@radix-ui/colors/violet.css";
@import "@radix-ui/colors/mauve.css";
/* Kid-friendly, keyboard-free room entry screen. */
/* reset */
input,
textarea,
button {
.JoinScreen {
width: 100vw;
height: 100vh;
box-sizing: border-box;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(160deg, #2b6cff 0%, #7b3ff2 100%);
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}
.JoinCard {
width: min(92vw, 420px);
display: flex;
flex-direction: column;
align-items: center;
gap: 18px;
padding: 32px 28px;
border-radius: 28px;
background: #ffffff;
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.35);
}
.JoinTitle {
margin: 0;
font-size: 2.2rem;
font-weight: 800;
color: #1b1b2b;
text-align: center;
}
.JoinSubtitle {
margin: 0 0 6px;
font-size: 1.15rem;
font-weight: 600;
color: #6b6b80;
text-align: center;
}
.BigButton {
all: unset;
box-sizing: border-box;
}
.FormRoot {
width: 260px;
}
.FormField {
display: grid;
margin-bottom: 10px;
}
.FormLabel {
font-size: 14px;
font-weight: 500;
line-height: 35px;
color: white;
}
.FormMessage {
font-size: 13px;
color: white;
opacity: 0.8;
}
.Input,
.Textarea {
width: 100%;
display: inline-flex;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 4px;
font-size: 15px;
color: white;
background-color: gray;
box-shadow: 0 0 0 1px var(--black-a9);
}
.Input:hover,
.Textarea:hover {
box-shadow: 0 0 0 1px white;
}
.Input:focus,
.Textarea:focus {
box-shadow: 0 0 0 2px white;
}
.Input::selection,
.Textarea::selection {
background-color: var(--black-a9);
color: white;
}
.Input {
padding: 0 10px;
height: 35px;
line-height: 1;
}
.Textarea {
resize: none;
padding: 10px;
}
.FormButton {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 4px;
padding: 0 15px;
font-size: 15px;
line-height: 1;
font-weight: 500;
height: 35px;
width: 100%;
background-color: white;
color: black;
box-shadow: 0 2px 10px var(--black-a7);
}
.FormButton:hover {
background-color: var(--mauve-3);
gap: 6px;
padding: 22px 18px;
border-radius: 22px;
font-size: 1.35rem;
font-weight: 800;
color: #ffffff;
text-align: center;
cursor: pointer;
user-select: none;
transition: transform 0.12s ease, box-shadow 0.12s ease;
box-shadow: 0 8px 0 rgba(0, 0, 0, 0.18);
}
.FormButton:focus {
box-shadow: 0 0 0 2px black;
.BigButtonEmoji {
font-size: 2.6rem;
line-height: 1;
}
.BigButton:hover {
transform: translateY(-2px);
}
.BigButton:active {
transform: translateY(4px);
box-shadow: 0 2px 0 rgba(0, 0, 0, 0.18);
}
.BigButton.solo {
background: #ff8a18;
}
.BigButton.together {
background: #2ca84a;
}
.PersoToggle {
all: unset;
box-sizing: border-box;
cursor: pointer;
margin-top: 4px;
font-size: 0.85rem;
font-weight: 600;
color: #9a9ab0;
}
.PersoToggle:hover {
color: #6b6b80;
}
.PersoBox {
display: flex;
width: 100%;
gap: 8px;
}
.PersoInput {
all: unset;
box-sizing: border-box;
flex: 1;
height: 40px;
padding: 0 12px;
border-radius: 12px;
font-size: 1rem;
color: #1b1b2b;
background: #f0f0f6;
box-shadow: inset 0 0 0 2px #d8d8e4;
}
.PersoInput:focus {
box-shadow: inset 0 0 0 2px #7b3ff2;
}
.PersoGo {
all: unset;
box-sizing: border-box;
cursor: pointer;
height: 40px;
padding: 0 18px;
border-radius: 12px;
font-weight: 700;
color: #ffffff;
background: #2b6cff;
display: inline-flex;
align-items: center;
}
.PersoGo:hover {
background: #1f57d6;
}
+4 -4
View File
@@ -18,18 +18,18 @@ export const ModeToggleBar = () => {
<ToggleGroup.Item
className="ToggleGroupItem"
value={CREATE_MODE}
aria-label={CREATE_MODE}
aria-label="Créer"
onClick={() => setMode(CREATE_MODE)}
>
{CREATE_MODE}
🧱 Créer
</ToggleGroup.Item>
<ToggleGroup.Item
className="ToggleGroupItem"
value={EDIT_MODE}
aria-label={EDIT_MODE}
aria-label="Modifier"
onClick={() => setMode(EDIT_MODE)}
>
{EDIT_MODE}
Modifier
</ToggleGroup.Item>
</ToggleGroup.Root>
);
+3 -10
View File
@@ -6,16 +6,9 @@ import { CheckIcon } from "@radix-ui/react-icons";
import "./styles.css";
import { useStore } from "../../../../store";
const stateMap = {
rotate: "rotate",
};
const setterMap = {
rotate: "setRotate",
};
export const Checkbox = ({ label = "rotate" }) => {
const setValue = useStore((state) => state[setterMap[label]]);
export const Checkbox = ({ label = "Tourner" }) => {
// Single toggle: rotate the active piece 90° before placing.
const setValue = useStore((state) => state.setRotate);
return (
<div
+1 -6
View File
@@ -2,18 +2,13 @@
/* eslint-disable no-unused-vars */
import React from "react";
import "./styles.css";
import { SliderWithLabel } from "./Slider";
import { Checkbox } from "./Checkbox";
import { ColorInput } from "./ColorInput";
export const Panel = () => {
return (
<div className="Panel">
<SliderWithLabel label="width" max={5} />
<SliderWithLabel label="depth" max={5} />
<SliderWithLabel label="anchor X" min={-2} defaultValue={0} max={2} />
<SliderWithLabel label="anchor Z" min={-2} defaultValue={0} max={2} />
<Checkbox />
<Checkbox label="Tourner" />
<ColorInput />
</div>
);
+1 -1
View File
@@ -78,7 +78,7 @@ export const PopoverPeopleList = () => {
color: "rgba(0, 0, 0, 0.5)",
}}
>
No one else in room
Personne d'autre ici
</h3>
)}
{others.map((user) => {