diff --git a/apps/isa-app/stories/ui/layout/breakpoint.mdx b/apps/isa-app/stories/ui/layout/breakpoint.mdx
new file mode 100644
index 000000000..83bf9103c
--- /dev/null
+++ b/apps/isa-app/stories/ui/layout/breakpoint.mdx
@@ -0,0 +1,36 @@
+import { Meta } from '@storybook/addon-docs';
+
+
+
+# Breakpoint Utility
+
+The `breakpoint` utility allows you to create a Signal that evaluates to `true` if the current viewport matches the specified breakpoints.
+
+## Usage
+
+### Example
+
+```typescript
+import { Component } from '@angular/core';
+import { breakpoint, Breakpoint } from 'ui-layout';
+
+@Component({
+ selector: 'app-breakpoint-demo',
+ template: `
+
This content is visible only on tablet viewports.
+ `,
+ imports: [BreakpointDirective],
+})
+export class BreakpointDemoComponent {
+ isTablet = breakpoint(Breakpoint.Tablet);
+}
+```
+
+### Breakpoints Table
+
+| Breakpoint | CSS Media Query Selector |
+| ------------ | --------------------------------------------- |
+| `tablet` | `(max-width: 1279px)` |
+| `desktop` | `(min-width: 1280px) and (max-width: 1439px)` |
+| `desktop-l` | `(min-width: 1440px) and (max-width: 1919px)` |
+| `desktop-xl` | `(min-width: 1920px)` |
diff --git a/jest.config.js b/jest.config.js
deleted file mode 100644
index 649f8a6e5..000000000
--- a/jest.config.js
+++ /dev/null
@@ -1,5 +0,0 @@
-const nxPreset = require('@nx/jest/preset').default;
-
-module.exports = {
- ...nxPreset,
-};
diff --git a/libs/oms/feature/return-search/src/lib/return-search-result/return-search-result.component.html b/libs/oms/feature/return-search/src/lib/return-search-result/return-search-result.component.html
index fa8825a9d..5b5eb18c1 100644
--- a/libs/oms/feature/return-search/src/lib/return-search-result/return-search-result.component.html
+++ b/libs/oms/feature/return-search/src/lib/return-search-result/return-search-result.component.html
@@ -12,18 +12,23 @@
[rollbackOnClose]="true"
>
- @if (isMobileDevice()) {
-
-
-
- } @else {
-
- }
+
+
+
+
+
-@if (isMobileDevice() && toggleOrderByToolbar()) {
-
+@if (orderByVisible()) {
+
}
diff --git a/libs/oms/feature/return-search/src/lib/return-search-result/return-search-result.component.ts b/libs/oms/feature/return-search/src/lib/return-search-result/return-search-result.component.ts
index 4b4efe074..d220e5306 100644
--- a/libs/oms/feature/return-search/src/lib/return-search-result/return-search-result.component.ts
+++ b/libs/oms/feature/return-search/src/lib/return-search-result/return-search-result.component.ts
@@ -22,11 +22,11 @@ import {
import { IconButtonComponent } from '@isa/ui/buttons';
import { EmptyStateComponent } from '@isa/ui/empty-state';
import { restoreScrollPosition } from '@isa/core/scroll-position';
-import { Platform } from '@angular/cdk/platform';
import { NgIconComponent, provideIcons } from '@ng-icons/core';
import { isaActionSort } from '@isa/icons';
import { ReturnSearchEntity, ReturnSearchStatus, ReturnSearchStore } from '@isa/oms/data-access';
import { ReturnSearchResultItemComponent } from './return-search-result-item/return-search-result-item.component';
+import { BreakpointDirective } from '@isa/ui/layout';
type EmptyState = {
title: string;
@@ -47,18 +47,20 @@ type EmptyState = {
EmptyStateComponent,
NgIconComponent,
FilterMenuButtonComponent,
+ BreakpointDirective,
],
providers: [provideIcons({ isaActionSort })],
})
export class ReturnSearchResultComponent {
#route = inject(ActivatedRoute);
#router = inject(Router);
- #platform = inject(Platform);
private _processId = injectActivatedProcessId();
private _returnSearchStore = inject(ReturnSearchStore);
private _filterService = inject(FilterService);
+ orderByVisible = signal(false);
+
ReturnSearchStatus = ReturnSearchStatus;
filterInputs = computed(() =>
@@ -94,9 +96,6 @@ export class ReturnSearchResultComponent {
listElements = viewChildren>('listElement');
- isMobileDevice = signal(this.#platform.ANDROID || this.#platform.IOS);
- toggleOrderByToolbar = signal(false);
-
searchEffectFn = () =>
effect(() => {
const processId = this._processId();
diff --git a/libs/shared/filter/src/lib/menus/filter-menu/filter-menu-button.component.html b/libs/shared/filter/src/lib/menus/filter-menu/filter-menu-button.component.html
index f71d71540..e5419a9fc 100644
--- a/libs/shared/filter/src/lib/menus/filter-menu/filter-menu-button.component.html
+++ b/libs/shared/filter/src/lib/menus/filter-menu/filter-menu-button.component.html
@@ -14,11 +14,15 @@
[cdkConnectedOverlayOrigin]="trigger"
[cdkConnectedOverlayOpen]="open()"
[cdkConnectedOverlayHasBackdrop]="true"
- [cdkConnectedOverlayBackdropClass]="'cdk-overlay-transparent-backdrop'"
+ cdkConnectedOverlayBackdropClass="cdk-overlay-transparent-backdrop "
[cdkConnectedOverlayScrollStrategy]="scrollStrategy"
[cdkConnectedOverlayOffsetX]="-10"
[cdkConnectedOverlayOffsetY]="18"
(backdropClick)="toggle()"
>
-
+
diff --git a/libs/shared/filter/src/lib/menus/filter-menu/filter-menu.component.scss b/libs/shared/filter/src/lib/menus/filter-menu/filter-menu.component.scss
index f3b2c494e..41a857605 100644
--- a/libs/shared/filter/src/lib/menus/filter-menu/filter-menu.component.scss
+++ b/libs/shared/filter/src/lib/menus/filter-menu/filter-menu.component.scss
@@ -1,7 +1,7 @@
:host {
- @apply grid grid-flow-row;
+ @apply inline-flex flex-col;
@apply bg-isa-white;
@apply rounded-[1.25rem];
@apply w-[14.3125rem] max-h-[33.5rem];
- box-shadow: 0px 0px 16px 0px rgba(0, 0, 0, 0.15);
+ @apply shadow-overlay;
}
diff --git a/libs/shared/filter/src/lib/menus/input-menu/input-menu.component.scss b/libs/shared/filter/src/lib/menus/input-menu/input-menu.component.scss
index 02943e416..5d5edbef1 100644
--- a/libs/shared/filter/src/lib/menus/input-menu/input-menu.component.scss
+++ b/libs/shared/filter/src/lib/menus/input-menu/input-menu.component.scss
@@ -1,3 +1,4 @@
:host {
- @apply grid grid-flow-row bg-isa-white rounded-[1.25rem] shadow-[0px,0px,16px,0px,rgba(0,0,0,0.15)] max-h-[32.3rem];
+ @apply inline-flex flex-col;
+ @apply shadow-overlay bg-isa-white rounded-[1.25rem] max-h-[32.3rem];
}
diff --git a/libs/shared/filter/src/lib/order-by/order-by-toolbar.component.html b/libs/shared/filter/src/lib/order-by/order-by-toolbar.component.html
index 83caa33b6..02b0780a5 100644
--- a/libs/shared/filter/src/lib/order-by/order-by-toolbar.component.html
+++ b/libs/shared/filter/src/lib/order-by/order-by-toolbar.component.html
@@ -3,7 +3,7 @@
@for (orderBy of orderByOptions(); track orderBy.by) {
+ This content is visible only on tablet viewports.
+
+```
+
+## Breakpoints Table
+
+| Breakpoint | CSS Media Query Selector |
+| ------------ | --------------------------------------------- |
+| `tablet` | `(max-width: 1279px)` |
+| `desktop` | `(min-width: 1280px) and (max-width: 1439px)` |
+| `dekstop-l` | `(min-width: 1440px) and (max-width: 1919px)` |
+| `dekstop-xl` | `(min-width: 1920px)` |
+
+## Running Unit Tests
+
+Run `nx test ui-layout` to execute the unit tests.
diff --git a/libs/ui/layout/eslint.config.mjs b/libs/ui/layout/eslint.config.mjs
new file mode 100644
index 000000000..c68787af3
--- /dev/null
+++ b/libs/ui/layout/eslint.config.mjs
@@ -0,0 +1,34 @@
+import nx from '@nx/eslint-plugin';
+import baseConfig from '../../../eslint.config.mjs';
+
+export default [
+ ...baseConfig,
+ ...nx.configs['flat/angular'],
+ ...nx.configs['flat/angular-template'],
+ {
+ files: ['**/*.ts'],
+ rules: {
+ '@angular-eslint/directive-selector': [
+ 'error',
+ {
+ type: 'attribute',
+ prefix: 'ui',
+ style: 'camelCase',
+ },
+ ],
+ '@angular-eslint/component-selector': [
+ 'error',
+ {
+ type: 'element',
+ prefix: 'ui',
+ style: 'kebab-case',
+ },
+ ],
+ },
+ },
+ {
+ files: ['**/*.html'],
+ // Override or add rules here
+ rules: {},
+ },
+];
diff --git a/libs/ui/layout/jest.config.ts b/libs/ui/layout/jest.config.ts
new file mode 100644
index 000000000..0b3bc7168
--- /dev/null
+++ b/libs/ui/layout/jest.config.ts
@@ -0,0 +1,21 @@
+export default {
+ displayName: 'ui-layout',
+ preset: '../../../jest.preset.js',
+ setupFilesAfterEnv: ['/src/test-setup.ts'],
+ coverageDirectory: '../../../coverage/libs/ui/layout',
+ transform: {
+ '^.+\\.(ts|mjs|js|html)$': [
+ 'jest-preset-angular',
+ {
+ tsconfig: '/tsconfig.spec.json',
+ stringifyContentPathRegex: '\\.(html|svg)$',
+ },
+ ],
+ },
+ transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
+ snapshotSerializers: [
+ 'jest-preset-angular/build/serializers/no-ng-attributes',
+ 'jest-preset-angular/build/serializers/ng-snapshot',
+ 'jest-preset-angular/build/serializers/html-comment',
+ ],
+};
diff --git a/libs/ui/layout/project.json b/libs/ui/layout/project.json
new file mode 100644
index 000000000..de567a79b
--- /dev/null
+++ b/libs/ui/layout/project.json
@@ -0,0 +1,20 @@
+{
+ "name": "ui-layout",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "sourceRoot": "libs/ui/layout/src",
+ "prefix": "ui",
+ "projectType": "library",
+ "tags": [],
+ "targets": {
+ "test": {
+ "executor": "@nx/jest:jest",
+ "outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
+ "options": {
+ "jestConfig": "libs/ui/layout/jest.config.ts"
+ }
+ },
+ "lint": {
+ "executor": "@nx/eslint:lint"
+ }
+ }
+}
diff --git a/libs/ui/layout/src/index.ts b/libs/ui/layout/src/index.ts
new file mode 100644
index 000000000..560ef3fee
--- /dev/null
+++ b/libs/ui/layout/src/index.ts
@@ -0,0 +1,2 @@
+export * from './lib/breakpoint.directive';
+export * from './lib/breakpoint';
diff --git a/libs/ui/layout/src/lib/breakpoint.directive.ts b/libs/ui/layout/src/lib/breakpoint.directive.ts
new file mode 100644
index 000000000..4ab98f3fa
--- /dev/null
+++ b/libs/ui/layout/src/lib/breakpoint.directive.ts
@@ -0,0 +1,50 @@
+import { Directive, TemplateRef, ViewContainerRef, effect, inject, input } from '@angular/core';
+import { Breakpoint, breakpoint } from './breakpoint';
+
+/**
+ * Structural directive that conditionally includes a template based on the viewport's breakpoint.
+ *
+ * @example
+ * ```html
+ *
+ * This content is visible only on tablet viewports.
+ *
+ * ```
+ */
+@Directive({
+ selector: '[uiBreakpoint]',
+})
+export class BreakpointDirective {
+ private readonly viewContainer = inject(ViewContainerRef);
+ private readonly templateRef = inject(TemplateRef);
+
+ /**
+ * Input property to specify the breakpoint(s) for the directive.
+ * Accepts a single Breakpoint or an array of Breakpoints.
+ */
+ uiBreakpoint = input.required();
+
+ /**
+ * Signal that evaluates to `true` if the current viewport matches the specified breakpoint(s).
+ */
+ matches = breakpoint(this.uiBreakpoint);
+
+ constructor() {
+ effect(() => {
+ this.updateView(this.matches());
+ });
+ }
+
+ /**
+ * Updates the view based on whether the viewport matches the specified breakpoint(s).
+ *
+ * @param matches - Boolean indicating if the viewport matches the breakpoint(s).
+ */
+ private updateView(matches: boolean | undefined): void {
+ if (matches) {
+ this.viewContainer.createEmbeddedView(this.templateRef);
+ } else {
+ this.viewContainer.clear();
+ }
+ }
+}
diff --git a/libs/ui/layout/src/lib/breakpoint.ts b/libs/ui/layout/src/lib/breakpoint.ts
new file mode 100644
index 000000000..3e321a1c7
--- /dev/null
+++ b/libs/ui/layout/src/lib/breakpoint.ts
@@ -0,0 +1,87 @@
+import { inject, isSignal, Signal } from '@angular/core';
+import { BreakpointObserver } from '@angular/cdk/layout';
+import { exhaustMap, isObservable, map, Observable, of, startWith } from 'rxjs';
+import { toObservable, toSignal } from '@angular/core/rxjs-interop';
+import { coerceArray } from '@angular/cdk/coercion';
+
+/**
+ * Enum-like object defining various breakpoints for responsive design.
+ */
+export const Breakpoint = {
+ Tablet: 'tablet',
+ Desktop: 'desktop',
+ DekstopL: 'dekstop-l',
+ DekstopXL: 'dekstop-xl',
+} as const;
+
+/**
+ * Type representing the possible values of the Breakpoint object.
+ */
+export type Breakpoint = (typeof Breakpoint)[keyof typeof Breakpoint];
+
+/**
+ * Mapping of Breakpoint values to their corresponding CSS media query selectors.
+ */
+const BreakpointSelector = {
+ [Breakpoint.Tablet]: '(max-width: 1279px)',
+ [Breakpoint.Desktop]: '(min-width: 1280px) and (max-width: 1439px)',
+ [Breakpoint.DekstopL]: '(min-width: 1440px) and (max-width: 1919px)',
+ [Breakpoint.DekstopXL]: '(min-width: 1920px)',
+};
+
+/**
+ * Observes viewport breakpoints and returns an Observable that emits whether the specified breakpoints match.
+ *
+ * This function accepts a Breakpoint, an array of Breakpoints, a Signal, or an Observable thereof and returns
+ * an Observable that emits a boolean indicating if the given breakpoints match the current viewport.
+ *
+ * @param breakpoints - A Breakpoint, array of Breakpoints, Signal, or Observable representing breakpoints.
+ * @returns An Observable that emits a boolean indicating if the specified breakpoints match.
+ */
+export function breakpoint$(
+ breakpoints:
+ | (Breakpoint | Breakpoint[])
+ | Signal
+ | Observable,
+): Observable {
+ const bpObserver = inject(BreakpointObserver);
+
+ const breakpoints$ = isObservable(breakpoints)
+ ? breakpoints
+ : isSignal(breakpoints)
+ ? toObservable(breakpoints)
+ : of(breakpoints);
+
+ const breakpointSelectors$ = breakpoints$.pipe(
+ map((bp) => coerceArray(bp).map((b) => BreakpointSelector[b])),
+ );
+
+ const match$ = breakpointSelectors$.pipe(
+ exhaustMap((selectors) =>
+ bpObserver.observe(selectors).pipe(
+ map((result) => result.matches),
+ startWith(bpObserver.isMatched(selectors)),
+ ),
+ ),
+ );
+ return match$;
+}
+
+/**
+ * Converts the breakpoint$ Observable into a Signal to provide reactive breakpoint matching.
+ *
+ * This function wraps the breakpoint$ function, converting its Observable result into a Signal,
+ * which can be used with Angular's reactive system. The Signal returns a boolean indicating whether
+ * the specified breakpoints currently match or undefined if not yet determined.
+ *
+ * @param breakpoints - A Breakpoint, array of Breakpoints, Signal, or Observable representing breakpoints.
+ * @returns A Signal indicating the match state of the breakpoints.
+ */
+export function breakpoint(
+ breakpoints:
+ | (Breakpoint | Breakpoint[])
+ | Signal
+ | Observable,
+): Signal {
+ return toSignal(breakpoint$(breakpoints));
+}
diff --git a/libs/ui/layout/src/test-setup.ts b/libs/ui/layout/src/test-setup.ts
new file mode 100644
index 000000000..ea414013f
--- /dev/null
+++ b/libs/ui/layout/src/test-setup.ts
@@ -0,0 +1,6 @@
+import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
+
+setupZoneTestEnv({
+ errorOnUnknownElements: true,
+ errorOnUnknownProperties: true,
+});
diff --git a/libs/ui/layout/tsconfig.json b/libs/ui/layout/tsconfig.json
new file mode 100644
index 000000000..fde35eab0
--- /dev/null
+++ b/libs/ui/layout/tsconfig.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.lib.json"
+ },
+ {
+ "path": "./tsconfig.spec.json"
+ }
+ ],
+ "extends": "../../../tsconfig.base.json",
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/libs/ui/layout/tsconfig.lib.json b/libs/ui/layout/tsconfig.lib.json
new file mode 100644
index 000000000..9b49be758
--- /dev/null
+++ b/libs/ui/layout/tsconfig.lib.json
@@ -0,0 +1,17 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "declaration": true,
+ "declarationMap": true,
+ "inlineSources": true,
+ "types": []
+ },
+ "exclude": [
+ "src/**/*.spec.ts",
+ "src/test-setup.ts",
+ "jest.config.ts",
+ "src/**/*.test.ts"
+ ],
+ "include": ["src/**/*.ts"]
+}
diff --git a/libs/ui/layout/tsconfig.spec.json b/libs/ui/layout/tsconfig.spec.json
new file mode 100644
index 000000000..f858ef78c
--- /dev/null
+++ b/libs/ui/layout/tsconfig.spec.json
@@ -0,0 +1,16 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "module": "commonjs",
+ "target": "es2016",
+ "types": ["jest", "node"]
+ },
+ "files": ["src/test-setup.ts"],
+ "include": [
+ "jest.config.ts",
+ "src/**/*.test.ts",
+ "src/**/*.spec.ts",
+ "src/**/*.d.ts"
+ ]
+}
diff --git a/tailwind.config.js b/tailwind.config.js
index 296f912f5..49d614eae 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -226,6 +226,7 @@ module.exports = {
'card': '0px -2px 24px 0px rgba(220, 226, 233, 0.8)',
'cta': '0px 0px 15px 0px rgba(0, 0, 0, 0.5)',
'action': '0 0 20px 0 #596470',
+ 'overlay': '0px 0px 16px 0px rgba(0, 0, 0, 0.15)',
},
borderRadius: {
DEFAULT: '0.3125rem',
diff --git a/tsconfig.base.json b/tsconfig.base.json
index a7df18348..1ff3b81de 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -65,6 +65,7 @@
"@isa/ui/empty-state": ["libs/ui/empty-state/src/index.ts"],
"@isa/ui/input-controls": ["libs/ui/input-controls/src/index.ts"],
"@isa/ui/item-rows": ["libs/ui/item-rows/src/index.ts"],
+ "@isa/ui/layout": ["libs/ui/layout/src/index.ts"],
"@isa/ui/progress-bar": ["libs/ui/progress-bar/src/index.ts"],
"@isa/ui/search-bar": ["libs/ui/search-bar/src/index.ts"],
"@isa/ui/toolbar": ["libs/ui/toolbar/src/index.ts"],