Add guided onboarding tour for first toolbox creation
All checks were successful
Deploy Sokko G / deploy (push) Successful in 14s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 14s
This commit is contained in:
parent
3007aa1389
commit
4f73e0068b
20 changed files with 470 additions and 20 deletions
|
|
@ -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;
|
||||
|
|
|
|||
109
website/src/components/GuidedTooltipTour.jsx
Normal file
109
website/src/components/GuidedTooltipTour.jsx
Normal 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
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
})}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -83,6 +83,14 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.home-onboarding-steps-action {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.home-onboarding-create-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hero,
|
||||
.game-hero,
|
||||
.page-hero,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue