@oicl/openbridge-webcomponents-full-bundle 2.0.0-next.88 → 2.0.0-next.89
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 +17344 -16794
- package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
- package/custom-elements.json +720 -43
- package/dist/building-blocks/readout-block/readout-block.css.js +229 -0
- package/dist/building-blocks/readout-block/readout-block.css.js.map +1 -0
- package/dist/building-blocks/readout-block/readout-block.d.ts +146 -0
- package/dist/building-blocks/readout-block/readout-block.d.ts.map +1 -0
- package/dist/building-blocks/readout-block/readout-block.js +273 -0
- package/dist/building-blocks/readout-block/readout-block.js.map +1 -0
- package/dist/navigation-instruments/readout-list/readout-list.css.js +24 -0
- package/dist/navigation-instruments/readout-list/readout-list.css.js.map +1 -0
- package/dist/navigation-instruments/readout-list/readout-list.d.ts +70 -0
- package/dist/navigation-instruments/readout-list/readout-list.d.ts.map +1 -0
- package/dist/navigation-instruments/readout-list/readout-list.js +154 -0
- package/dist/navigation-instruments/readout-list/readout-list.js.map +1 -0
- package/dist/navigation-instruments/readout-list-item/readout-list-item.css.js +80 -145
- package/dist/navigation-instruments/readout-list-item/readout-list-item.css.js.map +1 -1
- package/dist/navigation-instruments/readout-list-item/readout-list-item.d.ts +25 -26
- 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 +66 -118
- package/dist/navigation-instruments/readout-list-item/readout-list-item.js.map +1 -1
- package/package.json +1 -1
- package/src/building-blocks/readout-block/readout-block.css +192 -0
- package/src/building-blocks/readout-block/readout-block.stories.ts +346 -0
- package/src/building-blocks/readout-block/readout-block.ts +355 -0
- package/src/navigation-instruments/readout-list/readout-list.css +13 -0
- package/src/navigation-instruments/readout-list/readout-list.stories.ts +275 -0
- package/src/navigation-instruments/readout-list/readout-list.ts +216 -0
- package/src/navigation-instruments/readout-list-item/readout-list-item.css +75 -128
- package/src/navigation-instruments/readout-list-item/readout-list-item.stories.ts +18 -17
- package/src/navigation-instruments/readout-list-item/readout-list-item.ts +84 -140
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import type {Meta, StoryObj} from '@storybook/web-components-vite';
|
|
2
|
+
import {html, nothing} from 'lit';
|
|
3
|
+
import {
|
|
4
|
+
ReadoutBlockVariant,
|
|
5
|
+
ReadoutBlockSize,
|
|
6
|
+
ReadoutBlockDataQuality,
|
|
7
|
+
ObcTextboxFontWeight,
|
|
8
|
+
ObcTextboxAlignment,
|
|
9
|
+
} from './readout-block.js';
|
|
10
|
+
import './readout-block.js';
|
|
11
|
+
import '../../icons/icon-placeholder.js';
|
|
12
|
+
import {
|
|
13
|
+
ObcAlertFrameMode,
|
|
14
|
+
ObcAlertFrameType,
|
|
15
|
+
} from '../../components/alert-frame/alert-frame.js';
|
|
16
|
+
import {AlertType} from '../../types.js';
|
|
17
|
+
|
|
18
|
+
const NONE = 'none';
|
|
19
|
+
|
|
20
|
+
type BlockArgs = {
|
|
21
|
+
variant: ReadoutBlockVariant;
|
|
22
|
+
value: number;
|
|
23
|
+
size: ReadoutBlockSize;
|
|
24
|
+
enhanced: boolean;
|
|
25
|
+
weight: ObcTextboxFontWeight;
|
|
26
|
+
hasDegree: boolean;
|
|
27
|
+
hasIcon: boolean;
|
|
28
|
+
fractionDigits: number;
|
|
29
|
+
maxDigits: number;
|
|
30
|
+
hintedZeros: boolean;
|
|
31
|
+
spaceReserver: string;
|
|
32
|
+
off: boolean;
|
|
33
|
+
offText: string;
|
|
34
|
+
alignment: ObcTextboxAlignment;
|
|
35
|
+
dataQuality: ReadoutBlockDataQuality | typeof NONE;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// A faithful single-block render. The block inherits its colour from the host
|
|
39
|
+
// context (the list-item normally drives it), so standalone it shows the neutral
|
|
40
|
+
// default tone; `enhanced` switches to the accent tone.
|
|
41
|
+
function renderBlock(args: Partial<BlockArgs>) {
|
|
42
|
+
return html`
|
|
43
|
+
<obc-readout-block
|
|
44
|
+
.variant=${args.variant ?? ReadoutBlockVariant.value}
|
|
45
|
+
.value=${args.value ?? null}
|
|
46
|
+
.size=${args.size ?? ReadoutBlockSize.small}
|
|
47
|
+
.enhanced=${args.enhanced ?? false}
|
|
48
|
+
.weight=${args.weight ?? ObcTextboxFontWeight.regular}
|
|
49
|
+
.hasDegree=${args.hasDegree ?? false}
|
|
50
|
+
.hasIcon=${args.hasIcon ?? false}
|
|
51
|
+
.fractionDigits=${args.fractionDigits ?? 0}
|
|
52
|
+
.maxDigits=${args.maxDigits ?? 0}
|
|
53
|
+
.hintedZeros=${args.hintedZeros ?? false}
|
|
54
|
+
.spaceReserver=${args.spaceReserver || undefined}
|
|
55
|
+
.off=${args.off ?? false}
|
|
56
|
+
.offText=${args.offText ?? 'OFF'}
|
|
57
|
+
.alignment=${args.alignment ?? ObcTextboxAlignment.Right}
|
|
58
|
+
.dataQuality=${args.dataQuality === NONE ? undefined : args.dataQuality}
|
|
59
|
+
>
|
|
60
|
+
${args.hasIcon
|
|
61
|
+
? html`<obi-placeholder slot="icon"></obi-placeholder>`
|
|
62
|
+
: nothing}
|
|
63
|
+
</obc-readout-block>
|
|
64
|
+
`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const themedDecorator = (story: () => unknown) => html`
|
|
68
|
+
<div
|
|
69
|
+
data-obc-theme="day"
|
|
70
|
+
style="background: var(--container-background-color); padding: 24px; display: inline-block;"
|
|
71
|
+
>
|
|
72
|
+
${story()}
|
|
73
|
+
</div>
|
|
74
|
+
`;
|
|
75
|
+
|
|
76
|
+
const showcaseStyle = `
|
|
77
|
+
.rb-grid { display: flex; flex-wrap: wrap; gap: 24px; align-items: flex-end; }
|
|
78
|
+
.rb-card {
|
|
79
|
+
display: flex; flex-direction: column; gap: 8px; align-items: flex-start;
|
|
80
|
+
padding: 12px; border-radius: 8px; background: rgba(0, 0, 0, 0.03);
|
|
81
|
+
}
|
|
82
|
+
.rb-card-title {
|
|
83
|
+
font: 10px/1.2 var(--global-typography-ui-label-font-family, sans-serif);
|
|
84
|
+
text-transform: uppercase; letter-spacing: 0.06em; color: var(--element-neutral-color, #777);
|
|
85
|
+
}
|
|
86
|
+
.rb-cell { outline: 1px dashed rgba(0, 0, 0, 0.12); }
|
|
87
|
+
`;
|
|
88
|
+
|
|
89
|
+
type ShowcaseCard = {title: string; args: Partial<BlockArgs>};
|
|
90
|
+
|
|
91
|
+
function renderShowcase(cards: ShowcaseCard[]) {
|
|
92
|
+
return html`
|
|
93
|
+
<style>
|
|
94
|
+
${showcaseStyle}
|
|
95
|
+
</style>
|
|
96
|
+
<div class="rb-grid">
|
|
97
|
+
${cards.map(
|
|
98
|
+
(card) => html`
|
|
99
|
+
<div class="rb-card">
|
|
100
|
+
<div class="rb-card-title">${card.title}</div>
|
|
101
|
+
<div class="rb-cell">${renderBlock(card.args)}</div>
|
|
102
|
+
</div>
|
|
103
|
+
`
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const meta = {
|
|
110
|
+
title: 'Building Blocks/Readout Block',
|
|
111
|
+
tags: ['autodocs', '6.0', 'wip'],
|
|
112
|
+
component: 'obc-readout-block',
|
|
113
|
+
decorators: [themedDecorator],
|
|
114
|
+
parameters: {
|
|
115
|
+
docs: {
|
|
116
|
+
description: {
|
|
117
|
+
component:
|
|
118
|
+
'The most atomic readout primitive — a single cap-height, ' +
|
|
119
|
+
'width-reservable numeric segment (value / setpoint / advice). It is the ' +
|
|
120
|
+
'building block used inside `obc-readout-list-item` (and, in a future ' +
|
|
121
|
+
'refactor, inside `obc-readout`); it is not normally used on its own. ' +
|
|
122
|
+
'Colour is inherited from the host context, so standalone it shows the ' +
|
|
123
|
+
'neutral tone.',
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
render: (args) => renderBlock(args),
|
|
128
|
+
args: {
|
|
129
|
+
variant: ReadoutBlockVariant.value,
|
|
130
|
+
value: 123,
|
|
131
|
+
size: ReadoutBlockSize.small,
|
|
132
|
+
enhanced: false,
|
|
133
|
+
weight: ObcTextboxFontWeight.regular,
|
|
134
|
+
hasDegree: false,
|
|
135
|
+
hasIcon: false,
|
|
136
|
+
fractionDigits: 0,
|
|
137
|
+
maxDigits: 0,
|
|
138
|
+
hintedZeros: false,
|
|
139
|
+
spaceReserver: '',
|
|
140
|
+
off: false,
|
|
141
|
+
offText: 'OFF',
|
|
142
|
+
alignment: ObcTextboxAlignment.Right,
|
|
143
|
+
dataQuality: NONE,
|
|
144
|
+
},
|
|
145
|
+
argTypes: {
|
|
146
|
+
variant: {
|
|
147
|
+
control: {type: 'select'},
|
|
148
|
+
options: Object.values(ReadoutBlockVariant),
|
|
149
|
+
},
|
|
150
|
+
size: {
|
|
151
|
+
control: {type: 'select'},
|
|
152
|
+
options: Object.values(ReadoutBlockSize),
|
|
153
|
+
},
|
|
154
|
+
weight: {
|
|
155
|
+
control: {type: 'select'},
|
|
156
|
+
options: Object.values(ObcTextboxFontWeight),
|
|
157
|
+
},
|
|
158
|
+
alignment: {
|
|
159
|
+
control: {type: 'select'},
|
|
160
|
+
options: Object.values(ObcTextboxAlignment),
|
|
161
|
+
},
|
|
162
|
+
fractionDigits: {control: {type: 'number', min: 0, step: 1}},
|
|
163
|
+
maxDigits: {control: {type: 'number', min: 0, step: 1}},
|
|
164
|
+
spaceReserver: {control: {type: 'text'}},
|
|
165
|
+
offText: {control: {type: 'text'}},
|
|
166
|
+
dataQuality: {
|
|
167
|
+
control: {type: 'select'},
|
|
168
|
+
options: [NONE, ...Object.values(ReadoutBlockDataQuality)],
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
} satisfies Meta<BlockArgs>;
|
|
172
|
+
|
|
173
|
+
export default meta;
|
|
174
|
+
type Story = StoryObj<BlockArgs>;
|
|
175
|
+
|
|
176
|
+
export const Playground: Story = {};
|
|
177
|
+
|
|
178
|
+
export const Variants: Story = {
|
|
179
|
+
render: () =>
|
|
180
|
+
renderShowcase([
|
|
181
|
+
{title: 'value', args: {variant: ReadoutBlockVariant.value, value: 123}},
|
|
182
|
+
{
|
|
183
|
+
title: 'setpoint',
|
|
184
|
+
args: {variant: ReadoutBlockVariant.setpoint, value: 120},
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
title: 'advice',
|
|
188
|
+
args: {variant: ReadoutBlockVariant.advice, value: 118},
|
|
189
|
+
},
|
|
190
|
+
]),
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export const Sizes: Story = {
|
|
194
|
+
render: () =>
|
|
195
|
+
renderShowcase(
|
|
196
|
+
[
|
|
197
|
+
ReadoutBlockSize.small,
|
|
198
|
+
ReadoutBlockSize.medium,
|
|
199
|
+
ReadoutBlockSize.large,
|
|
200
|
+
].map((size) => ({
|
|
201
|
+
title: size,
|
|
202
|
+
args: {size, value: 123, hasDegree: true},
|
|
203
|
+
}))
|
|
204
|
+
),
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export const Tone: Story = {
|
|
208
|
+
render: () =>
|
|
209
|
+
renderShowcase([
|
|
210
|
+
{title: 'regular', args: {value: 123, enhanced: false}},
|
|
211
|
+
{title: 'enhanced', args: {value: 123, enhanced: true}},
|
|
212
|
+
]),
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
export const Weight: Story = {
|
|
216
|
+
render: () =>
|
|
217
|
+
renderShowcase(
|
|
218
|
+
[
|
|
219
|
+
ObcTextboxFontWeight.regular,
|
|
220
|
+
ObcTextboxFontWeight.semibold,
|
|
221
|
+
ObcTextboxFontWeight.bold,
|
|
222
|
+
].map((weight) => ({title: weight, args: {value: 123, weight}}))
|
|
223
|
+
),
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
export const Degree: Story = {
|
|
227
|
+
render: () =>
|
|
228
|
+
renderShowcase([
|
|
229
|
+
{title: 'no degree', args: {value: 287}},
|
|
230
|
+
{title: 'degree', args: {value: 287, hasDegree: true}},
|
|
231
|
+
]),
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* `off` renders `offText` (default `"OFF"`) in place of the value.
|
|
236
|
+
*/
|
|
237
|
+
export const OffText: Story = {
|
|
238
|
+
render: () =>
|
|
239
|
+
renderShowcase([
|
|
240
|
+
{title: 'OFF (default)', args: {off: true}},
|
|
241
|
+
{title: 'custom', args: {off: true, offText: 'unavailable'}},
|
|
242
|
+
]),
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Hinted zeros pad the integer part up to `maxDigits` as muted leading zeros.
|
|
247
|
+
* When enabled they take priority over `spaceReserver` (they already fill to
|
|
248
|
+
* `maxDigits`, so an explicit reserver is ignored).
|
|
249
|
+
*/
|
|
250
|
+
export const HintedZeros: Story = {
|
|
251
|
+
render: () =>
|
|
252
|
+
renderShowcase([
|
|
253
|
+
{title: 'value 8, maxDigits 4', args: {value: 8, maxDigits: 4}},
|
|
254
|
+
{
|
|
255
|
+
title: 'hinted zeros',
|
|
256
|
+
args: {value: 8, maxDigits: 4, hintedZeros: true},
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
title: 'hinted zeros + fraction',
|
|
260
|
+
args: {value: 8, maxDigits: 4, fractionDigits: 1, hintedZeros: true},
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
title: 'hinted wins over reserver',
|
|
264
|
+
args: {
|
|
265
|
+
value: 8,
|
|
266
|
+
maxDigits: 4,
|
|
267
|
+
hintedZeros: true,
|
|
268
|
+
spaceReserver: '00000000',
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
]),
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* `maxDigits` reserves INTEGER digits only — independent of `fractionDigits`
|
|
276
|
+
* (the decimal point and fraction digits never count toward `maxDigits`).
|
|
277
|
+
*/
|
|
278
|
+
export const MaxDigitsAndFractionDigits: Story = {
|
|
279
|
+
render: () =>
|
|
280
|
+
renderShowcase([
|
|
281
|
+
{title: 'maxDigits 4', args: {value: 12, maxDigits: 4}},
|
|
282
|
+
{
|
|
283
|
+
title: 'maxDigits 4, frac 1',
|
|
284
|
+
args: {value: 12, maxDigits: 4, fractionDigits: 1},
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
title: 'maxDigits 4, frac 2',
|
|
288
|
+
args: {value: 12.5, maxDigits: 4, fractionDigits: 2},
|
|
289
|
+
},
|
|
290
|
+
]),
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
export const Alignment: Story = {
|
|
294
|
+
render: () =>
|
|
295
|
+
renderShowcase(
|
|
296
|
+
[
|
|
297
|
+
ObcTextboxAlignment.Left,
|
|
298
|
+
ObcTextboxAlignment.Center,
|
|
299
|
+
ObcTextboxAlignment.Right,
|
|
300
|
+
].map((alignment) => ({
|
|
301
|
+
title: alignment,
|
|
302
|
+
// A wide reserver makes the alignment within the reserved width visible.
|
|
303
|
+
args: {value: 12, alignment, spaceReserver: '00000'},
|
|
304
|
+
}))
|
|
305
|
+
),
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
export const DataQuality: Story = {
|
|
309
|
+
render: () =>
|
|
310
|
+
renderShowcase([
|
|
311
|
+
{title: 'nominal', args: {value: 123}},
|
|
312
|
+
{
|
|
313
|
+
title: 'low-integrity',
|
|
314
|
+
args: {value: 123, dataQuality: ReadoutBlockDataQuality.lowIntegrity},
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
title: 'invalid',
|
|
318
|
+
args: {value: 123, dataQuality: ReadoutBlockDataQuality.invalid},
|
|
319
|
+
},
|
|
320
|
+
{title: 'null (dash)', args: {value: undefined}},
|
|
321
|
+
]),
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
export const Alert: Story = {
|
|
325
|
+
render: () => html`
|
|
326
|
+
<style>
|
|
327
|
+
${showcaseStyle}
|
|
328
|
+
</style>
|
|
329
|
+
<div class="rb-grid">
|
|
330
|
+
<div class="rb-card">
|
|
331
|
+
<div class="rb-card-title">value alert (warning)</div>
|
|
332
|
+
<div class="rb-cell">
|
|
333
|
+
<obc-readout-block
|
|
334
|
+
.variant=${ReadoutBlockVariant.value}
|
|
335
|
+
.value=${123}
|
|
336
|
+
.alert=${{
|
|
337
|
+
status: AlertType.Warning,
|
|
338
|
+
mode: ObcAlertFrameMode.unackedActive,
|
|
339
|
+
type: ObcAlertFrameType.Regular,
|
|
340
|
+
}}
|
|
341
|
+
></obc-readout-block>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
`,
|
|
346
|
+
};
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import {LitElement, html, nothing, unsafeCSS, type TemplateResult} from 'lit';
|
|
2
|
+
import {property, state} from 'lit/decorators.js';
|
|
3
|
+
import {classMap} from 'lit/directives/class-map.js';
|
|
4
|
+
import componentStyle from './readout-block.css?inline';
|
|
5
|
+
import {customElement} from '../../decorator.js';
|
|
6
|
+
import '../../components/textbox/textbox.js';
|
|
7
|
+
import {
|
|
8
|
+
ObcTextboxSize,
|
|
9
|
+
ObcTextboxFontWeight,
|
|
10
|
+
ObcTextboxAlignment,
|
|
11
|
+
} from '../../components/textbox/textbox.js';
|
|
12
|
+
import '../../icons/icon-input-right.js';
|
|
13
|
+
import '../../icons/icon-notification-advice.js';
|
|
14
|
+
import {
|
|
15
|
+
formatNumericValue,
|
|
16
|
+
readoutFormattedInteger,
|
|
17
|
+
type ReadoutNumericFormatOptions,
|
|
18
|
+
} from '../../navigation-instruments/readout/readout-formatters.js';
|
|
19
|
+
import {
|
|
20
|
+
type AlertFrameConfig,
|
|
21
|
+
wrapWithAlertFrame,
|
|
22
|
+
} from '../../components/alert-frame/alert-frame.js';
|
|
23
|
+
|
|
24
|
+
// Re-exported so consumers can configure typography without a second import path.
|
|
25
|
+
export {
|
|
26
|
+
ObcTextboxSize,
|
|
27
|
+
ObcTextboxFontWeight,
|
|
28
|
+
ObcTextboxAlignment,
|
|
29
|
+
} from '../../components/textbox/textbox.js';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Semantic variant of the block. Drives the default marker icon and the
|
|
33
|
+
* colour token; the layout is identical across variants.
|
|
34
|
+
* - `value`: the primary reading (no default marker icon).
|
|
35
|
+
* - `setpoint`: a setpoint reference (default `input-right` marker).
|
|
36
|
+
* - `advice`: an advisory reference (default `notification-advice` marker).
|
|
37
|
+
*/
|
|
38
|
+
export enum ReadoutBlockVariant {
|
|
39
|
+
value = 'value',
|
|
40
|
+
setpoint = 'setpoint',
|
|
41
|
+
advice = 'advice',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Density tier of the block — drives icon size, the icon↔number gap, and the
|
|
46
|
+
* degree-column width tier. The number typography size is controlled
|
|
47
|
+
* independently via {@link ObcReadoutBlock.valueSize} so a parent can de-emphasise
|
|
48
|
+
* one block relative to another within the same tier.
|
|
49
|
+
*/
|
|
50
|
+
export enum ReadoutBlockSize {
|
|
51
|
+
small = 'small',
|
|
52
|
+
medium = 'medium',
|
|
53
|
+
large = 'large',
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Per-block measurement quality, rendered as a non-shifting outline chip.
|
|
58
|
+
* - `low-integrity`: the value is suspect.
|
|
59
|
+
* - `invalid`: the value is invalid.
|
|
60
|
+
*/
|
|
61
|
+
export enum ReadoutBlockDataQuality {
|
|
62
|
+
lowIntegrity = 'low-integrity',
|
|
63
|
+
invalid = 'invalid',
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Pop-up fade phase for a setpoint block. Driven by the parent's setpoint
|
|
68
|
+
* interaction state machine; only meaningful for `role="setpoint"`.
|
|
69
|
+
*/
|
|
70
|
+
export enum ReadoutBlockHidePhase {
|
|
71
|
+
none = 'none',
|
|
72
|
+
hiding = 'hiding',
|
|
73
|
+
hidden = 'hidden',
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* `<obc-readout-block>` – the most atomic readout primitive: a single
|
|
78
|
+
* cap-height-aligned, width-reservable numeric segment (value / setpoint /
|
|
79
|
+
* advice).
|
|
80
|
+
*
|
|
81
|
+
* It renders one `obc-textbox` number with optional hinted leading zeros, a
|
|
82
|
+
* reservable width, an optional leading marker icon (via the `icon` slot or the
|
|
83
|
+
* role default), an optional trailing degree glyph, an `off`/unavailable text
|
|
84
|
+
* state, per-block data-quality and an optional per-block alert frame.
|
|
85
|
+
*
|
|
86
|
+
* This is a building block used inside `obc-readout-list-item` (and, in a future
|
|
87
|
+
* refactor, inside `obc-readout`); it is not normally used on its own. Colour is
|
|
88
|
+
* inherited from the host context (the parent sets the role colour), so the
|
|
89
|
+
* block stays neutral until placed.
|
|
90
|
+
*
|
|
91
|
+
* @experimental Pilot for the new primitives + per-block options Readout API; the
|
|
92
|
+
* API may change in a future release.
|
|
93
|
+
*
|
|
94
|
+
* @slot icon - Replaces the role's default marker icon.
|
|
95
|
+
*
|
|
96
|
+
* @csspart block - The block container (carries role / tone / data-quality).
|
|
97
|
+
* @csspart block-content - The number + degree group.
|
|
98
|
+
* @csspart block-text - The `obc-textbox` rendering the number.
|
|
99
|
+
* @csspart block-icon - The leading marker-icon container.
|
|
100
|
+
* @csspart degree - The trailing degree-glyph column.
|
|
101
|
+
*/
|
|
102
|
+
@customElement('obc-readout-block')
|
|
103
|
+
export class ObcReadoutBlock extends LitElement {
|
|
104
|
+
/** Semantic variant (value / setpoint / advice). */
|
|
105
|
+
@property({type: String}) variant: ReadoutBlockVariant =
|
|
106
|
+
ReadoutBlockVariant.value;
|
|
107
|
+
|
|
108
|
+
/** The numeric value; `null`/`undefined` renders a dash. */
|
|
109
|
+
@property({type: Number}) value: number | null = null;
|
|
110
|
+
|
|
111
|
+
/** Density tier — icon size, gap, degree tier. */
|
|
112
|
+
@property({type: String}) size: ReadoutBlockSize = ReadoutBlockSize.small;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Resolved number-typography size. When unset it is derived from `size`
|
|
116
|
+
* (small→s, medium→m, large→l), so a parent that de-emphasises a block (e.g.
|
|
117
|
+
* a secondary setpoint) can pass a smaller size without changing the tier.
|
|
118
|
+
*/
|
|
119
|
+
@property({type: String}) valueSize?: ObcTextboxSize;
|
|
120
|
+
|
|
121
|
+
/** Accent (in-command) colour tone. */
|
|
122
|
+
@property({type: Boolean}) enhanced = false;
|
|
123
|
+
|
|
124
|
+
/** Number font weight (regular / semibold / bold); colour is independent. */
|
|
125
|
+
@property({type: String}) weight: ObcTextboxFontWeight =
|
|
126
|
+
ObcTextboxFontWeight.regular;
|
|
127
|
+
|
|
128
|
+
/** Render the trailing cap-height degree glyph (`°`). */
|
|
129
|
+
@property({type: Boolean}) hasDegree = false;
|
|
130
|
+
|
|
131
|
+
/** Show the leading marker-icon container (always on for setpoint/advice). */
|
|
132
|
+
@property({type: Boolean}) hasIcon = false;
|
|
133
|
+
|
|
134
|
+
/** Number of fraction digits. */
|
|
135
|
+
@property({type: Number}) fractionDigits = 0;
|
|
136
|
+
|
|
137
|
+
/** Integer digits to reserve / hint (independent of `fractionDigits`). */
|
|
138
|
+
@property({type: Number}) maxDigits = 0;
|
|
139
|
+
|
|
140
|
+
/** Render muted leading zeros filling the integer part to `maxDigits`. */
|
|
141
|
+
@property({type: Boolean}) hintedZeros = false;
|
|
142
|
+
|
|
143
|
+
/** Explicit longest string to reserve width for (e.g. `"0000.0"`). */
|
|
144
|
+
@property({type: String}) spaceReserver?: string;
|
|
145
|
+
|
|
146
|
+
/** Render `offText` instead of a number (e.g. equipment powered down). */
|
|
147
|
+
@property({type: Boolean}) off = false;
|
|
148
|
+
|
|
149
|
+
/** Text shown when `off` is true. */
|
|
150
|
+
@property({type: String}) offText = 'OFF';
|
|
151
|
+
|
|
152
|
+
/** Text alignment of the number within its reserved width. */
|
|
153
|
+
@property({type: String}) alignment: ObcTextboxAlignment =
|
|
154
|
+
ObcTextboxAlignment.Right;
|
|
155
|
+
|
|
156
|
+
/** Per-block measurement quality (outline chip). */
|
|
157
|
+
@property({type: String}) dataQuality?: ReadoutBlockDataQuality;
|
|
158
|
+
|
|
159
|
+
// `boolean | …` (not `false | …`): the generated Angular wrapper widens a
|
|
160
|
+
// literal-`false` union to `boolean`. `wrapWithAlertFrame` treats any
|
|
161
|
+
// non-object as "no frame", so accepting `boolean` is harmless.
|
|
162
|
+
/** Per-block alert frame; nests inside any parent alert frame. */
|
|
163
|
+
@property({type: Object}) alert: boolean | AlertFrameConfig = false;
|
|
164
|
+
|
|
165
|
+
/** Setpoint focus (touch) state — only meaningful for `role="setpoint"`. */
|
|
166
|
+
@property({type: Boolean}) touching = false;
|
|
167
|
+
|
|
168
|
+
/** Setpoint pop-up fade phase — only meaningful for `role="setpoint"`. */
|
|
169
|
+
@property({type: String}) hidePhase: ReadoutBlockHidePhase =
|
|
170
|
+
ReadoutBlockHidePhase.none;
|
|
171
|
+
|
|
172
|
+
@state() private hasAssignedIcon = false;
|
|
173
|
+
|
|
174
|
+
private get resolvedValueSize(): ObcTextboxSize {
|
|
175
|
+
if (this.valueSize) {
|
|
176
|
+
return this.valueSize;
|
|
177
|
+
}
|
|
178
|
+
switch (this.size) {
|
|
179
|
+
case ReadoutBlockSize.large:
|
|
180
|
+
return ObcTextboxSize.l;
|
|
181
|
+
case ReadoutBlockSize.medium:
|
|
182
|
+
return ObcTextboxSize.m;
|
|
183
|
+
default:
|
|
184
|
+
return ObcTextboxSize.s;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private get numericFormatOptions(): ReadoutNumericFormatOptions {
|
|
189
|
+
return {
|
|
190
|
+
showZeroPadding: false,
|
|
191
|
+
minValueLength: this.maxDigits,
|
|
192
|
+
fractionDigits: this.fractionDigits,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Widest possible value string for width reservation (e.g. `"000.0"`). */
|
|
197
|
+
private get reserverText(): string {
|
|
198
|
+
const maxDigits = this.maxDigits;
|
|
199
|
+
if (maxDigits <= 0) {
|
|
200
|
+
return '';
|
|
201
|
+
}
|
|
202
|
+
const integer = '0'.repeat(Math.max(maxDigits, 1));
|
|
203
|
+
return this.fractionDigits > 0
|
|
204
|
+
? `${integer}.${'0'.repeat(this.fractionDigits)}`
|
|
205
|
+
: integer;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Effective width reserver: the wider of the explicit `spaceReserver` and the
|
|
210
|
+
* `maxDigits`/`fractionDigits`-derived reserve, so an explicit reserver can
|
|
211
|
+
* never reserve *less* than the formatted value needs. Under tabular-nums the
|
|
212
|
+
* rendered width is proportional to character count, so "wider" compares length.
|
|
213
|
+
*/
|
|
214
|
+
private widerReserver(explicit: string | undefined, derived: string): string {
|
|
215
|
+
if (!explicit) {
|
|
216
|
+
return derived;
|
|
217
|
+
}
|
|
218
|
+
if (!derived) {
|
|
219
|
+
return explicit;
|
|
220
|
+
}
|
|
221
|
+
return explicit.length >= derived.length ? explicit : derived;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private dataQualityClasses(): Record<string, boolean> {
|
|
225
|
+
return {
|
|
226
|
+
'data-low-integrity':
|
|
227
|
+
this.dataQuality === ReadoutBlockDataQuality.lowIntegrity,
|
|
228
|
+
'data-invalid': this.dataQuality === ReadoutBlockDataQuality.invalid,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private onIconSlotChange = (event: Event): void => {
|
|
233
|
+
// `slotchange` fires after the first render once content is (re)assigned,
|
|
234
|
+
// so it covers the initial state without a firstUpdated re-render.
|
|
235
|
+
this.hasAssignedIcon =
|
|
236
|
+
(event.target as HTMLSlotElement).assignedElements({flatten: true})
|
|
237
|
+
.length > 0;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
private renderIcon(): TemplateResult | typeof nothing {
|
|
241
|
+
// Value blocks only show an icon when asked; setpoint/advice always reserve
|
|
242
|
+
// their marker. The role default is rendered as a direct child (so it sizes
|
|
243
|
+
// via `.block-icon obi-*`) and hidden once an icon is assigned to the slot —
|
|
244
|
+
// `{flatten: true}` sees through a forwarded-but-empty parent slot, so the
|
|
245
|
+
// default still shows when the consumer overrides nothing.
|
|
246
|
+
const showIcon = this.variant !== ReadoutBlockVariant.value || this.hasIcon;
|
|
247
|
+
if (!showIcon) {
|
|
248
|
+
return nothing;
|
|
249
|
+
}
|
|
250
|
+
let fallback: TemplateResult | typeof nothing = nothing;
|
|
251
|
+
if (this.variant === ReadoutBlockVariant.setpoint) {
|
|
252
|
+
fallback = html`<obi-input-right></obi-input-right>`;
|
|
253
|
+
} else if (this.variant === ReadoutBlockVariant.advice) {
|
|
254
|
+
fallback = html`<obi-notification-advice></obi-notification-advice>`;
|
|
255
|
+
}
|
|
256
|
+
return html`<span class="block-icon" part="block-icon" aria-hidden="true">
|
|
257
|
+
<slot name="icon" @slotchange=${this.onIconSlotChange}></slot>
|
|
258
|
+
${this.hasAssignedIcon ? nothing : fallback}
|
|
259
|
+
</span>`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* A cap-height `°` column whose width scales with the number size. Inherits the
|
|
264
|
+
* block's colour.
|
|
265
|
+
*/
|
|
266
|
+
private renderDegreeGlyph(size: ObcTextboxSize): TemplateResult {
|
|
267
|
+
return html`
|
|
268
|
+
<span
|
|
269
|
+
class=${classMap({
|
|
270
|
+
'degree-column': true,
|
|
271
|
+
[`degree-${size}`]: true,
|
|
272
|
+
'degree-inherit': true,
|
|
273
|
+
})}
|
|
274
|
+
part="degree"
|
|
275
|
+
>
|
|
276
|
+
<obc-textbox class="degree-glyph" .size=${size} alignment="center"
|
|
277
|
+
>°</obc-textbox
|
|
278
|
+
>
|
|
279
|
+
</span>
|
|
280
|
+
`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
override render() {
|
|
284
|
+
const valueSize = this.resolvedValueSize;
|
|
285
|
+
const formatOptions = this.numericFormatOptions;
|
|
286
|
+
const valueForFormat = this.value ?? undefined;
|
|
287
|
+
const text = this.off
|
|
288
|
+
? this.offText
|
|
289
|
+
: formatNumericValue(valueForFormat, formatOptions);
|
|
290
|
+
// Hinted zeros pad the INTEGER part up to `maxDigits`, independent of
|
|
291
|
+
// `fractionDigits` (the decimal point and fraction digits never count toward
|
|
292
|
+
// `maxDigits`). Negative / dashed values are not padded. Example: value 1.2,
|
|
293
|
+
// maxDigits 3, fractionDigits 1 → "001.2".
|
|
294
|
+
const hintCount =
|
|
295
|
+
this.off ||
|
|
296
|
+
!this.hintedZeros ||
|
|
297
|
+
valueForFormat === undefined ||
|
|
298
|
+
valueForFormat < 0
|
|
299
|
+
? 0
|
|
300
|
+
: Math.max(this.maxDigits - readoutFormattedInteger(text), 0);
|
|
301
|
+
const hinted = hintCount > 0 ? '0'.repeat(hintCount) : '';
|
|
302
|
+
// Hinted zeros own the width — they already fill to `maxDigits` — so when
|
|
303
|
+
// `hintedZeros` is enabled an explicit `spaceReserver` is ignored (it has
|
|
304
|
+
// higher priority). Otherwise the wider of the explicit reserver and the
|
|
305
|
+
// `maxDigits`-derived reserve wins.
|
|
306
|
+
const reserver = this.hintedZeros
|
|
307
|
+
? this.reserverText
|
|
308
|
+
: this.widerReserver(this.spaceReserver, this.reserverText);
|
|
309
|
+
|
|
310
|
+
const block = html`
|
|
311
|
+
<div
|
|
312
|
+
class=${classMap({
|
|
313
|
+
block: true,
|
|
314
|
+
[`block-${this.variant}`]: true,
|
|
315
|
+
[`size-${this.size}`]: true,
|
|
316
|
+
'tone-enhanced': this.enhanced,
|
|
317
|
+
touching: this.touching,
|
|
318
|
+
'is-hiding': this.hidePhase === ReadoutBlockHidePhase.hiding,
|
|
319
|
+
'is-hidden': this.hidePhase === ReadoutBlockHidePhase.hidden,
|
|
320
|
+
...this.dataQualityClasses(),
|
|
321
|
+
})}
|
|
322
|
+
part="block block-${this.variant}"
|
|
323
|
+
>
|
|
324
|
+
${this.renderIcon()}
|
|
325
|
+
<span class="block-content" part="block-content">
|
|
326
|
+
<obc-textbox
|
|
327
|
+
class="block-text"
|
|
328
|
+
part="block-text"
|
|
329
|
+
.size=${valueSize}
|
|
330
|
+
.fontWeight=${this.weight}
|
|
331
|
+
.alignment=${this.alignment}
|
|
332
|
+
.tabularNums=${true}
|
|
333
|
+
>
|
|
334
|
+
${hinted
|
|
335
|
+
? html`<span class="hinted-zero" aria-hidden="true"
|
|
336
|
+
>${hinted}</span
|
|
337
|
+
>`
|
|
338
|
+
: nothing}${text}
|
|
339
|
+
${reserver ? html`<span slot="length">${reserver}</span>` : nothing}
|
|
340
|
+
</obc-textbox>
|
|
341
|
+
${this.hasDegree ? this.renderDegreeGlyph(valueSize) : nothing}
|
|
342
|
+
</span>
|
|
343
|
+
</div>
|
|
344
|
+
`;
|
|
345
|
+
return wrapWithAlertFrame(this.alert ?? false, block);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
static override styles = unsafeCSS(componentStyle);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
declare global {
|
|
352
|
+
interface HTMLElementTagNameMap {
|
|
353
|
+
'obc-readout-block': ObcReadoutBlock;
|
|
354
|
+
}
|
|
355
|
+
}
|