From 8239a631f2b1d0228c2c21677f90773008917b61 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Thu, 13 Aug 2026 21:34:29 +0200 Subject: [PATCH] feat: consolidate onboarding --- apps/nextjs/package.json | 1 + .../public/logo/homarr-wordmark-light.svg | 72 ------ apps/nextjs/src/app/[locale]/init/page.tsx | 4 +- .../components/backup/use-backup-analysis.ts | 6 +- packages/onboarding/package.json | 1 + packages/onboarding/src/account.tsx | 8 + packages/onboarding/src/finish.tsx | 25 +- .../onboarding/src/homarr-wordmark-light.tsx | 227 ++++++++++++++++++ .../src/onboarding-studio.module.css | 46 +++- .../onboarding/src/onboarding-wordmark.tsx | 82 +++---- packages/onboarding/src/setup-studio.tsx | 191 +++++++++++---- packages/onboarding/src/types.ts | 2 +- .../onboarding/src/use-onboarding-sounds.ts | 25 ++ packages/onboarding/src/welcome.tsx | 29 ++- .../onboarding/src/wordmark-colors.spec.ts | 18 -- packages/onboarding/src/wordmark-colors.ts | 23 -- packages/translation/src/lang/en.json | 5 +- .../ui/src/components/board-color-input.tsx | 14 +- .../integration-multi-select-grid.tsx | 1 - pnpm-lock.yaml | 29 ++- pnpm-workspace.yaml | 3 +- 21 files changed, 574 insertions(+), 238 deletions(-) delete mode 100644 apps/nextjs/public/logo/homarr-wordmark-light.svg create mode 100644 packages/onboarding/src/homarr-wordmark-light.tsx create mode 100644 packages/onboarding/src/use-onboarding-sounds.ts delete mode 100644 packages/onboarding/src/wordmark-colors.spec.ts delete mode 100644 packages/onboarding/src/wordmark-colors.ts diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json index 8675f6dc2..45c46acef 100644 --- a/apps/nextjs/package.json +++ b/apps/nextjs/package.json @@ -68,6 +68,7 @@ "@mantine/modals": "catalog:", "@mantine/tiptap": "catalog:", "@modelcontextprotocol/server": "catalog:", + "@rexa-developer/tiks": "catalog:", "@scalar/api-reference-react": "catalog:", "@tabler/icons-react": "catalog:", "@tanstack/react-devtools": "catalog:", diff --git a/apps/nextjs/public/logo/homarr-wordmark-light.svg b/apps/nextjs/public/logo/homarr-wordmark-light.svg deleted file mode 100644 index eeaf82f8a..000000000 --- a/apps/nextjs/public/logo/homarr-wordmark-light.svg +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/apps/nextjs/src/app/[locale]/init/page.tsx b/apps/nextjs/src/app/[locale]/init/page.tsx index b219d1c33..9588452d8 100644 --- a/apps/nextjs/src/app/[locale]/init/page.tsx +++ b/apps/nextjs/src/app/[locale]/init/page.tsx @@ -35,7 +35,7 @@ export default async function InitPage() { where: eq(groups.name, everyoneGroup), columns: { homeBoardId: true }, }), - db.query.boards.findMany({ columns: { id: true, name: true } }), + db.query.boards.findMany({ columns: { id: true, name: true, primaryColor: true, secondaryColor: true } }), ]) : [undefined, []]; const homeBoardId = defaultGroup?.homeBoardId; @@ -68,7 +68,7 @@ export default async function InitPage() { canConfigurePrivileged, hasUsers: Boolean(firstUser), initialBoard: initialBoard ?? null, - availableBoards, + availableBoards: availableBoards.map(({ id, name }) => ({ id, name })), }} sqliteRestore={databaseDriver === "sqlite" ? : undefined} assistantConfiguration={canConfigurePrivileged ? : undefined} diff --git a/apps/nextjs/src/components/backup/use-backup-analysis.ts b/apps/nextjs/src/components/backup/use-backup-analysis.ts index ee92a4464..7897c1d24 100644 --- a/apps/nextjs/src/components/backup/use-backup-analysis.ts +++ b/apps/nextjs/src/components/backup/use-backup-analysis.ts @@ -1,6 +1,7 @@ "use client"; import { useCallback, useRef, useState } from "react"; +import { useTiks } from "@rexa-developer/tiks/react"; import { useScopedI18n } from "@homarr/translation/client"; @@ -58,6 +59,7 @@ export interface MigrationProgress { export const useBackupAnalysis = () => { const t = useScopedI18n("management.page.tool.backup.restore"); + const sounds = useTiks({ theme: "soft", volume: 0.12, respectReducedMotion: true }); const [analysis, setAnalysis] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -135,6 +137,7 @@ export const useBackupAnalysis = () => { Date.now(), ]); } catch (migrationErr) { + sounds.error(); setMigrationProgress({ current: i + 1, total: pendingMigrations.length, @@ -153,6 +156,7 @@ export const useBackupAnalysis = () => { tag: migration.tag, phase: "done", }); + sounds.success(); await delay(50); } @@ -173,7 +177,7 @@ export const useBackupAnalysis = () => { setLoading(false); } }, - [t], + [sounds, t], ); const reset = useCallback(() => { diff --git a/packages/onboarding/package.json b/packages/onboarding/package.json index cfb577e0d..d71f5bf03 100644 --- a/packages/onboarding/package.json +++ b/packages/onboarding/package.json @@ -35,6 +35,7 @@ "@homarr/validation": "workspace:^0.1.0", "@mantine/core": "catalog:", "@mantine/hooks": "catalog:", + "@rexa-developer/tiks": "catalog:", "@tabler/icons-react": "catalog:", "canvas-confetti": "catalog:", "react": "catalog:", diff --git a/packages/onboarding/src/account.tsx b/packages/onboarding/src/account.tsx index 7e80926e2..39002b765 100644 --- a/packages/onboarding/src/account.tsx +++ b/packages/onboarding/src/account.tsx @@ -19,6 +19,7 @@ import { userInitSchema } from "@homarr/validation/user"; import { didCredentialsSignInFail } from "./account-recovery"; import { OnboardingBackdrop } from "./onboarding-backdrop"; import type { OnboardingStudioProps } from "./types"; +import { useOnboardingSounds } from "./use-onboarding-sounds"; import classes from "./onboarding-studio.module.css"; const AccountShell = ({ @@ -65,8 +66,10 @@ const CredentialsSetup = () => { const [requiresSignIn, setRequiresSignIn] = useState(false); const [submitError, setSubmitError] = useState(null); const recoveryActionRef = useRef(null); + const sounds = useOnboardingSounds(); const mutation = clientApi.user.initUser.useMutation({ onError() { + sounds.error(); const message = t("unknownError"); setSubmitError(message); showErrorNotification({ title: t("errorTitle"), message }); @@ -92,6 +95,7 @@ const CredentialsSetup = () => { }); if (didCredentialsSignInFail(result)) throw new Error(); } catch { + sounds.warning(); setRequiresSignIn(true); return; } @@ -148,11 +152,14 @@ const CredentialsSetup = () => { const ExternalGroupSetup = () => { const t = useScopedI18n("init.studio.externalGroup"); const [submitError, setSubmitError] = useState(null); + const sounds = useOnboardingSounds(); const mutation = clientApi.group.createInitialExternalGroup.useMutation({ async onSuccess() { + sounds.success(); await revalidatePathActionAsync("/init"); }, onError() { + sounds.error(); const message = t("unknownError"); setSubmitError(message); showErrorNotification({ title: t("errorTitle"), message }); @@ -164,6 +171,7 @@ const ExternalGroupSetup = () => {
{ + sounds.click(); setSubmitError(null); mutation.mutate(values); })} diff --git a/packages/onboarding/src/finish.tsx b/packages/onboarding/src/finish.tsx index 5e20eb425..7e1e64e92 100644 --- a/packages/onboarding/src/finish.tsx +++ b/packages/onboarding/src/finish.tsx @@ -1,6 +1,6 @@ "use client"; -import type { MouseEvent as ReactMouseEvent } from "react"; +import type { CSSProperties, MouseEvent as ReactMouseEvent } from "react"; import { Button, Group, Paper, SimpleGrid, Stack, Text, ThemeIcon, Title } from "@mantine/core"; import { IconArrowRight, IconBook2, IconSparkles, IconTool } from "@tabler/icons-react"; import confetti from "canvas-confetti"; @@ -10,14 +10,20 @@ import { useScopedI18n } from "@homarr/translation/client"; import { Link } from "@homarr/ui"; import { OnboardingBackdrop } from "./onboarding-backdrop"; +import { OnboardingWordmark } from "./onboarding-wordmark"; import type { OnboardingStudioProps } from "./types"; +import { useOnboardingSounds } from "./use-onboarding-sounds"; import classes from "./onboarding-studio.module.css"; export const Finish = ({ environment }: OnboardingStudioProps) => { const t = useScopedI18n("init.studio.finish"); + const sounds = useOnboardingSounds(); + const primaryColor = environment.initialBoard?.primaryColor ?? "#fa5252"; + const secondaryColor = environment.initialBoard?.secondaryColor ?? "#fd7e14"; const boardHref = `/boards/${encodeURIComponent(environment.initialBoard?.name ?? "dashboard")}`; const destination = environment.canConfigurePrivileged ? boardHref : "/auth/login?callbackUrl=%2Finit"; const openBoard = (event: ReactMouseEvent) => { + sounds.success(); if ( event.button !== 0 || event.metaKey || @@ -45,18 +51,22 @@ export const Finish = ({ environment }: OnboardingStudioProps) => { }; return ( -
+
-
- - - -
+ {t("title")} @@ -67,6 +77,7 @@ export const Finish = ({ environment }: OnboardingStudioProps) => { component={Link} href={destination} size="lg" + className={classes.finishPrimaryAction} rightSection={} onClick={openBoard} > diff --git a/packages/onboarding/src/homarr-wordmark-light.tsx b/packages/onboarding/src/homarr-wordmark-light.tsx new file mode 100644 index 000000000..a2c2a6c8f --- /dev/null +++ b/packages/onboarding/src/homarr-wordmark-light.tsx @@ -0,0 +1,227 @@ +import * as React from "react"; +import type { SVGProps } from "react"; +const SvgHomarrWordmarkLight = (props: SVGProps) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); +export default SvgHomarrWordmarkLight; diff --git a/packages/onboarding/src/onboarding-studio.module.css b/packages/onboarding/src/onboarding-studio.module.css index 7678aec1d..31b57b19d 100644 --- a/packages/onboarding/src/onboarding-studio.module.css +++ b/packages/onboarding/src/onboarding-studio.module.css @@ -83,8 +83,25 @@ max-width: min(92vw, 42rem); } -:global([data-mantine-color-scheme="light"]) .wordmark:not([data-recolored="true"]) { - filter: brightness(0); +.celebratingWordmark { + animation: wordmarkJump 360ms ease-out; +} + +@keyframes wordmarkJump { + 0%, + 100% { + transform: translateY(0); + } + + 45% { + transform: translateY(-0.4rem); + } +} + +@media (prefers-reduced-motion: reduce) { + .celebratingWordmark { + animation: none; + } } .hero { @@ -249,16 +266,37 @@ } .nextCard { + --next-card-accent: var(--studio-glow-color); color: inherit; text-decoration: none; + border-color: color-mix(in srgb, var(--next-card-accent) 42%, var(--mantine-color-default-border)); +} + +.nextCard:nth-child(2) { + --next-card-accent: var(--studio-secondary-glow-color); +} + +.nextCard :global(.mantine-ThemeIcon-root) { + color: var(--next-card-accent); + background: color-mix(in srgb, var(--next-card-accent) 14%, transparent); +} + +.finishPrimaryAction { + color: var(--mantine-color-white); + background: linear-gradient(135deg, var(--studio-glow-color), var(--studio-secondary-glow-color)); + text-shadow: 0 1px 2px rgb(0 0 0 / 35%); +} + +.finishPrimaryAction:hover { + filter: brightness(0.94); } .nextCard:hover { - background: var(--mantine-color-default-hover); + background: color-mix(in srgb, var(--next-card-accent) 9%, var(--mantine-color-body)); } .nextCard:focus-visible { - outline: 3px solid var(--mantine-primary-color-light-color); + outline: 3px solid color-mix(in srgb, var(--next-card-accent) 55%, transparent); outline-offset: 2px; } diff --git a/packages/onboarding/src/onboarding-wordmark.tsx b/packages/onboarding/src/onboarding-wordmark.tsx index 437583e9e..69ea04631 100644 --- a/packages/onboarding/src/onboarding-wordmark.tsx +++ b/packages/onboarding/src/onboarding-wordmark.tsx @@ -1,12 +1,13 @@ "use client"; -import type { MouseEvent as ReactMouseEvent } from "react"; -import { useEffect, useMemo, useState } from "react"; +import type { CSSProperties, MouseEvent as ReactMouseEvent } from "react"; +import { useState } from "react"; import { useComputedColorScheme } from "@mantine/core"; import confetti from "canvas-confetti"; +import HomarrWordmarkLight from "./homarr-wordmark-light"; import classes from "./onboarding-studio.module.css"; -import { recolorWordmark } from "./wordmark-colors"; +import { useOnboardingSounds } from "./use-onboarding-sounds"; interface OnboardingWordmarkProps { primaryColor?: string; @@ -14,53 +15,50 @@ interface OnboardingWordmarkProps { large?: boolean; } -const wordmarkUrl = "/logo/homarr-wordmark-light.svg"; - export const OnboardingWordmark = ({ primaryColor, secondaryColor, large = false }: OnboardingWordmarkProps) => { - const [source, setSource] = useState(null); + const [celebrating, setCelebrating] = useState(false); const colorScheme = useComputedColorScheme("light"); - const recolored = Boolean(source && primaryColor && secondaryColor); + const sounds = useOnboardingSounds(); + const colors = { + "--homarr-wordmark-primary": primaryColor ?? "#F92424", + "--homarr-wordmark-secondary": secondaryColor ?? "#FA5252", + "--homarr-wordmark-foreground": colorScheme === "light" ? "#1A1B1E" : "#FEFDFD", + } as CSSProperties; - useEffect(() => { - const controller = new AbortController(); - void fetch(wordmarkUrl, { signal: controller.signal }) - .then((response) => (response.ok ? response.text() : null)) - .then((svg) => { - if (svg) setSource(svg); - }) - .catch(() => undefined); - return () => controller.abort(); - }, []); - - const src = useMemo(() => { - if (!source || !primaryColor || !secondaryColor) return wordmarkUrl; - return `data:image/svg+xml,${encodeURIComponent( - recolorWordmark(source, primaryColor, secondaryColor, colorScheme === "light" ? "#1A1B1E" : undefined), - )}`; - }, [colorScheme, primaryColor, secondaryColor, source]); - - const celebrate = (event: ReactMouseEvent) => { + const celebrate = (event: ReactMouseEvent) => { + sounds.hover(); const logo = event.currentTarget.getBoundingClientRect(); - void confetti({ - particleCount: 60, - spread: 65, - startVelocity: 32, - origin: { - x: (logo.left + logo.width / 2) / window.innerWidth, - y: (logo.top + logo.height / 2) / window.innerHeight, - }, - colors: primaryColor && secondaryColor ? [primaryColor, secondaryColor] : undefined, - disableForReducedMotion: true, - }); + const originY = (logo.top + logo.height / 2) / window.innerHeight; + const colors = primaryColor && secondaryColor ? [primaryColor, secondaryColor] : undefined; + + if (!window.matchMedia("(prefers-reduced-motion: reduce)").matches) setCelebrating(true); + for (const { angle, originX } of [ + { angle: 165, originX: logo.left + logo.width * 0.12 }, + { angle: 15, originX: logo.right - logo.width * 0.12 }, + ]) { + void confetti({ + particleCount: 28, + angle, + spread: 35, + startVelocity: 14, + gravity: 0.7, + ticks: 90, + scalar: 0.8, + origin: { x: originX / window.innerWidth, y: originY }, + colors, + disableForReducedMotion: true, + }); + } }; return ( - Homarr setCelebrating(false)} /> ); }; diff --git a/packages/onboarding/src/setup-studio.tsx b/packages/onboarding/src/setup-studio.tsx index 80f1b91c9..24249895d 100644 --- a/packages/onboarding/src/setup-studio.tsx +++ b/packages/onboarding/src/setup-studio.tsx @@ -98,6 +98,7 @@ import { IntegrationMultiSelectGrid } from "@homarr/ui/integration-select-grid"; import type { OnboardingStudioProps } from "./types"; import { OnboardingBackdrop } from "./onboarding-backdrop"; import { OnboardingWordmark } from "./onboarding-wordmark"; +import { useOnboardingSounds } from "./use-onboarding-sounds"; import { isHttpUrl, normalizeServiceUrl, @@ -146,6 +147,8 @@ const sectionDefinitions = [ ] as const; const radiusValues = ["xs", "sm", "md", "lg", "xl"] as const; +const initialPrimaryColor = "#fa5252"; +const initialSecondaryColor = "#fd7e14"; const emptyDiscoveredIntegrations: DockerDiscoveryData["integrations"] = []; const emptyDiscoveredApps: DockerDiscoveryData["apps"] = []; @@ -164,6 +167,7 @@ const OnboardingFloatingControl = ({ }: OnboardingFloatingControlProps) => { const [rootRef, setRootRef] = useState(null); const controlRefs = useRef>({}); + const sounds = useOnboardingSounds(); return (
@@ -185,7 +189,10 @@ const OnboardingFloatingControl = ({ className={classes.floatingControl} data-active={selected} aria-pressed={selected} - onClick={() => onChange(option.value)} + onClick={() => { + sounds.click(); + onChange(option.value); + }} > {option.label} @@ -201,6 +208,7 @@ export const SetupStudio = ({ environment, assistantConfiguration }: OnboardingS const tCommon = useScopedI18n("common.action"); const currentLocale = useCurrentLocale(); const { colorScheme, setColorScheme } = useMantineColorScheme(); + const sounds = useOnboardingSounds(); const [activeSection, setActiveSection] = useState("essentials"); const [incompleteIntegrationConfirmationOpened, setIncompleteIntegrationConfirmationOpened] = useState(false); const [selectedLocale, setSelectedLocale] = useState(currentLocale); @@ -217,9 +225,8 @@ export const SetupStudio = ({ environment, assistantConfiguration }: OnboardingS environment.initialBoard ?? (environment.availableBoards.length === 1 ? environment.availableBoards[0] : null); const [selectedBoardId, setSelectedBoardId] = useState(initialBoard?.id ?? null); const [boardName, setBoardName] = useState(initialBoard?.name ?? "dashboard"); - const [primaryColor, setPrimaryColor] = useState("#fa5252"); - const [secondaryColor, setSecondaryColor] = useState("#fd7e14"); - const [boardColorsDirty, setBoardColorsDirty] = useState(false); + const [primaryColor, setPrimaryColor] = useState(initialPrimaryColor); + const [secondaryColor, setSecondaryColor] = useState(initialSecondaryColor); const [itemRadius, setItemRadius] = useState("lg"); const [columnCount, setColumnCount] = useState(10); const [leftSidebar, setLeftSidebar] = useState(false); @@ -248,7 +255,8 @@ export const SetupStudio = ({ environment, assistantConfiguration }: OnboardingS showErrorNotification({ title: t("review.errorTitle"), message: error.message }); }, }); - const isApplying = complete.isPending; + const validateIntegration = clientApi.onboard.testIntegration.useMutation(); + const isApplying = complete.isPending || validateIntegration.isPending; const dockerData = docker.data; const discoveredIntegrations = dockerData?.integrations ?? emptyDiscoveredIntegrations; @@ -364,6 +372,7 @@ export const SetupStudio = ({ environment, assistantConfiguration }: OnboardingS }; const selectSection = (section: StudioSection) => { + if (section !== activeSection) sounds.swoosh(); setActiveSection(section); }; @@ -427,6 +436,22 @@ export const SetupStudio = ({ environment, assistantConfiguration }: OnboardingS } setApplyMessage(t("review.progress.applying")); + for (const [index, draft] of draftsToApply.entries()) { + setApplyProgress(5 + Math.round(((index + 1) / Math.max(1, draftsToApply.length)) * 15)); + try { + const result = await validateIntegration.mutateAsync({ + sourceId: draft.sourceId ?? draft.id, + name: draft.name, + url: normalizeServiceUrl(draft.url) ?? draft.url, + kind: draft.kind, + secrets: draft.secrets, + }); + if (result.success) sounds.success(); + else sounds.error(); + } catch { + sounds.error(); + } + } setApplyProgress(20); await new Promise((resolve) => setTimeout(resolve, 300)); const selectedSourceIds = new Set(draftsToApply.flatMap((draft) => (draft.sourceId ? [draft.sourceId] : []))); @@ -506,15 +531,17 @@ export const SetupStudio = ({ environment, assistantConfiguration }: OnboardingS completion.docker.ignoredSourceIds.length + completion.docker.skippedWidgets.length; if (dockerWarningCount > 0) { + sounds.warning(); showWarningNotification({ title: t("review.dockerWarningTitle"), message: t("review.dockerWarningDescription", { count: dockerWarningCount }), }); - } + } else sounds.success(); setApplyMessage(t("review.progress.done")); setApplyProgress(100); await revalidatePathActionAsync("/init"); } catch { + sounds.error(); setApplyProgress(0); } }; @@ -528,19 +555,31 @@ export const SetupStudio = ({ environment, assistantConfiguration }: OnboardingS urlMode, setUrlMode, analyticsEnabled, - setAnalyticsEnabled, + setAnalyticsEnabled: (value) => { + sounds.toggle(value); + setAnalyticsEnabled(value); + }, selectedLocale, - setSelectedLocale, + setSelectedLocale: (value) => { + sounds.click(); + setSelectedLocale(value); + }, colorScheme: colorScheme as ColorScheme, setColorScheme, docker, dockerData, runtimeCapabilities, selectedKinds, - setSelectedKinds, + setSelectedKinds: (value) => { + sounds.click(); + setSelectedKinds(value); + }, discoveredIntegrations, selectedIntegrationSourceIds, - setSelectedIntegrationSourceIds, + setSelectedIntegrationSourceIds: (value) => { + sounds.click(); + setSelectedIntegrationSourceIds(value); + }, detectedKinds, drafts, updateDraft, @@ -555,29 +594,35 @@ export const SetupStudio = ({ environment, assistantConfiguration }: OnboardingS } }, selectedAppIds, - setSelectedAppIds, + setSelectedAppIds: (value) => { + sounds.click(); + setSelectedAppIds(value); + }, boardName, setBoardName, selectedBoardId, - setSelectedBoardId, + setSelectedBoardId: (value) => { + sounds.click(); + setSelectedBoardId(value); + }, primaryColor, - setPrimaryColor: (value) => { - setPrimaryColor(value); - setBoardColorsDirty(true); - }, + setPrimaryColor, secondaryColor, - setSecondaryColor: (value) => { - setSecondaryColor(value); - setBoardColorsDirty(true); - }, + setSecondaryColor, itemRadius, setItemRadius, columnCount, setColumnCount, leftSidebar, - setLeftSidebar, + setLeftSidebar: (value) => { + sounds.toggle(value); + setLeftSidebar(value); + }, rightSidebar, - setRightSidebar, + setRightSidebar: (value) => { + sounds.toggle(value); + setRightSidebar(value); + }, applyProgress, applyMessage, appError, @@ -607,10 +652,10 @@ export const SetupStudio = ({ environment, assistantConfiguration }: OnboardingS >
- + @@ -680,12 +725,7 @@ export const SetupStudio = ({ environment, assistantConfiguration }: OnboardingS {t("back")} {activeSection === "review" ? ( - ) : ( @@ -1232,6 +1272,7 @@ const IntegrationEditor = ({ selectSecretOption: (option: number) => void; }) => { const t = useScopedI18n("init.studio.connect"); + const sounds = useOnboardingSounds(); const options = getAllSecretKindOptions(draft.kind); const apiKeyUrl = getIntegrationApiKeyUrl(draft.url, draft.kind); const documentationUrl = getIntegrationDocumentationUrl(draft.kind); @@ -1239,12 +1280,15 @@ const IntegrationEditor = ({ const testConnection = clientApi.onboard.testIntegration.useMutation({ onSuccess(result) { if (result.success) { + sounds.success(); showSuccessNotification({ title: t("testSuccess"), message: t("testSuccessDescription") }); } else { + sounds.error(); showErrorNotification({ title: t("testError"), message: t("testErrorDescription") }); } }, onError() { + sounds.error(); showErrorNotification({ title: t("testError"), message: t("testErrorDescription") }); }, }); @@ -1370,15 +1414,16 @@ const IntegrationEditor = ({ ) } - onClick={() => + onClick={() => { + sounds.click(); testConnection.mutate({ sourceId: draft.sourceId ?? draft.id, name: draft.name, url: normalizeServiceUrl(draft.url) ?? draft.url, kind: draft.kind, secrets: draft.secrets, - }) - } + }); + }} > {t("testConnection")} @@ -1396,6 +1441,9 @@ const BoardBuilder = (props: StudioSectionProps) => { const t = useScopedI18n("init.studio.board"); const previewWidgetKinds = getPreviewWidgetKinds(props); const previewAppCount = getPreviewAppCount(props); + const colorsCustomized = + props.primaryColor.toLowerCase() !== initialPrimaryColor || + props.secondaryColor.toLowerCase() !== initialSecondaryColor; return ( @@ -1426,14 +1474,36 @@ const BoardBuilder = (props: StudioSectionProps) => { onChange={(event) => props.setBoardName(event.currentTarget.value.replace(/[^A-Za-z0-9-_]/g, ""))} withAsterisk /> - - - - + + + + + + {colorsCustomized ? ( + + ) : null} + {t("radius")} @@ -1583,24 +1653,24 @@ const Extensions = (props: StudioSectionProps) => { {t("title")} - + - }> - {t("assistant")} - }> {t("workshop")} + }> + {t("assistant")} + }> {t("mcp")} - - - + + + @@ -1700,11 +1770,14 @@ const WorkshopFeature = ({ title, description }: { title: string; description: s const McpSetup = ({ environment }: { environment: StudioSectionProps["environment"] }) => { const t = useScopedI18n("init.studio.extend.mcpSetup"); const [apiKey, setApiKey] = useState(null); + const sounds = useOnboardingSounds(); const createKey = clientApi.apiKeys.create.useMutation({ onSuccess(data) { + sounds.success(); setApiKey(data.apiKey); }, onError(error) { + sounds.error(); showErrorNotification({ title: t("errorTitle"), message: error.message }); }, }); @@ -1735,7 +1808,15 @@ const McpSetup = ({ environment }: { environment: StudioSectionProps["environmen {apiKey} {({ copied, copy }) => ( - + { + sounds.pop(); + copy(); + }} + aria-label={t("copyKey")} + color={copied ? "green" : undefined} + > {copied ? : } )} @@ -1749,7 +1830,10 @@ const McpSetup = ({ environment }: { environment: StudioSectionProps["environmen variant="light" leftSection={} loading={createKey.isPending} - onClick={() => createKey.mutate()} + onClick={() => { + sounds.click(); + createKey.mutate(); + }} > {t("createKey")} @@ -1769,7 +1853,10 @@ const McpSetup = ({ environment }: { environment: StudioSectionProps["environmen