diff --git a/.deepsource.toml b/.deepsource.toml index edc4a8907..e2f2ffcf8 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -3,9 +3,6 @@ version = 1 [[analyzers]] name = "javascript" - [analyzers.meta] - plugins = ["react"] - environment = ["nodejs"] - -[[transformers]] -name = "prettier" \ No newline at end of file +[analyzers.meta] +plugins = ["react"] +environment = ["nodejs"] diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index a5137601a..973aef4ab 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -33,6 +33,7 @@ body: options: # The below comment is used to insert a new version with on-release.yml #NEXT_VERSION# + - 1.62.0 - 1.61.0 - 1.60.1-beta.1 - 1.60.0 diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index e972a1b0c..40d4ceb08 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -24,6 +24,38 @@ env: TURBO_SCM_HEAD: ${{ github.sha }} jobs: + lint-format-autofix: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: write + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ github.head_ref }} + + - name: Setup + uses: ./tooling/github/setup + + - name: Get changed files + id: changed + run: | + git diff --name-only --diff-filter=ACMR origin/${{ github.base_ref }}...HEAD -- '*.ts' '*.tsx' '*.js' '*.jsx' > /tmp/changed-files + echo "count=$(wc -l < /tmp/changed-files | tr -d '[:space:]')" >> $GITHUB_OUTPUT + + - name: Auto-fix lint + format + if: steps.changed.outputs.count != '0' + run: | + xargs -r -a /tmp/changed-files pnpm exec oxlint --fix || true + xargs -r -a /tmp/changed-files pnpm exec oxfmt --write + + - name: Commit fixes + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: "style: auto-fix lint and format" + lint: runs-on: ubuntu-latest timeout-minutes: 10 @@ -54,7 +86,7 @@ jobs: uses: ./tooling/github/setup - name: Format - run: pnpm turbo format --affected --continue -- --cache --cache-location node_modules/.cache/.prettiercache + run: pnpm turbo format --affected --continue typecheck: runs-on: ubuntu-latest diff --git a/.github/workflows/deployment-docker-image.yml b/.github/workflows/deployment-docker-image.yml index c43094223..98f9357db 100644 --- a/.github/workflows/deployment-docker-image.yml +++ b/.github/workflows/deployment-docker-image.yml @@ -66,7 +66,7 @@ jobs: - uses: actions/setup-node@v6 if: env.SKIP_RELEASE == 'false' with: - node-version: 24.15.0 + node-version: 24.16.0 package-manager-cache: false - run: npm i -g pnpm if: env.SKIP_RELEASE == 'false' diff --git a/.nvmrc b/.nvmrc index 5bf4400f2..b832e4001 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -24.15.0 +24.16.0 diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 000000000..88c14e2f9 --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,7 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "printWidth": 120, + "endOfLine": "lf", + "sortPackageJson": true, + "ignorePatterns": ["**/*.cjs", "**/lang/**", "static-data/**", "CHANGELOG.md"] +} diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 000000000..1dbc54c92 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,37 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["eslint", "typescript", "unicorn", "oxc", "react", "import", "jsx-a11y"], + "categories": { + "correctness": "error", + "suspicious": "warn", + "pedantic": "off", + "perf": "off", + "style": "off", + "restriction": "off", + "nursery": "off" + }, + "rules": { + "eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }], + "typescript/consistent-type-imports": ["warn", { "prefer": "type-imports", "fixStyle": "separate-type-imports" }], + "typescript/no-non-null-assertion": "error", + "import/consistent-type-specifier-style": ["error", "prefer-top-level"], + "unicorn/no-new-array": "warn", + "unicorn/no-useless-spread": "warn", + "unicorn/no-useless-length-check": "warn", + "unicorn/no-useless-fallback-in-spread": "warn", + "jsx-a11y/prefer-tag-over-role": "warn", + "oxc/const-comparisons": "warn", + "react-hooks/exhaustive-deps": "warn", + "react/no-children-prop": "warn", + "jsx-a11y/no-autofocus": "warn", + "jsx-a11y/img-redundant-alt": "warn", + "react/react-in-jsx-scope": "off" + }, + "options": { + "reportUnusedDisableDirectives": "off" + }, + "ignorePatterns": ["**/*.config.js", "**/*.config.mjs", "**/*.cjs", "**/lang/*.d.json.ts", ".next/**"], + "settings": { + "react": { "version": "19" } + } +} diff --git a/.vscode/i18n-ally-custom-framework.yml b/.vscode/i18n-ally-custom-framework.yml index 1ee83bc7e..865d1ae28 100644 --- a/.vscode/i18n-ally-custom-framework.yml +++ b/.vscode/i18n-ally-custom-framework.yml @@ -31,6 +31,5 @@ scopeRangeRegex: "(?:const|let|var)\\s+t\\s*=\\s*(?:await\\s+)?(?:getScopedI18n| # refactorTemplates: # - i18n.get("$1") - # If set to true, only enables this custom framework (will disable all built-in frameworks) -monopoly: true \ No newline at end of file +monopoly: true diff --git a/.vscode/launch.json b/.vscode/launch.json index b24d8b507..f3f0886e0 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -15,9 +15,7 @@ "request": "launch", "command": "pnpm docker:dev", "cwd": "${workspaceFolder}", - "skipFiles": [ - "/**" - ] + "skipFiles": ["/**"] }, { "name": "lint fix", @@ -25,9 +23,7 @@ "request": "launch", "command": "pnpm lint:fix", "cwd": "${workspaceFolder}", - "skipFiles": [ - "/**" - ] + "skipFiles": ["/**"] }, { "name": "format fix", @@ -35,9 +31,7 @@ "request": "launch", "command": "pnpm format:fix", "cwd": "${workspaceFolder}", - "skipFiles": [ - "/**" - ] + "skipFiles": ["/**"] }, { "name": "db push", @@ -69,9 +63,7 @@ "request": "launch", "command": "pnpm test", "cwd": "${workspaceFolder}", - "skipFiles": [ - "/**" - ] + "skipFiles": ["/**"] }, { "name": "test ui", @@ -79,9 +71,7 @@ "request": "launch", "command": "pnpm test:ui", "cwd": "${workspaceFolder}", - "skipFiles": [ - "/**" - ] + "skipFiles": ["/**"] } ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index f018a27b5..aa5d827ff 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,13 +1,6 @@ { - "eslint.workingDirectories": [ - { - "mode": "auto" - } - ], - "eslint.experimental.useFlatConfig": true, "typescript.tsdk": "node_modules\\typescript\\lib", "js/ts.implicitProjectConfig.experimentalDecorators": true, - "prettier.configPath": "./tooling/prettier/index.mjs", "cSpell.words": [ "ajnart", "cqmin", @@ -34,11 +27,7 @@ ], "i18n-ally.dirStructure": "auto", "i18n-ally.displayLanguage": "en", - "i18n-ally.enabledFrameworks": [ - "custom" - ], - "i18n-ally.localesPaths": [ - "packages/translation/src/lang", - ], - "i18n-ally.keystyle": "nested", + "i18n-ally.enabledFrameworks": ["custom"], + "i18n-ally.localesPaths": ["packages/translation/src/lang"], + "i18n-ally.keystyle": "nested" } diff --git a/Dockerfile b/Dockerfile index 51b562f7d..fd852fd9e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:24.15.0-alpine AS base +FROM node:24.16.0-alpine AS base FROM base AS builder RUN apk add --no-cache libc6-compat diff --git a/apps/nextjs/eslint.config.js b/apps/nextjs/eslint.config.js deleted file mode 100644 index 3d593e34e..000000000 --- a/apps/nextjs/eslint.config.js +++ /dev/null @@ -1,30 +0,0 @@ -import baseConfig from "@homarr/eslint-config/base"; -import nextjsConfig from "@homarr/eslint-config/nextjs"; -import reactConfig from "@homarr/eslint-config/react"; - -/** @type {import('typescript-eslint').Config} */ -export default [ - { - ignores: [".next/**"], - }, - ...baseConfig, - ...reactConfig, - ...nextjsConfig, - { - files: ["public/sw.js"], - languageOptions: { - globals: { - self: "readonly", - clients: "readonly", - skipWaiting: "readonly", - importScripts: "readonly", - caches: "readonly", - fetch: "readonly", - }, - }, - rules: { - "@typescript-eslint/no-unsafe-call": "off", - "@typescript-eslint/no-unsafe-member-access": "off", - }, - }, -]; diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json index 409845f4e..b0ed1ea99 100644 --- a/apps/nextjs/package.json +++ b/apps/nextjs/package.json @@ -7,13 +7,12 @@ "build": "pnpm with-env next build", "clean": "git clean -xdf .next .turbo node_modules", "dev": "pnpm with-env next dev", - "format": "prettier --check . --ignore-path ../../.gitignore", - "lint": "eslint", + "format": "oxfmt .", + "lint": "oxlint", "start": "pnpm with-env next start", "typecheck": "tsc --noEmit", "with-env": "dotenv -e ../../.env --" }, - "prettier": "@homarr/prettier-config", "dependencies": { "@dnd-kit/core": "catalog:", "@dnd-kit/modifiers": "catalog:", @@ -92,8 +91,6 @@ "zod": "catalog:" }, "devDependencies": { - "@homarr/eslint-config": "workspace:^0.2.0", - "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", "@types/chroma-js": "catalog:", "@types/node": "catalog:", @@ -102,9 +99,7 @@ "@types/react-dom": "catalog:", "@types/swagger-ui-react": "catalog:", "concurrently": "catalog:", - "eslint": "catalog:", "node-loader": "catalog:", - "prettier": "catalog:", "typescript": "catalog:" } } diff --git a/apps/nextjs/src/app/[locale]/_client-providers/mantine.tsx b/apps/nextjs/src/app/[locale]/_client-providers/mantine.tsx index d81b828f3..508074ead 100644 --- a/apps/nextjs/src/app/[locale]/_client-providers/mantine.tsx +++ b/apps/nextjs/src/app/[locale]/_client-providers/mantine.tsx @@ -2,13 +2,14 @@ import type { PropsWithChildren } from "react"; import type { MantineColorScheme, MantineColorSchemeManager } from "@mantine/core"; -import { createTheme, DirectionProvider, MantineProvider } from "@mantine/core"; +import { DirectionProvider, MantineProvider } from "@mantine/core"; import dayjs from "dayjs"; import { clientApi } from "@homarr/api/client"; import { useSession } from "@homarr/auth/client"; import { parseCookies, setClientCookie } from "@homarr/common"; import { colorSchemeCookieKey } from "@homarr/definitions"; +import { theme } from "@homarr/ui"; export const CustomMantineProvider = ({ children, @@ -17,14 +18,7 @@ export const CustomMantineProvider = ({ const manager = useColorSchemeManager(); return ( - + {children} diff --git a/apps/nextjs/src/app/[locale]/auth/invite/[id]/page.tsx b/apps/nextjs/src/app/[locale]/auth/invite/[id]/page.tsx index cc6535703..dbf6306c3 100644 --- a/apps/nextjs/src/app/[locale]/auth/invite/[id]/page.tsx +++ b/apps/nextjs/src/app/[locale]/auth/invite/[id]/page.tsx @@ -59,7 +59,7 @@ export default async function InviteUsagePage(props: InviteUsagePageProps) { {t("subtitle")} - + diff --git a/apps/nextjs/src/app/[locale]/auth/login/_login-form.tsx b/apps/nextjs/src/app/[locale]/auth/login/_login-form.tsx index 71d249cf5..8d36d1e79 100644 --- a/apps/nextjs/src/app/[locale]/auth/login/_login-form.tsx +++ b/apps/nextjs/src/app/[locale]/auth/login/_login-form.tsx @@ -9,7 +9,7 @@ import { z } from "zod/v4"; import { signIn } from "@homarr/auth/client"; import { revalidatePathActionAsync } from "@homarr/common/client"; -import type { useForm } from "@homarr/form"; +import type { UseFormReturnType } from "@homarr/form"; import { useZodForm } from "@homarr/form"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { useScopedI18n } from "@homarr/translation/client"; @@ -162,7 +162,7 @@ export const LoginForm = ({ providers, oidcClientName, isOidcAutoLoginEnabled, c interface SubmitButtonProps { isPending: boolean; - form: ReturnType FormType>>; + form: UseFormReturnType; provider: "credentials" | "ldap"; } @@ -198,7 +198,7 @@ const PasswordForgottenCollapse = ({ username }: PasswordForgottenCollapseProps) {tForgotPassword("label")} - + {tForgotPassword("description")} diff --git a/apps/nextjs/src/app/[locale]/auth/login/page.tsx b/apps/nextjs/src/app/[locale]/auth/login/page.tsx index 22adf3b87..6cf00b6b3 100644 --- a/apps/nextjs/src/app/[locale]/auth/login/page.tsx +++ b/apps/nextjs/src/app/[locale]/auth/login/page.tsx @@ -37,7 +37,7 @@ export default async function Login(props: LoginProps) { {t("subtitle")} - + >( }), // eslint-disable-next-line no-restricted-syntax page: async ({ params }: { params: Promise }) => { - const session = await auth(); - const integrations = await getIntegrationsWithPermissionsAsync(session); - - const board = await getInitialBoard(await params); + const resolvedParams = await params; const queryClient = getQueryClient(); - // Prefetch item data + const [board, session] = await Promise.all([getInitialBoard(resolvedParams), auth()]); + const itemsMap = board.items.reduce((acc, item) => { const existing = acc.get(item.kind); if (existing) { @@ -58,17 +56,20 @@ export const createBoardContentPage = >( return acc; }, new Map()); - for (const [kind, items] of itemsMap) { - await prefetchForKindAsync(kind, queryClient, items).catch((error) => { - logger.error( - new ErrorWithMetadata( - "Failed to prefetch widget", - { widgetKind: kind, itemCount: items.length }, - { cause: error }, - ), - ); - }); - } + const [integrations] = await Promise.all([ + getIntegrationsWithPermissionsAsync(session), + ...Array.from(itemsMap).map(([kind, items]) => + prefetchForKindAsync(kind, queryClient, items).catch((error) => { + logger.error( + new ErrorWithMetadata( + "Failed to prefetch widget", + { widgetKind: kind, itemCount: items.length }, + { cause: error }, + ), + ); + }), + ), + ]); return ( diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx index f1185430c..3b6ea7a37 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx @@ -109,7 +109,7 @@ const AddMenu = () => { }, [openAppSelectModal, createItem]); return ( - + @@ -183,7 +183,7 @@ const SelectBoardsMenu = () => { const { data: boards = [] } = clientApi.board.getAllBoards.useQuery(); return ( - + diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_theme.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_theme.tsx index 27f3e9152..8fa031ad0 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_theme.tsx +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_theme.tsx @@ -24,6 +24,7 @@ export const BoardMantineProvider = ({ }, primaryColor: "primaryColor", autoContrast: true, + defaultRadius: "sm", fontSizes: { "2xl": rem(24), "3xl": rem(28), diff --git a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_appereance.tsx b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_appereance.tsx index 95b3602e6..4af4af15f 100644 --- a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_appereance.tsx +++ b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_appereance.tsx @@ -83,7 +83,7 @@ export const ColorSettingsContent = ({ board }: Props) => { {showPreview ? t("common.preview.hide") : t("common.preview.show")} - + diff --git a/apps/nextjs/src/app/[locale]/init/_steps/finish/init-finish.tsx b/apps/nextjs/src/app/[locale]/init/_steps/finish/init-finish.tsx index 288b9adae..133581d58 100644 --- a/apps/nextjs/src/app/[locale]/init/_steps/finish/init-finish.tsx +++ b/apps/nextjs/src/app/[locale]/init/_steps/finish/init-finish.tsx @@ -15,7 +15,7 @@ export const InitFinish = async () => { const tFinish = await getScopedI18n("init.step.finish"); return ( - + {tFinish("description")} diff --git a/apps/nextjs/src/app/[locale]/init/_steps/group/init-group.tsx b/apps/nextjs/src/app/[locale]/init/_steps/group/init-group.tsx index 0a91d4ced..94424ef7d 100644 --- a/apps/nextjs/src/app/[locale]/init/_steps/group/init-group.tsx +++ b/apps/nextjs/src/app/[locale]/init/_steps/group/init-group.tsx @@ -34,7 +34,7 @@ export const InitGroup = () => { }; return ( - +
{ const tFileInfo = useScopedI18n("init.step.import.fileInfo"); return ( - + diff --git a/apps/nextjs/src/app/[locale]/init/_steps/import/init-import.tsx b/apps/nextjs/src/app/[locale]/init/_steps/import/init-import.tsx index 8b7124a03..cdb3faf50 100644 --- a/apps/nextjs/src/app/[locale]/init/_steps/import/init-import.tsx +++ b/apps/nextjs/src/app/[locale]/init/_steps/import/init-import.tsx @@ -20,7 +20,7 @@ export const InitImport = () => { if (!file) { return ( - + { diff --git a/apps/nextjs/src/app/[locale]/init/_steps/settings/init-settings.tsx b/apps/nextjs/src/app/[locale]/init/_steps/settings/init-settings.tsx index 3badc014e..cb024fc2f 100644 --- a/apps/nextjs/src/app/[locale]/init/_steps/settings/init-settings.tsx +++ b/apps/nextjs/src/app/[locale]/init/_steps/settings/init-settings.tsx @@ -30,7 +30,7 @@ export const InitSettings = () => { return ( - + {tSection("analytics.title")} @@ -39,7 +39,7 @@ export const InitSettings = () => { - + {tSection("crawlingAndIndexing.title")} diff --git a/apps/nextjs/src/app/[locale]/init/_steps/start/init-start.tsx b/apps/nextjs/src/app/[locale]/init/_steps/start/init-start.tsx index b5005f8b4..da858f1f4 100644 --- a/apps/nextjs/src/app/[locale]/init/_steps/start/init-start.tsx +++ b/apps/nextjs/src/app/[locale]/init/_steps/start/init-start.tsx @@ -10,7 +10,7 @@ export const InitStart = async () => { const tStart = await getScopedI18n("init.step.start"); return ( - + {tStart("description")} diff --git a/apps/nextjs/src/app/[locale]/init/_steps/user/init-user.tsx b/apps/nextjs/src/app/[locale]/init/_steps/user/init-user.tsx index 0f52de3dc..fa0ed777d 100644 --- a/apps/nextjs/src/app/[locale]/init/_steps/user/init-user.tsx +++ b/apps/nextjs/src/app/[locale]/init/_steps/user/init-user.tsx @@ -4,7 +4,7 @@ import { InitUserForm } from "./init-user-form"; export const InitUser = () => { return ( - + ); diff --git a/apps/nextjs/src/app/[locale]/layout.tsx b/apps/nextjs/src/app/[locale]/layout.tsx index 5449e1252..e88f1965b 100644 --- a/apps/nextjs/src/app/[locale]/layout.tsx +++ b/apps/nextjs/src/app/[locale]/layout.tsx @@ -44,13 +44,13 @@ const fontSans = Inter({ export const generateMetadata = async (): Promise => ({ title: "Homarr", description: - "Simplify the management of your server with Homarr - a sleek, modern dashboard that puts all of your apps and services at your fingertips.", + "A self-hosted dashboard for the *arr stack and your entire homelab. Integrates with 50+ services, real-time widgets, no config files.", openGraph: { title: "Homarr Dashboard", description: - "Simplify the management of your server with Homarr - a sleek, modern dashboard that puts all of your apps and services at your fingertips.", + "A self-hosted dashboard for the *arr stack and your entire homelab. Integrates with 50+ services, real-time widgets, no config files.", url: "https://homarr.dev", - siteName: "Homarr Documentation", + siteName: "Homarr", }, icons: { icon: "/logo/logo.png", @@ -142,7 +142,7 @@ export default async function Layout(props: { - + {props.children} diff --git a/apps/nextjs/src/app/[locale]/manage/_components/hero-banner.tsx b/apps/nextjs/src/app/[locale]/manage/_components/hero-banner.tsx index b694ee81d..66c9e0398 100644 --- a/apps/nextjs/src/app/[locale]/manage/_components/hero-banner.tsx +++ b/apps/nextjs/src/app/[locale]/manage/_components/hero-banner.tsx @@ -45,7 +45,6 @@ export const HeroBanner = async () => { { - - Homarr - + Homarr {t("version", { version })} @@ -195,9 +193,9 @@ export default async function AboutPage() { href={`https://www.npmjs.com/package/${name}`} target="_blank" > - + - + diff --git a/apps/nextjs/src/app/[locale]/manage/apps/page.tsx b/apps/nextjs/src/app/[locale]/manage/apps/page.tsx index 27ab01fb5..1093524e7 100644 --- a/apps/nextjs/src/app/[locale]/manage/apps/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/apps/page.tsx @@ -1,6 +1,6 @@ import { Fragment } from "react"; import { redirect } from "next/navigation"; -import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text, Title } from "@mantine/core"; +import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text } from "@mantine/core"; import { IconBox, IconPencil } from "@tabler/icons-react"; import { z } from "zod/v4"; @@ -11,9 +11,8 @@ import type { inferSearchParamsFromSchema } from "@homarr/common/types"; import { getI18n, getScopedI18n } from "@homarr/translation/server"; import { Link, SearchInput, TablePagination } from "@homarr/ui"; -import { ManageContainer } from "~/components/manage/manage-container"; +import { ManagePageLayout } from "~/components/manage/manage-page-layout"; import { MobileAffixButton } from "~/components/manage/mobile-affix-button"; -import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb"; import { NoResults } from "~/components/no-results"; import { AppDeleteButton } from "./_app-delete-button"; @@ -39,34 +38,31 @@ export default async function AppsPage(props: AppsPageProps) { const { items: apps, totalCount } = await api.app.getPaginated(searchParams); const t = await getScopedI18n("app"); - return ( - - - - {t("page.list.title")} - - - {session.user.permissions.includes("app-create") && ( - - {t("page.create.title")} - - )} - - {apps.length === 0 && } - {apps.length > 0 && ( - - {apps.map((app) => ( - - ))} - - )} + const canCreate = session.user.permissions.includes("app-create"); - {/* Added margin to not hide pagination behind affix-button */} - - - - - + return ( + + {t("page.create.title")} + + ) : undefined + } + toolbar={} + footer={} + floatingPrimaryAction={canCreate} + > + {apps.length === 0 && } + {apps.length > 0 && ( + + {apps.map((app) => ( + + ))} + + )} + ); } @@ -79,7 +75,7 @@ const AppCard = async ({ app }: AppCardProps) => { const session = await auth(); return ( - + { const t = useI18n(); const { openModal: openAddModal } = useModalAction(AddBoardModal); const { openModal: openImportModal } = useModalAction(ImportBoardModal); - const buttonGroupContent = ( - <> - - - - - - - }> - {t("board.action.oldImport.label")} - - - - - ); - return ( - <> - {buttonGroupContent} - - {buttonGroupContent} - - + + + + + + + + + }> + {t("board.action.oldImport.label")} + + + + + ); }; diff --git a/apps/nextjs/src/app/[locale]/manage/boards/page.tsx b/apps/nextjs/src/app/[locale]/manage/boards/page.tsx index 9c0a28344..45bbcd2b5 100644 --- a/apps/nextjs/src/app/[locale]/manage/boards/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/boards/page.tsx @@ -9,12 +9,17 @@ import { Group, Menu, MenuTarget, - Stack, Text, - Title, Tooltip, } from "@mantine/core"; -import { IconDeviceMobile, IconDotsVertical, IconHomeFilled, IconLock, IconWorld } from "@tabler/icons-react"; +import { + IconDeviceMobile, + IconDotsVertical, + IconHomeFilled, + IconLayoutDashboard, + IconLock, + IconWorld, +} from "@tabler/icons-react"; import type { RouterOutputs } from "@homarr/api"; import { api } from "@homarr/api/server"; @@ -23,8 +28,8 @@ import { getScopedI18n } from "@homarr/translation/server"; import { Link, UserAvatar } from "@homarr/ui"; import { getBoardPermissionsAsync } from "~/components/board/permissions/server"; -import { ManageContainer } from "~/components/manage/manage-container"; -import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb"; +import { ManagePageLayout } from "~/components/manage/manage-page-layout"; +import { NoResults } from "~/components/no-results"; import { BoardCardMenuDropdown } from "./_components/board-card-menu-dropdown"; import { CreateBoardButton } from "./_components/create-board-button"; @@ -35,23 +40,22 @@ export default async function ManageBoardsPage() { const canCreateBoards = session?.user.permissions.includes("board-create"); return ( - - - - - {t("title")} - {canCreateBoards && } - - - + : undefined} + floatingPrimaryAction={canCreateBoards} + > + {boards.length === 0 && } + {boards.length > 0 && ( + {boards.map((board) => ( ))} - - + )} + ); } @@ -66,16 +70,14 @@ const BoardCard = async ({ board }: BoardCardProps) => { const VisibilityIcon = board.isPublic ? IconWorld : IconLock; return ( - + - - {board.name} - + {board.name} @@ -123,6 +125,7 @@ const BoardCard = async ({ board }: BoardCardProps) => { style={{ borderTop: "none", borderBottom: "none", borderRight: "none", borderRadius: 0 }} variant="default" size="lg" + aria-label={t("action.settings.label")} > diff --git a/apps/nextjs/src/app/[locale]/manage/integrations/_components/secrets/integration-secret-card.tsx b/apps/nextjs/src/app/[locale]/manage/integrations/_components/secrets/integration-secret-card.tsx index 06c4baff3..0bfe34ed0 100644 --- a/apps/nextjs/src/app/[locale]/manage/integrations/_components/secrets/integration-secret-card.tsx +++ b/apps/nextjs/src/app/[locale]/manage/integrations/_components/secrets/integration-secret-card.tsx @@ -33,7 +33,7 @@ export const SecretCard = ({ secret, children, onCancel }: SecretCardProps) => { const KindIcon = integrationSecretIcons[secret.kind]; return ( - + @@ -58,7 +58,12 @@ export const SecretCard = ({ secret, children, onCancel }: SecretCardProps) => { )} {isPublic ? ( - + ) : null} @@ -79,7 +84,7 @@ export const SecretCard = ({ secret, children, onCancel }: SecretCardProps) => { - {children} + {children} ); diff --git a/apps/nextjs/src/app/[locale]/manage/integrations/_components/secrets/integration-secret-segmented-control.tsx b/apps/nextjs/src/app/[locale]/manage/integrations/_components/secrets/integration-secret-segmented-control.tsx index e524b3870..9272e47c2 100644 --- a/apps/nextjs/src/app/[locale]/manage/integrations/_components/secrets/integration-secret-segmented-control.tsx +++ b/apps/nextjs/src/app/[locale]/manage/integrations/_components/secrets/integration-secret-segmented-control.tsx @@ -12,7 +12,7 @@ interface FormType { interface SecretKindsSegmentedControlProps { defaultKinds?: IntegrationSecretKind[]; secretKinds: IntegrationSecretKind[][]; - form: UseFormReturnType TFormType>; + form: UseFormReturnType; } export const SecretKindsSegmentedControl = ({ diff --git a/apps/nextjs/src/app/[locale]/manage/integrations/_components/test-connection/integration-test-connection-error.tsx b/apps/nextjs/src/app/[locale]/manage/integrations/_components/test-connection/integration-test-connection-error.tsx index cdaa70d93..a50625106 100644 --- a/apps/nextjs/src/app/[locale]/manage/integrations/_components/test-connection/integration-test-connection-error.tsx +++ b/apps/nextjs/src/app/[locale]/manage/integrations/_components/test-connection/integration-test-connection-error.tsx @@ -18,7 +18,7 @@ export const IntegrationTestConnectionError = ({ error, url }: IntegrationTestCo const causeArray = useMemo(() => toCauseArray(error.cause), [error.cause]); return ( - + diff --git a/apps/nextjs/src/app/[locale]/manage/integrations/_components/test-connection/test-connection-certificate.tsx b/apps/nextjs/src/app/[locale]/manage/integrations/_components/test-connection/test-connection-certificate.tsx index 9f7b00530..6ebbdbb6d 100644 --- a/apps/nextjs/src/app/[locale]/manage/integrations/_components/test-connection/test-connection-certificate.tsx +++ b/apps/nextjs/src/app/[locale]/manage/integrations/_components/test-connection/test-connection-certificate.tsx @@ -216,7 +216,7 @@ export const CertificateDetailsCard = ({ certificate }: CertificateDetailsProps) const tCertificateField = useScopedI18n("certificate.field"); return ( - + {tDetails("title")} diff --git a/apps/nextjs/src/app/[locale]/manage/integrations/new/_integration-new-form.tsx b/apps/nextjs/src/app/[locale]/manage/integrations/new/_integration-new-form.tsx index 016b9336c..b7fbf3291 100644 --- a/apps/nextjs/src/app/[locale]/manage/integrations/new/_integration-new-form.tsx +++ b/apps/nextjs/src/app/[locale]/manage/integrations/new/_integration-new-form.tsx @@ -226,7 +226,7 @@ const AppForm = ({ form, canCreateApps }: { form: UseFormReturnType; c description={t("integration.field.createApp.description")} /> - +
{canCreateApps && ( diff --git a/apps/nextjs/src/app/[locale]/manage/integrations/page.module.css b/apps/nextjs/src/app/[locale]/manage/integrations/page.module.css index 34001e53b..551deb5ab 100644 --- a/apps/nextjs/src/app/[locale]/manage/integrations/page.module.css +++ b/apps/nextjs/src/app/[locale]/manage/integrations/page.module.css @@ -8,7 +8,7 @@ border: 1px solid transparent; position: relative; z-index: 0; - transition: transform 150ms ease; + transition: background-color 150ms ease; overflow: hidden; &[data-first="true"] { @@ -20,11 +20,9 @@ } &[data-active] { - transform: scale(1.01); z-index: 1; background-color: var(--mantine-color-body); border-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4)); - box-shadow: var(--mantine-shadow-md); border-radius: var(--mantine-radius-lg); } } diff --git a/apps/nextjs/src/app/[locale]/manage/integrations/page.tsx b/apps/nextjs/src/app/[locale]/manage/integrations/page.tsx index 1fe3a83e2..994d63b3c 100644 --- a/apps/nextjs/src/app/[locale]/manage/integrations/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/integrations/page.tsx @@ -7,9 +7,7 @@ import { AccordionPanel, ActionIcon, ActionIconGroup, - Affix, Anchor, - Box, Button, Divider, Group, @@ -24,9 +22,8 @@ import { TableThead, TableTr, Text, - Title, } from "@mantine/core"; -import { IconChevronDown, IconChevronUp, IconPencil, IconPlugX } from "@tabler/icons-react"; +import { IconChevronDown, IconPencil, IconPlugX } from "@tabler/icons-react"; import type { RouterOutputs } from "@homarr/api"; import { api } from "@homarr/api/server"; @@ -37,8 +34,8 @@ import { getIntegrationName } from "@homarr/definitions"; import { getScopedI18n } from "@homarr/translation/server"; import { CountBadge, IntegrationAvatar, Link } from "@homarr/ui"; -import { ManageContainer } from "~/components/manage/manage-container"; -import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb"; +import { ManageMobilePrimaryAction } from "~/components/manage/manage-mobile-primary-action"; +import { ManagePageLayout } from "~/components/manage/manage-page-layout"; import { NoResults } from "~/components/no-results"; import { env } from "~/env"; import { ActiveTabAccordion } from "../../../../components/active-tab-accordion"; @@ -66,38 +63,23 @@ export default async function IntegrationsPage(props: IntegrationsPageProps) { const canCreateIntegrations = session.user.permissions.includes("integration-create"); return ( - - - - - {t("page.list.title")} - - {canCreateIntegrations && ( - <> - - - - - - - - - - - - - - - - - - - )} - - - - - + + + + + + + + ) : undefined + } + floatingPrimaryAction={canCreateIntegrations} + > + + ); } diff --git a/apps/nextjs/src/app/[locale]/manage/medias/page.tsx b/apps/nextjs/src/app/[locale]/manage/medias/page.tsx index b14ceb531..51d4a2caf 100644 --- a/apps/nextjs/src/app/[locale]/manage/medias/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/medias/page.tsx @@ -4,17 +4,15 @@ import { Anchor, Group, Image, - Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, - Title, Tooltip, } from "@mantine/core"; -import { IconExternalLink } from "@tabler/icons-react"; +import { IconExternalLink, IconPhoto } from "@tabler/icons-react"; import { z } from "zod/v4"; import type { RouterOutputs } from "@homarr/api"; @@ -26,8 +24,9 @@ import { createLocalImageUrl } from "@homarr/icons/local"; import { getI18n } from "@homarr/translation/server"; import { Link, SearchInput, TablePagination, UserAvatar } from "@homarr/ui"; -import { ManageContainer } from "~/components/manage/manage-container"; -import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb"; +import { ManageMobilePrimaryAction } from "~/components/manage/manage-mobile-primary-action"; +import { ManagePageLayout } from "~/components/manage/manage-page-layout"; +import { NoResults } from "~/components/no-results"; import { CopyMedia } from "./_actions/copy-media"; import { DeleteMedia } from "./_actions/delete-media"; import { IncludeFromAllUsersSwitch } from "./_actions/show-all"; @@ -48,7 +47,7 @@ interface MediaListPageProps { searchParams: Promise>; } -export default async function GroupsListPage(props: MediaListPageProps) { +export default async function MediaListPage(props: MediaListPageProps) { const session = await auth(); if (!session) { @@ -58,22 +57,31 @@ export default async function GroupsListPage(props: MediaListPageProps) { const t = await getI18n(); const searchParams = searchParamsSchema.parse(await props.searchParams); const { items: medias, totalCount } = await api.media.getPaginated(searchParams); + const canUpload = session.user.permissions.includes("media-upload"); return ( - - - - {t("media.plural")} - - - - {session.user.permissions.includes("media-view-all") && ( - - )} - - - {session.user.permissions.includes("media-upload") && } + + + + ) : undefined + } + toolbar={ + + + {session.user.permissions.includes("media-view-all") && ( + + )} + } + footer={} + floatingPrimaryAction={canUpload} + > + {medias.length === 0 && } + {medias.length > 0 && ( @@ -90,13 +98,8 @@ export default async function GroupsListPage(props: MediaListPageProps) { ))}
- - {/* Added margin to not hide pagination behind affix-button */} - - - -
-
+ )} + ); } diff --git a/apps/nextjs/src/app/[locale]/manage/not-found.tsx b/apps/nextjs/src/app/[locale]/manage/not-found.tsx index 8579e45c4..f6931af9f 100644 --- a/apps/nextjs/src/app/[locale]/manage/not-found.tsx +++ b/apps/nextjs/src/app/[locale]/manage/not-found.tsx @@ -7,9 +7,7 @@ export default async function NotFound() { return (
- - {t("title")} - + {t("title")} {t("text")}
diff --git a/apps/nextjs/src/app/[locale]/manage/page.tsx b/apps/nextjs/src/app/[locale]/manage/page.tsx index 256eaddb5..c7a3a1813 100644 --- a/apps/nextjs/src/app/[locale]/manage/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/page.tsx @@ -29,20 +29,20 @@ export default async function ManagementPage() { {statistics.map((statistic) => ( - + - + {statistic.count} - + {t(`statisticLabel.${statistic.subtitleKey}`)} - {t(`statistic.${statistic.titleKey}`)} + {t(`statistic.${statistic.titleKey}`)} - + ))} diff --git a/apps/nextjs/src/app/[locale]/manage/search-engines/page.tsx b/apps/nextjs/src/app/[locale]/manage/search-engines/page.tsx index ce2500d02..fd4066d64 100644 --- a/apps/nextjs/src/app/[locale]/manage/search-engines/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/search-engines/page.tsx @@ -1,5 +1,5 @@ import { redirect } from "next/navigation"; -import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text, Title } from "@mantine/core"; +import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text } from "@mantine/core"; import { IconPencil, IconSearch } from "@tabler/icons-react"; import { z } from "zod/v4"; @@ -10,9 +10,8 @@ import type { inferSearchParamsFromSchema } from "@homarr/common/types"; import { getI18n, getScopedI18n } from "@homarr/translation/server"; import { Link, SearchInput, TablePagination } from "@homarr/ui"; -import { ManageContainer } from "~/components/manage/manage-container"; +import { ManagePageLayout } from "~/components/manage/manage-page-layout"; import { MobileAffixButton } from "~/components/manage/mobile-affix-button"; -import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb"; import { NoResults } from "~/components/no-results"; import { SearchEngineDeleteButton } from "./_search-engine-delete-button"; @@ -38,34 +37,31 @@ export default async function SearchEnginesPage(props: SearchEnginesPageProps) { const tEngine = await getScopedI18n("search.engine"); - return ( - - - - {tEngine("page.list.title")} - - - {session.user.permissions.includes("search-engine-create") && ( - - {tEngine("page.create.title")} - - )} - - {searchEngines.length === 0 && } - {searchEngines.length > 0 && ( - - {searchEngines.map((searchEngine) => ( - - ))} - - )} + const canCreate = session.user.permissions.includes("search-engine-create"); - {/* Added margin to not hide pagination behind affix-button */} - - - - - + return ( + + {tEngine("page.create.title")} + + ) : undefined + } + toolbar={} + footer={} + floatingPrimaryAction={canCreate} + > + {searchEngines.length === 0 && } + {searchEngines.length > 0 && ( + + {searchEngines.map((searchEngine) => ( + + ))} + + )} + ); } @@ -78,7 +74,7 @@ const SearchEngineCard = async ({ searchEngine }: SearchEngineCardProps) => { const session = await auth(); return ( - + { <> {t("title")} - - + + diff --git a/apps/nextjs/src/app/[locale]/manage/settings/_components/crawling-and-indexing.settings.tsx b/apps/nextjs/src/app/[locale]/manage/settings/_components/crawling-and-indexing.settings.tsx index b99f298ee..29206a473 100644 --- a/apps/nextjs/src/app/[locale]/manage/settings/_components/crawling-and-indexing.settings.tsx +++ b/apps/nextjs/src/app/[locale]/manage/settings/_components/crawling-and-indexing.settings.tsx @@ -41,11 +41,11 @@ export const CrawlingAndIndexingSettings = ({ initialData }: CrawlingAndIndexing <> {t("title")} - + {t("warning")} - + diff --git a/apps/nextjs/src/app/[locale]/manage/settings/_components/setting-switch.tsx b/apps/nextjs/src/app/[locale]/manage/settings/_components/setting-switch.tsx index 293a8fa96..adec086ad 100644 --- a/apps/nextjs/src/app/[locale]/manage/settings/_components/setting-switch.tsx +++ b/apps/nextjs/src/app/[locale]/manage/settings/_components/setting-switch.tsx @@ -15,7 +15,7 @@ export const SwitchSetting = >({ disabled, }: { form: Modify< - UseFormReturnType TFormValue>, + UseFormReturnType, { setFieldValue: (key: keyof TFormValue, value: (previous: boolean) => boolean) => void; } diff --git a/apps/nextjs/src/app/[locale]/manage/tools/certificates/page.tsx b/apps/nextjs/src/app/[locale]/manage/tools/certificates/page.tsx index cf1730736..422b9f858 100644 --- a/apps/nextjs/src/app/[locale]/manage/tools/certificates/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/tools/certificates/page.tsx @@ -79,7 +79,7 @@ export default async function CertificatesPage({ params }: CertificatesPageProps {x509Certificates.map((cert) => ( - + {cert.isError ? ( - @@ -253,7 +253,6 @@ const ContainerActionBarButton = (props: ContainerActionBarButtonProps) => { onClick={handleClickAsync} loading={isPending} variant="light" - radius="md" > {t(`${props.action}.label`)} diff --git a/apps/nextjs/src/app/[locale]/manage/tools/kubernetes/cluster-dashboard/header-card/header-card.module.css b/apps/nextjs/src/app/[locale]/manage/tools/kubernetes/cluster-dashboard/header-card/header-card.module.css index b439e7f4b..dbc035932 100644 --- a/apps/nextjs/src/app/[locale]/manage/tools/kubernetes/cluster-dashboard/header-card/header-card.module.css +++ b/apps/nextjs/src/app/[locale]/manage/tools/kubernetes/cluster-dashboard/header-card/header-card.module.css @@ -4,28 +4,16 @@ overflow: hidden; padding: var(--mantine-spacing-xs); padding-left: calc(var(--mantine-spacing-xs) * 2); - - @mixin hover { - box-shadow: var(--mantine-shadow-md); - } - - &::before { - content: ""; - position: absolute; - top: 0; - bottom: 0; - left: 0; - width: 6px; - background-image: linear-gradient(0, var(--mantine-color-blue-4), var(--mantine-color-blue-9)); - } + border-left: 1px solid var(--mantine-color-blue-6); + background-color: light-dark( + var(--mantine-color-blue-0), + color-mix(in oklch, var(--mantine-color-blue-9), transparent 85%) + ); @mixin rtl { padding-left: var(--mantine-spacing-xs); padding-right: calc(var(--mantine-spacing-xs) * 2); - - &::before { - left: unset; - right: 0; - } + border-left: none; + border-right: 1px solid var(--mantine-color-blue-6); } } diff --git a/apps/nextjs/src/app/[locale]/manage/tools/kubernetes/cluster-dashboard/header-card/header-card.tsx b/apps/nextjs/src/app/[locale]/manage/tools/kubernetes/cluster-dashboard/header-card/header-card.tsx index a8704e1b5..37ed66fe2 100644 --- a/apps/nextjs/src/app/[locale]/manage/tools/kubernetes/cluster-dashboard/header-card/header-card.tsx +++ b/apps/nextjs/src/app/[locale]/manage/tools/kubernetes/cluster-dashboard/header-card/header-card.tsx @@ -23,7 +23,6 @@ export function HeaderCard(props: HeaderCardProps) { - + + diff --git a/apps/nextjs/src/app/[locale]/manage/tools/kubernetes/cluster-dashboard/resource-tile/resource-tile.tsx b/apps/nextjs/src/app/[locale]/manage/tools/kubernetes/cluster-dashboard/resource-tile/resource-tile.tsx index d2405fcf7..17c922c90 100644 --- a/apps/nextjs/src/app/[locale]/manage/tools/kubernetes/cluster-dashboard/resource-tile/resource-tile.tsx +++ b/apps/nextjs/src/app/[locale]/manage/tools/kubernetes/cluster-dashboard/resource-tile/resource-tile.tsx @@ -17,12 +17,7 @@ interface ResourceTileProps { export function ResourceTile(props: ResourceTileProps) { const t = useI18n(); return ( - + {props.label} diff --git a/apps/nextjs/src/app/[locale]/manage/tools/logs/level-selection.tsx b/apps/nextjs/src/app/[locale]/manage/tools/logs/level-selection.tsx index 71846ca02..243521471 100644 --- a/apps/nextjs/src/app/[locale]/manage/tools/logs/level-selection.tsx +++ b/apps/nextjs/src/app/[locale]/manage/tools/logs/level-selection.tsx @@ -2,12 +2,14 @@ import { Select } from "@mantine/core"; -import type { LogLevel } from "@homarr/core/infrastructure/logs/constants"; import { logLevelConfiguration, logLevels } from "@homarr/core/infrastructure/logs/constants"; +import type { LogLevel } from "@homarr/core/infrastructure/logs/constants"; import { useI18n } from "@homarr/translation/client"; import { useLogContext } from "./log-context"; +const levelByValue = Object.fromEntries(logLevels.map((logLevel) => [logLevel, logLevel])) as Record; + export const LogLevelSelection = () => { const { level, setLevel } = useLogContext(); const t = useI18n(); @@ -19,7 +21,12 @@ export const LogLevelSelection = () => { label: `${logLevelConfiguration[level].prefix} ${t(`log.level.option.${level}`)}`, }))} value={level} - onChange={(value) => setLevel(value as LogLevel)} + onChange={(value) => { + const nextLevel = value && levelByValue[value]; + if (nextLevel) { + setLevel(nextLevel); + } + }} checkIconPosition="right" /> ); diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_change-home-board.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_change-home-board.tsx deleted file mode 100644 index bdad9cfea..000000000 --- a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_change-home-board.tsx +++ /dev/null @@ -1,85 +0,0 @@ -"use client"; - -import { Button, Group, Stack } from "@mantine/core"; -import type { z } from "zod/v4"; - -import type { RouterOutputs } from "@homarr/api"; -import { clientApi } from "@homarr/api/client"; -import { revalidatePathActionAsync } from "@homarr/common/client"; -import { useZodForm } from "@homarr/form"; -import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; -import { useI18n } from "@homarr/translation/client"; -import { userChangeHomeBoardsSchema } from "@homarr/validation/user"; - -import type { Board } from "~/app/[locale]/boards/_types"; -import { BoardSelect } from "~/components/board/board-select"; - -interface ChangeHomeBoardFormProps { - user: RouterOutputs["user"]["getById"]; - boardsData: Pick[]; -} - -export const ChangeHomeBoardForm = ({ user, boardsData }: ChangeHomeBoardFormProps) => { - const t = useI18n(); - const { mutate, isPending } = clientApi.user.changeHomeBoards.useMutation({ - async onSettled() { - await revalidatePathActionAsync(`/manage/users/${user.id}`); - }, - onSuccess(_, variables) { - form.setInitialValues({ - homeBoardId: variables.homeBoardId, - mobileHomeBoardId: variables.mobileHomeBoardId, - }); - showSuccessNotification({ - message: t("user.action.changeHomeBoard.notification.success.message"), - }); - }, - onError() { - showErrorNotification({ - message: t("user.action.changeHomeBoard.notification.error.message"), - }); - }, - }); - const form = useZodForm(userChangeHomeBoardsSchema, { - initialValues: { - homeBoardId: user.homeBoardId, - mobileHomeBoardId: user.mobileHomeBoardId, - }, - }); - - const handleSubmit = (values: FormType) => { - mutate({ - userId: user.id, - ...values, - }); - }; - - return ( - - - - - - - - - - - ); -}; - -type FormType = z.infer; diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_change-search-preferences.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_change-search-preferences.tsx deleted file mode 100644 index afae60dc1..000000000 --- a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_change-search-preferences.tsx +++ /dev/null @@ -1,84 +0,0 @@ -"use client"; - -import { Button, Group, Select, Stack, Switch } from "@mantine/core"; -import type { z } from "zod/v4"; - -import type { RouterOutputs } from "@homarr/api"; -import { clientApi } from "@homarr/api/client"; -import { revalidatePathActionAsync } from "@homarr/common/client"; -import { useZodForm } from "@homarr/form"; -import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; -import { useI18n } from "@homarr/translation/client"; -import { userChangeSearchPreferencesSchema } from "@homarr/validation/user"; - -interface ChangeSearchPreferencesFormProps { - user: RouterOutputs["user"]["getById"]; - searchEnginesData: { value: string; label: string }[]; -} - -export const ChangeSearchPreferencesForm = ({ user, searchEnginesData }: ChangeSearchPreferencesFormProps) => { - const t = useI18n(); - const { mutate, isPending } = clientApi.user.changeSearchPreferences.useMutation({ - async onSettled() { - await revalidatePathActionAsync(`/manage/users/${user.id}`); - }, - onSuccess(_, variables) { - form.setInitialValues({ - defaultSearchEngineId: variables.defaultSearchEngineId, - openInNewTab: variables.openInNewTab, - ddgBangsEnabled: variables.ddgBangsEnabled, - }); - showSuccessNotification({ - message: t("user.action.changeSearchPreferences.notification.success.message"), - }); - }, - onError() { - showErrorNotification({ - message: t("user.action.changeSearchPreferences.notification.error.message"), - }); - }, - }); - const form = useZodForm(userChangeSearchPreferencesSchema, { - initialValues: { - defaultSearchEngineId: user.defaultSearchEngineId, - openInNewTab: user.openSearchInNewTab, - ddgBangsEnabled: user.ddgBangs, - }, - }); - - const handleSubmit = (values: FormType) => { - mutate({ - userId: user.id, - ...values, - }); - }; - - return ( -
- - + + + + + + + + + {tGeneral("item.language")} + + + {showLanguageSelector && } + {tGeneral("item.firstDayOfWeek")} + firstDayOfWeekOnChange(parseInt(nextValue, 10))} + > + + {FIRST_DAY_OPTIONS.map(({ value: dayValue, labelKey }) => ( + + ))} + + + + {tGeneral("item.accessibility")} + + + + + +
+ {form.isDirty() && ( + + + {t("common.unsavedChanges")} + + + + + + + )} +
+ +
+ ); +}; diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_ping-icons-enabled.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_ping-icons-enabled.tsx deleted file mode 100644 index d218ac39a..000000000 --- a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_ping-icons-enabled.tsx +++ /dev/null @@ -1,69 +0,0 @@ -"use client"; - -import { Button, Group, Stack, Switch } from "@mantine/core"; -import type { z } from "zod/v4"; - -import type { RouterOutputs } from "@homarr/api"; -import { clientApi } from "@homarr/api/client"; -import { revalidatePathActionAsync } from "@homarr/common/client"; -import { useZodForm } from "@homarr/form"; -import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; -import { useI18n } from "@homarr/translation/client"; -import { userPingIconsEnabledSchema } from "@homarr/validation/user"; - -interface PingIconsEnabledProps { - user: RouterOutputs["user"]["getById"]; -} - -export const PingIconsEnabled = ({ user }: PingIconsEnabledProps) => { - const t = useI18n(); - const { mutate, isPending } = clientApi.user.changePingIconsEnabled.useMutation({ - async onSettled() { - await revalidatePathActionAsync(`/manage/users/${user.id}`); - }, - onSuccess(_, variables) { - form.setInitialValues({ - pingIconsEnabled: variables.pingIconsEnabled, - }); - showSuccessNotification({ - message: t("user.action.changePingIconsEnabled.notification.success.message"), - }); - }, - onError() { - showErrorNotification({ - message: t("user.action.changePingIconsEnabled.notification.error.message"), - }); - }, - }); - const form = useZodForm(userPingIconsEnabledSchema, { - initialValues: { - pingIconsEnabled: user.pingIconsEnabled, - }, - }); - - const handleSubmit = (values: FormType) => { - mutate({ - id: user.id, - ...values, - }); - }; - - return ( -
- - - - - - - -
- ); -}; - -type FormType = z.infer; diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-avatar-form.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-avatar-form.tsx index e4ee64583..809345a0f 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-avatar-form.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-avatar-form.tsx @@ -2,7 +2,7 @@ import { useCallback } from "react"; import { Box, Button, FileButton, Menu, UnstyledButton } from "@mantine/core"; -import { useDisclosure } from "@mantine/hooks"; +import { useDisclosure, useMediaQuery } from "@mantine/hooks"; import { IconPencil, IconPhotoEdit, IconPhotoX } from "@tabler/icons-react"; import type { RouterOutputs } from "@homarr/api"; @@ -25,6 +25,7 @@ export const UserProfileAvatarForm = ({ user }: UserProfileAvatarForm) => { }, }); const [opened, { toggle }] = useDisclosure(false); + const isMobile = useMediaQuery("(max-width: 48em)"); const { openConfirmModal } = useConfirmModal(); const t = useI18n(); const tManageAvatar = useScopedI18n("user.action.manageAvatar"); @@ -94,11 +95,11 @@ export const UserProfileAvatarForm = ({ user }: UserProfileAvatarForm) => { }, [mutate, user.id, openConfirmModal, tManageAvatar]); return ( - - + + - + - - )} - - - ); -}; - -type FormType = Omit; diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/page.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/page.tsx index ee960b0f5..cf68c20e2 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/page.tsx @@ -6,18 +6,13 @@ import { api } from "@homarr/api/server"; import { auth } from "@homarr/auth/next"; import { getI18n, getScopedI18n } from "@homarr/translation/server"; -import { CurrentLanguageCombobox } from "~/components/language/current-language-combobox"; import { DangerZoneItem, DangerZoneRoot } from "~/components/manage/danger-zone"; import { catchTrpcNotFound } from "~/errors/trpc-catch-error"; import { createMetaTitle } from "~/metadata"; import { canAccessUserEditPage } from "../access"; -import { ChangeHomeBoardForm } from "./_components/_change-home-board"; -import { ChangeSearchPreferencesForm } from "./_components/_change-search-preferences"; import { DeleteUserButton } from "./_components/_delete-user-button"; -import { FirstDayOfWeek } from "./_components/_first-day-of-week"; -import { PingIconsEnabled } from "./_components/_ping-icons-enabled"; +import { UserGeneralSettingsForm } from "./_components/_general-settings-form"; import { UserProfileAvatarForm } from "./_components/_profile-avatar-form"; -import { UserProfileForm } from "./_components/_profile-form"; interface Props { params: Promise<{ @@ -63,7 +58,7 @@ export default async function EditUserPage(props: Props) { const boards = await api.board.getAllBoards(); const searchEngines = await api.searchEngine.getSelectable(); - + const isSelf = session?.user.id === user.id; const isCredentialsUser = user.provider === "credentials"; return ( @@ -73,51 +68,25 @@ export default async function EditUserPage(props: Props) { {t("management.page.user.fieldsDisabledExternalProvider")} )} - {tGeneral("title")} - - - + + + ({ + id: board.id, + name: board.name, + logoImageUrl: board.logoImageUrl, + }))} + searchEnginesData={searchEngines} + showLanguageSelector={isSelf} + /> - + - {session?.user.id === user.id && ( - - {tGeneral("item.language")} - - - )} - - - {tGeneral("item.board.title")} - ({ - id: board.id, - name: board.name, - logoImageUrl: board.logoImageUrl, - }))} - /> - - - - {tGeneral("item.search")} - - - - - {tGeneral("item.firstDayOfWeek")} - - - - - {tGeneral("item.accessibility")} - - - { - const tUserList = useScopedI18n("management.page.user.list"); +export const UserListComponent = ({ initialUserList }: UserListComponentProps) => { const t = useI18n(); const { data, isLoading } = clientApi.user.getAll.useQuery(undefined, { initialData: initialUserList, @@ -69,21 +67,10 @@ export const UserListComponent = ({ initialUserList, credentialsProviderEnabled enableFullScreenToggle: false, layoutMode: "grid-no-grow", getRowId: (row) => row.id, - renderTopToolbarCustomActions: () => - credentialsProviderEnabled ? ( - - ) : null, state: { isLoading, }, }); - return ( - <> - {tUserList("title")} - - - ); + return ; }; diff --git a/apps/nextjs/src/app/[locale]/manage/users/create/_components/create-user-stepper.tsx b/apps/nextjs/src/app/[locale]/manage/users/create/_components/create-user-stepper.tsx index fbaae76c7..f443ae585 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/create/_components/create-user-stepper.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/create/_components/create-user-stepper.tsx @@ -149,7 +149,7 @@ export const UserCreateStepperComponent = ({ initialGroups }: UserCreateStepperC color={!generalForm.isValid() ? "red" : undefined} >
- + - + - + @@ -198,20 +198,20 @@ export const UserCreateStepperComponent = ({ initialGroups }: UserCreateStepperC - + - + {generalForm.values.username} - + {t("step.completed.title")} diff --git a/apps/nextjs/src/app/[locale]/manage/users/create/_components/stepper-navigation.tsx b/apps/nextjs/src/app/[locale]/manage/users/create/_components/stepper-navigation.tsx index 24d003a84..675f3f435 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/create/_components/stepper-navigation.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/create/_components/stepper-navigation.tsx @@ -25,7 +25,7 @@ export const StepperNavigationComponent = ({ }: StepperNavigationComponentProps) => { const t = useI18n(); return ( - + {!isComplete ? ( + ); +}; diff --git a/apps/nextjs/src/app/[locale]/manage/users/invites/_components/invite-list.tsx b/apps/nextjs/src/app/[locale]/manage/users/invites/_components/invite-list.tsx index 5fa1b5eff..4bc1eaff1 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/invites/_components/invite-list.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/invites/_components/invite-list.tsx @@ -1,7 +1,7 @@ "use client"; import { useCallback, useMemo } from "react"; -import { ActionIcon, Button, Title } from "@mantine/core"; +import { ActionIcon } from "@mantine/core"; import { IconTrash } from "@tabler/icons-react"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; @@ -10,8 +10,7 @@ import { MantineReactTable } from "mantine-react-table"; import type { RouterOutputs } from "@homarr/api"; import { clientApi } from "@homarr/api/client"; -import { useConfirmModal, useModalAction } from "@homarr/modals"; -import { InviteCreateModal } from "@homarr/modals-collection"; +import { useConfirmModal } from "@homarr/modals"; import { useScopedI18n } from "@homarr/translation/client"; import { useTranslatedMantineReactTable } from "@homarr/ui/hooks"; @@ -65,7 +64,6 @@ export const InviteListComponent = ({ initialInvites }: InviteListComponentProps enableFullScreenToggle: false, layoutMode: "grid-no-grow", getRowId: (row) => row.id, - renderTopToolbarCustomActions: RenderTopToolbarCustomActions, state: { isLoading, }, @@ -74,22 +72,7 @@ export const InviteListComponent = ({ initialInvites }: InviteListComponentProps }, }); - return ( - <> - {t("title")} - - - ); -}; - -const RenderTopToolbarCustomActions = () => { - const t = useScopedI18n("management.page.user.invite"); - const { openModal } = useModalAction(InviteCreateModal); - const handleNewInvite = useCallback(() => { - openModal(); - }, [openModal]); - - return ; + return ; }; const RenderRowActions = ({ row }: { row: MRT_Row }) => { diff --git a/apps/nextjs/src/app/[locale]/manage/users/invites/page.tsx b/apps/nextjs/src/app/[locale]/manage/users/invites/page.tsx index f392e8282..cb5c0423c 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/invites/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/invites/page.tsx @@ -3,8 +3,11 @@ import { notFound } from "next/navigation"; import { api } from "@homarr/api/server"; import { auth } from "@homarr/auth/next"; import { isProviderEnabled } from "@homarr/auth/server"; +import { getScopedI18n } from "@homarr/translation/server"; -import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb"; +import { ManageMobilePrimaryAction } from "~/components/manage/manage-mobile-primary-action"; +import { ManagePageLayout } from "~/components/manage/manage-page-layout"; +import { InviteCreateButton } from "./_components/invite-create-button"; import { InviteListComponent } from "./_components/invite-list"; export default async function InvitesOverviewPage() { @@ -17,11 +20,20 @@ export default async function InvitesOverviewPage() { return notFound(); } + const t = await getScopedI18n("management.page.user.invite"); const initialInvites = await api.invite.getAll(); + return ( - <> - + + + + } + floatingPrimaryAction + > - + ); } diff --git a/apps/nextjs/src/app/[locale]/manage/users/page.tsx b/apps/nextjs/src/app/[locale]/manage/users/page.tsx index 94ae5c103..4059b9972 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/page.tsx @@ -4,8 +4,10 @@ import { api } from "@homarr/api/server"; import { auth } from "@homarr/auth/next"; import { isProviderEnabled } from "@homarr/auth/server"; import { getScopedI18n } from "@homarr/translation/server"; +import { Link } from "@homarr/ui"; -import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb"; +import { ManagePageLayout } from "~/components/manage/manage-page-layout"; +import { MobileAffixButton } from "~/components/manage/mobile-affix-button"; import { createMetaTitle } from "~/metadata"; import { UserListComponent } from "./_components/user-list"; @@ -27,13 +29,23 @@ export default async function UsersPage() { return notFound(); } + const t = await getScopedI18n("management.page.user"); const userList = await api.user.getAll(); const credentialsProviderEnabled = isProviderEnabled("credentials"); return ( - <> - - - + + {t("create.title")} + + ) : undefined + } + floatingPrimaryAction={credentialsProviderEnabled} + > + + ); } diff --git a/apps/nextjs/src/app/[locale]/widgets/[kind]/_content.tsx b/apps/nextjs/src/app/[locale]/widgets/[kind]/_content.tsx index cd8b67ea2..9bbc0ac93 100644 --- a/apps/nextjs/src/app/[locale]/widgets/[kind]/_content.tsx +++ b/apps/nextjs/src/app/[locale]/widgets/[kind]/_content.tsx @@ -99,7 +99,7 @@ export const WidgetPreviewPageContent = ({ kind, integrationData }: WidgetPrevie return ( <> - = 96 ? undefined : 4}> + = 96 ? undefined : 4}> {({ reset }) => ( - + - + {editMode ? : } - + diff --git a/apps/nextjs/src/components/board/items/actions/create-item.ts b/apps/nextjs/src/components/board/items/actions/create-item.ts index f71e5aed0..42b619364 100644 --- a/apps/nextjs/src/components/board/items/actions/create-item.ts +++ b/apps/nextjs/src/components/board/items/actions/create-item.ts @@ -8,12 +8,14 @@ import { getFirstEmptyPosition } from "./empty-position"; import { getSectionElements } from "./section-elements"; export interface CreateItemInput { + id?: string; kind: WidgetKind; options?: Record; + integrationIds?: string[]; } export const createItemCallback = - ({ kind, options = {} }: CreateItemInput) => + ({ id, kind, options = {}, integrationIds = [] }: CreateItemInput) => (previous: Board): Board => { const firstSection = previous.sections .filter((section): section is EmptySection => section.kind === "empty") @@ -23,11 +25,11 @@ export const createItemCallback = if (!firstSection) return previous; const widget = { - id: createId(), + id: id ?? createId(), kind, options, layouts: createItemLayouts(previous, firstSection), - integrationIds: [], + integrationIds, advancedOptions: { title: null, customCssClasses: [], diff --git a/apps/nextjs/src/components/board/items/item-content.tsx b/apps/nextjs/src/components/board/items/item-content.tsx index d61d05902..a6f6514e0 100644 --- a/apps/nextjs/src/components/board/items/item-content.tsx +++ b/apps/nextjs/src/components/board/items/item-content.tsx @@ -42,7 +42,6 @@ export const BoardItemContent = ({ item }: BoardItemContentProps) => { item.advancedOptions.customCssClasses.join(" "), )} radius={board.itemRadius} - withBorder styles={{ root: { "--opacity": board.opacity / 100, diff --git a/apps/nextjs/src/components/board/items/item-menu.tsx b/apps/nextjs/src/components/board/items/item-menu.tsx index a7d870a28..3b79dafd6 100644 --- a/apps/nextjs/src/components/board/items/item-menu.tsx +++ b/apps/nextjs/src/components/board/items/item-menu.tsx @@ -105,9 +105,17 @@ const BoardItemMenuInner = ({ offset, item, resetErrorBoundary }: BoardItemMenuP }; return ( - + - + diff --git a/apps/nextjs/src/components/board/items/item-select-modal.tsx b/apps/nextjs/src/components/board/items/item-select-modal.tsx index 5066f5539..23c9ddec6 100644 --- a/apps/nextjs/src/components/board/items/item-select-modal.tsx +++ b/apps/nextjs/src/components/board/items/item-select-modal.tsx @@ -2,20 +2,26 @@ import { useMemo, useState } from "react"; import { Avatar, Button, Card, Center, Grid, Group, Input, Stack, Text, Tooltip } from "@mantine/core"; import { IconSearch } from "@tabler/icons-react"; -import { objectEntries } from "@homarr/common"; +import { clientApi } from "@homarr/api/client"; +import { createId, objectEntries } from "@homarr/common"; import { getIconUrl, getIntegrationName } from "@homarr/definitions"; import type { IntegrationKind, WidgetKind } from "@homarr/definitions"; -import { createModal } from "@homarr/modals"; +import { createModal, useModalAction } from "@homarr/modals"; +import { useSettings } from "@homarr/settings"; import { useI18n } from "@homarr/translation/client"; import type { TablerIcon } from "@homarr/ui"; -import { widgetImports } from "@homarr/widgets"; +import { reduceWidgetOptionsWithDefaultValues, widgetImports } from "@homarr/widgets"; +import { WidgetEditModal } from "@homarr/widgets/modals"; import { useItemActions } from "./item-actions"; export const ItemSelectModal = createModal(({ actions }) => { const [search, setSearch] = useState(""); const t = useI18n(); - const { createItem } = useItemActions(); + const { createItem, updateItemOptions, updateItemAdvancedOptions, updateItemIntegrations } = useItemActions(); + const { openModal: openEditModal } = useModalAction(WidgetEditModal); + const { data: integrationData } = clientApi.integration.all.useQuery(); + const settings = useSettings(); const items = useMemo( () => @@ -40,8 +46,43 @@ export const ItemSelectModal = createModal(({ actions }) => { ); const handleAdd = (kind: WidgetKind) => { - createItem({ kind }); + const definition = widgetImports[kind].definition; + const hasIntegrationSupport = "supportedIntegrations" in definition; + + const matchingIntegrations = hasIntegrationSupport + ? (integrationData ?? []).filter((integration) => + (definition.supportedIntegrations as string[]).includes(integration.kind), + ) + : []; + + const integrationIds = matchingIntegrations.map((i) => i.id); + const itemId = createId(); + const defaultOptions = reduceWidgetOptionsWithDefaultValues(kind, settings); + + createItem({ id: itemId, kind, integrationIds }); actions.closeModal(); + + openEditModal( + { + kind, + value: { + advancedOptions: { title: null, customCssClasses: [], borderColor: "" }, + options: defaultOptions, + integrationIds, + }, + onSuccessfulEdit: ({ options, integrationIds: newIntegrationIds, advancedOptions }) => { + updateItemOptions({ itemId, newOptions: options }); + updateItemAdvancedOptions({ itemId, newAdvancedOptions: advancedOptions }); + updateItemIntegrations({ itemId, newIntegrations: newIntegrationIds }); + }, + integrationData: matchingIntegrations, + integrationSupport: hasIntegrationSupport, + settings, + }, + { + title: (titleT) => `${titleT("item.edit.title")} - ${titleT(`widget.${kind}.name`)}`, + }, + ); }; return ( @@ -104,7 +145,7 @@ const WidgetItem = ({ - @@ -129,7 +170,7 @@ const SupportedIntegrations = ({ integrations, mt }: SupportedIntegrationsProps) return (
- + {integrations.slice(0, countToShow).map((integration) => ( diff --git a/apps/nextjs/src/components/board/sections/category-section.tsx b/apps/nextjs/src/components/board/sections/category-section.tsx index df204998b..4c3958859 100644 --- a/apps/nextjs/src/components/board/sections/category-section.tsx +++ b/apps/nextjs/src/components/board/sections/category-section.tsx @@ -3,6 +3,7 @@ import { useDisclosure } from "@mantine/hooks"; import { IconChevronDown, IconChevronUp } from "@tabler/icons-react"; import { clientApi } from "@homarr/api/client"; +import { useSession } from "@homarr/auth/client"; import { useRequiredBoard } from "@homarr/boards/context"; import type { CategorySection } from "~/app/[locale]/boards/_types"; @@ -10,30 +11,41 @@ import { CategoryMenu } from "./category/category-menu"; import { GridStack } from "./gridstack/gridstack"; import classes from "./item.module.css"; +const localStorageKey = (sectionId: string) => `homarr-section-collapsed-${sectionId}`; + interface Props { section: CategorySection; } export const BoardCategorySection = ({ section }: Props) => { + const { status } = useSession(); const { mutate } = clientApi.section.changeCollapsed.useMutation(); const board = useRequiredBoard(); - const [opened, { toggle }] = useDisclosure(section.collapsed, { + + const initialCollapsed = + typeof window !== "undefined" + ? (localStorage.getItem(localStorageKey(section.id)) ?? String(section.collapsed)) === "true" + : section.collapsed; + + const [opened, { toggle }] = useDisclosure(initialCollapsed, { onOpen() { - mutate({ sectionId: section.id, collapsed: true }); + if (status === "authenticated") { + mutate({ sectionId: section.id, collapsed: true }); + } else if (status === "unauthenticated") { + localStorage.setItem(localStorageKey(section.id), "true"); + } }, onClose() { - mutate({ sectionId: section.id, collapsed: false }); + if (status === "authenticated") { + mutate({ sectionId: section.id, collapsed: false }); + } else if (status === "unauthenticated") { + localStorage.setItem(localStorageKey(section.id), "false"); + } }, }); return ( - + @@ -44,7 +56,7 @@ export const BoardCategorySection = ({ section }: Props) => { - + diff --git a/apps/nextjs/src/components/board/sections/category/category-menu.tsx b/apps/nextjs/src/components/board/sections/category/category-menu.tsx index 894db8949..9e3928198 100644 --- a/apps/nextjs/src/components/board/sections/category/category-menu.tsx +++ b/apps/nextjs/src/components/board/sections/category/category-menu.tsx @@ -30,9 +30,9 @@ export const CategoryMenu = ({ category }: Props) => { const t = useScopedI18n("section.category"); return ( - + - + diff --git a/apps/nextjs/src/components/board/sections/dynamic-section.tsx b/apps/nextjs/src/components/board/sections/dynamic-section.tsx index 39818c5de..8bb0a52c3 100644 --- a/apps/nextjs/src/components/board/sections/dynamic-section.tsx +++ b/apps/nextjs/src/components/board/sections/dynamic-section.tsx @@ -28,7 +28,6 @@ export const BoardDynamicSection = ({ section }: Props) => { className={combineClasses(classes.itemCard, section.options.customCssClasses.join(" "))} w="100%" h="100%" - withBorder styles={{ root: { overflow: "visible", diff --git a/apps/nextjs/src/components/board/sections/dynamic/dynamic-menu.tsx b/apps/nextjs/src/components/board/sections/dynamic/dynamic-menu.tsx index f73328024..cffc3446f 100644 --- a/apps/nextjs/src/components/board/sections/dynamic/dynamic-menu.tsx +++ b/apps/nextjs/src/components/board/sections/dynamic/dynamic-menu.tsx @@ -43,9 +43,17 @@ export const BoardDynamicSectionMenu = ({ section }: { section: DynamicSectionIt }; return ( - + - + diff --git a/apps/nextjs/src/components/board/sections/gridstack/use-gridstack.ts b/apps/nextjs/src/components/board/sections/gridstack/use-gridstack.ts index eda89a8e6..b5e468de6 100644 --- a/apps/nextjs/src/components/board/sections/gridstack/use-gridstack.ts +++ b/apps/nextjs/src/components/board/sections/gridstack/use-gridstack.ts @@ -1,6 +1,6 @@ -import type { RefObject } from "react"; +import type { Ref, RefObject } from "react"; import { createRef, useCallback, useEffect, useRef } from "react"; -import { useElementSize } from "@mantine/hooks"; +import { useElementSize, useMergedRef } from "@mantine/hooks"; import { useCurrentLayout, useRequiredBoard } from "@homarr/boards/context"; import { useEditMode } from "@homarr/boards/edit-mode"; @@ -13,7 +13,7 @@ import { useSectionActions } from "../section-actions"; import { initializeGridstack } from "./init-gridstack"; export interface UseGridstackRefs { - wrapper: RefObject; + wrapper: Ref; items: RefObject>>; gridstack: RefObject; } @@ -59,8 +59,9 @@ export const useGridstack = (section: Omit, itemIds: string[]) const { moveAndResizeItem, moveItemToSection } = useItemActions(); const { moveAndResizeInnerSection, moveInnerSectionToSection } = useSectionActions(); - // define reference for wrapper - is used to calculate the width of the wrapper - const { ref: wrapperRef, width, height } = useElementSize(); + const wrapperElementRef = useRef(null); + const { ref: measureWrapperRef, width, height } = useElementSize(); + const wrapperRef = useMergedRef(measureWrapperRef, wrapperElementRef); // references to the diffrent items contained in the gridstack const itemRefs = useRef>>({}); // reference of the gridstack object for modifications after initialization @@ -193,7 +194,7 @@ export const useGridstack = (section: Omit, itemIds: string[]) itemIds, refs: { items: itemRefs, - wrapper: wrapperRef, + wrapper: wrapperElementRef, gridstack: gridRef, }, sectionColumnCount: columnCount, @@ -276,7 +277,7 @@ export const useGridstack = (section: Omit, itemIds: string[]) useCssVariableConfiguration({ columnCount, gridRef, - wrapperRef, + wrapperRef: wrapperElementRef, width, height, isDynamic: section.kind === "dynamic", @@ -293,7 +294,7 @@ export const useGridstack = (section: Omit, itemIds: string[]) interface UseCssVariableConfiguration { gridRef: UseGridstackRefs["gridstack"]; - wrapperRef: UseGridstackRefs["wrapper"]; + wrapperRef: RefObject; width: number; height: number; columnCount: number; diff --git a/apps/nextjs/src/components/language/current-language-combobox.tsx b/apps/nextjs/src/components/language/current-language-combobox.tsx index e27f34c39..54d9a0f98 100644 --- a/apps/nextjs/src/components/language/current-language-combobox.tsx +++ b/apps/nextjs/src/components/language/current-language-combobox.tsx @@ -6,11 +6,20 @@ import { LanguageCombobox } from "./language-combobox"; interface CurrentLanguageComboboxProps { width?: string; + withinPortal?: boolean; } -export const CurrentLanguageCombobox = ({ width }: CurrentLanguageComboboxProps) => { +export const CurrentLanguageCombobox = ({ width, withinPortal }: CurrentLanguageComboboxProps) => { const currentLocale = useCurrentLocale(); const { changeLocale, isPending } = useChangeLocale(); - return ; + return ( + + ); }; diff --git a/apps/nextjs/src/components/language/language-combobox.tsx b/apps/nextjs/src/components/language/language-combobox.tsx index 6f4b39b33..f6eaaac69 100644 --- a/apps/nextjs/src/components/language/language-combobox.tsx +++ b/apps/nextjs/src/components/language/language-combobox.tsx @@ -16,9 +16,17 @@ interface LanguageComboboxProps { onChange: (value: SupportedLanguage) => void; isPending?: boolean; width?: string; + withinPortal?: boolean; } -export const LanguageCombobox = ({ label, value, onChange, isPending, width }: LanguageComboboxProps) => { +export const LanguageCombobox = ({ + label, + value, + onChange, + isPending, + width, + withinPortal = false, +}: LanguageComboboxProps) => { const combobox = useCombobox({ onDropdownClose: () => combobox.resetSelectedOption(), }); @@ -39,7 +47,7 @@ export const LanguageCombobox = ({ label, value, onChange, isPending, width }: L }, [combobox]); return ( - + { muted loop playsInline + aria-hidden="true" style={{ position: "fixed", width: "100vw", diff --git a/apps/nextjs/src/components/layout/navigation-link.tsx b/apps/nextjs/src/components/layout/navigation-link.tsx index dd6949682..2d98d42ec 100644 --- a/apps/nextjs/src/components/layout/navigation-link.tsx +++ b/apps/nextjs/src/components/layout/navigation-link.tsx @@ -12,16 +12,7 @@ export const CommonNavLink = (props: ClientNavigationLink) => const NavLinkHref = (props: NavigationLinkHref) => { const pathname = usePathname(); return props.external ? ( - + ) : ( { leftSection={props.icon} href={props.href} active={pathname === props.href} - style={{ - borderRadius: 5, - }} /> ); }; @@ -40,14 +28,7 @@ const NavLinkWithItems = (props: NavigationLinkWithItems) => { const pathname = usePathname(); const isActive = props.items.some((item) => item.href === pathname); return ( - + {props.items.map((item) => ( ))} diff --git a/apps/nextjs/src/components/manage/danger-zone.tsx b/apps/nextjs/src/components/manage/danger-zone.tsx index 30bfb3e37..38f63185b 100644 --- a/apps/nextjs/src/components/manage/danger-zone.tsx +++ b/apps/nextjs/src/components/manage/danger-zone.tsx @@ -15,7 +15,7 @@ export const DangerZoneRoot = async ({ children }: DangerZoneRootProps) => { {t("common.dangerZone")} - + {Array.isArray(children) ? children.map((child, index) => ( diff --git a/apps/nextjs/src/components/manage/manage-mobile-primary-action.tsx b/apps/nextjs/src/components/manage/manage-mobile-primary-action.tsx new file mode 100644 index 000000000..40192e0f8 --- /dev/null +++ b/apps/nextjs/src/components/manage/manage-mobile-primary-action.tsx @@ -0,0 +1,15 @@ +import type { PropsWithChildren } from "react"; +import { Affix, Box } from "@mantine/core"; + +import { MANAGE_ACTION_BUTTON_MIN_WIDTH, MANAGE_AFFIX_POSITION } from "./manage-page.constants"; + +export const ManageMobilePrimaryAction = ({ children }: PropsWithChildren) => ( + <> + + {children} + + + {children} + + +); diff --git a/apps/nextjs/src/components/manage/manage-page-layout.tsx b/apps/nextjs/src/components/manage/manage-page-layout.tsx new file mode 100644 index 000000000..d8655c58c --- /dev/null +++ b/apps/nextjs/src/components/manage/manage-page-layout.tsx @@ -0,0 +1,44 @@ +import type { ReactNode } from "react"; +import type { MantineSize } from "@mantine/core"; +import { Group, Stack, Title } from "@mantine/core"; + +import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb"; +import { ManageContainer } from "./manage-container"; +import { MANAGE_FLOATING_ACTION_BOTTOM_OFFSET } from "./manage-page.constants"; + +interface ManagePageLayoutProps { + title: ReactNode; + primaryAction?: ReactNode; + toolbar?: ReactNode; + footer?: ReactNode; + floatingPrimaryAction?: boolean; + size?: MantineSize; + children: ReactNode; +} + +export const ManagePageLayout = ({ + title, + primaryAction, + toolbar, + footer, + floatingPrimaryAction, + size, + children, +}: ManagePageLayoutProps) => { + const titleNode = typeof title === "string" ? {title} : title; + + return ( + + + + + {titleNode} + {primaryAction} + + {toolbar} + {children} + {footer && {footer}} + + + ); +}; diff --git a/apps/nextjs/src/components/manage/manage-page.constants.ts b/apps/nextjs/src/components/manage/manage-page.constants.ts new file mode 100644 index 000000000..7b3a59884 --- /dev/null +++ b/apps/nextjs/src/components/manage/manage-page.constants.ts @@ -0,0 +1,5 @@ +export const MANAGE_AFFIX_POSITION = { bottom: 20, right: 20 } as const; + +export const MANAGE_FLOATING_ACTION_BOTTOM_OFFSET = 48; + +export const MANAGE_ACTION_BUTTON_MIN_WIDTH = 160; diff --git a/apps/nextjs/src/components/manage/mobile-affix-button.tsx b/apps/nextjs/src/components/manage/mobile-affix-button.tsx index abe4f017c..f711751de 100644 --- a/apps/nextjs/src/components/manage/mobile-affix-button.tsx +++ b/apps/nextjs/src/components/manage/mobile-affix-button.tsx @@ -2,13 +2,15 @@ import { forwardRef } from "react"; import type { ButtonProps } from "@mantine/core"; import { Affix, Button, createPolymorphicComponent } from "@mantine/core"; +import { MANAGE_ACTION_BUTTON_MIN_WIDTH, MANAGE_AFFIX_POSITION } from "./manage-page.constants"; + type MobileAffixButtonProps = Omit; export const MobileAffixButton = createPolymorphicComponent<"button", MobileAffixButtonProps>( forwardRef((props, ref) => ( <> - @@ -100,7 +100,7 @@ export const AppSelectModal = createModal(({ actions, inner {app.description ?? ""} - diff --git a/packages/modals-collection/src/docker/add-docker-app-to-homarr.tsx b/packages/modals-collection/src/docker/add-docker-app-to-homarr.tsx index f94bf8f24..d83b16fdd 100644 --- a/packages/modals-collection/src/docker/add-docker-app-to-homarr.tsx +++ b/packages/modals-collection/src/docker/add-docker-app-to-homarr.tsx @@ -58,7 +58,7 @@ export const AddDockerAppToHomarrModal = createModal( }; return ( - + {innerProps.selectedContainers.map((container, index) => ( diff --git a/packages/modals-collection/src/search-engines/request-media-modal.tsx b/packages/modals-collection/src/search-engines/request-media-modal.tsx index efb92cf7a..89306f3af 100644 --- a/packages/modals-collection/src/search-engines/request-media-modal.tsx +++ b/packages/modals-collection/src/search-engines/request-media-modal.tsx @@ -83,15 +83,10 @@ export const RequestMediaModal = createModal(({ actions, return ( - + {data && ( - poster + poster {data.overview} diff --git a/packages/modals/eslint.config.js b/packages/modals/eslint.config.js deleted file mode 100644 index 1df6129af..000000000 --- a/packages/modals/eslint.config.js +++ /dev/null @@ -1,5 +0,0 @@ -import baseConfig from "@homarr/eslint-config/base"; -import reactConfig from "@homarr/eslint-config/react"; - -/** @type {import('typescript-eslint').Config} */ -export default [...baseConfig, ...reactConfig]; diff --git a/packages/modals/package.json b/packages/modals/package.json index 8387be196..e8ee0594d 100644 --- a/packages/modals/package.json +++ b/packages/modals/package.json @@ -4,9 +4,6 @@ "private": true, "license": "Apache-2.0", "type": "module", - "exports": { - ".": "./index.ts" - }, "typesVersions": { "*": { "*": [ @@ -14,13 +11,15 @@ ] } }, + "exports": { + ".": "./index.ts" + }, "scripts": { "clean": "rm -rf .turbo node_modules", - "format": "prettier --check . --ignore-path ../../.gitignore", - "lint": "eslint", + "format": "oxfmt .", + "lint": "oxlint", "typecheck": "tsc --noEmit" }, - "prettier": "@homarr/prettier-config", "dependencies": { "@homarr/translation": "workspace:^0.1.0", "@homarr/ui": "workspace:^0.1.0", @@ -29,10 +28,7 @@ "react": "catalog:" }, "devDependencies": { - "@homarr/eslint-config": "workspace:^0.2.0", - "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "catalog:", "typescript": "catalog:" } } diff --git a/packages/notifications/eslint.config.js b/packages/notifications/eslint.config.js deleted file mode 100644 index f7a5a7d36..000000000 --- a/packages/notifications/eslint.config.js +++ /dev/null @@ -1,4 +0,0 @@ -import baseConfig from "@homarr/eslint-config/base"; - -/** @type {import('typescript-eslint').Config} */ -export default [...baseConfig]; diff --git a/packages/notifications/package.json b/packages/notifications/package.json index 2409be83a..505e4e74b 100644 --- a/packages/notifications/package.json +++ b/packages/notifications/package.json @@ -4,10 +4,6 @@ "private": true, "license": "Apache-2.0", "type": "module", - "exports": { - ".": "./index.ts", - "./styles.css": "./src/styles.css" - }, "typesVersions": { "*": { "*": [ @@ -15,23 +11,23 @@ ] } }, + "exports": { + ".": "./index.ts", + "./styles.css": "./src/styles.css" + }, "scripts": { "clean": "rm -rf .turbo node_modules", - "format": "prettier --check . --ignore-path ../../.gitignore", - "lint": "eslint", + "format": "oxfmt .", + "lint": "oxlint", "typecheck": "tsc --noEmit" }, - "prettier": "@homarr/prettier-config", "dependencies": { "@homarr/ui": "workspace:^0.1.0", "@mantine/notifications": "catalog:", "@tabler/icons-react": "catalog:" }, "devDependencies": { - "@homarr/eslint-config": "workspace:^0.2.0", - "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "catalog:", "typescript": "catalog:" } } diff --git a/packages/old-import/eslint.config.js b/packages/old-import/eslint.config.js deleted file mode 100644 index 5b19b6f8a..000000000 --- a/packages/old-import/eslint.config.js +++ /dev/null @@ -1,9 +0,0 @@ -import baseConfig from "@homarr/eslint-config/base"; - -/** @type {import('typescript-eslint').Config} */ -export default [ - { - ignores: [], - }, - ...baseConfig, -]; diff --git a/packages/old-import/package.json b/packages/old-import/package.json index 4f60fcad2..14b6e3e3f 100644 --- a/packages/old-import/package.json +++ b/packages/old-import/package.json @@ -4,13 +4,6 @@ "private": true, "license": "Apache-2.0", "type": "module", - "exports": { - ".": "./index.ts", - "./analyse": "./src/analyse/index.ts", - "./components": "./src/components/index.ts", - "./import": "./src/import/index.ts", - "./shared": "./src/shared.ts" - }, "typesVersions": { "*": { "*": [ @@ -18,13 +11,19 @@ ] } }, + "exports": { + ".": "./index.ts", + "./analyse": "./src/analyse/index.ts", + "./components": "./src/components/index.ts", + "./import": "./src/import/index.ts", + "./shared": "./src/shared.ts" + }, "scripts": { "clean": "rm -rf .turbo node_modules", - "format": "prettier --check . --ignore-path ../../.gitignore", - "lint": "eslint", + "format": "oxfmt .", + "lint": "oxlint", "typecheck": "tsc --noEmit" }, - "prettier": "@homarr/prettier-config", "dependencies": { "@homarr/common": "workspace:^0.1.0", "@homarr/core": "workspace:^0.1.0", @@ -48,11 +47,8 @@ "zod-form-data": "catalog:" }, "devDependencies": { - "@homarr/eslint-config": "workspace:^0.2.0", - "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", "@types/adm-zip": "catalog:", - "eslint": "catalog:", "typescript": "catalog:" } } diff --git a/packages/old-import/src/components/initial/board-selection-card.tsx b/packages/old-import/src/components/initial/board-selection-card.tsx index 294e68885..4665ec2cd 100644 --- a/packages/old-import/src/components/initial/board-selection-card.tsx +++ b/packages/old-import/src/components/initial/board-selection-card.tsx @@ -38,7 +38,7 @@ export const BoardSelectionCard = ({ selections, updateSelections }: BoardSelect } return ( - + @@ -57,7 +57,7 @@ export const BoardSelectionCard = ({ selections, updateSelections }: BoardSelect {[...selections.entries()].map(([name, selected]) => ( - + { const tImportSettings = useScopedI18n("init.step.import.importSettings"); return ( - + {tImportSettings("title")} diff --git a/packages/old-import/src/components/initial/import-summary-card.tsx b/packages/old-import/src/components/initial/import-summary-card.tsx index 876d1f43c..9993b3ac2 100644 --- a/packages/old-import/src/components/initial/import-summary-card.tsx +++ b/packages/old-import/src/components/initial/import-summary-card.tsx @@ -13,7 +13,7 @@ interface ImportSummaryCardProps { export const ImportSummaryCard = ({ counts, onSubmit, loading }: ImportSummaryCardProps) => { const tSummary = useScopedI18n("init.step.import.summary"); return ( - + {tSummary("title")} @@ -24,7 +24,7 @@ export const ImportSummaryCard = ({ counts, onSubmit, loading }: ImportSummaryCa {objectEntries(counts).map(([key, count]) => ( - + {tSummary(`entities.${key}`)} diff --git a/packages/old-schema/eslint.config.js b/packages/old-schema/eslint.config.js deleted file mode 100644 index 5b19b6f8a..000000000 --- a/packages/old-schema/eslint.config.js +++ /dev/null @@ -1,9 +0,0 @@ -import baseConfig from "@homarr/eslint-config/base"; - -/** @type {import('typescript-eslint').Config} */ -export default [ - { - ignores: [], - }, - ...baseConfig, -]; diff --git a/packages/old-schema/package.json b/packages/old-schema/package.json index 9db60a184..328bd8f50 100644 --- a/packages/old-schema/package.json +++ b/packages/old-schema/package.json @@ -4,9 +4,6 @@ "private": true, "license": "Apache-2.0", "type": "module", - "exports": { - ".": "./index.ts" - }, "typesVersions": { "*": { "*": [ @@ -14,22 +11,21 @@ ] } }, + "exports": { + ".": "./index.ts" + }, "scripts": { "clean": "rm -rf .turbo node_modules", - "format": "prettier --check . --ignore-path ../../.gitignore", - "lint": "eslint", + "format": "oxfmt .", + "lint": "oxlint", "typecheck": "tsc --noEmit" }, - "prettier": "@homarr/prettier-config", "dependencies": { "@homarr/common": "workspace:^0.1.0", "zod": "catalog:" }, "devDependencies": { - "@homarr/eslint-config": "workspace:^0.2.0", - "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "catalog:", "typescript": "catalog:" } } diff --git a/packages/ping/eslint.config.js b/packages/ping/eslint.config.js deleted file mode 100644 index f7a5a7d36..000000000 --- a/packages/ping/eslint.config.js +++ /dev/null @@ -1,4 +0,0 @@ -import baseConfig from "@homarr/eslint-config/base"; - -/** @type {import('typescript-eslint').Config} */ -export default [...baseConfig]; diff --git a/packages/ping/package.json b/packages/ping/package.json index 6f0a9e6df..1ae965e3b 100644 --- a/packages/ping/package.json +++ b/packages/ping/package.json @@ -4,9 +4,6 @@ "private": true, "license": "Apache-2.0", "type": "module", - "exports": { - ".": "./index.ts" - }, "typesVersions": { "*": { "*": [ @@ -14,22 +11,21 @@ ] } }, + "exports": { + ".": "./index.ts" + }, "scripts": { "clean": "rm -rf .turbo node_modules", - "format": "prettier --check . --ignore-path ../../.gitignore", - "lint": "eslint", + "format": "oxfmt .", + "lint": "oxlint", "typecheck": "tsc --noEmit" }, - "prettier": "@homarr/prettier-config", "dependencies": { "@homarr/common": "workspace:^0.1.0", "@homarr/core": "workspace:^0.1.0" }, "devDependencies": { - "@homarr/eslint-config": "workspace:^0.2.0", - "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "catalog:", "typescript": "catalog:" } } diff --git a/packages/redis/eslint.config.js b/packages/redis/eslint.config.js deleted file mode 100644 index f7a5a7d36..000000000 --- a/packages/redis/eslint.config.js +++ /dev/null @@ -1,4 +0,0 @@ -import baseConfig from "@homarr/eslint-config/base"; - -/** @type {import('typescript-eslint').Config} */ -export default [...baseConfig]; diff --git a/packages/redis/package.json b/packages/redis/package.json index 6a5d14a38..71eb8838b 100644 --- a/packages/redis/package.json +++ b/packages/redis/package.json @@ -4,9 +4,6 @@ "private": true, "license": "Apache-2.0", "type": "module", - "exports": { - ".": "./index.ts" - }, "typesVersions": { "*": { "*": [ @@ -14,13 +11,15 @@ ] } }, + "exports": { + ".": "./index.ts" + }, "scripts": { "clean": "rm -rf .turbo node_modules", - "format": "prettier --check . --ignore-path ../../.gitignore", - "lint": "eslint", + "format": "oxfmt .", + "lint": "oxlint", "typecheck": "tsc --noEmit" }, - "prettier": "@homarr/prettier-config", "dependencies": { "@homarr/common": "workspace:^", "@homarr/core": "workspace:^", @@ -30,10 +29,7 @@ "superjson": "catalog:" }, "devDependencies": { - "@homarr/eslint-config": "workspace:^0.2.0", - "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "catalog:", "typescript": "catalog:" } } diff --git a/packages/request-handler/eslint.config.js b/packages/request-handler/eslint.config.js deleted file mode 100644 index 5b19b6f8a..000000000 --- a/packages/request-handler/eslint.config.js +++ /dev/null @@ -1,9 +0,0 @@ -import baseConfig from "@homarr/eslint-config/base"; - -/** @type {import('typescript-eslint').Config} */ -export default [ - { - ignores: [], - }, - ...baseConfig, -]; diff --git a/packages/request-handler/package.json b/packages/request-handler/package.json index 5504d6031..29609d87d 100644 --- a/packages/request-handler/package.json +++ b/packages/request-handler/package.json @@ -4,9 +4,6 @@ "private": true, "license": "Apache-2.0", "type": "module", - "exports": { - "./*": "./src/*.ts" - }, "typesVersions": { "*": { "*": [ @@ -14,13 +11,15 @@ ] } }, + "exports": { + "./*": "./src/*.ts" + }, "scripts": { "clean": "rm -rf .turbo node_modules", - "format": "prettier --check . --ignore-path ../../.gitignore", - "lint": "eslint", + "format": "oxfmt .", + "lint": "oxlint", "typecheck": "tsc --noEmit" }, - "prettier": "@homarr/prettier-config", "dependencies": { "@extractus/feed-extractor": "catalog:", "@homarr/common": "workspace:^0.1.0", @@ -37,10 +36,7 @@ "zod": "catalog:" }, "devDependencies": { - "@homarr/eslint-config": "workspace:^0.2.0", - "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "catalog:", "typescript": "catalog:" } } diff --git a/packages/request-handler/src/lib/cached-request-handler.ts b/packages/request-handler/src/lib/cached-request-handler.ts index d9b8dfa90..975448914 100644 --- a/packages/request-handler/src/lib/cached-request-handler.ts +++ b/packages/request-handler/src/lib/cached-request-handler.ts @@ -1,6 +1,7 @@ import dayjs from "dayjs"; import type { Duration } from "dayjs/plugin/duration"; +import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error"; import { createLogger } from "@homarr/core/infrastructure/logs"; import type { createChannelWithLatestAndEvents } from "@homarr/redis"; @@ -15,6 +16,7 @@ interface Options> { options: Options, ) => ReturnType>; cacheDuration: Duration; + fallbackToStaleOnError?: boolean; } export const createCachedRequestHandler = >( @@ -26,13 +28,33 @@ export const createCachedRequestHandler = { - const data = await options.requestAsync(input); - await channel.publishAndUpdateLastStateAsync(data); - return { - data, - timestamp: new Date(), - }; + try { + const data = await options.requestAsync(input); + await channel.publishAndUpdateLastStateAsync(data); + return { + data, + timestamp: new Date(), + }; + } catch (error) { + if (options.fallbackToStaleOnError && channelData) { + logger.warn( + new ErrorWithMetadata( + "Cached request handler using stale cache after fetch failure", + { + channel: channel.name, + queryKey: options.queryKey, + }, + { cause: error }, + ), + ); + return channelData; + } + + throw error; + } }; if (forceUpdate) { @@ -43,8 +65,6 @@ export const createCachedRequestHandler = options.cacheDuration.asMilliseconds(); diff --git a/packages/request-handler/src/test/update-checker.spec.ts b/packages/request-handler/src/test/update-checker.spec.ts index 4c8c1e94b..f615b6481 100644 --- a/packages/request-handler/src/test/update-checker.spec.ts +++ b/packages/request-handler/src/test/update-checker.spec.ts @@ -111,6 +111,24 @@ describe("getAvailableUpdatesAsync", () => { // Assert expect(result).toEqual([]); }); + + test("should propagate GitHub API auth errors without blocking callers", async () => { + const currentVersion = "v1.2.3"; + const listReleasesSpy = vi.spyOn(new Octokit().rest.repos, "listReleases"); + listReleasesSpy.mockRejectedValue(new Error("Bad credentials")); + + await expect(getAvailableUpdatesAsync(currentVersion)).rejects.toThrow("Bad credentials"); + }); + + test("should propagate GitHub API quota exhausted errors without blocking callers", async () => { + const currentVersion = "v1.2.3"; + const listReleasesSpy = vi.spyOn(new Octokit().rest.repos, "listReleases"); + listReleasesSpy.mockRejectedValue( + new Error("Request quota exhausted for request GET /repos/{owner}/{repo}/releases"), + ); + + await expect(getAvailableUpdatesAsync(currentVersion)).rejects.toThrow("quota exhausted"); + }); }); const fakeReleases = (...inputs: [string, boolean][]) => inputs.map(fakeRelease); diff --git a/packages/request-handler/src/update-checker.ts b/packages/request-handler/src/update-checker.ts index bd09ce89d..8709f20c5 100644 --- a/packages/request-handler/src/update-checker.ts +++ b/packages/request-handler/src/update-checker.ts @@ -15,7 +15,8 @@ const logger = createLogger({ module: "updateCheckerRequestHandler" }); export const updateCheckerRequestHandler = createCachedRequestHandler({ queryKey: "homarr-update-checker", - cacheDuration: dayjs.duration(1, "hour"), + cacheDuration: dayjs.duration(1, "day"), + fallbackToStaleOnError: true, async requestAsync(_) { return { availableUpdates: await getAvailableUpdatesAsync(packageJson.version), @@ -58,6 +59,7 @@ export const getAvailableUpdatesAsync = async (currentVersion: string) => { request: { fetch: fetchWithTrustedCertificatesAsync, }, + throttle: { enabled: false }, }); const isCurrentPrerelease = isPrereleaseTag(currentVersion); @@ -100,7 +102,7 @@ export const getAvailableUpdatesAsync = async (currentVersion: string) => { const availableUpdates = semanticReleases .filter((release) => isCurrentPrerelease || !release.isPrerelease) .filter((release) => compareSemVer(release.tagName, currentVersion) > 0) - .sort((releaseA, releaseB) => compareSemVer(releaseB.tagName, releaseA.tagName)); + .toSorted((releaseA, releaseB) => compareSemVer(releaseB.tagName, releaseA.tagName)); if (availableUpdates.length === 0) { logger.debug("No available updates found", { currentVersion }); diff --git a/packages/server-settings/eslint.config.js b/packages/server-settings/eslint.config.js deleted file mode 100644 index f7a5a7d36..000000000 --- a/packages/server-settings/eslint.config.js +++ /dev/null @@ -1,4 +0,0 @@ -import baseConfig from "@homarr/eslint-config/base"; - -/** @type {import('typescript-eslint').Config} */ -export default [...baseConfig]; diff --git a/packages/server-settings/package.json b/packages/server-settings/package.json index 494bc2a91..4decc00ae 100644 --- a/packages/server-settings/package.json +++ b/packages/server-settings/package.json @@ -4,9 +4,6 @@ "private": true, "license": "Apache-2.0", "type": "module", - "exports": { - ".": "./index.ts" - }, "typesVersions": { "*": { "*": [ @@ -14,22 +11,21 @@ ] } }, + "exports": { + ".": "./index.ts" + }, "scripts": { "clean": "rm -rf .turbo node_modules", - "format": "prettier --check . --ignore-path ../../.gitignore", - "lint": "eslint", + "format": "oxfmt .", + "lint": "oxlint", "typecheck": "tsc --noEmit" }, - "prettier": "@homarr/prettier-config", "dependencies": { "@homarr/definitions": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0" }, "devDependencies": { - "@homarr/eslint-config": "workspace:^0.2.0", - "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "catalog:", "typescript": "catalog:" } } diff --git a/packages/settings/eslint.config.js b/packages/settings/eslint.config.js deleted file mode 100644 index 5b19b6f8a..000000000 --- a/packages/settings/eslint.config.js +++ /dev/null @@ -1,9 +0,0 @@ -import baseConfig from "@homarr/eslint-config/base"; - -/** @type {import('typescript-eslint').Config} */ -export default [ - { - ignores: [], - }, - ...baseConfig, -]; diff --git a/packages/settings/package.json b/packages/settings/package.json index ab2e62a2e..18f93e280 100644 --- a/packages/settings/package.json +++ b/packages/settings/package.json @@ -4,10 +4,6 @@ "private": true, "license": "Apache-2.0", "type": "module", - "exports": { - ".": "./index.ts", - "./creator": "./src/creator.ts" - }, "typesVersions": { "*": { "*": [ @@ -15,13 +11,16 @@ ] } }, + "exports": { + ".": "./index.ts", + "./creator": "./src/creator.ts" + }, "scripts": { "clean": "rm -rf .turbo node_modules", - "format": "prettier --check . --ignore-path ../../.gitignore", - "lint": "eslint", + "format": "oxfmt .", + "lint": "oxlint", "typecheck": "tsc --noEmit" }, - "prettier": "@homarr/prettier-config", "dependencies": { "@homarr/db": "workspace:^0.1.0", "@homarr/server-settings": "workspace:^0.1.0", @@ -31,10 +30,7 @@ "react-dom": "catalog:" }, "devDependencies": { - "@homarr/eslint-config": "workspace:^0.2.0", - "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "catalog:", "typescript": "catalog:" } } diff --git a/packages/spotlight/eslint.config.js b/packages/spotlight/eslint.config.js deleted file mode 100644 index 1df6129af..000000000 --- a/packages/spotlight/eslint.config.js +++ /dev/null @@ -1,5 +0,0 @@ -import baseConfig from "@homarr/eslint-config/base"; -import reactConfig from "@homarr/eslint-config/react"; - -/** @type {import('typescript-eslint').Config} */ -export default [...baseConfig, ...reactConfig]; diff --git a/packages/spotlight/package.json b/packages/spotlight/package.json index c7903ee49..39868e02e 100644 --- a/packages/spotlight/package.json +++ b/packages/spotlight/package.json @@ -4,10 +4,6 @@ "private": true, "license": "Apache-2.0", "type": "module", - "exports": { - ".": "./index.ts", - "./styles.css": "./src/styles.css" - }, "typesVersions": { "*": { "*": [ @@ -15,13 +11,16 @@ ] } }, + "exports": { + ".": "./index.ts", + "./styles.css": "./src/styles.css" + }, "scripts": { "clean": "rm -rf .turbo node_modules", - "format": "prettier --check . --ignore-path ../../.gitignore", - "lint": "eslint", + "format": "oxfmt .", + "lint": "oxlint", "typecheck": "tsc --noEmit" }, - "prettier": "@homarr/prettier-config", "dependencies": { "@homarr/api": "workspace:^0.1.0", "@homarr/auth": "workspace:^0.1.0", @@ -44,10 +43,7 @@ "use-deep-compare-effect": "catalog:" }, "devDependencies": { - "@homarr/eslint-config": "workspace:^0.2.0", - "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "catalog:", "typescript": "catalog:" } } diff --git a/packages/spotlight/src/components/spotlight.tsx b/packages/spotlight/src/components/spotlight.tsx index c90542f45..4ca086f3b 100644 --- a/packages/spotlight/src/components/spotlight.tsx +++ b/packages/spotlight/src/components/spotlight.tsx @@ -110,6 +110,7 @@ const SpotlightWithActiveMode = ({ modeState, queryState, activeMode }: Spotligh inputRef.current?.focus(); }} variant="subtle" + aria-label={t("common.action.close")} > diff --git a/packages/translation/eslint.config.js b/packages/translation/eslint.config.js deleted file mode 100644 index f7a5a7d36..000000000 --- a/packages/translation/eslint.config.js +++ /dev/null @@ -1,4 +0,0 @@ -import baseConfig from "@homarr/eslint-config/base"; - -/** @type {import('typescript-eslint').Config} */ -export default [...baseConfig]; diff --git a/packages/translation/package.json b/packages/translation/package.json index e8e42b749..4dc9d8cd6 100644 --- a/packages/translation/package.json +++ b/packages/translation/package.json @@ -4,6 +4,13 @@ "private": true, "license": "Apache-2.0", "type": "module", + "typesVersions": { + "*": { + "*": [ + "src/*" + ] + } + }, "exports": { ".": "./index.ts", "./client": "./src/client/index.ts", @@ -12,20 +19,12 @@ "./request": "./src/request.ts", "./dayjs": "./src/dayjs.ts" }, - "typesVersions": { - "*": { - "*": [ - "src/*" - ] - } - }, "scripts": { "clean": "rm -rf .turbo node_modules", - "format": "prettier --check . --ignore-path ../../.gitignore", - "lint": "eslint", + "format": "oxfmt .", + "lint": "oxlint", "typecheck": "tsc --noEmit" }, - "prettier": "@homarr/prettier-config", "dependencies": { "@homarr/common": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0", @@ -38,10 +37,7 @@ "react-dom": "catalog:" }, "devDependencies": { - "@homarr/eslint-config": "workspace:^0.2.0", - "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "catalog:", "typescript": "catalog:" } } diff --git a/packages/translation/src/lang/ca.json b/packages/translation/src/lang/ca.json index 6cbd7acf6..3e5c14996 100644 --- a/packages/translation/src/lang/ca.json +++ b/packages/translation/src/lang/ca.json @@ -1054,6 +1054,9 @@ "open": { "label": "" } + }, + "noResults": { + "title": "" } }, "common": { @@ -1084,7 +1087,9 @@ "checkoutDocs": "", "checkLogs": "", "tryAgain": "", - "loading": "" + "loading": "", + "show": "", + "hide": "" }, "here": "", "iconPicker": { @@ -1249,7 +1254,8 @@ "menu": { "label": { "create": "", - "changePosition": "" + "changePosition": "", + "settings": "" } }, "openAllInNewTabs": { @@ -3452,6 +3458,9 @@ "public": "", "private": "" }, + "noResults": { + "title": "" + }, "modal": { "createBoard": { "field": { diff --git a/packages/translation/src/lang/cn.json b/packages/translation/src/lang/cn.json index 3a10e9210..05caccbe2 100644 --- a/packages/translation/src/lang/cn.json +++ b/packages/translation/src/lang/cn.json @@ -1054,6 +1054,9 @@ "open": { "label": "打开媒体" } + }, + "noResults": { + "title": "" } }, "common": { @@ -1084,7 +1087,9 @@ "checkoutDocs": "查阅文档", "checkLogs": "检查日志获取更多详情", "tryAgain": "重试", - "loading": "正在加载" + "loading": "正在加载", + "show": "", + "hide": "" }, "here": "这里", "iconPicker": { @@ -1249,7 +1254,8 @@ "menu": { "label": { "create": "新建分类", - "changePosition": "换位" + "changePosition": "换位", + "settings": "" } }, "openAllInNewTabs": { @@ -3452,6 +3458,9 @@ "public": "此面板是公开的", "private": "此面板是私有的" }, + "noResults": { + "title": "" + }, "modal": { "createBoard": { "field": { diff --git a/packages/translation/src/lang/cr.json b/packages/translation/src/lang/cr.json index 27a4588a8..ab1fec215 100644 --- a/packages/translation/src/lang/cr.json +++ b/packages/translation/src/lang/cr.json @@ -1054,6 +1054,9 @@ "open": { "label": "crwdns2680:0crwdne2680:0" } + }, + "noResults": { + "title": "crwdns4251:0crwdne4251:0" } }, "common": { @@ -1084,7 +1087,9 @@ "checkoutDocs": "crwdns546:0crwdne546:0", "checkLogs": "crwdns2260:0crwdne2260:0", "tryAgain": "crwdns548:0crwdne548:0", - "loading": "crwdns550:0crwdne550:0" + "loading": "crwdns550:0crwdne550:0", + "show": "crwdns4255:0crwdne4255:0", + "hide": "crwdns4257:0crwdne4257:0" }, "here": "crwdns552:0crwdne552:0", "iconPicker": { @@ -1249,7 +1254,8 @@ "menu": { "label": { "create": "crwdns686:0crwdne686:0", - "changePosition": "crwdns688:0crwdne688:0" + "changePosition": "crwdns688:0crwdne688:0", + "settings": "crwdns4259:0crwdne4259:0" } }, "openAllInNewTabs": { @@ -3452,6 +3458,9 @@ "public": "crwdns1656:0crwdne1656:0", "private": "crwdns1658:0crwdne1658:0" }, + "noResults": { + "title": "crwdns4253:0crwdne4253:0" + }, "modal": { "createBoard": { "field": { diff --git a/packages/translation/src/lang/cs.json b/packages/translation/src/lang/cs.json index 46d284ee5..73bd6774f 100644 --- a/packages/translation/src/lang/cs.json +++ b/packages/translation/src/lang/cs.json @@ -1054,6 +1054,9 @@ "open": { "label": "" } + }, + "noResults": { + "title": "" } }, "common": { @@ -1084,7 +1087,9 @@ "checkoutDocs": "Přečtěte si dokumentaci", "checkLogs": "Pro více podrobností zkontrolujte logy", "tryAgain": "Zkusit znovu", - "loading": "Načítání" + "loading": "Načítání", + "show": "", + "hide": "" }, "here": "zde", "iconPicker": { @@ -1249,7 +1254,8 @@ "menu": { "label": { "create": "Nová kategorie", - "changePosition": "Změnit pozici" + "changePosition": "Změnit pozici", + "settings": "" } }, "openAllInNewTabs": { @@ -3452,6 +3458,9 @@ "public": "", "private": "" }, + "noResults": { + "title": "" + }, "modal": { "createBoard": { "field": { diff --git a/packages/translation/src/lang/da.json b/packages/translation/src/lang/da.json index 09d071783..25088e56d 100644 --- a/packages/translation/src/lang/da.json +++ b/packages/translation/src/lang/da.json @@ -1054,6 +1054,9 @@ "open": { "label": "Åben medie" } + }, + "noResults": { + "title": "" } }, "common": { @@ -1084,7 +1087,9 @@ "checkoutDocs": "Tjek dokumentationen", "checkLogs": "Tjek logs for flere detaljer", "tryAgain": "Prøv igen", - "loading": "Indlæser" + "loading": "Indlæser", + "show": "", + "hide": "" }, "here": "her", "iconPicker": { @@ -1249,7 +1254,8 @@ "menu": { "label": { "create": "Ny kategori", - "changePosition": "Ændre placering" + "changePosition": "Ændre placering", + "settings": "" } }, "openAllInNewTabs": { @@ -3452,6 +3458,9 @@ "public": "Denne tavle er offentlig", "private": "Denne tavle er privat" }, + "noResults": { + "title": "" + }, "modal": { "createBoard": { "field": { diff --git a/packages/translation/src/lang/de-CH.json b/packages/translation/src/lang/de-CH.json index 024e95740..240cc8a8d 100644 --- a/packages/translation/src/lang/de-CH.json +++ b/packages/translation/src/lang/de-CH.json @@ -1054,6 +1054,9 @@ "open": { "label": "Medium öffnen" } + }, + "noResults": { + "title": "" } }, "common": { @@ -1084,7 +1087,9 @@ "checkoutDocs": "Die Dokumentation ansehen", "checkLogs": "Logs für weitere Details prüfen", "tryAgain": "Erneut versuchen", - "loading": "Wird geladen" + "loading": "Wird geladen", + "show": "", + "hide": "" }, "here": "Hier", "iconPicker": { @@ -1249,7 +1254,8 @@ "menu": { "label": { "create": "Neue Kategorie", - "changePosition": "Position wechseln" + "changePosition": "Position wechseln", + "settings": "" } }, "openAllInNewTabs": { @@ -3452,6 +3458,9 @@ "public": "Dieses Board ist öffentlich", "private": "Dieses Board ist privat" }, + "noResults": { + "title": "" + }, "modal": { "createBoard": { "field": { diff --git a/packages/translation/src/lang/de.json b/packages/translation/src/lang/de.json index db0479ba6..1ac3c8dcb 100644 --- a/packages/translation/src/lang/de.json +++ b/packages/translation/src/lang/de.json @@ -1054,6 +1054,9 @@ "open": { "label": "Medium öffnen" } + }, + "noResults": { + "title": "" } }, "common": { @@ -1084,7 +1087,9 @@ "checkoutDocs": "Die Dokumentation ansehen", "checkLogs": "Logs für weitere Details prüfen", "tryAgain": "Erneut versuchen", - "loading": "Wird geladen" + "loading": "Wird geladen", + "show": "", + "hide": "" }, "here": "Hier", "iconPicker": { @@ -1249,7 +1254,8 @@ "menu": { "label": { "create": "Neue Kategorie", - "changePosition": "Position wechseln" + "changePosition": "Position wechseln", + "settings": "" } }, "openAllInNewTabs": { @@ -3452,6 +3458,9 @@ "public": "Dieses Board ist öffentlich", "private": "Dieses Board ist privat" }, + "noResults": { + "title": "" + }, "modal": { "createBoard": { "field": { diff --git a/packages/translation/src/lang/el.json b/packages/translation/src/lang/el.json index 9c9b49876..2e86228dd 100644 --- a/packages/translation/src/lang/el.json +++ b/packages/translation/src/lang/el.json @@ -1054,6 +1054,9 @@ "open": { "label": "" } + }, + "noResults": { + "title": "" } }, "common": { @@ -1084,7 +1087,9 @@ "checkoutDocs": "", "checkLogs": "", "tryAgain": "Προσπαθήστε ξανά", - "loading": "" + "loading": "", + "show": "", + "hide": "" }, "here": "", "iconPicker": { @@ -1249,7 +1254,8 @@ "menu": { "label": { "create": "", - "changePosition": "Αλλαγή θέσης" + "changePosition": "Αλλαγή θέσης", + "settings": "" } }, "openAllInNewTabs": { @@ -3452,6 +3458,9 @@ "public": "", "private": "" }, + "noResults": { + "title": "" + }, "modal": { "createBoard": { "field": { diff --git a/packages/translation/src/lang/en-gb.json b/packages/translation/src/lang/en-gb.json index ae6870d90..04bc5a03c 100644 --- a/packages/translation/src/lang/en-gb.json +++ b/packages/translation/src/lang/en-gb.json @@ -1054,6 +1054,9 @@ "open": { "label": "Open media" } + }, + "noResults": { + "title": "" } }, "common": { @@ -1084,7 +1087,9 @@ "checkoutDocs": "Check the documentation", "checkLogs": "Check logs for more information", "tryAgain": "Try again", - "loading": "Loading" + "loading": "Loading", + "show": "", + "hide": "" }, "here": "here", "iconPicker": { @@ -1249,7 +1254,8 @@ "menu": { "label": { "create": "New category", - "changePosition": "Change position" + "changePosition": "Change position", + "settings": "" } }, "openAllInNewTabs": { @@ -3452,6 +3458,9 @@ "public": "", "private": "" }, + "noResults": { + "title": "" + }, "modal": { "createBoard": { "field": { diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index 29e86458e..a62728845 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -1054,6 +1054,9 @@ "open": { "label": "Open media" } + }, + "noResults": { + "title": "No media uploaded" } }, "common": { @@ -1084,7 +1087,9 @@ "checkoutDocs": "Check out the documentation", "checkLogs": "Check logs for more details", "tryAgain": "Try again", - "loading": "Loading" + "loading": "Loading", + "show": "Show", + "hide": "Hide" }, "here": "here", "iconPicker": { @@ -1249,7 +1254,8 @@ "menu": { "label": { "create": "New category", - "changePosition": "Change position" + "changePosition": "Change position", + "settings": "Settings" } }, "openAllInNewTabs": { @@ -3452,6 +3458,9 @@ "public": "This board is public", "private": "This board is private" }, + "noResults": { + "title": "No boards yet" + }, "modal": { "createBoard": { "field": { diff --git a/packages/translation/src/lang/es.json b/packages/translation/src/lang/es.json index cbbb17abd..148c022dd 100644 --- a/packages/translation/src/lang/es.json +++ b/packages/translation/src/lang/es.json @@ -1054,6 +1054,9 @@ "open": { "label": "Abrir imagen" } + }, + "noResults": { + "title": "" } }, "common": { @@ -1084,7 +1087,9 @@ "checkoutDocs": "Consultar la documentación", "checkLogs": "Comprueba los registros para más detalles", "tryAgain": "Intentar de nuevo", - "loading": "Cargando" + "loading": "Cargando", + "show": "", + "hide": "" }, "here": "aquí", "iconPicker": { @@ -1249,7 +1254,8 @@ "menu": { "label": { "create": "Nueva Categoría", - "changePosition": "Cambiar posición" + "changePosition": "Cambiar posición", + "settings": "" } }, "openAllInNewTabs": { @@ -3452,6 +3458,9 @@ "public": "Este tablero es público", "private": "Este tablero es privado" }, + "noResults": { + "title": "" + }, "modal": { "createBoard": { "field": { diff --git a/packages/translation/src/lang/et.json b/packages/translation/src/lang/et.json index d48280e5c..c5e54f495 100644 --- a/packages/translation/src/lang/et.json +++ b/packages/translation/src/lang/et.json @@ -1054,6 +1054,9 @@ "open": { "label": "" } + }, + "noResults": { + "title": "" } }, "common": { @@ -1084,7 +1087,9 @@ "checkoutDocs": "", "checkLogs": "", "tryAgain": "", - "loading": "" + "loading": "", + "show": "", + "hide": "" }, "here": "", "iconPicker": { @@ -1249,7 +1254,8 @@ "menu": { "label": { "create": "", - "changePosition": "" + "changePosition": "", + "settings": "" } }, "openAllInNewTabs": { @@ -3452,6 +3458,9 @@ "public": "", "private": "" }, + "noResults": { + "title": "" + }, "modal": { "createBoard": { "field": { diff --git a/packages/translation/src/lang/fr.json b/packages/translation/src/lang/fr.json index a28b4718b..2f173a25c 100644 --- a/packages/translation/src/lang/fr.json +++ b/packages/translation/src/lang/fr.json @@ -1054,6 +1054,9 @@ "open": { "label": "Ouvrir le média" } + }, + "noResults": { + "title": "" } }, "common": { @@ -1084,7 +1087,9 @@ "checkoutDocs": "Voir la documentation", "checkLogs": "Vérifiez les logs pour plus de détails", "tryAgain": "Réessayer", - "loading": "Chargement" + "loading": "Chargement", + "show": "", + "hide": "" }, "here": "ici", "iconPicker": { @@ -1249,7 +1254,8 @@ "menu": { "label": { "create": "Nouvelle catégorie", - "changePosition": "Modifier la position" + "changePosition": "Modifier la position", + "settings": "" } }, "openAllInNewTabs": { @@ -3452,6 +3458,9 @@ "public": "Ce tableau est public", "private": "Ce tableau est privé" }, + "noResults": { + "title": "" + }, "modal": { "createBoard": { "field": { diff --git a/packages/translation/src/lang/he.json b/packages/translation/src/lang/he.json index b23563ce9..86c2e8ee3 100644 --- a/packages/translation/src/lang/he.json +++ b/packages/translation/src/lang/he.json @@ -1054,6 +1054,9 @@ "open": { "label": "פתיחת מדיה" } + }, + "noResults": { + "title": "" } }, "common": { @@ -1084,7 +1087,9 @@ "checkoutDocs": "בדוק את התיעוד", "checkLogs": "בדוק לוגים לפרטים נוספים", "tryAgain": "נא לנסות שוב", - "loading": "טוען" + "loading": "טוען", + "show": "", + "hide": "" }, "here": "כאן", "iconPicker": { @@ -1249,7 +1254,8 @@ "menu": { "label": { "create": "קטגוריה חדשה", - "changePosition": "שנה מיקום" + "changePosition": "שנה מיקום", + "settings": "" } }, "openAllInNewTabs": { @@ -3452,6 +3458,9 @@ "public": "הלוח הזה ציבורי", "private": "הלוח הזה פרטי" }, + "noResults": { + "title": "" + }, "modal": { "createBoard": { "field": { diff --git a/packages/translation/src/lang/hr.json b/packages/translation/src/lang/hr.json index 9558b4bfa..dd637c9a6 100644 --- a/packages/translation/src/lang/hr.json +++ b/packages/translation/src/lang/hr.json @@ -1054,6 +1054,9 @@ "open": { "label": "" } + }, + "noResults": { + "title": "" } }, "common": { @@ -1084,7 +1087,9 @@ "checkoutDocs": "", "checkLogs": "", "tryAgain": "Pokušaj ponovno", - "loading": "" + "loading": "", + "show": "", + "hide": "" }, "here": "", "iconPicker": { @@ -1249,7 +1254,8 @@ "menu": { "label": { "create": "", - "changePosition": "Promijenjen položaj" + "changePosition": "Promijenjen položaj", + "settings": "" } }, "openAllInNewTabs": { @@ -3452,6 +3458,9 @@ "public": "", "private": "" }, + "noResults": { + "title": "" + }, "modal": { "createBoard": { "field": { diff --git a/packages/translation/src/lang/hu.json b/packages/translation/src/lang/hu.json index 44f4a8575..1bad936e8 100644 --- a/packages/translation/src/lang/hu.json +++ b/packages/translation/src/lang/hu.json @@ -1054,6 +1054,9 @@ "open": { "label": "Média megnyitása" } + }, + "noResults": { + "title": "" } }, "common": { @@ -1084,7 +1087,9 @@ "checkoutDocs": "Tekintse meg a dokumentációt", "checkLogs": "További részletekért ellenőrizze a naplókat", "tryAgain": "Próbálja újra", - "loading": "Töltés" + "loading": "Töltés", + "show": "", + "hide": "" }, "here": "itt", "iconPicker": { @@ -1249,7 +1254,8 @@ "menu": { "label": { "create": "Új kategória", - "changePosition": "Pozíció módosítása" + "changePosition": "Pozíció módosítása", + "settings": "" } }, "openAllInNewTabs": { @@ -3452,6 +3458,9 @@ "public": "", "private": "" }, + "noResults": { + "title": "" + }, "modal": { "createBoard": { "field": { diff --git a/packages/translation/src/lang/it.json b/packages/translation/src/lang/it.json index 60720ad7c..7a2202367 100644 --- a/packages/translation/src/lang/it.json +++ b/packages/translation/src/lang/it.json @@ -1054,6 +1054,9 @@ "open": { "label": "Apri media" } + }, + "noResults": { + "title": "" } }, "common": { @@ -1084,7 +1087,9 @@ "checkoutDocs": "Consulta la documentazione", "checkLogs": "Controlla i log per maggiori dettagli", "tryAgain": "Riprova", - "loading": "Caricamento" + "loading": "Caricamento", + "show": "", + "hide": "" }, "here": "qui", "iconPicker": { @@ -1249,7 +1254,8 @@ "menu": { "label": { "create": "Nuova categoria", - "changePosition": "Cambia posizione" + "changePosition": "Cambia posizione", + "settings": "" } }, "openAllInNewTabs": { @@ -3452,6 +3458,9 @@ "public": "Questa board è pubblica", "private": "Questa board è privata" }, + "noResults": { + "title": "" + }, "modal": { "createBoard": { "field": { diff --git a/packages/translation/src/lang/ja.json b/packages/translation/src/lang/ja.json index 752a3a529..aacb952c6 100644 --- a/packages/translation/src/lang/ja.json +++ b/packages/translation/src/lang/ja.json @@ -1054,6 +1054,9 @@ "open": { "label": "メディアを開く" } + }, + "noResults": { + "title": "" } }, "common": { @@ -1084,7 +1087,9 @@ "checkoutDocs": "ドキュメントを確認する", "checkLogs": "詳細はログを確認してください", "tryAgain": "リトライ", - "loading": "読み込み中" + "loading": "読み込み中", + "show": "", + "hide": "" }, "here": "こちら", "iconPicker": { @@ -1249,7 +1254,8 @@ "menu": { "label": { "create": "新しいカテゴリー", - "changePosition": "ポジションを変更する" + "changePosition": "ポジションを変更する", + "settings": "" } }, "openAllInNewTabs": { @@ -3452,6 +3458,9 @@ "public": "このボードは公開中です", "private": "このボードは非公開です" }, + "noResults": { + "title": "" + }, "modal": { "createBoard": { "field": { diff --git a/packages/translation/src/lang/ko.json b/packages/translation/src/lang/ko.json index 868611bd4..6a8639ffd 100644 --- a/packages/translation/src/lang/ko.json +++ b/packages/translation/src/lang/ko.json @@ -1054,6 +1054,9 @@ "open": { "label": "" } + }, + "noResults": { + "title": "" } }, "common": { @@ -1084,7 +1087,9 @@ "checkoutDocs": "", "checkLogs": "", "tryAgain": "다시 시도", - "loading": "" + "loading": "", + "show": "", + "hide": "" }, "here": "", "iconPicker": { @@ -1249,7 +1254,8 @@ "menu": { "label": { "create": "", - "changePosition": "위치 변경" + "changePosition": "위치 변경", + "settings": "" } }, "openAllInNewTabs": { @@ -3452,6 +3458,9 @@ "public": "", "private": "" }, + "noResults": { + "title": "" + }, "modal": { "createBoard": { "field": { diff --git a/packages/translation/src/lang/lt.json b/packages/translation/src/lang/lt.json index 5b165bb86..6811b62e5 100644 --- a/packages/translation/src/lang/lt.json +++ b/packages/translation/src/lang/lt.json @@ -1054,6 +1054,9 @@ "open": { "label": "" } + }, + "noResults": { + "title": "" } }, "common": { @@ -1084,7 +1087,9 @@ "checkoutDocs": "", "checkLogs": "", "tryAgain": "", - "loading": "" + "loading": "", + "show": "", + "hide": "" }, "here": "", "iconPicker": { @@ -1249,7 +1254,8 @@ "menu": { "label": { "create": "", - "changePosition": "" + "changePosition": "", + "settings": "" } }, "openAllInNewTabs": { @@ -3452,6 +3458,9 @@ "public": "", "private": "" }, + "noResults": { + "title": "" + }, "modal": { "createBoard": { "field": { diff --git a/packages/translation/src/lang/lv.json b/packages/translation/src/lang/lv.json index 762f7bff7..e3afee8da 100644 --- a/packages/translation/src/lang/lv.json +++ b/packages/translation/src/lang/lv.json @@ -1054,6 +1054,9 @@ "open": { "label": "" } + }, + "noResults": { + "title": "" } }, "common": { @@ -1084,7 +1087,9 @@ "checkoutDocs": "", "checkLogs": "", "tryAgain": "Mēģiniet vēlreiz", - "loading": "" + "loading": "", + "show": "", + "hide": "" }, "here": "", "iconPicker": { @@ -1249,7 +1254,8 @@ "menu": { "label": { "create": "", - "changePosition": "Mainīt pozīciju" + "changePosition": "Mainīt pozīciju", + "settings": "" } }, "openAllInNewTabs": { @@ -3452,6 +3458,9 @@ "public": "", "private": "" }, + "noResults": { + "title": "" + }, "modal": { "createBoard": { "field": { diff --git a/packages/translation/src/lang/nl.json b/packages/translation/src/lang/nl.json index c8fb6c654..bf16ada09 100644 --- a/packages/translation/src/lang/nl.json +++ b/packages/translation/src/lang/nl.json @@ -1054,6 +1054,9 @@ "open": { "label": "Media openen" } + }, + "noResults": { + "title": "" } }, "common": { @@ -1084,7 +1087,9 @@ "checkoutDocs": "De documentatie bekijken", "checkLogs": "Controleer logs voor meer details", "tryAgain": "Probeer opnieuw", - "loading": "Laden" + "loading": "Laden", + "show": "", + "hide": "" }, "here": "hier", "iconPicker": { @@ -1249,7 +1254,8 @@ "menu": { "label": { "create": "Nieuwe categorie", - "changePosition": "Positie wijzigen" + "changePosition": "Positie wijzigen", + "settings": "" } }, "openAllInNewTabs": { @@ -3452,6 +3458,9 @@ "public": "Dit bord is openbaar", "private": "Dit bord is privé" }, + "noResults": { + "title": "" + }, "modal": { "createBoard": { "field": { diff --git a/packages/translation/src/lang/no.json b/packages/translation/src/lang/no.json index 224f8c0a0..4badb0f64 100644 --- a/packages/translation/src/lang/no.json +++ b/packages/translation/src/lang/no.json @@ -1054,6 +1054,9 @@ "open": { "label": "Åpne medie" } + }, + "noResults": { + "title": "" } }, "common": { @@ -1084,7 +1087,9 @@ "checkoutDocs": "Les dokumentasjonen", "checkLogs": "Sjekk loggene for mer informasjon", "tryAgain": "Prøv igjen", - "loading": "Laster" + "loading": "Laster", + "show": "", + "hide": "" }, "here": "her", "iconPicker": { @@ -1249,7 +1254,8 @@ "menu": { "label": { "create": "Ny kategori", - "changePosition": "Endre posisjon" + "changePosition": "Endre posisjon", + "settings": "" } }, "openAllInNewTabs": { @@ -3452,6 +3458,9 @@ "public": "Denne tavlen er offentlig", "private": "Denne tavlen er privat" }, + "noResults": { + "title": "" + }, "modal": { "createBoard": { "field": { diff --git a/packages/translation/src/lang/pl.json b/packages/translation/src/lang/pl.json index c93bb30d8..ead2f3653 100644 --- a/packages/translation/src/lang/pl.json +++ b/packages/translation/src/lang/pl.json @@ -1054,6 +1054,9 @@ "open": { "label": "Otwórz media" } + }, + "noResults": { + "title": "" } }, "common": { @@ -1084,7 +1087,9 @@ "checkoutDocs": "Sprawdź dokumentację", "checkLogs": "Sprawdź logi, aby uzyskać więcej informacji.", "tryAgain": "Spróbuj ponownie", - "loading": "Ładowanie" + "loading": "Ładowanie", + "show": "", + "hide": "" }, "here": "tutaj", "iconPicker": { @@ -1249,7 +1254,8 @@ "menu": { "label": { "create": "Nowa kategoria", - "changePosition": "Zmiana pozycji" + "changePosition": "Zmiana pozycji", + "settings": "" } }, "openAllInNewTabs": { @@ -3452,6 +3458,9 @@ "public": "Ta tablica jest publiczna", "private": "Ta tablica jest prywatna" }, + "noResults": { + "title": "" + }, "modal": { "createBoard": { "field": { diff --git a/packages/translation/src/lang/pt-br.json b/packages/translation/src/lang/pt-br.json index 0e130afac..a436eac1d 100644 --- a/packages/translation/src/lang/pt-br.json +++ b/packages/translation/src/lang/pt-br.json @@ -159,7 +159,7 @@ "label": "Abrir resultados da busca em uma nova guia" }, "ddgBangs": { - "label": "" + "label": "Ativar bangs do DuckDuckGo" } }, "error": { @@ -252,10 +252,10 @@ "changeDdgBangs": { "notification": { "success": { - "message": "" + "message": "Bangs do DuckDuckGo alternados com sucesso" }, "error": { - "message": "" + "message": "Não foi possível alternar os bangs do DuckDuckGo" } } }, @@ -1054,6 +1054,9 @@ "open": { "label": "Abrir mídia" } + }, + "noResults": { + "title": "" } }, "common": { @@ -1084,7 +1087,9 @@ "checkoutDocs": "Confira a documentação", "checkLogs": "Verifique os logs para mais detalhes", "tryAgain": "Tente novamente", - "loading": "Carregando" + "loading": "Carregando", + "show": "", + "hide": "" }, "here": "aqui", "iconPicker": { @@ -1093,7 +1098,7 @@ }, "colorScheme": { "options": { - "auto": "", + "auto": "Automático", "light": "Claro", "dark": "Escuro" } @@ -1249,7 +1254,8 @@ "menu": { "label": { "create": "Nova categoria", - "changePosition": "Mudar de posição" + "changePosition": "Mudar de posição", + "settings": "" } }, "openAllInNewTabs": { @@ -2111,101 +2117,101 @@ } }, "umami": { - "name": "", - "description": "", - "visitors": "", - "active": "", - "pageviews": "", - "visits": "", - "bounceRate": "", - "avgDuration": "", + "name": "Umami Analytics", + "description": "Exibir estatísticas de visitantes da sua instância do Umami", + "visitors": "visitantes", + "active": "ativos", + "pageviews": "visualizações de página", + "visits": "visitas", + "bounceRate": "rejeição", + "avgDuration": "média", "option": { "websiteId": { - "label": "", - "description": "", - "placeholder": "", - "loading": "", - "noWebsites": "", - "configureIntegrationFirst": "" + "label": "site", + "description": "Selecione um site monitorado pela sua instância do Umami", + "placeholder": "Selecionar um site", + "loading": "Carregando sites...", + "noWebsites": "Nenhum site encontrado", + "configureIntegrationFirst": "Conecte uma integração do Umami primeiro" }, "timeFrame": { - "label": "", - "description": "", + "label": "Período de tempo", + "description": "O período de tempo para exibir os dados de visitantes", "option": { - "today": "", - "24h": "", - "7d": "", - "30d": "", - "month": "", - "lastMonth": "" + "today": "Hoje", + "24h": "Últimas 24 horas", + "7d": "Últimos 7 dias", + "30d": "Últimos 30 dias", + "month": "Este mês", + "lastMonth": "Mês passado" } }, "eventName": { - "label": "", - "description": "", - "placeholder": "", - "loading": "", - "noEvents": "", - "none": "", - "configureWebsiteFirst": "" + "label": "Evento", + "description": "Exibir total de ocorrências de um evento específico", + "placeholder": "Selecionar um evento", + "loading": "Carregando eventos...", + "noEvents": "Nenhum evento encontrado", + "none": "Nenhum evento", + "configureWebsiteFirst": "Selecione um site primeiro para carregar os eventos" }, "chartType": { - "label": "", - "description": "", + "label": "Tipo de gráfico", + "description": "Estilo de exibição do gráfico", "option": { - "bar": "", - "sparkline": "" + "bar": "Gráfico de barras", + "sparkline": "Sparkline" } }, "chartStyle": { - "label": "", - "description": "", + "label": "Estilo do gráfico", + "description": "Como exibir a série de eventos no gráfico", "option": { - "grouped": "", - "overlay": "" + "grouped": "Lado a lado", + "overlay": "Sobreposição (subconjunto)" } }, "viewMode": { - "label": "", - "description": "", + "label": "Visualização", + "description": "O que exibir no corpo do widget", "option": { - "chart": "", - "events": "", - "topPages": "", - "topReferrers": "" + "chart": "Gráfico", + "events": "Gráfico de eventos", + "topPages": "Principais páginas", + "topReferrers": "Principais referenciadores" } }, "eventNames": { - "label": "", - "description": "", - "placeholder": "", - "loading": "", - "noEvents": "", - "configureWebsiteFirst": "" + "label": "Eventos", + "description": "Selecione um ou mais eventos para o gráfico", + "placeholder": "Selecionar eventos...", + "loading": "Carregando eventos...", + "noEvents": "Nenhum evento encontrado", + "configureWebsiteFirst": "Selecione um site primeiro para carregar os eventos" }, "topCount": { - "label": "", - "description": "" + "label": "Itens para exibir", + "description": "Número de itens principais para exibir (máx 500)" } }, "event": { - "occurrences": "", - "label": "" + "occurrences": "Eventos", + "label": "Eventos" }, "topPages": { - "direct": "" + "direct": "Direto" }, "topReferrers": { - "direct": "" + "direct": "Direto" }, "events": { - "selectPrompt": "" + "selectPrompt": "Selecionar eventos para o gráfico" }, "error": { - "noIntegration": "", - "noWebsite": "", - "noData": "", - "internalServerError": "" + "noIntegration": "Nenhuma integração Umami configurada", + "noWebsite": "Nenhum site configurado", + "noData": "Nenhum dado disponível", + "internalServerError": "Falha ao buscar dados do Umami" } }, "common": { @@ -3452,6 +3458,9 @@ "public": "Este quadro é publico", "private": "Este quadro é privado" }, + "noResults": { + "title": "" + }, "modal": { "createBoard": { "field": { @@ -3680,7 +3689,7 @@ "defaultColorScheme": { "label": "Esquema de cores padrão", "options": { - "auto": "", + "auto": "Automático", "light": "Claro", "dark": "Escuro" } @@ -3783,7 +3792,7 @@ "label": "Tracearr" }, "umamiActiveVisitors": { - "label": "" + "label": "Visitantes Ativos do Umami" } }, "interval": { @@ -3972,7 +3981,7 @@ "label": "Portas" }, "host": { - "label": "" + "label": "Host" } }, "action": { @@ -4499,7 +4508,7 @@ "title": "Comandos globais", "option": { "colorScheme": { - "auto": "", + "auto": "Alternar modo de aparência", "light": "Alternar para o modo claro", "dark": "Alternar para o modo escuro" }, @@ -4551,13 +4560,13 @@ "group": { "searchEngine": { "title": "Mecanismos de busca", - "tipDdgBangs": "", + "tipDdgBangs": "Dica: Você também pode usar qualquer bang do DuckDuckGo aqui, ex: !something", "bang": { - "searchWithQuery": "", - "pressEnterToOpen": "", - "engineSelected": "", - "typeQueryToContinue": "", - "emptyBangHint": "" + "searchWithQuery": "Pesquisar \"{query}\" com {label}", + "pressEnterToOpen": "Pressione Enter para abrir", + "engineSelected": "{label} selecionado (!{bang})", + "typeQueryToContinue": "Digite sua pesquisa para continuar", + "emptyBangHint": "Digite um bang, ex: !yt, e pressione Espaço para selecionar" }, "children": { "action": { diff --git a/packages/translation/src/lang/pt.json b/packages/translation/src/lang/pt.json index 4d6cbd7f0..5d749ae0d 100644 --- a/packages/translation/src/lang/pt.json +++ b/packages/translation/src/lang/pt.json @@ -1054,6 +1054,9 @@ "open": { "label": "" } + }, + "noResults": { + "title": "" } }, "common": { @@ -1084,7 +1087,9 @@ "checkoutDocs": "", "checkLogs": "", "tryAgain": "", - "loading": "" + "loading": "", + "show": "", + "hide": "" }, "here": "", "iconPicker": { @@ -1249,7 +1254,8 @@ "menu": { "label": { "create": "", - "changePosition": "" + "changePosition": "", + "settings": "" } }, "openAllInNewTabs": { @@ -3452,6 +3458,9 @@ "public": "", "private": "" }, + "noResults": { + "title": "" + }, "modal": { "createBoard": { "field": { diff --git a/packages/translation/src/lang/ro.json b/packages/translation/src/lang/ro.json index 559f78e58..d4244df64 100644 --- a/packages/translation/src/lang/ro.json +++ b/packages/translation/src/lang/ro.json @@ -1054,6 +1054,9 @@ "open": { "label": "" } + }, + "noResults": { + "title": "" } }, "common": { @@ -1084,7 +1087,9 @@ "checkoutDocs": "", "checkLogs": "", "tryAgain": "Încearcă din nou", - "loading": "" + "loading": "", + "show": "", + "hide": "" }, "here": "", "iconPicker": { @@ -1249,7 +1254,8 @@ "menu": { "label": { "create": "", - "changePosition": "Schimbă locația" + "changePosition": "Schimbă locația", + "settings": "" } }, "openAllInNewTabs": { @@ -3452,6 +3458,9 @@ "public": "", "private": "" }, + "noResults": { + "title": "" + }, "modal": { "createBoard": { "field": { diff --git a/packages/translation/src/lang/ru.json b/packages/translation/src/lang/ru.json index 11d073d13..2be2fdabb 100644 --- a/packages/translation/src/lang/ru.json +++ b/packages/translation/src/lang/ru.json @@ -1054,6 +1054,9 @@ "open": { "label": "Открыть медиа" } + }, + "noResults": { + "title": "" } }, "common": { @@ -1084,7 +1087,9 @@ "checkoutDocs": "Посмотреть документацию", "checkLogs": "Проверить логи для подробностей", "tryAgain": "Попробовать снова", - "loading": "Загрузка" + "loading": "Загрузка", + "show": "", + "hide": "" }, "here": "здесь", "iconPicker": { @@ -1249,7 +1254,8 @@ "menu": { "label": { "create": "Новая категория", - "changePosition": "Изменить позицию" + "changePosition": "Изменить позицию", + "settings": "" } }, "openAllInNewTabs": { @@ -3452,6 +3458,9 @@ "public": "Эта панель публичная", "private": "Эта панель приватная" }, + "noResults": { + "title": "" + }, "modal": { "createBoard": { "field": { diff --git a/packages/translation/src/lang/sk.json b/packages/translation/src/lang/sk.json index 576aa56a3..bc5796623 100644 --- a/packages/translation/src/lang/sk.json +++ b/packages/translation/src/lang/sk.json @@ -1054,6 +1054,9 @@ "open": { "label": "Otvoriť médiá" } + }, + "noResults": { + "title": "" } }, "common": { @@ -1084,7 +1087,9 @@ "checkoutDocs": "Pozrite si dokumentáciu", "checkLogs": "Ďalšie podrobnosti nájdete v protokoloch", "tryAgain": "Skúste znova", - "loading": "Nahrávam" + "loading": "Nahrávam", + "show": "", + "hide": "" }, "here": "tu", "iconPicker": { @@ -1249,7 +1254,8 @@ "menu": { "label": { "create": "Nová kategória", - "changePosition": "Zmeniť pozíciu" + "changePosition": "Zmeniť pozíciu", + "settings": "" } }, "openAllInNewTabs": { @@ -3452,6 +3458,9 @@ "public": "Táto tabuľa je verejná", "private": "Táto tabuľa je súkromná" }, + "noResults": { + "title": "" + }, "modal": { "createBoard": { "field": { diff --git a/packages/translation/src/lang/sl.json b/packages/translation/src/lang/sl.json index 186e79151..51c078175 100644 --- a/packages/translation/src/lang/sl.json +++ b/packages/translation/src/lang/sl.json @@ -1054,6 +1054,9 @@ "open": { "label": "" } + }, + "noResults": { + "title": "" } }, "common": { @@ -1084,7 +1087,9 @@ "checkoutDocs": "", "checkLogs": "", "tryAgain": "Poskusite znova", - "loading": "" + "loading": "", + "show": "", + "hide": "" }, "here": "", "iconPicker": { @@ -1249,7 +1254,8 @@ "menu": { "label": { "create": "", - "changePosition": "Spremeni položaj" + "changePosition": "Spremeni položaj", + "settings": "" } }, "openAllInNewTabs": { @@ -3452,6 +3458,9 @@ "public": "", "private": "" }, + "noResults": { + "title": "" + }, "modal": { "createBoard": { "field": { diff --git a/packages/translation/src/lang/sv.json b/packages/translation/src/lang/sv.json index 83fb869fb..20301caf2 100644 --- a/packages/translation/src/lang/sv.json +++ b/packages/translation/src/lang/sv.json @@ -1054,6 +1054,9 @@ "open": { "label": "" } + }, + "noResults": { + "title": "" } }, "common": { @@ -1084,7 +1087,9 @@ "checkoutDocs": "", "checkLogs": "", "tryAgain": "Försök igen", - "loading": "" + "loading": "", + "show": "", + "hide": "" }, "here": "", "iconPicker": { @@ -1249,7 +1254,8 @@ "menu": { "label": { "create": "", - "changePosition": "Ändra position" + "changePosition": "Ändra position", + "settings": "" } }, "openAllInNewTabs": { @@ -3452,6 +3458,9 @@ "public": "Den här tavlan är offentlig", "private": "Den här tavlan är privat" }, + "noResults": { + "title": "" + }, "modal": { "createBoard": { "field": { diff --git a/packages/translation/src/lang/tr.json b/packages/translation/src/lang/tr.json index 35e29f18e..8b622b9dd 100644 --- a/packages/translation/src/lang/tr.json +++ b/packages/translation/src/lang/tr.json @@ -1054,6 +1054,9 @@ "open": { "label": "Medyayı aç" } + }, + "noResults": { + "title": "" } }, "common": { @@ -1084,7 +1087,9 @@ "checkoutDocs": "Dökümanlara göz at", "checkLogs": "Daha fazla ayrıntı için günlükleri kontrol edin", "tryAgain": "Tekrar Deneyin", - "loading": "Yükleniyor" + "loading": "Yükleniyor", + "show": "", + "hide": "" }, "here": "buraya", "iconPicker": { @@ -1249,7 +1254,8 @@ "menu": { "label": { "create": "Yeni Kategori", - "changePosition": "Pozisyonu değiştir" + "changePosition": "Pozisyonu değiştir", + "settings": "" } }, "openAllInNewTabs": { @@ -3452,6 +3458,9 @@ "public": "Bu panel herkese açıktır", "private": "Bu panel özeldir" }, + "noResults": { + "title": "" + }, "modal": { "createBoard": { "field": { diff --git a/packages/translation/src/lang/uk.json b/packages/translation/src/lang/uk.json index c573c65ba..9d08afe64 100644 --- a/packages/translation/src/lang/uk.json +++ b/packages/translation/src/lang/uk.json @@ -1054,6 +1054,9 @@ "open": { "label": "Відкрити медіа" } + }, + "noResults": { + "title": "" } }, "common": { @@ -1084,7 +1087,9 @@ "checkoutDocs": "Переглянути документацію", "checkLogs": "Щоб дізнатися більше, перевірте логи", "tryAgain": "Повторіть спробу", - "loading": "Завантаження" + "loading": "Завантаження", + "show": "", + "hide": "" }, "here": "тут", "iconPicker": { @@ -1249,7 +1254,8 @@ "menu": { "label": { "create": "Нова категорія", - "changePosition": "Змінити положення" + "changePosition": "Змінити положення", + "settings": "" } }, "openAllInNewTabs": { @@ -3452,6 +3458,9 @@ "public": "Ця дошка є публічною", "private": "Ця дошка є приватною" }, + "noResults": { + "title": "" + }, "modal": { "createBoard": { "field": { diff --git a/packages/translation/src/lang/vi.json b/packages/translation/src/lang/vi.json index f8f8823e1..a9466e9c8 100644 --- a/packages/translation/src/lang/vi.json +++ b/packages/translation/src/lang/vi.json @@ -1054,6 +1054,9 @@ "open": { "label": "" } + }, + "noResults": { + "title": "" } }, "common": { @@ -1084,7 +1087,9 @@ "checkoutDocs": "", "checkLogs": "", "tryAgain": "Thử lại", - "loading": "" + "loading": "", + "show": "", + "hide": "" }, "here": "", "iconPicker": { @@ -1249,7 +1254,8 @@ "menu": { "label": { "create": "", - "changePosition": "Đổi vị trí" + "changePosition": "Đổi vị trí", + "settings": "" } }, "openAllInNewTabs": { @@ -3452,6 +3458,9 @@ "public": "", "private": "" }, + "noResults": { + "title": "" + }, "modal": { "createBoard": { "field": { diff --git a/packages/translation/src/lang/zh.json b/packages/translation/src/lang/zh.json index 147db32f0..7d15715a2 100644 --- a/packages/translation/src/lang/zh.json +++ b/packages/translation/src/lang/zh.json @@ -1054,6 +1054,9 @@ "open": { "label": "開啟多媒體" } + }, + "noResults": { + "title": "" } }, "common": { @@ -1084,7 +1087,9 @@ "checkoutDocs": "查閱說明書", "checkLogs": "檢查 Logs 以取得更多詳情", "tryAgain": "重試", - "loading": "正在讀取" + "loading": "正在讀取", + "show": "", + "hide": "" }, "here": "此處", "iconPicker": { @@ -1249,7 +1254,8 @@ "menu": { "label": { "create": "創建分類", - "changePosition": "變更位置" + "changePosition": "變更位置", + "settings": "" } }, "openAllInNewTabs": { @@ -3452,6 +3458,9 @@ "public": "此面板是公開的", "private": "此面板是私人的" }, + "noResults": { + "title": "" + }, "modal": { "createBoard": { "field": { diff --git a/packages/ui/eslint.config.js b/packages/ui/eslint.config.js deleted file mode 100644 index 1df6129af..000000000 --- a/packages/ui/eslint.config.js +++ /dev/null @@ -1,5 +0,0 @@ -import baseConfig from "@homarr/eslint-config/base"; -import reactConfig from "@homarr/eslint-config/react"; - -/** @type {import('typescript-eslint').Config} */ -export default [...baseConfig, ...reactConfig]; diff --git a/packages/ui/package.json b/packages/ui/package.json index a781debfd..52cd4c15d 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -4,12 +4,6 @@ "private": true, "license": "Apache-2.0", "type": "module", - "exports": { - ".": "./index.ts", - "./styles.css": "./src/styles.css", - "./hooks": "./src/hooks/index.ts", - "./icons": "./src/icons/index.ts" - }, "typesVersions": { "*": { "*": [ @@ -17,13 +11,18 @@ ] } }, + "exports": { + ".": "./index.ts", + "./styles.css": "./src/styles.css", + "./hooks": "./src/hooks/index.ts", + "./icons": "./src/icons/index.ts" + }, "scripts": { "clean": "rm -rf .turbo node_modules", - "format": "prettier --check . --ignore-path ../../.gitignore", - "lint": "eslint", + "format": "oxfmt .", + "lint": "oxlint", "typecheck": "tsc --noEmit" }, - "prettier": "@homarr/prettier-config", "dependencies": { "@homarr/common": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0", @@ -42,12 +41,9 @@ "svgson": "catalog:" }, "devDependencies": { - "@homarr/eslint-config": "workspace:^0.2.0", - "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", "@types/crypto-js": "catalog:", "@types/css-modules": "catalog:", - "eslint": "catalog:", "typescript": "catalog:" } } diff --git a/packages/ui/src/components/select-with-custom-items.tsx b/packages/ui/src/components/select-with-custom-items.tsx index b1fe554e4..d4a9545ab 100644 --- a/packages/ui/src/components/select-with-custom-items.tsx +++ b/packages/ui/src/components/select-with-custom-items.tsx @@ -20,6 +20,7 @@ export interface SelectWithCustomItemsProps onBlur?: (event: React.FocusEvent) => void; onFocus?: (event: React.FocusEvent) => void; w?: string; + withinPortal?: boolean; } type Props = SelectWithCustomItemsProps & { @@ -35,6 +36,7 @@ export const SelectWithCustomItems = ({ SelectOption, w, clearable, + withinPortal = false, ...props }: Props) => { const combobox = useCombobox({ @@ -71,7 +73,7 @@ export const SelectWithCustomItems = ({ const _clearable = clearable && Boolean(_value); return ( - + { return ( - + {users.slice(0, limit).map((user) => ( diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 2a4bb91a1..364f7a394 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -2,6 +2,7 @@ import type { MantineProviderProps } from "@mantine/core"; import { theme } from "./theme"; +export { theme } from "./theme"; export * from "./components"; export const uiConfiguration = { diff --git a/packages/ui/src/theme.ts b/packages/ui/src/theme.ts index e1e928d57..9aea93dbf 100644 --- a/packages/ui/src/theme.ts +++ b/packages/ui/src/theme.ts @@ -1,12 +1,103 @@ -import { createTheme } from "@mantine/core"; - -import { primaryColor } from "./theme/colors/primary"; -import { secondaryColor } from "./theme/colors/secondary"; +import { createTheme, rem } from "@mantine/core"; export const theme = createTheme({ - colors: { - primaryColor, - secondaryColor, + primaryColor: "red", + autoContrast: true, + respectReducedMotion: true, + cursorType: "pointer", + + fontFamily: "Inter, system-ui, -apple-system, sans-serif", + fontFamilyMonospace: "ui-monospace, 'Cascadia Code', 'Fira Code', monospace", + + headings: { + fontFamily: "Inter, system-ui, -apple-system, sans-serif", + fontWeight: "600", + sizes: { + h1: { fontSize: rem(36), lineHeight: "1.1", fontWeight: "700" }, + h2: { fontSize: rem(24), lineHeight: "1.2", fontWeight: "700" }, + h3: { fontSize: rem(20), lineHeight: "1.3", fontWeight: "600" }, + h4: { fontSize: rem(16), lineHeight: "1.4", fontWeight: "600" }, + h5: { fontSize: rem(14), lineHeight: "1.5", fontWeight: "600" }, + h6: { fontSize: rem(12), lineHeight: "1.5", fontWeight: "600" }, + }, + }, + + fontSizes: { + xs: rem(12), + sm: rem(14), + md: rem(16), + lg: rem(18), + xl: rem(20), + }, + + spacing: { + xs: rem(10), + sm: rem(12), + md: rem(16), + lg: rem(20), + xl: rem(32), + }, + + radius: { + xs: rem(4), + sm: rem(4), + md: rem(8), + lg: rem(12), + xl: rem(16), + }, + defaultRadius: "md", + + shadows: { + xs: "0 1px 2px rgba(0, 0, 0, 0.04)", + sm: "0 1px 3px rgba(0, 0, 0, 0.06)", + md: "0 2px 8px rgba(0, 0, 0, 0.08)", + lg: "0 4px 12px rgba(0, 0, 0, 0.10)", + xl: "0 8px 24px rgba(0, 0, 0, 0.12)", + }, + + components: { + Card: { + defaultProps: { + withBorder: true, + radius: "lg", + }, + }, + Paper: { + defaultProps: { + withBorder: true, + radius: "md", + }, + }, + Button: { + defaultProps: { + radius: "md", + }, + }, + ActionIcon: { + defaultProps: { + radius: "md", + }, + }, + Tooltip: { + defaultProps: { + openDelay: 300, + }, + }, + Menu: { + defaultProps: { + withArrow: true, + }, + }, + NavLink: { + defaultProps: { + style: { borderRadius: 5 }, + }, + }, + LoadingOverlay: { + defaultProps: { + zIndex: 1000, + overlayProps: { radius: "sm", blur: 2 }, + }, + }, }, - primaryColor: "primaryColor", }); diff --git a/packages/ui/src/theme/colors/primary.ts b/packages/ui/src/theme/colors/primary.ts deleted file mode 100644 index 3bb5ef987..000000000 --- a/packages/ui/src/theme/colors/primary.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { MantineColorsTuple } from "@mantine/core"; - -export const primaryColor: MantineColorsTuple = [ - "#eafbf0", - "#ddefe3", - "#bedcc7", - "#9bc8aa", - "#7eb892", - "#6bad81", - "#60a878", - "#509265", - "#438359", - "#35724a", -]; diff --git a/packages/ui/src/theme/colors/secondary.ts b/packages/ui/src/theme/colors/secondary.ts deleted file mode 100644 index c3a10a691..000000000 --- a/packages/ui/src/theme/colors/secondary.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { MantineColorsTuple } from "@mantine/core"; - -export const secondaryColor: MantineColorsTuple = [ - "#e6f7ff", - "#d9e8f6", - "#b6cde6", - "#90b2d4", - "#6f9ac5", - "#5a8bbd", - "#4d84ba", - "#3d71a4", - "#326595", - "#205885", -]; diff --git a/packages/validation/eslint.config.js b/packages/validation/eslint.config.js deleted file mode 100644 index f7a5a7d36..000000000 --- a/packages/validation/eslint.config.js +++ /dev/null @@ -1,4 +0,0 @@ -import baseConfig from "@homarr/eslint-config/base"; - -/** @type {import('typescript-eslint').Config} */ -export default [...baseConfig]; diff --git a/packages/validation/package.json b/packages/validation/package.json index 8cde9c838..e96c6c3c0 100644 --- a/packages/validation/package.json +++ b/packages/validation/package.json @@ -4,9 +4,6 @@ "private": true, "license": "Apache-2.0", "type": "module", - "exports": { - "./*": "./src/*.ts" - }, "typesVersions": { "*": { "*": [ @@ -14,13 +11,15 @@ ] } }, + "exports": { + "./*": "./src/*.ts" + }, "scripts": { "clean": "rm -rf .turbo node_modules", - "format": "prettier --check . --ignore-path ../../.gitignore", - "lint": "eslint", + "format": "oxfmt .", + "lint": "oxlint", "typecheck": "tsc --noEmit" }, - "prettier": "@homarr/prettier-config", "dependencies": { "@homarr/definitions": "workspace:^0.1.0", "@homarr/translation": "workspace:^0.1.0", @@ -28,10 +27,7 @@ "zod-form-data": "catalog:" }, "devDependencies": { - "@homarr/eslint-config": "workspace:^0.2.0", - "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", - "eslint": "catalog:", "typescript": "catalog:" } } diff --git a/packages/widgets/eslint.config.js b/packages/widgets/eslint.config.js deleted file mode 100644 index 1df6129af..000000000 --- a/packages/widgets/eslint.config.js +++ /dev/null @@ -1,5 +0,0 @@ -import baseConfig from "@homarr/eslint-config/base"; -import reactConfig from "@homarr/eslint-config/react"; - -/** @type {import('typescript-eslint').Config} */ -export default [...baseConfig, ...reactConfig]; diff --git a/packages/widgets/package.json b/packages/widgets/package.json index 4ee4834f8..9c80d4bd8 100644 --- a/packages/widgets/package.json +++ b/packages/widgets/package.json @@ -4,12 +4,6 @@ "private": true, "license": "Apache-2.0", "type": "module", - "exports": { - ".": "./index.ts", - "./errors": "./src/errors/component.tsx", - "./modals": "./src/modals/index.ts", - "./prefetch": "./src/prefetch.ts" - }, "typesVersions": { "*": { "*": [ @@ -17,13 +11,18 @@ ] } }, + "exports": { + ".": "./index.ts", + "./errors": "./src/errors/component.tsx", + "./modals": "./src/modals/index.ts", + "./prefetch": "./src/prefetch.ts" + }, "scripts": { "clean": "rm -rf .turbo node_modules", - "format": "prettier --check . --ignore-path ../../.gitignore", - "lint": "eslint", + "format": "oxfmt .", + "lint": "oxlint", "typecheck": "tsc --noEmit" }, - "prettier": "@homarr/prettier-config", "dependencies": { "@dnd-kit/core": "catalog:", "@dnd-kit/sortable": "catalog:", @@ -50,6 +49,7 @@ "@homarr/validation": "workspace:^0.1.0", "@mantine/charts": "catalog:", "@mantine/core": "catalog:", + "@mantine/form": "catalog:", "@mantine/hooks": "catalog:", "@tabler/icons-react": "catalog:", "@tiptap/extension-color": "catalog:", @@ -70,7 +70,6 @@ "@tiptap/starter-kit": "catalog:", "clsx": "catalog:", "dayjs": "catalog:", - "mantine-form-zod-resolver": "catalog:", "mantine-react-table": "catalog:", "next": "catalog:", "react": "catalog:", @@ -82,11 +81,8 @@ "zod": "catalog:" }, "devDependencies": { - "@homarr/eslint-config": "workspace:^0.2.0", - "@homarr/prettier-config": "workspace:^0.1.0", "@homarr/tsconfig": "workspace:^0.1.0", "@types/video.js": "catalog:", - "eslint": "catalog:", "typescript": "catalog:" } } diff --git a/packages/widgets/src/_inputs/widget-dynamic-select-input.tsx b/packages/widgets/src/_inputs/widget-dynamic-select-input.tsx index 39b70b325..de8ef5021 100644 --- a/packages/widgets/src/_inputs/widget-dynamic-select-input.tsx +++ b/packages/widgets/src/_inputs/widget-dynamic-select-input.tsx @@ -25,9 +25,22 @@ export const WidgetDynamicSelectInput = ({ property, kind, options }: CommonWidg const [search, setSearch] = useState(""); const [debouncedSearch] = useDebouncedValue(search, 300); const { isPending, options: selectOptions } = options.useOptions(debouncedSearch, form.values.integrationIds); - const value = inputProps.value as DynamicSelectOption | null; + const currentOption = inputProps.value as DynamicSelectOption | null; const onChange = inputProps.onChange as (value: DynamicSelectOption | null) => void; + const getLabelForValue = (selectedValue: string) => { + const matched = selectOptions.find((option) => + typeof option === "string" ? option === selectedValue : option.value === selectedValue, + ); + if (!matched) { + return selectedValue; + } + if (typeof matched === "string") { + return matched; + } + return translateIfNecessary(t, matched.label) ?? selectedValue; + }; + return (