@feedmepos/mf-report 5.27.54 → 5.28.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.
- package/dist/{App-C6xpf-Q0.js → App-BDhSFQX_.js} +45 -44
- package/dist/DateRangeSelect-CRsKMi0U.js +437 -0
- package/dist/{Default.vue_vue_type_script_setup_true_lang-BwPZXC_d.js → Default.vue_vue_type_script_setup_true_lang-WGOOX2b8.js} +12 -11
- package/dist/{DynamicWidget.vue_vue_type_script_setup_true_lang-BpgXiPBY.js → DynamicWidget.vue_vue_type_script_setup_true_lang-CQM-l5Rk.js} +182 -181
- package/dist/{GlobalFilterSelect.vue_vue_type_script_setup_true_lang-CVShUWJC.js → GlobalFilterSelect.vue_vue_type_script_setup_true_lang-CMioHpn1.js} +5091 -5717
- package/dist/{InsightView-BYW66Wrv.js → InsightView-lnOobZAr.js} +17 -15
- package/dist/{Integration-DQPJBE-0.js → Integration-D5ssimUg.js} +27 -25
- package/dist/{Integrations-BW4H9lUS.js → Integrations-CaqGvkzI.js} +8 -7
- package/dist/{Layout-CMlh08aB.js → Layout-DS0XCT_E.js} +1 -1
- package/dist/LocationSelect.vue_vue_type_script_setup_true_lang-DC9QYJk-.js +8055 -0
- package/dist/{MenuTab.vue_vue_type_script_setup_true_lang-BUvD1_b0.js → MenuTab.vue_vue_type_script_setup_true_lang-nOukNXp3.js} +11 -10
- package/dist/{NavigationTab.vue_vue_type_script_setup_true_lang-TpeQ_Wm1.js → NavigationTab.vue_vue_type_script_setup_true_lang-DXNbt08D.js} +9636 -17677
- package/dist/{OverviewView-BtGpQkBY.js → OverviewView-BkIozvMs.js} +5 -5
- package/dist/{Report-CJ9myNLK.js → Report-HiSGRNhn.js} +27 -25
- package/dist/ReportCanvasView-ILVhB1KL.js +448 -0
- package/dist/{ReportEditor-DtnTBbY1.js → ReportEditor-B4tyVB8s.js} +30 -28
- package/dist/{ReportView-Ci8nFyI9.js → ReportView-CIQQHvWT.js} +14 -13
- package/dist/ReportViewRoute-B3KwVB0v.js +18 -0
- package/dist/{SelectComponent.vue_vue_type_script_setup_true_lang-Ckn2p74A.js → SelectComponent.vue_vue_type_script_setup_true_lang-DW7Dk7iQ.js} +1 -1
- package/dist/{Setting-QSiMSAQk.js → Setting-CvToMUKq.js} +5 -5
- package/dist/{Template-CAPlAQO9.js → Template-BBQEaHrM.js} +10 -9
- package/dist/{TileEditor-e7ZCWMU9.js → TileEditor-Cqy98LmJ.js} +19 -17
- package/dist/{TilePageStack.vue_vue_type_script_setup_true_lang-B4g3YPzM.js → TilePageStack.vue_vue_type_script_setup_true_lang-CzggUdfI.js} +3 -3
- package/dist/{TransitionFade-DqIazHHG.js → TransitionFade-D-8BYs6T.js} +36 -33
- package/dist/{_plugin-vue_export-helper-kkiiQoKl.js → _plugin-vue_export-helper-C6QDExPN.js} +276 -291
- package/dist/{app-BAgs-zHf.js → app-Bjt_JuJi.js} +670 -567
- package/dist/app.js +1 -1
- package/dist/assets/{formatChartData.worker-epD1fsTW.js → formatChartData.worker-BeBGCyYW.js} +1 -1
- package/dist/assets/{processTableData.worker-BJZ_Z45H.js → processTableData.worker-frTYV0ok.js} +39 -39
- package/dist/global-loader-BuxFfehu.js +19 -0
- package/dist/index-CtZE4sab.js +82 -0
- package/dist/report-DjGDWvm3.js +635 -0
- package/dist/report-canvas-frame.html +888 -0
- package/dist/report-canvas-libs/chart.js-4.js +14 -0
- package/dist/report-canvas-libs/fm-ui-1.js +904 -0
- package/dist/style.css +1 -1
- package/dist/{useRestaurantPermission-ChYMJfXG.js → useRestaurantPermission-Dkbh_cv8.js} +4 -4
- package/package.json +1 -1
- package/dist/DateRangeSelect-DYVOvSCA.js +0 -517
|
@@ -0,0 +1,888 @@
|
|
|
1
|
+
<meta charset="utf-8" />
|
|
2
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
3
|
+
<!--
|
|
4
|
+
Runtime shell for AI-written report sections. Loaded into ONE iframe per report with
|
|
5
|
+
sandbox="allow-scripts" and no allow-same-origin, so this document runs on an opaque
|
|
6
|
+
origin and cannot reach the host. Section code needs inline+eval by design; the sandbox
|
|
7
|
+
attribute is the boundary, and this CSP's job is to keep the frame off the network.
|
|
8
|
+
-->
|
|
9
|
+
<meta
|
|
10
|
+
http-equiv="Content-Security-Policy"
|
|
11
|
+
content="default-src 'none'; script-src 'unsafe-inline' 'unsafe-eval'; style-src 'unsafe-inline'; img-src data: blob:; font-src data:; connect-src 'none'; form-action 'none'; base-uri 'none'"
|
|
12
|
+
/>
|
|
13
|
+
<title>Report</title>
|
|
14
|
+
<style>
|
|
15
|
+
/* Design tokens, from the B2B_POS working file's Report page. A report reads these;
|
|
16
|
+
a merchant asking for a different look is one redefinition here, not a rewrite. */
|
|
17
|
+
:root {
|
|
18
|
+
color-scheme: light;
|
|
19
|
+
--fm-fg: #1c1c1e;
|
|
20
|
+
--fm-body: #2e2e2e;
|
|
21
|
+
--fm-muted: #6b6c70;
|
|
22
|
+
--fm-faint: #aeaeb2;
|
|
23
|
+
--fm-line: #f0f0f0;
|
|
24
|
+
--fm-card: #ffffff;
|
|
25
|
+
--fm-page: #f3f3f6;
|
|
26
|
+
--fm-signature: #ff7823;
|
|
27
|
+
--fm-signature-tint: #fff3e6;
|
|
28
|
+
--fm-signature-dark: #6b3100;
|
|
29
|
+
--fm-danger: #99231d;
|
|
30
|
+
--fm-danger-bg: #ffebea;
|
|
31
|
+
--fm-aqua-500: #0a5384;
|
|
32
|
+
--fm-aqua-300: #3ab0ff;
|
|
33
|
+
--fm-aqua-100: #b7e2ff;
|
|
34
|
+
--fm-purple-500: #512191;
|
|
35
|
+
--fm-purple-300: #8f61e8;
|
|
36
|
+
--fm-purple-100: #eddfff;
|
|
37
|
+
--fm-green-500: #306a01;
|
|
38
|
+
--fm-green-300: #aeef4b;
|
|
39
|
+
--fm-green-100: #f4fde6;
|
|
40
|
+
--fm-plum-500: #8c146a;
|
|
41
|
+
--fm-plum-300: #ffc4c3;
|
|
42
|
+
--fm-plum-100: #fff7fc;
|
|
43
|
+
--fm-mustard-500: #903e1f;
|
|
44
|
+
--fm-mustard-300: #ffc746;
|
|
45
|
+
--fm-mustard-100: #fef5ea;
|
|
46
|
+
--fm-berry-500: #5f090a;
|
|
47
|
+
--fm-berry-300: #e94850;
|
|
48
|
+
--fm-berry-100: #fee4dc;
|
|
49
|
+
--fm-success: #1f7735;
|
|
50
|
+
--fm-success-bg: #ebf9ee;
|
|
51
|
+
--fm-warning: #995900;
|
|
52
|
+
--fm-warning-bg: #fff4e6;
|
|
53
|
+
--fm-error: #99231d;
|
|
54
|
+
--fm-error-bg: #ffebea;
|
|
55
|
+
--fm-info: #004999;
|
|
56
|
+
--fm-info-bg: #e6f2ff;
|
|
57
|
+
--fm-radius: 12px;
|
|
58
|
+
--fm-radius-sm: 8px;
|
|
59
|
+
--fm-gap: 12px;
|
|
60
|
+
}
|
|
61
|
+
* {
|
|
62
|
+
box-sizing: border-box;
|
|
63
|
+
}
|
|
64
|
+
html,
|
|
65
|
+
body {
|
|
66
|
+
margin: 0;
|
|
67
|
+
padding: 0;
|
|
68
|
+
height: 100%;
|
|
69
|
+
}
|
|
70
|
+
body {
|
|
71
|
+
font: 14px/1.5 Inter, "Helvetica Neue", Arial, sans-serif;
|
|
72
|
+
color: var(--fm-fg);
|
|
73
|
+
background: #fff;
|
|
74
|
+
overflow-y: auto;
|
|
75
|
+
}
|
|
76
|
+
body.fmr-page {
|
|
77
|
+
background: var(--fm-page);
|
|
78
|
+
}
|
|
79
|
+
/* The board: 12 columns, ragged bottoms. A section spans all 12 unless it says otherwise. */
|
|
80
|
+
#report {
|
|
81
|
+
display: grid;
|
|
82
|
+
grid-template-columns: repeat(12, minmax(0, 1fr));
|
|
83
|
+
align-items: start;
|
|
84
|
+
gap: var(--fm-gap);
|
|
85
|
+
padding: 12px 16px 24px;
|
|
86
|
+
}
|
|
87
|
+
#report > section {
|
|
88
|
+
grid-column: 1 / -1;
|
|
89
|
+
min-width: 0;
|
|
90
|
+
}
|
|
91
|
+
/* ---- components ------------------------------------------------------
|
|
92
|
+
The FeedMe report component set. A report is composed from these; each one
|
|
93
|
+
is already the design system, so nothing here needs restyling to look right. */
|
|
94
|
+
.fmr-card {
|
|
95
|
+
background: var(--fm-card);
|
|
96
|
+
border: 1px solid var(--fm-line);
|
|
97
|
+
border-radius: var(--fm-radius);
|
|
98
|
+
}
|
|
99
|
+
.fmr-tile {
|
|
100
|
+
display: flex;
|
|
101
|
+
flex-direction: column;
|
|
102
|
+
overflow: hidden;
|
|
103
|
+
}
|
|
104
|
+
.fmr-tile-head {
|
|
105
|
+
display: flex;
|
|
106
|
+
align-items: flex-start;
|
|
107
|
+
justify-content: space-between;
|
|
108
|
+
gap: 16px;
|
|
109
|
+
flex-wrap: wrap;
|
|
110
|
+
padding: 16px 20px 12px;
|
|
111
|
+
}
|
|
112
|
+
.fmr-tile-title {
|
|
113
|
+
margin: 0;
|
|
114
|
+
font-size: 16px;
|
|
115
|
+
font-weight: 600;
|
|
116
|
+
color: var(--fm-fg);
|
|
117
|
+
}
|
|
118
|
+
/* One factual line. Not a place to explain the report. */
|
|
119
|
+
.fmr-tile-note {
|
|
120
|
+
margin: 4px 0 0;
|
|
121
|
+
font-size: 12px;
|
|
122
|
+
color: var(--fm-muted);
|
|
123
|
+
}
|
|
124
|
+
.fmr-tile-body {
|
|
125
|
+
padding: 0 20px 16px;
|
|
126
|
+
}
|
|
127
|
+
.fmr-tile-foot {
|
|
128
|
+
display: flex;
|
|
129
|
+
align-items: center;
|
|
130
|
+
justify-content: space-between;
|
|
131
|
+
gap: 12px;
|
|
132
|
+
flex-wrap: wrap;
|
|
133
|
+
border-top: 1px solid var(--fm-line);
|
|
134
|
+
padding: 10px 20px 12px;
|
|
135
|
+
font-size: 12px;
|
|
136
|
+
color: var(--fm-muted);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* Scorecards: label, value, optional trend. */
|
|
140
|
+
.fmr-kpis {
|
|
141
|
+
display: grid;
|
|
142
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
143
|
+
gap: var(--fm-gap);
|
|
144
|
+
}
|
|
145
|
+
.fmr-kpi {
|
|
146
|
+
padding: 14px 16px;
|
|
147
|
+
min-width: 0;
|
|
148
|
+
}
|
|
149
|
+
.fmr-kpi-label {
|
|
150
|
+
font-size: 14px;
|
|
151
|
+
color: var(--fm-muted);
|
|
152
|
+
}
|
|
153
|
+
.fmr-kpi-value {
|
|
154
|
+
display: flex;
|
|
155
|
+
align-items: baseline;
|
|
156
|
+
gap: 6px;
|
|
157
|
+
margin-top: 5px;
|
|
158
|
+
font-size: 24px;
|
|
159
|
+
font-weight: 700;
|
|
160
|
+
color: var(--fm-fg);
|
|
161
|
+
font-variant-numeric: tabular-nums;
|
|
162
|
+
}
|
|
163
|
+
.fmr-kpi-cur {
|
|
164
|
+
font-size: 14px;
|
|
165
|
+
font-weight: 600;
|
|
166
|
+
}
|
|
167
|
+
.fmr-kpi-note {
|
|
168
|
+
margin-top: 6px;
|
|
169
|
+
font-size: 12px;
|
|
170
|
+
color: var(--fm-muted);
|
|
171
|
+
}
|
|
172
|
+
.fmr-trend {
|
|
173
|
+
display: inline-flex;
|
|
174
|
+
align-items: center;
|
|
175
|
+
gap: 4px;
|
|
176
|
+
border-radius: 999px;
|
|
177
|
+
padding: 3px 8px;
|
|
178
|
+
font-size: 12px;
|
|
179
|
+
font-weight: 600;
|
|
180
|
+
}
|
|
181
|
+
.fmr-trend.up { color: var(--fm-success); background: var(--fm-green-100); }
|
|
182
|
+
.fmr-trend.down { color: var(--fm-error); background: var(--fm-berry-100); }
|
|
183
|
+
.fmr-trend.flat { color: var(--fm-muted); background: var(--fm-line); }
|
|
184
|
+
|
|
185
|
+
/* Breakdown: a share row over one stacked bar. */
|
|
186
|
+
.fmr-breakdown {
|
|
187
|
+
display: grid;
|
|
188
|
+
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
189
|
+
gap: 12px 24px;
|
|
190
|
+
}
|
|
191
|
+
.fmr-breakdown-label {
|
|
192
|
+
display: flex;
|
|
193
|
+
align-items: center;
|
|
194
|
+
gap: 6px;
|
|
195
|
+
font-size: 14px;
|
|
196
|
+
color: var(--fm-muted);
|
|
197
|
+
}
|
|
198
|
+
.fmr-breakdown-value {
|
|
199
|
+
margin-top: 2px;
|
|
200
|
+
font-size: 14px;
|
|
201
|
+
font-weight: 600;
|
|
202
|
+
color: var(--fm-fg);
|
|
203
|
+
font-variant-numeric: tabular-nums;
|
|
204
|
+
}
|
|
205
|
+
.fmr-stack {
|
|
206
|
+
display: flex;
|
|
207
|
+
gap: 2px;
|
|
208
|
+
height: 12px;
|
|
209
|
+
margin-top: 14px;
|
|
210
|
+
}
|
|
211
|
+
.fmr-stack > span {
|
|
212
|
+
border-radius: 4px;
|
|
213
|
+
min-width: 2px;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* Legend — you draw it in HTML; chart.js legends stay off. */
|
|
217
|
+
.fmr-legend {
|
|
218
|
+
display: flex;
|
|
219
|
+
align-items: center;
|
|
220
|
+
flex-wrap: wrap;
|
|
221
|
+
gap: 6px 20px;
|
|
222
|
+
}
|
|
223
|
+
.fmr-legend-item {
|
|
224
|
+
display: inline-flex;
|
|
225
|
+
align-items: center;
|
|
226
|
+
gap: 6px;
|
|
227
|
+
font-size: 12px;
|
|
228
|
+
color: var(--fm-muted);
|
|
229
|
+
}
|
|
230
|
+
.fmr-swatch {
|
|
231
|
+
width: 10px;
|
|
232
|
+
height: 10px;
|
|
233
|
+
border-radius: 2px;
|
|
234
|
+
flex: none;
|
|
235
|
+
}
|
|
236
|
+
.fmr-swatch.line {
|
|
237
|
+
width: 12px;
|
|
238
|
+
height: 2px;
|
|
239
|
+
border-radius: 0;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/* Table: no zebra, no vertical rules. */
|
|
243
|
+
table.fmr-table {
|
|
244
|
+
width: 100%;
|
|
245
|
+
border-collapse: collapse;
|
|
246
|
+
}
|
|
247
|
+
table.fmr-table th {
|
|
248
|
+
padding: 0 12px 10px;
|
|
249
|
+
text-align: left;
|
|
250
|
+
font-size: 14px;
|
|
251
|
+
font-weight: 400;
|
|
252
|
+
color: var(--fm-muted);
|
|
253
|
+
white-space: nowrap;
|
|
254
|
+
}
|
|
255
|
+
table.fmr-table td {
|
|
256
|
+
height: 52px;
|
|
257
|
+
padding: 7px 12px;
|
|
258
|
+
border-top: 1px solid var(--fm-line);
|
|
259
|
+
font-size: 14px;
|
|
260
|
+
color: var(--fm-fg);
|
|
261
|
+
vertical-align: middle;
|
|
262
|
+
}
|
|
263
|
+
table.fmr-table th:first-child,
|
|
264
|
+
table.fmr-table td:first-child {
|
|
265
|
+
padding-left: 20px;
|
|
266
|
+
}
|
|
267
|
+
table.fmr-table th:last-child,
|
|
268
|
+
table.fmr-table td:last-child {
|
|
269
|
+
padding-right: 20px;
|
|
270
|
+
}
|
|
271
|
+
table.fmr-table td:first-child {
|
|
272
|
+
font-weight: 600;
|
|
273
|
+
}
|
|
274
|
+
.fmr-num {
|
|
275
|
+
text-align: right;
|
|
276
|
+
font-variant-numeric: tabular-nums;
|
|
277
|
+
}
|
|
278
|
+
.fmr-dim {
|
|
279
|
+
color: var(--fm-muted);
|
|
280
|
+
}
|
|
281
|
+
/* the th/td rules above outrank a bare class, so the table needs its own. */
|
|
282
|
+
table.fmr-table th.fmr-num,
|
|
283
|
+
table.fmr-table td.fmr-num {
|
|
284
|
+
text-align: right;
|
|
285
|
+
}
|
|
286
|
+
table.fmr-table td.fmr-dim {
|
|
287
|
+
color: var(--fm-muted);
|
|
288
|
+
}
|
|
289
|
+
table.fmr-table td.fmr-empty {
|
|
290
|
+
font-weight: 400;
|
|
291
|
+
color: var(--fm-muted);
|
|
292
|
+
}
|
|
293
|
+
.fmr-scroll-x {
|
|
294
|
+
overflow-x: auto;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/* Pager: a table's footer. fmr-tile-foot gives it the frame, this adds the two buttons. */
|
|
298
|
+
.fmr-pager[hidden] {
|
|
299
|
+
display: none;
|
|
300
|
+
}
|
|
301
|
+
.fmr-pager-btns {
|
|
302
|
+
display: flex;
|
|
303
|
+
align-items: center;
|
|
304
|
+
gap: 16px;
|
|
305
|
+
}
|
|
306
|
+
.fmr-pager-btns .fmr-link[disabled] {
|
|
307
|
+
color: var(--fm-faint);
|
|
308
|
+
cursor: default;
|
|
309
|
+
text-decoration: none;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/* Tabs — orange marks the active one, the only place a report uses the signature. */
|
|
313
|
+
.fmr-tabs {
|
|
314
|
+
display: flex;
|
|
315
|
+
gap: 4px;
|
|
316
|
+
border-bottom: 1px solid var(--fm-line);
|
|
317
|
+
padding: 0 20px;
|
|
318
|
+
}
|
|
319
|
+
.fmr-tab {
|
|
320
|
+
border: 0;
|
|
321
|
+
background: none;
|
|
322
|
+
padding: 12px 16px;
|
|
323
|
+
font: inherit;
|
|
324
|
+
font-size: 14px;
|
|
325
|
+
font-weight: 600;
|
|
326
|
+
color: var(--fm-muted);
|
|
327
|
+
cursor: pointer;
|
|
328
|
+
border-bottom: 2px solid transparent;
|
|
329
|
+
margin-bottom: -1px;
|
|
330
|
+
}
|
|
331
|
+
.fmr-tab[aria-selected="true"] {
|
|
332
|
+
color: var(--fm-signature);
|
|
333
|
+
border-bottom-color: var(--fm-signature);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/* Status pairs — the dark tone on its own pale ground, never a brand tint. */
|
|
337
|
+
.fmr-badge {
|
|
338
|
+
display: inline-block;
|
|
339
|
+
border-radius: 999px;
|
|
340
|
+
padding: 3px 8px;
|
|
341
|
+
font-size: 12px;
|
|
342
|
+
font-weight: 600;
|
|
343
|
+
}
|
|
344
|
+
.fmr-badge.success { color: var(--fm-success); background: var(--fm-success-bg); }
|
|
345
|
+
.fmr-badge.warning { color: var(--fm-warning); background: var(--fm-warning-bg); }
|
|
346
|
+
.fmr-badge.error { color: var(--fm-error); background: var(--fm-error-bg); }
|
|
347
|
+
.fmr-badge.info { color: var(--fm-info); background: var(--fm-info-bg); }
|
|
348
|
+
|
|
349
|
+
.fmr-link {
|
|
350
|
+
border: 0;
|
|
351
|
+
background: none;
|
|
352
|
+
padding: 0;
|
|
353
|
+
font: inherit;
|
|
354
|
+
font-weight: 600;
|
|
355
|
+
color: var(--fm-info);
|
|
356
|
+
cursor: pointer;
|
|
357
|
+
text-decoration: underline;
|
|
358
|
+
text-decoration-color: transparent;
|
|
359
|
+
text-underline-offset: 2px;
|
|
360
|
+
}
|
|
361
|
+
.fmr-link:hover,
|
|
362
|
+
.fmr-link:focus-visible {
|
|
363
|
+
text-decoration-color: currentColor;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/* A chart canvas needs a height its parent does not give it. */
|
|
367
|
+
.fmr-chart {
|
|
368
|
+
position: relative;
|
|
369
|
+
height: 240px;
|
|
370
|
+
}
|
|
371
|
+
.fmr-chart.sm { height: 180px; }
|
|
372
|
+
.fmr-chart.lg { height: 320px; }
|
|
373
|
+
|
|
374
|
+
/* The two states the shell does not paint. */
|
|
375
|
+
.fmr-note {
|
|
376
|
+
margin: 0 20px 16px;
|
|
377
|
+
padding: 8px 0;
|
|
378
|
+
font-size: 14px;
|
|
379
|
+
color: var(--fm-muted);
|
|
380
|
+
}
|
|
381
|
+
.fmr-note.error {
|
|
382
|
+
border-radius: var(--fm-radius-sm);
|
|
383
|
+
padding: 11px 12px;
|
|
384
|
+
color: var(--fm-error);
|
|
385
|
+
background: var(--fm-error-bg);
|
|
386
|
+
}
|
|
387
|
+
.fm-section-error {
|
|
388
|
+
border: 1px solid var(--fm-line);
|
|
389
|
+
border-left: 3px solid var(--fm-danger);
|
|
390
|
+
background: var(--fm-danger-bg);
|
|
391
|
+
border-radius: 6px;
|
|
392
|
+
padding: 12px 14px;
|
|
393
|
+
}
|
|
394
|
+
.fm-section-error b {
|
|
395
|
+
color: var(--fm-danger);
|
|
396
|
+
}
|
|
397
|
+
.fm-section-error pre {
|
|
398
|
+
margin: 6px 0 0;
|
|
399
|
+
white-space: pre-wrap;
|
|
400
|
+
font-size: 12px;
|
|
401
|
+
color: var(--fm-muted);
|
|
402
|
+
}
|
|
403
|
+
.fm-placeholder {
|
|
404
|
+
color: var(--fm-muted);
|
|
405
|
+
padding: 24px 16px;
|
|
406
|
+
}
|
|
407
|
+
/* Loading belongs to the shell: a section declares its queries and never draws a skeleton. */
|
|
408
|
+
#fm-progress {
|
|
409
|
+
position: fixed;
|
|
410
|
+
inset: 0 0 auto;
|
|
411
|
+
z-index: 30;
|
|
412
|
+
height: 2px;
|
|
413
|
+
overflow: hidden;
|
|
414
|
+
background: var(--fm-signature-tint);
|
|
415
|
+
opacity: 0;
|
|
416
|
+
transition: opacity 0.15s linear;
|
|
417
|
+
pointer-events: none;
|
|
418
|
+
}
|
|
419
|
+
#fm-progress[data-on] {
|
|
420
|
+
opacity: 1;
|
|
421
|
+
}
|
|
422
|
+
#fm-progress i {
|
|
423
|
+
display: block;
|
|
424
|
+
width: 32%;
|
|
425
|
+
height: 100%;
|
|
426
|
+
background: var(--fm-signature);
|
|
427
|
+
}
|
|
428
|
+
section[data-fm-busy] {
|
|
429
|
+
position: relative;
|
|
430
|
+
min-height: 48px;
|
|
431
|
+
}
|
|
432
|
+
/* A scrim, not a replacement: the designed panel stays put and stale values stay clickable. */
|
|
433
|
+
section[data-fm-busy]::after {
|
|
434
|
+
content: "";
|
|
435
|
+
position: absolute;
|
|
436
|
+
inset: -4px;
|
|
437
|
+
border-radius: 10px;
|
|
438
|
+
background: rgba(255, 255, 255, 0.62);
|
|
439
|
+
pointer-events: none;
|
|
440
|
+
}
|
|
441
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
442
|
+
#fm-progress i {
|
|
443
|
+
animation: fm-slide 1.1s linear infinite;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
@keyframes fm-slide {
|
|
447
|
+
from {
|
|
448
|
+
transform: translateX(-100%);
|
|
449
|
+
}
|
|
450
|
+
to {
|
|
451
|
+
transform: translateX(320%);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
.fm-text h1,
|
|
455
|
+
.fm-text h2,
|
|
456
|
+
.fm-text h3 {
|
|
457
|
+
margin: 0 0 8px;
|
|
458
|
+
line-height: 1.3;
|
|
459
|
+
}
|
|
460
|
+
.fm-text p {
|
|
461
|
+
margin: 0 0 8px;
|
|
462
|
+
}
|
|
463
|
+
.fm-text code {
|
|
464
|
+
background: #f0f0f0;
|
|
465
|
+
border-radius: 3px;
|
|
466
|
+
padding: 1px 4px;
|
|
467
|
+
font-size: 12px;
|
|
468
|
+
}
|
|
469
|
+
</style>
|
|
470
|
+
|
|
471
|
+
<div id="fm-progress" data-on aria-hidden="true"><i></i></div>
|
|
472
|
+
<div id="report" aria-busy="true"></div>
|
|
473
|
+
|
|
474
|
+
<script>
|
|
475
|
+
;(function () {
|
|
476
|
+
"use strict"
|
|
477
|
+
|
|
478
|
+
var root = document.getElementById("report")
|
|
479
|
+
|
|
480
|
+
var env = {}
|
|
481
|
+
var params = {}
|
|
482
|
+
var results = Object.create(null) // queryId -> { rows, status, runId, truncated, error }
|
|
483
|
+
var subs = Object.create(null) // queryId -> [cb]
|
|
484
|
+
var loadedLibs = Object.create(null)
|
|
485
|
+
var mounted = Object.create(null) // sectionId -> container element
|
|
486
|
+
var versions = Object.create(null) // sectionId -> mounted source version
|
|
487
|
+
var failed = Object.create(null) // sectionId -> true once this mount reported an error
|
|
488
|
+
var order = [] // section ids, render order
|
|
489
|
+
var mounting = null // section id currently executing, for window.onerror attribution
|
|
490
|
+
var declared = Object.create(null) // sectionId -> the query ids that section reads
|
|
491
|
+
var progress = document.getElementById("fm-progress")
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Section code shares this window and can call parent.postMessage itself, so a render
|
|
495
|
+
* outcome carries a nonce only this closure holds — never on `window`, never in the DOM.
|
|
496
|
+
* The host learns it from `ready` and drops any ack without it.
|
|
497
|
+
*/
|
|
498
|
+
var NONCE = (function () {
|
|
499
|
+
try {
|
|
500
|
+
var buffer = new Uint32Array(4)
|
|
501
|
+
crypto.getRandomValues(buffer)
|
|
502
|
+
return Array.prototype.map
|
|
503
|
+
.call(buffer, function (value) {
|
|
504
|
+
return value.toString(36)
|
|
505
|
+
})
|
|
506
|
+
.join("")
|
|
507
|
+
} catch (err) {
|
|
508
|
+
return String(Math.random()).slice(2) + String(new Date().getTime())
|
|
509
|
+
}
|
|
510
|
+
})()
|
|
511
|
+
|
|
512
|
+
function post(message) {
|
|
513
|
+
// Opaque origins cannot be named, so "*" is the only usable target. The host
|
|
514
|
+
// authenticates us by comparing event.source to its own iframe.contentWindow.
|
|
515
|
+
parent.postMessage(message, "*")
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** Only the shell may claim a section rendered or threw. */
|
|
519
|
+
function postOutcome(message) {
|
|
520
|
+
message.nonce = NONCE
|
|
521
|
+
post(message)
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function reportError(sectionId, err) {
|
|
525
|
+
var message = err && err.message ? String(err.message) : String(err)
|
|
526
|
+
var stack = err && err.stack ? String(err.stack).slice(0, 4000) : undefined
|
|
527
|
+
failed[sectionId] = true
|
|
528
|
+
renderError(sectionId, message)
|
|
529
|
+
postOutcome({
|
|
530
|
+
type: "error",
|
|
531
|
+
sectionId: sectionId,
|
|
532
|
+
version: versions[sectionId],
|
|
533
|
+
message: message,
|
|
534
|
+
stack: stack
|
|
535
|
+
})
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function renderError(sectionId, message) {
|
|
539
|
+
var el = mounted[sectionId]
|
|
540
|
+
if (!el) return
|
|
541
|
+
el.innerHTML = ""
|
|
542
|
+
var box = document.createElement("div")
|
|
543
|
+
box.className = "fm-section-error"
|
|
544
|
+
var title = document.createElement("b")
|
|
545
|
+
title.textContent = "This part of the report could not be shown"
|
|
546
|
+
var detail = document.createElement("pre")
|
|
547
|
+
detail.textContent = message
|
|
548
|
+
box.appendChild(title)
|
|
549
|
+
box.appendChild(detail)
|
|
550
|
+
el.appendChild(box)
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// --- markdown (text sections): escape first, then a small safe subset
|
|
554
|
+
|
|
555
|
+
function escapeHtml(text) {
|
|
556
|
+
return text
|
|
557
|
+
.replace(/&/g, "&")
|
|
558
|
+
.replace(/</g, "<")
|
|
559
|
+
.replace(/>/g, ">")
|
|
560
|
+
.replace(/"/g, """)
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function renderMarkdown(source) {
|
|
564
|
+
var lines = escapeHtml(String(source || "")).split(/\r?\n/)
|
|
565
|
+
var out = []
|
|
566
|
+
var list = null
|
|
567
|
+
function closeList() {
|
|
568
|
+
if (list) {
|
|
569
|
+
out.push("<" + list.tag + ">" + list.items.join("") + "</" + list.tag + ">")
|
|
570
|
+
list = null
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
for (var i = 0; i < lines.length; i++) {
|
|
574
|
+
var line = lines[i]
|
|
575
|
+
var heading = /^(#{1,3})\s+(.*)$/.exec(line)
|
|
576
|
+
var bullet = /^\s*[-*]\s+(.*)$/.exec(line)
|
|
577
|
+
var numbered = /^\s*\d+\.\s+(.*)$/.exec(line)
|
|
578
|
+
if (heading) {
|
|
579
|
+
closeList()
|
|
580
|
+
var level = heading[1].length
|
|
581
|
+
out.push("<h" + level + ">" + inline(heading[2]) + "</h" + level + ">")
|
|
582
|
+
} else if (bullet || numbered) {
|
|
583
|
+
var tag = bullet ? "ul" : "ol"
|
|
584
|
+
if (!list || list.tag !== tag) {
|
|
585
|
+
closeList()
|
|
586
|
+
list = { tag: tag, items: [] }
|
|
587
|
+
}
|
|
588
|
+
list.items.push("<li>" + inline((bullet || numbered)[1]) + "</li>")
|
|
589
|
+
} else if (!line.trim()) {
|
|
590
|
+
closeList()
|
|
591
|
+
} else {
|
|
592
|
+
closeList()
|
|
593
|
+
out.push("<p>" + inline(line) + "</p>")
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
closeList()
|
|
597
|
+
return out.join("")
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function inline(text) {
|
|
601
|
+
return text
|
|
602
|
+
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
|
603
|
+
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
|
|
604
|
+
.replace(/(^|[^*])\*([^*]+)\*/g, "$1<em>$2</em>")
|
|
605
|
+
.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+|mailto:[^)\s]+)\)/g, function (_all, label, href) {
|
|
606
|
+
return '<a href="' + href + '" target="_blank" rel="noreferrer noopener">' + label + "</a>"
|
|
607
|
+
})
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// --- libs
|
|
611
|
+
|
|
612
|
+
function installLibs(bundles) {
|
|
613
|
+
if (!bundles) return
|
|
614
|
+
Object.keys(bundles).forEach(function (key) {
|
|
615
|
+
if (loadedLibs[key]) return
|
|
616
|
+
var script = document.createElement("script")
|
|
617
|
+
script.textContent = bundles[key]
|
|
618
|
+
document.head.appendChild(script)
|
|
619
|
+
loadedLibs[key] = true
|
|
620
|
+
})
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// --- sections
|
|
624
|
+
|
|
625
|
+
function container(sectionId) {
|
|
626
|
+
var el = mounted[sectionId]
|
|
627
|
+
if (el) {
|
|
628
|
+
el.innerHTML = ""
|
|
629
|
+
return el
|
|
630
|
+
}
|
|
631
|
+
el = document.createElement("section")
|
|
632
|
+
el.id = sectionId
|
|
633
|
+
el.dataset.sectionId = sectionId
|
|
634
|
+
mounted[sectionId] = el
|
|
635
|
+
return el
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Sections share one DOM and one CSS cascade on purpose, but top-level JS is wrapped
|
|
640
|
+
* so two sections cannot collide on a `const` — they share state through `window`.
|
|
641
|
+
* Always attempts the persisted source, even one saved as `status: "error"`: that is
|
|
642
|
+
* what "restore last good source" means, and a genuinely broken one throws and re-acks.
|
|
643
|
+
*/
|
|
644
|
+
function mountSection(section) {
|
|
645
|
+
var el = container(section.id)
|
|
646
|
+
versions[section.id] = section.version
|
|
647
|
+
failed[section.id] = false
|
|
648
|
+
declared[section.id] = (section.queryIds || []).slice()
|
|
649
|
+
mounting = section.id
|
|
650
|
+
try {
|
|
651
|
+
if (section.kind === "text") {
|
|
652
|
+
var text = document.createElement("div")
|
|
653
|
+
text.className = "fm-text"
|
|
654
|
+
text.innerHTML = renderMarkdown(section.source)
|
|
655
|
+
el.appendChild(text)
|
|
656
|
+
return
|
|
657
|
+
}
|
|
658
|
+
var doc = new DOMParser().parseFromString(
|
|
659
|
+
"<body>" + String(section.source || "") + "</body>",
|
|
660
|
+
"text/html"
|
|
661
|
+
)
|
|
662
|
+
var scripts = []
|
|
663
|
+
Array.prototype.slice.call(doc.body.childNodes).forEach(function (node) {
|
|
664
|
+
if (node.tagName === "SCRIPT") {
|
|
665
|
+
scripts.push(node.textContent || "")
|
|
666
|
+
} else {
|
|
667
|
+
el.appendChild(document.importNode(node, true))
|
|
668
|
+
}
|
|
669
|
+
})
|
|
670
|
+
// Inline scripts throw past the caller's try/catch, so wrap the source itself.
|
|
671
|
+
scripts.forEach(function (code, index) {
|
|
672
|
+
var tag = document.createElement("script")
|
|
673
|
+
tag.textContent = "try{(function(){\n" + code + "\n})()}catch(e){window.FM.__err(e)}"
|
|
674
|
+
el.appendChild(tag)
|
|
675
|
+
void index
|
|
676
|
+
})
|
|
677
|
+
} catch (err) {
|
|
678
|
+
reportError(section.id, err)
|
|
679
|
+
} finally {
|
|
680
|
+
mounting = null
|
|
681
|
+
if (!failed[section.id]) {
|
|
682
|
+
postOutcome({ type: "rendered", sectionId: section.id, version: section.version })
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function unmountSection(sectionId) {
|
|
688
|
+
var el = mounted[sectionId]
|
|
689
|
+
if (el && el.parentNode) el.parentNode.removeChild(el)
|
|
690
|
+
delete mounted[sectionId]
|
|
691
|
+
delete versions[sectionId]
|
|
692
|
+
delete failed[sectionId]
|
|
693
|
+
delete declared[sectionId]
|
|
694
|
+
Object.keys(subs).forEach(function (queryId) {
|
|
695
|
+
subs[queryId] = subs[queryId].filter(function (entry) {
|
|
696
|
+
return entry.sectionId !== sectionId
|
|
697
|
+
})
|
|
698
|
+
})
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function renderAll(sections) {
|
|
702
|
+
root.innerHTML = ""
|
|
703
|
+
mounted = Object.create(null)
|
|
704
|
+
versions = Object.create(null)
|
|
705
|
+
failed = Object.create(null)
|
|
706
|
+
declared = Object.create(null)
|
|
707
|
+
subs = Object.create(null)
|
|
708
|
+
order = sections.map(function (section) {
|
|
709
|
+
return section.id
|
|
710
|
+
})
|
|
711
|
+
sections.forEach(function (section) {
|
|
712
|
+
var el = container(section.id)
|
|
713
|
+
root.appendChild(el)
|
|
714
|
+
mountSection(section)
|
|
715
|
+
})
|
|
716
|
+
if (sections.length === 0) {
|
|
717
|
+
var empty = document.createElement("div")
|
|
718
|
+
empty.className = "fm-placeholder"
|
|
719
|
+
empty.textContent = "This report is empty. Ask in the chat to build it."
|
|
720
|
+
root.appendChild(empty)
|
|
721
|
+
}
|
|
722
|
+
root.removeAttribute("aria-busy")
|
|
723
|
+
paintBusy()
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/** Hot-swap: replace only the named sections, appending any that are new. */
|
|
727
|
+
function swapSections(sections) {
|
|
728
|
+
sections.forEach(function (section) {
|
|
729
|
+
var existing = mounted[section.id]
|
|
730
|
+
if (existing) {
|
|
731
|
+
unmountSection(section.id)
|
|
732
|
+
var replacement = container(section.id)
|
|
733
|
+
root.insertBefore(replacement, positionAfter(section.id))
|
|
734
|
+
mountSection(section)
|
|
735
|
+
return
|
|
736
|
+
}
|
|
737
|
+
order.push(section.id)
|
|
738
|
+
var el = container(section.id)
|
|
739
|
+
root.appendChild(el)
|
|
740
|
+
mountSection(section)
|
|
741
|
+
})
|
|
742
|
+
paintBusy()
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function positionAfter(sectionId) {
|
|
746
|
+
var index = order.indexOf(sectionId)
|
|
747
|
+
for (var i = index + 1; i < order.length; i++) {
|
|
748
|
+
if (mounted[order[i]]) return mounted[order[i]]
|
|
749
|
+
}
|
|
750
|
+
return null
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function notify(queryId) {
|
|
754
|
+
var list = subs[queryId] || []
|
|
755
|
+
list.slice().forEach(function (entry) {
|
|
756
|
+
try {
|
|
757
|
+
entry.cb(results[queryId] ? results[queryId].rows : [])
|
|
758
|
+
} catch (err) {
|
|
759
|
+
reportError(entry.sectionId, err)
|
|
760
|
+
}
|
|
761
|
+
})
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// --- loading
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Every report gets the same loading affordance, so no section hand-rolls one: a scrim
|
|
768
|
+
* over each section whose declared query is in flight, plus one bar for the report.
|
|
769
|
+
* Only `loading` counts — an idle lazy query keeps the empty state it was designed with,
|
|
770
|
+
* and a query the shell has not heard about yet is not assumed to be running.
|
|
771
|
+
*/
|
|
772
|
+
function paintBusy() {
|
|
773
|
+
Object.keys(mounted).forEach(function (sectionId) {
|
|
774
|
+
var el = mounted[sectionId]
|
|
775
|
+
var busy = !failed[sectionId] && (declared[sectionId] || []).some(isRunning)
|
|
776
|
+
if (busy) {
|
|
777
|
+
el.setAttribute("data-fm-busy", "")
|
|
778
|
+
el.setAttribute("aria-busy", "true")
|
|
779
|
+
} else {
|
|
780
|
+
el.removeAttribute("data-fm-busy")
|
|
781
|
+
el.removeAttribute("aria-busy")
|
|
782
|
+
}
|
|
783
|
+
})
|
|
784
|
+
if (progress) {
|
|
785
|
+
if (Object.keys(results).some(isRunning)) progress.setAttribute("data-on", "")
|
|
786
|
+
else progress.removeAttribute("data-on")
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function isRunning(queryId) {
|
|
791
|
+
var result = results[queryId]
|
|
792
|
+
return !!result && result.status === "loading"
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// --- FM: the only API section code may use (see the agent system prompt)
|
|
796
|
+
|
|
797
|
+
window.env = env
|
|
798
|
+
window.FM = {
|
|
799
|
+
rows: function (queryId) {
|
|
800
|
+
var result = results[queryId]
|
|
801
|
+
return result && result.rows ? result.rows : []
|
|
802
|
+
},
|
|
803
|
+
status: function (queryId) {
|
|
804
|
+
var result = results[queryId]
|
|
805
|
+
return result ? result.status : "idle"
|
|
806
|
+
},
|
|
807
|
+
onRows: function (queryId, cb) {
|
|
808
|
+
if (typeof cb !== "function") return
|
|
809
|
+
if (!subs[queryId]) subs[queryId] = []
|
|
810
|
+
subs[queryId].push({ sectionId: mounting, cb: cb })
|
|
811
|
+
// fire immediately so a section renders its current state without waiting
|
|
812
|
+
try {
|
|
813
|
+
cb(window.FM.rows(queryId))
|
|
814
|
+
} catch (err) {
|
|
815
|
+
reportError(mounting, err)
|
|
816
|
+
}
|
|
817
|
+
},
|
|
818
|
+
setParams: function (values) {
|
|
819
|
+
if (!values || typeof values !== "object") return
|
|
820
|
+
post({ type: "setParams", values: values })
|
|
821
|
+
},
|
|
822
|
+
params: function () {
|
|
823
|
+
return JSON.parse(JSON.stringify(params))
|
|
824
|
+
},
|
|
825
|
+
/** Takes no section id on purpose: the shell attributes the failure to what it is mounting. */
|
|
826
|
+
__err: function (err) {
|
|
827
|
+
if (mounting) reportError(mounting, err)
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
window.addEventListener("error", function (event) {
|
|
832
|
+
if (!mounting) return
|
|
833
|
+
reportError(mounting, event.error || new Error(event.message))
|
|
834
|
+
})
|
|
835
|
+
window.addEventListener("unhandledrejection", function (event) {
|
|
836
|
+
if (!mounting) return
|
|
837
|
+
reportError(mounting, event.reason || new Error("unhandled rejection"))
|
|
838
|
+
})
|
|
839
|
+
|
|
840
|
+
// --- host channel
|
|
841
|
+
|
|
842
|
+
window.addEventListener("message", function (event) {
|
|
843
|
+
// the host is the only window that can reach us; anything else is not ours
|
|
844
|
+
if (event.source !== window.parent) return
|
|
845
|
+
var data = event.data
|
|
846
|
+
if (!data || typeof data !== "object") return
|
|
847
|
+
switch (data.type) {
|
|
848
|
+
case "init":
|
|
849
|
+
env = data.env || {}
|
|
850
|
+
window.env = env
|
|
851
|
+
params = data.params || {}
|
|
852
|
+
installLibs(data.libs)
|
|
853
|
+
renderAll(data.sections || [])
|
|
854
|
+
break
|
|
855
|
+
case "sections":
|
|
856
|
+
installLibs(data.libs)
|
|
857
|
+
swapSections(data.sections || [])
|
|
858
|
+
break
|
|
859
|
+
case "params":
|
|
860
|
+
params = data.values || {}
|
|
861
|
+
Object.keys(subs).forEach(notify)
|
|
862
|
+
break
|
|
863
|
+
// Mutated in place, never replaced: sections captured `env` at mount and a new
|
|
864
|
+
// object would leave every one of them reading the empty first snapshot.
|
|
865
|
+
case "env":
|
|
866
|
+
Object.keys(env).forEach(function (key) { delete env[key] })
|
|
867
|
+
Object.keys(data.env || {}).forEach(function (key) { env[key] = data.env[key] })
|
|
868
|
+
Object.keys(subs).forEach(notify)
|
|
869
|
+
break
|
|
870
|
+
case "rows":
|
|
871
|
+
var previous = results[data.queryId]
|
|
872
|
+
if (previous && previous.runId > data.runId) return // superseded run
|
|
873
|
+
results[data.queryId] = {
|
|
874
|
+
rows: data.rows || [],
|
|
875
|
+
status: data.status,
|
|
876
|
+
runId: data.runId,
|
|
877
|
+
truncated: !!data.truncated,
|
|
878
|
+
error: data.error
|
|
879
|
+
}
|
|
880
|
+
notify(data.queryId)
|
|
881
|
+
paintBusy()
|
|
882
|
+
break
|
|
883
|
+
}
|
|
884
|
+
})
|
|
885
|
+
|
|
886
|
+
post({ type: "ready", nonce: NONCE })
|
|
887
|
+
})()
|
|
888
|
+
</script>
|