onCommand("bold")}>B
diff --git a/website/src/features/toolboxes/modules/RandomizerModule.jsx b/website/src/features/toolboxes/modules/RandomizerModule.jsx
index 0ff93e5..59ae099 100644
--- a/website/src/features/toolboxes/modules/RandomizerModule.jsx
+++ b/website/src/features/toolboxes/modules/RandomizerModule.jsx
@@ -1,71 +1,10 @@
-// Rôle : fournit l'outil Randomizer avec dé, roue pondérée et équipes équilibrées.
-import { useEffect, useMemo, useRef, useState } from "react";
-import { CompactDropdown } from "../../../components/CompactDropdown.jsx";
-import { Icon } from "../../../components/Icon.jsx";
+// Rôle : orchestre les onglets de l'outil Randomizer sans porter les sous-vues.
import { Tabs } from "../../../components/Tabs.jsx";
-import { useDraftForm } from "../../../hooks/useDraftForm.js";
import { TextExchangeActions } from "./TextExchangeActions.jsx";
-import {
- RANDOMIZER_LIMITS,
- RANDOMIZER_TABS,
- addRandomizerHistory,
- buildWheelSegments,
- clampNumber,
- createBalancedTeams,
- drawWeightedEntries,
- filterExclusionsForPlayers,
- getDiceShape,
- rollDice
-} from "./randomizerUtils.js";
-
-const WHEEL_COLORS = ["#8b5cf6", "#f6c453", "#22d3ee", "#d946ef", "#6366f1", "#22c55e"];
-const DRAW_PAUSE_MS = 500;
-const DICE_ROLL_DELAYS = [62, 70, 82, 98, 120, 148, 182, 224, 276, 338, 410];
-const TEAM_REEL_DELAYS = [58, 66, 78, 94, 116, 144, 180, 226, 286, 360];
-const WHEEL_SPIN_MS = 1120;
-const DEFAULT_EXCLUSION_DRAFT = { playerAId: "", playerBIds: [], mode: "group" };
-
-function hasReducedMotion() {
- return window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches;
-}
-
-function polarToCartesian(radius, angle) {
- const radians = (angle * Math.PI) / 180;
- return {
- x: 50 + radius * Math.cos(radians),
- y: 50 + radius * Math.sin(radians)
- };
-}
-
-function describeWheelArc(startAngle, endAngle) {
- const start = polarToCartesian(48, startAngle);
- const end = polarToCartesian(48, endAngle);
- const largeArc = endAngle - startAngle > 180 ? 1 : 0;
- return `M 50 50 L ${start.x} ${start.y} A 48 48 0 ${largeArc} 1 ${end.x} ${end.y} Z`;
-}
-
-function formatDateTime(value) {
- const date = new Date(value);
- return Number.isFinite(date.getTime()) ? date.toLocaleString("fr-FR", { dateStyle: "short", timeStyle: "short" }) : "";
-}
-
-function getWheelTargetRotation(currentRotation, targetAngle) {
- const pointerAngle = -90;
- const targetRotation = pointerAngle - targetAngle;
- const currentModulo = ((currentRotation % 360) + 360) % 360;
- const targetModulo = ((targetRotation % 360) + 360) % 360;
- const delta = (targetModulo - currentModulo + 360) % 360;
- return currentRotation + 1080 + delta;
-}
-
-function pickRandomItems(items, count) {
- const nextItems = [...items];
- for (let index = nextItems.length - 1; index > 0; index -= 1) {
- const targetIndex = Math.floor(Math.random() * (index + 1));
- [nextItems[index], nextItems[targetIndex]] = [nextItems[targetIndex], nextItems[index]];
- }
- return nextItems.slice(0, count);
-}
+import { RANDOMIZER_TABS } from "./randomizerUtils.js";
+import { DiceRandomizer } from "./randomizer/RandomizerDice.jsx";
+import { TeamsRandomizer } from "./randomizer/RandomizerTeams.jsx";
+import { WheelRandomizer } from "./randomizer/RandomizerWheel.jsx";
export function RandomizerModule({ toolboxId, moduleId, context, textExchangeOpen }) {
const data = context.normalizeRandomizerData(context.getModuleData(toolboxId, moduleId, {}));
@@ -101,773 +40,3 @@ export function RandomizerModule({ toolboxId, moduleId, context, textExchangeOpe
>
);
}
-
-function DiceRandomizer({ data, textContent, onChange, context }) {
- const [displayValue, setDisplayValue] = useState(data.results[0] || 1);
- const [displayResults, setDisplayResults] = useState(data.results);
- const [rolling, setRolling] = useState(false);
- const [rollingIndex, setRollingIndex] = useState(-1);
- const [rollDuration, setRollDuration] = useState(DICE_ROLL_DELAYS[0]);
- const timersRef = useRef([]);
- const skipDiceSyncRef = useRef(false);
- const shape = getDiceShape(data.faces);
-
- useEffect(() => {
- if (rolling) return;
- if (skipDiceSyncRef.current) {
- skipDiceSyncRef.current = false;
- return;
- }
- setDisplayValue(data.results[0] || 1);
- setDisplayResults(data.results);
- }, [data.results, rolling]);
-
- useEffect(() => () => {
- timersRef.current.forEach((timerId) => window.clearTimeout(timerId));
- timersRef.current = [];
- }, []);
-
- function queueTimer(callback, delay) {
- const timerId = window.setTimeout(callback, delay);
- timersRef.current.push(timerId);
- return timerId;
- }
-
- function clearDiceTimers() {
- timersRef.current.forEach((timerId) => window.clearTimeout(timerId));
- timersRef.current = [];
- }
-
- function stopDice() {
- clearDiceTimers();
- skipDiceSyncRef.current = true;
- setRolling(false);
- setRollingIndex(-1);
- }
-
- function updateDiceField(field, value) {
- const limitMin = field === "faces" ? RANDOMIZER_LIMITS.diceFacesMin : RANDOMIZER_LIMITS.diceRollsMin;
- const limitMax = field === "faces" ? RANDOMIZER_LIMITS.diceFacesMax : RANDOMIZER_LIMITS.diceRollsMax;
- onChange({ ...data, [field]: clampNumber(value, limitMin, limitMax, data[field]) });
- }
-
- function launchDice() {
- clearDiceTimers();
- skipDiceSyncRef.current = false;
- const results = rollDice(data.faces, data.rolls);
- const historyEntry = { id: context.uid("randomizer"), at: new Date().toISOString(), results };
- if (hasReducedMotion()) {
- onChange({ ...data, results, history: addRandomizerHistory(data.history, historyEntry) });
- return;
- }
- setRolling(true);
- setRollingIndex(0);
- setDisplayResults([]);
-
- function animateRoll(index) {
- let ticks = 0;
- const tick = () => {
- ticks += 1;
- const isFinalTick = ticks >= DICE_ROLL_DELAYS.length;
- const tickDelay = DICE_ROLL_DELAYS[Math.min(ticks, DICE_ROLL_DELAYS.length - 1)];
- setDisplayValue(isFinalTick ? results[index] : rollDice(data.faces, 1)[0]);
- setRollDuration(Math.max(54, Math.round(tickDelay * 0.86)));
- if (!isFinalTick) {
- queueTimer(tick, tickDelay);
- return;
- }
- setDisplayResults((current) => [...current, results[index]]);
- if (index < results.length - 1) {
- setRollingIndex(index + 1);
- queueTimer(() => animateRoll(index + 1), DRAW_PAUSE_MS);
- return;
- }
- setRolling(false);
- setRollingIndex(-1);
- onChange({ ...data, results, history: addRandomizerHistory(data.history, historyEntry) });
- };
- tick();
- }
-
- animateRoll(0);
- }
-
- function clearDiceHistory() {
- onChange({ ...data, history: [] });
- }
-
- function removeDiceHistoryEntry(entryId) {
- onChange({ ...data, history: data.history.filter((entry) => entry.id !== entryId) });
- }
-
- return (
-
-
-
-
-
-
`${entry.results.join(" - ")} · ${textContent.totalLabel || "Total"} ${entry.results.reduce((sum, result) => sum + result, 0)}`}
- onClear={clearDiceHistory}
- onRemove={removeDiceHistoryEntry}
- disabled={rolling}
- textContent={textContent}
- showEmpty
- />
-
-
- );
-}
-
-function RandomizerDie({ shape, rolling, value, duration, faces }) {
- const cleanFaces = clampNumber(faces, RANDOMIZER_LIMITS.diceFacesMin, RANDOMIZER_LIMITS.diceFacesMax, 6);
- const valueIndex = clampNumber(value, 1, cleanFaces, 1) - 1;
- const values = useMemo(() => Array.from({ length: cleanFaces }, (_, index) => index + 1), [cleanFaces]);
-
- return (
-
-
- {shape === "circle" && }
- {shape === "triangle" && }
- {shape === "square" && }
- {shape === "hexagon" && }
-
-
-
- {values.map((item) => {item} )}
-
- {value}
-
-
- );
-}
-
-function WheelRandomizer({ data, textContent, onChange, context }) {
- const [rotation, setRotation] = useState(0);
- const [spinning, setSpinning] = useState(false);
- const [drawing, setDrawing] = useState(false);
- const [displayResults, setDisplayResults] = useState(data.results);
- const [entriesText, setEntriesText] = useState("");
- const timersRef = useRef([]);
- const skipWheelSyncRef = useRef(false);
- const segments = useMemo(() => buildWheelSegments(data.entries), [data.entries]);
-
- useEffect(() => {
- if (drawing) return;
- if (skipWheelSyncRef.current) {
- skipWheelSyncRef.current = false;
- return;
- }
- setDisplayResults(data.results);
- }, [data.results, drawing]);
-
- useEffect(() => () => {
- timersRef.current.forEach((timerId) => window.clearTimeout(timerId));
- timersRef.current = [];
- }, []);
-
- function queueTimer(callback, delay) {
- const timerId = window.setTimeout(callback, delay);
- timersRef.current.push(timerId);
- return timerId;
- }
-
- function clearWheelTimers() {
- timersRef.current.forEach((timerId) => window.clearTimeout(timerId));
- timersRef.current = [];
- }
-
- function stopWheel() {
- clearWheelTimers();
- skipWheelSyncRef.current = true;
- setSpinning(false);
- setDrawing(false);
- }
-
- function addEntries(event) {
- event.preventDefault();
- const availableSlots = RANDOMIZER_LIMITS.wheelEntriesMax - data.entries.length;
- const labels = entriesText
- .split(/\r?\n/)
- .map((label) => label.trim())
- .filter(Boolean)
- .slice(0, availableSlots);
- if (!labels.length) return;
- onChange({
- ...data,
- entries: [
- ...data.entries,
- ...labels.map((label) => ({ id: context.uid("randomizer"), label, weight: 1 }))
- ]
- });
- setEntriesText("");
- }
-
- function updateEntry(entryId, patch) {
- onChange({ ...data, entries: data.entries.map((entry) => entry.id === entryId ? { ...entry, ...patch } : entry) });
- }
-
- function removeEntry(entryId) {
- onChange({ ...data, entries: data.entries.filter((entry) => entry.id !== entryId) });
- }
-
- function clearEntries() {
- onChange({ ...data, entries: [], results: [] });
- }
-
- function clearWheelHistory() {
- onChange({ ...data, history: [] });
- }
-
- function removeWheelHistoryEntry(entryId) {
- onChange({ ...data, history: data.history.filter((entry) => entry.id !== entryId) });
- }
-
- function launchWheel() {
- clearWheelTimers();
- skipWheelSyncRef.current = false;
- const drawnEntries = drawWeightedEntries(data.entries, data.draws);
- if (!drawnEntries.length) {
- context.notify?.(textContent.wheelInvalid || "Ajoutez au moins deux valeurs.");
- return;
- }
- const results = drawnEntries.map((entry) => entry.label);
- const historyEntry = { id: context.uid("randomizer"), at: new Date().toISOString(), results };
- if (hasReducedMotion()) {
- setDisplayResults(results);
- onChange({ ...data, results, history: addRandomizerHistory(data.history, historyEntry) });
- return;
- }
- setDrawing(true);
- setDisplayResults([]);
- let rotationCursor = rotation;
-
- function animateDraw(index) {
- const drawnEntry = drawnEntries[index];
- const winnerSegment = segments.find((segment) => segment.entry.id === drawnEntry.id) || segments[0];
- rotationCursor = getWheelTargetRotation(rotationCursor, winnerSegment.midAngle);
- setSpinning(true);
- setRotation(rotationCursor);
- queueTimer(() => {
- setDisplayResults((current) => [...current, drawnEntry.label]);
- setSpinning(false);
- if (index < drawnEntries.length - 1) {
- queueTimer(() => animateDraw(index + 1), DRAW_PAUSE_MS);
- return;
- }
- setDrawing(false);
- onChange({ ...data, results, history: addRandomizerHistory(data.history, historyEntry) });
- }, WHEEL_SPIN_MS);
- }
-
- animateDraw(0);
- }
-
- return (
-
-
-
-
-
-
- {segments.length ? segments.map((segment) => (
-
- )) : }
-
-
-
-
- {segments.length ? segments.map((segment) => (
-
-
- {segment.entry.label}
-
- )) :
{textContent.wheelInvalid || "Ajoutez au moins deux valeurs."}
}
-
-
-
-
-
-
-
-
-
-
- {textContent.savedValuesLabel || "Valeurs enregistrées"}
- {textContent.weightLabel || "Poids"}
-
-
-
- {textContent.clearValuesButton || "Supprimer toutes les valeurs"}
-
-
entry.results.join(" - ")}
- onClear={clearWheelHistory}
- onRemove={removeWheelHistoryEntry}
- disabled={drawing}
- textContent={textContent}
- showEmpty
- />
-
-
- );
-}
-
-function TeamsRandomizer({ data, textContent, onChange, context }) {
- const exclusionDraft = useDraftForm(DEFAULT_EXCLUSION_DRAFT);
- const [animating, setAnimating] = useState(false);
- const [animationTeams, setAnimationTeams] = useState([]);
- const [reelPlayers, setReelPlayers] = useState([]);
- const [reelIndex, setReelIndex] = useState(0);
- const [reelDuration, setReelDuration] = useState(TEAM_REEL_DELAYS[0]);
- const [namesText, setNamesText] = useState("");
- const timersRef = useRef([]);
- const activePlayers = data.players.filter((player) => player.active);
- const storedSelectionCount = clampNumber(data.playerSelectionCount, data.teamCount, RANDOMIZER_LIMITS.teamPlayersMax, data.teamCount);
- const [selectionCountDraft, setSelectionCountDraft] = useState(String(storedSelectionCount));
- const selectionCount = clampNumber(selectionCountDraft, data.teamCount, RANDOMIZER_LIMITS.teamPlayersMax, storedSelectionCount);
- const displayedTeams = animating ? animationTeams : data.teams;
- const exclusionMode = exclusionDraft.values.mode === "source" ? "source" : "group";
-
- useEffect(() => {
- setSelectionCountDraft(String(storedSelectionCount));
- }, [storedSelectionCount]);
-
- useEffect(() => () => {
- timersRef.current.forEach((timerId) => window.clearTimeout(timerId));
- timersRef.current = [];
- }, []);
-
- function queueTimer(callback, delay) {
- const timerId = window.setTimeout(callback, delay);
- timersRef.current.push(timerId);
- return timerId;
- }
-
- function clearTeamTimers() {
- timersRef.current.forEach((timerId) => window.clearTimeout(timerId));
- timersRef.current = [];
- }
-
- function commitSelectionCount(value = selectionCountDraft) {
- const nextSelectionCount = clampNumber(value, data.teamCount, RANDOMIZER_LIMITS.teamPlayersMax, storedSelectionCount);
- setSelectionCountDraft(String(nextSelectionCount));
- onChange({ ...data, playerSelectionCount: nextSelectionCount });
- }
-
- function importPlayers(event) {
- event.preventDefault();
- const availableSlots = RANDOMIZER_LIMITS.teamPlayersMax - data.players.length;
- const names = namesText
- .split(/\r?\n/)
- .map((name) => name.trim())
- .filter(Boolean)
- .slice(0, availableSlots);
- if (!names.length) return;
- onChange({
- ...data,
- players: [
- ...data.players,
- ...names.map((name) => ({ id: context.uid("randomizer"), name, score: 0, active: true }))
- ]
- });
- setNamesText("");
- }
-
- function updatePlayer(playerId, patch) {
- onChange({ ...data, players: data.players.map((player) => player.id === playerId ? { ...player, ...patch } : player) });
- }
-
- function removePlayer(playerId) {
- onChange({
- ...data,
- players: data.players.filter((player) => player.id !== playerId),
- exclusions: data.exclusions
- .filter((exclusion) => exclusion.playerAId !== playerId)
- .map((exclusion) => ({ ...exclusion, playerBIds: (exclusion.playerBIds || []).filter((targetId) => targetId !== playerId) }))
- .filter((exclusion) => exclusion.playerBIds.length)
- });
- }
-
- function clearPlayers() {
- onChange({ ...data, players: [], exclusions: [], teams: [] });
- }
-
- function toggleExclusionTarget(playerId) {
- const currentIds = exclusionDraft.values.playerBIds || [];
- exclusionDraft.setField(
- "playerBIds",
- currentIds.includes(playerId) ? currentIds.filter((targetId) => targetId !== playerId) : [...currentIds, playerId]
- );
- }
-
- function addExclusion(event) {
- exclusionDraft.handleSubmit(event, (draft, { reset }) => {
- const mode = draft.mode === "source" ? "source" : "group";
- const selectedIds = [...new Set(Array.isArray(draft.playerBIds) ? draft.playerBIds.filter(Boolean) : [])];
- const playerAId = mode === "group" ? selectedIds[0] : draft.playerAId;
- const playerBIds = mode === "group" ? selectedIds.slice(1) : selectedIds.filter((playerId) => playerId !== draft.playerAId);
- if (!playerAId || !playerBIds.length) return;
- const exists = data.exclusions.some((exclusion) => exclusion.playerAId === playerAId && (exclusion.mode || "source") === mode && [...(exclusion.playerBIds || [])].sort().join(":") === [...playerBIds].sort().join(":"));
- if (exists) return;
- onChange({ ...data, exclusions: [...data.exclusions, { id: context.uid("randomizer"), playerAId, playerBIds, mode }] });
- reset();
- });
- }
-
- function launchTeams() {
- clearTeamTimers();
- const selectedPlayers = data.playerSelectionMode === "count" ? pickRandomItems(activePlayers, Math.min(selectionCount, activePlayers.length)) : activePlayers;
- const selectedExclusions = filterExclusionsForPlayers(data.exclusions, selectedPlayers);
- const result = createBalancedTeams(selectedPlayers, data.teamCount, selectedExclusions);
- if (!result.ok) {
- context.notify?.(result.error === "not_enough_players" ? textContent.notEnoughPlayers || "Pas assez de joueurs actifs." : textContent.constraintsImpossible || "Contraintes impossibles à satisfaire.");
- return;
- }
- const historyEntry = { id: context.uid("randomizer"), at: new Date().toISOString(), teams: result.teams };
- if (hasReducedMotion()) {
- onChange({ ...data, teams: result.teams, history: addRandomizerHistory(data.history, historyEntry) });
- return;
- }
- const emptyTeams = result.teams.map((team) => ({ ...team, score: 0, players: [] }));
- const assignments = result.teams.flatMap((team) => team.players.map((player) => ({ teamId: team.id, player })));
- setAnimating(true);
- setAnimationTeams(emptyTeams);
- setReelPlayers(selectedPlayers);
- setReelIndex(0);
- setReelDuration(TEAM_REEL_DELAYS[0]);
-
- function animateAssignment(index) {
- const assignment = assignments[index];
- if (!assignment) {
- setAnimating(false);
- setReelPlayers([]);
- setReelIndex(0);
- onChange({ ...data, teams: result.teams, history: addRandomizerHistory(data.history, historyEntry) });
- return;
- }
-
- let ticks = 0;
- const tick = () => {
- ticks += 1;
- const isFinalTick = ticks >= TEAM_REEL_DELAYS.length;
- const tickDelay = TEAM_REEL_DELAYS[Math.min(ticks, TEAM_REEL_DELAYS.length - 1)];
- const nextIndex = isFinalTick
- ? Math.max(0, selectedPlayers.findIndex((player) => player.id === assignment.player.id))
- : Math.floor(Math.random() * selectedPlayers.length);
- setReelIndex(nextIndex);
- setReelDuration(Math.max(54, Math.round(tickDelay * 0.86)));
- if (!isFinalTick) {
- queueTimer(tick, tickDelay);
- return;
- }
- queueTimer(() => {
- setAnimationTeams((currentTeams) => currentTeams.map((team) => {
- if (team.id !== assignment.teamId) return team;
- return {
- ...team,
- score: team.score + assignment.player.score,
- players: [...team.players, { ...assignment.player, arriving: true }]
- };
- }));
- queueTimer(() => animateAssignment(index + 1), 260);
- }, 220);
- };
- tick();
- }
-
- animateAssignment(0);
- }
-
- function clearTeamsHistory() {
- onChange({ ...data, history: [] });
- }
-
- function removeTeamsHistoryEntry(entryId) {
- onChange({ ...data, history: data.history.filter((entry) => entry.id !== entryId) });
- }
-
- return (
-
-
-
- {reelPlayers.length ? (
- <>
-
- {reelPlayers.map((player) => {player.name} )}
-
-
{reelPlayers[reelIndex]?.name || ""}
- >
- ) :
}
-
-
- {displayedTeams.length ? displayedTeams.map((team) => (
-
- {team.name}
- {textContent.teamScoreLabel || "Score"} {team.score}
- {team.players.map((player) => {player.name} )}
-
- )) :
{textContent.emptyTeams ?? ""}
}
-
-
-
-
-
-
-
-
-
- {textContent.activeLabel || "Actif"}
- {textContent.nameLabel || "Nom"}
- {textContent.scoreLabel || "Score"}
-
-
-
- {textContent.clearNamesButton || "Supprimer tous les noms"}
-
-
-
-
-
- `${entry.teams.length} ${entry.teams.length > 1 ? textContent.teamsPluralLabel || "équipes" : textContent.teamsSingularLabel || "équipe"}`}
- renderDetails={(entry) => (
- 4 ? "has-team-rows" : ""}`}>
- {entry.teams.map((team) => (
-
-
{team.name}
-
{textContent.teamScoreLabel || "Score"} {team.score}
-
- {team.players.map((player) => {player.name} )}
-
-
- ))}
-
- )}
- onClear={clearTeamsHistory}
- onRemove={removeTeamsHistoryEntry}
- disabled={animating}
- textContent={textContent}
- showEmpty
- />
-
- );
-}
-
-function ResultChips({ results, onRemove, disabled = false, textContent = {} }) {
- if (!results?.length) return null;
- return (
-
- {results.map((result, index) => (
-
- {result}
- {onRemove && (
- onRemove(index)} disabled={disabled} aria-label={`${textContent.deleteTitle || "Supprimer"} ${result}`}>
-
-
- )}
-
- ))}
-
- );
-}
-
-function RandomizerHistory({ title, history, renderEntry, renderDetails, onClear, onRemove, disabled = false, textContent = {}, showEmpty = false }) {
- const hasHistory = Boolean(history?.length);
- if (!hasHistory && !showEmpty) return null;
- return (
-
-
- {title}
- {textContent.clearHistoryButton || "Effacer tout"}
-
- {hasHistory ? (
-
- {history.map((entry, index) => (
-
- {renderDetails ? (
-
-
-
- {renderEntry(entry)}
- {formatDateTime(entry.at)}
-
- {renderDetails(entry)}
-
- ) : (
-
- {renderEntry(entry)}
- {formatDateTime(entry.at)}
-
- )}
- {onRemove && (
-
onRemove(entry.id)} disabled={disabled} aria-label={`${textContent.deleteTitle || "Supprimer"} ${formatDateTime(entry.at)}`}>
-
-
- )}
-
- ))}
-
- ) : {textContent.emptyHistory || "Aucun historique."}
}
-
- );
-}
diff --git a/website/src/features/toolboxes/modules/randomizer/RandomizerDice.jsx b/website/src/features/toolboxes/modules/randomizer/RandomizerDice.jsx
new file mode 100644
index 0000000..d65ad4d
--- /dev/null
+++ b/website/src/features/toolboxes/modules/randomizer/RandomizerDice.jsx
@@ -0,0 +1,168 @@
+// Rôle : gère l'onglet dé du Randomizer et son animation.
+import { useEffect, useMemo, useRef, useState } from "react";
+import { Icon } from "../../../../components/Icon.jsx";
+import { RANDOMIZER_LIMITS, addRandomizerHistory, clampNumber, getDiceShape, rollDice } from "../randomizerUtils.js";
+import { ResultChips, RandomizerHistory } from "./RandomizerShared.jsx";
+import { DICE_ROLL_DELAYS, DRAW_PAUSE_MS, hasReducedMotion } from "./randomizerUiUtils.js";
+
+export function DiceRandomizer({ data, textContent, onChange, context }) {
+ const [displayValue, setDisplayValue] = useState(data.results[0] || 1);
+ const [displayResults, setDisplayResults] = useState(data.results);
+ const [rolling, setRolling] = useState(false);
+ const [rollingIndex, setRollingIndex] = useState(-1);
+ const [rollDuration, setRollDuration] = useState(DICE_ROLL_DELAYS[0]);
+ const timersRef = useRef([]);
+ const skipDiceSyncRef = useRef(false);
+ const shape = getDiceShape(data.faces);
+
+ useEffect(() => {
+ if (rolling) return;
+ if (skipDiceSyncRef.current) {
+ skipDiceSyncRef.current = false;
+ return;
+ }
+ setDisplayValue(data.results[0] || 1);
+ setDisplayResults(data.results);
+ }, [data.results, rolling]);
+
+ useEffect(() => () => {
+ timersRef.current.forEach((timerId) => window.clearTimeout(timerId));
+ timersRef.current = [];
+ }, []);
+
+ function queueTimer(callback, delay) {
+ const timerId = window.setTimeout(callback, delay);
+ timersRef.current.push(timerId);
+ return timerId;
+ }
+
+ function clearDiceTimers() {
+ timersRef.current.forEach((timerId) => window.clearTimeout(timerId));
+ timersRef.current = [];
+ }
+
+ function stopDice() {
+ clearDiceTimers();
+ skipDiceSyncRef.current = true;
+ setRolling(false);
+ setRollingIndex(-1);
+ }
+
+ function updateDiceField(field, value) {
+ const limitMin = field === "faces" ? RANDOMIZER_LIMITS.diceFacesMin : RANDOMIZER_LIMITS.diceRollsMin;
+ const limitMax = field === "faces" ? RANDOMIZER_LIMITS.diceFacesMax : RANDOMIZER_LIMITS.diceRollsMax;
+ onChange({ ...data, [field]: clampNumber(value, limitMin, limitMax, data[field]) });
+ }
+
+ function launchDice() {
+ clearDiceTimers();
+ skipDiceSyncRef.current = false;
+ const results = rollDice(data.faces, data.rolls);
+ const historyEntry = { id: context.uid("randomizer"), at: new Date().toISOString(), results };
+ if (hasReducedMotion()) {
+ onChange({ ...data, results, history: addRandomizerHistory(data.history, historyEntry) });
+ return;
+ }
+ setRolling(true);
+ setRollingIndex(0);
+ setDisplayResults([]);
+
+ function animateRoll(index) {
+ let ticks = 0;
+ const tick = () => {
+ ticks += 1;
+ const isFinalTick = ticks >= DICE_ROLL_DELAYS.length;
+ const tickDelay = DICE_ROLL_DELAYS[Math.min(ticks, DICE_ROLL_DELAYS.length - 1)];
+ setDisplayValue(isFinalTick ? results[index] : rollDice(data.faces, 1)[0]);
+ setRollDuration(Math.max(54, Math.round(tickDelay * 0.86)));
+ if (!isFinalTick) {
+ queueTimer(tick, tickDelay);
+ return;
+ }
+ setDisplayResults((current) => [...current, results[index]]);
+ if (index < results.length - 1) {
+ setRollingIndex(index + 1);
+ queueTimer(() => animateRoll(index + 1), DRAW_PAUSE_MS);
+ return;
+ }
+ setRolling(false);
+ setRollingIndex(-1);
+ onChange({ ...data, results, history: addRandomizerHistory(data.history, historyEntry) });
+ };
+ tick();
+ }
+
+ animateRoll(0);
+ }
+
+ function clearDiceHistory() {
+ onChange({ ...data, history: [] });
+ }
+
+ function removeDiceHistoryEntry(entryId) {
+ onChange({ ...data, history: data.history.filter((entry) => entry.id !== entryId) });
+ }
+
+ return (
+
+
+
+
+
+
`${entry.results.join(" - ")} · ${textContent.totalLabel || "Total"} ${entry.results.reduce((sum, result) => sum + result, 0)}`}
+ onClear={clearDiceHistory}
+ onRemove={removeDiceHistoryEntry}
+ disabled={rolling}
+ textContent={textContent}
+ showEmpty
+ />
+
+
+ );
+}
+
+function RandomizerDie({ shape, rolling, value, duration, faces }) {
+ const cleanFaces = clampNumber(faces, RANDOMIZER_LIMITS.diceFacesMin, RANDOMIZER_LIMITS.diceFacesMax, 6);
+ const valueIndex = clampNumber(value, 1, cleanFaces, 1) - 1;
+ const values = useMemo(() => Array.from({ length: cleanFaces }, (_, index) => index + 1), [cleanFaces]);
+
+ return (
+
+
+ {shape === "circle" && }
+ {shape === "triangle" && }
+ {shape === "square" && }
+ {shape === "hexagon" && }
+
+
+
+ {values.map((item) => {item} )}
+
+ {value}
+
+
+ );
+}
diff --git a/website/src/features/toolboxes/modules/randomizer/RandomizerShared.jsx b/website/src/features/toolboxes/modules/randomizer/RandomizerShared.jsx
new file mode 100644
index 0000000..fd6d815
--- /dev/null
+++ b/website/src/features/toolboxes/modules/randomizer/RandomizerShared.jsx
@@ -0,0 +1,62 @@
+// Rôle : fournit les composants partagés de résultats et historique du Randomizer.
+import { Icon } from "../../../../components/Icon.jsx";
+import { formatDateTime } from "./randomizerUiUtils.js";
+
+export function ResultChips({ results, onRemove, disabled = false, textContent = {} }) {
+ if (!results?.length) return null;
+ return (
+
+ {results.map((result, index) => (
+
+ {result}
+ {onRemove && (
+ onRemove(index)} disabled={disabled} aria-label={`${textContent.deleteTitle || "Supprimer"} ${result}`}>
+
+
+ )}
+
+ ))}
+
+ );
+}
+
+export function RandomizerHistory({ title, history, renderEntry, renderDetails, onClear, onRemove, disabled = false, textContent = {}, showEmpty = false }) {
+ const hasHistory = Boolean(history?.length);
+ if (!hasHistory && !showEmpty) return null;
+ return (
+
+
+ {title}
+ {textContent.clearHistoryButton || "Effacer tout"}
+
+ {hasHistory ? (
+
+ {history.map((entry, index) => (
+
+ {renderDetails ? (
+
+
+
+ {renderEntry(entry)}
+ {formatDateTime(entry.at)}
+
+ {renderDetails(entry)}
+
+ ) : (
+
+ {renderEntry(entry)}
+ {formatDateTime(entry.at)}
+
+ )}
+ {onRemove && (
+
onRemove(entry.id)} disabled={disabled} aria-label={`${textContent.deleteTitle || "Supprimer"} ${formatDateTime(entry.at)}`}>
+
+
+ )}
+
+ ))}
+
+ ) : {textContent.emptyHistory || "Aucun historique."}
}
+
+ );
+}
diff --git a/website/src/features/toolboxes/modules/randomizer/RandomizerTeams.jsx b/website/src/features/toolboxes/modules/randomizer/RandomizerTeams.jsx
new file mode 100644
index 0000000..ccd128f
--- /dev/null
+++ b/website/src/features/toolboxes/modules/randomizer/RandomizerTeams.jsx
@@ -0,0 +1,356 @@
+// Rôle : gère l'onglet équipes équilibrées du Randomizer et ses exclusions.
+import { useEffect, useRef, useState } from "react";
+import { CompactDropdown } from "../../../../components/CompactDropdown.jsx";
+import { Icon } from "../../../../components/Icon.jsx";
+import { useDraftForm } from "../../../../hooks/useDraftForm.js";
+import { RANDOMIZER_LIMITS, addRandomizerHistory, clampNumber, createBalancedTeams, filterExclusionsForPlayers } from "../randomizerUtils.js";
+import { RandomizerHistory } from "./RandomizerShared.jsx";
+import { DEFAULT_EXCLUSION_DRAFT, TEAM_REEL_DELAYS, hasReducedMotion, pickRandomItems } from "./randomizerUiUtils.js";
+
+export function TeamsRandomizer({ data, textContent, onChange, context }) {
+ const exclusionDraft = useDraftForm(DEFAULT_EXCLUSION_DRAFT);
+ const [animating, setAnimating] = useState(false);
+ const [animationTeams, setAnimationTeams] = useState([]);
+ const [reelPlayers, setReelPlayers] = useState([]);
+ const [reelIndex, setReelIndex] = useState(0);
+ const [reelDuration, setReelDuration] = useState(TEAM_REEL_DELAYS[0]);
+ const [namesText, setNamesText] = useState("");
+ const timersRef = useRef([]);
+ const activePlayers = data.players.filter((player) => player.active);
+ const storedSelectionCount = clampNumber(data.playerSelectionCount, data.teamCount, RANDOMIZER_LIMITS.teamPlayersMax, data.teamCount);
+ const [selectionCountDraft, setSelectionCountDraft] = useState(String(storedSelectionCount));
+ const selectionCount = clampNumber(selectionCountDraft, data.teamCount, RANDOMIZER_LIMITS.teamPlayersMax, storedSelectionCount);
+ const displayedTeams = animating ? animationTeams : data.teams;
+ const exclusionMode = exclusionDraft.values.mode === "source" ? "source" : "group";
+
+ useEffect(() => {
+ setSelectionCountDraft(String(storedSelectionCount));
+ }, [storedSelectionCount]);
+
+ useEffect(() => () => {
+ timersRef.current.forEach((timerId) => window.clearTimeout(timerId));
+ timersRef.current = [];
+ }, []);
+
+ function queueTimer(callback, delay) {
+ const timerId = window.setTimeout(callback, delay);
+ timersRef.current.push(timerId);
+ return timerId;
+ }
+
+ function clearTeamTimers() {
+ timersRef.current.forEach((timerId) => window.clearTimeout(timerId));
+ timersRef.current = [];
+ }
+
+ function commitSelectionCount(value = selectionCountDraft) {
+ const nextSelectionCount = clampNumber(value, data.teamCount, RANDOMIZER_LIMITS.teamPlayersMax, storedSelectionCount);
+ setSelectionCountDraft(String(nextSelectionCount));
+ onChange({ ...data, playerSelectionCount: nextSelectionCount });
+ }
+
+ function importPlayers(event) {
+ event.preventDefault();
+ const availableSlots = RANDOMIZER_LIMITS.teamPlayersMax - data.players.length;
+ const names = namesText
+ .split(/\r?\n/)
+ .map((name) => name.trim())
+ .filter(Boolean)
+ .slice(0, availableSlots);
+ if (!names.length) return;
+ onChange({
+ ...data,
+ players: [
+ ...data.players,
+ ...names.map((name) => ({ id: context.uid("randomizer"), name, score: 0, active: true }))
+ ]
+ });
+ setNamesText("");
+ }
+
+ function updatePlayer(playerId, patch) {
+ onChange({ ...data, players: data.players.map((player) => player.id === playerId ? { ...player, ...patch } : player) });
+ }
+
+ function removePlayer(playerId) {
+ onChange({
+ ...data,
+ players: data.players.filter((player) => player.id !== playerId),
+ exclusions: data.exclusions
+ .filter((exclusion) => exclusion.playerAId !== playerId)
+ .map((exclusion) => ({ ...exclusion, playerBIds: (exclusion.playerBIds || []).filter((targetId) => targetId !== playerId) }))
+ .filter((exclusion) => exclusion.playerBIds.length)
+ });
+ }
+
+ function clearPlayers() {
+ onChange({ ...data, players: [], exclusions: [], teams: [] });
+ }
+
+ function toggleExclusionTarget(playerId) {
+ const currentIds = exclusionDraft.values.playerBIds || [];
+ exclusionDraft.setField(
+ "playerBIds",
+ currentIds.includes(playerId) ? currentIds.filter((targetId) => targetId !== playerId) : [...currentIds, playerId]
+ );
+ }
+
+ function addExclusion(event) {
+ exclusionDraft.handleSubmit(event, (draft, { reset }) => {
+ const mode = draft.mode === "source" ? "source" : "group";
+ const selectedIds = [...new Set(Array.isArray(draft.playerBIds) ? draft.playerBIds.filter(Boolean) : [])];
+ const playerAId = mode === "group" ? selectedIds[0] : draft.playerAId;
+ const playerBIds = mode === "group" ? selectedIds.slice(1) : selectedIds.filter((playerId) => playerId !== draft.playerAId);
+ if (!playerAId || !playerBIds.length) return;
+ const exists = data.exclusions.some((exclusion) => exclusion.playerAId === playerAId && (exclusion.mode || "source") === mode && [...(exclusion.playerBIds || [])].sort().join(":") === [...playerBIds].sort().join(":"));
+ if (exists) return;
+ onChange({ ...data, exclusions: [...data.exclusions, { id: context.uid("randomizer"), playerAId, playerBIds, mode }] });
+ reset();
+ });
+ }
+
+ function launchTeams() {
+ clearTeamTimers();
+ const selectedPlayers = data.playerSelectionMode === "count" ? pickRandomItems(activePlayers, Math.min(selectionCount, activePlayers.length)) : activePlayers;
+ const selectedExclusions = filterExclusionsForPlayers(data.exclusions, selectedPlayers);
+ const result = createBalancedTeams(selectedPlayers, data.teamCount, selectedExclusions);
+ if (!result.ok) {
+ context.notify?.(result.error === "not_enough_players" ? textContent.notEnoughPlayers || "Pas assez de joueurs actifs." : textContent.constraintsImpossible || "Contraintes impossibles à satisfaire.");
+ return;
+ }
+ const historyEntry = { id: context.uid("randomizer"), at: new Date().toISOString(), teams: result.teams };
+ if (hasReducedMotion()) {
+ onChange({ ...data, teams: result.teams, history: addRandomizerHistory(data.history, historyEntry) });
+ return;
+ }
+ const emptyTeams = result.teams.map((team) => ({ ...team, score: 0, players: [] }));
+ const assignments = result.teams.flatMap((team) => team.players.map((player) => ({ teamId: team.id, player })));
+ setAnimating(true);
+ setAnimationTeams(emptyTeams);
+ setReelPlayers(selectedPlayers);
+ setReelIndex(0);
+ setReelDuration(TEAM_REEL_DELAYS[0]);
+
+ function animateAssignment(index) {
+ const assignment = assignments[index];
+ if (!assignment) {
+ setAnimating(false);
+ setReelPlayers([]);
+ setReelIndex(0);
+ onChange({ ...data, teams: result.teams, history: addRandomizerHistory(data.history, historyEntry) });
+ return;
+ }
+
+ let ticks = 0;
+ const tick = () => {
+ ticks += 1;
+ const isFinalTick = ticks >= TEAM_REEL_DELAYS.length;
+ const tickDelay = TEAM_REEL_DELAYS[Math.min(ticks, TEAM_REEL_DELAYS.length - 1)];
+ const nextIndex = isFinalTick
+ ? Math.max(0, selectedPlayers.findIndex((player) => player.id === assignment.player.id))
+ : Math.floor(Math.random() * selectedPlayers.length);
+ setReelIndex(nextIndex);
+ setReelDuration(Math.max(54, Math.round(tickDelay * 0.86)));
+ if (!isFinalTick) {
+ queueTimer(tick, tickDelay);
+ return;
+ }
+ queueTimer(() => {
+ setAnimationTeams((currentTeams) => currentTeams.map((team) => {
+ if (team.id !== assignment.teamId) return team;
+ return {
+ ...team,
+ score: team.score + assignment.player.score,
+ players: [...team.players, { ...assignment.player, arriving: true }]
+ };
+ }));
+ queueTimer(() => animateAssignment(index + 1), 260);
+ }, 220);
+ };
+ tick();
+ }
+
+ animateAssignment(0);
+ }
+
+ function clearTeamsHistory() {
+ onChange({ ...data, history: [] });
+ }
+
+ function removeTeamsHistoryEntry(entryId) {
+ onChange({ ...data, history: data.history.filter((entry) => entry.id !== entryId) });
+ }
+
+ return (
+
+
+
+ {reelPlayers.length ? (
+ <>
+
+ {reelPlayers.map((player) => {player.name} )}
+
+
{reelPlayers[reelIndex]?.name || ""}
+ >
+ ) :
}
+
+
+ {displayedTeams.length ? displayedTeams.map((team) => (
+
+ {team.name}
+ {textContent.teamScoreLabel || "Score"} {team.score}
+ {team.players.map((player) => {player.name} )}
+
+ )) :
{textContent.emptyTeams ?? ""}
}
+
+
+
+
+
+
+
+
+
+ {textContent.activeLabel || "Actif"}
+ {textContent.nameLabel || "Nom"}
+ {textContent.scoreLabel || "Score"}
+
+
+
+ {textContent.clearNamesButton || "Supprimer tous les noms"}
+
+
+
+
+
+ `${entry.teams.length} ${entry.teams.length > 1 ? textContent.teamsPluralLabel || "équipes" : textContent.teamsSingularLabel || "équipe"}`}
+ renderDetails={(entry) => (
+ 4 ? "has-team-rows" : ""}`}>
+ {entry.teams.map((team) => (
+
+
{team.name}
+
{textContent.teamScoreLabel || "Score"} {team.score}
+
+ {team.players.map((player) => {player.name} )}
+
+
+ ))}
+
+ )}
+ onClear={clearTeamsHistory}
+ onRemove={removeTeamsHistoryEntry}
+ disabled={animating}
+ textContent={textContent}
+ showEmpty
+ />
+
+ );
+}
diff --git a/website/src/features/toolboxes/modules/randomizer/RandomizerWheel.jsx b/website/src/features/toolboxes/modules/randomizer/RandomizerWheel.jsx
new file mode 100644
index 0000000..1b45b5a
--- /dev/null
+++ b/website/src/features/toolboxes/modules/randomizer/RandomizerWheel.jsx
@@ -0,0 +1,207 @@
+// Rôle : gère l'onglet roue pondérée du Randomizer et son animation.
+import { useEffect, useMemo, useRef, useState } from "react";
+import { Icon } from "../../../../components/Icon.jsx";
+import { RANDOMIZER_LIMITS, addRandomizerHistory, buildWheelSegments, clampNumber, drawWeightedEntries } from "../randomizerUtils.js";
+import { ResultChips, RandomizerHistory } from "./RandomizerShared.jsx";
+import { DRAW_PAUSE_MS, WHEEL_COLORS, WHEEL_SPIN_MS, describeWheelArc, getWheelTargetRotation, hasReducedMotion } from "./randomizerUiUtils.js";
+
+export function WheelRandomizer({ data, textContent, onChange, context }) {
+ const [rotation, setRotation] = useState(0);
+ const [spinning, setSpinning] = useState(false);
+ const [drawing, setDrawing] = useState(false);
+ const [displayResults, setDisplayResults] = useState(data.results);
+ const [entriesText, setEntriesText] = useState("");
+ const timersRef = useRef([]);
+ const skipWheelSyncRef = useRef(false);
+ const segments = useMemo(() => buildWheelSegments(data.entries), [data.entries]);
+
+ useEffect(() => {
+ if (drawing) return;
+ if (skipWheelSyncRef.current) {
+ skipWheelSyncRef.current = false;
+ return;
+ }
+ setDisplayResults(data.results);
+ }, [data.results, drawing]);
+
+ useEffect(() => () => {
+ timersRef.current.forEach((timerId) => window.clearTimeout(timerId));
+ timersRef.current = [];
+ }, []);
+
+ function queueTimer(callback, delay) {
+ const timerId = window.setTimeout(callback, delay);
+ timersRef.current.push(timerId);
+ return timerId;
+ }
+
+ function clearWheelTimers() {
+ timersRef.current.forEach((timerId) => window.clearTimeout(timerId));
+ timersRef.current = [];
+ }
+
+ function stopWheel() {
+ clearWheelTimers();
+ skipWheelSyncRef.current = true;
+ setSpinning(false);
+ setDrawing(false);
+ }
+
+ function addEntries(event) {
+ event.preventDefault();
+ const availableSlots = RANDOMIZER_LIMITS.wheelEntriesMax - data.entries.length;
+ const labels = entriesText
+ .split(/\r?\n/)
+ .map((label) => label.trim())
+ .filter(Boolean)
+ .slice(0, availableSlots);
+ if (!labels.length) return;
+ onChange({
+ ...data,
+ entries: [
+ ...data.entries,
+ ...labels.map((label) => ({ id: context.uid("randomizer"), label, weight: 1 }))
+ ]
+ });
+ setEntriesText("");
+ }
+
+ function updateEntry(entryId, patch) {
+ onChange({ ...data, entries: data.entries.map((entry) => entry.id === entryId ? { ...entry, ...patch } : entry) });
+ }
+
+ function removeEntry(entryId) {
+ onChange({ ...data, entries: data.entries.filter((entry) => entry.id !== entryId) });
+ }
+
+ function clearEntries() {
+ onChange({ ...data, entries: [], results: [] });
+ }
+
+ function clearWheelHistory() {
+ onChange({ ...data, history: [] });
+ }
+
+ function removeWheelHistoryEntry(entryId) {
+ onChange({ ...data, history: data.history.filter((entry) => entry.id !== entryId) });
+ }
+
+ function launchWheel() {
+ clearWheelTimers();
+ skipWheelSyncRef.current = false;
+ const drawnEntries = drawWeightedEntries(data.entries, data.draws);
+ if (!drawnEntries.length) {
+ context.notify?.(textContent.wheelInvalid || "Ajoutez au moins deux valeurs.");
+ return;
+ }
+ const results = drawnEntries.map((entry) => entry.label);
+ const historyEntry = { id: context.uid("randomizer"), at: new Date().toISOString(), results };
+ if (hasReducedMotion()) {
+ setDisplayResults(results);
+ onChange({ ...data, results, history: addRandomizerHistory(data.history, historyEntry) });
+ return;
+ }
+ setDrawing(true);
+ setDisplayResults([]);
+ let rotationCursor = rotation;
+
+ function animateDraw(index) {
+ const drawnEntry = drawnEntries[index];
+ const winnerSegment = segments.find((segment) => segment.entry.id === drawnEntry.id) || segments[0];
+ rotationCursor = getWheelTargetRotation(rotationCursor, winnerSegment.midAngle);
+ setSpinning(true);
+ setRotation(rotationCursor);
+ queueTimer(() => {
+ setDisplayResults((current) => [...current, drawnEntry.label]);
+ setSpinning(false);
+ if (index < drawnEntries.length - 1) {
+ queueTimer(() => animateDraw(index + 1), DRAW_PAUSE_MS);
+ return;
+ }
+ setDrawing(false);
+ onChange({ ...data, results, history: addRandomizerHistory(data.history, historyEntry) });
+ }, WHEEL_SPIN_MS);
+ }
+
+ animateDraw(0);
+ }
+
+ return (
+
+
+
+
+
+
+ {segments.length ? segments.map((segment) => (
+
+ )) : }
+
+
+
+
+ {segments.length ? segments.map((segment) => (
+
+
+ {segment.entry.label}
+
+ )) :
{textContent.wheelInvalid || "Ajoutez au moins deux valeurs."}
}
+
+
+
+
+
+
+
+
+
+
+ {textContent.savedValuesLabel || "Valeurs enregistrées"}
+ {textContent.weightLabel || "Poids"}
+
+
+
+ {textContent.clearValuesButton || "Supprimer toutes les valeurs"}
+
+
entry.results.join(" - ")}
+ onClear={clearWheelHistory}
+ onRemove={removeWheelHistoryEntry}
+ disabled={drawing}
+ textContent={textContent}
+ showEmpty
+ />
+
+
+ );
+}
diff --git a/website/src/features/toolboxes/modules/randomizer/randomizerUiUtils.js b/website/src/features/toolboxes/modules/randomizer/randomizerUiUtils.js
new file mode 100644
index 0000000..57bd76d
--- /dev/null
+++ b/website/src/features/toolboxes/modules/randomizer/randomizerUiUtils.js
@@ -0,0 +1,49 @@
+// Rôle : regroupe les helpers UI et animation purs du Randomizer.
+export const WHEEL_COLORS = ["#8b5cf6", "#f6c453", "#22d3ee", "#d946ef", "#6366f1", "#22c55e"];
+export const DRAW_PAUSE_MS = 500;
+export const DICE_ROLL_DELAYS = [62, 70, 82, 98, 120, 148, 182, 224, 276, 338, 410];
+export const TEAM_REEL_DELAYS = [58, 66, 78, 94, 116, 144, 180, 226, 286, 360];
+export const WHEEL_SPIN_MS = 1120;
+export const DEFAULT_EXCLUSION_DRAFT = { playerAId: "", playerBIds: [], mode: "group" };
+
+export function hasReducedMotion() {
+ return window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches;
+}
+
+function polarToCartesian(radius, angle) {
+ const radians = (angle * Math.PI) / 180;
+ return {
+ x: 50 + radius * Math.cos(radians),
+ y: 50 + radius * Math.sin(radians)
+ };
+}
+
+export function describeWheelArc(startAngle, endAngle) {
+ const start = polarToCartesian(48, startAngle);
+ const end = polarToCartesian(48, endAngle);
+ const largeArc = endAngle - startAngle > 180 ? 1 : 0;
+ return `M 50 50 L ${start.x} ${start.y} A 48 48 0 ${largeArc} 1 ${end.x} ${end.y} Z`;
+}
+
+export function formatDateTime(value) {
+ const date = new Date(value);
+ return Number.isFinite(date.getTime()) ? date.toLocaleString("fr-FR", { dateStyle: "short", timeStyle: "short" }) : "";
+}
+
+export function getWheelTargetRotation(currentRotation, targetAngle) {
+ const pointerAngle = -90;
+ const targetRotation = pointerAngle - targetAngle;
+ const currentModulo = ((currentRotation % 360) + 360) % 360;
+ const targetModulo = ((targetRotation % 360) + 360) % 360;
+ const delta = (targetModulo - currentModulo + 360) % 360;
+ return currentRotation + 1080 + delta;
+}
+
+export function pickRandomItems(items, count) {
+ const nextItems = [...items];
+ for (let index = nextItems.length - 1; index > 0; index -= 1) {
+ const targetIndex = Math.floor(Math.random() * (index + 1));
+ [nextItems[index], nextItems[targetIndex]] = [nextItems[targetIndex], nextItems[index]];
+ }
+ return nextItems.slice(0, count);
+}
diff --git a/website/src/features/toolboxes/modules/textImport/basic.js b/website/src/features/toolboxes/modules/textImport/basic.js
index 8e4b3a9..d9c26ea 100644
--- a/website/src/features/toolboxes/modules/textImport/basic.js
+++ b/website/src/features/toolboxes/modules/textImport/basic.js
@@ -104,7 +104,7 @@ export function importNotepadText(text, context) {
if (currentPage.drawings.strokes.length) currentPage.drawingMode = "permanent";
return;
}
- throw lineError(index, `ligne bloc note invalide: ${cleanLine}`);
+ throw lineError(index, `ligne Bloc Note invalide: ${cleanLine}`);
});
if (readingHtml) throw new Error("Bloc HTML non terminé.");
flushPage();
diff --git a/website/src/features/toolboxes/toolboxModuleRegistry.js b/website/src/features/toolboxes/toolboxModuleRegistry.js
index eef24cf..128055e 100644
--- a/website/src/features/toolboxes/toolboxModuleRegistry.js
+++ b/website/src/features/toolboxes/toolboxModuleRegistry.js
@@ -11,8 +11,8 @@ export const TOOLBOX_MODULE_DEFINITIONS = {
libraryCategory: "notesTracking"
},
memos: {
- label: "Memos",
- defaultTitle: "Memos",
+ label: "Mémos",
+ defaultTitle: "Mémos",
mode: "Post-it courts",
icon: "memos",
editable: true,
diff --git a/website/src/features/toolboxes/useToolboxActions.js b/website/src/features/toolboxes/useToolboxActions.js
index 86bb34f..862ea59 100644
--- a/website/src/features/toolboxes/useToolboxActions.js
+++ b/website/src/features/toolboxes/useToolboxActions.js
@@ -41,7 +41,7 @@ export function useToolboxActions({ store, notify, creationTemplate, setConfirmM
name: name.trim() || "Nouvelle toolbox",
updatedAt: now,
modules: [
- { id: memosModuleId, type: "memos", title: creationTemplate?.memos?.title || "Memos" },
+ { id: memosModuleId, type: "memos", title: creationTemplate?.memos?.title || "Mémos" },
{ id: linksModuleId, type: "links", title: creationTemplate?.links?.moduleTitle || "Liens utiles" }
]
};
diff --git a/website/src/styles/_cards.scss b/website/src/styles/_cards.scss
index a48920a..21d2b71 100644
--- a/website/src/styles/_cards.scss
+++ b/website/src/styles/_cards.scss
@@ -1,5 +1,6 @@
// Rôle : regroupe les styles de cards, panels et variantes compactes.
@use "mixins";
+@use "design-system/surfaces";
.section-grid,
.cards,
@@ -31,12 +32,7 @@
}
.card {
- border-color: transparent;
- border-radius: var(--radius-md);
- background:
- linear-gradient(rgba(16, 20, 38, 0.88), rgba(16, 20, 38, 0.88)) padding-box,
- radial-gradient(circle at 100% 0%, rgba(34, 211, 238, 0.42), transparent 28%) border-box,
- linear-gradient(135deg, rgba(246, 196, 83, 0.62) 0%, rgba(246, 196, 83, 0.42) 72%, rgba(34, 211, 238, 0.24) 100%) border-box;
+ @include surfaces.card-surface;
}
@supports (corner-shape: scoop) {
@@ -69,15 +65,8 @@
}
.card:hover {
+ @include surfaces.card-surface-hover;
transform: none;
- border-color: transparent;
- background:
- linear-gradient(rgba(21, 26, 48, 0.96), rgba(21, 26, 48, 0.96)) padding-box,
- linear-gradient(135deg, rgba(139, 92, 246, 0.9), rgba(196, 181, 253, 0.72)) border-box;
- box-shadow:
- var(--shadow-sm),
- 0 0 18px rgba(139, 92, 246, 0.22),
- 0 0 34px rgba(196, 181, 253, 0.12);
}
.compact-data-card {
@@ -136,7 +125,7 @@
.card-cover {
min-height: 124px;
- border-bottom: 1px solid rgba(246, 196, 83, 0.42);
+ @include surfaces.card-cover-divider;
}
.card-body {
diff --git a/website/src/styles/_mhwilds.scss b/website/src/styles/_mhwilds.scss
index bf714a5..24dbd95 100644
--- a/website/src/styles/_mhwilds.scss
+++ b/website/src/styles/_mhwilds.scss
@@ -1,4 +1,6 @@
// Rôle : contient les styles spécifiques aux pages Monster Hunter Wilds.
+@use "design-system/surfaces";
+
.monster-grid {
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
}
@@ -8,13 +10,9 @@
}
.mhwilds-card {
+ @include surfaces.card-surface;
overflow: hidden;
border: 1px solid transparent;
- border-radius: var(--radius-md);
- background:
- linear-gradient(rgba(16, 20, 38, 0.88), rgba(16, 20, 38, 0.88)) padding-box,
- radial-gradient(circle at 100% 0%, rgba(34, 211, 238, 0.42), transparent 28%) border-box,
- linear-gradient(135deg, rgba(246, 196, 83, 0.62) 0%, rgba(246, 196, 83, 0.42) 72%, rgba(34, 211, 238, 0.24) 100%) border-box;
box-shadow: var(--shadow-sm);
transition:
transform var(--duration-fast) var(--ease-standard),
@@ -29,15 +27,8 @@
}
.mhwilds-card:hover {
+ @include surfaces.card-surface-hover;
transform: none;
- border-color: transparent;
- background:
- linear-gradient(rgba(21, 26, 48, 0.96), rgba(21, 26, 48, 0.96)) padding-box,
- linear-gradient(135deg, rgba(139, 92, 246, 0.9), rgba(196, 181, 253, 0.72)) border-box;
- box-shadow:
- var(--shadow-sm),
- 0 0 18px rgba(139, 92, 246, 0.22),
- 0 0 34px rgba(196, 181, 253, 0.12);
}
.monster-card {
@@ -76,6 +67,7 @@
}
.mhwilds-card-face {
+ @include surfaces.card-surface(rgba(16, 20, 38, 0.92));
position: absolute;
inset: 0;
contain: paint;
@@ -83,11 +75,6 @@
overflow: hidden;
flex-direction: column;
border: 1px solid transparent;
- border-radius: var(--radius-md);
- background:
- linear-gradient(rgba(16, 20, 38, 0.92), rgba(16, 20, 38, 0.92)) padding-box,
- radial-gradient(circle at 100% 0%, rgba(34, 211, 238, 0.42), transparent 28%) border-box,
- linear-gradient(135deg, rgba(246, 196, 83, 0.62) 0%, rgba(246, 196, 83, 0.42) 72%, rgba(34, 211, 238, 0.24) 100%) border-box;
box-shadow: var(--shadow-sm);
backface-visibility: hidden;
}
@@ -144,21 +131,20 @@
}
.monster-card:hover .mhwilds-card-face {
- border-color: transparent;
- background:
- linear-gradient(rgba(16, 20, 38, 0.95), rgba(16, 20, 38, 0.95)) padding-box,
- linear-gradient(135deg, rgba(139, 92, 246, 0.76), rgba(196, 181, 253, 0.58)) border-box;
- box-shadow:
- var(--shadow-sm),
+ @include surfaces.card-surface-hover(
+ rgba(16, 20, 38, 0.95),
+ rgba(139, 92, 246, 0.76),
+ rgba(196, 181, 253, 0.58),
0 0 14px rgba(139, 92, 246, 0.14),
- 0 0 24px rgba(196, 181, 253, 0.08);
+ 0 0 24px rgba(196, 181, 253, 0.08)
+ );
}
.mhwilds-card-art {
display: grid;
min-height: 112px;
place-items: center;
- border-bottom: 1px solid rgba(246, 196, 83, 0.42);
+ @include surfaces.card-cover-divider;
background:
radial-gradient(circle at 50% 40%, rgba(139, 92, 246, 0.16), transparent 46%),
rgba(5, 7, 17, 0.36);
diff --git a/website/src/styles/design-system/_attributes.scss b/website/src/styles/design-system/_attributes.scss
index 754eb85..30be601 100644
--- a/website/src/styles/design-system/_attributes.scss
+++ b/website/src/styles/design-system/_attributes.scss
@@ -1,1121 +1,9 @@
-// Rôle : expose l'API publique NDS par classes de famille et attributs data-*.
-@use "controls";
-
-.nds-input,
-.nds-time,
-.nds-select,
-.nds-textarea {
- @include controls.field-control;
-}
-
-.nds-input::placeholder,
-.nds-textarea::placeholder {
- @include controls.field-placeholder;
-}
-
-.nds-input[data-size="sm"],
-.nds-textarea[data-size="sm"] {
- @include controls.field-control(sm);
-}
-
-.nds-input[data-size="xs"],
-.nds-textarea[data-size="xs"] {
- @include controls.field-control(xs);
-}
-
-.nds-time {
- @include controls.time-control;
-}
-
-.nds-time[data-size="sm"] {
- @include controls.time-control(sm);
-}
-
-.nds-time[data-size="xs"] {
- @include controls.time-control(xs);
-}
-
-.nds-select {
- @include controls.select-control;
-}
-
-.nds-select[data-size="sm"] {
- @include controls.select-control(sm);
-}
-
-.nds-select[data-size="xs"] {
- @include controls.select-control(xs);
-}
-
-.nds-select:hover,
-.nds-select:focus {
- @include controls.select-control-hover;
- box-shadow: none;
-}
-
-.nds-select:focus {
- border-color: var(--color-primary);
- box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.16);
-}
-
-.nds-select option {
- @include controls.select-option;
-}
-
-.nds-textarea {
- @include controls.field-textarea;
- @include controls.scrollbar-resizer;
-}
-
-.nds-textarea[data-size="sm"] {
- @include controls.field-textarea(sm);
-}
-
-.nds-textarea[data-size="xs"] {
- @include controls.field-textarea(xs);
-}
-
-.nds-textarea[data-variant="ghost"] {
- @include controls.field-textarea-ghost;
-}
-
-.nds-input:hover,
-.nds-time:hover,
-.nds-textarea:hover {
- @include controls.field-hover;
-}
-
-.nds-textarea[data-variant="ghost"]:hover {
- @include controls.inline-field-hover;
-}
-
-.nds-input:focus,
-.nds-time:focus,
-.nds-select:focus,
-.nds-select-trigger:focus,
-.nds-textarea:focus {
- @include controls.field-focus;
-}
-
-.nds-inline-input {
- @include controls.inline-field;
-}
-
-.nds-inline-input:hover,
-.nds-inline-input:focus {
- @include controls.inline-field-hover;
-}
-
-.nds-button {
- @include controls.text-button;
- font-weight: 800;
-}
-
-.nds-button:hover:not(:disabled, [data-variant="primary"], [data-variant="danger"], [data-variant="ghost"], .active, .is-active, [aria-pressed="true"]) {
- @include controls.text-button-hover;
-}
-
-.nds-button[data-size="sm"] {
- @include controls.text-button(sm);
-}
-
-.nds-button[data-size="xs"] {
- @include controls.text-button(xs);
-}
-
-.nds-button[data-variant="primary"] {
- @include controls.text-button-primary;
-}
-
-.nds-button[data-variant="primary"]:hover {
- @include controls.text-button-primary-hover;
-}
-
-.nds-button[data-variant="secondary"] {
- @include controls.text-button-secondary;
-}
-
-.nds-button[data-variant="ghost"] {
- @include controls.text-button-ghost;
-}
-
-.nds-button[data-variant="ghost"]:hover {
- @include controls.text-button-ghost-hover;
-}
-
-.nds-button[data-variant="dashed"] {
- @include controls.text-button-dashed;
-}
-
-.nds-button[data-variant="dashed"]:hover {
- @include controls.text-button-dashed-hover;
-}
-
-.nds-button[data-variant="danger"] {
- @include controls.text-button-danger;
-}
-
-.nds-button[data-variant="danger"]:hover {
- @include controls.text-button-danger-hover;
-}
-
-.nds-button[data-edge="left"] {
- @include controls.button-edge(left);
-}
-
-.nds-button[data-edge="middle"] {
- @include controls.button-edge(middle);
-}
-
-.nds-button[data-edge="top"] {
- @include controls.button-edge(top);
-}
-
-.nds-button[data-edge="center"] {
- @include controls.button-edge(center);
-}
-
-.nds-button[data-edge="right"] {
- @include controls.button-edge(right);
-}
-
-.nds-button[data-edge="bottom"] {
- @include controls.button-edge(bottom);
-}
-
-.nds-button[data-edge="top-left"] {
- @include controls.button-edge(top-left);
-}
-
-.nds-button[data-edge="top-right"] {
- @include controls.button-edge(top-right);
-}
-
-.nds-button[data-edge="bottom-right"] {
- @include controls.button-edge(bottom-right);
-}
-
-.nds-button[data-edge="bottom-left"] {
- @include controls.button-edge(bottom-left);
-}
-
-.nds-button[data-border="joined"][data-edge="left"] {
- @include controls.button-joined-border(left);
-}
-
-.nds-button[data-border="joined"][data-edge="middle"] {
- @include controls.button-joined-border(middle);
-}
-
-.nds-button[data-border="joined"][data-edge="top"] {
- @include controls.button-joined-border(top);
-}
-
-.nds-button[data-border="joined"][data-edge="center"] {
- @include controls.button-joined-border(center);
-}
-
-.nds-button[data-border="joined"][data-edge="right"] {
- @include controls.button-joined-border(right);
-}
-
-.nds-button[data-border="joined"][data-edge="bottom"] {
- @include controls.button-joined-border(bottom);
-}
-
-.nds-button[data-border="joined"][data-edge="top-left"] {
- @include controls.button-joined-border(top-left);
-}
-
-.nds-button[data-border="joined"][data-edge="top-right"] {
- @include controls.button-joined-border(top-right);
-}
-
-.nds-button[data-border="joined"][data-edge="bottom-right"] {
- @include controls.button-joined-border(bottom-right);
-}
-
-.nds-button[data-border="joined"][data-edge="bottom-left"] {
- @include controls.button-joined-border(bottom-left);
-}
-
-.nds-button[data-border="none"] {
- @include controls.button-borderless;
-}
-
-.nds-button.is-active,
-.nds-button.active,
-.nds-button.active,
-.nds-button.is-active,
-.nds-button[aria-pressed="true"] {
- @include controls.text-button-active;
-}
-
-.nds-icon-button {
- @include controls.icon-button;
- display: inline-grid;
- place-items: center;
- box-shadow: none;
-}
-
-.nds-icon-button .ui-icon {
- @include controls.icon-button-icon;
-}
-
-.nds-icon-button[class*="drag-handle"] {
- cursor: grab;
- touch-action: none;
- user-select: none;
-}
-
-.nds-icon-button[class*="drag-handle"]:active {
- cursor: grabbing;
-}
-
-.nds-icon-button[class*="drag-handle"]:disabled {
- cursor: not-allowed;
-}
-
-.nds-icon-button:hover:not([data-variant="primary"], [data-variant="danger"], [data-variant="ghost"], .active, .is-active, [aria-pressed="true"]) {
- @include controls.icon-button-hover;
-}
-
-.nds-icon-button:hover:not([data-variant="primary"], [data-variant="danger"], [data-variant="ghost"], .active, .is-active, [aria-pressed="true"]) .ui-icon {
- @include controls.icon-button-hover-icon;
-}
-
-.nds-icon-button[data-size="sm"] {
- @include controls.icon-button(sm);
-}
-
-.nds-icon-button[data-size="sm"] .ui-icon {
- @include controls.icon-button-icon(sm);
-}
-
-.nds-icon-button[data-size="xs"] {
- @include controls.icon-button(xs);
-}
-
-.nds-icon-button[data-size="xs"] .ui-icon {
- @include controls.icon-button-icon(xs);
-}
-
-.nds-icon-button[data-size="xxs"] {
- @include controls.icon-button(xxs);
-}
-
-.nds-icon-button[data-size="xxs"] .ui-icon {
- @include controls.icon-button-icon(xxs);
-}
-
-.nds-icon-button[data-size="tiny"] {
- @include controls.icon-button(tiny);
-}
-
-.nds-icon-button[data-size="tiny"] .ui-icon {
- @include controls.icon-button-icon(tiny);
-}
-
-.nds-icon-button[data-size="micro"] {
- @include controls.icon-button(micro);
-}
-
-.nds-icon-button[data-size="micro"] .ui-icon {
- @include controls.icon-button-icon(micro);
-}
-
-.nds-icon-button[data-variant="primary"] {
- @include controls.icon-button-primary;
-}
-
-.nds-icon-button[data-variant="primary"]:hover {
- @include controls.icon-button-primary-hover;
-}
-
-.nds-icon-button[data-variant="primary"]:hover .ui-icon {
- @include controls.icon-button-primary-hover-icon;
-}
-
-.nds-icon-button[data-variant="ghost"] {
- @include controls.icon-button-ghost;
-}
-
-.nds-icon-button[data-variant="panel-toggle"] {
- @include controls.icon-button-panel-toggle;
-}
-
-.nds-icon-button[data-variant="panel-toggle"]:hover,
-.nds-icon-button[data-variant="panel-toggle"].active,
-.nds-icon-button[data-variant="panel-toggle"].is-active,
-.nds-icon-button[data-variant="panel-toggle"][aria-pressed="true"] {
- @include controls.icon-button-panel-toggle-hover;
-}
-
-.nds-icon-button[data-variant="danger"] {
- @include controls.icon-button-danger;
-}
-
-.nds-icon-button[data-variant="danger"]:hover {
- @include controls.icon-button-danger-hover;
-}
-
-.nds-icon-button[data-variant="danger"]:hover .ui-icon {
- @include controls.icon-button-danger-hover-icon;
-}
-
-.nds-icon-button[class*="drag-handle"]:hover {
- cursor: grab;
-}
-
-.nds-icon-button[class*="drag-handle"]:active,
-.nds-icon-button[class*="drag-handle"]:hover:active {
- cursor: grabbing;
-}
-
-.nds-icon-button[data-edge="left"] {
- @include controls.button-edge(left);
-}
-
-.nds-icon-button[data-edge="middle"] {
- @include controls.button-edge(middle);
-}
-
-.nds-icon-button[data-edge="top"] {
- @include controls.button-edge(top);
-}
-
-.nds-icon-button[data-edge="center"] {
- @include controls.button-edge(center);
-}
-
-.nds-icon-button[data-edge="right"] {
- @include controls.button-edge(right);
-}
-
-.nds-icon-button[data-edge="bottom"] {
- @include controls.button-edge(bottom);
-}
-
-.nds-icon-button[data-edge="top-left"] {
- @include controls.button-edge(top-left);
-}
-
-.nds-icon-button[data-edge="top-right"] {
- @include controls.button-edge(top-right);
-}
-
-.nds-icon-button[data-edge="bottom-right"] {
- @include controls.button-edge(bottom-right);
-}
-
-.nds-icon-button[data-edge="bottom-left"] {
- @include controls.button-edge(bottom-left);
-}
-
-.nds-icon-button[data-border="joined"][data-edge="left"] {
- @include controls.button-joined-border(left);
-}
-
-.nds-icon-button[data-border="joined"][data-edge="middle"] {
- @include controls.button-joined-border(middle);
-}
-
-.nds-icon-button[data-border="joined"][data-edge="top"] {
- @include controls.button-joined-border(top);
-}
-
-.nds-icon-button[data-border="joined"][data-edge="center"] {
- @include controls.button-joined-border(center);
-}
-
-.nds-icon-button[data-border="joined"][data-edge="right"] {
- @include controls.button-joined-border(right);
-}
-
-.nds-icon-button[data-border="joined"][data-edge="bottom"] {
- @include controls.button-joined-border(bottom);
-}
-
-.nds-icon-button[data-border="joined"][data-edge="top-left"] {
- @include controls.button-joined-border(top-left);
-}
-
-.nds-icon-button[data-border="joined"][data-edge="top-right"] {
- @include controls.button-joined-border(top-right);
-}
-
-.nds-icon-button[data-border="joined"][data-edge="bottom-right"] {
- @include controls.button-joined-border(bottom-right);
-}
-
-.nds-icon-button[data-border="joined"][data-edge="bottom-left"] {
- @include controls.button-joined-border(bottom-left);
-}
-
-.nds-icon-button[data-border="none"] {
- @include controls.button-borderless;
-}
-
-.nds-icon-button.active,
-.nds-icon-button.is-active,
-.nds-icon-button[aria-pressed="true"] {
- @include controls.icon-button-active;
-}
-
-.nds-quantity-stepper {
- @include controls.quantity-stepper;
-}
-
-.nds-quantity-stepper[data-size="sm"] {
- @include controls.quantity-stepper(sm);
-}
-
-.nds-quantity-stepper[data-size="xs"] {
- @include controls.quantity-stepper(xs);
-}
-
-.nds-quantity-stepper button {
- @include controls.quantity-stepper-button;
-}
-
-.nds-quantity-stepper[data-size="sm"] button {
- @include controls.quantity-stepper-button(sm);
-}
-
-.nds-quantity-stepper[data-size="xs"] button {
- @include controls.quantity-stepper-button(xs);
-}
-
-.nds-quantity-stepper button:hover {
- @include controls.quantity-stepper-button-hover;
-}
-
-.nds-quantity-stepper .ui-icon {
- @include controls.quantity-stepper-button-icon;
-}
-
-.nds-quantity-stepper[data-size="sm"] .ui-icon {
- @include controls.quantity-stepper-button-icon(sm);
-}
-
-.nds-quantity-stepper[data-size="xs"] .ui-icon {
- @include controls.quantity-stepper-button-icon(xs);
-}
-
-.nds-quantity-stepper-value {
- @include controls.quantity-stepper-value;
-}
-
-.nds-quantity-stepper-input {
- @include controls.quantity-stepper-input;
- @include controls.number-spinner-reset;
-}
-
-.nds-quantity-stepper-input:hover,
-.nds-quantity-stepper-input:focus {
- @include controls.quantity-stepper-input-hover;
-}
-
-.nds-switch {
- @include controls.switch-control;
-}
-
-.nds-switch[data-size="lg"] {
- @include controls.switch-control(lg);
-}
-
-.nds-switch[data-size="sm"] {
- @include controls.switch-control(sm);
-}
-
-.nds-switch[data-size="xs"] {
- @include controls.switch-control(xs);
-}
-
-.nds-switch:has(> span:not(.ui-icon)) {
- grid-template-columns: var(--nds-switch-icon-size) minmax(0, 1fr) var(--nds-switch-track-width);
- min-width: max-content;
-}
-
-.nds-switch:has(> span:not(.ui-icon)) > span:not(.ui-icon) {
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.nds-switch .ui-icon {
- @include controls.switch-icon;
-}
-
-.nds-switch i {
- @include controls.switch-track;
-}
-
-.nds-switch i::before {
- @include controls.switch-thumb;
-}
-
-.nds-switch:hover {
- @include controls.switch-hover;
-}
-
-.nds-switch.active,
-.nds-switch.is-active,
-.nds-switch[aria-pressed="true"] {
- @include controls.switch-active;
-}
-
-.nds-switch.active i,
-.nds-switch.is-active i,
-.nds-switch[aria-pressed="true"] i {
- @include controls.switch-active-track;
-}
-
-.nds-switch.active i::before,
-.nds-switch.is-active i::before,
-.nds-switch[aria-pressed="true"] i::before {
- @include controls.switch-active-thumb;
-}
-
-.nds-native-switch {
- @include controls.native-switch-label;
-}
-
-.nds-native-switch input {
- @include controls.native-switch-input;
-}
-
-.nds-native-switch > span:first-of-type {
- --nds-switch-track-width: 36px;
- --nds-switch-track-height: 20px;
- --nds-switch-thumb-size: 12px;
- --nds-switch-thumb-left: 3px;
- --nds-switch-active-left: 19px;
- @include controls.switch-track;
- min-width: 36px;
-}
-
-.nds-native-switch > span:first-of-type::before {
- @include controls.switch-thumb;
-}
-
-.nds-native-switch input:checked + span {
- @include controls.switch-active-track;
-}
-
-.nds-native-switch input:checked + span::before {
- @include controls.switch-active-thumb;
-}
-
-.nds-native-switch:has(input:disabled) {
- @include controls.control-disabled;
- cursor: not-allowed;
-}
-
-.nds-native-switch:has(input:disabled) > span:first-of-type,
-.nds-native-switch:has(input:disabled) b {
- cursor: not-allowed;
-}
-
-.nds-radio-button-group {
- @include controls.radio-button-group;
-}
-
-.nds-radio-button-group[data-size="sm"] {
- @include controls.radio-button-group(sm);
-}
-
-.nds-radio-button-group[data-size="xs"] {
- @include controls.radio-button-group(xs);
-}
-
-.nds-radio-button-group > button,
-.nds-radio-button-group > span {
- @include controls.radio-button-group-option;
-}
-
-.nds-radio-button-group[data-size="sm"] > button,
-.nds-radio-button-group[data-size="sm"] > span {
- @include controls.radio-button-group-option(sm);
-}
-
-.nds-radio-button-group[data-size="xs"] > button,
-.nds-radio-button-group[data-size="xs"] > span {
- @include controls.radio-button-group-option(xs);
-}
-
-.nds-radio-button-group > button:hover,
-.nds-radio-button-group > span:hover {
- @include controls.radio-button-group-option-hover;
-}
-
-.nds-radio-button-group > button.active,
-.nds-radio-button-group > button.is-active,
-.nds-radio-button-group > span.active,
-.nds-radio-button-group > span.is-active {
- @include controls.radio-button-group-option-active;
-}
-
-.nds-radio-button-group .ui-icon {
- @include controls.radio-button-group-option-icon;
-}
-
-.nds-radio-button-group[data-size="sm"] .ui-icon {
- @include controls.radio-button-group-option-icon(sm);
-}
-
-.nds-radio-button-group[data-size="xs"] .ui-icon {
- @include controls.radio-button-group-option-icon(xs);
-}
-
-.nds-radio-button-group[aria-disabled="true"] {
- @include controls.control-disabled;
-}
-
-.nds-compound-toggle-field {
- @include controls.compound-toggle-field;
-}
-
-.nds-compound-toggle-field button {
- @include controls.compound-toggle-field-option;
-}
-
-.nds-compound-toggle-field button:hover:not(:disabled) {
- @include controls.radio-button-group-option-hover;
-}
-
-.nds-compound-toggle-field button.is-active,
-.nds-compound-toggle-field button.active,
-.nds-compound-toggle-field button[aria-pressed="true"] {
- @include controls.radio-button-group-option-active;
-}
-
-.nds-compound-toggle-field-input {
- @include controls.compound-toggle-field-input;
-}
-
-.nds-compound-toggle-field-input:hover,
-.nds-compound-toggle-field-input:focus {
- @include controls.inline-field-hover;
-}
-
-.nds-checkbox {
- @include controls.checkbox-control;
-}
-
-.nds-checkbox[data-size="xs"] {
- @include controls.checkbox-control(xs);
-}
-
-.nds-checkbox[data-size="sm"] {
- @include controls.checkbox-control(sm);
-}
-
-.nds-checkbox::before {
- @include controls.checkbox-check;
-}
-
-.nds-checkbox[data-size="xs"]::before {
- @include controls.checkbox-check(xs);
-}
-
-.nds-checkbox[data-size="sm"]::before {
- @include controls.checkbox-check(sm);
-}
-
-.nds-checkbox:hover:not(:disabled) {
- @include controls.checkbox-hover;
-}
-
-.nds-checkbox:checked {
- @include controls.checkbox-checked;
-}
-
-.nds-checkbox:checked::before {
- @include controls.checkbox-checked-check;
-}
-
-.nds-checkbox:disabled {
- @include controls.checkbox-disabled;
-}
-
-.nds-checkbox:disabled::before {
- opacity: 0.54;
-}
-
-.nds-swatch {
- @include controls.color-swatch;
-}
-
-.nds-swatch[data-variant="rainbow"] {
- @include controls.color-swatch-rainbow;
-}
-
-.nds-swatch.active,
-.nds-swatch.is-active,
-.nds-swatch[aria-pressed="true"] {
- @include controls.color-swatch-active;
-}
-
-.nds-select-trigger,
-.nds-date-trigger {
- @include controls.select-trigger;
-}
-
-.nds-select-trigger[data-size="sm"],
-.nds-date-trigger[data-size="sm"] {
- @include controls.select-trigger(sm);
-}
-
-.nds-select-trigger[data-size="xs"],
-.nds-date-trigger[data-size="xs"] {
- @include controls.select-trigger(xs);
-}
-
-.nds-select-trigger[data-variant="icon-picker"] {
- grid-template-columns: minmax(18px, 1fr);
- justify-items: center;
- min-width: 52px;
- padding-inline: 10px 30px;
-}
-
-.nds-select-trigger[data-variant="icon-picker"]:has(> span:not([aria-hidden="true"])) {
- grid-template-columns: 18px minmax(0, 1fr);
- justify-items: start;
- padding-inline: 10px 34px;
-}
-
-.nds-select-trigger[data-size="sm"][data-variant="icon-picker"] {
- grid-template-columns: minmax(18px, 1fr);
- min-width: 50px;
- padding-inline: 10px 30px;
-}
-
-.nds-select-trigger[data-size="sm"][data-variant="icon-picker"]:has(> span:not([aria-hidden="true"])) {
- grid-template-columns: 18px minmax(0, 1fr);
- padding-inline: 10px 34px;
-}
-
-.nds-select-trigger[data-size="xs"][data-variant="icon-picker"] {
- grid-template-columns: minmax(16px, 1fr);
- min-width: 46px;
- padding-inline: 9px 28px;
-}
-
-.nds-select-trigger[data-size="xs"][data-variant="icon-picker"]:has(> span:not([aria-hidden="true"])) {
- grid-template-columns: 16px minmax(0, 1fr);
- padding-inline: 8px 30px;
-}
-
-.nds-select-trigger:hover:not(:disabled),
-.nds-select-trigger.is-active,
-.nds-select-trigger.active,
-.nds-select-trigger[aria-pressed="true"],
-.nds-date-trigger:hover:not(:disabled),
-.nds-date-trigger.is-active,
-.nds-date-trigger.active,
-.nds-date-trigger[aria-pressed="true"] {
- @include controls.select-trigger-hover;
-}
-
-.nds-select-trigger:focus,
-.nds-date-trigger:focus {
- @include controls.select-trigger-hover;
- @include controls.field-focus;
- outline: 2px solid var(--color-accent-gold);
- outline-offset: 3px;
-}
-
-.nds-select-trigger[data-size="sm"]:hover:not(:disabled),
-.nds-select-trigger[data-size="sm"].is-active,
-.nds-select-trigger[data-size="sm"].active,
-.nds-select-trigger[data-size="sm"][aria-pressed="true"],
-.nds-date-trigger[data-size="sm"]:hover:not(:disabled),
-.nds-date-trigger[data-size="sm"].is-active,
-.nds-date-trigger[data-size="sm"].active,
-.nds-date-trigger[data-size="sm"][aria-pressed="true"] {
- @include controls.select-trigger-hover(sm);
-}
-
-.nds-select-trigger[data-size="sm"]:focus,
-.nds-date-trigger[data-size="sm"]:focus {
- @include controls.select-trigger-hover(sm);
- @include controls.field-focus;
- outline: 2px solid var(--color-accent-gold);
- outline-offset: 3px;
-}
-
-.nds-select-trigger[data-size="xs"]:hover:not(:disabled),
-.nds-select-trigger[data-size="xs"].is-active,
-.nds-select-trigger[data-size="xs"].active,
-.nds-select-trigger[data-size="xs"][aria-pressed="true"],
-.nds-date-trigger[data-size="xs"]:hover:not(:disabled),
-.nds-date-trigger[data-size="xs"].is-active,
-.nds-date-trigger[data-size="xs"].active,
-.nds-date-trigger[data-size="xs"][aria-pressed="true"] {
- @include controls.select-trigger-hover(xs);
-}
-
-.nds-select-trigger[data-size="xs"]:focus,
-.nds-date-trigger[data-size="xs"]:focus {
- @include controls.select-trigger-hover(xs);
- @include controls.field-focus;
- outline: 2px solid var(--color-accent-gold);
- outline-offset: 3px;
-}
-
-.nds-select-trigger .ui-icon,
-.nds-date-trigger .ui-icon {
- @include controls.select-trigger-icon;
-}
-
-.nds-select-trigger[data-size="sm"] .ui-icon,
-.nds-date-trigger[data-size="sm"] .ui-icon {
- @include controls.select-trigger-icon(sm);
-}
-
-.nds-select-trigger[data-size="xs"] .ui-icon,
-.nds-date-trigger[data-size="xs"] .ui-icon {
- @include controls.select-trigger-icon(xs);
-}
-
-.nds-select-trigger .ui-icon-chevron-down,
-.nds-date-trigger .ui-icon-chevron-down {
- @include controls.date-picker-trigger-chevron;
-}
-
-.nds-select-trigger[data-variant="icon-picker"] > .ui-icon:not(.ui-icon-chevron-down) {
- width: 18px;
- height: 18px;
-}
-
-.nds-select-trigger[data-variant="icon-picker"]:has(> span:not([aria-hidden="true"])) > .ui-icon:not(.ui-icon-chevron-down) {
- justify-self: center;
-}
-
-.nds-select-trigger[data-size="xs"][data-variant="icon-picker"] > .ui-icon:not(.ui-icon-chevron-down) {
- width: 16px;
- height: 16px;
-}
-
-.nds-select-trigger span,
-.nds-date-trigger span {
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.nds-list-row {
- @include controls.list-row-button;
-}
-
-.nds-list-row:hover,
-.nds-list-row.is-active,
-.nds-list-row.active {
- @include controls.list-row-button-hover;
-}
-
-.nds-list-row.is-active,
-.nds-list-row.active {
- @include controls.list-row-button-active;
-}
-
-.nds-list-row button {
- @include controls.list-row-button-content;
-}
-
-.nds-large-icon-picker-trigger {
- @include controls.large-icon-picker-trigger;
-}
-
-.nds-large-icon-picker-trigger:hover {
- @include controls.large-icon-picker-trigger-hover;
-}
-
-.nds-large-icon-picker-trigger.active,
-.nds-large-icon-picker-trigger.is-active,
-.nds-large-icon-picker-trigger[aria-pressed="true"] {
- @include controls.large-icon-picker-trigger-hover;
- border-color: rgba(246, 196, 83, 0.62);
- box-shadow:
- 0 0 0 1px rgba(246, 196, 83, 0.22),
- 0 0 22px rgba(139, 92, 246, 0.24);
-}
-
-.nds-large-icon-picker-trigger img,
-.nds-large-icon-picker-trigger .ui-icon {
- width: 72px;
- height: 72px;
- object-fit: contain;
-}
-
-.nds-event-surface {
- --nds-event-color: #8b5cf6;
- box-sizing: border-box;
- width: min(210px, 100%);
- min-height: 44px;
- overflow: hidden;
- border: 1px solid color-mix(in srgb, var(--nds-event-color) 24%, rgba(255, 255, 255, 0.22));
- border-radius: var(--radius-sm);
- background: color-mix(in srgb, var(--nds-event-color) 82%, transparent);
- box-shadow: 0 10px 22px rgba(0, 0, 0, 0.22);
-}
-
-.nds-event-surface[data-size="md"] {
- min-height: 44px;
-}
-
-.nds-event-surface[data-size="sm"] {
- min-height: 22px;
-}
-
-.nds-event-surface[data-size="xs"] {
- min-height: 10px;
- border-width: 0;
- border-radius: 4px;
-}
-
-.nds-event-surface[data-variant="dark"] {
- border-color: color-mix(in srgb, var(--nds-event-color) 42%, rgba(11, 14, 27, 0.28));
- background: color-mix(in srgb, var(--nds-event-color) 88%, transparent);
-}
-
-.nds-event-surface button {
- @include controls.event-surface-button;
- @include controls.event-surface-contrast-default;
-}
-
-.nds-event-surface button:hover,
-.nds-event-surface button:focus-visible {
- @include controls.event-surface-button-hover;
-}
-
-.nds-event-surface[data-size="sm"] button {
- display: block;
- padding: 4px 7px;
- text-align: center;
-}
-
-.nds-event-surface[data-size="xs"] button {
- position: relative;
- display: block;
- padding: 0 4px;
- text-align: center;
-}
-
-.nds-event-surface[data-size="xs"] button:hover,
-.nds-event-surface[data-size="xs"] button:focus-visible {
- background: rgba(0, 0, 0, 0.18);
-}
-
-.nds-event-surface[data-size="sm"] button > span {
- display: none;
-}
-
-.nds-event-surface[data-size="xs"] button > span {
- display: none;
-}
-
-.nds-event-surface strong,
-.nds-event-surface span {
- display: block;
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.nds-event-surface[data-variant="dark"] button {
- @include controls.event-surface-contrast-dark;
-}
-
-.nds-event-surface strong {
- font-size: 11px;
- line-height: 14px;
-}
-
-.nds-event-surface[data-size="sm"] strong {
- max-width: 100%;
- line-height: 14px;
-}
-
-.nds-event-surface[data-size="xs"] strong {
- position: absolute;
- top: 0;
- right: 4px;
- left: 4px;
- max-width: 100%;
- height: 10px;
- font-size: 8px;
- line-height: 10px;
- text-shadow: 0 1px 1px rgba(0, 0, 0, 0.45);
-}
-
-.nds-event-surface span {
- font-size: 10px;
- line-height: 12px;
- opacity: 0.86;
-}
-
-.nds-scrollbox,
-.nds-textarea {
- width: 100%;
- @include controls.scrollbar-control;
-}
-
-.nds-scrollbox {
- max-height: 400px;
- overflow: auto;
- padding: 12px;
- border: 1px solid rgba(165, 180, 252, 0.12);
- border-radius: var(--radius-md);
- background: rgba(5, 7, 17, 0.34);
- color: var(--color-text-secondary);
-}
-
-.nds-scrollbox[data-variant="ghost"] {
- padding: 0;
- border: 0;
- border-radius: 0;
- background: transparent;
-}
-
-.nds-scrollbox[data-axis="horizontal"] {
- max-height: none;
- overflow-x: auto;
- overflow-y: hidden;
- white-space: nowrap;
-}
-
-.nds-input:focus-visible,
-.nds-time:focus-visible,
-.nds-select:focus-visible,
-.nds-textarea:focus-visible,
-.nds-inline-input:focus-visible,
-.nds-button:focus-visible,
-.nds-icon-button:focus-visible,
-.nds-switch:focus-visible,
-.nds-radio-button-group button:focus-visible,
-.nds-checkbox:focus-visible,
-.nds-select-trigger:focus-visible,
-.nds-date-trigger:focus-visible,
-.nds-large-icon-picker-trigger:focus-visible {
- @include controls.field-focus;
- outline: 2px solid var(--color-accent-gold);
- outline-offset: 3px;
-}
-
-.nds-button:disabled,
-.nds-icon-button:disabled,
-.nds-quantity-stepper button:disabled,
-.nds-select:disabled,
-.nds-select-trigger:disabled,
-.nds-input:disabled,
-.nds-time:disabled,
-.nds-textarea:disabled,
-.nds-date-trigger:disabled,
-.nds-large-icon-picker-trigger:disabled {
- @include controls.control-disabled;
-}
+// Rôle : assemble l'API publique NDS par classes de famille et attributs data-*.
+@use "attributes/fields";
+@use "attributes/buttons";
+@use "attributes/steppers";
+@use "attributes/switches";
+@use "attributes/checkboxes";
+@use "attributes/menus";
+@use "attributes/events";
+@use "attributes/states";
diff --git a/website/src/styles/design-system/_controls.scss b/website/src/styles/design-system/_controls.scss
index 3c5c916..aea044f 100644
--- a/website/src/styles/design-system/_controls.scss
+++ b/website/src/styles/design-system/_controls.scss
@@ -1,1693 +1,9 @@
-// Rôle : fournit la source Sass commune des contrôles interactifs migrés.
-@use "../mixins";
-
-@mixin select-control($size: md) {
- $height: 44px;
- $padding-right: 40px;
- $padding-left: 14px;
- $border-alpha: 0.12;
- $chevron-offset: 14px;
-
- @if $size == sm {
- $height: 36px;
- $padding-right: 34px;
- $padding-left: 12px;
- $border-alpha: 0.14;
- $chevron-offset: 10px;
- } @else if $size == xs {
- $height: 32px;
- $padding-right: 30px;
- $padding-left: 10px;
- $border-alpha: 0.14;
- $chevron-offset: 9px;
- }
-
- min-height: $height;
- overflow: hidden;
- padding: 0 $padding-right 0 $padding-left;
- border: 1px solid rgba(165, 180, 252, $border-alpha);
- border-radius: var(--radius-md);
- appearance: none;
- -webkit-appearance: none;
- color: var(--color-text-primary);
- font: inherit;
- text-overflow: ellipsis;
- background-color: rgba(21, 26, 48, 0.86);
- background-image:
- url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23c4b5fd' viewBox='-6.5 0 32 32'%3E%3Cpath d='M18.813 11.406l-7.906 9.906c-.75.906-1.906.906-2.625 0L.376 11.406c-.75-.938-.375-1.656.781-1.656h16.875c1.188 0 1.531.719.781 1.656z'/%3E%3C/svg%3E"),
- linear-gradient(135deg, rgba(139, 92, 246, 0.04), rgba(31, 41, 78, 0.08)),
- linear-gradient(rgba(21, 26, 48, 0.42), rgba(21, 26, 48, 0.42));
- background-position:
- calc(100% - #{$chevron-offset}) 50%,
- 0 0,
- 0 0;
- background-size:
- 11px 11px,
- auto,
- auto;
- background-repeat: no-repeat;
-}
-
-@mixin select-control-hover {
- border-color: rgba(196, 181, 253, 0.22);
- background-color: rgba(21, 26, 48, 0.92);
- background-image:
- url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23f6c453' viewBox='-6.5 0 32 32'%3E%3Cpath d='M18.813 11.406l-7.906 9.906c-.75.906-1.906.906-2.625 0L.376 11.406c-.75-.938-.375-1.656.781-1.656h16.875c1.188 0 1.531.719.781 1.656z'/%3E%3C/svg%3E"),
- linear-gradient(135deg, rgba(246, 196, 83, 0.06), rgba(139, 92, 246, 0.08)),
- linear-gradient(rgba(21, 26, 48, 0.58), rgba(21, 26, 48, 0.58));
- color: var(--color-text-primary);
-}
-
-@mixin select-trigger($size: md) {
- $height: 44px;
- $padding-right: 40px;
- $padding-left: 12px;
- $icon-size: 16px;
- $font-size: inherit;
- $border-alpha: 0.12;
- $chevron-offset: 14px;
- $chevron-size: 11px;
-
- @if $size == sm {
- $height: 36px;
- $padding-right: 34px;
- $padding-left: 10px;
- $icon-size: 14px;
- $font-size: var(--font-size-sm);
- $border-alpha: 0.14;
- $chevron-offset: 10px;
- } @else if $size == xs {
- $height: 32px;
- $padding-right: 30px;
- $padding-left: 8px;
- $icon-size: 13px;
- $font-size: var(--font-size-xs);
- $border-alpha: 0.14;
- $chevron-offset: 9px;
- }
-
- position: relative;
- display: grid;
- grid-template-columns: $icon-size minmax(0, 1fr);
- align-items: center;
- gap: 6px;
- width: 100%;
- min-height: $height;
- padding: 0 $padding-right 0 $padding-left;
- border: 1px solid rgba(165, 180, 252, $border-alpha);
- border-radius: var(--radius-md);
- background-color: rgba(21, 26, 48, 0.86);
- background-image:
- linear-gradient(135deg, rgba(139, 92, 246, 0.04), rgba(31, 41, 78, 0.08)),
- linear-gradient(rgba(21, 26, 48, 0.42), rgba(21, 26, 48, 0.42));
- background-position:
- 0 0,
- 0 0;
- background-size:
- auto,
- auto;
- background-repeat: no-repeat;
- color: var(--color-text-primary);
- font: inherit;
- font-size: $font-size;
- font-weight: 900;
- font-variant-numeric: tabular-nums;
- text-align: left;
- transition:
- border-color var(--duration-fast) var(--ease-standard),
- background-color var(--duration-fast) var(--ease-standard),
- color var(--duration-fast) var(--ease-standard),
- box-shadow var(--duration-fast) var(--ease-standard);
-
- &::after {
- content: "";
- position: absolute;
- top: 50%;
- right: $chevron-offset;
- width: $chevron-size;
- height: $chevron-size;
- background-color: #c4b5fd;
- mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-6.5 0 32 32'%3E%3Cpath d='M18.813 11.406l-7.906 9.906c-.75.906-1.906.906-2.625 0L.376 11.406c-.75-.938-.375-1.656.781-1.656h16.875c1.188 0 1.531.719.781 1.656z'/%3E%3C/svg%3E") center / contain no-repeat;
- pointer-events: none;
- transform: translateY(-50%);
- transition: background-color var(--duration-fast) var(--ease-standard);
- }
-}
-
-@mixin select-trigger-hover($size: md) {
- border-color: rgba(196, 181, 253, 0.22);
- background-color: rgba(21, 26, 48, 0.92);
- background-image:
- linear-gradient(135deg, rgba(246, 196, 83, 0.06), rgba(139, 92, 246, 0.08)),
- linear-gradient(rgba(21, 26, 48, 0.58), rgba(21, 26, 48, 0.58));
- background-position: 0 0, 0 0;
- background-size: auto, auto;
- background-repeat: no-repeat;
- color: var(--color-text-primary);
-
- &::after {
- background-color: var(--color-accent-gold);
- }
-}
-
-@mixin select-trigger-icon($size: md) {
- $icon-size: 16px;
-
- @if $size == sm {
- $icon-size: 14px;
- } @else if $size == xs {
- $icon-size: 13px;
- }
-
- width: $icon-size;
- height: $icon-size;
- color: var(--color-text-secondary);
-}
-
-@mixin select-option {
- background: #11182f;
- color: var(--color-text-primary);
-}
-
-@mixin text-button($size: md) {
- $height: 42px;
- $padding: 0 18px;
- $font-size: inherit;
-
- @if $size == sm {
- $height: 34px;
- $padding: 0 12px;
- $font-size: var(--font-size-sm);
- } @else if $size == xs {
- $height: 30px;
- $padding: 0 10px;
- $font-size: var(--font-size-xs);
- }
-
- display: inline-flex;
- min-height: $height;
- align-items: center;
- justify-content: center;
- gap: 8px;
- padding: $padding;
- border: 1px solid var(--color-border);
- border-radius: var(--radius-md);
- background: rgba(21, 26, 48, 0.92);
- color: var(--color-text-primary);
- font: inherit;
- font-size: $font-size;
- cursor: pointer;
- transition:
- transform var(--duration-fast) var(--ease-standard),
- border-color var(--duration-fast) var(--ease-standard),
- background-color var(--duration-fast) var(--ease-standard),
- color var(--duration-fast) var(--ease-standard),
- box-shadow var(--duration-fast) var(--ease-standard);
-}
-
-@mixin text-button-hover {
- border-color: rgba(196, 181, 253, 0.34);
- background:
- linear-gradient(135deg, rgba(31, 37, 68, 0.96), rgba(22, 26, 51, 0.96)) padding-box,
- linear-gradient(135deg, rgba(139, 92, 246, 0.32), rgba(196, 181, 253, 0.2)) border-box;
- box-shadow:
- 0 0 14px rgba(139, 92, 246, 0.12),
- inset 0 1px 0 rgba(255, 255, 255, 0.05);
-}
-
-@mixin text-button-primary {
- border-color: rgba(246, 196, 83, 0.34);
- background:
- linear-gradient(135deg, rgba(72, 43, 160, 0.96), rgba(45, 52, 146, 0.94)) padding-box,
- linear-gradient(135deg, rgba(246, 196, 83, 0.66), rgba(139, 92, 246, 0.36)) border-box;
- box-shadow:
- 0 8px 22px rgba(5, 7, 17, 0.22),
- inset 0 1px 0 rgba(255, 255, 255, 0.08);
- color: #fff;
- font-weight: 700;
-}
-
-@mixin text-button-primary-hover {
- border-color: rgba(246, 196, 83, 0.72);
- background:
- radial-gradient(circle at 100% 0, rgba(246, 196, 83, 0.24), transparent 42%) padding-box,
- linear-gradient(135deg, rgba(125, 62, 232, 0.98), rgba(62, 72, 184, 0.98)) padding-box,
- linear-gradient(135deg, rgba(255, 219, 128, 0.98), rgba(139, 92, 246, 0.82), rgba(34, 211, 238, 0.42)) border-box;
- box-shadow:
- 0 0 0 1px rgba(246, 196, 83, 0.18),
- 0 0 18px rgba(139, 92, 246, 0.26),
- 0 0 24px rgba(246, 196, 83, 0.14),
- inset 0 1px 0 rgba(255, 255, 255, 0.2),
- inset 0 -2px 0 rgba(246, 196, 83, 0.24);
-}
-
-@mixin text-button-secondary {
- border-color: rgba(196, 181, 253, 0.18);
- background:
- linear-gradient(135deg, rgba(31, 37, 68, 0.72), rgba(15, 23, 42, 0.72)) padding-box,
- linear-gradient(135deg, rgba(139, 92, 246, 0.3), rgba(34, 211, 238, 0.14)) border-box;
- color: var(--color-text-primary);
-}
-
-@mixin text-button-ghost {
- border-color: transparent;
- background: transparent;
- color: var(--color-text-secondary);
- box-shadow: none;
-}
-
-@mixin text-button-ghost-hover {
- border-color: rgba(196, 181, 253, 0.18);
- background: rgba(139, 92, 246, 0.08);
- color: var(--color-text-primary);
- box-shadow: none;
-}
-
-@mixin text-button-dashed {
- border-style: dashed;
- border-color: rgba(246, 196, 83, 0.28);
- background:
- linear-gradient(135deg, rgba(246, 196, 83, 0.04), rgba(139, 92, 246, 0.06)),
- rgba(7, 10, 24, 0.72);
- color: var(--color-text-secondary);
- font-weight: 900;
- box-shadow: 0 10px 18px rgba(0, 0, 0, 0.16);
-}
-
-@mixin text-button-dashed-hover {
- border-color: rgba(246, 196, 83, 0.54);
- background:
- linear-gradient(135deg, rgba(246, 196, 83, 0.1), rgba(139, 92, 246, 0.1)),
- rgba(13, 17, 34, 0.82);
- color: var(--color-text-primary);
- box-shadow:
- 0 0 16px rgba(246, 196, 83, 0.12),
- 0 10px 18px rgba(0, 0, 0, 0.16);
-}
-
-@mixin text-button-danger {
- color: var(--color-danger);
-}
-
-@mixin text-button-danger-hover {
- border-color: rgba(251, 113, 133, 0.48);
- background:
- radial-gradient(circle at 100% 0, rgba(251, 113, 133, 0.22), transparent 42%) padding-box,
- linear-gradient(135deg, rgba(96, 22, 45, 0.94), rgba(58, 16, 34, 0.96)) padding-box,
- linear-gradient(135deg, rgba(251, 113, 133, 0.82), rgba(244, 114, 182, 0.42), rgba(196, 181, 253, 0.18)) border-box;
- color: #fff;
- box-shadow:
- 0 0 0 1px rgba(251, 113, 133, 0.12),
- 0 0 16px rgba(251, 113, 133, 0.16),
- inset 0 1px 0 rgba(255, 255, 255, 0.08),
- inset 0 -2px 0 rgba(251, 113, 133, 0.24);
-}
-
-@mixin text-button-active {
- @include mixins.temporary-toggle-active;
-}
-
-@mixin list-row-button {
- overflow: hidden;
- border: 1px solid rgba(150, 165, 205, 0.12);
- border-radius: var(--radius-sm);
- background: rgba(5, 7, 17, 0.36);
-}
-
-@mixin list-row-button-hover {
- border-color: rgba(150, 165, 205, 0.24);
- background: rgba(5, 7, 17, 0.5);
-}
-
-@mixin list-row-button-active {
- border-color: rgba(246, 196, 83, 0.42);
- background: rgba(246, 196, 83, 0.08);
-}
-
-@mixin list-row-button-content {
- display: inline-flex;
- width: 100%;
- min-height: 32px;
- align-items: center;
- justify-content: flex-start;
- overflow: hidden;
- padding: 0 8px;
- border-color: transparent;
- background: transparent;
- color: var(--color-text-primary);
- font-size: var(--font-size-sm);
- font-weight: 900;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-@mixin control-disabled {
- box-shadow: none;
- cursor: not-allowed;
- opacity: 0.48;
-}
-
-@mixin field-control($size: md) {
- $height: 44px;
- $padding: 0 14px;
- $font-size: inherit;
-
- @if $size == sm {
- $height: 36px;
- $padding: 0 10px;
- $font-size: var(--font-size-sm);
- } @else if $size == xs {
- $height: 32px;
- $padding: 0 8px;
- $font-size: var(--font-size-xs);
- }
-
- box-sizing: border-box;
- width: 100%;
- min-width: 0;
- min-height: $height;
- padding: $padding;
- border: 1px solid var(--color-border);
- border-radius: var(--radius-md);
- background:
- linear-gradient(135deg, rgba(139, 92, 246, 0.04), rgba(31, 41, 78, 0.08)),
- rgba(21, 26, 48, 0.38);
- color: var(--color-text-primary);
- font: inherit;
- font-size: $font-size;
- transition:
- border-color var(--duration-fast) var(--ease-standard),
- background-color var(--duration-fast) var(--ease-standard),
- color var(--duration-fast) var(--ease-standard),
- box-shadow var(--duration-fast) var(--ease-standard);
-}
-
-@mixin field-textarea($size: md) {
- $min-height: 108px;
- $padding: 14px 20px 20px 14px;
-
- @if $size == sm {
- $min-height: 88px;
- $padding: 12px 16px 16px 10px;
- } @else if $size == xs {
- $min-height: 72px;
- $padding: 10px 14px 14px 8px;
- }
-
- min-height: $min-height;
- padding: $padding;
- overflow: auto;
- resize: vertical;
- scrollbar-gutter: stable;
-}
-
-@mixin field-textarea-ghost {
- border-color: transparent;
- background: transparent;
- box-shadow: none;
- color: inherit;
-}
-
-@mixin field-placeholder {
- color: var(--color-text-muted);
- opacity: 0.58;
-}
-
-@mixin field-hover {
- border-color: rgba(196, 181, 253, 0.22);
- background:
- linear-gradient(135deg, rgba(246, 196, 83, 0.04), rgba(139, 92, 246, 0.08)),
- rgba(21, 26, 48, 0.5);
-}
-
-@mixin field-focus {
- border-color: var(--color-primary);
- box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.16);
-}
-
-@mixin time-control($size: md) {
- @include field-control($size);
- padding-right: 8px;
- color-scheme: dark;
- font-variant-numeric: tabular-nums;
-
- &::-webkit-calendar-picker-indicator {
- width: 16px;
- height: 16px;
- margin-left: 4px;
- border-radius: var(--radius-sm);
- cursor: pointer;
- filter: invert(77%) sepia(14%) saturate(810%) hue-rotate(209deg) brightness(99%) contrast(92%);
- opacity: 0.82;
- transition:
- background-color var(--duration-fast) var(--ease-standard),
- filter var(--duration-fast) var(--ease-standard),
- opacity var(--duration-fast) var(--ease-standard);
- }
-
- &::-webkit-datetime-edit {
- min-width: 0;
- color: var(--color-text-primary);
- }
-
- &::-webkit-datetime-edit-fields-wrapper {
- padding: 0;
- }
-
- &::-webkit-datetime-edit-hour-field,
- &::-webkit-datetime-edit-minute-field,
- &::-webkit-datetime-edit-ampm-field {
- border-radius: 4px;
- }
-
- &::-webkit-datetime-edit-hour-field:focus,
- &::-webkit-datetime-edit-minute-field:focus,
- &::-webkit-datetime-edit-ampm-field:focus {
- background: rgba(139, 92, 246, 0.28);
- color: var(--color-text-primary);
- }
-
- &:hover::-webkit-calendar-picker-indicator,
- &:focus::-webkit-calendar-picker-indicator {
- background-color: rgba(246, 196, 83, 0.12);
- filter: invert(80%) sepia(77%) saturate(455%) hue-rotate(346deg) brightness(101%) contrast(93%);
- opacity: 1;
- }
-
- &:disabled::-webkit-calendar-picker-indicator {
- cursor: not-allowed;
- opacity: 0.42;
- }
-}
-
-@mixin field-disabled {
- border-color: rgba(165, 180, 252, 0.08);
- background:
- linear-gradient(135deg, rgba(148, 163, 184, 0.05), transparent 62%),
- rgba(15, 23, 42, 0.42);
- box-shadow: none;
- color: rgba(203, 213, 225, 0.66);
- cursor: not-allowed;
-}
-
-@mixin inline-field($size: md) {
- $height: 30px;
- $padding: 5px 8px;
- $radius: var(--radius-md);
-
- @if $size == sm {
- $height: 28px;
- $padding: 4px 7px;
- $radius: var(--radius-sm);
- } @else if $size == xs {
- $height: 26px;
- $padding: 3px 6px;
- $radius: var(--radius-sm);
- }
-
- box-sizing: border-box;
- width: 100%;
- min-width: 0;
- min-height: $height;
- padding: $padding;
- overflow: hidden;
- border: 1px solid transparent;
- border-radius: $radius;
- background: transparent;
- color: var(--color-text-primary);
- font: inherit;
- text-overflow: ellipsis;
- white-space: nowrap;
- transition:
- border-color var(--duration-fast) var(--ease-standard),
- background-color var(--duration-fast) var(--ease-standard),
- color var(--duration-fast) var(--ease-standard),
- box-shadow var(--duration-fast) var(--ease-standard);
-}
-
-@mixin inline-field-hover {
- border-color: rgba(246, 196, 83, 0.34);
- background: rgba(21, 26, 48, 0.42);
-}
-
-@mixin inline-textarea {
- box-sizing: border-box;
- width: 100%;
- min-width: 0;
- border: 0;
- background: transparent;
- color: var(--color-text-primary);
- font: inherit;
- resize: vertical;
-}
-
-@mixin table-header-field {
- width: 100%;
- padding: 0 8px;
- border-top: 0;
- border-left: 0;
- border-radius: 0;
- text-align: center;
-}
-
-@mixin table-header-field-focus {
- z-index: 5;
- border-color: rgba(246, 196, 83, 0.38);
- background: rgba(21, 26, 48, 0.8);
- color: var(--color-text-primary);
- box-shadow: inset 0 0 0 1px rgba(246, 196, 83, 0.55), 0 0 0 2px rgba(139, 92, 246, 0.18);
- outline: 0;
-}
-
-@mixin table-cell-field {
- width: 100%;
- min-width: 0;
- min-height: 34px;
- padding: 6px 8px;
- border: 0;
- border-right: 1px solid rgba(165, 180, 252, 0.08);
- border-bottom: 1px solid rgba(165, 180, 252, 0.08);
- border-radius: 0;
- background: rgba(16, 20, 38, 0.34);
- color: var(--color-text-primary);
- caret-color: transparent;
- font-size: var(--font-size-sm);
- font-variant-numeric: tabular-nums;
-}
-
-@mixin table-cell-field-focus {
- position: relative;
- z-index: 4;
- border-color: rgba(34, 211, 238, 0.42);
- background:
- linear-gradient(135deg, rgba(34, 211, 238, 0.08), rgba(139, 92, 246, 0.06)),
- rgba(21, 26, 48, 0.58);
- box-shadow: inset 0 0 0 1px rgba(34, 211, 238, 0.58);
- outline: 0;
-}
-
-@mixin table-cell-field-editing-focus {
- border-color: rgba(246, 196, 83, 0.38);
- background: rgba(21, 26, 48, 0.72);
- caret-color: auto;
- box-shadow: inset 0 0 0 1px rgba(246, 196, 83, 0.55), 0 0 0 2px rgba(139, 92, 246, 0.18);
-}
-
-@mixin time-parts-control($size: md) {
- $height: 44px;
- $part-height: 42px;
- $column-min: 34px;
-
- @if $size == sm {
- $height: 38px;
- $part-height: 36px;
- $column-min: 22px;
- }
-
- display: grid;
- grid-template-columns: minmax($column-min, 1fr) 4px minmax($column-min, 1fr) 4px minmax($column-min, 1fr);
- align-items: center;
- min-height: $height;
- overflow: hidden;
- border: 1px solid var(--color-border);
- border-radius: var(--radius-md);
- background: rgba(15, 23, 42, 0.64);
-
- input {
- width: auto;
- min-width: 0;
- min-height: $part-height;
- padding: 0;
- border: 0;
- border-radius: 0;
- background: transparent;
- text-align: center;
- box-shadow: none;
- }
-}
-
-@mixin time-parts-focus {
- border-color: var(--color-primary);
- box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.16);
-}
-
-@mixin icon-button($size: md) {
- $control-size: 40px;
-
- @if $size == sm {
- $control-size: 34px;
- } @else if $size == xs {
- $control-size: 30px;
- } @else if $size == xxs {
- $control-size: 26px;
- } @else if $size == tiny {
- $control-size: 24px;
- } @else if $size == micro {
- $control-size: 22px;
- }
-
- display: inline-flex;
- width: $control-size;
- height: $control-size;
- min-width: $control-size;
- min-height: $control-size;
- align-items: center;
- justify-content: center;
- padding: 0;
- border: 1px solid rgba(165, 180, 252, 0.12);
- border-radius: var(--radius-md);
- background: rgba(21, 26, 48, 0.88);
- color: var(--color-text-secondary);
- font: inherit;
- cursor: pointer;
- transition:
- transform var(--duration-fast) var(--ease-standard),
- border-color var(--duration-fast) var(--ease-standard),
- background-color var(--duration-fast) var(--ease-standard),
- color var(--duration-fast) var(--ease-standard),
- box-shadow var(--duration-fast) var(--ease-standard);
-}
-
-@mixin icon-button-icon($size: md) {
- $icon-size: 21px;
-
- @if $size == sm {
- $icon-size: 19px;
- } @else if $size == xs {
- $icon-size: 17px;
- } @else if $size == xxs {
- $icon-size: 15px;
- } @else if $size == tiny {
- $icon-size: 13px;
- } @else if $size == micro {
- $icon-size: 12px;
- }
-
- width: $icon-size;
- height: $icon-size;
- transition:
- background-color var(--duration-fast) var(--ease-standard),
- filter var(--duration-fast) var(--ease-standard);
-}
-
-@mixin icon-button-hover {
- border-color: rgba(196, 181, 253, 0.32);
- background:
- linear-gradient(135deg, rgba(31, 37, 68, 0.94), rgba(18, 22, 44, 0.96)) padding-box,
- linear-gradient(135deg, rgba(139, 92, 246, 0.3), rgba(196, 181, 253, 0.2)) border-box;
- color: var(--color-text-primary);
- box-shadow:
- 0 0 12px rgba(139, 92, 246, 0.1),
- inset 0 1px 0 rgba(255, 255, 255, 0.05);
-}
-
-@mixin icon-button-hover-icon {
- background-color: #c4b5fd;
- filter: drop-shadow(0 0 6px rgba(196, 181, 253, 0.28));
-}
-
-@mixin icon-button-primary {
- border-color: rgba(246, 196, 83, 0.34);
- background:
- linear-gradient(135deg, rgba(72, 43, 160, 0.96), rgba(45, 52, 146, 0.94)) padding-box,
- linear-gradient(135deg, rgba(246, 196, 83, 0.66), rgba(139, 92, 246, 0.36)) border-box;
- color: #fff;
- box-shadow:
- 0 8px 22px rgba(5, 7, 17, 0.22),
- inset 0 1px 0 rgba(255, 255, 255, 0.08);
-}
-
-@mixin icon-button-primary-hover {
- border-color: rgba(246, 196, 83, 0.72);
- background:
- radial-gradient(circle at 100% 0, rgba(246, 196, 83, 0.24), transparent 42%) padding-box,
- linear-gradient(135deg, rgba(125, 62, 232, 0.98), rgba(62, 72, 184, 0.98)) padding-box,
- linear-gradient(135deg, rgba(255, 219, 128, 0.98), rgba(139, 92, 246, 0.82), rgba(34, 211, 238, 0.42)) border-box;
- color: #fff;
- box-shadow:
- 0 0 0 1px rgba(246, 196, 83, 0.18),
- 0 0 18px rgba(139, 92, 246, 0.26),
- 0 0 24px rgba(246, 196, 83, 0.14),
- inset 0 1px 0 rgba(255, 255, 255, 0.2),
- inset 0 -2px 0 rgba(246, 196, 83, 0.24);
-}
-
-@mixin icon-button-primary-hover-icon {
- background-color: #fff;
- filter: drop-shadow(0 0 6px rgba(246, 196, 83, 0.3));
-}
-
-@mixin icon-button-ghost {
- border-color: transparent;
- background: transparent;
- color: var(--color-text-muted);
- box-shadow: none;
-}
-
-@mixin icon-button-danger {
- border-color: rgba(251, 113, 133, 0.2);
- color: var(--color-danger);
-}
-
-@mixin icon-button-danger-hover {
- border-color: rgba(251, 113, 133, 0.56);
- background:
- radial-gradient(circle at 100% 0, rgba(251, 113, 133, 0.22), transparent 42%) padding-box,
- linear-gradient(135deg, rgba(96, 22, 45, 0.94), rgba(58, 16, 34, 0.96)) padding-box,
- linear-gradient(135deg, rgba(251, 113, 133, 0.82), rgba(244, 114, 182, 0.42), rgba(196, 181, 253, 0.18)) border-box;
- color: #fff;
- box-shadow:
- 0 0 0 1px rgba(251, 113, 133, 0.12),
- 0 0 16px rgba(251, 113, 133, 0.16),
- inset 0 1px 0 rgba(255, 255, 255, 0.08),
- inset 0 -2px 0 rgba(251, 113, 133, 0.24);
-}
-
-@mixin icon-button-danger-hover-icon {
- background-color: #fff;
- filter: drop-shadow(0 0 6px rgba(251, 113, 133, 0.26));
-}
-
-@mixin icon-button-active {
- @include mixins.temporary-toggle-active;
-}
-
-@mixin button-edge($edge: right) {
- @if $edge == left {
- border-radius: var(--radius-sm) 0 0 var(--radius-sm);
- } @else if $edge == middle {
- border-radius: 0;
- } @else if $edge == top {
- border-radius: var(--radius-sm) var(--radius-sm) 0 0;
- } @else if $edge == center {
- border-radius: 0;
- } @else if $edge == bottom {
- border-radius: 0 0 var(--radius-sm) var(--radius-sm);
- } @else if $edge == top-left {
- border-radius: var(--radius-sm) 0 0 0;
- } @else if $edge == top-right {
- border-radius: 0 var(--radius-sm) 0 0;
- } @else if $edge == bottom-right {
- border-radius: 0 0 var(--radius-sm) 0;
- } @else if $edge == bottom-left {
- border-radius: 0 0 0 var(--radius-sm);
- } @else {
- border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
- }
-}
-
-@mixin button-joined-border($edge: right) {
- @if $edge == left {
- border-right-color: transparent;
- } @else if $edge == middle {
- border-right-color: transparent;
- border-left-color: transparent;
- } @else if $edge == right {
- border-left-color: transparent;
- } @else if $edge == top {
- border-bottom-color: transparent;
- } @else if $edge == center {
- border-top-color: transparent;
- border-bottom-color: transparent;
- } @else if $edge == bottom {
- border-top-color: transparent;
- } @else if $edge == top-left {
- border-right-color: transparent;
- border-bottom-color: transparent;
- } @else if $edge == top-right {
- border-left-color: transparent;
- border-bottom-color: transparent;
- } @else if $edge == bottom-right {
- border-left-color: transparent;
- border-top-color: transparent;
- } @else if $edge == bottom-left {
- border-right-color: transparent;
- border-top-color: transparent;
- }
-}
-
-@mixin button-borderless {
- border-color: transparent;
-}
-
-@mixin icon-button-edge($edge: right) {
- @include button-edge($edge);
-}
-
-@mixin icon-button-panel-toggle {
- background: rgba(39, 18, 69, 0.86);
- color: var(--color-text-primary);
-}
-
-@mixin icon-button-panel-toggle-hover {
- border-color: rgba(246, 196, 83, 0.44);
- background: rgba(56, 28, 92, 0.94);
- color: var(--color-accent-gold);
-}
-
-@mixin button-split($position: start, $axis: horizontal) {
- @if $axis == vertical {
- @if $position == start {
- @include button-edge(top);
- } @else {
- @include button-edge(bottom);
- border-top-color: transparent;
- }
- } @else {
- @if $position == start {
- @include button-edge(left);
- } @else {
- @include button-edge(right);
- border-left-color: transparent;
- }
- }
-}
-
-@mixin toolbar-button($size: sm) {
- @include icon-button($size);
- display: inline-grid;
- place-items: center;
- font-size: var(--font-size-sm);
- font-weight: 900;
- box-shadow: none;
-}
-
-@mixin toolbar-button-icon($size: sm) {
- @include icon-button-icon($size);
-}
-
-@mixin toolbar-button-hover {
- @include icon-button-hover;
-}
-
-@mixin toolbar-button-active {
- @include icon-button-active;
-}
-
-@mixin quantity-stepper($size: md) {
- $button-size: 28px;
- $value-min: 52px;
- $gap: 6px;
- $font-size: var(--font-size-sm);
-
- @if $size == sm {
- $button-size: 18px;
- $value-min: 34px;
- $gap: 2px;
- $font-size: 0.68rem;
- } @else if $size == xs {
- $button-size: 16px;
- $value-min: 30px;
- $gap: 2px;
- $font-size: 0.64rem;
- }
-
- display: inline-grid;
- grid-template-columns: $button-size minmax($value-min, auto) $button-size;
- flex: 0 0 auto;
- align-items: center;
- gap: $gap;
- color: var(--color-text-secondary);
- font-size: $font-size;
- font-weight: 800;
- white-space: nowrap;
-}
-
-@mixin quantity-stepper-button($size: md) {
- $button-size: 28px;
-
- @if $size == sm {
- $button-size: 18px;
- } @else if $size == xs {
- $button-size: 16px;
- }
-
- display: inline-grid;
- width: $button-size;
- min-width: $button-size;
- height: $button-size;
- min-height: $button-size;
- place-items: center;
- padding: 0;
- border: 1px solid rgba(165, 180, 252, 0.12);
- border-radius: var(--radius-md);
- background: rgba(21, 26, 48, 0.88);
- color: var(--color-text-secondary);
- line-height: 1;
-}
-
-@mixin quantity-stepper-button-hover {
- border-color: rgba(196, 181, 253, 0.32);
- background:
- linear-gradient(135deg, rgba(31, 37, 68, 0.94), rgba(18, 22, 44, 0.96)) padding-box,
- linear-gradient(135deg, rgba(139, 92, 246, 0.3), rgba(196, 181, 253, 0.2)) border-box;
- color: var(--color-text-primary);
- box-shadow:
- 0 0 10px rgba(139, 92, 246, 0.1),
- inset 0 1px 0 rgba(255, 255, 255, 0.05);
-}
-
-@mixin quantity-stepper-button-icon($size: md) {
- $icon-size: 16px;
-
- @if $size == sm {
- $icon-size: 10px;
- } @else if $size == xs {
- $icon-size: 9px;
- }
-
- width: $icon-size;
- height: $icon-size;
- flex: 0 0 $icon-size;
-}
-
-@mixin quantity-stepper-value {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 3px;
- color: var(--color-text-secondary);
- text-align: center;
-}
-
-@mixin quantity-stepper-input {
- box-sizing: border-box;
- width: max(18px, calc((var(--qty-current-digits, 1) * 0.72rem) + 8px));
- min-height: 28px;
- padding: 0 4px;
- border: 1px solid transparent;
- border-radius: var(--radius-sm);
- background: transparent;
- color: var(--color-text-primary);
- font: inherit;
- text-align: right;
- -moz-appearance: textfield;
-}
-
-@mixin quantity-stepper-input-hover {
- border-color: rgba(246, 196, 83, 0.34);
- background: rgba(21, 26, 48, 0.42);
-}
-
-@mixin number-spinner-reset {
- -moz-appearance: textfield;
-
- &::-webkit-outer-spin-button,
- &::-webkit-inner-spin-button {
- margin: 0;
- appearance: none;
- -webkit-appearance: none;
- }
-}
-
-@mixin switch-control($size: md) {
- $icon-size: 16px;
- $track-width: 34px;
- $track-height: 18px;
- $thumb-size: 12px;
- $thumb-left: 3px;
- $active-left: 17px;
- $height: 34px;
- $gap: 8px;
- $padding: 0 6px 0 10px;
- $radius: var(--radius-pill);
- $font-size: var(--font-size-sm);
-
- @if $size == lg {
- $track-width: 30px;
- $track-height: 16px;
- $thumb-size: 10px;
- $active-left: 15px;
- $height: 40px;
- $gap: 7px;
- $padding: 0 7px 0 10px;
- } @else if $size == sm {
- $icon-size: 13px;
- $track-width: 24px;
- $track-height: 14px;
- $thumb-size: 8px;
- $thumb-left: 2px;
- $active-left: 12px;
- $height: 28px;
- $gap: 5px;
- $padding: 0 5px 0 8px;
- $font-size: var(--font-size-xs);
- } @else if $size == xs {
- $icon-size: 13px;
- $track-width: 18px;
- $track-height: 12px;
- $thumb-size: 8px;
- $thumb-left: 2px;
- $active-left: 7px;
- $height: 26px;
- $gap: 3px;
- $padding: 0 4px;
- $radius: var(--radius-sm);
- $font-size: var(--font-size-xs);
- }
-
- --nds-switch-icon-size: #{$icon-size};
- --nds-switch-track-width: #{$track-width};
- --nds-switch-track-height: #{$track-height};
- --nds-switch-thumb-size: #{$thumb-size};
- --nds-switch-thumb-left: #{$thumb-left};
- --nds-switch-active-left: #{$active-left};
- display: inline-grid;
- grid-template-columns: $icon-size $track-width;
- width: fit-content;
- min-height: $height;
- align-items: center;
- gap: $gap;
- padding: $padding;
- border: 1px solid rgba(165, 180, 252, 0.12);
- border-radius: $radius;
- background: rgba(5, 7, 17, 0.24);
- color: var(--color-text-secondary);
- font-size: $font-size;
- font-weight: 800;
-}
-
-@mixin switch-icon {
- width: var(--nds-switch-icon-size);
- height: var(--nds-switch-icon-size);
-}
-
-@mixin switch-track {
- position: relative;
- display: block;
- width: var(--nds-switch-track-width);
- height: var(--nds-switch-track-height);
- border: 1px solid rgba(165, 180, 252, 0.18);
- border-radius: var(--radius-pill);
- background: rgba(7, 10, 24, 0.72);
-}
-
-@mixin switch-thumb {
- content: "";
- position: absolute;
- top: 50%;
- left: var(--nds-switch-thumb-left);
- width: var(--nds-switch-thumb-size);
- height: var(--nds-switch-thumb-size);
- border-radius: 50%;
- background: var(--color-text-muted);
- transform: translateY(-50%);
- transition:
- left var(--duration-fast) var(--ease-standard),
- background-color var(--duration-fast) var(--ease-standard),
- box-shadow var(--duration-fast) var(--ease-standard);
-}
-
-@mixin switch-hover {
- @include icon-button-hover;
-}
-
-@mixin switch-active {
- @include mixins.temporary-toggle-active;
-}
-
-@mixin switch-active-track {
- border-color: rgba(246, 196, 83, 0.36);
- background: rgba(246, 196, 83, 0.12);
-}
-
-@mixin switch-active-thumb {
- left: var(--nds-switch-active-left);
- background: var(--color-accent-gold);
- box-shadow: 0 0 10px rgba(246, 196, 83, 0.28);
-}
-
-@mixin native-switch-label {
- display: inline-flex;
- align-items: center;
- gap: 8px;
- min-width: 0;
- color: var(--color-text-secondary);
- font-size: var(--font-size-xs);
- font-weight: 900;
- cursor: pointer;
-}
-
-@mixin native-switch-input {
- position: absolute;
- opacity: 0;
- pointer-events: none;
-}
-
-@mixin radio-button-group-surface($size: md) {
- $column-min: 36px;
- $gap: 4px;
- $padding: 4px;
- $radius: var(--radius-md);
- $background: rgba(5, 7, 17, 0.38);
-
- @if $size == sm {
- $column-min: 30px;
- $gap: 3px;
- $padding: 3px;
- } @else if $size == xs {
- $column-min: 24px;
- $gap: 0;
- $padding: 2px;
- $radius: var(--radius-sm);
- $background: rgba(5, 7, 17, 0.26);
- }
-
- --nds-radio-button-group-column-min: #{$column-min};
- gap: $gap;
- max-width: 100%;
- padding: $padding;
- border: 1px solid rgba(165, 180, 252, 0.12);
- border-radius: $radius;
- background: $background;
-}
-
-@mixin radio-button-group($size: md) {
- @include radio-button-group-surface($size);
- display: inline-grid;
- grid-auto-flow: column;
- grid-auto-columns: minmax(var(--nds-radio-button-group-column-min), 1fr);
-}
-
-@mixin radio-button-group-option($size: md) {
- $height: 34px;
- $padding: 0 10px;
- $radius: calc(var(--radius-md) - 2px);
- $font-size: var(--font-size-sm);
- $icon-size: 16px;
-
- @if $size == sm {
- $height: 28px;
- $padding: 0;
- $font-size: var(--font-size-xs);
- $icon-size: 19px;
- } @else if $size == xs {
- $height: 26px;
- $padding: 0;
- $radius: var(--radius-sm);
- $font-size: var(--font-size-xs);
- $icon-size: 13px;
- }
-
- --nds-radio-button-group-option-icon-size: #{$icon-size};
- display: inline-flex;
- min-width: 0;
- min-height: $height;
- align-items: center;
- justify-content: center;
- gap: 8px;
- padding: $padding;
- border: 1px solid transparent;
- border-radius: $radius;
- background: transparent;
- color: var(--color-text-secondary);
- font-size: $font-size;
- font-weight: 900;
-
- > .ui-icon {
- width: var(--nds-radio-button-group-option-icon-size);
- height: var(--nds-radio-button-group-option-icon-size);
- min-width: var(--nds-radio-button-group-option-icon-size);
- min-height: var(--nds-radio-button-group-option-icon-size);
- flex: 0 0 var(--nds-radio-button-group-option-icon-size);
- }
-}
-
-@mixin radio-button-group-option-compact-overflow {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-@mixin radio-button-group-option-hover {
- border-color: rgba(196, 181, 253, 0.22);
- background: rgba(21, 26, 48, 0.5);
- color: var(--color-text-primary);
- box-shadow: none;
-}
-
-@mixin radio-button-group-option-active {
- @include mixins.temporary-toggle-active;
-}
-
-@mixin radio-button-group-option-icon($size: md) {
- $icon-size: 16px;
-
- @if $size == sm {
- $icon-size: 19px;
- } @else if $size == xs {
- $icon-size: 13px;
- }
-
- --nds-radio-button-group-option-icon-size: #{$icon-size};
- width: var(--nds-radio-button-group-option-icon-size);
- height: var(--nds-radio-button-group-option-icon-size);
- min-width: var(--nds-radio-button-group-option-icon-size);
- min-height: var(--nds-radio-button-group-option-icon-size);
- flex: 0 0 var(--nds-radio-button-group-option-icon-size);
-}
-
-@mixin compound-toggle-field {
- @include radio-button-group(xs);
- align-items: center;
- grid-auto-columns: max-content;
-}
-
-@mixin compound-toggle-field-option {
- @include radio-button-group-option(xs);
- min-width: 50px;
-}
-
-@mixin compound-toggle-field-input {
- @include field-control(xs);
- @include number-spinner-reset;
- width: 34px;
- min-height: 26px;
- padding: 0 4px;
- border-color: transparent;
- border-radius: var(--radius-sm);
- background: transparent;
- text-align: center;
-}
-
-@mixin date-picker-button($size: md) {
- $height: 22px;
- $padding: 0;
- $font-size: 11px;
-
- @if $size == header {
- $height: 24px;
- $padding: 0 6px;
- $font-size: 10px;
- } @else if $size == weekday {
- $height: 26px;
- $padding: 0 3px;
- $font-size: 10px;
- }
-
- display: grid;
- min-width: 0;
- min-height: $height;
- place-items: center;
- padding: $padding;
- overflow: hidden;
- border: 1px solid transparent;
- border-radius: var(--radius-sm);
- background: rgba(16, 20, 38, 0.58);
- color: var(--color-text-secondary);
- font-size: $font-size;
- font-weight: 800;
- font-variant-numeric: tabular-nums;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-@mixin date-picker-button-hover {
- border-color: rgba(246, 196, 83, 0.58);
- background: rgba(139, 92, 246, 0.3);
- color: var(--color-text-primary);
- box-shadow: none;
-}
-
-@mixin date-picker-trigger {
- @include select-trigger(sm);
-}
-
-@mixin date-picker-trigger-hover {
- @include select-trigger-hover;
-}
-
-@mixin date-picker-trigger-icon {
- @include select-trigger-icon(sm);
-}
-
-@mixin date-picker-trigger-chevron {
- display: none;
-}
-
-@mixin compact-menu-button($size: md) {
- $button-size: 42px;
- $icon-size: 26px;
-
- @if $size == sm {
- $button-size: 34px;
- $icon-size: 21px;
- }
-
- @include icon-button;
- width: $button-size;
- min-width: $button-size;
- height: $button-size;
- min-height: $button-size;
-
- img,
- .ui-icon {
- width: $icon-size;
- height: $icon-size;
- }
-}
-
-@mixin compact-menu-button-hover {
- @include icon-button-hover;
-}
-
-@mixin floating-control-menu($columns: null, $item-size: 34px) {
- display: grid;
- gap: 6px;
- padding: 8px;
- border: 1px solid var(--color-border);
- border-radius: var(--radius-md);
- background: rgba(10, 13, 28, 0.99);
- box-shadow: var(--shadow-lg);
-
- @if $columns {
- grid-template-columns: repeat($columns, $item-size);
- }
-}
-
-@mixin color-swatch($size: 34px) {
- @include icon-button(sm);
- width: $size;
- min-width: $size;
- min-height: $size;
- padding: 0;
- box-shadow: none;
-
- &::before {
- content: "";
- width: max(12px, $size - 16px);
- height: max(12px, $size - 16px);
- border: 1px solid rgba(245, 247, 255, 0.22);
- border-radius: 50%;
- background: var(--swatch-color, var(--goal-tree-swatch-color, var(--color-accent-gold)));
- box-shadow: inset 0 0 0 2px rgba(5, 7, 17, 0.36);
- }
-}
-
-@mixin color-swatch-rainbow($size: 34px) {
- @include color-swatch($size);
-
- &::before {
- width: max(14px, $size - 14px);
- height: max(14px, $size - 14px);
- border-color: rgba(245, 247, 255, 0.28);
- background: conic-gradient(
- from 20deg,
- #f6c453,
- #22d3ee,
- #8b5cf6,
- #d946ef,
- #f5f7ff,
- #f6c453
- );
- box-shadow:
- inset 0 0 0 3px rgba(5, 7, 17, 0.34),
- 0 0 10px rgba(139, 92, 246, 0.16);
- }
-}
-
-@mixin color-swatch-active {
- @include icon-button-active;
-}
-
-@mixin large-icon-picker-trigger {
- display: grid;
- width: 100px;
- min-width: 100px;
- min-height: 100px;
- place-items: center;
- padding: 0;
- border: 1px solid var(--color-primary-border);
- border-radius: var(--radius-md);
- background:
- radial-gradient(circle at 50% 35%, rgba(246, 196, 83, 0.18), transparent 44%),
- rgba(21, 26, 48, 0.86);
- color: var(--color-text-primary);
-}
-
-@mixin large-icon-picker-trigger-hover {
- border-color: rgba(246, 196, 83, 0.42);
- background:
- radial-gradient(circle at 50% 35%, rgba(246, 196, 83, 0.24), transparent 44%),
- rgba(31, 37, 68, 0.94);
- box-shadow: 0 0 18px rgba(139, 92, 246, 0.18);
-}
-
-@mixin position-toggle {
- position: relative;
- display: inline-grid;
- grid-template-columns: repeat(2, 32px);
- width: 74px;
- min-height: 38px;
- align-items: center;
- justify-items: center;
- gap: 0;
- padding: 3px 4px;
- border: 1px solid rgba(165, 180, 252, 0.12);
- border-radius: var(--radius-pill);
- background: rgba(7, 10, 24, 0.32);
- color: var(--color-text-secondary);
-
- &::before {
- content: "";
- position: absolute;
- top: 3px;
- bottom: 3px;
- left: 4px;
- width: 30px;
- border: 1px solid rgba(246, 196, 83, 0.3);
- border-radius: 50%;
- background:
- linear-gradient(135deg, rgba(246, 196, 83, 0.1), rgba(139, 92, 246, 0.08)),
- rgba(21, 26, 48, 0.58);
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
- transition: transform var(--duration-fast) var(--ease-standard);
- }
-
- &[data-position="right"]::before {
- transform: translateX(32px);
- }
-
- span {
- position: relative;
- z-index: 1;
- display: inline-grid;
- width: 30px;
- height: 30px;
- place-items: center;
- justify-self: center;
- align-self: center;
- border-radius: 50%;
- }
-
- .ui-icon {
- width: 16px;
- height: 16px;
- }
-}
-
-@mixin position-toggle-hover {
- border-color: rgba(196, 181, 253, 0.24);
- color: var(--color-text-primary);
-}
-
-@mixin edge-toggle {
- @include icon-button;
- @include icon-button-panel-toggle;
- @include button-edge(right);
-}
-
-@mixin edge-toggle-hover {
- @include icon-button-panel-toggle-hover;
-}
-
-@mixin split-button-start {
- @include button-split(start);
-}
-
-@mixin split-button-end {
- justify-content: center;
- padding: 0;
- @include button-split(end);
-}
-
-@mixin dashed-action-button {
- @include text-button;
- @include text-button-dashed;
- font-size: var(--font-size-sm);
-}
-
-@mixin dashed-action-button-hover {
- @include text-button-dashed-hover;
-}
-
-@mixin event-surface-button {
- display: grid;
- width: 100%;
- height: 100%;
- min-width: 0;
- min-height: 0;
- box-sizing: border-box;
- align-content: start;
- padding: 5px 7px;
- border: 0;
- border-radius: 0;
- background: transparent;
- color: white;
- cursor: pointer;
- text-align: left;
-}
-
-@mixin event-surface-contrast-default {
- color: white;
-
- span {
- color: rgba(255, 255, 255, 0.86);
- opacity: 1;
- }
-}
-
-@mixin event-surface-contrast-dark {
- color: rgba(11, 14, 27, 0.92);
-
- span {
- color: rgba(11, 14, 27, 0.76);
- opacity: 1;
- }
-}
-
-@mixin event-surface-button-hover {
- background: rgba(0, 0, 0, 0.14);
- box-shadow: none;
-}
-
-@mixin checkbox-control($size: md) {
- $control-size: 20px;
- $radius: 3px;
-
- @if $size == xs {
- $control-size: 14px;
- } @else if $size == sm {
- $control-size: 18px;
- }
-
- appearance: none;
- -webkit-appearance: none;
- display: inline-grid;
- width: $control-size;
- min-width: $control-size;
- max-width: $control-size;
- inline-size: $control-size;
- min-inline-size: $control-size;
- max-inline-size: $control-size;
- aspect-ratio: 1;
- height: $control-size;
- min-height: $control-size;
- max-height: $control-size;
- block-size: $control-size;
- min-block-size: $control-size;
- max-block-size: $control-size;
- place-content: center;
- margin: 0;
- padding: 0;
- border: 1px solid transparent;
- border-radius: $radius;
- line-height: 0;
- background:
- linear-gradient(135deg, rgba(27, 34, 66, 0.92), rgba(16, 20, 44, 0.94)) padding-box,
- linear-gradient(135deg, rgba(246, 196, 83, 0.5), rgba(139, 92, 246, 0.44)) border-box;
- box-shadow:
- inset 0 0 0 1px rgba(255, 255, 255, 0.06),
- 0 0 0 1px rgba(5, 7, 17, 0.42);
- cursor: pointer;
-}
-
-@mixin checkbox-check($size: md) {
- $check-size: 11px;
-
- @if $size == xs {
- $check-size: 8px;
- } @else if $size == sm {
- $check-size: 10px;
- }
-
- content: "";
- width: $check-size;
- height: $check-size;
- background: #fff;
- transform: scale(0);
- transition: transform var(--duration-fast) var(--ease-standard);
- clip-path: polygon(14% 52%, 0 66%, 38% 100%, 100% 18%, 84% 5%, 34% 68%);
-}
-
-@mixin checkbox-hover {
- border-color: rgba(246, 196, 83, 0.6);
- box-shadow:
- inset 0 0 0 1px rgba(255, 255, 255, 0.04),
- 0 0 14px rgba(246, 196, 83, 0.14);
-}
-
-@mixin checkbox-checked {
- border-color: rgba(246, 196, 83, 0.88);
- background:
- linear-gradient(135deg, rgba(246, 196, 83, 0.96), rgba(139, 92, 246, 0.86)) padding-box,
- linear-gradient(135deg, var(--color-accent-gold), rgba(196, 181, 253, 0.88)) border-box;
- box-shadow:
- inset 0 0 0 1px rgba(255, 255, 255, 0.12),
- 0 0 16px rgba(246, 196, 83, 0.2);
-}
-
-@mixin checkbox-checked-check {
- transform: scale(1);
-}
-
-@mixin checkbox-disabled {
- border-color: rgba(165, 180, 252, 0.08);
- background:
- linear-gradient(135deg, rgba(148, 163, 184, 0.08), rgba(51, 65, 85, 0.12)) padding-box,
- linear-gradient(135deg, rgba(148, 163, 184, 0.16), rgba(99, 102, 241, 0.08)) border-box;
- box-shadow: none;
- cursor: not-allowed;
- opacity: 0.48;
-}
-
-@mixin scrollbar-control {
- scrollbar-color: rgb(88, 55, 174) transparent;
- scrollbar-width: thin;
-
- &::-webkit-scrollbar {
- width: 6px;
- height: 6px;
- border-radius: 0;
- background-color: transparent;
- }
-
- &::-webkit-scrollbar-track {
- border-radius: 0;
- background-color: transparent;
- box-shadow: none;
- }
-
- &::-webkit-scrollbar-thumb {
- border-radius: var(--radius-pill);
- background: linear-gradient(180deg, rgb(88, 55, 174), rgb(48, 30, 118));
- }
-
- &::-webkit-scrollbar-thumb:hover {
- background: linear-gradient(180deg, rgba(246, 196, 83, 0.9), rgb(88, 55, 174));
- }
-
- &::-webkit-scrollbar-corner {
- background: transparent;
- }
-}
-
-@mixin scrollbar-resizer {
- &::-webkit-resizer {
- background-color: transparent;
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M4 10L10 4M7 10L10 7' fill='none' stroke='%23f6c453' stroke-width='1.5' stroke-linecap='round' opacity='.82'/%3E%3Cpath d='M2 10L10 2' fill='none' stroke='%23c4b5fd' stroke-width='1' stroke-linecap='round' opacity='.34'/%3E%3C/svg%3E");
- background-repeat: no-repeat;
- background-position: calc(50% - 2px) calc(50% - 2px);
- background-size: 12px 12px;
- }
-}
+// Rôle : expose les mixins publics des contrôles interactifs.
+@forward "controls/fields";
+@forward "controls/buttons";
+@forward "controls/steppers";
+@forward "controls/switches";
+@forward "controls/menus";
+@forward "controls/events";
+@forward "controls/checkboxes";
+@forward "controls/scrollbars";
diff --git a/website/src/styles/design-system/_surfaces.scss b/website/src/styles/design-system/_surfaces.scss
new file mode 100644
index 0000000..0961b0f
--- /dev/null
+++ b/website/src/styles/design-system/_surfaces.scss
@@ -0,0 +1,31 @@
+// Rôle : fournit les primitives de surfaces réutilisables du design system.
+
+@mixin card-surface($fill: rgba(16, 20, 38, 0.88)) {
+ border-color: transparent;
+ border-radius: var(--radius-md);
+ background:
+ linear-gradient($fill, $fill) padding-box,
+ radial-gradient(circle at 100% 0%, rgba(34, 211, 238, 0.42), transparent 28%) border-box,
+ linear-gradient(135deg, rgba(246, 196, 83, 0.62) 0%, rgba(246, 196, 83, 0.42) 72%, rgba(34, 211, 238, 0.24) 100%) border-box;
+}
+
+@mixin card-surface-hover(
+ $fill: rgba(21, 26, 48, 0.96),
+ $start: rgba(139, 92, 246, 0.9),
+ $end: rgba(196, 181, 253, 0.72),
+ $glow-primary: 0 0 18px rgba(139, 92, 246, 0.22),
+ $glow-secondary: 0 0 34px rgba(196, 181, 253, 0.12)
+) {
+ border-color: transparent;
+ background:
+ linear-gradient($fill, $fill) padding-box,
+ linear-gradient(135deg, $start, $end) border-box;
+ box-shadow:
+ var(--shadow-sm),
+ $glow-primary,
+ $glow-secondary;
+}
+
+@mixin card-cover-divider {
+ border-bottom: 1px solid rgba(246, 196, 83, 0.42);
+}
diff --git a/website/src/styles/design-system/attributes/_buttons.scss b/website/src/styles/design-system/attributes/_buttons.scss
new file mode 100644
index 0000000..c55c5fb
--- /dev/null
+++ b/website/src/styles/design-system/attributes/_buttons.scss
@@ -0,0 +1,369 @@
+// Rôle : expose les classes NDS de boutons texte, icône et lignes.
+@use "../controls";
+
+.nds-button {
+ @include controls.text-button;
+ font-weight: 800;
+}
+
+.nds-button:hover:not(:disabled, [data-variant="primary"], [data-variant="danger"], [data-variant="ghost"], .active, .is-active, [aria-pressed="true"]) {
+ @include controls.text-button-hover;
+}
+
+.nds-button[data-size="sm"] {
+ @include controls.text-button(sm);
+}
+
+.nds-button[data-size="xs"] {
+ @include controls.text-button(xs);
+}
+
+.nds-button[data-variant="primary"] {
+ @include controls.text-button-primary;
+}
+
+.nds-button[data-variant="primary"]:hover {
+ @include controls.text-button-primary-hover;
+}
+
+.nds-button[data-variant="secondary"] {
+ @include controls.text-button-secondary;
+}
+
+.nds-button[data-variant="ghost"] {
+ @include controls.text-button-ghost;
+}
+
+.nds-button[data-variant="ghost"]:hover {
+ @include controls.text-button-ghost-hover;
+}
+
+.nds-button[data-variant="dashed"] {
+ @include controls.text-button-dashed;
+}
+
+.nds-button[data-variant="dashed"]:hover {
+ @include controls.text-button-dashed-hover;
+}
+
+.nds-button[data-variant="danger"] {
+ @include controls.text-button-danger;
+}
+
+.nds-button[data-variant="danger"]:hover {
+ @include controls.text-button-danger-hover;
+}
+
+.nds-button[data-edge="left"] {
+ @include controls.button-edge(left);
+}
+
+.nds-button[data-edge="middle"] {
+ @include controls.button-edge(middle);
+}
+
+.nds-button[data-edge="top"] {
+ @include controls.button-edge(top);
+}
+
+.nds-button[data-edge="center"] {
+ @include controls.button-edge(center);
+}
+
+.nds-button[data-edge="right"] {
+ @include controls.button-edge(right);
+}
+
+.nds-button[data-edge="bottom"] {
+ @include controls.button-edge(bottom);
+}
+
+.nds-button[data-edge="top-left"] {
+ @include controls.button-edge(top-left);
+}
+
+.nds-button[data-edge="top-right"] {
+ @include controls.button-edge(top-right);
+}
+
+.nds-button[data-edge="bottom-right"] {
+ @include controls.button-edge(bottom-right);
+}
+
+.nds-button[data-edge="bottom-left"] {
+ @include controls.button-edge(bottom-left);
+}
+
+.nds-button[data-border="joined"][data-edge="left"] {
+ @include controls.button-joined-border(left);
+}
+
+.nds-button[data-border="joined"][data-edge="middle"] {
+ @include controls.button-joined-border(middle);
+}
+
+.nds-button[data-border="joined"][data-edge="top"] {
+ @include controls.button-joined-border(top);
+}
+
+.nds-button[data-border="joined"][data-edge="center"] {
+ @include controls.button-joined-border(center);
+}
+
+.nds-button[data-border="joined"][data-edge="right"] {
+ @include controls.button-joined-border(right);
+}
+
+.nds-button[data-border="joined"][data-edge="bottom"] {
+ @include controls.button-joined-border(bottom);
+}
+
+.nds-button[data-border="joined"][data-edge="top-left"] {
+ @include controls.button-joined-border(top-left);
+}
+
+.nds-button[data-border="joined"][data-edge="top-right"] {
+ @include controls.button-joined-border(top-right);
+}
+
+.nds-button[data-border="joined"][data-edge="bottom-right"] {
+ @include controls.button-joined-border(bottom-right);
+}
+
+.nds-button[data-border="joined"][data-edge="bottom-left"] {
+ @include controls.button-joined-border(bottom-left);
+}
+
+.nds-button[data-border="none"] {
+ @include controls.button-borderless;
+}
+
+.nds-button.is-active,
+.nds-button.active,
+.nds-button.active,
+.nds-button.is-active,
+.nds-button[aria-pressed="true"] {
+ @include controls.text-button-active;
+}
+
+.nds-icon-button {
+ @include controls.icon-button;
+ display: inline-grid;
+ place-items: center;
+ box-shadow: none;
+}
+
+.nds-icon-button .ui-icon {
+ @include controls.icon-button-icon;
+}
+
+.nds-icon-button[class*="drag-handle"] {
+ cursor: grab;
+ touch-action: none;
+ user-select: none;
+}
+
+.nds-icon-button[class*="drag-handle"]:active {
+ cursor: grabbing;
+}
+
+.nds-icon-button[class*="drag-handle"]:disabled {
+ cursor: not-allowed;
+}
+
+.nds-icon-button:hover:not([data-variant="primary"], [data-variant="danger"], [data-variant="ghost"], .active, .is-active, [aria-pressed="true"]) {
+ @include controls.icon-button-hover;
+}
+
+.nds-icon-button:hover:not([data-variant="primary"], [data-variant="danger"], [data-variant="ghost"], .active, .is-active, [aria-pressed="true"]) .ui-icon {
+ @include controls.icon-button-hover-icon;
+}
+
+.nds-icon-button[data-size="sm"] {
+ @include controls.icon-button(sm);
+}
+
+.nds-icon-button[data-size="sm"] .ui-icon {
+ @include controls.icon-button-icon(sm);
+}
+
+.nds-icon-button[data-size="xs"] {
+ @include controls.icon-button(xs);
+}
+
+.nds-icon-button[data-size="xs"] .ui-icon {
+ @include controls.icon-button-icon(xs);
+}
+
+.nds-icon-button[data-size="xxs"] {
+ @include controls.icon-button(xxs);
+}
+
+.nds-icon-button[data-size="xxs"] .ui-icon {
+ @include controls.icon-button-icon(xxs);
+}
+
+.nds-icon-button[data-size="micro"] {
+ @include controls.icon-button(micro);
+}
+
+.nds-icon-button[data-size="micro"] .ui-icon {
+ @include controls.icon-button-icon(micro);
+}
+
+.nds-icon-button[data-variant="primary"] {
+ @include controls.icon-button-primary;
+}
+
+.nds-icon-button[data-variant="primary"]:hover {
+ @include controls.icon-button-primary-hover;
+}
+
+.nds-icon-button[data-variant="primary"]:hover .ui-icon {
+ @include controls.icon-button-primary-hover-icon;
+}
+
+.nds-icon-button[data-variant="ghost"] {
+ @include controls.icon-button-ghost;
+}
+
+.nds-icon-button[data-variant="panel-toggle"] {
+ @include controls.icon-button-panel-toggle;
+}
+
+.nds-icon-button[data-variant="panel-toggle"]:hover,
+.nds-icon-button[data-variant="panel-toggle"].active,
+.nds-icon-button[data-variant="panel-toggle"].is-active,
+.nds-icon-button[data-variant="panel-toggle"][aria-pressed="true"] {
+ @include controls.icon-button-panel-toggle-hover;
+}
+
+.nds-icon-button[data-variant="danger"] {
+ @include controls.icon-button-danger;
+}
+
+.nds-icon-button[data-variant="danger"]:hover {
+ @include controls.icon-button-danger-hover;
+}
+
+.nds-icon-button[data-variant="danger"]:hover .ui-icon {
+ @include controls.icon-button-danger-hover-icon;
+}
+
+.nds-icon-button[class*="drag-handle"]:hover {
+ cursor: grab;
+}
+
+.nds-icon-button[class*="drag-handle"]:active,
+.nds-icon-button[class*="drag-handle"]:hover:active {
+ cursor: grabbing;
+}
+
+.nds-icon-button[data-edge="left"] {
+ @include controls.button-edge(left);
+}
+
+.nds-icon-button[data-edge="middle"] {
+ @include controls.button-edge(middle);
+}
+
+.nds-icon-button[data-edge="top"] {
+ @include controls.button-edge(top);
+}
+
+.nds-icon-button[data-edge="center"] {
+ @include controls.button-edge(center);
+}
+
+.nds-icon-button[data-edge="right"] {
+ @include controls.button-edge(right);
+}
+
+.nds-icon-button[data-edge="bottom"] {
+ @include controls.button-edge(bottom);
+}
+
+.nds-icon-button[data-edge="top-left"] {
+ @include controls.button-edge(top-left);
+}
+
+.nds-icon-button[data-edge="top-right"] {
+ @include controls.button-edge(top-right);
+}
+
+.nds-icon-button[data-edge="bottom-right"] {
+ @include controls.button-edge(bottom-right);
+}
+
+.nds-icon-button[data-edge="bottom-left"] {
+ @include controls.button-edge(bottom-left);
+}
+
+.nds-icon-button[data-border="joined"][data-edge="left"] {
+ @include controls.button-joined-border(left);
+}
+
+.nds-icon-button[data-border="joined"][data-edge="middle"] {
+ @include controls.button-joined-border(middle);
+}
+
+.nds-icon-button[data-border="joined"][data-edge="top"] {
+ @include controls.button-joined-border(top);
+}
+
+.nds-icon-button[data-border="joined"][data-edge="center"] {
+ @include controls.button-joined-border(center);
+}
+
+.nds-icon-button[data-border="joined"][data-edge="right"] {
+ @include controls.button-joined-border(right);
+}
+
+.nds-icon-button[data-border="joined"][data-edge="bottom"] {
+ @include controls.button-joined-border(bottom);
+}
+
+.nds-icon-button[data-border="joined"][data-edge="top-left"] {
+ @include controls.button-joined-border(top-left);
+}
+
+.nds-icon-button[data-border="joined"][data-edge="top-right"] {
+ @include controls.button-joined-border(top-right);
+}
+
+.nds-icon-button[data-border="joined"][data-edge="bottom-right"] {
+ @include controls.button-joined-border(bottom-right);
+}
+
+.nds-icon-button[data-border="joined"][data-edge="bottom-left"] {
+ @include controls.button-joined-border(bottom-left);
+}
+
+.nds-icon-button[data-border="none"] {
+ @include controls.button-borderless;
+}
+
+.nds-icon-button.active,
+.nds-icon-button.is-active,
+.nds-icon-button[aria-pressed="true"] {
+ @include controls.icon-button-active;
+}
+
+.nds-list-row {
+ @include controls.list-row-button;
+}
+
+.nds-list-row:hover,
+.nds-list-row.is-active,
+.nds-list-row.active {
+ @include controls.list-row-button-hover;
+}
+
+.nds-list-row.is-active,
+.nds-list-row.active {
+ @include controls.list-row-button-active;
+}
+
+.nds-list-row button {
+ @include controls.list-row-button-content;
+}
diff --git a/website/src/styles/design-system/attributes/_checkboxes.scss b/website/src/styles/design-system/attributes/_checkboxes.scss
new file mode 100644
index 0000000..7464e9c
--- /dev/null
+++ b/website/src/styles/design-system/attributes/_checkboxes.scss
@@ -0,0 +1,46 @@
+// Rôle : expose les classes NDS de cases à cocher.
+@use "../controls";
+
+.nds-checkbox {
+ @include controls.checkbox-control;
+}
+
+.nds-checkbox[data-size="xs"] {
+ @include controls.checkbox-control(xs);
+}
+
+.nds-checkbox[data-size="sm"] {
+ @include controls.checkbox-control(sm);
+}
+
+.nds-checkbox::before {
+ @include controls.checkbox-check;
+}
+
+.nds-checkbox[data-size="xs"]::before {
+ @include controls.checkbox-check(xs);
+}
+
+.nds-checkbox[data-size="sm"]::before {
+ @include controls.checkbox-check(sm);
+}
+
+.nds-checkbox:hover:not(:disabled) {
+ @include controls.checkbox-hover;
+}
+
+.nds-checkbox:checked {
+ @include controls.checkbox-checked;
+}
+
+.nds-checkbox:checked::before {
+ @include controls.checkbox-checked-check;
+}
+
+.nds-checkbox:disabled {
+ @include controls.checkbox-disabled;
+}
+
+.nds-checkbox:disabled::before {
+ opacity: 0.54;
+}
diff --git a/website/src/styles/design-system/attributes/_events.scss b/website/src/styles/design-system/attributes/_events.scss
new file mode 100644
index 0000000..04d35ce
--- /dev/null
+++ b/website/src/styles/design-system/attributes/_events.scss
@@ -0,0 +1,110 @@
+// Rôle : expose les classes NDS de surfaces événement.
+@use "../controls";
+
+.nds-event-surface {
+ --nds-event-color: #8b5cf6;
+ box-sizing: border-box;
+ width: min(210px, 100%);
+ min-height: 44px;
+ overflow: hidden;
+ border: 1px solid color-mix(in srgb, var(--nds-event-color) 24%, rgba(255, 255, 255, 0.22));
+ border-radius: var(--radius-sm);
+ background: color-mix(in srgb, var(--nds-event-color) 82%, transparent);
+ box-shadow: 0 10px 22px rgba(0, 0, 0, 0.22);
+}
+
+.nds-event-surface[data-size="md"] {
+ min-height: 44px;
+}
+
+.nds-event-surface[data-size="sm"] {
+ min-height: 22px;
+}
+
+.nds-event-surface[data-size="xs"] {
+ min-height: 10px;
+ border-width: 0;
+ border-radius: 4px;
+}
+
+.nds-event-surface[data-variant="dark"] {
+ border-color: color-mix(in srgb, var(--nds-event-color) 42%, rgba(11, 14, 27, 0.28));
+ background: color-mix(in srgb, var(--nds-event-color) 88%, transparent);
+}
+
+.nds-event-surface button {
+ @include controls.event-surface-button;
+ @include controls.event-surface-contrast-default;
+}
+
+.nds-event-surface button:hover,
+.nds-event-surface button:focus-visible {
+ @include controls.event-surface-button-hover;
+}
+
+.nds-event-surface[data-size="sm"] button {
+ display: block;
+ padding: 4px 7px;
+ text-align: center;
+}
+
+.nds-event-surface[data-size="xs"] button {
+ position: relative;
+ display: block;
+ padding: 0 4px;
+ text-align: center;
+}
+
+.nds-event-surface[data-size="xs"] button:hover,
+.nds-event-surface[data-size="xs"] button:focus-visible {
+ background: rgba(0, 0, 0, 0.18);
+}
+
+.nds-event-surface[data-size="sm"] button > span {
+ display: none;
+}
+
+.nds-event-surface[data-size="xs"] button > span {
+ display: none;
+}
+
+.nds-event-surface strong,
+.nds-event-surface span {
+ display: block;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.nds-event-surface[data-variant="dark"] button {
+ @include controls.event-surface-contrast-dark;
+}
+
+.nds-event-surface strong {
+ font-size: 11px;
+ line-height: 14px;
+}
+
+.nds-event-surface[data-size="sm"] strong {
+ max-width: 100%;
+ line-height: 14px;
+}
+
+.nds-event-surface[data-size="xs"] strong {
+ position: absolute;
+ top: 0;
+ right: 4px;
+ left: 4px;
+ max-width: 100%;
+ height: 10px;
+ font-size: 8px;
+ line-height: 10px;
+ text-shadow: 0 1px 1px rgba(0, 0, 0, 0.45);
+}
+
+.nds-event-surface span {
+ font-size: 10px;
+ line-height: 12px;
+ opacity: 0.86;
+}
diff --git a/website/src/styles/design-system/attributes/_fields.scss b/website/src/styles/design-system/attributes/_fields.scss
new file mode 100644
index 0000000..dfaccb2
--- /dev/null
+++ b/website/src/styles/design-system/attributes/_fields.scss
@@ -0,0 +1,107 @@
+// Rôle : expose les classes NDS de champs, select et textarea.
+@use "../controls";
+
+.nds-input,
+.nds-time,
+.nds-select,
+.nds-textarea {
+ @include controls.field-control;
+}
+
+.nds-input::placeholder,
+.nds-textarea::placeholder {
+ @include controls.field-placeholder;
+}
+
+.nds-input[data-size="sm"],
+.nds-textarea[data-size="sm"] {
+ @include controls.field-control(sm);
+}
+
+.nds-input[data-size="xs"],
+.nds-textarea[data-size="xs"] {
+ @include controls.field-control(xs);
+}
+
+.nds-time {
+ @include controls.time-control;
+}
+
+.nds-time[data-size="sm"] {
+ @include controls.time-control(sm);
+}
+
+.nds-time[data-size="xs"] {
+ @include controls.time-control(xs);
+}
+
+.nds-select {
+ @include controls.select-control;
+}
+
+.nds-select[data-size="sm"] {
+ @include controls.select-control(sm);
+}
+
+.nds-select[data-size="xs"] {
+ @include controls.select-control(xs);
+}
+
+.nds-select:hover,
+.nds-select:focus {
+ @include controls.select-control-hover;
+ box-shadow: none;
+}
+
+.nds-select:focus {
+ border-color: var(--color-primary);
+ box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.16);
+}
+
+.nds-select option {
+ @include controls.select-option;
+}
+
+.nds-textarea {
+ @include controls.field-textarea;
+ @include controls.scrollbar-resizer;
+}
+
+.nds-textarea[data-size="sm"] {
+ @include controls.field-textarea(sm);
+}
+
+.nds-textarea[data-size="xs"] {
+ @include controls.field-textarea(xs);
+}
+
+.nds-textarea[data-variant="ghost"] {
+ @include controls.field-textarea-ghost;
+}
+
+.nds-input:hover,
+.nds-time:hover,
+.nds-textarea:hover {
+ @include controls.field-hover;
+}
+
+.nds-textarea[data-variant="ghost"]:hover {
+ @include controls.inline-field-hover;
+}
+
+.nds-input:focus,
+.nds-time:focus,
+.nds-select:focus,
+.nds-select-trigger:focus,
+.nds-textarea:focus {
+ @include controls.field-focus;
+}
+
+.nds-inline-input {
+ @include controls.inline-field;
+}
+
+.nds-inline-input:hover,
+.nds-inline-input:focus {
+ @include controls.inline-field-hover;
+}
diff --git a/website/src/styles/design-system/attributes/_menus.scss b/website/src/styles/design-system/attributes/_menus.scss
new file mode 100644
index 0000000..cac2b65
--- /dev/null
+++ b/website/src/styles/design-system/attributes/_menus.scss
@@ -0,0 +1,189 @@
+// Rôle : expose les classes NDS de swatches, triggers select et icon pickers.
+@use "../controls";
+
+.nds-swatch {
+ @include controls.color-swatch;
+}
+
+.nds-swatch[data-variant="rainbow"] {
+ @include controls.color-swatch-rainbow;
+}
+
+.nds-swatch.active,
+.nds-swatch.is-active,
+.nds-swatch[aria-pressed="true"] {
+ @include controls.color-swatch-active;
+}
+
+.nds-select-trigger,
+.nds-date-trigger {
+ @include controls.select-trigger;
+}
+
+.nds-select-trigger[data-size="sm"],
+.nds-date-trigger[data-size="sm"] {
+ @include controls.select-trigger(sm);
+}
+
+.nds-select-trigger[data-size="xs"],
+.nds-date-trigger[data-size="xs"] {
+ @include controls.select-trigger(xs);
+}
+
+.nds-select-trigger[data-variant="icon-picker"] {
+ grid-template-columns: minmax(18px, 1fr);
+ justify-items: center;
+ min-width: 52px;
+ padding-inline: 10px 30px;
+}
+
+.nds-select-trigger[data-variant="icon-picker"]:has(> span:not([aria-hidden="true"])) {
+ grid-template-columns: 18px minmax(0, 1fr);
+ justify-items: start;
+ padding-inline: 10px 34px;
+}
+
+.nds-select-trigger[data-size="sm"][data-variant="icon-picker"] {
+ grid-template-columns: minmax(18px, 1fr);
+ min-width: 50px;
+ padding-inline: 10px 30px;
+}
+
+.nds-select-trigger[data-size="sm"][data-variant="icon-picker"]:has(> span:not([aria-hidden="true"])) {
+ grid-template-columns: 18px minmax(0, 1fr);
+ padding-inline: 10px 34px;
+}
+
+.nds-select-trigger[data-size="xs"][data-variant="icon-picker"] {
+ grid-template-columns: minmax(16px, 1fr);
+ min-width: 46px;
+ padding-inline: 9px 28px;
+}
+
+.nds-select-trigger[data-size="xs"][data-variant="icon-picker"]:has(> span:not([aria-hidden="true"])) {
+ grid-template-columns: 16px minmax(0, 1fr);
+ padding-inline: 8px 30px;
+}
+
+.nds-select-trigger:hover:not(:disabled),
+.nds-select-trigger.is-active,
+.nds-select-trigger.active,
+.nds-select-trigger[aria-pressed="true"],
+.nds-date-trigger:hover:not(:disabled),
+.nds-date-trigger.is-active,
+.nds-date-trigger.active,
+.nds-date-trigger[aria-pressed="true"] {
+ @include controls.select-trigger-hover;
+}
+
+.nds-select-trigger:focus,
+.nds-date-trigger:focus {
+ @include controls.select-trigger-hover;
+ @include controls.field-focus;
+ outline: 2px solid var(--color-accent-gold);
+ outline-offset: 3px;
+}
+
+.nds-select-trigger[data-size="sm"]:hover:not(:disabled),
+.nds-select-trigger[data-size="sm"].is-active,
+.nds-select-trigger[data-size="sm"].active,
+.nds-select-trigger[data-size="sm"][aria-pressed="true"],
+.nds-date-trigger[data-size="sm"]:hover:not(:disabled),
+.nds-date-trigger[data-size="sm"].is-active,
+.nds-date-trigger[data-size="sm"].active,
+.nds-date-trigger[data-size="sm"][aria-pressed="true"] {
+ @include controls.select-trigger-hover(sm);
+}
+
+.nds-select-trigger[data-size="sm"]:focus,
+.nds-date-trigger[data-size="sm"]:focus {
+ @include controls.select-trigger-hover(sm);
+ @include controls.field-focus;
+ outline: 2px solid var(--color-accent-gold);
+ outline-offset: 3px;
+}
+
+.nds-select-trigger[data-size="xs"]:hover:not(:disabled),
+.nds-select-trigger[data-size="xs"].is-active,
+.nds-select-trigger[data-size="xs"].active,
+.nds-select-trigger[data-size="xs"][aria-pressed="true"],
+.nds-date-trigger[data-size="xs"]:hover:not(:disabled),
+.nds-date-trigger[data-size="xs"].is-active,
+.nds-date-trigger[data-size="xs"].active,
+.nds-date-trigger[data-size="xs"][aria-pressed="true"] {
+ @include controls.select-trigger-hover(xs);
+}
+
+.nds-select-trigger[data-size="xs"]:focus,
+.nds-date-trigger[data-size="xs"]:focus {
+ @include controls.select-trigger-hover(xs);
+ @include controls.field-focus;
+ outline: 2px solid var(--color-accent-gold);
+ outline-offset: 3px;
+}
+
+.nds-select-trigger .ui-icon,
+.nds-date-trigger .ui-icon {
+ @include controls.select-trigger-icon;
+}
+
+.nds-select-trigger[data-size="sm"] .ui-icon,
+.nds-date-trigger[data-size="sm"] .ui-icon {
+ @include controls.select-trigger-icon(sm);
+}
+
+.nds-select-trigger[data-size="xs"] .ui-icon,
+.nds-date-trigger[data-size="xs"] .ui-icon {
+ @include controls.select-trigger-icon(xs);
+}
+
+.nds-select-trigger .ui-icon-chevron-down,
+.nds-date-trigger .ui-icon-chevron-down {
+ @include controls.date-picker-trigger-chevron;
+}
+
+.nds-select-trigger[data-variant="icon-picker"] > .ui-icon:not(.ui-icon-chevron-down) {
+ width: 18px;
+ height: 18px;
+}
+
+.nds-select-trigger[data-variant="icon-picker"]:has(> span:not([aria-hidden="true"])) > .ui-icon:not(.ui-icon-chevron-down) {
+ justify-self: center;
+}
+
+.nds-select-trigger[data-size="xs"][data-variant="icon-picker"] > .ui-icon:not(.ui-icon-chevron-down) {
+ width: 16px;
+ height: 16px;
+}
+
+.nds-select-trigger span,
+.nds-date-trigger span {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.nds-large-icon-picker-trigger {
+ @include controls.large-icon-picker-trigger;
+}
+
+.nds-large-icon-picker-trigger:hover {
+ @include controls.large-icon-picker-trigger-hover;
+}
+
+.nds-large-icon-picker-trigger.active,
+.nds-large-icon-picker-trigger.is-active,
+.nds-large-icon-picker-trigger[aria-pressed="true"] {
+ @include controls.large-icon-picker-trigger-hover;
+ border-color: rgba(246, 196, 83, 0.62);
+ box-shadow:
+ 0 0 0 1px rgba(246, 196, 83, 0.22),
+ 0 0 22px rgba(139, 92, 246, 0.24);
+}
+
+.nds-large-icon-picker-trigger img,
+.nds-large-icon-picker-trigger .ui-icon {
+ width: 72px;
+ height: 72px;
+ object-fit: contain;
+}
diff --git a/website/src/styles/design-system/attributes/_states.scss b/website/src/styles/design-system/attributes/_states.scss
new file mode 100644
index 0000000..7d5f198
--- /dev/null
+++ b/website/src/styles/design-system/attributes/_states.scss
@@ -0,0 +1,63 @@
+// Rôle : expose les scrollboxes, focus et états désactivés NDS.
+@use "../controls";
+
+.nds-scrollbox,
+.nds-textarea {
+ width: 100%;
+ @include controls.scrollbar-control;
+}
+
+.nds-scrollbox {
+ max-height: 400px;
+ overflow: auto;
+ padding: 12px;
+ border: 1px solid rgba(165, 180, 252, 0.12);
+ border-radius: var(--radius-md);
+ background: rgba(5, 7, 17, 0.34);
+ color: var(--color-text-secondary);
+}
+
+.nds-scrollbox[data-variant="ghost"] {
+ padding: 0;
+ border: 0;
+ border-radius: 0;
+ background: transparent;
+}
+
+.nds-scrollbox[data-axis="horizontal"] {
+ max-height: none;
+ overflow-x: auto;
+ overflow-y: hidden;
+ white-space: nowrap;
+}
+
+.nds-input:focus-visible,
+.nds-time:focus-visible,
+.nds-select:focus-visible,
+.nds-textarea:focus-visible,
+.nds-inline-input:focus-visible,
+.nds-button:focus-visible,
+.nds-icon-button:focus-visible,
+.nds-switch:focus-visible,
+.nds-radio-button-group button:focus-visible,
+.nds-checkbox:focus-visible,
+.nds-select-trigger:focus-visible,
+.nds-date-trigger:focus-visible,
+.nds-large-icon-picker-trigger:focus-visible {
+ @include controls.field-focus;
+ outline: 2px solid var(--color-accent-gold);
+ outline-offset: 3px;
+}
+
+.nds-button:disabled,
+.nds-icon-button:disabled,
+.nds-quantity-stepper button:disabled,
+.nds-select:disabled,
+.nds-select-trigger:disabled,
+.nds-input:disabled,
+.nds-time:disabled,
+.nds-textarea:disabled,
+.nds-date-trigger:disabled,
+.nds-large-icon-picker-trigger:disabled {
+ @include controls.control-disabled;
+}
diff --git a/website/src/styles/design-system/attributes/_steppers.scss b/website/src/styles/design-system/attributes/_steppers.scss
new file mode 100644
index 0000000..9913c7d
--- /dev/null
+++ b/website/src/styles/design-system/attributes/_steppers.scss
@@ -0,0 +1,56 @@
+// Rôle : expose les classes NDS de steppers numériques.
+@use "../controls";
+
+.nds-quantity-stepper {
+ @include controls.quantity-stepper;
+}
+
+.nds-quantity-stepper[data-size="sm"] {
+ @include controls.quantity-stepper(sm);
+}
+
+.nds-quantity-stepper[data-size="xs"] {
+ @include controls.quantity-stepper(xs);
+}
+
+.nds-quantity-stepper button {
+ @include controls.quantity-stepper-button;
+}
+
+.nds-quantity-stepper[data-size="sm"] button {
+ @include controls.quantity-stepper-button(sm);
+}
+
+.nds-quantity-stepper[data-size="xs"] button {
+ @include controls.quantity-stepper-button(xs);
+}
+
+.nds-quantity-stepper button:hover {
+ @include controls.quantity-stepper-button-hover;
+}
+
+.nds-quantity-stepper .ui-icon {
+ @include controls.quantity-stepper-button-icon;
+}
+
+.nds-quantity-stepper[data-size="sm"] .ui-icon {
+ @include controls.quantity-stepper-button-icon(sm);
+}
+
+.nds-quantity-stepper[data-size="xs"] .ui-icon {
+ @include controls.quantity-stepper-button-icon(xs);
+}
+
+.nds-quantity-stepper-value {
+ @include controls.quantity-stepper-value;
+}
+
+.nds-quantity-stepper-input {
+ @include controls.quantity-stepper-input;
+ @include controls.number-spinner-reset;
+}
+
+.nds-quantity-stepper-input:hover,
+.nds-quantity-stepper-input:focus {
+ @include controls.quantity-stepper-input-hover;
+}
diff --git a/website/src/styles/design-system/attributes/_switches.scss b/website/src/styles/design-system/attributes/_switches.scss
new file mode 100644
index 0000000..20d2393
--- /dev/null
+++ b/website/src/styles/design-system/attributes/_switches.scss
@@ -0,0 +1,186 @@
+// Rôle : expose les classes NDS de switchs, radio groups et toggles composés.
+@use "../controls";
+
+.nds-switch {
+ @include controls.switch-control;
+}
+
+.nds-switch[data-size="lg"] {
+ @include controls.switch-control(lg);
+}
+
+.nds-switch[data-size="sm"] {
+ @include controls.switch-control(sm);
+}
+
+.nds-switch[data-size="xs"] {
+ @include controls.switch-control(xs);
+}
+
+.nds-switch:has(> span:not(.ui-icon)) {
+ grid-template-columns: var(--nds-switch-icon-size) minmax(0, 1fr) var(--nds-switch-track-width);
+ min-width: max-content;
+}
+
+.nds-switch:has(> span:not(.ui-icon)) > span:not(.ui-icon) {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.nds-switch .ui-icon {
+ @include controls.switch-icon;
+}
+
+.nds-switch i {
+ @include controls.switch-track;
+}
+
+.nds-switch i::before {
+ @include controls.switch-thumb;
+}
+
+.nds-switch:hover {
+ @include controls.switch-hover;
+}
+
+.nds-switch.active,
+.nds-switch.is-active,
+.nds-switch[aria-pressed="true"] {
+ @include controls.switch-active;
+}
+
+.nds-switch.active i,
+.nds-switch.is-active i,
+.nds-switch[aria-pressed="true"] i {
+ @include controls.switch-active-track;
+}
+
+.nds-switch.active i::before,
+.nds-switch.is-active i::before,
+.nds-switch[aria-pressed="true"] i::before {
+ @include controls.switch-active-thumb;
+}
+
+.nds-native-switch {
+ @include controls.native-switch-label;
+}
+
+.nds-native-switch input {
+ @include controls.native-switch-input;
+}
+
+.nds-native-switch > span:first-of-type {
+ --nds-switch-track-width: 36px;
+ --nds-switch-track-height: 20px;
+ --nds-switch-thumb-size: 12px;
+ --nds-switch-thumb-left: 3px;
+ --nds-switch-active-left: 19px;
+ @include controls.switch-track;
+ min-width: 36px;
+}
+
+.nds-native-switch > span:first-of-type::before {
+ @include controls.switch-thumb;
+}
+
+.nds-native-switch input:checked + span {
+ @include controls.switch-active-track;
+}
+
+.nds-native-switch input:checked + span::before {
+ @include controls.switch-active-thumb;
+}
+
+.nds-native-switch:has(input:disabled) {
+ @include controls.control-disabled;
+ cursor: not-allowed;
+}
+
+.nds-native-switch:has(input:disabled) > span:first-of-type,
+.nds-native-switch:has(input:disabled) b {
+ cursor: not-allowed;
+}
+
+.nds-radio-button-group {
+ @include controls.radio-button-group;
+}
+
+.nds-radio-button-group[data-size="sm"] {
+ @include controls.radio-button-group(sm);
+}
+
+.nds-radio-button-group[data-size="xs"] {
+ @include controls.radio-button-group(xs);
+}
+
+.nds-radio-button-group > button,
+.nds-radio-button-group > span {
+ @include controls.radio-button-group-option;
+}
+
+.nds-radio-button-group[data-size="sm"] > button,
+.nds-radio-button-group[data-size="sm"] > span {
+ @include controls.radio-button-group-option(sm);
+}
+
+.nds-radio-button-group[data-size="xs"] > button,
+.nds-radio-button-group[data-size="xs"] > span {
+ @include controls.radio-button-group-option(xs);
+}
+
+.nds-radio-button-group > button:hover,
+.nds-radio-button-group > span:hover {
+ @include controls.radio-button-group-option-hover;
+}
+
+.nds-radio-button-group > button.active,
+.nds-radio-button-group > button.is-active,
+.nds-radio-button-group > span.active,
+.nds-radio-button-group > span.is-active {
+ @include controls.radio-button-group-option-active;
+}
+
+.nds-radio-button-group .ui-icon {
+ @include controls.radio-button-group-option-icon;
+}
+
+.nds-radio-button-group[data-size="sm"] .ui-icon {
+ @include controls.radio-button-group-option-icon(sm);
+}
+
+.nds-radio-button-group[data-size="xs"] .ui-icon {
+ @include controls.radio-button-group-option-icon(xs);
+}
+
+.nds-radio-button-group[aria-disabled="true"] {
+ @include controls.control-disabled;
+}
+
+.nds-compound-toggle-field {
+ @include controls.compound-toggle-field;
+}
+
+.nds-compound-toggle-field button {
+ @include controls.compound-toggle-field-option;
+}
+
+.nds-compound-toggle-field button:hover:not(:disabled) {
+ @include controls.radio-button-group-option-hover;
+}
+
+.nds-compound-toggle-field button.is-active,
+.nds-compound-toggle-field button.active,
+.nds-compound-toggle-field button[aria-pressed="true"] {
+ @include controls.radio-button-group-option-active;
+}
+
+.nds-compound-toggle-field-input {
+ @include controls.compound-toggle-field-input;
+}
+
+.nds-compound-toggle-field-input:hover,
+.nds-compound-toggle-field-input:focus {
+ @include controls.inline-field-hover;
+}
diff --git a/website/src/styles/design-system/controls/_buttons.scss b/website/src/styles/design-system/controls/_buttons.scss
new file mode 100644
index 0000000..7ba9b84
--- /dev/null
+++ b/website/src/styles/design-system/controls/_buttons.scss
@@ -0,0 +1,429 @@
+// Rôle : regroupe les mixins de boutons texte, icône, bords joints et toolbar.
+@use "../../mixins";
+
+@mixin text-button($size: md) {
+ $height: 42px;
+ $padding: 0 18px;
+ $font-size: inherit;
+
+ @if $size == sm {
+ $height: 34px;
+ $padding: 0 12px;
+ $font-size: var(--font-size-sm);
+ } @else if $size == xs {
+ $height: 30px;
+ $padding: 0 10px;
+ $font-size: var(--font-size-xs);
+ }
+
+ display: inline-flex;
+ min-height: $height;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ padding: $padding;
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-md);
+ background: rgba(21, 26, 48, 0.92);
+ color: var(--color-text-primary);
+ font: inherit;
+ font-size: $font-size;
+ cursor: pointer;
+ transition:
+ transform var(--duration-fast) var(--ease-standard),
+ border-color var(--duration-fast) var(--ease-standard),
+ background-color var(--duration-fast) var(--ease-standard),
+ color var(--duration-fast) var(--ease-standard),
+ box-shadow var(--duration-fast) var(--ease-standard);
+}
+
+@mixin text-button-hover {
+ border-color: rgba(196, 181, 253, 0.34);
+ background:
+ linear-gradient(135deg, rgba(31, 37, 68, 0.96), rgba(22, 26, 51, 0.96)) padding-box,
+ linear-gradient(135deg, rgba(139, 92, 246, 0.32), rgba(196, 181, 253, 0.2)) border-box;
+ box-shadow:
+ 0 0 14px rgba(139, 92, 246, 0.12),
+ inset 0 1px 0 rgba(255, 255, 255, 0.05);
+}
+
+@mixin text-button-primary {
+ border-color: rgba(246, 196, 83, 0.34);
+ background:
+ linear-gradient(135deg, rgba(72, 43, 160, 0.96), rgba(45, 52, 146, 0.94)) padding-box,
+ linear-gradient(135deg, rgba(246, 196, 83, 0.66), rgba(139, 92, 246, 0.36)) border-box;
+ box-shadow:
+ 0 8px 22px rgba(5, 7, 17, 0.22),
+ inset 0 1px 0 rgba(255, 255, 255, 0.08);
+ color: #fff;
+ font-weight: 700;
+}
+
+@mixin text-button-primary-hover {
+ border-color: rgba(246, 196, 83, 0.72);
+ background:
+ radial-gradient(circle at 100% 0, rgba(246, 196, 83, 0.24), transparent 42%) padding-box,
+ linear-gradient(135deg, rgba(125, 62, 232, 0.98), rgba(62, 72, 184, 0.98)) padding-box,
+ linear-gradient(135deg, rgba(255, 219, 128, 0.98), rgba(139, 92, 246, 0.82), rgba(34, 211, 238, 0.42)) border-box;
+ box-shadow:
+ 0 0 0 1px rgba(246, 196, 83, 0.18),
+ 0 0 18px rgba(139, 92, 246, 0.26),
+ 0 0 24px rgba(246, 196, 83, 0.14),
+ inset 0 1px 0 rgba(255, 255, 255, 0.2),
+ inset 0 -2px 0 rgba(246, 196, 83, 0.24);
+}
+
+@mixin text-button-secondary {
+ border-color: rgba(196, 181, 253, 0.18);
+ background:
+ linear-gradient(135deg, rgba(31, 37, 68, 0.72), rgba(15, 23, 42, 0.72)) padding-box,
+ linear-gradient(135deg, rgba(139, 92, 246, 0.3), rgba(34, 211, 238, 0.14)) border-box;
+ color: var(--color-text-primary);
+}
+
+@mixin text-button-ghost {
+ border-color: transparent;
+ background: transparent;
+ color: var(--color-text-secondary);
+ box-shadow: none;
+}
+
+@mixin text-button-ghost-hover {
+ border-color: rgba(196, 181, 253, 0.18);
+ background: rgba(139, 92, 246, 0.08);
+ color: var(--color-text-primary);
+ box-shadow: none;
+}
+
+@mixin text-button-dashed {
+ border-style: dashed;
+ border-color: rgba(246, 196, 83, 0.28);
+ background:
+ linear-gradient(135deg, rgba(246, 196, 83, 0.04), rgba(139, 92, 246, 0.06)),
+ rgba(7, 10, 24, 0.72);
+ color: var(--color-text-secondary);
+ font-weight: 900;
+ box-shadow: 0 10px 18px rgba(0, 0, 0, 0.16);
+}
+
+@mixin text-button-dashed-hover {
+ border-color: rgba(246, 196, 83, 0.54);
+ background:
+ linear-gradient(135deg, rgba(246, 196, 83, 0.1), rgba(139, 92, 246, 0.1)),
+ rgba(13, 17, 34, 0.82);
+ color: var(--color-text-primary);
+ box-shadow:
+ 0 0 16px rgba(246, 196, 83, 0.12),
+ 0 10px 18px rgba(0, 0, 0, 0.16);
+}
+
+@mixin text-button-danger {
+ color: var(--color-danger);
+}
+
+@mixin text-button-danger-hover {
+ border-color: rgba(251, 113, 133, 0.48);
+ background:
+ radial-gradient(circle at 100% 0, rgba(251, 113, 133, 0.22), transparent 42%) padding-box,
+ linear-gradient(135deg, rgba(96, 22, 45, 0.94), rgba(58, 16, 34, 0.96)) padding-box,
+ linear-gradient(135deg, rgba(251, 113, 133, 0.82), rgba(244, 114, 182, 0.42), rgba(196, 181, 253, 0.18)) border-box;
+ color: #fff;
+ box-shadow:
+ 0 0 0 1px rgba(251, 113, 133, 0.12),
+ 0 0 16px rgba(251, 113, 133, 0.16),
+ inset 0 1px 0 rgba(255, 255, 255, 0.08),
+ inset 0 -2px 0 rgba(251, 113, 133, 0.24);
+}
+
+@mixin text-button-active {
+ @include mixins.temporary-toggle-active;
+}
+
+@mixin list-row-button {
+ overflow: hidden;
+ border: 1px solid rgba(150, 165, 205, 0.12);
+ border-radius: var(--radius-sm);
+ background: rgba(5, 7, 17, 0.36);
+}
+
+@mixin list-row-button-hover {
+ border-color: rgba(150, 165, 205, 0.24);
+ background: rgba(5, 7, 17, 0.5);
+}
+
+@mixin list-row-button-active {
+ border-color: rgba(246, 196, 83, 0.42);
+ background: rgba(246, 196, 83, 0.08);
+}
+
+@mixin list-row-button-content {
+ display: inline-flex;
+ width: 100%;
+ min-height: 32px;
+ align-items: center;
+ justify-content: flex-start;
+ overflow: hidden;
+ padding: 0 8px;
+ border-color: transparent;
+ background: transparent;
+ color: var(--color-text-primary);
+ font-size: var(--font-size-sm);
+ font-weight: 900;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+@mixin control-disabled {
+ box-shadow: none;
+ cursor: not-allowed;
+ opacity: 0.48;
+}
+
+@mixin icon-button($size: md) {
+ $control-size: 40px;
+
+ @if $size == sm {
+ $control-size: 34px;
+ } @else if $size == xs {
+ $control-size: 30px;
+ } @else if $size == xxs {
+ $control-size: 26px;
+ } @else if $size == micro {
+ $control-size: 22px;
+ }
+
+ display: inline-flex;
+ width: $control-size;
+ height: $control-size;
+ min-width: $control-size;
+ min-height: $control-size;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+ border: 1px solid rgba(165, 180, 252, 0.12);
+ border-radius: var(--radius-md);
+ background: rgba(21, 26, 48, 0.88);
+ color: var(--color-text-secondary);
+ font: inherit;
+ cursor: pointer;
+ transition:
+ transform var(--duration-fast) var(--ease-standard),
+ border-color var(--duration-fast) var(--ease-standard),
+ background-color var(--duration-fast) var(--ease-standard),
+ color var(--duration-fast) var(--ease-standard),
+ box-shadow var(--duration-fast) var(--ease-standard);
+}
+
+@mixin icon-button-icon($size: md) {
+ $icon-size: 21px;
+
+ @if $size == sm {
+ $icon-size: 19px;
+ } @else if $size == xs {
+ $icon-size: 17px;
+ } @else if $size == xxs {
+ $icon-size: 15px;
+ } @else if $size == micro {
+ $icon-size: 12px;
+ }
+
+ width: $icon-size;
+ height: $icon-size;
+ transition:
+ background-color var(--duration-fast) var(--ease-standard),
+ filter var(--duration-fast) var(--ease-standard);
+}
+
+@mixin icon-button-hover {
+ border-color: rgba(196, 181, 253, 0.32);
+ background:
+ linear-gradient(135deg, rgba(31, 37, 68, 0.94), rgba(18, 22, 44, 0.96)) padding-box,
+ linear-gradient(135deg, rgba(139, 92, 246, 0.3), rgba(196, 181, 253, 0.2)) border-box;
+ color: var(--color-text-primary);
+ box-shadow:
+ 0 0 12px rgba(139, 92, 246, 0.1),
+ inset 0 1px 0 rgba(255, 255, 255, 0.05);
+}
+
+@mixin icon-button-hover-icon {
+ background-color: #c4b5fd;
+ filter: drop-shadow(0 0 6px rgba(196, 181, 253, 0.28));
+}
+
+@mixin icon-button-primary {
+ border-color: rgba(246, 196, 83, 0.34);
+ background:
+ linear-gradient(135deg, rgba(72, 43, 160, 0.96), rgba(45, 52, 146, 0.94)) padding-box,
+ linear-gradient(135deg, rgba(246, 196, 83, 0.66), rgba(139, 92, 246, 0.36)) border-box;
+ color: #fff;
+ box-shadow:
+ 0 8px 22px rgba(5, 7, 17, 0.22),
+ inset 0 1px 0 rgba(255, 255, 255, 0.08);
+}
+
+@mixin icon-button-primary-hover {
+ border-color: rgba(246, 196, 83, 0.72);
+ background:
+ radial-gradient(circle at 100% 0, rgba(246, 196, 83, 0.24), transparent 42%) padding-box,
+ linear-gradient(135deg, rgba(125, 62, 232, 0.98), rgba(62, 72, 184, 0.98)) padding-box,
+ linear-gradient(135deg, rgba(255, 219, 128, 0.98), rgba(139, 92, 246, 0.82), rgba(34, 211, 238, 0.42)) border-box;
+ color: #fff;
+ box-shadow:
+ 0 0 0 1px rgba(246, 196, 83, 0.18),
+ 0 0 18px rgba(139, 92, 246, 0.26),
+ 0 0 24px rgba(246, 196, 83, 0.14),
+ inset 0 1px 0 rgba(255, 255, 255, 0.2),
+ inset 0 -2px 0 rgba(246, 196, 83, 0.24);
+}
+
+@mixin icon-button-primary-hover-icon {
+ background-color: #fff;
+ filter: drop-shadow(0 0 6px rgba(246, 196, 83, 0.3));
+}
+
+@mixin icon-button-ghost {
+ border-color: transparent;
+ background: transparent;
+ color: var(--color-text-muted);
+ box-shadow: none;
+}
+
+@mixin icon-button-danger {
+ border-color: rgba(251, 113, 133, 0.2);
+ color: var(--color-danger);
+}
+
+@mixin icon-button-danger-hover {
+ border-color: rgba(251, 113, 133, 0.56);
+ background:
+ radial-gradient(circle at 100% 0, rgba(251, 113, 133, 0.22), transparent 42%) padding-box,
+ linear-gradient(135deg, rgba(96, 22, 45, 0.94), rgba(58, 16, 34, 0.96)) padding-box,
+ linear-gradient(135deg, rgba(251, 113, 133, 0.82), rgba(244, 114, 182, 0.42), rgba(196, 181, 253, 0.18)) border-box;
+ color: #fff;
+ box-shadow:
+ 0 0 0 1px rgba(251, 113, 133, 0.12),
+ 0 0 16px rgba(251, 113, 133, 0.16),
+ inset 0 1px 0 rgba(255, 255, 255, 0.08),
+ inset 0 -2px 0 rgba(251, 113, 133, 0.24);
+}
+
+@mixin icon-button-danger-hover-icon {
+ background-color: #fff;
+ filter: drop-shadow(0 0 6px rgba(251, 113, 133, 0.26));
+}
+
+@mixin icon-button-active {
+ @include mixins.temporary-toggle-active;
+}
+
+@mixin button-edge($edge: right) {
+ @if $edge == left {
+ border-radius: var(--radius-sm) 0 0 var(--radius-sm);
+ } @else if $edge == middle {
+ border-radius: 0;
+ } @else if $edge == top {
+ border-radius: var(--radius-sm) var(--radius-sm) 0 0;
+ } @else if $edge == center {
+ border-radius: 0;
+ } @else if $edge == bottom {
+ border-radius: 0 0 var(--radius-sm) var(--radius-sm);
+ } @else if $edge == top-left {
+ border-radius: var(--radius-sm) 0 0 0;
+ } @else if $edge == top-right {
+ border-radius: 0 var(--radius-sm) 0 0;
+ } @else if $edge == bottom-right {
+ border-radius: 0 0 var(--radius-sm) 0;
+ } @else if $edge == bottom-left {
+ border-radius: 0 0 0 var(--radius-sm);
+ } @else {
+ border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
+ }
+}
+
+@mixin button-joined-border($edge: right) {
+ @if $edge == left {
+ border-right-color: transparent;
+ } @else if $edge == middle {
+ border-right-color: transparent;
+ border-left-color: transparent;
+ } @else if $edge == right {
+ border-left-color: transparent;
+ } @else if $edge == top {
+ border-bottom-color: transparent;
+ } @else if $edge == center {
+ border-top-color: transparent;
+ border-bottom-color: transparent;
+ } @else if $edge == bottom {
+ border-top-color: transparent;
+ } @else if $edge == top-left {
+ border-right-color: transparent;
+ border-bottom-color: transparent;
+ } @else if $edge == top-right {
+ border-left-color: transparent;
+ border-bottom-color: transparent;
+ } @else if $edge == bottom-right {
+ border-left-color: transparent;
+ border-top-color: transparent;
+ } @else if $edge == bottom-left {
+ border-right-color: transparent;
+ border-top-color: transparent;
+ }
+}
+
+@mixin button-borderless {
+ border-color: transparent;
+}
+
+@mixin icon-button-edge($edge: right) {
+ @include button-edge($edge);
+}
+
+@mixin icon-button-panel-toggle {
+ background: rgba(39, 18, 69, 0.86);
+ color: var(--color-text-primary);
+}
+
+@mixin icon-button-panel-toggle-hover {
+ border-color: rgba(246, 196, 83, 0.44);
+ background: rgba(56, 28, 92, 0.94);
+ color: var(--color-accent-gold);
+}
+
+@mixin button-split($position: start, $axis: horizontal) {
+ @if $axis == vertical {
+ @if $position == start {
+ @include button-edge(top);
+ } @else {
+ @include button-edge(bottom);
+ border-top-color: transparent;
+ }
+ } @else {
+ @if $position == start {
+ @include button-edge(left);
+ } @else {
+ @include button-edge(right);
+ border-left-color: transparent;
+ }
+ }
+}
+
+@mixin toolbar-button($size: sm) {
+ @include icon-button($size);
+ display: inline-grid;
+ place-items: center;
+ font-size: var(--font-size-sm);
+ font-weight: 900;
+ box-shadow: none;
+}
+
+@mixin toolbar-button-icon($size: sm) {
+ @include icon-button-icon($size);
+}
+
+@mixin toolbar-button-hover {
+ @include icon-button-hover;
+}
+
+@mixin toolbar-button-active {
+ @include icon-button-active;
+}
diff --git a/website/src/styles/design-system/controls/_checkboxes.scss b/website/src/styles/design-system/controls/_checkboxes.scss
new file mode 100644
index 0000000..210d525
--- /dev/null
+++ b/website/src/styles/design-system/controls/_checkboxes.scss
@@ -0,0 +1,90 @@
+// Rôle : regroupe les mixins de cases à cocher.
+@mixin checkbox-control($size: md) {
+ $control-size: 20px;
+ $radius: 3px;
+
+ @if $size == xs {
+ $control-size: 14px;
+ } @else if $size == sm {
+ $control-size: 18px;
+ }
+
+ appearance: none;
+ -webkit-appearance: none;
+ display: inline-grid;
+ width: $control-size;
+ min-width: $control-size;
+ max-width: $control-size;
+ inline-size: $control-size;
+ min-inline-size: $control-size;
+ max-inline-size: $control-size;
+ aspect-ratio: 1;
+ height: $control-size;
+ min-height: $control-size;
+ max-height: $control-size;
+ block-size: $control-size;
+ min-block-size: $control-size;
+ max-block-size: $control-size;
+ place-content: center;
+ margin: 0;
+ padding: 0;
+ border: 1px solid transparent;
+ border-radius: $radius;
+ line-height: 0;
+ background:
+ linear-gradient(135deg, rgba(27, 34, 66, 0.92), rgba(16, 20, 44, 0.94)) padding-box,
+ linear-gradient(135deg, rgba(246, 196, 83, 0.5), rgba(139, 92, 246, 0.44)) border-box;
+ box-shadow:
+ inset 0 0 0 1px rgba(255, 255, 255, 0.06),
+ 0 0 0 1px rgba(5, 7, 17, 0.42);
+ cursor: pointer;
+}
+
+@mixin checkbox-check($size: md) {
+ $check-size: 11px;
+
+ @if $size == xs {
+ $check-size: 8px;
+ } @else if $size == sm {
+ $check-size: 10px;
+ }
+
+ content: "";
+ width: $check-size;
+ height: $check-size;
+ background: #fff;
+ transform: scale(0);
+ transition: transform var(--duration-fast) var(--ease-standard);
+ clip-path: polygon(14% 52%, 0 66%, 38% 100%, 100% 18%, 84% 5%, 34% 68%);
+}
+
+@mixin checkbox-hover {
+ border-color: rgba(246, 196, 83, 0.6);
+ box-shadow:
+ inset 0 0 0 1px rgba(255, 255, 255, 0.04),
+ 0 0 14px rgba(246, 196, 83, 0.14);
+}
+
+@mixin checkbox-checked {
+ border-color: rgba(246, 196, 83, 0.88);
+ background:
+ linear-gradient(135deg, rgba(246, 196, 83, 0.96), rgba(139, 92, 246, 0.86)) padding-box,
+ linear-gradient(135deg, var(--color-accent-gold), rgba(196, 181, 253, 0.88)) border-box;
+ box-shadow:
+ inset 0 0 0 1px rgba(255, 255, 255, 0.12),
+ 0 0 16px rgba(246, 196, 83, 0.2);
+}
+
+@mixin checkbox-checked-check {
+ transform: scale(1);
+}
+
+@mixin checkbox-disabled {
+ border-color: rgba(165, 180, 252, 0.08);
+ background:
+ linear-gradient(135deg, rgba(148, 163, 184, 0.08), rgba(51, 65, 85, 0.12)) padding-box,
+ linear-gradient(135deg, rgba(148, 163, 184, 0.16), rgba(99, 102, 241, 0.08)) border-box;
+ box-shadow: none;
+ cursor: not-allowed;
+ opacity: 0.48;
+}
diff --git a/website/src/styles/design-system/controls/_events.scss b/website/src/styles/design-system/controls/_events.scss
new file mode 100644
index 0000000..c9a65ec
--- /dev/null
+++ b/website/src/styles/design-system/controls/_events.scss
@@ -0,0 +1,40 @@
+// Rôle : regroupe les mixins de surfaces événement calendrier.
+@mixin event-surface-button {
+ display: grid;
+ width: 100%;
+ height: 100%;
+ min-width: 0;
+ min-height: 0;
+ box-sizing: border-box;
+ align-content: start;
+ padding: 5px 7px;
+ border: 0;
+ border-radius: 0;
+ background: transparent;
+ color: white;
+ cursor: pointer;
+ text-align: left;
+}
+
+@mixin event-surface-contrast-default {
+ color: white;
+
+ span {
+ color: rgba(255, 255, 255, 0.86);
+ opacity: 1;
+ }
+}
+
+@mixin event-surface-contrast-dark {
+ color: rgba(11, 14, 27, 0.92);
+
+ span {
+ color: rgba(11, 14, 27, 0.76);
+ opacity: 1;
+ }
+}
+
+@mixin event-surface-button-hover {
+ background: rgba(0, 0, 0, 0.14);
+ box-shadow: none;
+}
diff --git a/website/src/styles/design-system/controls/_fields.scss b/website/src/styles/design-system/controls/_fields.scss
new file mode 100644
index 0000000..51c459d
--- /dev/null
+++ b/website/src/styles/design-system/controls/_fields.scss
@@ -0,0 +1,453 @@
+// Rôle : regroupe les mixins de champs texte, select, textarea et champs inline.
+
+@mixin select-control($size: md) {
+ $height: 44px;
+ $padding-right: 40px;
+ $padding-left: 14px;
+ $border-alpha: 0.12;
+ $chevron-offset: 14px;
+
+ @if $size == sm {
+ $height: 36px;
+ $padding-right: 34px;
+ $padding-left: 12px;
+ $border-alpha: 0.14;
+ $chevron-offset: 10px;
+ } @else if $size == xs {
+ $height: 32px;
+ $padding-right: 30px;
+ $padding-left: 10px;
+ $border-alpha: 0.14;
+ $chevron-offset: 9px;
+ }
+
+ min-height: $height;
+ overflow: hidden;
+ padding: 0 $padding-right 0 $padding-left;
+ border: 1px solid rgba(165, 180, 252, $border-alpha);
+ border-radius: var(--radius-md);
+ appearance: none;
+ -webkit-appearance: none;
+ color: var(--color-text-primary);
+ font: inherit;
+ text-overflow: ellipsis;
+ background-color: rgba(21, 26, 48, 0.86);
+ background-image:
+ url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23c4b5fd' viewBox='-6.5 0 32 32'%3E%3Cpath d='M18.813 11.406l-7.906 9.906c-.75.906-1.906.906-2.625 0L.376 11.406c-.75-.938-.375-1.656.781-1.656h16.875c1.188 0 1.531.719.781 1.656z'/%3E%3C/svg%3E"),
+ linear-gradient(135deg, rgba(139, 92, 246, 0.04), rgba(31, 41, 78, 0.08)),
+ linear-gradient(rgba(21, 26, 48, 0.42), rgba(21, 26, 48, 0.42));
+ background-position:
+ calc(100% - #{$chevron-offset}) 50%,
+ 0 0,
+ 0 0;
+ background-size:
+ 11px 11px,
+ auto,
+ auto;
+ background-repeat: no-repeat;
+}
+
+@mixin select-control-hover {
+ border-color: rgba(196, 181, 253, 0.22);
+ background-color: rgba(21, 26, 48, 0.92);
+ background-image:
+ url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23f6c453' viewBox='-6.5 0 32 32'%3E%3Cpath d='M18.813 11.406l-7.906 9.906c-.75.906-1.906.906-2.625 0L.376 11.406c-.75-.938-.375-1.656.781-1.656h16.875c1.188 0 1.531.719.781 1.656z'/%3E%3C/svg%3E"),
+ linear-gradient(135deg, rgba(246, 196, 83, 0.06), rgba(139, 92, 246, 0.08)),
+ linear-gradient(rgba(21, 26, 48, 0.58), rgba(21, 26, 48, 0.58));
+ color: var(--color-text-primary);
+}
+
+@mixin select-trigger($size: md) {
+ $height: 44px;
+ $padding-right: 40px;
+ $padding-left: 12px;
+ $icon-size: 16px;
+ $font-size: inherit;
+ $border-alpha: 0.12;
+ $chevron-offset: 14px;
+ $chevron-size: 11px;
+
+ @if $size == sm {
+ $height: 36px;
+ $padding-right: 34px;
+ $padding-left: 10px;
+ $icon-size: 14px;
+ $font-size: var(--font-size-sm);
+ $border-alpha: 0.14;
+ $chevron-offset: 10px;
+ } @else if $size == xs {
+ $height: 32px;
+ $padding-right: 30px;
+ $padding-left: 8px;
+ $icon-size: 13px;
+ $font-size: var(--font-size-xs);
+ $border-alpha: 0.14;
+ $chevron-offset: 9px;
+ }
+
+ position: relative;
+ display: grid;
+ grid-template-columns: $icon-size minmax(0, 1fr);
+ align-items: center;
+ gap: 6px;
+ width: 100%;
+ min-height: $height;
+ padding: 0 $padding-right 0 $padding-left;
+ border: 1px solid rgba(165, 180, 252, $border-alpha);
+ border-radius: var(--radius-md);
+ background-color: rgba(21, 26, 48, 0.86);
+ background-image:
+ linear-gradient(135deg, rgba(139, 92, 246, 0.04), rgba(31, 41, 78, 0.08)),
+ linear-gradient(rgba(21, 26, 48, 0.42), rgba(21, 26, 48, 0.42));
+ background-position:
+ 0 0,
+ 0 0;
+ background-size:
+ auto,
+ auto;
+ background-repeat: no-repeat;
+ color: var(--color-text-primary);
+ font: inherit;
+ font-size: $font-size;
+ font-weight: 900;
+ font-variant-numeric: tabular-nums;
+ text-align: left;
+ transition:
+ border-color var(--duration-fast) var(--ease-standard),
+ background-color var(--duration-fast) var(--ease-standard),
+ color var(--duration-fast) var(--ease-standard),
+ box-shadow var(--duration-fast) var(--ease-standard);
+
+ &::after {
+ content: "";
+ position: absolute;
+ top: 50%;
+ right: $chevron-offset;
+ width: $chevron-size;
+ height: $chevron-size;
+ background-color: #c4b5fd;
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-6.5 0 32 32'%3E%3Cpath d='M18.813 11.406l-7.906 9.906c-.75.906-1.906.906-2.625 0L.376 11.406c-.75-.938-.375-1.656.781-1.656h16.875c1.188 0 1.531.719.781 1.656z'/%3E%3C/svg%3E") center / contain no-repeat;
+ pointer-events: none;
+ transform: translateY(-50%);
+ transition: background-color var(--duration-fast) var(--ease-standard);
+ }
+}
+
+@mixin select-trigger-hover($size: md) {
+ border-color: rgba(196, 181, 253, 0.22);
+ background-color: rgba(21, 26, 48, 0.92);
+ background-image:
+ linear-gradient(135deg, rgba(246, 196, 83, 0.06), rgba(139, 92, 246, 0.08)),
+ linear-gradient(rgba(21, 26, 48, 0.58), rgba(21, 26, 48, 0.58));
+ background-position: 0 0, 0 0;
+ background-size: auto, auto;
+ background-repeat: no-repeat;
+ color: var(--color-text-primary);
+
+ &::after {
+ background-color: var(--color-accent-gold);
+ }
+}
+
+@mixin select-trigger-icon($size: md) {
+ $icon-size: 16px;
+
+ @if $size == sm {
+ $icon-size: 14px;
+ } @else if $size == xs {
+ $icon-size: 13px;
+ }
+
+ width: $icon-size;
+ height: $icon-size;
+ color: var(--color-text-secondary);
+}
+
+@mixin select-option {
+ background: #11182f;
+ color: var(--color-text-primary);
+}
+
+@mixin field-control($size: md) {
+ $height: 44px;
+ $padding: 0 14px;
+ $font-size: inherit;
+
+ @if $size == sm {
+ $height: 36px;
+ $padding: 0 10px;
+ $font-size: var(--font-size-sm);
+ } @else if $size == xs {
+ $height: 32px;
+ $padding: 0 8px;
+ $font-size: var(--font-size-xs);
+ }
+
+ box-sizing: border-box;
+ width: 100%;
+ min-width: 0;
+ min-height: $height;
+ padding: $padding;
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-md);
+ background:
+ linear-gradient(135deg, rgba(139, 92, 246, 0.04), rgba(31, 41, 78, 0.08)),
+ rgba(21, 26, 48, 0.38);
+ color: var(--color-text-primary);
+ font: inherit;
+ font-size: $font-size;
+ transition:
+ border-color var(--duration-fast) var(--ease-standard),
+ background-color var(--duration-fast) var(--ease-standard),
+ color var(--duration-fast) var(--ease-standard),
+ box-shadow var(--duration-fast) var(--ease-standard);
+}
+
+@mixin field-textarea($size: md) {
+ $min-height: 108px;
+ $padding: 14px 20px 20px 14px;
+
+ @if $size == sm {
+ $min-height: 88px;
+ $padding: 12px 16px 16px 10px;
+ } @else if $size == xs {
+ $min-height: 72px;
+ $padding: 10px 14px 14px 8px;
+ }
+
+ min-height: $min-height;
+ padding: $padding;
+ overflow: auto;
+ resize: vertical;
+ scrollbar-gutter: stable;
+}
+
+@mixin field-textarea-ghost {
+ border-color: transparent;
+ background: transparent;
+ box-shadow: none;
+ color: inherit;
+}
+
+@mixin field-placeholder {
+ color: var(--color-text-muted);
+ opacity: 0.58;
+}
+
+@mixin field-hover {
+ border-color: rgba(196, 181, 253, 0.22);
+ background:
+ linear-gradient(135deg, rgba(246, 196, 83, 0.04), rgba(139, 92, 246, 0.08)),
+ rgba(21, 26, 48, 0.5);
+}
+
+@mixin field-focus {
+ border-color: var(--color-primary);
+ box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.16);
+}
+
+@mixin time-control($size: md) {
+ @include field-control($size);
+ padding-right: 8px;
+ color-scheme: dark;
+ font-variant-numeric: tabular-nums;
+
+ &::-webkit-calendar-picker-indicator {
+ width: 16px;
+ height: 16px;
+ margin-left: 4px;
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ filter: invert(77%) sepia(14%) saturate(810%) hue-rotate(209deg) brightness(99%) contrast(92%);
+ opacity: 0.82;
+ transition:
+ background-color var(--duration-fast) var(--ease-standard),
+ filter var(--duration-fast) var(--ease-standard),
+ opacity var(--duration-fast) var(--ease-standard);
+ }
+
+ &::-webkit-datetime-edit {
+ min-width: 0;
+ color: var(--color-text-primary);
+ }
+
+ &::-webkit-datetime-edit-fields-wrapper {
+ padding: 0;
+ }
+
+ &::-webkit-datetime-edit-hour-field,
+ &::-webkit-datetime-edit-minute-field,
+ &::-webkit-datetime-edit-ampm-field {
+ border-radius: 4px;
+ }
+
+ &::-webkit-datetime-edit-hour-field:focus,
+ &::-webkit-datetime-edit-minute-field:focus,
+ &::-webkit-datetime-edit-ampm-field:focus {
+ background: rgba(139, 92, 246, 0.28);
+ color: var(--color-text-primary);
+ }
+
+ &:hover::-webkit-calendar-picker-indicator,
+ &:focus::-webkit-calendar-picker-indicator {
+ background-color: rgba(246, 196, 83, 0.12);
+ filter: invert(80%) sepia(77%) saturate(455%) hue-rotate(346deg) brightness(101%) contrast(93%);
+ opacity: 1;
+ }
+
+ &:disabled::-webkit-calendar-picker-indicator {
+ cursor: not-allowed;
+ opacity: 0.42;
+ }
+}
+
+@mixin field-disabled {
+ border-color: rgba(165, 180, 252, 0.08);
+ background:
+ linear-gradient(135deg, rgba(148, 163, 184, 0.05), transparent 62%),
+ rgba(15, 23, 42, 0.42);
+ box-shadow: none;
+ color: rgba(203, 213, 225, 0.66);
+ cursor: not-allowed;
+}
+
+@mixin inline-field($size: md) {
+ $height: 30px;
+ $padding: 5px 8px;
+ $radius: var(--radius-md);
+
+ @if $size == sm {
+ $height: 28px;
+ $padding: 4px 7px;
+ $radius: var(--radius-sm);
+ } @else if $size == xs {
+ $height: 26px;
+ $padding: 3px 6px;
+ $radius: var(--radius-sm);
+ }
+
+ box-sizing: border-box;
+ width: 100%;
+ min-width: 0;
+ min-height: $height;
+ padding: $padding;
+ overflow: hidden;
+ border: 1px solid transparent;
+ border-radius: $radius;
+ background: transparent;
+ color: var(--color-text-primary);
+ font: inherit;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ transition:
+ border-color var(--duration-fast) var(--ease-standard),
+ background-color var(--duration-fast) var(--ease-standard),
+ color var(--duration-fast) var(--ease-standard),
+ box-shadow var(--duration-fast) var(--ease-standard);
+}
+
+@mixin inline-field-hover {
+ border-color: rgba(246, 196, 83, 0.34);
+ background: rgba(21, 26, 48, 0.42);
+}
+
+@mixin inline-textarea {
+ box-sizing: border-box;
+ width: 100%;
+ min-width: 0;
+ border: 0;
+ background: transparent;
+ color: var(--color-text-primary);
+ font: inherit;
+ resize: vertical;
+}
+
+@mixin table-header-field {
+ width: 100%;
+ padding: 0 8px;
+ border-top: 0;
+ border-left: 0;
+ border-radius: 0;
+ text-align: center;
+}
+
+@mixin table-header-field-focus {
+ z-index: 5;
+ border-color: rgba(246, 196, 83, 0.38);
+ background: rgba(21, 26, 48, 0.8);
+ color: var(--color-text-primary);
+ box-shadow: inset 0 0 0 1px rgba(246, 196, 83, 0.55), 0 0 0 2px rgba(139, 92, 246, 0.18);
+ outline: 0;
+}
+
+@mixin table-cell-field {
+ width: 100%;
+ min-width: 0;
+ min-height: 34px;
+ padding: 6px 8px;
+ border: 0;
+ border-right: 1px solid rgba(165, 180, 252, 0.08);
+ border-bottom: 1px solid rgba(165, 180, 252, 0.08);
+ border-radius: 0;
+ background: rgba(16, 20, 38, 0.34);
+ color: var(--color-text-primary);
+ caret-color: transparent;
+ font-size: var(--font-size-sm);
+ font-variant-numeric: tabular-nums;
+}
+
+@mixin table-cell-field-focus {
+ position: relative;
+ z-index: 4;
+ border-color: rgba(34, 211, 238, 0.42);
+ background:
+ linear-gradient(135deg, rgba(34, 211, 238, 0.08), rgba(139, 92, 246, 0.06)),
+ rgba(21, 26, 48, 0.58);
+ box-shadow: inset 0 0 0 1px rgba(34, 211, 238, 0.58);
+ outline: 0;
+}
+
+@mixin table-cell-field-editing-focus {
+ border-color: rgba(246, 196, 83, 0.38);
+ background: rgba(21, 26, 48, 0.72);
+ caret-color: auto;
+ box-shadow: inset 0 0 0 1px rgba(246, 196, 83, 0.55), 0 0 0 2px rgba(139, 92, 246, 0.18);
+}
+
+@mixin time-parts-control($size: md) {
+ $height: 44px;
+ $part-height: 42px;
+ $column-min: 34px;
+
+ @if $size == sm {
+ $height: 38px;
+ $part-height: 36px;
+ $column-min: 22px;
+ }
+
+ display: grid;
+ grid-template-columns: minmax($column-min, 1fr) 4px minmax($column-min, 1fr) 4px minmax($column-min, 1fr);
+ align-items: center;
+ min-height: $height;
+ overflow: hidden;
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-md);
+ background: rgba(15, 23, 42, 0.64);
+
+ input {
+ width: auto;
+ min-width: 0;
+ min-height: $part-height;
+ padding: 0;
+ border: 0;
+ border-radius: 0;
+ background: transparent;
+ text-align: center;
+ box-shadow: none;
+ }
+}
+
+@mixin time-parts-focus {
+ border-color: var(--color-primary);
+ box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.16);
+}
diff --git a/website/src/styles/design-system/controls/_menus.scss b/website/src/styles/design-system/controls/_menus.scss
new file mode 100644
index 0000000..4385475
--- /dev/null
+++ b/website/src/styles/design-system/controls/_menus.scss
@@ -0,0 +1,254 @@
+// Rôle : regroupe les mixins de menus compacts, swatches et pickers.
+@use "buttons";
+@use "fields";
+
+@mixin date-picker-button($size: md) {
+ $height: 22px;
+ $padding: 0;
+ $font-size: 11px;
+
+ @if $size == header {
+ $height: 24px;
+ $padding: 0 6px;
+ $font-size: 10px;
+ } @else if $size == weekday {
+ $height: 26px;
+ $padding: 0 3px;
+ $font-size: 10px;
+ }
+
+ display: grid;
+ min-width: 0;
+ min-height: $height;
+ place-items: center;
+ padding: $padding;
+ overflow: hidden;
+ border: 1px solid transparent;
+ border-radius: var(--radius-sm);
+ background: rgba(16, 20, 38, 0.58);
+ color: var(--color-text-secondary);
+ font-size: $font-size;
+ font-weight: 800;
+ font-variant-numeric: tabular-nums;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+@mixin date-picker-button-hover {
+ border-color: rgba(246, 196, 83, 0.58);
+ background: rgba(139, 92, 246, 0.3);
+ color: var(--color-text-primary);
+ box-shadow: none;
+}
+
+@mixin date-picker-trigger {
+ @include fields.select-trigger(sm);
+}
+
+@mixin date-picker-trigger-hover {
+ @include fields.select-trigger-hover;
+}
+
+@mixin date-picker-trigger-icon {
+ @include fields.select-trigger-icon(sm);
+}
+
+@mixin date-picker-trigger-chevron {
+ display: none;
+}
+
+@mixin compact-menu-button($size: md) {
+ $button-size: 42px;
+ $icon-size: 26px;
+
+ @if $size == sm {
+ $button-size: 34px;
+ $icon-size: 21px;
+ }
+
+ @include buttons.icon-button;
+ width: $button-size;
+ min-width: $button-size;
+ height: $button-size;
+ min-height: $button-size;
+
+ img,
+ .ui-icon {
+ width: $icon-size;
+ height: $icon-size;
+ }
+}
+
+@mixin compact-menu-button-hover {
+ @include buttons.icon-button-hover;
+}
+
+@mixin floating-control-menu($columns: null, $item-size: 34px) {
+ display: grid;
+ gap: 6px;
+ padding: 8px;
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-md);
+ background: rgba(10, 13, 28, 0.99);
+ box-shadow: var(--shadow-lg);
+
+ @if $columns {
+ grid-template-columns: repeat($columns, $item-size);
+ }
+}
+
+@mixin color-swatch($size: 34px) {
+ @include buttons.icon-button(sm);
+ width: $size;
+ min-width: $size;
+ min-height: $size;
+ padding: 0;
+ box-shadow: none;
+
+ &::before {
+ content: "";
+ width: max(12px, $size - 16px);
+ height: max(12px, $size - 16px);
+ border: 1px solid rgba(245, 247, 255, 0.22);
+ border-radius: 50%;
+ background: var(--swatch-color, var(--goal-tree-swatch-color, var(--color-accent-gold)));
+ box-shadow: inset 0 0 0 2px rgba(5, 7, 17, 0.36);
+ }
+}
+
+@mixin color-swatch-rainbow($size: 34px) {
+ @include color-swatch($size);
+
+ &::before {
+ width: max(14px, $size - 14px);
+ height: max(14px, $size - 14px);
+ border-color: rgba(245, 247, 255, 0.28);
+ background: conic-gradient(
+ from 20deg,
+ #f6c453,
+ #22d3ee,
+ #8b5cf6,
+ #d946ef,
+ #f5f7ff,
+ #f6c453
+ );
+ box-shadow:
+ inset 0 0 0 3px rgba(5, 7, 17, 0.34),
+ 0 0 10px rgba(139, 92, 246, 0.16);
+ }
+}
+
+@mixin color-swatch-active {
+ @include buttons.icon-button-active;
+}
+
+@mixin large-icon-picker-trigger {
+ display: grid;
+ width: 100px;
+ min-width: 100px;
+ min-height: 100px;
+ place-items: center;
+ padding: 0;
+ border: 1px solid var(--color-primary-border);
+ border-radius: var(--radius-md);
+ background:
+ radial-gradient(circle at 50% 35%, rgba(246, 196, 83, 0.18), transparent 44%),
+ rgba(21, 26, 48, 0.86);
+ color: var(--color-text-primary);
+}
+
+@mixin large-icon-picker-trigger-hover {
+ border-color: rgba(246, 196, 83, 0.42);
+ background:
+ radial-gradient(circle at 50% 35%, rgba(246, 196, 83, 0.24), transparent 44%),
+ rgba(31, 37, 68, 0.94);
+ box-shadow: 0 0 18px rgba(139, 92, 246, 0.18);
+}
+
+@mixin position-toggle {
+ position: relative;
+ display: inline-grid;
+ grid-template-columns: repeat(2, 32px);
+ width: 74px;
+ min-height: 38px;
+ align-items: center;
+ justify-items: center;
+ gap: 0;
+ padding: 3px 4px;
+ border: 1px solid rgba(165, 180, 252, 0.12);
+ border-radius: var(--radius-pill);
+ background: rgba(7, 10, 24, 0.32);
+ color: var(--color-text-secondary);
+
+ &::before {
+ content: "";
+ position: absolute;
+ top: 3px;
+ bottom: 3px;
+ left: 4px;
+ width: 30px;
+ border: 1px solid rgba(246, 196, 83, 0.3);
+ border-radius: 50%;
+ background:
+ linear-gradient(135deg, rgba(246, 196, 83, 0.1), rgba(139, 92, 246, 0.08)),
+ rgba(21, 26, 48, 0.58);
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
+ transition: transform var(--duration-fast) var(--ease-standard);
+ }
+
+ &[data-position="right"]::before {
+ transform: translateX(32px);
+ }
+
+ span {
+ position: relative;
+ z-index: 1;
+ display: inline-grid;
+ width: 30px;
+ height: 30px;
+ place-items: center;
+ justify-self: center;
+ align-self: center;
+ border-radius: 50%;
+ }
+
+ .ui-icon {
+ width: 16px;
+ height: 16px;
+ }
+}
+
+@mixin position-toggle-hover {
+ border-color: rgba(196, 181, 253, 0.24);
+ color: var(--color-text-primary);
+}
+
+@mixin edge-toggle {
+ @include buttons.icon-button;
+ @include buttons.icon-button-panel-toggle;
+ @include buttons.button-edge(right);
+}
+
+@mixin edge-toggle-hover {
+ @include buttons.icon-button-panel-toggle-hover;
+}
+
+@mixin split-button-start {
+ @include buttons.button-split(start);
+}
+
+@mixin split-button-end {
+ justify-content: center;
+ padding: 0;
+ @include buttons.button-split(end);
+}
+
+@mixin dashed-action-button {
+ @include buttons.text-button;
+ @include buttons.text-button-dashed;
+ font-size: var(--font-size-sm);
+}
+
+@mixin dashed-action-button-hover {
+ @include buttons.text-button-dashed-hover;
+}
diff --git a/website/src/styles/design-system/controls/_scrollbars.scss b/website/src/styles/design-system/controls/_scrollbars.scss
new file mode 100644
index 0000000..3f7fb8a
--- /dev/null
+++ b/website/src/styles/design-system/controls/_scrollbars.scss
@@ -0,0 +1,41 @@
+// Rôle : regroupe les mixins de scrollbar et resize handle.
+@mixin scrollbar-control {
+ scrollbar-color: rgb(88, 55, 174) transparent;
+ scrollbar-width: thin;
+
+ &::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+ border-radius: 0;
+ background-color: transparent;
+ }
+
+ &::-webkit-scrollbar-track {
+ border-radius: 0;
+ background-color: transparent;
+ box-shadow: none;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ border-radius: var(--radius-pill);
+ background: linear-gradient(180deg, rgb(88, 55, 174), rgb(48, 30, 118));
+ }
+
+ &::-webkit-scrollbar-thumb:hover {
+ background: linear-gradient(180deg, rgba(246, 196, 83, 0.9), rgb(88, 55, 174));
+ }
+
+ &::-webkit-scrollbar-corner {
+ background: transparent;
+ }
+}
+
+@mixin scrollbar-resizer {
+ &::-webkit-resizer {
+ background-color: transparent;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M4 10L10 4M7 10L10 7' fill='none' stroke='%23f6c453' stroke-width='1.5' stroke-linecap='round' opacity='.82'/%3E%3Cpath d='M2 10L10 2' fill='none' stroke='%23c4b5fd' stroke-width='1' stroke-linecap='round' opacity='.34'/%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: calc(50% - 2px) calc(50% - 2px);
+ background-size: 12px 12px;
+ }
+}
diff --git a/website/src/styles/design-system/controls/_steppers.scss b/website/src/styles/design-system/controls/_steppers.scss
new file mode 100644
index 0000000..a326c33
--- /dev/null
+++ b/website/src/styles/design-system/controls/_steppers.scss
@@ -0,0 +1,116 @@
+// Rôle : regroupe les mixins de steppers numériques et reset de spinner.
+@mixin quantity-stepper($size: md) {
+ $button-size: 28px;
+ $value-min: 52px;
+ $gap: 6px;
+ $font-size: var(--font-size-sm);
+
+ @if $size == sm {
+ $button-size: 18px;
+ $value-min: 34px;
+ $gap: 2px;
+ $font-size: 0.68rem;
+ } @else if $size == xs {
+ $button-size: 16px;
+ $value-min: 30px;
+ $gap: 2px;
+ $font-size: 0.64rem;
+ }
+
+ display: inline-grid;
+ grid-template-columns: $button-size minmax($value-min, auto) $button-size;
+ flex: 0 0 auto;
+ align-items: center;
+ gap: $gap;
+ color: var(--color-text-secondary);
+ font-size: $font-size;
+ font-weight: 800;
+ white-space: nowrap;
+}
+
+@mixin quantity-stepper-button($size: md) {
+ $button-size: 28px;
+
+ @if $size == sm {
+ $button-size: 18px;
+ } @else if $size == xs {
+ $button-size: 16px;
+ }
+
+ display: inline-grid;
+ width: $button-size;
+ min-width: $button-size;
+ height: $button-size;
+ min-height: $button-size;
+ place-items: center;
+ padding: 0;
+ border: 1px solid rgba(165, 180, 252, 0.12);
+ border-radius: var(--radius-md);
+ background: rgba(21, 26, 48, 0.88);
+ color: var(--color-text-secondary);
+ line-height: 1;
+}
+
+@mixin quantity-stepper-button-hover {
+ border-color: rgba(196, 181, 253, 0.32);
+ background:
+ linear-gradient(135deg, rgba(31, 37, 68, 0.94), rgba(18, 22, 44, 0.96)) padding-box,
+ linear-gradient(135deg, rgba(139, 92, 246, 0.3), rgba(196, 181, 253, 0.2)) border-box;
+ color: var(--color-text-primary);
+ box-shadow:
+ 0 0 10px rgba(139, 92, 246, 0.1),
+ inset 0 1px 0 rgba(255, 255, 255, 0.05);
+}
+
+@mixin quantity-stepper-button-icon($size: md) {
+ $icon-size: 16px;
+
+ @if $size == sm {
+ $icon-size: 10px;
+ } @else if $size == xs {
+ $icon-size: 9px;
+ }
+
+ width: $icon-size;
+ height: $icon-size;
+ flex: 0 0 $icon-size;
+}
+
+@mixin quantity-stepper-value {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 3px;
+ color: var(--color-text-secondary);
+ text-align: center;
+}
+
+@mixin quantity-stepper-input {
+ box-sizing: border-box;
+ width: max(18px, calc((var(--qty-current-digits, 1) * 0.72rem) + 8px));
+ min-height: 28px;
+ padding: 0 4px;
+ border: 1px solid transparent;
+ border-radius: var(--radius-sm);
+ background: transparent;
+ color: var(--color-text-primary);
+ font: inherit;
+ text-align: right;
+ -moz-appearance: textfield;
+}
+
+@mixin quantity-stepper-input-hover {
+ border-color: rgba(246, 196, 83, 0.34);
+ background: rgba(21, 26, 48, 0.42);
+}
+
+@mixin number-spinner-reset {
+ -moz-appearance: textfield;
+
+ &::-webkit-outer-spin-button,
+ &::-webkit-inner-spin-button {
+ margin: 0;
+ appearance: none;
+ -webkit-appearance: none;
+ }
+}
diff --git a/website/src/styles/design-system/controls/_switches.scss b/website/src/styles/design-system/controls/_switches.scss
new file mode 100644
index 0000000..d92ef47
--- /dev/null
+++ b/website/src/styles/design-system/controls/_switches.scss
@@ -0,0 +1,275 @@
+// Rôle : regroupe les mixins de switchs, radio groups et toggles composés.
+@use "../../mixins";
+@use "buttons";
+@use "fields";
+@use "steppers";
+
+@mixin switch-control($size: md) {
+ $icon-size: 16px;
+ $track-width: 34px;
+ $track-height: 18px;
+ $thumb-size: 12px;
+ $thumb-left: 3px;
+ $active-left: 17px;
+ $height: 34px;
+ $gap: 8px;
+ $padding: 0 6px 0 10px;
+ $radius: var(--radius-pill);
+ $font-size: var(--font-size-sm);
+
+ @if $size == lg {
+ $track-width: 30px;
+ $track-height: 16px;
+ $thumb-size: 10px;
+ $active-left: 15px;
+ $height: 40px;
+ $gap: 7px;
+ $padding: 0 7px 0 10px;
+ } @else if $size == sm {
+ $icon-size: 13px;
+ $track-width: 24px;
+ $track-height: 14px;
+ $thumb-size: 8px;
+ $thumb-left: 2px;
+ $active-left: 12px;
+ $height: 28px;
+ $gap: 5px;
+ $padding: 0 5px 0 8px;
+ $font-size: var(--font-size-xs);
+ } @else if $size == xs {
+ $icon-size: 13px;
+ $track-width: 18px;
+ $track-height: 12px;
+ $thumb-size: 8px;
+ $thumb-left: 2px;
+ $active-left: 7px;
+ $height: 26px;
+ $gap: 3px;
+ $padding: 0 4px;
+ $radius: var(--radius-sm);
+ $font-size: var(--font-size-xs);
+ }
+
+ --nds-switch-icon-size: #{$icon-size};
+ --nds-switch-track-width: #{$track-width};
+ --nds-switch-track-height: #{$track-height};
+ --nds-switch-thumb-size: #{$thumb-size};
+ --nds-switch-thumb-left: #{$thumb-left};
+ --nds-switch-active-left: #{$active-left};
+ display: inline-grid;
+ grid-template-columns: $icon-size $track-width;
+ width: fit-content;
+ min-height: $height;
+ align-items: center;
+ gap: $gap;
+ padding: $padding;
+ border: 1px solid rgba(165, 180, 252, 0.12);
+ border-radius: $radius;
+ background: rgba(5, 7, 17, 0.24);
+ color: var(--color-text-secondary);
+ font-size: $font-size;
+ font-weight: 800;
+}
+
+@mixin switch-icon {
+ width: var(--nds-switch-icon-size);
+ height: var(--nds-switch-icon-size);
+}
+
+@mixin switch-track {
+ position: relative;
+ display: block;
+ width: var(--nds-switch-track-width);
+ height: var(--nds-switch-track-height);
+ border: 1px solid rgba(165, 180, 252, 0.18);
+ border-radius: var(--radius-pill);
+ background: rgba(7, 10, 24, 0.72);
+}
+
+@mixin switch-thumb {
+ content: "";
+ position: absolute;
+ top: 50%;
+ left: var(--nds-switch-thumb-left);
+ width: var(--nds-switch-thumb-size);
+ height: var(--nds-switch-thumb-size);
+ border-radius: 50%;
+ background: var(--color-text-muted);
+ transform: translateY(-50%);
+ transition:
+ left var(--duration-fast) var(--ease-standard),
+ background-color var(--duration-fast) var(--ease-standard),
+ box-shadow var(--duration-fast) var(--ease-standard);
+}
+
+@mixin switch-hover {
+ @include buttons.icon-button-hover;
+}
+
+@mixin switch-active {
+ @include mixins.temporary-toggle-active;
+}
+
+@mixin switch-active-track {
+ border-color: rgba(246, 196, 83, 0.36);
+ background: rgba(246, 196, 83, 0.12);
+}
+
+@mixin switch-active-thumb {
+ left: var(--nds-switch-active-left);
+ background: var(--color-accent-gold);
+ box-shadow: 0 0 10px rgba(246, 196, 83, 0.28);
+}
+
+@mixin native-switch-label {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ min-width: 0;
+ color: var(--color-text-secondary);
+ font-size: var(--font-size-xs);
+ font-weight: 900;
+ cursor: pointer;
+}
+
+@mixin native-switch-input {
+ position: absolute;
+ opacity: 0;
+ pointer-events: none;
+}
+
+@mixin radio-button-group-surface($size: md) {
+ $column-min: 36px;
+ $gap: 4px;
+ $padding: 4px;
+ $radius: var(--radius-md);
+ $background: rgba(5, 7, 17, 0.38);
+
+ @if $size == sm {
+ $column-min: 30px;
+ $gap: 3px;
+ $padding: 3px;
+ } @else if $size == xs {
+ $column-min: 24px;
+ $gap: 0;
+ $padding: 2px;
+ $radius: var(--radius-sm);
+ $background: rgba(5, 7, 17, 0.26);
+ }
+
+ --nds-radio-button-group-column-min: #{$column-min};
+ gap: $gap;
+ max-width: 100%;
+ padding: $padding;
+ border: 1px solid rgba(165, 180, 252, 0.12);
+ border-radius: $radius;
+ background: $background;
+}
+
+@mixin radio-button-group($size: md) {
+ @include radio-button-group-surface($size);
+ display: inline-grid;
+ grid-auto-flow: column;
+ grid-auto-columns: minmax(var(--nds-radio-button-group-column-min), 1fr);
+}
+
+@mixin radio-button-group-option($size: md) {
+ $height: 34px;
+ $padding: 0 10px;
+ $radius: calc(var(--radius-md) - 2px);
+ $font-size: var(--font-size-sm);
+ $icon-size: 16px;
+
+ @if $size == sm {
+ $height: 28px;
+ $padding: 0;
+ $font-size: var(--font-size-xs);
+ $icon-size: 19px;
+ } @else if $size == xs {
+ $height: 26px;
+ $padding: 0;
+ $radius: var(--radius-sm);
+ $font-size: var(--font-size-xs);
+ $icon-size: 13px;
+ }
+
+ --nds-radio-button-group-option-icon-size: #{$icon-size};
+ display: inline-flex;
+ min-width: 0;
+ min-height: $height;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ padding: $padding;
+ border: 1px solid transparent;
+ border-radius: $radius;
+ background: transparent;
+ color: var(--color-text-secondary);
+ font-size: $font-size;
+ font-weight: 900;
+
+ > .ui-icon {
+ width: var(--nds-radio-button-group-option-icon-size);
+ height: var(--nds-radio-button-group-option-icon-size);
+ min-width: var(--nds-radio-button-group-option-icon-size);
+ min-height: var(--nds-radio-button-group-option-icon-size);
+ flex: 0 0 var(--nds-radio-button-group-option-icon-size);
+ }
+}
+
+@mixin radio-button-group-option-compact-overflow {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+@mixin radio-button-group-option-hover {
+ border-color: rgba(196, 181, 253, 0.22);
+ background: rgba(21, 26, 48, 0.5);
+ color: var(--color-text-primary);
+ box-shadow: none;
+}
+
+@mixin radio-button-group-option-active {
+ @include mixins.temporary-toggle-active;
+}
+
+@mixin radio-button-group-option-icon($size: md) {
+ $icon-size: 16px;
+
+ @if $size == sm {
+ $icon-size: 19px;
+ } @else if $size == xs {
+ $icon-size: 13px;
+ }
+
+ --nds-radio-button-group-option-icon-size: #{$icon-size};
+ width: var(--nds-radio-button-group-option-icon-size);
+ height: var(--nds-radio-button-group-option-icon-size);
+ min-width: var(--nds-radio-button-group-option-icon-size);
+ min-height: var(--nds-radio-button-group-option-icon-size);
+ flex: 0 0 var(--nds-radio-button-group-option-icon-size);
+}
+
+@mixin compound-toggle-field {
+ @include radio-button-group(xs);
+ align-items: center;
+ grid-auto-columns: max-content;
+}
+
+@mixin compound-toggle-field-option {
+ @include radio-button-group-option(xs);
+ min-width: 50px;
+}
+
+@mixin compound-toggle-field-input {
+ @include fields.field-control(xs);
+ @include steppers.number-spinner-reset;
+ width: 34px;
+ min-height: 26px;
+ padding: 0 4px;
+ border-color: transparent;
+ border-radius: var(--radius-sm);
+ background: transparent;
+ text-align: center;
+}
diff --git a/website/src/styles/games/_home-cards.scss b/website/src/styles/games/_home-cards.scss
index f0f0e8b..029e370 100644
--- a/website/src/styles/games/_home-cards.scss
+++ b/website/src/styles/games/_home-cards.scss
@@ -1,4 +1,5 @@
// Rôle : regroupe les cartes et surfaces d'entrée des pages jeux.
+@use "../design-system/surfaces";
.game-home-grid {
display: grid;
@@ -9,15 +10,10 @@
}
.game-home-card {
+ @include surfaces.card-surface;
overflow: hidden;
- border-color: transparent;
- border-radius: var(--radius-md);
padding: 0;
min-height: 230px;
- background:
- linear-gradient(rgba(16, 20, 38, 0.88), rgba(16, 20, 38, 0.88)) padding-box,
- radial-gradient(circle at 100% 0%, rgba(34, 211, 238, 0.42), transparent 28%) border-box,
- linear-gradient(135deg, rgba(246, 196, 83, 0.62) 0%, rgba(246, 196, 83, 0.42) 72%, rgba(34, 211, 238, 0.24) 100%) border-box;
}
@supports (corner-shape: scoop) {
@@ -27,15 +23,8 @@
}
.game-home-card:hover {
+ @include surfaces.card-surface-hover;
transform: none;
- border-color: transparent;
- background:
- linear-gradient(rgba(21, 26, 48, 0.96), rgba(21, 26, 48, 0.96)) padding-box,
- linear-gradient(135deg, rgba(139, 92, 246, 0.9), rgba(196, 181, 253, 0.72)) border-box;
- box-shadow:
- var(--shadow-sm),
- 0 0 18px rgba(139, 92, 246, 0.22),
- 0 0 34px rgba(196, 181, 253, 0.12);
}
.game-home-card img {
@@ -44,7 +33,7 @@
height: 120px;
object-fit: contain;
padding: var(--space-4);
- border-bottom: 1px solid rgba(246, 196, 83, 0.42);
+ @include surfaces.card-cover-divider;
background: rgba(5, 7, 17, 0.28);
}
@@ -53,7 +42,7 @@
width: 100%;
height: 120px;
place-items: center;
- border-bottom: 1px solid rgba(246, 196, 83, 0.42);
+ @include surfaces.card-cover-divider;
background: rgba(5, 7, 17, 0.28);
color: #fff;
}
diff --git a/website/src/styles/toolboxes/_memos.scss b/website/src/styles/toolboxes/_memos.scss
index 989af2f..1e3800b 100644
--- a/website/src/styles/toolboxes/_memos.scss
+++ b/website/src/styles/toolboxes/_memos.scss
@@ -1,4 +1,4 @@
-// Rôle : stylise l'outil Memos, ses post-it et sa bande déroulante.
+// Rôle : stylise l'outil Mémos, ses post-it et sa bande déroulante.
.memos-module {
display: grid;
gap: var(--space-3);
diff --git a/website/src/styles/toolboxes/_randomizer.scss b/website/src/styles/toolboxes/_randomizer.scss
index 41c350c..fc04e99 100644
--- a/website/src/styles/toolboxes/_randomizer.scss
+++ b/website/src/styles/toolboxes/_randomizer.scss
@@ -1,1055 +1,8 @@
-// Rôle : porte les visualisations propres à l'outil Randomizer.
-.randomizer-tool {
- display: grid;
- gap: var(--space-4);
- padding: var(--space-4);
-}
-
-.randomizer-tabs {
- justify-self: stretch;
- width: min(100%, 560px);
-}
-
-.randomizer-tabs.tabs {
- grid-template-columns: repeat(3, minmax(0, 1fr));
-}
-
-.randomizer-tabs button {
- min-height: 42px;
- padding-inline: 14px;
- font-size: var(--font-size-sm);
-}
-
-.randomizer-tabs .module-icon-svg {
- width: 18px;
- height: 18px;
-}
-
-.randomizer-panel {
- display: grid;
- gap: var(--space-4);
-}
-
-.randomizer-dice-panel,
-.randomizer-wheel-panel {
- gap: var(--space-5);
-}
-
-.randomizer-wheel-panel {
- gap: var(--space-3);
-}
-
-.randomizer-controls {
- display: flex;
- flex-wrap: wrap;
- align-items: end;
- gap: var(--space-3);
-}
-
-.randomizer-controls label,
-.randomizer-field {
- display: grid;
- gap: 6px;
- min-width: 0;
- color: var(--color-text-secondary);
- font-size: var(--font-size-xs);
- font-weight: 800;
-}
-
-.randomizer-controls label {
- min-width: 96px;
-}
-
-.randomizer-field > .nds-input,
-.randomizer-field > .nds-select,
-.randomizer-field > .nds-textarea {
- width: 100%;
- min-width: 0;
-}
-
-.randomizer-wheel-stage {
- display: grid;
- justify-items: center;
- gap: var(--space-4);
- min-width: 0;
-}
-
-.randomizer-dice-stage,
-.randomizer-wheel-stage {
- display: grid;
- justify-items: center;
- gap: var(--space-3);
- min-width: 0;
-}
-
-.randomizer-live-results {
- display: grid;
- align-items: start;
- justify-items: center;
- width: min(100%, 560px);
- min-height: 46px;
- padding-block: 4px;
-}
-
-.randomizer-live-results .randomizer-results {
- justify-content: center;
- max-height: 84px;
-}
-
-.randomizer-wheel-results {
- width: 100%;
- min-height: 38px;
- padding-block: 0;
-}
-
-.randomizer-dice-stage .randomizer-live-results .randomizer-chip {
- width: 44px;
- justify-content: center;
- padding-inline: 0;
- font-variant-numeric: tabular-nums;
-}
-
-.randomizer-wheel-results .randomizer-chip {
- width: 112px;
- justify-content: center;
-}
-
-.randomizer-dice-grid,
-.randomizer-wheel-grid {
- display: grid;
- grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
- width: 100%;
- gap: var(--space-4);
- align-items: start;
- min-width: 0;
-}
-
-.randomizer-dice-grid > .randomizer-dice-form,
-.randomizer-dice-grid > .randomizer-history,
-.randomizer-wheel-grid > .randomizer-wheel-form,
-.randomizer-wheel-grid > .randomizer-wheel-values-section,
-.randomizer-wheel-grid > .randomizer-history {
- width: 100%;
- justify-self: stretch;
-}
-
-.randomizer-wheel-grid > .randomizer-history {
- grid-column: 1 / -1;
-}
-
-.randomizer-dice-form,
-.randomizer-wheel-form,
-.randomizer-wheel-values-section {
- display: grid;
- gap: var(--space-3);
- align-content: start;
- min-width: 0;
- padding: 12px;
-}
-
-.randomizer-dice-form .randomizer-controls,
-.randomizer-wheel-form .randomizer-controls {
- display: grid;
- gap: var(--space-3);
-}
-
-.randomizer-dice-form .randomizer-controls label,
-.randomizer-dice-form .randomizer-controls .nds-button,
-.randomizer-wheel-form .randomizer-controls label,
-.randomizer-wheel-form .randomizer-controls .nds-button {
- width: 100%;
-}
-
-.randomizer-die {
- position: relative;
- display: grid;
- width: 116px;
- aspect-ratio: 1;
- place-items: center;
- color: var(--color-text-primary);
- font-size: 42px;
- font-weight: 900;
- line-height: 1;
-}
-
-.randomizer-die::before {
- position: absolute;
- inset: 12px;
- border-radius: 999px;
- background: radial-gradient(circle, rgba(246, 196, 83, 0.22), transparent 62%);
- content: "";
- filter: blur(10px);
-}
-
-.randomizer-die-shape {
- position: absolute;
- inset: 0;
- width: 100%;
- height: 100%;
- overflow: visible;
- filter: drop-shadow(0 18px 30px rgba(0, 0, 0, 0.3));
-}
-
-.randomizer-die-shape circle,
-.randomizer-die-shape path,
-.randomizer-die-shape rect {
- fill: rgba(16, 20, 38, 0.92);
- stroke: rgba(246, 196, 83, 0.42);
- stroke-width: 1.2;
-}
-
-.randomizer-die-value {
- position: relative;
- z-index: 1;
- display: block;
- width: 3ch;
- height: 52px;
- min-width: 2ch;
- overflow: hidden;
- font-variant-numeric: tabular-nums;
- text-align: center;
-}
-
-.randomizer-die-value-track {
- display: grid;
- transform: translateY(calc(var(--randomizer-die-index, 0) * -52px));
- transition: transform var(--randomizer-die-duration, 120ms) cubic-bezier(0.2, 0.82, 0.25, 1);
- will-change: transform;
-}
-
-.randomizer-die-value-track span {
- display: block;
- height: 52px;
- line-height: 52px;
-}
-
-.randomizer-die.is-rolling {
- color: var(--color-accent-gold);
-}
-
-.randomizer-die.is-rolling .randomizer-die-shape {
- filter: drop-shadow(0 18px 30px rgba(0, 0, 0, 0.3)) drop-shadow(0 0 18px rgba(246, 196, 83, 0.18));
-}
-
-.randomizer-die.is-rolling .randomizer-die-shape circle,
-.randomizer-die.is-rolling .randomizer-die-shape path,
-.randomizer-die.is-rolling .randomizer-die-shape rect {
- stroke: rgba(246, 196, 83, 0.68);
-}
-
-.randomizer-die.is-rolling .randomizer-die-value {
- color: var(--color-accent-gold);
- text-shadow: 0 0 18px rgba(246, 196, 83, 0.28);
-}
-
-.randomizer-dice-form,
-.randomizer-wheel-form,
-.randomizer-wheel-values-section,
-.randomizer-team-basic,
-.randomizer-team-advanced-section,
-.randomizer-history {
- border: 1px solid rgba(165, 180, 252, 0.12);
- border-radius: var(--radius-md);
- background: rgba(7, 10, 24, 0.3);
-}
-
-.randomizer-results,
-.randomizer-exclusions {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- min-width: 0;
- max-width: 100%;
- max-height: 168px;
- overflow: auto;
- padding: 2px;
-}
-
-.randomizer-history > .randomizer-empty {
- align-self: center;
- margin: 0;
- padding: 8px 2px;
-}
-
-.randomizer-chip {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- min-height: 30px;
- max-width: 100%;
- overflow: hidden;
- padding: 4px 8px;
- border: 1px solid rgba(165, 180, 252, 0.16);
- border-radius: var(--radius-pill);
- background: rgba(16, 20, 38, 0.72);
- color: var(--color-text-primary);
- font-size: var(--font-size-xs);
- font-weight: 800;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.randomizer-chip-label {
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.randomizer-wheel-stage {
- position: relative;
- display: grid;
- grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
- gap: var(--space-4);
- align-items: center;
- justify-items: stretch;
- min-height: 228px;
- align-content: center;
-}
-
-.randomizer-wheel-spacer {
- display: none;
-}
-
-.randomizer-wheel-center {
- position: relative;
- display: grid;
- justify-self: center;
- justify-items: center;
- gap: var(--space-3);
- min-width: 210px;
- width: 100%;
-}
-
-.randomizer-wheel-pointer {
- position: absolute;
- top: 2px;
- left: 50%;
- z-index: 2;
- width: 0;
- height: 0;
- border-right: 10px solid transparent;
- border-left: 10px solid transparent;
- border-top: 18px solid var(--color-accent-gold);
- transform: translateX(-50%);
- filter: drop-shadow(0 4px 10px rgba(0, 0, 0, 0.42));
-}
-
-.randomizer-wheel {
- width: min(100%, 190px);
- aspect-ratio: 1;
- margin-top: 16px;
- border-radius: 50%;
- box-shadow: 0 18px 44px rgba(0, 0, 0, 0.32);
- transform: rotate(var(--randomizer-wheel-rotation, 0deg));
- transition: transform 1120ms cubic-bezier(0.15, 0.78, 0.18, 1);
-}
-
-.randomizer-wheel-legend {
- align-self: center;
- justify-self: stretch;
- display: grid;
- gap: 8px;
- align-content: start;
- width: 100%;
- min-width: 0;
- max-height: 238px;
- overflow: auto;
- padding: 8px;
- border: 1px solid rgba(165, 180, 252, 0.12);
- border-radius: var(--radius-md);
- background: rgba(7, 10, 24, 0.3);
-}
-
-.randomizer-wheel-legend.nds-scrollbox {
- max-height: 238px;
-}
-
-.randomizer-wheel-legend-row {
- display: grid;
- grid-template-columns: 14px minmax(0, 1fr);
- gap: 8px;
- align-items: center;
- min-width: 0;
-}
-
-.randomizer-wheel-swatch {
- display: block;
- width: 10px;
- height: 10px;
- border-radius: 50%;
- background: var(--randomizer-swatch);
- box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.04);
-}
-
-.randomizer-wheel-label {
- min-width: 0;
- overflow: hidden;
- color: var(--color-text-primary);
- font-size: var(--font-size-sm);
- font-weight: 800;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.randomizer-wheel-legend .randomizer-empty {
- margin: 0;
- color: var(--color-text-muted);
- font-size: var(--font-size-xs);
-}
-
-.randomizer-wheel.is-spinning {
- filter: drop-shadow(0 0 18px rgba(246, 196, 83, 0.18));
-}
-
-.randomizer-wheel path {
- stroke: rgba(7, 10, 24, 0.74);
- stroke-width: 0.8;
-}
-
-.randomizer-wheel-form .randomizer-entry-form {
- padding: 0;
-}
-
-.randomizer-wheel-form .randomizer-entry-form .nds-button {
- grid-column: auto;
-}
-
-.randomizer-wheel-form .randomizer-entry-form > .randomizer-field,
-.randomizer-wheel-form .randomizer-entry-form > .nds-button {
- width: 100%;
-}
-
-.randomizer-entry-form {
- display: grid;
- grid-template-columns: 1fr;
- gap: 8px;
-}
-
-.randomizer-player-import-form > .randomizer-field,
-.randomizer-exclusion-form > .randomizer-field {
- width: 100%;
-}
-
-.randomizer-player-import-form {
- display: grid;
- grid-template-columns: 1fr;
- gap: 8px;
-}
-
-.randomizer-player-import-form > .nds-button {
- width: 100%;
-}
-
-.randomizer-value-list {
- display: grid;
- gap: 8px;
- min-width: 0;
- max-height: 210px;
- overflow: auto;
-}
-
-.randomizer-value-row {
- display: grid;
- grid-template-columns: minmax(0, 1fr) auto;
- gap: 8px;
- align-items: center;
- min-width: 0;
-}
-
-.randomizer-list-header {
- min-height: auto;
- color: var(--color-text-secondary);
- font-size: var(--font-size-xs);
- font-weight: 800;
-}
-
-.randomizer-section-label {
- color: var(--color-text-secondary);
- font-size: var(--font-size-xs);
- font-weight: 800;
-}
-
-.randomizer-player-row {
- grid-template-columns: auto minmax(0, 1fr) minmax(68px, 82px) auto;
-}
-
-.randomizer-wheel-value-row {
- grid-template-columns: minmax(0, 1fr) minmax(68px, 82px) auto;
-}
-
-.randomizer-value-row > .nds-input,
-.randomizer-value-row > .nds-select {
- width: 100%;
- min-width: 0;
-}
-
-.randomizer-value-row > .nds-checkbox {
- align-self: center;
- justify-self: center;
-}
-
-.randomizer-exclusion-form {
- grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
- align-items: end;
- min-width: 0;
-}
-
-.randomizer-exclusion-mode-field {
- grid-column: 1 / -1;
-}
-
-.randomizer-exclusion-form.is-group-mode > .randomizer-field:not(.randomizer-exclusion-mode-field) {
- grid-column: 1 / 3;
-}
-
-.randomizer-multi-trigger.nds-select-trigger,
-.randomizer-multi-trigger.nds-select-trigger[data-size="sm"] {
- grid-template-columns: minmax(0, 1fr);
- width: 100%;
- color: var(--color-text-primary);
- font: inherit;
- font-size: inherit;
- font-weight: inherit;
-}
-
-.randomizer-multi-trigger.nds-select-trigger > .randomizer-multi-trigger-label {
- display: block;
- width: 100%;
- min-width: 0;
- max-width: none;
- overflow: hidden;
- text-align: left;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.randomizer-checkbox-dropdown {
- position: relative;
- min-width: 0;
- width: 100%;
-}
-
-.randomizer-checkbox-dropdown > .randomizer-checkbox-menu.nds-scrollbox {
- position: absolute;
- z-index: 80;
- top: calc(100% + 6px);
- right: 0;
- left: 0;
- display: grid;
- gap: 6px;
- max-height: 156px;
- overflow: auto;
- width: 100%;
- padding: 8px;
- border: 1px solid rgba(165, 180, 252, 0.12);
- border-radius: var(--radius-sm);
- background: rgb(7, 10, 24);
- box-shadow: 0 18px 38px rgba(0, 0, 0, 0.32);
- color: var(--color-text-secondary);
- opacity: 1;
-}
-
-.randomizer-checkbox-option {
- display: grid;
- grid-template-columns: auto minmax(0, 1fr);
- align-items: center;
- gap: 8px;
- min-width: 0;
- color: var(--color-text-primary);
- font-size: var(--font-size-sm);
- font-weight: 800;
-}
-
-.randomizer-checkbox-option span {
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.randomizer-team-advanced .randomizer-exclusion-form {
- padding: 0;
-}
-
-.randomizer-exclusion-block {
- display: grid;
- gap: var(--space-3);
- min-width: 0;
-}
-
-.randomizer-list {
- display: grid;
- gap: 8px;
- align-content: start;
- grid-auto-rows: minmax(44px, auto);
- max-height: 280px;
- overflow: auto;
- min-width: 0;
- padding: 8px;
-}
-
-.randomizer-teams-panel {
- gap: var(--space-4);
-}
-
-.randomizer-team-stage {
- display: grid;
- gap: var(--space-3);
- min-width: 0;
-}
-
-.randomizer-teams {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(min(100%, 150px), 1fr));
- gap: var(--space-3);
- min-width: 0;
-}
-
-.randomizer-teams.is-single-team {
- grid-template-columns: minmax(0, min(50%, 420px));
- justify-content: center;
-}
-
-.randomizer-team {
- display: grid;
- align-content: start;
- gap: 8px;
- min-height: 118px;
- min-width: 0;
- max-height: 280px;
- overflow: auto;
- padding: var(--space-3);
- border: 1px solid rgba(165, 180, 252, 0.12);
- border-radius: var(--radius-md);
- background: rgba(7, 10, 24, 0.3);
-}
-
-.randomizer-team h3 {
- margin: 0;
- color: var(--color-text-primary);
- font-size: var(--font-size-sm);
-}
-
-.randomizer-team span,
-.randomizer-history-row time,
-.randomizer-empty {
- color: var(--color-text-muted);
- font-size: var(--font-size-xs);
-}
-
-.randomizer-team b {
- display: block;
- min-width: 0;
- overflow: hidden;
- color: var(--color-text-secondary);
- font-size: var(--font-size-sm);
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.randomizer-team b.is-arriving {
- animation: randomizer-player-drop 420ms var(--ease-standard);
-}
-
-.randomizer-team-reel {
- display: grid;
- height: 34px;
- width: min(100%, 340px);
- justify-self: center;
- place-items: center;
- overflow: hidden;
- border: 1px solid rgba(246, 196, 83, 0.28);
- border-radius: var(--radius-md);
- background: rgba(7, 10, 24, 0.3);
-}
-
-.randomizer-team-reel-track {
- display: grid;
- width: 100%;
- transform: translateY(calc(var(--randomizer-reel-index, 0) * -34px));
- transition: transform var(--randomizer-reel-duration, 96ms) cubic-bezier(0.2, 0.82, 0.25, 1);
- will-change: transform;
-}
-
-.randomizer-team-reel-track span,
-.randomizer-team-reel-placeholder {
- display: block;
- max-width: min(100%, 260px);
- height: 34px;
- margin-inline: auto;
- overflow: hidden;
- color: var(--color-text-primary);
- font-size: var(--font-size-sm);
- font-weight: 900;
- line-height: 34px;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.randomizer-team-reel.is-spinning .randomizer-team-reel-track {
- filter: drop-shadow(0 0 10px rgba(246, 196, 83, 0.16));
-}
-
-.randomizer-team-settings-grid {
- display: grid;
- grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
- gap: var(--space-4);
- min-width: 0;
-}
-
-.randomizer-team-basic,
-.randomizer-team-advanced-section {
- display: grid;
- align-content: start;
- gap: 8px;
- min-width: 0;
- padding: 12px;
-}
-
-.randomizer-team-advanced {
- display: grid;
- align-content: start;
- gap: var(--space-4);
- min-width: 0;
-}
-
-.randomizer-team-basic .randomizer-controls {
- display: grid;
- gap: var(--space-3);
-}
-
-.randomizer-team-basic .randomizer-controls label,
-.randomizer-team-basic .randomizer-controls .nds-button {
- width: 100%;
-}
-
-.randomizer-history {
- display: grid;
- gap: var(--space-2);
- align-content: start;
- min-width: 0;
- min-height: 132px;
- max-height: 420px;
- overflow: auto;
- padding: 12px;
-}
-
-.randomizer-history.nds-scrollbox {
- max-height: 420px;
-}
-
-.randomizer-history > header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: var(--space-3);
- padding: 8px;
-}
-
-.randomizer-history > header .randomizer-history-clear {
- display: inline-flex;
- flex: 0 0 auto;
- width: auto;
- min-width: 0;
- justify-content: center;
- padding-inline: 10px;
-}
-
-.randomizer-history h3 {
- margin: 0;
- color: var(--color-text-secondary);
- font-size: var(--font-size-sm);
-}
-
-.randomizer-history .randomizer-list {
- max-height: none;
- overflow: visible;
- padding: 0;
-}
-
-.randomizer-history-row {
- display: grid;
- grid-template-columns: minmax(0, 1fr);
- gap: 8px;
- align-items: start;
- min-height: 38px;
- min-width: 0;
- padding: 0;
- overflow: visible;
-}
-
-.randomizer-history-row.has-action {
- grid-template-columns: minmax(0, 1fr) auto;
-}
-
-.randomizer-history-main {
- display: grid;
- grid-template-columns: minmax(0, 1fr) max-content;
- align-items: center;
- gap: var(--space-3);
- min-width: 0;
- min-height: 40px;
- padding: 7px 12px;
- border: 1px solid rgba(165, 180, 252, 0.12);
- border-radius: var(--radius-sm);
- background: rgba(12, 15, 30, 0.72);
-}
-
-.randomizer-history-value {
- display: block;
- min-width: 0;
- color: var(--color-text-primary);
- font-size: var(--font-size-sm);
- font-weight: 800;
- font-variant-numeric: tabular-nums;
- overflow-wrap: anywhere;
- line-height: 1.22;
-}
-
-.randomizer-history-row.is-latest .randomizer-history-main {
- border-color: rgba(246, 196, 83, 0.68);
- background:
- radial-gradient(circle at 100% 0%, rgba(246, 196, 83, 0.08), transparent 36%),
- rgba(12, 15, 30, 0.78);
-}
-
-.randomizer-history-row.is-latest .randomizer-history-value {
- color: var(--color-accent-gold);
-}
-
-.randomizer-history-row time {
- color: var(--color-text-muted);
- font-size: var(--font-size-xs);
- line-height: 1.22;
- white-space: nowrap;
-}
-
-.randomizer-history-accordion {
- display: block;
- padding: 0;
-}
-
-.randomizer-history-accordion > summary {
- display: grid;
- grid-template-columns: 18px minmax(0, 1fr) max-content;
- align-items: center;
- gap: var(--space-3);
- min-width: 0;
- min-height: 40px;
- padding: 7px 12px;
- cursor: pointer;
- list-style: none;
-}
-
-.randomizer-history-accordion > summary .ui-icon {
- width: 16px;
- height: 16px;
- background: currentColor;
- color: var(--color-text-muted);
- transition: transform 160ms var(--ease-standard);
-}
-
-.randomizer-history-accordion[open] > summary .ui-icon {
- transform: rotate(180deg);
-}
-
-.randomizer-history-accordion > summary::-webkit-details-marker {
- display: none;
-}
-
-.randomizer-history-teams {
- display: grid;
- grid-template-columns: repeat(4, minmax(0, 1fr));
- gap: 8px;
- padding: 0 12px 12px;
-}
-
-.randomizer-history-team {
- position: relative;
- display: grid;
- grid-template-columns: minmax(0, 1fr) max-content;
- gap: 4px 8px;
- min-width: 0;
- padding-left: 10px;
- border-left: 1px solid rgba(165, 180, 252, 0.18);
-}
-
-.randomizer-history-team:nth-child(4n + 1) {
- padding-left: 0;
- border-left: 0;
-}
-
-.randomizer-history-teams.has-team-rows .randomizer-history-team:nth-child(4n + 5)::before {
- content: "";
- position: absolute;
- top: -4px;
- left: 0;
- width: calc(400% + 24px);
- border-top: 1px solid rgba(165, 180, 252, 0.14);
-}
-
-.randomizer-history-team strong {
- min-width: 0;
- overflow: hidden;
- color: var(--color-text-primary);
- font-size: var(--font-size-sm);
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.randomizer-history-team span {
- color: var(--color-text-muted);
- font-size: var(--font-size-xs);
- white-space: nowrap;
-}
-
-.randomizer-history-team ul {
- grid-column: 1 / -1;
- margin: 0;
- padding: 0;
- list-style: none;
-}
-
-.randomizer-history-team li {
- min-width: 0;
- overflow: hidden;
- color: var(--color-text-secondary);
- font-size: var(--font-size-xs);
- line-height: 1.35;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-@keyframes randomizer-player-drop {
- from {
- opacity: 0;
- transform: translateY(-34px) scale(1.04);
- }
-
- 65% {
- opacity: 1;
- transform: translateY(4px) scale(1);
- }
-}
-
-@media (max-width: 920px) {
- .randomizer-wheel-stage {
- grid-template-columns: 1fr;
- justify-items: center;
- }
-
- .randomizer-wheel-spacer {
- display: none;
- }
-
- .randomizer-wheel-legend {
- width: min(100%, 320px);
- min-width: 0;
- }
-
- .randomizer-history-teams {
- grid-template-columns: repeat(2, minmax(0, 1fr));
- }
-
- .randomizer-history-team:nth-child(4n + 1) {
- padding-left: 10px;
- border-left: 1px solid rgba(165, 180, 252, 0.18);
- }
-
- .randomizer-history-team:nth-child(2n + 1) {
- padding-left: 0;
- border-left: 0;
- }
-
- .randomizer-history-teams.has-team-rows .randomizer-history-team:nth-child(4n + 5)::before {
- content: none;
- }
-
- .randomizer-history-teams.has-team-rows .randomizer-history-team:nth-child(2n + 3)::before {
- content: "";
- position: absolute;
- top: -4px;
- left: 0;
- width: calc(200% + 8px);
- border-top: 1px solid rgba(165, 180, 252, 0.14);
- }
-}
-
-@media (max-width: 720px) {
- .randomizer-dice-grid,
- .randomizer-wheel-grid,
- .randomizer-team-settings-grid,
- .randomizer-entry-form,
- .randomizer-exclusion-form {
- grid-template-columns: 1fr;
- }
-
- .randomizer-player-row {
- grid-template-columns: 1fr;
- }
-
- .randomizer-teams.is-single-team {
- grid-template-columns: 1fr;
- }
-
- .randomizer-history-row.has-action {
- grid-template-columns: minmax(0, 1fr) auto;
- }
-
- .randomizer-history-main {
- grid-template-columns: 1fr;
- gap: 4px;
- }
-
- .randomizer-history-accordion > summary {
- grid-template-columns: 18px minmax(0, 1fr);
- }
-
- .randomizer-history-row time {
- justify-self: start;
- }
-
- .randomizer-history-teams {
- grid-template-columns: 1fr;
- }
-
- .randomizer-history-team,
- .randomizer-history-team:nth-child(4n + 1),
- .randomizer-history-team:nth-child(2n + 1) {
- padding-left: 0;
- border-left: 0;
- }
-
- .randomizer-history-teams.has-team-rows .randomizer-history-team:nth-child(2n + 3)::before {
- content: none;
- }
-
- .randomizer-history-teams.has-team-rows .randomizer-history-team:nth-child(n + 2)::before {
- content: "";
- position: absolute;
- top: -4px;
- left: 0;
- width: 100%;
- border-top: 1px solid rgba(165, 180, 252, 0.14);
- }
-}
-
-@media (prefers-reduced-motion: reduce) {
- .randomizer-teams.is-animating .randomizer-team b {
- animation: none;
- }
-
- .randomizer-wheel {
- transition: none;
- }
-
- .randomizer-die-value,
- .randomizer-team b.is-arriving {
- animation: none;
- }
-
- .randomizer-die-value-track,
- .randomizer-team-reel-track {
- transition: none;
- }
-}
+// Rôle : assemble les styles de l'outil Randomizer par responsabilité.
+@use "randomizer/base";
+@use "randomizer/dice";
+@use "randomizer/shared";
+@use "randomizer/wheel";
+@use "randomizer/teams";
+@use "randomizer/history";
+@use "randomizer/responsive";
diff --git a/website/src/styles/toolboxes/goal-tree/_node-card.scss b/website/src/styles/toolboxes/goal-tree/_node-card.scss
index 18a8be9..85e9231 100644
--- a/website/src/styles/toolboxes/goal-tree/_node-card.scss
+++ b/website/src/styles/toolboxes/goal-tree/_node-card.scss
@@ -80,7 +80,7 @@
}
.goal-tree-node-hover-actions button {
- @include controls.icon-button(tiny);
+ @include controls.icon-button(xxs);
pointer-events: none;
}
@@ -90,7 +90,7 @@
}
.goal-tree-node-hover-actions .ui-icon {
- @include controls.icon-button-icon(tiny);
+ @include controls.icon-button-icon(xxs);
}
.goal-tree-node.is-text,
diff --git a/website/src/styles/toolboxes/randomizer/_base.scss b/website/src/styles/toolboxes/randomizer/_base.scss
new file mode 100644
index 0000000..4f9357a
--- /dev/null
+++ b/website/src/styles/toolboxes/randomizer/_base.scss
@@ -0,0 +1,161 @@
+// Rôle : définit la structure commune de l'outil Randomizer.
+.randomizer-tool {
+ display: grid;
+ gap: var(--space-4);
+ padding: var(--space-4);
+}
+
+.randomizer-tabs {
+ justify-self: stretch;
+ width: min(100%, 560px);
+}
+
+.randomizer-tabs.tabs {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+}
+
+.randomizer-tabs button {
+ min-height: 42px;
+ padding-inline: 14px;
+ font-size: var(--font-size-sm);
+}
+
+.randomizer-tabs .module-icon-svg {
+ width: 18px;
+ height: 18px;
+}
+
+.randomizer-panel {
+ display: grid;
+ gap: var(--space-4);
+}
+
+.randomizer-dice-panel,
+.randomizer-wheel-panel {
+ gap: var(--space-5);
+}
+
+.randomizer-wheel-panel {
+ gap: var(--space-3);
+}
+
+.randomizer-controls {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: end;
+ gap: var(--space-3);
+}
+
+.randomizer-controls label,
+.randomizer-field {
+ display: grid;
+ gap: 6px;
+ min-width: 0;
+ color: var(--color-text-secondary);
+ font-size: var(--font-size-xs);
+ font-weight: 800;
+}
+
+.randomizer-controls label {
+ min-width: 96px;
+}
+
+.randomizer-field > .nds-input,
+.randomizer-field > .nds-select,
+.randomizer-field > .nds-textarea {
+ width: 100%;
+ min-width: 0;
+}
+
+.randomizer-wheel-stage {
+ display: grid;
+ justify-items: center;
+ gap: var(--space-4);
+ min-width: 0;
+}
+
+.randomizer-dice-stage,
+.randomizer-wheel-stage {
+ display: grid;
+ justify-items: center;
+ gap: var(--space-3);
+ min-width: 0;
+}
+
+.randomizer-live-results {
+ display: grid;
+ align-items: start;
+ justify-items: center;
+ width: min(100%, 560px);
+ min-height: 46px;
+ padding-block: 4px;
+}
+
+.randomizer-live-results .randomizer-results {
+ justify-content: center;
+ max-height: 84px;
+}
+
+.randomizer-wheel-results {
+ width: 100%;
+ min-height: 38px;
+ padding-block: 0;
+}
+
+.randomizer-dice-stage .randomizer-live-results .randomizer-chip {
+ width: 44px;
+ justify-content: center;
+ padding-inline: 0;
+ font-variant-numeric: tabular-nums;
+}
+
+.randomizer-wheel-results .randomizer-chip {
+ width: 112px;
+ justify-content: center;
+}
+
+.randomizer-dice-grid,
+.randomizer-wheel-grid {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
+ width: 100%;
+ gap: var(--space-4);
+ align-items: start;
+ min-width: 0;
+}
+
+.randomizer-dice-grid > .randomizer-dice-form,
+.randomizer-dice-grid > .randomizer-history,
+.randomizer-wheel-grid > .randomizer-wheel-form,
+.randomizer-wheel-grid > .randomizer-wheel-values-section,
+.randomizer-wheel-grid > .randomizer-history {
+ width: 100%;
+ justify-self: stretch;
+}
+
+.randomizer-wheel-grid > .randomizer-history {
+ grid-column: 1 / -1;
+}
+
+.randomizer-dice-form,
+.randomizer-wheel-form,
+.randomizer-wheel-values-section {
+ display: grid;
+ gap: var(--space-3);
+ align-content: start;
+ min-width: 0;
+ padding: 12px;
+}
+
+.randomizer-dice-form .randomizer-controls,
+.randomizer-wheel-form .randomizer-controls {
+ display: grid;
+ gap: var(--space-3);
+}
+
+.randomizer-dice-form .randomizer-controls label,
+.randomizer-dice-form .randomizer-controls .nds-button,
+.randomizer-wheel-form .randomizer-controls label,
+.randomizer-wheel-form .randomizer-controls .nds-button {
+ width: 100%;
+}
diff --git a/website/src/styles/toolboxes/randomizer/_dice.scss b/website/src/styles/toolboxes/randomizer/_dice.scss
new file mode 100644
index 0000000..9ba0d24
--- /dev/null
+++ b/website/src/styles/toolboxes/randomizer/_dice.scss
@@ -0,0 +1,82 @@
+// Rôle : porte la visualisation du dé du Randomizer.
+.randomizer-die {
+ position: relative;
+ display: grid;
+ width: 116px;
+ aspect-ratio: 1;
+ place-items: center;
+ color: var(--color-text-primary);
+ font-size: 42px;
+ font-weight: 900;
+ line-height: 1;
+}
+
+.randomizer-die::before {
+ position: absolute;
+ inset: 12px;
+ border-radius: 999px;
+ background: radial-gradient(circle, rgba(246, 196, 83, 0.22), transparent 62%);
+ content: "";
+ filter: blur(10px);
+}
+
+.randomizer-die-shape {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ overflow: visible;
+ filter: drop-shadow(0 18px 30px rgba(0, 0, 0, 0.3));
+}
+
+.randomizer-die-shape circle,
+.randomizer-die-shape path,
+.randomizer-die-shape rect {
+ fill: rgba(16, 20, 38, 0.92);
+ stroke: rgba(246, 196, 83, 0.42);
+ stroke-width: 1.2;
+}
+
+.randomizer-die-value {
+ position: relative;
+ z-index: 1;
+ display: block;
+ width: 3ch;
+ height: 52px;
+ min-width: 2ch;
+ overflow: hidden;
+ font-variant-numeric: tabular-nums;
+ text-align: center;
+}
+
+.randomizer-die-value-track {
+ display: grid;
+ transform: translateY(calc(var(--randomizer-die-index, 0) * -52px));
+ transition: transform var(--randomizer-die-duration, 120ms) cubic-bezier(0.2, 0.82, 0.25, 1);
+ will-change: transform;
+}
+
+.randomizer-die-value-track span {
+ display: block;
+ height: 52px;
+ line-height: 52px;
+}
+
+.randomizer-die.is-rolling {
+ color: var(--color-accent-gold);
+}
+
+.randomizer-die.is-rolling .randomizer-die-shape {
+ filter: drop-shadow(0 18px 30px rgba(0, 0, 0, 0.3)) drop-shadow(0 0 18px rgba(246, 196, 83, 0.18));
+}
+
+.randomizer-die.is-rolling .randomizer-die-shape circle,
+.randomizer-die.is-rolling .randomizer-die-shape path,
+.randomizer-die.is-rolling .randomizer-die-shape rect {
+ stroke: rgba(246, 196, 83, 0.68);
+}
+
+.randomizer-die.is-rolling .randomizer-die-value {
+ color: var(--color-accent-gold);
+ text-shadow: 0 0 18px rgba(246, 196, 83, 0.28);
+}
diff --git a/website/src/styles/toolboxes/randomizer/_history.scss b/website/src/styles/toolboxes/randomizer/_history.scss
new file mode 100644
index 0000000..95318be
--- /dev/null
+++ b/website/src/styles/toolboxes/randomizer/_history.scss
@@ -0,0 +1,209 @@
+// Rôle : porte l'historique détaillé du Randomizer.
+.randomizer-history {
+ display: grid;
+ gap: var(--space-2);
+ align-content: start;
+ min-width: 0;
+ min-height: 132px;
+ max-height: 420px;
+ overflow: auto;
+ padding: 12px;
+}
+
+.randomizer-history.nds-scrollbox {
+ max-height: 420px;
+}
+
+.randomizer-history > header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--space-3);
+ padding: 8px;
+}
+
+.randomizer-history > header .randomizer-history-clear {
+ display: inline-flex;
+ flex: 0 0 auto;
+ width: auto;
+ min-width: 0;
+ justify-content: center;
+ padding-inline: 10px;
+}
+
+.randomizer-history h3 {
+ margin: 0;
+ color: var(--color-text-secondary);
+ font-size: var(--font-size-sm);
+}
+
+.randomizer-history .randomizer-list {
+ max-height: none;
+ overflow: visible;
+ padding: 0;
+}
+
+.randomizer-history-row {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr);
+ gap: 8px;
+ align-items: start;
+ min-height: 38px;
+ min-width: 0;
+ padding: 0;
+ overflow: visible;
+}
+
+.randomizer-history-row.has-action {
+ grid-template-columns: minmax(0, 1fr) auto;
+}
+
+.randomizer-history-main {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) max-content;
+ align-items: center;
+ gap: var(--space-3);
+ min-width: 0;
+ min-height: 40px;
+ padding: 7px 12px;
+ border: 1px solid rgba(165, 180, 252, 0.12);
+ border-radius: var(--radius-sm);
+ background: rgba(12, 15, 30, 0.72);
+}
+
+.randomizer-history-value {
+ display: block;
+ min-width: 0;
+ color: var(--color-text-primary);
+ font-size: var(--font-size-sm);
+ font-weight: 800;
+ font-variant-numeric: tabular-nums;
+ overflow-wrap: anywhere;
+ line-height: 1.22;
+}
+
+.randomizer-history-row.is-latest .randomizer-history-main {
+ border-color: rgba(246, 196, 83, 0.68);
+ background:
+ radial-gradient(circle at 100% 0%, rgba(246, 196, 83, 0.08), transparent 36%),
+ rgba(12, 15, 30, 0.78);
+}
+
+.randomizer-history-row.is-latest .randomizer-history-value {
+ color: var(--color-accent-gold);
+}
+
+.randomizer-history-row time {
+ color: var(--color-text-muted);
+ font-size: var(--font-size-xs);
+ line-height: 1.22;
+ white-space: nowrap;
+}
+
+.randomizer-history-accordion {
+ display: block;
+ padding: 0;
+}
+
+.randomizer-history-accordion > summary {
+ display: grid;
+ grid-template-columns: 18px minmax(0, 1fr) max-content;
+ align-items: center;
+ gap: var(--space-3);
+ min-width: 0;
+ min-height: 40px;
+ padding: 7px 12px;
+ cursor: pointer;
+ list-style: none;
+}
+
+.randomizer-history-accordion > summary .ui-icon {
+ width: 16px;
+ height: 16px;
+ background: currentColor;
+ color: var(--color-text-muted);
+ transition: transform 160ms var(--ease-standard);
+}
+
+.randomizer-history-accordion[open] > summary .ui-icon {
+ transform: rotate(180deg);
+}
+
+.randomizer-history-accordion > summary::-webkit-details-marker {
+ display: none;
+}
+
+.randomizer-history-teams {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 8px;
+ padding: 0 12px 12px;
+}
+
+.randomizer-history-team {
+ position: relative;
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) max-content;
+ gap: 4px 8px;
+ min-width: 0;
+ padding-left: 10px;
+ border-left: 1px solid rgba(165, 180, 252, 0.18);
+}
+
+.randomizer-history-team:nth-child(4n + 1) {
+ padding-left: 0;
+ border-left: 0;
+}
+
+.randomizer-history-teams.has-team-rows .randomizer-history-team:nth-child(4n + 5)::before {
+ content: "";
+ position: absolute;
+ top: -4px;
+ left: 0;
+ width: calc(400% + 24px);
+ border-top: 1px solid rgba(165, 180, 252, 0.14);
+}
+
+.randomizer-history-team strong {
+ min-width: 0;
+ overflow: hidden;
+ color: var(--color-text-primary);
+ font-size: var(--font-size-sm);
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.randomizer-history-team span {
+ color: var(--color-text-muted);
+ font-size: var(--font-size-xs);
+ white-space: nowrap;
+}
+
+.randomizer-history-team ul {
+ grid-column: 1 / -1;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.randomizer-history-team li {
+ min-width: 0;
+ overflow: hidden;
+ color: var(--color-text-secondary);
+ font-size: var(--font-size-xs);
+ line-height: 1.35;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+@keyframes randomizer-player-drop {
+ from {
+ opacity: 0;
+ transform: translateY(-34px) scale(1.04);
+ }
+
+ 65% {
+ opacity: 1;
+ transform: translateY(4px) scale(1);
+ }
+}
diff --git a/website/src/styles/toolboxes/randomizer/_responsive.scss b/website/src/styles/toolboxes/randomizer/_responsive.scss
new file mode 100644
index 0000000..6e4031f
--- /dev/null
+++ b/website/src/styles/toolboxes/randomizer/_responsive.scss
@@ -0,0 +1,122 @@
+// Rôle : regroupe les adaptations responsive et mouvement du Randomizer.
+@media (max-width: 920px) {
+ .randomizer-wheel-stage {
+ grid-template-columns: 1fr;
+ justify-items: center;
+ }
+
+ .randomizer-wheel-spacer {
+ display: none;
+ }
+
+ .randomizer-wheel-legend {
+ width: min(100%, 320px);
+ min-width: 0;
+ }
+
+ .randomizer-history-teams {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
+ .randomizer-history-team:nth-child(4n + 1) {
+ padding-left: 10px;
+ border-left: 1px solid rgba(165, 180, 252, 0.18);
+ }
+
+ .randomizer-history-team:nth-child(2n + 1) {
+ padding-left: 0;
+ border-left: 0;
+ }
+
+ .randomizer-history-teams.has-team-rows .randomizer-history-team:nth-child(4n + 5)::before {
+ content: none;
+ }
+
+ .randomizer-history-teams.has-team-rows .randomizer-history-team:nth-child(2n + 3)::before {
+ content: "";
+ position: absolute;
+ top: -4px;
+ left: 0;
+ width: calc(200% + 8px);
+ border-top: 1px solid rgba(165, 180, 252, 0.14);
+ }
+}
+
+@media (max-width: 720px) {
+ .randomizer-dice-grid,
+ .randomizer-wheel-grid,
+ .randomizer-team-settings-grid,
+ .randomizer-entry-form,
+ .randomizer-exclusion-form {
+ grid-template-columns: 1fr;
+ }
+
+ .randomizer-player-row {
+ grid-template-columns: 1fr;
+ }
+
+ .randomizer-teams.is-single-team {
+ grid-template-columns: 1fr;
+ }
+
+ .randomizer-history-row.has-action {
+ grid-template-columns: minmax(0, 1fr) auto;
+ }
+
+ .randomizer-history-main {
+ grid-template-columns: 1fr;
+ gap: 4px;
+ }
+
+ .randomizer-history-accordion > summary {
+ grid-template-columns: 18px minmax(0, 1fr);
+ }
+
+ .randomizer-history-row time {
+ justify-self: start;
+ }
+
+ .randomizer-history-teams {
+ grid-template-columns: 1fr;
+ }
+
+ .randomizer-history-team,
+ .randomizer-history-team:nth-child(4n + 1),
+ .randomizer-history-team:nth-child(2n + 1) {
+ padding-left: 0;
+ border-left: 0;
+ }
+
+ .randomizer-history-teams.has-team-rows .randomizer-history-team:nth-child(2n + 3)::before {
+ content: none;
+ }
+
+ .randomizer-history-teams.has-team-rows .randomizer-history-team:nth-child(n + 2)::before {
+ content: "";
+ position: absolute;
+ top: -4px;
+ left: 0;
+ width: 100%;
+ border-top: 1px solid rgba(165, 180, 252, 0.14);
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .randomizer-teams.is-animating .randomizer-team b {
+ animation: none;
+ }
+
+ .randomizer-wheel {
+ transition: none;
+ }
+
+ .randomizer-die-value,
+ .randomizer-team b.is-arriving {
+ animation: none;
+ }
+
+ .randomizer-die-value-track,
+ .randomizer-team-reel-track {
+ transition: none;
+ }
+}
diff --git a/website/src/styles/toolboxes/randomizer/_shared.scss b/website/src/styles/toolboxes/randomizer/_shared.scss
new file mode 100644
index 0000000..f4f4ee1
--- /dev/null
+++ b/website/src/styles/toolboxes/randomizer/_shared.scss
@@ -0,0 +1,100 @@
+// Rôle : regroupe les cartes et listes partagées du Randomizer.
+.randomizer-dice-form,
+.randomizer-wheel-form,
+.randomizer-wheel-values-section,
+.randomizer-team-basic,
+.randomizer-team-advanced-section,
+.randomizer-history {
+ border: 1px solid rgba(165, 180, 252, 0.12);
+ border-radius: var(--radius-md);
+ background: rgba(7, 10, 24, 0.3);
+}
+
+.randomizer-results,
+.randomizer-exclusions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ min-width: 0;
+ max-width: 100%;
+ max-height: 168px;
+ overflow: auto;
+ padding: 2px;
+}
+
+.randomizer-history > .randomizer-empty {
+ align-self: center;
+ margin: 0;
+ padding: 8px 2px;
+}
+
+.randomizer-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ min-height: 30px;
+ max-width: 100%;
+ overflow: hidden;
+ padding: 4px 8px;
+ border: 1px solid rgba(165, 180, 252, 0.16);
+ border-radius: var(--radius-pill);
+ background: rgba(16, 20, 38, 0.72);
+ color: var(--color-text-primary);
+ font-size: var(--font-size-xs);
+ font-weight: 800;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.randomizer-chip-label {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.randomizer-value-list {
+ display: grid;
+ gap: 8px;
+ min-width: 0;
+ max-height: 210px;
+ overflow: auto;
+}
+
+.randomizer-value-row {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ gap: 8px;
+ align-items: center;
+ min-width: 0;
+}
+
+.randomizer-list-header {
+ min-height: auto;
+ color: var(--color-text-secondary);
+ font-size: var(--font-size-xs);
+ font-weight: 800;
+}
+
+.randomizer-section-label {
+ color: var(--color-text-secondary);
+ font-size: var(--font-size-xs);
+ font-weight: 800;
+}
+
+.randomizer-player-row {
+ grid-template-columns: auto minmax(0, 1fr) minmax(68px, 82px) auto;
+}
+
+.randomizer-wheel-value-row {
+ grid-template-columns: minmax(0, 1fr) minmax(68px, 82px) auto;
+}
+
+.randomizer-value-row > .nds-input,
+.randomizer-value-row > .nds-select {
+ width: 100%;
+ min-width: 0;
+}
+
+.randomizer-value-row > .nds-checkbox {
+ align-self: center;
+ justify-self: center;
+}
diff --git a/website/src/styles/toolboxes/randomizer/_teams.scss b/website/src/styles/toolboxes/randomizer/_teams.scss
new file mode 100644
index 0000000..8f0c277
--- /dev/null
+++ b/website/src/styles/toolboxes/randomizer/_teams.scss
@@ -0,0 +1,250 @@
+// Rôle : porte la création d'équipes et les exclusions du Randomizer.
+.randomizer-player-import-form > .randomizer-field,
+.randomizer-exclusion-form > .randomizer-field {
+ width: 100%;
+}
+
+.randomizer-player-import-form {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 8px;
+}
+
+.randomizer-player-import-form > .nds-button {
+ width: 100%;
+}
+
+.randomizer-exclusion-form {
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
+ align-items: end;
+ min-width: 0;
+}
+
+.randomizer-exclusion-mode-field {
+ grid-column: 1 / -1;
+}
+
+.randomizer-exclusion-form.is-group-mode > .randomizer-field:not(.randomizer-exclusion-mode-field) {
+ grid-column: 1 / 3;
+}
+
+.randomizer-multi-trigger.nds-select-trigger,
+.randomizer-multi-trigger.nds-select-trigger[data-size="sm"] {
+ grid-template-columns: minmax(0, 1fr);
+ width: 100%;
+ color: var(--color-text-primary);
+ font: inherit;
+ font-size: inherit;
+ font-weight: inherit;
+}
+
+.randomizer-multi-trigger.nds-select-trigger > .randomizer-multi-trigger-label {
+ display: block;
+ width: 100%;
+ min-width: 0;
+ max-width: none;
+ overflow: hidden;
+ text-align: left;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.randomizer-checkbox-dropdown {
+ position: relative;
+ min-width: 0;
+ width: 100%;
+}
+
+.randomizer-checkbox-dropdown > .randomizer-checkbox-menu.nds-scrollbox {
+ position: absolute;
+ z-index: 80;
+ top: calc(100% + 6px);
+ right: 0;
+ left: 0;
+ display: grid;
+ gap: 6px;
+ max-height: 156px;
+ overflow: auto;
+ width: 100%;
+ padding: 8px;
+ border: 1px solid rgba(165, 180, 252, 0.12);
+ border-radius: var(--radius-sm);
+ background: rgb(7, 10, 24);
+ box-shadow: 0 18px 38px rgba(0, 0, 0, 0.32);
+ color: var(--color-text-secondary);
+ opacity: 1;
+}
+
+.randomizer-checkbox-option {
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr);
+ align-items: center;
+ gap: 8px;
+ min-width: 0;
+ color: var(--color-text-primary);
+ font-size: var(--font-size-sm);
+ font-weight: 800;
+}
+
+.randomizer-checkbox-option span {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.randomizer-team-advanced .randomizer-exclusion-form {
+ padding: 0;
+}
+
+.randomizer-exclusion-block {
+ display: grid;
+ gap: var(--space-3);
+ min-width: 0;
+}
+
+.randomizer-list {
+ display: grid;
+ gap: 8px;
+ align-content: start;
+ grid-auto-rows: minmax(44px, auto);
+ max-height: 280px;
+ overflow: auto;
+ min-width: 0;
+ padding: 8px;
+}
+
+.randomizer-teams-panel {
+ gap: var(--space-4);
+}
+
+.randomizer-team-stage {
+ display: grid;
+ gap: var(--space-3);
+ min-width: 0;
+}
+
+.randomizer-teams {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 150px), 1fr));
+ gap: var(--space-3);
+ min-width: 0;
+}
+
+.randomizer-teams.is-single-team {
+ grid-template-columns: minmax(0, min(50%, 420px));
+ justify-content: center;
+}
+
+.randomizer-team {
+ display: grid;
+ align-content: start;
+ gap: 8px;
+ min-height: 118px;
+ min-width: 0;
+ max-height: 280px;
+ overflow: auto;
+ padding: var(--space-3);
+ border: 1px solid rgba(165, 180, 252, 0.12);
+ border-radius: var(--radius-md);
+ background: rgba(7, 10, 24, 0.3);
+}
+
+.randomizer-team h3 {
+ margin: 0;
+ color: var(--color-text-primary);
+ font-size: var(--font-size-sm);
+}
+
+.randomizer-team span,
+.randomizer-history-row time,
+.randomizer-empty {
+ color: var(--color-text-muted);
+ font-size: var(--font-size-xs);
+}
+
+.randomizer-team b {
+ display: block;
+ min-width: 0;
+ overflow: hidden;
+ color: var(--color-text-secondary);
+ font-size: var(--font-size-sm);
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.randomizer-team b.is-arriving {
+ animation: randomizer-player-drop 420ms var(--ease-standard);
+}
+
+.randomizer-team-reel {
+ display: grid;
+ height: 34px;
+ width: min(100%, 340px);
+ justify-self: center;
+ place-items: center;
+ overflow: hidden;
+ border: 1px solid rgba(246, 196, 83, 0.28);
+ border-radius: var(--radius-md);
+ background: rgba(7, 10, 24, 0.3);
+}
+
+.randomizer-team-reel-track {
+ display: grid;
+ width: 100%;
+ transform: translateY(calc(var(--randomizer-reel-index, 0) * -34px));
+ transition: transform var(--randomizer-reel-duration, 96ms) cubic-bezier(0.2, 0.82, 0.25, 1);
+ will-change: transform;
+}
+
+.randomizer-team-reel-track span,
+.randomizer-team-reel-placeholder {
+ display: block;
+ max-width: min(100%, 260px);
+ height: 34px;
+ margin-inline: auto;
+ overflow: hidden;
+ color: var(--color-text-primary);
+ font-size: var(--font-size-sm);
+ font-weight: 900;
+ line-height: 34px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.randomizer-team-reel.is-spinning .randomizer-team-reel-track {
+ filter: drop-shadow(0 0 10px rgba(246, 196, 83, 0.16));
+}
+
+.randomizer-team-settings-grid {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
+ gap: var(--space-4);
+ min-width: 0;
+}
+
+.randomizer-team-basic,
+.randomizer-team-advanced-section {
+ display: grid;
+ align-content: start;
+ gap: 8px;
+ min-width: 0;
+ padding: 12px;
+}
+
+.randomizer-team-advanced {
+ display: grid;
+ align-content: start;
+ gap: var(--space-4);
+ min-width: 0;
+}
+
+.randomizer-team-basic .randomizer-controls {
+ display: grid;
+ gap: var(--space-3);
+}
+
+.randomizer-team-basic .randomizer-controls label,
+.randomizer-team-basic .randomizer-controls .nds-button {
+ width: 100%;
+}
diff --git a/website/src/styles/toolboxes/randomizer/_wheel.scss b/website/src/styles/toolboxes/randomizer/_wheel.scss
new file mode 100644
index 0000000..fddf81f
--- /dev/null
+++ b/website/src/styles/toolboxes/randomizer/_wheel.scss
@@ -0,0 +1,130 @@
+// Rôle : porte la roue pondérée du Randomizer.
+.randomizer-wheel-stage {
+ position: relative;
+ display: grid;
+ grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
+ gap: var(--space-4);
+ align-items: center;
+ justify-items: stretch;
+ min-height: 228px;
+ align-content: center;
+}
+
+.randomizer-wheel-spacer {
+ display: none;
+}
+
+.randomizer-wheel-center {
+ position: relative;
+ display: grid;
+ justify-self: center;
+ justify-items: center;
+ gap: var(--space-3);
+ min-width: 210px;
+ width: 100%;
+}
+
+.randomizer-wheel-pointer {
+ position: absolute;
+ top: 2px;
+ left: 50%;
+ z-index: 2;
+ width: 0;
+ height: 0;
+ border-right: 10px solid transparent;
+ border-left: 10px solid transparent;
+ border-top: 18px solid var(--color-accent-gold);
+ transform: translateX(-50%);
+ filter: drop-shadow(0 4px 10px rgba(0, 0, 0, 0.42));
+}
+
+.randomizer-wheel {
+ width: min(100%, 190px);
+ aspect-ratio: 1;
+ margin-top: 16px;
+ border-radius: 50%;
+ box-shadow: 0 18px 44px rgba(0, 0, 0, 0.32);
+ transform: rotate(var(--randomizer-wheel-rotation, 0deg));
+ transition: transform 1120ms cubic-bezier(0.15, 0.78, 0.18, 1);
+}
+
+.randomizer-wheel-legend {
+ align-self: center;
+ justify-self: stretch;
+ display: grid;
+ gap: 8px;
+ align-content: start;
+ width: 100%;
+ min-width: 0;
+ max-height: 238px;
+ overflow: auto;
+ padding: 8px;
+ border: 1px solid rgba(165, 180, 252, 0.12);
+ border-radius: var(--radius-md);
+ background: rgba(7, 10, 24, 0.3);
+}
+
+.randomizer-wheel-legend.nds-scrollbox {
+ max-height: 238px;
+}
+
+.randomizer-wheel-legend-row {
+ display: grid;
+ grid-template-columns: 14px minmax(0, 1fr);
+ gap: 8px;
+ align-items: center;
+ min-width: 0;
+}
+
+.randomizer-wheel-swatch {
+ display: block;
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ background: var(--randomizer-swatch);
+ box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.04);
+}
+
+.randomizer-wheel-label {
+ min-width: 0;
+ overflow: hidden;
+ color: var(--color-text-primary);
+ font-size: var(--font-size-sm);
+ font-weight: 800;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.randomizer-wheel-legend .randomizer-empty {
+ margin: 0;
+ color: var(--color-text-muted);
+ font-size: var(--font-size-xs);
+}
+
+.randomizer-wheel.is-spinning {
+ filter: drop-shadow(0 0 18px rgba(246, 196, 83, 0.18));
+}
+
+.randomizer-wheel path {
+ stroke: rgba(7, 10, 24, 0.74);
+ stroke-width: 0.8;
+}
+
+.randomizer-wheel-form .randomizer-entry-form {
+ padding: 0;
+}
+
+.randomizer-wheel-form .randomizer-entry-form .nds-button {
+ grid-column: auto;
+}
+
+.randomizer-wheel-form .randomizer-entry-form > .randomizer-field,
+.randomizer-wheel-form .randomizer-entry-form > .nds-button {
+ width: 100%;
+}
+
+.randomizer-entry-form {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 8px;
+}