From 3b265f6906d8ee9bf860242f067f0c0349d0a9d9 Mon Sep 17 00:00:00 2001 From: ajnart Date: Thu, 10 Sep 2026 20:18:54 +0200 Subject: [PATCH] fix: preserve app text and nested widget scaling --- .agents/skills/codebase-context/SKILL.md | 2 +- .agents/skills/documentation-sync/SKILL.md | 18 ++++-------- .../documentation-sync/agents/openai.yaml | 4 +-- .github/pull_request_template.md | 2 +- AGENTS.md | 4 +-- apps/docs/docs/management/boards/index.mdx | 3 ++ apps/docs/docs/widgets/app/index.mdx | 3 +- .../layout/scaled-board-canvas.module.css | 3 ++ .../board/sections/grid/section-grid.tsx | 22 ++++++++++----- packages/widgets/src/app/component.tsx | 28 +++++++++++++------ 10 files changed, 54 insertions(+), 35 deletions(-) diff --git a/.agents/skills/codebase-context/SKILL.md b/.agents/skills/codebase-context/SKILL.md index 3415a44d1..679c86db6 100644 --- a/.agents/skills/codebase-context/SKILL.md +++ b/.agents/skills/codebase-context/SKILL.md @@ -37,4 +37,4 @@ Orient from the current checkout before editing. Treat package manifests, export - Cron jobs: `packages/cron-jobs/src/jobs/` - App routes: `apps/nextjs/src/app/[locale]/` -When a change affects setup, usage, troubleshooting, or the accuracy of existing docs, invoke `documentation-sync`. When exposing tRPC through MCP, invoke `mcp-integration`. +When a code change affects users, invoke `documentation-sync`. When exposing tRPC through MCP, invoke `mcp-integration`. diff --git a/.agents/skills/documentation-sync/SKILL.md b/.agents/skills/documentation-sync/SKILL.md index 6eec70552..31d4b4852 100644 --- a/.agents/skills/documentation-sync/SKILL.md +++ b/.agents/skills/documentation-sync/SKILL.md @@ -1,21 +1,13 @@ --- name: documentation-sync -description: Keep Homarr's Docusaurus documentation useful and accurate. Use when a change affects what users need to know to configure, use, or troubleshoot Homarr, or makes existing guidance inaccurate. +description: Keep Homarr's Docusaurus documentation aligned with user-facing code. Use when adding or changing integrations, widgets, APIs, environment variables, CLI commands, authentication, permissions, cron jobs, settings, or UI behavior that users must understand. --- # Documentation Sync -Update `apps/docs/` when users need new or corrected guidance to configure, use, or troubleshoot Homarr. Keep documentation straight to the point. +Update `apps/docs/` in the same change as user-facing behavior. Inspect adjacent pages and the docs types before copying a pattern; the source and docs evolve together. -## Decide whether documentation is needed - -Identify the user action, decision, prerequisite, limitation, or troubleshooting step that changes. Update the relevant docs when that information is missing or inaccurate. If nothing users need to know changes, leave the docs untouched. - -Routine visual fixes such as icon sizing, spacing, alignment, responsive layout, and animation usually need no documentation. Describe them in the PR when useful for review. Document a visual change only when it changes a user workflow, setting, or meaningful limitation. - -Write the minimum information needed to complete the user's task. Inspect adjacent pages and the docs types before editing. - -## Map changes that need documentation +## Map the change | Code change | Documentation target | | ----------------------------------- | -------------------------------------------------------------------- | @@ -52,8 +44,8 @@ For a widget: ## Completion criteria -1. Every addition helps a user configure, use, or troubleshoot Homarr; omit incidental rendering and implementation details. +1. Describe the shipped behavior, not an implementation plan. 2. Keep names, defaults, paths, screenshots, links, and prerequisites consistent with code. -3. Correct affected guidance and remove superseded instructions. A code change alone does not require a docs edit. +3. Update every affected page and remove superseded guidance. 4. Run the narrowest useful docs validation. Use `pnpm turbo build --filter=@homarr/docs` when links, MDX, generated definitions, or navigation can fail; otherwise run the docs package formatter on touched files. 5. Treat broken links and anchors as failures; Docusaurus checks them strictly. diff --git a/.agents/skills/documentation-sync/agents/openai.yaml b/.agents/skills/documentation-sync/agents/openai.yaml index 7c61256be..9c9b77cc5 100644 --- a/.agents/skills/documentation-sync/agents/openai.yaml +++ b/.agents/skills/documentation-sync/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Homarr Documentation Sync" - short_description: "Keep user docs concise, useful, and accurate" - default_prompt: "Use $documentation-sync to decide whether this Homarr change needs user guidance and update only what users need to know." + short_description: "Keep user-facing docs aligned with code" + default_prompt: "Use $documentation-sync to map this Homarr code change to the required documentation updates." diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 225a00a21..236fd1787 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -10,7 +10,7 @@ - [ ] Pull request targets the intended base branch (`dev` by default; `release/v2` for v2 release work) - [ ] Commits follow the [conventional commits guideline](https://www.conventionalcommits.org/en/v1.0.0/) - [ ] No shorthand variable names are used (for example, `x`, `y`, `i`, or abbreviations) -- [ ] Changes to setup, usage, troubleshooting, or existing guidance include relevant documentation updates under `apps/docs` when needed +- [ ] User-facing changes include matching documentation updates under `apps/docs` - [ ] When using AI, no temporary files are checked in and the code style follows the rest of the project **When contributing a new integration or widget:** diff --git a/AGENTS.md b/AGENTS.md index 41170f581..b0e8a9f9e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -59,7 +59,7 @@ homarr/ The documentation site lives at `apps/docs/` (Docusaurus 3, `@homarr/docs`). -Update documentation when a change affects what users need to know to configure, use, or troubleshoot Homarr, or makes existing guidance inaccurate. Follow `.agents/skills/documentation-sync/SKILL.md` to decide whether an update is needed and keep it concise. For changes that need documentation: +When modifying user-facing code, you MUST also update the corresponding documentation: - New integration → `apps/docs/docs/integrations//index.mdx` + `index.ts` - New widget → `apps/docs/docs/widgets//index.mdx` + `index.ts` @@ -99,6 +99,6 @@ Update documentation when a change affects what users need to know to configure, Portable skills live in `.agents/skills/`. Read the relevant `SKILL.md` before working in that domain; detailed references are loaded only when needed. Claude-compatible discovery is provided through `.claude/skills`. - `codebase-context` — architecture, package boundaries, and shared utilities -- `documentation-sync` — concise, relevant user guidance +- `documentation-sync` — required user-facing documentation updates - `mcp-integration` — safe tRPC-to-MCP exposure - `homarr-custom-widget` — safe Custom JSX v2 authoring diff --git a/apps/docs/docs/management/boards/index.mdx b/apps/docs/docs/management/boards/index.mdx index 9c1f75a03..46f2118f2 100644 --- a/apps/docs/docs/management/boards/index.mdx +++ b/apps/docs/docs/management/boards/index.mdx @@ -31,6 +31,9 @@ Tiles use fixed `200 × 200` logical-pixel cells with a `24px` gap. Homarr scale width instead of changing each widget's proportions. **Reset from Base** regenerates a Mobile or custom layout from the current Base layout. +Widgets choose compact layouts from their visible size, including scaling inside nested Containers. Text and controls +retain readable sizing as the board shrinks; opening a widget's advanced view uses the available space in that view. + ### Containers and rails A **Container** groups apps, widgets, and nested Containers. It can have a label, collapse control, app-opening action, diff --git a/apps/docs/docs/widgets/app/index.mdx b/apps/docs/docs/widgets/app/index.mdx index fde54d943..1945102b5 100644 --- a/apps/docs/docs/widgets/app/index.mdx +++ b/apps/docs/docs/widgets/app/index.mdx @@ -12,7 +12,8 @@ import { appWidget } from "."; The App widget opens an application or website and can show whether Homarr can reach it. Configure the target and status -check from the tile's edit actions. +check from the tile's edit actions. Very small tiles shorten the title and description to leave room for the icon; +enlarge the tile to show more text. ### Adding the widget diff --git a/apps/nextjs/src/components/board/layout/scaled-board-canvas.module.css b/apps/nextjs/src/components/board/layout/scaled-board-canvas.module.css index 49f71bf7b..cf3845722 100644 --- a/apps/nextjs/src/components/board/layout/scaled-board-canvas.module.css +++ b/apps/nextjs/src/components/board/layout/scaled-board-canvas.module.css @@ -17,7 +17,10 @@ .canvas { position: absolute; inset: 0 auto auto 0; +} +.canvas, +.contentScale { /* * Keep dashboard UI at its normal readable density while the fixed canvas * is fitted below 100%. Above 100%, the canvas still behaves like browser diff --git a/apps/nextjs/src/components/board/sections/grid/section-grid.tsx b/apps/nextjs/src/components/board/sections/grid/section-grid.tsx index e4ed87223..0f8976c36 100644 --- a/apps/nextjs/src/components/board/sections/grid/section-grid.tsx +++ b/apps/nextjs/src/components/board/sections/grid/section-grid.tsx @@ -24,6 +24,7 @@ import { useBoardCanvasScale, useBoardContentScale, } from "~/components/board/layout/scaled-board-canvas"; +import canvasClasses from "~/components/board/layout/scaled-board-canvas.module.css"; import { useGridEditorRuntimeStatus } from "./grid-editor-runtime"; import { createGridEntryElementStore, useGridEditorRegistry } from "./grid-editor-registry"; import type { SectionGridPlacement } from "./use-grid-layout-actions"; @@ -150,10 +151,16 @@ export const SectionGrid = ({ const viewportRowCount = viewportRowCountOverride ?? (isScrollableContainer ? Math.max(requestedRowCount, 1) : rowCount); const effectiveCanvasScale = Number.isFinite(canvasScale) && canvasScale > 0 ? canvasScale : 1; - // The collapsible label keeps a fixed physical height while the grid uses logical canvas pixels. - // Convert that height so the label reserves the same space at every board scale. + let effectiveParentContentScale = effectiveCanvasScale; + if (Number.isFinite(parentContentScale) && parentContentScale > 0) { + effectiveParentContentScale = parentContentScale; + } + // Compensate every ancestor container while retaining the board's enlargement above 100%. + const parentUiScale = + (calculateBoardUiScale(effectiveCanvasScale) * effectiveCanvasScale) / effectiveParentContentScale; + // Match the label's Mantine height in its parent's logical coordinate system. const collapsibleHeaderInset = - section.kind === "container" && section.options.collapsible ? CONTAINER_HEADER_HEIGHT / effectiveCanvasScale : 0; + section.kind === "container" && section.options.collapsible ? CONTAINER_HEADER_HEIGHT * parentUiScale : 0; const fullGridWidth = getLogicalGridSize(columnCount); const fullGridHeight = getLogicalGridSize(rowCount); const logicalWidth = fullGridWidth; @@ -178,7 +185,8 @@ export const SectionGrid = ({ // both reads and writes --board-canvas-ui-scale (even indirectly, through another property) // is a circular reference - CSS invalidates the whole group rather than using "the old value", // silently breaking every icon/text/custom-CSS size that compensates off it for descendants. - const combinedUiScale = calculateBoardUiScale(canvasScale) / containerContentScale; + const combinedUiScale = parentUiScale / containerContentScale; + const contentScale = effectiveParentContentScale * containerContentScale; // A collapsed container's compact coordinates are display-only. Its own // nested grid stays inactive until an explicit edit interaction expands it. const isInteractionDisabled = section.kind === "container" && collapsedSectionIds.has(section.id); @@ -288,21 +296,21 @@ export const SectionGrid = ({ onKeyDownCapture={expandCollapsedSectionsForKeyboardEdit} > - + diff --git a/packages/widgets/src/app/component.tsx b/packages/widgets/src/app/component.tsx index 52cf4c7e8..15bd673e7 100644 --- a/packages/widgets/src/app/component.tsx +++ b/packages/widgets/src/app/component.tsx @@ -1,6 +1,6 @@ "use client"; -import type { PropsWithChildren } from "react"; +import type { CSSProperties, PropsWithChildren } from "react"; import { Fragment, Suspense } from "react"; import { Box, Flex, Stack, Text, Tooltip, UnstyledButton } from "@mantine/core"; import { IconMinus } from "@tabler/icons-react"; @@ -48,6 +48,22 @@ export default function AppWidget({ const textSize = `${14 * contentScale}px`; const spacing = 12 * contentScale; const isColumnLayout = options.layout.startsWith("column"); + const isTiny = Math.min(width, height) * scale < 100; + const textStyle: CSSProperties = {}; + let titleLineClamp: number | undefined; + let descriptionLineClamp = 4; + if (isColumnLayout) textStyle.flexShrink = 0; + if (isTiny) { + titleLineClamp = 2; + descriptionLineClamp = 2; + textStyle.flexShrink = 1; + textStyle.maxWidth = "50%"; + textStyle.maxHeight = "100%"; + if (isColumnLayout) { + textStyle.maxWidth = "100%"; + textStyle.maxHeight = "50%"; + } + } return ( @@ -66,16 +82,12 @@ export default function AppWidget({ style={{ padding: spacing, gap: isColumnLayout ? 0 : spacing / 2 }} onContextMenu={isEditMode ? (e) => e.preventDefault() : undefined} > - + {options.showTitle && ( @@ -88,7 +100,7 @@ export default function AppWidget({ style={{ fontSize: textSize }} ta={isColumnLayout ? "center" : undefined} c="dimmed" - lineClamp={2} + lineClamp={descriptionLineClamp} > {app.description?.split("\n").map((line, index) => (