@oicl/openbridge-webcomponents-full-bundle 2.0.0-next.83 → 2.0.0-next.85
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 +1223 -493
- package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
- package/custom-elements.json +642 -196
- package/dist/components/tab-item/tab-item.css.js +8 -1
- package/dist/components/tab-item/tab-item.css.js.map +1 -1
- package/dist/components/tab-item/tab-item.d.ts +44 -0
- package/dist/components/tab-item/tab-item.d.ts.map +1 -1
- package/dist/components/tab-item/tab-item.js +44 -25
- package/dist/components/tab-item/tab-item.js.map +1 -1
- package/dist/components/tab-row/tab-row.d.ts +23 -9
- package/dist/components/tab-row/tab-row.d.ts.map +1 -1
- package/dist/components/tab-row/tab-row.js +24 -7
- package/dist/components/tab-row/tab-row.js.map +1 -1
- package/dist/components/textbox/textbox.css.js +29 -5
- package/dist/components/textbox/textbox.css.js.map +1 -1
- package/dist/components/textbox/textbox.d.ts +5 -0
- package/dist/components/textbox/textbox.d.ts.map +1 -1
- package/dist/components/textbox/textbox.js +6 -1
- package/dist/components/textbox/textbox.js.map +1 -1
- package/dist/navigation-instruments/readout-list-item/readout-list-item.css.js +611 -230
- 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 +280 -61
- 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 +529 -247
- package/dist/navigation-instruments/readout-list-item/readout-list-item.js.map +1 -1
- package/package.json +1 -1
- package/src/components/tab-item/tab-item.css +8 -1
- package/src/components/tab-item/tab-item.stories.ts +30 -0
- package/src/components/tab-item/tab-item.ts +89 -31
- package/src/components/tab-row/tab-row.stories.ts +20 -10
- package/src/components/tab-row/tab-row.ts +54 -19
- package/src/components/textbox/textbox.css +27 -5
- package/src/components/textbox/textbox.ts +6 -0
- package/src/navigation-instruments/readout-list-item/readout-list-item.css +505 -238
- package/src/navigation-instruments/readout-list-item/readout-list-item.stories.ts +1604 -356
- package/src/navigation-instruments/readout-list-item/readout-list-item.ts +792 -295
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import type {Meta, StoryObj} from '@storybook/web-components-vite';
|
|
2
2
|
import {html, nothing} from 'lit';
|
|
3
|
-
import {
|
|
3
|
+
import {userEvent} from 'storybook/test';
|
|
4
|
+
import {gsap} from 'gsap';
|
|
4
5
|
import {
|
|
5
6
|
ReadoutListItemSize,
|
|
6
7
|
ReadoutListItemStacking,
|
|
7
|
-
ReadoutListItemDataState,
|
|
8
8
|
ReadoutListItemPriority,
|
|
9
|
+
ReadoutListItemDataQuality,
|
|
10
|
+
ReadoutListItemBorder,
|
|
11
|
+
ReadoutListItemSetpointInteraction,
|
|
12
|
+
ObcTextboxFontWeight,
|
|
13
|
+
type ReadoutListItemClickable,
|
|
14
|
+
type ReadoutValueOptions,
|
|
15
|
+
type ReadoutSetpointOptions,
|
|
16
|
+
type ReadoutAdviceOptions,
|
|
17
|
+
type ReadoutReserverOptions,
|
|
18
|
+
type ReadoutSrcOptions,
|
|
9
19
|
} from './readout-list-item.js';
|
|
10
20
|
import type {AlertFrameConfig} from '../../components/alert-frame/alert-frame.js';
|
|
11
21
|
import {
|
|
@@ -15,216 +25,207 @@ import {
|
|
|
15
25
|
import {AlertType} from '../../types.js';
|
|
16
26
|
import './readout-list-item.js';
|
|
17
27
|
import '../../icons/icon-placeholder.js';
|
|
28
|
+
import '../azimuth-thruster/azimuth-thruster.js';
|
|
29
|
+
import {InstrumentState, Priority} from '../types.js';
|
|
30
|
+
|
|
31
|
+
const NONE = 'none';
|
|
18
32
|
|
|
19
33
|
type ReadoutListItemStoryArgs = {
|
|
20
|
-
|
|
21
|
-
stacking: ReadoutListItemStacking;
|
|
22
|
-
priority: ReadoutListItemPriority;
|
|
23
|
-
dataState: ReadoutListItemDataState;
|
|
24
|
-
alert: AlertFrameConfig | false;
|
|
25
|
-
hasSetpoint: boolean;
|
|
26
|
-
setpointValue: number;
|
|
27
|
-
hasLabel: boolean;
|
|
28
|
-
hasDegree: boolean;
|
|
29
|
-
hasUnit: boolean;
|
|
30
|
-
hasSource: boolean;
|
|
31
|
-
hasLeadingIcon: boolean;
|
|
32
|
-
hasValueIcon: boolean;
|
|
33
|
-
minValueLength: number;
|
|
34
|
-
hasHintedZeros: boolean;
|
|
34
|
+
// Primitives
|
|
35
35
|
label: string;
|
|
36
36
|
unit: string;
|
|
37
37
|
src: string;
|
|
38
|
+
hasValue: boolean;
|
|
38
39
|
value: number;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
off: boolean;
|
|
41
|
+
hasSetpoint: boolean;
|
|
42
|
+
setpoint: number;
|
|
43
|
+
hasAdvice: boolean;
|
|
44
|
+
advice: number;
|
|
45
|
+
// Flattened options
|
|
46
|
+
'options.size': ReadoutListItemSize;
|
|
47
|
+
'options.priority': ReadoutListItemPriority;
|
|
48
|
+
'options.stacking': ReadoutListItemStacking;
|
|
49
|
+
'options.clickable': boolean;
|
|
50
|
+
'options.clickable.border': ReadoutListItemBorder;
|
|
51
|
+
'options.hasLeadingIcon': boolean;
|
|
52
|
+
'options.hasDegree': boolean;
|
|
53
|
+
'options.hasDegreeSpacer': boolean;
|
|
54
|
+
'options.fractionDigits': number;
|
|
55
|
+
'options.maxDigits': number;
|
|
56
|
+
'options.dataQuality': ReadoutListItemDataQuality | typeof NONE;
|
|
57
|
+
'options.value.weight': ObcTextboxFontWeight;
|
|
58
|
+
'options.value.hasIcon': boolean;
|
|
59
|
+
'options.value.hintedZeros': boolean;
|
|
60
|
+
'options.setpoint.interaction': ReadoutListItemSetpointInteraction;
|
|
61
|
+
'options.setpoint.touching': boolean;
|
|
62
|
+
'options.unit.spaceReserver': string;
|
|
42
63
|
};
|
|
43
64
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
65
|
+
// Authoring convenience for the stories only: the component's API is flat
|
|
66
|
+
// (global props + per-block `*Options`), but grouping them under one `options`
|
|
67
|
+
// object keeps the showcase cases compact. `renderItem` spreads this onto the
|
|
68
|
+
// element's real props.
|
|
69
|
+
type StoryOptions = {
|
|
70
|
+
size?: ReadoutListItemSize;
|
|
71
|
+
priority?: ReadoutListItemPriority;
|
|
72
|
+
stacking?: ReadoutListItemStacking;
|
|
73
|
+
clickable?: boolean | ReadoutListItemClickable;
|
|
74
|
+
hasLeadingIcon?: boolean;
|
|
75
|
+
hasDegree?: boolean;
|
|
76
|
+
hasDegreeSpacer?: boolean;
|
|
77
|
+
fractionDigits?: number;
|
|
78
|
+
maxDigits?: number;
|
|
79
|
+
dataQuality?: ReadoutListItemDataQuality;
|
|
80
|
+
alert?: false | AlertFrameConfig;
|
|
81
|
+
value?: ReadoutValueOptions;
|
|
82
|
+
setpoint?: ReadoutSetpointOptions;
|
|
83
|
+
advice?: ReadoutAdviceOptions;
|
|
84
|
+
unit?: ReadoutReserverOptions;
|
|
85
|
+
src?: ReadoutSrcOptions;
|
|
47
86
|
};
|
|
48
87
|
|
|
49
|
-
type
|
|
88
|
+
type ReadoutItemConfig = {
|
|
89
|
+
label?: string;
|
|
90
|
+
unit?: string;
|
|
91
|
+
src?: string;
|
|
92
|
+
hasValue?: boolean;
|
|
93
|
+
value?: number | null;
|
|
94
|
+
off?: boolean;
|
|
95
|
+
hasSetpoint?: boolean;
|
|
96
|
+
setpoint?: number;
|
|
97
|
+
hasAdvice?: boolean;
|
|
98
|
+
advice?: number;
|
|
99
|
+
options?: StoryOptions;
|
|
100
|
+
hasLeadingIcon?: boolean;
|
|
101
|
+
hasValueIcon?: boolean;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
type ShowcaseCase = {label: string; config: ReadoutItemConfig};
|
|
105
|
+
// `columns` pins the grid to a fixed column count so cases group logically
|
|
106
|
+
// (e.g. one row per size, columns = the state variants) instead of `auto-fit`
|
|
107
|
+
// packing different sizes onto the same line. Omit for a free-flowing grid.
|
|
108
|
+
type ShowcaseSection = {
|
|
50
109
|
title: string;
|
|
51
|
-
cases:
|
|
110
|
+
cases: ShowcaseCase[];
|
|
111
|
+
columns?: number;
|
|
52
112
|
};
|
|
53
113
|
|
|
54
|
-
const centeredCanvasDecorator = (story: () => unknown) =>
|
|
55
|
-
|
|
114
|
+
const centeredCanvasDecorator = (story: () => unknown) => html`
|
|
115
|
+
<div
|
|
116
|
+
style="min-height: 100vh; width: 100%; display: flex; align-items: center; padding: 24px;"
|
|
117
|
+
>
|
|
56
118
|
<div
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
width: 100%;
|
|
60
|
-
display: flex;
|
|
61
|
-
align-items: center;
|
|
62
|
-
justify-content: center;
|
|
63
|
-
padding: 24px;
|
|
64
|
-
"
|
|
119
|
+
data-obc-theme="day"
|
|
120
|
+
style="background: var(--container-background-color); padding: 24px; width: 100%; box-sizing: border-box;"
|
|
65
121
|
>
|
|
66
|
-
|
|
67
|
-
data-obc-theme="day"
|
|
68
|
-
style="
|
|
69
|
-
background: var(--container-background-color);
|
|
70
|
-
padding: 24px;
|
|
71
|
-
width: 100%;
|
|
72
|
-
max-width: none;
|
|
73
|
-
display: block;
|
|
74
|
-
box-sizing: border-box;
|
|
75
|
-
"
|
|
76
|
-
>
|
|
77
|
-
${story()}
|
|
78
|
-
</div>
|
|
122
|
+
${story()}
|
|
79
123
|
</div>
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const readoutShowcaseStyle = `
|
|
84
|
-
.obc-readout-list-item-sections {
|
|
85
|
-
display: flex;
|
|
86
|
-
flex-direction: column;
|
|
87
|
-
align-items: stretch;
|
|
88
|
-
gap: 32px;
|
|
89
|
-
width: 100%;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
.obc-readout-list-item-section {
|
|
93
|
-
display: flex;
|
|
94
|
-
flex-direction: column;
|
|
95
|
-
align-items: stretch;
|
|
96
|
-
gap: 16px;
|
|
97
|
-
width: 100%;
|
|
98
|
-
}
|
|
124
|
+
</div>
|
|
125
|
+
`;
|
|
99
126
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
color: var(--element-neutral-color, #777);
|
|
127
|
+
const showcaseStyle = `
|
|
128
|
+
.rli-sections { display: flex; flex-direction: column; gap: 32px; width: 100%; }
|
|
129
|
+
.rli-section { display: flex; flex-direction: column; gap: 16px; width: 100%; }
|
|
130
|
+
.rli-section-title {
|
|
131
|
+
margin: 0; font: 12px/1.2 var(--global-typography-ui-label-font-family, inherit);
|
|
132
|
+
text-transform: uppercase; letter-spacing: 0.06em; color: var(--element-neutral-color, #777);
|
|
106
133
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
grid-template-columns: repeat(auto-fit, minmax(240px, max-content));
|
|
111
|
-
gap: 20px;
|
|
112
|
-
width: 100%;
|
|
113
|
-
align-items: start;
|
|
114
|
-
justify-content: center;
|
|
115
|
-
justify-items: center;
|
|
134
|
+
.rli-grid {
|
|
135
|
+
display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, max-content));
|
|
136
|
+
gap: 20px; width: 100%; align-items: start; justify-items: stretch;
|
|
116
137
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
flex-direction: column;
|
|
121
|
-
align-items: center;
|
|
122
|
-
gap: 10px;
|
|
123
|
-
width: 240px;
|
|
124
|
-
padding: 12px;
|
|
125
|
-
border-radius: 8px;
|
|
126
|
-
background: rgba(0, 0, 0, 0.03);
|
|
138
|
+
.rli-card {
|
|
139
|
+
display: flex; flex-direction: column; gap: 10px; padding: 12px;
|
|
140
|
+
border-radius: 8px; background: rgba(0, 0, 0, 0.03);
|
|
127
141
|
}
|
|
128
|
-
|
|
129
|
-
.obc-readout-list-item-title {
|
|
142
|
+
.rli-card-title {
|
|
130
143
|
font: 10px/1.2 var(--global-typography-ui-label-font-family, inherit);
|
|
131
|
-
text-transform: uppercase;
|
|
132
|
-
letter-spacing: 0.06em;
|
|
133
|
-
color: var(--element-neutral-color, #777);
|
|
134
|
-
text-align: center;
|
|
144
|
+
text-transform: uppercase; letter-spacing: 0.06em; color: var(--element-neutral-color, #777);
|
|
135
145
|
}
|
|
136
146
|
`;
|
|
137
147
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
hasSetpoint: false,
|
|
145
|
-
setpointValue: 123,
|
|
146
|
-
hasDegree: true,
|
|
147
|
-
hasUnit: true,
|
|
148
|
-
hasSource: false,
|
|
149
|
-
hasLeadingIcon: false,
|
|
150
|
-
hasValueIcon: false,
|
|
151
|
-
minValueLength: 0,
|
|
152
|
-
hasHintedZeros: false,
|
|
153
|
-
hasLabel: true,
|
|
154
|
-
label: 'Label',
|
|
155
|
-
unit: 'Unit',
|
|
156
|
-
src: 'SRC',
|
|
157
|
-
value: 123,
|
|
158
|
-
fractionDigits: 0,
|
|
159
|
-
showZeroPadding: false,
|
|
160
|
-
labelOnly: false,
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
function renderReadoutListItem(args: Partial<ReadoutListItemStoryArgs>) {
|
|
164
|
-
const resolved = {...defaultArgs, ...args};
|
|
148
|
+
// Faithful render: data primitives pass through as-is, so an emptied Storybook
|
|
149
|
+
// control (undefined) shows the nil state — value/setpoint/advice render a dash,
|
|
150
|
+
// label/unit/src are omitted (and the row keeps its height). Used by the
|
|
151
|
+
// Playground and the alignment column.
|
|
152
|
+
function renderItem(config: ReadoutItemConfig) {
|
|
153
|
+
const o = config.options ?? {};
|
|
165
154
|
return html`
|
|
166
155
|
<obc-readout-list-item
|
|
167
|
-
.
|
|
168
|
-
.
|
|
169
|
-
.
|
|
170
|
-
.
|
|
171
|
-
.
|
|
172
|
-
.
|
|
173
|
-
.
|
|
174
|
-
.
|
|
175
|
-
.
|
|
176
|
-
.
|
|
177
|
-
.
|
|
178
|
-
.
|
|
179
|
-
.
|
|
180
|
-
.
|
|
181
|
-
.
|
|
182
|
-
.
|
|
183
|
-
.
|
|
184
|
-
.
|
|
185
|
-
.
|
|
186
|
-
.
|
|
187
|
-
.
|
|
188
|
-
.
|
|
156
|
+
.label=${config.label}
|
|
157
|
+
.unit=${config.unit}
|
|
158
|
+
.src=${config.src}
|
|
159
|
+
.hasValue=${config.hasValue ?? true}
|
|
160
|
+
.value=${config.value ?? null}
|
|
161
|
+
.off=${config.off ?? false}
|
|
162
|
+
.hasSetpoint=${config.hasSetpoint ?? false}
|
|
163
|
+
.setpoint=${config.setpoint}
|
|
164
|
+
.hasAdvice=${config.hasAdvice ?? false}
|
|
165
|
+
.advice=${config.advice}
|
|
166
|
+
.size=${o.size}
|
|
167
|
+
.priority=${o.priority}
|
|
168
|
+
.stacking=${o.stacking}
|
|
169
|
+
.clickable=${o.clickable ?? false}
|
|
170
|
+
.hasLeadingIcon=${o.hasLeadingIcon ?? false}
|
|
171
|
+
.hasDegree=${o.hasDegree ?? false}
|
|
172
|
+
.hasDegreeSpacer=${o.hasDegreeSpacer ?? false}
|
|
173
|
+
.fractionDigits=${o.fractionDigits ?? 0}
|
|
174
|
+
.maxDigits=${o.maxDigits ?? 0}
|
|
175
|
+
.dataQuality=${o.dataQuality}
|
|
176
|
+
.alert=${o.alert ?? false}
|
|
177
|
+
.valueOptions=${o.value}
|
|
178
|
+
.setpointOptions=${o.setpoint}
|
|
179
|
+
.adviceOptions=${o.advice}
|
|
180
|
+
.unitOptions=${o.unit}
|
|
181
|
+
.srcOptions=${o.src}
|
|
189
182
|
>
|
|
190
|
-
${
|
|
191
|
-
? html`<
|
|
192
|
-
slot="leading-icon"
|
|
193
|
-
style="display:block; width:100%; height:100%;"
|
|
194
|
-
>
|
|
195
|
-
<obi-placeholder></obi-placeholder>
|
|
196
|
-
</span>`
|
|
183
|
+
${config.hasLeadingIcon
|
|
184
|
+
? html`<obi-placeholder slot="leading-icon"></obi-placeholder>`
|
|
197
185
|
: nothing}
|
|
198
|
-
${
|
|
199
|
-
? html`<
|
|
200
|
-
slot="value-icon"
|
|
201
|
-
style="display:block; width:100%; height:100%;"
|
|
202
|
-
>
|
|
203
|
-
<obi-placeholder></obi-placeholder>
|
|
204
|
-
</span>`
|
|
186
|
+
${config.hasValueIcon
|
|
187
|
+
? html`<obi-placeholder slot="value-icon"></obi-placeholder>`
|
|
205
188
|
: nothing}
|
|
206
189
|
</obc-readout-list-item>
|
|
207
190
|
`;
|
|
208
191
|
}
|
|
209
192
|
|
|
210
|
-
|
|
193
|
+
// Showcase render: fills demo defaults (Label / Unit / SRC / 123 / 120 / 118)
|
|
194
|
+
// for the static preset cards that omit those fields. The Playground uses
|
|
195
|
+
// renderItem directly so emptying a control surfaces the nil state.
|
|
196
|
+
function showcaseItem(config: ReadoutItemConfig) {
|
|
197
|
+
return renderItem({
|
|
198
|
+
...config,
|
|
199
|
+
label: config.label ?? 'Label',
|
|
200
|
+
unit: config.unit ?? 'Unit',
|
|
201
|
+
src: config.src ?? 'SRC',
|
|
202
|
+
value: config.value === undefined ? 123 : config.value,
|
|
203
|
+
setpoint: config.setpoint ?? 120,
|
|
204
|
+
advice: config.advice ?? 118,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function renderShowcase(sections: ShowcaseSection[]) {
|
|
211
209
|
return html`
|
|
212
210
|
<style>
|
|
213
|
-
${
|
|
211
|
+
${showcaseStyle}
|
|
214
212
|
</style>
|
|
215
|
-
<div class="
|
|
213
|
+
<div class="rli-sections">
|
|
216
214
|
${sections.map(
|
|
217
215
|
(section) => html`
|
|
218
|
-
<section class="
|
|
219
|
-
<h3 class="
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
216
|
+
<section class="rli-section">
|
|
217
|
+
<h3 class="rli-section-title">${section.title}</h3>
|
|
218
|
+
<div
|
|
219
|
+
class="rli-grid"
|
|
220
|
+
style=${section.columns
|
|
221
|
+
? `grid-template-columns: repeat(${section.columns}, max-content);`
|
|
222
|
+
: nothing}
|
|
223
|
+
>
|
|
223
224
|
${section.cases.map(
|
|
224
225
|
(item) => html`
|
|
225
|
-
<div class="
|
|
226
|
-
<div class="
|
|
227
|
-
${
|
|
226
|
+
<div class="rli-card">
|
|
227
|
+
<div class="rli-card-title">${item.label}</div>
|
|
228
|
+
${showcaseItem(item.config)}
|
|
228
229
|
</div>
|
|
229
230
|
`
|
|
230
231
|
)}
|
|
@@ -236,123 +237,198 @@ function renderShowcaseSections(sections: ReadoutShowcaseSection[]) {
|
|
|
236
237
|
`;
|
|
237
238
|
}
|
|
238
239
|
|
|
240
|
+
const defaultArgs: ReadoutListItemStoryArgs = {
|
|
241
|
+
label: 'Label',
|
|
242
|
+
unit: 'Unit',
|
|
243
|
+
src: 'SRC',
|
|
244
|
+
hasValue: true,
|
|
245
|
+
value: 123,
|
|
246
|
+
off: false,
|
|
247
|
+
hasSetpoint: false,
|
|
248
|
+
setpoint: 120,
|
|
249
|
+
hasAdvice: false,
|
|
250
|
+
advice: 118,
|
|
251
|
+
'options.size': ReadoutListItemSize.small,
|
|
252
|
+
'options.priority': ReadoutListItemPriority.regular,
|
|
253
|
+
'options.stacking': ReadoutListItemStacking.trailingUnit,
|
|
254
|
+
'options.clickable': false,
|
|
255
|
+
'options.clickable.border': ReadoutListItemBorder.squared,
|
|
256
|
+
'options.hasLeadingIcon': false,
|
|
257
|
+
'options.hasDegree': true,
|
|
258
|
+
'options.hasDegreeSpacer': false,
|
|
259
|
+
'options.fractionDigits': 0,
|
|
260
|
+
'options.maxDigits': 0,
|
|
261
|
+
'options.dataQuality': NONE,
|
|
262
|
+
'options.value.weight': ObcTextboxFontWeight.regular,
|
|
263
|
+
'options.value.hasIcon': false,
|
|
264
|
+
'options.value.hintedZeros': false,
|
|
265
|
+
'options.setpoint.interaction':
|
|
266
|
+
ReadoutListItemSetpointInteraction.alwaysVisible,
|
|
267
|
+
'options.setpoint.touching': false,
|
|
268
|
+
'options.unit.spaceReserver': '',
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
function argsToOptions(args: ReadoutListItemStoryArgs): StoryOptions {
|
|
272
|
+
return {
|
|
273
|
+
size: args['options.size'],
|
|
274
|
+
priority: args['options.priority'],
|
|
275
|
+
stacking: args['options.stacking'],
|
|
276
|
+
clickable: args['options.clickable']
|
|
277
|
+
? {border: args['options.clickable.border']}
|
|
278
|
+
: false,
|
|
279
|
+
hasLeadingIcon: args['options.hasLeadingIcon'],
|
|
280
|
+
hasDegree: args['options.hasDegree'],
|
|
281
|
+
hasDegreeSpacer: args['options.hasDegreeSpacer'],
|
|
282
|
+
fractionDigits: args['options.fractionDigits'],
|
|
283
|
+
maxDigits: args['options.maxDigits'],
|
|
284
|
+
dataQuality:
|
|
285
|
+
args['options.dataQuality'] === NONE
|
|
286
|
+
? undefined
|
|
287
|
+
: args['options.dataQuality'],
|
|
288
|
+
value: {
|
|
289
|
+
weight: args['options.value.weight'],
|
|
290
|
+
hasIcon: args['options.value.hasIcon'],
|
|
291
|
+
hintedZeros: args['options.value.hintedZeros'],
|
|
292
|
+
},
|
|
293
|
+
setpoint: {
|
|
294
|
+
interaction: args['options.setpoint.interaction'],
|
|
295
|
+
touching: args['options.setpoint.touching'],
|
|
296
|
+
},
|
|
297
|
+
unit: {spaceReserver: args['options.unit.spaceReserver'] || undefined},
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
239
301
|
const meta = {
|
|
240
302
|
title: 'Instruments/Readout List Item',
|
|
241
|
-
tags: ['autodocs', '6.0'],
|
|
303
|
+
tags: ['autodocs', '6.0', 'wip'],
|
|
242
304
|
component: 'obc-readout-list-item',
|
|
243
305
|
decorators: [centeredCanvasDecorator],
|
|
244
|
-
render: (args) =>
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
306
|
+
render: (args) =>
|
|
307
|
+
html`<div style="display:flex; width:100%;">
|
|
308
|
+
${renderItem({
|
|
309
|
+
label: args.label,
|
|
310
|
+
unit: args.unit,
|
|
311
|
+
src: args.src,
|
|
312
|
+
hasValue: args.hasValue,
|
|
313
|
+
value: args.value,
|
|
314
|
+
off: args.off,
|
|
315
|
+
hasSetpoint: args.hasSetpoint,
|
|
316
|
+
setpoint: args.setpoint,
|
|
317
|
+
hasAdvice: args.hasAdvice,
|
|
318
|
+
advice: args.advice,
|
|
319
|
+
options: argsToOptions(args),
|
|
320
|
+
hasLeadingIcon: args['options.hasLeadingIcon'],
|
|
321
|
+
hasValueIcon: args['options.value.hasIcon'],
|
|
322
|
+
})}
|
|
323
|
+
</div>`,
|
|
253
324
|
args: defaultArgs,
|
|
254
325
|
argTypes: {
|
|
255
|
-
|
|
326
|
+
label: {name: 'Label', control: {type: 'text'}, table: {category: 'Data'}},
|
|
327
|
+
unit: {name: 'Unit', control: {type: 'text'}, table: {category: 'Data'}},
|
|
328
|
+
src: {name: 'Source', control: {type: 'text'}, table: {category: 'Data'}},
|
|
329
|
+
hasValue: {name: 'Has Value', table: {category: 'Data'}},
|
|
330
|
+
value: {
|
|
331
|
+
name: 'Value',
|
|
332
|
+
control: {type: 'number'},
|
|
333
|
+
table: {category: 'Data'},
|
|
334
|
+
},
|
|
335
|
+
off: {name: 'Off', table: {category: 'Data'}},
|
|
336
|
+
hasSetpoint: {name: 'Has Setpoint', table: {category: 'Data'}},
|
|
337
|
+
setpoint: {
|
|
338
|
+
name: 'Setpoint',
|
|
339
|
+
control: {type: 'number'},
|
|
340
|
+
if: {arg: 'hasSetpoint', truthy: true},
|
|
341
|
+
table: {category: 'Data'},
|
|
342
|
+
},
|
|
343
|
+
hasAdvice: {name: 'Has Advice', table: {category: 'Data'}},
|
|
344
|
+
advice: {
|
|
345
|
+
name: 'Advice',
|
|
346
|
+
control: {type: 'number'},
|
|
347
|
+
if: {arg: 'hasAdvice', truthy: true},
|
|
348
|
+
table: {category: 'Data'},
|
|
349
|
+
},
|
|
350
|
+
'options.size': {
|
|
256
351
|
name: 'Size',
|
|
257
352
|
control: {type: 'select'},
|
|
258
353
|
options: Object.values(ReadoutListItemSize),
|
|
259
|
-
table: {category: '
|
|
260
|
-
},
|
|
261
|
-
stacking: {
|
|
262
|
-
name: 'Stacking',
|
|
263
|
-
control: {type: 'select'},
|
|
264
|
-
options: Object.values(ReadoutListItemStacking),
|
|
265
|
-
table: {category: 'Readout'},
|
|
354
|
+
table: {category: 'Layout'},
|
|
266
355
|
},
|
|
267
|
-
priority: {
|
|
356
|
+
'options.priority': {
|
|
268
357
|
name: 'Priority',
|
|
269
358
|
control: {type: 'select'},
|
|
270
359
|
options: Object.values(ReadoutListItemPriority),
|
|
271
|
-
table: {category: '
|
|
360
|
+
table: {category: 'Layout'},
|
|
272
361
|
},
|
|
273
|
-
|
|
274
|
-
name: '
|
|
362
|
+
'options.stacking': {
|
|
363
|
+
name: 'Stacking',
|
|
275
364
|
control: {type: 'select'},
|
|
276
|
-
options: Object.values(
|
|
277
|
-
table: {category: '
|
|
278
|
-
},
|
|
279
|
-
hasSetpoint: {
|
|
280
|
-
name: 'Has Setpoint',
|
|
281
|
-
table: {category: 'Readout'},
|
|
282
|
-
},
|
|
283
|
-
setpointValue: {
|
|
284
|
-
name: 'Setpoint Value',
|
|
285
|
-
control: {type: 'number'},
|
|
286
|
-
if: {
|
|
287
|
-
arg: 'hasSetpoint',
|
|
288
|
-
truthy: true,
|
|
289
|
-
},
|
|
290
|
-
table: {category: 'Value'},
|
|
291
|
-
},
|
|
292
|
-
hasDegree: {
|
|
293
|
-
name: 'Has Degree',
|
|
294
|
-
table: {category: 'Content'},
|
|
295
|
-
},
|
|
296
|
-
hasLabel: {
|
|
297
|
-
name: 'Has Label',
|
|
298
|
-
table: {category: 'Content'},
|
|
299
|
-
},
|
|
300
|
-
hasUnit: {
|
|
301
|
-
name: 'Has Unit',
|
|
302
|
-
table: {category: 'Content'},
|
|
365
|
+
options: Object.values(ReadoutListItemStacking),
|
|
366
|
+
table: {category: 'Layout'},
|
|
303
367
|
},
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
368
|
+
'options.clickable': {name: 'Clickable', table: {category: 'Layout'}},
|
|
369
|
+
'options.clickable.border': {
|
|
370
|
+
name: 'Clickable Border',
|
|
371
|
+
control: {type: 'select'},
|
|
372
|
+
options: Object.values(ReadoutListItemBorder),
|
|
373
|
+
if: {arg: 'options.clickable', truthy: true},
|
|
374
|
+
table: {category: 'Layout'},
|
|
307
375
|
},
|
|
308
|
-
hasLeadingIcon: {
|
|
376
|
+
'options.hasLeadingIcon': {
|
|
309
377
|
name: 'Has Leading Icon',
|
|
310
|
-
table: {category: '
|
|
378
|
+
table: {category: 'Layout'},
|
|
311
379
|
},
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
380
|
+
'options.hasDegree': {name: 'Has Degree', table: {category: 'Format'}},
|
|
381
|
+
'options.hasDegreeSpacer': {
|
|
382
|
+
name: 'Has Degree Spacer',
|
|
383
|
+
table: {category: 'Format'},
|
|
315
384
|
},
|
|
316
|
-
|
|
317
|
-
name: '
|
|
318
|
-
control: {type: '
|
|
319
|
-
table: {category: '
|
|
385
|
+
'options.fractionDigits': {
|
|
386
|
+
name: 'Fraction Digits',
|
|
387
|
+
control: {type: 'number', min: 0, step: 1},
|
|
388
|
+
table: {category: 'Format'},
|
|
320
389
|
},
|
|
321
|
-
|
|
322
|
-
name: '
|
|
323
|
-
control: {type: '
|
|
324
|
-
|
|
325
|
-
table: {category: 'Content'},
|
|
390
|
+
'options.maxDigits': {
|
|
391
|
+
name: 'Max Digits',
|
|
392
|
+
control: {type: 'number', min: 0, step: 1},
|
|
393
|
+
table: {category: 'Format'},
|
|
326
394
|
},
|
|
327
|
-
|
|
328
|
-
name: '
|
|
329
|
-
control: {type: '
|
|
330
|
-
|
|
331
|
-
table: {category: '
|
|
395
|
+
'options.dataQuality': {
|
|
396
|
+
name: 'Data Quality',
|
|
397
|
+
control: {type: 'select'},
|
|
398
|
+
options: [NONE, ...Object.values(ReadoutListItemDataQuality)],
|
|
399
|
+
table: {category: 'State'},
|
|
332
400
|
},
|
|
333
|
-
value: {
|
|
334
|
-
name: 'Value',
|
|
335
|
-
control: {type: '
|
|
401
|
+
'options.value.weight': {
|
|
402
|
+
name: 'Value Weight',
|
|
403
|
+
control: {type: 'select'},
|
|
404
|
+
options: Object.values(ObcTextboxFontWeight),
|
|
336
405
|
table: {category: 'Value'},
|
|
337
406
|
},
|
|
338
|
-
|
|
339
|
-
name: '
|
|
340
|
-
|
|
341
|
-
table: {category: 'Formatting'},
|
|
407
|
+
'options.value.hasIcon': {
|
|
408
|
+
name: 'Value Has Icon',
|
|
409
|
+
table: {category: 'Value'},
|
|
342
410
|
},
|
|
343
|
-
|
|
344
|
-
name: '
|
|
345
|
-
|
|
411
|
+
'options.value.hintedZeros': {
|
|
412
|
+
name: 'Value Hinted Zeros',
|
|
413
|
+
if: {arg: 'options.maxDigits', truthy: true},
|
|
414
|
+
table: {category: 'Value'},
|
|
346
415
|
},
|
|
347
|
-
|
|
348
|
-
name: '
|
|
349
|
-
control: {type: '
|
|
350
|
-
|
|
416
|
+
'options.setpoint.interaction': {
|
|
417
|
+
name: 'Setpoint Interaction',
|
|
418
|
+
control: {type: 'select'},
|
|
419
|
+
options: Object.values(ReadoutListItemSetpointInteraction),
|
|
420
|
+
if: {arg: 'hasSetpoint', truthy: true},
|
|
421
|
+
table: {category: 'Setpoint'},
|
|
351
422
|
},
|
|
352
|
-
|
|
353
|
-
name: '
|
|
354
|
-
if: {arg: '
|
|
355
|
-
table: {category: '
|
|
423
|
+
'options.setpoint.touching': {
|
|
424
|
+
name: 'Setpoint Touching',
|
|
425
|
+
if: {arg: 'hasSetpoint', truthy: true},
|
|
426
|
+
table: {category: 'Setpoint'},
|
|
427
|
+
},
|
|
428
|
+
'options.unit.spaceReserver': {
|
|
429
|
+
name: 'Unit Space Reserver',
|
|
430
|
+
control: {type: 'text'},
|
|
431
|
+
table: {category: 'Format'},
|
|
356
432
|
},
|
|
357
433
|
},
|
|
358
434
|
} satisfies Meta<ReadoutListItemStoryArgs>;
|
|
@@ -362,130 +438,1302 @@ type Story = StoryObj<ReadoutListItemStoryArgs>;
|
|
|
362
438
|
|
|
363
439
|
export const Playground: Story = {};
|
|
364
440
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
ReadoutListItemSize.priority,
|
|
377
|
-
ReadoutListItemSize.enhanced,
|
|
378
|
-
] as const;
|
|
379
|
-
const priorities = [
|
|
380
|
-
ReadoutListItemPriority.regular,
|
|
381
|
-
ReadoutListItemPriority.enhanced,
|
|
382
|
-
ReadoutListItemPriority.setpoint,
|
|
383
|
-
ReadoutListItemPriority.setpointFlipFlop,
|
|
384
|
-
] as const;
|
|
385
|
-
const hasSetpoints = [false, true] as const;
|
|
386
|
-
|
|
387
|
-
const cases: ReadoutShowcaseCase[] = [];
|
|
388
|
-
|
|
389
|
-
for (const size of sizes) {
|
|
390
|
-
for (const priority of priorities) {
|
|
391
|
-
for (const hasSetpoint of hasSetpoints) {
|
|
392
|
-
cases.push({
|
|
393
|
-
label: `${size} / ${priority} / hasSetpoint=${hasSetpoint}`,
|
|
394
|
-
args: {
|
|
395
|
-
size,
|
|
396
|
-
stacking,
|
|
397
|
-
priority,
|
|
398
|
-
hasSetpoint,
|
|
399
|
-
dataState: ReadoutListItemDataState.none,
|
|
400
|
-
hasUnit,
|
|
401
|
-
hasSource,
|
|
402
|
-
value: 123,
|
|
403
|
-
setpointValue: 123,
|
|
404
|
-
hasDegree: true,
|
|
405
|
-
hasLabel: true,
|
|
441
|
+
export const Off: Story = {
|
|
442
|
+
render: () =>
|
|
443
|
+
renderShowcase([
|
|
444
|
+
{
|
|
445
|
+
title: 'Off (value only, setpoint and advice remain)',
|
|
446
|
+
columns: 3,
|
|
447
|
+
cases: [
|
|
448
|
+
{label: 'value off', config: {off: true}},
|
|
449
|
+
{
|
|
450
|
+
label: 'value off + setpoint',
|
|
451
|
+
config: {off: true, hasSetpoint: true, setpoint: 120},
|
|
406
452
|
},
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
453
|
+
{
|
|
454
|
+
label: 'value off + advice',
|
|
455
|
+
config: {off: true, hasAdvice: true, advice: 118},
|
|
456
|
+
},
|
|
457
|
+
],
|
|
458
|
+
},
|
|
459
|
+
]),
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
const SIZES = [
|
|
463
|
+
ReadoutListItemSize.small,
|
|
464
|
+
ReadoutListItemSize.medium,
|
|
465
|
+
ReadoutListItemSize.large,
|
|
466
|
+
] as const;
|
|
411
467
|
|
|
412
|
-
|
|
468
|
+
export const Sizes: Story = {
|
|
469
|
+
render: () =>
|
|
470
|
+
renderShowcase([
|
|
471
|
+
{
|
|
472
|
+
title: 'Sizes × Priority',
|
|
473
|
+
columns: 2,
|
|
474
|
+
cases: SIZES.flatMap((size) =>
|
|
475
|
+
[
|
|
476
|
+
ReadoutListItemPriority.regular,
|
|
477
|
+
ReadoutListItemPriority.enhanced,
|
|
478
|
+
].map((priority) => ({
|
|
479
|
+
label: `${size} / ${priority}`,
|
|
480
|
+
config: {options: {size, priority, hasDegree: true}},
|
|
481
|
+
}))
|
|
482
|
+
),
|
|
483
|
+
},
|
|
484
|
+
]),
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
function stackingCases(stacking: ReadoutListItemStacking): ShowcaseCase[] {
|
|
488
|
+
return SIZES.flatMap((size) => [
|
|
489
|
+
{
|
|
490
|
+
label: `${size} / value`,
|
|
491
|
+
config: {options: {size, stacking, hasDegree: true}},
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
label: `${size} / value + setpoint`,
|
|
495
|
+
config: {
|
|
496
|
+
hasSetpoint: true,
|
|
497
|
+
setpoint: 120,
|
|
498
|
+
options: {size, stacking, hasDegree: true},
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
label: `${size} / value + advice + setpoint`,
|
|
503
|
+
config: {
|
|
504
|
+
hasAdvice: true,
|
|
505
|
+
advice: 118,
|
|
506
|
+
hasSetpoint: true,
|
|
507
|
+
setpoint: 120,
|
|
508
|
+
options: {size, stacking, hasDegree: true},
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
]);
|
|
413
512
|
}
|
|
414
513
|
|
|
415
514
|
export const TrailingUnit: Story = {
|
|
416
515
|
render: () =>
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
516
|
+
renderShowcase([
|
|
517
|
+
{
|
|
518
|
+
title: 'Stacking: trailing-unit',
|
|
519
|
+
columns: 3,
|
|
520
|
+
cases: stackingCases(ReadoutListItemStacking.trailingUnit),
|
|
521
|
+
},
|
|
522
|
+
]),
|
|
422
523
|
};
|
|
423
524
|
|
|
424
525
|
export const LeadingUnit: Story = {
|
|
425
526
|
render: () =>
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
527
|
+
renderShowcase([
|
|
528
|
+
{
|
|
529
|
+
title: 'Stacking: leading-unit',
|
|
530
|
+
columns: 3,
|
|
531
|
+
cases: stackingCases(ReadoutListItemStacking.leadingUnit),
|
|
532
|
+
},
|
|
533
|
+
]),
|
|
431
534
|
};
|
|
432
535
|
|
|
433
536
|
export const LeadingSrc: Story = {
|
|
434
537
|
render: () =>
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
538
|
+
renderShowcase([
|
|
539
|
+
{
|
|
540
|
+
title: 'Stacking: leading-src',
|
|
541
|
+
columns: 3,
|
|
542
|
+
cases: stackingCases(ReadoutListItemStacking.leadingSrc),
|
|
543
|
+
},
|
|
544
|
+
]),
|
|
440
545
|
};
|
|
441
546
|
|
|
442
|
-
export const
|
|
443
|
-
render: () =>
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
547
|
+
export const SetpointFlipFlop: Story = {
|
|
548
|
+
render: () =>
|
|
549
|
+
renderShowcase([
|
|
550
|
+
{
|
|
551
|
+
title: 'Setpoint flip-flop (value vs. setpoint focus)',
|
|
552
|
+
columns: 2,
|
|
553
|
+
cases: SIZES.flatMap((size) => [
|
|
554
|
+
{
|
|
555
|
+
label: `${size} / not at setpoint`,
|
|
556
|
+
config: {
|
|
557
|
+
value: 123,
|
|
558
|
+
hasSetpoint: true,
|
|
559
|
+
setpoint: 120,
|
|
560
|
+
options: {
|
|
561
|
+
size,
|
|
562
|
+
setpoint: {
|
|
563
|
+
interaction: ReadoutListItemSetpointInteraction.flipFlop,
|
|
564
|
+
},
|
|
565
|
+
hasDegree: true,
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
label: `${size} / at setpoint`,
|
|
571
|
+
config: {
|
|
572
|
+
value: 120,
|
|
573
|
+
hasSetpoint: true,
|
|
574
|
+
setpoint: 120,
|
|
575
|
+
options: {
|
|
576
|
+
size,
|
|
577
|
+
setpoint: {
|
|
578
|
+
interaction: ReadoutListItemSetpointInteraction.flipFlop,
|
|
579
|
+
},
|
|
580
|
+
hasDegree: true,
|
|
581
|
+
},
|
|
582
|
+
},
|
|
583
|
+
},
|
|
584
|
+
]),
|
|
453
585
|
},
|
|
454
|
-
|
|
586
|
+
]),
|
|
587
|
+
};
|
|
455
588
|
|
|
456
|
-
|
|
589
|
+
// The four Figma "states" (sandbox file g9gUFzN6MzzvNudv4XOqMT), each with a
|
|
590
|
+
// setpoint, mapped onto the component API. Value 123 ≠ setpoint 120 so the
|
|
591
|
+
// flip-flop emphasises the setpoint. Locks the both-gray/both-blue colour rule
|
|
592
|
+
// and the setpoint emphasis (SemiBold only when emphasised; value stays Regular).
|
|
593
|
+
const STATE_VARIANTS: {label: string; options: StoryOptions}[] = [
|
|
594
|
+
{label: 'regular', options: {priority: ReadoutListItemPriority.regular}},
|
|
595
|
+
{label: 'enhanced', options: {priority: ReadoutListItemPriority.enhanced}},
|
|
596
|
+
{
|
|
597
|
+
label: 'input (adjusting)',
|
|
598
|
+
options: {
|
|
599
|
+
priority: ReadoutListItemPriority.enhanced,
|
|
600
|
+
setpoint: {touching: true},
|
|
601
|
+
},
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
label: 'input flip-flop',
|
|
605
|
+
options: {
|
|
606
|
+
priority: ReadoutListItemPriority.enhanced,
|
|
607
|
+
setpoint: {interaction: ReadoutListItemSetpointInteraction.flipFlop},
|
|
608
|
+
},
|
|
457
609
|
},
|
|
610
|
+
];
|
|
611
|
+
|
|
612
|
+
export const States: Story = {
|
|
613
|
+
render: () =>
|
|
614
|
+
renderShowcase([
|
|
615
|
+
{
|
|
616
|
+
title: 'States (value + setpoint) — both neutral or both enhanced',
|
|
617
|
+
columns: 4,
|
|
618
|
+
cases: SIZES.flatMap((size) =>
|
|
619
|
+
STATE_VARIANTS.map((variant) => ({
|
|
620
|
+
label: `${size} / ${variant.label}`,
|
|
621
|
+
config: {
|
|
622
|
+
value: 123,
|
|
623
|
+
hasSetpoint: true,
|
|
624
|
+
setpoint: 120,
|
|
625
|
+
options: {size, hasDegree: true, ...variant.options},
|
|
626
|
+
},
|
|
627
|
+
}))
|
|
628
|
+
),
|
|
629
|
+
},
|
|
630
|
+
]),
|
|
458
631
|
};
|
|
459
632
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
633
|
+
// `flip-flop` (value/setpoint swap) and `pop-up` (setpoint fades out once
|
|
634
|
+
// reached) only read clearly in motion, and they need `value === setpoint`
|
|
635
|
+
// transitions rather than a single control — so each gets a focused static
|
|
636
|
+
// story plus a GSAP-driven animated one below.
|
|
637
|
+
|
|
638
|
+
export const SetpointPopUp: Story = {
|
|
639
|
+
render: () =>
|
|
640
|
+
renderShowcase([
|
|
641
|
+
{
|
|
642
|
+
title: 'Pop-up — setpoint shown only until the value reaches it',
|
|
643
|
+
columns: 2,
|
|
644
|
+
cases: [
|
|
645
|
+
{
|
|
646
|
+
label: 'value ≠ setpoint → shown',
|
|
647
|
+
config: {
|
|
648
|
+
value: 123,
|
|
649
|
+
hasSetpoint: true,
|
|
650
|
+
setpoint: 120,
|
|
651
|
+
options: {
|
|
652
|
+
setpoint: {
|
|
653
|
+
interaction: ReadoutListItemSetpointInteraction.popUp,
|
|
654
|
+
},
|
|
655
|
+
},
|
|
656
|
+
},
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
label: 'value = setpoint → fades out (space kept)',
|
|
660
|
+
config: {
|
|
661
|
+
value: 120,
|
|
662
|
+
hasSetpoint: true,
|
|
663
|
+
setpoint: 120,
|
|
664
|
+
options: {
|
|
665
|
+
setpoint: {
|
|
666
|
+
interaction: ReadoutListItemSetpointInteraction.popUp,
|
|
667
|
+
},
|
|
668
|
+
},
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
],
|
|
672
|
+
},
|
|
673
|
+
]),
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
export const SetpointTouch: Story = {
|
|
677
|
+
render: () =>
|
|
678
|
+
renderShowcase([
|
|
679
|
+
{
|
|
680
|
+
title:
|
|
681
|
+
'Touch (focus) — the setpoint triangle is highlighted while adjusting',
|
|
682
|
+
columns: 2,
|
|
683
|
+
cases: [
|
|
684
|
+
{
|
|
685
|
+
label: 'normal',
|
|
686
|
+
config: {
|
|
687
|
+
value: 123,
|
|
688
|
+
hasSetpoint: true,
|
|
689
|
+
setpoint: 120,
|
|
690
|
+
options: {setpoint: {}},
|
|
691
|
+
},
|
|
692
|
+
},
|
|
693
|
+
{
|
|
694
|
+
label: 'touching (focus)',
|
|
695
|
+
config: {
|
|
696
|
+
value: 123,
|
|
697
|
+
hasSetpoint: true,
|
|
698
|
+
setpoint: 120,
|
|
699
|
+
options: {setpoint: {touching: true}},
|
|
700
|
+
},
|
|
701
|
+
},
|
|
702
|
+
],
|
|
703
|
+
},
|
|
704
|
+
]),
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
// Shared scaffold for the GSAP-driven interaction demos: a single readout whose
|
|
708
|
+
// `value` is swept toward / away from the setpoint so the flip-flop swap and the
|
|
709
|
+
// pop-up show/hide can be seen in motion. Tagged `skip-test` (the animation makes
|
|
710
|
+
// snapshots flaky).
|
|
711
|
+
function animatedInteractionStory(config: {
|
|
712
|
+
name: string;
|
|
713
|
+
interaction: ReadoutListItemSetpointInteraction;
|
|
714
|
+
intro: string;
|
|
715
|
+
awayLabel: string;
|
|
716
|
+
reachLabel: string;
|
|
717
|
+
}): Story {
|
|
718
|
+
const SETPOINT = 120;
|
|
719
|
+
const AWAY = 124;
|
|
720
|
+
return {
|
|
721
|
+
name: config.name,
|
|
722
|
+
render: () => html`
|
|
723
|
+
<div
|
|
724
|
+
style="display:flex; flex-direction:column; gap:16px; width:360px; padding:24px;"
|
|
725
|
+
>
|
|
726
|
+
<div style="font-size:14px; color:var(--element-neutral-color, #888);">
|
|
727
|
+
${config.intro}
|
|
728
|
+
</div>
|
|
729
|
+
<div
|
|
730
|
+
style="padding:8px; border:1px dashed var(--border-divider-color, #ccc); border-radius:8px;"
|
|
731
|
+
>
|
|
732
|
+
<obc-readout-list-item
|
|
733
|
+
id="anim-demo"
|
|
734
|
+
.label=${'Heading'}
|
|
735
|
+
.value=${AWAY}
|
|
736
|
+
.hasSetpoint=${true}
|
|
737
|
+
.setpoint=${SETPOINT}
|
|
738
|
+
.size=${ReadoutListItemSize.medium}
|
|
739
|
+
.hasDegree=${true}
|
|
740
|
+
.setpointOptions=${{interaction: config.interaction}}
|
|
741
|
+
></obc-readout-list-item>
|
|
742
|
+
</div>
|
|
743
|
+
<div style="display:flex; gap:12px; flex-wrap:wrap;">
|
|
744
|
+
<button id="btn-away" style="padding:8px 16px; cursor:pointer;">
|
|
745
|
+
${config.awayLabel}
|
|
746
|
+
</button>
|
|
747
|
+
<button id="btn-reach" style="padding:8px 16px; cursor:pointer;">
|
|
748
|
+
${config.reachLabel}
|
|
749
|
+
</button>
|
|
750
|
+
</div>
|
|
751
|
+
<div
|
|
752
|
+
id="anim-status"
|
|
753
|
+
style="font:12px/1.4 monospace; color:var(--element-neutral-color, #666);"
|
|
754
|
+
>
|
|
755
|
+
value=${AWAY}, setpoint=${SETPOINT}
|
|
756
|
+
</div>
|
|
757
|
+
</div>
|
|
758
|
+
`,
|
|
759
|
+
play: async ({canvasElement}) => {
|
|
760
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
761
|
+
const el = canvasElement.querySelector('#anim-demo') as
|
|
762
|
+
| (HTMLElement & {
|
|
763
|
+
value: number;
|
|
764
|
+
setpointOptions: ReadoutSetpointOptions;
|
|
765
|
+
})
|
|
766
|
+
| null;
|
|
767
|
+
const status = canvasElement.querySelector(
|
|
768
|
+
'#anim-status'
|
|
769
|
+
) as HTMLElement | null;
|
|
770
|
+
const btnAway = canvasElement.querySelector(
|
|
771
|
+
'#btn-away'
|
|
772
|
+
) as HTMLButtonElement | null;
|
|
773
|
+
const btnReach = canvasElement.querySelector(
|
|
774
|
+
'#btn-reach'
|
|
775
|
+
) as HTMLButtonElement | null;
|
|
776
|
+
if (!el || !status || !btnAway || !btnReach) return;
|
|
777
|
+
|
|
778
|
+
const anim = {value: AWAY};
|
|
779
|
+
// Focus/touch state — on while actively adjusting (sweeping toward the
|
|
780
|
+
// setpoint), off once settled. Rebuild the object so Lit re-renders.
|
|
781
|
+
const setTouch = (on: boolean) => {
|
|
782
|
+
el.setpointOptions = {interaction: config.interaction, touching: on};
|
|
783
|
+
};
|
|
784
|
+
const setVal = (v: number) => {
|
|
785
|
+
el.value = v;
|
|
786
|
+
status.textContent = `value=${Math.round(v)}, setpoint=${SETPOINT}`;
|
|
787
|
+
};
|
|
788
|
+
// Snap to the exact target on complete so `value === setpoint` holds (the
|
|
789
|
+
// flip-flop swap and pop-up hide both key off strict equality).
|
|
790
|
+
const tweenTo = (target: number) => {
|
|
791
|
+
gsap.killTweensOf(anim);
|
|
792
|
+
setTouch(true);
|
|
793
|
+
gsap.to(anim, {
|
|
794
|
+
value: target,
|
|
795
|
+
duration: 0.8,
|
|
796
|
+
ease: 'power2.inOut',
|
|
797
|
+
onUpdate: () => setVal(anim.value),
|
|
798
|
+
onComplete: () => {
|
|
799
|
+
setVal(target);
|
|
800
|
+
setTouch(false);
|
|
801
|
+
},
|
|
802
|
+
});
|
|
803
|
+
};
|
|
804
|
+
btnAway.onclick = () => tweenTo(AWAY);
|
|
805
|
+
btnReach.onclick = () => tweenTo(SETPOINT);
|
|
806
|
+
|
|
807
|
+
// Auto-play a couple of cycles for the "Play" button.
|
|
808
|
+
for (let i = 0; i < 2; i++) {
|
|
809
|
+
await userEvent.click(btnReach);
|
|
810
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
811
|
+
await userEvent.click(btnAway);
|
|
812
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
813
|
+
}
|
|
467
814
|
},
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// `tags: ['skip-test']` must sit on the export literal (not inside the helper) —
|
|
819
|
+
// Storybook's CSF indexer reads tags statically, so a tag returned from a
|
|
820
|
+
// function call would not exclude the story from the visual snapshot run.
|
|
821
|
+
export const SetpointFlipFlopAnimated: Story = {
|
|
822
|
+
...animatedInteractionStory({
|
|
823
|
+
name: 'Flip-Flop (Animated)',
|
|
824
|
+
interaction: ReadoutListItemSetpointInteraction.flipFlop,
|
|
825
|
+
intro:
|
|
826
|
+
'Flip-flop: as the value reaches the setpoint (120) the emphasis swaps from the setpoint to the value (100ms). Press Play, or use the buttons.',
|
|
827
|
+
awayLabel: 'Away (124)',
|
|
828
|
+
reachLabel: 'Reach setpoint (120)',
|
|
829
|
+
}),
|
|
830
|
+
tags: ['skip-test'],
|
|
831
|
+
};
|
|
832
|
+
|
|
833
|
+
export const SetpointPopUpAnimated: Story = {
|
|
834
|
+
...animatedInteractionStory({
|
|
835
|
+
name: 'Pop-Up (Animated)',
|
|
836
|
+
interaction: ReadoutListItemSetpointInteraction.popUp,
|
|
837
|
+
intro:
|
|
838
|
+
'Pop-up: the setpoint shows while the value differs, then fades out (100ms) once the value reaches it. Press Play, or use the buttons.',
|
|
839
|
+
awayLabel: 'Leave setpoint (124)',
|
|
840
|
+
reachLabel: 'Reach setpoint (120)',
|
|
841
|
+
}),
|
|
842
|
+
tags: ['skip-test'],
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
// Demo durations (s) for the synced azimuth-thruster flow below — the vessel
|
|
846
|
+
// response after "confirm": thrust catches up faster than the heavier angle.
|
|
847
|
+
const SYNC_THRUST_DURATION = 2;
|
|
848
|
+
const SYNC_ANGLE_DURATION = 10;
|
|
849
|
+
|
|
850
|
+
// Reserve integer-digit width so the value / setpoint don't shift left↔right as
|
|
851
|
+
// they cross the 2↔3 digit boundary during the sweep (angle 30↔120, up to 360°;
|
|
852
|
+
// power up to 100%). `maxDigits` reserves the width for the value AND the
|
|
853
|
+
// setpoint block at once, without printing leading zeros.
|
|
854
|
+
const SYNC_MAX_DIGITS = 3;
|
|
855
|
+
|
|
856
|
+
// The readouts' flip-flop setpoint options; only `touch` changes during the flow
|
|
857
|
+
// (size/maxDigits are static flat props on the elements).
|
|
858
|
+
function syncSetpointOptions(touching: boolean): ReadoutSetpointOptions {
|
|
859
|
+
return {
|
|
860
|
+
interaction: ReadoutListItemSetpointInteraction.flipFlop,
|
|
861
|
+
touching,
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* **Synced with an instrument (Azimuth Thruster)** — replays the exact setpoint
|
|
867
|
+
* adjustment flow from `Building Blocks/Setpoint → Setpoint Azimuth Thruster
|
|
868
|
+
* Flow`, with two `obc-readout-list-item`s wired to the same demo state below the
|
|
869
|
+
* instrument.
|
|
870
|
+
*
|
|
871
|
+
* These are **two independent components driven by one shared animation** (synced
|
|
872
|
+
* manually, for this demo only): the `obc-azimuth-thruster` instrument and the
|
|
873
|
+
* `obc-readout-list-item` rows. `obc-azimuth-thruster-labeled` pairs the thruster
|
|
874
|
+
* with the older `obc-readout`; this shows the new readout list item mirroring the
|
|
875
|
+
* same angle/thrust adjustment — the setpoint triangle is highlighted while
|
|
876
|
+
* touching, the setpoint value slides during "move", and the value catches up
|
|
877
|
+
* (with the flip-flop emphasis swap) on "confirm".
|
|
878
|
+
*
|
|
879
|
+
* Press "Play", or use the buttons, to run reset → initiate → move → confirm on
|
|
880
|
+
* both at once.
|
|
881
|
+
*/
|
|
882
|
+
export const SyncedWithAzimuthThruster: Story = {
|
|
883
|
+
name: 'Synced With Azimuth Thruster (Interactive)',
|
|
884
|
+
tags: ['skip-test'],
|
|
885
|
+
render: () => html`
|
|
886
|
+
<div
|
|
887
|
+
style="display:flex; flex-direction:column; gap:24px; width:360px; padding:24px;"
|
|
888
|
+
>
|
|
889
|
+
<div style="font-size:14px; color:var(--element-neutral-color, #888);">
|
|
890
|
+
Two independent components driven by one shared demo state (synced
|
|
891
|
+
manually for this demo): the azimuth-thruster instrument and two
|
|
892
|
+
<code>obc-readout-list-item</code> rows below it. Press "Play", or use
|
|
893
|
+
the buttons, to run the reset → initiate → move → confirm setpoint
|
|
894
|
+
adjustment on both at once.
|
|
895
|
+
</div>
|
|
896
|
+
|
|
897
|
+
<div style="width:280px; height:280px; align-self:center;">
|
|
898
|
+
<obc-azimuth-thruster
|
|
899
|
+
id="azimuth-demo"
|
|
900
|
+
.angle=${30}
|
|
901
|
+
.angleSetpoint=${30}
|
|
902
|
+
.thrustSetpoint=${25}
|
|
903
|
+
.thrust=${25}
|
|
904
|
+
.state=${InstrumentState.active}
|
|
905
|
+
.priority=${Priority.enhanced}
|
|
906
|
+
.animateSetpoint=${true}
|
|
907
|
+
.primaryTickmarkInterval=${45}
|
|
908
|
+
.secondaryTickmarkInterval=${5}
|
|
909
|
+
.tertiaryTickmarkInterval=${1}
|
|
910
|
+
.showLabels=${true}
|
|
911
|
+
.tickmarksInside=${true}
|
|
912
|
+
></obc-azimuth-thruster>
|
|
913
|
+
</div>
|
|
914
|
+
|
|
915
|
+
<div
|
|
916
|
+
style="display:flex; flex-direction:column; gap:8px; padding:8px; border:1px dashed var(--border-divider-color, #ccc); border-radius:8px;"
|
|
917
|
+
>
|
|
918
|
+
<obc-readout-list-item
|
|
919
|
+
id="rli-angle"
|
|
920
|
+
.label=${'Angle'}
|
|
921
|
+
.unit=${'DEG'}
|
|
922
|
+
.value=${30}
|
|
923
|
+
.hasSetpoint=${true}
|
|
924
|
+
.setpoint=${30}
|
|
925
|
+
.size=${ReadoutListItemSize.medium}
|
|
926
|
+
.hasDegree=${true}
|
|
927
|
+
.maxDigits=${SYNC_MAX_DIGITS}
|
|
928
|
+
.setpointOptions=${syncSetpointOptions(false)}
|
|
929
|
+
></obc-readout-list-item>
|
|
930
|
+
<obc-readout-list-item
|
|
931
|
+
id="rli-power"
|
|
932
|
+
.label=${'Power'}
|
|
933
|
+
.unit=${'%'}
|
|
934
|
+
.value=${25}
|
|
935
|
+
.hasSetpoint=${true}
|
|
936
|
+
.setpoint=${25}
|
|
937
|
+
.size=${ReadoutListItemSize.medium}
|
|
938
|
+
.maxDigits=${SYNC_MAX_DIGITS}
|
|
939
|
+
.setpointOptions=${syncSetpointOptions(false)}
|
|
940
|
+
></obc-readout-list-item>
|
|
941
|
+
</div>
|
|
942
|
+
|
|
943
|
+
<div style="display:flex; gap:12px; flex-wrap:wrap;">
|
|
944
|
+
<button id="btn-at-reset" style="padding:8px 16px; cursor:pointer;">
|
|
945
|
+
Reset (t=0)
|
|
946
|
+
</button>
|
|
947
|
+
<button id="btn-at-initiate" style="padding:8px 16px; cursor:pointer;">
|
|
948
|
+
Initiate (t=1)
|
|
949
|
+
</button>
|
|
950
|
+
<button id="btn-at-move" style="padding:8px 16px; cursor:pointer;">
|
|
951
|
+
Move (t=2)
|
|
952
|
+
</button>
|
|
953
|
+
<button id="btn-at-confirm" style="padding:8px 16px; cursor:pointer;">
|
|
954
|
+
Confirm (t=3)
|
|
955
|
+
</button>
|
|
956
|
+
</div>
|
|
957
|
+
|
|
958
|
+
<div
|
|
959
|
+
id="at-status"
|
|
960
|
+
style="font:12px/1.4 monospace; color:var(--element-neutral-color, #666);"
|
|
961
|
+
>
|
|
962
|
+
State: t=0 (at setpoint) | angle=30°, angleSP=30°, thrust=25%,
|
|
963
|
+
thrustSP=25%
|
|
964
|
+
</div>
|
|
965
|
+
</div>
|
|
966
|
+
`,
|
|
967
|
+
play: async ({canvasElement}) => {
|
|
968
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
969
|
+
|
|
970
|
+
const at = canvasElement.querySelector('#azimuth-demo') as
|
|
971
|
+
| (HTMLElement & {
|
|
972
|
+
angle: number;
|
|
973
|
+
angleSetpoint: number | undefined;
|
|
974
|
+
newAngleSetpoint: number | undefined;
|
|
975
|
+
thrust: number;
|
|
976
|
+
thrustSetpoint: number | undefined;
|
|
977
|
+
touching: boolean;
|
|
978
|
+
})
|
|
979
|
+
| null;
|
|
980
|
+
const status = canvasElement.querySelector(
|
|
981
|
+
'#at-status'
|
|
982
|
+
) as HTMLElement | null;
|
|
983
|
+
const angleReadout = canvasElement.querySelector('#rli-angle') as
|
|
984
|
+
| (HTMLElement & {
|
|
985
|
+
value: number;
|
|
986
|
+
setpoint: number;
|
|
987
|
+
setpointOptions: ReadoutSetpointOptions;
|
|
988
|
+
})
|
|
989
|
+
| null;
|
|
990
|
+
const powerReadout = canvasElement.querySelector('#rli-power') as
|
|
991
|
+
| (HTMLElement & {
|
|
992
|
+
value: number;
|
|
993
|
+
setpoint: number;
|
|
994
|
+
setpointOptions: ReadoutSetpointOptions;
|
|
995
|
+
})
|
|
996
|
+
| null;
|
|
997
|
+
const btnReset = canvasElement.querySelector(
|
|
998
|
+
'#btn-at-reset'
|
|
999
|
+
) as HTMLButtonElement | null;
|
|
1000
|
+
const btnInitiate = canvasElement.querySelector(
|
|
1001
|
+
'#btn-at-initiate'
|
|
1002
|
+
) as HTMLButtonElement | null;
|
|
1003
|
+
const btnMove = canvasElement.querySelector(
|
|
1004
|
+
'#btn-at-move'
|
|
1005
|
+
) as HTMLButtonElement | null;
|
|
1006
|
+
const btnConfirm = canvasElement.querySelector(
|
|
1007
|
+
'#btn-at-confirm'
|
|
1008
|
+
) as HTMLButtonElement | null;
|
|
1009
|
+
|
|
1010
|
+
if (
|
|
1011
|
+
!at ||
|
|
1012
|
+
!status ||
|
|
1013
|
+
!angleReadout ||
|
|
1014
|
+
!powerReadout ||
|
|
1015
|
+
!btnReset ||
|
|
1016
|
+
!btnInitiate ||
|
|
1017
|
+
!btnMove ||
|
|
1018
|
+
!btnConfirm
|
|
1019
|
+
) {
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
const ANGLE_FROM = 30;
|
|
1024
|
+
const ANGLE_TO = 120;
|
|
1025
|
+
const THRUST_FROM = 25;
|
|
1026
|
+
const THRUST_TO = 70;
|
|
1027
|
+
|
|
1028
|
+
const anim = {
|
|
1029
|
+
angle: ANGLE_FROM,
|
|
1030
|
+
thrust: THRUST_FROM,
|
|
1031
|
+
newAngle: ANGLE_FROM,
|
|
1032
|
+
newThrust: THRUST_FROM,
|
|
1033
|
+
};
|
|
1034
|
+
|
|
1035
|
+
// Toggle the readouts' touch (focus) state together. Lit re-renders on a
|
|
1036
|
+
// fresh `setpointOptions` reference (the value / setpoint primitives and the
|
|
1037
|
+
// static size/maxDigits props update independently).
|
|
1038
|
+
const setReadoutTouch = (touch: boolean) => {
|
|
1039
|
+
angleReadout.setpointOptions = syncSetpointOptions(touch);
|
|
1040
|
+
powerReadout.setpointOptions = syncSetpointOptions(touch);
|
|
1041
|
+
};
|
|
1042
|
+
|
|
1043
|
+
const updateStatus = (state: string) => {
|
|
1044
|
+
const nAngle = at.newAngleSetpoint;
|
|
1045
|
+
status.textContent = `State: ${state} | angle=${Math.round(anim.angle)}°, angleSP=${Math.round(at.angleSetpoint ?? 0)}°, newAngleSP=${nAngle !== undefined ? Math.round(nAngle) + '°' : 'n/a'}, thrust=${Math.round(anim.thrust)}%, thrustSP=${Math.round(at.thrustSetpoint ?? 0)}%`;
|
|
1046
|
+
};
|
|
1047
|
+
|
|
1048
|
+
const killAll = () => gsap.killTweensOf(anim);
|
|
1049
|
+
|
|
1050
|
+
// t=0 — vessel at setpoint, no adjustment in progress.
|
|
1051
|
+
btnReset.onclick = () => {
|
|
1052
|
+
killAll();
|
|
1053
|
+
anim.angle = ANGLE_FROM;
|
|
1054
|
+
anim.thrust = THRUST_FROM;
|
|
1055
|
+
anim.newAngle = ANGLE_FROM;
|
|
1056
|
+
anim.newThrust = THRUST_FROM;
|
|
1057
|
+
at.angle = ANGLE_FROM;
|
|
1058
|
+
at.angleSetpoint = ANGLE_FROM;
|
|
1059
|
+
at.newAngleSetpoint = undefined;
|
|
1060
|
+
at.thrust = THRUST_FROM;
|
|
1061
|
+
at.thrustSetpoint = THRUST_FROM;
|
|
1062
|
+
(at as unknown as {newThrustSetpoint?: number}).newThrustSetpoint =
|
|
1063
|
+
undefined;
|
|
1064
|
+
at.touching = false;
|
|
1065
|
+
setReadoutTouch(false);
|
|
1066
|
+
angleReadout.value = ANGLE_FROM;
|
|
1067
|
+
angleReadout.setpoint = ANGLE_FROM;
|
|
1068
|
+
powerReadout.value = THRUST_FROM;
|
|
1069
|
+
powerReadout.setpoint = THRUST_FROM;
|
|
1070
|
+
updateStatus('t=0 (at setpoint)');
|
|
1071
|
+
};
|
|
1072
|
+
|
|
1073
|
+
// t=1 — start adjusting: new setpoint appears at the current position, the
|
|
1074
|
+
// readouts enter the touch (focus) state.
|
|
1075
|
+
btnInitiate.onclick = () => {
|
|
1076
|
+
killAll();
|
|
1077
|
+
anim.newAngle = at.angleSetpoint ?? ANGLE_FROM;
|
|
1078
|
+
anim.newThrust = at.thrustSetpoint ?? THRUST_FROM;
|
|
1079
|
+
at.newAngleSetpoint = anim.newAngle;
|
|
1080
|
+
(at as unknown as {newThrustSetpoint?: number}).newThrustSetpoint =
|
|
1081
|
+
anim.newThrust;
|
|
1082
|
+
at.touching = true;
|
|
1083
|
+
setReadoutTouch(true);
|
|
1084
|
+
updateStatus('t=1 (initiate)');
|
|
1085
|
+
};
|
|
1086
|
+
|
|
1087
|
+
// t=2 — slide the new setpoint; the readouts' setpoint value follows while
|
|
1088
|
+
// the value (vessel) stays put, so flip-flop emphasises the setpoint.
|
|
1089
|
+
btnMove.onclick = () => {
|
|
1090
|
+
killAll();
|
|
1091
|
+
if (at.newAngleSetpoint === undefined) {
|
|
1092
|
+
at.newAngleSetpoint = at.angleSetpoint ?? ANGLE_FROM;
|
|
1093
|
+
anim.newAngle = at.newAngleSetpoint;
|
|
1094
|
+
(at as unknown as {newThrustSetpoint?: number}).newThrustSetpoint =
|
|
1095
|
+
at.thrustSetpoint ?? THRUST_FROM;
|
|
1096
|
+
anim.newThrust = at.thrustSetpoint ?? THRUST_FROM;
|
|
1097
|
+
at.touching = true;
|
|
1098
|
+
setReadoutTouch(true);
|
|
1099
|
+
}
|
|
1100
|
+
gsap.to(anim, {
|
|
1101
|
+
newAngle: ANGLE_TO,
|
|
1102
|
+
newThrust: THRUST_TO,
|
|
1103
|
+
duration: 1.5,
|
|
1104
|
+
ease: 'power2.inOut',
|
|
1105
|
+
onUpdate: () => {
|
|
1106
|
+
at.newAngleSetpoint = anim.newAngle;
|
|
1107
|
+
(at as unknown as {newThrustSetpoint?: number}).newThrustSetpoint =
|
|
1108
|
+
anim.newThrust;
|
|
1109
|
+
angleReadout.setpoint = Math.round(anim.newAngle);
|
|
1110
|
+
powerReadout.setpoint = Math.round(anim.newThrust);
|
|
1111
|
+
updateStatus('t=2 (move)');
|
|
1112
|
+
},
|
|
1113
|
+
});
|
|
1114
|
+
};
|
|
1115
|
+
|
|
1116
|
+
// t=3 — confirm: setpoint commits, focus clears, the value catches up
|
|
1117
|
+
// (thrust faster than the heavier angle); flip-flop swaps back to the value.
|
|
1118
|
+
btnConfirm.onclick = () => {
|
|
1119
|
+
killAll();
|
|
1120
|
+
const targetAngle = Math.round(at.newAngleSetpoint ?? ANGLE_TO);
|
|
1121
|
+
const targetThrust = Math.round(anim.newThrust);
|
|
1122
|
+
|
|
1123
|
+
at.angleSetpoint = targetAngle;
|
|
1124
|
+
at.newAngleSetpoint = undefined;
|
|
1125
|
+
at.thrustSetpoint = targetThrust;
|
|
1126
|
+
(at as unknown as {newThrustSetpoint?: number}).newThrustSetpoint =
|
|
1127
|
+
undefined;
|
|
1128
|
+
at.touching = false;
|
|
1129
|
+
|
|
1130
|
+
anim.angle = at.angle;
|
|
1131
|
+
anim.thrust = at.thrust;
|
|
1132
|
+
|
|
1133
|
+
setReadoutTouch(false);
|
|
1134
|
+
angleReadout.setpoint = targetAngle;
|
|
1135
|
+
powerReadout.setpoint = targetThrust;
|
|
1136
|
+
|
|
1137
|
+
gsap.to(anim, {
|
|
1138
|
+
thrust: targetThrust,
|
|
1139
|
+
duration: SYNC_THRUST_DURATION,
|
|
1140
|
+
ease: 'sine.inOut',
|
|
1141
|
+
onUpdate: () => {
|
|
1142
|
+
at.thrust = anim.thrust;
|
|
1143
|
+
powerReadout.value = Math.round(anim.thrust);
|
|
1144
|
+
updateStatus('t=3 (confirm)');
|
|
1145
|
+
},
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
gsap.to(anim, {
|
|
1149
|
+
angle: targetAngle,
|
|
1150
|
+
duration: SYNC_ANGLE_DURATION,
|
|
1151
|
+
ease: 'sine.inOut',
|
|
1152
|
+
onUpdate: () => {
|
|
1153
|
+
at.angle = anim.angle;
|
|
1154
|
+
angleReadout.value = Math.round(anim.angle);
|
|
1155
|
+
updateStatus('t=3 (confirm)');
|
|
1156
|
+
},
|
|
1157
|
+
});
|
|
1158
|
+
};
|
|
1159
|
+
|
|
1160
|
+
// ── Auto-play sequence ──
|
|
1161
|
+
await userEvent.click(btnReset);
|
|
1162
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
1163
|
+
await userEvent.click(btnInitiate);
|
|
1164
|
+
await new Promise((r) => setTimeout(r, 1200));
|
|
1165
|
+
await userEvent.click(btnMove);
|
|
1166
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
1167
|
+
await userEvent.click(btnConfirm);
|
|
1168
|
+
await new Promise((r) => setTimeout(r, SYNC_ANGLE_DURATION * 1000 + 1000));
|
|
468
1169
|
},
|
|
469
1170
|
};
|
|
470
1171
|
|
|
471
|
-
export const
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
1172
|
+
export const DataQuality: Story = {
|
|
1173
|
+
render: () =>
|
|
1174
|
+
renderShowcase([
|
|
1175
|
+
{
|
|
1176
|
+
title: 'Data quality',
|
|
1177
|
+
columns: 4,
|
|
1178
|
+
cases: [
|
|
1179
|
+
{label: 'nominal', config: {options: {}}},
|
|
1180
|
+
{
|
|
1181
|
+
label: 'low-integrity',
|
|
1182
|
+
config: {
|
|
1183
|
+
options: {dataQuality: ReadoutListItemDataQuality.lowIntegrity},
|
|
1184
|
+
},
|
|
1185
|
+
},
|
|
1186
|
+
{
|
|
1187
|
+
label: 'invalid',
|
|
1188
|
+
config: {
|
|
1189
|
+
options: {dataQuality: ReadoutListItemDataQuality.invalid},
|
|
1190
|
+
},
|
|
1191
|
+
},
|
|
1192
|
+
{
|
|
1193
|
+
label: 'null value (dash)',
|
|
1194
|
+
config: {value: null, options: {}},
|
|
1195
|
+
},
|
|
1196
|
+
],
|
|
1197
|
+
},
|
|
1198
|
+
]),
|
|
1199
|
+
};
|
|
1200
|
+
|
|
1201
|
+
export const PerBlockState: Story = {
|
|
1202
|
+
render: () =>
|
|
1203
|
+
renderShowcase([
|
|
1204
|
+
{
|
|
1205
|
+
title: 'Per-block data quality (independent of the row)',
|
|
1206
|
+
cases: [
|
|
1207
|
+
{
|
|
1208
|
+
label: 'value low-integrity',
|
|
1209
|
+
config: {
|
|
1210
|
+
hasSetpoint: true,
|
|
1211
|
+
hasAdvice: true,
|
|
1212
|
+
options: {
|
|
1213
|
+
value: {dataQuality: ReadoutListItemDataQuality.lowIntegrity},
|
|
1214
|
+
},
|
|
1215
|
+
},
|
|
1216
|
+
},
|
|
1217
|
+
{
|
|
1218
|
+
label: 'setpoint invalid',
|
|
1219
|
+
config: {
|
|
1220
|
+
hasSetpoint: true,
|
|
1221
|
+
options: {
|
|
1222
|
+
setpoint: {dataQuality: ReadoutListItemDataQuality.invalid},
|
|
1223
|
+
},
|
|
1224
|
+
},
|
|
1225
|
+
},
|
|
1226
|
+
{
|
|
1227
|
+
label: 'advice low-integrity',
|
|
1228
|
+
config: {
|
|
1229
|
+
hasAdvice: true,
|
|
1230
|
+
options: {
|
|
1231
|
+
advice: {dataQuality: ReadoutListItemDataQuality.lowIntegrity},
|
|
1232
|
+
},
|
|
1233
|
+
},
|
|
1234
|
+
},
|
|
1235
|
+
{
|
|
1236
|
+
label: 'src invalid',
|
|
1237
|
+
config: {
|
|
1238
|
+
options: {src: {dataQuality: ReadoutListItemDataQuality.invalid}},
|
|
1239
|
+
},
|
|
1240
|
+
},
|
|
1241
|
+
{
|
|
1242
|
+
label: 'row invalid + value low-integrity (nested)',
|
|
1243
|
+
config: {
|
|
1244
|
+
options: {
|
|
1245
|
+
dataQuality: ReadoutListItemDataQuality.invalid,
|
|
1246
|
+
value: {dataQuality: ReadoutListItemDataQuality.lowIntegrity},
|
|
1247
|
+
},
|
|
1248
|
+
},
|
|
1249
|
+
},
|
|
1250
|
+
],
|
|
1251
|
+
},
|
|
1252
|
+
{
|
|
1253
|
+
title: 'Per-block alert frames (nest inside the row)',
|
|
1254
|
+
columns: 3,
|
|
1255
|
+
cases: [
|
|
1256
|
+
{
|
|
1257
|
+
label: 'value alert',
|
|
1258
|
+
config: {
|
|
1259
|
+
options: {
|
|
1260
|
+
value: {
|
|
1261
|
+
alert: {
|
|
1262
|
+
status: AlertType.Warning,
|
|
1263
|
+
mode: ObcAlertFrameMode.unackedActive,
|
|
1264
|
+
type: ObcAlertFrameType.Regular,
|
|
1265
|
+
},
|
|
1266
|
+
},
|
|
1267
|
+
},
|
|
1268
|
+
},
|
|
1269
|
+
},
|
|
1270
|
+
{
|
|
1271
|
+
label: 'setpoint alert',
|
|
1272
|
+
config: {
|
|
1273
|
+
hasSetpoint: true,
|
|
1274
|
+
options: {
|
|
1275
|
+
setpoint: {
|
|
1276
|
+
alert: {
|
|
1277
|
+
status: AlertType.Caution,
|
|
1278
|
+
mode: ObcAlertFrameMode.ackedActive,
|
|
1279
|
+
type: ObcAlertFrameType.Regular,
|
|
1280
|
+
},
|
|
1281
|
+
},
|
|
1282
|
+
},
|
|
1283
|
+
},
|
|
1284
|
+
},
|
|
1285
|
+
{
|
|
1286
|
+
label: 'row alarm + value warning (nested)',
|
|
1287
|
+
config: {
|
|
1288
|
+
options: {
|
|
1289
|
+
alert: {
|
|
1290
|
+
status: AlertType.Alarm,
|
|
1291
|
+
mode: ObcAlertFrameMode.ackedActive,
|
|
1292
|
+
type: ObcAlertFrameType.Regular,
|
|
1293
|
+
},
|
|
1294
|
+
value: {
|
|
1295
|
+
alert: {
|
|
1296
|
+
status: AlertType.Warning,
|
|
1297
|
+
mode: ObcAlertFrameMode.unackedActive,
|
|
1298
|
+
type: ObcAlertFrameType.Regular,
|
|
1299
|
+
},
|
|
1300
|
+
},
|
|
1301
|
+
},
|
|
1302
|
+
},
|
|
1303
|
+
},
|
|
1304
|
+
],
|
|
1305
|
+
},
|
|
1306
|
+
{
|
|
1307
|
+
// `dataQuality` (the surface bg + outline) is orthogonal to `alert` (the
|
|
1308
|
+
// surrounding frame), so they stack: the low-integrity / invalid styling
|
|
1309
|
+
// sits INSIDE the alert frame. A side-flip frame type shows the alert
|
|
1310
|
+
// category badge in the flap for extra emphasis.
|
|
1311
|
+
title: 'Data quality inside an alert frame (combined)',
|
|
1312
|
+
columns: 2,
|
|
1313
|
+
cases: [
|
|
1314
|
+
{
|
|
1315
|
+
label: 'low-integrity + warning frame (badge flap)',
|
|
1316
|
+
config: {
|
|
1317
|
+
hasSetpoint: true,
|
|
1318
|
+
setpoint: 120,
|
|
1319
|
+
options: {
|
|
1320
|
+
dataQuality: ReadoutListItemDataQuality.lowIntegrity,
|
|
1321
|
+
alert: {
|
|
1322
|
+
status: AlertType.Warning,
|
|
1323
|
+
mode: ObcAlertFrameMode.unackedActive,
|
|
1324
|
+
type: ObcAlertFrameType.SmallSideFlip,
|
|
1325
|
+
showAlertCategoryIcon: true,
|
|
1326
|
+
},
|
|
1327
|
+
},
|
|
1328
|
+
},
|
|
1329
|
+
},
|
|
1330
|
+
{
|
|
1331
|
+
label: 'invalid + alarm frame (large badge flap)',
|
|
1332
|
+
config: {
|
|
1333
|
+
value: null,
|
|
1334
|
+
options: {
|
|
1335
|
+
dataQuality: ReadoutListItemDataQuality.invalid,
|
|
1336
|
+
alert: {
|
|
1337
|
+
status: AlertType.Alarm,
|
|
1338
|
+
mode: ObcAlertFrameMode.unackedActive,
|
|
1339
|
+
type: ObcAlertFrameType.LargeSideFlip,
|
|
1340
|
+
showAlertCategoryIcon: true,
|
|
1341
|
+
},
|
|
1342
|
+
},
|
|
1343
|
+
},
|
|
1344
|
+
},
|
|
1345
|
+
],
|
|
1346
|
+
},
|
|
1347
|
+
]),
|
|
1348
|
+
};
|
|
1349
|
+
|
|
1350
|
+
export const Clickable: Story = {
|
|
1351
|
+
render: () =>
|
|
1352
|
+
renderShowcase([
|
|
1353
|
+
{
|
|
1354
|
+
title: 'Clickable borders',
|
|
1355
|
+
columns: 3,
|
|
1356
|
+
cases: [
|
|
1357
|
+
{
|
|
1358
|
+
label: 'squared',
|
|
1359
|
+
config: {
|
|
1360
|
+
options: {clickable: {border: ReadoutListItemBorder.squared}},
|
|
1361
|
+
},
|
|
1362
|
+
},
|
|
1363
|
+
{
|
|
1364
|
+
label: 'round-corners',
|
|
1365
|
+
config: {
|
|
1366
|
+
options: {
|
|
1367
|
+
clickable: {border: ReadoutListItemBorder.roundCorners},
|
|
1368
|
+
},
|
|
1369
|
+
},
|
|
1370
|
+
},
|
|
1371
|
+
{
|
|
1372
|
+
label: 'round',
|
|
1373
|
+
config: {
|
|
1374
|
+
options: {clickable: {border: ReadoutListItemBorder.round}},
|
|
1375
|
+
},
|
|
1376
|
+
},
|
|
1377
|
+
],
|
|
1378
|
+
},
|
|
1379
|
+
]),
|
|
1380
|
+
};
|
|
1381
|
+
|
|
1382
|
+
const alertConfig = (alert: AlertFrameConfig): StoryOptions => ({
|
|
1383
|
+
alert,
|
|
1384
|
+
});
|
|
1385
|
+
|
|
1386
|
+
export const Alarm: Story = {
|
|
1387
|
+
render: () =>
|
|
1388
|
+
showcaseItem({
|
|
1389
|
+
options: alertConfig({
|
|
1390
|
+
status: AlertType.Alarm,
|
|
1391
|
+
mode: ObcAlertFrameMode.ackedActive,
|
|
1392
|
+
type: ObcAlertFrameType.Regular,
|
|
1393
|
+
}),
|
|
1394
|
+
}),
|
|
1395
|
+
};
|
|
1396
|
+
|
|
1397
|
+
// Each row drops a different part; the per-row outline makes it obvious that the
|
|
1398
|
+
// row height and the bottom baseline stay constant regardless of what's missing.
|
|
1399
|
+
const MISSING_PARTS_ROWS: {title: string; config: ReadoutItemConfig}[] = [
|
|
1400
|
+
{
|
|
1401
|
+
title: 'all parts',
|
|
1402
|
+
config: {label: 'Heading', value: 123, unit: 'kn', src: 'GPS'},
|
|
479
1403
|
},
|
|
1404
|
+
{title: 'no label', config: {value: 123, unit: 'kn', src: 'GPS'}},
|
|
1405
|
+
{
|
|
1406
|
+
title: 'no value (hasValue false)',
|
|
1407
|
+
config: {label: 'Heading', hasValue: false, unit: 'kn', src: 'GPS'},
|
|
1408
|
+
},
|
|
1409
|
+
{
|
|
1410
|
+
title: 'null value (dash)',
|
|
1411
|
+
config: {label: 'Heading', value: null, unit: 'kn', src: 'GPS'},
|
|
1412
|
+
},
|
|
1413
|
+
{title: 'no unit', config: {label: 'Heading', value: 123, src: 'GPS'}},
|
|
1414
|
+
{title: 'no src', config: {label: 'Heading', value: 123, unit: 'kn'}},
|
|
1415
|
+
{title: 'label only', config: {label: 'Heading', hasValue: false}},
|
|
1416
|
+
{title: 'value only', config: {value: 123}},
|
|
1417
|
+
{
|
|
1418
|
+
title: 'off',
|
|
1419
|
+
config: {label: 'Heading', off: true, unit: 'kn', src: 'GPS'},
|
|
1420
|
+
},
|
|
1421
|
+
];
|
|
1422
|
+
|
|
1423
|
+
export const MissingParts: Story = {
|
|
1424
|
+
render: () => html`
|
|
1425
|
+
<style>
|
|
1426
|
+
.rli-mp {
|
|
1427
|
+
display: flex;
|
|
1428
|
+
flex-direction: column;
|
|
1429
|
+
gap: 4px;
|
|
1430
|
+
width: 380px;
|
|
1431
|
+
}
|
|
1432
|
+
.rli-mp-row {
|
|
1433
|
+
display: flex;
|
|
1434
|
+
align-items: center;
|
|
1435
|
+
gap: 12px;
|
|
1436
|
+
}
|
|
1437
|
+
.rli-mp-tag {
|
|
1438
|
+
width: 150px;
|
|
1439
|
+
flex: none;
|
|
1440
|
+
font: 10px/1.2 var(--global-typography-ui-label-font-family, sans-serif);
|
|
1441
|
+
text-transform: uppercase;
|
|
1442
|
+
letter-spacing: 0.06em;
|
|
1443
|
+
color: var(--element-neutral-color, #777);
|
|
1444
|
+
}
|
|
1445
|
+
.rli-mp-item {
|
|
1446
|
+
flex: 1;
|
|
1447
|
+
outline: 1px solid rgba(0, 0, 0, 0.12);
|
|
1448
|
+
}
|
|
1449
|
+
</style>
|
|
1450
|
+
<div class="rli-mp">
|
|
1451
|
+
${MISSING_PARTS_ROWS.map(
|
|
1452
|
+
(row) => html`
|
|
1453
|
+
<div class="rli-mp-row">
|
|
1454
|
+
<div class="rli-mp-tag">${row.title}</div>
|
|
1455
|
+
<div class="rli-mp-item">
|
|
1456
|
+
${renderItem({
|
|
1457
|
+
...row.config,
|
|
1458
|
+
options: {
|
|
1459
|
+
...row.config.options,
|
|
1460
|
+
size: ReadoutListItemSize.medium,
|
|
1461
|
+
},
|
|
1462
|
+
})}
|
|
1463
|
+
</div>
|
|
1464
|
+
</div>
|
|
1465
|
+
`
|
|
1466
|
+
)}
|
|
1467
|
+
</div>
|
|
1468
|
+
`,
|
|
480
1469
|
};
|
|
481
1470
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
1471
|
+
/**
|
|
1472
|
+
* Layout / alignment stress test — many rows stacked in a fixed-width column,
|
|
1473
|
+
* deliberately combining as much of the API as can coexist in one aligned
|
|
1474
|
+
* column: degree / no-degree, short and long units (`C`, `Pa`, `m/s`, `miles`),
|
|
1475
|
+
* large/small/negative numbers, fractional values, a `null` (dash) value,
|
|
1476
|
+
* hinted zeros, `low-integrity` and `invalid` data quality, an `enhanced`
|
|
1477
|
+
* priority and a `bold` value weight, and a "kitchen-sink" row (Depth) with a
|
|
1478
|
+
* setpoint, advice and both leading + value icons.
|
|
1479
|
+
*
|
|
1480
|
+
* Each `obc-readout-list-item` is its own custom element, so cross-row column
|
|
1481
|
+
* alignment is **not** automatic — it is driven per-row by the width reservers.
|
|
1482
|
+
* The first column sets none, so the unit column and value right edge drift.
|
|
1483
|
+
* The second column gives every row the same reservers — the longest value
|
|
1484
|
+
* (`"0000.0"` via `options.value/setpoint/advice.spaceReserver`) and the longest
|
|
1485
|
+
* unit (`"miles"` via `options.unit.spaceReserver`) — plus reserves the degree
|
|
1486
|
+
* column (`hasDegreeSpacer`) on non-degree rows. So every block reserves the
|
|
1487
|
+
* same width regardless of each row's own value length / `fractionDigits`, and
|
|
1488
|
+
* the columns line up.
|
|
1489
|
+
*
|
|
1490
|
+
* (Source/stacking variations are exercised in the `LeadingSrc` / `LeadingUnit`
|
|
1491
|
+
* stories; mixing them here would move the unit out of the rightmost column.)
|
|
1492
|
+
*
|
|
1493
|
+
* The last two rows use `size=medium` / `size=large`. Their value digit edges do
|
|
1494
|
+
* NOT fully align with the small rows (~8px stagger): the `°` column scales with
|
|
1495
|
+
* the value size (6 / 8 / 12px), so a degree row's value edge sits `degree-width`
|
|
1496
|
+
* to the left of the unit. This is a genuine cross-size trade-off (#7) — for
|
|
1497
|
+
* degree rows of different sizes you can align the unit column (as here) OR the
|
|
1498
|
+
* value digit edges, not both. Aligning the value edges would need either a
|
|
1499
|
+
* constant degree reserve (which widens the smaller rows' `°` — visually
|
|
1500
|
+
* undesirable) or pinning the value edge and letting the unit column stagger;
|
|
1501
|
+
* left for the designer. Two rows also carry per-block (nested) state — a value
|
|
1502
|
+
* `invalid` chip and a value alert frame — to confirm per-block state does not
|
|
1503
|
+
* shift the value edge.
|
|
1504
|
+
*/
|
|
1505
|
+
type AlignmentRow = {
|
|
1506
|
+
label: string;
|
|
1507
|
+
value: number | null;
|
|
1508
|
+
unit: string;
|
|
1509
|
+
size?: ReadoutListItemSize;
|
|
1510
|
+
hasDegree?: boolean;
|
|
1511
|
+
fractionDigits?: number;
|
|
1512
|
+
priority?: ReadoutListItemPriority;
|
|
1513
|
+
weight?: ObcTextboxFontWeight;
|
|
1514
|
+
hintedZeros?: boolean;
|
|
1515
|
+
dataQuality?: ReadoutListItemDataQuality;
|
|
1516
|
+
hasSetpoint?: boolean;
|
|
1517
|
+
setpoint?: number;
|
|
1518
|
+
hasAdvice?: boolean;
|
|
1519
|
+
advice?: number;
|
|
1520
|
+
hasLeadingIcon?: boolean;
|
|
1521
|
+
hasValueIcon?: boolean;
|
|
1522
|
+
valueDataQuality?: ReadoutListItemDataQuality;
|
|
1523
|
+
valueAlert?: AlertFrameConfig;
|
|
1524
|
+
};
|
|
1525
|
+
|
|
1526
|
+
const ALIGNMENT_ROWS: AlignmentRow[] = [
|
|
1527
|
+
// degree + enhanced priority (accent value)
|
|
1528
|
+
{
|
|
1529
|
+
label: 'Temperature',
|
|
1530
|
+
value: 45,
|
|
1531
|
+
unit: 'C',
|
|
1532
|
+
hasDegree: true,
|
|
1533
|
+
priority: ReadoutListItemPriority.enhanced,
|
|
1534
|
+
},
|
|
1535
|
+
// degree + hinted zeros (small value padded to maxDigits in the aligned column)
|
|
1536
|
+
{label: 'Heading', value: 8, unit: 'T', hasDegree: true, hintedZeros: true},
|
|
1537
|
+
// advice + setpoint (no icons) — to check that advice/setpoint also
|
|
1538
|
+
// column-align across rows (they should: the value block is width-reserved,
|
|
1539
|
+
// so the blocks left of it sit at fixed offsets)
|
|
1540
|
+
{
|
|
1541
|
+
label: 'Pressure',
|
|
1542
|
+
value: 1013,
|
|
1543
|
+
unit: 'Pa',
|
|
1544
|
+
fractionDigits: 1,
|
|
1545
|
+
hasSetpoint: true,
|
|
1546
|
+
setpoint: 1015,
|
|
1547
|
+
hasAdvice: true,
|
|
1548
|
+
advice: 1008,
|
|
1549
|
+
},
|
|
1550
|
+
// negative value + fraction + low-integrity data quality
|
|
1551
|
+
{
|
|
1552
|
+
label: 'Flow speed',
|
|
1553
|
+
value: -12.5,
|
|
1554
|
+
unit: 'm/s',
|
|
1555
|
+
fractionDigits: 1,
|
|
1556
|
+
dataQuality: ReadoutListItemDataQuality.lowIntegrity,
|
|
1557
|
+
},
|
|
1558
|
+
// long unit + advice + setpoint (the "miles" row)
|
|
1559
|
+
{
|
|
1560
|
+
label: 'Distance',
|
|
1561
|
+
value: 4.2,
|
|
1562
|
+
unit: 'miles',
|
|
1563
|
+
fractionDigits: 1,
|
|
1564
|
+
hasSetpoint: true,
|
|
1565
|
+
setpoint: 4.5,
|
|
1566
|
+
hasAdvice: true,
|
|
1567
|
+
advice: 4,
|
|
1568
|
+
},
|
|
1569
|
+
// kitchen sink: value + setpoint + advice + leading icon + value icon
|
|
1570
|
+
{
|
|
1571
|
+
label: 'Depth',
|
|
1572
|
+
value: 1013.7,
|
|
1573
|
+
unit: 'm',
|
|
1574
|
+
fractionDigits: 1,
|
|
1575
|
+
hasSetpoint: true,
|
|
1576
|
+
setpoint: 1015,
|
|
1577
|
+
hasAdvice: true,
|
|
1578
|
+
advice: 1010,
|
|
1579
|
+
hasLeadingIcon: true,
|
|
1580
|
+
hasValueIcon: true,
|
|
1581
|
+
},
|
|
1582
|
+
// bold value weight — heavier weight only, no colour change (stays neutral)
|
|
1583
|
+
{
|
|
1584
|
+
label: 'Speed',
|
|
1585
|
+
value: 18,
|
|
1586
|
+
unit: 'kn',
|
|
1587
|
+
fractionDigits: 1,
|
|
1588
|
+
weight: ObcTextboxFontWeight.bold,
|
|
1589
|
+
},
|
|
1590
|
+
// null (dash) + invalid data quality
|
|
1591
|
+
{
|
|
1592
|
+
label: 'Wind',
|
|
1593
|
+
value: null,
|
|
1594
|
+
unit: 'kn',
|
|
1595
|
+
dataQuality: ReadoutListItemDataQuality.invalid,
|
|
1596
|
+
},
|
|
1597
|
+
// per-block (nested) value invalid — the chip uses outline, so the value edge
|
|
1598
|
+
// must stay aligned with the other rows
|
|
1599
|
+
{
|
|
1600
|
+
label: 'Battery',
|
|
1601
|
+
value: 11.8,
|
|
1602
|
+
unit: 'V',
|
|
1603
|
+
fractionDigits: 1,
|
|
1604
|
+
valueDataQuality: ReadoutListItemDataQuality.invalid,
|
|
1605
|
+
},
|
|
1606
|
+
// per-block (nested) value alert frame — must not shift the value edge
|
|
1607
|
+
{
|
|
1608
|
+
label: 'Fuel',
|
|
1609
|
+
value: 42,
|
|
1610
|
+
unit: '%',
|
|
1611
|
+
valueAlert: {
|
|
1612
|
+
status: AlertType.Warning,
|
|
1613
|
+
mode: ObcAlertFrameMode.unackedActive,
|
|
488
1614
|
type: ObcAlertFrameType.Regular,
|
|
489
1615
|
},
|
|
490
1616
|
},
|
|
1617
|
+
// size variants — value digit edges do NOT fully align cross-size (the degree
|
|
1618
|
+
// column scales 6/8/12px with the value size); see the ColumnAlignment doc.
|
|
1619
|
+
|
|
1620
|
+
{
|
|
1621
|
+
label: 'Course',
|
|
1622
|
+
value: 287,
|
|
1623
|
+
unit: 'T',
|
|
1624
|
+
hasDegree: true,
|
|
1625
|
+
size: ReadoutListItemSize.medium,
|
|
1626
|
+
},
|
|
1627
|
+
{
|
|
1628
|
+
label: 'COG',
|
|
1629
|
+
value: 92,
|
|
1630
|
+
unit: 'T',
|
|
1631
|
+
hasDegree: true,
|
|
1632
|
+
size: ReadoutListItemSize.large,
|
|
1633
|
+
},
|
|
1634
|
+
];
|
|
1635
|
+
|
|
1636
|
+
const LONGEST_UNIT = 'miles';
|
|
1637
|
+
const MAX_INTEGER_DIGITS = 4;
|
|
1638
|
+
// Longest value string in the column (4 integer digits + 1 fraction). Passed to
|
|
1639
|
+
// every row's value/setpoint/advice spaceReserver so they all reserve the same
|
|
1640
|
+
// width regardless of each row's own fractionDigits — like LONGEST_UNIT does for
|
|
1641
|
+
// the unit column.
|
|
1642
|
+
const VALUE_RESERVER = `${'0'.repeat(MAX_INTEGER_DIGITS)}.0`;
|
|
1643
|
+
|
|
1644
|
+
const alignmentStyle = `
|
|
1645
|
+
.rli-align-wrap { display: flex; flex-direction: column; gap: 24px; width: 100%; }
|
|
1646
|
+
.rli-align-section { display: flex; flex-direction: column; gap: 8px; }
|
|
1647
|
+
.rli-align-section-title {
|
|
1648
|
+
margin: 0; font: 12px/1.2 var(--global-typography-ui-label-font-family, inherit);
|
|
1649
|
+
text-transform: uppercase; letter-spacing: 0.06em; color: var(--element-neutral-color, #777);
|
|
1650
|
+
}
|
|
1651
|
+
.rli-align-col {
|
|
1652
|
+
display: flex; flex-direction: column; gap: 2px; width: 480px;
|
|
1653
|
+
padding: 8px; border: 1px dashed var(--border-divider-color, #ccc); border-radius: 8px;
|
|
1654
|
+
}
|
|
1655
|
+
/* Debug aid (temporary): outline each readout building block (red) and the
|
|
1656
|
+
degree column (blue) so reserver widths / alignment are visible. Scoped to
|
|
1657
|
+
this story only via .rli-align-wrap — safe to delete when no longer needed. */
|
|
1658
|
+
.rli-align-wrap obc-readout-list-item::part(block) {
|
|
1659
|
+
outline: 1px solid rgba(220, 0, 0, 0.7); outline-offset: -1px;
|
|
1660
|
+
}
|
|
1661
|
+
.rli-align-wrap obc-readout-list-item::part(degree) {
|
|
1662
|
+
outline: 1px solid rgba(0, 0, 220, 0.7); outline-offset: -1px;
|
|
1663
|
+
}
|
|
1664
|
+
.rli-align-wrap obc-readout-list-item::part(degree-spacer) {
|
|
1665
|
+
outline: 1px solid rgba(0, 160, 0, 0.8); outline-offset: -1px;
|
|
1666
|
+
}
|
|
1667
|
+
`;
|
|
1668
|
+
|
|
1669
|
+
function renderAlignmentColumn(aligned: boolean) {
|
|
1670
|
+
return html`
|
|
1671
|
+
<div class="rli-align-col">
|
|
1672
|
+
${ALIGNMENT_ROWS.map((row) =>
|
|
1673
|
+
renderItem({
|
|
1674
|
+
label: row.label,
|
|
1675
|
+
unit: row.unit,
|
|
1676
|
+
src: '',
|
|
1677
|
+
value: row.value,
|
|
1678
|
+
hasSetpoint: row.hasSetpoint,
|
|
1679
|
+
setpoint: row.setpoint,
|
|
1680
|
+
hasAdvice: row.hasAdvice,
|
|
1681
|
+
advice: row.advice,
|
|
1682
|
+
hasLeadingIcon: row.hasLeadingIcon,
|
|
1683
|
+
hasValueIcon: row.hasValueIcon,
|
|
1684
|
+
options: {
|
|
1685
|
+
size: row.size ?? ReadoutListItemSize.small,
|
|
1686
|
+
hasDegree: row.hasDegree ?? false,
|
|
1687
|
+
fractionDigits: row.fractionDigits ?? 0,
|
|
1688
|
+
priority: row.priority,
|
|
1689
|
+
dataQuality: row.dataQuality,
|
|
1690
|
+
hasLeadingIcon: row.hasLeadingIcon,
|
|
1691
|
+
value: {
|
|
1692
|
+
hasIcon: row.hasValueIcon,
|
|
1693
|
+
weight: row.weight,
|
|
1694
|
+
hintedZeros: row.hintedZeros,
|
|
1695
|
+
dataQuality: row.valueDataQuality,
|
|
1696
|
+
alert: row.valueAlert,
|
|
1697
|
+
// Same value reserver on every row -> uniform value-block width,
|
|
1698
|
+
// independent of each row's own fractionDigits.
|
|
1699
|
+
...(aligned ? {spaceReserver: VALUE_RESERVER} : {}),
|
|
1700
|
+
},
|
|
1701
|
+
...(aligned
|
|
1702
|
+
? {
|
|
1703
|
+
// Non-degree rows reserve the degree column so their digits
|
|
1704
|
+
// and units line up with the degree rows.
|
|
1705
|
+
hasDegreeSpacer: !(row.hasDegree ?? false),
|
|
1706
|
+
maxDigits: MAX_INTEGER_DIGITS,
|
|
1707
|
+
unit: {spaceReserver: LONGEST_UNIT},
|
|
1708
|
+
setpoint: {spaceReserver: VALUE_RESERVER},
|
|
1709
|
+
advice: {spaceReserver: VALUE_RESERVER},
|
|
1710
|
+
}
|
|
1711
|
+
: {}),
|
|
1712
|
+
},
|
|
1713
|
+
})
|
|
1714
|
+
)}
|
|
1715
|
+
</div>
|
|
1716
|
+
`;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
export const ColumnAlignment: Story = {
|
|
1720
|
+
render: () => html`
|
|
1721
|
+
<style>
|
|
1722
|
+
${alignmentStyle}
|
|
1723
|
+
</style>
|
|
1724
|
+
<div class="rli-align-wrap">
|
|
1725
|
+
<section class="rli-align-section">
|
|
1726
|
+
<h3 class="rli-align-section-title">
|
|
1727
|
+
Without reservers — columns drift
|
|
1728
|
+
</h3>
|
|
1729
|
+
${renderAlignmentColumn(false)}
|
|
1730
|
+
</section>
|
|
1731
|
+
<section class="rli-align-section">
|
|
1732
|
+
<h3 class="rli-align-section-title">
|
|
1733
|
+
With shared reservers — unit column & value right edge aligned
|
|
1734
|
+
</h3>
|
|
1735
|
+
${renderAlignmentColumn(true)}
|
|
1736
|
+
</section>
|
|
1737
|
+
</div>
|
|
1738
|
+
`,
|
|
491
1739
|
};
|