@agent-native/core 0.76.8 → 0.76.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +48 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/scripts/generate-og-font-data.mjs +9 -5
  5. package/corpus/core/src/agent/run-store.ts +21 -0
  6. package/corpus/core/src/client/AgentPanel.tsx +70 -1
  7. package/corpus/core/src/client/AssistantChat.tsx +1 -3
  8. package/corpus/core/src/client/MultiTabAssistantChat.tsx +0 -62
  9. package/corpus/core/src/client/composer/TiptapComposer.tsx +6 -13
  10. package/corpus/core/src/server/agent-chat-plugin.ts +26 -0
  11. package/corpus/core/src/server/og-fonts-data.ts +7 -4
  12. package/corpus/core/src/server/og-fonts.ts +10 -3
  13. package/corpus/core/src/server/social-og-image.ts +45 -6
  14. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +199 -19
  15. package/corpus/templates/analytics/app/pages/adhoc/AdhocRouter.tsx +1 -21
  16. package/corpus/templates/analytics/app/pages/adhoc/DashboardSkeleton.tsx +35 -0
  17. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +3 -23
  18. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +3 -26
  19. package/corpus/templates/analytics/changelog/2026-06-24-daily-dashboard-charts-now-use-local-day-buckets-and-show-th.md +6 -0
  20. package/corpus/templates/analytics/changelog/2026-06-24-dashboard-loading-placeholders-now-span-the-available-page-w.md +6 -0
  21. package/corpus/templates/analytics/changelog/2026-06-24-first-party-dashboard-dates-now-align-to-pacific-time-for-da.md +6 -0
  22. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +19 -7
  23. package/dist/agent/run-store.d.ts +11 -0
  24. package/dist/agent/run-store.d.ts.map +1 -1
  25. package/dist/agent/run-store.js +20 -0
  26. package/dist/agent/run-store.js.map +1 -1
  27. package/dist/client/AgentPanel.d.ts +5 -1
  28. package/dist/client/AgentPanel.d.ts.map +1 -1
  29. package/dist/client/AgentPanel.js +23 -4
  30. package/dist/client/AgentPanel.js.map +1 -1
  31. package/dist/client/AssistantChat.d.ts +1 -3
  32. package/dist/client/AssistantChat.d.ts.map +1 -1
  33. package/dist/client/AssistantChat.js.map +1 -1
  34. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  35. package/dist/client/MultiTabAssistantChat.js +1 -17
  36. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  37. package/dist/client/composer/TiptapComposer.d.ts +4 -0
  38. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  39. package/dist/client/composer/TiptapComposer.js +5 -9
  40. package/dist/client/composer/TiptapComposer.js.map +1 -1
  41. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  42. package/dist/server/agent-chat-plugin.js +26 -0
  43. package/dist/server/agent-chat-plugin.js.map +1 -1
  44. package/dist/server/og-fonts-data.d.ts +1 -0
  45. package/dist/server/og-fonts-data.d.ts.map +1 -1
  46. package/dist/server/og-fonts-data.js +5 -4
  47. package/dist/server/og-fonts-data.js.map +1 -1
  48. package/dist/server/og-fonts.d.ts +1 -0
  49. package/dist/server/og-fonts.d.ts.map +1 -1
  50. package/dist/server/og-fonts.js +10 -4
  51. package/dist/server/og-fonts.js.map +1 -1
  52. package/dist/server/social-og-image.d.ts.map +1 -1
  53. package/dist/server/social-og-image.js +33 -6
  54. package/dist/server/social-og-image.js.map +1 -1
  55. package/package.json +1 -1
@@ -5,14 +5,16 @@ import path from "node:path";
5
5
  import {
6
6
  LIBERATION_SANS_BOLD_BASE64,
7
7
  LIBERATION_SANS_REGULAR_BASE64,
8
+ NOTO_NASKH_ARABIC_BASE64,
8
9
  } from "./og-fonts-data.js";
9
10
 
10
11
  /**
11
12
  * Liberation Sans is the metric-compatible libre replacement for
12
- * Arial/Helvetica that the OG image SVG asks for. It ships embedded as base64
13
+ * Arial/Helvetica that the OG image SVG asks for. Noto Naskh Arabic fills the
14
+ * Arabic script gap for localized docs titles. They ship embedded as base64
13
15
  * (see {@link ./og-fonts-data.ts}) so the renderer never depends on the host's
14
- * system fonts — Linux serverless runtimes (Netlify/Lambda) have neither Arial
15
- * nor Inter, which previously left every `<text>` element rendering nothing.
16
+ * system fonts — Linux serverless runtimes (Netlify/Lambda) have neither Arial,
17
+ * Inter, nor Arabic fallback fonts.
16
18
  */
17
19
  const OG_FONT_FILES = [
18
20
  {
@@ -20,9 +22,14 @@ const OG_FONT_FILES = [
20
22
  base64: LIBERATION_SANS_REGULAR_BASE64,
21
23
  },
22
24
  { filename: "LiberationSans-Bold.ttf", base64: LIBERATION_SANS_BOLD_BASE64 },
25
+ {
26
+ filename: "NotoNaskhArabic-Variable.ttf",
27
+ base64: NOTO_NASKH_ARABIC_BASE64,
28
+ },
23
29
  ] as const;
24
30
 
25
31
  export const OG_FONT_FAMILY = "Liberation Sans";
32
+ export const OG_ARABIC_FONT_FAMILY = "Noto Naskh Arabic";
26
33
 
27
34
  let cachedFontFiles: string[] | null | undefined;
28
35
 
@@ -11,7 +11,11 @@ import {
11
11
  resolveBuiltInAuthMarketingByName,
12
12
  } from "./auth-marketing.js";
13
13
  import { getAppName } from "./app-name.js";
14
- import { OG_FONT_FAMILY, resolveOgFontFiles } from "./og-fonts.js";
14
+ import {
15
+ OG_ARABIC_FONT_FAMILY,
16
+ OG_FONT_FAMILY,
17
+ resolveOgFontFiles,
18
+ } from "./og-fonts.js";
15
19
 
16
20
  export interface AgentNativeOgImageInput {
17
21
  appName?: string | null;
@@ -32,7 +36,8 @@ const BRAND_BLUE = "#00B5FF";
32
36
  const BRAND_MINT = "#48FFE4";
33
37
  const BG = "#000000";
34
38
  const FG = "#f5f5f5";
35
- const FONT_FAMILY = `${OG_FONT_FAMILY}, Arial, Helvetica, system-ui, sans-serif`;
39
+ const DEFAULT_FONT_FAMILY = `${OG_FONT_FAMILY}, Arial, Helvetica, system-ui, sans-serif`;
40
+ const ARABIC_FONT_FAMILY = `${OG_ARABIC_FONT_FAMILY}, ${OG_FONT_FAMILY}, Arial, Helvetica, system-ui, sans-serif`;
36
41
  const DEFAULT_ACCENT_TEXT = "100% free and open source";
37
42
 
38
43
  const LOGO_MARK = `
@@ -101,6 +106,16 @@ function estimateTextWidth(value: string, fontSize: number): number {
101
106
  return units * fontSize;
102
107
  }
103
108
 
109
+ function containsArabicText(value: string): boolean {
110
+ return /[\u0600-\u06ff\u0750-\u077f\u0870-\u089f\ufb50-\ufdff\ufe70-\ufeff]/u.test(
111
+ value,
112
+ );
113
+ }
114
+
115
+ function fontFamilyForText(value: string): string {
116
+ return containsArabicText(value) ? ARABIC_FONT_FAMILY : DEFAULT_FONT_FAMILY;
117
+ }
118
+
104
119
  function trimTextToWidth(
105
120
  value: string,
106
121
  fontSize: number,
@@ -205,6 +220,8 @@ function textBlock({
205
220
  weight,
206
221
  fill,
207
222
  anchor = "start",
223
+ direction,
224
+ fontFamily = DEFAULT_FONT_FAMILY,
208
225
  }: {
209
226
  lines: string[];
210
227
  x: number;
@@ -213,9 +230,14 @@ function textBlock({
213
230
  lineHeight: number;
214
231
  weight: number;
215
232
  fill: string;
216
- anchor?: "start" | "middle";
233
+ anchor?: "start" | "middle" | "end";
234
+ direction?: "ltr" | "rtl";
235
+ fontFamily?: string;
217
236
  }): string {
218
- return `<text x="${x}" y="${y}" text-anchor="${anchor}" font-family="${FONT_FAMILY}" font-size="${fontSize}" font-weight="${weight}" fill="${fill}">${lines
237
+ const directionAttrs = direction
238
+ ? ` direction="${direction}" unicode-bidi="plaintext"`
239
+ : "";
240
+ return `<text x="${x}" y="${y}" text-anchor="${anchor}"${directionAttrs} font-family="${fontFamily}" font-size="${fontSize}" font-weight="${weight}" fill="${fill}">${lines
219
241
  .map(
220
242
  (line, index) =>
221
243
  `<tspan x="${x}" dy="${index === 0 ? 0 : lineHeight}">${escapeSvg(line)}</tspan>`,
@@ -289,6 +311,10 @@ export function renderAgentNativeOgImageSvg(
289
311
  const title = cleanText(input.title) || titleFromAppName(appName);
290
312
  const accentText = cleanText(input.accentText) || DEFAULT_ACCENT_TEXT;
291
313
  const titleLayout = getTitleLayout(title);
314
+ const titleIsRtl = containsArabicText(title);
315
+ const textX = titleIsRtl ? WIDTH - 80 : 80;
316
+ const accentX = titleIsRtl ? WIDTH - 84 : 84;
317
+ const textAnchor = titleIsRtl ? "end" : "start";
292
318
  const titleY = titleLayout.lines.length > 1 ? 288 : 330;
293
319
  const accentY =
294
320
  titleY + titleLayout.lineHeight * (titleLayout.lines.length - 1) + 70;
@@ -312,7 +338,7 @@ export function renderAgentNativeOgImageSvg(
312
338
  <g>
313
339
  ${textBlock({
314
340
  lines: titleLayout.lines,
315
- x: 80,
341
+ x: textX,
316
342
  y: titleY,
317
343
  fontSize: titleLayout.fontSize,
318
344
  lineHeight: titleLayout.lineHeight,
@@ -321,8 +347,21 @@ export function renderAgentNativeOgImageSvg(
321
347
  // bundle, which is the intended look for the display title.
322
348
  weight: 800,
323
349
  fill: FG,
350
+ anchor: textAnchor,
351
+ direction: titleIsRtl ? "rtl" : undefined,
352
+ fontFamily: fontFamilyForText(title),
353
+ })}
354
+ ${textBlock({
355
+ lines: [accentText],
356
+ x: accentX,
357
+ y: accentY,
358
+ fontSize: 34,
359
+ lineHeight: 40,
360
+ weight: 800,
361
+ fill: BRAND_BLUE,
362
+ anchor: textAnchor,
363
+ fontFamily: fontFamilyForText(accentText),
324
364
  })}
325
- <text x="84" y="${accentY}" font-family="${FONT_FAMILY}" font-size="34" font-weight="800" fill="${BRAND_BLUE}">${escapeSvg(accentText)}</text>
326
365
  </g>
327
366
  </svg>`;
328
367
  }
@@ -97,6 +97,10 @@ const CHART_LEGEND_PROPS = {
97
97
  wrapperStyle: CHART_LEGEND_WRAPPER_STYLE,
98
98
  } as const;
99
99
 
100
+ const PARTIAL_DAY_TIME_ZONE = "America/Los_Angeles";
101
+ const PARTIAL_DAY_DASH = "3 5";
102
+ const PARTIAL_DAY_KEY_PREFIX = "__sql_chart_partial_day";
103
+
100
104
  function formatYValue(
101
105
  value: number,
102
106
  formatter?: "number" | "currency" | "percent",
@@ -167,6 +171,122 @@ function truncateLabel(value: string, max = 48): string {
167
171
  return `${value.slice(0, max - 3)}...`;
168
172
  }
169
173
 
174
+ function parseCalendarDate(value: string): Date | null {
175
+ const normalized = /^\d{8}$/.test(value)
176
+ ? `${value.slice(0, 4)}-${value.slice(4, 6)}-${value.slice(6, 8)}`
177
+ : value;
178
+ const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(normalized);
179
+ if (dateOnly) {
180
+ return new Date(
181
+ Number(dateOnly[1]),
182
+ Number(dateOnly[2]) - 1,
183
+ Number(dateOnly[3]),
184
+ );
185
+ }
186
+ const parsed = new Date(normalized);
187
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
188
+ }
189
+
190
+ export function toSqlChartDateKey(value: unknown): string | null {
191
+ if (typeof value === "string") {
192
+ const trimmed = value.trim();
193
+ const ymd = /^(\d{4})-(\d{2})-(\d{2})$/.exec(trimmed);
194
+ if (ymd) return `${ymd[1]}-${ymd[2]}-${ymd[3]}`;
195
+ const compact = /^(\d{4})(\d{2})(\d{2})$/.exec(trimmed);
196
+ if (compact) return `${compact[1]}-${compact[2]}-${compact[3]}`;
197
+ }
198
+
199
+ const parsed = new Date(String(value ?? ""));
200
+ return Number.isNaN(parsed.getTime()) ? null : sqlChartLocalDateKey(parsed);
201
+ }
202
+
203
+ export function sqlChartLocalDateKey(
204
+ date = new Date(),
205
+ timeZone = PARTIAL_DAY_TIME_ZONE,
206
+ ): string {
207
+ try {
208
+ const parts = new Intl.DateTimeFormat("en-US", {
209
+ timeZone,
210
+ year: "numeric",
211
+ month: "2-digit",
212
+ day: "2-digit",
213
+ }).formatToParts(date);
214
+ const get = (type: string) =>
215
+ parts.find((part) => part.type === type)?.value ?? "";
216
+ const year = get("year");
217
+ const month = get("month");
218
+ const day = get("day");
219
+ if (year && month && day) return `${year}-${month}-${day}`;
220
+ } catch {}
221
+ return date.toISOString().slice(0, 10);
222
+ }
223
+
224
+ export interface SplitTimeSeries {
225
+ key: string;
226
+ solidKey: string;
227
+ partialKey: string | null;
228
+ }
229
+
230
+ export function splitCurrentDayTimeSeriesRows(
231
+ rows: Record<string, unknown>[],
232
+ xKey: string,
233
+ yKeys: string[],
234
+ todayKey = sqlChartLocalDateKey(),
235
+ ): { rows: Record<string, unknown>[]; series: SplitTimeSeries[] } {
236
+ const todayIndexes = rows
237
+ .map((row, index) => ({ index, dateKey: toSqlChartDateKey(row[xKey]) }))
238
+ .filter((entry) => entry.dateKey === todayKey)
239
+ .map((entry) => entry.index);
240
+ const firstTodayIndex = todayIndexes[0] ?? -1;
241
+
242
+ if (firstTodayIndex <= 0 || yKeys.length === 0) {
243
+ return {
244
+ rows,
245
+ series: yKeys.map((key) => ({ key, solidKey: key, partialKey: null })),
246
+ };
247
+ }
248
+
249
+ const todaySet = new Set(todayIndexes);
250
+ const previousIndex = firstTodayIndex - 1;
251
+ const series = yKeys.map((key, index) => ({
252
+ key,
253
+ solidKey: `${PARTIAL_DAY_KEY_PREFIX}_${index}_solid`,
254
+ partialKey: `${PARTIAL_DAY_KEY_PREFIX}_${index}_partial`,
255
+ }));
256
+ const splitRows = rows.map((row, index) => {
257
+ const next = { ...row };
258
+ const isToday = todaySet.has(index);
259
+ const isPartialSegmentStart = index === previousIndex;
260
+ for (const item of series) {
261
+ next[item.solidKey] = isToday ? null : row[item.key];
262
+ next[item.partialKey] =
263
+ isToday || isPartialSegmentStart ? row[item.key] : null;
264
+ }
265
+ return next;
266
+ });
267
+
268
+ return { rows: splitRows, series };
269
+ }
270
+
271
+ export function shouldSplitCurrentDayTimeSeries(
272
+ panel: Pick<SqlPanel, "source">,
273
+ xKey: string,
274
+ ): boolean {
275
+ if (panel.source === "prometheus") return false;
276
+
277
+ const normalizedKey = xKey.trim().toLowerCase();
278
+ if (normalizedKey === "timestamp" || normalizedKey.endsWith("_timestamp")) {
279
+ return false;
280
+ }
281
+
282
+ return (
283
+ normalizedKey === "date" ||
284
+ normalizedKey === "day" ||
285
+ normalizedKey.endsWith("_date") ||
286
+ normalizedKey.endsWith("_day")
287
+ );
288
+ }
289
+
170
290
  function formatSeriesLabel(value: string): string {
171
291
  const { metric, labels } = parsePrometheusSeriesLabel(value);
172
292
  const target =
@@ -232,11 +352,8 @@ function formatSeriesLabelForPanel(panel: SqlPanel, value: string): string {
232
352
  function formatXLabel(value: string, panel: SqlPanel): string {
233
353
  try {
234
354
  const s = String(value);
235
- const normalized = /^\d{8}$/.test(s)
236
- ? `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)}`
237
- : s;
238
- const d = new Date(normalized);
239
- if (!isNaN(d.getTime()) && s.length >= 8) {
355
+ const d = parseCalendarDate(s);
356
+ if (d && s.length >= 8) {
240
357
  return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
241
358
  }
242
359
  } catch {}
@@ -252,12 +369,21 @@ function numericTooltipValue(value: unknown): number {
252
369
  return Number.isFinite(numeric) ? numeric : Number.NEGATIVE_INFINITY;
253
370
  }
254
371
 
372
+ function tooltipItemName(item: {
373
+ dataKey?: string | number;
374
+ name?: string | number;
375
+ }): string {
376
+ return String(item.name ?? item.dataKey ?? "");
377
+ }
378
+
255
379
  export function sortTooltipPayloadItems<
256
380
  T extends {
381
+ dataKey?: string | number;
382
+ name?: string | number;
257
383
  value?: unknown;
258
384
  },
259
385
  >(items: T[]): T[] {
260
- return items
386
+ const sorted = items
261
387
  .map((item, index) => ({ item, index }))
262
388
  .sort((a, b) => {
263
389
  const diff =
@@ -265,6 +391,13 @@ export function sortTooltipPayloadItems<
265
391
  return diff !== 0 ? diff : a.index - b.index;
266
392
  })
267
393
  .map(({ item }) => item);
394
+ const seen = new Set<string>();
395
+ return sorted.filter((item) => {
396
+ const name = tooltipItemName(item);
397
+ if (seen.has(name)) return false;
398
+ seen.add(name);
399
+ return true;
400
+ });
268
401
  }
269
402
 
270
403
  function useSeriesVisibility(keys: string[]): {
@@ -1390,6 +1523,21 @@ function TimeSeriesRenderer({
1390
1523
  const seriesNameFormatter = (name: string) =>
1391
1524
  formatSeriesLabelForPanel(panel, name);
1392
1525
  const { hiddenKeys, visibleKeys, toggleSeries } = useSeriesVisibility(yKeys);
1526
+ const splitPartialDay = shouldSplitCurrentDayTimeSeries(panel, xKey);
1527
+ const { rows: chartRows, series } = useMemo(
1528
+ () =>
1529
+ splitPartialDay
1530
+ ? splitCurrentDayTimeSeriesRows(rows, xKey, yKeys)
1531
+ : {
1532
+ rows,
1533
+ series: yKeys.map((key) => ({
1534
+ key,
1535
+ solidKey: key,
1536
+ partialKey: null,
1537
+ })),
1538
+ },
1539
+ [rows, xKey, yKeys, splitPartialDay],
1540
+ );
1393
1541
 
1394
1542
  if (chartType === "line") {
1395
1543
  return (
@@ -1402,7 +1550,7 @@ function TimeSeriesRenderer({
1402
1550
  showCustomLegend
1403
1551
  >
1404
1552
  <ResponsiveContainer width="100%" height="100%">
1405
- <LineChart data={rows}>
1553
+ <LineChart data={chartRows}>
1406
1554
  <XAxis
1407
1555
  dataKey={xKey}
1408
1556
  stroke="hsl(var(--muted-foreground))"
@@ -1435,18 +1583,33 @@ function TimeSeriesRenderer({
1435
1583
  }
1436
1584
  itemSorter={(item) => -(Number(item.value) || 0)}
1437
1585
  />
1438
- {yKeys.map((key, i) => (
1586
+ {series.map((item, i) => (
1439
1587
  <Line
1440
- key={key}
1588
+ key={item.solidKey}
1441
1589
  type="monotone"
1442
- dataKey={key}
1443
- name={seriesNameFormatter(key)}
1590
+ dataKey={item.solidKey}
1591
+ name={seriesNameFormatter(item.key)}
1444
1592
  stroke={colors[i % colors.length]}
1445
1593
  strokeWidth={2}
1446
1594
  dot={false}
1447
- hide={hiddenKeys.has(key)}
1595
+ hide={hiddenKeys.has(item.key)}
1448
1596
  />
1449
1597
  ))}
1598
+ {series.map((item, i) =>
1599
+ item.partialKey ? (
1600
+ <Line
1601
+ key={item.partialKey}
1602
+ type="monotone"
1603
+ dataKey={item.partialKey}
1604
+ name={seriesNameFormatter(item.key)}
1605
+ stroke={colors[i % colors.length]}
1606
+ strokeWidth={2}
1607
+ strokeDasharray={PARTIAL_DAY_DASH}
1608
+ dot={false}
1609
+ hide={hiddenKeys.has(item.key)}
1610
+ />
1611
+ ) : null,
1612
+ )}
1450
1613
  </LineChart>
1451
1614
  </ResponsiveContainer>
1452
1615
  </ChartFrame>
@@ -1468,7 +1631,7 @@ function TimeSeriesRenderer({
1468
1631
  showCustomLegend
1469
1632
  >
1470
1633
  <ResponsiveContainer width="100%" height="100%">
1471
- <AreaChart data={rows}>
1634
+ <AreaChart data={chartRows}>
1472
1635
  {showFill && (
1473
1636
  <defs>
1474
1637
  {yKeys.map((key, i) => (
@@ -1526,20 +1689,37 @@ function TimeSeriesRenderer({
1526
1689
  }
1527
1690
  itemSorter={(item) => -(Number(item.value) || 0)}
1528
1691
  />
1529
- {yKeys.map((key, i) => (
1692
+ {series.map((item, i) => (
1530
1693
  <Area
1531
- key={key}
1694
+ key={item.solidKey}
1532
1695
  type="monotone"
1533
- dataKey={key}
1534
- name={seriesNameFormatter(key)}
1696
+ dataKey={item.solidKey}
1697
+ name={seriesNameFormatter(item.key)}
1535
1698
  stroke={colors[i % colors.length]}
1536
1699
  strokeWidth={2}
1537
1700
  fillOpacity={showFill ? 1 : 0}
1538
- fill={showFill ? `url(#sql-gradient-${key})` : "none"}
1701
+ fill={showFill ? `url(#sql-gradient-${item.key})` : "none"}
1539
1702
  stackId={stacked ? "stack" : undefined}
1540
- hide={hiddenKeys.has(key)}
1703
+ hide={hiddenKeys.has(item.key)}
1541
1704
  />
1542
1705
  ))}
1706
+ {series.map((item, i) =>
1707
+ item.partialKey ? (
1708
+ <Area
1709
+ key={item.partialKey}
1710
+ type="monotone"
1711
+ dataKey={item.partialKey}
1712
+ name={seriesNameFormatter(item.key)}
1713
+ stroke={colors[i % colors.length]}
1714
+ strokeWidth={2}
1715
+ strokeDasharray={PARTIAL_DAY_DASH}
1716
+ fill="none"
1717
+ fillOpacity={0}
1718
+ stackId={stacked ? "partial-stack" : undefined}
1719
+ hide={hiddenKeys.has(item.key)}
1720
+ />
1721
+ ) : null,
1722
+ )}
1543
1723
  </AreaChart>
1544
1724
  </ResponsiveContainer>
1545
1725
  </ChartFrame>
@@ -1,9 +1,8 @@
1
1
  import { Suspense, lazy, useEffect } from "react";
2
2
  import { useParams } from "react-router";
3
- import { Card, CardContent, CardHeader } from "@/components/ui/card";
4
- import { Skeleton } from "@/components/ui/skeleton";
5
3
  import { dashboardComponents } from "./registry";
6
4
  import { incrementItemView } from "@/lib/item-popularity";
5
+ import { DashboardSkeleton } from "./DashboardSkeleton";
7
6
 
8
7
  const SqlDashboardPage = lazy(() => import("./sql-dashboard"));
9
8
 
@@ -11,25 +10,6 @@ const SqlDashboardPage = lazy(() => import("./sql-dashboard"));
11
10
  // Suspense → dashboard config load. Matches the real SqlChartCard shape (Card
12
11
  // chrome + title row + chart-body skeleton) so the user sees one continuous
13
12
  // skeleton state rather than four different ones morphing into each other.
14
- function DashboardSkeleton() {
15
- return (
16
- <div className="space-y-4">
17
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
18
- {[0, 1].map((i) => (
19
- <Card key={i} className="flex flex-col overflow-visible">
20
- <CardHeader className="pb-2 shrink-0">
21
- <Skeleton className="h-4 w-32" />
22
- </CardHeader>
23
- <CardContent className="flex flex-1 flex-col pt-0">
24
- <Skeleton className="w-full flex-1 min-h-[250px]" />
25
- </CardContent>
26
- </Card>
27
- ))}
28
- </div>
29
- </div>
30
- );
31
- }
32
-
33
13
  function SqlDashboardLoader() {
34
14
  return (
35
15
  <Suspense fallback={<DashboardSkeleton />}>
@@ -0,0 +1,35 @@
1
+ import type { CSSProperties } from "react";
2
+ import { Card, CardContent, CardHeader } from "@/components/ui/card";
3
+ import { Skeleton } from "@/components/ui/skeleton";
4
+
5
+ interface DashboardSkeletonProps {
6
+ columns?: number;
7
+ count?: number;
8
+ }
9
+
10
+ export function DashboardSkeleton({
11
+ columns = 2,
12
+ count = 2,
13
+ }: DashboardSkeletonProps) {
14
+ return (
15
+ <div className="dashboard-grid-container space-y-4">
16
+ <div
17
+ className="dashboard-grid"
18
+ style={{ "--dash-cols": columns } as CSSProperties}
19
+ >
20
+ {Array.from({ length: count }, (_, i) => (
21
+ <div key={i} className="dashboard-grid-cell h-full">
22
+ <Card className="flex h-full flex-col overflow-visible">
23
+ <CardHeader className="pb-2 shrink-0">
24
+ <Skeleton className="h-4 w-32" />
25
+ </CardHeader>
26
+ <CardContent className="flex flex-1 flex-col pt-0">
27
+ <Skeleton className="w-full flex-1 min-h-[250px]" />
28
+ </CardContent>
29
+ </Card>
30
+ </div>
31
+ ))}
32
+ </div>
33
+ </div>
34
+ );
35
+ }
@@ -3,7 +3,7 @@ import { useSearchParams, useNavigate } from "react-router";
3
3
  import { useQuery, useQueryClient } from "@tanstack/react-query";
4
4
  import { Button } from "@/components/ui/button";
5
5
  import { Input } from "@/components/ui/input";
6
- import { Card, CardContent, CardHeader } from "@/components/ui/card";
6
+ import { Card, CardContent } from "@/components/ui/card";
7
7
  import {
8
8
  Dialog,
9
9
  DialogContent,
@@ -21,7 +21,6 @@ import {
21
21
  AlertDialogHeader,
22
22
  AlertDialogTitle,
23
23
  } from "@/components/ui/alert-dialog";
24
- import { Skeleton } from "@/components/ui/skeleton";
25
24
  import {
26
25
  IconPlus,
27
26
  IconTrash,
@@ -82,6 +81,7 @@ import {
82
81
  TooltipContent,
83
82
  TooltipTrigger,
84
83
  } from "@/components/ui/tooltip";
84
+ import { DashboardSkeleton } from "../DashboardSkeleton";
85
85
 
86
86
  export interface DashboardChart {
87
87
  id: string;
@@ -485,27 +485,7 @@ export default function ExplorerDashboardPage() {
485
485
  }
486
486
 
487
487
  if (!loaded) {
488
- // Match the eventual DashboardChartCard layout (Card chrome + title row +
489
- // chart-body skeleton) so the transition from "dashboard config loading"
490
- // to "queries loading" doesn't morph skeleton shape — the title text just
491
- // fills in. Otherwise the bare h-64 rectangles jump into Card-chromed
492
- // panels and the page visibly shifts.
493
- return (
494
- <div className="space-y-4">
495
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
496
- {[0, 1].map((i) => (
497
- <Card key={i} className="flex flex-col overflow-visible">
498
- <CardHeader className="pb-2 shrink-0">
499
- <Skeleton className="h-4 w-32" />
500
- </CardHeader>
501
- <CardContent className="flex flex-1 flex-col pt-0">
502
- <Skeleton className="w-full flex-1 min-h-[250px]" />
503
- </CardContent>
504
- </Card>
505
- ))}
506
- </div>
507
- </div>
508
- );
488
+ return <DashboardSkeleton />;
509
489
  }
510
490
 
511
491
  if (!dashboard) return null;
@@ -12,8 +12,7 @@ import { toast } from "sonner";
12
12
  import { Button } from "@/components/ui/button";
13
13
  import { Input } from "@/components/ui/input";
14
14
  import { Textarea } from "@/components/ui/textarea";
15
- import { Card, CardContent, CardHeader } from "@/components/ui/card";
16
- import { Skeleton } from "@/components/ui/skeleton";
15
+ import { Card, CardContent } from "@/components/ui/card";
17
16
  import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
18
17
  import {
19
18
  AlertDialog,
@@ -90,6 +89,7 @@ import {
90
89
  sqlDashboardPrefetchKey,
91
90
  type PrefetchSnapshot,
92
91
  } from "@/lib/prefetch-keys";
92
+ import { DashboardSkeleton } from "../DashboardSkeleton";
93
93
  import {
94
94
  resourceCanEdit,
95
95
  resourceCanManage,
@@ -1119,30 +1119,7 @@ export default function SqlDashboardPage() {
1119
1119
  }
1120
1120
 
1121
1121
  if (!loaded) {
1122
- // Match the eventual SqlChartCard layout exactly (Card chrome + title row +
1123
- // chart-body skeleton) so the transition from "dashboard config loading" to
1124
- // "queries loading" doesn't morph the skeleton's shape — only the title
1125
- // text fills in. Otherwise the bare h-64 rectangles jump into Card-chromed
1126
- // panels and the page visibly shifts.
1127
- return (
1128
- <div className="dashboard-grid-container space-y-4">
1129
- <div
1130
- className="dashboard-grid"
1131
- style={{ "--dash-cols": 2 } as React.CSSProperties}
1132
- >
1133
- {[0, 1].map((i) => (
1134
- <Card key={i} className="flex flex-col overflow-visible">
1135
- <CardHeader className="pb-2 shrink-0">
1136
- <Skeleton className="h-4 w-32" />
1137
- </CardHeader>
1138
- <CardContent className="flex flex-1 flex-col pt-0">
1139
- <Skeleton className="w-full flex-1 min-h-[250px]" />
1140
- </CardContent>
1141
- </Card>
1142
- ))}
1143
- </div>
1144
- </div>
1145
- );
1122
+ return <DashboardSkeleton />;
1146
1123
  }
1147
1124
 
1148
1125
  if (!dashboard) return <BlankDashboard />;
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-24
4
+ ---
5
+
6
+ Daily dashboard charts now show the in-progress day as a dashed segment.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-24
4
+ ---
5
+
6
+ Dashboard loading placeholders now span the available page width.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-06-24
4
+ ---
5
+
6
+ First-party dashboard dates now align to Pacific time for daily activity and retention panels.