mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-02 00:18:17 +00:00
pagescene: finish anonymous-use polish
- Gate File → Load my scene for anon: route through promptSignup
instead of opening a dialog that would just error-toast on the
silent listUserMediaFiles failure.
- Remove dead SettingsModal mount in Controls3D (no UI ever triggered
it). Drops the @storyteller/ui-settings-modal import from this file.
- New AnonHintChip pill in the top-right corner of the playground
("Sign up to save"). Renders only when currentUserToken is undefined
AND not in view-only mode; click routes to promptSignup. Tauri users
never see it.
- Scaffolding for view-only PreviewBox: new component renders a small
card with the author's generation result; returns null when no URL
is supplied. Mounted in Stage3DBody under the isViewOnly guard,
reading sceneMeta.previewImageUrl (new optional field). Data-path
wiring (populating previewImageUrl from adapter.loadScene's
response) is the next step — leaving the UI plumbing in place so
that follow-up is purely data, not layout surgery.
This commit is contained in:
1 parent
a9fa8d710a
commit
008fcfeaa7
9 files changed
+112
-25
No files matched your search
@@ -187,6 +187,7 @@ export { getSceneGenerationMetaData } from "./lib/sceneMetadata";
|
||||
// signalScene, addToast, loadingBarData/IsShowing signals, etc.)
|
||||
// are either surfaced via adapter slots or replaced with
|
||||
// lib-internal state.
|
||||
export { AnonHintChip } from "./lib/comps/AnonHintChip";
|
||||
export { AssetMenu, AssetModal } from "./lib/comps/AssetMenu";
|
||||
export { Controls3D } from "./lib/comps/Controls3D";
|
||||
export { ControlsTopButtons } from "./lib/comps/ControlsTopButtons";
|
||||
@@ -199,6 +200,8 @@ export { EditorCanvas, CameraViewCanvas } from "./lib/comps/EngineCanvases";
|
||||
export { Outliner } from "./lib/comps/Outliner";
|
||||
export { ControlPanelSceneObject } from "./lib/comps/ControlPanelSceneObject";
|
||||
export { SceneContainer } from "./lib/comps/SceneContainer";
|
||||
export { PreviewBox } from "./lib/comps/PreviewBox";
|
||||
export type { PreviewBoxProps } from "./lib/comps/PreviewBox";
|
||||
export { PreviewEngineCamera } from "./lib/comps/PreviewEngineCamera";
|
||||
export { PreviewImages } from "./lib/comps/PreviewImages";
|
||||
export { OnboardingHelper } from "./lib/comps/OnboardingHelper";
|
||||
|
||||
@@ -22,6 +22,10 @@ export interface SceneMeta {
|
||||
ownerToken: string | undefined;
|
||||
isModified: boolean | undefined;
|
||||
isInitializing: boolean;
|
||||
// URL to the author's generation result for this scene, shown in
|
||||
// view-only mode's PreviewBox. Host populates this from the
|
||||
// adapter.loadScene response. Optional; absent ⇒ no preview rendered.
|
||||
previewImageUrl?: string;
|
||||
}
|
||||
|
||||
export type SceneObjectKind = "object" | "character" | "shape";
|
||||
|
||||
@@ -33,6 +33,7 @@ import type { PopoverItem } from "@storyteller/ui-popover";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { EngineContext } from "./contexts/EngineContext/EngineContext";
|
||||
import { AnonHintChip } from "./comps/AnonHintChip";
|
||||
import { ControlPanelSceneObject } from "./comps/ControlPanelSceneObject";
|
||||
import { Controls3D } from "./comps/Controls3D";
|
||||
import { ControlsTopButtons } from "./comps/ControlsTopButtons";
|
||||
@@ -41,6 +42,7 @@ import { FocalLengthDisplay } from "./comps/FocalLengthDisplay/FocalLengthDispla
|
||||
import { OnboardingHelper } from "./comps/OnboardingHelper";
|
||||
import { Outliner } from "./comps/Outliner";
|
||||
import { PoseModeSelector } from "./comps/PoseModeSelector";
|
||||
import { PreviewBox } from "./comps/PreviewBox";
|
||||
import { PreviewEngineCamera } from "./comps/PreviewEngineCamera";
|
||||
import { SceneContainer } from "./comps/SceneContainer";
|
||||
import { addCharacter, addObject, setCameraAspect } from "./actions";
|
||||
@@ -74,6 +76,7 @@ export const Stage3DBody = () => {
|
||||
(s) => s.setIsPromptBoxFocused,
|
||||
);
|
||||
const gridVisible = usePageSceneStore((s) => s.gridVisible);
|
||||
const previewImageUrl = usePageSceneStore((s) => s.sceneMeta.previewImageUrl);
|
||||
const isViewOnly = useIsViewOnly();
|
||||
const addCamera = usePageSceneStore((s) => s.addCamera);
|
||||
const updateCamera = usePageSceneStore((s) => s.updateCamera);
|
||||
@@ -344,6 +347,9 @@ export const Stage3DBody = () => {
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<ControlsTopButtons />
|
||||
<Controls3D />
|
||||
<div className="flex items-start justify-end gap-2 pr-2 pt-2">
|
||||
<AnonHintChip />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -364,8 +370,7 @@ export const Stage3DBody = () => {
|
||||
{!isViewOnly && <ControlPanelSceneObject />}
|
||||
</div>
|
||||
|
||||
{/* Future: <PreviewBox /> slot when isViewOnly — shows the
|
||||
author's generated still next to the camera. */}
|
||||
{isViewOnly && <PreviewBox imageUrl={previewImageUrl} />}
|
||||
|
||||
{!isViewOnly && <PromptBox3D
|
||||
cameras={cameras}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useContext } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faArrowRightToBracket } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { EngineContext } from "../../contexts/EngineContext/EngineContext";
|
||||
import { usePageSceneStore, useIsViewOnly } from "../../PageSceneStore";
|
||||
|
||||
// Quiet "you're signed out" affordance shown in the editor playground for
|
||||
// anonymous visitors. Hidden in view-only mode (the visitor is consuming
|
||||
// someone else's scene; no upsell pressure needed). The actual per-action
|
||||
// signup CTAs (Save, Generate, Upload) still fire when the user clicks
|
||||
// those buttons — this chip just sets expectations up front.
|
||||
export const AnonHintChip = () => {
|
||||
const editor = useContext(EngineContext);
|
||||
const currentUserToken = usePageSceneStore((s) => s.currentUserToken);
|
||||
const isViewOnly = useIsViewOnly();
|
||||
|
||||
if (currentUserToken || isViewOnly) return null;
|
||||
|
||||
const handleClick = () => {
|
||||
editor?.adapter.promptSignup?.("hint");
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
className="glass flex items-center gap-2 rounded-full border border-white/10 px-3 py-1.5 text-xs font-medium text-white/80 shadow-md transition-colors hover:bg-ui-controls/100 hover:text-white"
|
||||
>
|
||||
<FontAwesomeIcon icon={faArrowRightToBracket} className="opacity-70" />
|
||||
Sign up to save
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { AnonHintChip } from "./AnonHintChip";
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
import { ButtonIconSelect } from "@storyteller/ui-button-icon-select";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { Tooltip } from "@storyteller/ui-tooltip";
|
||||
import { SettingsModal } from "@storyteller/ui-settings-modal";
|
||||
import { PopoverMenu } from "@storyteller/ui-popover";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
@@ -52,7 +51,6 @@ export const Controls3D = () => {
|
||||
})),
|
||||
);
|
||||
const [showEmptySceneTooltip, setShowEmptySceneTooltip] = useState(false);
|
||||
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
|
||||
const [upload3DIsShowing, setUpload3DIsShowing] = useState(false);
|
||||
const [isAddAssetPopoverOpen, setIsAddAssetPopoverOpen] = useState(false);
|
||||
const [uploadImageIsShowing, setUploadImageIsShowing] = useState(false);
|
||||
@@ -306,13 +304,6 @@ export const Controls3D = () => {
|
||||
|
||||
<AssetModal />
|
||||
|
||||
<SettingsModal
|
||||
isOpen={isSettingsModalOpen}
|
||||
onClose={() => setIsSettingsModalOpen(false)}
|
||||
globalAccountLogoutCallback={() => editor?.adapter.performLogout()}
|
||||
initialSection="accounts"
|
||||
/>
|
||||
|
||||
{editor &&
|
||||
editor.adapter.renderAssetUploader({
|
||||
isOpen: upload3DIsShowing,
|
||||
|
||||
+20
-14
@@ -169,20 +169,26 @@ export const ControlsTopButtons = () => {
|
||||
{
|
||||
label: "Load my scene",
|
||||
description: "Ctrl+O",
|
||||
dialogProps: {
|
||||
title: "Load a Saved Scene",
|
||||
content: (
|
||||
<LoadUserScenes onSceneSelect={handleSceneSelection} />
|
||||
),
|
||||
confirmButtonProps: {
|
||||
label: "Load",
|
||||
disabled: sceneTokenSelected === "",
|
||||
onClick: handleButtonLoadScene,
|
||||
},
|
||||
closeButtonProps: { label: "Cancel" },
|
||||
showClose: true,
|
||||
className: "max-w-5xl",
|
||||
},
|
||||
...(currentUserToken
|
||||
? {
|
||||
dialogProps: {
|
||||
title: "Load a Saved Scene",
|
||||
content: (
|
||||
<LoadUserScenes onSceneSelect={handleSceneSelection} />
|
||||
),
|
||||
confirmButtonProps: {
|
||||
label: "Load",
|
||||
disabled: sceneTokenSelected === "",
|
||||
onClick: handleButtonLoadScene,
|
||||
},
|
||||
closeButtonProps: { label: "Cancel" },
|
||||
showClose: true,
|
||||
className: "max-w-5xl",
|
||||
},
|
||||
}
|
||||
: {
|
||||
onClick: () => editor?.adapter.promptSignup?.("load"),
|
||||
}),
|
||||
},
|
||||
{
|
||||
disabled: !canSave,
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useState } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faXmark } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
// Small fixed-position card shown next to the camera in view-only mode.
|
||||
// Renders the author's generation result so a visitor can see what the
|
||||
// scene was made to produce. Today this is scaffolding only — the URL
|
||||
// data path (likely a new field returned by adapter.loadScene's response
|
||||
// payload, propagated into sceneMeta.previewImageUrl) is wired up in a
|
||||
// follow-up PR. Renders null when no URL is supplied, so it's safe to
|
||||
// mount unconditionally inside the isViewOnly branch.
|
||||
export interface PreviewBoxProps {
|
||||
imageUrl?: string;
|
||||
}
|
||||
|
||||
export const PreviewBox = ({ imageUrl }: PreviewBoxProps) => {
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
|
||||
if (!imageUrl || dismissed) return null;
|
||||
|
||||
return (
|
||||
<div className="glass absolute bottom-6 right-6 z-20 flex w-48 flex-col gap-2 rounded-lg border border-white/10 p-2 shadow-lg">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDismissed(true)}
|
||||
className="absolute right-1 top-1 flex h-5 w-5 items-center justify-center rounded-full text-white/60 hover:bg-white/10 hover:text-white"
|
||||
aria-label="Hide preview"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} className="text-xs" />
|
||||
</button>
|
||||
<img
|
||||
src={imageUrl}
|
||||
crossOrigin="anonymous"
|
||||
alt="Author's generation"
|
||||
className="rounded-md"
|
||||
/>
|
||||
<span className="text-center text-[11px] text-white/60">
|
||||
Author's generation
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export { PreviewBox } from "./PreviewBox";
|
||||
export type { PreviewBoxProps } from "./PreviewBox";
|
||||
Reference in new issue
Block a user