feat(widgets): readable dashboard widths (per-widget min width, ultra-wide caps, table card layout) #55

Merged
KofTwentyTwo merged 6 commits from feature/widget-width-readability into main 2026-10-12 16:55:08 +00:00
KofTwentyTwo commented 2026-10-12 00:57:09 +00:00 (Migrated from github.com)

Summary

Admin dashboards stay readable at every width (360px to 3200px, sidebar open or closed). This finishes the work Website-Backend started in KofTwentyTwo/Website-Backend#91 (v0.1.40) on the Next UI side.

Stacked on #50. widgetColumnClasses and the container-query grid come from #50, which hasn't merged yet. Until it does, this diff also contains #50's commits. Only the commits from a163dc5 onward are new here. I'll merge main into this branch as the release train moves; no rebase.

1. Per-widget minimum width (defaultValues, no QQQ release needed)

  • gridMinWidth: the narrowest the widget may be, in rem. It accepts a number, "28rem" or "448px". A widget shares a row only when the grid container gives it, and a neighbour with the same minimum, at least that width. The check uses container queries, so an open sidebar is accounted for. Values snap up to tiers of 16/20/24/28/32/40/48/64rem, and anything wider always takes the full row. Each span t turns on at 12(m-(t-1)g)/t + 11g (gap g = 1.25rem). Spans above 6 also require the remaining 12-t columns to fit, so they never leave a hole that is too narrow. The thresholds are static Tailwind classes, and a unit test regenerates them from the formula.
  • gridProfile: widgets | cards | table (alias of cards) | fields. It picks one of the existing #50 ladders instead of the type default.
  • Precedence: gridCols:sizeClass:* > gridMinWidth > gridProfile > the type default.

2. Ultra-wide

  • table and multiTable widget content (table, header, paging) stops growing at 96rem. defaultValues.tableMaxWidth overrides this with rem or a CSS length, or "none" removes the cap.
  • A page max-width option: supplementalAppMetaData.dashboardLayout.maxContentWidth (rem or a CSS length) centres the app home at that width. The backend needs a small QSupplementalAppMetaData subclass with getType() = "dashboardLayout", included in frontend metadata. This uses an app's existing extension point, so no QQQ release is needed.

3. QqqTableWidget card layout

  • Rows render as stacked label: value cards in two cases:
    • the widget is narrower than 30rem;
    • the widget is narrower than its declared column widths need (6rem per fr/unsized column plus the fixed widths).
  • The layout switch is data-qqq-table-layout="cards", measured with the existing useContainerWidth hook. It is styled by unlayered CSS in globals.css and keeps a single DOM.
  • In card layout the table has no inline min-width, no table-fixed and no colgroup, and its wrapper is overflow-x: hidden. It never scrolls sideways.

Notes

  • CSS budget: perf-budget.json raises the CSS cap from 21 to 22 KB gzip. CI measured 21.1 KB, against 20.4 KB on #50. The extra 0.7 KB is the ladder classes plus the card-layout CSS.

  • Acceptance specs: on phones and tablets, the WID-019 and WID-068 table widgets now use card layout, so those specs check the layout first:

    • card layout: no sideways scroll, and every value has its data-label;
    • table layout: the existing sticky-header, fr-width, header-help and overflow-x: auto checks.

    In card layout the header row is screen-reader-only (clipped), not display: none, so column headers stay in the accessibility tree.

  • Trade-off: in card layout, column-header help (the columnHeader=* slot) can't be hovered, because the header is visually hidden. It is still there for screen readers.

Tests

  • Unit tests (vitest), covering:

    • the ladder table against the formula;
    • each item and its neighbour getting at least their minimum at every threshold;
    • hint parsing and precedence;
    • tableCardBreakpointPx;
    • table/card switching with a stubbed ResizeObserver;
    • tableMaxWidth and dashboardLayout.maxContentWidth.
  • Playwright tests/e2e/dashboard-widths.spec.ts: a mock dashboard with two gridMinWidth: 28 tables, two HTML widgets, a full-width table and a table with declared widths. Run at 390, 768, 1024 (sidebar), 1440, 1920 and 3200. At each width it asserts:

    • no sideways overflow on the page or in any scroll area;
    • the two tables pair only when the grid is at least 57.5rem wide;
    • card vs table layout matches the measured widths;
    • the full-width table content is at most 96rem.

    It also checks the card labels on a phone and the capped page at 3200. Locally: 8/8 pass against the production build. Set QQQ_E2E_SCREENSHOT_DIR to save the screenshots below.

Screenshots

Width Dashboard
390px
768px
1024px (sidebar)
1440px
1920px
3200px
3200px, maxContentWidth: 120

Backend hints (Website-Backend, once released)

  • Paired table widgets: "defaultValues": {"gridMinWidth": 28} (or 24 for 3-4 short columns).
  • Charts or HTML that should stay half width longer: "gridProfile": "fields". To make them stack earlier, use "gridMinWidth": 32 or "gridProfile": "table".
  • Optional page cap: a dashboardLayout supplemental app metadata with maxContentWidth: 160 (2560px).

🤖 Generated with Claude Code

## Summary Admin dashboards stay readable at every width (360px to 3200px, sidebar open or closed). This finishes the work Website-Backend started in KofTwentyTwo/Website-Backend#91 (v0.1.40) on the Next UI side. > **Stacked on #50.** `widgetColumnClasses` and the container-query grid come from #50, which hasn't merged yet. Until it does, this diff also contains #50's commits. Only the commits from **a163dc5** onward are new here. I'll merge main into this branch as the release train moves; no rebase. ### 1. Per-widget minimum width (`defaultValues`, no QQQ release needed) - `gridMinWidth`: the narrowest the widget may be, in rem. It accepts a number, `"28rem"` or `"448px"`. A widget shares a row only when the **grid container** gives it, and a neighbour with the same minimum, at least that width. The check uses container queries, so an open sidebar is accounted for. Values snap up to tiers of 16/20/24/28/32/40/48/64rem, and anything wider always takes the full row. Each span `t` turns on at `12(m-(t-1)g)/t + 11g` (gap g = 1.25rem). Spans above 6 also require the remaining `12-t` columns to fit, so they never leave a hole that is too narrow. The thresholds are static Tailwind classes, and a unit test regenerates them from the formula. - `gridProfile`: `widgets` | `cards` | `table` (alias of `cards`) | `fields`. It picks one of the existing #50 ladders instead of the type default. - Precedence: `gridCols:sizeClass:*` > `gridMinWidth` > `gridProfile` > the type default. ### 2. Ultra-wide - `table` and `multiTable` widget content (table, header, paging) stops growing at **96rem**. `defaultValues.tableMaxWidth` overrides this with rem or a CSS length, or `"none"` removes the cap. - A page max-width option: `supplementalAppMetaData.dashboardLayout.maxContentWidth` (rem or a CSS length) centres the app home at that width. The backend needs a small `QSupplementalAppMetaData` subclass with `getType() = "dashboardLayout"`, included in frontend metadata. This uses an app's existing extension point, so no QQQ release is needed. ### 3. `QqqTableWidget` card layout - Rows render as stacked `label: value` cards in two cases: - the widget is narrower than 30rem; - the widget is narrower than its declared column widths need (6rem per `fr`/unsized column plus the fixed widths). - The layout switch is `data-qqq-table-layout="cards"`, measured with the existing `useContainerWidth` hook. It is styled by unlayered CSS in `globals.css` and keeps a single DOM. - In card layout the table has no inline `min-width`, no `table-fixed` and no `colgroup`, and its wrapper is `overflow-x: hidden`. It never scrolls sideways. ### Notes - **CSS budget:** `perf-budget.json` raises the CSS cap from 21 to 22 KB gzip. CI measured 21.1 KB, against 20.4 KB on #50. The extra 0.7 KB is the ladder classes plus the card-layout CSS. - **Acceptance specs:** on phones and tablets, the WID-019 and WID-068 table widgets now use card layout, so those specs check the layout first: - card layout: no sideways scroll, and every value has its `data-label`; - table layout: the existing sticky-header, `fr`-width, header-help and `overflow-x: auto` checks. In card layout the header row is screen-reader-only (clipped), not `display: none`, so column headers stay in the accessibility tree. - **Trade-off:** in card layout, column-header help (the `columnHeader=*` slot) can't be hovered, because the header is visually hidden. It is still there for screen readers. ## Tests - Unit tests (vitest), covering: - the ladder table against the formula; - each item and its neighbour getting at least their minimum at every threshold; - hint parsing and precedence; - `tableCardBreakpointPx`; - table/card switching with a stubbed `ResizeObserver`; - `tableMaxWidth` and `dashboardLayout.maxContentWidth`. - Playwright `tests/e2e/dashboard-widths.spec.ts`: a mock dashboard with two `gridMinWidth: 28` tables, two HTML widgets, a full-width table and a table with declared widths. Run at 390, 768, 1024 (sidebar), 1440, 1920 and 3200. At each width it asserts: - no sideways overflow on the page or in any scroll area; - the two tables pair only when the grid is at least 57.5rem wide; - card vs table layout matches the measured widths; - the full-width table content is at most 96rem. It also checks the card labels on a phone and the capped page at 3200. Locally: 8/8 pass against the production build. Set `QQQ_E2E_SCREENSHOT_DIR` to save the screenshots below. ## Screenshots | Width | Dashboard | |---|---| | 390px | <img src="https://github.com/QRun-IO/qqq-frontend-next/blob/pr-assets/widget-width-readability/dashboard-390.png?raw=true" width="300"> | | 768px | <img src="https://github.com/QRun-IO/qqq-frontend-next/blob/pr-assets/widget-width-readability/dashboard-768.png?raw=true" width="420"> | | 1024px (sidebar) | <img src="https://github.com/QRun-IO/qqq-frontend-next/blob/pr-assets/widget-width-readability/dashboard-1024-sidebar.png?raw=true" width="420"> | | 1440px | <img src="https://github.com/QRun-IO/qqq-frontend-next/blob/pr-assets/widget-width-readability/dashboard-1440.png?raw=true" width="420"> | | 1920px | <img src="https://github.com/QRun-IO/qqq-frontend-next/blob/pr-assets/widget-width-readability/dashboard-1920.png?raw=true" width="420"> | | 3200px | <img src="https://github.com/QRun-IO/qqq-frontend-next/blob/pr-assets/widget-width-readability/dashboard-3200.png?raw=true" width="420"> | | 3200px, `maxContentWidth: 120` | <img src="https://github.com/QRun-IO/qqq-frontend-next/blob/pr-assets/widget-width-readability/dashboard-3200-capped.png?raw=true" width="420"> | ## Backend hints (Website-Backend, once released) - Paired table widgets: `"defaultValues": {"gridMinWidth": 28}` (or 24 for 3-4 short columns). - Charts or HTML that should stay half width longer: `"gridProfile": "fields"`. To make them stack earlier, use `"gridMinWidth": 32` or `"gridProfile": "table"`. - Optional page cap: a `dashboardLayout` supplemental app metadata with `maxContentWidth: 160` (2560px). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.