Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
a617a72
feat(onboarding): onboarding components
Dhruv-0-Arora Jul 8, 2026
b5299c7
feat(onboarding): adding initial onboarding ui
Dhruv-0-Arora Jul 8, 2026
4f9e17e
ci(preferences): onboarding preferences tests
Dhruv-0-Arora Jul 8, 2026
3ac1180
fix(onboarding): card ui
Dhruv-0-Arora Jul 13, 2026
99bc0cd
feat(mirabuf): add year and thumbnail metadata to asset pipeline
Dhruv-0-Arora Jul 13, 2026
05f74f2
feat(ui): replace spawn asset panel with year-grouped Library modal
Dhruv-0-Arora Jul 13, 2026
aab72a7
refactor(onboarding): code quality
Dhruv-0-Arora Jul 14, 2026
2c5aca4
refactor(library): extract Autodesk Hub accordion into its own component
Dhruv-0-Arora Jul 14, 2026
ad56d11
fix(library): populate asset year metadata so the modal groups by year
Dhruv-0-Arora Jul 14, 2026
6b66c57
feat(library): use the thumbnail embedded in mira metadata for asset …
Dhruv-0-Arora Jul 14, 2026
65def65
fix(onboarding): onboarding handling for library modal
Dhruv-0-Arora Jul 15, 2026
51b1577
feat(onboarding): added informational tourcards
Dhruv-0-Arora Jul 15, 2026
ccd7dfa
feat(onboarding): add assembly setup input-scheme tour step
Dhruv-0-Arora Jul 15, 2026
08d3c51
chore(librarymodal): cleaning savedassets accordian
Dhruv-0-Arora Jul 21, 2026
f7908b6
fix!(onboarding): auto-advancing despite minification
Dhruv-0-Arora Jul 21, 2026
25fd398
fix(onboarding): field precedes robots & double scroll bug
Dhruv-0-Arora Jul 22, 2026
22ee90b
Merge remote-tracking branch 'origin/feat/63/ui-redesign' into darora…
Dhruv-0-Arora Jul 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion fission/manifest.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export type ManifestFileType = Record<"robots" | "private" | "fields", { filename: string; hash: string }[]>
export type ManifestFileEntry = { filename: string; hash: string; year?: number; thumbnail?: string }
export type ManifestFileType = Record<"robots" | "private" | "fields", ManifestFileEntry[]>
4 changes: 2 additions & 2 deletions fission/public/assetpack.zip
Git LFS file not shown
35 changes: 20 additions & 15 deletions fission/src/Synthesis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { StateProvider } from "./ui/StateProvider.tsx"
import { ThemeProvider } from "./ui/ThemeProvider.tsx"
import { UIProvider } from "./ui/UIProvider.tsx"
import CommandPalette from "@/ui/components/CommandPalette.tsx"
import { TourProvider } from "./ui/tour/TourProvider.tsx"
import TourOverlay from "./ui/tour/TourOverlay.tsx"

function Synthesis() {
const [consentPopupDisable, setConsentPopupDisable] = useState<boolean>(true)
Expand Down Expand Up @@ -78,22 +80,25 @@ function Synthesis() {
>
<StateProvider>
<UIProvider>
<Scene useStats={import.meta.env.DEV} key="scene-in-toast-provider" />
<TouchControls />
<SceneOverlay />
<ContextMenu />
<MultiplayerHUD />
<MainHUD key={"main-hud"} />
<UIRenderer />
<CommandPalette />
<ProgressNotifications key={"progress-notifications"} />
<WPILibConnectionStatus />
<DragModeIndicator />
<PortraitOverlay />
<TourProvider>
<Scene useStats={import.meta.env.DEV} key="scene-in-toast-provider" />
<TouchControls />
<SceneOverlay />
<ContextMenu />
<MultiplayerHUD />
<MainHUD key={"main-hud"} />
<UIRenderer />
<CommandPalette />
<ProgressNotifications key={"progress-notifications"} />
<WPILibConnectionStatus />
<DragModeIndicator />
<PortraitOverlay />
<TourOverlay />

{!consentPopupDisable && (
<AnalyticsConsent onClose={onDisableConsent} onConsent={onConsent} />
)}
{!consentPopupDisable && (
<AnalyticsConsent onClose={onDisableConsent} onConsent={onConsent} />
)}
</TourProvider>
</UIProvider>
</StateProvider>
</SnackbarProvider>
Expand Down
7 changes: 6 additions & 1 deletion fission/src/mirabuf/DefaultAssetLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { type MirabufCacheInfo, MiraType } from "@/mirabuf/MirabufLoader.ts"
import type { ManifestFileType } from "../../manifest.d.ts"
import { API_URL } from "@/util/Consts.ts"

export type DefaultAssetInfo = Required<Pick<MirabufCacheInfo, "hash" | "remotePath" | "miraType" | "name">>
export type DefaultAssetInfo = Required<Pick<MirabufCacheInfo, "hash" | "remotePath" | "miraType" | "name">> & {
year?: number
thumbnail?: string
}

class DefaultAssetLoader {
private static _assets: DefaultAssetInfo[] = []
Expand Down Expand Up @@ -34,6 +37,8 @@ class DefaultAssetLoader {
hash: obj.hash,
miraType,
name: obj.filename,
year: obj.year,
thumbnail: obj.thumbnail ? `${baseUrl}/${dir}/${obj.thumbnail}` : undefined,
})
})
})
Expand Down
26 changes: 23 additions & 3 deletions fission/src/mirabuf/MirabufLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,21 @@ export interface MirabufCacheInfo {
miraType: MiraType
remotePath?: string
thumbnailStorageID?: string
/** Competition year, when known (defaults from the remote manifest). */
year?: number
/** Servable URL of a preview thumbnail, when available. */
thumbnail?: string
}

export interface CacheRemoteOptions {
/** Display name for the cached file; defaults to the assembly's own name. */
name?: string
/** When set, warn if the downloaded content's hash differs. */
expectedHash?: string
/** Competition year to store on the cache entry. */
year?: number
/** Servable thumbnail URL to store on the cache entry. */
thumbnail?: string
}

export interface MirabufRemoteInfo {
Expand Down Expand Up @@ -150,16 +165,17 @@ class MirabufCachingService {
*
* @param {string} fetchLocation Location of Mirabuf file.
* @param {MiraType} miraType Type of Mirabuf Assembly.
* @param {string} name Optional display name for the cached file.
* @param {CacheRemoteOptions} options Optional metadata to store alongside the cached file.
*
* @returns {Promise<MirabufCacheInfo | undefined>} Promise with the result of the promise. Metadata on the mirabuf file if successful, undefined if not.
*/
public static async cacheRemote(
fetchLocation: string,
miraType: MiraType,
name?: string,
expectedHash?: string
options: CacheRemoteOptions = {}
): Promise<MirabufCacheInfo | undefined> {
const { expectedHash, year, thumbnail } = options
let { name } = options
try {
// grab file remote
const resp = await fetch(encodeURI(fetchLocation), import.meta.env.DEV ? { cache: "no-store" } : undefined)
Expand All @@ -180,6 +196,8 @@ class MirabufCachingService {
miraType,
name,
remotePath: fetchLocation,
year,
thumbnail,
},
expectedHash
)
Expand All @@ -199,6 +217,8 @@ class MirabufCachingService {
hash: await hashBuffer(miraBuff),
miraType: miraType,
name: name,
year,
thumbnail,
}
} catch (e) {
console.warn("Caching failed", e)
Expand Down
2 changes: 2 additions & 0 deletions fission/src/systems/preferences/PreferenceTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export type UserPreferences = {
ShowCenterOfMassIndicators: boolean
MultiplayerUsername: string
MultiplayerClientID: string
HasSeenOnboardingTour: boolean
}

export type UserPreference = keyof UserPreferences
Expand Down Expand Up @@ -73,6 +74,7 @@ export function defaultUserPreferences(): UserPreferences {
ShowCenterOfMassIndicators: false,
MultiplayerClientID: "",
MultiplayerUsername: "",
HasSeenOnboardingTour: false,
}
}

Expand Down
9 changes: 3 additions & 6 deletions fission/src/systems/scene/SceneRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@ import {
CustomTargetControls,
} from "@/systems/scene/CameraControls"
import type { ContextData } from "@/ui/components/ContextMenuData"
import { globalOpenPanel } from "@/ui/components/GlobalUIControls"
import { globalOpenModal } from "@/ui/components/GlobalUIControls"
import type { PixelSpaceCoord } from "@/ui/components/SceneOverlayEvents"
import type { ConfigurationType } from "@/ui/panels/configuring/assembly-config/ConfigTypes"
import ImportMirabufPanel from "@/ui/panels/mirabuf/ImportMirabufPanel"
import LibraryModal from "@/ui/modals/mirabuf/LibraryModal"
import { rayCastForRigidBody } from "@/util/RaycastUtils"
import PreferencesSystem from "../preferences/PreferencesSystem"
import type { GraphicsPreferences } from "../preferences/PreferenceTypes"
Expand Down Expand Up @@ -613,9 +612,7 @@ class SceneRenderer extends WorldSystem {
miraSupplierData.items.push({
name: "Add",
func: () => {
globalOpenPanel(ImportMirabufPanel, {
configurationType: "ROBOTS" as ConfigurationType,
})
globalOpenModal(LibraryModal, undefined)
},
})
}
Expand Down
12 changes: 12 additions & 0 deletions fission/src/test/PreferencesSystem.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,18 @@ describe("Preferences System Global Values", () => {
expect(PreferencesSystem.getUserPreference("RenderScoreboard")).toBe(true)
})

test("Onboarding tour flag defaults to false and persists", () => {
// First-visit detection relies on this defaulting to false for a fresh browser.
PreferencesSystem.clearPreferences()
expect(PreferencesSystem.getUserPreference("HasSeenOnboardingTour")).toBe(false)

PreferencesSystem.setUserPreference("HasSeenOnboardingTour", true)
PreferencesSystem.savePreferences()
PreferencesSystem.loadPreferences()

expect(PreferencesSystem.getUserPreference("HasSeenOnboardingTour")).toBe(true)
})

test("Graphics preferences", () => {
PreferencesSystem.getGraphicsPreferences()

Expand Down
5 changes: 4 additions & 1 deletion fission/src/ui/StateProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,23 @@ import type { InputScheme } from "@/systems/input/InputTypes"
import { StateContext, type StateProviderProps } from "./helpers/StateProviderHelpers"

export const StateProvider: React.FC<StateProviderProps> = ({ children }) => {
const [unconfirmedImport, setUnconfirmedImport] = useState<boolean>(false)
const [selectedScheme, setSelectedScheme] = useState<InputScheme | undefined>(undefined)
const [appMode, setAppMode] = useState<AppMode>("Configure")
const [selectedConfigAssembly, setSelectedConfigAssembly] = useState<MirabufSceneObject | undefined>(undefined)

const stateContextValue = useMemo(
() => ({
unconfirmedImport,
setUnconfirmedImport,
selectedScheme,
setSelectedScheme,
appMode,
setAppMode,
selectedConfigAssembly,
setSelectedConfigAssembly,
}),
[selectedScheme, appMode, selectedConfigAssembly]
[unconfirmedImport, selectedScheme, appMode, selectedConfigAssembly]
)

return <StateContext.Provider value={stateContextValue}>{children}</StateContext.Provider>
Expand Down
35 changes: 29 additions & 6 deletions fission/src/ui/UIProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ function shallowEqualProps(a: unknown, b: unknown): boolean {
return aKeys.every(k => a[k] === b[k])
}

const getContentName = (content: unknown): string => (content as { name?: string } | undefined)?.name ?? ""

/** True for a ConfigurePanel with unsaved work (an assembly selected or a config mode set). */
function isActivelyConfiguring(panel: { content: unknown; props: unknown }): boolean {
if (getContentName(panel.content) !== "ConfigurePanel") return false
const custom = (panel.props as { custom?: { selectedAssembly?: unknown; configMode?: unknown } }).custom ?? {}
return Boolean(custom.selectedAssembly) || custom.configMode !== undefined
}

const UNSAVED_CONFIG_WARNING = "You have unsaved configuration open. Close it before spawning."

// biome-ignore-start lint/suspicious/noExplicitAny: need to be able to extend
export const UIProvider: React.FC<UIProviderProps> = ({ children }) => {
const [modal, setModal] = useState<Modal<any, any> | undefined>(undefined)
Expand Down Expand Up @@ -96,6 +107,13 @@ export const UIProvider: React.FC<UIProviderProps> = ({ children }) => {
props: Omit<ModalProps<P>, "type" | "configured" | "custom"> &
Omit<UIScreenCallbacks<T>, "onBeforeAccept"> = DEFAULT_PROPS
) => {
// Block opening the asset Library while an assembly is actively being configured
// (mirrors the panel-level guard that used to apply when the Library was a panel).
if (getContentName(content) === "LibraryModal" && panels.some(isActivelyConfiguring)) {
enqueueSnackbar(UNSAVED_CONFIG_WARNING, { variant: "warning", action: snackbarAction })
return ""
}

const id = uuidv4()
const newModal = {
id,
Expand Down Expand Up @@ -126,7 +144,7 @@ export const UIProvider: React.FC<UIProviderProps> = ({ children }) => {
setModal(newModal as Modal<any, any>)
return id
},
[modal]
[modal, panels]
)

const openPanel: OpenPanelFn = useCallback(
Expand Down Expand Up @@ -180,14 +198,19 @@ export const UIProvider: React.FC<UIProviderProps> = ({ children }) => {
panel.onCancel = new UICallback()
if (props.onCancel) panel.onCancel.setUserDefinedFunc(props.onCancel)

const contentName = (content as unknown as { name?: string })?.name ?? ""
const mutuallyExclusive = ["ImportMirabufPanel", "ConfigurePanel", "InitialConfigPanel"]
const contentName = getContentName(content)
const mutuallyExclusive = ["ConfigurePanel", "InitialConfigPanel"]
const nextPanels = existingDuplicate ? panels.filter(p => p !== existingDuplicate) : panels
if (mutuallyExclusive.includes(contentName)) {
const existing = panels.find(p =>
mutuallyExclusive.includes((p.content as unknown as { name?: string })?.name ?? "")
)
const existing = panels.find(p => mutuallyExclusive.includes(getContentName(p.content)))
if (existing) {
// Opening InitialConfigPanel over an actively-edited ConfigurePanel would drop
// unsaved work; warn the user and keep Configure open.
if (contentName === "InitialConfigPanel" && isActivelyConfiguring(existing)) {
enqueueSnackbar(UNSAVED_CONFIG_WARNING, { variant: "warning", action: snackbarAction })
setPanels(p => [...p.filter(x => x !== existing), existing])
return existing.id
}
// Replace existing with the new one
setPanels(p => [...p.filter(x => x !== existing), panel as Panel<any, any>])
return id
Expand Down
2 changes: 1 addition & 1 deletion fission/src/ui/components/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useUIContext } from "@/ui/helpers/UIProviderHelpers"
import CommandRegistry, { type CommandDefinition } from "@/ui/components/CommandRegistry"
import "@/ui/panels/DebugPanel"
import "@/ui/modals/configuring/SettingsModal"
import "@/ui/panels/mirabuf/ImportMirabufPanel"
import "@/ui/modals/mirabuf/LibraryModal"
import "@/ui/panels/configuring/assembly-config/ConfigurePanel"
import "@/ui/panels/configuring/MatchModeConfigPanel"

Expand Down
9 changes: 2 additions & 7 deletions fission/src/ui/components/MobileHUD.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ import APSManagementModal from "../modals/APSManagementModal"
import SettingsModal from "../modals/configuring/SettingsModal"
import MultiplayerStartModal from "../modals/MultiplayerStartModal"
import { startMultiplayerWorld } from "../helpers/StartMultiplayerWorld"
import type { ConfigurationType } from "../panels/configuring/assembly-config/ConfigTypes"
import ImportMirabufPanel from "../panels/mirabuf/ImportMirabufPanel"
import LibraryModal from "../modals/mirabuf/LibraryModal"
import { setAddToast, setOpenModal, setOpenPanel } from "./GlobalUIControls"
import { IconButton, SynthesisIcons } from "./StyledComponents"
import { AssemblySelect } from "./topbar/AssemblySelect"
Expand Down Expand Up @@ -77,11 +76,7 @@ const MobileHUD: React.FC = () => {
<HUDMenuButton
label="Add Assembly"
iconName="add"
onClick={() =>
runAction(() =>
openPanel(ImportMirabufPanel, { configurationType: "ROBOTS" as ConfigurationType })
)
}
onClick={() => runAction(() => openModal(LibraryModal, undefined))}
/>

<HUDMenuButton label="Configure" iconName="mode-configure" onClick={() => setView("configure")} />
Expand Down
2 changes: 1 addition & 1 deletion fission/src/ui/components/StyledComponents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ export const PositiveIconButton: React.FC<IconButtonProps> = ({ children, onClic
export const DownloadButton: React.FC<IconButtonProps> = ({ onClick, ...props }) => {
return (
<PositiveIconButton onClick={onClick} {...props}>
<SynthesisIcons.DELETE_LARGE />
<SynthesisIcons.DOWNLOAD_LARGE />
</PositiveIconButton>
)
}
Expand Down
25 changes: 14 additions & 11 deletions fission/src/ui/components/TopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ import World from "@/systems/World.ts"
import { useStateContext } from "@/ui/helpers/StateProviderHelpers"
import { useIsTouchDevice } from "@/ui/helpers/useIsMobile"
import { deobf } from "@/util/Utility"
import { useUIContext } from "@/ui/helpers/UIProviderHelpers"
import APSManagementModal from "@/modals/APSManagementModal"
import SettingsModal from "@/modals/configuring/SettingsModal"
import type { ConfigurationType } from "@/panels/configuring/assembly-config/ConfigTypes"
import CameraSelectionPanel from "@/panels/configuring/CameraSelectionPanel"
import DeveloperToolPanel from "@/panels/DeveloperToolPanel"
import DebugPanel from "@/panels/DebugPanel"
import ImportMirabufPanel from "@/ui/panels/mirabuf/ImportMirabufPanel"
import { useUIContext } from "../helpers/UIProviderHelpers"
import APSManagementModal from "../modals/APSManagementModal"
import SettingsModal from "../modals/configuring/SettingsModal"
import CameraSelectionPanel from "../panels/configuring/CameraSelectionPanel"
import DeveloperToolPanel from "../panels/DeveloperToolPanel"
import DebugPanel from "../panels/DebugPanel"
import LibraryModal from "../modals/mirabuf/LibraryModal"
import { setAddToast, setOpenModal, setOpenPanel } from "./GlobalUIControls"
import { IconButton, SynthesisIcons } from "./StyledComponents"
import { useTourAnchor } from "@/ui/tour/useTourAnchor"
import ConfigureControls from "./topbar/ConfigureControls"
import GameplayControls from "./topbar/GameplayControls"
import ModeDropdown from "./topbar/ModeDropdown"
Expand All @@ -29,6 +29,9 @@ const TopBar: React.FC = () => {
const { appMode } = useStateContext()
const isTouchDevice = useIsTouchDevice()

const addAssemblyRef = useTourAnchor("add-assembly")
const modeDropdownRef = useTourAnchor("mode-dropdown")

setAddToast(addToast)
setOpenPanel(openPanel)
setOpenModal(openModal)
Expand Down Expand Up @@ -86,6 +89,7 @@ const TopBar: React.FC = () => {
disableInteractive
>
<Box
ref={modeDropdownRef}
component="span"
sx={{ display: "inline-flex" }}
onMouseEnter={() => setModeHovered(true)}
Expand All @@ -103,12 +107,11 @@ const TopBar: React.FC = () => {
</Tooltip>
<Tooltip title="Add Assembly">
<IconButton
ref={addAssemblyRef}
size="medium"
disableRipple
sx={TOP_BAR_ICON_BUTTON_SX}
onClick={() =>
openPanel(ImportMirabufPanel, { configurationType: "ROBOTS" as ConfigurationType })
}
onClick={() => openModal(LibraryModal, undefined)}
>
<TopBarIcon name="add" size={30} />
</IconButton>
Expand Down
Loading