@ng-prism/plugin-visual-regression 22.2.0-beta.2 → 22.2.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,14 @@
1
+ import type { PanelBadge, RuntimeComponent } from '@ng-prism/core/plugin';
2
+ /** True when the plugin recorded at least one result for this component. */
3
+ export declare function hasResults(component: RuntimeComponent): boolean;
4
+ /**
5
+ * How many of this component's variants are waiting on a person.
6
+ *
7
+ * The same set the panel's first group holds, counted from the same function,
8
+ * so the number on the tab and the number in the group header cannot disagree.
9
+ * Red once anything actually changed, amber while the only open items are ones
10
+ * that could not be compared — resized and new. Null when there is nothing to
11
+ * say, which is what keeps a clean component's tab free of a green "0".
12
+ */
13
+ export declare function reviewBadge(component: RuntimeComponent): PanelBadge | null;
14
+ //# sourceMappingURL=panel-contributions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"panel-contributions.d.ts","sourceRoot":"","sources":["../src/panel-contributions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAuB1E,4EAA4E;AAC5E,wBAAgB,UAAU,CAAC,SAAS,EAAE,gBAAgB,GAAG,OAAO,CAG/D;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,gBAAgB,GAAG,UAAU,GAAG,IAAI,CAe1E"}
@@ -0,0 +1,42 @@
1
+ import { groupRows } from './vrt-summarize.js';
2
+ /**
3
+ * The runtime contributions both entry points declare.
4
+ *
5
+ * `visual-regression-plugin.ts` carries the build-time hooks and must never be
6
+ * reachable from a browser bundle, so the browser entry cannot import through
7
+ * it — which used to mean the panel's visibility rule was written out twice,
8
+ * once per entry, with nothing but `entry-parity.spec.ts` noticing if the two
9
+ * drifted. This module is the half both can share: no `node:` imports, no
10
+ * Angular, just the predicates the panel definition needs.
11
+ */
12
+ function componentMeta(component) {
13
+ return (component.meta?.showcaseConfig?.meta?.['visualRegression'] ?? null);
14
+ }
15
+ /** True when the plugin recorded at least one result for this component. */
16
+ export function hasResults(component) {
17
+ const meta = componentMeta(component);
18
+ return Boolean(meta?.found && meta.variants.length > 0);
19
+ }
20
+ /**
21
+ * How many of this component's variants are waiting on a person.
22
+ *
23
+ * The same set the panel's first group holds, counted from the same function,
24
+ * so the number on the tab and the number in the group header cannot disagree.
25
+ * Red once anything actually changed, amber while the only open items are ones
26
+ * that could not be compared — resized and new. Null when there is nothing to
27
+ * say, which is what keeps a clean component's tab free of a green "0".
28
+ */
29
+ export function reviewBadge(component) {
30
+ const meta = componentMeta(component);
31
+ if (!meta?.found)
32
+ return null;
33
+ const review = groupRows(meta.variants).find((group) => group.key === 'review');
34
+ if (!review)
35
+ return null;
36
+ return {
37
+ text: String(review.count),
38
+ variant: review.rows.some((row) => row.status === 'changed')
39
+ ? 'danger'
40
+ : 'warn',
41
+ };
42
+ }
@@ -1,3 +1,4 @@
1
+ import { type VrtGroupKey } from './vrt-summarize.js';
1
2
  import type { VrtComponentMeta, VrtVariantResult } from './visual-regression.types.js';
2
3
  import * as i0 from "@angular/core";
3
4
  export declare class VisualRegressionPanelComponent {
@@ -10,11 +11,33 @@ export declare class VisualRegressionPanelComponent {
10
11
  protected readonly rows: import("@angular/core").Signal<{
11
12
  key: string;
12
13
  variant: VrtVariantResult;
14
+ status: import("./visual-regression.types.js").VrtStatus;
13
15
  label: string;
14
16
  statusLabel: string;
15
17
  tone: import("./vrt-summarize.js").VrtTone;
16
18
  diffLabel: string;
17
19
  }[]>;
20
+ protected readonly groups: import("@angular/core").Signal<import("./vrt-summarize.js").VrtGroup<{
21
+ key: string;
22
+ variant: VrtVariantResult;
23
+ status: import("./visual-regression.types.js").VrtStatus;
24
+ label: string;
25
+ statusLabel: string;
26
+ tone: import("./vrt-summarize.js").VrtTone;
27
+ diffLabel: string;
28
+ }>[]>;
29
+ /**
30
+ * Which groups the reader has opened.
31
+ *
32
+ * `null` means "nobody has touched this yet", which is what lets the default
33
+ * follow the data — shut while something needs review, the first group open
34
+ * when nothing does. A plain set initialised once would freeze the first
35
+ * component's answer and apply it to every component after it.
36
+ */
37
+ private readonly expanded;
38
+ private readonly expandedGroups;
39
+ protected isExpanded(key: VrtGroupKey): boolean;
40
+ protected toggleGroup(key: VrtGroupKey): void;
18
41
  protected readonly selected: import("@angular/core").Signal<VrtVariantResult | null>;
19
42
  static ɵfac: i0.ɵɵFactoryDeclaration<VisualRegressionPanelComponent, never>;
20
43
  static ɵcmp: i0.ɵɵComponentDeclaration<VisualRegressionPanelComponent, "prism-visual-regression-panel", never, { "activeComponent": { "alias": "activeComponent"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
@@ -1 +1 @@
1
- {"version":3,"file":"visual-regression-panel.component.d.ts","sourceRoot":"","sources":["../src/visual-regression-panel.component.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EACV,gBAAgB,EAChB,gBAAgB,EACjB,MAAM,8BAA8B,CAAC;;AAEtC,qBA2La,8BAA8B;IACzC,QAAQ,CAAC,eAAe,+CAAwB;IAEhD,SAAS,CAAC,QAAQ,CAAC,WAAW,wDAA+B;IAE7D,SAAS,CAAC,QAAQ,CAAC,IAAI,0DASpB;IAEH,SAAS,CAAC,QAAQ,CAAC,YAAY,yCAE7B;IAEF,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA+C;IAExE,SAAS,CAAC,QAAQ,CAAC,OAAO,0EAA8C;IAExE,SAAS,CAAC,QAAQ,CAAC,IAAI;;;;;;;SAYrB;IAEF,SAAS,CAAC,QAAQ,CAAC,QAAQ,0DAWxB;yCAjDQ,8BAA8B;2CAA9B,8BAA8B;CAkD1C"}
1
+ {"version":3,"file":"visual-regression-panel.component.d.ts","sourceRoot":"","sources":["../src/visual-regression-panel.component.ts"],"names":[],"mappings":"AASA,OAAO,EAOL,KAAK,WAAW,EACjB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EACV,gBAAgB,EAChB,gBAAgB,EACjB,MAAM,8BAA8B,CAAC;;AAEtC,qBA2Ra,8BAA8B;IACzC,QAAQ,CAAC,eAAe,+CAAwB;IAEhD,SAAS,CAAC,QAAQ,CAAC,WAAW,wDAA+B;IAE7D,SAAS,CAAC,QAAQ,CAAC,IAAI,0DASpB;IAEH,SAAS,CAAC,QAAQ,CAAC,YAAY,yCAE7B;IAEF,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA+C;IAExE,SAAS,CAAC,QAAQ,CAAC,OAAO,0EAA8C;IAExE,SAAS,CAAC,QAAQ,CAAC,IAAI;;;;;;;;SAarB;IAEF,SAAS,CAAC,QAAQ,CAAC,MAAM;;;;;;;;UAA0C;IAEnE;;;;;;;OAOG;IACH,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiD;IAE1E,OAAO,CAAC,QAAQ,CAAC,cAAc,CAE7B;IAEF,SAAS,CAAC,UAAU,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO;IAI/C,SAAS,CAAC,WAAW,CAAC,GAAG,EAAE,WAAW,GAAG,IAAI;IAM7C,SAAS,CAAC,QAAQ,CAAC,QAAQ,0DAWxB;yCA5EQ,8BAA8B;2CAA9B,8BAA8B;CA6E1C"}
@@ -1,34 +1,81 @@
1
1
  import { ChangeDetectionStrategy, Component, computed, input, signal, } from '@angular/core';
2
2
  import { VrtCompareComponent } from './vrt-compare.component.js';
3
3
  import { VrtSummaryComponent } from './vrt-summary.component.js';
4
- import { formatPercent, STATUS_LABEL, STATUS_TONE, summarize, } from './vrt-summarize.js';
4
+ import { defaultExpandedGroups, formatPercent, groupRows, STATUS_LABEL, STATUS_TONE, summarize, } from './vrt-summarize.js';
5
5
  import * as i0 from "@angular/core";
6
6
  const _forTrack0 = ($index, $item) => $item.key;
7
- function VisualRegressionPanelComponent_Conditional_0_Conditional_0_For_6_Template(rf, ctx) { if (rf & 1) {
7
+ function VisualRegressionPanelComponent_Conditional_0_Conditional_0_For_6_Conditional_0_Template(rf, ctx) { if (rf & 1) {
8
8
  const _r1 = i0.ɵɵgetCurrentView();
9
- i0.ɵɵelementStart(0, "li")(1, "button", 8);
10
- i0.ɵɵlistener("click", function VisualRegressionPanelComponent_Conditional_0_Conditional_0_For_6_Template_button_click_1_listener() { const row_r2 = i0.ɵɵrestoreView(_r1).$implicit; const ctx_r2 = i0.ɵɵnextContext(3); return i0.ɵɵresetView(ctx_r2.selectedKey.set(row_r2.key)); });
11
- i0.ɵɵelementStart(2, "span", 9);
9
+ i0.ɵɵelementStart(0, "button", 11);
10
+ i0.ɵɵlistener("click", function VisualRegressionPanelComponent_Conditional_0_Conditional_0_For_6_Conditional_0_Template_button_click_0_listener() { i0.ɵɵrestoreView(_r1); const group_r2 = i0.ɵɵnextContext().$implicit; const ctx_r2 = i0.ɵɵnextContext(3); return i0.ɵɵresetView(ctx_r2.toggleGroup(group_r2.key)); });
11
+ i0.ɵɵelementStart(1, "span", 12);
12
+ i0.ɵɵtext(2);
13
+ i0.ɵɵelementEnd();
14
+ i0.ɵɵelementStart(3, "span", 13);
15
+ i0.ɵɵtext(4, "\u25B6");
16
+ i0.ɵɵelementEnd()();
17
+ } if (rf & 2) {
18
+ const group_r2 = i0.ɵɵnextContext().$implicit;
19
+ const ctx_r2 = i0.ɵɵnextContext(3);
20
+ i0.ɵɵattribute("aria-expanded", ctx_r2.isExpanded(group_r2.key));
21
+ i0.ɵɵadvance(2);
22
+ i0.ɵɵtextInterpolate2("", group_r2.label, " \u00B7 ", group_r2.count);
23
+ i0.ɵɵadvance();
24
+ i0.ɵɵclassProp("vrt__caret--open", ctx_r2.isExpanded(group_r2.key));
25
+ } }
26
+ function VisualRegressionPanelComponent_Conditional_0_Conditional_0_For_6_Conditional_1_Template(rf, ctx) { if (rf & 1) {
27
+ i0.ɵɵelementStart(0, "p", 9)(1, "span", 12);
28
+ i0.ɵɵtext(2);
29
+ i0.ɵɵelementEnd()();
30
+ } if (rf & 2) {
31
+ const group_r2 = i0.ɵɵnextContext().$implicit;
32
+ i0.ɵɵadvance(2);
33
+ i0.ɵɵtextInterpolate2("", group_r2.label, " \u00B7 ", group_r2.count);
34
+ } }
35
+ function VisualRegressionPanelComponent_Conditional_0_Conditional_0_For_6_Conditional_2_For_2_Template(rf, ctx) { if (rf & 1) {
36
+ const _r4 = i0.ɵɵgetCurrentView();
37
+ i0.ɵɵelementStart(0, "li")(1, "button", 14);
38
+ i0.ɵɵlistener("click", function VisualRegressionPanelComponent_Conditional_0_Conditional_0_For_6_Conditional_2_For_2_Template_button_click_1_listener() { const row_r5 = i0.ɵɵrestoreView(_r4).$implicit; const ctx_r2 = i0.ɵɵnextContext(5); return i0.ɵɵresetView(ctx_r2.selectedKey.set(row_r5.key)); });
39
+ i0.ɵɵelementStart(2, "span", 15);
12
40
  i0.ɵɵtext(3);
13
41
  i0.ɵɵelementEnd();
14
- i0.ɵɵelementStart(4, "span", 10);
42
+ i0.ɵɵelementStart(4, "span", 16);
15
43
  i0.ɵɵtext(5);
16
44
  i0.ɵɵelementEnd();
17
- i0.ɵɵelementStart(6, "span", 11);
45
+ i0.ɵɵelementStart(6, "span", 17);
18
46
  i0.ɵɵtext(7);
19
47
  i0.ɵɵelementEnd()()();
20
48
  } if (rf & 2) {
21
- const row_r2 = ctx.$implicit;
22
- const ctx_r2 = i0.ɵɵnextContext(3);
49
+ const row_r5 = ctx.$implicit;
50
+ const ctx_r2 = i0.ɵɵnextContext(5);
23
51
  i0.ɵɵadvance();
24
- i0.ɵɵclassProp("vrt__row--active", row_r2.variant === ctx_r2.selected());
25
- i0.ɵɵattribute("data-tone", row_r2.tone);
52
+ i0.ɵɵclassProp("vrt__row--active", row_r5.variant === ctx_r2.selected());
53
+ i0.ɵɵattribute("data-tone", row_r5.tone);
26
54
  i0.ɵɵadvance(2);
27
- i0.ɵɵtextInterpolate(row_r2.label);
55
+ i0.ɵɵtextInterpolate(row_r5.label);
28
56
  i0.ɵɵadvance(2);
29
- i0.ɵɵtextInterpolate(row_r2.statusLabel);
57
+ i0.ɵɵtextInterpolate(row_r5.statusLabel);
30
58
  i0.ɵɵadvance(2);
31
- i0.ɵɵtextInterpolate(row_r2.diffLabel);
59
+ i0.ɵɵtextInterpolate(row_r5.diffLabel);
60
+ } }
61
+ function VisualRegressionPanelComponent_Conditional_0_Conditional_0_For_6_Conditional_2_Template(rf, ctx) { if (rf & 1) {
62
+ i0.ɵɵelementStart(0, "ul", 10);
63
+ i0.ɵɵrepeaterCreate(1, VisualRegressionPanelComponent_Conditional_0_Conditional_0_For_6_Conditional_2_For_2_Template, 8, 6, "li", null, _forTrack0);
64
+ i0.ɵɵelementEnd();
65
+ } if (rf & 2) {
66
+ const group_r2 = i0.ɵɵnextContext().$implicit;
67
+ i0.ɵɵadvance();
68
+ i0.ɵɵrepeater(group_r2.rows);
69
+ } }
70
+ function VisualRegressionPanelComponent_Conditional_0_Conditional_0_For_6_Template(rf, ctx) { if (rf & 1) {
71
+ i0.ɵɵconditionalCreate(0, VisualRegressionPanelComponent_Conditional_0_Conditional_0_For_6_Conditional_0_Template, 5, 5, "button", 8)(1, VisualRegressionPanelComponent_Conditional_0_Conditional_0_For_6_Conditional_1_Template, 3, 2, "p", 9);
72
+ i0.ɵɵconditionalCreate(2, VisualRegressionPanelComponent_Conditional_0_Conditional_0_For_6_Conditional_2_Template, 3, 0, "ul", 10);
73
+ } if (rf & 2) {
74
+ const group_r2 = ctx.$implicit;
75
+ const ctx_r2 = i0.ɵɵnextContext(3);
76
+ i0.ɵɵconditional(group_r2.collapsible ? 0 : 1);
77
+ i0.ɵɵadvance(2);
78
+ i0.ɵɵconditional(!group_r2.collapsible || ctx_r2.isExpanded(group_r2.key) ? 2 : -1);
32
79
  } }
33
80
  function VisualRegressionPanelComponent_Conditional_0_Conditional_0_Conditional_8_Template(rf, ctx) { if (rf & 1) {
34
81
  i0.ɵɵelement(0, "prism-vrt-compare", 7);
@@ -42,22 +89,22 @@ function VisualRegressionPanelComponent_Conditional_0_Conditional_0_Conditional_
42
89
  i0.ɵɵelementEnd();
43
90
  } }
44
91
  function VisualRegressionPanelComponent_Conditional_0_Conditional_0_Template(rf, ctx) { if (rf & 1) {
45
- i0.ɵɵelementStart(0, "div", 1)(1, "div", 2);
46
- i0.ɵɵelement(2, "prism-vrt-summary", 3);
47
- i0.ɵɵelementEnd();
48
- i0.ɵɵelementStart(3, "div", 4)(4, "ul", 5);
49
- i0.ɵɵrepeaterCreate(5, VisualRegressionPanelComponent_Conditional_0_Conditional_0_For_6_Template, 8, 6, "li", null, _forTrack0);
92
+ i0.ɵɵelementStart(0, "div", 1)(1, "div", 2)(2, "div", 3);
93
+ i0.ɵɵelement(3, "prism-vrt-summary", 4);
50
94
  i0.ɵɵelementEnd();
95
+ i0.ɵɵelementStart(4, "div", 5);
96
+ i0.ɵɵrepeaterCreate(5, VisualRegressionPanelComponent_Conditional_0_Conditional_0_For_6_Template, 3, 2, null, null, _forTrack0);
97
+ i0.ɵɵelementEnd()();
51
98
  i0.ɵɵelementStart(7, "div", 6);
52
99
  i0.ɵɵconditionalCreate(8, VisualRegressionPanelComponent_Conditional_0_Conditional_0_Conditional_8_Template, 1, 2, "prism-vrt-compare", 7)(9, VisualRegressionPanelComponent_Conditional_0_Conditional_0_Conditional_9_Template, 2, 0, "p", 0);
53
- i0.ɵɵelementEnd()()();
100
+ i0.ɵɵelementEnd()();
54
101
  } if (rf & 2) {
55
102
  let tmp_5_0;
56
103
  const ctx_r2 = i0.ɵɵnextContext(2);
57
- i0.ɵɵadvance(2);
58
- i0.ɵɵproperty("summary", ctx_r2.summary());
59
104
  i0.ɵɵadvance(3);
60
- i0.ɵɵrepeater(ctx_r2.rows());
105
+ i0.ɵɵproperty("summary", ctx_r2.summary());
106
+ i0.ɵɵadvance(2);
107
+ i0.ɵɵrepeater(ctx_r2.groups());
61
108
  i0.ɵɵadvance(3);
62
109
  i0.ɵɵconditional((tmp_5_0 = ctx_r2.selected()) ? 8 : 9, tmp_5_0);
63
110
  } }
@@ -105,6 +152,7 @@ export class VisualRegressionPanelComponent {
105
152
  rows = computed(() => this.variants().map((variant) => ({
106
153
  key: `${variant.className}:${variant.variantIndex}`,
107
154
  variant,
155
+ status: variant.status,
108
156
  label: variant.variantName ??
109
157
  variant.title ??
110
158
  `Variant ${variant.variantIndex}`,
@@ -113,6 +161,29 @@ export class VisualRegressionPanelComponent {
113
161
  diffLabel: formatDiff(variant),
114
162
  })), /* @ts-ignore */
115
163
  ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
164
+ groups = computed(() => groupRows(this.rows()), /* @ts-ignore */
165
+ ...(ngDevMode ? [{ debugName: "groups" }] : /* istanbul ignore next */ []));
166
+ /**
167
+ * Which groups the reader has opened.
168
+ *
169
+ * `null` means "nobody has touched this yet", which is what lets the default
170
+ * follow the data — shut while something needs review, the first group open
171
+ * when nothing does. A plain set initialised once would freeze the first
172
+ * component's answer and apply it to every component after it.
173
+ */
174
+ expanded = signal(null, /* @ts-ignore */
175
+ ...(ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []));
176
+ expandedGroups = computed(() => this.expanded() ?? new Set(defaultExpandedGroups(this.groups())), /* @ts-ignore */
177
+ ...(ngDevMode ? [{ debugName: "expandedGroups" }] : /* istanbul ignore next */ []));
178
+ isExpanded(key) {
179
+ return this.expandedGroups().has(key);
180
+ }
181
+ toggleGroup(key) {
182
+ const next = new Set(this.expandedGroups());
183
+ if (!next.delete(key))
184
+ next.add(key);
185
+ this.expanded.set(next);
186
+ }
116
187
  selected = computed(() => {
117
188
  const rows = this.rows();
118
189
  if (!rows.length)
@@ -126,49 +197,76 @@ export class VisualRegressionPanelComponent {
126
197
  }, /* @ts-ignore */
127
198
  ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
128
199
  static ɵfac = function VisualRegressionPanelComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || VisualRegressionPanelComponent)(); };
129
- static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: VisualRegressionPanelComponent, selectors: [["prism-visual-regression-panel"]], inputs: { activeComponent: [1, "activeComponent"] }, decls: 2, vars: 1, consts: [[1, "vrt__empty"], [1, "vrt"], [1, "vrt__summary"], [3, "summary"], [1, "vrt__split"], [1, "vrt__list"], [1, "vrt__viewer"], [3, "variant", "assetBaseUrl"], ["type", "button", 1, "vrt__row", 3, "click"], [1, "vrt__name"], [1, "vrt__status"], [1, "vrt__diff"]], template: function VisualRegressionPanelComponent_Template(rf, ctx) { if (rf & 1) {
200
+ static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: VisualRegressionPanelComponent, selectors: [["prism-visual-regression-panel"]], inputs: { activeComponent: [1, "activeComponent"] }, decls: 2, vars: 1, consts: [[1, "vrt__empty"], [1, "vrt"], [1, "vrt__aside"], [1, "vrt__summary"], [3, "summary"], [1, "vrt__list"], [1, "vrt__viewer"], [3, "variant", "assetBaseUrl"], ["type", "button", 1, "vrt__group", "vrt__group--toggle"], [1, "vrt__group"], [1, "vrt__rows"], ["type", "button", 1, "vrt__group", "vrt__group--toggle", 3, "click"], [1, "vrt__group-label"], ["aria-hidden", "true", 1, "vrt__caret"], ["type", "button", 1, "vrt__row", 3, "click"], [1, "vrt__name"], [1, "vrt__status"], [1, "vrt__diff"]], template: function VisualRegressionPanelComponent_Template(rf, ctx) { if (rf & 1) {
130
201
  i0.ɵɵconditionalCreate(0, VisualRegressionPanelComponent_Conditional_0_Template, 2, 1)(1, VisualRegressionPanelComponent_Conditional_1_Template, 8, 0, "div", 0);
131
202
  } if (rf & 2) {
132
203
  let tmp_0_0;
133
204
  i0.ɵɵconditional((tmp_0_0 = ctx.meta()) ? 0 : 1, tmp_0_0);
134
- } }, dependencies: [VrtCompareComponent, VrtSummaryComponent], styles: ["[_nghost-%COMP%] {\n display: block;\n height: 100%;\n overflow: auto;\n font-size: var(--fs-md);\n color: var(--prism-text);\n }\n\n .vrt[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n min-height: 100%;\n }\n\n .vrt__summary[_ngcontent-%COMP%] { padding: 14px 16px 0; }\n\n .vrt__split[_ngcontent-%COMP%] {\n display: grid;\n grid-template-columns: minmax(200px, 250px) 1fr;\n flex: 1;\n min-height: 0;\n padding-top: 14px;\n }\n\n .vrt__list[_ngcontent-%COMP%] {\n margin: 0;\n padding: 0 4px 14px 16px;\n list-style: none;\n display: flex;\n flex-direction: column;\n gap: 6px;\n min-width: 0;\n }\n\n \n\n .vrt__row[_ngcontent-%COMP%] {\n display: grid;\n grid-template-columns: 1fr auto;\n grid-template-areas: 'name diff' 'status diff';\n align-items: center;\n gap: 0 10px;\n width: 100%;\n padding: 8px 11px;\n background: var(--prism-bg-surface);\n border: 1px solid var(--prism-border);\n border-left: 3px solid var(--tone-color, var(--prism-border-strong));\n border-radius: var(--radius-md);\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n transition: border-color var(--dur-fast), background var(--dur-fast);\n }\n .vrt__row[_ngcontent-%COMP%]:hover { border-color: var(--prism-border-strong); }\n .vrt__row[_ngcontent-%COMP%]:hover { border-left-color: var(--tone-color, var(--prism-border-strong)); }\n .vrt__row--active[_ngcontent-%COMP%] {\n background: color-mix(in srgb, var(--prism-primary) 10%, var(--prism-bg-surface));\n border-color: color-mix(in srgb, var(--prism-primary) 40%, transparent);\n border-left-color: var(--tone-color, var(--prism-primary));\n }\n\n .vrt__name[_ngcontent-%COMP%] {\n grid-area: name;\n font-weight: 600;\n font-size: var(--fs-md);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n .vrt__status[_ngcontent-%COMP%] {\n grid-area: status;\n justify-self: start;\n margin-top: 3px;\n font-family: var(--font-mono);\n font-size: 9.5px;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n padding: 2px 6px;\n border-radius: var(--radius-xs);\n background: color-mix(in srgb, var(--tone-color) 15%, transparent);\n color: var(--tone-color);\n }\n\n .vrt__diff[_ngcontent-%COMP%] {\n grid-area: diff;\n font-family: var(--font-mono);\n font-size: var(--fs-sm);\n color: var(--prism-text-muted);\n white-space: nowrap;\n }\n\n [data-tone='success'][_ngcontent-%COMP%] { --tone-color: var(--prism-success); }\n [data-tone='danger'][_ngcontent-%COMP%] { --tone-color: var(--prism-danger); }\n [data-tone='warn'][_ngcontent-%COMP%] { --tone-color: var(--prism-warn); }\n [data-tone='neutral'][_ngcontent-%COMP%] { --tone-color: var(--prism-primary); }\n [data-tone='muted'][_ngcontent-%COMP%] { --tone-color: var(--prism-text-muted); }\n\n .vrt__viewer[_ngcontent-%COMP%] {\n min-width: 0;\n min-height: 0;\n border-left: 1px solid var(--prism-border);\n }\n\n .vrt__empty[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 4px;\n min-height: 140px;\n padding: 24px;\n text-align: center;\n font-size: var(--fs-md);\n }\n .vrt__empty[_ngcontent-%COMP%] b[_ngcontent-%COMP%] { font-weight: 600; color: var(--prism-text-2); }\n .vrt__empty[_ngcontent-%COMP%] span[_ngcontent-%COMP%] { color: var(--prism-text-muted); }\n .vrt__empty[_ngcontent-%COMP%] code[_ngcontent-%COMP%] {\n font-family: var(--font-mono);\n font-size: var(--fs-sm);\n padding: 1px 5px;\n border-radius: var(--radius-xs);\n background: var(--prism-input-bg);\n }\n\n @media (max-width: 860px) {\n .vrt__split[_ngcontent-%COMP%] { grid-template-columns: 1fr; }\n .vrt__list[_ngcontent-%COMP%] { padding-right: 16px; }\n .vrt__viewer[_ngcontent-%COMP%] { border-left: none; border-top: 1px solid var(--prism-border); }\n }"] });
205
+ } }, dependencies: [VrtCompareComponent, VrtSummaryComponent], styles: ["\n\n\n\n\n\n\n\n\n\n\n [_nghost-%COMP%] {\n display: block;\n height: 100%;\n overflow: hidden;\n font-size: var(--fs-md);\n color: var(--prism-text);\n }\n\n .vrt[_ngcontent-%COMP%] {\n display: grid;\n grid-template-columns: minmax(200px, 250px) 1fr;\n height: 100%;\n min-height: 0;\n }\n\n .vrt__aside[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n min-height: 0;\n min-width: 0;\n }\n\n .vrt__summary[_ngcontent-%COMP%] {\n flex: none;\n padding: 12px 12px 11px 16px;\n border-bottom: 1px solid var(--prism-border);\n }\n\n .vrt__list[_ngcontent-%COMP%] {\n flex: 1;\n min-height: 0;\n overflow: auto;\n margin: 0;\n padding: 12px 4px 12px 16px;\n }\n\n .vrt__rows[_ngcontent-%COMP%] {\n margin: 0 0 2px;\n padding: 0;\n list-style: none;\n display: flex;\n flex-direction: column;\n gap: 6px;\n }\n\n \n\n\n\n .vrt__group[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n width: 100%;\n margin: 0;\n padding: 10px 8px 6px;\n background: none;\n border: none;\n color: inherit;\n font: inherit;\n text-align: left;\n }\n .vrt__group--toggle[_ngcontent-%COMP%] { cursor: pointer; }\n .vrt__group--toggle[_ngcontent-%COMP%]:hover .vrt__group-label[_ngcontent-%COMP%] { color: var(--prism-text-2); }\n\n .vrt__group-label[_ngcontent-%COMP%] {\n font-size: var(--fs-sm);\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--prism-text-muted);\n transition: color var(--dur-fast);\n }\n\n .vrt__caret[_ngcontent-%COMP%] {\n font-size: 9px;\n line-height: 1;\n color: var(--prism-text-ghost);\n transition: transform var(--dur-fast) var(--ease-default);\n }\n .vrt__caret--open[_ngcontent-%COMP%] { transform: rotate(90deg); }\n\n \n\n .vrt__row[_ngcontent-%COMP%] {\n display: grid;\n grid-template-columns: 1fr auto;\n grid-template-areas: 'name diff' 'status diff';\n align-items: center;\n gap: 0 10px;\n width: 100%;\n padding: 8px 11px;\n background: var(--prism-bg-surface);\n border: 1px solid var(--prism-border);\n border-left: 3px solid var(--tone-color, var(--prism-border-strong));\n border-radius: var(--radius-md);\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n transition: border-color var(--dur-fast), background var(--dur-fast);\n }\n .vrt__row[_ngcontent-%COMP%]:hover { border-color: var(--prism-border-strong); }\n .vrt__row[_ngcontent-%COMP%]:hover { border-left-color: var(--tone-color, var(--prism-border-strong)); }\n .vrt__row--active[_ngcontent-%COMP%] {\n background: color-mix(in srgb, var(--prism-primary) 10%, var(--prism-bg-surface));\n border-color: color-mix(in srgb, var(--prism-primary) 40%, transparent);\n border-left-color: var(--tone-color, var(--prism-primary));\n }\n\n .vrt__name[_ngcontent-%COMP%] {\n grid-area: name;\n font-weight: 600;\n font-size: var(--fs-md);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n .vrt__status[_ngcontent-%COMP%] {\n grid-area: status;\n justify-self: start;\n margin-top: 3px;\n font-family: var(--font-mono);\n font-size: 9.5px;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n padding: 2px 6px;\n border-radius: var(--radius-xs);\n background: color-mix(in srgb, var(--tone-color) 15%, transparent);\n color: var(--tone-color);\n }\n\n .vrt__diff[_ngcontent-%COMP%] {\n grid-area: diff;\n font-family: var(--font-mono);\n font-size: var(--fs-sm);\n color: var(--prism-text-muted);\n white-space: nowrap;\n }\n\n [data-tone='success'][_ngcontent-%COMP%] { --tone-color: var(--prism-success); }\n [data-tone='danger'][_ngcontent-%COMP%] { --tone-color: var(--prism-danger); }\n [data-tone='warn'][_ngcontent-%COMP%] { --tone-color: var(--prism-warn); }\n [data-tone='neutral'][_ngcontent-%COMP%] { --tone-color: var(--prism-primary); }\n [data-tone='muted'][_ngcontent-%COMP%] { --tone-color: var(--prism-text-muted); }\n\n .vrt__viewer[_ngcontent-%COMP%] {\n min-width: 0;\n min-height: 0;\n overflow: auto;\n border-left: 1px solid var(--prism-border);\n }\n\n .vrt__empty[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 4px;\n min-height: 140px;\n padding: 24px;\n text-align: center;\n font-size: var(--fs-md);\n }\n .vrt__empty[_ngcontent-%COMP%] b[_ngcontent-%COMP%] { font-weight: 600; color: var(--prism-text-2); }\n .vrt__empty[_ngcontent-%COMP%] span[_ngcontent-%COMP%] { color: var(--prism-text-muted); }\n .vrt__empty[_ngcontent-%COMP%] code[_ngcontent-%COMP%] {\n font-family: var(--font-mono);\n font-size: var(--fs-sm);\n padding: 1px 5px;\n border-radius: var(--radius-xs);\n background: var(--prism-input-bg);\n }\n\n \n\n\n @media (max-width: 860px) {\n [_nghost-%COMP%] { overflow: auto; }\n .vrt[_ngcontent-%COMP%] { grid-template-columns: 1fr; height: auto; }\n .vrt__list[_ngcontent-%COMP%] { overflow: visible; padding-right: 16px; }\n .vrt__viewer[_ngcontent-%COMP%] {\n overflow: visible;\n border-left: none;\n border-top: 1px solid var(--prism-border);\n }\n }"] });
135
206
  }
136
207
  (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(VisualRegressionPanelComponent, [{
137
208
  type: Component,
138
209
  args: [{ selector: 'prism-visual-regression-panel', standalone: true, imports: [VrtCompareComponent, VrtSummaryComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
139
210
  @if (meta(); as m) { @if (m.found) {
140
211
  <div class="vrt">
141
- <div class="vrt__summary">
142
- <prism-vrt-summary [summary]="summary()" />
143
- </div>
212
+ <div class="vrt__aside">
213
+ <div class="vrt__summary">
214
+ <prism-vrt-summary [summary]="summary()" />
215
+ </div>
144
216
 
145
- <div class="vrt__split">
146
- <ul class="vrt__list">
147
- @for (row of rows(); track row.key) {
148
- <li>
149
- <button
150
- type="button"
151
- class="vrt__row"
152
- [attr.data-tone]="row.tone"
153
- [class.vrt__row--active]="row.variant === selected()"
154
- (click)="selectedKey.set(row.key)"
217
+ <div class="vrt__list">
218
+ @for (group of groups(); track group.key) { @if (group.collapsible) {
219
+ <button
220
+ type="button"
221
+ class="vrt__group vrt__group--toggle"
222
+ [attr.aria-expanded]="isExpanded(group.key)"
223
+ (click)="toggleGroup(group.key)"
224
+ >
225
+ <span class="vrt__group-label"
226
+ >{{ group.label }} · {{ group.count }}</span
155
227
  >
156
- <span class="vrt__name">{{ row.label }}</span>
157
- <span class="vrt__status">{{ row.statusLabel }}</span>
158
- <span class="vrt__diff">{{ row.diffLabel }}</span>
159
- </button>
160
- </li>
161
- }
162
- </ul>
163
-
164
- <div class="vrt__viewer">
165
- @if (selected(); as v) {
166
- <prism-vrt-compare [variant]="v" [assetBaseUrl]="assetBaseUrl()" />
228
+ <span
229
+ class="vrt__caret"
230
+ [class.vrt__caret--open]="isExpanded(group.key)"
231
+ aria-hidden="true"
232
+ >&#9654;</span
233
+ >
234
+ </button>
167
235
  } @else {
168
- <p class="vrt__empty">Select a variant.</p>
169
- }
236
+ <p class="vrt__group">
237
+ <span class="vrt__group-label"
238
+ >{{ group.label }} · {{ group.count }}</span
239
+ >
240
+ </p>
241
+ } @if (!group.collapsible || isExpanded(group.key)) {
242
+ <ul class="vrt__rows">
243
+ @for (row of group.rows; track row.key) {
244
+ <li>
245
+ <button
246
+ type="button"
247
+ class="vrt__row"
248
+ [attr.data-tone]="row.tone"
249
+ [class.vrt__row--active]="row.variant === selected()"
250
+ (click)="selectedKey.set(row.key)"
251
+ >
252
+ <span class="vrt__name">{{ row.label }}</span>
253
+ <span class="vrt__status">{{ row.statusLabel }}</span>
254
+ <span class="vrt__diff">{{ row.diffLabel }}</span>
255
+ </button>
256
+ </li>
257
+ }
258
+ </ul>
259
+ } }
170
260
  </div>
171
261
  </div>
262
+
263
+ <div class="vrt__viewer">
264
+ @if (selected(); as v) {
265
+ <prism-vrt-compare [variant]="v" [assetBaseUrl]="assetBaseUrl()" />
266
+ } @else {
267
+ <p class="vrt__empty">Select a variant.</p>
268
+ }
269
+ </div>
172
270
  </div>
173
271
  } @else {
174
272
  <div class="vrt__empty">
@@ -181,9 +279,9 @@ export class VisualRegressionPanelComponent {
181
279
  <span>Check the plugin's <code>reportPath</code>.</span>
182
280
  </div>
183
281
  }
184
- `, styles: ["\n :host {\n display: block;\n height: 100%;\n overflow: auto;\n font-size: var(--fs-md);\n color: var(--prism-text);\n }\n\n .vrt {\n display: flex;\n flex-direction: column;\n min-height: 100%;\n }\n\n .vrt__summary { padding: 14px 16px 0; }\n\n .vrt__split {\n display: grid;\n grid-template-columns: minmax(200px, 250px) 1fr;\n flex: 1;\n min-height: 0;\n padding-top: 14px;\n }\n\n .vrt__list {\n margin: 0;\n padding: 0 4px 14px 16px;\n list-style: none;\n display: flex;\n flex-direction: column;\n gap: 6px;\n min-width: 0;\n }\n\n /* Same card-with-severity-edge as the a11y violations list, so the two\n panels read as one product. */\n .vrt__row {\n display: grid;\n grid-template-columns: 1fr auto;\n grid-template-areas: 'name diff' 'status diff';\n align-items: center;\n gap: 0 10px;\n width: 100%;\n padding: 8px 11px;\n background: var(--prism-bg-surface);\n border: 1px solid var(--prism-border);\n border-left: 3px solid var(--tone-color, var(--prism-border-strong));\n border-radius: var(--radius-md);\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n transition: border-color var(--dur-fast), background var(--dur-fast);\n }\n .vrt__row:hover { border-color: var(--prism-border-strong); }\n .vrt__row:hover { border-left-color: var(--tone-color, var(--prism-border-strong)); }\n .vrt__row--active {\n background: color-mix(in srgb, var(--prism-primary) 10%, var(--prism-bg-surface));\n border-color: color-mix(in srgb, var(--prism-primary) 40%, transparent);\n border-left-color: var(--tone-color, var(--prism-primary));\n }\n\n .vrt__name {\n grid-area: name;\n font-weight: 600;\n font-size: var(--fs-md);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n .vrt__status {\n grid-area: status;\n justify-self: start;\n margin-top: 3px;\n font-family: var(--font-mono);\n font-size: 9.5px;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n padding: 2px 6px;\n border-radius: var(--radius-xs);\n background: color-mix(in srgb, var(--tone-color) 15%, transparent);\n color: var(--tone-color);\n }\n\n .vrt__diff {\n grid-area: diff;\n font-family: var(--font-mono);\n font-size: var(--fs-sm);\n color: var(--prism-text-muted);\n white-space: nowrap;\n }\n\n [data-tone='success'] { --tone-color: var(--prism-success); }\n [data-tone='danger'] { --tone-color: var(--prism-danger); }\n [data-tone='warn'] { --tone-color: var(--prism-warn); }\n [data-tone='neutral'] { --tone-color: var(--prism-primary); }\n [data-tone='muted'] { --tone-color: var(--prism-text-muted); }\n\n .vrt__viewer {\n min-width: 0;\n min-height: 0;\n border-left: 1px solid var(--prism-border);\n }\n\n .vrt__empty {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 4px;\n min-height: 140px;\n padding: 24px;\n text-align: center;\n font-size: var(--fs-md);\n }\n .vrt__empty b { font-weight: 600; color: var(--prism-text-2); }\n .vrt__empty span { color: var(--prism-text-muted); }\n .vrt__empty code {\n font-family: var(--font-mono);\n font-size: var(--fs-sm);\n padding: 1px 5px;\n border-radius: var(--radius-xs);\n background: var(--prism-input-bg);\n }\n\n @media (max-width: 860px) {\n .vrt__split { grid-template-columns: 1fr; }\n .vrt__list { padding-right: 16px; }\n .vrt__viewer { border-left: none; border-top: 1px solid var(--prism-border); }\n }\n "] }]
282
+ `, styles: ["\n /*\n * Not a scroll container.\n *\n * It used to be overflow:auto, which made the whole panel one scroller:\n * both columns simply grew and the panel scrolled as a unit, so picking a\n * variant further down the list scrolled the comparison off the top and\n * you had to scroll back up to see what you had picked. The two columns\n * own their scrolling now, and the chain of min-height:0 below is what\n * lets them \u2014 a grid or flex item defaults to min-height:auto, refuses\n * to shrink below its content, and pushes the overflow back up here.\n */\n :host {\n display: block;\n height: 100%;\n overflow: hidden;\n font-size: var(--fs-md);\n color: var(--prism-text);\n }\n\n .vrt {\n display: grid;\n grid-template-columns: minmax(200px, 250px) 1fr;\n height: 100%;\n min-height: 0;\n }\n\n .vrt__aside {\n display: flex;\n flex-direction: column;\n min-height: 0;\n min-width: 0;\n }\n\n .vrt__summary {\n flex: none;\n padding: 12px 12px 11px 16px;\n border-bottom: 1px solid var(--prism-border);\n }\n\n .vrt__list {\n flex: 1;\n min-height: 0;\n overflow: auto;\n margin: 0;\n padding: 12px 4px 12px 16px;\n }\n\n .vrt__rows {\n margin: 0 0 2px;\n padding: 0;\n list-style: none;\n display: flex;\n flex-direction: column;\n gap: 6px;\n }\n\n /* The group header, in both its forms: a paragraph for the group that\n cannot collapse and a button for the ones that can, so the disclosure is\n a real control rather than a click handler on a label \u2014 same box either\n way, which is what keeps the two from jumping as groups appear. */\n .vrt__group {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n width: 100%;\n margin: 0;\n padding: 10px 8px 6px;\n background: none;\n border: none;\n color: inherit;\n font: inherit;\n text-align: left;\n }\n .vrt__group--toggle { cursor: pointer; }\n .vrt__group--toggle:hover .vrt__group-label { color: var(--prism-text-2); }\n\n .vrt__group-label {\n font-size: var(--fs-sm);\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--prism-text-muted);\n transition: color var(--dur-fast);\n }\n\n .vrt__caret {\n font-size: 9px;\n line-height: 1;\n color: var(--prism-text-ghost);\n transition: transform var(--dur-fast) var(--ease-default);\n }\n .vrt__caret--open { transform: rotate(90deg); }\n\n /* Same card-with-severity-edge as the a11y violations list, so the two\n panels read as one product. */\n .vrt__row {\n display: grid;\n grid-template-columns: 1fr auto;\n grid-template-areas: 'name diff' 'status diff';\n align-items: center;\n gap: 0 10px;\n width: 100%;\n padding: 8px 11px;\n background: var(--prism-bg-surface);\n border: 1px solid var(--prism-border);\n border-left: 3px solid var(--tone-color, var(--prism-border-strong));\n border-radius: var(--radius-md);\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n transition: border-color var(--dur-fast), background var(--dur-fast);\n }\n .vrt__row:hover { border-color: var(--prism-border-strong); }\n .vrt__row:hover { border-left-color: var(--tone-color, var(--prism-border-strong)); }\n .vrt__row--active {\n background: color-mix(in srgb, var(--prism-primary) 10%, var(--prism-bg-surface));\n border-color: color-mix(in srgb, var(--prism-primary) 40%, transparent);\n border-left-color: var(--tone-color, var(--prism-primary));\n }\n\n .vrt__name {\n grid-area: name;\n font-weight: 600;\n font-size: var(--fs-md);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n .vrt__status {\n grid-area: status;\n justify-self: start;\n margin-top: 3px;\n font-family: var(--font-mono);\n font-size: 9.5px;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n padding: 2px 6px;\n border-radius: var(--radius-xs);\n background: color-mix(in srgb, var(--tone-color) 15%, transparent);\n color: var(--tone-color);\n }\n\n .vrt__diff {\n grid-area: diff;\n font-family: var(--font-mono);\n font-size: var(--fs-sm);\n color: var(--prism-text-muted);\n white-space: nowrap;\n }\n\n [data-tone='success'] { --tone-color: var(--prism-success); }\n [data-tone='danger'] { --tone-color: var(--prism-danger); }\n [data-tone='warn'] { --tone-color: var(--prism-warn); }\n [data-tone='neutral'] { --tone-color: var(--prism-primary); }\n [data-tone='muted'] { --tone-color: var(--prism-text-muted); }\n\n .vrt__viewer {\n min-width: 0;\n min-height: 0;\n overflow: auto;\n border-left: 1px solid var(--prism-border);\n }\n\n .vrt__empty {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 4px;\n min-height: 140px;\n padding: 24px;\n text-align: center;\n font-size: var(--fs-md);\n }\n .vrt__empty b { font-weight: 600; color: var(--prism-text-2); }\n .vrt__empty span { color: var(--prism-text-muted); }\n .vrt__empty code {\n font-family: var(--font-mono);\n font-size: var(--fs-sm);\n padding: 1px 5px;\n border-radius: var(--radius-xs);\n background: var(--prism-input-bg);\n }\n\n /* Too narrow for two columns: stack them, and hand the scrolling back to\n the panel as a whole \u2014 side by side the two scrollers keep the list and\n the image in view at once, stacked there is nothing to keep in view. */\n @media (max-width: 860px) {\n :host { overflow: auto; }\n .vrt { grid-template-columns: 1fr; height: auto; }\n .vrt__list { overflow: visible; padding-right: 16px; }\n .vrt__viewer {\n overflow: visible;\n border-left: none;\n border-top: 1px solid var(--prism-border);\n }\n }\n "] }]
185
283
  }], null, { activeComponent: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeComponent", required: false }] }] }); })();
186
- (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(VisualRegressionPanelComponent, { className: "VisualRegressionPanelComponent", filePath: "visual-regression-panel.component.ts", lineNumber: 208 }); })();
284
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(VisualRegressionPanelComponent, { className: "VisualRegressionPanelComponent", filePath: "visual-regression-panel.component.ts", lineNumber: 307 }); })();
187
285
  function formatDiff(variant) {
188
286
  if (variant.diffRatio === undefined)
189
287
  return '—';
@@ -1 +1 @@
1
- {"version":3,"file":"visual-regression-plugin.browser.d.ts","sourceRoot":"","sources":["../src/visual-regression-plugin.browser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAoB,MAAM,uBAAuB,CAAC;AAC7E,OAAO,KAAK,EACV,6BAA6B,EAE9B,MAAM,8BAA8B,CAAC;AAStC;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,CAAC,EAAE,6BAA6B,GACvC,aAAa,CA4Bf"}
1
+ {"version":3,"file":"visual-regression-plugin.browser.d.ts","sourceRoot":"","sources":["../src/visual-regression-plugin.browser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,8BAA8B,CAAC;AAGlF;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,CAAC,EAAE,6BAA6B,GACvC,aAAa,CA6Bf"}
@@ -1,7 +1,4 @@
1
- function hasResults(component) {
2
- const meta = component.meta?.showcaseConfig?.meta?.['visualRegression'];
3
- return Boolean(meta?.found && meta.variants.length > 0);
4
- }
1
+ import { hasResults, reviewBadge } from './panel-contributions.js';
5
2
  /**
6
3
  * Browser-safe twin of {@link visualRegressionPlugin}.
7
4
  *
@@ -19,6 +16,7 @@ export function visualRegressionPlugin(_options) {
19
16
  icon: 'camera',
20
17
  position: 'bottom',
21
18
  isVisible: hasResults,
19
+ badge: reviewBadge,
22
20
  loadComponent: () => import('./visual-regression-panel.component.js').then((m) => m.VisualRegressionPanelComponent),
23
21
  },
24
22
  ],
@@ -1 +1 @@
1
- {"version":3,"file":"visual-regression-plugin.d.ts","sourceRoot":"","sources":["../src/visual-regression-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAoB,MAAM,uBAAuB,CAAC;AAC7E,OAAO,KAAK,EACV,6BAA6B,EAE9B,MAAM,8BAA8B,CAAC;AAatC;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,CAAC,EAAE,6BAA6B,GACtC,aAAa,CAqEf"}
1
+ {"version":3,"file":"visual-regression-plugin.d.ts","sourceRoot":"","sources":["../src/visual-regression-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EACV,6BAA6B,EAE9B,MAAM,8BAA8B,CAAC;AAOtC;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,CAAC,EAAE,6BAA6B,GACtC,aAAa,CA2Ef"}
@@ -1,10 +1,7 @@
1
+ import { hasResults, reviewBadge } from './panel-contributions.js';
1
2
  import { resolveVrtThresholds } from './thresholds.js';
3
+ import { statSummary, summarize } from './vrt-summarize.js';
2
4
  const DEFAULT_REPORT_PATH = 'vrt-report.json';
3
- /** True when the plugin recorded at least one result for this component. */
4
- function hasResults(component) {
5
- const meta = component.meta?.showcaseConfig?.meta?.['visualRegression'];
6
- return Boolean(meta?.found && meta.variants.length > 0);
7
- }
8
5
  /**
9
6
  * Renders a visual regression report produced by an external runner.
10
7
  *
@@ -21,10 +18,15 @@ export function visualRegressionPlugin(options) {
21
18
  async onComponentScanned(component) {
22
19
  const { readVariantsForComponent } = await import('./report-reader.js');
23
20
  const variants = readVariantsForComponent(reportPath, component.className);
21
+ // The headline is derived here rather than in the component head so the
22
+ // core app never has to reimplement this plugin's status semantics.
24
23
  const meta = {
25
24
  found: variants.length > 0,
26
25
  variants,
27
26
  assetBaseUrl,
27
+ ...(variants.length
28
+ ? { summary: statSummary(summarize(variants)) }
29
+ : {}),
28
30
  };
29
31
  return {
30
32
  ...component,
@@ -52,6 +54,7 @@ export function visualRegressionPlugin(options) {
52
54
  icon: 'camera',
53
55
  position: 'bottom',
54
56
  isVisible: hasResults,
57
+ badge: reviewBadge,
55
58
  loadComponent: () => import('./visual-regression-panel.component.js').then((m) => m.VisualRegressionPanelComponent),
56
59
  },
57
60
  ],
@@ -83,10 +83,23 @@ export interface VisualRegressionPluginOptions {
83
83
  thresholds?: number | Partial<VrtThresholds>;
84
84
  }
85
85
  /** Shape stored under `showcaseConfig.meta.visualRegression`. */
86
+ /** A component's visual regression standing, as the component head shows it. */
87
+ export interface VrtStat {
88
+ /** The worst diff among compared variants, or `—` when none was. */
89
+ value: string;
90
+ variant: 'ok' | 'warn' | 'danger';
91
+ }
86
92
  export interface VrtComponentMeta {
87
93
  found: boolean;
88
94
  variants: VrtVariantResult[];
89
95
  assetBaseUrl: string;
96
+ /**
97
+ * Pre-derived headline for the component head, written by the build-time
98
+ * hook. Optional because a report written before this field existed — or a
99
+ * browser-only plugin setup, which runs no build-time hooks — simply has
100
+ * none, and the stat is then left out rather than guessed at.
101
+ */
102
+ summary?: VrtStat;
90
103
  }
91
104
  /** Shape stored under `manifest.meta.visualRegression`. */
92
105
  export interface VrtManifestMeta {
@@ -1 +1 @@
1
- {"version":3,"file":"visual-regression.types.d.ts","sourceRoot":"","sources":["../src/visual-regression.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAEtD;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,SAAS,GACjB,WAAW,GACX,SAAS,GACT,eAAe,GACf,KAAK,GACL,UAAU,CAAC;AAEf,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,SAAS,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gFAAgF;IAChF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;OAMG;IACH,EAAE,CAAC,EAAE,QAAQ,CAAC;IACd;;;;;;;;OAQG;IACH,UAAU,CAAC,EAAE,QAAQ,CAAC;IACtB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAC;CACf;AAED,mDAAmD;AACnD,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,SAAS,CAAC;IACjB,SAAS,EAAE,gBAAgB,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,aAAa;IAC5B,+DAA+D;IAC/D,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,6BAA6B;IAC5C,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;CAC9C;AAED,iEAAiE;AACjE,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IAC7B,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,2DAA2D;AAC3D,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,SAAS,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,aAAa,CAAC;CAC3B"}
1
+ {"version":3,"file":"visual-regression.types.d.ts","sourceRoot":"","sources":["../src/visual-regression.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAEtD;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,SAAS,GACjB,WAAW,GACX,SAAS,GACT,eAAe,GACf,KAAK,GACL,UAAU,CAAC;AAEf,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,SAAS,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gFAAgF;IAChF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;OAMG;IACH,EAAE,CAAC,EAAE,QAAQ,CAAC;IACd;;;;;;;;OAQG;IACH,UAAU,CAAC,EAAE,QAAQ,CAAC;IACtB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAC;CACf;AAED,mDAAmD;AACnD,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,SAAS,CAAC;IACjB,SAAS,EAAE,gBAAgB,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,aAAa;IAC5B,+DAA+D;IAC/D,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,6BAA6B;IAC5C,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;CAC9C;AAED,iEAAiE;AACjE,gFAAgF;AAChF,MAAM,WAAW,OAAO;IACtB,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,IAAI,GAAG,MAAM,GAAG,QAAQ,CAAC;CACnC;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,2DAA2D;AAC3D,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,SAAS,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,aAAa,CAAC;CAC3B"}
@@ -1 +1 @@
1
- {"version":3,"file":"vrt-compare.component.d.ts","sourceRoot":"","sources":["../src/vrt-compare.component.ts"],"names":[],"mappings":"AAUA,OAAO,EAA0B,KAAK,IAAI,EAAE,MAAM,eAAe,CAAC;AAClE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;;AAErE,KAAK,WAAW,GAAG,MAAM,GAAG,cAAc,GAAG,MAAM,CAAC;AAQpD;;;;;GAKG;AACH,qBAkZa,mBAAmB;IAC9B,QAAQ,CAAC,OAAO,wDAAsC;IACtD,QAAQ,CAAC,YAAY,8CAAa;IAElC,SAAS,CAAC,QAAQ,CAAC,aAAa,6DAAoC;IACpE,SAAS,CAAC,QAAQ,CAAC,IAAI,+CAAuB;IAC9C,SAAS,CAAC,QAAQ,CAAC,IAAI,iDAAc;IAErC,SAAS,CAAC,QAAQ,CAAC,UAAU,8BAAc;IAC3C,SAAS,CAAC,QAAQ,CAAC,KAAK,kBAAS;IAEjC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAE1B;IACF,OAAO,CAAC,QAAQ,CAAC,UAAU,CAEzB;IACF,SAAS,CAAC,QAAQ,CAAC,OAAO,qDAExB;IAEF;;;;;OAKG;IACH,SAAS,CAAC,QAAQ,CAAC,MAAM;;;;;cAUtB;IAEH;;;;;OAKG;IACH,SAAS,CAAC,QAAQ,CAAC,cAAc,yDAE/B;IAEF,SAAS,CAAC,QAAQ,CAAC,eAAe,yDAEhC;IAEF,6EAA6E;IAC7E,SAAS,CAAC,QAAQ,CAAC,MAAM;;;cAA4C;IAErE,SAAS,CAAC,QAAQ,CAAC,WAAW;;;;;;cAe3B;IAEH,SAAS,CAAC,QAAQ,CAAC,cAAc,gDAgB9B;IAEH,SAAS,CAAC,QAAQ,CAAC,UAAU,8CAM1B;IAEH,yEAAyE;IACzE,SAAS,CAAC,QAAQ,CAAC,OAAO,0CAExB;IAEF;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,WAAW,0CAQ3B;IAEH,qEAAqE;IACrE,SAAS,CAAC,QAAQ,CAAC,UAAU,yDAG1B;IAEH,SAAS,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;yCA5H7B,mBAAmB;2CAAnB,mBAAmB;CA+H/B"}
1
+ {"version":3,"file":"vrt-compare.component.d.ts","sourceRoot":"","sources":["../src/vrt-compare.component.ts"],"names":[],"mappings":"AAUA,OAAO,EAA0B,KAAK,IAAI,EAAE,MAAM,eAAe,CAAC;AAClE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;;AAErE,KAAK,WAAW,GAAG,MAAM,GAAG,cAAc,GAAG,MAAM,CAAC;AAQpD;;;;;GAKG;AACH,qBAiaa,mBAAmB;IAC9B,QAAQ,CAAC,OAAO,wDAAsC;IACtD,QAAQ,CAAC,YAAY,8CAAa;IAElC,SAAS,CAAC,QAAQ,CAAC,aAAa,6DAAoC;IACpE,SAAS,CAAC,QAAQ,CAAC,IAAI,+CAAuB;IAC9C,SAAS,CAAC,QAAQ,CAAC,IAAI,iDAAc;IAErC,SAAS,CAAC,QAAQ,CAAC,UAAU,8BAAc;IAC3C,SAAS,CAAC,QAAQ,CAAC,KAAK,kBAAS;IAEjC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAE1B;IACF,OAAO,CAAC,QAAQ,CAAC,UAAU,CAEzB;IACF,SAAS,CAAC,QAAQ,CAAC,OAAO,qDAExB;IAEF;;;;;OAKG;IACH,SAAS,CAAC,QAAQ,CAAC,MAAM;;;;;cAUtB;IAEH;;;;;OAKG;IACH,SAAS,CAAC,QAAQ,CAAC,cAAc,yDAE/B;IAEF,SAAS,CAAC,QAAQ,CAAC,eAAe,yDAEhC;IAEF,6EAA6E;IAC7E,SAAS,CAAC,QAAQ,CAAC,MAAM;;;cAA4C;IAErE,SAAS,CAAC,QAAQ,CAAC,WAAW;;;;;;cAe3B;IAEH,SAAS,CAAC,QAAQ,CAAC,cAAc,gDAgB9B;IAEH,SAAS,CAAC,QAAQ,CAAC,UAAU,8CAM1B;IAEH,yEAAyE;IACzE,SAAS,CAAC,QAAQ,CAAC,OAAO,0CAExB;IAEF;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,WAAW,0CAQ3B;IAEH,qEAAqE;IACrE,SAAS,CAAC,QAAQ,CAAC,UAAU,yDAG1B;IAEH,SAAS,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;yCA5H7B,mBAAmB;2CAAnB,mBAAmB;CA+H/B"}
@@ -433,7 +433,7 @@ export class VrtCompareComponent {
433
433
  i0.ɵɵconditional(ctx.showToolbar() ? 1 : -1);
434
434
  i0.ɵɵadvance();
435
435
  i0.ɵɵconditional(ctx.variant().status === "excluded" ? 2 : 3);
436
- } }, styles: ["[_nghost-%COMP%] { display: block; min-width: 0; }\n\n .vrt-cmp[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding: 14px 16px;\n min-width: 0;\n }\n\n .vrt-cmp__bar[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n flex-wrap: wrap;\n border-bottom: 1px solid var(--prism-border);\n }\n\n .vrt-cmp__right[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 12px;\n padding-bottom: 7px;\n }\n\n \n\n .vrt-cmp__modes[_ngcontent-%COMP%] { display: flex; gap: 2px; }\n\n .vrt-cmp__mode[_ngcontent-%COMP%] {\n position: relative;\n padding: 6px 11px 8px;\n background: none;\n border: none;\n cursor: pointer;\n font-family: var(--font-sans);\n font-size: 12px;\n font-weight: 500;\n color: var(--prism-text-muted);\n transition: color var(--dur-fast);\n }\n .vrt-cmp__mode[_ngcontent-%COMP%]:hover { color: var(--prism-text-2); }\n .vrt-cmp__mode--active[_ngcontent-%COMP%] { color: var(--prism-text); }\n .vrt-cmp__mode--active[_ngcontent-%COMP%]::after {\n content: '';\n position: absolute;\n left: 6px;\n right: 6px;\n bottom: -1px;\n height: 2px;\n border-radius: 1px;\n background: var(--prism-primary);\n }\n\n .vrt-cmp__meta[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 8px;\n font-family: var(--font-mono);\n font-size: var(--fs-sm);\n color: var(--prism-text-muted);\n }\n .vrt-cmp__meta[_ngcontent-%COMP%] > span[_ngcontent-%COMP%] + span[_ngcontent-%COMP%]::before {\n content: '\u00B7';\n margin-right: 8px;\n color: var(--prism-text-ghost);\n }\n .vrt-cmp__ratio[_ngcontent-%COMP%] { color: var(--prism-success); }\n .vrt-cmp__ratio[data-hot][_ngcontent-%COMP%] { color: var(--prism-danger); }\n\n .vrt-cmp__zooms[_ngcontent-%COMP%] {\n display: flex;\n gap: 1px;\n padding: 2px;\n background: var(--prism-input-bg);\n border-radius: var(--radius-md);\n }\n .vrt-cmp__zoom[_ngcontent-%COMP%] {\n padding: 3px 8px;\n border: none;\n background: none;\n border-radius: var(--radius-sm);\n cursor: pointer;\n font-family: var(--font-mono);\n font-size: var(--fs-sm);\n color: var(--prism-text-muted);\n transition: color var(--dur-fast), background var(--dur-fast);\n }\n .vrt-cmp__zoom[_ngcontent-%COMP%]:hover { color: var(--prism-text-2); }\n .vrt-cmp__zoom--active[_ngcontent-%COMP%] {\n background: var(--prism-bg-elevated);\n color: var(--prism-text);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);\n }\n\n .vrt-cmp__stage[_ngcontent-%COMP%] {\n display: flex;\n \n\n overflow: auto;\n padding: 14px;\n background: var(--prism-bg-surface);\n border: 1px solid var(--prism-border);\n border-radius: var(--radius-lg);\n min-width: 0;\n \n\n\n\n\n --vrt-gap: 16px;\n --vrt-fit-basis: 100%;\n }\n .vrt-cmp__stage--split[_ngcontent-%COMP%] {\n gap: var(--vrt-gap);\n --vrt-fit-basis: calc((100% - var(--vrt-gap)) / 2);\n }\n\n .vrt-cmp__frame[_ngcontent-%COMP%] {\n margin: 0 auto;\n display: flex;\n flex-direction: column;\n gap: 8px;\n flex: none;\n }\n .vrt-cmp__stage--split[_ngcontent-%COMP%] .vrt-cmp__frame[_ngcontent-%COMP%] { margin: 0; }\n\n figure.vrt-cmp__frame[_ngcontent-%COMP%] figcaption[_ngcontent-%COMP%] {\n font-size: var(--fs-sm);\n color: var(--prism-text-muted);\n }\n\n \n .vrt-cmp__shot[_ngcontent-%COMP%] {\n position: relative;\n border: 1px solid var(--prism-border);\n border-radius: var(--radius-sm);\n overflow: hidden;\n background-image:\n linear-gradient(45deg, var(--prism-border) 25%, transparent 25%),\n linear-gradient(-45deg, var(--prism-border) 25%, transparent 25%),\n linear-gradient(45deg, transparent 75%, var(--prism-border) 75%),\n linear-gradient(-45deg, transparent 75%, var(--prism-border) 75%);\n background-size: 12px 12px;\n background-position: 0 0, 0 6px, 6px -6px, -6px 0;\n }\n\n .vrt-cmp__img[_ngcontent-%COMP%] {\n display: block;\n width: 100%;\n height: auto;\n \n image-rendering: pixelated;\n }\n\n .vrt-cmp__wipe-top[_ngcontent-%COMP%] { position: absolute; inset: 0; }\n .vrt-cmp__wipe-handle[_ngcontent-%COMP%] {\n position: absolute;\n top: 0;\n bottom: 0;\n width: 2px;\n margin-left: -1px;\n background: var(--prism-primary);\n box-shadow: 0 0 0 1px color-mix(in srgb, var(--prism-void) 45%, transparent);\n pointer-events: none;\n }\n\n .vrt-cmp__slider[_ngcontent-%COMP%] {\n width: 100%;\n margin: 0;\n appearance: none;\n background: none;\n cursor: ew-resize;\n }\n .vrt-cmp__slider[_ngcontent-%COMP%]::-webkit-slider-runnable-track {\n height: 4px;\n border-radius: 2px;\n background: var(--prism-input-bg);\n }\n .vrt-cmp__slider[_ngcontent-%COMP%]::-moz-range-track {\n height: 4px;\n border-radius: 2px;\n background: var(--prism-input-bg);\n }\n .vrt-cmp__slider[_ngcontent-%COMP%]::-webkit-slider-thumb {\n appearance: none;\n width: 13px;\n height: 13px;\n margin-top: -4.5px;\n border-radius: 50%;\n background: var(--prism-primary);\n border: 2px solid var(--prism-bg-elevated);\n box-shadow: 0 0 0 1px var(--prism-border-strong);\n }\n .vrt-cmp__slider[_ngcontent-%COMP%]::-moz-range-thumb {\n width: 13px;\n height: 13px;\n border-radius: 50%;\n background: var(--prism-primary);\n border: 2px solid var(--prism-bg-elevated);\n box-shadow: 0 0 0 1px var(--prism-border-strong);\n }\n\n .vrt-cmp__legend[_ngcontent-%COMP%] {\n display: flex;\n justify-content: space-between;\n font-size: var(--fs-sm);\n color: var(--prism-text-muted);\n }\n\n .vrt-cmp__note[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 3px;\n padding: 10px 12px;\n background: var(--prism-bg-surface);\n border: 1px solid var(--prism-border);\n border-left: 3px solid var(--prism-text-muted);\n border-radius: var(--radius-md);\n font-size: var(--fs-md);\n }\n .vrt-cmp__note[_ngcontent-%COMP%] b[_ngcontent-%COMP%] { font-weight: 600; color: var(--prism-text); }\n .vrt-cmp__note[_ngcontent-%COMP%] span[_ngcontent-%COMP%] { color: var(--prism-text-muted); }\n .vrt-cmp__note--warn[_ngcontent-%COMP%] { border-left-color: var(--prism-warn); }\n \n\n .vrt-cmp__note[_ngcontent-%COMP%] code[_ngcontent-%COMP%] {\n font-family: var(--font-mono);\n font-size: var(--fs-sm);\n padding: 1px 5px;\n border-radius: var(--radius-xs);\n background: var(--prism-input-bg);\n }"] });
436
+ } }, styles: ["[_nghost-%COMP%] { display: block; min-width: 0; height: 100%; }\n\n \n\n\n\n .vrt-cmp[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding: 14px 16px;\n min-width: 0;\n height: 100%;\n }\n\n .vrt-cmp__bar[_ngcontent-%COMP%] {\n flex: none;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n flex-wrap: wrap;\n border-bottom: 1px solid var(--prism-border);\n }\n\n .vrt-cmp__right[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 12px;\n padding-bottom: 7px;\n }\n\n \n\n .vrt-cmp__modes[_ngcontent-%COMP%] { display: flex; gap: 2px; }\n\n .vrt-cmp__mode[_ngcontent-%COMP%] {\n position: relative;\n padding: 6px 11px 8px;\n background: none;\n border: none;\n cursor: pointer;\n font-family: var(--font-sans);\n font-size: 12px;\n font-weight: 500;\n color: var(--prism-text-muted);\n transition: color var(--dur-fast);\n }\n .vrt-cmp__mode[_ngcontent-%COMP%]:hover { color: var(--prism-text-2); }\n .vrt-cmp__mode--active[_ngcontent-%COMP%] { color: var(--prism-text); }\n .vrt-cmp__mode--active[_ngcontent-%COMP%]::after {\n content: '';\n position: absolute;\n left: 6px;\n right: 6px;\n bottom: -1px;\n height: 2px;\n border-radius: 1px;\n background: var(--prism-primary);\n }\n\n .vrt-cmp__meta[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 8px;\n font-family: var(--font-mono);\n font-size: var(--fs-sm);\n color: var(--prism-text-muted);\n }\n .vrt-cmp__meta[_ngcontent-%COMP%] > span[_ngcontent-%COMP%] + span[_ngcontent-%COMP%]::before {\n content: '\u00B7';\n margin-right: 8px;\n color: var(--prism-text-ghost);\n }\n .vrt-cmp__ratio[_ngcontent-%COMP%] { color: var(--prism-success); }\n .vrt-cmp__ratio[data-hot][_ngcontent-%COMP%] { color: var(--prism-danger); }\n\n .vrt-cmp__zooms[_ngcontent-%COMP%] {\n display: flex;\n gap: 1px;\n padding: 2px;\n background: var(--prism-input-bg);\n border-radius: var(--radius-md);\n }\n .vrt-cmp__zoom[_ngcontent-%COMP%] {\n padding: 3px 8px;\n border: none;\n background: none;\n border-radius: var(--radius-sm);\n cursor: pointer;\n font-family: var(--font-mono);\n font-size: var(--fs-sm);\n color: var(--prism-text-muted);\n transition: color var(--dur-fast), background var(--dur-fast);\n }\n .vrt-cmp__zoom[_ngcontent-%COMP%]:hover { color: var(--prism-text-2); }\n .vrt-cmp__zoom--active[_ngcontent-%COMP%] {\n background: var(--prism-bg-elevated);\n color: var(--prism-text);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);\n }\n\n .vrt-cmp__stage[_ngcontent-%COMP%] {\n \n\n\n flex: 1;\n min-height: 140px;\n display: flex;\n \n\n overflow: auto;\n padding: 14px;\n background: var(--prism-bg-surface);\n border: 1px solid var(--prism-border);\n border-radius: var(--radius-lg);\n min-width: 0;\n \n\n\n\n\n --vrt-gap: 16px;\n --vrt-fit-basis: 100%;\n }\n .vrt-cmp__stage--split[_ngcontent-%COMP%] {\n gap: var(--vrt-gap);\n --vrt-fit-basis: calc((100% - var(--vrt-gap)) / 2);\n }\n\n \n\n\n .vrt-cmp__frame[_ngcontent-%COMP%] {\n margin: auto;\n display: flex;\n flex-direction: column;\n gap: 8px;\n flex: none;\n }\n .vrt-cmp__stage--split[_ngcontent-%COMP%] .vrt-cmp__frame[_ngcontent-%COMP%] { margin: auto 0; }\n\n figure.vrt-cmp__frame[_ngcontent-%COMP%] figcaption[_ngcontent-%COMP%] {\n font-size: var(--fs-sm);\n color: var(--prism-text-muted);\n }\n\n \n .vrt-cmp__shot[_ngcontent-%COMP%] {\n position: relative;\n border: 1px solid var(--prism-border);\n border-radius: var(--radius-sm);\n overflow: hidden;\n background-image:\n linear-gradient(45deg, var(--prism-border) 25%, transparent 25%),\n linear-gradient(-45deg, var(--prism-border) 25%, transparent 25%),\n linear-gradient(45deg, transparent 75%, var(--prism-border) 75%),\n linear-gradient(-45deg, transparent 75%, var(--prism-border) 75%);\n background-size: 12px 12px;\n background-position: 0 0, 0 6px, 6px -6px, -6px 0;\n }\n\n .vrt-cmp__img[_ngcontent-%COMP%] {\n display: block;\n width: 100%;\n height: auto;\n \n image-rendering: pixelated;\n }\n\n .vrt-cmp__wipe-top[_ngcontent-%COMP%] { position: absolute; inset: 0; }\n .vrt-cmp__wipe-handle[_ngcontent-%COMP%] {\n position: absolute;\n top: 0;\n bottom: 0;\n width: 2px;\n margin-left: -1px;\n background: var(--prism-primary);\n box-shadow: 0 0 0 1px color-mix(in srgb, var(--prism-void) 45%, transparent);\n pointer-events: none;\n }\n\n .vrt-cmp__slider[_ngcontent-%COMP%] {\n width: 100%;\n margin: 0;\n appearance: none;\n background: none;\n cursor: ew-resize;\n }\n .vrt-cmp__slider[_ngcontent-%COMP%]::-webkit-slider-runnable-track {\n height: 4px;\n border-radius: 2px;\n background: var(--prism-input-bg);\n }\n .vrt-cmp__slider[_ngcontent-%COMP%]::-moz-range-track {\n height: 4px;\n border-radius: 2px;\n background: var(--prism-input-bg);\n }\n .vrt-cmp__slider[_ngcontent-%COMP%]::-webkit-slider-thumb {\n appearance: none;\n width: 13px;\n height: 13px;\n margin-top: -4.5px;\n border-radius: 50%;\n background: var(--prism-primary);\n border: 2px solid var(--prism-bg-elevated);\n box-shadow: 0 0 0 1px var(--prism-border-strong);\n }\n .vrt-cmp__slider[_ngcontent-%COMP%]::-moz-range-thumb {\n width: 13px;\n height: 13px;\n border-radius: 50%;\n background: var(--prism-primary);\n border: 2px solid var(--prism-bg-elevated);\n box-shadow: 0 0 0 1px var(--prism-border-strong);\n }\n\n .vrt-cmp__legend[_ngcontent-%COMP%] {\n display: flex;\n justify-content: space-between;\n font-size: var(--fs-sm);\n color: var(--prism-text-muted);\n }\n\n .vrt-cmp__note[_ngcontent-%COMP%] {\n flex: none;\n display: flex;\n flex-direction: column;\n gap: 3px;\n padding: 10px 12px;\n background: var(--prism-bg-surface);\n border: 1px solid var(--prism-border);\n border-left: 3px solid var(--prism-text-muted);\n border-radius: var(--radius-md);\n font-size: var(--fs-md);\n }\n .vrt-cmp__note[_ngcontent-%COMP%] b[_ngcontent-%COMP%] { font-weight: 600; color: var(--prism-text); }\n .vrt-cmp__note[_ngcontent-%COMP%] span[_ngcontent-%COMP%] { color: var(--prism-text-muted); }\n .vrt-cmp__note--warn[_ngcontent-%COMP%] { border-left-color: var(--prism-warn); }\n \n\n .vrt-cmp__note[_ngcontent-%COMP%] code[_ngcontent-%COMP%] {\n font-family: var(--font-mono);\n font-size: var(--fs-sm);\n padding: 1px 5px;\n border-radius: var(--radius-xs);\n background: var(--prism-input-bg);\n }"] });
437
437
  }
438
438
  (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(VrtCompareComponent, [{
439
439
  type: Component,
@@ -597,6 +597,6 @@ export class VrtCompareComponent {
597
597
  </div>
598
598
  } }
599
599
  </div>
600
- `, styles: ["\n :host { display: block; min-width: 0; }\n\n .vrt-cmp {\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding: 14px 16px;\n min-width: 0;\n }\n\n .vrt-cmp__bar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n flex-wrap: wrap;\n border-bottom: 1px solid var(--prism-border);\n }\n\n .vrt-cmp__right {\n display: flex;\n align-items: center;\n gap: 12px;\n padding-bottom: 7px;\n }\n\n /* Mode switching is the same act as the a11y panel's sub-tabs, so it gets\n the same underline treatment rather than a third kind of button. */\n .vrt-cmp__modes { display: flex; gap: 2px; }\n\n .vrt-cmp__mode {\n position: relative;\n padding: 6px 11px 8px;\n background: none;\n border: none;\n cursor: pointer;\n font-family: var(--font-sans);\n font-size: 12px;\n font-weight: 500;\n color: var(--prism-text-muted);\n transition: color var(--dur-fast);\n }\n .vrt-cmp__mode:hover { color: var(--prism-text-2); }\n .vrt-cmp__mode--active { color: var(--prism-text); }\n .vrt-cmp__mode--active::after {\n content: '';\n position: absolute;\n left: 6px;\n right: 6px;\n bottom: -1px;\n height: 2px;\n border-radius: 1px;\n background: var(--prism-primary);\n }\n\n .vrt-cmp__meta {\n display: flex;\n align-items: center;\n gap: 8px;\n font-family: var(--font-mono);\n font-size: var(--fs-sm);\n color: var(--prism-text-muted);\n }\n .vrt-cmp__meta > span + span::before {\n content: '\u00B7';\n margin-right: 8px;\n color: var(--prism-text-ghost);\n }\n .vrt-cmp__ratio { color: var(--prism-success); }\n .vrt-cmp__ratio[data-hot] { color: var(--prism-danger); }\n\n .vrt-cmp__zooms {\n display: flex;\n gap: 1px;\n padding: 2px;\n background: var(--prism-input-bg);\n border-radius: var(--radius-md);\n }\n .vrt-cmp__zoom {\n padding: 3px 8px;\n border: none;\n background: none;\n border-radius: var(--radius-sm);\n cursor: pointer;\n font-family: var(--font-mono);\n font-size: var(--fs-sm);\n color: var(--prism-text-muted);\n transition: color var(--dur-fast), background var(--dur-fast);\n }\n .vrt-cmp__zoom:hover { color: var(--prism-text-2); }\n .vrt-cmp__zoom--active {\n background: var(--prism-bg-elevated);\n color: var(--prism-text);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);\n }\n\n .vrt-cmp__stage {\n display: flex;\n /* overflow-safe centring: justify-content would clip the left edge of an\n image wider than the stage, margin auto scrolls to it instead. */\n overflow: auto;\n padding: 14px;\n background: var(--prism-bg-surface);\n border: 1px solid var(--prism-border);\n border-radius: var(--radius-lg);\n min-width: 0;\n /* How wide one frame may grow under the fit zoom. Side by side there are\n two of them, so each gets half the stage minus the gap. This is an\n inherited custom property rather than flex-grow, because a growing\n flex item ignores its own width -- which is exactly what the explicit\n zoom steps set. */\n --vrt-gap: 16px;\n --vrt-fit-basis: 100%;\n }\n .vrt-cmp__stage--split {\n gap: var(--vrt-gap);\n --vrt-fit-basis: calc((100% - var(--vrt-gap)) / 2);\n }\n\n .vrt-cmp__frame {\n margin: 0 auto;\n display: flex;\n flex-direction: column;\n gap: 8px;\n flex: none;\n }\n .vrt-cmp__stage--split .vrt-cmp__frame { margin: 0; }\n\n figure.vrt-cmp__frame figcaption {\n font-size: var(--fs-sm);\n color: var(--prism-text-muted);\n }\n\n /* A checkerboard makes transparent regions of a PNG legible. */\n .vrt-cmp__shot {\n position: relative;\n border: 1px solid var(--prism-border);\n border-radius: var(--radius-sm);\n overflow: hidden;\n background-image:\n linear-gradient(45deg, var(--prism-border) 25%, transparent 25%),\n linear-gradient(-45deg, var(--prism-border) 25%, transparent 25%),\n linear-gradient(45deg, transparent 75%, var(--prism-border) 75%),\n linear-gradient(-45deg, transparent 75%, var(--prism-border) 75%);\n background-size: 12px 12px;\n background-position: 0 0, 0 6px, 6px -6px, -6px 0;\n }\n\n .vrt-cmp__img {\n display: block;\n width: 100%;\n height: auto;\n /* Zooming a diff is only useful if the pixels stay pixels. */\n image-rendering: pixelated;\n }\n\n .vrt-cmp__wipe-top { position: absolute; inset: 0; }\n .vrt-cmp__wipe-handle {\n position: absolute;\n top: 0;\n bottom: 0;\n width: 2px;\n margin-left: -1px;\n background: var(--prism-primary);\n box-shadow: 0 0 0 1px color-mix(in srgb, var(--prism-void) 45%, transparent);\n pointer-events: none;\n }\n\n .vrt-cmp__slider {\n width: 100%;\n margin: 0;\n appearance: none;\n background: none;\n cursor: ew-resize;\n }\n .vrt-cmp__slider::-webkit-slider-runnable-track {\n height: 4px;\n border-radius: 2px;\n background: var(--prism-input-bg);\n }\n .vrt-cmp__slider::-moz-range-track {\n height: 4px;\n border-radius: 2px;\n background: var(--prism-input-bg);\n }\n .vrt-cmp__slider::-webkit-slider-thumb {\n appearance: none;\n width: 13px;\n height: 13px;\n margin-top: -4.5px;\n border-radius: 50%;\n background: var(--prism-primary);\n border: 2px solid var(--prism-bg-elevated);\n box-shadow: 0 0 0 1px var(--prism-border-strong);\n }\n .vrt-cmp__slider::-moz-range-thumb {\n width: 13px;\n height: 13px;\n border-radius: 50%;\n background: var(--prism-primary);\n border: 2px solid var(--prism-bg-elevated);\n box-shadow: 0 0 0 1px var(--prism-border-strong);\n }\n\n .vrt-cmp__legend {\n display: flex;\n justify-content: space-between;\n font-size: var(--fs-sm);\n color: var(--prism-text-muted);\n }\n\n .vrt-cmp__note {\n display: flex;\n flex-direction: column;\n gap: 3px;\n padding: 10px 12px;\n background: var(--prism-bg-surface);\n border: 1px solid var(--prism-border);\n border-left: 3px solid var(--prism-text-muted);\n border-radius: var(--radius-md);\n font-size: var(--fs-md);\n }\n .vrt-cmp__note b { font-weight: 600; color: var(--prism-text); }\n .vrt-cmp__note span { color: var(--prism-text-muted); }\n .vrt-cmp__note--warn { border-left-color: var(--prism-warn); }\n /* Matches the panel shell's own inline code, so a background name reads\n the same wherever it appears. */\n .vrt-cmp__note code {\n font-family: var(--font-mono);\n font-size: var(--fs-sm);\n padding: 1px 5px;\n border-radius: var(--radius-xs);\n background: var(--prism-input-bg);\n }\n "] }]
600
+ `, styles: ["\n :host { display: block; min-width: 0; height: 100%; }\n\n /* Fills the viewer column rather than growing with its content, so the\n stage gets whatever height is left over instead of the panel scrolling.\n The toolbar then stays put while a large capture scrolls inside the\n stage, which is the one place scrolling belongs here. */\n .vrt-cmp {\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding: 14px 16px;\n min-width: 0;\n height: 100%;\n }\n\n .vrt-cmp__bar {\n flex: none;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n flex-wrap: wrap;\n border-bottom: 1px solid var(--prism-border);\n }\n\n .vrt-cmp__right {\n display: flex;\n align-items: center;\n gap: 12px;\n padding-bottom: 7px;\n }\n\n /* Mode switching is the same act as the a11y panel's sub-tabs, so it gets\n the same underline treatment rather than a third kind of button. */\n .vrt-cmp__modes { display: flex; gap: 2px; }\n\n .vrt-cmp__mode {\n position: relative;\n padding: 6px 11px 8px;\n background: none;\n border: none;\n cursor: pointer;\n font-family: var(--font-sans);\n font-size: 12px;\n font-weight: 500;\n color: var(--prism-text-muted);\n transition: color var(--dur-fast);\n }\n .vrt-cmp__mode:hover { color: var(--prism-text-2); }\n .vrt-cmp__mode--active { color: var(--prism-text); }\n .vrt-cmp__mode--active::after {\n content: '';\n position: absolute;\n left: 6px;\n right: 6px;\n bottom: -1px;\n height: 2px;\n border-radius: 1px;\n background: var(--prism-primary);\n }\n\n .vrt-cmp__meta {\n display: flex;\n align-items: center;\n gap: 8px;\n font-family: var(--font-mono);\n font-size: var(--fs-sm);\n color: var(--prism-text-muted);\n }\n .vrt-cmp__meta > span + span::before {\n content: '\u00B7';\n margin-right: 8px;\n color: var(--prism-text-ghost);\n }\n .vrt-cmp__ratio { color: var(--prism-success); }\n .vrt-cmp__ratio[data-hot] { color: var(--prism-danger); }\n\n .vrt-cmp__zooms {\n display: flex;\n gap: 1px;\n padding: 2px;\n background: var(--prism-input-bg);\n border-radius: var(--radius-md);\n }\n .vrt-cmp__zoom {\n padding: 3px 8px;\n border: none;\n background: none;\n border-radius: var(--radius-sm);\n cursor: pointer;\n font-family: var(--font-mono);\n font-size: var(--fs-sm);\n color: var(--prism-text-muted);\n transition: color var(--dur-fast), background var(--dur-fast);\n }\n .vrt-cmp__zoom:hover { color: var(--prism-text-2); }\n .vrt-cmp__zoom--active {\n background: var(--prism-bg-elevated);\n color: var(--prism-text);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);\n }\n\n .vrt-cmp__stage {\n /* Takes the leftover height, but only down to a floor \u2014 below that the\n viewer column scrolls instead, because a stage squashed to nothing is\n not a comparison. */\n flex: 1;\n min-height: 140px;\n display: flex;\n /* overflow-safe centring: justify-content would clip the left edge of an\n image wider than the stage, margin auto scrolls to it instead. */\n overflow: auto;\n padding: 14px;\n background: var(--prism-bg-surface);\n border: 1px solid var(--prism-border);\n border-radius: var(--radius-lg);\n min-width: 0;\n /* How wide one frame may grow under the fit zoom. Side by side there are\n two of them, so each gets half the stage minus the gap. This is an\n inherited custom property rather than flex-grow, because a growing\n flex item ignores its own width -- which is exactly what the explicit\n zoom steps set. */\n --vrt-gap: 16px;\n --vrt-fit-basis: 100%;\n }\n .vrt-cmp__stage--split {\n gap: var(--vrt-gap);\n --vrt-fit-basis: calc((100% - var(--vrt-gap)) / 2);\n }\n\n /* auto margins rather than the stage centring its children: with\n overflow:auto on the stage, centring clips the leading edge of\n anything larger than it, while auto margins scroll to it. */\n .vrt-cmp__frame {\n margin: auto;\n display: flex;\n flex-direction: column;\n gap: 8px;\n flex: none;\n }\n .vrt-cmp__stage--split .vrt-cmp__frame { margin: auto 0; }\n\n figure.vrt-cmp__frame figcaption {\n font-size: var(--fs-sm);\n color: var(--prism-text-muted);\n }\n\n /* A checkerboard makes transparent regions of a PNG legible. */\n .vrt-cmp__shot {\n position: relative;\n border: 1px solid var(--prism-border);\n border-radius: var(--radius-sm);\n overflow: hidden;\n background-image:\n linear-gradient(45deg, var(--prism-border) 25%, transparent 25%),\n linear-gradient(-45deg, var(--prism-border) 25%, transparent 25%),\n linear-gradient(45deg, transparent 75%, var(--prism-border) 75%),\n linear-gradient(-45deg, transparent 75%, var(--prism-border) 75%);\n background-size: 12px 12px;\n background-position: 0 0, 0 6px, 6px -6px, -6px 0;\n }\n\n .vrt-cmp__img {\n display: block;\n width: 100%;\n height: auto;\n /* Zooming a diff is only useful if the pixels stay pixels. */\n image-rendering: pixelated;\n }\n\n .vrt-cmp__wipe-top { position: absolute; inset: 0; }\n .vrt-cmp__wipe-handle {\n position: absolute;\n top: 0;\n bottom: 0;\n width: 2px;\n margin-left: -1px;\n background: var(--prism-primary);\n box-shadow: 0 0 0 1px color-mix(in srgb, var(--prism-void) 45%, transparent);\n pointer-events: none;\n }\n\n .vrt-cmp__slider {\n width: 100%;\n margin: 0;\n appearance: none;\n background: none;\n cursor: ew-resize;\n }\n .vrt-cmp__slider::-webkit-slider-runnable-track {\n height: 4px;\n border-radius: 2px;\n background: var(--prism-input-bg);\n }\n .vrt-cmp__slider::-moz-range-track {\n height: 4px;\n border-radius: 2px;\n background: var(--prism-input-bg);\n }\n .vrt-cmp__slider::-webkit-slider-thumb {\n appearance: none;\n width: 13px;\n height: 13px;\n margin-top: -4.5px;\n border-radius: 50%;\n background: var(--prism-primary);\n border: 2px solid var(--prism-bg-elevated);\n box-shadow: 0 0 0 1px var(--prism-border-strong);\n }\n .vrt-cmp__slider::-moz-range-thumb {\n width: 13px;\n height: 13px;\n border-radius: 50%;\n background: var(--prism-primary);\n border: 2px solid var(--prism-bg-elevated);\n box-shadow: 0 0 0 1px var(--prism-border-strong);\n }\n\n .vrt-cmp__legend {\n display: flex;\n justify-content: space-between;\n font-size: var(--fs-sm);\n color: var(--prism-text-muted);\n }\n\n .vrt-cmp__note {\n flex: none;\n display: flex;\n flex-direction: column;\n gap: 3px;\n padding: 10px 12px;\n background: var(--prism-bg-surface);\n border: 1px solid var(--prism-border);\n border-left: 3px solid var(--prism-text-muted);\n border-radius: var(--radius-md);\n font-size: var(--fs-md);\n }\n .vrt-cmp__note b { font-weight: 600; color: var(--prism-text); }\n .vrt-cmp__note span { color: var(--prism-text-muted); }\n .vrt-cmp__note--warn { border-left-color: var(--prism-warn); }\n /* Matches the panel shell's own inline code, so a background name reads\n the same wherever it appears. */\n .vrt-cmp__note code {\n font-family: var(--font-mono);\n font-size: var(--fs-sm);\n padding: 1px 5px;\n border-radius: var(--radius-xs);\n background: var(--prism-input-bg);\n }\n "] }]
601
601
  }], null, { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: true }] }], assetBaseUrl: [{ type: i0.Input, args: [{ isSignal: true, alias: "assetBaseUrl", required: false }] }] }); })();
602
- (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(VrtCompareComponent, { className: "VrtCompareComponent", filePath: "vrt-compare.component.ts", lineNumber: 430 }); })();
602
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(VrtCompareComponent, { className: "VrtCompareComponent", filePath: "vrt-compare.component.ts", lineNumber: 445 }); })();
@@ -1,4 +1,4 @@
1
- import type { VrtStatus, VrtVariantResult } from './visual-regression.types.js';
1
+ import type { VrtStat, VrtStatus, VrtVariantResult } from './visual-regression.types.js';
2
2
  /**
3
3
  * Colour role of a status, resolved to a theme token by the components.
4
4
  *
@@ -17,26 +17,29 @@ export interface VrtSummary {
17
17
  /** Largest diff among compared variants; `null` when nothing was compared. */
18
18
  maxDiffRatio: number | null;
19
19
  }
20
- export interface VrtTile {
21
- key: string;
20
+ export interface VrtSegment {
21
+ key: VrtStatus;
22
22
  label: string;
23
- value: string;
24
- /** How far the tile's bar fills, 0–1. */
25
- ratio: number;
23
+ /** Raw count — the bar weights its slices by this, so it needs no ratio. */
24
+ count: number;
26
25
  tone: VrtTone;
27
26
  }
28
27
  /** Counts and the worst diff, derived from the variants already in `meta`. */
29
28
  export declare function summarize(variants: readonly VrtVariantResult[]): VrtSummary;
30
29
  /**
31
- * The tiles the summary strip renders.
30
+ * The slices of the composition bar, worst news first.
32
31
  *
33
- * `Changed` and `Unchanged` are always present: a strip that loses a column
34
- * when a run goes green would make the panel change shape with its content,
35
- * and "no Changed tile" is easy to misread as "no data". Everything else
36
- * appears only when it has something to report, so a library that never
37
- * excludes a variant is never asked to wonder what `Excluded` means.
32
+ * Only statuses that actually occurred. The tile strip this replaced had to
33
+ * keep `changed` and `unchanged` present at zero so the panel would not change
34
+ * shape with its content a bar has no such problem, because a zero count is
35
+ * a zero-width slice. Carrying it would add an invisible segment and a legend
36
+ * entry claiming a colour nothing on screen has.
37
+ *
38
+ * Counts rather than ratios: the bar divides itself with `flex-grow`, so it
39
+ * needs the weights, not pre-divided shares, and an empty run yields an empty
40
+ * bar instead of a division by zero.
38
41
  */
39
- export declare function summaryTiles(summary: VrtSummary): VrtTile[];
42
+ export declare function summarySegments(summary: VrtSummary): VrtSegment[];
40
43
  /**
41
44
  * A ratio as the percentage a reviewer reads.
42
45
  *
@@ -45,4 +48,67 @@ export declare function summaryTiles(summary: VrtSummary): VrtTile[];
45
48
  * that rounds to zero is shown as `<0.01%`, never as `0%`.
46
49
  */
47
50
  export declare function formatPercent(ratio: number): string;
51
+ /** The three buckets the variant list is grouped into, in display order. */
52
+ export type VrtGroupKey = 'review' | 'unchanged' | 'excluded';
53
+ export interface VrtGroup<T> {
54
+ key: VrtGroupKey;
55
+ label: string;
56
+ count: number;
57
+ /**
58
+ * Whether the group may be collapsed. `review` never can: it is the reason
59
+ * the panel exists, and a list whose only interesting rows can be hidden
60
+ * behind a disclosure triangle is a list that gets skimmed past.
61
+ */
62
+ collapsible: boolean;
63
+ rows: T[];
64
+ }
65
+ /**
66
+ * The variant list split into what needs a decision and what does not.
67
+ *
68
+ * `new` sits in `review` beside `changed`, which is the one placement worth
69
+ * explaining. A variant with no baseline is explicitly *not* a failure — the
70
+ * report contract says so and the row keeps its own neutral tone — but it is
71
+ * the one state that cannot resolve itself: somebody has to accept a baseline.
72
+ * `excluded` is the mirror image and gets its own group rather than being
73
+ * folded into `unchanged`: both are "not your problem", but "compared and
74
+ * matched" and "never captured" are different claims and a reviewer counting
75
+ * coverage needs to tell them apart.
76
+ *
77
+ * Empty groups are dropped instead of rendered at zero. The summary bar above
78
+ * the list has the same rule, and for the same reason — a zero-count header is
79
+ * a line of chrome asserting a colour nothing on screen has.
80
+ *
81
+ * Generic over the row rather than taking `VrtVariantResult`: the panel groups
82
+ * its own view models, which already carry the label and the formatted diff,
83
+ * and threading those back through a variant-shaped API would mean building
84
+ * them twice.
85
+ */
86
+ export declare function groupRows<T extends {
87
+ status: VrtStatus;
88
+ }>(rows: readonly T[]): VrtGroup<T>[];
89
+ /**
90
+ * Which groups start open.
91
+ *
92
+ * Nothing, while something needs review — the point of the grouping is that
93
+ * the fourteen unchanged variants stop competing with the one that changed.
94
+ * When there is no review group the rule inverts and the first group opens,
95
+ * because two collapsed headers and no rows reads as a panel that failed to
96
+ * load rather than as a clean run.
97
+ */
98
+ export declare function defaultExpandedGroups<T>(groups: readonly VrtGroup<T>[]): VrtGroupKey[];
99
+ /**
100
+ * The headline figure and its colour for one component.
101
+ *
102
+ * The colour comes from the *statuses*, not from the percentage, and that is
103
+ * deliberate: {@link DEFAULT_VRT_THRESHOLDS} already says a run is only green
104
+ * at a perfect score, so a "small enough to stay amber" diff would contradict
105
+ * the badge sitting in the same header. Magnitude is what the value carries.
106
+ * Amber is reserved for the variants that could not be measured at all —
107
+ * resized and new — which are neither a regression nor a clean pass.
108
+ *
109
+ * Computed at build time and stored in the component's meta, so the component
110
+ * head can read two primitives instead of reimplementing this plugin's status
111
+ * semantics in the core app.
112
+ */
113
+ export declare function statSummary(summary: VrtSummary): VrtStat;
48
114
  //# sourceMappingURL=vrt-summarize.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"vrt-summarize.d.ts","sourceRoot":"","sources":["../src/vrt-summarize.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAEhF;;;;;;GAMG;AACH,MAAM,MAAM,OAAO,GAAG,SAAS,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;AAE1E,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAMlD,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAMlD,CAAC;AAWF,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IAClC,mEAAmE;IACnE,QAAQ,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,OAAO;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;CACf;AAED,8EAA8E;AAC9E,wBAAgB,SAAS,CAAC,QAAQ,EAAE,SAAS,gBAAgB,EAAE,GAAG,UAAU,CAyB3E;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,EAAE,CAiC3D;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKnD"}
1
+ {"version":3,"file":"vrt-summarize.d.ts","sourceRoot":"","sources":["../src/vrt-summarize.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EACP,SAAS,EACT,gBAAgB,EACjB,MAAM,8BAA8B,CAAC;AAEtC;;;;;;GAMG;AACH,MAAM,MAAM,OAAO,GAAG,SAAS,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;AAE1E,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAMlD,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAMlD,CAAC;AAWF,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IAClC,mEAAmE;IACnE,QAAQ,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,SAAS,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;CACf;AAED,8EAA8E;AAC9E,wBAAgB,SAAS,CAAC,QAAQ,EAAE,SAAS,gBAAgB,EAAE,GAAG,UAAU,CAyB3E;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,UAAU,GAAG,UAAU,EAAE,CASjE;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKnD;AAED,4EAA4E;AAC5E,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,WAAW,GAAG,UAAU,CAAC;AAE9D,MAAM,WAAW,QAAQ,CAAC,CAAC;IACzB,GAAG,EAAE,WAAW,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,WAAW,EAAE,OAAO,CAAC;IACrB,IAAI,EAAE,CAAC,EAAE,CAAC;CACX;AAkBD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,EACvD,IAAI,EAAE,SAAS,CAAC,EAAE,GACjB,QAAQ,CAAC,CAAC,CAAC,EAAE,CAWf;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,EACrC,MAAM,EAAE,SAAS,QAAQ,CAAC,CAAC,CAAC,EAAE,GAC7B,WAAW,EAAE,CAGf;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAWxD"}
@@ -45,40 +45,25 @@ export function summarize(variants) {
45
45
  return { total: variants.length, counts, compared, maxDiffRatio };
46
46
  }
47
47
  /**
48
- * The tiles the summary strip renders.
48
+ * The slices of the composition bar, worst news first.
49
49
  *
50
- * `Changed` and `Unchanged` are always present: a strip that loses a column
51
- * when a run goes green would make the panel change shape with its content,
52
- * and "no Changed tile" is easy to misread as "no data". Everything else
53
- * appears only when it has something to report, so a library that never
54
- * excludes a variant is never asked to wonder what `Excluded` means.
50
+ * Only statuses that actually occurred. The tile strip this replaced had to
51
+ * keep `changed` and `unchanged` present at zero so the panel would not change
52
+ * shape with its content a bar has no such problem, because a zero count is
53
+ * a zero-width slice. Carrying it would add an invisible segment and a legend
54
+ * entry claiming a colour nothing on screen has.
55
+ *
56
+ * Counts rather than ratios: the bar divides itself with `flex-grow`, so it
57
+ * needs the weights, not pre-divided shares, and an empty run yields an empty
58
+ * bar instead of a division by zero.
55
59
  */
56
- export function summaryTiles(summary) {
57
- const share = (count) => summary.total === 0 ? 0 : count / summary.total;
58
- const tiles = STATUS_ORDER.filter((status) => summary.counts[status] > 0 ||
59
- status === 'changed' ||
60
- status === 'unchanged').map((status) => {
61
- const count = summary.counts[status];
62
- return {
63
- key: status,
64
- label: STATUS_LABEL[status],
65
- value: String(count),
66
- ratio: share(count),
67
- // A count of zero is the absence of news, not news in that colour:
68
- // "Changed 0" rendered in danger red screams the opposite of what it says.
69
- tone: count === 0 ? 'muted' : STATUS_TONE[status],
70
- };
71
- });
72
- if (summary.maxDiffRatio !== null) {
73
- tiles.push({
74
- key: 'max-diff',
75
- label: 'Max diff',
76
- value: formatPercent(summary.maxDiffRatio),
77
- ratio: summary.maxDiffRatio,
78
- tone: summary.maxDiffRatio > 0 ? 'danger' : 'success',
79
- });
80
- }
81
- return tiles;
60
+ export function summarySegments(summary) {
61
+ return STATUS_ORDER.filter((status) => summary.counts[status] > 0).map((status) => ({
62
+ key: status,
63
+ label: STATUS_LABEL[status],
64
+ count: summary.counts[status],
65
+ tone: STATUS_TONE[status],
66
+ }));
82
67
  }
83
68
  /**
84
69
  * A ratio as the percentage a reviewer reads.
@@ -95,3 +80,86 @@ export function formatPercent(ratio) {
95
80
  return '<0.01%';
96
81
  return `${pct.toFixed(2)}%`;
97
82
  }
83
+ const GROUP_OF = {
84
+ changed: 'review',
85
+ 'size-mismatch': 'review',
86
+ new: 'review',
87
+ unchanged: 'unchanged',
88
+ excluded: 'excluded',
89
+ };
90
+ const GROUP_LABEL = {
91
+ review: 'Needs review',
92
+ unchanged: 'Unchanged',
93
+ excluded: 'Excluded',
94
+ };
95
+ const GROUP_ORDER = ['review', 'unchanged', 'excluded'];
96
+ /**
97
+ * The variant list split into what needs a decision and what does not.
98
+ *
99
+ * `new` sits in `review` beside `changed`, which is the one placement worth
100
+ * explaining. A variant with no baseline is explicitly *not* a failure — the
101
+ * report contract says so and the row keeps its own neutral tone — but it is
102
+ * the one state that cannot resolve itself: somebody has to accept a baseline.
103
+ * `excluded` is the mirror image and gets its own group rather than being
104
+ * folded into `unchanged`: both are "not your problem", but "compared and
105
+ * matched" and "never captured" are different claims and a reviewer counting
106
+ * coverage needs to tell them apart.
107
+ *
108
+ * Empty groups are dropped instead of rendered at zero. The summary bar above
109
+ * the list has the same rule, and for the same reason — a zero-count header is
110
+ * a line of chrome asserting a colour nothing on screen has.
111
+ *
112
+ * Generic over the row rather than taking `VrtVariantResult`: the panel groups
113
+ * its own view models, which already carry the label and the formatted diff,
114
+ * and threading those back through a variant-shaped API would mean building
115
+ * them twice.
116
+ */
117
+ export function groupRows(rows) {
118
+ return GROUP_ORDER.map((key) => ({
119
+ key,
120
+ label: GROUP_LABEL[key],
121
+ collapsible: key !== 'review',
122
+ rows: STATUS_ORDER.filter((status) => GROUP_OF[status] === key).flatMap((status) => rows.filter((row) => row.status === status)),
123
+ }))
124
+ .filter((group) => group.rows.length > 0)
125
+ .map((group) => ({ ...group, count: group.rows.length }));
126
+ }
127
+ /**
128
+ * Which groups start open.
129
+ *
130
+ * Nothing, while something needs review — the point of the grouping is that
131
+ * the fourteen unchanged variants stop competing with the one that changed.
132
+ * When there is no review group the rule inverts and the first group opens,
133
+ * because two collapsed headers and no rows reads as a panel that failed to
134
+ * load rather than as a clean run.
135
+ */
136
+ export function defaultExpandedGroups(groups) {
137
+ if (groups.some((group) => group.key === 'review'))
138
+ return [];
139
+ return groups.length ? [groups[0].key] : [];
140
+ }
141
+ /**
142
+ * The headline figure and its colour for one component.
143
+ *
144
+ * The colour comes from the *statuses*, not from the percentage, and that is
145
+ * deliberate: {@link DEFAULT_VRT_THRESHOLDS} already says a run is only green
146
+ * at a perfect score, so a "small enough to stay amber" diff would contradict
147
+ * the badge sitting in the same header. Magnitude is what the value carries.
148
+ * Amber is reserved for the variants that could not be measured at all —
149
+ * resized and new — which are neither a regression nor a clean pass.
150
+ *
151
+ * Computed at build time and stored in the component's meta, so the component
152
+ * head can read two primitives instead of reimplementing this plugin's status
153
+ * semantics in the core app.
154
+ */
155
+ export function statSummary(summary) {
156
+ const { counts, maxDiffRatio } = summary;
157
+ return {
158
+ value: maxDiffRatio === null ? '—' : formatPercent(maxDiffRatio),
159
+ variant: counts.changed > 0
160
+ ? 'danger'
161
+ : counts['size-mismatch'] > 0 || counts.new > 0
162
+ ? 'warn'
163
+ : 'ok',
164
+ };
165
+ }
@@ -1,16 +1,40 @@
1
1
  import { type VrtSummary } from './vrt-summarize.js';
2
2
  import * as i0 from "@angular/core";
3
3
  /**
4
- * The strip that opens the panel.
4
+ * The run's headline, sitting above the variant list.
5
5
  *
6
- * Every other ng-prism panel leads with a summary the a11y score ring, the
7
- * coverage stat row so the visual regression panel does too, and it borrows
8
- * their card, micro-label and mono-numeral treatment rather than inventing a
9
- * third one.
6
+ * It lives in the 250px column rather than in a band across the panel, and
7
+ * that placement is the whole design. This panel docks at the bottom of the
8
+ * app at 260px by default, which leaves ~228px inside it; a full-width summary
9
+ * strip took ~89px of that before the comparison had rendered a single pixel.
10
+ * In the narrow column it costs the variant list about one row and costs the
11
+ * image nothing — and the image is what the panel is for.
12
+ *
13
+ * The cost of that is precision, so the split is deliberate: the bar carries
14
+ * the *composition* (which statuses, in what proportion) where a glance is
15
+ * enough, and the line under it carries the one figure a reviewer acts on.
16
+ * Exact per-status counts live one place over, in the grouped list below,
17
+ * whose headers already read "Needs review · 1" and "Unchanged · 14" —
18
+ * restating them here would be the tile strip again, in a narrower column.
19
+ * The line therefore disappears entirely when nothing could be compared,
20
+ * rather than printing a dash under a bar that already says so.
10
21
  */
11
22
  export declare class VrtSummaryComponent {
12
23
  readonly summary: import("@angular/core").InputSignal<VrtSummary>;
13
- protected readonly tiles: import("@angular/core").Signal<import("./vrt-summarize.js").VrtTile[]>;
24
+ protected readonly segments: import("@angular/core").Signal<import("./vrt-summarize.js").VrtSegment[]>;
25
+ protected readonly maxDiff: import("@angular/core").Signal<{
26
+ value: string;
27
+ tone: string;
28
+ } | null>;
29
+ /**
30
+ * The bar's content as a sentence.
31
+ *
32
+ * The slices are the only place the per-status breakdown is shown, and they
33
+ * are colour and proportion — nothing a screen reader can read. Each slice
34
+ * carries a `title` for a pointer; this carries the same thing for everyone
35
+ * else.
36
+ */
37
+ protected readonly barLabel: import("@angular/core").Signal<string>;
14
38
  static ɵfac: i0.ɵɵFactoryDeclaration<VrtSummaryComponent, never>;
15
39
  static ɵcmp: i0.ɵɵComponentDeclaration<VrtSummaryComponent, "prism-vrt-summary", never, { "summary": { "alias": "summary"; "required": true; "isSignal": true; }; }, {}, never, never, true, never>;
16
40
  }
@@ -1 +1 @@
1
- {"version":3,"file":"vrt-summary.component.d.ts","sourceRoot":"","sources":["../src/vrt-summary.component.ts"],"names":[],"mappings":"AAMA,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAC;;AAEnE;;;;;;;GAOG;AACH,qBAiFa,mBAAmB;IAC9B,QAAQ,CAAC,OAAO,kDAAgC;IAEhD,SAAS,CAAC,QAAQ,CAAC,KAAK,yEAAgD;yCAH7D,mBAAmB;2CAAnB,mBAAmB;CAI/B"}
1
+ {"version":3,"file":"vrt-summary.component.d.ts","sourceRoot":"","sources":["../src/vrt-summary.component.ts"],"names":[],"mappings":"AAMA,OAAO,EAGL,KAAK,UAAU,EAChB,MAAM,oBAAoB,CAAC;;AAE5B;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAqEa,mBAAmB;IAC9B,QAAQ,CAAC,OAAO,kDAAgC;IAEhD,SAAS,CAAC,QAAQ,CAAC,QAAQ,4EAAmD;IAE9E,SAAS,CAAC,QAAQ,CAAC,OAAO;;;cAOvB;IAEH;;;;;;;OAOG;IACH,SAAS,CAAC,QAAQ,CAAC,QAAQ,yCAMzB;yCA5BS,mBAAmB;2CAAnB,mBAAmB;CA6B/B"}
@@ -1,67 +1,116 @@
1
1
  import { ChangeDetectionStrategy, Component, computed, input, } from '@angular/core';
2
- import { summaryTiles } from './vrt-summarize.js';
2
+ import { formatPercent, summarySegments, } from './vrt-summarize.js';
3
3
  import * as i0 from "@angular/core";
4
4
  const _forTrack0 = ($index, $item) => $item.key;
5
- function VrtSummaryComponent_For_2_Template(rf, ctx) { if (rf & 1) {
6
- i0.ɵɵdomElementStart(0, "div", 1)(1, "div", 2);
7
- i0.ɵɵtext(2);
8
- i0.ɵɵdomElementEnd();
9
- i0.ɵɵdomElementStart(3, "div", 3);
5
+ function VrtSummaryComponent_For_3_Template(rf, ctx) { if (rf & 1) {
6
+ i0.ɵɵdomElement(0, "div", 4);
7
+ } if (rf & 2) {
8
+ const segment_r1 = ctx.$implicit;
9
+ i0.ɵɵstyleProp("flex-grow", segment_r1.count);
10
+ i0.ɵɵdomProperty("title", segment_r1.label + ": " + segment_r1.count);
11
+ i0.ɵɵattribute("data-tone", segment_r1.tone);
12
+ } }
13
+ function VrtSummaryComponent_Conditional_4_Template(rf, ctx) { if (rf & 1) {
14
+ i0.ɵɵdomElementStart(0, "div", 3)(1, "span", 5);
15
+ i0.ɵɵtext(2, " max ");
16
+ i0.ɵɵdomElementStart(3, "b");
10
17
  i0.ɵɵtext(4);
11
- i0.ɵɵdomElementEnd();
12
- i0.ɵɵdomElementStart(5, "div", 4);
13
- i0.ɵɵdomElement(6, "div", 5);
14
- i0.ɵɵdomElementEnd()();
18
+ i0.ɵɵdomElementEnd()()();
15
19
  } if (rf & 2) {
16
- const tile_r1 = ctx.$implicit;
17
- i0.ɵɵattribute("data-tone", tile_r1.tone);
18
- i0.ɵɵadvance(2);
19
- i0.ɵɵtextInterpolate(tile_r1.label);
20
- i0.ɵɵadvance(2);
21
- i0.ɵɵtextInterpolate(tile_r1.value);
22
- i0.ɵɵadvance(2);
23
- i0.ɵɵstyleProp("width", tile_r1.ratio * 100, "%");
20
+ const max_r2 = ctx;
21
+ i0.ɵɵadvance(3);
22
+ i0.ɵɵattribute("data-tone", max_r2.tone);
23
+ i0.ɵɵadvance();
24
+ i0.ɵɵtextInterpolate(max_r2.value);
24
25
  } }
25
26
  /**
26
- * The strip that opens the panel.
27
+ * The run's headline, sitting above the variant list.
28
+ *
29
+ * It lives in the 250px column rather than in a band across the panel, and
30
+ * that placement is the whole design. This panel docks at the bottom of the
31
+ * app at 260px by default, which leaves ~228px inside it; a full-width summary
32
+ * strip took ~89px of that before the comparison had rendered a single pixel.
33
+ * In the narrow column it costs the variant list about one row and costs the
34
+ * image nothing — and the image is what the panel is for.
27
35
  *
28
- * Every other ng-prism panel leads with a summary the a11y score ring, the
29
- * coverage stat row so the visual regression panel does too, and it borrows
30
- * their card, micro-label and mono-numeral treatment rather than inventing a
31
- * third one.
36
+ * The cost of that is precision, so the split is deliberate: the bar carries
37
+ * the *composition* (which statuses, in what proportion) where a glance is
38
+ * enough, and the line under it carries the one figure a reviewer acts on.
39
+ * Exact per-status counts live one place over, in the grouped list below,
40
+ * whose headers already read "Needs review · 1" and "Unchanged · 14" —
41
+ * restating them here would be the tile strip again, in a narrower column.
42
+ * The line therefore disappears entirely when nothing could be compared,
43
+ * rather than printing a dash under a bar that already says so.
32
44
  */
33
45
  export class VrtSummaryComponent {
34
46
  summary = input.required(/* @ts-ignore */
35
47
  ...(ngDevMode ? [{ debugName: "summary" }] : /* istanbul ignore next */ []));
36
- tiles = computed(() => summaryTiles(this.summary()), /* @ts-ignore */
37
- ...(ngDevMode ? [{ debugName: "tiles" }] : /* istanbul ignore next */ []));
48
+ segments = computed(() => summarySegments(this.summary()), /* @ts-ignore */
49
+ ...(ngDevMode ? [{ debugName: "segments" }] : /* istanbul ignore next */ []));
50
+ maxDiff = computed(() => {
51
+ const ratio = this.summary().maxDiffRatio;
52
+ if (ratio === null)
53
+ return null;
54
+ return {
55
+ value: formatPercent(ratio),
56
+ tone: ratio > 0 ? 'danger' : 'success',
57
+ };
58
+ }, /* @ts-ignore */
59
+ ...(ngDevMode ? [{ debugName: "maxDiff" }] : /* istanbul ignore next */ []));
60
+ /**
61
+ * The bar's content as a sentence.
62
+ *
63
+ * The slices are the only place the per-status breakdown is shown, and they
64
+ * are colour and proportion — nothing a screen reader can read. Each slice
65
+ * carries a `title` for a pointer; this carries the same thing for everyone
66
+ * else.
67
+ */
68
+ barLabel = computed(() => this.segments().length === 0
69
+ ? 'No variants'
70
+ : this.segments()
71
+ .map((segment) => `${segment.count} ${segment.label.toLowerCase()}`)
72
+ .join(', '), /* @ts-ignore */
73
+ ...(ngDevMode ? [{ debugName: "barLabel" }] : /* istanbul ignore next */ []));
38
74
  static ɵfac = function VrtSummaryComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || VrtSummaryComponent)(); };
39
- static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: VrtSummaryComponent, selectors: [["prism-vrt-summary"]], inputs: { summary: [1, "summary"] }, decls: 3, vars: 0, consts: [[1, "vrt-sum"], [1, "vrt-sum__tile"], [1, "vrt-sum__label"], [1, "vrt-sum__value"], [1, "vrt-sum__bar"], [1, "vrt-sum__bar-fill"]], template: function VrtSummaryComponent_Template(rf, ctx) { if (rf & 1) {
40
- i0.ɵɵdomElementStart(0, "div", 0);
41
- i0.ɵɵrepeaterCreate(1, VrtSummaryComponent_For_2_Template, 7, 5, "div", 1, _forTrack0);
75
+ static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: VrtSummaryComponent, selectors: [["prism-vrt-summary"]], inputs: { summary: [1, "summary"] }, decls: 5, vars: 2, consts: [[1, "vrt-sum"], ["role", "img", 1, "vrt-sum__bar"], [1, "vrt-sum__seg", 3, "flex-grow", "title"], [1, "vrt-sum__line"], [1, "vrt-sum__seg", 3, "title"], [1, "vrt-sum__stat"]], template: function VrtSummaryComponent_Template(rf, ctx) { if (rf & 1) {
76
+ i0.ɵɵdomElementStart(0, "div", 0)(1, "div", 1);
77
+ i0.ɵɵrepeaterCreate(2, VrtSummaryComponent_For_3_Template, 1, 4, "div", 2, _forTrack0);
78
+ i0.ɵɵdomElementEnd();
79
+ i0.ɵɵconditionalCreate(4, VrtSummaryComponent_Conditional_4_Template, 5, 2, "div", 3);
42
80
  i0.ɵɵdomElementEnd();
43
81
  } if (rf & 2) {
82
+ let tmp_2_0;
83
+ i0.ɵɵadvance();
84
+ i0.ɵɵattribute("aria-label", ctx.barLabel());
44
85
  i0.ɵɵadvance();
45
- i0.ɵɵrepeater(ctx.tiles());
46
- } }, styles: ["[_nghost-%COMP%] { display: block; }\n\n .vrt-sum[_ngcontent-%COMP%] {\n display: grid;\n \n\n grid-template-columns: repeat(auto-fit, minmax(118px, 1fr));\n gap: 10px;\n }\n\n .vrt-sum__tile[_ngcontent-%COMP%] {\n padding: 10px 12px;\n background: var(--prism-bg-surface);\n border: 1px solid var(--prism-border);\n border-radius: var(--radius-lg);\n min-width: 0;\n }\n\n .vrt-sum__label[_ngcontent-%COMP%] {\n font-size: 10.5px;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n font-weight: 700;\n color: var(--prism-text-ghost);\n margin-bottom: 5px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n .vrt-sum__value[_ngcontent-%COMP%] {\n font-family: var(--font-mono);\n font-size: 21px;\n font-weight: 700;\n letter-spacing: -0.02em;\n color: var(--tone-color, var(--prism-text));\n line-height: 1.1;\n }\n\n .vrt-sum__bar[_ngcontent-%COMP%] {\n margin-top: 8px;\n height: 4px;\n background: var(--prism-input-bg);\n border-radius: 2px;\n overflow: hidden;\n }\n .vrt-sum__bar-fill[_ngcontent-%COMP%] {\n height: 100%;\n border-radius: 2px;\n background: var(--tone-color, var(--prism-primary));\n transition: width var(--dur-slow) var(--ease-default);\n }\n\n [data-tone='success'][_ngcontent-%COMP%] { --tone-color: var(--prism-success); }\n [data-tone='danger'][_ngcontent-%COMP%] { --tone-color: var(--prism-danger); }\n [data-tone='warn'][_ngcontent-%COMP%] { --tone-color: var(--prism-warn); }\n [data-tone='neutral'][_ngcontent-%COMP%] { --tone-color: var(--prism-primary); }\n [data-tone='muted'][_ngcontent-%COMP%] { --tone-color: var(--prism-text-muted); }"] });
86
+ i0.ɵɵrepeater(ctx.segments());
87
+ i0.ɵɵadvance(2);
88
+ i0.ɵɵconditional((tmp_2_0 = ctx.maxDiff()) ? 4 : -1, tmp_2_0);
89
+ } }, styles: ["[_nghost-%COMP%] { display: block; }\n\n .vrt-sum__bar[_ngcontent-%COMP%] {\n display: flex;\n gap: 1px;\n height: 6px;\n border-radius: 3px;\n overflow: hidden;\n background: var(--prism-input-bg);\n }\n \n\n\n .vrt-sum__seg[_ngcontent-%COMP%] {\n flex-basis: 0;\n background: var(--tone-color, var(--prism-text-muted));\n transition: flex-grow var(--dur-slow) var(--ease-default);\n }\n\n .vrt-sum__line[_ngcontent-%COMP%] {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 8px;\n margin-top: 9px;\n font-family: var(--font-mono);\n font-size: var(--fs-sm);\n color: var(--prism-text-muted);\n }\n .vrt-sum__stat[_ngcontent-%COMP%] { white-space: nowrap; }\n .vrt-sum__line[_ngcontent-%COMP%] b[_ngcontent-%COMP%] {\n font-weight: 700;\n color: var(--tone-color, var(--prism-text));\n }\n\n [data-tone='success'][_ngcontent-%COMP%] { --tone-color: var(--prism-success); }\n [data-tone='danger'][_ngcontent-%COMP%] { --tone-color: var(--prism-danger); }\n [data-tone='warn'][_ngcontent-%COMP%] { --tone-color: var(--prism-warn); }\n [data-tone='neutral'][_ngcontent-%COMP%] { --tone-color: var(--prism-primary); }\n [data-tone='muted'][_ngcontent-%COMP%] { --tone-color: var(--prism-text-muted); }"] });
47
90
  }
48
91
  (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(VrtSummaryComponent, [{
49
92
  type: Component,
50
93
  args: [{ selector: 'prism-vrt-summary', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
51
94
  <div class="vrt-sum">
52
- @for (tile of tiles(); track tile.key) {
53
- <div class="vrt-sum__tile" [attr.data-tone]="tile.tone">
54
- <div class="vrt-sum__label">{{ tile.label }}</div>
55
- <div class="vrt-sum__value">{{ tile.value }}</div>
56
- <div class="vrt-sum__bar">
57
- <div
58
- class="vrt-sum__bar-fill"
59
- [style.width.%]="tile.ratio * 100"
60
- ></div>
61
- </div>
95
+ <div class="vrt-sum__bar" role="img" [attr.aria-label]="barLabel()">
96
+ @for (segment of segments(); track segment.key) {
97
+ <div
98
+ class="vrt-sum__seg"
99
+ [attr.data-tone]="segment.tone"
100
+ [style.flex-grow]="segment.count"
101
+ [title]="segment.label + ': ' + segment.count"
102
+ ></div>
103
+ }
104
+ </div>
105
+
106
+ @if (maxDiff(); as max) {
107
+ <div class="vrt-sum__line">
108
+ <span class="vrt-sum__stat">
109
+ max <b [attr.data-tone]="max.tone">{{ max.value }}</b>
110
+ </span>
62
111
  </div>
63
112
  }
64
113
  </div>
65
- `, styles: ["\n :host { display: block; }\n\n .vrt-sum {\n display: grid;\n /* auto-fit rather than a fixed column count: the strip carries between\n two and six tiles depending on what the run produced. */\n grid-template-columns: repeat(auto-fit, minmax(118px, 1fr));\n gap: 10px;\n }\n\n .vrt-sum__tile {\n padding: 10px 12px;\n background: var(--prism-bg-surface);\n border: 1px solid var(--prism-border);\n border-radius: var(--radius-lg);\n min-width: 0;\n }\n\n .vrt-sum__label {\n font-size: 10.5px;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n font-weight: 700;\n color: var(--prism-text-ghost);\n margin-bottom: 5px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n .vrt-sum__value {\n font-family: var(--font-mono);\n font-size: 21px;\n font-weight: 700;\n letter-spacing: -0.02em;\n color: var(--tone-color, var(--prism-text));\n line-height: 1.1;\n }\n\n .vrt-sum__bar {\n margin-top: 8px;\n height: 4px;\n background: var(--prism-input-bg);\n border-radius: 2px;\n overflow: hidden;\n }\n .vrt-sum__bar-fill {\n height: 100%;\n border-radius: 2px;\n background: var(--tone-color, var(--prism-primary));\n transition: width var(--dur-slow) var(--ease-default);\n }\n\n [data-tone='success'] { --tone-color: var(--prism-success); }\n [data-tone='danger'] { --tone-color: var(--prism-danger); }\n [data-tone='warn'] { --tone-color: var(--prism-warn); }\n [data-tone='neutral'] { --tone-color: var(--prism-primary); }\n [data-tone='muted'] { --tone-color: var(--prism-text-muted); }\n "] }]
114
+ `, styles: ["\n :host { display: block; }\n\n .vrt-sum__bar {\n display: flex;\n gap: 1px;\n height: 6px;\n border-radius: 3px;\n overflow: hidden;\n background: var(--prism-input-bg);\n }\n /* flex-basis 0 so a slice's width comes from its count alone \u2014 with the\n default of auto an empty div still claims its content box and every\n segment would render the same width. */\n .vrt-sum__seg {\n flex-basis: 0;\n background: var(--tone-color, var(--prism-text-muted));\n transition: flex-grow var(--dur-slow) var(--ease-default);\n }\n\n .vrt-sum__line {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 8px;\n margin-top: 9px;\n font-family: var(--font-mono);\n font-size: var(--fs-sm);\n color: var(--prism-text-muted);\n }\n .vrt-sum__stat { white-space: nowrap; }\n .vrt-sum__line b {\n font-weight: 700;\n color: var(--tone-color, var(--prism-text));\n }\n\n [data-tone='success'] { --tone-color: var(--prism-success); }\n [data-tone='danger'] { --tone-color: var(--prism-danger); }\n [data-tone='warn'] { --tone-color: var(--prism-warn); }\n [data-tone='neutral'] { --tone-color: var(--prism-primary); }\n [data-tone='muted'] { --tone-color: var(--prism-text-muted); }\n "] }]
66
115
  }], null, { summary: [{ type: i0.Input, args: [{ isSignal: true, alias: "summary", required: true }] }] }); })();
67
- (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(VrtSummaryComponent, { className: "VrtSummaryComponent", filePath: "vrt-summary.component.ts", lineNumber: 98 }); })();
116
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(VrtSummaryComponent, { className: "VrtSummaryComponent", filePath: "vrt-summary.component.ts", lineNumber: 101 }); })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ng-prism/plugin-visual-regression",
3
- "version": "22.2.0-beta.2",
3
+ "version": "22.2.0-beta.4",
4
4
  "description": "Visual regression report panel for ng-prism.",
5
5
  "ng-add": {
6
6
  "save": "devDependencies"