@happyvertical/smrt-reports 0.57.6 → 0.57.7
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.
- package/AGENTS.md +10 -0
- package/dist/chunks/runtime-time-D7JZ1e4J.js +1052 -0
- package/dist/chunks/runtime-time-D7JZ1e4J.js.map +1 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +58 -591
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +28 -4
- package/dist/playground.js +1 -1
- package/dist/playground.js.map +1 -1
- package/dist/recipes.d.ts +21 -0
- package/dist/recipes.d.ts.map +1 -1
- package/dist/runtime-compiler.d.ts +7 -40
- package/dist/runtime-compiler.d.ts.map +1 -1
- package/dist/runtime-dimensions.d.ts +43 -0
- package/dist/runtime-dimensions.d.ts.map +1 -0
- package/dist/runtime-memory.d.ts +81 -0
- package/dist/runtime-memory.d.ts.map +1 -0
- package/dist/runtime-plan.d.ts +121 -0
- package/dist/runtime-plan.d.ts.map +1 -0
- package/dist/runtime-report.d.ts.map +1 -1
- package/dist/runtime-server.d.ts.map +1 -1
- package/dist/runtime-spec-hash.d.ts +11 -0
- package/dist/runtime-spec-hash.d.ts.map +1 -0
- package/dist/runtime-spec.d.ts +16 -4
- package/dist/runtime-spec.d.ts.map +1 -1
- package/dist/runtime-time.d.ts +41 -0
- package/dist/runtime-time.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +37 -12
- package/dist/spec.d.ts +25 -0
- package/dist/spec.d.ts.map +1 -0
- package/dist/spec.js +2 -0
- package/dist/state.js +1 -1
- package/dist/state.js.map +1 -1
- package/dist/svelte/ReportConfirmation.svelte +97 -24
- package/dist/svelte/ReportConfirmation.svelte.d.ts +20 -0
- package/dist/svelte/ReportConfirmation.svelte.d.ts.map +1 -1
- package/dist/svelte/ReportDetail.svelte +263 -12
- package/dist/svelte/ReportDetail.svelte.d.ts +10 -1
- package/dist/svelte/ReportDetail.svelte.d.ts.map +1 -1
- package/dist/svelte/ReportPicker.svelte +47 -14
- package/dist/svelte/ReportPicker.svelte.d.ts +3 -0
- package/dist/svelte/ReportPicker.svelte.d.ts.map +1 -1
- package/dist/svelte/ReportResult.svelte +216 -35
- package/dist/svelte/ReportResult.svelte.d.ts +8 -2
- package/dist/svelte/ReportResult.svelte.d.ts.map +1 -1
- package/dist/svelte/RuntimeReportWidgetOptions.svelte +9 -1
- package/dist/svelte/RuntimeReportWidgetOptions.svelte.d.ts.map +1 -1
- package/dist/svelte/SavedReportCard.svelte +242 -0
- package/dist/svelte/SavedReportCard.svelte.d.ts +30 -0
- package/dist/svelte/SavedReportCard.svelte.d.ts.map +1 -0
- package/dist/svelte/SavedReports.svelte +206 -33
- package/dist/svelte/SavedReports.svelte.d.ts +13 -0
- package/dist/svelte/SavedReports.svelte.d.ts.map +1 -1
- package/dist/svelte/SavedReportsWidget.svelte +59 -0
- package/dist/svelte/SavedReportsWidget.svelte.d.ts +19 -0
- package/dist/svelte/SavedReportsWidget.svelte.d.ts.map +1 -0
- package/dist/svelte/client.d.ts.map +1 -1
- package/dist/svelte/client.js +3 -0
- package/dist/svelte/describe.d.ts +94 -0
- package/dist/svelte/describe.d.ts.map +1 -0
- package/dist/svelte/describe.js +249 -0
- package/dist/svelte/index.d.ts +4 -1
- package/dist/svelte/index.d.ts.map +1 -1
- package/dist/svelte/index.js +4 -1
- package/dist/svelte/messages.d.ts +48 -0
- package/dist/svelte/messages.d.ts.map +1 -1
- package/dist/svelte/messages.js +59 -11
- package/dist/svelte/playground.d.ts +2 -2
- package/dist/svelte/playground.d.ts.map +1 -1
- package/dist/svelte/playground.js +1 -1
- package/dist/svelte/types.d.ts +2 -0
- package/dist/svelte/types.d.ts.map +1 -1
- package/package.json +13 -9
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { type ReportSourceInfo } from './describe.js';
|
|
1
2
|
import type { RuntimeReportsClient } from './types.js';
|
|
2
3
|
export interface Props {
|
|
3
4
|
client?: RuntimeReportsClient;
|
|
4
5
|
reportId?: string;
|
|
5
6
|
measure?: string;
|
|
6
7
|
dimension?: string;
|
|
8
|
+
/** The report sources by label and fields, for measure and grouping names. */
|
|
9
|
+
sources?: readonly ReportSourceInfo[];
|
|
7
10
|
onchange: (value: {
|
|
8
11
|
reportId: string;
|
|
9
12
|
measure: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReportPicker.svelte.d.ts","sourceRoot":"","sources":["../../src/svelte/ReportPicker.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ReportPicker.svelte.d.ts","sourceRoot":"","sources":["../../src/svelte/ReportPicker.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,EAAkB,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAGtE,OAAO,KAAK,EAAE,oBAAoB,EAAsB,MAAM,YAAY,CAAC;AAG3E,MAAM,WAAW,KAAK;IACpB,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8EAA8E;IAC9E,OAAO,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACtC,QAAQ,EAAE,CAAC,KAAK,EAAE;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;KACnB,KAAK,IAAI,CAAC;CACZ;AA2FD,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -1,69 +1,250 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* A report's result: one measure charted at a time (a measure switch when
|
|
4
|
+
* there are several), or the whole result as a table with formatted columns
|
|
5
|
+
* (money, "Jan 2025" buckets). `full` is a report's page: a Chart/Table
|
|
6
|
+
* switch, headline figures for the shown measure (total, average, peak) and a
|
|
7
|
+
* tall chart with a value axis. `compact` is the assistant's preview card: a
|
|
8
|
+
* quiet table toggle and a regular chart.
|
|
9
|
+
*/
|
|
10
|
+
import {
|
|
11
|
+
Chart,
|
|
12
|
+
formatTimeBucket,
|
|
13
|
+
type TimeBucketUnit,
|
|
14
|
+
} from '@happyvertical/smrt-ui/charts';
|
|
15
|
+
import { SegmentedControl, Select } from '@happyvertical/smrt-ui/forms';
|
|
3
16
|
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
4
17
|
import { Button } from '@happyvertical/smrt-ui/ui';
|
|
18
|
+
import {
|
|
19
|
+
measureColumns,
|
|
20
|
+
measureFormat,
|
|
21
|
+
reportFigures,
|
|
22
|
+
timeAxis,
|
|
23
|
+
} from './describe.js';
|
|
5
24
|
import { M } from './messages.js';
|
|
6
25
|
import { reportCell, validReportResult } from './result.js';
|
|
7
|
-
import type { RuntimeReportResult } from './types.js';
|
|
26
|
+
import type { RuntimeReportResult, RuntimeReportSpec } from './types.js';
|
|
27
|
+
|
|
28
|
+
type Column = RuntimeReportResult['columns'][number];
|
|
8
29
|
|
|
9
30
|
export interface Props {
|
|
10
31
|
result: RuntimeReportResult;
|
|
32
|
+
/** `compact` (default) for a preview card, `full` for a report's page. */
|
|
33
|
+
variant?: 'compact' | 'full';
|
|
34
|
+
/** The report's spec, so figures know which measures add up. */
|
|
35
|
+
spec?: RuntimeReportSpec;
|
|
36
|
+
/** Chart or table, bindable so the page's actions can follow it. */
|
|
37
|
+
view?: 'chart' | 'table';
|
|
11
38
|
locale?: string;
|
|
12
39
|
currency?: string;
|
|
13
40
|
}
|
|
14
|
-
let {
|
|
41
|
+
let {
|
|
42
|
+
result,
|
|
43
|
+
variant = 'compact',
|
|
44
|
+
spec,
|
|
45
|
+
view = $bindable('chart'),
|
|
46
|
+
locale = 'en',
|
|
47
|
+
currency = 'USD',
|
|
48
|
+
}: Props = $props();
|
|
15
49
|
const { t } = useI18n();
|
|
16
|
-
let table = $state(false);
|
|
17
50
|
const valid = $derived(validReportResult(result));
|
|
18
|
-
const measures = $derived(
|
|
51
|
+
const measures = $derived(valid ? measureColumns(result) : []);
|
|
52
|
+
let picked = $state<string | null>(null);
|
|
53
|
+
const measure = $derived(measures.find((c) => c.id === picked) ?? measures[0]);
|
|
54
|
+
const xColumn = $derived(
|
|
19
55
|
valid
|
|
20
|
-
? result.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
).includes(c.id),
|
|
25
|
-
)
|
|
26
|
-
: [],
|
|
56
|
+
? result.chart.x
|
|
57
|
+
? result.columns.find((c) => c.id === result.chart.x)
|
|
58
|
+
: result.columns.find((c) => c.role === 'dimension')
|
|
59
|
+
: undefined,
|
|
27
60
|
);
|
|
28
|
-
|
|
61
|
+
const axis = $derived(valid ? timeAxis(result) : undefined);
|
|
62
|
+
const tableOnly = $derived(valid && result.chart.type === 'table');
|
|
63
|
+
const showTable = $derived(tableOnly || view === 'table');
|
|
64
|
+
|
|
65
|
+
/** A time bucket reads by its granularity ("Jan 2025"), never as an ISO instant. */
|
|
66
|
+
function bucketLabel(value: unknown, column: Column | undefined) {
|
|
67
|
+
if (typeof value === 'string' && column?.bucket) {
|
|
68
|
+
return formatTimeBucket(value, column.bucket as TimeBucketUnit, locale);
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
function cell(value: unknown, column: Column) {
|
|
29
73
|
return typeof value === 'boolean'
|
|
30
74
|
? t(M[value ? 'reports.yes' : 'reports.no'])
|
|
31
|
-
:
|
|
75
|
+
: (bucketLabel(value, column) ??
|
|
76
|
+
reportCell(value, column, locale, currency));
|
|
32
77
|
}
|
|
78
|
+
const groupLabel = (row: RuntimeReportResult['rows'][number]) =>
|
|
79
|
+
xColumn ? cell(row[xColumn.id], xColumn) : (measure?.label ?? '');
|
|
80
|
+
const figures = $derived(
|
|
81
|
+
variant === 'full' && valid && measure
|
|
82
|
+
? reportFigures(result, measure, {
|
|
83
|
+
t,
|
|
84
|
+
locale,
|
|
85
|
+
currency,
|
|
86
|
+
groupLabel,
|
|
87
|
+
fn: spec?.measures.find((m) => m.as === measure.id)?.fn,
|
|
88
|
+
})
|
|
89
|
+
: [],
|
|
90
|
+
);
|
|
91
|
+
const chartData = $derived(
|
|
92
|
+
valid && measure
|
|
93
|
+
? {
|
|
94
|
+
points: result.rows
|
|
95
|
+
.filter((row) => typeof row[measure.id] === 'number')
|
|
96
|
+
.map((row) => ({
|
|
97
|
+
label: groupLabel(row),
|
|
98
|
+
value: row[measure.id] as number,
|
|
99
|
+
})),
|
|
100
|
+
format: measureFormat(measure),
|
|
101
|
+
currency,
|
|
102
|
+
}
|
|
103
|
+
: null,
|
|
104
|
+
);
|
|
105
|
+
/** "Total amount by month", "Number of invoices by Status". */
|
|
106
|
+
const seriesLabel = $derived(
|
|
107
|
+
measure
|
|
108
|
+
? axis?.bucket
|
|
109
|
+
? t(M['reports.dimension_bucket'], {
|
|
110
|
+
field: measure.label,
|
|
111
|
+
bucket: t(`reports.bucket_unit.${axis.bucket}`),
|
|
112
|
+
})
|
|
113
|
+
: xColumn
|
|
114
|
+
? t(M['reports.dimension_bucket'], {
|
|
115
|
+
field: measure.label,
|
|
116
|
+
bucket: xColumn.label.toLowerCase(),
|
|
117
|
+
})
|
|
118
|
+
: measure.label
|
|
119
|
+
: '',
|
|
120
|
+
);
|
|
121
|
+
const numeric = (column: Column) =>
|
|
122
|
+
column.type === 'integer' || column.type === 'decimal';
|
|
123
|
+
const measureOptions = $derived(
|
|
124
|
+
measures.map((c) => ({ value: c.id, label: c.label })),
|
|
125
|
+
);
|
|
33
126
|
</script>
|
|
34
127
|
|
|
35
|
-
<div data-smrt-component="ReportResult">
|
|
128
|
+
<div class="report-result" data-smrt-component="ReportResult" data-variant={variant}>
|
|
36
129
|
{#if !valid}
|
|
37
130
|
<p role="alert">{t(M['reports.invalid'])}</p>
|
|
38
131
|
{:else if result.rows.length === 0}
|
|
39
|
-
<p>{t(M['reports.no_rows'])}</p>
|
|
132
|
+
<p class="report-result__empty">{t(M['reports.no_rows'])}</p>
|
|
40
133
|
{:else}
|
|
41
|
-
{#if
|
|
42
|
-
<
|
|
134
|
+
{#if variant === 'full'}
|
|
135
|
+
<div class="report-result__controls">
|
|
136
|
+
{#if !tableOnly}
|
|
137
|
+
<SegmentedControl
|
|
138
|
+
label={t(M['reports.view'])}
|
|
139
|
+
options={[
|
|
140
|
+
{ value: 'chart', label: t(M['reports.view_chart']) },
|
|
141
|
+
{ value: 'table', label: t(M['reports.view_table']) },
|
|
142
|
+
]}
|
|
143
|
+
value={view}
|
|
144
|
+
onvaluechange={(value) => (view = value === 'table' ? 'table' : 'chart')}
|
|
145
|
+
/>
|
|
146
|
+
{/if}
|
|
147
|
+
{#if measures.length > 1 && !showTable}
|
|
148
|
+
{#if measures.length <= 3}
|
|
149
|
+
<SegmentedControl
|
|
150
|
+
label={t(M['reports.measure_pick'])}
|
|
151
|
+
options={measureOptions}
|
|
152
|
+
value={measure?.id}
|
|
153
|
+
onvaluechange={(value) => (picked = String(value))}
|
|
154
|
+
/>
|
|
155
|
+
{:else}
|
|
156
|
+
<Select
|
|
157
|
+
aria-label={t(M['reports.measure_pick'])}
|
|
158
|
+
value={measure?.id}
|
|
159
|
+
onchange={(event) => (picked = event.currentTarget.value)}
|
|
160
|
+
>
|
|
161
|
+
{#each measureOptions as option (option.value)}<option value={option.value}>{option.label}</option>{/each}
|
|
162
|
+
</Select>
|
|
163
|
+
{/if}
|
|
164
|
+
{/if}
|
|
165
|
+
</div>
|
|
166
|
+
{#if figures.length > 0 && !showTable}
|
|
167
|
+
<dl class="report-result__figures" aria-label={t(M['reports.figures'])}>
|
|
168
|
+
{#each figures as figure (figure.label)}
|
|
169
|
+
<div class="report-result__figure">
|
|
170
|
+
<dt>{figure.label}{#if figure.detail}<span class="report-result__figure-detail">{' · '}{figure.detail}</span>{/if}</dt>
|
|
171
|
+
<dd><span class="report-result__figure-value">{figure.value}</span></dd>
|
|
172
|
+
</div>
|
|
173
|
+
{/each}
|
|
174
|
+
</dl>
|
|
175
|
+
{/if}
|
|
176
|
+
{:else if !tableOnly}
|
|
177
|
+
<div class="report-result__toggle">
|
|
178
|
+
<Button type="button" variant="ghost" size="sm" aria-pressed={view === 'table'} onclick={() => (view = view === 'table' ? 'chart' : 'table')}>{t(M[view === 'table' ? 'reports.chart' : 'reports.table'])}</Button>
|
|
179
|
+
</div>
|
|
43
180
|
{/if}
|
|
44
|
-
{#if
|
|
45
|
-
<
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
181
|
+
{#if showTable}
|
|
182
|
+
<div class="report-result__table">
|
|
183
|
+
<table>
|
|
184
|
+
<caption>{result.title}</caption>
|
|
185
|
+
<thead>
|
|
186
|
+
<tr>
|
|
187
|
+
{#each result.columns as column (column.id)}
|
|
188
|
+
<th scope="col" class:report-result__num={numeric(column)}>{column.label}</th>
|
|
189
|
+
{/each}
|
|
190
|
+
</tr>
|
|
191
|
+
</thead>
|
|
192
|
+
<tbody>
|
|
193
|
+
{#each result.rows as row, i (i)}
|
|
194
|
+
<tr>
|
|
195
|
+
{#each result.columns as column (column.id)}
|
|
196
|
+
<td class:report-result__num={numeric(column)}>{cell(row[column.id], column)}</td>
|
|
197
|
+
{/each}
|
|
198
|
+
</tr>
|
|
199
|
+
{/each}
|
|
200
|
+
</tbody>
|
|
201
|
+
</table>
|
|
202
|
+
</div>
|
|
203
|
+
{:else if measure && chartData}
|
|
204
|
+
{#if variant === 'full' || measures.length > 1}
|
|
205
|
+
<p class="report-result__series">{seriesLabel}</p>
|
|
206
|
+
{/if}
|
|
207
|
+
<Chart
|
|
208
|
+
title={measure.label}
|
|
209
|
+
style={result.chart.type}
|
|
210
|
+
size={variant === 'full' ? 'tall' : 'regular'}
|
|
211
|
+
valueAxis={variant === 'full'}
|
|
212
|
+
readout={variant !== 'full'}
|
|
213
|
+
{locale}
|
|
214
|
+
data={chartData}
|
|
215
|
+
/>
|
|
216
|
+
{#if variant === 'compact' && measures.length > 1}
|
|
217
|
+
<p class="report-result__more">
|
|
218
|
+
{measures.slice(1).map((c) => c.label).join(', ')} · {t(M['reports.table']).toLowerCase()}
|
|
219
|
+
</p>
|
|
220
|
+
{/if}
|
|
57
221
|
{/if}
|
|
58
|
-
{#if result.truncated}<p>{t(M['reports.truncated'])}</p>{/if}
|
|
222
|
+
{#if result.truncated}<p class="report-result__empty">{t(M['reports.truncated'])}</p>{/if}
|
|
59
223
|
{/if}
|
|
60
224
|
</div>
|
|
61
225
|
|
|
62
226
|
<style>
|
|
63
227
|
@layer smrt.reset, smrt.structure, smrt.theme;
|
|
64
228
|
@layer smrt.theme {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
229
|
+
.report-result { display: grid; gap: var(--smrt-spacing-3); inline-size: 100%; min-inline-size: 0; }
|
|
230
|
+
.report-result[data-variant='full'] { gap: var(--smrt-spacing-4); }
|
|
231
|
+
.report-result__controls { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--smrt-spacing-2) var(--smrt-spacing-4); }
|
|
232
|
+
.report-result__toggle { display: flex; justify-content: flex-end; }
|
|
233
|
+
.report-result__empty, .report-result__more { margin: 0; color: var(--smrt-color-on-surface-variant); font-size: var(--smrt-typography-body-small-size); }
|
|
234
|
+
.report-result__series { margin: 0; color: var(--smrt-color-on-surface-variant); font: var(--smrt-typography-label-large-font); }
|
|
235
|
+
.report-result__figures { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 10rem), 1fr)); gap: var(--smrt-spacing-3); margin: 0; }
|
|
236
|
+
.report-result__figure { display: grid; gap: var(--smrt-spacing-1); padding: var(--smrt-spacing-3) var(--smrt-spacing-4); border: var(--smrt-hairline); border-radius: var(--smrt-radius-medium); background: var(--smrt-color-surface-container-low); }
|
|
237
|
+
.report-result__figure dt { color: var(--smrt-color-on-surface-variant); font-size: var(--smrt-typography-label-medium-size); }
|
|
238
|
+
.report-result__figure dd { display: grid; margin: 0; }
|
|
239
|
+
.report-result__figure-value { font: var(--smrt-typography-headline-small-font); font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
|
|
240
|
+
.report-result__figure-detail { color: var(--smrt-color-on-surface-variant); }
|
|
241
|
+
.report-result__table { overflow-x: auto; border: var(--smrt-hairline); border-radius: var(--smrt-radius-medium); }
|
|
242
|
+
table { border-collapse: collapse; inline-size: 100%; font-size: var(--smrt-typography-body-small-size); }
|
|
243
|
+
caption { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
|
|
244
|
+
th, td { padding: var(--smrt-spacing-2) var(--smrt-spacing-3); border-block-end: var(--smrt-hairline); text-align: start; white-space: nowrap; }
|
|
245
|
+
thead th { background: var(--smrt-surface-sunken); color: var(--smrt-color-on-surface-variant); font-weight: var(--smrt-typography-label-large-weight); }
|
|
246
|
+
tbody tr:last-child td { border-block-end: none; }
|
|
247
|
+
.report-result__num { text-align: end; font-variant-numeric: tabular-nums; }
|
|
248
|
+
.report-result[data-variant='compact'] th, .report-result[data-variant='compact'] td { padding: var(--smrt-spacing-1) var(--smrt-spacing-2); }
|
|
68
249
|
}
|
|
69
250
|
</style>
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
import type { RuntimeReportResult } from './types.js';
|
|
1
|
+
import type { RuntimeReportResult, RuntimeReportSpec } from './types.js';
|
|
2
2
|
export interface Props {
|
|
3
3
|
result: RuntimeReportResult;
|
|
4
|
+
/** `compact` (default) for a preview card, `full` for a report's page. */
|
|
5
|
+
variant?: 'compact' | 'full';
|
|
6
|
+
/** The report's spec, so figures know which measures add up. */
|
|
7
|
+
spec?: RuntimeReportSpec;
|
|
8
|
+
/** Chart or table, bindable so the page's actions can follow it. */
|
|
9
|
+
view?: 'chart' | 'table';
|
|
4
10
|
locale?: string;
|
|
5
11
|
currency?: string;
|
|
6
12
|
}
|
|
7
|
-
declare const ReportResult: import("svelte").Component<Props, {}, "">;
|
|
13
|
+
declare const ReportResult: import("svelte").Component<Props, {}, "view">;
|
|
8
14
|
type ReportResult = ReturnType<typeof ReportResult>;
|
|
9
15
|
export default ReportResult;
|
|
10
16
|
//# sourceMappingURL=ReportResult.svelte.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReportResult.svelte.d.ts","sourceRoot":"","sources":["../../src/svelte/ReportResult.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ReportResult.svelte.d.ts","sourceRoot":"","sources":["../../src/svelte/ReportResult.svelte.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAKzE,MAAM,WAAW,KAAK;IACpB,MAAM,EAAE,mBAAmB,CAAC;IAC5B,0EAA0E;IAC1E,OAAO,CAAC,EAAE,SAAS,GAAG,MAAM,CAAC;IAC7B,gEAAgE;IAChE,IAAI,CAAC,EAAE,iBAAiB,CAAC;IACzB,oEAAoE;IACpE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAoLD,QAAA,MAAM,YAAY,+CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -42,7 +42,7 @@ function save() {
|
|
|
42
42
|
}
|
|
43
43
|
</script>
|
|
44
44
|
|
|
45
|
-
<div data-smrt-component="RuntimeReportWidgetOptions">
|
|
45
|
+
<div class="report-widget-options" data-smrt-component="RuntimeReportWidgetOptions">
|
|
46
46
|
<ReportPicker
|
|
47
47
|
{client}
|
|
48
48
|
reportId={value.reportId}
|
|
@@ -56,3 +56,11 @@ function save() {
|
|
|
56
56
|
<Button type="button" variant="primary" onclick={save}>{t(M['reports.save'])}</Button>
|
|
57
57
|
</FormActionBar>
|
|
58
58
|
</div>
|
|
59
|
+
|
|
60
|
+
<style>
|
|
61
|
+
@layer smrt.reset, smrt.structure, smrt.theme;
|
|
62
|
+
@layer smrt.theme {
|
|
63
|
+
.report-widget-options { display: grid; gap: var(--smrt-spacing-4); }
|
|
64
|
+
.report-widget-options > p { margin: 0; color: var(--smrt-color-error); }
|
|
65
|
+
}
|
|
66
|
+
</style>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RuntimeReportWidgetOptions.svelte.d.ts","sourceRoot":"","sources":["../../src/svelte/RuntimeReportWidgetOptions.svelte.ts"],"names":[],"mappings":"AAaA,KAAK,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC,CAAC;AAChE,UAAU,KAAK;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AACD,MAAM,WAAW,KAAK;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,KAAK,EAAE,GAAG,IAAI,CAAC;IAC7C,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB;
|
|
1
|
+
{"version":3,"file":"RuntimeReportWidgetOptions.svelte.d.ts","sourceRoot":"","sources":["../../src/svelte/RuntimeReportWidgetOptions.svelte.ts"],"names":[],"mappings":"AAaA,KAAK,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC,CAAC;AAChE,UAAU,KAAK;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AACD,MAAM,WAAW,KAAK;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,KAAK,EAAE,GAAG,IAAI,CAAC;IAC7C,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB;AAkDD,QAAA,MAAM,0BAA0B,2CAAwC,CAAC;AACzE,KAAK,0BAA0B,GAAG,UAAU,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAChF,eAAe,0BAA0B,CAAC"}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* One saved report as a card: its title (the whole card opens it), a line
|
|
4
|
+
* saying what it covers, a headline figure from its latest run with a
|
|
5
|
+
* sparkline over time, and when it was saved and run. The same card is the
|
|
6
|
+
* Saved reports page's and the Reports section page's.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { MenuItem } from '@happyvertical/smrt-ui';
|
|
10
|
+
import {
|
|
11
|
+
Chart,
|
|
12
|
+
formatTimeBucket,
|
|
13
|
+
type TimeBucketUnit,
|
|
14
|
+
} from '@happyvertical/smrt-ui/charts';
|
|
15
|
+
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
16
|
+
import { Badge, Menu, Skeleton } from '@happyvertical/smrt-ui/ui';
|
|
17
|
+
import {
|
|
18
|
+
describeReport,
|
|
19
|
+
measureColumns,
|
|
20
|
+
measureFormat,
|
|
21
|
+
type ReportSourceInfo,
|
|
22
|
+
relativeTime,
|
|
23
|
+
reportFigures,
|
|
24
|
+
timeAxis,
|
|
25
|
+
} from './describe.js';
|
|
26
|
+
import { M } from './messages.js';
|
|
27
|
+
import { reportCell } from './result.js';
|
|
28
|
+
import type { RuntimeReportResult, SavedRuntimeReport } from './types.js';
|
|
29
|
+
|
|
30
|
+
export interface Props {
|
|
31
|
+
report: SavedRuntimeReport;
|
|
32
|
+
/** The report's page. */
|
|
33
|
+
href: string;
|
|
34
|
+
/** Its latest run; `undefined` while it runs, `null` when it could not run. */
|
|
35
|
+
result?: RuntimeReportResult | null;
|
|
36
|
+
/** The report's source by label and fields, for its description line. */
|
|
37
|
+
source?: ReportSourceInfo;
|
|
38
|
+
/** The secondary actions (Archive, Restore) of its "more" menu. */
|
|
39
|
+
actions?: MenuItem[];
|
|
40
|
+
/** Who saved it, as the host names them ("You"). */
|
|
41
|
+
author?: string;
|
|
42
|
+
/** Heading level of the title, under the surrounding page's heading. */
|
|
43
|
+
headingLevel?: 2 | 3;
|
|
44
|
+
locale?: string;
|
|
45
|
+
currency?: string;
|
|
46
|
+
}
|
|
47
|
+
let {
|
|
48
|
+
report,
|
|
49
|
+
href,
|
|
50
|
+
result,
|
|
51
|
+
source,
|
|
52
|
+
actions = [],
|
|
53
|
+
author,
|
|
54
|
+
headingLevel = 2,
|
|
55
|
+
locale = 'en',
|
|
56
|
+
currency = 'USD',
|
|
57
|
+
}: Props = $props();
|
|
58
|
+
const { t } = useI18n();
|
|
59
|
+
|
|
60
|
+
const description = $derived(
|
|
61
|
+
describeReport(report.spec, {
|
|
62
|
+
t,
|
|
63
|
+
locale,
|
|
64
|
+
currency,
|
|
65
|
+
source,
|
|
66
|
+
columns: result?.columns,
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
const measure = $derived(result ? measureColumns(result)[0] : undefined);
|
|
70
|
+
const axis = $derived(result ? timeAxis(result) : undefined);
|
|
71
|
+
const groupLabel = (row: RuntimeReportResult['rows'][number]) => {
|
|
72
|
+
const x = axis ?? result?.columns.find((c) => c.role === 'dimension');
|
|
73
|
+
const value = x ? row[x.id] : undefined;
|
|
74
|
+
if (typeof value === 'string' && axis?.bucket) {
|
|
75
|
+
return formatTimeBucket(value, axis.bucket as TimeBucketUnit, locale);
|
|
76
|
+
}
|
|
77
|
+
return x ? reportCell(value, x, locale, currency) : '';
|
|
78
|
+
};
|
|
79
|
+
const fn = $derived(
|
|
80
|
+
measure
|
|
81
|
+
? report.spec.measures.find((m) => m.as === measure.id)?.fn
|
|
82
|
+
: undefined,
|
|
83
|
+
);
|
|
84
|
+
/** The card's one figure: the total when it adds up, else the latest or peak. */
|
|
85
|
+
const figure = $derived.by(() => {
|
|
86
|
+
if (!result || !measure) return null;
|
|
87
|
+
const figures = reportFigures(result, measure, {
|
|
88
|
+
t,
|
|
89
|
+
locale,
|
|
90
|
+
currency,
|
|
91
|
+
groupLabel,
|
|
92
|
+
fn,
|
|
93
|
+
});
|
|
94
|
+
const total = figures.find((f) => f.label === t(M['reports.figure.total']));
|
|
95
|
+
if (figures.length === 1)
|
|
96
|
+
return { label: measure.label, value: figures[0].value };
|
|
97
|
+
// A total is the measure itself over the period.
|
|
98
|
+
if (total) return { label: measure.label, value: total.value };
|
|
99
|
+
const peak = figures.at(-1);
|
|
100
|
+
return peak
|
|
101
|
+
? {
|
|
102
|
+
label: `${measure.label} · ${t(M['reports.figure.peak']).toLowerCase()}`,
|
|
103
|
+
value: peak.value,
|
|
104
|
+
detail: peak.detail,
|
|
105
|
+
}
|
|
106
|
+
: null;
|
|
107
|
+
});
|
|
108
|
+
/** An ungrouped report's other measures, one line each ("Total amount $12,400"). */
|
|
109
|
+
const extra = $derived.by(() => {
|
|
110
|
+
if (!result || axis || result.columns.some((c) => c.role === 'dimension'))
|
|
111
|
+
return [];
|
|
112
|
+
const row = result.rows[0];
|
|
113
|
+
return row
|
|
114
|
+
? measureColumns(result)
|
|
115
|
+
.slice(1, 3)
|
|
116
|
+
.map((column) => ({
|
|
117
|
+
label: column.label,
|
|
118
|
+
value: reportCell(row[column.id], column, locale, currency),
|
|
119
|
+
}))
|
|
120
|
+
: [];
|
|
121
|
+
});
|
|
122
|
+
const series = $derived.by(() => {
|
|
123
|
+
if (!result || !measure || !axis) return null;
|
|
124
|
+
const points = result.rows
|
|
125
|
+
.filter((row) => typeof row[measure.id] === 'number')
|
|
126
|
+
.map((row) => ({
|
|
127
|
+
label: groupLabel(row),
|
|
128
|
+
value: row[measure.id] as number,
|
|
129
|
+
}));
|
|
130
|
+
return points.length > 1
|
|
131
|
+
? { points, format: measureFormat(measure), currency }
|
|
132
|
+
: null;
|
|
133
|
+
});
|
|
134
|
+
const groups = $derived(
|
|
135
|
+
result && !axis && result.columns.some((c) => c.role === 'dimension')
|
|
136
|
+
? result.rows.length
|
|
137
|
+
: 0,
|
|
138
|
+
);
|
|
139
|
+
const saved = $derived(
|
|
140
|
+
report.savedAt
|
|
141
|
+
? author
|
|
142
|
+
? t(M['reports.saved_by_at'], {
|
|
143
|
+
author,
|
|
144
|
+
time: relativeTime(report.savedAt, locale),
|
|
145
|
+
})
|
|
146
|
+
: t(M['reports.saved_at'], { time: relativeTime(report.savedAt, locale) })
|
|
147
|
+
: author
|
|
148
|
+
? t(M['reports.saved_by'], { author })
|
|
149
|
+
: '',
|
|
150
|
+
);
|
|
151
|
+
</script>
|
|
152
|
+
|
|
153
|
+
<article
|
|
154
|
+
class="saved-report-card"
|
|
155
|
+
data-smrt-component="SavedReportCard"
|
|
156
|
+
data-report-id={report.id}
|
|
157
|
+
data-archived={report.status === 'archived' ? '' : undefined}
|
|
158
|
+
>
|
|
159
|
+
<div class="saved-report-card__head">
|
|
160
|
+
<svelte:element this={`h${headingLevel}`} class="saved-report-card__title">
|
|
161
|
+
<a href={href}>{report.title}</a>
|
|
162
|
+
</svelte:element>
|
|
163
|
+
{#if report.status === 'archived'}<Badge size="sm">{t(M['reports.archived_badge'])}</Badge>{/if}
|
|
164
|
+
{#if actions.length}
|
|
165
|
+
<div class="saved-report-card__menu">
|
|
166
|
+
<Menu variant="icon" placement="bottom-end" triggerLabel={t(M['reports.more'], { title: report.title })} items={actions}>
|
|
167
|
+
{#snippet trigger()}<span aria-hidden="true">⋯</span>{/snippet}
|
|
168
|
+
</Menu>
|
|
169
|
+
</div>
|
|
170
|
+
{/if}
|
|
171
|
+
</div>
|
|
172
|
+
<p class="saved-report-card__line">{description.line}</p>
|
|
173
|
+
<div class="saved-report-card__figure">
|
|
174
|
+
{#if result === undefined}
|
|
175
|
+
<Skeleton variant="text" lines={2} label={t(M['reports.running'])} />
|
|
176
|
+
{:else if result === null}
|
|
177
|
+
<p class="saved-report-card__muted">{t(M['reports.run_failed'])}</p>
|
|
178
|
+
{:else if result.rows.length === 0}
|
|
179
|
+
<p class="saved-report-card__muted">{t(M['reports.no_rows'])}</p>
|
|
180
|
+
{:else if figure}
|
|
181
|
+
<p class="saved-report-card__label">{figure.label}</p>
|
|
182
|
+
<p class="saved-report-card__value">{figure.value}{#if figure.detail}<span class="saved-report-card__detail"> · {figure.detail}</span>{/if}</p>
|
|
183
|
+
{#if series && measure}
|
|
184
|
+
<div class="saved-report-card__spark">
|
|
185
|
+
<Chart data={series} style="line" size="compact" title={measure.label} {locale} />
|
|
186
|
+
</div>
|
|
187
|
+
{:else if extra.length}
|
|
188
|
+
<dl class="saved-report-card__extra">
|
|
189
|
+
{#each extra as item (item.label)}<div><dt>{item.label}</dt><dd>{item.value}</dd></div>{/each}
|
|
190
|
+
</dl>
|
|
191
|
+
{:else if groups > 1}
|
|
192
|
+
<p class="saved-report-card__muted">{t(M['reports.groups_count'], { count: groups })}</p>
|
|
193
|
+
{/if}
|
|
194
|
+
{/if}
|
|
195
|
+
</div>
|
|
196
|
+
<!-- Its figures are fresh from this run, so the card says only when it was
|
|
197
|
+
saved; the report's page also says when it last ran. -->
|
|
198
|
+
{#if saved}
|
|
199
|
+
<p class="saved-report-card__meta">{saved}</p>
|
|
200
|
+
{/if}
|
|
201
|
+
</article>
|
|
202
|
+
|
|
203
|
+
<style>
|
|
204
|
+
@layer smrt.reset, smrt.structure, smrt.theme;
|
|
205
|
+
@layer smrt.theme {
|
|
206
|
+
.saved-report-card {
|
|
207
|
+
position: relative;
|
|
208
|
+
display: grid;
|
|
209
|
+
grid-template-rows: auto auto 1fr auto;
|
|
210
|
+
gap: var(--smrt-spacing-2);
|
|
211
|
+
min-inline-size: 0;
|
|
212
|
+
padding: var(--smrt-spacing-4);
|
|
213
|
+
border: var(--smrt-hairline);
|
|
214
|
+
border-radius: var(--smrt-radius-large);
|
|
215
|
+
background: var(--smrt-color-surface-container-low);
|
|
216
|
+
color: var(--smrt-color-on-surface);
|
|
217
|
+
transition: border-color var(--smrt-duration-fast) var(--smrt-easing-standard), background-color var(--smrt-duration-fast) var(--smrt-easing-standard);
|
|
218
|
+
}
|
|
219
|
+
.saved-report-card:hover { background: var(--smrt-color-surface-container); border-color: var(--smrt-color-outline); }
|
|
220
|
+
.saved-report-card:focus-within:has(.saved-report-card__title a:focus-visible) { outline: var(--smrt-focus-ring-width) solid var(--smrt-focus-ring-color); outline-offset: var(--smrt-focus-ring-offset); }
|
|
221
|
+
.saved-report-card[data-archived] { background: var(--smrt-color-surface); }
|
|
222
|
+
.saved-report-card__head { display: flex; align-items: flex-start; gap: var(--smrt-spacing-2); min-inline-size: 0; }
|
|
223
|
+
.saved-report-card__title { flex: 1 1 auto; min-inline-size: 0; margin: 0; font: var(--smrt-typography-title-medium-font); overflow-wrap: anywhere; }
|
|
224
|
+
.saved-report-card__title a { color: inherit; text-decoration: none; outline: none; }
|
|
225
|
+
/* The whole card opens the report; the menu sits above the link. */
|
|
226
|
+
.saved-report-card__title a::after { content: ''; position: absolute; inset: 0; border-radius: inherit; }
|
|
227
|
+
.saved-report-card__menu { position: relative; z-index: 1; flex: none; margin-block: calc(var(--smrt-spacing-2) * -1); margin-inline-end: calc(var(--smrt-spacing-2) * -1); }
|
|
228
|
+
.saved-report-card__line { margin: 0; color: var(--smrt-color-on-surface-variant); font-size: var(--smrt-typography-body-small-size); overflow-wrap: anywhere; }
|
|
229
|
+
.saved-report-card__figure { display: grid; align-content: start; gap: var(--smrt-spacing-1); min-block-size: 4.5rem; }
|
|
230
|
+
.saved-report-card__label { margin: 0; color: var(--smrt-color-on-surface-variant); font-size: var(--smrt-typography-label-medium-size); }
|
|
231
|
+
.saved-report-card__value { margin: 0; font: var(--smrt-typography-headline-small-font); font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
|
|
232
|
+
.saved-report-card__detail { color: var(--smrt-color-on-surface-variant); font: var(--smrt-typography-body-small-font); }
|
|
233
|
+
.saved-report-card__spark { margin-block-start: var(--smrt-spacing-1); }
|
|
234
|
+
.saved-report-card__extra { display: grid; gap: var(--smrt-spacing-1); margin: 0; font-size: var(--smrt-typography-body-small-size); }
|
|
235
|
+
.saved-report-card__extra > div { display: flex; flex-wrap: wrap; gap: 0 var(--smrt-spacing-2); }
|
|
236
|
+
.saved-report-card__extra dt { color: var(--smrt-color-on-surface-variant); }
|
|
237
|
+
.saved-report-card__extra dd { margin: 0; font-variant-numeric: tabular-nums; }
|
|
238
|
+
.saved-report-card__muted { margin: 0; color: var(--smrt-color-on-surface-variant); font-size: var(--smrt-typography-body-small-size); }
|
|
239
|
+
.saved-report-card__meta { margin: 0; color: var(--smrt-color-on-surface-variant); font-size: var(--smrt-typography-label-medium-size); }
|
|
240
|
+
@media (prefers-reduced-motion: reduce) { .saved-report-card { transition: none; } }
|
|
241
|
+
}
|
|
242
|
+
</style>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One saved report as a card: its title (the whole card opens it), a line
|
|
3
|
+
* saying what it covers, a headline figure from its latest run with a
|
|
4
|
+
* sparkline over time, and when it was saved and run. The same card is the
|
|
5
|
+
* Saved reports page's and the Reports section page's.
|
|
6
|
+
*/
|
|
7
|
+
import type { MenuItem } from '@happyvertical/smrt-ui';
|
|
8
|
+
import { type ReportSourceInfo } from './describe.js';
|
|
9
|
+
import type { RuntimeReportResult, SavedRuntimeReport } from './types.js';
|
|
10
|
+
export interface Props {
|
|
11
|
+
report: SavedRuntimeReport;
|
|
12
|
+
/** The report's page. */
|
|
13
|
+
href: string;
|
|
14
|
+
/** Its latest run; `undefined` while it runs, `null` when it could not run. */
|
|
15
|
+
result?: RuntimeReportResult | null;
|
|
16
|
+
/** The report's source by label and fields, for its description line. */
|
|
17
|
+
source?: ReportSourceInfo;
|
|
18
|
+
/** The secondary actions (Archive, Restore) of its "more" menu. */
|
|
19
|
+
actions?: MenuItem[];
|
|
20
|
+
/** Who saved it, as the host names them ("You"). */
|
|
21
|
+
author?: string;
|
|
22
|
+
/** Heading level of the title, under the surrounding page's heading. */
|
|
23
|
+
headingLevel?: 2 | 3;
|
|
24
|
+
locale?: string;
|
|
25
|
+
currency?: string;
|
|
26
|
+
}
|
|
27
|
+
declare const SavedReportCard: import("svelte").Component<Props, {}, "">;
|
|
28
|
+
type SavedReportCard = ReturnType<typeof SavedReportCard>;
|
|
29
|
+
export default SavedReportCard;
|
|
30
|
+
//# sourceMappingURL=SavedReportCard.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SavedReportCard.svelte.d.ts","sourceRoot":"","sources":["../../src/svelte/SavedReportCard.svelte.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAQvD,OAAO,EAIL,KAAK,gBAAgB,EAItB,MAAM,eAAe,CAAC;AAGvB,OAAO,KAAK,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAG1E,MAAM,WAAW,KAAK;IACpB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,yBAAyB;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,+EAA+E;IAC/E,MAAM,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAAC;IACpC,yEAAyE;IACzE,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,mEAAmE;IACnE,OAAO,CAAC,EAAE,QAAQ,EAAE,CAAC;IACrB,oDAAoD;IACpD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,YAAY,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAuKD,QAAA,MAAM,eAAe,2CAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
|