Skip to content
Open
Show file tree
Hide file tree
Changes from all 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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
:host {
display: block;
}

.frame {
box-sizing: border-box;
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
border-radius: 6px;
background: var(--container-section-color);
border: 1px solid var(--border-outline-color);
}

.frame.orientation-vertical {
flex-direction: column;
align-items: stretch;
}

.leading-container {
display: flex;
flex: 1 0 0;
align-items: center;
gap: 4px;
min-width: 0;
}

.icon {
flex-shrink: 0;
width: 24px;
height: 24px;
color: var(--on-indent-neutral-color);
}

.icon.no-icon {
display: none;
}

.label {
@mixin font-body;
flex: 1 0 0;
min-width: 0;
padding-left: 4px;
color: var(--element-inactive-color);
font-feature-settings: "ss04" 1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}

Comment on lines +40 to +51

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix Stylelint declaration-empty-line-before violations.

Static analysis flags missing blank lines after @mixin calls at lines 42, 54, and 70.

🎨 Proposed fix
 .label {
   `@mixin` font-body;
+
   flex: 1 0 0;
   min-width: 0;
   padding-left: 4px;
   color: var(--element-inactive-color);
   font-feature-settings: "ss04" 1;
   overflow: hidden;
   white-space: nowrap;
   text-overflow: ellipsis;
 }

 .frame.has-alert .label {
   `@mixin` font-body-active;
+
   color: var(--on-indent-active-color);
 }

 .empty {
   `@mixin` font-label;
+
   color: var(--element-inactive-color);
   white-space: nowrap;
   font-feature-settings: "ss04" 1;
 }

Also applies to: 52-55, 68-73

🧰 Tools
🪛 Stylelint (17.14.0)

[error] 42-42: Expected empty line before declaration (declaration-empty-line-before)

(declaration-empty-line-before)

🤖 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/components/alert-subsystem-counter/alert-subsystem-counter.css`
around lines 40 - 51, The CSS in the alert-subsystem-counter styles is missing
the required blank line after each `@mixin`, triggering stylelint
declaration-empty-line-before violations. Update the affected selectors in
alert-subsystem-counter.css, including .label and the other matching blocks in
the referenced range, so there is an empty line immediately after each `@mixin`
font-body (and any similar mixin usage) before the next declaration.

Source: Linters/SAST tools

.frame.has-alert .label {
@mixin font-body-active;
color: var(--on-indent-active-color);
}

.trailing-container {
display: flex;
flex-shrink: 0;
align-items: center;
gap: 4px;
}

.frame.orientation-horizontal .trailing-container {
justify-content: flex-end;
}

.empty {
@mixin font-label;
color: var(--element-inactive-color);
white-space: nowrap;
font-feature-settings: "ss04" 1;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import type {Meta, StoryObj} from '@storybook/web-components-vite';
import {html} from 'lit';
import {
ObcAlertSubsystemCounter,
ObcAlertSubsystemCounterOrientation,
} from './alert-subsystem-counter.js';
import './alert-subsystem-counter.js';
import '../badge/badge.js';
import '../../icons/icon-placeholder.js';

const badges = html`
<obc-badge slot="badges" size="large" type="alarm" .number=${9}></obc-badge>
<obc-badge slot="badges" size="large" type="warning" .number=${4}></obc-badge>
<obc-badge slot="badges" size="large" type="caution" .number=${2}></obc-badge>
`;

const meta: Meta<typeof ObcAlertSubsystemCounter> = {
title: 'Application Components/Alerts/Alert Subsystem Counter',
tags: ['autodocs', '6.0'],
component: 'obc-alert-subsystem-counter',
args: {
label: 'Label',
orientation: ObcAlertSubsystemCounterOrientation.Horizontal,
hasAlert: true,
emptyText: 'No alerts',
},
argTypes: {
orientation: {
control: {type: 'inline-radio'},
options: Object.values(ObcAlertSubsystemCounterOrientation),
},
hasAlert: {control: {type: 'boolean'}},
label: {control: {type: 'text'}},
emptyText: {control: {type: 'text'}},
},
render: (args) =>
html`<div style="width:191px">
<obc-alert-subsystem-counter
.label=${args.label}
.orientation=${args.orientation}
.hasAlert=${args.hasAlert}
.emptyText=${args.emptyText}
>
<obi-placeholder slot="icon"></obi-placeholder>
${badges}
</obc-alert-subsystem-counter>
</div>`,
} satisfies Meta<ObcAlertSubsystemCounter>;

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

export const Default: Story = {};

export const Abbreviation: Story = {
args: {label: 'ABC'},
};

export const NoAlerts: Story = {
args: {hasAlert: false},
};

export const Vertical: Story = {
args: {orientation: ObcAlertSubsystemCounterOrientation.Vertical},
};

export const VerticalNoAlerts: Story = {
args: {
orientation: ObcAlertSubsystemCounterOrientation.Vertical,
hasAlert: false,
},
};

export const AbbreviationVertical: Story = {
args: {
label: 'ABC',
orientation: ObcAlertSubsystemCounterOrientation.Vertical,
},
render: (args) =>
html`<div style="width:fit-content">
<obc-alert-subsystem-counter
.label=${args.label}
.orientation=${args.orientation}
.hasAlert=${args.hasAlert}
.emptyText=${args.emptyText}
>
<obi-placeholder slot="icon"></obi-placeholder>
${badges}
</obc-alert-subsystem-counter>
</div>`,
};

export const WithoutIcon: Story = {
render: (args) =>
html`<div style="width:191px">
<obc-alert-subsystem-counter
.label=${args.label}
.orientation=${args.orientation}
.hasAlert=${args.hasAlert}
.emptyText=${args.emptyText}
>
${badges}
</obc-alert-subsystem-counter>
</div>`,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import {LitElement, html, unsafeCSS} from 'lit';
import {property, state} from 'lit/decorators.js';
import {classMap} from 'lit/directives/class-map.js';
import componentStyle from './alert-subsystem-counter.css?inline';
import {customElement} from '../../decorator.js';

/**
* `ObcAlertSubsystemCounterOrientation` – Layout direction for the counter.
*
* - `horizontal`: Icon, label, and badges sit in a single row.
* - `vertical`: Badges (or empty text) sit on their own line below the label.
*/
export enum ObcAlertSubsystemCounterOrientation {
Horizontal = 'horizontal',
Vertical = 'vertical',
}

/**
* `<obc-alert-subsystem-counter>` – A framed summary of a subsystem's alert
* counts: a leading icon, a label, and a trailing set of slotted count badges.
*
* When there are no active alerts it shows a muted label and an empty-state
* message instead of the badges. The count badges themselves are supplied by
* the consumer as slotted `obc-badge` elements; this component provides only
* the frame, layout, label, and empty state.
*
* ---
*
* ### Features
* - **Orientation:** `horizontal` (single row) or `vertical` (badges on a line
* below the label).
* - **Alert state:** `hasAlert` toggles between an active (bold label, badges)
* and an inactive (muted label, empty text) presentation.
* - **Freeform label:** Pass a full title or an abbreviation — the label is
* plain text decided by the caller.
* - **Configurable empty text:** Customize the message shown when there are no
* alerts via `emptyText`.
* - **Optional leading icon:** Provide an icon through the `icon` slot; omit it
* and the leading box collapses with no empty gap.
*
* ---
*
* ### Usage Guidelines
* - Use this component to give an at-a-glance count of outstanding alerts for a
* single subsystem, grouped by severity.
* - Provide one slotted badge per severity in the `badges` slot, typically
* `<obc-badge size="large">` with a severity `type` and a `number`.
* - Set `hasAlert` to `false` to present the resolved/clear state; the badges
* are then hidden and `emptyText` is shown.
* - Choose `vertical` orientation in narrow containers where the badges do not
* fit beside the label.
*
* ---
*
* ### Slots
*
* | Slot Name | Renders When... | Purpose |
* |-----------|------------------------|----------------------------------------------------|
* | `icon` | The slot has content | Leading 24px icon (e.g. `<obi-placeholder>`). |
* | `badges` | `hasAlert` is true | Count badges, one per severity (`<obc-badge>`). |
*
* ---
*
* ### Example
*
* ```html
* <obc-alert-subsystem-counter label="Label" hasAlert>
* <obi-placeholder slot="icon"></obi-placeholder>
* <obc-badge slot="badges" size="large" type="alarm" .number=${9}></obc-badge>
* <obc-badge slot="badges" size="large" type="warning" .number=${4}></obc-badge>
* <obc-badge slot="badges" size="large" type="caution" .number=${2}></obc-badge>
* </obc-alert-subsystem-counter>
* ```
*
* @slot icon - Optional leading 24px icon.
* @slot badges - Count badges shown when `hasAlert` is true.
*/
@customElement('obc-alert-subsystem-counter')
export class ObcAlertSubsystemCounter extends LitElement {
/**
* The subsystem label. Pass a full title or an abbreviation as plain text.
*/
@property({type: String}) label = '';

/**
* Layout direction.
*
* - `horizontal` (default): icon, label, and badges in one row.
* - `vertical`: badges (or empty text) on a line below the label.
*/
@property({type: String}) orientation: ObcAlertSubsystemCounterOrientation =
ObcAlertSubsystemCounterOrientation.Horizontal;

/**
* Whether the subsystem has active alerts.
*
* When `true`, the label is shown active (bold) and the `badges` slot is
* rendered. When `false`, the label is muted and `emptyText` is shown
* instead of the badges.
*/
@property({type: Boolean}) hasAlert = false;

/**
* Text shown in the trailing area when `hasAlert` is `false`.
*/
@property({type: String}) emptyText = 'No alerts';

@state() private hasIcon = false;

private handleIconSlotChange(e: Event) {
const slot = e.target as HTMLSlotElement;
this.hasIcon = slot.assignedNodes({flatten: true}).length > 0;
}

override render() {
return html`
<div
class=${classMap({
frame: true,
['orientation-' + this.orientation]: true,
'has-alert': this.hasAlert,
})}
>
<div class="leading-container">
<div class=${classMap({icon: true, 'no-icon': !this.hasIcon})}>
<slot name="icon" @slotchange=${this.handleIconSlotChange}></slot>
</div>
<div class="label">${this.label}</div>
</div>
<div class="trailing-container">
${this.hasAlert
? html`<slot name="badges" class="badges"></slot>`
: html`<span class="empty">${this.emptyText}</span>`}
</div>
</div>
`;
}

static override styles = unsafeCSS(componentStyle);
}

declare global {
interface HTMLElementTagNameMap {
'obc-alert-subsystem-counter': ObcAlertSubsystemCounter;
}
}
Loading