Skip to content

Commit d19d251

Browse files
committed
feat(dashboards): Apple-inspired motion/materials polish, fix chart sizing/labels, add chart expand
- DashboardWorkspace/DashboardsHome: translucent chrome, press feedback, spring-flavored hover/entrance transitions, reduced-motion and reduced-transparency fallbacks, refined chat trace panel with a step timeline. - DashboardArtifact: fade artifact in on load; fix a feedback loop where a CSS height transition plus a stale interval poll made dashboard height grow continuously on any reflow (e.g. page scroll). - dashboardChartLib: cap chart height regardless of container width (was inflating on wide cards); add a built-in per-chart expand-to-fullscreen control; darken/enlarge and truncate x-axis labels so long names no longer render illegible or overlap their neighbor. - dashboard-design SKILL.md: document the automatic expand control, add a named "avoid AI-slop patterns" section (gradients, emoji icons, decorative blobs, off-scale spacing/type, chart color discipline) wired into the existing self-review checklist; bump skill version to 2.1.0.
1 parent 4d0c523 commit d19d251

6 files changed

Lines changed: 547 additions & 133 deletions

File tree

agent/skills/dashboard-design/SKILL.md

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
name: dashboard-design
33
description: Design and code a self-contained HTML dashboard for DeepSQL — ground on the schema, verify SQL, then write a beautiful single-file dashboard that loads data via the deepsql.query bridge.
4-
version: 2.0.0
4+
version: 2.1.0
55
platforms: [linux, macos, windows]
66
metadata:
77
hermes:
@@ -23,16 +23,26 @@ await deepsql.query("SELECT ...") // -> { columns: string[], rows: any[]
2323
deepsql.ready(fn) // runs fn() once the bridge is live (use this to kick off loading)
2424

2525
// Charts — ALWAYS use these instead of hand-writing SVG. Built-in hover tooltips
26-
// (show the value on mouse-over), number formatting, sparse axis labels, and a
27-
// graceful "No data" empty state. Pass the deepsql.query result straight in, or
28-
// [{label,value}] / [[label,value]]. First column = label, second = value (or
29-
// opts.labelKey/valueKey). opts: { valueFormat(fn), height, color, emptyText }.
26+
// (show the value on mouse-over), number formatting, sparse axis labels, a
27+
// graceful "No data" empty state, and a corner expand button that opens the
28+
// same chart larger in an overlay — all automatic, nothing to wire yourself.
29+
// Pass the deepsql.query result straight in, or [{label,value}] / [[label,value]].
30+
// First column = label, second = value (or opts.labelKey/valueKey).
31+
// opts: { valueFormat(fn), height, color, emptyText, title }.
3032
deepsql.charts.bar(elOrSelector, data, opts) // rankings, counts by day
3133
deepsql.charts.line(elOrSelector, data, opts) // trends over time (area+line)
3234
deepsql.charts.donut(elOrSelector, data, opts) // share/composition (with legend + %)
3335
deepsql.charts.format(n) // human number formatter
3436
```
3537

38+
**Chart sizing and the expand control are handled by the runtime — do not build your own.**
39+
Height is fixed regardless of container width (a chart in a wide card never balloons), and every
40+
chart already gets a corner "expand" button that opens a larger re-render in an overlay — this is
41+
exactly the kind of per-chart chrome that's tempting to hand-roll and easy to get inconsistent
42+
across widgets, so it's built into `deepsql.charts.*` once instead. Pass `opts.title` (the chart's
43+
plain-business-language heading) so the expanded overlay has something to show as its title — do
44+
not add your own zoom/expand/fullscreen button, modal, or lightbox; one already exists per chart.
45+
3646
Hard rules:
3747
- Inline everything — one `<style>`, one or more `<script>`. **No external URLs, CDNs, fonts, or images** (blocked by CSP) and **no `fetch()`/XHR/WebSocket** — data comes only from `deepsql.query`.
3848
- Never hardcode result data. Query live on load, and re-query when a control changes.
@@ -52,6 +62,9 @@ Hard rules:
5262
- No `undefined` / `null` / `NaN` can reach the screen — every injected value is guarded with a fallback. Pay special attention to KPI sub-labels and any computed % (e.g. a "top source share" caption).
5363
- Every explicit user ask from the intent checklist is present and wired (controls default correctly and re-query on change).
5464
- No table/column/SQL/connection-id text is visible anywhere.
65+
- No AI-slop pattern from the section below is present (gradient background/hero, emoji-as-icon,
66+
decorative blobs/glassmorphism, uniform shadows, off-scale spacing/type, more than one accented
67+
"hero" card, hand-rolled chart colors or expand/zoom controls).
5568
A dashboard that renders with a blank chart or an "undefined" label is a failed build — catch it here.
5669

5770
## NEVER expose internals (security + UX — non-negotiable)
@@ -80,6 +93,45 @@ Rules:
8093
- Numbers formatted for humans (`deepsql.charts.format(n)` / thousands separators; currency symbol from the business rule) — never raw.
8194
- **Never render `undefined`, `null`, or `NaN`.** Guard every value you inject into the DOM (`v == null ? '—' : v`); a KPI sub-line/label with no value must fall back to a dash or be omitted — not the literal text "undefined".
8295

96+
## Avoid AI-slop patterns (named, so you can catch yourself)
97+
98+
These are the specific tells that make a generated dashboard look generated instead of
99+
designed. Each one is easy to reach for by default — that's exactly why it needs to be named
100+
and ruled out explicitly, not left to taste.
101+
102+
- **No default purple/blue/pink gradient backgrounds.** A `linear-gradient(135deg, #667eea, #764ba2)`-style
103+
hero band, header, or card is the single most recognizable AI-generated-UI signature. `--ds-grad`
104+
exists for exactly one purpose — a subtle lift on the single most important KPI card — never a page
105+
background, never a header banner, never more than one card on the whole dashboard.
106+
- **No emoji as icons, bullets, or section markers.** Not in KPI labels, not in section headers, not
107+
as a substitute for a real icon. If a visual marker is needed, use a plain shape (a dot, a small
108+
colored square in a legend) — never 📊📈💰✨ etc.
109+
- **No oversized rounded "blob" shapes, decorative background circles, or glassmorphism for its own
110+
sake.** `backdrop-filter`/translucency is not part of this theme — don't add it. Every visual
111+
element must carry information (a card, a chart, a legend swatch); nothing is decoration.
112+
- **No uniform drop-shadow on every element.** `--ds-shadow` is for cards that sit on `--ds-bg`
113+
don't add extra shadows to buttons, badges, or text, and don't stack multiple shadow layers for
114+
"depth." Flat and quiet is correct here.
115+
- **No arbitrary one-off spacing or font sizes.** Pick from a small fixed scale and stay on it for
116+
the whole document:
117+
- Spacing: `4px 8px 12px 16px 24px 32px` — nothing between these, nothing larger without a real reason.
118+
- Type: 3 sizes total — a KPI number (~28–32px, bold), section/card headings (~14–15px, semibold),
119+
body/labels (~12–13px, regular). Don't introduce a fourth size for a one-off caption.
120+
- **No centered "hero" layout with everything stacked in one narrow column.** This is a working
121+
dashboard, not a landing page — use a real grid (`auto-fit`/`auto-fill` KPI row, multi-column chart
122+
layout) that uses the available width purposefully.
123+
- **Chart color discipline:** stick to the theme's own greyscale chart palette (already built into
124+
`deepsql.charts.*` — you don't choose chart colors). Don't override `opts.color` per chart to
125+
introduce your own arbitrary hues; the built-in palette is the whole point of using the shared
126+
chart runtime instead of hand-rolled SVG.
127+
- **One hero KPI, not a "hero row."** If more than one card gets the gradient/accent treatment,
128+
none of them read as important — that defeats the point. Pick the single number the business
129+
question is actually about and reserve the accent for it alone.
130+
131+
Before emitting, ask: **would this ship, unedited, from a design team that obsesses over every
132+
pixel — or does it look like the first thing a template generator produced?** If any of the
133+
patterns above are present, that's your answer.
134+
83135
## Interaction
84136

85137
- Wire controls to re-run only the affected queries and re-render — never reload the page. A date range picker defaults to what the user asked for (e.g. today) and drives every time-sensitive query.

src/components/DashboardArtifact.jsx

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,25 @@ const BRIDGE = `
3333
if (d.error) p.reject(new Error(d.error));
3434
else p.resolve({ columns: d.columns || [], rows: d.rows || [] });
3535
});
36+
// Report only the content's own height, never the iframe's current rendered
37+
// height — scrollHeight on a body with height:auto reflects content size and
38+
// can't be inflated by whatever height the parent last set, so this can't
39+
// feed back into itself. Debounced and deduped so parent-side layout thrash
40+
// (e.g. a page scroll) can't retrigger it with the same value.
41+
var lastReported=-1, reportTimer=null;
3642
function reportHeight(){
37-
var h = Math.max(document.body ? document.body.scrollHeight : 0,
38-
document.documentElement ? document.documentElement.scrollHeight : 0);
39-
send({ __deepsql:true, type:'height', value: h });
43+
if (reportTimer) return;
44+
reportTimer = setTimeout(function(){
45+
reportTimer = null;
46+
var h = document.documentElement ? document.documentElement.scrollHeight : 0;
47+
if (h === lastReported) return;
48+
lastReported = h;
49+
send({ __deepsql:true, type:'height', value: h });
50+
}, 50);
4051
}
4152
window.addEventListener('load', function(){
4253
reportHeight();
4354
try { new ResizeObserver(reportHeight).observe(document.body); } catch(e){}
44-
setInterval(reportHeight, 1000);
4555
});
4656
window.addEventListener('error', function(e){
4757
send({ __deepsql:true, type:'jserror', message: (e && e.message) || 'script error' });
@@ -84,9 +94,13 @@ const MAX_TOTAL_QUERIES = 400
8494
const QUERY_TIMEOUT_MS = 25000
8595
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
8696

97+
const REDUCED_MOTION = typeof window !== 'undefined'
98+
&& window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
99+
87100
export default function DashboardArtifact({ connectionId, html, onError, queryFn }) {
88101
const iframeRef = useRef(null)
89102
const [height, setHeight] = useState(600)
103+
const [loaded, setLoaded] = useState(false)
90104
const queueRef = useRef([])
91105
const inflightRef = useRef(0)
92106
const totalRef = useRef(0)
@@ -162,11 +176,13 @@ export default function DashboardArtifact({ connectionId, html, onError, queryFn
162176
}, [onMessage])
163177

164178
// New artifact (generate/edit) reloads the iframe — reset the throttle so a
165-
// fresh dashboard isn't blocked by the prior one's runaway cap.
179+
// fresh dashboard isn't blocked by the prior one's runaway cap, and fade the
180+
// new one in rather than popping at whatever height it first reports.
166181
useEffect(() => {
167182
queueRef.current = []
168183
inflightRef.current = 0
169184
totalRef.current = 0
185+
setLoaded(false)
170186
}, [html])
171187

172188
return (
@@ -175,7 +191,19 @@ export default function DashboardArtifact({ connectionId, html, onError, queryFn
175191
title="Dashboard"
176192
sandbox="allow-scripts"
177193
srcDoc={buildSrcDoc(html || '', connectionId)}
178-
style={{ width: '100%', height, border: 'none', display: 'block', background: '#f8fafc' }}
194+
onLoad={() => setLoaded(true)}
195+
style={{
196+
width: '100%',
197+
height,
198+
border: 'none',
199+
display: 'block',
200+
background: '#f8fafc',
201+
opacity: loaded ? 1 : 0,
202+
// Height snaps immediately, never transitions — animating it risked measuring
203+
// the iframe's own in-transition rendered height as if it were new content,
204+
// a feedback loop that made the page grow on every scroll/resize tick.
205+
transition: REDUCED_MOTION ? 'opacity 150ms linear' : 'opacity 320ms cubic-bezier(0.2, 0.8, 0.2, 1)',
206+
}}
179207
/>
180208
)
181209
}

0 commit comments

Comments
 (0)