Add guided onboarding tour for first toolbox creation
All checks were successful
Deploy Sokko G / deploy (push) Successful in 14s

This commit is contained in:
Shinuwa 2026-08-14 13:20:47 +02:00
parent 3007aa1389
commit 4f73e0068b
20 changed files with 470 additions and 20 deletions

View file

@ -5,6 +5,12 @@ import { ConfirmModal, CreateToolboxModal, LinkToolboxModal, NotificationToast }
import { navigate } from "../router/hashRouter.js";
import { uid } from "../features/toolboxes/storage/toolboxStorage.js";
function scrollToTopAfterNavigation() {
window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => window.scrollTo({ top: 0, left: 0, behavior: "auto" }));
});
}
export function InlineNotice({ className = "", iconName = "link", message, tone = "warning" }) {
if (!message) return null;
const classes = ["inline-notice", `is-${tone}`, className].filter(Boolean).join(" ");
@ -34,7 +40,8 @@ export function AppOverlays({
toastPosition,
getGame,
toolboxes,
links
links,
setPendingTutorialToolboxId
}) {
return (
<>
@ -47,9 +54,13 @@ export function AppOverlays({
const gameId = createModal.gameId || "";
setCreateModal(null);
if (!name) return;
const toolbox = actions.createToolbox(name, gameId);
const toolbox = actions.createToolbox(name, gameId, { tutorial: createModal.tutorial === true });
if (toolbox && createModal.tutorial === true) setPendingTutorialToolboxId?.(toolbox.id);
if (toolbox && gameId) setDrawerGameId(gameId);
else if (toolbox) navigate(`/toolbox/${toolbox.id}`);
else if (toolbox) {
navigate(`/toolbox/${toolbox.id}`);
scrollToTopAfterNavigation();
}
}} />}
{linkModalGameId && <LinkToolboxModal gameId={linkModalGameId} toolboxes={toolboxes} selectedId={links[linkModalGameId] || ""} onClose={(toolboxId) => {
const gameId = linkModalGameId;

View file

@ -0,0 +1,109 @@
// Rôle : rend un tutoriel séquentiel sous forme de tooltips ciblés sur des éléments du DOM.
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { Icon } from "./Icon.jsx";
export function GuidedTooltipTour({ steps, labels = {}, targetAttribute = "data-guided-tooltip-target", onDismiss, onStarted }) {
const [stepIndex, setStepIndex] = useState(0);
const [style, setStyle] = useState(null);
const tooltipRef = useRef(null);
const startedRef = useRef(false);
const currentStep = steps?.[stepIndex];
const isLastStep = stepIndex >= (steps?.length || 0) - 1;
const placeTooltip = useCallback(() => {
if (!currentStep?.target) return;
const target = document.querySelector(`[${targetAttribute}="${currentStep.target}"]`);
if (!target) {
setStyle(null);
return;
}
let rect = target.getBoundingClientRect();
if (rect.top < 72 || rect.bottom > window.innerHeight - 72) {
target.scrollIntoView({ block: "center", inline: "nearest", behavior: "auto" });
rect = target.getBoundingClientRect();
}
const width = Math.min(320, window.innerWidth - 24);
if (currentStep.placement === "right") {
const availableRight = window.innerWidth - rect.right - 24;
const nextWidth = Math.min(300, Math.max(230, availableRight));
const left = Math.min(window.innerWidth - nextWidth - 12, rect.right + 12);
const top = Math.max(12, rect.top + rect.height / 2 - 75);
const arrowTop = Math.min(132, Math.max(18, rect.top + rect.height / 2 - top));
setStyle({ placement: "right", top: `${top}px`, left: `${left}px`, width: `${nextWidth}px`, "--tutorial-arrow-top": `${arrowTop}px` });
if (!startedRef.current) {
startedRef.current = true;
onStarted?.();
}
return;
}
const targetCenter = rect.left + rect.width / 2;
const left = Math.min(window.innerWidth - width - 12, Math.max(12, targetCenter - width / 2));
const arrowLeft = Math.min(width - 18, Math.max(18, targetCenter - left));
const below = rect.bottom + 12;
const placement = below + 170 > window.innerHeight ? "above" : "below";
const top = placement === "above" ? Math.max(12, rect.top - 158) : below;
setStyle({ placement, top: `${top}px`, left: `${left}px`, width: `${width}px`, "--tutorial-arrow-left": `${arrowLeft}px` });
if (!startedRef.current) {
startedRef.current = true;
onStarted?.();
}
}, [currentStep, onStarted, targetAttribute]);
useEffect(() => {
let frameId = 0;
let attempts = 0;
function placeWhenReady() {
attempts += 1;
placeTooltip();
if (attempts < 20 && currentStep?.target && !document.querySelector(`[${targetAttribute}="${currentStep.target}"]`)) {
frameId = window.requestAnimationFrame(placeWhenReady);
}
}
placeWhenReady();
window.addEventListener("resize", placeTooltip);
window.addEventListener("scroll", placeTooltip, true);
return () => {
window.cancelAnimationFrame(frameId);
window.removeEventListener("resize", placeTooltip);
window.removeEventListener("scroll", placeTooltip, true);
};
}, [currentStep, placeTooltip, targetAttribute]);
useLayoutEffect(() => {
const tooltip = tooltipRef.current;
if (!tooltip || !style) return;
const viewportHeight = window.visualViewport?.height || window.innerHeight;
const rect = tooltip.getBoundingClientRect();
const nextTop = Math.min(Math.max(12, rect.top), Math.max(12, viewportHeight - rect.height - 12));
if (Math.abs(nextTop - rect.top) < 1) return;
const arrowTop = style.placement === "right" && style["--tutorial-arrow-top"]
? `${Number.parseFloat(style["--tutorial-arrow-top"]) + rect.top - nextTop}px`
: style["--tutorial-arrow-top"];
setStyle((current) => current ? { ...current, top: `${nextTop}px`, "--tutorial-arrow-top": arrowTop } : current);
}, [style, currentStep]);
if (!steps?.length || !currentStep || !style) return null;
const { placement, ...tooltipStyle } = style;
return createPortal(
<section ref={tooltipRef} className="toolbox-tutorial-tooltip" aria-live="polite" aria-labelledby="toolbox-tutorial-title" data-placement={placement} role="dialog" style={tooltipStyle}>
<button className="toolbox-tutorial-close" type="button" onClick={onDismiss} aria-label={labels.dismiss || "Fermer le tutoriel"} title={labels.dismiss || "Fermer"}>
<Icon name="close" />
</button>
<span className="toolbox-tutorial-count">{stepIndex + 1}/{steps.length}</span>
<div className="toolbox-tutorial-heading">
<Icon name={currentStep.icon || "info"} />
<strong id="toolbox-tutorial-title">{currentStep.title}</strong>
</div>
<p>{currentStep.text}</p>
<footer>
<button className="primary" type="button" onClick={isLastStep ? onDismiss : () => setStepIndex((index) => Math.min(index + 1, steps.length - 1))}>
{isLastStep ? labels.done || "Terminer" : labels.next || "Suivant"}
</button>
</footer>
</section>,
document.body
);
}

View file

@ -32,7 +32,7 @@ export function Shell({ route, content, games, toolboxes, links, actions, childr
<span className="badge">{content.sidebar.badge}</span>
<p>{content.sidebar.note}</p>
<div className="sidebar-storage-actions" aria-label="Actions globales de stockage">
<button className="sidebar-action-button primary" onClick={actions.exportAllToolboxes} aria-label="Exporter toutes les toolboxes" title="Exporter tout"><Icon name="export" /></button>
<button className="sidebar-action-button primary" onClick={actions.exportAllToolboxes} aria-label="Exporter toutes les toolboxes" data-toolbox-tutorial-target="exportAll" title="Exporter tout"><Icon name="export" /></button>
<ImportButton className="sidebar-action-button import-icon-button" title="Importer tout" ariaLabel="Importer toutes les toolboxes" onFile={actions.importAllToolboxes} icon="import" />
</div>
</div>

View file

@ -71,7 +71,7 @@ export function ToolboxCard({ toolbox, game, actions, reorder }) {
);
}
export function ToolboxIconPicker({ toolbox, onChange }) {
export function ToolboxIconPicker({ toolbox, onChange, tutorialTarget = "" }) {
const icon = normalizeToolboxIcon(toolbox.icon);
return (
@ -86,6 +86,7 @@ export function ToolboxIconPicker({ toolbox, onChange }) {
onClick={toggle}
aria-label="Changer l'icône de la toolbox"
aria-expanded={open}
data-toolbox-tutorial-target={tutorialTarget || undefined}
title="Changer l'icône"
>
<img src={icon} alt="" aria-hidden="true" />

View file

@ -1,5 +1,6 @@
// Rôle : affiche les pages toolbox, le panneau latéral et leurs contrôles.
import { useEffect, useRef, useState } from "react";
import { GuidedTooltipTour } from "../../components/GuidedTooltipTour.jsx";
import { Icon } from "../../components/Icon.jsx";
import { ImportButton } from "../../components/ImportButton.jsx";
import { StorageQuota } from "../../components/StorageQuota.jsx";
@ -169,10 +170,18 @@ export function ToolboxPage(props) {
return <ToolboxView {...props} toolbox={toolbox} toolboxGame={getToolboxGame(toolbox)} embedded={false} />;
}
function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, storageUsage, getModuleData, updateToolbox, updateModuleData, addImageFiles }) {
function shouldShowTutorial(toolbox, pendingTutorialToolboxId) {
return toolbox?.tutorial === true || pendingTutorialToolboxId === toolbox?.id;
}
function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, storageUsage, getModuleData, updateToolbox, updateModuleData, addImageFiles, pendingTutorialToolboxId = "", setPendingTutorialToolboxId }) {
const toolboxId = toolbox.id;
const toolboxTutorial = toolbox.tutorial;
const moduleColumns = embedded || Number(toolbox.moduleColumns) === 1 ? 1 : 2;
const toolboxGameCover = getGameCardCover(toolboxGame);
const moduleText = siteContent.toolboxes.modules;
const [showTutorial, setShowTutorial] = useState(() => shouldShowTutorial(toolbox, pendingTutorialToolboxId));
const currentToolboxIdRef = useRef(toolboxId);
const moduleContext = {
getModuleData,
moduleText,
@ -202,6 +211,15 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
refresh: () => {}
};
useEffect(() => {
if (currentToolboxIdRef.current === toolboxId) {
if (shouldShowTutorial(toolbox, pendingTutorialToolboxId)) setShowTutorial(true);
return;
}
currentToolboxIdRef.current = toolboxId;
setShowTutorial(shouldShowTutorial(toolbox, pendingTutorialToolboxId));
}, [pendingTutorialToolboxId, toolbox, toolboxId, toolboxTutorial]);
function addModule(type) {
if (!type) return;
const module = { id: uid("mod"), type };
@ -231,10 +249,10 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
<div>
<p className="eyebrow">Toolbox</p>
<div className="toolbox-title-row">
{!embedded && !toolboxGame && <ToolboxIconPicker toolbox={toolbox} onChange={(icon) => updateToolbox({ ...toolbox, icon })} />}
{!embedded && !toolboxGame && <ToolboxIconPicker toolbox={toolbox} onChange={(icon) => updateToolbox({ ...toolbox, icon })} tutorialTarget="icon" />}
{!embedded && toolboxGameCover && <ToolboxGameIcon game={toolboxGame} />}
{embedded ? <h1>{toolbox.name}</h1> : (
<EditableTitle className="toolbox-title" value={toolbox.name} fallback="Nouvelle toolbox" onSave={(name) => name !== toolbox.name && updateToolbox({ ...toolbox, name })} />
<EditableTitle className="toolbox-title" value={toolbox.name} fallback="Nouvelle toolbox" tutorialTarget="title" onSave={(name) => name !== toolbox.name && updateToolbox({ ...toolbox, name })} />
)}
</div>
{toolboxGame && (
@ -252,6 +270,25 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
<AddToolControls onAdd={addModule} />
</div>
</section>
{!embedded && showTutorial && (
<GuidedTooltipTour
steps={siteContent.toolboxes.tutorial?.items || []}
labels={{
dismiss: siteContent.toolboxes.tutorial?.dismissLabel,
next: siteContent.toolboxes.tutorial?.nextLabel,
done: siteContent.toolboxes.tutorial?.doneLabel
}}
targetAttribute="data-toolbox-tutorial-target"
onDismiss={() => {
if (pendingTutorialToolboxId === toolbox.id) setPendingTutorialToolboxId?.("");
setShowTutorial(false);
}}
onStarted={() => {
if (pendingTutorialToolboxId === toolbox.id) setPendingTutorialToolboxId?.("");
if (toolbox.tutorial === true) updateToolbox({ ...toolbox, tutorial: false });
}}
/>
)}
{!embedded && (
<section className="modules-toolbar" aria-label="Options d'affichage des outils">
<div className="layout-switch" role="group" aria-label="Mode d'affichage des outils">
@ -359,7 +396,7 @@ export function ToolboxDrawer({ gameId, game, siteContent, toolboxes, links, act
);
}
function EditableTitle({ value, fallback, onSave, className = "module-title" }) {
function EditableTitle({ value, fallback, onSave, className = "module-title", tutorialTarget = "" }) {
const inlineEdit = useInlineEdit({
value,
onCommit: (title) => onSave(title || fallback),
@ -370,6 +407,7 @@ function EditableTitle({ value, fallback, onSave, className = "module-title" })
<h1
{...inlineEdit.getContentEditableProps({
className,
"data-toolbox-tutorial-target": tutorialTarget || undefined,
title: "Cliquer pour renommer"
})}
>{value}</h1>

View file

@ -115,6 +115,7 @@ export function AddToolControls({ onAdd }) {
className="primary tool-add-modal-button"
type="button"
onClick={() => setOpen(true)}
data-toolbox-tutorial-target="addTool"
>
Ajouter un outil
</button>
@ -311,7 +312,7 @@ function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", onRena
<span className="module-icon" aria-hidden="true">
<span className={`module-icon-svg module-icon-${definition.icon || "notepad"}`} />
</span>
<EditableModuleTitle value={module.title || label} fallback={label} onSave={(title) => onRename(module.id, title)} />
<EditableModuleTitle value={module.title || label} fallback={label} tutorialTarget={module.type === "notepad" ? "moduleTitle" : ""} onSave={(title) => onRename(module.id, title)} />
</div>
<div>
{definition.scrollable && (
@ -345,7 +346,7 @@ function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", onRena
<Icon name="add" />
</button>
)}
<button className="module-delete-button danger" onClick={() => onDelete(module.id)} aria-label={`Retirer ${module.title || label}`} title="Retirer">
<button className="module-delete-button danger" onClick={() => onDelete(module.id)} aria-label={`Retirer ${module.title || label}`} data-toolbox-tutorial-target={module.type === "links" ? "deleteTool" : undefined} title="Retirer">
<span className="ui-icon ui-icon-trash" aria-hidden="true" />
</button>
</div>
@ -357,7 +358,7 @@ function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", onRena
);
}
function EditableModuleTitle({ value, fallback, onSave }) {
function EditableModuleTitle({ value, fallback, onSave, tutorialTarget = "" }) {
const inlineEdit = useInlineEdit({
value,
onCommit: (title) => onSave(title || fallback),
@ -368,6 +369,7 @@ function EditableModuleTitle({ value, fallback, onSave }) {
<h2
{...inlineEdit.getContentEditableProps({
className: "module-title",
"data-toolbox-tutorial-target": tutorialTarget || undefined,
title: "Cliquer pour renommer"
})}
>

View file

@ -114,7 +114,7 @@ export function remapModuleOrderIds(moduleOrder, moduleIdMap) {
export function normalizeToolbox(toolbox) {
if (!toolbox || typeof toolbox !== "object") return null;
const modules = (Array.isArray(toolbox.modules) ? toolbox.modules : []).map(normalizeToolboxModule).filter(Boolean);
return {
const normalized = {
id: toolbox.id || uid("tbx"),
name: String(toolbox.name || "Nouvelle toolbox").trim() || "Nouvelle toolbox",
icon: normalizeToolboxIcon(toolbox.icon),
@ -123,6 +123,8 @@ export function normalizeToolbox(toolbox) {
moduleOrder: normalizeModuleOrder(toolbox.moduleOrder, modules),
updatedAt: toolbox.updatedAt || new Date().toISOString()
};
if (toolbox.tutorial === true) normalized.tutorial = true;
return normalized;
}
export function compactToolboxForStorage(toolbox) {
@ -137,6 +139,7 @@ export function compactToolboxForStorage(toolbox) {
};
if (normalized.moduleColumns === 1) compact.moduleColumns = 1;
if (normalized.icon !== DEFAULT_TOOLBOX_ICON) compact.icon = normalized.icon;
if (normalized.tutorial === true) compact.tutorial = true;
return compact;
}

View file

@ -20,19 +20,36 @@ function downloadJson(payload, filename) {
URL.revokeObjectURL(link.href);
}
export function useToolboxActions({ store, notify, setConfirmModal, setCreateModal, setLinkModalGameId, setDrawerGameId, setImage }) {
function createToolbox(name, gameId = "") {
function getLibraryUrl() {
if (typeof window === "undefined") return "https://sokko-g.local/#/library";
return `${window.location.origin}${window.location.pathname}#/library`;
}
export function useToolboxActions({ store, notify, creationTemplate, setConfirmModal, setCreateModal, setLinkModalGameId, setDrawerGameId, setImage }) {
function createToolbox(name, gameId = "", options = {}) {
const now = new Date().toISOString();
const notepadModuleId = uid("mod");
const linksModuleId = uid("mod");
const welcomeText = creationTemplate?.notepad?.text || "Profite bien de ta nouvelle toolbox.";
const welcomeHtml = creationTemplate?.notepad?.html || `<p>${welcomeText}</p>`;
const libraryLink = {
id: uid("link"),
title: creationTemplate?.links?.title || "Librairie des outils",
url: getLibraryUrl()
};
const toolbox = {
id: uid("tbx"),
name: name.trim() || "Nouvelle toolbox",
updatedAt: now,
modules: [
{ id: uid("mod"), type: "notepad", title: "Notes rapides" },
{ id: uid("mod"), type: "checklist" }
{ id: notepadModuleId, type: "notepad", title: creationTemplate?.notepad?.title || "Bienvenue" },
{ id: linksModuleId, type: "links", title: creationTemplate?.links?.moduleTitle || "Liens utiles" }
]
};
if (options.tutorial === true) toolbox.tutorial = true;
if (!store.setToolboxes([toolbox, ...store.toolboxes])) return null;
store.updateModuleData(toolbox.id, notepadModuleId, { html: welcomeHtml, text: welcomeText, updatedAt: now }, "notepad");
store.updateModuleData(toolbox.id, linksModuleId, { links: [libraryLink] }, "links");
if (gameId) store.setLinks({ ...store.links, [gameId]: toolbox.id });
return toolbox;
}

View file

@ -25,6 +25,7 @@ function App() {
const [linkModalGameId, setLinkModalGameId] = useState("");
const [drawerGameId, setDrawerGameId] = useState("");
const [image, setImage] = useState(null);
const [pendingTutorialToolboxId, setPendingTutorialToolboxId] = useState("");
const [notifications, setNotifications] = useState([]);
const [storageError, setStorageError] = useState("");
const handleStorageError = useCallback((message) => setStorageError(message), []);
@ -61,6 +62,7 @@ function App() {
const actions = useToolboxActions({
store,
notify,
creationTemplate: siteContent?.toolboxes?.creationTemplate,
setCreateModal,
setConfirmModal,
setLinkModalGameId,
@ -114,6 +116,8 @@ function App() {
updateToolbox={actions.updateToolbox}
updateModuleData={store.updateModuleData}
addImageFiles={actions.addImageFiles}
pendingTutorialToolboxId={pendingTutorialToolboxId}
setPendingTutorialToolboxId={setPendingTutorialToolboxId}
/>
{drawerGameId && (
<Suspense fallback={null}>
@ -151,6 +155,7 @@ function App() {
getGame={getGame}
toolboxes={store.toolboxes}
links={store.links}
setPendingTutorialToolboxId={setPendingTutorialToolboxId}
/>
</Shell>
);

View file

@ -7,7 +7,7 @@ function prefersReducedMotion() {
return typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
}
export function HomeOnboardingSection({ content, className = "" }) {
export function HomeOnboardingSection({ content, className = "", onCreateToolbox }) {
const slides = useMemo(() => content?.slides || [], [content]);
const [activeSlide, setActiveSlide] = useState(0);
const [isPaused, setIsPaused] = useState(false);
@ -45,6 +45,13 @@ export function HomeOnboardingSection({ content, className = "" }) {
</li>
))}
</ol>
{onCreateToolbox && content.primaryAction && (
<div className="home-onboarding-steps-action">
<button className="primary home-onboarding-create-button" type="button" onClick={onCreateToolbox}>
{content.primaryAction}
</button>
</div>
)}
</div>
<div className="home-onboarding-preview">
<h3>{content.sliderTitle}</h3>

View file

@ -3,7 +3,7 @@ import { TOOLBOX_MODULES } from "../features/toolboxes/toolboxModuleRegistry.js"
import { RichText } from "../components/RichText.jsx";
import { HomeOnboardingSection } from "./HomeOnboardingSection.jsx";
export function HomePage({ siteContent, toolboxes }) {
export function HomePage({ siteContent, toolboxes, actions }) {
const content = siteContent.home;
const toolCount = Object.keys(TOOLBOX_MODULES).length;
const showOnboarding = import.meta.env.DEV || toolboxes.length === 0;
@ -24,7 +24,7 @@ export function HomePage({ siteContent, toolboxes }) {
<article><strong>{toolCount}</strong><span>{toolCount > 1 ? content.stats.toolPlural : content.stats.toolSingular}</span></article>
</div>
</section>
{showOnboarding && <HomeOnboardingSection content={content.onboarding} />}
{showOnboarding && <HomeOnboardingSection content={content.onboarding} onCreateToolbox={() => actions.setCreateModal({ tutorial: true })} />}
<section className="origin-section nebula-panel" aria-labelledby="origin-title">
<div className="origin-copy">
<p className="eyebrow">{content.origin.eyebrow}</p>

View file

@ -231,6 +231,18 @@ span {
font-size: var(--font-size-lg);
}
.home-onboarding-create-button {
min-height: 40px;
padding-inline: var(--space-4);
}
.home-onboarding-steps-action {
display: flex;
justify-content: flex-start;
padding-top: var(--space-2);
padding-left: calc(28px + var(--space-3));
}
.home-onboarding-preview {
display: grid;
align-content: start;

View file

@ -62,6 +62,16 @@
-webkit-mask-image: url("/static/icons/link.svg");
}
.ui-icon-picture {
mask-image: url("/static/icons/picture.svg");
-webkit-mask-image: url("/static/icons/picture.svg");
}
.ui-icon-toolbox {
mask-image: url("/static/icons/toolbox.svg");
-webkit-mask-image: url("/static/icons/toolbox.svg");
}
.ui-icon-settings {
mask-image: url("/static/icons/settings.svg");
-webkit-mask-image: url("/static/icons/settings.svg");

View file

@ -83,6 +83,14 @@
width: 100%;
}
.home-onboarding-steps-action {
padding-left: 0;
}
.home-onboarding-create-button {
width: 100%;
}
.hero,
.game-hero,
.page-hero,

View file

@ -178,6 +178,125 @@
outline-offset: 3px;
}
.toolbox-tutorial-tooltip {
position: fixed;
z-index: 640;
display: grid;
gap: var(--space-3);
padding: var(--space-4);
border: 1px solid rgba(246, 196, 83, 0.34);
border-radius: var(--radius-md);
background:
radial-gradient(circle at 12% 0%, rgba(246, 196, 83, 0.12), transparent 36%),
rgba(7, 10, 24, 0.98);
box-shadow:
var(--shadow-lg),
0 0 24px rgba(139, 92, 246, 0.18);
}
.toolbox-tutorial-tooltip::before {
content: "";
position: absolute;
top: -6px;
left: var(--tutorial-arrow-left, 50%);
width: 10px;
height: 10px;
border-top: 1px solid rgba(246, 196, 83, 0.34);
border-left: 1px solid rgba(246, 196, 83, 0.34);
background: rgba(7, 10, 24, 0.98);
transform: translateX(-50%) rotate(45deg);
}
.toolbox-tutorial-tooltip[data-placement="above"]::before {
top: auto;
bottom: -6px;
border-top: 0;
border-left: 0;
border-right: 1px solid rgba(246, 196, 83, 0.34);
border-bottom: 1px solid rgba(246, 196, 83, 0.34);
}
.toolbox-tutorial-tooltip[data-placement="right"]::before {
top: var(--tutorial-arrow-top, 50%);
left: -6px;
border-top: 0;
border-left: 1px solid rgba(246, 196, 83, 0.34);
border-right: 0;
border-bottom: 1px solid rgba(246, 196, 83, 0.34);
transform: translateY(-50%) rotate(45deg);
}
.toolbox-tutorial-heading {
display: flex;
align-items: center;
gap: var(--space-2);
padding-right: var(--space-6);
}
.toolbox-tutorial-heading .ui-icon {
width: 20px;
height: 20px;
color: var(--color-accent-gold);
}
.toolbox-tutorial-heading strong {
color: var(--color-text-primary);
line-height: 1.25;
}
.toolbox-tutorial-tooltip p {
margin: 0;
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
line-height: 1.5;
}
.toolbox-tutorial-count {
width: fit-content;
padding: 2px 7px;
border: 1px solid rgba(246, 196, 83, 0.22);
border-radius: var(--radius-pill);
color: var(--color-accent-gold);
font-size: var(--font-size-xs);
font-weight: 900;
}
.toolbox-tutorial-close {
position: absolute;
top: var(--space-3);
right: var(--space-3);
width: 30px;
min-width: 30px;
min-height: 30px;
justify-content: center;
padding: 0;
border-color: transparent;
background: transparent;
color: var(--color-text-muted);
}
.toolbox-tutorial-close:hover {
border-color: rgba(150, 165, 205, 0.18);
background: rgba(21, 26, 48, 0.62);
color: var(--color-text-primary);
box-shadow: none;
}
.toolbox-tutorial-close .ui-icon {
width: 16px;
height: 16px;
}
.toolbox-tutorial-tooltip footer {
display: flex;
justify-content: flex-end;
}
.toolbox-tutorial-tooltip footer button {
min-height: 36px;
padding-inline: var(--space-3);
}
.tool-add-controls {
position: relative;
z-index: 280;