@bonnard/react 0.1.1

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 (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +161 -0
  3. package/dist/bonnard-chart.d.ts +21 -0
  4. package/dist/bonnard-chart.d.ts.map +1 -0
  5. package/dist/charts/area-chart.d.ts +3 -0
  6. package/dist/charts/area-chart.d.ts.map +1 -0
  7. package/dist/charts/bar-chart.d.ts +3 -0
  8. package/dist/charts/bar-chart.d.ts.map +1 -0
  9. package/dist/charts/big-value.d.ts +3 -0
  10. package/dist/charts/big-value.d.ts.map +1 -0
  11. package/dist/charts/data-table.d.ts +3 -0
  12. package/dist/charts/data-table.d.ts.map +1 -0
  13. package/dist/charts/index.d.ts +7 -0
  14. package/dist/charts/index.d.ts.map +1 -0
  15. package/dist/charts/line-chart.d.ts +3 -0
  16. package/dist/charts/line-chart.d.ts.map +1 -0
  17. package/dist/charts/pie-chart.d.ts +3 -0
  18. package/dist/charts/pie-chart.d.ts.map +1 -0
  19. package/dist/context.d.ts +10 -0
  20. package/dist/context.d.ts.map +1 -0
  21. package/dist/dashboard/dashboard-viewer.d.ts +11 -0
  22. package/dist/dashboard/dashboard-viewer.d.ts.map +1 -0
  23. package/dist/dashboard/dashboard.d.ts +8 -0
  24. package/dist/dashboard/dashboard.d.ts.map +1 -0
  25. package/dist/dashboard/index.d.ts +6 -0
  26. package/dist/dashboard/index.d.ts.map +1 -0
  27. package/dist/dashboard/inputs/date-range-input.d.ts +10 -0
  28. package/dist/dashboard/inputs/date-range-input.d.ts.map +1 -0
  29. package/dist/dashboard/inputs/dropdown-input.d.ts +11 -0
  30. package/dist/dashboard/inputs/dropdown-input.d.ts.map +1 -0
  31. package/dist/dashboard/parser.d.ts +9 -0
  32. package/dist/dashboard/parser.d.ts.map +1 -0
  33. package/dist/dashboard/query-load.d.ts +13 -0
  34. package/dist/dashboard/query-load.d.ts.map +1 -0
  35. package/dist/dashboard.d.ts +8 -0
  36. package/dist/dashboard.d.ts.map +1 -0
  37. package/dist/dashboard.js +935 -0
  38. package/dist/data-table-DQKxzbS3.js +985 -0
  39. package/dist/hooks/use-dashboard.d.ts +8 -0
  40. package/dist/hooks/use-dashboard.d.ts.map +1 -0
  41. package/dist/hooks/use-query.d.ts +13 -0
  42. package/dist/hooks/use-query.d.ts.map +1 -0
  43. package/dist/index.d.ts +18 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +142 -0
  46. package/dist/lib/apply-inputs.d.ts +19 -0
  47. package/dist/lib/apply-inputs.d.ts.map +1 -0
  48. package/dist/lib/build-series.d.ts +33 -0
  49. package/dist/lib/build-series.d.ts.map +1 -0
  50. package/dist/lib/date-presets.d.ts +11 -0
  51. package/dist/lib/date-presets.d.ts.map +1 -0
  52. package/dist/lib/echarts-series.d.ts +22 -0
  53. package/dist/lib/echarts-series.d.ts.map +1 -0
  54. package/dist/lib/format-value.d.ts +16 -0
  55. package/dist/lib/format-value.d.ts.map +1 -0
  56. package/dist/lib/types.d.ts +104 -0
  57. package/dist/lib/types.d.ts.map +1 -0
  58. package/dist/provider.d.ts +12 -0
  59. package/dist/provider.d.ts.map +1 -0
  60. package/dist/styles/bonnard.css +68 -0
  61. package/dist/theme/chart-theme.d.ts +76 -0
  62. package/dist/theme/chart-theme.d.ts.map +1 -0
  63. package/dist/theme/use-chart-theme.d.ts +39 -0
  64. package/dist/theme/use-chart-theme.d.ts.map +1 -0
  65. package/package.json +55 -0
@@ -0,0 +1,935 @@
1
+ import { a as BarChart, i as LineChart, n as PieChart, o as BigValue, r as AreaChart, t as DataTable, u as useBonnard } from "./data-table-DQKxzbS3.js";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+ import DOMPurify from "isomorphic-dompurify";
4
+ import matter from "gray-matter";
5
+ import { unified } from "unified";
6
+ import remarkParse from "remark-parse";
7
+ import remarkRehype from "remark-rehype";
8
+ import rehypeStringify from "rehype-stringify";
9
+ import { SKIP, visit } from "unist-util-visit";
10
+ import yaml from "yaml";
11
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
12
+
13
+ //#region src/hooks/use-dashboard.ts
14
+ function useDashboard(slug) {
15
+ const { client } = useBonnard();
16
+ const [dashboard, setDashboard] = useState(void 0);
17
+ const [loading, setLoading] = useState(true);
18
+ const [error, setError] = useState(void 0);
19
+ useEffect(() => {
20
+ let cancelled = false;
21
+ setLoading(true);
22
+ setError(void 0);
23
+ client.getDashboard(slug).then((result) => {
24
+ if (!cancelled) {
25
+ setDashboard(result.dashboard);
26
+ setLoading(false);
27
+ }
28
+ }, (err) => {
29
+ if (!cancelled) {
30
+ setError(err instanceof Error ? err.message : "Failed to load dashboard");
31
+ setLoading(false);
32
+ }
33
+ });
34
+ return () => {
35
+ cancelled = true;
36
+ };
37
+ }, [client, slug]);
38
+ return {
39
+ dashboard,
40
+ loading,
41
+ error
42
+ };
43
+ }
44
+
45
+ //#endregion
46
+ //#region src/dashboard/parser.ts
47
+ const VALID_QUERY_NAME = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
48
+ const VALID_COMPONENTS = new Set([
49
+ "BigValue",
50
+ "LineChart",
51
+ "BarChart",
52
+ "AreaChart",
53
+ "DataTable",
54
+ "PieChart",
55
+ "DateRange",
56
+ "Dropdown"
57
+ ]);
58
+ /** Input component types that get hoisted into the inputs bar */
59
+ const INPUT_COMPONENTS = new Set(["DateRange", "Dropdown"]);
60
+ /**
61
+ * Regex to extract individual props from an attributes string.
62
+ * Matches:
63
+ * data={name} → key="data", value from group 2 (curly)
64
+ * x="field" → key="x", value from group 3 (double-quoted)
65
+ * x='field' → key="x", value from group 4 (single-quoted)
66
+ * horizontal → key="horizontal", value undefined (boolean shorthand)
67
+ */
68
+ const PROP_RE = /(\w+)(?:=\{(\w+)\}|="([^"]*)"|='([^']*)')?/g;
69
+ /**
70
+ * Parse a dashboard markdown string into structured output.
71
+ *
72
+ * Collects all validation errors and reports them together rather than
73
+ * failing on the first error (pattern from Evidence.dev).
74
+ */
75
+ function parseDashboard(markdown) {
76
+ const errors = [];
77
+ const { data: rawFrontmatter, content: bodyWithoutFrontmatter } = matter(markdown);
78
+ const frontmatter = {
79
+ title: typeof rawFrontmatter.title === "string" ? rawFrontmatter.title : "Untitled Dashboard",
80
+ description: typeof rawFrontmatter.description === "string" ? rawFrontmatter.description : void 0
81
+ };
82
+ const queries = /* @__PURE__ */ new Map();
83
+ const tree = unified().use(remarkParse).parse(bodyWithoutFrontmatter);
84
+ const nodesToRemove = [];
85
+ visit(tree, "code", (node, index, parent) => {
86
+ if (node.lang !== "query") return;
87
+ const name = node.meta?.trim();
88
+ if (!name) {
89
+ errors.push("Query block must have a name: ```query my_query_name");
90
+ return;
91
+ }
92
+ if (!VALID_QUERY_NAME.test(name)) {
93
+ errors.push(`Invalid query name "${name}": must be a valid identifier (letters, numbers, _, $)`);
94
+ return;
95
+ }
96
+ if (queries.has(name)) {
97
+ errors.push(`Duplicate query name "${name}"`);
98
+ return;
99
+ }
100
+ try {
101
+ const options = yaml.parse(node.value);
102
+ if (!options || !options.cube) {
103
+ errors.push(`Query "${name}" must specify a cube`);
104
+ return;
105
+ }
106
+ queries.set(name, options);
107
+ } catch (e) {
108
+ errors.push(`Query "${name}" has invalid YAML: ${e instanceof Error ? e.message : String(e)}`);
109
+ return;
110
+ }
111
+ if (parent && typeof index === "number") nodesToRemove.push({
112
+ parent,
113
+ index
114
+ });
115
+ return SKIP;
116
+ });
117
+ for (let i = nodesToRemove.length - 1; i >= 0; i--) {
118
+ const { parent, index } = nodesToRemove[i];
119
+ parent.children.splice(index, 1);
120
+ }
121
+ if (errors.length > 0) throw new Error(`Dashboard parse errors:\n- ${errors.join("\n- ")}`);
122
+ const htmlResult = unified().use(remarkRehype, { allowDangerousHtml: true }).use(rehypeStringify, { allowDangerousHtml: true }).stringify(unified().use(remarkRehype, { allowDangerousHtml: true }).runSync(tree));
123
+ const { sections, inputs } = splitIntoSections(String(htmlResult));
124
+ return {
125
+ frontmatter,
126
+ queries,
127
+ inputs,
128
+ sections
129
+ };
130
+ }
131
+ /**
132
+ * Split HTML string into sections of HTML content, component tags, and grid layouts.
133
+ * Input components (DateRange, Dropdown) are hoisted into a separate inputs array.
134
+ */
135
+ function splitIntoSections(html) {
136
+ const sections = [];
137
+ const inputs = [];
138
+ let lastIndex = 0;
139
+ const COMBINED_RE = /<Grid\s+([\s\S]*?)>([\s\S]*?)<\/Grid>|<(BigValue|LineChart|BarChart|AreaChart|DataTable|PieChart|DateRange|Dropdown)\s+([\s\S]*?)\/>/g;
140
+ COMBINED_RE.lastIndex = 0;
141
+ let match;
142
+ while ((match = COMBINED_RE.exec(html)) !== null) {
143
+ const before = html.slice(lastIndex, match.index).trim();
144
+ if (before) sections.push({
145
+ kind: "html",
146
+ content: before
147
+ });
148
+ if (match[1] !== void 0) {
149
+ const props = parseProps(match[1]);
150
+ const inner = splitIntoSections(match[2]);
151
+ inputs.push(...inner.inputs);
152
+ sections.push({
153
+ kind: "grid",
154
+ props,
155
+ children: inner.sections
156
+ });
157
+ } else {
158
+ const component = parseComponentTag(match[3], match[4]);
159
+ if (component) if (INPUT_COMPONENTS.has(component.type)) inputs.push(component);
160
+ else sections.push({
161
+ kind: "component",
162
+ component
163
+ });
164
+ }
165
+ lastIndex = match.index + match[0].length;
166
+ }
167
+ const remaining = html.slice(lastIndex).trim();
168
+ if (remaining) sections.push({
169
+ kind: "html",
170
+ content: remaining
171
+ });
172
+ return {
173
+ sections,
174
+ inputs
175
+ };
176
+ }
177
+ /**
178
+ * Parse an attributes string into a props Record.
179
+ */
180
+ function parseProps(attrsString) {
181
+ const props = {};
182
+ PROP_RE.lastIndex = 0;
183
+ let propMatch;
184
+ while ((propMatch = PROP_RE.exec(attrsString)) !== null) {
185
+ const key = propMatch[1];
186
+ props[key] = propMatch[2] ?? propMatch[3] ?? propMatch[4] ?? "true";
187
+ }
188
+ return props;
189
+ }
190
+ /**
191
+ * Parse component attributes string into a ComponentTag.
192
+ */
193
+ function parseComponentTag(name, attrsString) {
194
+ if (!VALID_COMPONENTS.has(name)) return null;
195
+ return {
196
+ type: name,
197
+ props: parseProps(attrsString)
198
+ };
199
+ }
200
+
201
+ //#endregion
202
+ //#region src/lib/date-presets.ts
203
+ function fmt(d) {
204
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
205
+ }
206
+ function today() {
207
+ const d = /* @__PURE__ */ new Date();
208
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate());
209
+ }
210
+ const PRESETS = {
211
+ "last-7-days": {
212
+ label: "Last 7 Days",
213
+ getRange: () => {
214
+ const end = today();
215
+ const start = new Date(end);
216
+ start.setDate(start.getDate() - 6);
217
+ return [fmt(start), fmt(end)];
218
+ }
219
+ },
220
+ "last-30-days": {
221
+ label: "Last 30 Days",
222
+ getRange: () => {
223
+ const end = today();
224
+ const start = new Date(end);
225
+ start.setDate(start.getDate() - 29);
226
+ return [fmt(start), fmt(end)];
227
+ }
228
+ },
229
+ "last-3-months": {
230
+ label: "Last 3 Months",
231
+ getRange: () => {
232
+ const end = today();
233
+ const start = new Date(end);
234
+ start.setMonth(start.getMonth() - 3);
235
+ return [fmt(start), fmt(end)];
236
+ }
237
+ },
238
+ "last-6-months": {
239
+ label: "Last 6 Months",
240
+ getRange: () => {
241
+ const end = today();
242
+ const start = new Date(end);
243
+ start.setMonth(start.getMonth() - 6);
244
+ return [fmt(start), fmt(end)];
245
+ }
246
+ },
247
+ "last-12-months": {
248
+ label: "Last 12 Months",
249
+ getRange: () => {
250
+ const end = today();
251
+ const start = new Date(end);
252
+ start.setFullYear(start.getFullYear() - 1);
253
+ return [fmt(start), fmt(end)];
254
+ }
255
+ },
256
+ "month-to-date": {
257
+ label: "Month to Date",
258
+ getRange: () => {
259
+ const end = today();
260
+ return [fmt(new Date(end.getFullYear(), end.getMonth(), 1)), fmt(end)];
261
+ }
262
+ },
263
+ "year-to-date": {
264
+ label: "Year to Date",
265
+ getRange: () => {
266
+ const end = today();
267
+ return [fmt(new Date(end.getFullYear(), 0, 1)), fmt(end)];
268
+ }
269
+ },
270
+ "last-year": {
271
+ label: "Last Year",
272
+ getRange: () => {
273
+ const end = today();
274
+ const start = new Date(end.getFullYear() - 1, 0, 1);
275
+ const yearEnd = new Date(end.getFullYear() - 1, 11, 31);
276
+ return [fmt(start), fmt(yearEnd)];
277
+ }
278
+ },
279
+ "all-time": {
280
+ label: "All Time",
281
+ getRange: () => ["2000-01-01", fmt(today())]
282
+ }
283
+ };
284
+ /** Resolve a preset key to a concrete [start, end] date range */
285
+ function getPresetRange(preset) {
286
+ const def = PRESETS[preset];
287
+ if (!def) throw new Error(`Unknown date preset: ${preset}`);
288
+ return def.getRange();
289
+ }
290
+ /** Ordered array of preset options for UI rendering */
291
+ const PRESET_OPTIONS = [
292
+ {
293
+ key: "last-7-days",
294
+ label: "Last 7 Days"
295
+ },
296
+ {
297
+ key: "last-30-days",
298
+ label: "Last 30 Days"
299
+ },
300
+ {
301
+ key: "last-3-months",
302
+ label: "Last 3 Months"
303
+ },
304
+ {
305
+ key: "last-6-months",
306
+ label: "Last 6 Months"
307
+ },
308
+ {
309
+ key: "last-12-months",
310
+ label: "Last 12 Months"
311
+ },
312
+ {
313
+ key: "month-to-date",
314
+ label: "Month to Date"
315
+ },
316
+ {
317
+ key: "year-to-date",
318
+ label: "Year to Date"
319
+ },
320
+ {
321
+ key: "last-year",
322
+ label: "Last Year"
323
+ },
324
+ {
325
+ key: "all-time",
326
+ label: "All Time"
327
+ }
328
+ ];
329
+
330
+ //#endregion
331
+ //#region src/lib/apply-inputs.ts
332
+ /**
333
+ * Apply all active inputs to a single query's options.
334
+ *
335
+ * - DateRange: overrides timeDimension.dateRange on targeted queries
336
+ * - Dropdown: adds/replaces a filter on the specified dimension
337
+ */
338
+ function applyInputsToQuery(queryName, baseOptions, inputTags, inputsState) {
339
+ let options = structuredClone(baseOptions);
340
+ for (const input of inputTags) {
341
+ const { type, props } = input;
342
+ const inputName = props.name;
343
+ if (!inputName) continue;
344
+ const state = inputsState[inputName];
345
+ if (state === void 0) continue;
346
+ if (type === "DateRange") options = applyDateRange(queryName, options, props, state);
347
+ else if (type === "Dropdown") options = applyDropdown(queryName, options, props, state);
348
+ }
349
+ return options;
350
+ }
351
+ function applyDateRange(queryName, options, props, state) {
352
+ if (!options.timeDimension) return options;
353
+ if (props.queries) {
354
+ if (!props.queries.split(",").map((s) => s.trim()).includes(queryName)) return options;
355
+ }
356
+ if (!state) return options;
357
+ options.timeDimension = {
358
+ ...options.timeDimension,
359
+ dateRange: state.range
360
+ };
361
+ return options;
362
+ }
363
+ function applyDropdown(queryName, options, props, state) {
364
+ const dimension = props.dimension;
365
+ if (!dimension) return options;
366
+ if (!props.queries) return options;
367
+ const targets = props.queries.split(",").map((s) => s.trim());
368
+ const dataQuery = props.data;
369
+ if (dataQuery && dataQuery === queryName) return options;
370
+ if (!targets.includes(queryName)) return options;
371
+ if (state === null || state === "") {
372
+ if (options.filters) {
373
+ options.filters = options.filters.filter((f) => f.dimension !== dimension);
374
+ if (options.filters.length === 0) delete options.filters;
375
+ }
376
+ return options;
377
+ }
378
+ const newFilter = {
379
+ dimension,
380
+ operator: "equals",
381
+ values: [state]
382
+ };
383
+ if (!options.filters) options.filters = [newFilter];
384
+ else {
385
+ const idx = options.filters.findIndex((f) => f.dimension === dimension);
386
+ if (idx >= 0) options.filters[idx] = newFilter;
387
+ else options.filters.push(newFilter);
388
+ }
389
+ return options;
390
+ }
391
+ /**
392
+ * Build the initial inputsState from input tags (using their defaults).
393
+ */
394
+ function buildInitialInputsState(inputs) {
395
+ const state = {};
396
+ for (const input of inputs) {
397
+ const { type, props } = input;
398
+ const name = props.name;
399
+ if (!name) continue;
400
+ if (type === "DateRange") {
401
+ const preset = props.default || "last-6-months";
402
+ state[name] = {
403
+ preset,
404
+ range: getPresetRange(preset)
405
+ };
406
+ } else if (type === "Dropdown") state[name] = props.default || null;
407
+ }
408
+ return state;
409
+ }
410
+
411
+ //#endregion
412
+ //#region src/dashboard/query-load.tsx
413
+ /**
414
+ * QueryLoad — wraps chart components to handle loading / error / empty states.
415
+ */
416
+ function QueryLoad({ data, loading, error, children }) {
417
+ if (loading) return /* @__PURE__ */ jsx("div", {
418
+ style: {
419
+ display: "flex",
420
+ alignItems: "center",
421
+ justifyContent: "center",
422
+ height: 200,
423
+ fontSize: 14,
424
+ color: "var(--bon-text-muted)"
425
+ },
426
+ children: /* @__PURE__ */ jsxs("div", {
427
+ style: {
428
+ display: "flex",
429
+ alignItems: "center",
430
+ gap: 8
431
+ },
432
+ children: [/* @__PURE__ */ jsx("div", { style: {
433
+ width: 16,
434
+ height: 16,
435
+ borderRadius: "50%",
436
+ border: "2px solid var(--bon-spinner-color)",
437
+ borderTopColor: "transparent",
438
+ animation: "bonnard-spin 0.6s linear infinite"
439
+ } }), "Loading data…"]
440
+ })
441
+ });
442
+ if (error) return /* @__PURE__ */ jsxs("div", {
443
+ style: {
444
+ borderRadius: "var(--bon-radius)",
445
+ border: "1px solid var(--bon-border-error)",
446
+ backgroundColor: "var(--bon-bg-error)",
447
+ padding: 16,
448
+ fontSize: 14,
449
+ color: "var(--bon-text-error)"
450
+ },
451
+ children: ["Query error: ", error]
452
+ });
453
+ if (!data || data.length === 0) return /* @__PURE__ */ jsx("div", {
454
+ style: {
455
+ display: "flex",
456
+ alignItems: "center",
457
+ justifyContent: "center",
458
+ height: 200,
459
+ fontSize: 14,
460
+ color: "var(--bon-text-muted)"
461
+ },
462
+ children: "No data available"
463
+ });
464
+ return /* @__PURE__ */ jsx(Fragment, { children: children(data) });
465
+ }
466
+
467
+ //#endregion
468
+ //#region src/dashboard/inputs/date-range-input.tsx
469
+ function DateRangeInput({ name, label, value, onInputChange }) {
470
+ const handleChange = (e) => {
471
+ const preset = e.target.value;
472
+ onInputChange(name, {
473
+ preset,
474
+ range: getPresetRange(preset)
475
+ });
476
+ };
477
+ return /* @__PURE__ */ jsxs("div", {
478
+ style: {
479
+ display: "flex",
480
+ flexDirection: "column",
481
+ gap: 4
482
+ },
483
+ children: [label && /* @__PURE__ */ jsx("label", {
484
+ style: {
485
+ fontSize: 12,
486
+ fontWeight: 500,
487
+ color: "var(--bon-text-muted)"
488
+ },
489
+ children: label
490
+ }), /* @__PURE__ */ jsx("select", {
491
+ value: value.preset,
492
+ onChange: handleChange,
493
+ style: {
494
+ width: 180,
495
+ height: 32,
496
+ fontSize: 14,
497
+ padding: "0 8px",
498
+ borderRadius: "var(--bon-radius)",
499
+ border: "1px solid var(--bon-input-border)",
500
+ backgroundColor: "var(--bon-input-bg)",
501
+ color: "var(--bon-text)",
502
+ cursor: "pointer"
503
+ },
504
+ children: PRESET_OPTIONS.map((opt) => /* @__PURE__ */ jsx("option", {
505
+ value: opt.key,
506
+ children: opt.label
507
+ }, opt.key))
508
+ })]
509
+ });
510
+ }
511
+
512
+ //#endregion
513
+ //#region src/dashboard/inputs/dropdown-input.tsx
514
+ function DropdownInput({ name, dimension, label, data, value, onInputChange }) {
515
+ const options = Array.from(new Set(data.map((row) => String(row[dimension] ?? "")).filter(Boolean))).sort();
516
+ const handleChange = (e) => {
517
+ const val = e.target.value;
518
+ onInputChange(name, val === "" ? null : val);
519
+ };
520
+ return /* @__PURE__ */ jsxs("div", {
521
+ style: {
522
+ display: "flex",
523
+ flexDirection: "column",
524
+ gap: 4
525
+ },
526
+ children: [label && /* @__PURE__ */ jsx("label", {
527
+ style: {
528
+ fontSize: 12,
529
+ fontWeight: 500,
530
+ color: "var(--bon-text-muted)"
531
+ },
532
+ children: label
533
+ }), /* @__PURE__ */ jsxs("select", {
534
+ value: value ?? "",
535
+ onChange: handleChange,
536
+ style: {
537
+ width: 180,
538
+ height: 32,
539
+ fontSize: 14,
540
+ padding: "0 8px",
541
+ borderRadius: "var(--bon-radius)",
542
+ border: "1px solid var(--bon-input-border)",
543
+ backgroundColor: "var(--bon-input-bg)",
544
+ color: "var(--bon-text)",
545
+ cursor: "pointer"
546
+ },
547
+ children: [/* @__PURE__ */ jsx("option", {
548
+ value: "",
549
+ children: "All"
550
+ }), options.map((opt) => /* @__PURE__ */ jsx("option", {
551
+ value: opt,
552
+ children: opt
553
+ }, opt))]
554
+ })]
555
+ });
556
+ }
557
+
558
+ //#endregion
559
+ //#region src/dashboard/dashboard-viewer.tsx
560
+ const GRID_COL_STYLES = {
561
+ "1": { gridTemplateColumns: "1fr" },
562
+ "2": { gridTemplateColumns: "repeat(2, 1fr)" },
563
+ "3": { gridTemplateColumns: "repeat(3, 1fr)" },
564
+ "4": { gridTemplateColumns: "repeat(4, 1fr)" },
565
+ "5": { gridTemplateColumns: "repeat(5, 1fr)" },
566
+ "6": { gridTemplateColumns: "repeat(6, 1fr)" }
567
+ };
568
+ /**
569
+ * DashboardViewer — parses dashboard markdown and renders it.
570
+ */
571
+ function DashboardViewer({ content, onInputsChange }) {
572
+ const { client } = useBonnard();
573
+ const [parsed, setParsed] = useState(null);
574
+ const [parseError, setParseError] = useState(null);
575
+ const [queryStates, setQueryStates] = useState(/* @__PURE__ */ new Map());
576
+ const [inputsState, setInputsState] = useState({});
577
+ const isInitialRender = useRef(true);
578
+ useEffect(() => {
579
+ try {
580
+ const result = parseDashboard(content);
581
+ setParsed(result);
582
+ setParseError(null);
583
+ setInputsState(buildInitialInputsState(result.inputs));
584
+ isInitialRender.current = true;
585
+ } catch (err) {
586
+ setParseError(err instanceof Error ? err.message : "Failed to parse dashboard");
587
+ setParsed(null);
588
+ }
589
+ }, [content]);
590
+ const executeQueries = useCallback(async (queries, inputs, currentInputsState, queryNames) => {
591
+ const namesToExecute = queryNames ?? Array.from(queries.keys());
592
+ setQueryStates((prev) => {
593
+ const next = new Map(prev);
594
+ for (const name of namesToExecute) next.set(name, { loading: true });
595
+ return next;
596
+ });
597
+ await Promise.all(namesToExecute.map(async (name) => {
598
+ const baseOptions = queries.get(name);
599
+ if (!baseOptions) return;
600
+ const options = applyInputsToQuery(name, baseOptions, inputs, currentInputsState);
601
+ try {
602
+ const result = await client.query(options);
603
+ setQueryStates((prev) => {
604
+ const next = new Map(prev);
605
+ next.set(name, {
606
+ loading: false,
607
+ data: result.data,
608
+ limit: options.limit ?? null
609
+ });
610
+ return next;
611
+ });
612
+ } catch (err) {
613
+ setQueryStates((prev) => {
614
+ const next = new Map(prev);
615
+ next.set(name, {
616
+ loading: false,
617
+ error: err instanceof Error ? err.message : "Query failed"
618
+ });
619
+ return next;
620
+ });
621
+ }
622
+ }));
623
+ }, [client]);
624
+ useEffect(() => {
625
+ if (parsed && parsed.queries.size > 0 && isInitialRender.current) {
626
+ isInitialRender.current = false;
627
+ executeQueries(parsed.queries, parsed.inputs, inputsState);
628
+ }
629
+ }, [
630
+ parsed,
631
+ inputsState,
632
+ executeQueries
633
+ ]);
634
+ const handleInputChange = useCallback((inputName, value) => {
635
+ if (!parsed) return;
636
+ setInputsState((prev) => {
637
+ const next = {
638
+ ...prev,
639
+ [inputName]: value
640
+ };
641
+ const changedInput = parsed.inputs.find((i) => i.props.name === inputName);
642
+ if (!changedInput) return next;
643
+ let affectedQueries;
644
+ if (changedInput.type === "DateRange") if (changedInput.props.queries) affectedQueries = changedInput.props.queries.split(",").map((s) => s.trim());
645
+ else affectedQueries = Array.from(parsed.queries.entries()).filter(([, opts]) => opts.timeDimension).map(([name]) => name);
646
+ else affectedQueries = changedInput.props.queries ? changedInput.props.queries.split(",").map((s) => s.trim()) : [];
647
+ if (affectedQueries.length > 0) executeQueries(parsed.queries, parsed.inputs, next, affectedQueries);
648
+ return next;
649
+ });
650
+ }, [parsed, executeQueries]);
651
+ if (parseError) return /* @__PURE__ */ jsxs("div", {
652
+ style: {
653
+ borderRadius: "var(--bon-radius)",
654
+ border: "1px solid var(--bon-border-error)",
655
+ backgroundColor: "var(--bon-bg-error)",
656
+ padding: 16
657
+ },
658
+ children: [/* @__PURE__ */ jsx("h3", {
659
+ style: {
660
+ fontWeight: 500,
661
+ color: "var(--bon-text-error)",
662
+ marginBottom: 4,
663
+ margin: 0
664
+ },
665
+ children: "Dashboard Parse Error"
666
+ }), /* @__PURE__ */ jsx("pre", {
667
+ style: {
668
+ fontSize: 14,
669
+ color: "var(--bon-text-error)",
670
+ whiteSpace: "pre-wrap",
671
+ margin: 0
672
+ },
673
+ children: parseError
674
+ })]
675
+ });
676
+ if (!parsed) return null;
677
+ const groupedSections = groupConsecutiveBigValues(parsed.sections);
678
+ return /* @__PURE__ */ jsxs("div", {
679
+ style: {
680
+ display: "flex",
681
+ flexDirection: "column",
682
+ gap: 16
683
+ },
684
+ children: [parsed.inputs.length > 0 && /* @__PURE__ */ jsx(InputsBar, {
685
+ inputs: parsed.inputs,
686
+ inputsState,
687
+ queryStates,
688
+ onInputChange: handleInputChange
689
+ }), groupedSections.map((section, i) => /* @__PURE__ */ jsx(SectionRenderer, {
690
+ section,
691
+ queryStates
692
+ }, i))]
693
+ });
694
+ }
695
+ function InputsBar({ inputs, inputsState, queryStates, onInputChange }) {
696
+ return /* @__PURE__ */ jsx("div", {
697
+ style: {
698
+ display: "flex",
699
+ flexWrap: "wrap",
700
+ alignItems: "flex-end",
701
+ gap: 16,
702
+ borderRadius: "var(--bon-radius)",
703
+ backgroundColor: "var(--bon-bg-muted)",
704
+ padding: "12px 16px"
705
+ },
706
+ children: inputs.map((input) => {
707
+ const { type, props } = input;
708
+ const name = props.name;
709
+ if (!name) return null;
710
+ if (type === "DateRange") {
711
+ const value = inputsState[name];
712
+ if (!value) return null;
713
+ return /* @__PURE__ */ jsx(DateRangeInput, {
714
+ name,
715
+ label: props.label,
716
+ value,
717
+ onInputChange
718
+ }, name);
719
+ }
720
+ if (type === "Dropdown") {
721
+ const dataRef = props.data;
722
+ const data = dataRef ? queryStates.get(dataRef)?.data ?? [] : [];
723
+ return /* @__PURE__ */ jsx(DropdownInput, {
724
+ name,
725
+ dimension: props.dimension,
726
+ label: props.label,
727
+ data,
728
+ value: inputsState[name] ?? null,
729
+ onInputChange
730
+ }, name);
731
+ }
732
+ return null;
733
+ })
734
+ });
735
+ }
736
+ function groupConsecutiveBigValues(sections) {
737
+ const result = [];
738
+ let bigValueRun = [];
739
+ const flushRun = () => {
740
+ if (bigValueRun.length > 1) result.push({
741
+ kind: "grid",
742
+ props: { cols: String(Math.min(bigValueRun.length, 4)) },
743
+ children: bigValueRun
744
+ });
745
+ else if (bigValueRun.length === 1) result.push(bigValueRun[0]);
746
+ bigValueRun = [];
747
+ };
748
+ for (const section of sections) if (section.kind === "component" && section.component.type === "BigValue") bigValueRun.push(section);
749
+ else {
750
+ flushRun();
751
+ result.push(section);
752
+ }
753
+ flushRun();
754
+ return result;
755
+ }
756
+ function SectionRenderer({ section, queryStates }) {
757
+ if (section.kind === "html") return /* @__PURE__ */ jsx("div", {
758
+ style: {
759
+ color: "var(--bon-text)",
760
+ fontSize: 14,
761
+ lineHeight: 1.6
762
+ },
763
+ dangerouslySetInnerHTML: { __html: DOMPurify.sanitize(section.content) }
764
+ });
765
+ if (section.kind === "grid") return /* @__PURE__ */ jsx("div", {
766
+ style: {
767
+ display: "grid",
768
+ gap: 16,
769
+ ...GRID_COL_STYLES[section.props.cols || "2"] || GRID_COL_STYLES["2"]
770
+ },
771
+ children: section.children.map((child, i) => /* @__PURE__ */ jsx(SectionRenderer, {
772
+ section: child,
773
+ queryStates
774
+ }, i))
775
+ });
776
+ return /* @__PURE__ */ jsx(ComponentRenderer, {
777
+ component: section.component,
778
+ queryStates
779
+ });
780
+ }
781
+ function ComponentRenderer({ component, queryStates }) {
782
+ const { type, props } = component;
783
+ const dataRef = props.data;
784
+ if (!dataRef) return /* @__PURE__ */ jsxs("div", {
785
+ style: {
786
+ borderRadius: "var(--bon-radius)",
787
+ border: "1px solid var(--bon-border-warn)",
788
+ backgroundColor: "var(--bon-bg-warn)",
789
+ padding: 12,
790
+ fontSize: 14,
791
+ color: "var(--bon-text-warn)"
792
+ },
793
+ children: [
794
+ "Component <",
795
+ type,
796
+ "> is missing a \"data\" prop"
797
+ ]
798
+ });
799
+ const qs = queryStates.get(dataRef);
800
+ return /* @__PURE__ */ jsx(QueryLoad, {
801
+ data: qs?.data,
802
+ loading: qs?.loading ?? true,
803
+ error: qs?.error,
804
+ children: (data) => {
805
+ switch (type) {
806
+ case "BigValue": return /* @__PURE__ */ jsx(BigValue, {
807
+ data,
808
+ value: props.value,
809
+ title: props.title,
810
+ fmt: props.fmt
811
+ });
812
+ case "BarChart": return /* @__PURE__ */ jsx(BarChart, {
813
+ data,
814
+ x: props.x,
815
+ y: props.y,
816
+ title: props.title,
817
+ horizontal: props.horizontal === "true",
818
+ series: props.series,
819
+ type: props.type,
820
+ yFmt: props.yFmt
821
+ });
822
+ case "LineChart": return /* @__PURE__ */ jsx(LineChart, {
823
+ data,
824
+ x: props.x,
825
+ y: props.y,
826
+ title: props.title,
827
+ series: props.series,
828
+ type: props.type,
829
+ yFmt: props.yFmt
830
+ });
831
+ case "AreaChart": return /* @__PURE__ */ jsx(AreaChart, {
832
+ data,
833
+ x: props.x,
834
+ y: props.y,
835
+ title: props.title,
836
+ series: props.series,
837
+ type: props.type,
838
+ yFmt: props.yFmt
839
+ });
840
+ case "PieChart": return /* @__PURE__ */ jsx(PieChart, {
841
+ data,
842
+ name: props.name,
843
+ value: props.value,
844
+ title: props.title
845
+ });
846
+ case "DataTable": return /* @__PURE__ */ jsx(DataTable, {
847
+ data,
848
+ columns: props.columns ? props.columns.split(",").map((c) => c.trim()) : void 0,
849
+ fmt: props.fmt,
850
+ rows: props.rows === "all" ? "all" : props.rows ? Number(props.rows) : void 0,
851
+ queryLimit: qs?.limit
852
+ });
853
+ default: return /* @__PURE__ */ jsxs("div", {
854
+ style: {
855
+ borderRadius: "var(--bon-radius)",
856
+ border: "1px solid var(--bon-border-warn)",
857
+ backgroundColor: "var(--bon-bg-warn)",
858
+ padding: 12,
859
+ fontSize: 14,
860
+ color: "var(--bon-text-warn)"
861
+ },
862
+ children: ["Unknown component: ", type]
863
+ });
864
+ }
865
+ }
866
+ });
867
+ }
868
+
869
+ //#endregion
870
+ //#region src/dashboard/dashboard.tsx
871
+ function Dashboard({ slug, content, onInputsChange, className }) {
872
+ if (content) return /* @__PURE__ */ jsx("div", {
873
+ className,
874
+ children: /* @__PURE__ */ jsx(DashboardViewer, {
875
+ content,
876
+ onInputsChange
877
+ })
878
+ });
879
+ if (slug) return /* @__PURE__ */ jsx(DashboardFromSlug, {
880
+ slug,
881
+ onInputsChange,
882
+ className
883
+ });
884
+ return null;
885
+ }
886
+ function DashboardFromSlug({ slug, onInputsChange, className }) {
887
+ const { dashboard, loading, error } = useDashboard(slug);
888
+ if (loading) return /* @__PURE__ */ jsx("div", {
889
+ style: {
890
+ display: "flex",
891
+ alignItems: "center",
892
+ justifyContent: "center",
893
+ height: 200,
894
+ fontSize: 14,
895
+ color: "var(--bon-text-muted)"
896
+ },
897
+ children: /* @__PURE__ */ jsxs("div", {
898
+ style: {
899
+ display: "flex",
900
+ alignItems: "center",
901
+ gap: 8
902
+ },
903
+ children: [/* @__PURE__ */ jsx("div", { style: {
904
+ width: 16,
905
+ height: 16,
906
+ borderRadius: "50%",
907
+ border: "2px solid var(--bon-spinner-color)",
908
+ borderTopColor: "transparent",
909
+ animation: "bonnard-spin 0.6s linear infinite"
910
+ } }), "Loading dashboard…"]
911
+ })
912
+ });
913
+ if (error) return /* @__PURE__ */ jsxs("div", {
914
+ style: {
915
+ borderRadius: "var(--bon-radius)",
916
+ border: "1px solid var(--bon-border-error)",
917
+ backgroundColor: "var(--bon-bg-error)",
918
+ padding: 16,
919
+ fontSize: 14,
920
+ color: "var(--bon-text-error)"
921
+ },
922
+ children: ["Failed to load dashboard: ", error]
923
+ });
924
+ if (!dashboard) return null;
925
+ return /* @__PURE__ */ jsx("div", {
926
+ className,
927
+ children: /* @__PURE__ */ jsx(DashboardViewer, {
928
+ content: dashboard.content,
929
+ onInputsChange
930
+ })
931
+ });
932
+ }
933
+
934
+ //#endregion
935
+ export { Dashboard, DashboardViewer, parseDashboard, useDashboard };