An open API service indexing awesome lists of open source software.

https://github.com/jean-merelis/angular-components


https://github.com/jean-merelis/angular-components

Last synced: about 2 months ago
JSON representation

Awesome Lists containing this project

README

        

# Merelis Angular Components

A library of reusable Angular components and utilities that provides high-quality UI elements for your applications.

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

## Showcase
https://jean-merelis.github.io/angular-components/

## Installation

```bash
npm install @merelis/angular --save
```

## Available Components

Currently, the library provides the following components:

### MerSelect

An advanced select component with filtering and typeahead capabilities. Supports single or multiple selection, full customization, reactive forms integration, and conditional rendering.

### MerProgressBar

A progress bar component that can be used independently or integrated with other components.

## Installation and Usage

### Initial Setup

After installing the package, you need to import the necessary styles in your application's `styles.scss` file:

```scss
@use '@angular/cdk/overlay-prebuilt.css';
@use '@merelis/angular/select/styles';
```
This will import both the CDK overlay styles (required for the dropdown functionality) and the component-specific styles.

Since these are standalone components, you can import them directly in your components:

#### Direct import in a component

```typescript
import { Component } from '@angular/core';
import { MerSelect } from '@merelis/angular/select';
import { MerProgressBar } from '@merelis/angular/progress-bar';

@Component({
selector: 'app-example',
standalone: true,
imports: [
MerSelect,
MerProgressBar
],
template: `


`
})
export class ExampleComponent {
// ...
}
```

### MerSelect

The `MerSelect` offers a robust alternative to the native HTML select, with additional features like filtering and typeahead.

#### Basic HTML

```html

```

#### Input Properties

| Name | Type | Default | Description |
|------|------|---------|-------------|
| dataSource | Array\ | SelectDataSource\ | undefined | List of available options for selection or data source for the component |
| value | T \| T[] \| null | undefined | Currently selected value |
| loading | boolean | false | Displays loading indicator using MerProgressBar |
| disabled | boolean | false | Disables the component |
| readOnly | boolean | false | Sets the component as read-only |
| disableSearch | boolean | false | Disables text search functionality |
| disableOpeningWhenFocusedByKeyboard | boolean | false | Prevents the panel from opening automatically when focused via keyboard |
| multiple | boolean | false | Allows multiple selection |
| canClear | boolean | true | Allows clearing the selection |
| alwaysIncludesSelected | boolean | false | Always includes the selected item in the dropdown, even if it doesn't match the filter. **Note: Only effective when using an array as dataSource, not when using a custom SelectDataSource.** |
| autoActiveFirstOption | boolean | true | Automatically activates the first option when the panel is opened |
| debounceTime | number | 100 | Debounce time for text input (in ms) |
| panelOffsetY | number | 0 | Vertical offset of the options panel |
| compareWith | Comparable\ | undefined | Function to compare values |
| displayWith | DisplayWith\ | undefined | Function to display values as text |
| filterPredicate | FilterPredicate\ | undefined | Function to filter options based on typed text. **Note: Only effective when using an array as dataSource, not when using a custom SelectDataSource.** |
| disableOptionPredicate | OptionPredicate\ | () => false | Function to determine which options should be disabled |
| disabledOptions | T[] | [] | List of options that should be disabled |
| connectedTo | MerSelectPanelOrigin | undefined | Element to which the panel should connect |
| panelClass | string \| string[] | undefined | CSS class(es) applied to the options panel |
| panelWidth | string \| number | undefined | Width of the options panel |
| position | 'auto' \| 'above' \| 'below' | 'auto' | Position of the panel relative to the input |
| placeholder | string | undefined | Text to display when no item is selected |

#### Output Events

| Name | Description |
|------|-------------|
| opened | Emitted when the options panel is opened |
| closed | Emitted when the options panel is closed |
| focus | Emitted when the component receives focus |
| blur | Emitted when the component loses focus |
| inputChanges | Emitted when the text input value changes |

#### Complete Example

```typescript
import { Component } from '@angular/core';

interface User {
id: number;
name: string;
}

@Component({
selector: 'app-example',
template: `


`
})
export class ExampleComponent {
users: User[] = [
{ id: 1, name: 'John Smith' },
{ id: 2, name: 'Mary Johnson' },
{ id: 3, name: 'Peter Williams' }
];
selectedUser: User | null = null;
isLoading = false;

displayUserName(user: User): string {
return user.name;
}

compareUsers(user1: User, user2: User): boolean {
return user1?.id === user2?.id;
}

onPanelOpened(): void {
console.log('Options panel opened');
}

onPanelClosed(): void {
console.log('Options panel closed');
}

onInputChanged(text: string): void {
console.log('Search text:', text);
}
}
```

## Filtering Behavior and DataSource

The `MerSelect` supports two operation modes for data filtering:

### 1. Automatic Filtering (Array as dataSource)

When you provide an array as `dataSource`, the component performs automatic filtering based on the typed text. In this case, the following inputs control the filtering behavior:

| Name | Type | Description |
|------|------|-------------|
| filterPredicate | FilterPredicate\ | Custom function to filter options based on typed text. **Only applied when the dataSource is an array, not a custom SelectDataSource.** |
| alwaysIncludesSelected | boolean | When true, always includes the selected item(s) in the dropdown, even if they don't match the filter. **Only applied when the dataSource is an array, not a custom SelectDataSource.** |

### 2. Custom Filtering (SelectDataSource)

When you implement and provide a custom `SelectDataSource`, the filtering behavior is determined by the implementation of the dataSource's `applyFilter` method. In this case:

- The component invokes the `applyFilter` method when the user types
- The `filterPredicate` and `alwaysIncludesSelected` inputs are ignored
- The filtering logic is entirely controlled by the dataSource

```typescript
export class CustomDataSource implements SelectDataSource {
// ...

async applyFilter(criteria: FilterCriteria): void | Promise {
// Here you implement your own filtering logic
// The criteria parameter contains:
// - searchText: the text typed by the user
// - selected: the currently selected item(s)

// You can decide to include selected items even if they don't match the filter
// (equivalent to the alwaysIncludesSelected behavior)

// You can also implement your own filtering logic
// (equivalent to the filterPredicate behavior)
}
}
```

### Choosing Between Array and SelectDataSource

- **Use a simple array** when you have a small set of static data that doesn't require server-side filtering.
- **Implement a SelectDataSource** when you need complete control over filtering, especially for:
- Fetching data from the server based on typed text (typeahead)
- Handling large datasets
- Implementing complex filtering logic
- Showing loading indicators during asynchronous operations

## Typeahead Functionality with TypeaheadDataSource

The `MerSelect` supports typeahead functionality, allowing you to search for options as you type. The library provides a generic `TypeaheadDataSource` implementation that handles common typeahead requirements including search request cancellation, loading states, and result management.

### TypeaheadSearchService Interface

First, implement the `TypeaheadSearchService` interface to define how search operations will be performed:

```typescript
export interface TypeaheadSearchService {
/**
* Search method that takes a query string and returns an Observable of results
* @param query The search query string
* @returns Observable of search results
*/
search(query: string): Observable;
}
```

### TypeaheadDataSourceOptions Interface

The `TypeaheadDataSource` now accepts a configuration options object:

```typescript
export interface TypeaheadDataSourceOptions {
/**
* Whether to always include selected items in the results. Default false.
*/
alwaysIncludeSelected?: boolean;

/**
* Whether to suppress loading events. Default false.
*/
suppressLoadingEvents?: boolean;

/**
* Custom function to compare items for equality (defaults to comparing by reference)
* @param a First item to compare
* @param b Second item to compare
*/
compareWith?: (a: T, b: T) => boolean;
}
```

### Using the TypeaheadDataSource

The `TypeaheadDataSource` provides a robust solution for typeahead functionality with automatic cancellation of previous requests, which is essential for a smooth user experience.

#### Implementation

```typescript
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { delay } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';
import { TypeaheadSearchService, TypeaheadDataSource, TypeaheadDataSourceOptions } from '@merelis/angular/select';

// Define your data model
interface User {
id: number;
name: string;
email: string;
}

// Implement TypeaheadSearchService for your data type
@Injectable({ providedIn: 'root' })
export class UserSearchService implements TypeaheadSearchService {
constructor(private http: HttpClient) {}

search(query: string): Observable {
// Real implementation would use HttpClient
return this.http.get(`/api/users?q=${query}`);

// Example of a mock implementation for testing:
/*
const users = [
{ id: 1, name: 'John Smith', email: '[email protected]' },
{ id: 2, name: 'Mary Johnson', email: '[email protected]' },
{ id: 3, name: 'Peter Williams', email: '[email protected]' }
];

const results = query
? users.filter(user => user.name.toLowerCase().includes(query.toLowerCase()))
: users;

return of(results).pipe(delay(300)); // Simulate network delay
*/
}
}
```

#### Usage in a Component

```typescript
import { Component, OnDestroy } from '@angular/core';
import { MerSelect } from '@merelis/angular/select';
import { TypeaheadDataSource, TypeaheadDataSourceOptions } from '@merelis/angular/select';
import { UserSearchService, User } from './user-search.service';

@Component({
selector: 'app-user-search',
standalone: true,
imports: [MerSelect],
template: `


Selected: {{selectedUser.name}}


`
})
export class UserSearchComponent implements OnDestroy {
selectedUser: User | null = null;
userDataSource: TypeaheadDataSource;

constructor(private userSearchService: UserSearchService) {
// Define options for the data source
const options: TypeaheadDataSourceOptions = {
alwaysIncludeSelected: true,
compareWith: (a, b) => a.id === b.id,
suppressLoadingEvents: false
};

// Create the data source with the service and options
this.userDataSource = new TypeaheadDataSource(
userSearchService,
options
);
}

ngOnDestroy(): void {
// Cleanup resources
this.userDataSource.disconnect();
}

// Display function for the select component
displayUserName(user: User): string {
return user?.name || '';
}
}
```

### TypeaheadDataSource API

The `TypeaheadDataSource` constructor now accepts the following parameters:

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| searchService | TypeaheadSearchService | Yes | The service implementing the search functionality |
| options | TypeaheadDataSourceOptions | No | Configuration options object |

#### TypeaheadDataSourceOptions Properties

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| alwaysIncludeSelected | boolean | false | Whether to always include selected items in the results even if they don't match the search criteria |
| suppressLoadingEvents | boolean | false | Whether to suppress loading event emissions |
| compareWith | (a: T, b: T) => boolean | (a, b) => a === b | Custom function to determine equality between items |

### How It Works

1. **Efficient Request Handling**: When the user types in the search input, previous in-flight requests are automatically cancelled using RxJS `switchMap`, ensuring only the most recent search query is processed.

2. **Loading State Management**: The data source emits loading states that the `MerSelect` can display as a progress indicator. This can be suppressed using the `suppressLoadingEvents` option.

3. **Selected Items Preservation**: When `alwaysIncludeSelected` is true, selected items will always appear in the dropdown results even if they don't match the current search query.

4. **Error Handling**: If the search service encounters an error, the data source will handle it gracefully, preventing the component from breaking and falling back to an empty result set.

### Benefits of Using TypeaheadDataSource

1. **Performance**: Efficiently handles rapid typing by cancelling outdated requests
2. **User Experience**: Shows loading indicators at appropriate times
3. **Resilience**: Provides graceful error handling
4. **Flexibility**: Works with any data type and search implementation
5. **Integration**: Seamlessly works with MerSelect's search capabilities

The `TypeaheadDataSource` implementation follows best practices for reactive programming with RxJS and works with both simple and complex typeahead scenarios.

---

### MerProgressBar

The `MerProgressBar` component displays a visual progress bar, useful for indicating the progress of operations.

#### Basic HTML

```html

```

#### Input Properties

| Name | Type | Default | Description |
|------|------|---------|-------------|
| value | number | 0 | Current progress value (between 0 and 1) |
| indeterminate | boolean | false | Determines if the progress bar should show an indeterminate progress indicator |

#### Usage Example

```typescript
import { Component } from '@angular/core';

@Component({
selector: 'app-progress-example',
template: `



Start Download
`
})
export class ProgressExampleComponent {
downloadProgress = 0;
downloadInProgress = false;

startDownload() {
this.downloadInProgress = true;

// Simulating a download
const interval = setInterval(() => {
this.downloadProgress += 0.1;

if (this.downloadProgress >= 1) {
clearInterval(interval);
this.downloadInProgress = false;
this.downloadProgress = 1;
}
}, 500);
}
}
```

---

## Custom Templates

The `MerSelect` allows customization of the trigger (clickable area) and options through templates.

### Custom Trigger Template

```html




{{ selectedUser?.name }}

```

### Custom Option Template

```html






```

---

## Testing with Component Harnesses

The library provides testing harnesses for the `MerSelect` and its options, making it easier to test components that use these elements. These harnesses are built on top of Angular's Component Test Harnesses (CDK Testing) and provide a clean, implementation-detail-free way to interact with components in tests.

### Installation

The testing harnesses are included in the package and can be imported from:

```typescript
import { MerSelectHarness } from '@merelis/angular/select/testing';
import { MerSelectOptionHarness } from '@merelis/angular/select/testing';
```

### Setting Up Component Test Harnesses

To use the harnesses in your tests, you'll need to set up the Angular test environment with the harness environment:

```typescript
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MerSelectHarness } from '@merelis/angular/select/testing';

describe('YourComponent', () => {
let fixture: ComponentFixture;
let component: YourComponent;
let loader: HarnessLoader;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [YourComponent],
// Include other necessary imports here
}).compileComponents();

fixture = TestBed.createComponent(YourComponent);
component = fixture.componentInstance;
loader = TestbedHarnessEnvironment.loader(fixture);
});

// Tests go here
});
```

### MerSelectHarness API

The `MerSelectHarness` provides methods to interact with and query the state of a `MerSelect`:

| Method | Description |
|--------|-------------|
| `static with(filters: MerSelectHarnessFilters)` | Gets a `HarnessPredicate` that can be used to find a select with specific attributes |
| `click()` | Clicks on the select trigger to open/close the panel |
| `clickOnClearIcon()` | Clicks on the clear icon to clear the selection |
| `focus()` | Focuses the select input |
| `blur()` | Removes focus from the select input |
| `isFocused()` | Gets whether the select is focused |
| `getValue()` | Gets the text value displayed in the select trigger |
| `isDisabled()` | Gets whether the select is disabled |
| `getSearchText()` | Gets the current text in the search input |
| `setTextSearch(value: string)` | Sets the text in the search input |
| `isOpen()` | Gets whether the options panel is open |
| `getOptions(filters?: Omit)` | Gets the options inside the panel |
| `clickOptions(filters: SelectOptionHarnessFilters)` | Clicks the option(s) matching the given filters |

### MerSelectOptionHarness API

The `MerSelectOptionHarness` provides methods to interact with and query the state of a select option:

| Method | Description |
|--------|-------------|
| `static with(filters: SelectOptionHarnessFilters)` | Gets a `HarnessPredicate` that can be used to find an option with specific attributes |
| `click()` | Clicks the option |
| `getText()` | Gets the text of the option |
| `isDisabled()` | Gets whether the option is disabled |
| `isSelected()` | Gets whether the option is selected |
| `isActive()` | Gets whether the option is active |
| `isMultiple()` | Gets whether the option is in multiple selection mode |

### Example Test

Here's an example of testing a component that uses `MerSelect`:

```typescript
import { Component } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { HarnessLoader } from '@angular/cdk/testing';
import { MerSelectHarness, MerSelectOptionHarness } from '@merelis/angular/select/testing';
import { MerSelect } from '@merelis/angular/select';

@Component({
template: `


`,
standalone: true,
imports: [MerSelect]
})
class TestComponent {
fruits = ['Apple', 'Banana', 'Orange', 'Strawberry'];
selectedFruit: string | null = null;
}

describe('TestComponent', () => {
let fixture: ComponentFixture;
let component: TestComponent;
let loader: HarnessLoader;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TestComponent]
}).compileComponents();

fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
loader = TestbedHarnessEnvironment.loader(fixture);
fixture.detectChanges();
});

it('should open the select and select an option', async () => {
// Get the select harness
const select = await loader.getHarness(MerSelectHarness);

// Check initial state
expect(await select.getValue()).toBe('');
expect(await select.isOpen()).toBe(false);

// Open the select
await select.click();
expect(await select.isOpen()).toBe(true);

// Get all options
const options = await select.getOptions();
expect(options.length).toBe(4);

// Click the "Banana" option
await select.clickOptions({ text: 'Banana' });

// Check that the panel is closed after selection
expect(await select.isOpen()).toBe(false);

// Check that the value is updated
expect(await select.getValue()).toBe('Banana');
expect(component.selectedFruit).toBe('Banana');
});

it('should filter options based on search text', async () => {
const select = await loader.getHarness(MerSelectHarness);

// Open the select
await select.click();

// Enter search text
await select.setTextSearch('ber');

// Get filtered options
const options = await select.getOptions();
expect(options.length).toBe(1);
expect(await options[0].getText()).toBe('Strawberry');

// Select the filtered option
await options[0].click();
expect(await select.getValue()).toBe('Strawberry');
});
});
```

### Testing with Complex Data Structures

When using objects as options, you can leverage the harness methods to test more complex scenarios:

```typescript
import { Component } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { HarnessLoader } from '@angular/cdk/testing';
import { MerSelectHarness } from '@merelis/angular/select/testing';
import { MerSelect } from '@merelis/angular/select';

interface User {
id: number;
name: string;
email: string;
}

@Component({
template: `


`,
standalone: true,
imports: [MerSelect]
})
class UserSelectComponent {
users: User[] = [
{ id: 1, name: 'John Doe', email: '[email protected]' },
{ id: 2, name: 'Jane Smith', email: '[email protected]' },
{ id: 3, name: 'Bob Johnson', email: '[email protected]' }
];
selectedUser: User | null = null;

displayUser(user: User): string {
return user?.name || '';
}

compareUsers(user1: User, user2: User): boolean {
return user1?.id === user2?.id;
}
}

describe('UserSelectComponent', () => {
let fixture: ComponentFixture;
let component: UserSelectComponent;
let loader: HarnessLoader;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UserSelectComponent]
}).compileComponents();

fixture = TestBed.createComponent(UserSelectComponent);
component = fixture.componentInstance;
loader = TestbedHarnessEnvironment.loader(fixture);
fixture.detectChanges();
});

it('should select a user by name and update the component model', async () => {
const select = await loader.getHarness(MerSelectHarness);

// Open the select
await select.click();

// Click the option with Jane's name
await select.clickOptions({ text: 'Jane Smith' });

// Check that the select shows the correct text
expect(await select.getValue()).toBe('Jane Smith');

// Check that the component model is updated with the correct object
expect(component.selectedUser).toEqual(component.users[1]);
expect(component.selectedUser?.id).toBe(2);
});
});
```

### Testing Multiple Selection

You can also test the multiple selection mode of the `MerSelect`:

```typescript
@Component({
template: `


`,
standalone: true,
imports: [MerSelect]
})
class ColorSelectComponent {
colors = ['Red', 'Green', 'Blue', 'Yellow', 'Purple'];
selectedColors: string[] = [];
}

describe('ColorSelectComponent', () => {
// Test setup...

it('should support multiple selection', async () => {
const select = await loader.getHarness(MerSelectHarness);

// Open the select
await select.click();

// Select multiple options
await select.clickOptions({ text: 'Red' });
await select.clickOptions({ text: 'Blue' });
await select.clickOptions({ text: 'Yellow' });

// Check component model
expect(component.selectedColors).toEqual(['Red', 'Blue', 'Yellow']);

// Verify that the selected options are marked as selected
const options = await select.getOptions();
for (const option of options) {
const text = await option.getText();
const isSelected = await option.isSelected();

if (['Red', 'Blue', 'Yellow'].includes(text)) {
expect(isSelected).toBe(true);
} else {
expect(isSelected).toBe(false);
}
}
});
});
```

---

## Integration with Angular Material
The MerSelect can be integrated with Angular Material's mat-form-field component through the @merelis/angular-material package. This integration allows you to use the select component within Material's form field, benefiting from features like floating labels, hints, and error messages.

### Installation

```bash
npm install @merelis/angular-material --save
```

### Usage with mat-form-field

```typescript
import { Component } from '@angular/core';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MerSelect } from '@merelis/angular/select';
import { MerSelectFormFieldControl } from "@merelis/angular-material/select";

@Component({
selector: 'app-material-example',
standalone: true,
imports: [
MatFormFieldModule,
MatInputModule,
MerSelect,
MerSelectFormFieldControl
],
providers: [
provideMerMaterialIntegration() // Enable integration with Angular Material
],
template: `

Select a user


Select a user from the list
Please select a valid user

`
})
export class MaterialExampleComponent {
users = [
{ id: 1, name: 'John Smith' },
{ id: 2, name: 'Mary Johnson' },
{ id: 3, name: 'Peter Williams' }
];
selectedUser = null;

displayUserName(user: any): string {
return user?.name || '';
}

compareUsers(user1: any, user2: any): boolean {
return user1?.id === user2?.id;
}
}
```

---

## Component Integration

The `MerSelect` internally uses the `MerProgressBar` to display a loading indicator when the `loading` property is set to `true`.

```html

```

---

### CSS Customization

The components can be customized using CSS variables. Below are the available variables for each component.

#### MerSelect

```scss
.mer-select {
// Base select appearance
--mer-select-font: system-ui, Roboto, sans-serif;
--mer-select-font-size: 1em;
--mer-select-font-weight: normal;
--mer-select-line-height: 1em;
--mer-select-letter-spacing: normal;
--mer-select-min-height: 32px;
--mer-select-side-padding: 8px;
--mer-select-input-height: 100%;
--mer-select-input-width: 100%;
--mer-select-trigger-wrapper-gap: 4px;


// multiple select
--mer-select-multiple-trigger-wrapper-gap: 4px;
--mer-select-multiple-side-padding: 2px;
--mer-select-multiple-input-min-width: 33%;
--mer-select-multiple-input-height: 24px;
--mer-select-multiple-input-padding: 0 4px;
--mer-select-multiple-values-gap: 4px;
--mer-select-multiple-values-padding: 0;
--mer-select-chip-background-color: #e6e6e6;
--mer-select-chip-border-radius: 8px;
--mer-select-chip-padding: 2px 2px 2px 8px;
--mer-select-chip-font-size: 0.875rem;

--mer-select-chip-remove-cursor: pointer;
--mer-select-chip-remove-margin-left: 4px;
--mer-select-chip-remove-font-size: 1rem;
--mer-select-chip-remove-line-height: 1rem;
--mer-select-chip-remove-font-weight: normal;
--mer-select-chip-remove-text-color: #000;
--mer-select-chip-remove-bg-color: #d1d1d1;
--mer-select-chip-remove-border-radius: 9999px;
--mer-select-chip-remove-padding: 0;
--mer-select-chip-remove-width: 12px;
--mer-select-chip-remove-height: 12px;
--mer-select-chip-remove-opacity: .5;

--mer-select-chip-remove-text-color-hover: white;
--mer-select-chip-remove-bg-color-hover: #505050;
--mer-select-chip-remove-opacity-hover: 1;


// Colors and states
--mer-select-background-color: white;
--mer-select-color: black;
--mer-select-border: 1px solid #8c8a8a;

// Focus state
--mer-select-background-color--focused: white;
--mer-select-color--focused: black;
--mer-select-border--focused: 1px solid #8c8a8a;
--mer-select-outline--focused: solid #4e95e8 2px;
--mer-select-outline-offset--focused: -1px;

// Disabled state
--mer-select-background-color--disabled: #ececec;
--mer-select-color--disabled: #707070;
--mer-select-border--disabled: 1px solid #8c8a8a;

// Invalid state
--mer-select-background-color--invalid: white;
--mer-select-color--invalid: black;
--mer-select-border--invalid: 1px solid #c10909;
--mer-select-outline--invalid: solid #c10909 2px;
--mer-select-outline-offset--invalid: -1px;

// Icons
--mer-select-chevron-icon-color: #b3b3b3;
--mer-select-chevron-icon-color--hover: #353535;

// Loading indicator
--mer-select-loading-height: 2px;
--mer-select-loading-background-color: #d7e8fb;
--mer-select-loading-color: #0772CD;
}
```

#### MerSelect Panel

```scss
.mer-select-panel {
--mer-select-panel-background-color: #ffffff;
--mer-select-panel-border-radius: 8px;
--mer-select-panel-box-shadow: rgba(0, 0, 0, 0.19) 0px 10px 20px, rgba(0, 0, 0, 0.23) 0px 6px 6px;
}
```

#### MerOption

```scss
.mer-option {
// Base option appearance
--mer-option-font: system-ui, Roboto, sans-serif;
--mer-option-font-size: 1em;
--mer-option-font-weight: normal;
--mer-option-line-height: 1em;
--mer-option-letter-spacing: normal;
--mer-option-min-height: 48px;
--mer-option-side-padding: 8px;
--mer-option-material-side-padding: 16px;
--mer-option-group-indent: 20px;

// Colors and states
--mer-option-color: #121212;
--mer-option-hover-background-color: #f6f6f6;
--mer-option-active-background-color: #ececec;
--mer-option-selected-color: #0d67ca;
--mer-option-selected-background-color: #eef6ff;

--mer-option-selected-hover-color: #0d67ca;
--mer-option-selected-hover-background-color: #e1eef8;
--mer-option-selected-active-color: #0d67ca;
--mer-option-selected-active-background-color: #dcecfb;
--mer-option-selected-active-hover-color: #0d67ca;
--mer-option-selected-active-hover-background-color: #dceafa;
}

```

#### MerProgressBar

```scss
.mer-progress-bar {
--mer-progress-bar-height: 4px;
--mer-progress-bar-background-color: rgba(5, 114, 206, 0.2);
--mer-progress-bar-color: rgb(5, 114, 206);
}
```

---

## Contributing

Contributions are welcome! Feel free to open issues or submit pull requests.

1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request

## License

This project is licensed under the MIT License - see the LICENSE file for details.