Skip to content

feat(render): Dashboard UX improvements — 12-column grid, smart defaults, card styling#2720

Open
jswir wants to merge 41 commits into
malloydata:mainfrom
jswir:feat/dashboard-ux
Open

feat(render): Dashboard UX improvements — 12-column grid, smart defaults, card styling#2720
jswir wants to merge 41 commits into
malloydata:mainfrom
jswir:feat/dashboard-ux

Conversation

@jswir
Copy link
Copy Markdown
Contributor

@jswir jswir commented Mar 12, 2026

Summary

  • Dual layout modes: Dashboards without explicit # span, # dashboard { columns }, or # dashboard { gap } tags use a flex layout where items size naturally based on content. When any of those tags are present, the dashboard switches to a 12-column CSS grid with smart default spans (measures → span 3, nests → span 4-12 based on weighted child content).
  • Card styling: All dashboard items (KPIs, charts, tables) render in cards with shadow, border-radius, and hover effects. KPI measure labels match big_value styling (uppercase, letter-spacing, 32px value). KPIs self-size height instead of stretching to match adjacent items.
  • # span = N tag: Explicit 12-column grid span (1-12) on any dashboard item, overrides defaults. Triggers grid mode.
  • # break tag: Starts a new row before the annotated field.
  • # subtitle tag: Adds subtitle text below the item title.
  • # borderless tag: Opt out of card styling on any item type.
  • # dashboard { columns = N } tag: Force N equal columns instead of 12-col grid.
  • # dashboard { gap = N } tag: Custom gap spacing in pixels.
  • # label tag: Custom display label; default is snake_case → Title Case conversion.
  • Responsive breakpoints: @container queries collapse grid to single column at ≤600px, halve to 6 columns at ≤900px. Flex mode wraps naturally.
  • Tables fill width via shouldFillWidth: true, no double scrollbars (overflow-x clipped only on fill-width tables, preserving scroll on wide tables with many columns).
  • big_value integration: Flattened inner card when inside dashboard, hidden redundant header, centered layout.
  • Tag validation: Semantic validation for span (1-12), columns (>0), gap (>=0). Dashboard tags resolved at setup time for headless validation support.
  • KPI card fix: container-type: inline-size and cqi font scaling scoped to grid mode only. In flex mode, KPI cards size dynamically based on title/value content instead of collapsing due to the circular dependency between fit-content width and inline-size containment.
  • Storybook fix: Pre-bundle heavy dependencies (@duckdb/duckdb-wasm, apache-arrow, etc.) via optimizeDeps.include to fix first-load failures from Vite runtime dep discovery.
  • 20+ storybook stories covering all features plus legacy backwards-compat testing.

https://www.loom.com/share/581a55d7cc1c4cd487fec943555d2825

@mtoy-googly-moogly
Copy link
Copy Markdown
Collaborator

mtoy-googly-moogly commented Mar 22, 2026

These three automated review comments I understand and agree with

  1. Fix falsy numeric handling for dashboard tags
    ◦ In packages/malloy-render/src/component/dashboard/dashboard.tsx:
    ▪ Replace truthy checks with explicit undefined checks:
    ▪ if (gap) ... → if (gap !== undefined) ...
    ▪ if (columns || gap) ... → if (columns !== undefined || gap !== undefined) ...
    ▪ if (columns) ... → if (columns !== undefined) ...
    ◦ Why: # dashboard { gap = 0 } is currently ignored (no grid trigger, no gap override), despite validation allowing non-negative gap.

  2. Remove new render-time label/tag reads introduced in dashboard, this breaks how validation works
    ◦ packages/malloy-render/src/component/field-label-utils.ts + dashboard.tsx currently read field.tag at render time (label, subtitle, borderless, span).
    ◦ Direction:
    ▪ Stop using getFieldLabel(...) in dashboard and use existing field.getLabel() (already part of the resolved-field contract in src/data_tree/fields/base.ts).
    ▪ Do not add new render-time tag parsing paths; resolve dashboard item metadata once during setup (same principle used elsewhere in renderer).
    ◦ Why: this is architectural drift and makes headless/virtualized behavior harder to reason about.

This one is outside my area of competence, I pass it on. It feels when reading this like an LLM made a change for you and it made an expedient but not thoughtful change ...

  1. Reduce cross-component CSS coupling from big-value plugin
    ◦ packages/malloy-render/src/plugins/big-value/big-value.css now contains many .dashboard-item... selectors.
    ◦ Direction:
    ▪ Move dashboard-layout-specific overrides into dashboard-owned styling (or gate them tightly to dashboard scope) so big-value plugin styles don’t implicitly control dashboard container behavior.
    ◦ Why: this is scope leak and will create future regressions when dashboard/big-value evolve independently.

There are two more comments from the view session that I am going to think about before I pass them on, but I thought I would hand these back to you now.

@mtoy-googly-moogly
Copy link
Copy Markdown
Collaborator

My AI is pretty unhappy with the storybook deps loading fix. It feels they are expedient, poorly explained and possibly maintenance problem. Again this is not my area of expertise. But until both our AIs are happy that one is a problem.

@mtoy-googly-moogly
Copy link
Copy Markdown
Collaborator

mtoy-googly-moogly commented Mar 22, 2026

The AI thinks there is missing "story" coverage:

Please fill in/confirm only the unresolved rows in this verification matrix:

• gap = 0 behavior (edge case): no explicit story/test currently identified.
• Responsive breakpoint behavior (<=600, 601–900 container widths): currently manual-only; no explicit verification artifact identified.
• New validator branches (span bounds, columns > 0, gap >= 0, and “span ignored in columns mode” warning): no dedicated tests currently identified.

@mtoy-googly-moogly
Copy link
Copy Markdown
Collaborator

mtoy-googly-moogly commented Mar 22, 2026

When generating error messages, to make validation work better for an LLM responding to vlaidation, it would be great if the errors followed something like this pattern (i.e. this is not the rule, just a guideline)

Invalid <tag-path> on '<field>': expected <constraint>, got <value>. Fix: <example> (or <fallback>).

@mtoy-googly-moogly
Copy link
Copy Markdown
Collaborator

This pr seems to accidentally include #2746 -- you need to rebase it so it can be judged separately.

I think grid dashboards stress the validation framework in a way which makes them impossible to correctly validate and I am working on PR to extend validation which will help this.

Also the AI suggests: "Invalid layout values should not still drive layout. Right now # span, # dashboard { columns=... }, and # dashboard { gap=... } are validated, but the raw values are still consumed by the dashboard layout code. Those should log and then fall back to default behavior, not feed broken CSS/layout.

@mtoy-googly-moogly
Copy link
Copy Markdown
Collaborator

Ok #2750 is merged to main now.

I think this PR should be updated to use that new validation ownership machinery for the dashboard child tags. In particular, # span, # subtitle, and # borderless look like dashboard-owned child tags in the same sense that # break is, and I think they should be validated and consumed through the new renderer validation ownership mechanism rather than being resolved globally up front. That would let this PR keep the nice short dashboard markup while fitting cleanly into the new validation architecture.

@jswir jswir force-pushed the feat/dashboard-ux branch from 25f6ca5 to e93bdd0 Compare April 5, 2026 19:43
jswir added 21 commits April 5, 2026 13:52
- Replace flexbox with CSS Grid (auto-fit + 12-column span system)
- Add per-item # span=N tag for explicit grid column control
- Add # columns=N tag for fixed equal-column layouts
- Add # subtitle="..." tag for dashboard item subtitles
- Add # break tag for explicit row breaks
- Add # borderless tag for table items without card chrome
- Add KPI measure styling (no card, centered, auto-scaling font via cqi)
- Add card styling with hover effects and CSS variable-driven theming
- Flatten big_value inner card when inside dashboard (no double border)
- Fix sparkline cropping by scaling chart SVGs to fill container
- Auto-span nest items (tables/charts) to 2 columns by default
- Add getFieldLabel utility for snake_case to Title Case conversion
- Add dashboard CSS variable defaults and theme pass-through
- Add storybook stories for all new layout features

Signed-off-by: James Swirhun <james@credibledata.com>
Made-with: Cursor
- Add container queries for responsive dashboard layout:
  - Below 600px: single column, all spans collapse to full width
  - 601-900px: 12-col grid collapses to 6-col, wide spans go full width
- Add configurable gap via # dashboard { gap=N } tag
- Add --malloy-render--dashboard-gap CSS variable with 16px default
- Add dashboard_custom_gap storybook story

Signed-off-by: James Swirhun <james@credibledata.com>
Made-with: Cursor
- KPI measure titles/subtitles now properly center-aligned (was
  overridden by the base .dashboard-item-title left-align rule)
- Remove redundant CSS width:100% on dashboard tables; the
  shouldFillWidth prop already adds the .full-width class

Made-with: Cursor
The borderless card style applies to any item type, not just tables.
Removed the table-only gate so # borderless works on charts too.

Made-with: Cursor
Measures now render inside cards by default (same as charts/tables).
Use # borderless to opt out of card styling on any dashboard item.
Added dashboard_borderless_kpis story to demonstrate the opt-out.

Made-with: Cursor
- Remove auto span 2 for nests and max-width 500px for big_value;
  let the grid auto-fit or explicit # span control sizing
- When a group mixes # span items with non-span items, compute a
  sensible default span for unspanned items based on remaining cols
- Add stories: tall chart (size.height=400), size=lg in narrow span,
  mixed span defaults

Made-with: Cursor
Two stories with zero new annotations, mimicking real-world dashboards
like malloy-samples/auto_recalls: KPI measures + charts + tables using
only pre-existing features (# dashboard, # line_chart, # bar_chart,
# currency). Tests that the default auto-fit grid handles legacy
dashboards gracefully.

Made-with: Cursor
- Label: add uppercase + letter-spacing 0.025em (matches big_value)
- Value: 32px (was 28px), line-height 1.2 (matches big_value)
- Update CSS variable default to 32px

Made-with: Cursor
The table's own overflow:auto handles scrolling; the dashboard-item-value
wrapper was adding a redundant horizontal scrollbar.

Made-with: Cursor
All dashboard groups now use a 12-column grid instead of auto-fit.
Default spans by item type: measures get span 2, nests (charts/tables)
get span 4. Items wrap naturally when they exceed 12 columns.
Explicit # span still overrides the default.

Made-with: Cursor
Measures at span 2 were too narrow for formatted values. New defaults:
- Measures: span 3 (4 per row)
- Nests (charts/tables): span 6 (2 per row)

Made-with: Cursor
- Measure cards use align-self:start so they don't stretch to match
  taller siblings (charts/tables)
- Nest default span now depends on visible column count:
  <=3 cols -> span 4, <=5 cols -> span 6, >5 cols -> span 8

Made-with: Cursor
Tables or views containing nested children (sparklines, subtables,
embedded charts) need full width. Detect via child.isNest() and
auto-assign span 12.

Made-with: Cursor
Tests that a table containing an embedded bar chart column auto-detects
the sub-nest and gets span 12 (full width).

Made-with: Cursor
Instead of blanket span 12 for any nest with sub-nests, weight each
child: scalar columns = 1, nested children (charts/subtables) = 3.
Thresholds: <=3 -> span 4, <=5 -> span 6, <=8 -> span 8, >8 -> span 12.

E.g. brand + product_count + total_retail + bar_chart = 1+1+1+3 = 6
-> span 6 instead of 12.

Made-with: Cursor
- Validate span (1-12), columns (>0), gap (>=0) in validateFieldTags
- Touch span/subtitle/borderless in resolveBuiltInTags to prevent
  false "unknown tag" warnings in headless validation
- Read columns/gap in resolveDashboardTags and use resolved config
  in Dashboard component instead of re-reading tags at render time
- Fix table horizontal scrollbar: only clip overflow-x on fill-width
  tables inside dashboard cards, preserving scroll on wide tables

Made-with: Cursor
- Replace fragile style*= CSS selectors with data-span attribute
- Preserve null maxTableHeight (no-limit mode) instead of collapsing to 361
- Skip span overrides in columns mode (span and columns are mutually exclusive)
- Warn when # span is used inside a columns-mode dashboard
- Fix invalid // CSS comment to /* */
- Use strict undefined check in getFieldLabel so # label="" is respected
- Add !important explanation comment on sparkline chart overrides
- Remove redundant span tag read in DashboardItem (spanOverride handles it)

Signed-off-by: James Swirhun <james@credibledata.com>
Move container-type: inline-size and cqi font scaling to grid-only
context so KPI measure cards size based on content in flex layout.
The inline-size containment was collapsing cards when combined with
width: fit-content. Also pre-bundle Vite deps to fix Storybook
first-load failures.

Signed-off-by: James Swirhun <james@credibledata.com>
Made-with: Cursor
…dation messages

- Fix falsy numeric handling: gap=0 and columns=0 no longer ignored
- Replace render-time tag reads with setup-time resolved properties
  (label, subtitle, span, break, borderless) on FieldBase
- Delete field-label-utils.ts; use field.getLabel() throughout
- Move dashboard-specific CSS from big-value.css to dashboard.css
- Add explanatory comments for storybook optimizeDeps config
- Rewrite validation messages to follow structured format:
  Invalid <tag-path> on '<field>': expected <constraint>, got <value>. Fix: <example>.
- Add dashboard_gap_zero story for edge case coverage

Signed-off-by: James Swirhun <james@credibledata.com>
Move span, subtitle, break, and borderless tag resolution from global
cross-cutting into dashboard-specific context so these tags are only
consumed when the field is a dashboard child. Register them as
childOwnedPaths in renderer-validation-specs so the ownership model
drives unread-tag warnings correctly.

Signed-off-by: James Swirhun <james@credibledata.com>
@jswir jswir force-pushed the feat/dashboard-ux branch from e93bdd0 to 65a78cd Compare April 5, 2026 19:57
jswir added 7 commits April 5, 2026 13:58
I, James Swirhun <james@credibledata.com>, hereby add my Signed-off-by to this commit: 494df73
I, James Swirhun <james@credibledata.com>, hereby add my Signed-off-by to this commit: f5d8c7a
I, James Swirhun <james@credibledata.com>, hereby add my Signed-off-by to this commit: 9e281ab
I, James Swirhun <james@credibledata.com>, hereby add my Signed-off-by to this commit: 77d7c17
I, James Swirhun <james@credibledata.com>, hereby add my Signed-off-by to this commit: e7203da
I, James Swirhun <james@credibledata.com>, hereby add my Signed-off-by to this commit: 5a17044
I, James Swirhun <james@credibledata.com>, hereby add my Signed-off-by to this commit: b21d1d5
I, James Swirhun <james@credibledata.com>, hereby add my Signed-off-by to this commit: 98fa36a
I, James Swirhun <james@credibledata.com>, hereby add my Signed-off-by to this commit: 68debb4
I, James Swirhun <james@credibledata.com>, hereby add my Signed-off-by to this commit: 10302d1
I, James Swirhun <james@credibledata.com>, hereby add my Signed-off-by to this commit: 45e4327
I, James Swirhun <james@credibledata.com>, hereby add my Signed-off-by to this commit: 64d75be
I, James Swirhun <james@credibledata.com>, hereby add my Signed-off-by to this commit: 44d2518
I, James Swirhun <james@credibledata.com>, hereby add my Signed-off-by to this commit: 95256d3
I, James Swirhun <james@credibledata.com>, hereby add my Signed-off-by to this commit: a3234f3

Signed-off-by: James Swirhun <james@credibledata.com>
…tent-aware collapse

Replace brittle 12-column grid with per-span CSS rules with a per-row
approach. Each row group gets its own grid-template-columns computed
from item span proportions (e.g. 8fr 4fr), and a content-aware
collapse class (sm/md/lg) based on minimum viable width. Rows
collapse independently: all items proportional or all stacked.

- Remove grid-column: span N from individual items
- Compute frTemplate per row group from span proportions
- Bucket collapse thresholds: 400px (KPI rows), 600px (mixed), 900px (wide)
- Columns mode uses repeat(N, 1fr) with same collapse buckets
- Rename stories with _flex/_grid suffixes, reorder flex-first
- Remove debug console.log statements

Signed-off-by: James Swirhun <james@credibledata.com>
# Conflicts:
#	packages/malloy-render/src/render-field-metadata.ts
The merge with main silently took this branch's rewritten error
messages over malloydata#2774's wording. Those message rewrites were scope
drift from this PR's goal (dashboard UX) and malloydata#2774's tests check
for the upstream wording. Revert the pre-existing validator
messages to main's format; keep only the net-new dashboard block.
Add cases for out-of-range # span (too high, zero), non-positive
# dashboard.columns, negative # dashboard.gap, and the span-with-columns
conflict warn. Closes the gap noted in validation.md's pre-PR
checklist — each dashboard validation rule now has a test.
Centralize item min-widths (MIN_WIDTH_MEASURE, MIN_WIDTH_ITEM) and
collapse buckets (COLLAPSE_BUCKETS) at the top of dashboard.tsx.
Replace duplicated <=400/<=600/else ternaries with bucketFor().
Sync flex-mode min-widths in dashboard.css to match the TSX constants
(300/120 in both places) and fix per-row columnsMinWidth to use actual
content type instead of a hardcoded 120 — this was why rows with
charts/tables in columns=N mode were stacking too late.
I, James Swirhun <james@credibledata.com>, hereby add my Signed-off-by to this commit: a819411
I, James Swirhun <james@credibledata.com>, hereby add my Signed-off-by to this commit: c3edb24
I, James Swirhun <james@credibledata.com>, hereby add my Signed-off-by to this commit: 0480a30

Signed-off-by: James Swirhun <james@credibledata.com>
mlennie added 4 commits May 27, 2026 09:25
Drops out-of-range columns, gap, and span values in resolveDashboardTags
so the layout falls back to its default behavior instead of feeding the
bad value into CSS (e.g. repeat(-3, 1fr), gap: -5px, span 15 of 12).

The renderer validation contract from malloydata#2774 already logs a clear error
when the user supplies these values; this change makes sure the dashboard
keeps rendering correctly while the user fixes the tag. Addresses mtoy's
Apr 4 review item on PR malloydata#2720.

Bounds mirror the validator at render-field-metadata.ts:485-525:
  columns: positive integer
  gap:     >= 0
  span:    integer 1..12

The renderer exposes no public hook for inspecting a field's resolved
tag config from a test, so the new behavior is covered by three
Storybook fixtures under DEFENSIVE FALLBACKS in dashboard.stories.malloy
(columns=-3, gap=-5, span=15). The existing validator tests in
render-validator.spec.ts already cover the error-logging side.

Signed-off-by: Monty Lennie <montylennie@gmail.com>
CI's prettier check fails on four pre-existing format issues in
dashboard.tsx that landed on feat/dashboard-ux. Running prettier --write
on just those lines clears them so the rest of the PR can be reviewed
against a green CI. No logic change.

Signed-off-by: Monty Lennie <montylennie@gmail.com>
Drop the inaccurate parenthetical from the dashboard_fallback_bad_span
story comment; the previous wording claimed "4 for nests with one inner
field" but the example has two visible children, and the actual default
comes from computeSpan's weight logic. Comment-only.

Signed-off-by: Monty Lennie <montylennie@gmail.com>
Replace the em-dash with a period and capitalize the next sentence;
otherwise no change to meaning. Comment-only cleanup made while
touching the surrounding lines in the prior prettier commit.

Signed-off-by: Monty Lennie <montylennie@gmail.com>
fix(render): clamp invalid dashboard layout values
@mtoy-googly-moogly
Copy link
Copy Markdown
Collaborator

mtoy-googly-moogly commented May 28, 2026

In collaboration with Claude -- I have read or written all of this ... a little more verbose than I would reply myself but I am letting Claude decide how much to write:

First off — this is looking great. The setup-time resolution, the defensive fallbacks, and the error-message wording are all exactly what I was hoping for, and the responsiveness to the earlier rounds shows. Thank you.

A couple of the things I flagged earlier I communicated badly, and you (reasonably) did extra work to satisfy contradictory asks. Let me clear those up. And there's one new thing that recent renderer work has me thinking about that I'd like to get ahead of in this PR.

  1. Ownership vs. setup-time resolution — my fault, two asks that pulled opposite ways

Back in March I said "resolve dashboard item metadata at setup time, stop reading tags at render time." Then in April I said "consume span/subtitle/borderless through the #2750 ownership mechanism rather than resolving globally up front." Those two notes point in opposite directions, and you ended up doing both: resolving the child tags at setup time in resolveBuiltInTags and declaring them in childOwnedPaths. That's on me for not reconciling them.

The setup-time resolution is the one I actually want — it's what docs/validation.md calls the default. Given that, the childOwnedPaths entries are now redundant: the setup-time reads already mark those tags as read, so the unread-tag detector never fires on them. I confirmed this — emptying childOwnedPaths entirely leaves the "no warnings for # span / # subtitle / # borderless / # break" tests green.

So: keep the setup-time resolution, and drop the span/subtitle/borderless/break entries from renderer-validation-specs.ts (the dashboard spec can go back to empty). One mechanism, not two.

Smaller related thought, not a blocker: the resolved values currently live as four new fields on FieldBase (_resolvedSpan, _resolvedBorderless, …). Those are dashboard-only concepts sitting on the universal field base. If it's cheap, I'd lean toward stashing them in a per-child config object rather than growing the base class — but I don't want to over-rotate on this if the base-class route is genuinely simpler, so your call.

  1. The big-value CSS coupling — also imprecise on my part

When I said "move the dashboard overrides into dashboard-owned styling," that got actioned as relocating the .dashboard-item… selectors out of big-value.css and into dashboard.css — which is good, but dashboard.css now reaches the other way into big-value's internals (.malloy-big-value-card, -value-row, -comparison, …). The leak moved sides rather than closing.

What I was actually after is reducing how much either component knows about the other's internal class names, not just which file the selectors live in. The clean shape is big-value exposing an "embedded/flattened" mode that dashboard opts into, instead of dashboard overriding big-value's guts. That's more than a CSS shuffle, so I don't want to block the PR on it — but let's name it as known debt so it doesn't quietly become a regression source when the two evolve apart.

  1. New issue: grid/flex is an inferred mode you can't cleanly override

This is the one from recent experience. I was copying and re-theming dashboards by extending views, and hit a wall that this PR should get ahead of.

Right now the grid-vs-flex decision is inferred: grid if columns set, OR gap set, OR any item has span, OR any item has break. That makes the mode hard to override when you copy a dashboard:

flex → grid is fine — add # dashboard { columns = 3 } to the copy and you're in grid.
grid → flex only works if grid came from columns/gap (negate with # dashboard { -columns }). If grid was triggered by per-item span/break, you can't turn it off from an extension — those triggers live on inherited child fields, and a refinement can't reach back to negate them. I confirmed both: # -span in a refinement only affects fields you redeclare there, and negating columns while a child still has a span leaves you in grid.

I think the real fix isn't a new mode tag — it's that span and break shouldn't be mode triggers at all:

break is meaningful in flex. The row-grouping already runs in both modes (nonDimensionsGrouped splits on break regardless of grid/flex), so in flex a break just forces a new wrapping row. It doesn't need grid and shouldn't switch you into it.
span is meaningless outside grid. It's "N of 12 columns" — flex has no column track, and nothing in the flex CSS path reads it. So span should be a grid-scoped hint: ignored with a warning when there's no grid, not a silent mode switch.

So my proposal: narrow the grid signal to columns/gap only. Make span a grid-scoped sizing hint (warn if used without a grid). Let break work in both modes. The payoff is that the mode is then controlled entirely at the view level, which is cleanly negatable — so grid↔flex round-trips on a copy with a single annotation, and stray spans become harmless no-ops in flex instead of forcing a layout you can't escape.

(Happy to share the little probe I ran if it'd help — it walks the resolved tags for each override case.)

Minor / logistics

The new validator tests are good and cover the right branches. One gap: they assert that an error/warning fires, not what it says. Since I asked for the Invalid on '': expected … got … Fix: … shape, could you add one assertion that pins Fix: (or the "expected … got") so a regression back to a terse message gets caught? And a gap = 0 "no error" assertion would close the last row of that verification matrix.
The branch is a good way behind main and won't build on the current TypeScript toolchain (moduleResolution=node10 is now a hard error). It merges cleanly, so a main merge/rebase should sort it. Looks like #2746 is properly separated out now — thanks for that.

jswir and others added 7 commits June 2, 2026 11:58
Previously the dashboard inferred grid mode from columns, gap, or any child # span / # break. That made the mode impossible to negate from an extension, since a refinement cannot reach back to drop an inherited child span or break. Grid mode is now driven only by the view-level columns/gap settings, so grid/flex round-trips cleanly on a copied dashboard.

# span is now a grid-scoped sizing hint: the validator warns when it is used without a grid (mirroring the columns/gap clamping in resolveDashboardTags) instead of silently switching layout. # break works in both modes; it already groups rows via nonDimensionsGrouped, so in flex it just forces a new wrapping row.

Also moves the four dashboard-only resolved values (span, subtitle, break, borderless) off the universal FieldBase into a single per-child config object, and drops the now-redundant dashboard childOwnedPaths: resolveBuiltInTags reads those child tags at setup time, which marks them read so the unread-tag detector never fires.

Signed-off-by: Monty Lennie <montylennie@gmail.com>
…ng internals

Dashboard CSS reached into big-value internal classes (.malloy-big-value-card, -label-container, -value-row, -comparison) to flatten the card. Big-value now exposes an embedded mode that a host opts into via customProps, and owns the flattened styling under .malloy-big-value--embedded. The dashboard no longer overrides big-value internals.

Signed-off-by: Monty Lennie <montylennie@gmail.com>
…sage

Adds tests that # span warns on a grid-less dashboard and does not warn when a grid is present via gap, asserts the span range error names the expected range, the bad value, and the Fix, and adds a gap=0 no-error case. Updates the existing no-warnings span test to use a grid (gap) so the new grid-scoped span warning does not fire.

Signed-off-by: Monty Lennie <montylennie@gmail.com>
The grid stories relied on # span / # break to trigger grid, which no longer flips the mode. Adds gap to those stories so they stay grids, and adds a flex story demonstrating # break forcing a new row without a grid.

Signed-off-by: Monty Lennie <montylennie@gmail.com>
…an warning

The columns-mode span warning gated on the raw columns value, so an invalid columns (e.g. columns=0 or a non-integer) plus # span emitted "span is ignored ... uses columns mode" even though resolveDashboardTags clamps invalid columns to undefined and the dashboard falls back to flex. Gate the columns-mode branch on a valid columns value (the same predicate hasGrid uses) so the invalid case falls through to the accurate no-grid message. Adds a test for columns=0 + span.

Signed-off-by: Monty Lennie <montylennie@gmail.com>
Reword the tag-configs child-tag comment to be precise (absent tags are not "marked read"; the unread detector only walks present properties). Fix the FLEX stories section header, which still claimed no break tags after dashboard_break_flex was added. Replace a leftover long dash in a story comment.

Signed-off-by: Monty Lennie <montylennie@gmail.com>
  feat(render): Dashboard UX round 2 (view-level grid mode, big-value decoupling, FieldBase cleanup)
@mlennie
Copy link
Copy Markdown

mlennie commented Jun 2, 2026

@mtoy-googly-moogly round 2 is merged into feat/dashboard-ux (via jswir#2), so it's in this PR now. Walking your points:

Ownership vs. setup-time resolution

Kept the setup-time resolution and dropped the span/subtitle/break/borderless entries from the dashboard spec in renderer-validation-specs.ts, so childOwnedPaths is back to []. The "no warnings for # span / # subtitle / # borderless / # break" tests stay green. One mechanism now.

I also took the per-child config route you suggested: the four resolved values no longer live on FieldBase. resolveBuiltInTags builds a single DashboardChildConfig per child and stashes it via setDashboardChildConfig, and the dashboard reads it back with getDashboardChildConfig<DashboardChildConfig>(). FieldBase keeps only _resolvedLabel.

Big-value coupling

Closed it rather than just flagging it as debt. big-value now has an embedded/flattened mode (.malloy-big-value--embedded in big-value.css), and the dashboard opts in via customProps ({ big_value: { embedded: true } }) instead of overriding big-value's internals. dashboard.css no longer references .malloy-big-value-card / -value-row / -comparison; the only big-value mentions left are :has(.malloy-big-value) presence-conditions on the dashboard's own elements. Standalone big-value is unchanged (embedded defaults off).

Grid/flex mode

Narrowed the grid signal to columns/gap only: useGrid is now just columns !== undefined || gap !== undefined. span and break are no longer mode triggers. break works in both modes (the row grouping was already mode-agnostic). span is a grid-scoped hint that warns when there's no grid instead of silently switching into one. Net effect is the one you described: the mode is view-level and cleanly negatable, so a copied dashboard round-trips grid/flex with a single annotation and a stray span is a harmless warned no-op. Thanks for the probe offer; I worked through the override cases (negating columns with a child span still present, # -span in a refinement) and believe they're covered.

Validator tests

Pinned the message text: the span-range test asserts the expected ... got ... Fix: # span=6 shape, so a regression to a terse message fails. Added the gap=0 no-error case, plus span-without-grid-warns and span-with-grid-stays-clean.

Build / main

Resolved. feat/dashboard-ux is synced with main now, so the branch builds on the current toolchain and the round-2 work is rebased cleanly on top.

One thing I noticed and left alone: a span out of range under columns mode (e.g. # span=15 with columns=3) emits both the range error and the "ignored in columns mode" warning. It's pre-existing, both messages are true, and suppressing one turns it into a two-step fix, so I left it. Easy to tidy if you'd rather see only one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants