@duckcodeailabs/dql-compiler 0.1.0

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 (182) hide show
  1. package/LICENSE +123 -0
  2. package/dist/charts/area.d.ts +5 -0
  3. package/dist/charts/area.d.ts.map +1 -0
  4. package/dist/charts/area.js +30 -0
  5. package/dist/charts/area.js.map +1 -0
  6. package/dist/charts/bar.d.ts +5 -0
  7. package/dist/charts/bar.d.ts.map +1 -0
  8. package/dist/charts/bar.js +37 -0
  9. package/dist/charts/bar.js.map +1 -0
  10. package/dist/charts/boxplot.d.ts +5 -0
  11. package/dist/charts/boxplot.d.ts.map +1 -0
  12. package/dist/charts/boxplot.js +36 -0
  13. package/dist/charts/boxplot.js.map +1 -0
  14. package/dist/charts/chart-registry.d.ts +24 -0
  15. package/dist/charts/chart-registry.d.ts.map +1 -0
  16. package/dist/charts/chart-registry.js +14 -0
  17. package/dist/charts/chart-registry.js.map +1 -0
  18. package/dist/charts/combo.d.ts +5 -0
  19. package/dist/charts/combo.d.ts.map +1 -0
  20. package/dist/charts/combo.js +54 -0
  21. package/dist/charts/combo.js.map +1 -0
  22. package/dist/charts/funnel.d.ts +5 -0
  23. package/dist/charts/funnel.d.ts.map +1 -0
  24. package/dist/charts/funnel.js +36 -0
  25. package/dist/charts/funnel.js.map +1 -0
  26. package/dist/charts/gauge.d.ts +5 -0
  27. package/dist/charts/gauge.d.ts.map +1 -0
  28. package/dist/charts/gauge.js +53 -0
  29. package/dist/charts/gauge.js.map +1 -0
  30. package/dist/charts/geo.d.ts +5 -0
  31. package/dist/charts/geo.d.ts.map +1 -0
  32. package/dist/charts/geo.js +48 -0
  33. package/dist/charts/geo.js.map +1 -0
  34. package/dist/charts/grouped-bar.d.ts +5 -0
  35. package/dist/charts/grouped-bar.d.ts.map +1 -0
  36. package/dist/charts/grouped-bar.js +37 -0
  37. package/dist/charts/grouped-bar.js.map +1 -0
  38. package/dist/charts/heatmap.d.ts +5 -0
  39. package/dist/charts/heatmap.d.ts.map +1 -0
  40. package/dist/charts/heatmap.js +31 -0
  41. package/dist/charts/heatmap.js.map +1 -0
  42. package/dist/charts/histogram.d.ts +5 -0
  43. package/dist/charts/histogram.d.ts.map +1 -0
  44. package/dist/charts/histogram.js +32 -0
  45. package/dist/charts/histogram.js.map +1 -0
  46. package/dist/charts/index.d.ts +25 -0
  47. package/dist/charts/index.d.ts.map +1 -0
  48. package/dist/charts/index.js +66 -0
  49. package/dist/charts/index.js.map +1 -0
  50. package/dist/charts/kpi.d.ts +14 -0
  51. package/dist/charts/kpi.d.ts.map +1 -0
  52. package/dist/charts/kpi.js +71 -0
  53. package/dist/charts/kpi.js.map +1 -0
  54. package/dist/charts/line.d.ts +5 -0
  55. package/dist/charts/line.d.ts.map +1 -0
  56. package/dist/charts/line.js +38 -0
  57. package/dist/charts/line.js.map +1 -0
  58. package/dist/charts/pie.d.ts +5 -0
  59. package/dist/charts/pie.d.ts.map +1 -0
  60. package/dist/charts/pie.js +25 -0
  61. package/dist/charts/pie.js.map +1 -0
  62. package/dist/charts/sankey.d.ts +10 -0
  63. package/dist/charts/sankey.d.ts.map +1 -0
  64. package/dist/charts/sankey.js +120 -0
  65. package/dist/charts/sankey.js.map +1 -0
  66. package/dist/charts/scatter.d.ts +5 -0
  67. package/dist/charts/scatter.d.ts.map +1 -0
  68. package/dist/charts/scatter.js +34 -0
  69. package/dist/charts/scatter.js.map +1 -0
  70. package/dist/charts/shared.d.ts +11 -0
  71. package/dist/charts/shared.d.ts.map +1 -0
  72. package/dist/charts/shared.js +52 -0
  73. package/dist/charts/shared.js.map +1 -0
  74. package/dist/charts/small-multiples.d.ts +5 -0
  75. package/dist/charts/small-multiples.d.ts.map +1 -0
  76. package/dist/charts/small-multiples.js +59 -0
  77. package/dist/charts/small-multiples.js.map +1 -0
  78. package/dist/charts/sparkline.d.ts +5 -0
  79. package/dist/charts/sparkline.d.ts.map +1 -0
  80. package/dist/charts/sparkline.js +41 -0
  81. package/dist/charts/sparkline.js.map +1 -0
  82. package/dist/charts/stacked-bar.d.ts +5 -0
  83. package/dist/charts/stacked-bar.d.ts.map +1 -0
  84. package/dist/charts/stacked-bar.js +34 -0
  85. package/dist/charts/stacked-bar.js.map +1 -0
  86. package/dist/charts/table.d.ts +14 -0
  87. package/dist/charts/table.d.ts.map +1 -0
  88. package/dist/charts/table.js +58 -0
  89. package/dist/charts/table.js.map +1 -0
  90. package/dist/charts/treemap.d.ts +9 -0
  91. package/dist/charts/treemap.d.ts.map +1 -0
  92. package/dist/charts/treemap.js +60 -0
  93. package/dist/charts/treemap.js.map +1 -0
  94. package/dist/charts/waterfall.d.ts +5 -0
  95. package/dist/charts/waterfall.d.ts.map +1 -0
  96. package/dist/charts/waterfall.js +36 -0
  97. package/dist/charts/waterfall.js.map +1 -0
  98. package/dist/codegen/bundle.d.ts +34 -0
  99. package/dist/codegen/bundle.d.ts.map +1 -0
  100. package/dist/codegen/bundle.js +20 -0
  101. package/dist/codegen/bundle.js.map +1 -0
  102. package/dist/codegen/html-emitter.d.ts +10 -0
  103. package/dist/codegen/html-emitter.d.ts.map +1 -0
  104. package/dist/codegen/html-emitter.js +1082 -0
  105. package/dist/codegen/html-emitter.js.map +1 -0
  106. package/dist/codegen/index.d.ts +6 -0
  107. package/dist/codegen/index.d.ts.map +1 -0
  108. package/dist/codegen/index.js +6 -0
  109. package/dist/codegen/index.js.map +1 -0
  110. package/dist/codegen/react-emitter.d.ts +42 -0
  111. package/dist/codegen/react-emitter.d.ts.map +1 -0
  112. package/dist/codegen/react-emitter.js +135 -0
  113. package/dist/codegen/react-emitter.js.map +1 -0
  114. package/dist/codegen/runtime-emitter.d.ts +2 -0
  115. package/dist/codegen/runtime-emitter.d.ts.map +1 -0
  116. package/dist/codegen/runtime-emitter.js +1807 -0
  117. package/dist/codegen/runtime-emitter.js.map +1 -0
  118. package/dist/codegen/vega-lite-emitter.d.ts +23 -0
  119. package/dist/codegen/vega-lite-emitter.d.ts.map +1 -0
  120. package/dist/codegen/vega-lite-emitter.js +92 -0
  121. package/dist/codegen/vega-lite-emitter.js.map +1 -0
  122. package/dist/compiler.d.ts +17 -0
  123. package/dist/compiler.d.ts.map +1 -0
  124. package/dist/compiler.js +106 -0
  125. package/dist/compiler.js.map +1 -0
  126. package/dist/index.d.ts +7 -0
  127. package/dist/index.d.ts.map +1 -0
  128. package/dist/index.js +9 -0
  129. package/dist/index.js.map +1 -0
  130. package/dist/ir/index.d.ts +3 -0
  131. package/dist/ir/index.d.ts.map +1 -0
  132. package/dist/ir/index.js +2 -0
  133. package/dist/ir/index.js.map +1 -0
  134. package/dist/ir/ir-nodes.d.ts +169 -0
  135. package/dist/ir/ir-nodes.d.ts.map +1 -0
  136. package/dist/ir/ir-nodes.js +2 -0
  137. package/dist/ir/ir-nodes.js.map +1 -0
  138. package/dist/ir/lowering.d.ts +9 -0
  139. package/dist/ir/lowering.d.ts.map +1 -0
  140. package/dist/ir/lowering.js +980 -0
  141. package/dist/ir/lowering.js.map +1 -0
  142. package/dist/modules/index.d.ts +4 -0
  143. package/dist/modules/index.d.ts.map +1 -0
  144. package/dist/modules/index.js +3 -0
  145. package/dist/modules/index.js.map +1 -0
  146. package/dist/modules/resolver.d.ts +21 -0
  147. package/dist/modules/resolver.d.ts.map +1 -0
  148. package/dist/modules/resolver.js +112 -0
  149. package/dist/modules/resolver.js.map +1 -0
  150. package/dist/modules/use-expander.d.ts +11 -0
  151. package/dist/modules/use-expander.d.ts.map +1 -0
  152. package/dist/modules/use-expander.js +42 -0
  153. package/dist/modules/use-expander.js.map +1 -0
  154. package/dist/themes/colorful.d.ts +3 -0
  155. package/dist/themes/colorful.d.ts.map +1 -0
  156. package/dist/themes/colorful.js +14 -0
  157. package/dist/themes/colorful.js.map +1 -0
  158. package/dist/themes/corporate.d.ts +3 -0
  159. package/dist/themes/corporate.d.ts.map +1 -0
  160. package/dist/themes/corporate.js +14 -0
  161. package/dist/themes/corporate.js.map +1 -0
  162. package/dist/themes/dark.d.ts +3 -0
  163. package/dist/themes/dark.d.ts.map +1 -0
  164. package/dist/themes/dark.js +14 -0
  165. package/dist/themes/dark.js.map +1 -0
  166. package/dist/themes/index.d.ts +11 -0
  167. package/dist/themes/index.d.ts.map +1 -0
  168. package/dist/themes/index.js +29 -0
  169. package/dist/themes/index.js.map +1 -0
  170. package/dist/themes/light.d.ts +3 -0
  171. package/dist/themes/light.d.ts.map +1 -0
  172. package/dist/themes/light.js +14 -0
  173. package/dist/themes/light.js.map +1 -0
  174. package/dist/themes/minimal.d.ts +3 -0
  175. package/dist/themes/minimal.d.ts.map +1 -0
  176. package/dist/themes/minimal.js +14 -0
  177. package/dist/themes/minimal.js.map +1 -0
  178. package/dist/themes/theme-types.d.ts +14 -0
  179. package/dist/themes/theme-types.d.ts.map +1 -0
  180. package/dist/themes/theme-types.js +2 -0
  181. package/dist/themes/theme-types.js.map +1 -0
  182. package/package.json +43 -0
@@ -0,0 +1,1082 @@
1
+ import { emitChartSpecs } from './vega-lite-emitter.js';
2
+ export function emitDashboardHTML(dashboard, chartSpecs, theme, runtimeJS, options = {}) {
3
+ const chartContainers = dashboard.layout.items
4
+ .map((item) => {
5
+ const spec = chartSpecs.find((s) => s.chartId === item.chartId);
6
+ const chartIR = dashboard.charts.find((c) => c.id === item.chartId);
7
+ const chartType = spec?.kind ?? 'vega-lite';
8
+ const hasInteraction = spec?.interaction && (spec.interaction.drillDown || spec.interaction.linkTo || spec.interaction.onClick);
9
+ const hasDrill = Boolean(chartIR?.drillConfig || spec?.interaction?.drillDown);
10
+ const conditionAttr = chartIR?.condition ? ` data-condition="${escapeHTML(chartIR.condition)}"` : '';
11
+ const conditionStyle = chartIR?.condition ? ' display: none;' : '';
12
+ return `
13
+ <div class="dql-chart-wrapper" style="grid-column: ${item.gridColumn}; grid-row: ${item.gridRow};${conditionStyle}"${conditionAttr}>
14
+ ${hasDrill ? `<div class="dql-drill-indicator">Drill</div>` : ''}
15
+ <button class="dql-inline-drill-up" type="button" data-chart-id="${item.chartId}" style="display:none;">Drill Up</button>
16
+ <div id="${item.chartId}" class="dql-chart dql-chart-${chartType}${hasInteraction ? ' dql-interactive' : ''}" style="
17
+ background: ${theme.cardBackground};
18
+ border: 1px solid ${theme.borderColor};
19
+ border-radius: 8px;
20
+ padding: 16px;
21
+ min-height: 200px;
22
+ "><div class="dql-skeleton"></div></div>
23
+ </div>`;
24
+ })
25
+ .join('\n');
26
+ const vegaLiteSpecs = {};
27
+ const interactions = {};
28
+ for (const spec of chartSpecs) {
29
+ if (spec.kind === 'vega-lite') {
30
+ vegaLiteSpecs[spec.chartId] = spec.spec;
31
+ }
32
+ if (spec.interaction) {
33
+ interactions[spec.chartId] = spec.interaction;
34
+ }
35
+ }
36
+ const filterBar = emitFilterBar(dashboard.filters, theme);
37
+ return `<!DOCTYPE html>
38
+ <html lang="en">
39
+ <head>
40
+ <meta charset="UTF-8">
41
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
42
+ <title>${escapeHTML(dashboard.title)}</title>
43
+ ${emitVegaScriptTags(options)}
44
+ <style>
45
+ :root {
46
+ --dql-bg: ${theme.background};
47
+ --dql-fg: ${theme.foreground};
48
+ --dql-card-bg: ${theme.cardBackground};
49
+ --dql-border: ${theme.borderColor};
50
+ --dql-font: ${theme.fontFamily};
51
+ --dql-title-color: ${theme.titleColor};
52
+ --dql-text-color: ${theme.textColor};
53
+ --dql-grid-color: ${theme.gridColor};
54
+ --dql-axis-color: ${theme.axisColor};
55
+ --dql-accent: ${theme.colors[0]};
56
+ }
57
+
58
+ * { margin: 0; padding: 0; box-sizing: border-box; }
59
+
60
+ body {
61
+ font-family: var(--dql-font);
62
+ background: var(--dql-bg);
63
+ color: var(--dql-fg);
64
+ padding: 24px;
65
+ min-height: 100vh;
66
+ }
67
+
68
+ .dql-dashboard-header {
69
+ display: flex;
70
+ align-items: center;
71
+ justify-content: space-between;
72
+ margin-bottom: 24px;
73
+ padding-bottom: 16px;
74
+ border-bottom: 1px solid var(--dql-border);
75
+ }
76
+
77
+ .dql-dashboard-title {
78
+ font-size: 24px;
79
+ font-weight: 700;
80
+ color: ${theme.titleColor};
81
+ }
82
+
83
+ .dql-header-actions {
84
+ display: flex;
85
+ gap: 8px;
86
+ align-items: center;
87
+ }
88
+
89
+ .dql-header-btn {
90
+ padding: 6px 12px;
91
+ border: 1px solid ${theme.borderColor};
92
+ border-radius: 6px;
93
+ background: ${theme.cardBackground};
94
+ color: ${theme.textColor};
95
+ font-size: 13px;
96
+ font-family: inherit;
97
+ cursor: pointer;
98
+ transition: background 0.15s, border-color 0.15s;
99
+ }
100
+
101
+ .dql-header-btn:hover {
102
+ border-color: ${theme.colors[0]};
103
+ color: ${theme.colors[0]};
104
+ }
105
+
106
+ .dql-header-select {
107
+ padding: 6px 10px;
108
+ border: 1px solid ${theme.borderColor};
109
+ border-radius: 6px;
110
+ background: ${theme.cardBackground};
111
+ color: ${theme.textColor};
112
+ font-size: 13px;
113
+ font-family: inherit;
114
+ min-width: 110px;
115
+ }
116
+
117
+ /* Filter Bar */
118
+ .dql-filter-bar {
119
+ display: flex;
120
+ flex-wrap: wrap;
121
+ gap: 16px;
122
+ align-items: flex-end;
123
+ margin-bottom: 20px;
124
+ padding: 16px;
125
+ background: ${theme.cardBackground};
126
+ border: 1px solid ${theme.borderColor};
127
+ border-radius: 8px;
128
+ }
129
+
130
+ .dql-filter-group {
131
+ display: flex;
132
+ flex-direction: column;
133
+ gap: 4px;
134
+ }
135
+
136
+ .dql-filter-label {
137
+ font-size: 12px;
138
+ font-weight: 600;
139
+ color: ${theme.textColor};
140
+ text-transform: uppercase;
141
+ letter-spacing: 0.5px;
142
+ }
143
+
144
+ .dql-filter-select,
145
+ .dql-filter-input,
146
+ .dql-filter-date {
147
+ padding: 8px 12px;
148
+ border: 1px solid ${theme.borderColor};
149
+ border-radius: 6px;
150
+ background: ${theme.background};
151
+ color: ${theme.foreground};
152
+ font-size: 14px;
153
+ font-family: inherit;
154
+ min-width: 160px;
155
+ }
156
+
157
+ .dql-filter-select:focus,
158
+ .dql-filter-input:focus,
159
+ .dql-filter-date:focus {
160
+ outline: none;
161
+ border-color: ${theme.colors[0]};
162
+ box-shadow: 0 0 0 2px ${theme.colors[0]}33;
163
+ }
164
+
165
+ .dql-filter-apply {
166
+ padding: 8px 20px;
167
+ background: ${theme.colors[0]};
168
+ color: white;
169
+ border: none;
170
+ border-radius: 6px;
171
+ font-size: 14px;
172
+ font-weight: 600;
173
+ cursor: pointer;
174
+ }
175
+
176
+ .dql-filter-apply:hover {
177
+ opacity: 0.9;
178
+ }
179
+
180
+ .dql-filter-reset {
181
+ padding: 8px 16px;
182
+ background: transparent;
183
+ color: ${theme.textColor};
184
+ border: 1px solid ${theme.borderColor};
185
+ border-radius: 6px;
186
+ font-size: 14px;
187
+ cursor: pointer;
188
+ }
189
+
190
+ .dql-dashboard-grid {
191
+ display: grid;
192
+ grid-template-columns: repeat(${dashboard.layout.columns}, 1fr);
193
+ gap: 16px;
194
+ }
195
+
196
+ .dql-chart-wrapper {
197
+ min-width: 0;
198
+ position: relative;
199
+ }
200
+
201
+ .dql-drill-indicator {
202
+ position: absolute;
203
+ top: 10px;
204
+ right: 12px;
205
+ z-index: 2;
206
+ padding: 2px 8px;
207
+ font-size: 11px;
208
+ font-weight: 600;
209
+ border-radius: 999px;
210
+ border: 1px solid ${theme.borderColor};
211
+ background: ${theme.cardBackground};
212
+ color: ${theme.textColor};
213
+ opacity: 0.8;
214
+ pointer-events: none;
215
+ }
216
+
217
+ .dql-inline-drill-up {
218
+ position: absolute;
219
+ top: 10px;
220
+ left: 12px;
221
+ z-index: 3;
222
+ padding: 4px 8px;
223
+ border: 1px solid ${theme.borderColor};
224
+ border-radius: 6px;
225
+ background: ${theme.cardBackground};
226
+ color: ${theme.textColor};
227
+ font-size: 12px;
228
+ cursor: pointer;
229
+ }
230
+
231
+ .dql-chart canvas, .dql-chart svg {
232
+ max-width: 100%;
233
+ }
234
+
235
+ .dql-interactive {
236
+ cursor: pointer;
237
+ }
238
+
239
+ .dql-loading {
240
+ display: flex;
241
+ align-items: center;
242
+ justify-content: center;
243
+ min-height: 200px;
244
+ color: ${theme.textColor};
245
+ }
246
+
247
+ .dql-error {
248
+ color: #E45756;
249
+ padding: 16px;
250
+ background: ${theme.cardBackground};
251
+ border: 1px solid #E45756;
252
+ border-radius: 8px;
253
+ }
254
+
255
+ /* Drill-down Modal */
256
+ .dql-modal-overlay {
257
+ display: none;
258
+ position: fixed;
259
+ top: 0;
260
+ left: 0;
261
+ right: 0;
262
+ bottom: 0;
263
+ background: rgba(0, 0, 0, 0.6);
264
+ z-index: 1000;
265
+ align-items: center;
266
+ justify-content: center;
267
+ }
268
+
269
+ .dql-modal-overlay.active {
270
+ display: flex;
271
+ }
272
+
273
+ .dql-modal {
274
+ background: ${theme.cardBackground};
275
+ border: 1px solid ${theme.borderColor};
276
+ border-radius: 12px;
277
+ width: 90%;
278
+ max-width: 900px;
279
+ max-height: 80vh;
280
+ overflow: auto;
281
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
282
+ }
283
+
284
+ .dql-modal-header {
285
+ display: flex;
286
+ align-items: center;
287
+ justify-content: space-between;
288
+ padding: 20px 24px;
289
+ border-bottom: 1px solid ${theme.borderColor};
290
+ }
291
+
292
+ .dql-modal-title {
293
+ font-size: 18px;
294
+ font-weight: 700;
295
+ color: ${theme.titleColor};
296
+ }
297
+
298
+ .dql-drill-stack {
299
+ margin-top: 4px;
300
+ font-size: 12px;
301
+ color: ${theme.textColor};
302
+ opacity: 0.8;
303
+ }
304
+
305
+ .dql-drill-controls {
306
+ display: flex;
307
+ align-items: center;
308
+ gap: 8px;
309
+ }
310
+
311
+ .dql-drill-up {
312
+ padding: 4px 10px;
313
+ font-size: 12px;
314
+ }
315
+
316
+ .dql-modal-close {
317
+ width: 32px;
318
+ height: 32px;
319
+ border: none;
320
+ background: transparent;
321
+ color: ${theme.textColor};
322
+ font-size: 20px;
323
+ cursor: pointer;
324
+ border-radius: 6px;
325
+ display: flex;
326
+ align-items: center;
327
+ justify-content: center;
328
+ }
329
+
330
+ .dql-modal-close:hover {
331
+ background: ${theme.borderColor};
332
+ }
333
+
334
+ .dql-modal-body {
335
+ padding: 24px;
336
+ }
337
+
338
+ .dql-drill-table {
339
+ width: 100%;
340
+ border-collapse: collapse;
341
+ }
342
+
343
+ .dql-drill-table th {
344
+ text-align: left;
345
+ padding: 10px 12px;
346
+ font-size: 12px;
347
+ font-weight: 600;
348
+ text-transform: uppercase;
349
+ letter-spacing: 0.5px;
350
+ color: ${theme.textColor};
351
+ background: ${theme.background};
352
+ border-bottom: 2px solid ${theme.borderColor};
353
+ }
354
+
355
+ .dql-drill-table td {
356
+ padding: 10px 12px;
357
+ font-size: 14px;
358
+ color: ${theme.foreground};
359
+ border-bottom: 1px solid ${theme.borderColor};
360
+ }
361
+
362
+ .dql-drill-table tr:hover td {
363
+ background: ${theme.background};
364
+ }
365
+
366
+ /* Breadcrumb for navigation */
367
+ .dql-breadcrumb {
368
+ display: flex;
369
+ gap: 8px;
370
+ align-items: center;
371
+ margin-bottom: 16px;
372
+ font-size: 14px;
373
+ }
374
+
375
+ .dql-breadcrumb a {
376
+ color: ${theme.colors[0]};
377
+ text-decoration: none;
378
+ }
379
+
380
+ .dql-breadcrumb a:hover {
381
+ text-decoration: underline;
382
+ }
383
+
384
+ .dql-breadcrumb-sep {
385
+ color: ${theme.textColor};
386
+ }
387
+
388
+ /* Loading skeleton shimmer */
389
+ .dql-skeleton {
390
+ background: linear-gradient(90deg, var(--dql-card-bg) 25%, var(--dql-border) 50%, var(--dql-card-bg) 75%);
391
+ background-size: 200% 100%;
392
+ animation: dql-shimmer 1.5s infinite;
393
+ border-radius: 4px;
394
+ height: 200px;
395
+ width: 100%;
396
+ }
397
+
398
+ @keyframes dql-shimmer {
399
+ 0% { background-position: 200% 0; }
400
+ 100% { background-position: -200% 0; }
401
+ }
402
+
403
+ /* Fullscreen support */
404
+ .dql-fullscreen-btn {
405
+ background: none;
406
+ border: 1px solid var(--dql-border);
407
+ border-radius: 6px;
408
+ padding: 6px 10px;
409
+ cursor: pointer;
410
+ font-size: 14px;
411
+ color: var(--dql-fg);
412
+ }
413
+
414
+ .dql-fullscreen-btn:hover {
415
+ background: var(--dql-border);
416
+ }
417
+
418
+ /* Responsive breakpoints */
419
+ @media (max-width: 1024px) {
420
+ .dql-dashboard-grid {
421
+ grid-template-columns: repeat(6, 1fr) !important;
422
+ }
423
+ .dql-chart-wrapper {
424
+ grid-column: 1 / -1 !important;
425
+ }
426
+ }
427
+
428
+ @media (max-width: 768px) {
429
+ body { padding: 12px; }
430
+ .dql-dashboard-grid {
431
+ grid-template-columns: 1fr !important;
432
+ gap: 12px;
433
+ }
434
+ .dql-chart-wrapper {
435
+ grid-column: 1 / -1 !important;
436
+ }
437
+ .dql-dashboard-header {
438
+ flex-direction: column;
439
+ align-items: flex-start;
440
+ gap: 12px;
441
+ }
442
+ .dql-filter-bar {
443
+ flex-direction: column;
444
+ }
445
+ .dql-filter-select,
446
+ .dql-filter-input,
447
+ .dql-filter-date {
448
+ min-width: 100% !important;
449
+ }
450
+ .dql-modal {
451
+ width: 98%;
452
+ max-height: 90vh;
453
+ }
454
+ }
455
+
456
+ @media print {
457
+ .dql-header-actions,
458
+ .dql-filter-bar,
459
+ .dql-modal-overlay { display: none !important; }
460
+ body { background: white; color: black; padding: 0; }
461
+ .dql-chart { border: none !important; box-shadow: none !important; }
462
+ }
463
+ </style>
464
+ </head>
465
+ <body>
466
+ <header class="dql-dashboard-header">
467
+ <div>
468
+ <div class="dql-breadcrumb" id="dql-breadcrumb"></div>
469
+ <h1 class="dql-dashboard-title">${escapeHTML(dashboard.title)}</h1>
470
+ </div>
471
+ <div class="dql-header-actions">
472
+ <button class="dql-header-btn" id="dql-theme-toggle" title="Toggle theme">🌓 Theme</button>
473
+ <select class="dql-header-select" id="dql-view-presets" title="Saved views">
474
+ <option value="">Views</option>
475
+ </select>
476
+ <button class="dql-header-btn" id="dql-save-view" title="Save current view">Save View</button>
477
+ <button class="dql-header-btn" id="dql-export-csv" title="Export data as CSV">⬇ Export</button>
478
+ <button class="dql-header-btn" id="dql-fullscreen" title="Toggle fullscreen">⛶ Fullscreen</button>
479
+ </div>
480
+ </header>
481
+
482
+ ${filterBar}
483
+
484
+ <main class="dql-dashboard-grid">
485
+ ${chartContainers}
486
+ </main>
487
+
488
+ <!-- Drill-down Modal -->
489
+ <div class="dql-modal-overlay" id="dql-drill-modal">
490
+ <div class="dql-modal">
491
+ <div class="dql-modal-header">
492
+ <div>
493
+ <div class="dql-modal-title" id="dql-drill-title">Detail View</div>
494
+ <div class="dql-drill-stack" id="dql-drill-stack"></div>
495
+ </div>
496
+ <div class="dql-drill-controls">
497
+ <button class="dql-header-btn dql-drill-up" id="dql-drill-up" disabled>Drill Up</button>
498
+ <button class="dql-modal-close" id="dql-drill-close">&times;</button>
499
+ </div>
500
+ </div>
501
+ <div class="dql-modal-body" id="dql-drill-body">
502
+ <div class="dql-loading">Loading...</div>
503
+ </div>
504
+ </div>
505
+ </div>
506
+
507
+ <script>
508
+ // DQL Dashboard Configuration
509
+ const DQL_CONFIG = {
510
+ title: ${JSON.stringify(dashboard.title)},
511
+ charts: ${JSON.stringify(dashboard.charts.map((c) => ({
512
+ id: c.id,
513
+ type: c.chartType,
514
+ sql: c.sql,
515
+ params: c.sqlParams,
516
+ condition: c.condition,
517
+ connection: c.connection,
518
+ cacheTTL: c.cacheTTL,
519
+ materializeRefresh: c.materializeRefresh,
520
+ drillConfig: c.drillConfig,
521
+ })))},
522
+ vegaLiteSpecs: ${JSON.stringify(vegaLiteSpecs, null, 2)},
523
+ interactions: ${JSON.stringify(interactions)},
524
+ filters: ${JSON.stringify(dashboard.filters.map((f) => ({ id: f.id, type: f.filterType, sql: f.sql, params: f.sqlParams, param: f.param, label: f.label, defaultValue: f.defaultValue })))},
525
+ params: ${JSON.stringify(dashboard.params.map((p) => ({ name: p.name, type: p.paramType, defaultValue: p.defaultValue })))},
526
+ apiEndpoint: '/api/query',
527
+ refreshInterval: ${dashboard.refreshInterval ?? 0},
528
+ features: {
529
+ hierarchyDrillEnabled: true,
530
+ runtimeCacheEnabled: true,
531
+ materializationEnabled: true,
532
+ },
533
+ };
534
+
535
+ // Dashboard Shell UI
536
+ (function() {
537
+ // Theme toggle
538
+ var themeBtn = document.getElementById('dql-theme-toggle');
539
+ if (themeBtn) {
540
+ themeBtn.addEventListener('click', function() {
541
+ var body = document.body;
542
+ var isDark = body.getAttribute('data-theme') === 'dark';
543
+ if (isDark) {
544
+ body.removeAttribute('data-theme');
545
+ body.style.background = '';
546
+ body.style.color = '';
547
+ } else {
548
+ body.setAttribute('data-theme', 'dark');
549
+ body.style.background = '#1a1a2e';
550
+ body.style.color = '#e2e8f0';
551
+ }
552
+ });
553
+ }
554
+
555
+ // CSV export
556
+ var exportBtn = document.getElementById('dql-export-csv');
557
+ if (exportBtn) {
558
+ exportBtn.addEventListener('click', function() {
559
+ if (!DQL_CONFIG.charts || DQL_CONFIG.charts.length === 0) return;
560
+ var firstChart = DQL_CONFIG.charts[0];
561
+ // Collect variables from filters + URL params + dashboard params defaults
562
+ var variables = {};
563
+ var urlParams = new URLSearchParams(window.location.search);
564
+
565
+ if (DQL_CONFIG.filters) {
566
+ for (var i = 0; i < DQL_CONFIG.filters.length; i++) {
567
+ var f = DQL_CONFIG.filters[i];
568
+ var el = document.getElementById(f.id);
569
+ if (el && el.value !== '') {
570
+ variables[f.param] = el.value;
571
+ } else if (f.defaultValue && variables[f.param] === undefined) {
572
+ variables[f.param] = f.defaultValue;
573
+ }
574
+ }
575
+ }
576
+
577
+ if (DQL_CONFIG.params) {
578
+ for (var j = 0; j < DQL_CONFIG.params.length; j++) {
579
+ var p = DQL_CONFIG.params[j];
580
+ if (variables[p.name] === undefined) {
581
+ variables[p.name] = urlParams.get(p.name) || p.defaultValue;
582
+ }
583
+ }
584
+ }
585
+
586
+ urlParams.forEach(function(value, key) {
587
+ if (key === '_from' || key === '_fromTitle') return;
588
+ if (variables[key] === undefined) variables[key] = value;
589
+ });
590
+
591
+ var exportEndpoint = (DQL_CONFIG.apiEndpoint || '/api/query').replace('/query', '/export/csv');
592
+ fetch(exportEndpoint, {
593
+ method: 'POST',
594
+ headers: { 'Content-Type': 'application/json' },
595
+ body: JSON.stringify({
596
+ sql: firstChart.sql,
597
+ sqlParams: firstChart.params,
598
+ variables: variables,
599
+ connectionId: firstChart.connection,
600
+ filename: DQL_CONFIG.title
601
+ })
602
+ }).then(function(r) { return r.blob(); }).then(function(blob) {
603
+ var url = URL.createObjectURL(blob);
604
+ var a = document.createElement('a');
605
+ a.href = url;
606
+ a.download = (DQL_CONFIG.title || 'export') + '.csv';
607
+ a.click();
608
+ URL.revokeObjectURL(url);
609
+ });
610
+ });
611
+ }
612
+
613
+ // Fullscreen toggle
614
+ var fsBtn = document.getElementById('dql-fullscreen');
615
+ if (fsBtn) {
616
+ fsBtn.addEventListener('click', function() {
617
+ if (!document.fullscreenElement) {
618
+ document.documentElement.requestFullscreen();
619
+ } else {
620
+ document.exitFullscreen();
621
+ }
622
+ });
623
+ }
624
+
625
+ // Evaluate @if conditions based on URL params and param defaults
626
+ function evaluateConditions() {
627
+ var urlParams = new URLSearchParams(window.location.search);
628
+ var paramValues = {};
629
+ if (DQL_CONFIG.params) {
630
+ for (var i = 0; i < DQL_CONFIG.params.length; i++) {
631
+ var p = DQL_CONFIG.params[i];
632
+ paramValues[p.name] = urlParams.get(p.name) || p.defaultValue;
633
+ }
634
+ }
635
+ var conditionalEls = document.querySelectorAll('[data-condition]');
636
+ conditionalEls.forEach(function(el) {
637
+ var condParam = el.getAttribute('data-condition');
638
+ var val = paramValues[condParam] || urlParams.get(condParam);
639
+ if (val && val !== 'false' && val !== '0' && val !== '') {
640
+ el.style.display = '';
641
+ }
642
+ });
643
+ }
644
+ evaluateConditions();
645
+ })();
646
+ </script>
647
+ ${runtimeJS === '__EXTERNAL__'
648
+ ? '<script src="/dql-runtime.js"></script>'
649
+ : `<script>\n ${runtimeJS}\n </script>`}
650
+ </body>
651
+ </html>`;
652
+ }
653
+ function emitFilterBar(filters, theme) {
654
+ if (filters.length === 0)
655
+ return '';
656
+ const filterWidgets = filters.map((f) => {
657
+ switch (f.filterType) {
658
+ case 'dropdown':
659
+ return `
660
+ <div class="dql-filter-group">
661
+ <label class="dql-filter-label" for="${f.id}">${escapeHTML(f.label)}</label>
662
+ <select id="${f.id}" class="dql-filter-select" data-param="${escapeHTML(f.param)}"${f.options?.width ? ` style="min-width: ${f.options.width}px"` : ''}>
663
+ <option value="">${escapeHTML(f.placeholder ?? 'All')}</option>
664
+ </select>
665
+ </div>`;
666
+ case 'multi_select':
667
+ return `
668
+ <div class="dql-filter-group">
669
+ <label class="dql-filter-label" for="${f.id}">${escapeHTML(f.label)}</label>
670
+ <select id="${f.id}" class="dql-filter-select" data-param="${escapeHTML(f.param)}" multiple${f.options?.width ? ` style="min-width: ${f.options.width}px"` : ''}>
671
+ </select>
672
+ </div>`;
673
+ case 'date_range':
674
+ return `
675
+ <div class="dql-filter-group">
676
+ <label class="dql-filter-label" for="${f.id}">${escapeHTML(f.label)}</label>
677
+ <input type="date" id="${f.id}" class="dql-filter-date" data-param="${escapeHTML(f.param)}"${f.defaultValue ? ` value="${escapeHTML(String(f.defaultValue))}"` : ''}${f.options?.width ? ` style="min-width: ${f.options.width}px"` : ''} />
678
+ </div>`;
679
+ case 'text':
680
+ return `
681
+ <div class="dql-filter-group">
682
+ <label class="dql-filter-label" for="${f.id}">${escapeHTML(f.label)}</label>
683
+ <input type="text" id="${f.id}" class="dql-filter-input" data-param="${escapeHTML(f.param)}" placeholder="${escapeHTML(f.placeholder ?? 'Search...')}"${f.options?.width ? ` style="min-width: ${f.options.width}px"` : ''} />
684
+ </div>`;
685
+ case 'range':
686
+ return `
687
+ <div class="dql-filter-group">
688
+ <label class="dql-filter-label" for="${f.id}">${escapeHTML(f.label)}</label>
689
+ <input type="range" id="${f.id}" class="dql-filter-input" data-param="${escapeHTML(f.param)}"${f.options?.min != null ? ` min="${f.options.min}"` : ''}${f.options?.max != null ? ` max="${f.options.max}"` : ''}${f.options?.step != null ? ` step="${f.options.step}"` : ''}${f.defaultValue ? ` value="${escapeHTML(String(f.defaultValue))}"` : ''} />
690
+ </div>`;
691
+ default:
692
+ return '';
693
+ }
694
+ }).join('\n');
695
+ return `
696
+ <div class="dql-filter-bar" id="dql-filter-bar">
697
+ ${filterWidgets}
698
+ <div class="dql-filter-group" style="flex-direction: row; gap: 8px;">
699
+ <button class="dql-filter-apply" id="dql-filter-apply">Apply</button>
700
+ <button class="dql-filter-reset" id="dql-filter-reset">Reset</button>
701
+ </div>
702
+ </div>`;
703
+ }
704
+ export function emitWorkbookHTML(workbook, theme, runtimeJS, options = {}) {
705
+ // Build page tabs
706
+ const pageTabs = workbook.pages
707
+ .map((page, i) => {
708
+ const activeClass = i === 0 ? ' dql-tab-active' : '';
709
+ return `<button class="dql-tab${activeClass}" data-page="${i}" onclick="dqlSwitchPage(${i})">${escapeHTML(page.title)}</button>`;
710
+ })
711
+ .join('\n ');
712
+ // Build page content sections
713
+ const pageContents = workbook.pages
714
+ .map((page, pageIndex) => {
715
+ const pageChartSpecs = emitChartSpecs(page.charts, theme);
716
+ const filterBar = emitFilterBar(page.filters, theme);
717
+ const vegaLiteSpecs = {};
718
+ const interactions = {};
719
+ for (const spec of pageChartSpecs) {
720
+ if (spec.kind === 'vega-lite') {
721
+ vegaLiteSpecs[spec.chartId] = spec.spec;
722
+ }
723
+ if (spec.interaction) {
724
+ interactions[spec.chartId] = spec.interaction;
725
+ }
726
+ }
727
+ const chartContainers = page.layout.items
728
+ .map((item) => {
729
+ const spec = pageChartSpecs.find((s) => s.chartId === item.chartId);
730
+ const chartIR = page.charts.find((c) => c.id === item.chartId);
731
+ const chartType = spec?.kind ?? 'vega-lite';
732
+ const hasInteraction = spec?.interaction && (spec.interaction.drillDown || spec.interaction.linkTo || spec.interaction.onClick);
733
+ const hasDrill = Boolean(chartIR?.drillConfig || spec?.interaction?.drillDown);
734
+ return `
735
+ <div class="dql-chart-wrapper" style="grid-column: ${item.gridColumn}; grid-row: ${item.gridRow};">
736
+ ${hasDrill ? `<div class="dql-drill-indicator">Drill</div>` : ''}
737
+ <button class="dql-inline-drill-up" type="button" data-chart-id="${item.chartId}" style="display:none;">Drill Up</button>
738
+ <div id="${item.chartId}" class="dql-chart dql-chart-${chartType}${hasInteraction ? ' dql-interactive' : ''}" style="
739
+ background: ${theme.cardBackground};
740
+ border: 1px solid ${theme.borderColor};
741
+ border-radius: 8px;
742
+ padding: 16px;
743
+ min-height: 200px;
744
+ "></div>
745
+ </div>`;
746
+ })
747
+ .join('\n');
748
+ const displayStyle = pageIndex === 0 ? 'block' : 'none';
749
+ return `
750
+ <div class="dql-page" id="dql-page-${pageIndex}" style="display: ${displayStyle};" data-config='${JSON.stringify({
751
+ charts: page.charts.map((c) => ({
752
+ id: c.id,
753
+ type: c.chartType,
754
+ sql: c.sql,
755
+ params: c.sqlParams,
756
+ connection: c.connection,
757
+ cacheTTL: c.cacheTTL,
758
+ materializeRefresh: c.materializeRefresh,
759
+ drillConfig: c.drillConfig,
760
+ })),
761
+ vegaLiteSpecs,
762
+ interactions,
763
+ filters: page.filters.map((f) => ({ id: f.id, type: f.filterType, sql: f.sql, params: f.sqlParams, param: f.param, label: f.label, defaultValue: f.defaultValue })),
764
+ })}'>
765
+ ${filterBar}
766
+ <div class="dql-dashboard-grid" style="display: grid; grid-template-columns: repeat(${page.layout.columns}, 1fr); gap: 16px;">
767
+ ${chartContainers}
768
+ </div>
769
+ </div>`;
770
+ })
771
+ .join('\n');
772
+ return `<!DOCTYPE html>
773
+ <html lang="en">
774
+ <head>
775
+ <meta charset="UTF-8">
776
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
777
+ <title>${escapeHTML(workbook.title)}</title>
778
+ ${emitVegaScriptTags(options)}
779
+ <style>
780
+ * { margin: 0; padding: 0; box-sizing: border-box; }
781
+
782
+ body {
783
+ font-family: ${theme.fontFamily};
784
+ background: ${theme.background};
785
+ color: ${theme.foreground};
786
+ padding: 24px;
787
+ min-height: 100vh;
788
+ }
789
+
790
+ .dql-dashboard-header {
791
+ margin-bottom: 24px;
792
+ padding-bottom: 16px;
793
+ border-bottom: 1px solid ${theme.borderColor};
794
+ }
795
+
796
+ .dql-dashboard-title {
797
+ font-size: 24px;
798
+ font-weight: 700;
799
+ color: ${theme.titleColor};
800
+ margin-bottom: 16px;
801
+ }
802
+
803
+ .dql-tabs {
804
+ display: flex;
805
+ gap: 0;
806
+ border-bottom: 2px solid ${theme.borderColor};
807
+ }
808
+
809
+ .dql-tab {
810
+ padding: 10px 20px;
811
+ border: none;
812
+ background: transparent;
813
+ color: ${theme.textColor};
814
+ font-size: 14px;
815
+ font-weight: 600;
816
+ font-family: inherit;
817
+ cursor: pointer;
818
+ border-bottom: 2px solid transparent;
819
+ margin-bottom: -2px;
820
+ transition: color 0.2s, border-color 0.2s;
821
+ }
822
+
823
+ .dql-tab:hover {
824
+ color: ${theme.titleColor};
825
+ }
826
+
827
+ .dql-tab-active {
828
+ color: ${theme.colors[0]};
829
+ border-bottom-color: ${theme.colors[0]};
830
+ }
831
+
832
+ .dql-filter-bar {
833
+ display: flex;
834
+ flex-wrap: wrap;
835
+ gap: 16px;
836
+ align-items: flex-end;
837
+ margin-bottom: 20px;
838
+ padding: 16px;
839
+ background: ${theme.cardBackground};
840
+ border: 1px solid ${theme.borderColor};
841
+ border-radius: 8px;
842
+ }
843
+
844
+ .dql-filter-group {
845
+ display: flex;
846
+ flex-direction: column;
847
+ gap: 4px;
848
+ }
849
+
850
+ .dql-filter-label {
851
+ font-size: 12px;
852
+ font-weight: 600;
853
+ color: ${theme.textColor};
854
+ text-transform: uppercase;
855
+ letter-spacing: 0.5px;
856
+ }
857
+
858
+ .dql-filter-select,
859
+ .dql-filter-input,
860
+ .dql-filter-date {
861
+ padding: 8px 12px;
862
+ border: 1px solid ${theme.borderColor};
863
+ border-radius: 6px;
864
+ background: ${theme.background};
865
+ color: ${theme.foreground};
866
+ font-size: 14px;
867
+ font-family: inherit;
868
+ min-width: 160px;
869
+ }
870
+
871
+ .dql-filter-apply {
872
+ padding: 8px 20px;
873
+ background: ${theme.colors[0]};
874
+ color: white;
875
+ border: none;
876
+ border-radius: 6px;
877
+ font-size: 14px;
878
+ font-weight: 600;
879
+ cursor: pointer;
880
+ }
881
+
882
+ .dql-filter-reset {
883
+ padding: 8px 16px;
884
+ background: transparent;
885
+ color: ${theme.textColor};
886
+ border: 1px solid ${theme.borderColor};
887
+ border-radius: 6px;
888
+ font-size: 14px;
889
+ cursor: pointer;
890
+ }
891
+
892
+ .dql-chart-wrapper { min-width: 0; position: relative; }
893
+ .dql-drill-indicator { position: absolute; top: 10px; right: 12px; z-index: 2; padding: 2px 8px; font-size: 11px; font-weight: 600; border-radius: 999px; border: 1px solid ${theme.borderColor}; background: ${theme.cardBackground}; color: ${theme.textColor}; opacity: 0.8; pointer-events: none; }
894
+ .dql-inline-drill-up { position: absolute; top: 10px; left: 12px; z-index: 3; padding: 4px 8px; border: 1px solid ${theme.borderColor}; border-radius: 6px; background: ${theme.cardBackground}; color: ${theme.textColor}; font-size: 12px; cursor: pointer; }
895
+ .dql-chart canvas, .dql-chart svg { max-width: 100%; }
896
+ .dql-interactive { cursor: pointer; }
897
+ .dql-loading { display: flex; align-items: center; justify-content: center; min-height: 200px; color: ${theme.textColor}; }
898
+ .dql-error { color: #E45756; padding: 16px; background: ${theme.cardBackground}; border: 1px solid #E45756; border-radius: 8px; }
899
+
900
+ .dql-modal-overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.6); z-index: 1000; align-items: center; justify-content: center; }
901
+ .dql-modal-overlay.active { display: flex; }
902
+ .dql-modal { background: ${theme.cardBackground}; border: 1px solid ${theme.borderColor}; border-radius: 12px; width: 90%; max-width: 900px; max-height: 80vh; overflow: auto; }
903
+ .dql-modal-header { display: flex; align-items: center; justify-content: space-between; padding: 20px 24px; border-bottom: 1px solid ${theme.borderColor}; }
904
+ .dql-modal-title { font-size: 18px; font-weight: 700; color: ${theme.titleColor}; }
905
+ .dql-drill-stack { margin-top: 4px; font-size: 12px; color: ${theme.textColor}; opacity: 0.8; }
906
+ .dql-drill-controls { display: flex; align-items: center; gap: 8px; }
907
+ .dql-drill-up { padding: 4px 10px; font-size: 12px; border: 1px solid ${theme.borderColor}; border-radius: 6px; background: ${theme.cardBackground}; color: ${theme.textColor}; cursor: pointer; }
908
+ .dql-drill-up[disabled] { opacity: 0.6; cursor: not-allowed; }
909
+ .dql-modal-close { width: 32px; height: 32px; border: none; background: transparent; color: ${theme.textColor}; font-size: 20px; cursor: pointer; border-radius: 6px; display: flex; align-items: center; justify-content: center; }
910
+ .dql-modal-close:hover { background: ${theme.borderColor}; }
911
+ .dql-modal-body { padding: 24px; }
912
+ .dql-drill-table { width: 100%; border-collapse: collapse; }
913
+ .dql-drill-table th { text-align: left; padding: 10px 12px; font-size: 12px; font-weight: 600; text-transform: uppercase; color: ${theme.textColor}; background: ${theme.background}; border-bottom: 2px solid ${theme.borderColor}; }
914
+ .dql-drill-table td { padding: 10px 12px; font-size: 14px; color: ${theme.foreground}; border-bottom: 1px solid ${theme.borderColor}; }
915
+
916
+ /* Responsive breakpoints */
917
+ @media (max-width: 1024px) {
918
+ .dql-dashboard-grid { grid-template-columns: repeat(6, 1fr) !important; }
919
+ .dql-chart-wrapper { grid-column: 1 / -1 !important; }
920
+ }
921
+ @media (max-width: 768px) {
922
+ body { padding: 12px; }
923
+ .dql-dashboard-grid { grid-template-columns: 1fr !important; gap: 12px; }
924
+ .dql-chart-wrapper { grid-column: 1 / -1 !important; }
925
+ .dql-tabs { overflow-x: auto; }
926
+ .dql-tab { white-space: nowrap; }
927
+ .dql-filter-bar { flex-direction: column; }
928
+ .dql-filter-select, .dql-filter-input, .dql-filter-date { min-width: 100% !important; }
929
+ .dql-modal { width: 98%; max-height: 90vh; }
930
+ }
931
+ @media print {
932
+ .dql-tabs, .dql-filter-bar, .dql-modal-overlay { display: none !important; }
933
+ .dql-page { display: block !important; page-break-after: always; }
934
+ body { background: white; color: black; padding: 0; }
935
+ .dql-chart { border: none !important; }
936
+ }
937
+ </style>
938
+ </head>
939
+ <body>
940
+ <header class="dql-dashboard-header">
941
+ <h1 class="dql-dashboard-title">${escapeHTML(workbook.title)}</h1>
942
+ <nav class="dql-tabs">
943
+ ${pageTabs}
944
+ </nav>
945
+ </header>
946
+
947
+ ${pageContents}
948
+
949
+ <!-- Drill-down Modal -->
950
+ <div class="dql-modal-overlay" id="dql-drill-modal">
951
+ <div class="dql-modal">
952
+ <div class="dql-modal-header">
953
+ <div>
954
+ <div class="dql-modal-title" id="dql-drill-title">Detail View</div>
955
+ <div class="dql-drill-stack" id="dql-drill-stack"></div>
956
+ </div>
957
+ <div class="dql-drill-controls">
958
+ <button class="dql-drill-up" id="dql-drill-up" disabled>Drill Up</button>
959
+ <button class="dql-modal-close" id="dql-drill-close">&times;</button>
960
+ </div>
961
+ </div>
962
+ <div class="dql-modal-body" id="dql-drill-body">
963
+ <div class="dql-loading">Loading...</div>
964
+ </div>
965
+ </div>
966
+ </div>
967
+
968
+ <script>
969
+ // DQL Workbook Configuration
970
+ var DQL_CURRENT_PAGE = 0;
971
+ var DQL_PAGE_CONFIGS = [];
972
+
973
+ // Initialize page configs from data attributes
974
+ document.querySelectorAll('.dql-page').forEach(function(el, i) {
975
+ DQL_PAGE_CONFIGS[i] = JSON.parse(el.getAttribute('data-config'));
976
+ });
977
+
978
+ function dqlGetInitialPage() {
979
+ try {
980
+ var u = new URL(window.location.href);
981
+ var sp = u.searchParams.get('page');
982
+ if (sp !== null && sp !== undefined && sp !== '') {
983
+ var n = parseInt(sp, 10);
984
+ if (!isNaN(n)) return n;
985
+ }
986
+ var h = (window.location.hash || '').replace(/^#/, '');
987
+ var m = h.match(/(?:^|&)page=(\\d+)(?:&|$)/);
988
+ if (m) {
989
+ var n2 = parseInt(m[1], 10);
990
+ if (!isNaN(n2)) return n2;
991
+ }
992
+ } catch (_) {}
993
+ return 0;
994
+ }
995
+
996
+ (function() {
997
+ var initial = dqlGetInitialPage();
998
+ if (typeof initial === 'number' && initial >= 0 && initial < DQL_PAGE_CONFIGS.length) {
999
+ DQL_CURRENT_PAGE = initial;
1000
+ // Update page visibility + tab state before the runtime initializes.
1001
+ document.querySelectorAll('.dql-page').forEach(function(el, i) { el.style.display = (i === initial ? 'block' : 'none'); });
1002
+ document.querySelectorAll('.dql-tab').forEach(function(el) { el.classList.remove('dql-tab-active'); });
1003
+ if (document.querySelectorAll('.dql-tab')[initial]) document.querySelectorAll('.dql-tab')[initial].classList.add('dql-tab-active');
1004
+ }
1005
+ })();
1006
+
1007
+ // Set initial DQL_CONFIG from current page
1008
+ var DQL_CONFIG = Object.assign({}, DQL_PAGE_CONFIGS[DQL_CURRENT_PAGE] || DQL_PAGE_CONFIGS[0], {
1009
+ title: ${JSON.stringify(workbook.title)},
1010
+ apiEndpoint: '/api/query',
1011
+ features: {
1012
+ hierarchyDrillEnabled: true,
1013
+ runtimeCacheEnabled: true,
1014
+ materializationEnabled: true,
1015
+ },
1016
+ });
1017
+
1018
+ function dqlSwitchPage(pageIndex) {
1019
+ if (pageIndex < 0 || pageIndex >= DQL_PAGE_CONFIGS.length) return;
1020
+ // Hide all pages
1021
+ document.querySelectorAll('.dql-page').forEach(function(el) { el.style.display = 'none'; });
1022
+ // Show target page
1023
+ var targetPage = document.getElementById('dql-page-' + pageIndex);
1024
+ if (targetPage) targetPage.style.display = 'block';
1025
+
1026
+ // Update active tab
1027
+ document.querySelectorAll('.dql-tab').forEach(function(el) { el.classList.remove('dql-tab-active'); });
1028
+ document.querySelectorAll('.dql-tab')[pageIndex].classList.add('dql-tab-active');
1029
+
1030
+ // Update DQL_CONFIG for the new page
1031
+ DQL_CONFIG = Object.assign({}, DQL_PAGE_CONFIGS[pageIndex], {
1032
+ title: ${JSON.stringify(workbook.title)},
1033
+ apiEndpoint: '/api/query',
1034
+ features: {
1035
+ hierarchyDrillEnabled: true,
1036
+ runtimeCacheEnabled: true,
1037
+ materializationEnabled: true,
1038
+ },
1039
+ });
1040
+ DQL_CURRENT_PAGE = pageIndex;
1041
+
1042
+ // Keep URL in sync (makes deep-links and iframe navigation possible).
1043
+ try {
1044
+ var u = new URL(window.location.href);
1045
+ u.searchParams.set('page', String(pageIndex));
1046
+ history.replaceState(null, '', u.toString());
1047
+ } catch (_) {}
1048
+
1049
+ // Re-initialize charts for the new page
1050
+ if (typeof initDashboard === 'function') initDashboard();
1051
+ }
1052
+ </script>
1053
+ ${runtimeJS === '__EXTERNAL__'
1054
+ ? '<script src="/dql-runtime.js"></script>'
1055
+ : `<script>\n ${runtimeJS}\n </script>`}
1056
+ </body>
1057
+ </html>`;
1058
+ }
1059
+ function emitVegaScriptTags(options) {
1060
+ const assets = options.vegaAssets ?? 'cdn';
1061
+ if (assets === 'local') {
1062
+ const base = (options.vegaBasePath ?? '/vendor/vega').replace(/\/+$/, '');
1063
+ return [
1064
+ `<script src="${base}/vega@5.js"></script>`,
1065
+ `<script src="${base}/vega-lite@5.js"></script>`,
1066
+ `<script src="${base}/vega-embed@6.js"></script>`,
1067
+ ].join('\n ');
1068
+ }
1069
+ return [
1070
+ '<script src="https://cdn.jsdelivr.net/npm/vega@5"></script>',
1071
+ '<script src="https://cdn.jsdelivr.net/npm/vega-lite@5"></script>',
1072
+ '<script src="https://cdn.jsdelivr.net/npm/vega-embed@6"></script>',
1073
+ ].join('\n ');
1074
+ }
1075
+ function escapeHTML(str) {
1076
+ return str
1077
+ .replace(/&/g, '&amp;')
1078
+ .replace(/</g, '&lt;')
1079
+ .replace(/>/g, '&gt;')
1080
+ .replace(/"/g, '&quot;');
1081
+ }
1082
+ //# sourceMappingURL=html-emitter.js.map