@oicl/openbridge-webcomponents-full-bundle 2.0.0-next.151 → 2.0.0-next.152

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.
Files changed (136) hide show
  1. package/.storybook/vitest.setup.ts +12 -1
  2. package/bundle/openbridge-webcomponents.bundle.js +989 -319
  3. package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
  4. package/custom-elements.json +1514 -77
  5. package/dist/alert-severity.d.ts +12 -9
  6. package/dist/alert-severity.d.ts.map +1 -1
  7. package/dist/alert-severity.js +30 -38
  8. package/dist/alert-severity.js.map +1 -1
  9. package/dist/building-blocks/bar-horizontal/bar-horizontal.d.ts +6 -0
  10. package/dist/building-blocks/bar-horizontal/bar-horizontal.d.ts.map +1 -1
  11. package/dist/building-blocks/bar-horizontal/bar-horizontal.js +6 -1
  12. package/dist/building-blocks/bar-horizontal/bar-horizontal.js.map +1 -1
  13. package/dist/building-blocks/bar-vertical/bar-vertical.d.ts +6 -0
  14. package/dist/building-blocks/bar-vertical/bar-vertical.d.ts.map +1 -1
  15. package/dist/building-blocks/bar-vertical/bar-vertical.js +6 -1
  16. package/dist/building-blocks/bar-vertical/bar-vertical.js.map +1 -1
  17. package/dist/building-blocks/chart-line/chart-line-base.d.ts +129 -0
  18. package/dist/building-blocks/chart-line/chart-line-base.d.ts.map +1 -1
  19. package/dist/building-blocks/chart-line/chart-line-base.js +464 -79
  20. package/dist/building-blocks/chart-line/chart-line-base.js.map +1 -1
  21. package/dist/building-blocks/external-scale/external-scale.d.ts +11 -0
  22. package/dist/building-blocks/external-scale/external-scale.d.ts.map +1 -1
  23. package/dist/building-blocks/external-scale/external-scale.js +45 -12
  24. package/dist/building-blocks/external-scale/external-scale.js.map +1 -1
  25. package/dist/charthelpers/index.d.ts +1 -0
  26. package/dist/charthelpers/index.d.ts.map +1 -1
  27. package/dist/charthelpers/index.js +7 -1
  28. package/dist/charthelpers/index.js.map +1 -1
  29. package/dist/charthelpers/range-labels.d.ts +55 -0
  30. package/dist/charthelpers/range-labels.d.ts.map +1 -0
  31. package/dist/charthelpers/range-labels.js +36 -0
  32. package/dist/charthelpers/range-labels.js.map +1 -0
  33. package/dist/components/alert-button/alert-button.css.js +13 -21
  34. package/dist/components/alert-button/alert-button.css.js.map +1 -1
  35. package/dist/components/alert-button/alert-button.d.ts +9 -4
  36. package/dist/components/alert-button/alert-button.d.ts.map +1 -1
  37. package/dist/components/alert-button/alert-button.js +24 -15
  38. package/dist/components/alert-button/alert-button.js.map +1 -1
  39. package/dist/components/alert-frame/alert-frame.css.js +109 -33
  40. package/dist/components/alert-frame/alert-frame.css.js.map +1 -1
  41. package/dist/components/alert-frame/alert-frame.d.ts +34 -7
  42. package/dist/components/alert-frame/alert-frame.d.ts.map +1 -1
  43. package/dist/components/alert-frame/alert-frame.js +134 -30
  44. package/dist/components/alert-frame/alert-frame.js.map +1 -1
  45. package/dist/components/alert-icon/alert-icon.d.ts +10 -7
  46. package/dist/components/alert-icon/alert-icon.d.ts.map +1 -1
  47. package/dist/components/alert-icon/alert-icon.js +37 -60
  48. package/dist/components/alert-icon/alert-icon.js.map +1 -1
  49. package/dist/components/alert-menu-item/alert-menu-item.css.js +1 -4
  50. package/dist/components/alert-menu-item/alert-menu-item.css.js.map +1 -1
  51. package/dist/components/date-item/date-item.css.js +3 -3
  52. package/dist/components/keyboard-full/keyboard-full.css.js +4 -4
  53. package/dist/components/message-menu-item/message-menu-item.css.js +2 -6
  54. package/dist/components/message-menu-item/message-menu-item.css.js.map +1 -1
  55. package/dist/components/sequence-toolbar/sequence-toolbar.css.js +1 -2
  56. package/dist/components/sequence-toolbar/sequence-toolbar.css.js.map +1 -1
  57. package/dist/index.js +2 -1
  58. package/dist/navigation-instruments/gauge-trend/gauge-trend.d.ts +6 -0
  59. package/dist/navigation-instruments/gauge-trend/gauge-trend.d.ts.map +1 -1
  60. package/dist/navigation-instruments/gauge-trend/gauge-trend.js +8 -0
  61. package/dist/navigation-instruments/gauge-trend/gauge-trend.js.map +1 -1
  62. package/dist/navigation-instruments/instrument-field/instrument-field.css.js +2 -2
  63. package/dist/navigation-instruments/readout/readout.d.ts.map +1 -1
  64. package/dist/navigation-instruments/readout/readout.js +2 -1
  65. package/dist/navigation-instruments/readout/readout.js.map +1 -1
  66. package/dist/navigation-instruments/readout-list-item/readout-list-item.d.ts.map +1 -1
  67. package/dist/navigation-instruments/readout-list-item/readout-list-item.js +2 -1
  68. package/dist/navigation-instruments/readout-list-item/readout-list-item.js.map +1 -1
  69. package/dist/openbridge.css +19 -4
  70. package/dist/palettes/blinking.d.ts +14 -4
  71. package/dist/palettes/blinking.d.ts.map +1 -1
  72. package/dist/palettes/blinking.js +41 -35
  73. package/dist/palettes/blinking.js.map +1 -1
  74. package/dist/palettes/flashing-controller.d.ts +21 -0
  75. package/dist/palettes/flashing-controller.d.ts.map +1 -0
  76. package/dist/palettes/flashing-controller.js +45 -0
  77. package/dist/palettes/flashing-controller.js.map +1 -0
  78. package/dist/svghelpers/index.d.ts +1 -0
  79. package/dist/svghelpers/index.d.ts.map +1 -1
  80. package/dist/svghelpers/index.js +2 -0
  81. package/dist/svghelpers/index.js.map +1 -1
  82. package/dist/svghelpers/rounded-rect.d.ts +15 -0
  83. package/dist/svghelpers/rounded-rect.d.ts.map +1 -0
  84. package/dist/svghelpers/rounded-rect.js +29 -0
  85. package/dist/svghelpers/rounded-rect.js.map +1 -0
  86. package/dist/types.d.ts +14 -0
  87. package/dist/types.d.ts.map +1 -1
  88. package/dist/types.js +9 -0
  89. package/dist/types.js.map +1 -1
  90. package/package.json +1 -1
  91. package/script/check-css-variables.ts +2 -15
  92. package/src/alert-severity.spec.ts +56 -0
  93. package/src/alert-severity.ts +45 -43
  94. package/src/bars-graphs/area-graph/area-graph.stories.ts +235 -1
  95. package/src/bars-graphs/line-graph/line-graph.stories.ts +24 -0
  96. package/src/building-blocks/bar-horizontal/bar-horizontal.ts +7 -0
  97. package/src/building-blocks/bar-vertical/bar-vertical.ts +7 -0
  98. package/src/building-blocks/chart-line/chart-line-base.spec.ts +767 -0
  99. package/src/building-blocks/chart-line/chart-line-base.stories.ts +124 -0
  100. package/src/building-blocks/chart-line/chart-line-base.ts +671 -97
  101. package/src/building-blocks/chart-line/chart-sizing-battleground.stories.ts +17 -0
  102. package/src/building-blocks/external-scale/external-scale.spec.ts +154 -0
  103. package/src/building-blocks/external-scale/external-scale.ts +78 -20
  104. package/src/charthelpers/index.ts +1 -0
  105. package/src/charthelpers/range-labels.spec.ts +46 -0
  106. package/src/charthelpers/range-labels.ts +93 -0
  107. package/src/components/alert-button/alert-button.css +10 -23
  108. package/src/components/alert-button/alert-button.spec.ts +56 -15
  109. package/src/components/alert-button/alert-button.stories.ts +5 -1
  110. package/src/components/alert-button/alert-button.ts +33 -24
  111. package/src/components/alert-frame/alert-flash-scenario.stories.ts +669 -0
  112. package/src/components/alert-frame/alert-frame.css +106 -33
  113. package/src/components/alert-frame/alert-frame.spec.ts +176 -17
  114. package/src/components/alert-frame/alert-frame.stories.ts +61 -0
  115. package/src/components/alert-frame/alert-frame.ts +179 -41
  116. package/src/components/alert-icon/alert-icon.spec.ts +82 -27
  117. package/src/components/alert-icon/alert-icon.stories.ts +7 -1
  118. package/src/components/alert-icon/alert-icon.ts +48 -74
  119. package/src/components/alert-menu-item/alert-menu-item.css +1 -4
  120. package/src/components/date-item/date-item.css +3 -3
  121. package/src/components/keyboard-full/keyboard-full.css +4 -4
  122. package/src/components/message-menu-item/message-menu-item.css +2 -6
  123. package/src/components/sequence-toolbar/sequence-toolbar.css +1 -2
  124. package/src/navigation-instruments/gauge-trend/gauge-trend.stories.ts +29 -0
  125. package/src/navigation-instruments/gauge-trend/gauge-trend.ts +11 -0
  126. package/src/navigation-instruments/instrument-field/instrument-field.css +2 -2
  127. package/src/navigation-instruments/readout/readout.ts +2 -1
  128. package/src/navigation-instruments/readout-list-item/readout-list-item.ts +2 -1
  129. package/src/palettes/blinking.spec.ts +83 -0
  130. package/src/palettes/blinking.ts +47 -35
  131. package/src/palettes/flashing-controller.ts +62 -0
  132. package/src/palettes/manual.css +18 -4
  133. package/src/svghelpers/index.ts +1 -0
  134. package/src/svghelpers/rounded-rect.spec.ts +47 -0
  135. package/src/svghelpers/rounded-rect.ts +42 -0
  136. package/src/types.ts +20 -0
@@ -27,8 +27,6 @@
27
27
  width: 100%;
28
28
  max-width: var(--menu-navigation-components-date-item-touch-target);
29
29
  height: var(--menu-navigation-components-date-item-touch-target);
30
- padding: var(--menu-navigation-components-date-item-padding-vertical)
31
- var(--menu-navigation-components-date-item-padding-horizontal);
32
30
 
33
31
  /* Keyboard press visual feedback */
34
32
  &.pressing .date-container {
@@ -175,7 +173,9 @@
175
173
  display: flex;
176
174
  width: var(--menu-navigation-components-date-item-touch-target, 48px);
177
175
  height: var(--menu-navigation-components-date-item-touch-target, 48px);
178
- padding: var(--menu-navigation-components-date-item-padding-vertical, 8px)
176
+ /* TODO(designer): Figma pads this frame with Date item/padding-bottom, 12 and 16px in
177
+ large and xl, less room than the date container needs; 8px fits every size (#929) */
178
+ padding: 8px
179
179
  var(--menu-navigation-components-date-item-padding-horizontal, 8px);
180
180
  flex-direction: column;
181
181
  justify-content: center;
@@ -109,8 +109,8 @@ obc-input::part(input) {
109
109
  .key-button {
110
110
  display: flex;
111
111
  flex: 1 0 0;
112
- min-width: var(--ui-components-keyboard-numeric-touch-target-size);
113
- min-height: var(--ui-components-keyboard-numeric-button-size-min);
112
+ min-width: var(--ui-components-keyboard-button-size-min);
113
+ min-height: var(--ui-components-keyboard-button-size-min);
114
114
  }
115
115
 
116
116
  obc-button.key-button::part(wrapper) {
@@ -121,8 +121,8 @@ obc-button.key-button::part(visible-wrapper) {
121
121
  box-sizing: border-box;
122
122
  display: flex;
123
123
  width: 100%;
124
- min-width: var(--ui-components-keyboard-numeric-touch-target-size);
125
- min-height: var(--ui-components-keyboard-numeric-button-size-min);
124
+ min-width: var(--ui-components-keyboard-button-size-min);
125
+ min-height: var(--ui-components-keyboard-button-size-min);
126
126
  align-items: center;
127
127
  justify-content: center;
128
128
  align-self: stretch;
@@ -167,12 +167,8 @@
167
167
  }
168
168
 
169
169
  .trailing-icon {
170
- width: var(
171
- --app-components-alert-components-general-notification-item-icon-size-large
172
- );
173
- height: var(
174
- --app-components-alert-components-general-notification-item-icon-size-large
175
- );
170
+ width: var(--app-components-message-menu-item-icon-size-large);
171
+ height: var(--app-components-message-menu-item-icon-size-large);
176
172
  display: flex;
177
173
  align-items: center;
178
174
  justify-content: center;
@@ -12,7 +12,7 @@
12
12
  align-items: center;
13
13
  padding: 5px;
14
14
  border: 1px solid var(--border-outline-color);
15
- background-color: var(--enabled-background-color);
15
+ background-color: var(--container-global-color);
16
16
 
17
17
  &.type-sequential,
18
18
  &.type-unordered {
@@ -28,7 +28,6 @@
28
28
 
29
29
  &.type-condensed {
30
30
  border-radius: 100px;
31
- background-color: var(--normal-enabled-background-color);
32
31
 
33
32
  .condensed-label__current {
34
33
  font-weight: var(--font-weight-Bold, 670);
@@ -170,6 +170,10 @@ const meta: Meta = {
170
170
  chartFill: {
171
171
  control: 'boolean',
172
172
  },
173
+ rangeLabels: {
174
+ control: {type: 'radio'},
175
+ options: ['none', 'y', 'x', 'xy'],
176
+ },
173
177
  touching: {
174
178
  control: 'boolean',
175
179
  },
@@ -190,6 +194,7 @@ const meta: Meta = {
190
194
  fillMin: 0,
191
195
  // Note: fillMax intentionally omitted - in 'fill' mode it auto-derives from value
192
196
  chartFill: true,
197
+ rangeLabels: 'none',
193
198
  touching: false,
194
199
  },
195
200
  render: (args) => html`
@@ -199,6 +204,7 @@ const meta: Meta = {
199
204
  .height=${args.height}
200
205
  .priority=${args.priority}
201
206
  .chartFill=${args.chartFill}
207
+ .rangeLabels=${args.rangeLabels}
202
208
  .minValue=${args.minValue}
203
209
  .maxValue=${args.maxValue}
204
210
  .value=${args.value}
@@ -242,6 +248,7 @@ export const GaugeTrend: Story = {
242
248
  .height=${_args.height}
243
249
  .priority=${_args.priority}
244
250
  .chartFill=${_args.chartFill}
251
+ .rangeLabels=${_args.rangeLabels}
245
252
  .minValue=${_args.minValue ?? 0}
246
253
  .maxValue=${_args.maxValue ?? 100}
247
254
  .value=${_args.value}
@@ -266,6 +273,21 @@ export const GaugeTrend: Story = {
266
273
  `,
267
274
  };
268
275
 
276
+ export const CompactRangeLabels: Story = {
277
+ name: 'Compact With Range Labels (160px)',
278
+ play: async () => {
279
+ await new Promise((resolve) => setTimeout(resolve, 300));
280
+ },
281
+ args: {
282
+ browserContainerWidth: 160,
283
+ width: 160,
284
+ height: 160,
285
+ rangeLabels: 'xy',
286
+ hasBar: true,
287
+ hasScale: true,
288
+ },
289
+ };
290
+
269
291
  export const UnevenTimeIntervals: Story = {
270
292
  name: 'Uneven Time Intervals (Auto Time Axis)',
271
293
  play: async () => {
@@ -279,6 +301,7 @@ export const UnevenTimeIntervals: Story = {
279
301
  .height=${_args.height}
280
302
  .priority=${_args.priority}
281
303
  .chartFill=${_args.chartFill}
304
+ .rangeLabels=${_args.rangeLabels}
282
305
  .minValue=${_args.minValue ?? 0}
283
306
  .maxValue=${_args.maxValue ?? 100}
284
307
  .value=${_args.value}
@@ -311,6 +334,7 @@ export const GaugeTrendScaleReferenceSize: Story = {
311
334
  .height=${_args.height}
312
335
  .priority=${_args.priority}
313
336
  .chartFill=${_args.chartFill}
337
+ .rangeLabels=${_args.rangeLabels}
314
338
  .minValue=${_args.minValue ?? 0}
315
339
  .maxValue=${_args.maxValue ?? 100}
316
340
  .value=${_args.value}
@@ -531,6 +555,7 @@ export const GaugeTrendWithoutScale: Story = {
531
555
  .height=${_args.height}
532
556
  .priority=${_args.priority}
533
557
  .chartFill=${_args.chartFill}
558
+ .rangeLabels=${_args.rangeLabels}
534
559
  .minValue=${_args.minValue ?? 0}
535
560
  .maxValue=${_args.maxValue ?? 100}
536
561
  .value=${_args.value}
@@ -572,6 +597,7 @@ export const GaugeTrendWithoutBar: Story = {
572
597
  .height=${_args.height}
573
598
  .priority=${_args.priority}
574
599
  .chartFill=${_args.chartFill}
600
+ .rangeLabels=${_args.rangeLabels}
575
601
  .minValue=${_args.minValue ?? 0}
576
602
  .maxValue=${_args.maxValue ?? 100}
577
603
  .value=${_args.value}
@@ -617,6 +643,7 @@ export const GaugeTrendWithAdvice: Story = {
617
643
  .height=${_args.height}
618
644
  .priority=${_args.priority}
619
645
  .chartFill=${_args.chartFill}
646
+ .rangeLabels=${_args.rangeLabels}
620
647
  .minValue=${_args.minValue ?? 0}
621
648
  .maxValue=${_args.maxValue ?? 100}
622
649
  .value=${_args.value}
@@ -708,6 +735,7 @@ export const GaugeTrendCustomScaleRange: Story = {
708
735
  .height=${_args.height}
709
736
  .priority=${_args.priority}
710
737
  .chartFill=${_args.chartFill}
738
+ .rangeLabels=${_args.rangeLabels}
711
739
  .minValue=${_args.minValue}
712
740
  .maxValue=${_args.maxValue}
713
741
  .value=${_args.value}
@@ -753,6 +781,7 @@ export const GaugeTrendLabelsOnly: Story = {
753
781
  .height=${_args.height}
754
782
  .priority=${_args.priority}
755
783
  .chartFill=${_args.chartFill}
784
+ .rangeLabels=${_args.rangeLabels}
756
785
  .minValue=${_args.minValue ?? 0}
757
786
  .maxValue=${_args.maxValue ?? 100}
758
787
  .value=${_args.value}
@@ -442,6 +442,17 @@ export class ObcGaugeTrend extends SetpointMixin(ObcChartLineBase) {
442
442
  return false;
443
443
  }
444
444
 
445
+ /**
446
+ * The right scale is this component's own bar, ranged by `minValue` /
447
+ * `maxValue`. `chartMinValue` / `chartMaxValue` exist so the plotted range
448
+ * can differ from it, so the chart's axis range must not overwrite it.
449
+ */
450
+ protected override ownsSlottedScaleRange(
451
+ side: 'left' | 'right' | 'top' | 'bottom'
452
+ ): boolean {
453
+ return side !== 'right';
454
+ }
455
+
445
456
  override willUpdate(changed: Map<PropertyKey, unknown>) {
446
457
  // Auto axis detection: {x, value} data switches to time spacing, {label,
447
458
  // value} stays category. An explicit xAxisType assignment (anything we
@@ -76,7 +76,7 @@
76
76
 
77
77
  .tag {
78
78
  @mixin font-instrument-label;
79
- width: var(--obc-instrument-field-tag-width);
79
+ width: var(--obc-instrument-field-tag-width, auto);
80
80
  }
81
81
 
82
82
  .unit {
@@ -109,7 +109,7 @@
109
109
  @mixin font-instrument-unit;
110
110
  color: var(--element-neutral-color);
111
111
  justify-content: end;
112
- width: var(--obc-instrument-field-source-width);
112
+ width: var(--obc-instrument-field-source-width, auto);
113
113
  box-sizing: content-box;
114
114
  }
115
115
 
@@ -55,7 +55,7 @@ import {
55
55
  ObcAlertFrameThickness,
56
56
  ObcAlertFrameMode,
57
57
  } from '../../components/alert-frame/alert-frame.js';
58
- import {AlertType} from '../../types.js';
58
+ import {AlertType, FlashingSpeed} from '../../types.js';
59
59
  import '../../components/button/button.js';
60
60
  import '../../components/context-menu-input/context-menu-input.js';
61
61
  import {
@@ -866,6 +866,7 @@ export class ObcReadout extends LitElement {
866
866
  .thickness=${thickness}
867
867
  .status=${alert.status ?? AlertType.Alarm}
868
868
  .mode=${alert.mode ?? ObcAlertFrameMode.ackedActive}
869
+ .flashingSpeed=${alert.flashingSpeed ?? FlashingSpeed.Default}
869
870
  .showIcon=${alert.showIcon ?? false}
870
871
  .showAlertCategoryIcon=${alert.showAlertCategoryIcon ?? true}
871
872
  .wrapContent=${false}
@@ -44,7 +44,7 @@ import {
44
44
  ObcAlertFrameThickness,
45
45
  ObcAlertFrameMode,
46
46
  } from '../../components/alert-frame/alert-frame.js';
47
- import {AlertType} from '../../types.js';
47
+ import {AlertType, FlashingSpeed} from '../../types.js';
48
48
 
49
49
  // The value weight maps straight to obc-textbox's font weights (regular /
50
50
  // semibold / bold). Re-exported so consumers can set `valueOptions.weight`
@@ -941,6 +941,7 @@ export class ObcReadoutListItem extends LitElement {
941
941
  .thickness=${thickness}
942
942
  .status=${alert.status ?? AlertType.Alarm}
943
943
  .mode=${alert.mode ?? ObcAlertFrameMode.ackedActive}
944
+ .flashingSpeed=${alert.flashingSpeed ?? FlashingSpeed.Default}
944
945
  .showIcon=${alert.showIcon ?? false}
945
946
  .showAlertCategoryIcon=${alert.showAlertCategoryIcon ?? true}
946
947
  .wrapContent=${false}
@@ -0,0 +1,83 @@
1
+ import {afterEach, describe, expect, it} from 'vitest';
2
+ import '../main.css';
3
+ import {
4
+ FLASH_OFF_MS,
5
+ FLASH_ON_MS,
6
+ FLASH_TEMPOS,
7
+ blinkingAll,
8
+ flashPeriodMs,
9
+ flashVariable,
10
+ installFlashing,
11
+ } from './blinking.js';
12
+ import {FlashingSpeed} from '../types.js';
13
+
14
+ describe('installFlashing', () => {
15
+ const hosts: HTMLElement[] = [];
16
+ function host(): HTMLElement {
17
+ const el = document.createElement('div');
18
+ document.body.append(el);
19
+ hosts.push(el);
20
+ return el;
21
+ }
22
+ afterEach(() => {
23
+ hosts.splice(0).forEach((el) => el.remove());
24
+ });
25
+
26
+ it.each(FLASH_TEMPOS)(
27
+ '%s runs on for its on-time, off for 400 ms, from the document origin',
28
+ (tempo) => {
29
+ const el = host();
30
+ const cancel = installFlashing(el, tempo);
31
+ const [anim] = el.getAnimations();
32
+ const effect = anim.effect as KeyframeEffect;
33
+ const timing = effect.getTiming();
34
+ expect(timing.duration).toBe(FLASH_ON_MS[tempo] + FLASH_OFF_MS);
35
+ expect(timing.iterations).toBe(Infinity);
36
+ const frames = effect.getKeyframes();
37
+ expect(frames).toHaveLength(3);
38
+ // Chromium reports the normalised form of `step-end`.
39
+ expect(frames[0].easing).toMatch(/^(step-end|steps\(1\))$/);
40
+ expect(frames[1].offset).toBeCloseTo(
41
+ FLASH_ON_MS[tempo] / flashPeriodMs(tempo)
42
+ );
43
+ expect(anim.startTime).toBe(0);
44
+ cancel();
45
+ expect(el.getAnimations()).toHaveLength(0);
46
+ }
47
+ );
48
+
49
+ it('nests the cycles so every tempo dips together', () => {
50
+ expect(flashPeriodMs(FlashingSpeed.Fast)).toBe(800);
51
+ expect(flashPeriodMs(FlashingSpeed.Slow)).toBe(1600);
52
+ expect(flashPeriodMs(FlashingSpeed.VerySlow)).toBe(3200);
53
+ });
54
+
55
+ it('drives the on/off variables in step', () => {
56
+ const el = host();
57
+ installFlashing(el, FlashingSpeed.Fast);
58
+ const [anim] = el.getAnimations();
59
+ const on = () =>
60
+ getComputedStyle(el).getPropertyValue(
61
+ flashVariable(FlashingSpeed.Fast, 'on')
62
+ );
63
+ const off = () =>
64
+ getComputedStyle(el).getPropertyValue(
65
+ flashVariable(FlashingSpeed.Fast, 'off')
66
+ );
67
+ anim.pause();
68
+ anim.currentTime = 100;
69
+ expect(on().trim()).toBe('1');
70
+ expect(off().trim()).toBe('0');
71
+ anim.currentTime = 500;
72
+ expect(on().trim()).toBe('0');
73
+ expect(off().trim()).toBe('1');
74
+ });
75
+
76
+ it('blinkingAll installs one animation per tempo and cancels them together', () => {
77
+ const el = host();
78
+ const cancel = blinkingAll(el);
79
+ expect(el.getAnimations()).toHaveLength(FLASH_TEMPOS.length);
80
+ cancel();
81
+ expect(el.getAnimations()).toHaveLength(0);
82
+ });
83
+ });
@@ -1,46 +1,58 @@
1
- const CRITICAL_PERIOD = 1000; // ms
2
- const ALARM_PERIOD = 2000; // ms
3
- const WARNING_PERIOD = 4000; // ms
4
- const LOW_PERIOD = 8000; // ms
1
+ import {FlashingSpeed, type FlashTempo} from '../types.js';
5
2
 
6
- function blinkingInstall(
7
- el: HTMLElement,
8
- period: number,
9
- label: string
10
- ): () => void {
11
- const anim = el.animate(
12
- [
13
- {[label + '-on']: 1, [label + '-off']: 0},
14
- {[label + '-on']: 0, [label + '-off']: 1},
15
- ],
16
- {duration: period, iterations: Infinity, easing: 'steps(2, jump-none)'}
17
- );
18
- anim.startTime = 0; // Common time origin for all animations, so they blink in sync.
19
- return () => anim.cancel();
20
- }
3
+ /** Shared off phase: nested cycles (800/1600/3200 ms) then dip together (#1224). */
4
+ export const FLASH_OFF_MS = 400;
21
5
 
22
- export function blinkingCritical(el: HTMLElement): () => void {
23
- return blinkingInstall(el, CRITICAL_PERIOD, '--critical-blink');
24
- }
6
+ export const FLASH_ON_MS: Record<FlashTempo, number> = {
7
+ [FlashingSpeed.Fast]: 400,
8
+ [FlashingSpeed.Slow]: 1200,
9
+ [FlashingSpeed.VerySlow]: 2800,
10
+ };
11
+
12
+ export const FLASH_TEMPOS: readonly FlashTempo[] = [
13
+ FlashingSpeed.Fast,
14
+ FlashingSpeed.Slow,
15
+ FlashingSpeed.VerySlow,
16
+ ];
25
17
 
26
- export function blinkingAlarm(el: HTMLElement): () => void {
27
- return blinkingInstall(el, ALARM_PERIOD, '--alarm-blink');
18
+ export function flashPeriodMs(tempo: FlashTempo): number {
19
+ return FLASH_ON_MS[tempo] + FLASH_OFF_MS;
28
20
  }
29
21
 
30
- export function blinkingWarning(el: HTMLElement): () => void {
31
- return blinkingInstall(el, WARNING_PERIOD, '--warning-blink');
22
+ /** Animated custom property name; `on` reads 1 during the on phase, `off` 1 during the off phase. */
23
+ export function flashVariable(tempo: FlashTempo, phase: 'on' | 'off'): string {
24
+ return `--flash-${tempo}-${phase}`;
32
25
  }
33
26
 
34
- export function blinkingLow(el: HTMLElement): () => void {
35
- return blinkingInstall(el, LOW_PERIOD, '--low-blink');
27
+ /**
28
+ * Animates `--flash-<tempo>-on` / `-off` on `el` for one tempo. Returns the
29
+ * cancel function.
30
+ */
31
+ export function installFlashing(
32
+ el: HTMLElement,
33
+ tempo: FlashTempo
34
+ ): () => void {
35
+ const on = flashVariable(tempo, 'on');
36
+ const off = flashVariable(tempo, 'off');
37
+ const period = flashPeriodMs(tempo);
38
+ const frames: Keyframe[] = [
39
+ {[on]: 1, [off]: 0, easing: 'step-end'},
40
+ {
41
+ [on]: 0,
42
+ [off]: 1,
43
+ offset: FLASH_ON_MS[tempo] / period,
44
+ easing: 'step-end',
45
+ },
46
+ {[on]: 0, [off]: 1},
47
+ ];
48
+ const anim = el.animate(frames, {duration: period, iterations: Infinity});
49
+ // Document-timeline origin, so every flashing element is in the same phase.
50
+ anim.startTime = 0;
51
+ return () => anim.cancel();
36
52
  }
37
53
 
54
+ /** Installs every tempo on `el`, for CSS that reads the variables directly. */
38
55
  export function blinkingAll(el: HTMLElement): () => void {
39
- const animes = [
40
- blinkingCritical(el),
41
- blinkingAlarm(el),
42
- blinkingWarning(el),
43
- blinkingLow(el),
44
- ];
45
- return () => animes.forEach((anim) => anim());
56
+ const cancels = FLASH_TEMPOS.map((tempo) => installFlashing(el, tempo));
57
+ return () => cancels.forEach((cancel) => cancel());
46
58
  }
@@ -0,0 +1,62 @@
1
+ import type {LitElement, ReactiveController} from 'lit';
2
+ import {
3
+ FlashingSpeed,
4
+ type FlashTempo,
5
+ type ResolvedFlashingSpeed,
6
+ } from '../types.js';
7
+ import {installFlashing} from './blinking.js';
8
+
9
+ /**
10
+ * Keeps one flash animation on the host in step with the tempo the host
11
+ * resolves after every update. `fixed` or a disconnect removes it; a
12
+ * reconnect re-syncs without waiting for a property change (#1224).
13
+ */
14
+ export class FlashingController implements ReactiveController {
15
+ private cancel?: () => void;
16
+ private installed?: FlashTempo;
17
+
18
+ constructor(
19
+ private readonly host: LitElement,
20
+ private readonly resolve: () => ResolvedFlashingSpeed
21
+ ) {
22
+ host.addController(this);
23
+ }
24
+
25
+ get tempo(): FlashTempo | undefined {
26
+ return this.installed;
27
+ }
28
+
29
+ hostConnected(): void {
30
+ if (this.host.hasUpdated) {
31
+ this.sync();
32
+ }
33
+ }
34
+
35
+ hostUpdated(): void {
36
+ this.sync();
37
+ }
38
+
39
+ hostDisconnected(): void {
40
+ this.stop();
41
+ }
42
+
43
+ private sync(): void {
44
+ const wanted = this.host.isConnected ? this.resolve() : FlashingSpeed.Fixed;
45
+ if (wanted === FlashingSpeed.Fixed) {
46
+ this.stop();
47
+ return;
48
+ }
49
+ if (wanted === this.installed) {
50
+ return;
51
+ }
52
+ this.stop();
53
+ this.cancel = installFlashing(this.host, wanted);
54
+ this.installed = wanted;
55
+ }
56
+
57
+ private stop(): void {
58
+ this.cancel?.();
59
+ this.cancel = undefined;
60
+ this.installed = undefined;
61
+ }
62
+ }
@@ -98,25 +98,39 @@
98
98
  --alert-notification-container-border-color: rgb(226, 240, 255);
99
99
  }
100
100
 
101
- @property --critical-blink-on {
101
+ /* Animated per element by palettes/blinking.ts; registered so the numbers
102
+ feed calc() (#1224). */
103
+ @property --flash-fast-on {
102
104
  syntax: "<number>";
103
105
  inherits: true;
104
106
  initial-value: 1;
105
107
  }
106
108
 
107
- @property --critical-blink-off {
109
+ @property --flash-fast-off {
108
110
  syntax: "<number>";
109
111
  inherits: true;
110
112
  initial-value: 0;
111
113
  }
112
114
 
113
- @property --low-blink-on {
115
+ @property --flash-slow-on {
114
116
  syntax: "<number>";
115
117
  inherits: true;
116
118
  initial-value: 1;
117
119
  }
118
120
 
119
- @property --low-blink-off {
121
+ @property --flash-slow-off {
122
+ syntax: "<number>";
123
+ inherits: true;
124
+ initial-value: 0;
125
+ }
126
+
127
+ @property --flash-very-slow-on {
128
+ syntax: "<number>";
129
+ inherits: true;
130
+ initial-value: 1;
131
+ }
132
+
133
+ @property --flash-very-slow-off {
120
134
  syntax: "<number>";
121
135
  inherits: true;
122
136
  initial-value: 0;
@@ -3,3 +3,4 @@ export * from './stroke-aware.js';
3
3
  export * from './setpoint.js';
4
4
  export * from './setpoint-mixin.js';
5
5
  export * from './setpoint-bundle.js';
6
+ export * from './rounded-rect.js';
@@ -0,0 +1,47 @@
1
+ import {describe, expect, it} from 'vitest';
2
+ import {roundedRectPath} from './rounded-rect.js';
3
+
4
+ describe('roundedRectPath', () => {
5
+ it('draws square corners for zero radii', () => {
6
+ expect(
7
+ roundedRectPath({x: 0, y: 0, width: 100, height: 50, radii: [0, 0, 0, 0]})
8
+ ).toBe('M0 0 H100 V50 H0 V0 Z');
9
+ });
10
+
11
+ it('draws a clockwise arc per rounded corner', () => {
12
+ expect(
13
+ roundedRectPath({
14
+ x: 1,
15
+ y: 1,
16
+ width: 100,
17
+ height: 50,
18
+ radii: [10, 10, 10, 10],
19
+ })
20
+ ).toBe(
21
+ 'M11 1 H91 A10 10 0 0 1 101 11 V41 A10 10 0 0 1 91 51 H11 A10 10 0 0 1 1 41 V11 A10 10 0 0 1 11 1 Z'
22
+ );
23
+ });
24
+
25
+ it('keeps individual corners square', () => {
26
+ const d = roundedRectPath({
27
+ x: 0,
28
+ y: 0,
29
+ width: 100,
30
+ height: 50,
31
+ radii: [0, 8, 0, 8],
32
+ });
33
+ expect(d.startsWith('M0 0 H92 A8 8 0 0 1 100 8 V50 H8')).toBe(true);
34
+ });
35
+
36
+ it('clamps radii to half the shorter side', () => {
37
+ const d = roundedRectPath({
38
+ x: 0,
39
+ y: 0,
40
+ width: 100,
41
+ height: 20,
42
+ radii: [50, 50, 50, 50],
43
+ });
44
+ expect(d).toContain('A10 10 0 0 1');
45
+ expect(d).not.toContain('A50');
46
+ });
47
+ });
@@ -0,0 +1,42 @@
1
+ export interface RoundedRect {
2
+ x: number;
3
+ y: number;
4
+ width: number;
5
+ height: number;
6
+ /** Corner radii in CSS order: top-left, top-right, bottom-right, bottom-left. */
7
+ radii: [number, number, number, number];
8
+ }
9
+
10
+ /**
11
+ * SVG path of a rectangle with per-corner radii, so a stroke can follow a
12
+ * box whose corners are individually sharp (the alert frame's
13
+ * `sharpEdge*` flags). Radii are clamped to half the shorter side.
14
+ */
15
+ export function roundedRectPath({
16
+ x,
17
+ y,
18
+ width,
19
+ height,
20
+ radii,
21
+ }: RoundedRect): string {
22
+ const max = Math.min(width, height) / 2;
23
+ const [tl, tr, br, bl] = radii.map((r) => Math.max(0, Math.min(r, max)));
24
+ const right = x + width;
25
+ const bottom = y + height;
26
+ const arc = (r: number, endX: number, endY: number) =>
27
+ r > 0 ? `A${r} ${r} 0 0 1 ${endX} ${endY}` : '';
28
+ return [
29
+ `M${x + tl} ${y}`,
30
+ `H${right - tr}`,
31
+ arc(tr, right, y + tr),
32
+ `V${bottom - br}`,
33
+ arc(br, right - br, bottom),
34
+ `H${x + bl}`,
35
+ arc(bl, x, bottom - bl),
36
+ `V${y + tl}`,
37
+ arc(tl, x + tl, y),
38
+ 'Z',
39
+ ]
40
+ .filter(Boolean)
41
+ .join(' ');
42
+ }
package/src/types.ts CHANGED
@@ -16,6 +16,26 @@ export enum AlertType {
16
16
  LevelDiagnostic = 'level-diagnostic',
17
17
  }
18
18
 
19
+ /**
20
+ * Flash tempo of an alert element. `default` resolves per alert type and
21
+ * phase (see `resolveFlashingSpeed` in `alert-severity.ts`); `fixed` never
22
+ * flashes.
23
+ */
24
+ export enum FlashingSpeed {
25
+ Default = 'default',
26
+ Fast = 'fast',
27
+ Slow = 'slow',
28
+ VerySlow = 'very-slow',
29
+ Fixed = 'fixed',
30
+ }
31
+
32
+ export type ResolvedFlashingSpeed = Exclude<
33
+ FlashingSpeed,
34
+ FlashingSpeed.Default
35
+ >;
36
+
37
+ export type FlashTempo = Exclude<ResolvedFlashingSpeed, FlashingSpeed.Fixed>;
38
+
19
39
  export const ALERT_SEVERITY_PRIORITY = [
20
40
  AlertType.LevelCritical,
21
41
  AlertType.Alarm,