Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
* {
box-sizing: border-box;
}

:host {
display: inline-block;
}

.button {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: fit-content;
padding: 0;
appearance: none;
background: none;
border: none;
cursor: pointer;

@mixin style style=normal className=value visibleWrapperClass=.visible-wrapper;
}

.visible-wrapper {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 4px;
border: 1px solid var(--normal-enabled-border-color);
border-radius: 4px;
}

.value-container {
display: inline-flex;
align-items: center;
gap: 2px;
padding: 0 2px;
}

.icon,
.advice {
display: inline-flex;
align-items: center;
justify-content: center;
}

.value {
color: var(--element-neutral-color);
text-align: right;
}

.unit {
@mixin font-instrument-unit;
color: var(--element-neutral-color);
}

.size-small .icon ::slotted(*),
.size-small .advice ::slotted(*) {
width: 12px;
height: 12px;
}

.size-regular .icon ::slotted(*),
.size-regular .advice ::slotted(*) {
width: 16px;
height: 16px;
}

.size-medium .icon ::slotted(*),
.size-medium .advice ::slotted(*) {
width: 20px;
height: 20px;
}

.size-large .icon ::slotted(*),
.size-large .advice ::slotted(*) {
width: 24px;
height: 24px;
}

.size-small .value {
@mixin font-instrument-value-small-neutral;
}

.size-regular .value {
@mixin font-instrument-value-regular-neutral;
}

.size-medium .value {
@mixin font-instrument-value-m-neutral;
}

.size-large .value {
@mixin font-instrument-value-enhanced-neutral;
}

.size-medium .visible-wrapper:not(.tag),
.size-large .visible-wrapper:not(.tag) {
padding: 4px 8px;
border-radius: 6px;
}

.visible-wrapper.tag {
min-height: 24px;
padding: 2px 8px;
border-radius: 100px;
background-color: var(--container-section-color);
}

.label {
@mixin font-instrument-value-regular-neutral;
color: var(--element-neutral-color);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import type {Meta, StoryObj} from '@storybook/web-components-vite';
import {html} from 'lit';
import {ObcTransmitterButton} from './transmitter-button.js';
import './transmitter-button.js';
import {
TransmitterButtonSize,
TransmitterButtonVariant,
} from './transmitter-button.js';
import '../../icons/icon-temperature-air.js';
import '../../icons/icon-placeholder.js';

const meta: Meta<typeof ObcTransmitterButton> = {
title: 'Automation/Transmitter/Transmitter Button',
tags: ['autodocs'],
component: 'obc-transmitter-button',
args: {
variant: TransmitterButtonVariant.value,
size: TransmitterButtonSize.regular,
value: 12.3,
unit: '°C',
fractionDigits: 1,
hasIcon: false,
hasAdvice: false,
label: 'TT',
},
argTypes: {
variant: {
options: [TransmitterButtonVariant.value, TransmitterButtonVariant.tag],
control: {type: 'radio'},
},
size: {
options: [
TransmitterButtonSize.small,
TransmitterButtonSize.regular,
TransmitterButtonSize.medium,
TransmitterButtonSize.large,
],
control: {type: 'radio'},
},
value: {control: {type: 'range', min: -99, max: 999, step: 0.1}},
},
} satisfies Meta<ObcTransmitterButton>;

export default meta;
type Story = StoryObj<ObcTransmitterButton>;

function renderComponent(args: ObcTransmitterButton) {
return html`
<obc-transmitter-button
.variant=${args.variant}
.size=${args.size}
.value=${args.value}
.unit=${args.unit}
.fractionDigits=${args.fractionDigits}
.hasIcon=${args.hasIcon}
.hasAdvice=${args.hasAdvice}
.label=${args.label}
>
${args.hasIcon
? html`<obi-temperature-air slot="icon"></obi-temperature-air>`
: ''}
${args.hasAdvice
? html`<obi-placeholder slot="advice"></obi-placeholder>`
: ''}
</obc-transmitter-button>
`;
}

export const Default: Story = {
render: (args) => renderComponent(args as ObcTransmitterButton),
};

export const WithIcon: Story = {
args: {hasIcon: true},
render: (args) => renderComponent(args as ObcTransmitterButton),
};

export const WithAdvice: Story = {
args: {hasIcon: true, hasAdvice: true},
render: (args) => renderComponent(args as ObcTransmitterButton),
};

export const Small: Story = {
args: {size: TransmitterButtonSize.small, hasIcon: true},
render: (args) => renderComponent(args as ObcTransmitterButton),
};

export const Regular: Story = {
args: {size: TransmitterButtonSize.regular, hasIcon: true},
render: (args) => renderComponent(args as ObcTransmitterButton),
};

export const Medium: Story = {
args: {size: TransmitterButtonSize.medium, hasIcon: true},
render: (args) => renderComponent(args as ObcTransmitterButton),
};

export const Large: Story = {
args: {size: TransmitterButtonSize.large, hasIcon: true},
render: (args) => renderComponent(args as ObcTransmitterButton),
};

export const Tag: Story = {
args: {variant: TransmitterButtonVariant.tag, label: 'TT'},
render: (args) => renderComponent(args as ObcTransmitterButton),
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import {LitElement, html, nothing, unsafeCSS} from 'lit';
import {property} from 'lit/decorators.js';
import {classMap} from 'lit/directives/class-map.js';
import componentStyle from './transmitter-button.css?inline';
import {customElement} from '../../decorator.js';

export enum TransmitterButtonVariant {
value = 'value',
tag = 'tag',
}

export enum TransmitterButtonSize {
small = 'small',
regular = 'regular',
medium = 'medium',
large = 'large',
}

/**
* `<obc-transmitter-button>` – The pressable readout chip used as the core of a
* transmitter on a process diagram.
*
* The value content (leading icon, value, unit, optional advice) is laid out
* inline, mirroring the structure of `<obc-automation-readout>`. The leading
* icon and the advice segment are slotted so the consumer provides the
* type-specific content rather than it being hard-coded here.
*
* ### Features / Variants
* - **`value`** – white, bordered box showing an icon, value and unit. Opt into
* an advice segment with `hasAdvice`.
* - **`tag`** – a static rounded pill showing a short identifier (e.g. `TT`)
* from the `label` property, with no live value.
* - **`size`** – `small`, `regular`, `medium` or `large`, scaling the value text
* and the icon/advice glyphs. The unit stays at a fixed size across all sizes.
*
* ### Usage Guidelines
* Use as a building block for `<obc-transmitter>`; it is the part that carries
* the measured value or the tag identifier. Slot in the type-specific icon and,
* when needed, the advice content rather than hard-coding them.
*
* ### Slots
* | Slot Name | Conditions | Purpose |
* |-----------|---------------------------------|----------------------------------------|
* | icon | `value` variant and `hasIcon` | Leading icon beside the value. |
* | advice | `value` variant and `hasAdvice` | Advice segment shown before the value. |
*/
@customElement('obc-transmitter-button')
export class ObcTransmitterButton extends LitElement {
@property({type: String}) variant: TransmitterButtonVariant =
TransmitterButtonVariant.value;
@property({type: String}) size: TransmitterButtonSize =
TransmitterButtonSize.regular;
@property({type: Number}) value?: number;
@property({type: String}) unit = '';
@property({type: Number}) fractionDigits = 1;
@property({type: Boolean}) hasIcon = false;
@property({type: Boolean}) hasAdvice = false;

/** Short tag identifier shown in the `tag` variant (e.g. `TT`). */
@property({type: String}) label = '';

private get isTag() {
return this.variant === TransmitterButtonVariant.tag;
}

private get formattedValue() {
if (this.value === undefined || Number.isNaN(this.value)) {
return '--';
}
return this.value.toFixed(this.fractionDigits);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Clamp fractionDigits before calling toFixed().

fractionDigits is a public property here and is also forwarded unchanged by <obc-transmitter>, so values like -1, 1.5, or 101 will throw a RangeError during render instead of falling back gracefully.

Suggested fix
   private get formattedValue() {
     if (this.value === undefined || Number.isNaN(this.value)) {
       return '--';
     }
-    return this.value.toFixed(this.fractionDigits);
+    const digits =
+      Number.isFinite(this.fractionDigits) &&
+      this.fractionDigits >= 0 &&
+      this.fractionDigits <= 100
+        ? Math.trunc(this.fractionDigits)
+        : 1;
+    return this.value.toFixed(digits);
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private get formattedValue() {
if (this.value === undefined || Number.isNaN(this.value)) {
return '--';
}
return this.value.toFixed(this.fractionDigits);
private get formattedValue() {
if (this.value === undefined || Number.isNaN(this.value)) {
return '--';
}
const digits =
Number.isFinite(this.fractionDigits) &&
this.fractionDigits >= 0 &&
this.fractionDigits <= 100
? Math.trunc(this.fractionDigits)
: 1;
return this.value.toFixed(digits);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/openbridge-webcomponents/src/automation/transmitter-button/transmitter-button.ts`
around lines 66 - 70, The formattedValue getter in transmitter-button currently
passes fractionDigits straight into toFixed(), so invalid public values can
throw during render. Clamp or normalize fractionDigits inside formattedValue
before formatting, ensuring non-integer, negative, or overly large values fall
back to a safe range. Keep the existing undefined/NaN guard and apply the
bounded digit count only when calling toFixed().

}

private renderContent() {
if (this.isTag) {
return html`<span class="label">${this.label}</span>`;
}

return html`
${this.hasAdvice
? html`<div class="advice"><slot name="advice"></slot></div>`
: nothing}
<div class="value-container">
${this.hasIcon
? html`<div class="icon"><slot name="icon"></slot></div>`
: nothing}
<span class="value">${this.formattedValue}</span>
${this.unit ? html`<span class="unit">${this.unit}</span>` : nothing}
</div>
`;
}

override render() {
return html`
<button class="button ${this.variant} size-${this.size}">
<div
part="button"
class=${classMap({
'visible-wrapper': true,
tag: this.isTag,
})}
>
${this.renderContent()}
</div>
</button>
`;
}

static override styles = unsafeCSS(componentStyle);
}

declare global {
interface HTMLElementTagNameMap {
'obc-transmitter-button': ObcTransmitterButton;
}
}
Loading
Loading