@oicl/openbridge-webcomponents-full-bundle 2.0.0-next.121 → 2.0.0-next.123
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/openbridge-webcomponents.bundle.js +252 -53
- package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
- package/custom-elements.json +66 -17
- package/dist/automation/automation-tank/automation-tank.css.js +94 -14
- package/dist/automation/automation-tank/automation-tank.css.js.map +1 -1
- package/dist/automation/automation-tank/automation-tank.d.ts +53 -5
- package/dist/automation/automation-tank/automation-tank.d.ts.map +1 -1
- package/dist/automation/automation-tank/automation-tank.js +39 -20
- package/dist/automation/automation-tank/automation-tank.js.map +1 -1
- package/dist/building-blocks/readout-block/readout-block.d.ts +27 -1
- package/dist/building-blocks/readout-block/readout-block.d.ts.map +1 -1
- package/dist/building-blocks/readout-block/readout-block.js +31 -10
- package/dist/building-blocks/readout-block/readout-block.js.map +1 -1
- package/dist/navigation-instruments/compass-sector/compass-sector.css.js +10 -0
- package/dist/navigation-instruments/compass-sector/compass-sector.css.js.map +1 -1
- package/dist/navigation-instruments/readout/readout-formatters.d.ts +61 -0
- package/dist/navigation-instruments/readout/readout-formatters.d.ts.map +1 -1
- package/dist/navigation-instruments/readout/readout-formatters.js +35 -2
- package/dist/navigation-instruments/readout/readout-formatters.js.map +1 -1
- package/dist/navigation-instruments/readout/readout.d.ts +19 -2
- package/dist/navigation-instruments/readout/readout.d.ts.map +1 -1
- package/dist/navigation-instruments/readout/readout.js +18 -4
- package/dist/navigation-instruments/readout/readout.js.map +1 -1
- package/dist/navigation-instruments/readout-list/readout-list.d.ts.map +1 -1
- package/dist/navigation-instruments/readout-list/readout-list.js +6 -3
- package/dist/navigation-instruments/readout-list/readout-list.js.map +1 -1
- package/dist/navigation-instruments/readout-list-item/readout-list-item.d.ts +19 -2
- package/dist/navigation-instruments/readout-list-item/readout-list-item.d.ts.map +1 -1
- package/dist/navigation-instruments/readout-list-item/readout-list-item.js +18 -4
- package/dist/navigation-instruments/readout-list-item/readout-list-item.js.map +1 -1
- package/dist/navigation-instruments/thruster/thruster.d.ts.map +1 -1
- package/dist/navigation-instruments/thruster/thruster.js +1 -0
- package/dist/navigation-instruments/thruster/thruster.js.map +1 -1
- package/dist/navigation-instruments/watch/watch.css.js +4 -0
- package/dist/navigation-instruments/watch/watch.css.js.map +1 -1
- package/dist/navigation-instruments/watch-flat/watch-flat.css.js +4 -0
- package/dist/navigation-instruments/watch-flat/watch-flat.css.js.map +1 -1
- package/package.json +1 -1
- package/src/automation/automation-tank/automation-tank.css +70 -3
- package/src/automation/automation-tank/automation-tank.stories.ts +140 -1
- package/src/automation/automation-tank/automation-tank.ts +106 -27
- package/src/building-blocks/readout-block/readout-block.stories.ts +20 -0
- package/src/building-blocks/readout-block/readout-block.ts +56 -10
- package/src/navigation-instruments/compass-sector/compass-sector.css +10 -0
- package/src/navigation-instruments/readout/readout-formatters.spec.ts +205 -0
- package/src/navigation-instruments/readout/readout-formatters.ts +128 -2
- package/src/navigation-instruments/readout/readout.ts +38 -5
- package/src/navigation-instruments/readout-list/readout-list.ts +10 -2
- package/src/navigation-instruments/readout-list-item/readout-list-item.ts +38 -5
- package/src/navigation-instruments/thruster/thruster.ts +1 -0
- package/src/navigation-instruments/watch/watch.css +4 -0
- package/src/navigation-instruments/watch-flat/watch-flat.css +4 -0
|
@@ -77,6 +77,39 @@
|
|
|
77
77
|
min-width: 0;
|
|
78
78
|
min-height: 0;
|
|
79
79
|
}
|
|
80
|
+
|
|
81
|
+
/* Indefinite-axis fallback.
|
|
82
|
+
*
|
|
83
|
+
* `width`/`height: 100%` only resolve when the parent's corresponding axis is
|
|
84
|
+
* definite. In a flex/grid dashboard slot one axis is very often left to
|
|
85
|
+
* content — `align-self: center` makes the cross axis shrink-to-fit, and a
|
|
86
|
+
* parent sized with `min-height`/`max-height` (rather than `height`) leaves the
|
|
87
|
+
* block axis indefinite. In those cases the percentage computes to `auto` and
|
|
88
|
+
* the tank would size to its own content, which includes the chart cell — and
|
|
89
|
+
* the chart sizes itself from the cell it was measured in. That is a circular
|
|
90
|
+
* constraint, and it resolves as a collapsed cell, unbounded growth or
|
|
91
|
+
* pixel-level ringing depending on the loop gain (issue #1121).
|
|
92
|
+
*
|
|
93
|
+
* `aspect-ratio` supplies the missing axis from the design footprint the
|
|
94
|
+
* `point` variant uses above, so an indefinite axis lands on a definite value
|
|
95
|
+
* that no longer depends on the tank's own content. It is inert when the parent
|
|
96
|
+
* makes both axes definite, so the normal "fill my slot" behavior is unchanged.
|
|
97
|
+
*/
|
|
98
|
+
:host([positioning="button"]) {
|
|
99
|
+
aspect-ratio: 256 / 376;
|
|
100
|
+
}
|
|
101
|
+
:host([positioning="button"][orientation="horizontal"]) {
|
|
102
|
+
aspect-ratio: 420 / 256;
|
|
103
|
+
}
|
|
104
|
+
:host([positioning="button"][compact]),
|
|
105
|
+
:host([positioning="button"][static]) {
|
|
106
|
+
aspect-ratio: 170 / 282;
|
|
107
|
+
}
|
|
108
|
+
:host([positioning="button"][orientation="horizontal"][compact]),
|
|
109
|
+
:host([positioning="button"][orientation="horizontal"][static]) {
|
|
110
|
+
aspect-ratio: 244 / 208;
|
|
111
|
+
}
|
|
112
|
+
|
|
80
113
|
:host([positioning="button"]) .root {
|
|
81
114
|
position: static;
|
|
82
115
|
top: auto;
|
|
@@ -134,10 +167,30 @@
|
|
|
134
167
|
display: flex;
|
|
135
168
|
flex-direction: column;
|
|
136
169
|
gap: 4px;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* Interactive (`clickable`, the default): the full six-state flat mixin —
|
|
173
|
+
* enabled, activated, hover, pressed, focus-visible, disabled — painted on
|
|
174
|
+
* `.halo`. */
|
|
175
|
+
.root.clickable {
|
|
137
176
|
cursor: pointer;
|
|
138
177
|
/* prettier-ignore */
|
|
139
178
|
@mixin style style=flat visibleWrapperClass=.halo;
|
|
140
179
|
}
|
|
180
|
+
|
|
181
|
+
/* Non-interactive (`clickable` is `false`, and `static`, which is display-only
|
|
182
|
+
* by definition): the mixin's `noClick` variant emits the enabled state ONLY, so
|
|
183
|
+
* the halo keeps exactly the same resting colors and the same 1px border box
|
|
184
|
+
* — no layout shift, nothing else in the tank changes — while hover, pressed
|
|
185
|
+
* and focus-visible disappear. The root is a `<div>` in this case, so there is
|
|
186
|
+
* no focus ring or tab stop to suppress either. Same split as
|
|
187
|
+
* `obc-elevated-card`'s `.not-clickable` / `:not(.not-clickable)` pair. */
|
|
188
|
+
.root:not(.clickable) {
|
|
189
|
+
cursor: default;
|
|
190
|
+
/* prettier-ignore */
|
|
191
|
+
@mixin style style=flat noClick visibleWrapperClass=.halo;
|
|
192
|
+
}
|
|
193
|
+
|
|
141
194
|
.root:focus {
|
|
142
195
|
outline: none;
|
|
143
196
|
}
|
|
@@ -156,10 +209,12 @@
|
|
|
156
209
|
top: auto;
|
|
157
210
|
}
|
|
158
211
|
|
|
159
|
-
/* Static is display-only
|
|
160
|
-
*
|
|
212
|
+
/* Static is display-only, so it never gets the `.clickable` class and the
|
|
213
|
+
* hover/pressed visuals are already gone via the `noClick` rule above. Static
|
|
214
|
+
* additionally refuses pointer events outright — unlike a non-clickable tank,
|
|
215
|
+
* which still lets clicks reach the host so a consumer can opt into handling
|
|
216
|
+
* them. */
|
|
161
217
|
:host([static]) .root {
|
|
162
|
-
cursor: default;
|
|
163
218
|
pointer-events: none;
|
|
164
219
|
}
|
|
165
220
|
|
|
@@ -510,9 +565,21 @@
|
|
|
510
565
|
overflow: hidden;
|
|
511
566
|
}
|
|
512
567
|
|
|
568
|
+
/* The renderers are taken out of flow so the cell sizes them, never the other
|
|
569
|
+
* way round. Both of them derive a pixel size from the box they are measured
|
|
570
|
+
* in — `obc-gauge-trend` publishes an inline `--chart-height` computed from its
|
|
571
|
+
* own measured width, and `obc-bar-vertical` writes `height: 100%; width: auto`
|
|
572
|
+
* onto its own host — so while they are in flow the cell's content height is a
|
|
573
|
+
* function of the cell's own height. Absolute positioning severs that return
|
|
574
|
+
* path: `.bar-container` is `position: relative`, so `inset: 0` pins the
|
|
575
|
+
* renderer to the cell the tank's grid/flex layout already decided on, and a
|
|
576
|
+
* rounding difference between the two can no longer relayout the tank
|
|
577
|
+
* (issue #1121). */
|
|
513
578
|
.bar-container > obc-gauge-trend,
|
|
514
579
|
.bar-container > obc-bar-vertical {
|
|
515
580
|
display: block;
|
|
581
|
+
position: absolute;
|
|
582
|
+
inset: 0;
|
|
516
583
|
width: 100%;
|
|
517
584
|
height: 100%;
|
|
518
585
|
}
|
|
@@ -99,6 +99,7 @@ const renderTankEl = (args: StoryArgs, richReadout: unknown = nothing) => html`
|
|
|
99
99
|
.compact=${args.compact}
|
|
100
100
|
.static=${args.static}
|
|
101
101
|
?activated=${args.activated}
|
|
102
|
+
.clickable=${args.clickable}
|
|
102
103
|
.positioning=${args.positioning}
|
|
103
104
|
.chartMode=${args.chartMode}
|
|
104
105
|
.chartData=${args.chartData}
|
|
@@ -142,6 +143,7 @@ const meta: Meta<StoryArgs> = {
|
|
|
142
143
|
compact: false,
|
|
143
144
|
static: false,
|
|
144
145
|
activated: false,
|
|
146
|
+
clickable: true,
|
|
145
147
|
positioning: TankPositioning.point,
|
|
146
148
|
chartMode: TankChartMode.bar,
|
|
147
149
|
chartData: SAMPLE_DATA,
|
|
@@ -200,7 +202,12 @@ const meta: Meta<StoryArgs> = {
|
|
|
200
202
|
activated: {
|
|
201
203
|
control: {type: 'boolean'},
|
|
202
204
|
description:
|
|
203
|
-
'Enables the activated background color, used to indicate that the tank is activated/selected.',
|
|
205
|
+
'Enables the activated background color, used to indicate that the tank is activated/selected. Requires `clickable` — a non-clickable tank only paints the resting enabled state.',
|
|
206
|
+
},
|
|
207
|
+
clickable: {
|
|
208
|
+
control: {type: 'boolean'},
|
|
209
|
+
description:
|
|
210
|
+
'Whether the tank is interactive. Default `true`. Set to `false` for a display-only tank that still shows live data: the resting appearance, chart, badges, readout, tag and alert frame are unchanged, but hover / pressed / focus states are removed and the tank leaves the tab order. Unlike `static`, the chart keeps rendering.',
|
|
204
211
|
},
|
|
205
212
|
hasAdvice: {
|
|
206
213
|
control: {type: 'boolean'},
|
|
@@ -330,6 +337,32 @@ export const CompactActivated: Story = {
|
|
|
330
337
|
},
|
|
331
338
|
};
|
|
332
339
|
|
|
340
|
+
/**
|
|
341
|
+
* Display-only tank — `.clickable=${false}`. `clickable` is property-only
|
|
342
|
+
* (`attribute: false`), so it has to be set as a property; a
|
|
343
|
+
* `clickable="false"` attribute in plain HTML is not observed. The root
|
|
344
|
+
* renders as a `<div>`
|
|
345
|
+
* instead of a `<button>`, so there is no hover, pressed or focus-visible
|
|
346
|
+
* state and the tank is out of the tab order. Everything else is untouched:
|
|
347
|
+
* the resting surface keeps the same colors and the same 1px border box (the
|
|
348
|
+
* flat mixin's `noClick` variant paints the enabled state only), and the
|
|
349
|
+
* chart, badges, readout and tag all render exactly as on a clickable tank.
|
|
350
|
+
*
|
|
351
|
+
* Use this for a tank that aggregates the ones beside it — a row total that
|
|
352
|
+
* still shows live data but has nothing to open. Contrast with `static`,
|
|
353
|
+
* which represents "device present, current state unknown": that one also
|
|
354
|
+
* hides the chart, swaps the percent for capacity, and shrinks to the compact
|
|
355
|
+
* footprint.
|
|
356
|
+
*/
|
|
357
|
+
export const NotClickable: Story = {
|
|
358
|
+
args: {
|
|
359
|
+
type: TankType.atmospheric,
|
|
360
|
+
chartMode: TankChartMode.graphAndBar,
|
|
361
|
+
tag: 'TOTAL',
|
|
362
|
+
clickable: false,
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
|
|
333
366
|
export const CompactAtmospheric: Story = {
|
|
334
367
|
args: {compact: true, type: TankType.atmospheric},
|
|
335
368
|
};
|
|
@@ -523,6 +556,7 @@ export const WithFractionDigits: Story = {
|
|
|
523
556
|
.orientation=${args.orientation}
|
|
524
557
|
.compact=${args.compact}
|
|
525
558
|
.static=${args.static}
|
|
559
|
+
.clickable=${args.clickable}
|
|
526
560
|
.positioning=${args.positioning}
|
|
527
561
|
.chartMode=${args.chartMode}
|
|
528
562
|
.chartData=${args.chartData}
|
|
@@ -562,6 +596,7 @@ export const WithAlertAlarm: Story = {
|
|
|
562
596
|
.orientation=${args.orientation}
|
|
563
597
|
.compact=${args.compact}
|
|
564
598
|
.static=${args.static}
|
|
599
|
+
.clickable=${args.clickable}
|
|
565
600
|
.positioning=${args.positioning}
|
|
566
601
|
.chartMode=${args.chartMode}
|
|
567
602
|
.chartData=${args.chartData}
|
|
@@ -583,6 +618,24 @@ export const WithAlertAlarm: Story = {
|
|
|
583
618
|
`,
|
|
584
619
|
};
|
|
585
620
|
|
|
621
|
+
/**
|
|
622
|
+
* A non-clickable tank still raises its alert frame — `clickable` governs the
|
|
623
|
+
* interaction surface only, not what the tank is allowed to display. The chart
|
|
624
|
+
* keeps rendering too, which is the difference from `static`.
|
|
625
|
+
*/
|
|
626
|
+
export const NotClickableWithAlert: Story = {
|
|
627
|
+
...WithAlertAlarm,
|
|
628
|
+
args: {
|
|
629
|
+
type: TankType.atmospheric,
|
|
630
|
+
chartMode: TankChartMode.graphAndBar,
|
|
631
|
+
tag: 'TOTAL',
|
|
632
|
+
clickable: false,
|
|
633
|
+
alert: true,
|
|
634
|
+
alertFrameStatus: ObcAlertFrameStatus.Warning,
|
|
635
|
+
alertFrameType: ObcAlertFrameType.SmallSideFlip,
|
|
636
|
+
},
|
|
637
|
+
};
|
|
638
|
+
|
|
586
639
|
/**
|
|
587
640
|
* Warning-status alert on a horizontal atmospheric tank — demonstrates the
|
|
588
641
|
* ring tracks the tank's host orientation correctly.
|
|
@@ -650,6 +703,91 @@ export const WithAlertLevelDiagnostic: Story = {
|
|
|
650
703
|
* story, which gives the host fixed default dimensions and a P&ID
|
|
651
704
|
* top-center anchor for placement on a pipe-grid coordinate.
|
|
652
705
|
*/
|
|
706
|
+
/**
|
|
707
|
+
* A dashboard row of tanks in a flex container — the layout family that
|
|
708
|
+
* `positioning="button"` exists for, and the one that regressed in issue #1121.
|
|
709
|
+
*
|
|
710
|
+
* Each cell is a flex item sized with `min-height` / `max-height` rather than
|
|
711
|
+
* `height`, and the tank inside it is centered on the cross axis. Both of those
|
|
712
|
+
* leave an axis indefinite, so the tank cannot take its size from the parent
|
|
713
|
+
* alone; it falls back to the design aspect ratio instead of sizing to its own
|
|
714
|
+
* content. Sizing to content would be circular — the chart derives its pixel
|
|
715
|
+
* size from the cell it was measured in — and used to surface as tanks that
|
|
716
|
+
* collapsed, grew without bound, or oscillated by a few pixels forever.
|
|
717
|
+
*
|
|
718
|
+
* Pin both axes on the parent whenever the exact footprint matters; the
|
|
719
|
+
* fallback keeps the layout stable, it does not guess the size you wanted.
|
|
720
|
+
*/
|
|
721
|
+
export const DashboardRow: Story = {
|
|
722
|
+
args: {
|
|
723
|
+
type: TankType.generic,
|
|
724
|
+
chartMode: TankChartMode.graphAndBar,
|
|
725
|
+
positioning: TankPositioning.button,
|
|
726
|
+
showTrendSymbol: false,
|
|
727
|
+
percentFractionDigits: 1,
|
|
728
|
+
},
|
|
729
|
+
decorators: [],
|
|
730
|
+
render(args) {
|
|
731
|
+
const tanks = [
|
|
732
|
+
{tag: 'FO TOT', value: 61.4, max: 76.21},
|
|
733
|
+
{tag: 'FO SERV SB', value: 0.82, max: 1.06},
|
|
734
|
+
{tag: 'FO SERV PS', value: 0.41, max: 1.06},
|
|
735
|
+
{tag: 'OVERFLOW', value: 2.3, max: 17.4},
|
|
736
|
+
{tag: 'AFT SB FO', value: 14.9, max: 17.4},
|
|
737
|
+
{tag: 'UREA', value: 3.9, max: 5.5},
|
|
738
|
+
];
|
|
739
|
+
// Each tank has its own capacity, so the series has to be built on that
|
|
740
|
+
// tank's scale — a shared fixture would render off-axis.
|
|
741
|
+
const series = (value: number, max: number, seed: number) =>
|
|
742
|
+
Array.from({length: 16}, (_, i) => ({
|
|
743
|
+
label: String(i).padStart(2, '0'),
|
|
744
|
+
value: Math.min(
|
|
745
|
+
max,
|
|
746
|
+
Math.max(0, value * (0.55 + 0.03 * ((i + seed) % 15)))
|
|
747
|
+
),
|
|
748
|
+
}));
|
|
749
|
+
// `position: static` opts out of the shared `crossDecorator`'s
|
|
750
|
+
// `position: absolute; top: 50%; left: 50%`, which would push a full-width
|
|
751
|
+
// row into the bottom-right quadrant.
|
|
752
|
+
return html`
|
|
753
|
+
<div
|
|
754
|
+
style="position: static; display: flex; align-items: stretch; gap: 8px;"
|
|
755
|
+
>
|
|
756
|
+
${tanks.map(
|
|
757
|
+
(tank, index) => html`
|
|
758
|
+
<div
|
|
759
|
+
style="
|
|
760
|
+
display: flex;
|
|
761
|
+
flex-direction: column;
|
|
762
|
+
align-items: center;
|
|
763
|
+
flex: 1 1 auto;
|
|
764
|
+
min-width: 0;
|
|
765
|
+
min-height: 340px;
|
|
766
|
+
max-height: 340px;
|
|
767
|
+
border: 1px solid var(--border-divider-color);
|
|
768
|
+
"
|
|
769
|
+
>
|
|
770
|
+
<obc-automation-tank
|
|
771
|
+
style="align-self: center; flex: 1 1 auto; min-width: 0;"
|
|
772
|
+
.value=${tank.value}
|
|
773
|
+
.max=${tank.max}
|
|
774
|
+
.tag=${tank.tag}
|
|
775
|
+
.type=${args.type}
|
|
776
|
+
.positioning=${args.positioning}
|
|
777
|
+
.chartMode=${args.chartMode}
|
|
778
|
+
.chartData=${series(tank.value, tank.max, index)}
|
|
779
|
+
.showTrendSymbol=${args.showTrendSymbol}
|
|
780
|
+
.percentFractionDigits=${args.percentFractionDigits}
|
|
781
|
+
.priority=${args.priority}
|
|
782
|
+
></obc-automation-tank>
|
|
783
|
+
</div>
|
|
784
|
+
`
|
|
785
|
+
)}
|
|
786
|
+
</div>
|
|
787
|
+
`;
|
|
788
|
+
},
|
|
789
|
+
};
|
|
790
|
+
|
|
653
791
|
export const Responsive: Story = {
|
|
654
792
|
args: {
|
|
655
793
|
type: TankType.atmospheric,
|
|
@@ -684,6 +822,7 @@ export const Responsive: Story = {
|
|
|
684
822
|
.orientation=${args.orientation}
|
|
685
823
|
.compact=${args.compact}
|
|
686
824
|
.static=${args.static}
|
|
825
|
+
.clickable=${args.clickable}
|
|
687
826
|
.positioning=${args.positioning}
|
|
688
827
|
.chartMode=${args.chartMode}
|
|
689
828
|
.chartData=${args.chartData}
|
|
@@ -82,6 +82,22 @@ export enum TankOrientation {
|
|
|
82
82
|
* slot — the parent controls the footprint and the tank renders
|
|
83
83
|
* responsively inside it, just like a regular button. Compact / static
|
|
84
84
|
* inner layout still applies; only the host box is changed.
|
|
85
|
+
*
|
|
86
|
+
* If the parent leaves *one* axis indefinite — a flex/grid slot sized with
|
|
87
|
+
* `min-height`/`max-height` rather than `height`, or a cross axis freed by
|
|
88
|
+
* `align-self: center` — the corresponding `100%` computes to `auto` and the
|
|
89
|
+
* tank derives that axis from the other one through the design aspect ratio
|
|
90
|
+
* of the matching `point` footprint (256×376 vertical, 420×256 horizontal,
|
|
91
|
+
* 170×282 compact/static, 244×208 horizontal compact/static). The size then
|
|
92
|
+
* does not depend on the chart cell, which takes its own size from the cell
|
|
93
|
+
* it was measured in and would otherwise make the constraint circular
|
|
94
|
+
* (issue #1121).
|
|
95
|
+
*
|
|
96
|
+
* If *both* axes are indefinite (a shrink-to-fit parent, e.g. an unsized
|
|
97
|
+
* `inline-block`) there is no axis left to derive from, so the host falls
|
|
98
|
+
* back to its text content. That is stable and non-circular, but much
|
|
99
|
+
* smaller than the design footprint — give the parent a definite size on at
|
|
100
|
+
* least one axis, and on both whenever the exact footprint matters.
|
|
85
101
|
*/
|
|
86
102
|
export enum TankPositioning {
|
|
87
103
|
point = 'point',
|
|
@@ -122,7 +138,7 @@ export enum TankChartMode {
|
|
|
122
138
|
* @slot alert-icon - Custom icon for the alert frame.
|
|
123
139
|
* @slot alert-label - Label for the alert frame.
|
|
124
140
|
* @slot alert-timer - Timer for the alert frame.
|
|
125
|
-
* @fires click - Fired when the tank is clicked.
|
|
141
|
+
* @fires click - Fired when the tank is clicked. When `clickable` is `false` the tank renders a plain `<div>`, and in `static` mode a `<div role="img">`, instead of a `<button>` — in both cases it is not focusable or keyboard-activatable; pointer clicks still reach the host.
|
|
126
142
|
* @beta
|
|
127
143
|
*/
|
|
128
144
|
@customElement('obc-automation-tank')
|
|
@@ -137,9 +153,10 @@ export class ObcAutomationTank extends SetpointMixin(LitElement) {
|
|
|
137
153
|
@property({type: Boolean, reflect: true}) compact: boolean = false;
|
|
138
154
|
/**
|
|
139
155
|
* Host positioning model — see `TankPositioning` for details. Defaults to
|
|
140
|
-
* `button` (host fills parent container, 100% × 100%,
|
|
141
|
-
*
|
|
142
|
-
*
|
|
156
|
+
* `button` (host fills parent container, 100% × 100%, falling back to the
|
|
157
|
+
* design aspect ratio on any axis the parent leaves indefinite, no anchor
|
|
158
|
+
* offset). Set to `point` for the legacy P&ID canvas mode (fixed default
|
|
159
|
+
* dimensions + top-center anchor offset).
|
|
143
160
|
*/
|
|
144
161
|
@property({type: String, reflect: true}) positioning: TankPositioning =
|
|
145
162
|
TankPositioning.button;
|
|
@@ -156,7 +173,38 @@ export class ObcAutomationTank extends SetpointMixin(LitElement) {
|
|
|
156
173
|
* activatable controls.
|
|
157
174
|
*/
|
|
158
175
|
@property({type: Boolean, reflect: true}) static: boolean = false;
|
|
159
|
-
/**
|
|
176
|
+
/**
|
|
177
|
+
* Whether the tank is interactive. `true` (default) renders the root as a
|
|
178
|
+
* `<button>` with the full flat-mixin interaction surface. `false` renders a
|
|
179
|
+
* non-interactive `<div>` — the resting appearance is unchanged (same
|
|
180
|
+
* enabled-state colors and the same 1px border box, via the mixin's
|
|
181
|
+
* `noClick` variant), but the hover / pressed / focus-visible states are
|
|
182
|
+
* gone and the tank leaves the tab order.
|
|
183
|
+
*
|
|
184
|
+
* Everything else keeps rendering: the chart / bar, badges, readout, tag and
|
|
185
|
+
* the `alert` frame all behave exactly as they do on a clickable tank. Use
|
|
186
|
+
* this for a display-only tank that still shows live data — e.g. a row total
|
|
187
|
+
* aggregating the tanks beside it. For "device present, current state
|
|
188
|
+
* unknown" use `static` instead, which also hides the chart and shrinks to
|
|
189
|
+
* the compact footprint.
|
|
190
|
+
*
|
|
191
|
+
* `static` is already non-interactive, so this has no effect there.
|
|
192
|
+
*
|
|
193
|
+
* Property-only (`attribute: false`, per the repo's positive-default-true
|
|
194
|
+
* boolean convention — a `true` default cannot round-trip through an HTML
|
|
195
|
+
* boolean attribute). Set it as a property: `el.clickable = false`,
|
|
196
|
+
* `.clickable=${false}` in a Lit template, or the equivalent binding in the
|
|
197
|
+
* React / Vue / Angular / Svelte wrappers. A `clickable="false"` attribute in
|
|
198
|
+
* plain HTML is **not** observed and leaves the tank interactive.
|
|
199
|
+
*/
|
|
200
|
+
@property({type: Boolean, attribute: false}) clickable: boolean = true;
|
|
201
|
+
/**
|
|
202
|
+
* Enables the activated background color, used to indicate that the tank is
|
|
203
|
+
* activated/selected. Requires an interactive tank — the `noClick` mixin
|
|
204
|
+
* variant used when `clickable` is `false` only paints the enabled state, so
|
|
205
|
+
* a non-clickable tank ignores this (matching `obc-elevated-card`).
|
|
206
|
+
* @availableWhen clickable==true
|
|
207
|
+
*/
|
|
160
208
|
@property({type: Boolean}) activated: boolean = false;
|
|
161
209
|
@property({type: String}) tag: string = '';
|
|
162
210
|
|
|
@@ -1001,7 +1049,18 @@ export class ObcAutomationTank extends SetpointMixin(LitElement) {
|
|
|
1001
1049
|
// The `activated` class goes on the interactive `.root` so the shared
|
|
1002
1050
|
// `flat` style mixin paints the activated background/border on `.halo`
|
|
1003
1051
|
// (its `visibleWrapperClass`), same as the mixin's hover/pressed states.
|
|
1004
|
-
|
|
1052
|
+
//
|
|
1053
|
+
// `.clickable` selects between the two flat-mixin variants in CSS: the
|
|
1054
|
+
// full six-state one, or the `noClick` one that paints only the resting
|
|
1055
|
+
// enabled state. `static` is already display-only, so it never counts as
|
|
1056
|
+
// clickable. Same shape as `obc-elevated-card`'s `.not-clickable` split
|
|
1057
|
+
// and `obc-readout-list-item`'s `.root.clickable`.
|
|
1058
|
+
const isClickable = this.clickable && !this.static;
|
|
1059
|
+
const rootClasses = classMap({
|
|
1060
|
+
root: true,
|
|
1061
|
+
activated: this.activated,
|
|
1062
|
+
clickable: isClickable,
|
|
1063
|
+
});
|
|
1005
1064
|
|
|
1006
1065
|
// `aria-live="polite"` + `aria-atomic="true"` on the root so the
|
|
1007
1066
|
// slotted alert label (and any state change of the alert frame) is
|
|
@@ -1010,27 +1069,47 @@ export class ObcAutomationTank extends SetpointMixin(LitElement) {
|
|
|
1010
1069
|
// TODO(a11y): the rest of the automation component family still lacks
|
|
1011
1070
|
// this live-region announcement; consolidate when alert support is
|
|
1012
1071
|
// factored into a shared mixin.
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1072
|
+
// Three root shapes:
|
|
1073
|
+
// - static: <div role="img"> — an opaque graphic standing in
|
|
1074
|
+
// for a device whose state is unknown, named by its tag.
|
|
1075
|
+
// - clickable false: a plain <div>. Deliberately no `role="img"` and no
|
|
1076
|
+
// `aria-label` here: unlike a static tank this one still shows live
|
|
1077
|
+
// data, and both would collapse the readout into a single opaque name
|
|
1078
|
+
// and hide the percent / value / tag from screen readers. The visible
|
|
1079
|
+
// content is the accessible content. (Same reasoning as the non-
|
|
1080
|
+
// clickable branch of `obc-readout-list-item`.)
|
|
1081
|
+
// - default: <button>.
|
|
1082
|
+
// The live region stays on all three so an `alert` label is announced
|
|
1083
|
+
// regardless of interactivity.
|
|
1084
|
+
if (this.static) {
|
|
1085
|
+
return html`<div
|
|
1086
|
+
class=${rootClasses}
|
|
1087
|
+
role="img"
|
|
1088
|
+
aria-label=${this.tag || 'Tank'}
|
|
1089
|
+
aria-live="polite"
|
|
1090
|
+
aria-atomic="true"
|
|
1091
|
+
>
|
|
1092
|
+
${halo}
|
|
1093
|
+
</div>`;
|
|
1094
|
+
}
|
|
1095
|
+
if (!isClickable) {
|
|
1096
|
+
return html`<div
|
|
1097
|
+
class=${rootClasses}
|
|
1098
|
+
aria-live="polite"
|
|
1099
|
+
aria-atomic="true"
|
|
1100
|
+
>
|
|
1101
|
+
${halo}
|
|
1102
|
+
</div>`;
|
|
1103
|
+
}
|
|
1104
|
+
return html`<button
|
|
1105
|
+
class=${rootClasses}
|
|
1106
|
+
type="button"
|
|
1107
|
+
aria-label=${this.tag || 'Tank'}
|
|
1108
|
+
aria-live="polite"
|
|
1109
|
+
aria-atomic="true"
|
|
1110
|
+
>
|
|
1111
|
+
${halo}
|
|
1112
|
+
</button>`;
|
|
1034
1113
|
}
|
|
1035
1114
|
|
|
1036
1115
|
static override styles = unsafeCSS(compentStyle);
|
|
@@ -481,6 +481,19 @@ const UNAVAILABLE_CASES: {
|
|
|
481
481
|
label: 'null · no fraction digits',
|
|
482
482
|
args: {value: null, maxDigits: 3},
|
|
483
483
|
},
|
|
484
|
+
// The knobs, not the value: a `fractionDigits` written by a failed runtime
|
|
485
|
+
// (`NaN`) must not silently format with zero decimals — a critical 0.4
|
|
486
|
+
// printed as a plausible-looking "0" reads as healthy. The reading dashes
|
|
487
|
+
// instead. A missing `maxDigits` dashes too, for consistency; with the
|
|
488
|
+
// precision still known, the placeholder keeps its fraction shape.
|
|
489
|
+
{
|
|
490
|
+
label: 'value 0.4 · fractionDigits NaN — precision failed to arrive',
|
|
491
|
+
args: {value: 0.4, maxDigits: 3, fractionDigits: Number.NaN},
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
label: 'value 0.4 · maxDigits NaN — same, for consistency',
|
|
495
|
+
args: {value: 0.4, maxDigits: Number.NaN, fractionDigits: 1},
|
|
496
|
+
},
|
|
484
497
|
];
|
|
485
498
|
|
|
486
499
|
// Stacked in one column under identical settings so the decimal points and the
|
|
@@ -509,6 +522,13 @@ const ALIGNMENT_CASES: Partial<BlockArgs>[] = [
|
|
|
509
522
|
* runtime data condition (sensor dropout, `0/0`, a bad parse) rather than a
|
|
510
523
|
* programmer error, so they resolve to the dash rather than throwing.
|
|
511
524
|
*
|
|
525
|
+
* 4. **A digit knob that never arrived dashes the reading too.** `NaN`,
|
|
526
|
+
* `null` or `undefined` in `fractionDigits` / `maxDigits` is a runtime
|
|
527
|
+
* failure of the writing system, and formatting with a default the author
|
|
528
|
+
* never chose would let a critical `0.4` pass for a healthy `0`. Finite
|
|
529
|
+
* out-of-range values are different: `fractionDigits` throws (programmer
|
|
530
|
+
* error), `maxDigits` clamps (width-only).
|
|
531
|
+
*
|
|
512
532
|
* Hinted zeros are suppressed for an unavailable value, so the two "not
|
|
513
533
|
* available" rows are identical and nothing reads `----Na.N`.
|
|
514
534
|
*
|
|
@@ -15,6 +15,9 @@ import {
|
|
|
15
15
|
formatNumericValue,
|
|
16
16
|
readoutFormattedInteger,
|
|
17
17
|
assertReadoutValueType,
|
|
18
|
+
assertReadoutFractionDigits,
|
|
19
|
+
isReadoutDigitCountMissing,
|
|
20
|
+
resolveReadoutDigitCount,
|
|
18
21
|
resolveReadoutNumericValue,
|
|
19
22
|
resolveReadoutTextValue,
|
|
20
23
|
READOUT_UNAVAILABLE_DASH,
|
|
@@ -152,13 +155,23 @@ export class ObcReadoutBlock extends LitElement {
|
|
|
152
155
|
@property({type: Boolean}) hasIcon = false;
|
|
153
156
|
|
|
154
157
|
/**
|
|
155
|
-
* Number of fraction digits.
|
|
158
|
+
* Number of fraction digits. Must be between 0 and 100 — the range
|
|
159
|
+
* `Number.prototype.toFixed` accepts; outside it throws a `RangeError`.
|
|
160
|
+
* A fractional count truncates (`2.7` → `2`). A count that never arrived
|
|
161
|
+
* (`NaN`, `null` or unset) renders the reading as the unavailable dash —
|
|
162
|
+
* formatting with a precision the author never chose would let a critical
|
|
163
|
+
* `0.4` pass for a healthy `0`.
|
|
156
164
|
* @availableWhen valueType==number
|
|
157
165
|
*/
|
|
158
166
|
@property({type: Number}) fractionDigits = 0;
|
|
159
167
|
|
|
160
168
|
/**
|
|
161
169
|
* Integer digits to reserve / hint (independent of `fractionDigits`).
|
|
170
|
+
* Bounded before use: a fractional count
|
|
171
|
+
* truncates (`2.7` → `2`), anything above 100 — including `Infinity` —
|
|
172
|
+
* caps at 100, and a negative count reserves nothing. A count that never
|
|
173
|
+
* arrived (`NaN`, `null` or unset) renders the reading as the unavailable
|
|
174
|
+
* dash, consistent with `fractionDigits`.
|
|
162
175
|
* @availableWhen valueType==number
|
|
163
176
|
*/
|
|
164
177
|
@property({type: Number}) maxDigits = 0;
|
|
@@ -214,24 +227,54 @@ export class ObcReadoutBlock extends LitElement {
|
|
|
214
227
|
}
|
|
215
228
|
}
|
|
216
229
|
|
|
230
|
+
/**
|
|
231
|
+
* `maxDigits` bounded to a usable count. Every width calculation below goes
|
|
232
|
+
* through this rather than the raw property, so an `Infinity` (or absurdly
|
|
233
|
+
* large) value cannot reach `String.prototype.repeat`.
|
|
234
|
+
*/
|
|
235
|
+
private get resolvedMaxDigits(): number {
|
|
236
|
+
return resolveReadoutDigitCount(this.maxDigits);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Whether a digit knob failed to arrive (`NaN` / `null` / `undefined`).
|
|
241
|
+
* The reading then renders as the unavailable dash rather than silently
|
|
242
|
+
* formatting with a default the author never chose — a critical `0.4`
|
|
243
|
+
* formatted with a failed `fractionDigits` would otherwise print as a
|
|
244
|
+
* plausible-looking `0`. A missing `maxDigits` dashes too, for consistency:
|
|
245
|
+
* either knob arriving broken means the row's configuration cannot be
|
|
246
|
+
* trusted.
|
|
247
|
+
*/
|
|
248
|
+
private get digitCountsMissing(): boolean {
|
|
249
|
+
return (
|
|
250
|
+
isReadoutDigitCountMissing(this.fractionDigits) ||
|
|
251
|
+
isReadoutDigitCountMissing(this.maxDigits)
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
217
255
|
private get numericFormatOptions(): ReadoutNumericFormatOptions {
|
|
218
256
|
return {
|
|
219
257
|
// The unavailable placeholder stays short (`\u2012.\u2012\u2012`) rather than
|
|
220
258
|
// spelling out every reserved digit position — `maxDigits` already
|
|
221
259
|
// reserves the width, so it simply sits at the right edge of it.
|
|
222
260
|
showZeroPadding: false,
|
|
223
|
-
minValueLength: this.
|
|
224
|
-
|
|
261
|
+
minValueLength: this.resolvedMaxDigits,
|
|
262
|
+
// A missing precision shapes the placeholder as zero fraction digits (a
|
|
263
|
+
// single dash) — and keeps a runtime `undefined`/`null` out of the
|
|
264
|
+
// options object, which is typed `number` throughout the formatters.
|
|
265
|
+
fractionDigits: isReadoutDigitCountMissing(this.fractionDigits)
|
|
266
|
+
? 0
|
|
267
|
+
: this.fractionDigits,
|
|
225
268
|
};
|
|
226
269
|
}
|
|
227
270
|
|
|
228
271
|
/** Widest possible value string for width reservation (e.g. `"000.0"`). */
|
|
229
272
|
private get reserverText(): string {
|
|
230
|
-
const maxDigits = this.
|
|
273
|
+
const maxDigits = this.resolvedMaxDigits;
|
|
231
274
|
if (maxDigits <= 0) {
|
|
232
275
|
return '';
|
|
233
276
|
}
|
|
234
|
-
const integer = '0'.repeat(
|
|
277
|
+
const integer = '0'.repeat(maxDigits);
|
|
235
278
|
return this.fractionDigits > 0
|
|
236
279
|
? `${integer}.${'0'.repeat(this.fractionDigits)}`
|
|
237
280
|
: integer;
|
|
@@ -322,16 +365,19 @@ export class ObcReadoutBlock extends LitElement {
|
|
|
322
365
|
// `changed`, skip the check, and render the invalid value as a plain dash:
|
|
323
366
|
// exactly the silent failure this assertion exists to prevent.
|
|
324
367
|
assertReadoutValueType('obc-readout-block', this.value, this.valueType);
|
|
368
|
+
assertReadoutFractionDigits('obc-readout-block', this.fractionDigits);
|
|
325
369
|
}
|
|
326
370
|
|
|
327
371
|
override render() {
|
|
328
372
|
const valueSize = this.resolvedValueSize;
|
|
329
373
|
const formatOptions = this.numericFormatOptions;
|
|
330
374
|
const isTextMode = this.valueType === ReadoutValueType.text;
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
375
|
+
// A missing digit knob makes the reading untrustworthy, so it resolves to
|
|
376
|
+
// "no reading" and renders the dash (see `digitCountsMissing`). Text mode
|
|
377
|
+
// is unaffected — it ignores the numeric knobs entirely.
|
|
378
|
+
const valueForFormat = this.digitCountsMissing
|
|
379
|
+
? undefined
|
|
380
|
+
: resolveReadoutNumericValue(this.value, this.valueType);
|
|
335
381
|
const textValue = resolveReadoutTextValue(this.value, this.valueType);
|
|
336
382
|
// Text mode renders verbatim and ignores the numeric format options; a
|
|
337
383
|
// blank / missing text value still falls back to the unavailable dash.
|
|
@@ -351,7 +397,7 @@ export class ObcReadoutBlock extends LitElement {
|
|
|
351
397
|
valueForFormat === undefined ||
|
|
352
398
|
valueForFormat < 0
|
|
353
399
|
? 0
|
|
354
|
-
: Math.max(this.
|
|
400
|
+
: Math.max(this.resolvedMaxDigits - readoutFormattedInteger(text), 0);
|
|
355
401
|
const hinted = hintCount > 0 ? '0'.repeat(hintCount) : '';
|
|
356
402
|
// Hinted zeros own the width — they already fill to `maxDigits` — so when
|
|
357
403
|
// `hintedZeros` is enabled an explicit `spaceReserver` is ignored (it has
|
|
@@ -27,4 +27,14 @@
|
|
|
27
27
|
display: block;
|
|
28
28
|
width: 100%;
|
|
29
29
|
height: 100%;
|
|
30
|
+
/* Clips the rotated overlay <svg transform="rotate(...)">, whose corners
|
|
31
|
+
swing outside the host and leak arc pixels in wide layouts (same
|
|
32
|
+
protection obc-watch has had since #994). Known trade-off: the HDG/COG
|
|
33
|
+
arrow apexes previously painted 1-2px past the host's top edge and are
|
|
34
|
+
now shaved flat at the box edge — imperceptible at normal viewing.
|
|
35
|
+
TODO(designer): if the shave is unwanted, the frame needs headroom so
|
|
36
|
+
the arrow tips fit inside the box — see "Host clipping & the arrow-apex
|
|
37
|
+
shave" in docs/agents/watch-radial-instruments.md before changing
|
|
38
|
+
(interacts with the #1021/#1049 label-reserve geometry). */
|
|
39
|
+
overflow: hidden;
|
|
30
40
|
}
|