@@ -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" ? (
- }
- onClick={continueOnboarding}
- >
+
) : (
@@ -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 ? (
+ }
+ onClick={() => {
+ props.setPrimaryColor(initialPrimaryColor);
+ props.setSecondaryColor(initialSecondaryColor);
+ }}
+ w="fit-content"
+ >
+ Reset colors
+
+ ) : 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
diff --git a/packages/onboarding/src/wordmark-colors.spec.ts b/packages/onboarding/src/wordmark-colors.spec.ts
deleted file mode 100644
index 0295ff995..000000000
--- a/packages/onboarding/src/wordmark-colors.spec.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { describe, expect, it } from "vitest";
-
-import { recolorWordmark } from "./wordmark-colors";
-
-describe("recolorWordmark", () => {
- it("recolors red fills while preserving white details", () => {
- const svg = '';
-
- expect(recolorWordmark(svg, "#228BE6", "#15AABF", "#1A1B1E")).toBe(
- '',
- );
- });
-
- it("leaves the source unchanged for incomplete colors", () => {
- const svg = '';
- expect(recolorWordmark(svg, "#228BE6", "")).toBe(svg);
- });
-});
diff --git a/packages/onboarding/src/wordmark-colors.ts b/packages/onboarding/src/wordmark-colors.ts
deleted file mode 100644
index a85248208..000000000
--- a/packages/onboarding/src/wordmark-colors.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-const hexColorPattern = /^#[0-9a-f]{6}$/iu;
-
-export const recolorWordmark = (
- svg: string,
- primaryColor: string,
- secondaryColor: string,
- foregroundColor?: string,
-) => {
- if (!hexColorPattern.test(primaryColor) || !hexColorPattern.test(secondaryColor)) return svg;
-
- return svg.replace(/fill="(#[0-9a-f]{6})"/giu, (fill, color: string) => {
- const red = Number.parseInt(color.slice(1, 3), 16);
- const green = Number.parseInt(color.slice(3, 5), 16);
- const blue = Number.parseInt(color.slice(5, 7), 16);
- const redDifference = red - Math.max(green, blue);
-
- if (foregroundColor && hexColorPattern.test(foregroundColor) && Math.min(red, green, blue) >= 235) {
- return `fill="${foregroundColor}"`;
- }
- if (redDifference < 15) return fill;
- return `fill="${Math.min(green, blue) >= 150 ? secondaryColor : primaryColor}"`;
- });
-};
diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json
index fe42c3858..84d32dfc6 100644
--- a/packages/translation/src/lang/en.json
+++ b/packages/translation/src/lang/en.json
@@ -28,9 +28,8 @@
"review": "Review"
},
"welcome": {
- "title": "Welcome home",
- "version": "Welcome to Homarr v{version}",
- "description": "Let's get you started with homarr",
+ "title": "Make home feel organized.",
+ "description": "Welcome to Homarr v{version}. We’ll guide you through the essentials and help you build your first board.",
"start": "Get started",
"restore": "Come from a backup",
"restoreTitle": "Restore this Homarr",
diff --git a/packages/ui/src/components/board-color-input.tsx b/packages/ui/src/components/board-color-input.tsx
index 018465257..d66e957c9 100644
--- a/packages/ui/src/components/board-color-input.tsx
+++ b/packages/ui/src/components/board-color-input.tsx
@@ -3,8 +3,16 @@
import { ColorInput, useMantineTheme } from "@mantine/core";
import type { ColorInputProps } from "@mantine/core";
-export const BoardColorInput = (props: ColorInputProps) => {
- const theme = useMantineTheme();
+interface BoardColorInputProps extends ColorInputProps {
+ defaultColor?: string;
+}
- return color[6])} />;
+export const BoardColorInput = ({ defaultColor, ...props }: BoardColorInputProps) => {
+ const theme = useMantineTheme();
+ const themeSwatches = Object.values(theme.colors).map((color) => color[6]);
+ const swatches = defaultColor
+ ? [defaultColor, ...themeSwatches.filter((color) => color.toLowerCase() !== defaultColor.toLowerCase())]
+ : themeSwatches;
+
+ return ;
};
diff --git a/packages/ui/src/integration-select-grid/integration-multi-select-grid.tsx b/packages/ui/src/integration-select-grid/integration-multi-select-grid.tsx
index 00aa5f2ac..f6523085b 100644
--- a/packages/ui/src/integration-select-grid/integration-multi-select-grid.tsx
+++ b/packages/ui/src/integration-select-grid/integration-multi-select-grid.tsx
@@ -58,7 +58,6 @@ export const IntegrationMultiSelectGrid = ({
onSearchChange={setSearch}
placeholder={`${t("integration.page.list.search")}...`}
ariaLabel={t("integration.page.list.search")}
- disableScroll={onboarding}
disableAutoFocus={onboarding}
>
{filtered.map((integration) => {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9aa1f5a06..060ed81c8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -156,6 +156,9 @@ catalogs:
'@playwright/test':
specifier: ^1.61.1
version: 1.61.1
+ '@rexa-developer/tiks':
+ specifier: ^0.3.0
+ version: 0.3.0
'@scalar/api-reference-react':
specifier: ^0.9.54
version: 0.9.54
@@ -1114,6 +1117,9 @@ importers:
'@modelcontextprotocol/server':
specifier: 'catalog:'
version: 2.0.0
+ '@rexa-developer/tiks':
+ specifier: 'catalog:'
+ version: 0.3.0(react@19.2.7)(vue@3.5.35(typescript@7.0.2))
'@scalar/api-reference-react':
specifier: 'catalog:'
version: 0.9.54(nprogress@0.2.0)(react@19.2.7)(supports-color@10.2.2)(tailwindcss@4.3.2)(typescript@7.0.2)(zod@4.4.3)
@@ -2444,6 +2450,9 @@ importers:
'@mantine/hooks':
specifier: 'catalog:'
version: 9.4.1(react@19.2.7)
+ '@rexa-developer/tiks':
+ specifier: 'catalog:'
+ version: 0.3.0(react@19.2.7)(vue@3.5.35(typescript@7.0.2))
'@tabler/icons-react':
specifier: 'catalog:'
version: 3.44.0(react@19.2.7)
@@ -7943,6 +7952,17 @@ packages:
'@codemirror/state': ^6.0.0
'@codemirror/view': ^6.0.0
+ '@rexa-developer/tiks@0.3.0':
+ resolution: {integrity: sha512-7tW/lb53Nr8U0dn9ZlWOvDXtsZ7fK3dZN90Xvp/S4AkjruopuP8992+3KhFCrIQ7bXFEsaVFUZsm/+NC3ufA1w==}
+ peerDependencies:
+ react: '>=18.0.0'
+ vue: '>=3.0.0'
+ peerDependenciesMeta:
+ react:
+ optional: true
+ vue:
+ optional: true
+
'@rolldown/binding-android-arm64@1.1.4':
resolution: {integrity: sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -22853,6 +22873,11 @@ snapshots:
'@codemirror/state': 6.6.0
'@codemirror/view': 6.43.0
+ '@rexa-developer/tiks@0.3.0(react@19.2.7)(vue@3.5.35(typescript@7.0.2))':
+ optionalDependencies:
+ react: 19.2.7
+ vue: 3.5.35(typescript@7.0.2)
+
'@rolldown/binding-android-arm64@1.1.4':
optional: true
@@ -24766,7 +24791,7 @@ snapshots:
obug: 2.1.1
std-env: 4.0.0
tinyrainbow: 3.1.0
- vitest: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.14.6(@types/node@24.13.3)(typescript@7.0.2))(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.16))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0))
+ vitest: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.14.6(@types/node@24.13.3)(typescript@7.0.2))(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.16))(terser@5.46.1)(tsx@4.20.4)(yaml@2.9.0))
'@vitest/expect@4.1.10':
dependencies:
@@ -24822,7 +24847,7 @@ snapshots:
sirv: 3.0.2
tinyglobby: 0.2.17
tinyrainbow: 3.1.0
- vitest: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.14.6(@types/node@24.13.3)(typescript@7.0.2))(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.16))(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0))
+ vitest: 4.1.10(@opentelemetry/api@1.9.0)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@29.1.1(@noble/hashes@2.0.1))(msw@2.14.6(@types/node@24.13.3)(typescript@7.0.2))(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.101.0)(sugarss@5.0.0(postcss@8.5.16))(terser@5.46.1)(tsx@4.20.4)(yaml@2.9.0))
'@vitest/utils@4.1.10':
dependencies:
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index a551b83be..665580a1f 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -228,7 +228,8 @@ catalog:
remark-gfm: ^4.0.1
react-shiki: ^0.11.0
canvas-confetti: ^1.9.4
- '@types/canvas-confetti': ^1.9.0
+ "@types/canvas-confetti": ^1.9.0
+ "@rexa-developer/tiks": ^0.3.0
catalogMode: strict
nodeLinker: hoisted