@reportforge/playwright-pdf 0.6.2 → 0.7.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.
@@ -292,12 +292,90 @@ code {
292
292
  .release-gate__sep { color: var(--border-3); }
293
293
  .release-gate__flaky-warn { color: var(--flaky); font-weight: 600; }
294
294
 
295
- /* ── Failure severitycolour the card's left edge by triage rank ───────── */
296
- /* Two-class specificity beats the per-template `.failure-item` border shorthand. */
297
- .failure-item.failure-item--critical { border-left-color: var(--fail); }
298
- .failure-item.failure-item--high { border-left-color: #A35E1E; }
299
- .failure-item.failure-item--medium { border-left-color: var(--timeout); }
300
- .failure-item.failure-item--low { border-left-color: var(--text-3); }
295
+ /* ── Inline failure detail ONE block hanging off the failed test row ────────
296
+ A single severity-coloured left rail + hairline dividers tie the error,
297
+ retry history, and root cause together as the failed test's detail (not
298
+ three separate boxes). Lives here so every template's breakdown can use it. */
299
+ .failure-row > td { padding: 0; border: none; background: transparent; }
300
+ .failure-detail {
301
+ border-left: 3px solid var(--fail);
302
+ margin: 2px 10px 10px 12px; /* slight indent under the test row + gap before next row */
303
+ padding: 11px 0 11px 16px;
304
+ page-break-inside: avoid;
305
+ }
306
+ .failure-detail--critical { border-left-color: var(--fail); }
307
+ .failure-detail--high { border-left-color: #A35E1E; }
308
+ .failure-detail--medium { border-left-color: var(--timeout); }
309
+ .failure-detail--low { border-left-color: var(--text-3); }
310
+
311
+ /* error — flat on the rail, no inner box (override the base pre panel) */
312
+ .failure-detail pre {
313
+ margin: 0; padding: 0;
314
+ background: transparent; border: none; border-radius: 0; max-height: none;
315
+ }
316
+ .failure-detail .failure-item__message {
317
+ background: transparent; border: none; border-radius: 0; padding: 0; margin: 0;
318
+ font-family: 'JetBrains Mono', monospace;
319
+ font-size: 9.5px; color: #8A2E1E; word-break: break-word; line-height: 1.6;
320
+ }
321
+
322
+ /* retry history — divider above, same block */
323
+ .retry-history {
324
+ font-size: 9px; color: var(--text-3); margin-top: 11px;
325
+ padding-top: 10px; border-top: 1px solid var(--border);
326
+ font-family: 'Inter', system-ui, sans-serif; font-weight: 600;
327
+ letter-spacing: 0.5px; text-transform: uppercase;
328
+ }
329
+ .retry-attempt {
330
+ display: inline-block; padding: 2px 7px;
331
+ border-radius: 3px; margin-right: 4px;
332
+ font-size: 8.5px; font-family: 'Inter', system-ui, sans-serif; font-weight: 600;
333
+ -webkit-print-color-adjust: exact;
334
+ print-color-adjust: exact;
335
+ }
336
+ .retry-attempt--failed { background: var(--fail-dim); color: var(--fail); border: 1px solid #E5C9BD; }
337
+ .retry-attempt--passed { background: var(--pass-dim); color: var(--pass); border: 1px solid #C5DBC9; }
338
+
339
+ /* Slow-test badge — a compact tag inline beside the status badge (min-width:0
340
+ overrides the .badge 50px floor so it doesn't wrap onto its own line). */
341
+ .badge--slow {
342
+ background: var(--timeout-dim); color: var(--timeout);
343
+ border: 1px solid #E5D2A8;
344
+ min-width: 0; margin-left: 5px;
345
+ padding: 1px 5px; font-size: 7.5px; letter-spacing: 0.4px;
346
+ }
347
+
348
+ /* Root cause — footer of the same failure block: a divider above, no box and no
349
+ second rail (the block already carries one). */
350
+ .root-cause-chip {
351
+ display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px;
352
+ margin-top: 11px; padding-top: 10px;
353
+ border-top: 1px solid var(--border);
354
+ font-family: 'Inter', system-ui, sans-serif; font-size: 9.5px;
355
+ }
356
+ .root-cause-chip__tag {
357
+ font-size: 8px; font-weight: 700; letter-spacing: 0.8px; text-transform: uppercase;
358
+ color: var(--text-3);
359
+ }
360
+ .root-cause-chip__label { font-weight: 700; color: var(--text-1); }
361
+ .root-cause-chip__msg {
362
+ font-family: 'JetBrains Mono', monospace; color: var(--text-2);
363
+ word-break: break-word;
364
+ }
365
+ .root-cause-chip__strength {
366
+ margin-left: auto; font-size: 8.5px; font-weight: 600;
367
+ letter-spacing: 0.5px; text-transform: uppercase; color: var(--text-3);
368
+ }
369
+ /* Subtle category cue — tint only the category name (no box, no second rail), so
370
+ the chip stays "quiet" but failures are still scannable by root-cause type. */
371
+ .root-cause-chip--assertion .root-cause-chip__label,
372
+ .root-cause-chip--locator-not-found .root-cause-chip__label { color: oklch(52% 0.19 25); }
373
+ .root-cause-chip--timeout .root-cause-chip__label { color: oklch(50% 0.13 65); }
374
+ .root-cause-chip--network .root-cause-chip__label,
375
+ .root-cause-chip--navigation .root-cause-chip__label { color: oklch(50% 0.15 260); }
376
+ .root-cause-chip--flaky-by-retry .root-cause-chip__label { color: oklch(50% 0.15 290); }
377
+ .root-cause-chip--env-config .root-cause-chip__label { color: oklch(48% 0.14 145); }
378
+ .root-cause-chip--unknown .root-cause-chip__label { color: var(--text-2); }
301
379
 
302
380
  /* ── History trend card — shared by detailed + executive (charts.hbs) ─────── */
303
381
  .trend-card {
@@ -656,23 +734,6 @@ code {
656
734
  font-size: 9.5px; color: var(--text-1);
657
735
  }
658
736
 
659
- /* ── Failure Analysis shared so any template can toggle the section on ────── */
660
- /* NOTE: .analysis-oneliner lives in base.css above — the executive template renders it too. */
661
- .failure-analysis { margin-top: 24px; }
662
- .failure-analysis .oneliner { font-weight: 600; }
663
- .analysis-truncated { font-size: 0.85em; opacity: 0.8; }
664
-
665
- .cluster { border-left: 3px solid; padding-left: 12px; margin-bottom: 16px; }
666
- .cluster-header { display: flex; gap: 12px; align-items: baseline; }
667
- .cluster-strength--strong { color: oklch(55% 0.15 145); }
668
- .cluster-strength--moderate { color: oklch(60% 0.18 65); }
669
- .cluster-strength--weak { color: oklch(55% 0.20 25); }
670
- .cluster--timeout { border-color: oklch(60% 0.18 65); }
671
- .cluster--assertion { border-color: oklch(55% 0.20 25); }
672
- .cluster--locator-not-found { border-color: oklch(55% 0.20 25); }
673
- .cluster--network { border-color: oklch(55% 0.15 260); }
674
- .cluster--navigation { border-color: oklch(55% 0.15 260); }
675
- .cluster--flaky-by-retry { border-color: oklch(55% 0.15 290); }
676
- .cluster--env-config { border-color: oklch(55% 0.15 145); }
677
- .cluster--unknown { border-color: oklch(60% 0.00 0); }
678
- .confidence-warning { font-size: 0.8em; opacity: 0.75; }
737
+ /* The aggregate failure-analysis cluster section was retired root causes now
738
+ render as inline per-test chips in the suite breakdown (.root-cause-chip above).
739
+ The .analysis-oneliner one-liner lives further up and is still used by executive. */
@@ -130,50 +130,7 @@
130
130
 
131
131
  /* ── Suite breakdown · styles live in base.css (shared across templates) ──── */
132
132
 
133
- /* ── Failure item ────────────────────────────────────────────────────────── */
134
- .failure-item {
135
- background: var(--surface);
136
- border: 1px solid var(--border-2);
137
- border-left: 3px solid var(--fail);
138
- border-radius: 6px; padding: 16px 18px;
139
- margin-bottom: 12px; page-break-inside: avoid;
140
- }
141
- .failure-item__header {
142
- display: flex; justify-content: space-between;
143
- align-items: flex-start; margin-bottom: 10px;
144
- }
145
- .failure-item__title {
146
- font-family: 'Inter', system-ui, sans-serif;
147
- font-weight: 600; font-size: 12px; color: var(--text-1);
148
- }
149
- .failure-item__path {
150
- font-family: 'JetBrains Mono', monospace;
151
- font-size: 8.5px; color: var(--text-3); margin-bottom: 10px;
152
- }
153
- .failure-item__message {
154
- background: var(--fail-dim);
155
- border: 1px solid #E5C9BD;
156
- border-left: 2px solid var(--fail);
157
- border-radius: 4px; padding: 10px 14px; margin-bottom: 12px;
158
- font-family: 'JetBrains Mono', monospace;
159
- font-size: 9.5px; color: #8A2E1E;
160
- word-break: break-word; line-height: 1.6;
161
- }
162
- .retry-history {
163
- font-size: 9px; color: var(--text-3); margin-top: 10px;
164
- padding-top: 10px; border-top: 1px solid var(--border);
165
- font-family: 'Inter', system-ui, sans-serif; font-weight: 600;
166
- letter-spacing: 0.5px; text-transform: uppercase;
167
- }
168
- .retry-attempt {
169
- display: inline-block; padding: 2px 7px;
170
- border-radius: 3px; margin-right: 4px;
171
- font-size: 8.5px; font-family: 'Inter', system-ui, sans-serif; font-weight: 600;
172
- -webkit-print-color-adjust: exact;
173
- print-color-adjust: exact;
174
- }
175
- .retry-attempt--failed { background: var(--fail-dim); color: var(--fail); border: 1px solid #E5C9BD; }
176
- .retry-attempt--passed { background: var(--pass-dim); color: var(--pass); border: 1px solid #C5DBC9; }
133
+ /* ── Failure card · styles live in base.css (rendered inside the breakdown) ── */
177
134
 
178
135
  /* ── Defect log, coverage bar, environment table, failure analysis ───────── */
179
136
  /* All moved to base.css so executive/minimal can toggle these sections on too. */
@@ -132,35 +132,5 @@
132
132
  letter-spacing: 1.3px;
133
133
  }
134
134
 
135
- /* ── Executive failure summary ───────────────────────────────────────────── */
136
- .failure-summary-item {
137
- display: flex; align-items: center; justify-content: space-between;
138
- padding: 12px 16px;
139
- border-bottom: 1px solid var(--border);
140
- background: var(--surface);
141
- border-left: 3px solid var(--fail);
142
- }
143
- .failure-summary-item:first-child {
144
- border-radius: 6px 6px 0 0;
145
- border-top: 1px solid var(--border-2);
146
- border-right: 1px solid var(--border-2);
147
- }
148
- .failure-summary-item:last-child {
149
- border-radius: 0 0 6px 6px;
150
- border-bottom: 1px solid var(--border-2);
151
- border-right: 1px solid var(--border-2);
152
- }
153
- .failure-summary-item:only-child {
154
- border-radius: 6px;
155
- border: 1px solid var(--border-2);
156
- border-left: 3px solid var(--fail);
157
- }
158
- .failure-summary-item__title {
159
- font-size: 11px; font-weight: 600; color: var(--text-1);
160
- padding-left: 6px;
161
- }
162
- .failure-summary-item__suite {
163
- font-size: 9px; color: var(--text-3); margin-top: 3px;
164
- font-family: 'JetBrains Mono', monospace;
165
- padding-left: 6px;
166
- }
135
+ /* Executive failures now render inline in the suite breakdown (compact, no
136
+ screenshots) — card styles live in base.css. */
@@ -143,49 +143,4 @@
143
143
  }
144
144
  .suite-stats { gap: 6px; }
145
145
 
146
- /* ── Failure item ────────────────────────────────────────────────────────── */
147
- .failure-item {
148
- background: var(--surface);
149
- border: 1px solid var(--border-2);
150
- border-left: 3px solid var(--fail);
151
- border-radius: 6px;
152
- padding: 14px 16px; margin-bottom: 10px;
153
- page-break-inside: avoid;
154
- }
155
- .failure-item__header {
156
- display: flex; justify-content: space-between;
157
- align-items: flex-start; margin-bottom: 8px;
158
- }
159
- .failure-item__title {
160
- font-family: 'Inter', system-ui, sans-serif;
161
- font-weight: 600; font-size: 12px;
162
- color: var(--text-1); margin-bottom: 4px;
163
- }
164
- .failure-item__path {
165
- font-family: 'JetBrains Mono', monospace;
166
- font-size: 8.5px; color: var(--text-3); margin-bottom: 10px;
167
- }
168
- .failure-item__message {
169
- background: var(--fail-dim);
170
- border: 1px solid #E5C9BD;
171
- border-left: 2px solid var(--fail);
172
- border-radius: 4px;
173
- padding: 9px 12px; margin-bottom: 10px;
174
- font-family: 'JetBrains Mono', monospace;
175
- font-size: 9.5px; color: #8A2E1E;
176
- word-break: break-word; line-height: 1.6;
177
- }
178
- .retry-history {
179
- font-size: 9px; color: var(--text-3); margin-top: 10px;
180
- padding-top: 8px; border-top: 1px solid var(--border);
181
- }
182
- .retry-attempt {
183
- display: inline-block; padding: 2px 7px;
184
- border-radius: 3px; margin-right: 4px; font-size: 8.5px;
185
- font-family: 'Inter', system-ui, sans-serif; font-weight: 600;
186
- letter-spacing: 0.4px; text-transform: uppercase;
187
- -webkit-print-color-adjust: exact;
188
- print-color-adjust: exact;
189
- }
190
- .retry-attempt--failed { background: var(--fail-dim); color: var(--fail); border: 1px solid #E5C9BD; }
191
- .retry-attempt--passed { background: var(--pass-dim); color: var(--pass); border: 1px solid #C5DBC9; }
146
+ /* ── Failure card · styles live in base.css (rendered inside the breakdown) ── */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reportforge/playwright-pdf",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "description": "Enterprise-ready PDF reports for Playwright Test — minimal, detailed, and executive templates with CI/CD integrations",
5
5
  "license": "MIT",
6
6
  "author": "ReportForge",
@@ -34,8 +34,7 @@
34
34
  }
35
35
  },
36
36
  "bin": {
37
- "reportforge-demo": "./dist/demo/cli.js",
38
- "reportforge-export-feedback": "./dist/cli/export-feedback.js"
37
+ "playwright-pdf": "./dist/cli/index.js"
39
38
  },
40
39
  "publishConfig": {
41
40
  "access": "public"
@@ -1,87 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
- var __create = Object.create;
4
- var __defProp = Object.defineProperty;
5
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
- var __getOwnPropNames = Object.getOwnPropertyNames;
7
- var __getProtoOf = Object.getPrototypeOf;
8
- var __hasOwnProp = Object.prototype.hasOwnProperty;
9
- var __export = (target, all) => {
10
- for (var name in all)
11
- __defProp(target, name, { get: all[name], enumerable: true });
12
- };
13
- var __copyProps = (to, from, except, desc) => {
14
- if (from && typeof from === "object" || typeof from === "function") {
15
- for (let key of __getOwnPropNames(from))
16
- if (!__hasOwnProp.call(to, key) && key !== except)
17
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
- }
19
- return to;
20
- };
21
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
- // If the importer is in node compatibility mode or this is not an ESM
23
- // file that has been converted to a CommonJS file using a Babel-
24
- // compatible transform (i.e. "__esModule" has not been set), then set
25
- // "default" to the CommonJS "module.exports" for node compatibility.
26
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
- mod
28
- ));
29
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
-
31
- // src/cli/export-feedback.ts
32
- var export_feedback_exports = {};
33
- __export(export_feedback_exports, {
34
- exportFeedback: () => exportFeedback
35
- });
36
- module.exports = __toCommonJS(export_feedback_exports);
37
- var import_node_fs = __toESM(require("fs"));
38
- var import_node_os = __toESM(require("os"));
39
- var import_node_path = __toESM(require("path"));
40
-
41
- // src/analysis/feedback-csv.ts
42
- var FEEDBACK_HEADER = "category,margin,text";
43
- function parseFeedbackRows(content) {
44
- return content.trim().split("\n").slice(1).filter(Boolean).map((line) => {
45
- const parts = line.split(",");
46
- return { category: parts[0], margin: parts[1], text: parts.slice(2).join(","), raw: line };
47
- });
48
- }
49
-
50
- // src/cli/export-feedback.ts
51
- function exportFeedback(deps = {}) {
52
- const home = deps.home ?? import_node_os.default.homedir();
53
- const cwd = deps.cwd ?? process.cwd();
54
- const root = import_node_path.default.join(home, ".reportforge");
55
- const rows = [];
56
- if (import_node_fs.default.existsSync(root)) {
57
- for (const entry of import_node_fs.default.readdirSync(root)) {
58
- const file = import_node_path.default.join(root, entry, "unclassified.csv");
59
- if (!import_node_fs.default.existsSync(file)) continue;
60
- for (const r of parseFeedbackRows(import_node_fs.default.readFileSync(file, "utf8"))) {
61
- rows.push(r.raw);
62
- }
63
- }
64
- }
65
- if (rows.length === 0) return { rowCount: 0, outFile: null };
66
- const outFile = import_node_path.default.join(cwd, "reportforge-feedback.csv");
67
- import_node_fs.default.writeFileSync(outFile, `${FEEDBACK_HEADER}
68
- ${rows.join("\n")}
69
- `, "utf8");
70
- return { rowCount: rows.length, outFile };
71
- }
72
- if (process.argv[1] && process.argv[1].endsWith("export-feedback.js")) {
73
- const res = exportFeedback();
74
- if (res.rowCount === 0) {
75
- process.stdout.write("No locally collected failures found (~/.reportforge/*/unclassified.csv).\n");
76
- } else {
77
- process.stdout.write(
78
- `Saved ${res.rowCount} row(s) to ${res.outFile} - tokenized + redacted, LOCAL only.
79
- Review it before sharing; there is no upload.
80
- `
81
- );
82
- }
83
- }
84
- // Annotate the CommonJS export names for ESM import in node:
85
- 0 && (module.exports = {
86
- exportFeedback
87
- });
@@ -1,42 +0,0 @@
1
- {{#if analysis.clusters.length}}
2
- <section class="failure-analysis">
3
- <h2>Failure Analysis</h2>
4
- <p class="oneliner">{{analysis.oneLiner}}</p>
5
-
6
- {{#if analysis.truncated}}
7
- <p class="analysis-truncated">
8
- Showing analysis of the first {{analysis.analysedCount}} of {{analysis.totalFailures}} failures.
9
- Raise <code>failureAnalysis.maxFailuresToAnalyse</code> for full coverage.
10
- </p>
11
- {{/if}}
12
-
13
- {{#each analysis.clusters}}
14
- <div class="cluster cluster--{{category}}">
15
- <div class="cluster-header">
16
- <span class="cluster-category">{{categoryLabel category}}</span>
17
- <span class="cluster-count">{{count}} {{pluralise count "test" "tests"}}</span>
18
- <span class="cluster-strength cluster-strength--{{strength}}">{{strengthLabel strength}} match</span>
19
- </div>
20
-
21
- {{#if stackFrame}}
22
- <div class="cluster-frame"><code>{{stackFrame}}</code></div>
23
- {{/if}}
24
-
25
- <div class="cluster-message">{{representativeMessage}}</div>
26
-
27
- <ul class="cluster-tests">
28
- {{#each (take tests 5)}}
29
- <li>{{this}}</li>
30
- {{/each}}
31
- {{#if (gt testsTotal 5)}}
32
- <li class="more">+{{subtract testsTotal 5}} more</li>
33
- {{/if}}
34
- </ul>
35
-
36
- {{#if (eq strength "weak")}}
37
- <p class="confidence-warning">* Weak match — manual review recommended</p>
38
- {{/if}}
39
- </div>
40
- {{/each}}
41
- </section>
42
- {{/if}}
@@ -1,62 +0,0 @@
1
- {{#if failures.length}}
2
- <div class="section">
3
- <h2>Failures ({{failures.length}}{{#if overflowFailureCount}} shown · {{overflowFailureCount}} more in sidecar{{/if}})</h2>
4
- {{#if overflowFailureCount}}
5
- <div class="overflow-banner">
6
- Showing the first {{failures.length}} failures. {{overflowFailureCount}} additional
7
- failure{{#if (gt overflowFailureCount 1)}}s are{{else}} is{{/if}} available in
8
- <code>{{overflowSidecarName}}</code> alongside this report.
9
- </div>
10
- {{/if}}
11
- {{#each failures}}
12
- <div class="failure-item failure-item--{{severity}} no-break">
13
- {{#if ../options.showFullFailures}}
14
-
15
- <div class="failure-item__header">
16
- <div>
17
- <div class="failure-item__title">{{testTitle}}</div>
18
- <div class="failure-item__path">{{join suitePath " › "}}</div>
19
- </div>
20
- <span class="badge badge--failed severity--{{severity}}" style="white-space:nowrap;">
21
- {{upperCase severity}}
22
- </span>
23
- </div>
24
-
25
- {{!-- Playwright's error.stack already begins with the message, so rendering
26
- both duplicates it. Show the formatted stack when available; otherwise
27
- fall back to the bare message. --}}
28
- {{#if (and error.stack ../options.showStackTraces)}}
29
- <pre>{{error.stack}}</pre>
30
- {{else}}
31
- <div class="failure-item__message">{{error.message}}</div>
32
- {{/if}}
33
-
34
- {{#if screenshotBase64}}
35
- <img class="screenshot" src="{{screenshotBase64}}" alt="Screenshot at failure">
36
- {{/if}}
37
-
38
- {{#if retryHistory.length}}
39
- <div class="retry-history">
40
- Retry history:&nbsp;
41
- {{#each retryHistory}}
42
- <span class="retry-attempt retry-attempt--{{status}}">
43
- #{{addOne attempt}} {{status}} · {{formatDuration duration}}
44
- </span>
45
- {{/each}}
46
- </div>
47
- {{/if}}
48
-
49
- {{else}}
50
- {{!-- Executive: title + suite only --}}
51
- <div class="failure-summary-item">
52
- <div>
53
- <div class="failure-summary-item__title">{{testTitle}}</div>
54
- <div class="failure-summary-item__suite">{{join suitePath " › "}}</div>
55
- </div>
56
- <span class="badge badge--failed">FAILED</span>
57
- </div>
58
- {{/if}}
59
- </div>
60
- {{/each}}
61
- </div>
62
- {{/if}}
@@ -1,27 +0,0 @@
1
- {{#if slowTests.length}}
2
- <div class="section">
3
- <h2>Slowest Tests</h2>
4
- <table>
5
- <thead>
6
- <tr>
7
- <th>Test</th>
8
- <th style="width:130px;">Suite</th>
9
- {{#if showProject}}<th style="width:80px;">Project</th>{{/if}}
10
- <th style="width:76px;text-align:right;">Duration</th>
11
- <th style="width:68px;">Status</th>
12
- </tr>
13
- </thead>
14
- <tbody>
15
- {{#each (take slowTests limit)}}
16
- <tr class="no-break">
17
- <td style="font-weight:500;">{{title}}</td>
18
- <td style="font-size:9px;color:var(--text-3);font-family:'JetBrains Mono',monospace;">{{suiteTitle}}</td>
19
- {{#if ../showProject}}<td style="font-size:9px;color:var(--text-3);white-space:nowrap;">{{project}}</td>{{/if}}
20
- <td style="font-family:'JetBrains Mono',monospace;font-size:9.5px;color:var(--text-2);text-align:right;">{{durationFormatted}}</td>
21
- <td>{{statusBadge status}}</td>
22
- </tr>
23
- {{/each}}
24
- </tbody>
25
- </table>
26
- </div>
27
- {{/if}}