Compare commits

..
Author SHA1 Message Date
ajnart 378696739d style: auto-fix lint and format 2026-06-10 16:19:20 +00:00
Thomas Camlong 57f585416a fix: gracefully skip widgets with unknown kind instead of crashing 2026-06-10 18:14:42 +02:00
homarr-renovate[bot] b3cdab579b chore(deps): update dependency isomorphic-dompurify to ^3.16.0 (#5913)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-10 14:31:02 +00:00
homarr-renovate[bot] 44a0c6ce54 chore(deps): update dependency semver-parser to ^4.2.1 (#5868)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-09 12:47:00 +00:00
homarr-renovate[bot] 7f05e0325d chore(deps): update dependency rollup@>=4.0.0 <4.22.4 to >=4.61.1 (#5906)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-09 09:53:16 +00:00
homarr-renovate[bot] f71b145403 chore(deps): update tiptap monorepo to v3.25.0 (#5898)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-09 05:17:18 +00:00
homarr-renovate[bot] 607fd498f4 chore(deps): update tanstack-query monorepo to ^5.101.0 (#5894)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-09 00:17:40 +00:00
homarr-renovate[bot] 3f2fb54cdf chore(deps): update mantine monorepo to ^9.3.0 (#5890)
Co-authored-by: homarr-renovate[bot] <158783068+homarr-renovate[bot]@users.noreply.github.com>
2026-06-08 22:39:48 +00:00
8 changed files with 655 additions and 1021 deletions

No files matched your search

@@ -1,379 +0,0 @@
"use client";
import type { PropsWithChildren } from "react";
import { useEffect, useState, useSyncExternalStore } from "react";
import {
Button,
Checkbox,
Group,
Indicator,
Menu,
Modal,
Radio,
Stack,
Stepper,
Text,
Textarea,
TextInput,
Title,
} from "@mantine/core";
import { IconMessageCircle } from "@tabler/icons-react";
import { z } from "zod/v4";
import { parseCookies, setClientCookie } from "@homarr/common";
import { surveyAnsweredCookieKey } from "@homarr/definitions";
import { useZodForm } from "@homarr/form";
const surveySchema = z
.object({
features: z.array(z.string()).min(1),
integration: z.string().min(1),
integrationOther: z.string().optional(),
aiImportance: z.string().min(1),
wishlist: z.string().optional(),
frustrations: z.string().optional(),
})
.superRefine((data, ctx) => {
if (data.integration === "Other" && !data.integrationOther?.trim()) {
ctx.addIssue({
code: "custom",
message: "Required",
path: ["integrationOther"],
});
}
});
type SurveyFormValues = z.infer<typeof surveySchema>;
const featureChoices = [
"Export / import config",
"MCP (Model Context Protocol) support",
"New widgets (weather, calendar, RSS, etc.)",
"New integrations",
"Custom widgets (create your own)",
"Automatic mobile layouts / better drag and drop",
] as const;
const integrationChoices = [
"Home automation (Home Assistant, openHAB, Zigbee2MQTT…)",
"Media servers (Plex, Jellyfin, Emby, Navidrome…)",
"Monitoring & observability (Prometheus, Grafana, Uptime Kuma…)",
"Infrastructure (Proxmox, TrueNAS, Portainer…)",
"Cloud & productivity (Nextcloud, Paperless, Vaultwarden…)",
"Other",
] as const;
const aiImportanceChoices = [
"Very important – I'd use it immediately",
"Somewhat important – nice to have",
"Not important – I don't care",
"I'm not sure what this means yet",
] as const;
const requiredSteps = [0, 1, 2] as const;
const integrationStepValidators: Record<string, (values: SurveyFormValues) => boolean> = {
Other: (values) => Boolean(values.integrationOther?.trim()),
...(Object.fromEntries(
integrationChoices.filter((choice) => choice !== "Other").map((choice) => [choice, () => true]),
) as Record<string, () => true>),
};
const stepValidators: Record<number, (values: SurveyFormValues) => boolean> = {
0: (values) => values.features.length >= 1,
1: (values) => integrationStepValidators[values.integration]?.(values) ?? false,
2: (values) => values.aiImportance.length >= 1,
};
const surveyResponse1Getters: Record<string, (values: SurveyFormValues) => string | undefined> = {
Other: (values) => values.integrationOther,
};
function createStore<T>(initialValue: T) {
let value = initialValue;
const listeners = new Set<() => void>();
return {
subscribe(listener: () => void) {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
},
getSnapshot() {
return value;
},
set(nextValue: T) {
value = nextValue;
listeners.forEach((listener) => listener());
},
};
}
const showSurveyStore = createStore(false);
const surveyModalStore = createStore(false);
function useShowSurvey() {
const showSurvey = useSyncExternalStore(
(listener) => showSurveyStore.subscribe(listener),
() => showSurveyStore.getSnapshot(),
() => false,
);
useEffect(() => {
const beforeDeadline = new Date() < new Date("2026-07-01");
const cookies = parseCookies(document.cookie);
const hasAnswered = cookies[surveyAnsweredCookieKey] !== undefined;
if (beforeDeadline && !hasAnswered) {
showSurveyStore.set(true);
}
}, []);
return showSurvey;
}
function useSurveyModalOpen() {
return useSyncExternalStore(
(listener) => surveyModalStore.subscribe(listener),
() => surveyModalStore.getSnapshot(),
() => false,
);
}
function isStepValid(step: number, values: SurveyFormValues) {
const validator = stepValidators[step];
if (!validator) {
return true;
}
return validator(values);
}
function canNavigateToStep(targetStep: number, activeStep: number, values: SurveyFormValues) {
if (targetStep <= activeStep) {
return true;
}
return requiredSteps.filter((step) => step < targetStep).every((step) => isStepValid(step, values));
}
function getSurveyResponse1(values: SurveyFormValues) {
const getter = surveyResponse1Getters[values.integration];
if (getter) {
return getter(values);
}
return values.integration;
}
function optionalResponse(value: string | undefined) {
if (value) {
return value;
}
return undefined;
}
const SurveyIndicator = ({ children }: PropsWithChildren) => {
const showSurvey = useShowSurvey();
return (
<Indicator color="grape" size={15} processing withBorder disabled={!showSurvey}>
{children}
</Indicator>
);
};
const SurveyMenuItem = () => {
const showSurvey = useShowSurvey();
if (!showSurvey) {
return null;
}
return (
<Menu.Item onClick={() => surveyModalStore.set(true)} leftSection={<IconMessageCircle size="1rem" />} color="grape">
Share your feedback
</Menu.Item>
);
};
const SurveyModal = () => {
const showSurvey = useShowSurvey();
const opened = useSurveyModalOpen();
const [active, setActive] = useState(0);
const [isSubmitting, setIsSubmitting] = useState(false);
const close = () => surveyModalStore.set(false);
const form = useZodForm(surveySchema, {
mode: "controlled",
initialValues: {
features: [],
integration: "",
integrationOther: "",
aiImportance: "",
wishlist: "",
frustrations: "",
},
});
const handleSubmit = form.onSubmit(async (values) => {
setIsSubmitting(true);
try {
const response = await fetch("https://hog.homarr.dev/i/v0/e/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
api_key: "phc_pWxeD1hbl4ip02JYReX1Crjkt5DhB3dduigirHMCtFE",
event: "survey sent",
distinct_id: crypto.randomUUID(),
properties: {
$survey_id: "019ea900-2649-0000-5914-8bdfacc01219",
$survey_response: values.features,
$survey_response_1: getSurveyResponse1(values),
$survey_response_2: values.aiImportance,
$survey_response_3: optionalResponse(values.wishlist),
$survey_response_4: optionalResponse(values.frustrations),
},
}),
});
if (!response.ok) {
return;
}
setClientCookie(surveyAnsweredCookieKey, "true", {
expires: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000),
path: "/",
});
showSurveyStore.set(false);
close();
} finally {
setIsSubmitting(false);
}
});
const handleStepClick = (step: number) => {
if (step === active) {
return;
}
if (!canNavigateToStep(step, active, form.values)) {
return;
}
setActive(step);
};
if (!showSurvey) {
return null;
}
return (
<Modal opened={opened} onClose={close} size="xl" title="Homarr – Feature feedback & wishlist" centered>
<Stepper active={active} onStepClick={handleStepClick} orientation="horizontal">
<Stepper.Step label="Features">
<Stack gap="md" mt="md">
<Title order={4}>Which new features would you most like to see in Homarr? (pick all that apply)</Title>
<Checkbox.Group {...form.getInputProps("features")}>
<Stack gap="xs">
{featureChoices.map((choice) => (
<Checkbox key={choice} value={choice} label={choice} />
))}
</Stack>
</Checkbox.Group>
<Group justify="flex-end">
<Button color="grape" disabled={!isStepValid(0, form.values)} onClick={() => setActive(1)}>
Next
</Button>
</Group>
</Stack>
</Stepper.Step>
<Stepper.Step label="Integrations">
<Stack gap="md" mt="md">
<Title order={4}>What type of new integration would benefit you the most?</Title>
<Radio.Group {...form.getInputProps("integration")}>
<Stack gap="xs">
{integrationChoices.map((choice) => (
<Radio key={choice} value={choice} label={choice} />
))}
</Stack>
</Radio.Group>
{form.values.integration === "Other" && (
<TextInput label="Please specify" {...form.getInputProps("integrationOther")} />
)}
<Group justify="flex-end">
<Button color="grape" disabled={!isStepValid(1, form.values)} onClick={() => setActive(2)}>
Next
</Button>
</Group>
</Stack>
</Stepper.Step>
<Stepper.Step label="AI">
<Stack gap="md" mt="md">
<Title order={4}>
How important is AI compatibility to you? (e.g. AI widgets, MCP tools, LLM integrations)
</Title>
<Radio.Group {...form.getInputProps("aiImportance")}>
<Stack gap="xs">
{aiImportanceChoices.map((choice) => (
<Radio key={choice} value={choice} label={choice} />
))}
</Stack>
</Radio.Group>
<Group justify="flex-end">
<Button color="grape" disabled={!isStepValid(2, form.values)} onClick={() => setActive(3)}>
Next
</Button>
</Group>
</Stack>
</Stepper.Step>
<Stepper.Step label="Wishlist">
<Stack gap="md" mt="md">
<Title order={4}>
Anything else you&apos;d like to see in Homarr? Feel free to describe a feature, integration, or
improvement you&apos;re missing.
</Title>
<Text size="sm" c="dimmed">
Optional
</Text>
<Textarea autosize minRows={3} {...form.getInputProps("wishlist")} />
<Group justify="flex-end">
<Button variant="subtle" color="gray" onClick={() => setActive(4)}>
Skip
</Button>
<Button color="grape" onClick={() => setActive(4)}>
Next
</Button>
</Group>
</Stack>
</Stepper.Step>
<Stepper.Step label="Feedback">
<Stack gap="md" mt="md">
<Title order={4}>
What frustrates you most about Homarr today? Any small fix or quality-of-life improvement you wish it
already did?
</Title>
<Text size="sm" c="dimmed">
Optional
</Text>
<Textarea autosize minRows={3} {...form.getInputProps("frustrations")} />
<Group justify="flex-end">
<Button variant="subtle" color="gray" loading={isSubmitting} onClick={handleSubmit}>
Skip & Submit
</Button>
<Button color="grape" loading={isSubmitting} onClick={handleSubmit}>
Submit
</Button>
</Group>
</Stack>
</Stepper.Step>
</Stepper>
</Modal>
);
};
export { SurveyIndicator, SurveyMenuItem, SurveyModal };
@@ -6,7 +6,6 @@ import { auth } from "@homarr/auth/next";
import { CurrentUserAvatar } from "~/components/user-avatar";
import { UserAvatarMenu } from "~/components/user-avatar-menu";
import { SurveyIndicator } from "./survey";
import { UpdateIndicator } from "./update";
export const UserButton = async () => {
@@ -17,11 +16,9 @@ export const UserButton = async () => {
<UserAvatarMenu availableUpdatesPromise={availableUpdatesPromise}>
<UnstyledButton>
<Suspense fallback={<CurrentUserAvatar size="md" />}>
<SurveyIndicator>
<UpdateIndicator availableUpdatesPromise={availableUpdatesPromise} disabled={!isAdmin}>
<CurrentUserAvatar size="md" />
</UpdateIndicator>
</SurveyIndicator>
<UpdateIndicator availableUpdatesPromise={availableUpdatesPromise} disabled={!isAdmin}>
<CurrentUserAvatar size="md" />
</UpdateIndicator>
</Suspense>
</UnstyledButton>
</UserAvatarMenu>
+41 -46
View File
@@ -18,7 +18,6 @@ import { useAuthContext } from "~/app/[locale]/_client-providers/session";
import { CurrentColorSchemeCombobox } from "./color-scheme/current-color-scheme-combobox";
import { CurrentLanguageCombobox } from "./language/current-language-combobox";
import { AvailableUpdatesMenuItem } from "./layout/header/update";
import { SurveyMenuItem, SurveyModal } from "./layout/header/survey";
interface UserAvatarMenuProps {
children: ReactNode;
@@ -52,54 +51,50 @@ export const UserAvatarMenu = ({ children, availableUpdatesPromise }: UserAvatar
}, [logoutUrl, openModal, router]);
return (
<>
<SurveyModal />
{/* We use keepMounted so we can add event listeners to prevent navigating away without saving the board */}
<Menu width={300} withinPortal keepMounted>
<Menu.Dropdown>
<AvailableUpdatesMenuItem availableUpdatesPromise={availableUpdatesPromise} />
<SurveyMenuItem />
<Menu.Item component={Link} href="/boards" leftSection={<IconHome size="1rem" />}>
{t("homeBoard")}
</Menu.Item>
<Menu.Divider />
// We use keepMounted so we can add event listeners to prevent navigating away without saving the board
<Menu width={300} withinPortal keepMounted>
<Menu.Dropdown>
<AvailableUpdatesMenuItem availableUpdatesPromise={availableUpdatesPromise} />
<Menu.Item component={Link} href="/boards" leftSection={<IconHome size="1rem" />}>
{t("homeBoard")}
</Menu.Item>
<Menu.Divider />
<Menu.Item p={0} closeMenuOnClick={false} component="div">
<CurrentColorSchemeCombobox />
</Menu.Item>
<Menu.Item p={0} closeMenuOnClick={false} component="div">
<CurrentLanguageCombobox />
</Menu.Item>
<Menu.Divider />
{Boolean(session.data) && (
<>
<Menu.Item
component={Link}
href={`/manage/users/${session.data?.user.id}/general`}
leftSection={<IconSettings size="1rem" />}
>
{t("preferences")}
</Menu.Item>
<Menu.Item p={0} closeMenuOnClick={false} component="div">
<CurrentColorSchemeCombobox />
</Menu.Item>
<Menu.Item p={0} closeMenuOnClick={false} component="div">
<CurrentLanguageCombobox />
</Menu.Item>
<Menu.Divider />
{Boolean(session.data) && (
<>
<Menu.Item
component={Link}
href={`/manage/users/${session.data?.user.id}/general`}
leftSection={<IconSettings size="1rem" />}
>
{t("preferences")}
</Menu.Item>
<Menu.Item component={Link} href="/manage" leftSection={<IconTool size="1rem" />}>
{t("management")}
</Menu.Item>
</>
)}
<Menu.Divider />
{session.status === "authenticated" ? (
<Menu.Item onClick={handleSignout} leftSection={<IconLogout size="1rem" />} color="red">
{t("logout")}
<Menu.Item component={Link} href="/manage" leftSection={<IconTool size="1rem" />}>
{t("management")}
</Menu.Item>
) : (
<Menu.Item component={Link} href="/auth/login" leftSection={<IconLogin size="1rem" />}>
{t("login")}
</Menu.Item>
)}
</Menu.Dropdown>
<Menu.Target>{children}</Menu.Target>
</Menu>
</>
</>
)}
<Menu.Divider />
{session.status === "authenticated" ? (
<Menu.Item onClick={handleSignout} leftSection={<IconLogout size="1rem" />} color="red">
{t("logout")}
</Menu.Item>
) : (
<Menu.Item component={Link} href="/auth/login" leftSection={<IconLogin size="1rem" />}>
{t("login")}
</Menu.Item>
)}
</Menu.Dropdown>
<Menu.Target>{children}</Menu.Target>
</Menu>
);
};
+1 -1
View File
@@ -89,7 +89,7 @@
"prismjs@<1.30.0": ">=1.30.0",
"proxmox-api>undici": "7.24.6",
"react-is": "^19.2.7",
"rollup@>=4.0.0 <4.22.4": ">=4.61.0",
"rollup@>=4.0.0 <4.22.4": ">=4.61.1",
"sha.js@<=2.4.11": ">=2.4.12",
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.2",
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.2",
+24 -22
View File
@@ -613,7 +613,7 @@ export const boardRouter = createTRPCRouter({
boardId: board.id,
});
const sortedLayouts = board.layouts.sort((layoutA, layoutB) => layoutA.columnCount - layoutB.columnCount);
const sortedLayouts = board.layouts.toSorted((layoutA, layoutB) => layoutA.columnCount - layoutB.columnCount);
// Fallback to biggest if none exists with columnCount bigger than addedLayout.columnCount
const layoutToClone =
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -1215,7 +1215,7 @@ export const boardRouter = createTRPCRouter({
});
return {
inherited: dbGroupPermissions.sort((permissionA, permissionB) => {
inherited: dbGroupPermissions.toSorted((permissionA, permissionB) => {
return permissionA.group.name.localeCompare(permissionB.group.name);
}),
users: userPermissions
@@ -1223,7 +1223,7 @@ export const boardRouter = createTRPCRouter({
user,
permission,
}))
.sort((permissionA, permissionB) => {
.toSorted((permissionA, permissionB) => {
return (permissionA.user.name ?? "").localeCompare(permissionB.user.name ?? "");
}),
groups: dbGroupBoardPermission
@@ -1234,7 +1234,7 @@ export const boardRouter = createTRPCRouter({
},
permission,
}))
.sort((permissionA, permissionB) => {
.toSorted((permissionA, permissionB) => {
return permissionA.group.name.localeCompare(permissionB.group.name);
}),
};
@@ -1586,7 +1586,7 @@ const getFullBoardWithWhereAsync = async (db: Database, where: SQL<unknown>, use
...otherBoardProperties,
layouts: layouts
.map(({ boardId: _, ...layout }) => layout)
.sort((layoutA, layoutB) => layoutA.breakpoint - layoutB.breakpoint),
.toSorted((layoutA, layoutB) => layoutA.breakpoint - layoutB.breakpoint),
sections: sections.map(({ collapseStates, ...section }) =>
parseSection({
...section,
@@ -1604,22 +1604,24 @@ const getFullBoardWithWhereAsync = async (db: Database, where: SQL<unknown>, use
collapsed: collapseStates.at(0)?.collapsed ?? false,
}),
),
items: items.map(({ integrations: itemIntegrations, ...item }) =>
parseItem({
...item,
layouts: item.layouts.map((layout) => ({
xOffset: layout.xOffset,
yOffset: layout.yOffset,
width: layout.width,
height: layout.height,
layoutId: layout.layoutId,
sectionId: layout.sectionId,
})),
integrationIds: itemIntegrations.map((item) => item.integration.id),
advancedOptions: superjson.parse<BoardItemAdvancedOptions>(item.advancedOptions),
options: superjson.parse<Record<string, unknown>>(item.options),
}),
),
items: items
.map(({ integrations: itemIntegrations, ...item }) =>
parseItem({
...item,
layouts: item.layouts.map((layout) => ({
xOffset: layout.xOffset,
yOffset: layout.yOffset,
width: layout.width,
height: layout.height,
layoutId: layout.layoutId,
sectionId: layout.sectionId,
})),
integrationIds: itemIntegrations.map((item) => item.integration.id),
advancedOptions: superjson.parse<BoardItemAdvancedOptions>(item.advancedOptions),
options: superjson.parse<Record<string, unknown>>(item.options),
}),
)
.filter((item) => item !== null),
};
};
@@ -1635,7 +1637,7 @@ const parseItem = (item: unknown) => {
const result = outputItemSchema.safeParse(item);
if (!result.success) {
throw new Error(result.error.message);
return null;
}
return result.data;
};
-1
View File
@@ -1,3 +1,2 @@
export const colorSchemeCookieKey = "homarr.color-scheme";
export const localeCookieKey = "homarr.locale";
export const surveyAnsweredCookieKey = "homarr-answered-survey";
+554 -534
View File
File diff suppressed because it is too large. Load diff
+32 -32
View File
@@ -21,17 +21,17 @@ catalog:
"@immich/sdk": ^2.7.5
"@jellyfin/sdk": ^0.13.0
"@kubernetes/client-node": ^1.4.0
"@mantine/charts": ^9.2.2
"@mantine/colors-generator": ^9.2.2
"@mantine/core": ^9.2.2
"@mantine/dates": ^9.2.2
"@mantine/dropzone": ^9.2.2
"@mantine/form": ^9.2.2
"@mantine/hooks": ^9.2.2
"@mantine/modals": ^9.2.2
"@mantine/notifications": ^9.2.2
"@mantine/spotlight": ^9.2.2
"@mantine/tiptap": ^9.2.2
"@mantine/charts": ^9.3.0
"@mantine/colors-generator": ^9.3.0
"@mantine/core": ^9.3.0
"@mantine/dates": ^9.3.0
"@mantine/dropzone": ^9.3.0
"@mantine/form": ^9.3.0
"@mantine/hooks": ^9.3.0
"@mantine/modals": ^9.3.0
"@mantine/notifications": ^9.3.0
"@mantine/spotlight": ^9.3.0
"@mantine/tiptap": ^9.3.0
"@next/eslint-plugin-next": 16.2.7
"@octokit/auth-app": ^8.2.0
"@paralleldrive/cuid2": ^3.3.0
@@ -44,28 +44,28 @@ catalog:
"@semantic-release/release-notes-generator": ^14.1.1
"@t3-oss/env-nextjs": ^0.13.11
"@tabler/icons-react": ^3.44.0
"@tanstack/react-query": ^5.100.14
"@tanstack/react-query-devtools": ^5.100.14
"@tanstack/react-query-next-experimental": ^5.100.14
"@tanstack/react-query": ^5.101.0
"@tanstack/react-query-devtools": ^5.101.0
"@tanstack/react-query-next-experimental": ^5.101.0
"@testcontainers/mysql": ^12.0.1
"@testcontainers/postgresql": ^12.0.1
"@testcontainers/redis": ^12.0.1
"@tiptap/extension-color": 3.24.0
"@tiptap/extension-highlight": 3.24.0
"@tiptap/extension-image": 3.24.0
"@tiptap/extension-link": ^3.24.0
"@tiptap/extension-placeholder": ^3.24.0
"@tiptap/extension-table": 3.24.0
"@tiptap/extension-table-cell": 3.24.0
"@tiptap/extension-table-header": 3.24.0
"@tiptap/extension-table-row": 3.24.0
"@tiptap/extension-task-item": 3.24.0
"@tiptap/extension-task-list": 3.24.0
"@tiptap/extension-text-align": 3.24.0
"@tiptap/extension-text-style": 3.24.0
"@tiptap/extension-underline": 3.24.0
"@tiptap/react": ^3.24.0
"@tiptap/starter-kit": ^3.24.0
"@tiptap/extension-color": 3.25.0
"@tiptap/extension-highlight": 3.25.0
"@tiptap/extension-image": 3.25.0
"@tiptap/extension-link": ^3.25.0
"@tiptap/extension-placeholder": ^3.25.0
"@tiptap/extension-table": 3.25.0
"@tiptap/extension-table-cell": 3.25.0
"@tiptap/extension-table-header": 3.25.0
"@tiptap/extension-table-row": 3.25.0
"@tiptap/extension-task-item": 3.25.0
"@tiptap/extension-task-list": 3.25.0
"@tiptap/extension-text-align": 3.25.0
"@tiptap/extension-text-style": 3.25.0
"@tiptap/extension-underline": 3.25.0
"@tiptap/react": ^3.25.0
"@tiptap/starter-kit": ^3.25.0
"@trpc/client": ^11.17.0
"@trpc/next": ^11.17.0
"@trpc/react-query": ^11.17.0
@@ -126,7 +126,7 @@ catalog:
glob: ^13.0.6
ical.js: ^2.2.1
ioredis: 5.11.0
isomorphic-dompurify: ^3.15.0
isomorphic-dompurify: ^3.16.0
jotai: ^2.20.0
jsdom: ^29.1.1
json5: ^2.2.3
@@ -155,7 +155,7 @@ catalog:
recharts: ^3.8.1
sass: ^1.100.0
semantic-release: ^25.0.3
semver-parser: ^4.2.0
semver-parser: ^4.2.1
superjson: 2.2.6
svgson: ^5.3.1
swagger-ui-react: ^5.32.6