mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
committed by
Nino Righi
parent
bfd151dd84
commit
c769af7021
@@ -10,6 +10,7 @@
|
||||
@layer components {
|
||||
@import "../../../libs/ui/buttons/src/buttons.scss";
|
||||
@import "../../../libs/ui/bullet-list/src/bullet-list.scss";
|
||||
@import "../../../libs/ui/carousel/src/lib/_carousel.scss";
|
||||
@import "../../../libs/ui/datepicker/src/datepicker.scss";
|
||||
@import "../../../libs/ui/dialog/src/dialog.scss";
|
||||
@import "../../../libs/ui/input-controls/src/input-controls.scss";
|
||||
|
||||
141
apps/isa-app/stories/ui/carousel/carousel.stories.ts
Normal file
141
apps/isa-app/stories/ui/carousel/carousel.stories.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { type Meta, type StoryObj, moduleMetadata } from '@storybook/angular';
|
||||
import { CarouselComponent } from '@isa/ui/carousel';
|
||||
import { QuoteCardComponent } from './quote-card.component';
|
||||
|
||||
// Collection of developer/inspirational quotes
|
||||
const quotes = [
|
||||
{ id: 1, text: 'Code is like humor. When you have to explain it, it\'s bad.', author: 'Cory House' },
|
||||
{ id: 2, text: 'First, solve the problem. Then, write the code.', author: 'John Johnson' },
|
||||
{ id: 3, text: 'Simplicity is the soul of efficiency.', author: 'Austin Freeman' },
|
||||
{ id: 4, text: 'Make it work, make it right, make it fast.', author: 'Kent Beck' },
|
||||
{ id: 5, text: 'Clean code always looks like it was written by someone who cares.', author: 'Robert C. Martin' },
|
||||
{ id: 6, text: 'Any fool can write code that a computer can understand. Good programmers write code that humans can understand.', author: 'Martin Fowler' },
|
||||
{ id: 7, text: 'Experience is the name everyone gives to their mistakes.', author: 'Oscar Wilde' },
|
||||
{ id: 8, text: 'In order to be irreplaceable, one must always be different.', author: 'Coco Chanel' },
|
||||
{ id: 9, text: 'The best way to predict the future is to invent it.', author: 'Alan Kay' },
|
||||
{ id: 10, text: 'Programs must be written for people to read, and only incidentally for machines to execute.', author: 'Harold Abelson' },
|
||||
{ id: 11, text: 'Testing leads to failure, and failure leads to understanding.', author: 'Burt Rutan' },
|
||||
{ id: 12, text: 'It\'s not a bug – it\'s an undocumented feature.', author: 'Anonymous' },
|
||||
{ id: 13, text: 'Software is a great combination between artistry and engineering.', author: 'Bill Gates' },
|
||||
{ id: 14, text: 'Talk is cheap. Show me the code.', author: 'Linus Torvalds' },
|
||||
{ id: 15, text: 'Sometimes it pays to stay in bed on Monday, rather than spending the rest of the week debugging Monday\'s code.', author: 'Dan Salomon' },
|
||||
];
|
||||
|
||||
// Helper function to generate a specified number of quotes
|
||||
function generateQuotes(count: number) {
|
||||
const result = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
result.push(quotes[i % quotes.length]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
interface CarouselStoryProps {
|
||||
gap: string;
|
||||
arrowAutoHide: boolean;
|
||||
itemCount: number;
|
||||
}
|
||||
|
||||
const meta: Meta<CarouselStoryProps> = {
|
||||
component: CarouselComponent,
|
||||
title: 'ui/carousel/Carousel',
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [QuoteCardComponent],
|
||||
}),
|
||||
],
|
||||
argTypes: {
|
||||
gap: {
|
||||
control: 'text',
|
||||
description: 'CSS gap value for spacing between carousel items',
|
||||
},
|
||||
arrowAutoHide: {
|
||||
control: 'boolean',
|
||||
description: 'Whether to auto-hide arrows until carousel is hovered or focused',
|
||||
},
|
||||
itemCount: {
|
||||
control: { type: 'number', min: 3, max: 20 },
|
||||
description: 'Number of quote cards to render',
|
||||
},
|
||||
},
|
||||
args: {
|
||||
gap: '1rem',
|
||||
arrowAutoHide: true,
|
||||
itemCount: 6,
|
||||
},
|
||||
render: (args) => ({
|
||||
props: {
|
||||
...args,
|
||||
quotes: generateQuotes(args.itemCount),
|
||||
},
|
||||
template: `
|
||||
<div style="padding: 2.5rem; background: #f5f5f5;">
|
||||
<ui-carousel
|
||||
[gap]="gap"
|
||||
[arrowAutoHide]="arrowAutoHide"
|
||||
>
|
||||
@for (quote of quotes; track quote.id) {
|
||||
<quote-card [quote]="quote.text" [author]="quote.author"></quote-card>
|
||||
}
|
||||
</ui-carousel>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<CarouselStoryProps>;
|
||||
|
||||
/**
|
||||
* Default carousel with 6 quote cards.
|
||||
* Demonstrates basic horizontal scrolling with auto-hide arrows.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
itemCount: 6,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Carousel with many items (15 cards).
|
||||
* Shows behavior with extensive scrolling and tests navigation performance.
|
||||
*/
|
||||
export const ManyItems: Story = {
|
||||
args: {
|
||||
itemCount: 15,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Carousel with few items (3 cards).
|
||||
* Demonstrates behavior when content might not overflow.
|
||||
* Arrows should disable if not scrollable.
|
||||
*/
|
||||
export const FewItems: Story = {
|
||||
args: {
|
||||
itemCount: 3,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Carousel with persistent arrows (no auto-hide).
|
||||
* Arrows are always visible when content is scrollable.
|
||||
*/
|
||||
export const AlwaysShowArrows: Story = {
|
||||
args: {
|
||||
itemCount: 8,
|
||||
arrowAutoHide: false,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Carousel with custom gap spacing (2rem).
|
||||
* Demonstrates configurable spacing between items.
|
||||
*/
|
||||
export const CustomGap: Story = {
|
||||
args: {
|
||||
itemCount: 6,
|
||||
gap: '2rem',
|
||||
},
|
||||
};
|
||||
63
apps/isa-app/stories/ui/carousel/quote-card.component.ts
Normal file
63
apps/isa-app/stories/ui/carousel/quote-card.component.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { Component, input, ChangeDetectionStrategy } from '@angular/core';
|
||||
|
||||
/**
|
||||
* Quote card component for Storybook carousel examples.
|
||||
* Displays a quote with author in a styled card matching the Figma design.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'quote-card',
|
||||
template: `
|
||||
<div class="quote-card">
|
||||
<div class="quote-card__content">
|
||||
<p class="quote-card__quote isa-text-body-1-regular text-isa-neutral-900">
|
||||
"{{ quote() }}"
|
||||
</p>
|
||||
@if (author()) {
|
||||
<p class="quote-card__author isa-text-body-2-bold text-isa-neutral-700">
|
||||
— {{ author() }}
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
.quote-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 20px 22px;
|
||||
min-width: 334px;
|
||||
max-width: 334px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.quote-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.quote-card__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.quote-card__quote {
|
||||
font-style: italic;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.quote-card__author {
|
||||
margin: 0;
|
||||
font-style: normal;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
})
|
||||
export class QuoteCardComponent {
|
||||
quote = input.required<string>();
|
||||
author = input<string>('');
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
# Library Reference Guide
|
||||
|
||||
> **Last Updated:** 2025-10-22
|
||||
> **Last Updated:** 2025-10-27
|
||||
> **Angular Version:** 20.1.2
|
||||
> **Nx Version:** 21.3.2
|
||||
> **Total Libraries:** 61
|
||||
> **Total Libraries:** 62
|
||||
|
||||
All 61 libraries in the monorepo have comprehensive README.md documentation located at `libs/[domain]/[layer]/[feature]/README.md`.
|
||||
All 62 libraries in the monorepo have comprehensive README.md documentation located at `libs/[domain]/[layer]/[feature]/README.md`.
|
||||
|
||||
**IMPORTANT: Always use the `docs-researcher` subagent** to retrieve and analyze library documentation. This keeps the main context clean and prevents pollution.
|
||||
|
||||
@@ -278,6 +278,11 @@ A comprehensive button component library for Angular applications providing five
|
||||
|
||||
**Location:** `libs/ui/buttons/`
|
||||
|
||||
### `@isa/ui/carousel`
|
||||
A horizontal scroll container component with left/right navigation arrows, responsive behavior, keyboard support, and auto-hide functionality for Angular applications.
|
||||
|
||||
**Location:** `libs/ui/carousel/`
|
||||
|
||||
### `@isa/ui/datepicker`
|
||||
A comprehensive date range picker component library for Angular applications with calendar and month/year selection views, form integration, and robust validation.
|
||||
|
||||
|
||||
281
libs/ui/carousel/README.md
Normal file
281
libs/ui/carousel/README.md
Normal file
@@ -0,0 +1,281 @@
|
||||
# @isa/ui/carousel
|
||||
|
||||
A horizontal scroll container component with left/right navigation arrows, responsive behavior, keyboard support, and auto-hide functionality.
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ **Smooth horizontal scrolling** with viewport-width scroll amount
|
||||
- ✅ **Navigation arrows** with auto-hide on hover (configurable)
|
||||
- ✅ **Keyboard navigation** (Arrow Left/Right when focused)
|
||||
- ✅ **Responsive design** with native touch gesture support
|
||||
- ✅ **Disabled states** when at scroll boundaries
|
||||
- ✅ **Touch gesture support** (native browser behavior)
|
||||
- ✅ **Content projection** for flexible item rendering
|
||||
- ✅ **Angular signals** for reactive state management
|
||||
|
||||
## Installation
|
||||
|
||||
The carousel library is part of the ISA-Frontend monorepo. Import it using the path alias:
|
||||
|
||||
```typescript
|
||||
import { CarouselComponent } from '@isa/ui/carousel';
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```typescript
|
||||
import { Component } from '@angular/core';
|
||||
import { CarouselComponent } from '@isa/ui/carousel';
|
||||
|
||||
@Component({
|
||||
selector: 'app-example',
|
||||
template: `
|
||||
<ui-carousel>
|
||||
@for (item of items; track item.id) {
|
||||
<div class="card">{{ item.name }}</div>
|
||||
}
|
||||
</ui-carousel>
|
||||
`,
|
||||
imports: [CarouselComponent],
|
||||
})
|
||||
export class ExampleComponent {
|
||||
items = [
|
||||
{ id: 1, name: 'Item 1' },
|
||||
{ id: 2, name: 'Item 2' },
|
||||
{ id: 3, name: 'Item 3' },
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Component Selector
|
||||
|
||||
`ui-carousel`
|
||||
|
||||
### Input Properties
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `gap` | `string` | `'1rem'` | CSS gap value for spacing between carousel items (e.g., `'1rem'`, `'2rem'`, `'0.5rem'`). |
|
||||
| `arrowAutoHide` | `boolean` | `true` | Whether to auto-hide arrows until the carousel is hovered or focused. When `true`, arrows fade in on hover/focus. |
|
||||
|
||||
### Methods
|
||||
|
||||
| Method | Parameters | Description |
|
||||
|--------|------------|-------------|
|
||||
| `scrollToPrevious()` | `event?: Event` | Scrolls to the previous set of items (left). Scrolls by viewport width. |
|
||||
| `scrollToNext()` | `event?: Event` | Scrolls to the next set of items (right). Scrolls by viewport width. |
|
||||
|
||||
### Keyboard Navigation
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `Arrow Left` | Scroll to previous items |
|
||||
| `Arrow Right` | Scroll to next items |
|
||||
|
||||
**Note:** The carousel must be focused (click or tab to it) for keyboard navigation to work.
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Example 1: Basic Carousel
|
||||
|
||||
```typescript
|
||||
import { Component } from '@angular/core';
|
||||
import { CarouselComponent } from '@isa/ui/carousel';
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<ui-carousel>
|
||||
<div class="card">Card 1</div>
|
||||
<div class="card">Card 2</div>
|
||||
<div class="card">Card 3</div>
|
||||
</ui-carousel>
|
||||
`,
|
||||
imports: [CarouselComponent],
|
||||
})
|
||||
export class BasicCarouselExample {}
|
||||
```
|
||||
|
||||
### Example 2: Always Show Arrows
|
||||
|
||||
```typescript
|
||||
import { Component } from '@angular/core';
|
||||
import { CarouselComponent } from '@isa/ui/carousel';
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<ui-carousel [arrowAutoHide]="false">
|
||||
@for (product of products; track product.id) {
|
||||
<product-card [product]="product"></product-card>
|
||||
}
|
||||
</ui-carousel>
|
||||
`,
|
||||
imports: [CarouselComponent],
|
||||
})
|
||||
export class AlwaysShowArrowsExample {
|
||||
products = [/* ... */];
|
||||
}
|
||||
```
|
||||
|
||||
### Example 3: Custom Gap
|
||||
|
||||
```typescript
|
||||
import { Component } from '@angular/core';
|
||||
import { CarouselComponent } from '@isa/ui/carousel';
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<ui-carousel [gap]="'2rem'">
|
||||
@for (card of cards; track card.id) {
|
||||
<div class="wide-card">{{ card.content }}</div>
|
||||
}
|
||||
</ui-carousel>
|
||||
`,
|
||||
imports: [CarouselComponent],
|
||||
})
|
||||
export class CustomGapExample {
|
||||
cards = [/* ... */];
|
||||
}
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
### CSS Classes
|
||||
|
||||
The carousel component uses the following CSS classes:
|
||||
|
||||
| Class | Element | Description |
|
||||
|-------|---------|-------------|
|
||||
| `.ui-carousel` | Host element | Main container with focus styles |
|
||||
| `.ui-carousel--auto-hide` | Host element | Applied when `arrowAutoHide` is `true` |
|
||||
| `.ui-carousel__wrapper` | Wrapper | Contains buttons and scroll container |
|
||||
| `.ui-carousel__container` | Scroll container | Horizontal scroll area with hidden scrollbar |
|
||||
| `.ui-carousel__button` | Navigation button | Left/right arrow buttons |
|
||||
| `.ui-carousel__button--left` | Left button | Positioned on the left side |
|
||||
| `.ui-carousel__button--right` | Right button | Positioned on the right side |
|
||||
|
||||
### Custom Styling
|
||||
|
||||
You can override the default styles using CSS:
|
||||
|
||||
```scss
|
||||
// Custom arrow button colors
|
||||
.ui-carousel__button {
|
||||
background-color: #your-color;
|
||||
border-color: #your-border-color;
|
||||
color: #your-icon-color;
|
||||
|
||||
&:hover {
|
||||
background-color: #your-hover-color;
|
||||
}
|
||||
}
|
||||
|
||||
// Custom focus outline
|
||||
.ui-carousel:focus {
|
||||
outline-color: #your-focus-color;
|
||||
}
|
||||
|
||||
// Custom scroll container
|
||||
.ui-carousel__container {
|
||||
// Add custom styles
|
||||
}
|
||||
```
|
||||
|
||||
## Accessibility
|
||||
|
||||
### Keyboard Navigation
|
||||
|
||||
- The carousel is focusable with `tabindex="0"`
|
||||
- Arrow Left/Right keys scroll the carousel when focused
|
||||
- Focus outline visible for keyboard users (`:focus-visible`)
|
||||
|
||||
### ARIA Attributes
|
||||
|
||||
- Navigation buttons have `aria-label` attributes:
|
||||
- Left button: `"Previous"`
|
||||
- Right button: `"Next"`
|
||||
- Buttons are properly marked with `type="button"` to prevent form submission
|
||||
|
||||
### E2E Testing Attributes
|
||||
|
||||
All interactive elements include proper data attributes for automated testing:
|
||||
|
||||
```html
|
||||
<button data-what="button" data-which="carousel-previous">...</button>
|
||||
<button data-what="button" data-which="carousel-next">...</button>
|
||||
```
|
||||
|
||||
## Behavior
|
||||
|
||||
### Scroll Logic
|
||||
|
||||
- **Scroll Amount**: Scrolls by the viewport width of the container
|
||||
- **Smooth Scrolling**: Uses CSS `scroll-behavior: smooth` for animated transitions
|
||||
- **Boundary Detection**: Automatically disables arrows when at the start/end of content
|
||||
- **Responsive**: Arrows always show when content is scrollable, on all screen sizes
|
||||
|
||||
### Auto-Hide Arrows
|
||||
|
||||
When `arrowAutoHide` is `true`:
|
||||
|
||||
- Arrows are invisible by default (`opacity: 0`)
|
||||
- Arrows fade in on carousel hover or focus
|
||||
- Smooth CSS transitions for fade effects
|
||||
|
||||
### Touch Gestures
|
||||
|
||||
The carousel supports native touch gestures on mobile/tablet devices:
|
||||
|
||||
- Swipe left/right to scroll
|
||||
- No custom JavaScript needed (browser default behavior)
|
||||
|
||||
## Browser Compatibility
|
||||
|
||||
- Modern browsers with CSS Grid and Flexbox support
|
||||
- Native smooth scrolling support
|
||||
- ResizeObserver API for content change detection
|
||||
|
||||
## Performance
|
||||
|
||||
- **OnPush Change Detection**: Optimized for minimal re-renders
|
||||
- **Angular Signals**: Fine-grained reactivity with automatic dependency tracking
|
||||
- **Native Scroll**: Leverages browser's native scroll performance
|
||||
- **Cleanup**: Automatic event listener and observer cleanup on destroy
|
||||
|
||||
## Testing
|
||||
|
||||
Run unit tests:
|
||||
|
||||
```bash
|
||||
npx nx test ui-carousel --skip-nx-cache
|
||||
```
|
||||
|
||||
The carousel component includes comprehensive tests for:
|
||||
|
||||
- Component creation and default values
|
||||
- Input property changes
|
||||
- CSS class application
|
||||
- Content projection
|
||||
- Keyboard navigation
|
||||
- Navigation button rendering
|
||||
- E2E data attributes
|
||||
|
||||
## Related Components
|
||||
|
||||
- `@isa/ui/buttons` - Button components used for navigation arrows
|
||||
- `@isa/icons` - Icon library for arrow icons
|
||||
|
||||
## Contributing
|
||||
|
||||
When contributing to this component:
|
||||
|
||||
1. Follow the ISA-Frontend coding conventions
|
||||
2. Use Angular signals for reactive state
|
||||
3. Include E2E data attributes (`data-what`, `data-which`)
|
||||
4. Write comprehensive Vitest tests
|
||||
5. Update this README for API changes
|
||||
|
||||
## License
|
||||
|
||||
Internal ISA-Frontend library.
|
||||
34
libs/ui/carousel/eslint.config.cjs
Normal file
34
libs/ui/carousel/eslint.config.cjs
Normal file
@@ -0,0 +1,34 @@
|
||||
const nx = require('@nx/eslint-plugin');
|
||||
const baseConfig = require('../../../eslint.config.js');
|
||||
|
||||
module.exports = [
|
||||
...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: {},
|
||||
},
|
||||
];
|
||||
20
libs/ui/carousel/project.json
Normal file
20
libs/ui/carousel/project.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "ui-carousel",
|
||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/ui/carousel/src",
|
||||
"prefix": "ui",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"test": {
|
||||
"executor": "@nx/vite:test",
|
||||
"outputs": ["{options.reportsDirectory}"],
|
||||
"options": {
|
||||
"reportsDirectory": "../../../coverage/libs/ui/carousel"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint"
|
||||
}
|
||||
}
|
||||
}
|
||||
1
libs/ui/carousel/src/index.ts
Normal file
1
libs/ui/carousel/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './lib/carousel.component';
|
||||
73
libs/ui/carousel/src/lib/_carousel.scss
Normal file
73
libs/ui/carousel/src/lib/_carousel.scss
Normal file
@@ -0,0 +1,73 @@
|
||||
.ui-carousel {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
// Focus outline for keyboard navigation
|
||||
&:focus {
|
||||
outline: 0.125rem solid var(--isa-accent-primary, #354acb);
|
||||
outline-offset: 0.125rem;
|
||||
}
|
||||
|
||||
&:focus:not(:focus-visible) {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-carousel__wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: visible; // Allow items to overflow and be visible at edges
|
||||
}
|
||||
|
||||
.ui-carousel__container {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
overflow-y: visible; // Allow vertical overflow to be visible
|
||||
scroll-behavior: smooth;
|
||||
width: 100%;
|
||||
|
||||
// Hide scrollbar while maintaining scroll functionality
|
||||
scrollbar-width: none; // Firefox
|
||||
-ms-overflow-style: none; // IE/Edge
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none; // Chrome/Safari
|
||||
}
|
||||
|
||||
// Enable touch scrolling on mobile
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.ui-carousel__button {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 10;
|
||||
transition: opacity 0.3s ease;
|
||||
|
||||
// Left button positioning
|
||||
&--left {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
// Right button positioning
|
||||
&--right {
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-hide arrows variant
|
||||
.ui-carousel--auto-hide {
|
||||
.ui-carousel__button {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:hover .ui-carousel__button,
|
||||
&:focus-within .ui-carousel__button {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
39
libs/ui/carousel/src/lib/carousel.component.html
Normal file
39
libs/ui/carousel/src/lib/carousel.component.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<div class="ui-carousel__wrapper">
|
||||
@if (showLeftArrow()) {
|
||||
<button
|
||||
uiIconButton
|
||||
class="ui-carousel__button ui-carousel__button--left"
|
||||
type="button"
|
||||
name="isaActionChevronLeft"
|
||||
size="large"
|
||||
color="tertiary"
|
||||
data-what="button"
|
||||
data-which="carousel-previous"
|
||||
(click)="scrollToPrevious()"
|
||||
[attr.aria-label]="'Previous'"
|
||||
></button>
|
||||
}
|
||||
|
||||
<div
|
||||
#scrollContainer
|
||||
class="ui-carousel__container"
|
||||
[style.gap]="gap()"
|
||||
>
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
|
||||
@if (showRightArrow()) {
|
||||
<button
|
||||
uiIconButton
|
||||
class="ui-carousel__button ui-carousel__button--right"
|
||||
type="button"
|
||||
name="isaActionChevronRight"
|
||||
size="large"
|
||||
color="tertiary"
|
||||
data-what="button"
|
||||
data-which="carousel-next"
|
||||
(click)="scrollToNext()"
|
||||
[attr.aria-label]="'Next'"
|
||||
></button>
|
||||
}
|
||||
</div>
|
||||
176
libs/ui/carousel/src/lib/carousel.component.spec.ts
Normal file
176
libs/ui/carousel/src/lib/carousel.component.spec.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { CarouselComponent } from './carousel.component';
|
||||
|
||||
// Test host component with mock items
|
||||
@Component({
|
||||
selector: 'ui-test-host',
|
||||
template: `
|
||||
<ui-carousel
|
||||
[gap]="gap()"
|
||||
[arrowAutoHide]="arrowAutoHide()"
|
||||
>
|
||||
@for (item of items(); track item) {
|
||||
<div class="test-item" [style.width.px]="300">{{ item }}</div>
|
||||
}
|
||||
</ui-carousel>
|
||||
`,
|
||||
imports: [CarouselComponent],
|
||||
})
|
||||
class TestHostComponent {
|
||||
items = signal(['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5']);
|
||||
gap = signal('1rem');
|
||||
arrowAutoHide = signal(true);
|
||||
}
|
||||
|
||||
describe('CarouselComponent', () => {
|
||||
let component: CarouselComponent;
|
||||
let fixture: ComponentFixture<CarouselComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [CarouselComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(CarouselComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have default input values', () => {
|
||||
expect(component.gap()).toBe('1rem');
|
||||
expect(component.arrowAutoHide()).toBe(true);
|
||||
});
|
||||
|
||||
it('should apply auto-hide class when arrowAutoHide is true', () => {
|
||||
fixture.componentRef.setInput('arrowAutoHide', true);
|
||||
fixture.detectChanges();
|
||||
|
||||
const hostElement = fixture.nativeElement as HTMLElement;
|
||||
expect(hostElement.classList.contains('ui-carousel--auto-hide')).toBe(true);
|
||||
});
|
||||
|
||||
it('should not apply auto-hide class when arrowAutoHide is false', () => {
|
||||
fixture.componentRef.setInput('arrowAutoHide', false);
|
||||
fixture.detectChanges();
|
||||
|
||||
const hostElement = fixture.nativeElement as HTMLElement;
|
||||
expect(hostElement.classList.contains('ui-carousel--auto-hide')).toBe(false);
|
||||
});
|
||||
|
||||
it('should have tabindex 0 for keyboard navigation', () => {
|
||||
const hostElement = fixture.nativeElement as HTMLElement;
|
||||
expect(hostElement.getAttribute('tabindex')).toBe('0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CarouselComponent with content', () => {
|
||||
let hostComponent: TestHostComponent;
|
||||
let fixture: ComponentFixture<TestHostComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [TestHostComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(TestHostComponent);
|
||||
hostComponent = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should render projected content', () => {
|
||||
const items = fixture.nativeElement.querySelectorAll('.test-item');
|
||||
expect(items.length).toBe(5);
|
||||
expect(items[0].textContent.trim()).toBe('Item 1');
|
||||
});
|
||||
|
||||
it('should apply gap style to scroll container', () => {
|
||||
hostComponent.gap.set('2rem');
|
||||
fixture.detectChanges();
|
||||
|
||||
const container = fixture.nativeElement.querySelector('.ui-carousel__container');
|
||||
expect(container.style.gap).toBe('2rem');
|
||||
});
|
||||
|
||||
it('should show navigation buttons with proper data attributes', () => {
|
||||
const buttons = fixture.nativeElement.querySelectorAll('.ui-carousel__button');
|
||||
|
||||
if (buttons.length > 0) {
|
||||
const leftButton = fixture.nativeElement.querySelector('[data-which="carousel-previous"]');
|
||||
const rightButton = fixture.nativeElement.querySelector('[data-which="carousel-next"]');
|
||||
|
||||
if (leftButton) {
|
||||
expect(leftButton.getAttribute('data-what')).toBe('button');
|
||||
expect(leftButton.getAttribute('aria-label')).toBe('Previous');
|
||||
}
|
||||
|
||||
if (rightButton) {
|
||||
expect(rightButton.getAttribute('data-what')).toBe('button');
|
||||
expect(rightButton.getAttribute('aria-label')).toBe('Next');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should use ui-icon-button directive for navigation buttons', () => {
|
||||
const buttons = fixture.nativeElement.querySelectorAll('.ui-carousel__button');
|
||||
|
||||
if (buttons.length > 0) {
|
||||
buttons.forEach((button: Element) => {
|
||||
expect(button.hasAttribute('uiiconbutton')).toBe(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('should have scroll container with proper classes', () => {
|
||||
const container = fixture.nativeElement.querySelector('.ui-carousel__container');
|
||||
expect(container).toBeTruthy();
|
||||
expect(container.classList.contains('ui-carousel__container')).toBe(true);
|
||||
});
|
||||
|
||||
it('should change arrowAutoHide input', () => {
|
||||
hostComponent.arrowAutoHide.set(false);
|
||||
fixture.detectChanges();
|
||||
|
||||
const carousel = fixture.debugElement.children[0].componentInstance as CarouselComponent;
|
||||
expect(carousel.arrowAutoHide()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CarouselComponent keyboard navigation', () => {
|
||||
let component: CarouselComponent;
|
||||
let fixture: ComponentFixture<CarouselComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [CarouselComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(CarouselComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should call scrollToPrevious on ArrowLeft keydown', () => {
|
||||
const spy = vi.spyOn(component, 'scrollToPrevious');
|
||||
const event = new KeyboardEvent('keydown', { key: 'ArrowLeft' });
|
||||
|
||||
const hostElement = fixture.nativeElement as HTMLElement;
|
||||
hostElement.dispatchEvent(event);
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call scrollToNext on ArrowRight keydown', () => {
|
||||
const spy = vi.spyOn(component, 'scrollToNext');
|
||||
const event = new KeyboardEvent('keydown', { key: 'ArrowRight' });
|
||||
|
||||
const hostElement = fixture.nativeElement as HTMLElement;
|
||||
hostElement.dispatchEvent(event);
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
180
libs/ui/carousel/src/lib/carousel.component.ts
Normal file
180
libs/ui/carousel/src/lib/carousel.component.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ViewEncapsulation,
|
||||
input,
|
||||
computed,
|
||||
signal,
|
||||
viewChild,
|
||||
ElementRef,
|
||||
effect,
|
||||
} from '@angular/core';
|
||||
import { IconButtonComponent } from '@isa/ui/buttons';
|
||||
import { provideIcons } from '@ng-icons/core';
|
||||
import { isaActionChevronLeft, isaActionChevronRight } from '@isa/icons';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-carousel',
|
||||
imports: [IconButtonComponent],
|
||||
templateUrl: './carousel.component.html',
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
providers: [provideIcons({ isaActionChevronLeft, isaActionChevronRight })],
|
||||
host: {
|
||||
'[class]': '["ui-carousel", arrowAutoHideClass()]',
|
||||
'[tabindex]': '0',
|
||||
'(keydown.ArrowLeft)': 'scrollToPrevious($event)',
|
||||
'(keydown.ArrowRight)': 'scrollToNext($event)',
|
||||
},
|
||||
})
|
||||
export class CarouselComponent {
|
||||
// Input signals
|
||||
gap = input<string>('1rem');
|
||||
arrowAutoHide = input<boolean>(true);
|
||||
|
||||
// View child for scroll container
|
||||
scrollContainer =
|
||||
viewChild.required<ElementRef<HTMLDivElement>>('scrollContainer');
|
||||
|
||||
// Internal state signals
|
||||
canScrollLeft = signal(false);
|
||||
canScrollRight = signal(false);
|
||||
|
||||
// Computed signals
|
||||
arrowAutoHideClass = computed(() =>
|
||||
this.arrowAutoHide() ? 'ui-carousel--auto-hide' : '',
|
||||
);
|
||||
|
||||
showLeftArrow = computed(() => this.canScrollLeft());
|
||||
|
||||
showRightArrow = computed(() => this.canScrollRight());
|
||||
|
||||
constructor() {
|
||||
// Update scroll state whenever the scroll container changes
|
||||
effect(() => {
|
||||
const container = this.scrollContainer()?.nativeElement;
|
||||
if (container) {
|
||||
this.updateScrollState();
|
||||
|
||||
// Add scroll event listener
|
||||
const handleScroll = () => this.updateScrollState();
|
||||
container.addEventListener('scroll', handleScroll);
|
||||
|
||||
// Add resize observer to detect content changes
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
this.updateScrollState();
|
||||
});
|
||||
resizeObserver.observe(container);
|
||||
|
||||
// Cleanup on destroy
|
||||
return () => {
|
||||
container.removeEventListener('scroll', handleScroll);
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the scroll state (can scroll left/right)
|
||||
*/
|
||||
private updateScrollState(): void {
|
||||
const container = this.scrollContainer()?.nativeElement;
|
||||
if (!container) return;
|
||||
|
||||
const { scrollLeft, scrollWidth, clientWidth } = container;
|
||||
|
||||
// Check if we can scroll left (not at the start)
|
||||
this.canScrollLeft.set(scrollLeft > 0);
|
||||
|
||||
// Check if we can scroll right (not at the end)
|
||||
// Add a small threshold (1px) to account for rounding errors
|
||||
this.canScrollRight.set(scrollLeft < scrollWidth - clientWidth - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the scroll amount based on fully visible items
|
||||
*/
|
||||
private calculateScrollAmount(): number {
|
||||
const container = this.scrollContainer()?.nativeElement;
|
||||
if (!container) return 0;
|
||||
|
||||
const children = Array.from(container.children) as HTMLElement[];
|
||||
if (children.length === 0) return container.clientWidth;
|
||||
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const containerLeft = containerRect.left;
|
||||
const containerRight = containerRect.right;
|
||||
|
||||
// Count fully visible items and get the first one
|
||||
let firstFullyVisibleItem: HTMLElement | null = null;
|
||||
let fullyVisibleCount = 0;
|
||||
|
||||
for (const child of children) {
|
||||
const childRect = child.getBoundingClientRect();
|
||||
const isFullyVisible =
|
||||
childRect.left >= containerLeft - 1 &&
|
||||
childRect.right <= containerRight + 1;
|
||||
|
||||
if (isFullyVisible) {
|
||||
if (!firstFullyVisibleItem) {
|
||||
firstFullyVisibleItem = child as HTMLElement;
|
||||
}
|
||||
fullyVisibleCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// If we have fully visible items, use their width as scroll amount
|
||||
if (fullyVisibleCount >= 1 && firstFullyVisibleItem) {
|
||||
// Get the next sibling to calculate the width including gap
|
||||
const nextSibling =
|
||||
firstFullyVisibleItem.nextElementSibling as HTMLElement;
|
||||
if (nextSibling) {
|
||||
const firstRect = firstFullyVisibleItem.getBoundingClientRect();
|
||||
const nextRect = nextSibling.getBoundingClientRect();
|
||||
const itemWidthWithGap = nextRect.left - firstRect.left;
|
||||
|
||||
// Scroll by the width of fully visible items
|
||||
const scrollAmount = itemWidthWithGap * fullyVisibleCount;
|
||||
return scrollAmount;
|
||||
}
|
||||
|
||||
// Fallback: just use the first item's width
|
||||
return firstFullyVisibleItem.getBoundingClientRect().width;
|
||||
}
|
||||
|
||||
// Fallback: scroll by container width
|
||||
return container.clientWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to the previous set of items (left)
|
||||
*/
|
||||
scrollToPrevious(event?: Event): void {
|
||||
event?.preventDefault();
|
||||
const container = this.scrollContainer()?.nativeElement;
|
||||
if (!container) return;
|
||||
|
||||
const scrollAmount = this.calculateScrollAmount();
|
||||
container.scrollBy({
|
||||
left: -scrollAmount,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll to the next set of items (right)
|
||||
*/
|
||||
scrollToNext(event?: Event): void {
|
||||
event?.preventDefault();
|
||||
const container = this.scrollContainer()?.nativeElement;
|
||||
if (!container) return;
|
||||
|
||||
const scrollAmount = this.calculateScrollAmount();
|
||||
container.scrollBy({
|
||||
left: scrollAmount,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
}
|
||||
24
libs/ui/carousel/src/test-setup.ts
Normal file
24
libs/ui/carousel/src/test-setup.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import '@angular/compiler';
|
||||
import '@analogjs/vitest-angular/setup-zone';
|
||||
|
||||
import {
|
||||
BrowserTestingModule,
|
||||
platformBrowserTesting,
|
||||
} from '@angular/platform-browser/testing';
|
||||
import { getTestBed } from '@angular/core/testing';
|
||||
|
||||
getTestBed().initTestEnvironment(
|
||||
BrowserTestingModule,
|
||||
platformBrowserTesting(),
|
||||
);
|
||||
|
||||
// Mock ResizeObserver for tests
|
||||
|
||||
global.ResizeObserver = class ResizeObserver {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
observe() {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
unobserve() {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
disconnect() {}
|
||||
};
|
||||
30
libs/ui/carousel/tsconfig.json
Normal file
30
libs/ui/carousel/tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"importHelpers": true,
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "preserve"
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"typeCheckHostBindings": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
27
libs/ui/carousel/tsconfig.lib.json
Normal file
27
libs/ui/carousel/tsconfig.lib.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"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",
|
||||
"vite.config.ts",
|
||||
"vite.config.mts",
|
||||
"vitest.config.ts",
|
||||
"vitest.config.mts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx"
|
||||
],
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
29
libs/ui/carousel/tsconfig.spec.json
Normal file
29
libs/ui/carousel/tsconfig.spec.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"types": [
|
||||
"vitest/globals",
|
||||
"vitest/importMeta",
|
||||
"vite/client",
|
||||
"node",
|
||||
"vitest"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts",
|
||||
"vite.config.mts",
|
||||
"vitest.config.ts",
|
||||
"vitest.config.mts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx",
|
||||
"src/**/*.d.ts"
|
||||
],
|
||||
"files": ["src/test-setup.ts"]
|
||||
}
|
||||
27
libs/ui/carousel/vite.config.mts
Normal file
27
libs/ui/carousel/vite.config.mts
Normal file
@@ -0,0 +1,27 @@
|
||||
/// <reference types='vitest' />
|
||||
import { defineConfig } from 'vite';
|
||||
import angular from '@analogjs/vite-plugin-angular';
|
||||
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
|
||||
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
|
||||
|
||||
export default defineConfig(() => ({
|
||||
root: __dirname,
|
||||
cacheDir: '../../../node_modules/.vite/libs/ui/carousel',
|
||||
plugins: [angular(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
|
||||
// Uncomment this if you are using workers.
|
||||
// worker: {
|
||||
// plugins: [ nxViteTsPaths() ],
|
||||
// },
|
||||
test: {
|
||||
watch: false,
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||
setupFiles: ['src/test-setup.ts'],
|
||||
reporters: ['default'],
|
||||
coverage: {
|
||||
reportsDirectory: '../../../coverage/libs/ui/carousel',
|
||||
provider: 'v8' as const,
|
||||
},
|
||||
},
|
||||
}));
|
||||
76791
package-lock.json
generated
76791
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
262
package.json
262
package.json
@@ -1,131 +1,131 @@
|
||||
{
|
||||
"name": "hima",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "nx serve isa-app --ssl",
|
||||
"pretest": "npx trash-cli testresults",
|
||||
"test": "npx nx run-many --tuiAutoExit true -t test --exclude isa-app",
|
||||
"ci": "npx nx run-many -t test --exclude isa-app -c ci --tuiAutoExit true",
|
||||
"build": "nx build isa-app --configuration=development",
|
||||
"build-prod": "nx build isa-app --configuration=production",
|
||||
"lint": "nx lint",
|
||||
"e2e": "nx e2e",
|
||||
"generate:swagger": "nx run-many -t generate -p tag:generated,swagger",
|
||||
"fix:files:swagger": "node ./tools/fix-files.js generated/swagger",
|
||||
"prettier": "prettier --write .",
|
||||
"pretty-quick": "pretty-quick --staged",
|
||||
"prepare": "husky",
|
||||
"storybook": "npx nx run isa-app:storybook",
|
||||
"docs:generate": "node tools/generate-library-reference.js"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular-architects/ngrx-toolkit": "^20.4.0",
|
||||
"@angular/animations": "20.3.6",
|
||||
"@angular/cdk": "20.2.9",
|
||||
"@angular/common": "20.3.6",
|
||||
"@angular/compiler": "20.3.6",
|
||||
"@angular/core": "20.3.6",
|
||||
"@angular/forms": "20.3.6",
|
||||
"@angular/localize": "20.3.6",
|
||||
"@angular/platform-browser": "20.3.6",
|
||||
"@angular/platform-browser-dynamic": "20.3.6",
|
||||
"@angular/router": "20.3.6",
|
||||
"@angular/service-worker": "20.3.6",
|
||||
"@microsoft/signalr": "^8.0.7",
|
||||
"@ng-icons/core": "32.2.0",
|
||||
"@ng-icons/material-icons": "32.2.0",
|
||||
"@ngrx/component-store": "^20.0.0",
|
||||
"@ngrx/effects": "^20.0.0",
|
||||
"@ngrx/entity": "^20.0.0",
|
||||
"@ngrx/operators": "^20.0.0",
|
||||
"@ngrx/signals": "^20.0.0",
|
||||
"@ngrx/store": "^20.0.0",
|
||||
"@ngrx/store-devtools": "^20.0.0",
|
||||
"angular-oauth2-oidc": "^20.0.2",
|
||||
"angular-oauth2-oidc-jwks": "^20.0.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.30.1",
|
||||
"ng2-pdf-viewer": "^10.4.0",
|
||||
"ngx-matomo-client": "^8.0.0",
|
||||
"parse-duration": "^2.1.3",
|
||||
"rxjs": "~7.8.2",
|
||||
"scandit-web-datacapture-barcode": "^6.28.1",
|
||||
"scandit-web-datacapture-core": "^6.28.1",
|
||||
"tslib": "^2.3.0",
|
||||
"uuid": "^8.3.2",
|
||||
"zod": "^3.24.2",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@analogjs/vite-plugin-angular": "1.21.3",
|
||||
"@analogjs/vitest-angular": "1.21.3",
|
||||
"@angular-devkit/build-angular": "^20.3.6",
|
||||
"@angular-devkit/core": "20.3.6",
|
||||
"@angular-devkit/schematics": "20.3.6",
|
||||
"@angular/build": "^20.3.6",
|
||||
"@angular/cli": "^20.3.6",
|
||||
"@angular/compiler-cli": "20.3.6",
|
||||
"@angular/language-service": "20.3.6",
|
||||
"@angular/pwa": "20.3.6",
|
||||
"@eslint/js": "^9.8.0",
|
||||
"@ngneat/spectator": "22.0.0",
|
||||
"@nx/angular": "21.3.2",
|
||||
"@nx/eslint": "21.3.2",
|
||||
"@nx/eslint-plugin": "21.3.2",
|
||||
"@nx/jest": "21.3.2",
|
||||
"@nx/js": "21.3.2",
|
||||
"@nx/storybook": "21.3.2",
|
||||
"@nx/vite": "21.3.2",
|
||||
"@nx/web": "21.3.2",
|
||||
"@nx/workspace": "21.3.2",
|
||||
"@schematics/angular": "20.3.6",
|
||||
"@storybook/addon-docs": "^9.0.11",
|
||||
"@storybook/angular": "^9.0.5",
|
||||
"@swc-node/register": "1.10.10",
|
||||
"@swc/core": "1.12.1",
|
||||
"@swc/helpers": "0.5.17",
|
||||
"@types/jest": "30.0.0",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@types/node": "18.16.9",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@typescript-eslint/utils": "^8.33.1",
|
||||
"@vitest/coverage-v8": "^3.1.1",
|
||||
"@vitest/ui": "^3.1.1",
|
||||
"angular-eslint": "20.4.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.28.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-environment-node": "^29.7.0",
|
||||
"jest-junit": "^16.0.0",
|
||||
"jest-preset-angular": "14.6.0",
|
||||
"jiti": "2.4.2",
|
||||
"jsdom": "~22.1.0",
|
||||
"jsonc-eslint-parser": "^2.1.0",
|
||||
"ng-mocks": "14.14.0",
|
||||
"ng-packagr": "20.3.0",
|
||||
"ng-swagger-gen": "^2.3.1",
|
||||
"nx": "21.3.2",
|
||||
"postcss": "^8.5.3",
|
||||
"postcss-url": "~10.1.3",
|
||||
"prettier": "^3.5.2",
|
||||
"pretty-quick": "~4.0.0",
|
||||
"storybook": "^9.0.5",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-node": "10.9.1",
|
||||
"typescript": "5.8.3",
|
||||
"typescript-eslint": "^8.33.1",
|
||||
"vite": "6.3.5",
|
||||
"vitest": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.12.0 <23.0.0",
|
||||
"npm": ">=11.6.0 <11.7.0"
|
||||
}
|
||||
}
|
||||
{
|
||||
"name": "hima",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "nx serve isa-app --ssl",
|
||||
"pretest": "npx trash-cli testresults",
|
||||
"test": "npx nx run-many --tuiAutoExit true -t test --exclude isa-app",
|
||||
"ci": "npx nx run-many -t test --exclude isa-app -c ci --tuiAutoExit true",
|
||||
"build": "nx build isa-app --configuration=development",
|
||||
"build-prod": "nx build isa-app --configuration=production",
|
||||
"lint": "nx lint",
|
||||
"e2e": "nx e2e",
|
||||
"generate:swagger": "nx run-many -t generate -p tag:generated,swagger",
|
||||
"fix:files:swagger": "node ./tools/fix-files.js generated/swagger",
|
||||
"prettier": "prettier --write .",
|
||||
"pretty-quick": "pretty-quick --staged",
|
||||
"prepare": "husky",
|
||||
"storybook": "npx nx run isa-app:storybook",
|
||||
"docs:generate": "node tools/generate-library-reference.js"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular-architects/ngrx-toolkit": "^20.4.0",
|
||||
"@angular/animations": "20.3.6",
|
||||
"@angular/cdk": "20.2.9",
|
||||
"@angular/common": "20.3.6",
|
||||
"@angular/compiler": "20.3.6",
|
||||
"@angular/core": "20.3.6",
|
||||
"@angular/forms": "20.3.6",
|
||||
"@angular/localize": "20.3.6",
|
||||
"@angular/platform-browser": "20.3.6",
|
||||
"@angular/platform-browser-dynamic": "20.3.6",
|
||||
"@angular/router": "20.3.6",
|
||||
"@angular/service-worker": "20.3.6",
|
||||
"@microsoft/signalr": "^8.0.7",
|
||||
"@ng-icons/core": "32.2.0",
|
||||
"@ng-icons/material-icons": "32.2.0",
|
||||
"@ngrx/component-store": "^20.0.0",
|
||||
"@ngrx/effects": "^20.0.0",
|
||||
"@ngrx/entity": "^20.0.0",
|
||||
"@ngrx/operators": "^20.0.0",
|
||||
"@ngrx/signals": "^20.0.0",
|
||||
"@ngrx/store": "^20.0.0",
|
||||
"@ngrx/store-devtools": "^20.0.0",
|
||||
"angular-oauth2-oidc": "^20.0.2",
|
||||
"angular-oauth2-oidc-jwks": "^20.0.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.30.1",
|
||||
"ng2-pdf-viewer": "^10.4.0",
|
||||
"ngx-matomo-client": "^8.0.0",
|
||||
"parse-duration": "^2.1.3",
|
||||
"rxjs": "~7.8.2",
|
||||
"scandit-web-datacapture-barcode": "^6.28.1",
|
||||
"scandit-web-datacapture-core": "^6.28.1",
|
||||
"tslib": "^2.3.0",
|
||||
"uuid": "^8.3.2",
|
||||
"zod": "^3.24.2",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@analogjs/vite-plugin-angular": "1.21.3",
|
||||
"@analogjs/vitest-angular": "1.21.3",
|
||||
"@angular-devkit/build-angular": "^20.3.6",
|
||||
"@angular-devkit/core": "20.3.6",
|
||||
"@angular-devkit/schematics": "20.3.6",
|
||||
"@angular/build": "^20.3.6",
|
||||
"@angular/cli": "^20.3.6",
|
||||
"@angular/compiler-cli": "20.3.6",
|
||||
"@angular/language-service": "20.3.6",
|
||||
"@angular/pwa": "20.3.6",
|
||||
"@eslint/js": "^9.8.0",
|
||||
"@ngneat/spectator": "22.0.0",
|
||||
"@nx/angular": "21.3.2",
|
||||
"@nx/eslint": "21.3.2",
|
||||
"@nx/eslint-plugin": "21.3.2",
|
||||
"@nx/jest": "21.3.2",
|
||||
"@nx/js": "21.3.2",
|
||||
"@nx/storybook": "21.3.2",
|
||||
"@nx/vite": "21.3.2",
|
||||
"@nx/web": "21.3.2",
|
||||
"@nx/workspace": "21.3.2",
|
||||
"@schematics/angular": "20.3.6",
|
||||
"@storybook/addon-docs": "^9.0.11",
|
||||
"@storybook/angular": "^9.0.5",
|
||||
"@swc-node/register": "1.10.10",
|
||||
"@swc/core": "1.12.1",
|
||||
"@swc/helpers": "0.5.17",
|
||||
"@types/jest": "30.0.0",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@types/node": "18.16.9",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@typescript-eslint/utils": "^8.33.1",
|
||||
"@vitest/coverage-v8": "^3.1.1",
|
||||
"@vitest/ui": "^3.1.1",
|
||||
"angular-eslint": "20.4.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.28.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-environment-node": "^29.7.0",
|
||||
"jest-junit": "^16.0.0",
|
||||
"jest-preset-angular": "14.6.0",
|
||||
"jiti": "2.4.2",
|
||||
"jsdom": "~22.1.0",
|
||||
"jsonc-eslint-parser": "^2.1.0",
|
||||
"ng-mocks": "14.14.0",
|
||||
"ng-packagr": "20.3.0",
|
||||
"ng-swagger-gen": "^2.3.1",
|
||||
"nx": "21.3.2",
|
||||
"postcss": "^8.5.3",
|
||||
"postcss-url": "~10.1.3",
|
||||
"prettier": "^3.5.2",
|
||||
"pretty-quick": "~4.0.0",
|
||||
"storybook": "^9.0.5",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-node": "10.9.1",
|
||||
"typescript": "5.8.3",
|
||||
"typescript-eslint": "^8.33.1",
|
||||
"vite": "6.3.5",
|
||||
"vitest": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.12.0 <23.0.0",
|
||||
"npm": ">=11.6.0 <11.7.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +125,7 @@
|
||||
"@isa/shared/scanner": ["libs/shared/scanner/src/index.ts"],
|
||||
"@isa/ui/bullet-list": ["libs/ui/bullet-list/src/index.ts"],
|
||||
"@isa/ui/buttons": ["libs/ui/buttons/src/index.ts"],
|
||||
"@isa/ui/carousel": ["libs/ui/carousel/src/index.ts"],
|
||||
"@isa/ui/datepicker": ["libs/ui/datepicker/src/index.ts"],
|
||||
"@isa/ui/dialog": ["libs/ui/dialog/src/index.ts"],
|
||||
"@isa/ui/empty-state": ["libs/ui/empty-state/src/index.ts"],
|
||||
|
||||
Reference in New Issue
Block a user