diff --git a/.agents/skills/homarr-local-harness/SKILL.md b/.agents/skills/homarr-local-harness/SKILL.md new file mode 100644 index 000000000..19ae5a412 --- /dev/null +++ b/.agents/skills/homarr-local-harness/SKILL.md @@ -0,0 +1,60 @@ +--- +name: homarr-local-harness +description: Build and inspect a writable Docker demo harness from the current Homarr checkout when validating a local branch in a browser. +--- + +# Homarr Local Harness + +Use this skill when a Homarr change needs a real production image and browser validation. Work from the repository root and keep the harness name Docker-safe, such as `feature-board`. + +## Set up the harness + +Run the TypeScript operations CLI; this workflow does not use the interactive developer TUI: + +```sh +pnpm cli harness setup feature-board +``` + +`setup` builds the current checkout as `homarr:feature-board`, replaces the named harness container, and starts it with a persistent data volume. It enables demo mode, mock integrations, and writable demo mutations by default: + +- `DEMO_MODE=true` +- `DEMO_READ_ONLY=false` +- `UNSAFE_ENABLE_MOCK_INTEGRATION=true` + +Pass repeatable environment overrides when the feature needs them: + +```sh +pnpm cli harness setup feature-board \ + --env WORKSHOP_WEB_URL=https://example.test \ + --env FEATURE_FLAG=true +``` + +Use `pnpm cli harness build ` when only the image is needed, and `pnpm cli harness run ` to start an already-built image. `--port ` pins a host port; otherwise Docker assigns one. + +Always use the URL printed by the command. If the output is no longer available, query it directly: + +```sh +pnpm cli harness port feature-board +``` + +Do not assume port `7575`; the container listens on `7575`, but the host port is dynamic by default. + +## Browser validation + +Open the printed URL with the environment's `browser-harness` tool. In this workspace, the `agent-browser` CLI is the supported browser automation fallback. Load its current workflow before invoking it: + +```sh +agent-browser skills get core +``` + +Navigate to the exact harness URL, wait for the app to become usable, exercise the changed flow, and capture screenshots or other browser evidence. Keep browser state in a named session when the workflow needs login persistence. Report the exact URL, viewport/session used, and screenshot paths; a Docker process or HTTP health response alone is not browser acceptance. + +## Cleanup + +Stop the named container when finished: + +```sh +pnpm cli harness stop feature-board +``` + +Stopping keeps the data volume so the same harness can be restarted. A later `setup` rebuilds the image and replaces only the container. Preserve unrelated containers, images, volumes, and working-tree changes. diff --git a/.agents/skills/homarr-local-harness/agents/openai.yaml b/.agents/skills/homarr-local-harness/agents/openai.yaml new file mode 100644 index 000000000..fbe8007db --- /dev/null +++ b/.agents/skills/homarr-local-harness/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Homarr Local Harness" + short_description: "Build and browse a writable local Homarr branch harness" + default_prompt: "Use $homarr-local-harness to set up a writable Homarr harness for the current branch and validate it in the browser." diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index faf08f5bc..89aa72bba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -374,21 +374,31 @@ jobs: AMD64_DIGEST: ${{ needs.preview-amd64.outputs.digest }} ARM64_DIGEST: ${{ needs.preview-arm64.outputs.digest }} run: | - docker buildx imagetools create -t "${PREVIEW_IMAGE}:${PREVIEW_TAG}" \ + tags=(-t "${PREVIEW_IMAGE}:${PREVIEW_TAG}") + if [[ "${GITHUB_EVENT_NAME}" == "push" && "${GITHUB_REF}" == "refs/heads/release/v2" ]]; then + tags+=(-t "${PREVIEW_IMAGE}:pr-6545") + fi + docker buildx imagetools create "${tags[@]}" \ "${PREVIEW_IMAGE}@${AMD64_DIGEST}" \ "${PREVIEW_IMAGE}@${ARM64_DIGEST}" - name: Verify preview manifest platforms if: "!env.ACT" run: | - image="${PREVIEW_IMAGE}:${PREVIEW_TAG}" - manifest="$(docker buildx imagetools inspect "$image" --raw)" - for architecture in amd64 arm64; do - jq --exit-status --arg architecture "$architecture" \ - 'any(.manifests[]; .platform.os == "linux" and .platform.architecture == $architecture)' \ - <<<"$manifest" >/dev/null + tags=("${PREVIEW_TAG}") + if [[ "${GITHUB_EVENT_NAME}" == "push" && "${GITHUB_REF}" == "refs/heads/release/v2" ]]; then + tags+=("pr-6545") + fi + for tag in "${tags[@]}"; do + image="${PREVIEW_IMAGE}:${tag}" + manifest="$(docker buildx imagetools inspect "$image" --raw)" + for architecture in amd64 arm64; do + jq --exit-status --arg architecture "$architecture" \ + 'any(.manifests[]; .platform.os == "linux" and .platform.architecture == $architecture)' \ + <<<"$manifest" >/dev/null + done + docker buildx imagetools inspect "$image" done - docker buildx imagetools inspect "$image" cleanup: name: Clean up preview image diff --git a/.github/workflows/workshop.yml b/.github/workflows/workshop.yml index b8e1e34f5..3fec8610d 100644 --- a/.github/workflows/workshop.yml +++ b/.github/workflows/workshop.yml @@ -27,37 +27,103 @@ jobs: - run: pnpm test:workshop - run: pnpm test:workshop-image - publish: + publish-amd64: if: github.event_name == 'push' && (github.ref_name == 'v2' || github.ref_name == 'release/v2' || github.ref_name == 'main') needs: validate runs-on: ubuntu-24.04 - timeout-minutes: 30 + timeout-minutes: 20 permissions: contents: read packages: write + outputs: + digest: ${{ steps.build.outputs.digest }} steps: - uses: actions/checkout@v7 - - uses: docker/setup-qemu-action@v4 - uses: docker/setup-buildx-action@v4 - uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - uses: docker/metadata-action@v6 - id: metadata - with: - images: ghcr.io/homarr-labs/workshop - tags: | - type=sha,prefix=sha-,format=long - type=raw,value=v2,enable=${{ github.ref_name == 'v2' || github.ref_name == 'release/v2' }} - type=raw,value=latest,enable=${{ github.ref_name == 'main' }} - uses: docker/build-push-action@v7 + id: build with: context: . file: apps/workshop/Dockerfile target: production - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ steps.metadata.outputs.tags }} - labels: ${{ steps.metadata.outputs.labels }} + platforms: linux/amd64 + outputs: type=image,"name=ghcr.io/homarr-labs/workshop",push-by-digest=true,name-canonical=true,push=true + cache-from: type=gha,scope=workshop-linux-amd64 + cache-to: type=gha,scope=workshop-linux-amd64,mode=max + + publish-arm64: + if: github.event_name == 'push' && (github.ref_name == 'v2' || github.ref_name == 'release/v2' || github.ref_name == 'main') + needs: validate + runs-on: ubuntu-24.04-arm + timeout-minutes: 20 + permissions: + contents: read + packages: write + outputs: + digest: ${{ steps.build.outputs.digest }} + steps: + - uses: actions/checkout@v7 + - uses: docker/setup-buildx-action@v4 + - uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - uses: docker/build-push-action@v7 + id: build + with: + context: . + file: apps/workshop/Dockerfile + target: production + platforms: linux/arm64 + outputs: type=image,"name=ghcr.io/homarr-labs/workshop",push-by-digest=true,name-canonical=true,push=true + cache-from: type=gha,scope=workshop-linux-arm64 + cache-to: type=gha,scope=workshop-linux-arm64,mode=max + + publish: + if: github.event_name == 'push' && (github.ref_name == 'v2' || github.ref_name == 'release/v2' || github.ref_name == 'main') + needs: [publish-amd64, publish-arm64] + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + contents: read + packages: write + env: + WORKSHOP_IMAGE: ghcr.io/homarr-labs/workshop + steps: + - uses: docker/setup-buildx-action@v4 + - uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Publish multi-platform image + env: + AMD64_DIGEST: ${{ needs.publish-amd64.outputs.digest }} + ARM64_DIGEST: ${{ needs.publish-arm64.outputs.digest }} + run: | + tags=(-t "${WORKSHOP_IMAGE}:sha-${GITHUB_SHA}") + if [[ "${GITHUB_REF_NAME}" == "v2" || "${GITHUB_REF_NAME}" == "release/v2" ]]; then + tags+=(-t "${WORKSHOP_IMAGE}:v2") + fi + if [[ "${GITHUB_REF_NAME}" == "main" ]]; then + tags+=(-t "${WORKSHOP_IMAGE}:latest") + fi + docker buildx imagetools create "${tags[@]}" \ + "${WORKSHOP_IMAGE}@${AMD64_DIGEST}" \ + "${WORKSHOP_IMAGE}@${ARM64_DIGEST}" + - name: Verify multi-platform image + run: | + image="${WORKSHOP_IMAGE}:sha-${GITHUB_SHA}" + manifest="$(docker buildx imagetools inspect "$image" --raw)" + for architecture in amd64 arm64; do + jq --exit-status --arg architecture "$architecture" \ + 'any(.manifests[]; .platform.os == "linux" and .platform.architecture == $architecture)' \ + <<<"$manifest" >/dev/null + done + docker buildx imagetools inspect "$image" diff --git a/.gitignore b/.gitignore index 64749f817..be3e30235 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,8 @@ packages/db/migrations/*/migrate.cjs # production build +tools/homarr-dev/homarr +tools/homarr-dev/homarr-dev # misc .DS_Store diff --git a/apps/docs/docs/advanced/command-line/development/index.mdx b/apps/docs/docs/advanced/command-line/development/index.mdx index ee5036a72..6f4701af8 100644 --- a/apps/docs/docs/advanced/command-line/development/index.mdx +++ b/apps/docs/docs/advanced/command-line/development/index.mdx @@ -1,43 +1,175 @@ # Developer Docker CLI -The host-side developer CLI builds and runs local Homarr images and pull-request previews. Run it directly from a checkout with `pnpm dev:cli -- ...`; no shell-specific setup script is required. It is separate from the recovery CLI included in production images. +The host-side developer CLI builds and runs local Homarr images and pull-request previews with non-blocking background operations. Run it directly from a checkout with `pnpm dev:cli -- ...`; no shell-specific setup script is required. It is separate from the recovery CLI included in production images. -To install an optional `homarr` binary, run `pnpm dev:cli:install` and make your Go bin directory available on `PATH`. GitHub CLI authentication is only needed for pull-request features. +To install an optional `homarr` binary, run `pnpm dev:cli:install` and make your Go bin directory available on `PATH`. GitHub CLI authentication (`gh auth login`) is only needed for pull-request features. -## Browse development images +## Requirements + +- Docker or OrbStack +- Go 1.25+ (when building the CLI from source) +- GitHub CLI authenticated with `gh auth login` (for pull-request and CI features) +- A [Nerd Font](https://www.nerdfonts.com/) enabled in your terminal (recommended for icons, status indicators, and TUI glyphs) + +## Interactive Development Browser ```bash pnpm dev:cli -- +# or with global binary: +homarr ``` -The main screen combines local `homarr:*` images with open pull requests and their GHCR preview status. Select an image to start or stop it, use `R` to build a remote pull request locally in the background or rebuild a local image, or use `p` to force a remote preview pull. Press `d` to switch to the instance dashboard and `d` again to return without losing the browser state. The explicit `dev` and `dash` subcommands start on either screen. +The main browser combines local `homarr:*` images with open pull requests and their GHCR preview status. Select an image to start or stop it, use `R` to build a remote pull request locally in the background or rebuild a local image, or use `p` to force a remote preview pull. Press `d` (or `1`/`2`) to toggle between the image browser and the instance dashboard. -A locally built PR image is folded into its remote PR row only when its recorded revision exactly matches the current GitHub head SHA. The row is marked `◆ local`, and starting it prefers that local image without a registry pull. Outdated local builds remain separate rows. Navigation and dashboard switching remain available during background builds. +A locally built PR image is folded into its remote PR row only when its recorded revision matches the current GitHub head SHA (marked `◆ local`). When the matching GHCR image is also available, pressing `Enter` opens a local/remote selector with local preselected. Outdated local builds remain separate rows. -## Build images +### Multi-Source Sidebar + +Press `Tab` to cycle between sidebar tabs: + +- **`[logs]`**: Live container output streams (press `f` to follow/pause, `L` to pin to the selected container, `Page Up`/`Page Down` to scroll). +- **`[build]`**: Real-time BuildKit step streaming for active local builds. +- **`[ci]`**: GitHub Actions CI check statuses, run duration, and workflow links for pull requests. + +### Background Task Overlay + +Pulls and builds run asynchronously in the background. Press `t` to open the tasks overlay, showing: + +- Layer-by-layer download progress and transfer speeds (via Docker Engine API). +- Active build stages and logs. +- Task cancellation and status. + +### Browser Shortcuts + +| Key | Action | +| ---------------------- | --------------------------------------------------------------------------------------------- | +| `/` | Filter rows by PR number, title, author, branch, CI, image, state, or port | +| `↑` / `↓` or `j` / `k` | Navigate rows | +| `Enter` or `Space` | Start the available image; choose local or remote when both exist; stop if running | +| `R` | Build locally in background (or rebuild local image) | +| `p` | Pull remote PR image in background and start container | +| `m` | Manage mode (`b` rebuild, `i` delete image, `d` delete data, `c` remove container, `p` prune) | +| `t` | Toggle background tasks overlay | +| `Tab` | Cycle sidebar tabs (`[logs]`, `[build]`, `[ci]`) | +| `l` / `L` | Toggle sidebar visibility / Pin logs to selected instance | +| `f` | Follow or pause logs | +| `M` | Toggle demo mode for next launch | +| `b` | Toggle bot pull requests | +| `o` / `a` | Open PR on GitHub / Open running instance in browser | +| `c` | Copy instance URL to clipboard | +| `r` | Refresh data | +| `1` / `2` / `d` | Switch screen (development browser / instance dashboard) | +| `?` / `q` | Help overlay / Quit (cancels active background tasks) | + +## Instance Dashboard ```bash -pnpm dev:cli -- build -pnpm dev:cli -- build --pr [name] +homarr dash +# or +pnpm dev:cli -- dash ``` -The first command builds the current checkout as `homarr:`. The pull-request form uses a depth-one clone in a temporary directory and defaults to `homarr:pr-` when `name` is omitted. +The instance dashboard displays all running and stopped Homarr containers, published ports, health states, and resource volumes. -## Rebuild images +| Key | Action | +| ------------------ | ---------------------------------------- | +| `Enter` or `Space` | Start or stop selected instance | +| `s` / `S` | Stop / Restart instance | +| `x` | Remove container (preserves data volume) | +| `m` | Manage mode for selected instance | +| `R` | Rebuild from recorded build provenance | + +## CLI Commands + +The commands below use the installed `homarr` binary. Without global installation, prefix each command with `pnpm dev:cli --` (for example, `pnpm dev:cli -- list`). + +### Launch Instances ```bash -pnpm dev:cli -- rebuild +homarr run dev +homarr run --detach dev +homarr run --pr 6441 +homarr run --pr 6441 --demo +homarr --pr 6441 --env WORKSHOP_WEB_URL=https://app-v2.preview.homarr.dev/ ``` -Images built by this CLI record their source checkout, revision, and optional pull-request number. Rebuild uses that provenance to select the original checkout or create a fresh temporary pull-request checkout. +- `--pr `: Pull and run a PR image from GHCR (shorthand: `homarr --pr `). +- `--demo` (`-m`): Start with mock integrations and sample data. +- `--detach`: Start container in background. +- `--env` (`-e`): Pass additional environment variables (repeatable: `--env KEY=VALUE`). -## Run images +### Build & Rebuild ```bash -homarr --pr --env HOMARR_WEBSITE_URL=https://app-v2.preview.homarr.dev/ -homarr run -homarr run --pr +# Build current checkout as homarr: +homarr build feature + +# Build a pull request locally in a temporary checkout +homarr build --pr 6441 + +# Rebuild an existing image from recorded provenance +homarr rebuild feature ``` -The direct `homarr --pr ` form is shorthand for `homarr run --pr `. Add `--detach` to run in the -background, `--demo` to enable demo data, or repeat `--env KEY=VALUE` to provide additional environment variables. +Images built with the CLI record their source checkout, revision SHA, and optional PR number. `homarr rebuild` reads this provenance to reconstruct the build without manual configuration. + +### GitHub CI Checks + +```bash +# View CI checks for a specific PR +homarr ci 6441 + +# View CI checks for the current branch +homarr ci + +# Watch CI checks until completion +homarr ci 6441 --watch +``` + +### Interactive Shell + +```bash +# Open interactive shell in running container +homarr shell homarr-dev + +# Run specific command in container +homarr shell homarr-dev pnpm db:migration:sqlite:run +``` + +### Image & Volume Management + +```bash +# List local images with build provenance (source, revision, PR) +homarr images + +# Delete local image +homarr images rm feature + +# List data volumes with owner PR/tag and in-use status +homarr data + +# Delete data volume (safely stops and removes owning container first) +homarr data rm homarr_feature_data + +# Remove stopped containers (prompts without --force) +homarr prune --force +``` + +### Instance Operations & Diagnostics + +```bash +# List instances in a script-friendly table +homarr list + +# Follow container logs +homarr logs homarr-dev + +# Stop, restart, remove, or open container in browser +homarr stop homarr-dev +homarr restart homarr-dev +homarr remove homarr-dev +homarr open homarr-dev + +# Verify Docker daemon, Engine API, GHCR access, and gh CLI authentication +homarr doctor +``` diff --git a/apps/docs/docs/advanced/command-line/index.mdx b/apps/docs/docs/advanced/command-line/index.mdx index 287985739..04024254f 100644 --- a/apps/docs/docs/advanced/command-line/index.mdx +++ b/apps/docs/docs/advanced/command-line/index.mdx @@ -25,3 +25,27 @@ You should also create a backup of the database, in case you break something. 4. Click "Execute" in the top right of the Window. ![](img/334116411-df1482f9-3aa9-4686-b77f-e0dca09e60b2.png) 5. Select /bin/bash and click Execute ![](img/334116518-e1ddb515-99c7-4159-81bf-76e889d1e808.png) 6. Run the Homarr command and press enter (eg. homarr reset-owner-password) ![](img/334116642-08c4f204-5cd0-4e4e-a785-0142e1b279ca.png) + +## Build a local branch harness + +The host-side operations CLI can build the current checkout and run it as a named, browser-ready Docker harness. This is separate from the recovery CLI described above and does not open the interactive developer TUI. + +```sh +pnpm cli harness setup feature-board +``` + +The command builds `homarr:feature-board`, starts a container with a persistent data volume, enables demo mode, and keeps demo mutations writable (`DEMO_READ_ONLY=false`). Docker assigns a free host port by default; use the URL printed by the command or query it later: + +```sh +pnpm cli harness port feature-board +``` + +Pass environment overrides by repeating `--env`: + +```sh +pnpm cli harness setup feature-board \ + --env WORKSHOP_WEB_URL=https://example.test \ + --env FEATURE_FLAG=true +``` + +Other commands are `harness build `, `harness run `, and `harness stop `. Use the returned URL with a browser automation harness for feature validation; do not assume the host port is `7575`. diff --git a/apps/docs/docs/advanced/keyboard-shortcuts/index.mdx b/apps/docs/docs/advanced/keyboard-shortcuts/index.mdx index 30b61fcf6..07765cf11 100644 --- a/apps/docs/docs/advanced/keyboard-shortcuts/index.mdx +++ b/apps/docs/docs/advanced/keyboard-shortcuts/index.mdx @@ -6,19 +6,19 @@ tags: Homarr has the following keyboard shortcuts: -| Shortcut | Description | -| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -| `MOD` + `K` | Opens the search bar | -| `MOD` + `J` | Toggles Dark Mode | -| `MOD` + `E` | Toggles Edit Mode | -| Hold `Shift` for 500 ms over a supported widget | Opens its temporary advanced view | -| `Shift` + `Enter` on a supported widget | Opens its advanced view until it is closed | -| `Esc` | Stops keyboard tile editing, cancels an active pointer or touch move/resize, or closes an open advanced view or board menu | +| Shortcut | Description | +| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| `MOD` + `K` | Opens the search bar | +| `MOD` + `J` | Toggles Dark Mode | +| `MOD` + `E` | Toggles Edit Mode | +| Hold `Shift` for 500 ms over a supported widget | Opens its temporary advanced view | +| Hold `Shift` + `Ctrl`/`Cmd` over a supported widget | Keeps its advanced view open until it is closed | +| `Esc` | Stops keyboard tile editing, cancels an active pointer or touch move/resize, or closes an open advanced view or board menu | ## Widget advanced view -Supported widgets open a temporary advanced view when you hold `Shift` over them for 500 ms. Focus a widget and press -`Shift` + `Enter`, or use its menu, to keep the advanced view open. +Supported widgets open a temporary advanced view when you hold `Shift` over them for 500 ms. Hold `Shift` plus `Ctrl` +on Windows/Linux or `Cmd` on macOS over the widget to keep the advanced view open, or use its menu. ## Board editing diff --git a/apps/docs/docs/getting-started/after-the-installation.mdx b/apps/docs/docs/getting-started/after-the-installation.mdx index 68b4d11f1..3fda726e3 100644 --- a/apps/docs/docs/getting-started/after-the-installation.mdx +++ b/apps/docs/docs/getting-started/after-the-installation.mdx @@ -151,7 +151,7 @@ When entering the edit mode, you'll get some additional buttons to work with: Outside edit mode, right-click a widget for its live status, refresh, settings, quick options, and supported actions. Hold `Shift` while hovering a widget that supports advanced view to expand it in place while the rest of the board -stays visible but dimmed. Release `Shift` to close it. You can also focus that widget and press `Shift` + `Enter` to +stays visible but dimmed. Release `Shift` to close it. Hold `Shift` plus `Ctrl` on Windows/Linux or `Cmd` on macOS to keep its advanced view open until you close it. Widgets without a distinct advanced experience adapt in place. To continue, click **New item** in the add-content menu. New content is placed at the first available position on the main diff --git a/apps/docs/docs/management/boards/index.mdx b/apps/docs/docs/management/boards/index.mdx index 6f1710e3b..92d709e3c 100644 --- a/apps/docs/docs/management/boards/index.mdx +++ b/apps/docs/docs/management/boards/index.mdx @@ -204,10 +204,14 @@ changes, Homarr asks whether to discard them. If the editor cannot load, the boa style={{ maxWidth: "100%" }} /> -While edit mode is active, **Add board content** opens the same Universal Create surface as the global header action and -Spotlight. It lists the available actions directly and can add a widget, app, integration, Container, board, Workshop item, or Custom Widget when the -current user and context allow it. Widget setup offers the main canvas, enabled rails, and existing Containers as -destinations before the widget is created, so the common flow does not require a second move step. +While edit mode is active, **Add board content** opens a menu for adding a widget, app, integration, or Container. The +available actions respect the current user's permissions. Widget setup offers the main canvas, enabled rails, and existing +Containers as destinations before the widget is created, so the common flow does not require a second move step. + +Hold `Ctrl` on Windows or Linux, or `⌘` on macOS, while clicking board items to select several at once. The selection +toolbar can move the complete selection to another canvas, rail, or Container, delete it with an undo option, or clear the +selection. A short tip appears after entering edit mode as a reminder of the modifier key. A move is applied only when every +selected item fits in the destination. The widget editor also lists its selected integrations. Users with full access can choose **Edit connection** to test, diagnose, and repair an integration without leaving the board. A successful repair refreshes the affected widget; the @@ -227,11 +231,7 @@ During a pointer or touch interaction, Escape cancels the move or resize and res Invalid, outside, or otherwise forbidden drops are cancelled in the same way. The **Move / resize item** action in the edit menu remains available for precise numeric input. -A focused board tile in keyboard editing mode +A focused board tile in keyboard editing mode #### Preview @@ -330,8 +330,9 @@ This will disable the status check for apps. Widgets expose their routine controls without requiring edit mode: - Hold `Shift` while hovering a widget that supports advanced view to expand it in place while the other widgets - remain visible but dimmed. Release `Shift` to close it. Focus that widget and press `Shift` + `Enter` to keep the view - open until you close it. On touch devices, open it from the widget menu. Other widgets adapt in place instead. + remain visible but dimmed. Release `Shift` to close it. Hold `Shift` plus `Ctrl` on Windows/Linux or `Cmd` on macOS to + keep the view open until you close it. On touch devices, open it from the widget menu. Other widgets adapt in place + instead. - When signed in, outside edit mode, and with **Enable right click on widgets** enabled, right-click a widget to inspect its live query status, refresh its data, change quick options, open settings, or run supported actions. App tiles do not use this widget menu, and available controls follow your board and integration permissions. diff --git a/apps/docs/docs/widgets/beszel-system-grid/index.mdx b/apps/docs/docs/widgets/beszel-system-grid/index.mdx index 137d3d003..1748521e4 100644 --- a/apps/docs/docs/widgets/beszel-system-grid/index.mdx +++ b/apps/docs/docs/widgets/beszel-system-grid/index.mdx @@ -16,7 +16,7 @@ import beszelSystemGrid from "./img/beszel-system-grid.png"; -This widget displays all your Beszel-monitored systems as a responsive card grid. Each card shows real-time metrics like CPU, memory, disk, network, temperature, and more. Cards adapt their layout based on available space. If Beszel reports multiple filesystems, the disk bar exposes a hover breakdown for the root disk and each extra mount. +This widget displays all your Beszel-monitored systems as a responsive card grid. Each card shows real-time metrics like CPU, memory, disk, network, temperature, and more. Resizing the widget adjusts its rows and columns to keep the cards balanced. If Beszel reports multiple filesystems, the disk bar exposes a hover breakdown for the root disk and each extra mount. Select a system card to open the complete statistics view, including system, filesystem, Docker, historical, and realtime charts. diff --git a/apps/docs/docs/widgets/calendar/index.mdx b/apps/docs/docs/widgets/calendar/index.mdx index 57ca565fe..82d8a1cbd 100644 --- a/apps/docs/docs/widgets/calendar/index.mdx +++ b/apps/docs/docs/widgets/calendar/index.mdx @@ -69,6 +69,7 @@ import advancedView from "./img/advanced-view.png"; ### Interactions +- Colored dots directly below a date mark its events. They scale with the widget and are hidden when the calendar is too short. - Hover over a marked date to preview its events in a popover. - The advanced view replaces the month grid with a grouped event list, keeps every event attributed to its source integration, and expands titles, locations, and descriptions. Use its previous and next controls to change months. - If one selected integration is unavailable, its warning indicator remains visible while events from healthy integrations continue to load. diff --git a/apps/docs/docs/widgets/today/index.mdx b/apps/docs/docs/widgets/today/index.mdx deleted file mode 100644 index 000861d20..000000000 --- a/apps/docs/docs/widgets/today/index.mdx +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: "Today" -description: "Displays the current date and progress through the calendar." -hide_title: true ---- - -import { WidgetInteractionGuide } from "@site/src/components/widgets/interactions"; - -import { AddingWidget } from "@site/src/components/widgets/adding"; -import { WidgetConfig } from "@site/src/components/widgets/configuration"; -import { WidgetHeader } from "@site/src/components/widgets/header"; -import { todayWidget } from "."; - - - -The Today widget presents the current local date without duplicating a live clock. It progressively adds the week -number, day of year, quarter, remaining days, and calendar progress as more space becomes available. - -The advanced view shows week, month, and year progress together with a twelve-month overview. Week numbers can follow -your locale or the ISO 8601 convention. “Days after today” excludes the current day to keep the remaining-day count -unambiguous. - -The widget runs entirely in your browser and does not send date or location data to an external service. It follows -the timezone and locale of the device viewing the board, so viewers in different regions can see their own local date. - - - -### Adding the widget - - - -### Configuration - - diff --git a/apps/docs/docs/widgets/today/index.ts b/apps/docs/docs/widgets/today/index.ts deleted file mode 100644 index 8922d4b6e..000000000 --- a/apps/docs/docs/widgets/today/index.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { WidgetDefinition } from "@site/src/types"; -import { IconCalendarWeek } from "@tabler/icons-react"; - -export const todayWidget: WidgetDefinition = { - icon: IconCalendarWeek, - name: "Today", - description: "Displays the current date and progress through the calendar.", - path: "../../widgets/today", - configuration: { - items: [ - { - name: "Week numbering", - description: "Use locale-specific week numbering or ISO 8601 week numbers", - values: { type: "select", options: ["Use locale rules", "ISO 8601"] }, - defaultValue: "Use locale rules", - }, - { - name: "Show week number", - description: "Display the current week number when the widget has room", - values: { type: "boolean" }, - defaultValue: "yes", - }, - { - name: "Show day of year", - description: "Display the current day number within the year", - values: { type: "boolean" }, - defaultValue: "yes", - }, - { - name: "Show quarter", - description: "Display the current calendar quarter", - values: { type: "boolean" }, - defaultValue: "yes", - }, - { - name: "Show year progress", - description: "Display progress through the current year", - values: { type: "boolean" }, - defaultValue: "yes", - }, - ], - }, -}; diff --git a/apps/docs/static/api/open-api-schema.json b/apps/docs/static/api/open-api-schema.json index 8e5da38c1..83b48a665 100644 --- a/apps/docs/static/api/open-api-schema.json +++ b/apps/docs/static/api/open-api-schema.json @@ -2106,7 +2106,6 @@ "enum": [ "clock", "weather", - "today", "airQuality", "countdown", "timer", diff --git a/apps/nextjs/src/app/[locale]/auth/invite/[id]/_registration-form.tsx b/apps/nextjs/src/app/[locale]/auth/invite/[id]/_registration-form.tsx index 9bfe3bfd5..677356dd4 100644 --- a/apps/nextjs/src/app/[locale]/auth/invite/[id]/_registration-form.tsx +++ b/apps/nextjs/src/app/[locale]/auth/invite/[id]/_registration-form.tsx @@ -7,7 +7,7 @@ import type { z } from "zod/v4"; import { clientApi } from "@homarr/api/client"; import { useZodForm } from "@homarr/form"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; -import { useScopedI18n } from "@homarr/translation/client"; +import { useI18n } from "@homarr/translation/client"; import { CustomPasswordInput } from "@homarr/ui"; import { userRegistrationSchema } from "@homarr/validation/user"; @@ -19,7 +19,7 @@ interface RegistrationFormProps { } export const RegistrationForm = ({ invite }: RegistrationFormProps) => { - const t = useScopedI18n("user"); + const t = useI18n("user"); const router = useRouter(); const { mutate, isPending } = clientApi.user.register.useMutation(); const form = useZodForm(userRegistrationSchema, { 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 475464aff..0f7ddefc7 100644 --- a/apps/nextjs/src/app/[locale]/auth/invite/[id]/page.tsx +++ b/apps/nextjs/src/app/[locale]/auth/invite/[id]/page.tsx @@ -6,7 +6,7 @@ import { isProviderEnabled } from "@homarr/auth/server"; import { and, db, eq } from "@homarr/db"; import { invites } from "@homarr/db/schema"; import { OnboardingAuthShell } from "@homarr/onboarding"; -import { getScopedI18n } from "@homarr/translation/server"; +import { getI18n } from "@homarr/translation/server"; import { RegistrationForm } from "./_registration-form"; @@ -45,7 +45,7 @@ export default async function InviteUsagePage(props: InviteUsagePageProps) { if (!invite || invite.expirationDate < new Date()) notFound(); - const t = await getScopedI18n("user.page.invite"); + const t = await getI18n("user.page.invite"); return ( { - const t = useScopedI18n("user"); + const t = useI18n("user"); const searchParams = useSearchParams(); const isError = searchParams.has("error"); const router = useRouter(); @@ -196,7 +196,7 @@ interface PasswordForgottenCollapseProps { } const PasswordForgottenCollapse = ({ username }: PasswordForgottenCollapseProps) => { const [visible, { toggle }] = useDisclosure(false); - const tForgotPassword = useScopedI18n("user.action.login.forgotPassword"); + const tForgotPassword = useI18n("user.action.login.forgotPassword"); const commandUsername = username.trim().length >= 1 ? username.trim() : ""; diff --git a/apps/nextjs/src/app/[locale]/auth/login/page.tsx b/apps/nextjs/src/app/[locale]/auth/login/page.tsx index 8286c6205..b1c0a1d1b 100644 --- a/apps/nextjs/src/app/[locale]/auth/login/page.tsx +++ b/apps/nextjs/src/app/[locale]/auth/login/page.tsx @@ -5,7 +5,7 @@ import { IconLogin } from "@tabler/icons-react"; import { env } from "@homarr/auth/env"; import { auth } from "@homarr/auth/next"; import { OnboardingAuthShell } from "@homarr/onboarding"; -import { getScopedI18n } from "@homarr/translation/server"; +import { getI18n } from "@homarr/translation/server"; import { sanitizeRedirectionUrl } from "@homarr/validation/redirection-url"; import { env as appEnv } from "~/env"; @@ -26,7 +26,7 @@ export default async function Login(props: LoginProps) { redirect(sanitizeRedirectionUrl(searchParams.callbackUrl)); } - const t = await getScopedI18n("user.page.login"); + const t = await getI18n("user.page.login"); return ( }> diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/[name]/not-found.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/[name]/not-found.tsx index 9c398669b..12128b7e4 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/[name]/not-found.tsx +++ b/apps/nextjs/src/app/[locale]/boards/(content)/[name]/not-found.tsx @@ -1,11 +1,11 @@ import { IconLayoutOff } from "@tabler/icons-react"; -import { getScopedI18n } from "@homarr/translation/server"; +import { getI18n } from "@homarr/translation/server"; import { BoardNotFound } from "~/components/board/not-found"; export default async function BoardNotFoundPage() { - const tNotFound = await getScopedI18n("board.error.notFound"); + const tNotFound = await getI18n("board.error.notFound"); return ( { const board = useRequiredBoard(); - const t = useScopedI18n("board.landmark"); - const tPreview = useScopedI18n("board.setting.section.layout.preview"); + const t = useI18n("board.landmark"); + const tPreview = useI18n("board.setting.section.layout.preview"); + const tTips = useI18n("tips"); const [isEditMode] = useEditMode(); const currentLayoutId = useCurrentLayout(); const initialViewportWidth = useInitialViewportWidth(); @@ -63,71 +68,83 @@ export const ClientBoard = () => { .join(" "); const content = ( - - - - - - - - - - -
- {leftColumnCount > 0 && leftSection && ( - - )} + + + + + + + + + + +
+ {leftColumnCount > 0 && leftSection && ( + + )} -
- -
- - {rightColumnCount > 0 && rightSection && ( - - )} -
-
-
-
-
-
-
+ + + {rightColumnCount > 0 && rightSection && ( + + )} +
+
+
+
+
+ + , variant: "light" }} + > + {tTips("multiSelectApps")} + +
+
+ ); if (representativeWidth === null) return content; diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx index 9833cc4e2..9cf907b5e 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_creator.tsx @@ -104,10 +104,10 @@ export const createBoardContentPage = >( generateMetadataAsync: async ({ params }: { params: Promise }): Promise => { try { const board = await getInitialBoard(await params); - const t = await getI18n(); + const t = await getI18n("board"); return { - title: board.metaTitle ?? createMetaTitle(t("board.content.metaTitle", { boardName: board.name })), + title: board.metaTitle ?? createMetaTitle(t("content.metaTitle", { boardName: board.name })), icons: { icon: !isNullOrWhitespace(board.faviconImageUrl) ? board.faviconImageUrl : undefined, apple: !isNullOrWhitespace(board.faviconImageUrl) ? board.faviconImageUrl : undefined, diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/_edit-actions.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/_edit-actions.tsx index 87eda8813..29dbc8ac0 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_edit-actions.tsx +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_edit-actions.tsx @@ -2,14 +2,19 @@ import { useEffect, useRef } from "react"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; -import { IconPlus } from "@tabler/icons-react"; +import { Group, Menu } from "@mantine/core"; +import { IconBox, IconChevronDown, IconLayoutGridAdd, IconPlug, IconPlus, IconResize } from "@tabler/icons-react"; +import { useSession } from "@homarr/auth/client"; import { useRequiredBoard } from "@homarr/boards/context"; import { useModalAction } from "@homarr/modals"; +import { AppSelectModal } from "@homarr/modals-collection"; import { useI18n } from "@homarr/translation/client"; -import { useBoardAddActions } from "~/components/board/use-board-add-actions"; -import { UniversalCreateModal } from "~/components/create/universal-create-modal"; +import { useItemActions } from "~/components/board/items/item-actions"; +import { ItemSelectModal } from "~/components/board/items/item-select-modal"; +import { useContainerActions } from "~/components/board/sections/container/container-actions"; +import { IntegrationSelectModal } from "~/components/integration/integration-select-modal"; import { HeaderButton } from "~/components/layout/header/button"; export default function BoardEditActions() { @@ -17,10 +22,16 @@ export default function BoardEditActions() { } const AddMenu = () => { + const { data: session } = useSession(); const board = useRequiredBoard(); - const { addWidget, addApp, connectService, addContainer, canConnectService } = useBoardAddActions(); - const { openModal } = useModalAction(UniversalCreateModal); - const t = useI18n(); + const { openModal: openItemSelectModal } = useModalAction(ItemSelectModal); + const { openModal: openAppSelectModal } = useModalAction(AppSelectModal); + const { openModal: openIntegrationSelectModal } = useModalAction(IntegrationSelectModal); + const { addContainer } = useContainerActions(); + const { createItem } = useItemActions(); + const tBoard = useI18n("board"); + const tIntegration = useI18n("integration"); + const tContainer = useI18n("section.container"); const pathname = usePathname(); const router = useRouter(); const searchParams = useSearchParams(); @@ -29,29 +40,69 @@ const AddMenu = () => { useEffect(() => { if (handledInitialAdd.current || searchParams.get("add") !== "true") return; handledInitialAdd.current = true; - addWidget(); + openItemSelectModal({ boardId: board.id }); const nextSearchParams = new URLSearchParams(searchParams.toString()); nextSearchParams.delete("add"); const query = nextSearchParams.toString(); router.replace(query ? `${pathname}?${query}` : pathname, { scroll: false }); - }, [addWidget, board.id, pathname, router, searchParams]); + }, [board.id, openItemSelectModal, pathname, router, searchParams]); + + const handleSelectItem = () => { + openItemSelectModal({ boardId: board.id }); + }; + + const handleSelectApp = () => { + openAppSelectModal({ + onSelect: (app) => { + createItem({ + kind: "app", + options: { appId: app.id }, + }); + }, + onSelectMany: (apps) => { + for (const app of apps) { + createItem({ + kind: "app", + options: { appId: app.id }, + }); + } + }, + withCreate: session?.user.permissions.includes("app-create") ?? false, + }); + }; + + const handleAddIntegration = () => { + openIntegrationSelectModal({ completionBoardId: board.id }); + }; return ( - - openModal({ - boardActions: { - widget: addWidget, - app: addApp, - integration: canConnectService ? connectService : undefined, - container: addContainer, - }, - }) - } - > - - + + + + + + + + + + + } onClick={handleSelectItem}> + {tBoard("emptyState.addWidget")} + + } onClick={handleSelectApp}> + {tBoard("emptyState.addApp")} + + {session?.user.permissions.includes("integration-create") && ( + } onClick={handleAddIntegration}> + {tIntegration("action.create")} + + )} + + } onClick={addContainer}> + {tContainer("action.create")} + + + ); }; 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 397d5274f..f85d78d27 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx +++ b/apps/nextjs/src/app/[locale]/boards/(content)/_header-actions.tsx @@ -24,7 +24,7 @@ import { env } from "@homarr/common/env"; import { hotkeys } from "@homarr/definitions"; import { useConfirmModal } from "@homarr/modals"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; -import { useI18n, useScopedI18n } from "@homarr/translation/client"; +import { useI18n } from "@homarr/translation/client"; import { Link } from "@homarr/ui"; import { useBoardPermissions } from "~/components/board/permissions/client"; @@ -53,7 +53,7 @@ const BoardEditActions = dynamic(loadEditActionsAsync, { ssr: false }); export const BoardContentHeaderActions = ({ demoReadOnly }: { demoReadOnly: boolean }) => { const [isEditMode] = useEditMode(); const board = useRequiredBoard(); - const t = useI18n(); + const t = useI18n("board"); const { hasChangeAccess } = useBoardPermissions(board); if (!hasChangeAccess) { @@ -68,7 +68,7 @@ export const BoardContentHeaderActions = ({ demoReadOnly }: { demoReadOnly: bool {!demoReadOnly && ( - + @@ -88,8 +88,9 @@ const EditModeMenu = ({ demoReadOnly }: { demoReadOnly: boolean }) => { const router = useRouter(); const searchParams = useSearchParams(); const { openConfirmModal } = useConfirmModal(); - const commonT = useI18n(); - const t = useScopedI18n("board.action.edit"); + const tCommon = useI18n("common"); + const tBoardSetting = useI18n("board.setting"); + const t = useI18n("board.action.edit"); const returnToLayoutSettings = isEditMode && searchParams.get("edit") === "true" && @@ -104,7 +105,7 @@ const EditModeMenu = ({ demoReadOnly }: { demoReadOnly: boolean }) => { }, onSuccess() { showSuccessNotification({ - title: t("notification.success.title"), + title: tCommon("notification.update.success"), message: t("notification.success.message"), }); void utils.board.getBoardByName.invalidate({ name: board.name }); @@ -115,7 +116,7 @@ const EditModeMenu = ({ demoReadOnly }: { demoReadOnly: boolean }) => { }, onError() { showErrorNotification({ - title: t("notification.error.title"), + title: tCommon("notification.update.error"), message: t("notification.error.message"), }); }, @@ -178,9 +179,9 @@ const EditModeMenu = ({ demoReadOnly }: { demoReadOnly: boolean }) => { const cancelLayoutEdit = useCallback(() => { openConfirmModal({ - title: commonT("board.setting.section.layout.edit.cancel.title"), - children: commonT("board.setting.section.layout.edit.cancel.message"), - confirmProps: { children: commonT("common.action.discard"), color: "red" }, + title: tBoardSetting("section.layout.edit.cancel.title"), + children: tBoardSetting("section.layout.edit.cancel.message"), + confirmProps: { children: tCommon("action.discard"), color: "red" }, onConfirm() { void (async () => { await utils.board.getBoardByName.invalidate({ name: board.name }); @@ -189,7 +190,7 @@ const EditModeMenu = ({ demoReadOnly }: { demoReadOnly: boolean }) => { })(); }, }); - }, [board.name, close, commonT, openConfirmModal, router, utils.board.getBoardByName]); + }, [board.name, close, openConfirmModal, router, tBoardSetting, tCommon, utils.board.getBoardByName]); useHotkeys([[hotkeys.toggleBoardEdit, () => void toggle()]]); usePreventLeaveWithDirty(isEditMode); @@ -200,14 +201,14 @@ const EditModeMenu = ({ demoReadOnly }: { demoReadOnly: boolean }) => { saveBoard(board)} loading={isPending} - aria-label={commonT("board.setting.section.layout.edit.save")} + aria-label={tBoardSetting("section.layout.edit.save")} > @@ -235,7 +236,7 @@ const EditModeMenu = ({ demoReadOnly }: { demoReadOnly: boolean }) => { }; const SelectBoardsMenu = () => { - const t = useI18n(); + const t = useI18n("board"); const [isOpen, setIsOpen] = useState(false); const utils = clientApi.useUtils(); const { data: boards = [], isPending } = clientApi.board.getAllBoards.useQuery(undefined, { enabled: isOpen }); @@ -246,7 +247,7 @@ const SelectBoardsMenu = () => { - + @@ -277,7 +278,8 @@ const SelectBoardsMenu = () => { const anchorSelector = "a[href]:not([target='_blank'])"; const usePreventLeaveWithDirty = (isDirty: boolean) => { - const t = useI18n(); + const tBoard = useI18n("board"); + const tCommon = useI18n("common"); const { openConfirmModal } = useConfirmModal(); const router = useRouter(); @@ -293,13 +295,13 @@ const usePreventLeaveWithDirty = (isDirty: boolean) => { event.stopPropagation(); openConfirmModal({ - title: t("board.action.edit.confirmLeave.title"), - children: t("board.action.edit.confirmLeave.message"), + title: tBoard("action.edit.confirmLeave.title"), + children: tBoard("action.edit.confirmLeave.message"), onConfirm() { router.push(target.href); }, confirmProps: { - children: t("common.action.discard"), + children: tCommon("action.discard"), }, }); }; @@ -325,5 +327,5 @@ const usePreventLeaveWithDirty = (isDirty: boolean) => { window.removeEventListener("popstate", handlePopState); window.removeEventListener("beforeunload", handleBeforeUnload); }; - }, [isDirty, openConfirmModal, router, t]); + }, [isDirty, openConfirmModal, router, tBoard, tCommon]); }; diff --git a/apps/nextjs/src/app/[locale]/boards/(content)/not-found.tsx b/apps/nextjs/src/app/[locale]/boards/(content)/not-found.tsx index 5ab6c8c61..016d0163e 100644 --- a/apps/nextjs/src/app/[locale]/boards/(content)/not-found.tsx +++ b/apps/nextjs/src/app/[locale]/boards/(content)/not-found.tsx @@ -16,15 +16,15 @@ export default async function NotFoundBoardHomePage() { const getPropsAsync = async (): Promise => { const boardCount = await db.$count(boards); - const t = await getI18n(); + const t = await getI18n("board"); if (boardCount === 0) { return { icon: { src: "/favicon.ico", alt: "Homarr logo" }, - title: t("board.error.noBoard.title"), - description: t("board.error.noBoard.description"), - link: { label: t("board.error.noBoard.link"), href: "/manage/boards" }, - notice: t("board.error.noBoard.notice"), + title: t("error.noBoard.title"), + description: t("error.noBoard.description"), + link: { label: t("error.noBoard.link"), href: "/manage/boards" }, + notice: t("error.noBoard.notice"), }; } @@ -39,9 +39,9 @@ const getPropsAsync = async (): Promise => { return { icon: IconHomeOff, - title: t(`board.error.homeBoard.title`), - description: t(`board.error.homeBoard.${type}.description`), - link: { label: t(`board.error.homeBoard.${type}.link`), href }, - notice: t(`board.error.homeBoard.${type}.notice`), + title: t("error.homeBoard.title"), + description: t(`error.homeBoard.${type}.description` as never), + link: { label: t(`error.homeBoard.${type}.link` as never), href }, + notice: t(`error.homeBoard.${type}.notice` as never), }; }; 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 b0e75d34d..674e5f29b 100644 --- a/apps/nextjs/src/app/[locale]/boards/[name]/settings/_appereance.tsx +++ b/apps/nextjs/src/app/[locale]/boards/[name]/settings/_appereance.tsx @@ -37,22 +37,23 @@ const progressPercentageLabel = (value: number) => `${value}%`; export const ColorSettingsContent = ({ form }: Props) => { const [showPreview, { toggle }] = useDisclosure(false); - const t = useI18n(); + const tBoard = useI18n("board"); + const tCommon = useI18n("common"); const theme = useMantineTheme(); return ( - + - + - + - {showPreview ? t("common.preview.hide") : t("common.preview.show")} + {showPreview ? tCommon("preview.hide") : tCommon("preview.show")} @@ -63,7 +64,7 @@ export const ColorSettingsContent = ({ form }: Props) => { - + { color[6])} flex={1} @@ -90,20 +91,20 @@ export const ColorSettingsContent = ({ form }: Props) => { onClick={() => form.setFieldValue("iconColor", "")} disabled={!form.values.iconColor} > - {t("board.field.clearColor.label")} + {tBoard("field.clearColor.label")} } value={values[kind] ?? ""} onChange={(event) => setValues((current) => ({ ...current, [kind]: event.currentTarget.value }))} 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 e4ea2fc02..52340a72e 100644 --- a/apps/nextjs/src/app/[locale]/manage/_components/hero-banner.tsx +++ b/apps/nextjs/src/app/[locale]/manage/_components/hero-banner.tsx @@ -1,12 +1,12 @@ import { Box, Group, Image, Stack, Title } from "@mantine/core"; -import { getScopedI18n } from "@homarr/translation/server"; +import { getI18n } from "@homarr/translation/server"; import { IntegrationMarquee } from "@homarr/ui"; import classes from "./hero-banner.module.css"; export const HeroBanner = async () => { - const t = await getScopedI18n("management.page.home"); + const t = await getI18n("management.page.home"); return ( diff --git a/apps/nextjs/src/app/[locale]/manage/about/page.tsx b/apps/nextjs/src/app/[locale]/manage/about/page.tsx index 9577d39a0..d0388f427 100644 --- a/apps/nextjs/src/app/[locale]/manage/about/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/about/page.tsx @@ -35,7 +35,7 @@ import { import { capitalize, objectEntries } from "@homarr/common"; import { hotkeys } from "@homarr/definitions"; -import { getScopedI18n } from "@homarr/translation/server"; +import { getI18n } from "@homarr/translation/server"; import { homarrLogoPath } from "~/components/layout/logo/homarr-logo"; import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb"; @@ -47,7 +47,7 @@ import crowdinContributors from "@static-data/translators.json"; import classes from "./about.module.css"; export async function generateMetadata() { - const t = await getScopedI18n("management"); + const t = await getI18n("management"); return { title: createMetaTitle(t("metaTitle")), @@ -55,7 +55,7 @@ export async function generateMetadata() { } export default async function AboutPage() { - const t = await getScopedI18n("management.page.about"); + const t = await getI18n("management.page.about"); const version = getPackageVersion(); const dependencies = await getDependenciesAsync(); diff --git a/apps/nextjs/src/app/[locale]/manage/apps/_app-delete-button.tsx b/apps/nextjs/src/app/[locale]/manage/apps/_app-delete-button.tsx index 190737611..477dffa24 100644 --- a/apps/nextjs/src/app/[locale]/manage/apps/_app-delete-button.tsx +++ b/apps/nextjs/src/app/[locale]/manage/apps/_app-delete-button.tsx @@ -9,15 +9,15 @@ import { clientApi } from "@homarr/api/client"; import { revalidatePathActionAsync } from "@homarr/common/client"; import { useConfirmModal } from "@homarr/modals"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; -import { useScopedI18n } from "@homarr/translation/client"; +import { useI18n } from "@homarr/translation/client"; interface AppDeleteButtonProps { app: RouterOutputs["app"]["all"][number]; } export const AppDeleteButton = ({ app }: AppDeleteButtonProps) => { - const t = useScopedI18n("app.page.delete"); - const tList = useScopedI18n("app.page.list.action"); + const t = useI18n("app.page.delete"); + const tCommon = useI18n("common"); const { openConfirmModal } = useConfirmModal(); const { mutate, isPending } = clientApi.app.delete.useMutation(); @@ -33,14 +33,14 @@ export const AppDeleteButton = ({ app }: AppDeleteButtonProps) => { { onSuccess: () => { showSuccessNotification({ - title: t("notification.success.title"), + title: tCommon("notification.delete.success"), message: t("notification.success.message"), }); void revalidatePathActionAsync("/manage/apps"); }, onError: () => { showErrorNotification({ - title: t("notification.error.title"), + title: tCommon("notification.delete.error"), message: t("notification.error.message"), }); }, @@ -48,7 +48,7 @@ export const AppDeleteButton = ({ app }: AppDeleteButtonProps) => { ); }, }); - }, [app, mutate, t, openConfirmModal]); + }, [app, mutate, t, tCommon, openConfirmModal]); return ( { color="red" size={44} onClick={onClick} - aria-label={tList("delete", { name: app.name })} + aria-label={tCommon("action.deleteNamed", { name: app.name })} > diff --git a/apps/nextjs/src/app/[locale]/manage/apps/edit/[id]/_app-edit-form.tsx b/apps/nextjs/src/app/[locale]/manage/apps/edit/[id]/_app-edit-form.tsx index 0f14be6b4..cf4be14b8 100644 --- a/apps/nextjs/src/app/[locale]/manage/apps/edit/[id]/_app-edit-form.tsx +++ b/apps/nextjs/src/app/[locale]/manage/apps/edit/[id]/_app-edit-form.tsx @@ -9,7 +9,7 @@ import { clientApi } from "@homarr/api/client"; import { revalidatePathActionAsync } from "@homarr/common/client"; import { AppForm } from "@homarr/forms-collection"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; -import { useI18n, useScopedI18n } from "@homarr/translation/client"; +import { useI18n } from "@homarr/translation/client"; import type { appManageSchema } from "@homarr/validation/app"; interface AppEditFormProps { @@ -17,14 +17,14 @@ interface AppEditFormProps { } export const AppEditForm = ({ app }: AppEditFormProps) => { - const tScoped = useScopedI18n("app.page.edit.notification"); - const t = useI18n(); + const tScoped = useI18n("app.page.edit.notification"); + const tCommon = useI18n("common"); const router = useRouter(); const { mutate, isPending } = clientApi.app.update.useMutation({ onSuccess: () => { showSuccessNotification({ - title: tScoped("success.title"), + title: tCommon("notification.update.success"), message: tScoped("success.message"), }); void revalidatePathActionAsync("/manage/apps").then(() => { @@ -33,7 +33,7 @@ export const AppEditForm = ({ app }: AppEditFormProps) => { }, onError: () => { showErrorNotification({ - title: tScoped("error.title"), + title: tCommon("notification.update.error"), message: tScoped("error.message"), }); }, @@ -52,7 +52,7 @@ export const AppEditForm = ({ app }: AppEditFormProps) => { return ( ) : ( ))} diff --git a/apps/nextjs/src/app/[locale]/manage/assistant/_components/assistant-configuration.tsx b/apps/nextjs/src/app/[locale]/manage/assistant/_components/assistant-configuration.tsx index 4c1b86132..9c757048b 100644 --- a/apps/nextjs/src/app/[locale]/manage/assistant/_components/assistant-configuration.tsx +++ b/apps/nextjs/src/app/[locale]/manage/assistant/_components/assistant-configuration.tsx @@ -51,7 +51,7 @@ import { } from "@homarr/definitions"; import type { AssistantProvider, AssistantProviderCategory } from "@homarr/definitions"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; -import { useScopedI18n } from "@homarr/translation/client"; +import { useI18n } from "@homarr/translation/client"; import { getWorkshopAssistantProviderUrl } from "~/components/workshop/workshop-client"; @@ -140,7 +140,8 @@ const ConfigurationSkeleton = ({ label }: { label: string }) => ( ); export const AssistantConfiguration = () => { - const t = useScopedI18n("management.page.settings.section.assistant"); + const t = useI18n("management.page.settings.section.assistant"); + const tCommon = useI18n("common"); const router = useRouter(); const utils = clientApi.useUtils(); const { data: configuration, isLoading } = clientApi.assistant.getAdminConfiguration.useQuery(); @@ -208,7 +209,7 @@ export const AssistantConfiguration = () => { .filter((providerId) => assistantProviderPresets[providerId].category === category) .map((providerId) => ({ value: providerId, - label: t(`provider.options.${providerId}.label`), + label: assistantProviderPresets[providerId].label ?? t("provider.options.custom.label"), })); return { group: t(`provider.groups.${category}`), items }; @@ -372,7 +373,7 @@ export const AssistantConfiguration = () => { - {t(`provider.options.${provider}.label`)} + {assistantProviderPresets[provider].label ?? t("provider.options.custom.label")} { description={t("connection.description")} status={ - {t(`provider.options.${provider}.label`)} + {assistantProviderPresets[provider].label ?? t("provider.options.custom.label")} } > @@ -758,7 +759,7 @@ export const AssistantConfiguration = () => { {t("serverTools.webSearch.label")} - {t("serverTools.beta")} + {tCommon("beta")} diff --git a/apps/nextjs/src/app/[locale]/manage/assistant/page.tsx b/apps/nextjs/src/app/[locale]/manage/assistant/page.tsx index d8482fcfc..479beeec1 100644 --- a/apps/nextjs/src/app/[locale]/manage/assistant/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/assistant/page.tsx @@ -2,13 +2,13 @@ import { notFound } from "next/navigation"; import { Text } from "@mantine/core"; import { auth } from "@homarr/auth/next"; -import { getScopedI18n } from "@homarr/translation/server"; +import { getI18n } from "@homarr/translation/server"; import { ManagePageLayout } from "~/components/manage/manage-page-layout"; import { AssistantConfiguration } from "./_components/assistant-configuration"; export async function generateMetadata() { - const t = await getScopedI18n("management.page.settings.section.assistant"); + const t = await getI18n("management.page.settings.section.assistant"); return { title: `${t("title")} • Homarr`, }; @@ -21,7 +21,7 @@ export default async function AssistantPage() { notFound(); } - const t = await getScopedI18n("management.page.settings.section.assistant"); + const t = await getI18n("management.page.settings.section.assistant"); return ( diff --git a/apps/nextjs/src/app/[locale]/manage/boards/_components/board-card-menu-dropdown.tsx b/apps/nextjs/src/app/[locale]/manage/boards/_components/board-card-menu-dropdown.tsx index 744f998d5..0d15e68a1 100644 --- a/apps/nextjs/src/app/[locale]/manage/boards/_components/board-card-menu-dropdown.tsx +++ b/apps/nextjs/src/app/[locale]/manage/boards/_components/board-card-menu-dropdown.tsx @@ -10,7 +10,7 @@ import { useSession } from "@homarr/auth/client"; import { revalidatePathActionAsync } from "@homarr/common/client"; import { useConfirmModal, useModalAction } from "@homarr/modals"; import { DuplicateBoardModal } from "@homarr/modals-collection"; -import { useScopedI18n } from "@homarr/translation/client"; +import { useI18n } from "@homarr/translation/client"; import { Link } from "@homarr/ui"; import { useBoardPermissions } from "~/components/board/permissions/client"; @@ -28,8 +28,9 @@ interface BoardCardMenuDropdownProps { } export const BoardCardMenuDropdown = ({ board }: BoardCardMenuDropdownProps) => { - const t = useScopedI18n("management.page.board.action"); - const tCommon = useScopedI18n("common"); + const t = useI18n("management.page.board.action"); + const tRoot = useI18n(); + const tCommon = useI18n("common"); const { hasFullAccess, hasChangeAccess } = useBoardPermissions(board); const { data: session } = useSession(); @@ -57,7 +58,7 @@ export const BoardCardMenuDropdown = ({ board }: BoardCardMenuDropdownProps) => const handleDeletion = useCallback(() => { openConfirmModal({ - title: t("delete.confirm.title"), + title: tCommon("action.deleteNamed", { name: board.name }), children: t("delete.confirm.description", { name: board.name, }), @@ -68,7 +69,7 @@ export const BoardCardMenuDropdown = ({ board }: BoardCardMenuDropdownProps) => }); }, }); - }, [board.id, board.name, deleteBoardMutation, openConfirmModal, t]); + }, [board.id, board.name, deleteBoardMutation, openConfirmModal, t, tCommon]); const handleSetHomeBoard = useCallback(async () => { await setHomeBoardMutation.mutateAsync({ id: board.id }); @@ -97,7 +98,7 @@ export const BoardCardMenuDropdown = ({ board }: BoardCardMenuDropdownProps) => {session?.user.permissions.includes("board-create") && ( }> - {t("duplicate.label")} + {tRoot("board.action.duplicate.title")} )} {hasChangeAccess && ( @@ -122,7 +123,7 @@ export const BoardCardMenuDropdown = ({ board }: BoardCardMenuDropdownProps) => onClick={handleDeletion} disabled={deleteBoardMutation.isPending} > - {t("delete.label")} + {tCommon("action.delete")} )} diff --git a/apps/nextjs/src/app/[locale]/manage/boards/_components/create-board-button.tsx b/apps/nextjs/src/app/[locale]/manage/boards/_components/create-board-button.tsx index 77fed0399..f6c9ed735 100644 --- a/apps/nextjs/src/app/[locale]/manage/boards/_components/create-board-button.tsx +++ b/apps/nextjs/src/app/[locale]/manage/boards/_components/create-board-button.tsx @@ -10,13 +10,13 @@ import { useI18n } from "@homarr/translation/client"; import { ManageMobilePrimaryAction } from "~/components/manage/manage-mobile-primary-action"; export const CreateBoardButton = () => { - const t = useI18n(); + const t = useI18n("management.page.board.action"); const { openModal } = useModalAction(AddBoardModal); return ( ); diff --git a/apps/nextjs/src/app/[locale]/manage/boards/page.tsx b/apps/nextjs/src/app/[locale]/manage/boards/page.tsx index 4fb20c461..c20cf6c74 100644 --- a/apps/nextjs/src/app/[locale]/manage/boards/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/boards/page.tsx @@ -25,7 +25,7 @@ import type { RouterOutputs } from "@homarr/api"; import { api } from "@homarr/api/server"; import { auth } from "@homarr/auth/next"; import { constructBoardPermissions } from "@homarr/auth/shared"; -import { getScopedI18n } from "@homarr/translation/server"; +import { getI18n } from "@homarr/translation/server"; import { Link, UserAvatar } from "@homarr/ui"; import { BoardLayoutThumbnail } from "~/components/board/board-layout-thumbnail"; @@ -36,7 +36,7 @@ import { BoardCardMenuDropdown } from "./_components/board-card-menu-dropdown"; import { CreateBoardButton } from "./_components/create-board-button"; export default async function ManageBoardsPage() { - const t = await getScopedI18n("management.page.board"); + const t = await getI18n("management.page.board"); const session = await auth(); const boards = await api.board.getManageOverview(); const canCreateBoards = session?.user.permissions.includes("board-create"); diff --git a/apps/nextjs/src/app/[locale]/manage/custom-widgets/_beta-banner.tsx b/apps/nextjs/src/app/[locale]/manage/custom-widgets/_beta-banner.tsx index c26960c4d..81e38ec33 100644 --- a/apps/nextjs/src/app/[locale]/manage/custom-widgets/_beta-banner.tsx +++ b/apps/nextjs/src/app/[locale]/manage/custom-widgets/_beta-banner.tsx @@ -3,12 +3,12 @@ import { Alert, Anchor, Text } from "@mantine/core"; import { IconFlask } from "@tabler/icons-react"; -import { useScopedI18n } from "@homarr/translation/client"; +import { useI18n } from "@homarr/translation/client"; const FEEDBACK_URL = "https://github.com/homarr-labs/homarr/discussions/categories/custom-widgets"; export function CustomWidgetBetaBanner() { - const t = useScopedI18n("customWidget.betaBanner"); + const t = useI18n("customWidget.betaBanner"); return ( }> diff --git a/apps/nextjs/src/app/[locale]/manage/custom-widgets/_code-editor.tsx b/apps/nextjs/src/app/[locale]/manage/custom-widgets/_code-editor.tsx index 82d9db64a..275ab1897 100644 --- a/apps/nextjs/src/app/[locale]/manage/custom-widgets/_code-editor.tsx +++ b/apps/nextjs/src/app/[locale]/manage/custom-widgets/_code-editor.tsx @@ -3,20 +3,22 @@ import type { ComponentProps } from "react"; import { CustomWidgetCodeEditor } from "@homarr/custom-widgets/workbench"; -import { useScopedI18n } from "@homarr/translation/client"; +import { invariantTechnicalLabels } from "@homarr/definitions"; +import { useI18n } from "@homarr/translation/client"; type SharedEditorProps = ComponentProps; type CodeEditorProps = Omit; export function CodeEditor(props: CodeEditorProps) { - const t = useScopedI18n("customWidget.editor"); + const t = useI18n("customWidget.editor"); + const tCommon = useI18n("common"); return ( t("componentReference.count", { count }), insertStarter: t("action.insertStarter"), format: t("action.format"), - copy: t("action.copy"), + copy: tCommon("action.copy"), copied: t("action.copied"), schema: t("action.schema"), - schemaTab: t("reference.schema"), + schemaTab: invariantTechnicalLabels.jsonSchema, minimalTab: t("reference.minimal"), fullTab: t("reference.full"), errors: (count) => t("status.errors", { count }), diff --git a/apps/nextjs/src/app/[locale]/manage/custom-widgets/_copy-ai-prompt-button.tsx b/apps/nextjs/src/app/[locale]/manage/custom-widgets/_copy-ai-prompt-button.tsx index 2cc4cb1e4..2e05917c7 100644 --- a/apps/nextjs/src/app/[locale]/manage/custom-widgets/_copy-ai-prompt-button.tsx +++ b/apps/nextjs/src/app/[locale]/manage/custom-widgets/_copy-ai-prompt-button.tsx @@ -5,7 +5,7 @@ import { IconCopy, IconRobot } from "@tabler/icons-react"; import { buildCustomWidgetAiPrompt } from "@homarr/custom-widgets/authoring-prompt"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; -import { useScopedI18n } from "@homarr/translation/client"; +import { useI18n } from "@homarr/translation/client"; import { useOptionalHomarrAssistant } from "~/components/assistant/assistant-context"; @@ -22,7 +22,7 @@ export const CopyAiPromptButton = ({ request, documentationUrl, }: CopyAiPromptButtonProps) => { - const t = useScopedI18n("customWidget"); + const t = useI18n("customWidget"); const assistant = useOptionalHomarrAssistant(); const prompt = () => buildCustomWidgetAiPrompt(undefined, rawResponse, currentConfig, request, documentationUrl); diff --git a/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-actions.tsx b/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-actions.tsx index 02ee78c46..8e376330b 100644 --- a/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-actions.tsx +++ b/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-actions.tsx @@ -22,7 +22,7 @@ import { formatCustomWidgetImportIssues, parseCustomWidgetClipboardDetailed } fr import type { HomarrCustomWidgetV2 } from "@homarr/custom-widgets/core"; import { useConfirmModal } from "@homarr/modals"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; -import { useScopedI18n } from "@homarr/translation/client"; +import { useI18n } from "@homarr/translation/client"; import { Link } from "@homarr/ui"; import { CustomWidgetImportDialog } from "~/components/custom-widgets/custom-widget-import-dialog"; @@ -39,7 +39,7 @@ interface WidgetRef { } export const CustomWidgetRowActions = ({ widget }: { widget: WidgetRef }) => { - const t = useScopedI18n("customWidget"); + const t = useI18n("customWidget"); const { openConfirmModal } = useConfirmModal(); const deleteMutation = clientApi.customWidget.delete.useMutation(); const duplicateMutation = clientApi.customWidget.duplicate.useMutation(); diff --git a/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-advanced-manifest.tsx b/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-advanced-manifest.tsx index 2f2f42e21..4e558f880 100644 --- a/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-advanced-manifest.tsx +++ b/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-advanced-manifest.tsx @@ -4,14 +4,14 @@ import { useEffect, useState } from "react"; import { Accordion } from "@mantine/core"; import { customWidgetDefinitionSchema } from "@homarr/custom-widgets/core"; -import { useScopedI18n } from "@homarr/translation/client"; +import { useI18n } from "@homarr/translation/client"; import { CodeEditor } from "./_code-editor"; import { applyDefinition, parseJson } from "./_custom-widget-form-utils"; import type { CustomWidgetWorkbenchForm } from "./_custom-widget-form-utils"; export function CustomWidgetAdvancedManifest({ form }: { form: CustomWidgetWorkbenchForm }) { - const t = useScopedI18n("customWidget.workbench.builder"); + const t = useI18n("customWidget.workbench.builder"); const serialized = JSON.stringify( { $schema: "homarr-custom-widget-v2", diff --git a/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-ai-card.tsx b/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-ai-card.tsx index 3e817a595..0d41ce0ee 100644 --- a/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-ai-card.tsx +++ b/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-ai-card.tsx @@ -4,7 +4,7 @@ import { Accordion, Button, Group, Stack, Text, TextInput, Textarea } from "@man import { IconClipboard, IconSparkles } from "@tabler/icons-react"; import type { HomarrCustomWidgetV2 } from "@homarr/custom-widgets/core"; -import { useScopedI18n } from "@homarr/translation/client"; +import { useI18n } from "@homarr/translation/client"; import { CopyAiPromptButton } from "./_copy-ai-prompt-button"; @@ -18,7 +18,7 @@ interface AiCardProps { } export function CustomWidgetAiCard(props: AiCardProps) { - const t = useScopedI18n("customWidget.workbench.ai"); + const t = useI18n("customWidget.workbench.ai"); return ( diff --git a/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-form-layout.tsx b/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-form-layout.tsx index 90829066b..85ab7c6e5 100644 --- a/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-form-layout.tsx +++ b/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-form-layout.tsx @@ -5,7 +5,7 @@ import type { Icon } from "@tabler/icons-react"; import { Button, Card, Group, Stack, Text, ThemeIcon } from "@mantine/core"; import { IconAlertTriangle, IconCheck, IconPlayerPlay } from "@tabler/icons-react"; -import { useScopedI18n } from "@homarr/translation/client"; +import { useI18n } from "@homarr/translation/client"; import classes from "./_custom-widget-form.module.css"; @@ -52,7 +52,8 @@ export function SaveActions({ mode: "create" | "edit"; onPreview(): void; }) { - const t = useScopedI18n("customWidget.workbench"); + const t = useI18n("customWidget.workbench"); + const tCommon = useI18n("common"); return ( @@ -77,7 +78,7 @@ export function SaveActions({ {previewPending ? t("action.previewLoading") : t("action.preview")} diff --git a/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-form.tsx b/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-form.tsx index c1cb2b876..3a416a250 100644 --- a/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-form.tsx +++ b/apps/nextjs/src/app/[locale]/manage/custom-widgets/_custom-widget-form.tsx @@ -22,7 +22,7 @@ import { customWidgetOptionsSchema, getCustomWidgetDefaultOptions } from "@homar import type { CustomWidgetFormValues } from "@homarr/custom-widgets/workbench"; import { useZodForm } from "@homarr/form"; import { IconPicker } from "@homarr/forms-collection"; -import { useScopedI18n } from "@homarr/translation/client"; +import { useI18n } from "@homarr/translation/client"; import { CodeEditor } from "./_code-editor"; import { CustomWidgetAiCard } from "./_custom-widget-ai-card"; @@ -57,8 +57,9 @@ const sectionLinks = [ ] as const; export function CustomWidgetForm({ mode, initialValues, definitionId }: CustomWidgetFormProps) { - const t = useScopedI18n("customWidget"); - const w = useScopedI18n("customWidget.workbench"); + const t = useI18n("customWidget"); + const tCommon = useI18n("common"); + const w = useI18n("customWidget.workbench"); const formInitialValues: CustomWidgetFormValues = { ...DEFAULT_CUSTOM_WIDGET_FORM_VALUES, ...initialValues }; const form = useZodForm(customWidgetFormSchema, { initialValues: formInitialValues }); const [mobilePane, setMobilePane] = useState<"configure" | "preview">("configure"); @@ -169,7 +170,7 @@ export function CustomWidgetForm({ mode, initialValues, definitionId }: CustomWi /> - +