salesforce-reports-mcp 0.2.0 → 0.3.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.
@@ -0,0 +1,946 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Salesforce dashboard</title>
7
+ <style>
8
+ /*
9
+ * Salesforce Lightning Design System token values for the report card and the dashboard card (decision 0018).
10
+ * Values only: no SLDS stylesheet, no Salesforce Sans font file, nothing loaded from the network.
11
+ * scripts/build-widget.ts inlines this file into both cards in place of their tokens marker.
12
+ *
13
+ * Source: @salesforce-ux/design-system 2.264.1 (npm, BSD-3-Clause), design-tokens/dist:
14
+ * font.raw.json font-family (the fallbacks after "Salesforce Sans")
15
+ * theme-one-salesforce.json FONT_SIZE_2 to FONT_SIZE_9, FONT_WEIGHT_REGULAR, FONT_WEIGHT_BOLD, LINE_HEIGHT_TEXT,
16
+ * LINE_HEIGHT_HEADING, SPACING_XXX_SMALL to SPACING_MEDIUM, BORDER_RADIUS_SMALL,
17
+ * BORDER_RADIUS_MEDIUM, CARD_COLOR_BORDER, CARD_SHADOW, BRAND_ACCESSIBLE,
18
+ * BRAND_ACCESSIBLE_ACTIVE
19
+ * slds-hooks.raw.json link-color, link-color-hover (shadow-outset-focus-1 is drawn from --link)
20
+ * palettes.json color-palette-neutral-*, color-palette-blue-70 and -80 (dark links),
21
+ * color-palette-red-60, color-palette-orange-65, color-palette-green-60 (gauge bands)
22
+ * The data-visualization palettes are not in the package's token files. They come from the Lightning Design System
23
+ * guidelines, Data Visualization, Charts, "Chart Color": the Default Palette and the named Bluegrass, Sunrise,
24
+ * Water and Watermelon palettes, each in the guideline's order.
25
+ *
26
+ * Copyright (c) 2015, salesforce.com, inc. All rights reserved.
27
+ *
28
+ * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
29
+ * following conditions are met: Redistributions of source code must retain the above copyright notice, this list of
30
+ * conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice,
31
+ * this list of conditions and the following disclaimer in the documentation and/or other materials provided with the
32
+ * distribution. Neither the name of salesforce.com, inc. nor the names of its contributors may be used to endorse or
33
+ * promote products derived from this software without specific prior written permission.
34
+ *
35
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
36
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
37
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
38
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
39
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
40
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
41
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42
+ */
43
+ :root {
44
+ /* Type: the font stack, the type scale, weights and line heights. */
45
+ --slds-font-family: "Salesforce Sans", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
46
+ --slds-font-size-2: 0.75rem;
47
+ --slds-font-size-3: 0.8125rem;
48
+ --slds-font-size-4: 0.875rem;
49
+ --slds-font-size-5: 1rem;
50
+ --slds-font-size-6: 1.125rem;
51
+ --slds-font-size-8: 1.5rem;
52
+ --slds-font-size-9: 1.75rem;
53
+ --slds-font-weight-regular: 400;
54
+ --slds-font-weight-bold: 700;
55
+ --slds-line-height-text: 1.5;
56
+ --slds-line-height-heading: 1.25;
57
+
58
+ /* Spacing steps. */
59
+ --slds-spacing-xxx-small: 0.125rem;
60
+ --slds-spacing-xx-small: 0.25rem;
61
+ --slds-spacing-x-small: 0.5rem;
62
+ --slds-spacing-small: 0.75rem;
63
+ --slds-spacing-medium: 1rem;
64
+
65
+ /* The card: border, radius and shadow. */
66
+ --slds-radius-small: 0.125rem;
67
+ --slds-card-radius: 0.25rem;
68
+ --slds-card-border: rgb(201, 201, 201);
69
+ --slds-card-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.10);
70
+
71
+ /* Brand blue: buttons, links and the focus ring. */
72
+ --slds-brand-accessible: rgb(1, 118, 211);
73
+ --slds-brand-accessible-active: rgb(1, 68, 134);
74
+ --slds-link: #0b5cab;
75
+ --slds-link-hover: #014486;
76
+ --slds-link-dark: rgb(120, 176, 253);
77
+ --slds-link-dark-hover: rgb(170, 203, 255);
78
+
79
+ /* Neutrals, light and dark. */
80
+ --slds-neutral-10: rgb(24, 24, 24);
81
+ --slds-neutral-15: rgb(36, 36, 36);
82
+ --slds-neutral-20: rgb(46, 46, 46);
83
+ --slds-neutral-30: rgb(68, 68, 68);
84
+ --slds-neutral-50: rgb(116, 116, 116);
85
+ --slds-neutral-70: rgb(174, 174, 174);
86
+ --slds-neutral-80: rgb(201, 201, 201);
87
+ --slds-neutral-90: rgb(229, 229, 229);
88
+ --slds-neutral-95: rgb(243, 243, 243);
89
+ --slds-neutral-100: rgb(255, 255, 255);
90
+
91
+ /* Gauge bands when a dashboard sets none: fills only, never text. */
92
+ --slds-red-60: rgb(254, 92, 76);
93
+ --slds-orange-65: rgb(243, 131, 3);
94
+ --slds-green-60: rgb(59, 167, 85);
95
+
96
+ /* The data-visualization Default Palette, in order. */
97
+ --slds-dataviz-1: #52B7D8;
98
+ --slds-dataviz-2: #E16032;
99
+ --slds-dataviz-3: #FFB03B;
100
+ --slds-dataviz-4: #54A77B;
101
+ --slds-dataviz-5: #4FD2D2;
102
+ --slds-dataviz-6: #E287B2;
103
+
104
+ /* The named chart palettes a dashboard can choose, in order. */
105
+ --slds-dataviz-bluegrass-1: #C7F296;
106
+ --slds-dataviz-bluegrass-2: #94E7A8;
107
+ --slds-dataviz-bluegrass-3: #51D2BB;
108
+ --slds-dataviz-bluegrass-4: #27AAB0;
109
+ --slds-dataviz-bluegrass-5: #116985;
110
+ --slds-dataviz-bluegrass-6: #053661;
111
+ --slds-dataviz-sunrise-1: #F5DE98;
112
+ --slds-dataviz-sunrise-2: #F5C062;
113
+ --slds-dataviz-sunrise-3: #F59623;
114
+ --slds-dataviz-sunrise-4: #CE6716;
115
+ --slds-dataviz-sunrise-5: #762F3D;
116
+ --slds-dataviz-sunrise-6: #300561;
117
+ --slds-dataviz-water-1: #96F2EE;
118
+ --slds-dataviz-water-2: #68CEEE;
119
+ --slds-dataviz-water-3: #2D9CED;
120
+ --slds-dataviz-water-4: #0E6ECE;
121
+ --slds-dataviz-water-5: #073E92;
122
+ --slds-dataviz-water-6: #051C61;
123
+ --slds-dataviz-watermelon-1: #F598A7;
124
+ --slds-dataviz-watermelon-2: #F56580;
125
+ --slds-dataviz-watermelon-3: #F4284E;
126
+ --slds-dataviz-watermelon-4: #C11C2F;
127
+ --slds-dataviz-watermelon-5: #5C3F22;
128
+ --slds-dataviz-watermelon-6: #0A611B;
129
+ }
130
+ /* The card's own names for the SLDS values. The host's theme variables set the surface (background, text and
131
+ border) first; the SLDS neutrals are the fallbacks, light and dark. */
132
+ :root {
133
+ --fallback-bg: var(--slds-neutral-100);
134
+ --fallback-bg-2: var(--slds-neutral-95);
135
+ --fallback-text: var(--slds-neutral-10);
136
+ --fallback-text-2: var(--slds-neutral-30);
137
+ --fallback-border: var(--slds-card-border);
138
+ --fallback-rule: var(--slds-neutral-90);
139
+ --link: var(--slds-link);
140
+ --link-hover: var(--slds-link-hover);
141
+ --series-other: var(--slds-neutral-50);
142
+ --area-mix: 16%;
143
+ color-scheme: light dark;
144
+ }
145
+ @media (prefers-color-scheme: dark) {
146
+ :root:not([data-theme="light"]) {
147
+ --fallback-bg: var(--slds-neutral-15);
148
+ --fallback-bg-2: var(--slds-neutral-20);
149
+ --fallback-text: var(--slds-neutral-95);
150
+ --fallback-text-2: var(--slds-neutral-70);
151
+ --fallback-border: var(--slds-neutral-30);
152
+ --fallback-rule: var(--slds-neutral-30);
153
+ --link: var(--slds-link-dark);
154
+ --link-hover: var(--slds-link-dark-hover);
155
+ --series-other: var(--slds-neutral-70);
156
+ --area-mix: 22%;
157
+ }
158
+ }
159
+ :root[data-theme="dark"] {
160
+ --fallback-bg: var(--slds-neutral-15);
161
+ --fallback-bg-2: var(--slds-neutral-20);
162
+ --fallback-text: var(--slds-neutral-95);
163
+ --fallback-text-2: var(--slds-neutral-70);
164
+ --fallback-border: var(--slds-neutral-30);
165
+ --fallback-rule: var(--slds-neutral-30);
166
+ --link: var(--slds-link-dark);
167
+ --link-hover: var(--slds-link-dark-hover);
168
+ --series-other: var(--slds-neutral-70);
169
+ --area-mix: 22%;
170
+ }
171
+ /* The chart palette: the SLDS data-visualization Default Palette, or the named palette the dashboard or the
172
+ component chose when it is one decision 0018 lists (the view sets data-palette on the root or the tile). */
173
+ :root {
174
+ --series-1: var(--slds-dataviz-1);
175
+ --series-2: var(--slds-dataviz-2);
176
+ --series-3: var(--slds-dataviz-3);
177
+ --series-4: var(--slds-dataviz-4);
178
+ --series-5: var(--slds-dataviz-5);
179
+ --series-6: var(--slds-dataviz-6);
180
+ }
181
+ [data-palette="bluegrass"] { --series-1: var(--slds-dataviz-bluegrass-1); --series-2: var(--slds-dataviz-bluegrass-2); --series-3: var(--slds-dataviz-bluegrass-3); --series-4: var(--slds-dataviz-bluegrass-4); --series-5: var(--slds-dataviz-bluegrass-5); --series-6: var(--slds-dataviz-bluegrass-6); }
182
+ [data-palette="sunrise"] { --series-1: var(--slds-dataviz-sunrise-1); --series-2: var(--slds-dataviz-sunrise-2); --series-3: var(--slds-dataviz-sunrise-3); --series-4: var(--slds-dataviz-sunrise-4); --series-5: var(--slds-dataviz-sunrise-5); --series-6: var(--slds-dataviz-sunrise-6); }
183
+ [data-palette="water"] { --series-1: var(--slds-dataviz-water-1); --series-2: var(--slds-dataviz-water-2); --series-3: var(--slds-dataviz-water-3); --series-4: var(--slds-dataviz-water-4); --series-5: var(--slds-dataviz-water-5); --series-6: var(--slds-dataviz-water-6); }
184
+ [data-palette="watermelon"] { --series-1: var(--slds-dataviz-watermelon-1); --series-2: var(--slds-dataviz-watermelon-2); --series-3: var(--slds-dataviz-watermelon-3); --series-4: var(--slds-dataviz-watermelon-4); --series-5: var(--slds-dataviz-watermelon-5); --series-6: var(--slds-dataviz-watermelon-6); }
185
+ :root {
186
+ --bg: var(--color-background-primary, var(--fallback-bg));
187
+ --bg-2: var(--color-background-secondary, var(--fallback-bg-2));
188
+ --text: var(--color-text-primary, var(--fallback-text));
189
+ --text-2: var(--color-text-secondary, var(--fallback-text-2));
190
+ --border: var(--color-border-primary, var(--fallback-border));
191
+ --rule: var(--color-border-primary, var(--fallback-rule));
192
+ /* SLDS shadow-outset-focus-1 (2px of the surface, then 2px of link blue), drawn in the theme's own colours. */
193
+ --focus-ring: 0 0 0 2px var(--bg), 0 0 0 4px var(--link);
194
+ --xs: var(--slds-font-size-2);
195
+ }
196
+ * { box-sizing: border-box; }
197
+ html, body { margin: 0; padding: 0; }
198
+ body {
199
+ background: var(--bg);
200
+ color: var(--text);
201
+ font-family: var(--slds-font-family);
202
+ font-size: var(--slds-font-size-3);
203
+ line-height: var(--slds-line-height-text);
204
+ }
205
+ a, button { border-radius: var(--slds-card-radius); }
206
+ a:focus-visible, button:focus-visible { outline: none; box-shadow: var(--focus-ring); }
207
+ .card { padding: var(--slds-spacing-medium); display: flex; flex-direction: column; gap: var(--slds-spacing-small); min-width: 0; }
208
+ .head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: var(--slds-spacing-xx-small) var(--slds-spacing-small); }
209
+ h1 {
210
+ margin: 0;
211
+ font-size: var(--slds-font-size-6);
212
+ line-height: var(--slds-line-height-heading);
213
+ font-weight: var(--slds-font-weight-bold);
214
+ overflow-wrap: anywhere;
215
+ }
216
+ h1 a, h2 a { color: var(--link); text-decoration: none; }
217
+ h1 a:hover, h2 a:hover { color: var(--link-hover); text-decoration: underline; }
218
+ .meta { color: var(--text-2); font-size: var(--xs); }
219
+ .chips { display: flex; flex-wrap: wrap; gap: var(--slds-spacing-xx-small); margin: 0; padding: 0; list-style: none; }
220
+ .chips li {
221
+ background: var(--bg);
222
+ border: 1px solid var(--border);
223
+ border-radius: var(--slds-card-radius);
224
+ padding: var(--slds-spacing-xxx-small) var(--slds-spacing-x-small);
225
+ font-size: var(--xs);
226
+ color: var(--text);
227
+ overflow-wrap: anywhere;
228
+ }
229
+ .empty { color: var(--text-2); }
230
+ .notes { margin: 0; padding-left: var(--slds-spacing-medium); color: var(--text-2); font-size: var(--xs); }
231
+ .actions { display: flex; flex-wrap: wrap; gap: var(--slds-spacing-x-small); }
232
+ /* An SLDS brand button: Open in Salesforce is the card's one action. */
233
+ button {
234
+ font: inherit;
235
+ font-size: var(--slds-font-size-3);
236
+ line-height: 1.875rem;
237
+ padding: 0 var(--slds-spacing-medium);
238
+ border-radius: var(--slds-card-radius);
239
+ border: 1px solid var(--slds-brand-accessible);
240
+ background: var(--slds-brand-accessible);
241
+ color: var(--slds-neutral-100);
242
+ cursor: pointer;
243
+ }
244
+ button:hover { background: var(--slds-brand-accessible-active); border-color: var(--slds-brand-accessible-active); }
245
+
246
+ /* The grid: tiles in the dashboard's layout order. Three across on a wide card, two at tablet width, one on a phone.
247
+ Each tile is an SLDS card: a 1px neutral border, the card radius and the card shadow. */
248
+ .tiles { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--slds-spacing-small); margin: 0; padding: 0; list-style: none; }
249
+ @media (min-width: 520px) { .tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
250
+ @media (min-width: 700px) { .tiles { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
251
+ .tile {
252
+ min-width: 0;
253
+ display: flex;
254
+ flex-direction: column;
255
+ gap: var(--slds-spacing-x-small);
256
+ padding: var(--slds-spacing-small);
257
+ border: 1px solid var(--border);
258
+ border-radius: var(--slds-card-radius);
259
+ background: var(--bg);
260
+ box-shadow: var(--slds-card-shadow);
261
+ }
262
+ .tile h2 {
263
+ margin: 0;
264
+ font-size: var(--slds-font-size-4);
265
+ line-height: var(--slds-line-height-heading);
266
+ font-weight: var(--slds-font-weight-bold);
267
+ overflow-wrap: anywhere;
268
+ }
269
+ .tile .sub { margin: calc(-1 * var(--slds-spacing-xx-small)) 0 0; color: var(--text-2); font-size: var(--xs); overflow-wrap: anywhere; }
270
+ .tile .body { min-width: 0; flex: 1 1 auto; display: flex; flex-direction: column; justify-content: flex-start; }
271
+ .tile[data-type="Metric"] .body { justify-content: center; }
272
+ .tile .foot { margin: 0; color: var(--text-2); font-size: var(--xs); overflow-wrap: anywhere; }
273
+ .nodata-text, .error-text { margin: 0; color: var(--text-2); font-size: var(--xs); }
274
+ .tile.nodata { background: var(--bg-2); border-style: dashed; box-shadow: none; }
275
+
276
+ /* A metric: one big number. */
277
+ .metric { display: flex; flex-direction: column; gap: var(--slds-spacing-xxx-small); padding: var(--slds-spacing-xx-small) 0; }
278
+ .metric-value {
279
+ font-size: var(--slds-font-size-9);
280
+ line-height: var(--slds-line-height-heading);
281
+ font-weight: var(--slds-font-weight-bold);
282
+ font-variant-numeric: tabular-nums;
283
+ overflow-wrap: anywhere;
284
+ animation: lift 600ms cubic-bezier(0.2, 0.7, 0.2, 1) both;
285
+ }
286
+ .metric-label { color: var(--text-2); font-size: var(--xs); overflow-wrap: anywhere; }
287
+
288
+ /* The charts: inline SVG drawn to the tile's width in pixels, so text never scales.
289
+ Marks take the data-visualization palette in order; text takes the text colours only. */
290
+ svg.chart { display: block; width: 100%; overflow: visible; }
291
+ svg.chart text { font-family: inherit; font-size: var(--xs); fill: var(--text-2); }
292
+ svg.chart text.value { fill: var(--text); font-weight: var(--slds-font-weight-bold); font-variant-numeric: tabular-nums; }
293
+ svg.chart text.big { fill: var(--text); font-size: var(--slds-font-size-6); font-weight: var(--slds-font-weight-bold); font-variant-numeric: tabular-nums; }
294
+ svg.chart line.base { stroke: var(--border); stroke-width: 1; }
295
+ .mark { fill: var(--series-1); }
296
+ .mark.other { fill: var(--series-other); }
297
+ rect.bar, rect.band { transform-box: fill-box; transform-origin: left center; animation: grow 700ms cubic-bezier(0.2, 0.7, 0.2, 1) both; }
298
+ rect.band { transform-origin: center center; }
299
+ rect.col { transform-box: fill-box; transform-origin: center bottom; animation: rise 700ms cubic-bezier(0.2, 0.7, 0.2, 1) both; }
300
+ path.line { fill: none; stroke: var(--series-1); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; stroke-dasharray: 1; animation: draw 900ms cubic-bezier(0.2, 0.7, 0.2, 1) both; }
301
+ path.area { fill: color-mix(in srgb, var(--series-1) var(--area-mix), transparent); animation: fade 900ms ease-out both; }
302
+ circle.dot { fill: var(--series-1); stroke: var(--bg); stroke-width: 2; }
303
+ circle.hit { fill: transparent; }
304
+ g.ring { transform-box: fill-box; transform-origin: center; animation: spin 800ms cubic-bezier(0.2, 0.7, 0.2, 1) both; }
305
+ circle.slice { fill: none; stroke-width: 18; }
306
+ path.arc { fill: none; stroke-width: 14; }
307
+ g.needle { animation: sweep 900ms cubic-bezier(0.2, 0.7, 0.2, 1) both; }
308
+ g.needle line { stroke: var(--text); stroke-width: 3; stroke-linecap: round; }
309
+ g.needle circle { fill: var(--text); }
310
+ @keyframes grow { from { transform: scaleX(0); } }
311
+ @keyframes rise { from { transform: scaleY(0); } }
312
+ @keyframes draw { from { stroke-dashoffset: 1; } }
313
+ @keyframes fade { from { opacity: 0; } }
314
+ @keyframes spin { from { transform: rotate(-90deg) scale(0.7); opacity: 0; } }
315
+ @keyframes sweep { from { transform: rotate(var(--sweep, 0deg)); } }
316
+ @keyframes lift { from { transform: translateY(6px); opacity: 0; } }
317
+ /* Redrawn at a new width, a chart does not grow in again. */
318
+ .still *, .still { animation: none !important; }
319
+ @media (prefers-reduced-motion: reduce) {
320
+ rect.bar, rect.band, rect.col, path.line, path.area, g.ring, g.needle, .metric-value { animation: none; }
321
+ }
322
+
323
+ /* The donut's legend: a swatch beside each label, the share in text. */
324
+ .legend { display: flex; flex-direction: column; gap: var(--slds-spacing-xxx-small); margin: var(--slds-spacing-x-small) 0 0; padding: 0; list-style: none; font-size: var(--xs); }
325
+ .legend li { display: flex; align-items: center; gap: var(--slds-spacing-xx-small); min-width: 0; }
326
+ .legend .swatch { flex: none; width: 10px; height: 10px; border-radius: var(--slds-radius-small); }
327
+ .legend .name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
328
+ .legend .share { flex: none; color: var(--text-2); font-variant-numeric: tabular-nums; }
329
+
330
+ /* A table component, and the fallback for any type the card does not draw. */
331
+ .scroll { overflow-x: auto; }
332
+ /* A fixed layout shares the tile's width between the columns, so a long label wraps instead of pushing the numbers out. */
333
+ table { border-collapse: collapse; width: 100%; table-layout: fixed; font-size: var(--xs); font-variant-numeric: tabular-nums; }
334
+ table.lead col:first-child { width: 55%; }
335
+ th, td { padding: var(--slds-spacing-xx-small); text-align: left; border-bottom: 1px solid var(--rule); overflow-wrap: break-word; }
336
+ th { color: var(--text-2); font-weight: var(--slds-font-weight-bold); }
337
+ td.num, th.num { text-align: right; }
338
+ tbody tr:last-child td { border-bottom: none; }
339
+ td.blank { color: var(--text-2); }
340
+ .more { margin: var(--slds-spacing-xx-small) 0 0; color: var(--text-2); font-size: var(--xs); }
341
+ </style>
342
+ </head>
343
+ <body>
344
+ <main class="card" id="root"><p class="empty">Waiting for the dashboard.</p></main>
345
+ <script type="module">
346
+ /*@ext-apps-bundle*/
347
+
348
+ // The dashboard view. It draws one DashboardResult (structuredContent of run_dashboard or refresh_dashboard):
349
+ // a header with the filters and the refresh time, then every component as a tile in layout order.
350
+ // Every value from the result goes in through textContent, never innerHTML.
351
+ const { App, applyDocumentTheme, applyHostStyleVariables, applyHostFonts } = ExtApps;
352
+
353
+ const root = document.getElementById("root");
354
+ const app = new App({ name: "salesforce-dashboard-view", version: "1.0.0" }, {}, { autoResize: true });
355
+
356
+ function el(tag, attrs, children) {
357
+ const node = document.createElement(tag);
358
+ for (const [key, value] of Object.entries(attrs || {})) {
359
+ if (key === "text") node.textContent = value;
360
+ else if (key === "class") node.className = value;
361
+ else node.setAttribute(key, value);
362
+ }
363
+ for (const child of children || []) if (child) node.appendChild(child);
364
+ return node;
365
+ }
366
+
367
+ const SVG = "http://www.w3.org/2000/svg";
368
+ function svgEl(tag, attrs, children) {
369
+ const node = document.createElementNS(SVG, tag);
370
+ for (const [key, value] of Object.entries(attrs || {})) {
371
+ if (key === "text") node.textContent = value;
372
+ else node.setAttribute(key, String(value));
373
+ }
374
+ for (const child of children || []) if (child) node.appendChild(child);
375
+ return node;
376
+ }
377
+
378
+ const NUMERIC = new Set(["currency", "double", "int", "percent", "number"]);
379
+ const BLANK = "-";
380
+ // The same formats as the report card (decision 0015): currency as US dollars, dates in UTC, a blank as "-".
381
+ const numberFormat = new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 });
382
+ const moneyWhole = new Intl.NumberFormat(undefined, { style: "currency", currency: "USD", minimumFractionDigits: 0, maximumFractionDigits: 0 });
383
+ const moneyCents = new Intl.NumberFormat(undefined, { style: "currency", currency: "USD", minimumFractionDigits: 2, maximumFractionDigits: 2 });
384
+ const compactMoney = new Intl.NumberFormat(undefined, { style: "currency", currency: "USD", notation: "compact", maximumFractionDigits: 1 });
385
+ const compactNumber = new Intl.NumberFormat(undefined, { notation: "compact", maximumFractionDigits: 1 });
386
+ const shareFormat = new Intl.NumberFormat(undefined, { style: "percent", maximumFractionDigits: 0 });
387
+ const dateFormat = new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeZone: "UTC" });
388
+ const dateTimeFormat = new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" });
389
+
390
+ function isBlank(value) {
391
+ return value === null || value === undefined || value === "";
392
+ }
393
+
394
+ function cellText(value, dataType) {
395
+ if (isBlank(value)) return BLANK;
396
+ if (typeof value === "number") {
397
+ if (dataType === "currency") return (Number.isInteger(value) ? moneyWhole : moneyCents).format(value);
398
+ if (dataType === "percent") return numberFormat.format(value) + "%";
399
+ return numberFormat.format(value);
400
+ }
401
+ if (typeof value === "boolean") return value ? "Yes" : "No";
402
+ const day = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value));
403
+ if (day) return dateFormat.format(new Date(Date.UTC(Number(day[1]), Number(day[2]) - 1, Number(day[3]))));
404
+ return String(value);
405
+ }
406
+
407
+ function compactText(value, dataType) {
408
+ if (typeof value !== "number") return BLANK;
409
+ if (dataType === "currency") return compactMoney.format(value);
410
+ if (dataType === "percent") return numberFormat.format(value) + "%";
411
+ return compactNumber.format(value);
412
+ }
413
+
414
+ // Salesforce sends "2026-10-17T02:13:35.000+0000"; the offset needs a colon to parse everywhere.
415
+ function parseTime(value) {
416
+ if (typeof value !== "string") return null;
417
+ const time = new Date(value.replace(/([+-]\d{2})(\d{2})$/, "$1:$2"));
418
+ return Number.isNaN(time.getTime()) ? null : time;
419
+ }
420
+
421
+ // Only an https link is drawn or opened.
422
+ function safeUrl(value) {
423
+ return typeof value === "string" && /^https:\/\//i.test(value) ? value : null;
424
+ }
425
+
426
+ const round = (n) => Math.round(n * 100) / 100;
427
+
428
+ // Shorten a label with an ellipsis until it fits. The full label stays in the mark's title and the chart's desc.
429
+ function fitLabel(node, full, room) {
430
+ node.textContent = full;
431
+ if (node.getComputedTextLength() <= room) return;
432
+ for (let n = full.length - 1; n > 0; n--) {
433
+ node.textContent = full.slice(0, n).trimEnd() + "…";
434
+ if (node.getComputedTextLength() <= room) return;
435
+ }
436
+ node.textContent = "";
437
+ }
438
+
439
+ // ---- reading a component's numbers ----
440
+
441
+ // The component's measure: the one the server names (its first aggregate), else the first group measure,
442
+ // else the first total. RowCount is a whole number.
443
+ function measureOf(component) {
444
+ const result = component.result || {};
445
+ const named = component.measure;
446
+ const groups = result.groups;
447
+ if (named && named.name) {
448
+ const found = groups && groups.measures && groups.measures.find((m) => m.name === named.name);
449
+ return found || named;
450
+ }
451
+ if (groups && groups.measures && groups.measures.length) return groups.measures[0];
452
+ const first = result.totals && result.totals.measures && result.totals.measures[0];
453
+ if (!first) return null;
454
+ return { name: first, label: first === "RowCount" ? "Record Count" : first, dataType: first === "RowCount" ? "int" : "number" };
455
+ }
456
+
457
+ // A sum or a count adds up across groups; an average, a minimum or a maximum does not.
458
+ function additive(measure) {
459
+ return measure.name === "RowCount" || measure.name.startsWith("s!");
460
+ }
461
+
462
+ function totalOf(result, measure) {
463
+ const totals = result && result.totals;
464
+ if (!totals || !totals.measures || !totals.values) return null;
465
+ let index = measure ? totals.measures.indexOf(measure.name) : -1;
466
+ if (index < 0) index = 0;
467
+ const value = totals.values[index];
468
+ return typeof value === "number" ? value : null;
469
+ }
470
+
471
+ // The top-level groups with their value of the measure, in report order. Leaf rows are summed by the first
472
+ // grouping, so this needs a measure that adds up, or a single grouping where the rows are the groups.
473
+ // With largestFirst the groups are sorted by value, largest first. Past `keep` groups, the rest are summed into one Other; a measure that does not add up draws nothing then.
474
+ function seriesOf(component, measure, keep, largestFirst) {
475
+ const groups = component.result && component.result.groups;
476
+ if (!groups || !groups.rows || !groups.rows.length || !groups.dimensions || !groups.dimensions.length || !measure) return null;
477
+ const index = groups.measures.findIndex((m) => m.name === measure.name);
478
+ if (index < 0) return null;
479
+ const column = groups.dimensions.length + index;
480
+ const sums = additive(measure);
481
+ if (!sums && groups.dimensions.length !== 1) return null;
482
+ const order = [];
483
+ const values = new Map();
484
+ for (const row of groups.rows) {
485
+ const key = cellText(row[0], "string");
486
+ if (!values.has(key)) {
487
+ order.push(key);
488
+ values.set(key, null);
489
+ }
490
+ const value = row[column];
491
+ if (typeof value === "number") values.set(key, sums ? (values.get(key) || 0) + value : value);
492
+ }
493
+ const all = order.map((label) => ({ label, value: values.get(label) }));
494
+ if (largestFirst) all.sort((a, b) => num(b.value) - num(a.value));
495
+ const series = { dimension: groups.dimensions[0].label, points: all };
496
+ if (all.length <= keep) return series;
497
+ if (!sums) return null;
498
+ const rest = all.slice(keep - 1);
499
+ const other = rest.reduce((a, g) => a + (typeof g.value === "number" ? g.value : 0), 0);
500
+ series.points = [...all.slice(0, keep - 1), { label: "Other (" + rest.length + ")", value: other, other: true }];
501
+ return series;
502
+ }
503
+
504
+ const num = (v) => (typeof v === "number" ? v : 0);
505
+
506
+ let chartId = 0;
507
+ // An SVG chart with role img, a title that names it and a desc that says every value in words.
508
+ function chartSvg(name, desc, width, height) {
509
+ const id = "c" + ++chartId;
510
+ return svgEl("svg", { class: "chart", role: "img", "aria-labelledby": id + "-t " + id + "-d", width, height, viewBox: "0 0 " + width + " " + height }, [
511
+ svgEl("title", { id: id + "-t", text: name }),
512
+ svgEl("desc", { id: id + "-d", text: desc }),
513
+ ]);
514
+ }
515
+
516
+ function describeSeries(kind, measure, series) {
517
+ return kind + " of " + measure.label + " by " + series.dimension + ". " + series.points.map((p) => p.label + ": " + cellText(p.value, measure.dataType) + ".").join(" ");
518
+ }
519
+
520
+ // ---- the charts: each takes the tile body's width in pixels and returns a node, or null to fall back ----
521
+
522
+ function metric(component, measure) {
523
+ const value = totalOf(component.result, measure);
524
+ if (value === null) return null;
525
+ return el("div", { class: "metric" }, [
526
+ el("span", { class: "metric-value", text: cellText(value, measure && measure.dataType) }),
527
+ el("span", { class: "metric-label", text: component.metricLabel || (measure ? measure.label : "") }),
528
+ ]);
529
+ }
530
+
531
+ function gauge(component, measure, name, width) {
532
+ const range = component.gauge;
533
+ const value = totalOf(component.result, measure);
534
+ if (!range || value === null || !(range.max > range.min)) return metric(component, measure);
535
+ const type = measure && measure.dataType;
536
+ const r = Math.max(40, Math.min((width - 48) / 2, 92));
537
+ const top = 24;
538
+ const cx = width / 2;
539
+ const cy = top + r;
540
+ const height = cy + 40;
541
+ const at = (v) => Math.max(0, Math.min(1, (v - range.min) / (range.max - range.min)));
542
+ const point = (t, radius) => [round(cx + radius * Math.cos(Math.PI * (1 - t))), round(cy - radius * Math.sin(Math.PI * (1 - t)))];
543
+ const stops = [range.min, ...range.breakpoints, range.max];
544
+ // The dashboard's own band colours when it sets them, else the SLDS red, orange and green (fills only).
545
+ const defaults = ["var(--slds-red-60)", "var(--slds-orange-65)", "var(--slds-green-60)"];
546
+ const colors = range.colors && range.colors.length === stops.length - 1 ? range.colors : stops.slice(1).map((_, i) => defaults[Math.min(i, 2)]);
547
+ const above = value > range.max ? " It is above the range." : value < range.min ? " It is below the range." : "";
548
+ const svg = chartSvg(name, "Gauge of " + (measure ? measure.label : "the value") + ": " + cellText(value, type) + ", on a range from " + cellText(range.min, type) + " to " + cellText(range.max, type) +
549
+ (range.breakpoints.length ? ", with breakpoints at " + range.breakpoints.map((b) => cellText(b, type)).join(" and ") : "") + "." + above, width, height);
550
+ for (let i = 0; i < stops.length - 1; i++) {
551
+ const [x0, y0] = point(at(stops[i]), r);
552
+ const [x1, y1] = point(at(stops[i + 1]), r);
553
+ const arc = svgEl("path", { class: "arc", d: "M" + x0 + " " + y0 + " A" + r + " " + r + " 0 0 1 " + x1 + " " + y1 }, [
554
+ svgEl("title", { text: cellText(stops[i], type) + " to " + cellText(stops[i + 1], type) }),
555
+ ]);
556
+ arc.style.setProperty("stroke", colors[i]);
557
+ svg.appendChild(arc);
558
+ }
559
+ // The breakpoints, outside the arc.
560
+ for (const b of range.breakpoints) {
561
+ const t = at(b);
562
+ const [x, y] = point(t, r + 12);
563
+ svg.appendChild(svgEl("text", { x, y: round(y + 3), "text-anchor": t < 0.4 ? "end" : t > 0.6 ? "start" : "middle", text: compactText(b, type) }));
564
+ }
565
+ svg.appendChild(svgEl("text", { x: round(cx - r), y: cy + 16, "text-anchor": "middle", text: compactText(range.min, type) }));
566
+ svg.appendChild(svgEl("text", { x: round(cx + r), y: cy + 16, "text-anchor": "middle", text: compactText(range.max, type) }));
567
+ const t = at(value);
568
+ const [nx, ny] = point(t, r - 12);
569
+ const needle = svgEl("g", { class: "needle" }, [
570
+ svgEl("line", { x1: round(cx), y1: cy, x2: nx, y2: ny }),
571
+ svgEl("circle", { cx: round(cx), cy, r: 5 }),
572
+ ]);
573
+ needle.style.setProperty("transform-origin", round(cx) + "px " + cy + "px");
574
+ needle.style.setProperty("--sweep", round(-180 * t) + "deg");
575
+ svg.appendChild(needle);
576
+ svg.appendChild(svgEl("text", { class: "big", x: round(cx), y: cy + 34, "text-anchor": "middle", text: cellText(value, type) }));
577
+ return svg;
578
+ }
579
+
580
+ const SLICES = 6;
581
+ function donut(component, measure, name, width) {
582
+ // Largest first, so the named slices are the big ones and Other holds the tail.
583
+ const series = seriesOf(component, measure, SLICES, true);
584
+ if (!series || !additive(measure)) return null;
585
+ const points = series.points.filter((p) => num(p.value) > 0);
586
+ const total = points.reduce((a, p) => a + p.value, 0);
587
+ if (!points.length || total <= 0) return null;
588
+ const size = Math.min(width, 148);
589
+ const r = size / 2 - 12;
590
+ const c = size / 2;
591
+ const svg = chartSvg(name, describeSeries("Donut chart", measure, { dimension: series.dimension, points }), width, size);
592
+ const ring = svgEl("g", { class: "ring" });
593
+ // Each slice is an arc of one circle, drawn with a dash; a 1.5 point gap keeps neighbours apart.
594
+ const gap = points.length > 1 ? 1.5 : 0;
595
+ let start = 0;
596
+ const legend = el("ul", { class: "legend" });
597
+ points.forEach((p, i) => {
598
+ const share = (p.value / total) * 100;
599
+ // Each category takes the next palette colour, in order.
600
+ const color = p.other ? "var(--series-other)" : "var(--series-" + ((i % 6) + 1) + ")";
601
+ const slice = svgEl("circle", {
602
+ class: "slice",
603
+ cx: round(width / 2), cy: c, r: round(r),
604
+ pathLength: 100,
605
+ "stroke-dasharray": round(Math.max(0.1, share - gap)) + " " + round(100 - Math.max(0.1, share - gap)),
606
+ "stroke-dashoffset": round(25 - start),
607
+ }, [svgEl("title", { text: p.label + ": " + cellText(p.value, measure.dataType) + " (" + shareFormat.format(p.value / total) + ")" })]);
608
+ slice.style.setProperty("stroke", color);
609
+ ring.appendChild(slice);
610
+ start += share;
611
+ const swatch = el("span", { class: "swatch", "aria-hidden": "true" });
612
+ swatch.style.setProperty("background", color);
613
+ legend.appendChild(el("li", { title: p.label }, [swatch, el("span", { class: "name", text: p.label }), el("span", { class: "share", text: shareFormat.format(p.value / total) })]));
614
+ });
615
+ svg.appendChild(ring);
616
+ svg.appendChild(svgEl("text", { class: "big", x: round(width / 2), y: c + 2, "text-anchor": "middle", "dominant-baseline": "middle", text: compactText(total, measure.dataType) }));
617
+ return el("div", {}, [svg, legend]);
618
+ }
619
+
620
+ const BAR_ROWS = 10;
621
+ // Horizontal bars (Bar) or centred bands (Funnel): a label on the left, the mark, and the compact value on the right.
622
+ function rows(component, measure, name, width, funnel) {
623
+ const series = seriesOf(component, measure, BAR_ROWS);
624
+ if (!series) return null;
625
+ const points = series.points;
626
+ const max = Math.max(0, ...points.map((p) => num(p.value)));
627
+ if (max <= 0) return null;
628
+ const rowH = 22;
629
+ const height = points.length * rowH;
630
+ const labelW = Math.round(width * 0.36);
631
+ const valueW = 52;
632
+ const x0 = labelW + 6;
633
+ const room = Math.max(10, width - x0 - valueW - 4);
634
+ const svg = chartSvg(name, describeSeries(funnel ? "Funnel" : "Bar chart", measure, series), width, height);
635
+ points.forEach((p, i) => {
636
+ const y = i * rowH;
637
+ const w = round((room * Math.max(0, num(p.value))) / max);
638
+ const x = funnel ? round(x0 + (room - w) / 2) : x0;
639
+ const mark = svgEl("rect", { class: (funnel ? "band mark" : "bar mark") + (p.other ? " other" : ""), x, y: y + 4, width: Math.max(w, 0.5), height: rowH - 8, rx: 2 }, [
640
+ svgEl("title", { text: p.label + ": " + cellText(p.value, measure.dataType) }),
641
+ ]);
642
+ // A funnel's stages take the palette in order; a bar chart is one measure, so one colour.
643
+ if (funnel && !p.other) mark.style.setProperty("fill", "var(--series-" + ((i % 6) + 1) + ")");
644
+ svg.appendChild(mark);
645
+ const label = svgEl("text", { class: "label", x: 0, y: y + rowH / 2 + 4, text: p.label });
646
+ svg.appendChild(label);
647
+ svg.appendChild(svgEl("text", { class: "value", x: width, y: y + rowH / 2 + 4, "text-anchor": "end", text: compactText(p.value, measure.dataType) }));
648
+ fits.push(() => fitLabel(label, p.label, labelW));
649
+ });
650
+ return svg;
651
+ }
652
+
653
+ const COLUMNS = 24;
654
+ function columns(component, measure, name, width) {
655
+ const series = seriesOf(component, measure, COLUMNS);
656
+ if (!series || series.points.length < 1) return null;
657
+ const points = series.points;
658
+ const max = Math.max(0, ...points.map((p) => num(p.value)));
659
+ if (max <= 0) return null;
660
+ const top = 18;
661
+ const plot = 104;
662
+ const base = top + plot;
663
+ const height = base + 22;
664
+ const slot = width / Math.max(points.length, 4);
665
+ const offset = (width - slot * points.length) / 2;
666
+ const svg = chartSvg(name, describeSeries("Column chart", measure, series), width, height);
667
+ svg.appendChild(svgEl("line", { class: "base", x1: 0, x2: width, y1: base - 0.5, y2: base - 0.5 }));
668
+ const every = points.length <= 6;
669
+ let tallest = 0;
670
+ points.forEach((p, i) => { if (num(p.value) > num(points[tallest].value)) tallest = i; });
671
+ points.forEach((p, i) => {
672
+ const center = offset + slot * (i + 0.5);
673
+ const h = round((plot * Math.max(0, num(p.value))) / max);
674
+ svg.appendChild(svgEl("rect", { class: "col mark" + (p.other ? " other" : ""), x: round(center - slot * 0.35), width: round(Math.max(1, slot * 0.7)), y: round(base - h), height: h, rx: Math.min(2, slot * 0.2) }, [
675
+ svgEl("title", { text: p.label + ": " + cellText(p.value, measure.dataType) }),
676
+ ]));
677
+ // Values only where they help: on every column when there are few, else on the tallest.
678
+ if (every || i === tallest) {
679
+ const anchor = every ? "middle" : center < width * 0.2 ? "start" : center > width * 0.8 ? "end" : "middle";
680
+ svg.appendChild(svgEl("text", { class: "value", x: round(center), y: round(base - h - 5), "text-anchor": anchor, text: compactText(p.value, measure.dataType) }));
681
+ }
682
+ // Labels under every column when there are few, else under the first and the last.
683
+ if (every || i === 0 || i === points.length - 1) {
684
+ const anchor = every ? "middle" : i === 0 ? "start" : "end";
685
+ const x = every ? center : i === 0 ? Math.max(0, center - slot / 2) : Math.min(width, center + slot / 2);
686
+ const label = svgEl("text", { class: "label", x: round(x), y: height - 6, "text-anchor": anchor, text: p.label });
687
+ svg.appendChild(label);
688
+ fits.push(() => fitLabel(label, p.label, every ? slot - 4 : width / 2 - 4));
689
+ }
690
+ });
691
+ return svg;
692
+ }
693
+
694
+ const POINTS = 60;
695
+ function line(component, measure, name, width) {
696
+ const series = seriesOf(component, measure, POINTS);
697
+ if (!series || series.points.length < 2 || series.points.some((p) => p.other)) return null;
698
+ const points = series.points;
699
+ const values = points.map((p) => num(p.value));
700
+ const max = Math.max(0, ...values);
701
+ const min = Math.min(0, ...values);
702
+ if (max <= min) return null;
703
+ const top = 18;
704
+ const plot = 104;
705
+ const base = top + plot;
706
+ const height = base + 22;
707
+ const pad = 6;
708
+ const x = (i) => round(pad + ((width - 2 * pad) * i) / (points.length - 1));
709
+ const y = (v) => round(top + (plot * (max - v)) / (max - min));
710
+ const svg = chartSvg(name, describeSeries("Line chart", measure, series), width, height);
711
+ svg.appendChild(svgEl("line", { class: "base", x1: 0, x2: width, y1: y(0) - 0.5, y2: y(0) - 0.5 }));
712
+ const d = values.map((v, i) => (i ? "L" : "M") + x(i) + " " + y(v)).join(" ");
713
+ svg.appendChild(svgEl("path", { class: "area", d: d + " L" + x(points.length - 1) + " " + y(0) + " L" + x(0) + " " + y(0) + " Z" }));
714
+ svg.appendChild(svgEl("path", { class: "line", d, pathLength: 1 }));
715
+ // A point for each value, as a hover target with its title; the largest and the last are labelled.
716
+ let peak = 0;
717
+ values.forEach((v, i) => { if (v > values[peak]) peak = i; });
718
+ points.forEach((p, i) => {
719
+ const labelled = i === peak || i === points.length - 1;
720
+ svg.appendChild(svgEl("circle", { class: labelled ? "dot" : "hit", cx: x(i), cy: y(values[i]), r: labelled ? 4 : 6 }, [
721
+ svgEl("title", { text: p.label + ": " + cellText(p.value, measure.dataType) }),
722
+ ]));
723
+ if (labelled) {
724
+ const anchor = x(i) < width * 0.2 ? "start" : x(i) > width * 0.8 ? "end" : "middle";
725
+ svg.appendChild(svgEl("text", { class: "value", x: x(i), y: round(y(values[i]) - 8), "text-anchor": anchor, text: compactText(p.value, measure.dataType) }));
726
+ }
727
+ });
728
+ const first = svgEl("text", { class: "label", x: 0, y: height - 6, text: points[0].label });
729
+ const last = svgEl("text", { class: "label", x: width, y: height - 6, "text-anchor": "end", text: points[points.length - 1].label });
730
+ svg.append(first, last);
731
+ fits.push(() => fitLabel(first, points[0].label, width / 2 - 4), () => fitLabel(last, points[points.length - 1].label, width / 2 - 4));
732
+ return svg;
733
+ }
734
+
735
+ const TABLE_ROWS = 10;
736
+ // A compact table: the groups (grouping labels then measures), else the detail rows, else the totals.
737
+ function compactTable(component) {
738
+ const result = component.result || {};
739
+ const groups = result.groups;
740
+ let headers;
741
+ let body;
742
+ if (groups && groups.rows && groups.rows.length) {
743
+ headers = [...groups.dimensions.map((d) => ({ label: d.label, dataType: "string" })), ...groups.measures.map((m) => ({ label: m.label, dataType: m.dataType, numeric: true }))];
744
+ body = groups.rows;
745
+ } else if (result.detail && result.detail.rows && result.detail.rows.length) {
746
+ const columns = result.detail.columns.slice(0, 4);
747
+ headers = columns.map((c) => ({ label: c.label, dataType: c.dataType, numeric: NUMERIC.has(c.dataType) }));
748
+ body = result.detail.rows.map((row) => row.slice(0, 4));
749
+ } else if (result.totals && result.totals.values && result.totals.values.length) {
750
+ const measure = measureOf(component);
751
+ headers = result.totals.measures.map((name) => ({ label: measure && measure.name === name ? measure.label : name === "RowCount" ? "Record Count" : name, dataType: measure && measure.name === name ? measure.dataType : "number", numeric: true }));
752
+ body = [result.totals.values];
753
+ } else {
754
+ return el("p", { class: "nodata-text", text: "This component returned no rows." });
755
+ }
756
+ const shown = body.slice(0, TABLE_ROWS);
757
+ // With one label column and one number, the label takes the wider share.
758
+ const lead = headers.length === 2 && !headers[0].numeric && headers[1].numeric;
759
+ const table = el("table", { class: lead ? "lead" : "" }, [
760
+ el("colgroup", {}, headers.map(() => el("col"))),
761
+ el("thead", {}, [el("tr", {}, headers.map((h) => el("th", { text: h.label, class: h.numeric ? "num" : "" })))]),
762
+ el("tbody", {}, shown.map((row) => el("tr", {}, row.map((value, i) => {
763
+ const type = headers[i] && headers[i].dataType;
764
+ const classes = [];
765
+ if (typeof value === "number" || (headers[i] && headers[i].numeric)) classes.push("num");
766
+ if (isBlank(value)) classes.push("blank");
767
+ return el("td", { text: cellText(value, type), class: classes.join(" ") });
768
+ })))),
769
+ ]);
770
+ const box = el("div", { class: "scroll" }, [table]);
771
+ if (body.length > shown.length) return el("div", {}, [box, el("p", { class: "more", text: (body.length - shown.length) + " more rows in the report" })]);
772
+ return box;
773
+ }
774
+
775
+ // ---- tiles ----
776
+
777
+ let fits = [];
778
+
779
+ function tileName(component) {
780
+ return component.header || component.title || component.visualizationType || "Component";
781
+ }
782
+
783
+ function drawChart(component, width) {
784
+ const measure = measureOf(component);
785
+ const name = tileName(component);
786
+ const type = String(component.visualizationType || "");
787
+ let node = null;
788
+ if (measure) {
789
+ if (type === "Metric") node = metric(component, measure);
790
+ else if (type === "Gauge") node = gauge(component, measure, name, width);
791
+ else if (type === "Donut" || type === "Pie") node = donut(component, measure, name, width);
792
+ else if (type === "Bar") node = rows(component, measure, name, width, false);
793
+ else if (type === "Funnel") node = rows(component, measure, name, width, true);
794
+ else if (type === "Column") node = columns(component, measure, name, width);
795
+ else if (type === "Line") node = line(component, measure, name, width);
796
+ }
797
+ return node || compactTable(component);
798
+ }
799
+
800
+ const drawn = [];
801
+
802
+ // The named Salesforce palettes the card can draw (decision 0018). Any other name, "unity" included, draws the default.
803
+ const PALETTES = new Set(["bluegrass", "sunrise", "water", "watermelon"]);
804
+ function paletteOf(holder) {
805
+ const name = holder && typeof holder.colorPalette === "string" ? holder.colorPalette.toLowerCase() : "";
806
+ return PALETTES.has(name) ? name : null;
807
+ }
808
+
809
+ function tile(component) {
810
+ const status = component.status;
811
+ const node = el("li", { class: "tile" + (status === "no data" ? " nodata" : status === "error" ? " failed" : "") });
812
+ node.dataset.type = component.visualizationType || "";
813
+ const palette = paletteOf(component);
814
+ if (palette) node.dataset.palette = palette;
815
+ const name = tileName(component);
816
+ const source = component.sourceReport || {};
817
+ const url = safeUrl(source.url) || safeUrl(component.result && component.result.report && component.result.report.url);
818
+ const heading = el("h2", {});
819
+ if (url) {
820
+ const link = el("a", { href: url, target: "_blank", rel: "noopener", title: "Open the source report in Salesforce", text: name });
821
+ link.addEventListener("click", (event) => {
822
+ event.preventDefault();
823
+ openLink(url);
824
+ });
825
+ heading.appendChild(link);
826
+ } else {
827
+ heading.textContent = name;
828
+ }
829
+ node.appendChild(heading);
830
+ if (component.header && component.title) node.appendChild(el("p", { class: "sub", text: component.title }));
831
+ const body = el("div", { class: "body" });
832
+ node.appendChild(body);
833
+ if (status === "no data") {
834
+ body.appendChild(el("p", { class: "nodata-text", text: "No data yet for these filters. Refresh the dashboard to fill it." }));
835
+ } else if (status === "error") {
836
+ const error = component.error || {};
837
+ body.appendChild(el("p", { class: "error-text", text: "Salesforce could not run this component" + (error.message ? ": " + error.message : ".") }));
838
+ } else {
839
+ drawn.push({ component, body });
840
+ }
841
+ if (component.footer) node.appendChild(el("p", { class: "foot", text: component.footer }));
842
+ return node;
843
+ }
844
+
845
+ // Draw every chart to its tile's width. A redraw at a new width keeps the chart still.
846
+ let widths = new Map();
847
+ function drawCharts(still) {
848
+ fits = [];
849
+ for (const item of drawn) {
850
+ const width = Math.floor(item.body.clientWidth);
851
+ if (!width || widths.get(item.body) === width) continue;
852
+ widths.set(item.body, width);
853
+ const chart = drawChart(item.component, width);
854
+ if (still) chart.classList.add("still");
855
+ item.body.replaceChildren(chart);
856
+ }
857
+ for (const fit of fits) fit();
858
+ }
859
+
860
+ function render(result) {
861
+ root.replaceChildren();
862
+ drawn.length = 0;
863
+ widths = new Map();
864
+ chartId = 0;
865
+ if (!result || !result.dashboard) {
866
+ root.appendChild(el("p", { class: "empty", text: "This result has no dashboard to show." }));
867
+ return;
868
+ }
869
+ const dashboard = result.dashboard;
870
+ const palette = paletteOf(result) || paletteOf(dashboard);
871
+ if (palette) document.documentElement.dataset.palette = palette;
872
+ else delete document.documentElement.dataset.palette;
873
+ const url = safeUrl(dashboard.url);
874
+ const title = el("h1", {});
875
+ if (url) {
876
+ const link = el("a", { href: url, target: "_blank", rel: "noopener", text: dashboard.name });
877
+ link.addEventListener("click", (event) => {
878
+ event.preventDefault();
879
+ openLink(url);
880
+ });
881
+ title.appendChild(link);
882
+ } else {
883
+ title.textContent = dashboard.name;
884
+ }
885
+ const time = parseTime(result.refreshedAt);
886
+ const refreshed = el("span", { class: "meta refreshed", text: time ? "Refreshed " + dateTimeFormat.format(time) : "Not refreshed for these filters" });
887
+ if (result.runningUser) refreshed.setAttribute("title", result.runningUser);
888
+ root.appendChild(el("div", { class: "head" }, [title, refreshed]));
889
+ if (result.filters && result.filters.length) {
890
+ root.appendChild(el("ul", { class: "chips", "aria-label": "Filters" }, result.filters.map((f) => el("li", { text: f }))));
891
+ }
892
+ const components = result.components || [];
893
+ if (components.length) root.appendChild(el("ul", { class: "tiles", "aria-label": "Components" }, components.map(tile)));
894
+ else root.appendChild(el("p", { class: "empty", text: "This dashboard has no components." }));
895
+ if (result.notes && result.notes.length) {
896
+ root.appendChild(el("ul", { class: "notes", "aria-label": "Notes" }, result.notes.map((n) => el("li", { text: n }))));
897
+ }
898
+ if (url) {
899
+ const open = el("button", { type: "button", text: "Open in Salesforce" });
900
+ open.addEventListener("click", () => openLink(url));
901
+ root.appendChild(el("div", { class: "actions" }, [open]));
902
+ }
903
+ drawCharts(false);
904
+ }
905
+
906
+ if (typeof ResizeObserver === "function") {
907
+ let last = 0;
908
+ new ResizeObserver(() => {
909
+ const width = root.clientWidth;
910
+ if (width === last) return;
911
+ const first = last === 0;
912
+ last = width;
913
+ if (!first) drawCharts(true);
914
+ }).observe(root);
915
+ }
916
+
917
+ async function openLink(url) {
918
+ try {
919
+ await app.openLink({ url });
920
+ } catch (error) {
921
+ // The host refused or does not support links; nothing else to do.
922
+ }
923
+ }
924
+
925
+ function applyContext(context) {
926
+ if (!context) return;
927
+ if (context.theme) applyDocumentTheme(context.theme);
928
+ if (context.styles && context.styles.variables) applyHostStyleVariables(context.styles.variables);
929
+ if (context.styles && context.styles.css && context.styles.css.fonts) applyHostFonts(context.styles.css.fonts);
930
+ }
931
+
932
+ // Handlers first, then connect, so the first tool result is not missed.
933
+ app.ontoolresult = (result) => {
934
+ if (result.isError) {
935
+ root.replaceChildren(el("p", { class: "empty", text: "The dashboard did not load. Claude has the error." }));
936
+ return;
937
+ }
938
+ render(result.structuredContent);
939
+ };
940
+ app.onhostcontextchanged = (context) => applyContext(context);
941
+
942
+ await app.connect();
943
+ applyContext(app.getHostContext());
944
+ </script>
945
+ </body>
946
+ </html>