diff --git a/apps/nextjs/src/components/board/advanced-focus/advanced-focus.module.css b/apps/nextjs/src/components/board/advanced-focus/advanced-focus.module.css index 60cc31954..c108d72c8 100644 --- a/apps/nextjs/src/components/board/advanced-focus/advanced-focus.module.css +++ b/apps/nextjs/src/components/board/advanced-focus/advanced-focus.module.css @@ -35,7 +35,7 @@ .surfaceHostAdvanced { position: fixed; inset: 0; - z-index: calc(var(--mantine-z-index-modal) - 1); + z-index: calc(var(--mantine-z-index-popover) - 1); width: auto; height: auto; pointer-events: none; diff --git a/apps/nextjs/src/components/board/advanced-focus/context.tsx b/apps/nextjs/src/components/board/advanced-focus/context.tsx index 3f38bdc44..4a5935efe 100644 --- a/apps/nextjs/src/components/board/advanced-focus/context.tsx +++ b/apps/nextjs/src/components/board/advanced-focus/context.tsx @@ -201,7 +201,7 @@ export const BoardAdvancedFocusProvider = ({ children }: PropsWithChildren) => { data-advanced-focus-overlay backgroundOpacity={0.38} blur={0} - zIndex="calc(var(--mantine-z-index-modal) - 2)" + zIndex="calc(var(--mantine-z-index-popover) - 2)" className={`${classes.backdrop} ${active.phase === "closing" ? classes.backdropClosing : ""}`} aria-hidden /> diff --git a/apps/nextjs/src/components/board/advanced-focus/wheel.ts b/apps/nextjs/src/components/board/advanced-focus/wheel.ts new file mode 100644 index 000000000..4cf5bc36d --- /dev/null +++ b/apps/nextjs/src/components/board/advanced-focus/wheel.ts @@ -0,0 +1,30 @@ +const canScrollVertically = (element: HTMLElement, delta: number) => { + if (element.scrollHeight <= element.clientHeight) return false; + + const overflowY = getComputedStyle(element).overflowY; + if (overflowY !== "auto" && overflowY !== "scroll") return false; + + return delta < 0 ? element.scrollTop > 0 : element.scrollTop + element.clientHeight < element.scrollHeight; +}; + +export const redirectShiftWheel = (root: HTMLElement, target: EventTarget | null, delta: number) => { + if (delta === 0) return false; + + let current = target instanceof HTMLElement ? target : null; + while (current && root.contains(current)) { + if (canScrollVertically(current, delta)) { + current.scrollTop += delta; + return true; + } + if (current === root) break; + current = current.parentElement; + } + + const viewport = [...root.querySelectorAll("[data-scrollbars='y'], [data-scrollbars='xy']")].find( + (element) => canScrollVertically(element, delta), + ); + if (!viewport) return false; + + viewport.scrollTop += delta; + return true; +}; diff --git a/e2e/board-advanced-interactions.spec.ts b/e2e/board-advanced-interactions.spec.ts index eabb0a33f..2a2a32303 100644 --- a/e2e/board-advanced-interactions.spec.ts +++ b/e2e/board-advanced-interactions.spec.ts @@ -115,6 +115,18 @@ describe("Board advanced interactions", () => { await expect(page.getByRole("button", { name: /advanced view is pinned|keep advanced view open/i })).toHaveCount( 0, ); + await advancedSurface.evaluate((element) => { + const probe = document.createElement("div"); + probe.dataset.shiftScrollProbe = "true"; + probe.style.cssText = "height:80px;overflow-y:auto"; + probe.innerHTML = '
'; + element.append(probe); + }); + const scrollProbe = advancedSurface.locator("[data-shift-scroll-probe]"); + await page.keyboard.down("Shift"); + await scrollProbe.dispatchEvent("wheel", { shiftKey: true, deltaX: 120, deltaY: 0 }); + await page.keyboard.up("Shift"); + expect(await scrollProbe.evaluate((element) => element.scrollTop)).toBeGreaterThan(0); await page.keyboard.press("Escape"); await expect(advancedSurface).toBeHidden(); await expect(widget).toBeFocused(); diff --git a/packages/integrations/src/interfaces/media-server/media-server-types.ts b/packages/integrations/src/interfaces/media-server/media-server-types.ts index c9a287155..42b6f0615 100644 --- a/packages/integrations/src/interfaces/media-server/media-server-types.ts +++ b/packages/integrations/src/interfaces/media-server/media-server-types.ts @@ -1,7 +1,7 @@ export interface StreamSession { sessionId: string; sessionName: string; - user: { + user?: { userId: string; username: string; profilePictureUrl: string | null; diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index 7c0ed58ce..1368070be 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -3506,6 +3506,7 @@ "items": { "currentlyPlaying": "Currently playing", "user": "User", + "unknownUser": "Unknown user", "name": "Name", "id": "Id", "status": "Status", diff --git a/packages/widgets/src/app/index.ts b/packages/widgets/src/app/index.ts index 2b03f0e99..7b8416ca9 100644 --- a/packages/widgets/src/app/index.ts +++ b/packages/widgets/src/app/index.ts @@ -15,6 +15,7 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("app", { icon: IconApps, + supportsAdvancedFocus: false, queryKeys: [[["app", "byId"]], [["widget", "app", "ping"]]], createOptions(settings) { return optionsBuilder.from( diff --git a/packages/widgets/src/beszel-system-grid/index.ts b/packages/widgets/src/beszel-system-grid/index.ts index 9a5444e28..b3ef14806 100644 --- a/packages/widgets/src/beszel-system-grid/index.ts +++ b/packages/widgets/src/beszel-system-grid/index.ts @@ -6,6 +6,7 @@ import { optionsBuilder } from "../options"; export const { definition, componentLoader } = createWidgetDefinition("beszelSystemGrid", { icon: IconLayoutGrid, + supportsAdvancedFocus: false, queryKey: [["widget", "beszel", "getSystems"]], supportedIntegrations: ["beszel", "mock"], integrationsRequired: true, diff --git a/packages/widgets/src/beszel-system-table/index.ts b/packages/widgets/src/beszel-system-table/index.ts index 76fb0264f..4007bebc0 100644 --- a/packages/widgets/src/beszel-system-table/index.ts +++ b/packages/widgets/src/beszel-system-table/index.ts @@ -25,6 +25,7 @@ const sortDirectionOptions = [ export const { definition, componentLoader } = createWidgetDefinition("beszelSystemTable", { icon: IconTable, + supportsAdvancedFocus: false, queryKeys: [[["widget", "beszel", "getSystems"]], [["widget", "beszel", "getSystemStats"]]], queryMatcher: (query, scope) => query.path.at(-1) === "getSystems" diff --git a/packages/widgets/src/beszel/_shared/disk-usage.tsx b/packages/widgets/src/beszel/_shared/disk-usage.tsx index 9aa2a14ad..826d5c3c8 100644 --- a/packages/widgets/src/beszel/_shared/disk-usage.tsx +++ b/packages/widgets/src/beszel/_shared/disk-usage.tsx @@ -1,4 +1,4 @@ -import { Box, Group, HoverCard, Progress, Stack, Text, UnstyledButton } from "@mantine/core"; +import { Box, getDefaultZIndex, Group, HoverCard, Progress, Stack, Text, UnstyledButton } from "@mantine/core"; import type { BeszelSystemRow } from "./types"; import { thresholdColor } from "./colors"; @@ -28,6 +28,7 @@ export const DiskUsage = ({ system, fontSize, progressSize, valueMiw, valueGap = {formatPercent(system.disk)} undefined; @@ -32,3 +40,22 @@ describe("getWidgetQueryKeys", () => { expect(getWidgetRuntimeQueries(widgetStateRef)).toEqual(runtimeQueries); }); }); + +describe("supportsAdvancedFocus", () => { + const definition = { supportsAdvancedFocus: undefined } as Parameters[0]; + + test("defaults to enabled", () => { + expect(supportsAdvancedFocus(definition)).toBe(true); + }); + + test("allows widgets to opt out", () => { + expect(supportsAdvancedFocus({ ...definition, supportsAdvancedFocus: false })).toBe(false); + }); + + test.each([appDefinition, beszelSystemGridDefinition, beszelSystemTableDefinition])( + "keeps non-enhanced widgets compact", + (widgetDefinition) => { + expect(supportsAdvancedFocus(widgetDefinition)).toBe(false); + }, + ); +}); diff --git a/packages/widgets/src/index.tsx b/packages/widgets/src/index.tsx index ba5aad490..879befb72 100644 --- a/packages/widgets/src/index.tsx +++ b/packages/widgets/src/index.tsx @@ -79,6 +79,7 @@ export { getWidgetQueryKeys, getWidgetRuntimeQueries, normalizeWidgetQuery, + supportsAdvancedFocus, widgetQueryValueEquals, } from "./definition"; export type { WidgetComponentProps };