-
{{ title }}
-
+
{{ title }}
+
diff --git a/packages/ng/empty-state/empty-state-section/empty-state-section.component.ts b/packages/ng/empty-state/empty-state-section/empty-state-section.component.ts
index e85bb12a16..c8af0810d7 100644
--- a/packages/ng/empty-state/empty-state-section/empty-state-section.component.ts
+++ b/packages/ng/empty-state/empty-state-section/empty-state-section.component.ts
@@ -1,7 +1,7 @@
+import { NgIf } from '@angular/common';
import { booleanAttribute, ChangeDetectionStrategy, Component, Input, ViewEncapsulation } from '@angular/core';
import { Palette, PortalContent, PortalDirective } from '@lucca-front/ng/core';
import { LuSafeExternalSvgPipe } from '@lucca-front/ng/safe-content';
-import { NgIf } from '@angular/common';
@Component({
selector: 'lu-empty-state-section',
@@ -27,13 +27,12 @@ export class EmptyStateSectionComponent {
})
center = false;
- @Input({
- required: true,
- })
+ @Input()
title: string;
- @Input({
- required: true,
- })
+ @Input()
description: PortalContent;
+
+ @Input()
+ hx: 1 | 2 | 3 | 4 | 5 | 6 = 3;
}
diff --git a/packages/ng/form-field/form-field.component.html b/packages/ng/form-field/form-field.component.html
index cb1ec241e8..f6ab29d4ad 100644
--- a/packages/ng/form-field/form-field.component.html
+++ b/packages/ng/form-field/form-field.component.html
@@ -3,7 +3,14 @@
*
-
+
*
-
+
0"
class="formLabel-counter"
diff --git a/packages/ng/form-field/form-field.component.ts b/packages/ng/form-field/form-field.component.ts
index 9b4891bd38..f2ee3c24e0 100644
--- a/packages/ng/form-field/form-field.component.ts
+++ b/packages/ng/form-field/form-field.component.ts
@@ -4,11 +4,11 @@ import { InputDirective } from './input.directive';
import { FormFieldSize } from './form-field-size';
import { BehaviorSubject, map, merge, startWith, Subject, switchMap } from 'rxjs';
import { InlineMessageComponent, InlineMessageState } from '@lucca-front/ng/inline-message';
+import { AbstractControl, NG_VALIDATORS, NgControl, ReactiveFormsModule, RequiredValidator, Validator, Validators } from '@angular/forms';
import { SafeHtml } from '@angular/platform-browser';
-import { LuTooltipModule } from '@lucca-front/ng/tooltip';
import { getIntl, IntlParamsPipe, LuClass, PortalContent, PortalDirective } from '@lucca-front/ng/core';
-import { AbstractControl, NG_VALIDATORS, NgControl, ReactiveFormsModule, RequiredValidator, Validator, Validators } from '@angular/forms';
import { IconComponent } from '@lucca-front/ng/icon';
+import { LuTooltipModule } from '@lucca-front/ng/tooltip';
import { FORM_FIELD_INSTANCE } from './form-field.token';
import { LU_FORM_FIELD_TRANSLATIONS } from './form-field.translate';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@@ -84,6 +84,11 @@ export class FormFieldComponent implements OnChanges, OnDestroy, DoCheck {
})
hiddenLabel = false;
+ @Input({
+ transform: booleanAttribute,
+ })
+ rolePresentationLabel = false;
+
@Input()
statusControl: AbstractControl;
diff --git a/packages/ng/form-field/input.directive.ts b/packages/ng/form-field/input.directive.ts
index 7c338f92fc..591d2eefce 100644
--- a/packages/ng/form-field/input.directive.ts
+++ b/packages/ng/form-field/input.directive.ts
@@ -15,7 +15,7 @@ export class InputDirective implements OnInit {
public readonly formFieldRef = inject(FORM_FIELD_INSTANCE, { optional: true });
/**
- * prevents message and label ids from being propagated, useful if the input holds its own message and label (like for radios)
+ * Prevents message and label ids from being propagated, useful if the input holds its own message and label (like for radios)
*/
@Input({ transform: booleanAttribute, alias: 'luInputStandalone' })
standalone = false;
diff --git a/packages/ng/forms/radio-group-input/radio/radio.component.html b/packages/ng/forms/radio-group-input/radio/radio.component.html
index d8668905f4..6e35e14f3b 100644
--- a/packages/ng/forms/radio-group-input/radio/radio.component.html
+++ b/packages/ng/forms/radio-group-input/radio/radio.component.html
@@ -8,7 +8,7 @@
type="radio"
class="radioField-input"
id="{{id}}-input"
- [name]="name"
+ [attr.name]="name"
[value]="value"
luInput
luInputStandalone
diff --git a/packages/ng/forms/text-input/text-input.component.html b/packages/ng/forms/text-input/text-input.component.html
index ea779a35ad..3f9cc397cb 100644
--- a/packages/ng/forms/text-input/text-input.component.html
+++ b/packages/ng/forms/text-input/text-input.component.html
@@ -1,4 +1,4 @@
-
+
{{ addon.content }}
diff --git a/packages/ng/forms/text-input/text-input.component.ts b/packages/ng/forms/text-input/text-input.component.ts
index 03cc3c5945..3eaa76fa0e 100644
--- a/packages/ng/forms/text-input/text-input.component.ts
+++ b/packages/ng/forms/text-input/text-input.component.ts
@@ -32,6 +32,9 @@ export class TextInputComponent {
@Input({ transform: booleanAttribute })
hasSearchIcon = false;
+ @Input({ transform: booleanAttribute })
+ valueAlignRight = false;
+
@ViewChild('inputElement', { static: true })
inputElementRef: ElementRef;
diff --git a/packages/ng/forms/textarea-input/textarea-input.component.html b/packages/ng/forms/textarea-input/textarea-input.component.html
index d68cb8c7ae..bf1d83b363 100644
--- a/packages/ng/forms/textarea-input/textarea-input.component.html
+++ b/packages/ng/forms/textarea-input/textarea-input.component.html
@@ -1,5 +1,11 @@
diff --git a/packages/ng/forms/textarea-input/textarea-input.component.ts b/packages/ng/forms/textarea-input/textarea-input.component.ts
index 7584971555..562061b9b9 100644
--- a/packages/ng/forms/textarea-input/textarea-input.component.ts
+++ b/packages/ng/forms/textarea-input/textarea-input.component.ts
@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, Input, ViewEncapsulation } from '@angular/core';
-import { InputDirective } from '@lucca-front/ng/form-field';
import { ReactiveFormsModule } from '@angular/forms';
+import { InputDirective } from '@lucca-front/ng/form-field';
import { injectNgControl } from '../inject-ng-control';
import { NoopValueAccessorDirective } from '../noop-value-accessor.directive';
@@ -18,4 +18,7 @@ export class TextareaInputComponent {
@Input()
placeholder: string = '';
+
+ @Input()
+ rows?: number;
}
diff --git a/packages/ng/material/style/_components.scss b/packages/ng/material/style/_components.scss
deleted file mode 100644
index 037b4e9606..0000000000
--- a/packages/ng/material/style/_components.scss
+++ /dev/null
@@ -1,13 +0,0 @@
-// deprecated
-
-@import
- 'components/mixins',
- 'components/buttons',
- 'components/datepicker',
- 'components/dialog',
- 'components/input',
- 'components/select',
- 'components/autocomplete',
- 'components/menu',
- 'components/options',
- 'components/tooltip';
diff --git a/packages/ng/material/style/components/_autocomplete.scss b/packages/ng/material/style/components/_autocomplete.scss
deleted file mode 100644
index 1708bf0ca4..0000000000
--- a/packages/ng/material/style/components/_autocomplete.scss
+++ /dev/null
@@ -1,15 +0,0 @@
-// deprecated
-
-.mat-autocomplete-panel {
- @extend .textfield-options;
-
- background: white;
- position: relative;
- top: auto;
-
- &.mat-autocomplete-visible {
- opacity: 1;
- overflow-x: hidden;
- transform: scaleY(1);
- }
-}
diff --git a/packages/ng/material/style/components/_buttons.scss b/packages/ng/material/style/components/_buttons.scss
deleted file mode 100644
index a3e6c5e999..0000000000
--- a/packages/ng/material/style/components/_buttons.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-// deprecated
-
-.mat-button-focus-overlay {
- display: none;
-}
diff --git a/packages/ng/material/style/components/_datepicker.scss b/packages/ng/material/style/components/_datepicker.scss
deleted file mode 100644
index 2df553195d..0000000000
--- a/packages/ng/material/style/components/_datepicker.scss
+++ /dev/null
@@ -1,124 +0,0 @@
-// deprecated
-
-@use '@lucca-front/icons/src/commons/utils/icon';
-@use '@lucca-front/scss/src/commons/utils/form';
-
-.mat-datepicker-content {
- @include box-shadow-override();
- @extend .card;
- display: block;
- margin-top: 2px;
-}
-
-.mat-datepicker-toggle {
- @extend .textfield-suffix;
- pointer-events: auto;
-
- button {
- background: none !important;
- border-radius: 0;
- height: auto;
- line-height: 1;
- width: auto;
-
- &::after {
- @include icon.generate('calendar_date');
- }
-
- mat-icon {
- display: none;
- }
- }
-}
-
-// CALENDAR
-.mat-calendar {
- width: 296px;
-
- .mat-button {
- @extend .button, .mod-text;
- font-size: 1em;
- text-decoration: none;
- }
-
- .mat-calendar-arrow {
- display: none;
- }
-
- .mat-calendar-arrow {
- display: none;
- }
-
- .mat-icon-button {
- @extend .button, .mod-text;
- border-radius: 3px;
- }
-
- .mat-calendar-content {
- padding: 0 0.5em 0.5em 0.5em;
- }
-}
-
-// CALENDAR HEADER
-.mat-calendar-controls {
- margin-top: 0 !important;
- .mat-icon-button[disabled] {
- opacity: 0.4;
- }
- .mat-calendar-period-button {
- flex: 1;
- order: 1;
- }
-
- .mat-calendar-next-button {
- order: 2;
- }
-
- .mat-calendar-spacer {
- display: none;
- }
-}
-
-// TABLE
-.mat-calendar-body-cell {
- .mat-calendar-body-cell-content {
- box-shadow: form.fakeBorderOverlay(var(--commons-divider-color));
- border: none;
- border-radius: 0;
- color: var(--palettes-neutral-600);
- height: 100%;
- left: 0;
- top: 0;
- width: 100%;
-
- &:hover,
- &.mat-calendar-body-selected {
- background-color: var(--palettes-product-700);
- color: var(--palettes-product-text);
- }
- }
-
- &.mat-calendar-body-disabled {
- opacity: 0.5;
- pointer-events: none;
- }
-}
-
-// SPECIFIC MONTH-VIEW
-md-month-view,
-mat-month-view {
- .mat-calendar-table-header th {
- color: var(--palettes-neutral-600);
- font-size: 0.9em;
- padding-bottom: 0.4em;
- }
-
- .mat-calendar-body-label {
- overflow: hidden;
- text-indent: -9999px;
-
- &[colspan='7'] {
- display: none; // REMOVE MONTH LABEL ABOVE NUMBERS IF FULL WIDTH
- }
- }
-}
diff --git a/packages/ng/material/style/components/_dialog.scss b/packages/ng/material/style/components/_dialog.scss
deleted file mode 100644
index 30f8438a54..0000000000
--- a/packages/ng/material/style/components/_dialog.scss
+++ /dev/null
@@ -1,34 +0,0 @@
-// deprecated
-
-@use '@lucca-front/scss/src/commons/utils/media';
-@use '@lucca-front/icons/src/commons/utils/icon';
-
-.mat-dialog-container {
- @extend .card;
-
- background: white;
- max-width: 80vw; // MIGHT BE CHANGED INTO A THEME VARIABLE
- overflow: visible !important;
- padding: 0 !important;
-}
-
-.cdk-overlay-pane {
- &.mod-sidePanel {
- position: fixed !important;
- top: var(--commons-banner-height);
- right: 0;
- bottom: 0;
- width: 60%;
- max-width: 800px;
- @include media.max('S') {
- width: 80%;
- }
- @include media.max('XS') {
- width: 100%;
- }
-
- .mat-dialog-container {
- max-width: 100%;
- }
- }
-}
diff --git a/packages/ng/material/style/components/_input.scss b/packages/ng/material/style/components/_input.scss
deleted file mode 100644
index fb1cf3ebee..0000000000
--- a/packages/ng/material/style/components/_input.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-// deprecated
-
-input.mat-input-element {
- margin-top: 0 !important;
-}
diff --git a/packages/ng/material/style/components/_menu.scss b/packages/ng/material/style/components/_menu.scss
deleted file mode 100644
index 0a3cebd444..0000000000
--- a/packages/ng/material/style/components/_menu.scss
+++ /dev/null
@@ -1,23 +0,0 @@
-// deprecated
-
-.mat-menu-item {
- @extend .textfield-options-entry;
- background-color: transparent;
- &.mat-selected,
- &.mat-active {
- @extend .is-focus;
- }
-}
-
-.mat-menu-panel {
- @extend .textfield-options;
- position: relative;
- top: auto;
- opacity: 1;
- transform: none;
-}
-
-.mat-menu-content {
- padding-top: 0 !important;
- padding-bottom: 0 !important;
-}
diff --git a/packages/ng/material/style/components/_mixins.scss b/packages/ng/material/style/components/_mixins.scss
deleted file mode 100644
index acdc3de641..0000000000
--- a/packages/ng/material/style/components/_mixins.scss
+++ /dev/null
@@ -1,10 +0,0 @@
-// deprecated
-
-@mixin box-shadow-override($hasHover: false) {
- box-shadow: var(--commons-boxShadow-XS) !important;
- @if $hasHover {
- &:hover {
- box-shadow: var(--commons-boxShadow-M) !important;
- }
- }
-}
diff --git a/packages/ng/material/style/components/_options.scss b/packages/ng/material/style/components/_options.scss
deleted file mode 100644
index 9aa56e2b46..0000000000
--- a/packages/ng/material/style/components/_options.scss
+++ /dev/null
@@ -1,26 +0,0 @@
-// deprecated
-
-.mat-option {
- @extend .textfield-options-entry;
-
- display: block;
- line-height: unset !important;
- outline: none;
- &.mat-selected,
- &.mat-active {
- @extend .textfield-options-entry, .is-focus;
- }
-
- .mat-option-ripple {
- bottom: 0;
- left: 0;
- pointer-events: none;
- position: absolute;
- right: 0;
- top: 0;
- }
-
- .mat-ripple {
- overflow: hidden;
- }
-}
diff --git a/packages/ng/material/style/components/_select.scss b/packages/ng/material/style/components/_select.scss
deleted file mode 100644
index 1a7edf1b4f..0000000000
--- a/packages/ng/material/style/components/_select.scss
+++ /dev/null
@@ -1,46 +0,0 @@
-// deprecated
-
-mat-select {
- @extend .textfield;
-}
-
-.textfield.mod-framed mat-select {
- padding: 0 !important;
-
- .mat-select-trigger {
- font-size: var(--sizes-M-fontSize);
- margin: 2.2rem var(--pr-t-spacings-200) 0.7rem;
- &::after {
- bottom: -0.7rem;
- content: '';
- display: block;
- left: calc(var(--pr-t-spacings-200) * -1);
- position: absolute;
- right: calc(var(--pr-t-spacings-200) * -1);
- top: -2.2rem;
- }
- }
-}
-
-.textfield:not(.mod-compact) mat-select {
- padding-top: 0 !important;
-}
-
-.mat-select-trigger {
- height: 1.1rem !important;
- min-width: 0 !important;
- text-overflow: ellipsis;
-}
-
-.mat-select-placeholder {
- width: auto !important;
-}
-
-.mat-select-panel {
- @extend .textfield-options;
-
- position: relative;
- top: auto;
- transform: none;
- opacity: 1;
-}
diff --git a/packages/ng/material/style/components/_tooltip.scss b/packages/ng/material/style/components/_tooltip.scss
deleted file mode 100644
index cdd036b7fa..0000000000
--- a/packages/ng/material/style/components/_tooltip.scss
+++ /dev/null
@@ -1,11 +0,0 @@
-// deprecated
-
-.mat-tooltip {
- border-radius: var(--commons-borderRadius-M);
- background: var(--components-tooltip-background-color);
- color: var(--components-tooltip-color);
- font-size: var(--sizes-XS-lineHeight) !important;
- margin: var(--pr-t-spacings-100) !important;
- padding: var(--pr-t-spacings-50) var(--pr-t-spacings-100) !important;
- line-height: var(--sizes-XS-lineHeight);
-}
diff --git a/packages/ng/material/style/main.scss b/packages/ng/material/style/main.scss
deleted file mode 100644
index 84a43efca9..0000000000
--- a/packages/ng/material/style/main.scss
+++ /dev/null
@@ -1,12 +0,0 @@
-// deprecated
-
-@use '@angular/material' as mat;
-
-@import 'components';
-
-// CHANGE DEFAULT FONTS TO LUCCA-FRONT FONTS
-$lucca-front-mat-typography: mat.define-typography-config(
- $font-family: var(--commons-font-family),
-);
-
-@include mat.all-component-typographies($lucca-front-mat-typography);
diff --git a/packages/ng/multi-select/displayer/counter-displayer/counter-displayer.component.scss b/packages/ng/multi-select/displayer/counter-displayer/counter-displayer.component.scss
new file mode 100644
index 0000000000..2486878d86
--- /dev/null
+++ b/packages/ng/multi-select/displayer/counter-displayer/counter-displayer.component.scss
@@ -0,0 +1,4 @@
+:host {
+ display: block;
+ width: 100%;
+}
diff --git a/packages/ng/multi-select/displayer/counter-displayer/counter-displayer.component.ts b/packages/ng/multi-select/displayer/counter-displayer/counter-displayer.component.ts
new file mode 100644
index 0000000000..5f9126653d
--- /dev/null
+++ b/packages/ng/multi-select/displayer/counter-displayer/counter-displayer.component.ts
@@ -0,0 +1,99 @@
+import { AsyncPipe, NgFor, NgIf, NgPlural, NgPluralCase } from '@angular/common';
+import { ChangeDetectionStrategy, Component, DestroyRef, ElementRef, inject, Input, OnInit, ViewChild } from '@angular/core';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { FormsModule } from '@angular/forms';
+import { ILuOptionContext, LU_OPTION_CONTEXT, ɵLuOptionOutletDirective } from '@lucca-front/ng/core-select';
+import { InputDirective } from '@lucca-front/ng/form-field';
+import { LuTooltipModule } from '@lucca-front/ng/tooltip';
+import { switchMap } from 'rxjs/operators';
+import { BehaviorSubject, of } from 'rxjs';
+import { LuMultiSelectInputComponent } from '../../input';
+
+@Component({
+ selector: 'lu-multi-select-counter-displayer',
+ standalone: true,
+ imports: [AsyncPipe, LuTooltipModule, NgIf, NgFor, NgPlural, NgPluralCase, ɵLuOptionOutletDirective, FormsModule, InputDirective],
+ template: `
+ 0">
+
+
+
+
+
+
1"
+ >{{ selectedOptions?.length }}{{ label }}
+
+
+ `,
+ styleUrls: ['./counter-displayer.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class LuMultiSelectCounterDisplayerComponent implements OnInit {
+ select = inject>(LuMultiSelectInputComponent);
+
+ protected destroyRef = inject(DestroyRef);
+
+ @ViewChild('inputElement')
+ inputElementRef: ElementRef;
+
+ get value(): T[] {
+ return this.select.value || [];
+ }
+
+ get ariaControls() {
+ return this.select.ariaControls;
+ }
+
+ context = inject>(LU_OPTION_CONTEXT);
+
+ selectedOptions$ = new BehaviorSubject([]);
+
+ @Input()
+ set selected(options: T[]) {
+ this.selectedOptions$.next(options);
+ }
+
+ placeholder$ = this.context.option$.pipe(
+ switchMap((options) => {
+ if ((options || []).length > 0) {
+ return of('');
+ }
+ return this.select.placeholder$;
+ }),
+ );
+
+ @Input({ required: true })
+ label: string;
+
+ ngOnInit(): void {
+ this.select.focusInput$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data?: { keepClue: true }) => {
+ // Everytime we want to focus, we need to reset the input
+ // This is done when a value is selected and when panel is opened.
+ if (!data?.keepClue) {
+ this.inputElementRef.nativeElement.value = '';
+ this.select.clueChanged('');
+ }
+ this.inputElementRef.nativeElement.focus();
+ });
+ this.select.emptyClue$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
+ this.inputElementRef.nativeElement.value = '';
+ });
+ }
+}
diff --git a/packages/ng/multi-select/displayer/default-displayer.component.ts b/packages/ng/multi-select/displayer/default-displayer.component.ts
index c0e3d48d36..2a3612b967 100644
--- a/packages/ng/multi-select/displayer/default-displayer.component.ts
+++ b/packages/ng/multi-select/displayer/default-displayer.component.ts
@@ -1,5 +1,5 @@
import { AsyncPipe, NgFor, NgIf, NgPlural, NgPluralCase } from '@angular/common';
-import { ChangeDetectionStrategy, Component, DestroyRef, ElementRef, OnInit, ViewChild, inject } from '@angular/core';
+import { ChangeDetectionStrategy, Component, DestroyRef, ElementRef, inject, OnInit, ViewChild } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormsModule } from '@angular/forms';
import { getIntl } from '@lucca-front/ng/core';
@@ -40,7 +40,7 @@ import { of } from 'rxjs';
{{ intl.removeOption }}
-
+ {{ overflow }}
+
+ {{ overflow }}
`,
styleUrls: ['./default-displayer.component.scss'],
diff --git a/packages/ng/multi-select/displayer/index.ts b/packages/ng/multi-select/displayer/index.ts
index 75776a1405..73776ea3d9 100644
--- a/packages/ng/multi-select/displayer/index.ts
+++ b/packages/ng/multi-select/displayer/index.ts
@@ -1,3 +1,4 @@
+export * from './counter-displayer/counter-displayer.component';
export * from './default-displayer.component';
export * from './displayer.directive';
export * from './displayer-input.directive';
diff --git a/packages/ng/popover2/content/popover-content/popover-content.component.html b/packages/ng/popover2/content/popover-content/popover-content.component.html
new file mode 100644
index 0000000000..5a84ab536d
--- /dev/null
+++ b/packages/ng/popover2/content/popover-content/popover-content.component.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/packages/ng/popover2/content/popover-content/popover-content.component.scss b/packages/ng/popover2/content/popover-content/popover-content.component.scss
new file mode 100644
index 0000000000..ce6df49f6f
--- /dev/null
+++ b/packages/ng/popover2/content/popover-content/popover-content.component.scss
@@ -0,0 +1,2 @@
+@use '@lucca-front/scss/src/components/button';
+@use '@lucca-front/scss/src/components/popover';
diff --git a/packages/ng/popover2/content/popover-content/popover-content.component.ts b/packages/ng/popover2/content/popover-content/popover-content.component.ts
new file mode 100644
index 0000000000..a90a8e4c7b
--- /dev/null
+++ b/packages/ng/popover2/content/popover-content/popover-content.component.ts
@@ -0,0 +1,80 @@
+import { AfterViewInit, ChangeDetectionStrategy, Component, DestroyRef, ElementRef, HostBinding, HostListener, inject, TemplateRef, ViewEncapsulation } from '@angular/core';
+import { NgTemplateOutlet } from '@angular/common';
+import { ButtonComponent } from '@lucca-front/ng/button';
+import { IconComponent } from '@lucca-front/ng/icon';
+import { PopoverFocusTrap } from '../../popover-focus-trap';
+import { Subject } from 'rxjs';
+import { POPOVER_CONFIG } from '../../popover-tokens';
+import { LU_POPOVER2_TRANSLATIONS } from '../../popover.translate';
+import { getIntl } from '@lucca-front/ng/core';
+
+@Component({
+ selector: 'lu-popover-content',
+ standalone: true,
+ imports: [NgTemplateOutlet, ButtonComponent, IconComponent],
+ templateUrl: './popover-content.component.html',
+ styleUrl: './popover-content.component.scss',
+
+ encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class PopoverContentComponent implements AfterViewInit {
+ intl = getIntl(LU_POPOVER2_TRANSLATIONS);
+
+ #elementRef = inject>(ElementRef);
+
+ #config = inject(POPOVER_CONFIG);
+
+ destroyRef = inject(DestroyRef);
+
+ @HostBinding('attr.id')
+ contentId = this.#config.contentId;
+
+ content: TemplateRef = this.#config.content;
+
+ #focusManager = new PopoverFocusTrap(this.#elementRef.nativeElement, this.#config.triggerElement);
+
+ closed$ = new Subject();
+
+ mouseEnter$ = new Subject();
+
+ @HostListener('mouseenter')
+ mouseEnter(): void {
+ this.mouseEnter$.next();
+ }
+
+ mouseLeave$ = new Subject();
+
+ @HostListener('mouseleave')
+ mouseLeave(): void {
+ this.mouseLeave$.next();
+ }
+
+ ngAfterViewInit(): void {
+ this.#focusManager.attachAnchors();
+ if (!this.#config.disableFocusManipulation) {
+ void this.#focusManager.focusInitialElementWhenReady();
+ }
+ }
+
+ grabFocus(): void {
+ if (!this.#config.disableFocusManipulation) {
+ this.#focusManager.focusInitialElement();
+ }
+ }
+
+ @HostListener('window:keydown.escape')
+ close(): void {
+ if (!this.#config.disableFocusManipulation) {
+ // Focus initial trigger element
+ this.#config.triggerElement.focus();
+ }
+ // Tell the directive we're closed now
+ this.closed$.next();
+ this.closed$.complete();
+ this.mouseEnter$.complete();
+ this.mouseLeave$.complete();
+ // Detach overlay
+ this.#config.ref.detach();
+ }
+}
diff --git a/packages/ng/popover2/ng-package.json b/packages/ng/popover2/ng-package.json
new file mode 100644
index 0000000000..35e7407829
--- /dev/null
+++ b/packages/ng/popover2/ng-package.json
@@ -0,0 +1,6 @@
+{
+ "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
+ "lib": {
+ "entryFile": "public-api.ts"
+ }
+}
diff --git a/packages/ng/popover2/popover-focus-trap.ts b/packages/ng/popover2/popover-focus-trap.ts
new file mode 100644
index 0000000000..a58daff9f5
--- /dev/null
+++ b/packages/ng/popover2/popover-focus-trap.ts
@@ -0,0 +1,23 @@
+import { FocusTrap, InteractivityChecker } from '@angular/cdk/a11y';
+import { inject, Injectable, NgZone } from '@angular/core';
+import { DOCUMENT } from '@angular/common';
+
+@Injectable()
+export class PopoverFocusTrap extends FocusTrap {
+ override startAnchorListener = () => {
+ this.triggerElement.focus();
+ return true;
+ };
+
+ override endAnchorListener = () => {
+ this.triggerElement.focus();
+ return true;
+ };
+
+ constructor(
+ element: HTMLElement,
+ private triggerElement: HTMLElement,
+ ) {
+ super(element, inject(InteractivityChecker), inject(NgZone), inject(DOCUMENT), true);
+ }
+}
diff --git a/packages/ng/popover2/popover-tokens.ts b/packages/ng/popover2/popover-tokens.ts
new file mode 100644
index 0000000000..5586e4276c
--- /dev/null
+++ b/packages/ng/popover2/popover-tokens.ts
@@ -0,0 +1,12 @@
+import { InjectionToken, TemplateRef } from '@angular/core';
+import { OverlayRef } from '@angular/cdk/overlay';
+
+export interface PopoverConfig {
+ triggerElement: HTMLElement;
+ content: TemplateRef;
+ ref: OverlayRef;
+ contentId: string;
+ disableFocusManipulation: boolean;
+}
+
+export const POPOVER_CONFIG = new InjectionToken('Popover:Config');
diff --git a/packages/ng/popover2/popover.directive.ts b/packages/ng/popover2/popover.directive.ts
new file mode 100644
index 0000000000..1ce8c05a2a
--- /dev/null
+++ b/packages/ng/popover2/popover.directive.ts
@@ -0,0 +1,207 @@
+import { booleanAttribute, DestroyRef, Directive, ElementRef, HostBinding, HostListener, inject, Injector, input, Input, InputSignal, signal, TemplateRef, ViewContainerRef } from '@angular/core';
+import { ConnectedPosition, ConnectionPositionPair, Overlay, OverlayRef } from '@angular/cdk/overlay';
+import { ComponentPortal } from '@angular/cdk/portal';
+import { PopoverContentComponent } from './content/popover-content/popover-content.component';
+import { POPOVER_CONFIG, PopoverConfig } from './popover-tokens';
+import { combineLatest, debounce, filter, map, merge, Subject, switchMap, timer } from 'rxjs';
+import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
+
+export type PopoverPosition = 'above' | 'below' | 'before' | 'after';
+
+let nextId = 0;
+
+const defaultPositionPairs: Record = {
+ above: new ConnectionPositionPair(
+ { originX: 'center', originY: 'top' },
+ {
+ overlayX: 'center',
+ overlayY: 'bottom',
+ },
+ ),
+ below: new ConnectionPositionPair(
+ { originX: 'center', originY: 'bottom' },
+ {
+ overlayX: 'center',
+ overlayY: 'top',
+ },
+ ),
+ before: new ConnectionPositionPair(
+ { originX: 'start', originY: 'center' },
+ {
+ overlayX: 'end',
+ overlayY: 'center',
+ },
+ ),
+ after: new ConnectionPositionPair(
+ { originX: 'end', originY: 'center' },
+ {
+ overlayX: 'start',
+ overlayY: 'center',
+ },
+ ),
+};
+
+@Directive({
+ selector: '[luPopover2]',
+ host: {
+ '[attr.aria-expanded]': 'opened()',
+ },
+ standalone: true,
+})
+export class PopoverDirective {
+ #overlay = inject(Overlay);
+
+ #elementRef = inject>(ElementRef);
+
+ #vcr = inject(ViewContainerRef);
+
+ #destroyRef = inject(DestroyRef);
+
+ @Input({
+ alias: 'luPopover2',
+ })
+ content: TemplateRef;
+
+ @Input()
+ luPopoverPosition: PopoverPosition = 'above';
+
+ @Input({
+ transform: booleanAttribute,
+ })
+ luPopoverDisabled = false;
+
+ luPopoverTrigger = input<'click' | 'click+hover'>('click');
+
+ @Input()
+ customPositions?: ConnectionPositionPair[];
+
+ // We have to type these two for Compodoc to find the right type and tell Storybook these aren't strings
+ luPopoverOpenDelay: InputSignal = input(300);
+
+ luPopoverCloseDelay: InputSignal = input(100);
+
+ open$ = new Subject();
+
+ close$ = new Subject();
+
+ #overlayRef: OverlayRef;
+
+ #componentRef?: PopoverContentComponent;
+
+ positionPairs: Record = defaultPositionPairs;
+
+ opened = signal(false);
+
+ @HostBinding('attr.aria-controls')
+ ariaControls = `popover-content-${nextId++}`;
+
+ constructor() {
+ combineLatest([toObservable(this.luPopoverOpenDelay), toObservable(this.luPopoverCloseDelay), toObservable(this.luPopoverTrigger)])
+ .pipe(
+ filter(([, , trigger]) => {
+ return trigger.includes('hover');
+ }),
+ switchMap(([openDelay, closeDelay]) => {
+ return merge(this.open$.pipe(map(() => 'open')), this.close$.pipe(map(() => 'close'))).pipe(
+ debounce((event) => {
+ return timer(event === 'open' ? openDelay : closeDelay);
+ }),
+ );
+ }),
+ takeUntilDestroyed(this.#destroyRef),
+ )
+ .subscribe((event) => {
+ if (event === 'open') {
+ this.openPopover(true);
+ } else {
+ this.#componentRef?.close();
+ }
+ });
+ }
+
+ @HostListener('mouseenter')
+ onMouseEnter() {
+ this.open$.next();
+ }
+
+ @HostListener('mouseleave')
+ onMouseLeave() {
+ this.close$.next();
+ }
+
+ @HostListener('click')
+ click(): void {
+ if (this.opened()) {
+ this.#componentRef?.close();
+ } else {
+ this.openPopover();
+ }
+ }
+
+ openPopover(disableFocusHandler = false): void {
+ if (!this.opened() && !this.luPopoverDisabled) {
+ this.opened.set(true);
+ this.#overlayRef = this.#overlay.create({
+ positionStrategy: this.#overlay
+ .position()
+ .flexibleConnectedTo(this.#elementRef)
+ .withPositions(this.customPositions || this.#buildPositions()),
+ scrollStrategy: this.#overlay.scrollStrategies.block(),
+ hasBackdrop: this.luPopoverTrigger() === 'click',
+ backdropClass: '',
+ disposeOnNavigation: true,
+ });
+ // Close on backdrop click even if backdrop is invisible
+ this.#overlayRef
+ .backdropClick()
+ .pipe(takeUntilDestroyed(this.#destroyRef))
+ .subscribe(() => this.#componentRef.close());
+ const config: PopoverConfig = {
+ content: this.content,
+ ref: this.#overlayRef,
+ contentId: this.ariaControls,
+ triggerElement: this.#elementRef.nativeElement,
+ disableFocusManipulation: disableFocusHandler,
+ };
+ this.#componentRef = this.#overlayRef.attach(
+ new ComponentPortal(
+ PopoverContentComponent,
+ this.#vcr,
+ Injector.create({
+ providers: [{ provide: POPOVER_CONFIG, useValue: config }],
+ }),
+ ),
+ ).instance;
+ // On tooltip leave => trigger close
+ this.#componentRef.mouseLeave$.pipe(takeUntilDestroyed(this.#componentRef.destroyRef), takeUntilDestroyed(this.#destroyRef)).subscribe(() => this.close$.next());
+ // On tooltip enter => trigger open to keep it opened
+ this.#componentRef.mouseEnter$.pipe(takeUntilDestroyed(this.#componentRef.destroyRef), takeUntilDestroyed(this.#destroyRef)).subscribe(() => this.open$.next());
+ this.#componentRef.closed$.pipe(takeUntilDestroyed(this.#componentRef.destroyRef), takeUntilDestroyed(this.#destroyRef)).subscribe(() => this.opened.set(false));
+ }
+ }
+
+ @HostListener('keydown.Tab', ['$event'])
+ focusBackToContent(event: KeyboardEvent): void {
+ if (this.opened()) {
+ event.preventDefault();
+ this.#componentRef.grabFocus();
+ }
+ }
+
+ #buildPositions(): ConnectedPosition[] {
+ const opposite: Record = {
+ before: 'after',
+ after: 'before',
+ above: 'below',
+ below: 'above',
+ };
+ // Once we have opposite, what's remaining?
+ const remaining: Record = {
+ before: ['above', 'below'],
+ after: ['above', 'below'],
+ above: ['before', 'after'],
+ below: ['before', 'after'],
+ };
+ return [this.positionPairs[this.luPopoverPosition], this.positionPairs[opposite[this.luPopoverPosition]], ...remaining[this.luPopoverPosition].map((r) => this.positionPairs[r])];
+ }
+}
diff --git a/packages/ng/popover2/popover.providers.ts b/packages/ng/popover2/popover.providers.ts
new file mode 100644
index 0000000000..7e1969e101
--- /dev/null
+++ b/packages/ng/popover2/popover.providers.ts
@@ -0,0 +1,6 @@
+import { EnvironmentProviders, importProvidersFrom, makeEnvironmentProviders } from '@angular/core';
+import { OverlayModule } from '@angular/cdk/overlay';
+
+export function configureLuPopover(): EnvironmentProviders {
+ return makeEnvironmentProviders([importProvidersFrom(OverlayModule)]);
+}
diff --git a/packages/ng/popover2/popover.translate.ts b/packages/ng/popover2/popover.translate.ts
new file mode 100644
index 0000000000..c5af414760
--- /dev/null
+++ b/packages/ng/popover2/popover.translate.ts
@@ -0,0 +1,28 @@
+import { InjectionToken } from '@angular/core';
+import { ILuTranslation } from '@lucca-front/ng/core';
+
+export const LU_POPOVER2_TRANSLATIONS = new InjectionToken('LuPopover2Translations', {
+ factory: () => luPopoverTranslations,
+});
+
+export interface ILuPopover2Label {
+ close: string;
+}
+
+export const luPopoverTranslations: ILuTranslation = {
+ en: {
+ close: 'Close',
+ },
+ fr: {
+ close: 'Fermer',
+ },
+ de: {
+ close: 'Schließen',
+ },
+ es: {
+ close: 'Cerrar',
+ },
+ pt: {
+ close: 'Fechar',
+ },
+};
diff --git a/packages/ng/popover2/public-api.ts b/packages/ng/popover2/public-api.ts
new file mode 100644
index 0000000000..6221fd2290
--- /dev/null
+++ b/packages/ng/popover2/public-api.ts
@@ -0,0 +1,2 @@
+export * from './popover.directive';
+export * from './popover.providers';
diff --git a/packages/ng/popup-employee/card/panel/user-popover-panel.component.html b/packages/ng/popup-employee/card/panel/user-popover-panel.component.html
index e23fcd60c3..530249ed67 100644
--- a/packages/ng/popup-employee/card/panel/user-popover-panel.component.html
+++ b/packages/ng/popup-employee/card/panel/user-popover-panel.component.html
@@ -10,7 +10,7 @@
(mousedown)="onMouseDown()"
>
-
+
@@ -22,27 +22,26 @@
-
+
diff --git a/packages/ng/skeleton/ng-package.json b/packages/ng/skeleton/ng-package.json
new file mode 100644
index 0000000000..68facef35b
--- /dev/null
+++ b/packages/ng/skeleton/ng-package.json
@@ -0,0 +1,7 @@
+{
+ "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
+ "lib": {
+ "entryFile": "public-api.ts",
+ "styleIncludePaths": ["../styles"]
+ }
+}
diff --git a/packages/ng/skeleton/public-api.ts b/packages/ng/skeleton/public-api.ts
new file mode 100644
index 0000000000..95380e3e8e
--- /dev/null
+++ b/packages/ng/skeleton/public-api.ts
@@ -0,0 +1,3 @@
+export * from './skeleton-button/skeleton-button.component';
+export * from './skeleton-header/skeleton-header.component';
+export * from './skeleton-field/skeleton-field.component';
diff --git a/packages/ng/skeleton/skeleton-button/skeleton-button.component.html b/packages/ng/skeleton/skeleton-button/skeleton-button.component.html
new file mode 100644
index 0000000000..5158168cd3
--- /dev/null
+++ b/packages/ng/skeleton/skeleton-button/skeleton-button.component.html
@@ -0,0 +1,5 @@
+
diff --git a/packages/ng/skeleton/skeleton-button/skeleton-button.component.scss b/packages/ng/skeleton/skeleton-button/skeleton-button.component.scss
new file mode 100644
index 0000000000..1df0f0cb39
--- /dev/null
+++ b/packages/ng/skeleton/skeleton-button/skeleton-button.component.scss
@@ -0,0 +1,12 @@
+@use '@lucca-front/scss/src/components/button';
+@use '@lucca-front/scss/src/components/skeleton';
+
+.skeleton.is-loading {
+ .button {
+ background-color: transparent;
+
+ .skeleton-item {
+ width: 4rem;
+ }
+ }
+}
diff --git a/packages/ng/skeleton/skeleton-button/skeleton-button.component.ts b/packages/ng/skeleton/skeleton-button/skeleton-button.component.ts
new file mode 100644
index 0000000000..83517763db
--- /dev/null
+++ b/packages/ng/skeleton/skeleton-button/skeleton-button.component.ts
@@ -0,0 +1,13 @@
+import { booleanAttribute, ChangeDetectionStrategy, Component, Input } from '@angular/core';
+
+@Component({
+ selector: 'lu-skeleton-button',
+ standalone: true,
+ templateUrl: './skeleton-button.component.html',
+ styleUrl: './skeleton-button.component.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class SkeletonButtonComponent {
+ @Input({ transform: booleanAttribute })
+ dark = false;
+}
diff --git a/packages/ng/skeleton/skeleton-field/skeleton-field.component.html b/packages/ng/skeleton/skeleton-field/skeleton-field.component.html
new file mode 100644
index 0000000000..2a26f2ba00
--- /dev/null
+++ b/packages/ng/skeleton/skeleton-field/skeleton-field.component.html
@@ -0,0 +1,10 @@
+
diff --git a/packages/ng/skeleton/skeleton-field/skeleton-field.component.scss b/packages/ng/skeleton/skeleton-field/skeleton-field.component.scss
new file mode 100644
index 0000000000..f00a69f5a7
--- /dev/null
+++ b/packages/ng/skeleton/skeleton-field/skeleton-field.component.scss
@@ -0,0 +1,2 @@
+@use '@lucca-front/scss/src/components/textField';
+@use '@lucca-front/scss/src/components/skeleton';
diff --git a/packages/ng/skeleton/skeleton-field/skeleton-field.component.ts b/packages/ng/skeleton/skeleton-field/skeleton-field.component.ts
new file mode 100644
index 0000000000..99b9cf8af6
--- /dev/null
+++ b/packages/ng/skeleton/skeleton-field/skeleton-field.component.ts
@@ -0,0 +1,13 @@
+import { booleanAttribute, ChangeDetectionStrategy, Component, Input } from '@angular/core';
+
+@Component({
+ selector: 'lu-skeleton-field',
+ standalone: true,
+ templateUrl: './skeleton-field.component.html',
+ styleUrl: './skeleton-field.component.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class SkeletonFieldComponent {
+ @Input({ transform: booleanAttribute })
+ dark = false;
+}
diff --git a/packages/ng/skeleton/skeleton-header/skeleton-header.component.html b/packages/ng/skeleton/skeleton-header/skeleton-header.component.html
new file mode 100644
index 0000000000..39efc05ce8
--- /dev/null
+++ b/packages/ng/skeleton/skeleton-header/skeleton-header.component.html
@@ -0,0 +1,14 @@
+
diff --git a/packages/ng/skeleton/skeleton-header/skeleton-header.component.scss b/packages/ng/skeleton/skeleton-header/skeleton-header.component.scss
new file mode 100644
index 0000000000..df10d29a8b
--- /dev/null
+++ b/packages/ng/skeleton/skeleton-header/skeleton-header.component.scss
@@ -0,0 +1,12 @@
+@use '@lucca-front/scss/src/components/pageHeader';
+@use '@lucca-front/scss/src/components/skeleton';
+@use '@lucca-front/scss/src/commons/utils/media';
+
+.pageHeader.skeleton.is-loading {
+ .pageHeader-content-actions {
+ width: 100px;
+ @include media.max('XS') {
+ display: none;
+ }
+ }
+}
diff --git a/packages/ng/skeleton/skeleton-header/skeleton-header.component.ts b/packages/ng/skeleton/skeleton-header/skeleton-header.component.ts
new file mode 100644
index 0000000000..710a9388fe
--- /dev/null
+++ b/packages/ng/skeleton/skeleton-header/skeleton-header.component.ts
@@ -0,0 +1,13 @@
+import { booleanAttribute, ChangeDetectionStrategy, Component, Input } from '@angular/core';
+
+@Component({
+ selector: 'lu-skeleton-header',
+ standalone: true,
+ templateUrl: './skeleton-header.component.html',
+ styleUrl: './skeleton-header.component.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class SkeletonHeaderComponent {
+ @Input({ transform: booleanAttribute })
+ dark = false;
+}
diff --git a/packages/ng/styles/_definitions.scss b/packages/ng/styles/_definitions.scss
index 5ef8287ebd..f4c7498df7 100644
--- a/packages/ng/styles/_definitions.scss
+++ b/packages/ng/styles/_definitions.scss
@@ -6,4 +6,3 @@
@import 'definitions/option/option-searcher';
@import 'definitions/option/option-selector';
@import 'definitions/option/option-placeholder';
-@import 'definitions/tooltip/tooltip';
diff --git a/packages/ng/styles/definitions/option/_option-item.scss b/packages/ng/styles/definitions/option/_option-item.scss
index 3c01b20bb0..9bf8c6eea7 100644
--- a/packages/ng/styles/definitions/option/_option-item.scss
+++ b/packages/ng/styles/definitions/option/_option-item.scss
@@ -6,8 +6,6 @@
--components-options-item-padding-horizontal: var(--pr-t-spacings-100);
--components-options-item-multiple-padding: 2.25rem;
--components-options-item-icon-color: var(--palettes-neutral-800);
-
- --components-options-checkbox-left: 0.5rem;
--components-options-checkbox-size: 1.25rem;
--components-options-checkbox-color: var(--palettes-product-700);
--components-options-checkbox-border-radius: 6px;
@@ -116,9 +114,8 @@
&::before {
display: block;
position: absolute;
- left: var(--components-options-checkbox-left);
- top: 50%;
- transform: translateY(-50%);
+ left: var(--pr-t-spacings-100);
+ top: var(--pr-t-spacings-75);
}
&::before {
@@ -140,7 +137,7 @@
line-height: var(--components-options-checkbox-size);
position: absolute;
text-align: center;
- transform: translateY(-50%) scale(0);
+ transform: scale(0);
transition: all 100ms;
width: var(--components-options-checkbox-size);
}
@@ -153,7 +150,7 @@
&::after {
color: var(--colors-white-color);
- transform: translateY(-50%) scale(1);
+ transform: scale(1);
}
}
diff --git a/packages/ng/styles/definitions/select/_select-input.scss b/packages/ng/styles/definitions/select/_select-input.scss
index 414860597d..60963bcdff 100644
--- a/packages/ng/styles/definitions/select/_select-input.scss
+++ b/packages/ng/styles/definitions/select/_select-input.scss
@@ -82,11 +82,6 @@
}
::ng-deep .lu-select-value {
- .label {
- padding: var(--pr-t-spacings-50) var(--pr-t-spacings-100);
- margin-left: 0;
- }
-
.chip {
vertical-align: baseline;
max-width: 100%;
@@ -127,16 +122,6 @@
}
::ng-deep .lu-select-value {
- .label {
- // deprecated
- font-size: var(--sizes-S-fontSize);
- line-height: var(--sizes-S-lineHeight);
- font-weight: 600;
- margin: 0;
- padding: 0;
- background-color: transparent;
- }
-
.chip {
height: var(--sizes-XS-lineHeight);
line-height: var(--sizes-XS-lineHeight);
diff --git a/packages/ng/styles/definitions/tooltip/_tooltip.scss b/packages/ng/styles/definitions/tooltip/_tooltip.scss
deleted file mode 100644
index 0251f02751..0000000000
--- a/packages/ng/styles/definitions/tooltip/_tooltip.scss
+++ /dev/null
@@ -1,36 +0,0 @@
-@mixin tooltipStyle {
- .lu-tooltip-panel {
- --components-tooltip-background-color: var(--palettes-neutral-900);
- --components-tooltip-color: var(--colors-white-color);
- --components-tooltip-max-width: 15rem;
-
- background: var(--components-tooltip-background-color);
- color: var(--components-tooltip-color);
- padding: var(--pr-t-spacings-50) var(--pr-t-spacings-100);
- max-width: var(--components-tooltip-max-width);
- border-radius: var(--commons-borderRadius-M);
- font-size: var(--sizes-XS-fontSize);
- line-height: var(--sizes-XS-lineHeight);
- display: block;
- text-align: center;
-
- &.is-above {
- transform-origin: bottom center;
- }
-
- &.is-below {
- transform-origin: top center;
- margin-top: 2px;
- }
-
- &.is-before {
- transform-origin: center right;
- margin-right: 5px;
- }
-
- &.is-after {
- transform-origin: center left;
- margin-left: 5px;
- }
- }
-}
diff --git a/packages/ng/styles/definitions/user/user-picture.scss b/packages/ng/styles/definitions/user/user-picture.scss
index 79b974b47e..0d7d880556 100644
--- a/packages/ng/styles/definitions/user/user-picture.scss
+++ b/packages/ng/styles/definitions/user/user-picture.scss
@@ -4,25 +4,30 @@
@mixin userPictureVars {
--components-userPicture-XS-image: 1.5rem;
- --components-userPicture-XS-fontSize: var(--sizes-XS-fontSize);
+ --components-userPicture-XS-fontSize: 0.75rem;
--components-userPicture-XS-placeholder: 0.75rem;
--components-userPicture-S-image: 2rem;
- --components-userPicture-S-fontSize: var(--sizes-S-fontSize);
- --components-userPicture-S-placeholder: var(--sizes-XS-lineHeight);
+ --components-userPicture-S-fontSize: 0.875rem;
+ --components-userPicture-S-placeholder: 1rem;
--components-userPicture-M-image: 2.5rem;
- --components-userPicture-M-fontSize: var(--sizes-M-fontSize);
- --components-userPicture-M-placeholder: var(--sizes-S-lineHeight);
+ --components-userPicture-M-fontSize: 1rem;
+ --components-userPicture-M-placeholder: 1.25rem;
--components-userPicture-L-image: 3rem;
- --components-userPicture-L-fontSize: var(--sizes-L-fontSize);
- --components-userPicture-L-placeholder: var(--sizes-M-lineHeight);
+ --components-userPicture-L-fontSize: 1.125rem;
+ --components-userPicture-L-placeholder: 1.5rem;
+ --components-userPicture-XL-image: 4rem;
+ --components-userPicture-XL-fontSize: 1.25rem;
+ --components-userPicture-XL-placeholder: 2rem;
+ --components-userPicture-XXL-image: 5rem;
+ --components-userPicture-XXL-fontSize: 1.5rem;
+ --components-userPicture-XXL-placeholder: 2.25rem;
+ --components-userPicture-XXXL-image: 6rem;
+ --components-userPicture-XXXL-fontSize: 1.75rem;
+ --components-userPicture-XXXL-placeholder: 2.5rem;
// deprecated
--components-userPicture-XXS-image: 1rem;
--components-userPicture-XXS-fontSize: 0.5625rem;
- --components-userPicture-XL-image: 4.5rem;
- --components-userPicture-XL-fontSize: 1.8rem;
- --components-userPicture-XXL-image: 7.5rem;
- --components-userPicture-XXL-fontSize: 3rem;
}
@mixin userPictureStyle {
@@ -74,11 +79,19 @@
&.mod-XL {
--components-user-picture-image-size: var(--components-userPicture-XL-image);
--components-user-picture-font-size: var(--components-userPicture-XL-fontSize);
+ --components-user-picture-placeholder: var(--components-userPicture-XL-placeholder);
}
&.mod-XXL {
--components-user-picture-image-size: var(--components-userPicture-XXL-image);
--components-user-picture-font-size: var(--components-userPicture-XXL-fontSize);
+ --components-user-picture-placeholder: var(--components-userPicture-XXL-placeholder);
+ }
+
+ &.mod-XXXL {
+ --components-user-picture-image-size: var(--components-userPicture-XXXL-image);
+ --components-user-picture-font-size: var(--components-userPicture-XXXL-fontSize);
+ --components-user-picture-placeholder: var(--components-userPicture-XXXL-placeholder);
}
&.mod-border {
diff --git a/packages/ng/styles/definitions/user/user-tile.scss b/packages/ng/styles/definitions/user/user-tile.scss
index 335c0c6c42..3db60c11b2 100644
--- a/packages/ng/styles/definitions/user/user-tile.scss
+++ b/packages/ng/styles/definitions/user/user-tile.scss
@@ -79,4 +79,9 @@
--components-user-picture-image-size: var(--components-userPicture-XXL-image);
--components-user-picture-font-size: var(--components-userPicture-XXL-fontSize);
}
+
+ &.mod-XXXL {
+ --components-user-picture-image-size: var(--components-userPicture-XXXL-image);
+ --components-user-picture-font-size: var(--components-userPicture-XXXL-fontSize);
+ }
}
diff --git a/packages/ng/tag/ng-package.json b/packages/ng/tag/ng-package.json
new file mode 100644
index 0000000000..35e7407829
--- /dev/null
+++ b/packages/ng/tag/ng-package.json
@@ -0,0 +1,6 @@
+{
+ "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
+ "lib": {
+ "entryFile": "public-api.ts"
+ }
+}
diff --git a/packages/ng/tag/public-api.ts b/packages/ng/tag/public-api.ts
new file mode 100644
index 0000000000..060ebad79d
--- /dev/null
+++ b/packages/ng/tag/public-api.ts
@@ -0,0 +1 @@
+export * from './tag.component';
diff --git a/packages/ng/tag/tag.component.html b/packages/ng/tag/tag.component.html
new file mode 100644
index 0000000000..027e4f5009
--- /dev/null
+++ b/packages/ng/tag/tag.component.html
@@ -0,0 +1,11 @@
+@if (link) {
+
+ @if (icon) {
+ {{ label }} } @else { {{ label }} }
+} @else {
+
+ @if (icon) {
+ {{ label }} } @else { {{ label }} }
+}
diff --git a/packages/ng/tag/tag.component.scss b/packages/ng/tag/tag.component.scss
new file mode 100644
index 0000000000..e0d6d6c260
--- /dev/null
+++ b/packages/ng/tag/tag.component.scss
@@ -0,0 +1 @@
+@use '@lucca-front/scss/src/components/tag';
diff --git a/packages/ng/tag/tag.component.ts b/packages/ng/tag/tag.component.ts
new file mode 100644
index 0000000000..af185efda7
--- /dev/null
+++ b/packages/ng/tag/tag.component.ts
@@ -0,0 +1,51 @@
+import { booleanAttribute, ChangeDetectionStrategy, Component, Input, ViewEncapsulation } from '@angular/core';
+import { RouterLink } from '@angular/router';
+import { LuccaIcon } from '@lucca-front/icons';
+import { Palette } from '@lucca-front/ng/core';
+import { IconComponent } from '@lucca-front/ng/icon';
+
+@Component({
+ selector: 'lu-tag',
+ standalone: true,
+ templateUrl: './tag.component.html',
+ styleUrls: ['./tag.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+ imports: [IconComponent, RouterLink],
+})
+export class TagComponent {
+ @Input({ required: true })
+ label: string;
+
+ @Input()
+ /**
+ * Which size should the callout be? Defaults to medium
+ */
+ size: 'M' | 'S' = 'M';
+
+ @Input()
+ /**
+ * Which palette should be used for the entire callout.
+ * Defaults to none (inherits parent palette)
+ */
+ palette: Palette = 'none';
+
+ @Input({ transform: booleanAttribute })
+ /**
+ * Should display be outlined?
+ */
+ outlined = false;
+
+ @Input()
+ /**
+ * For routerLink usage
+ */
+ link: string;
+
+ @Input()
+ /**
+ * Which icon should we display in the callout if any?
+ * Defaults to no icon.
+ */
+ icon: LuccaIcon;
+}
diff --git a/packages/ng/time/core/base-picker.component.ts b/packages/ng/time/core/base-picker.component.ts
new file mode 100644
index 0000000000..4398c6af23
--- /dev/null
+++ b/packages/ng/time/core/base-picker.component.ts
@@ -0,0 +1,59 @@
+import { ISO8601Duration, ISO8601Time } from './date-primitives';
+import { Component, computed, input, model, ViewChild } from '@angular/core';
+import { TimePickerPartComponent } from './time-picker-part.component';
+import { ControlValueAccessor } from '@angular/forms';
+
+@Component({
+ template: '',
+})
+export abstract class BasePickerComponent implements ControlValueAccessor {
+ onChange: (value: ISO8601Time | ISO8601Duration) => void;
+ onTouched: () => void;
+
+ step = input
(null);
+
+ disabled = model(false);
+
+ size = input<'S' | 'M'>();
+
+ @ViewChild('hoursPart')
+ hoursPart?: TimePickerPartComponent;
+
+ @ViewChild('minutesPart')
+ minutesPart?: TimePickerPartComponent;
+
+ protected hoursIncrement = computed(() => this.getHoursIncrement());
+ protected minutesIncrement = computed(() => this.getMinutesIncrement());
+
+ registerOnChange(fn: () => void): void {
+ this.onChange = fn;
+ }
+
+ registerOnTouched(fn: () => void): void {
+ this.onTouched = fn;
+ }
+
+ abstract writeValue(value: ISO8601Time | ISO8601Duration): void;
+
+ abstract getHoursIncrement(): number;
+
+ abstract getMinutesIncrement(): number;
+
+ protected focusPart(type: 'hours' | 'minutes') {
+ this.onTouched?.();
+ if (type === 'hours') {
+ if (this.hoursIncrement() === 0) {
+ return;
+ }
+
+ this.hoursPart?.focus();
+ }
+ if (type === 'minutes') {
+ if (this.minutesIncrement() === 0) {
+ return;
+ }
+
+ this.minutesPart?.focus();
+ }
+ }
+}
diff --git a/packages/ng/time/core/date-primitives.ts b/packages/ng/time/core/date-primitives.ts
new file mode 100644
index 0000000000..22a91b5ecc
--- /dev/null
+++ b/packages/ng/time/core/date-primitives.ts
@@ -0,0 +1,2 @@
+export type ISO8601Duration = `${string}P${string}`; // Looks like P1DT2H30M
+export type ISO8601Time = `${string}:${string}:${string}`; // Looks like 00:30:00
diff --git a/packages/ng/time/core/date.utils.ts b/packages/ng/time/core/date.utils.ts
new file mode 100644
index 0000000000..2e68e89d9f
--- /dev/null
+++ b/packages/ng/time/core/date.utils.ts
@@ -0,0 +1,52 @@
+import { ISO8601Time } from './date-primitives';
+
+export const castToIsoTime = (str: string) => str as ISO8601Time;
+
+export const convertStringToIsoTime = (time: string): ISO8601Time => {
+ if (/^\d{1,2}:\d{2}:\d{2}$/.test(time)) {
+ return castToIsoTime(time);
+ }
+
+ if (/^\d{1,2}:\d{2}$/.test(time)) {
+ return castToIsoTime(`${time}:00`);
+ }
+
+ if (/^\d{1,2}h\d{2}$/.test(time)) {
+ return convertStringToIsoTime(time.replace('h', ':'));
+ }
+
+ throw new Error(`Invalid time format: ${time}`);
+};
+
+export const createIsoTimeFromHoursAndMinutes = (hours: number, minutes: number, seconds: number = 0): ISO8601Time => {
+ const hoursStr = hours.toString().padStart(2, '0');
+ const minutesStr = minutes.toString().padStart(2, '0');
+ const secondsStr = seconds.toString().padStart(2, '0');
+
+ return `${hoursStr}:${minutesStr}:${secondsStr}`;
+};
+
+export const isoTimeToSeconds = (time: ISO8601Time): number => {
+ return getHoursPartFromIsoTime(time) * 3600 + getMinutesPartFromIsoTime(time) * 60 + getSecondsPartFromIsoTime(time);
+};
+
+export const getHoursPartFromIsoTime = (time: ISO8601Time): number => Number(time.split(':')[0]) || 0;
+
+export const getMinutesPartFromIsoTime = (time: ISO8601Time): number => Number(time.split(':')[1]) || 0;
+export const getSecondsPartFromIsoTime = (time: ISO8601Time): number => Number(time.split(':')[2]) || 0;
+
+export const getHoursDisplayPartFromIsoTime = (time: ISO8601Time): number | '--' => {
+ const hours = time.split(':')[0];
+ if (hours === '--') {
+ return hours;
+ }
+ return Number(hours);
+};
+
+export const getMinutesDisplayPartFromIsoTime = (time: ISO8601Time): number | '--' => {
+ const minutes = time.split(':')[1];
+ if (minutes === '--') {
+ return minutes;
+ }
+ return Number(minutes);
+};
diff --git a/packages/ng/time/core/duration.utils.ts b/packages/ng/time/core/duration.utils.ts
new file mode 100644
index 0000000000..2b210b2292
--- /dev/null
+++ b/packages/ng/time/core/duration.utils.ts
@@ -0,0 +1,86 @@
+import { Duration } from 'date-fns';
+import { ISO8601Duration } from './date-primitives';
+import { isNotNil } from './misc.utils';
+
+export type NonNullableDateFnsDuration = {
+ years: number;
+ months: number;
+ days: number;
+ hours: number;
+ minutes: number;
+ seconds: number;
+};
+
+type DateFnsDuration = Duration;
+
+const ISO8601DurationRegExp =
+ /^(?-)?P(?:(?-?\d+)Y)?(?:(?-?\d+)M)?(?:(?-?\d+)W)?(?:(?-?\d+)D)?(?:T(?:(?-?\d+)H)?(?:(?-?\d+)M)?(?:(?-?\d+(?:\.\d+)?)S)?)?$/;
+
+// TODO memoize
+export const isoDurationToDateFnsDuration = (isoDuration: ISO8601Duration): NonNullableDateFnsDuration => {
+ const matches = ISO8601DurationRegExp.exec(isoDuration);
+
+ const groups = matches?.groups;
+
+ if (!groups) {
+ throw new Error(`Invalid ISO 8601 duration: ${isoDuration}`);
+ }
+
+ const withSign = groups['sign'] === '-' ? -1 : 1;
+
+ const result: NonNullableDateFnsDuration = {
+ years: groups['years'] ? Number(groups['years']) : 0,
+ months: groups['months'] ? Number(groups['months']) : 0,
+ days: groups['days'] ? Number(groups['days']) : 0,
+ hours: groups['hours'] ? Number(groups['hours']) : 0,
+ minutes: groups['minutes'] ? Number(groups['minutes']) : 0,
+ seconds: groups['seconds'] ? Number(groups['seconds']) : 0,
+ };
+
+ result.years *= withSign;
+ result.months *= withSign;
+ result.days *= withSign;
+ result.hours *= withSign;
+ result.minutes *= withSign;
+ result.seconds *= withSign;
+
+ return result;
+};
+
+export function isISO8601Duration(value: string): value is ISO8601Duration {
+ return ISO8601DurationRegExp.test(value);
+}
+
+export const dateFnsDurationToSeconds = (durationFns: DateFnsDuration): number => {
+ const { years, months } = durationFns;
+ let { days, hours, minutes, seconds } = durationFns;
+
+ if ((isNotNil(years) && years !== 0) || (isNotNil(months) && months !== 0)) {
+ throw new Error('years and months are not supported');
+ }
+
+ days = days || 0;
+ hours = hours || 0;
+ minutes = minutes || 0;
+ seconds = seconds || 0;
+
+ return days * 24 * 60 * 60 + hours * 60 * 60 + minutes * 60 + seconds;
+};
+
+export const isoDurationToSeconds = (duration: ISO8601Duration): number => {
+ const durationFns = isoDurationToDateFnsDuration(duration);
+
+ return dateFnsDurationToSeconds(durationFns);
+};
+
+export const getHoursPartFromDuration = (duration: ISO8601Duration): number => {
+ return Math.floor(isoDurationToSeconds(duration) / 3600);
+};
+
+export const getMinutesPartFromDuration = (duration: ISO8601Duration): number => {
+ return Math.floor((isoDurationToSeconds(duration) % 3600) / 60);
+};
+
+export const createDurationFromHoursAndMinutes = (hours: number, minutes: number): ISO8601Duration => {
+ return `PT${hours}H${minutes}M`;
+};
diff --git a/packages/ng/time/core/math.utils.ts b/packages/ng/time/core/math.utils.ts
new file mode 100644
index 0000000000..c5388c2f85
--- /dev/null
+++ b/packages/ng/time/core/math.utils.ts
@@ -0,0 +1,19 @@
+export const roundToNearest = (value: number, step: number) => Math.round(value / step) * step;
+
+export const floorToNearest = (value: number, step: number) => Math.floor(value / step) * step;
+
+export const ceilToNearest = (value: number, step: number) => Math.ceil(value / step) * step;
+
+/**
+ * Returns the value of n circularized between 0 and max.
+ * Given n = 0 and max = 10, returns 0.
+ * Given n = 10 and max = 10, returns 10.
+ * Given n = 11 and max = 10, returns 1.
+ * Given n = -1 and max = 10, returns 9.
+ */
+export const circularize = (n: number, max: number) => {
+ if (n % max === 0) {
+ return n === 0 ? 0 : max;
+ }
+ return ((n % max) + max) % max;
+};
diff --git a/packages/ng/time/core/misc.utils.ts b/packages/ng/time/core/misc.utils.ts
new file mode 100644
index 0000000000..9a9a7b586b
--- /dev/null
+++ b/packages/ng/time/core/misc.utils.ts
@@ -0,0 +1,5 @@
+export const isNil = (value: T | null | undefined): value is null | undefined => typeof value === 'undefined' || value === null;
+
+export const isNotNil = (value: T): value is NonNullable => !isNil(value);
+
+export type PickerControlDirection = 'up' | 'down';
diff --git a/packages/ng/time/core/repeat-on-hold.directive.ts b/packages/ng/time/core/repeat-on-hold.directive.ts
new file mode 100644
index 0000000000..ec531ca7d9
--- /dev/null
+++ b/packages/ng/time/core/repeat-on-hold.directive.ts
@@ -0,0 +1,79 @@
+import { Directive, ElementRef, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
+
+const INITIAL_INTERVAL = 500;
+
+@Directive({
+ selector: '[luRepeatOnHold]',
+ standalone: true,
+})
+export class RepeatOnHoldDirective implements OnDestroy, OnInit {
+ @Output() hold = new EventEmitter();
+
+ onMouseDown = () => {
+ if (this.startTime !== undefined) {
+ return;
+ }
+
+ this.hold.emit();
+ this.animationFrameId = window.requestAnimationFrame(this.animationFrameHandler);
+
+ window.removeEventListener('mouseup', this.onWindowMouseUp);
+ window.addEventListener('mouseup', this.onWindowMouseUp);
+ };
+
+ onWindowMouseUp = () => {
+ this.cancelAnimationFrame();
+ };
+
+ constructor(private elementRef: ElementRef) {}
+
+ ngOnInit(): void {
+ this.elementRef.nativeElement.addEventListener('mousedown', this.onMouseDown);
+ }
+
+ interval = INITIAL_INTERVAL;
+ startTime: number | undefined = undefined;
+ previousTime: number | undefined = undefined;
+ animationFrameId: number | undefined = undefined;
+
+ private cancelAnimationFrame(): void {
+ if (this.animationFrameId) {
+ this.previousTime = undefined;
+ this.interval = INITIAL_INTERVAL;
+ this.startTime = undefined;
+ window.cancelAnimationFrame(this.animationFrameId);
+ this.animationFrameId = undefined;
+ }
+ }
+
+ private animationFrameHandler = (time: number): void => {
+ if (this.startTime === undefined) {
+ this.startTime = time;
+ }
+
+ if (this.previousTime === undefined) {
+ this.previousTime = time;
+ }
+
+ if (time - this.previousTime > this.interval) {
+ this.hold.emit();
+ this.previousTime = time;
+ }
+
+ if (time - this.startTime > 1000) {
+ this.interval = 200;
+ }
+
+ if (time - this.startTime > 2000) {
+ this.interval = 50;
+ }
+
+ this.animationFrameId = window.requestAnimationFrame(this.animationFrameHandler);
+ };
+
+ ngOnDestroy(): void {
+ this.elementRef.nativeElement.removeEventListener('mousedown', this.onMouseDown);
+ window.removeEventListener('mouseup', this.onWindowMouseUp);
+ this.cancelAnimationFrame();
+ }
+}
diff --git a/packages/ng/time/core/time-picker-part.component.html b/packages/ng/time/core/time-picker-part.component.html
new file mode 100644
index 0000000000..61172f7260
--- /dev/null
+++ b/packages/ng/time/core/time-picker-part.component.html
@@ -0,0 +1,30 @@
+
+
+
+
+ {{ valueLabel() }}
+
+ @if (displayArrows()) {
+
+
+ }
+
diff --git a/packages/ng/time/core/time-picker-part.component.ts b/packages/ng/time/core/time-picker-part.component.ts
new file mode 100644
index 0000000000..04b0b19de2
--- /dev/null
+++ b/packages/ng/time/core/time-picker-part.component.ts
@@ -0,0 +1,176 @@
+import { DecimalPipe, formatNumber } from '@angular/common';
+import {
+ booleanAttribute,
+ ChangeDetectionStrategy,
+ Component,
+ computed,
+ ElementRef,
+ EventEmitter,
+ Inject,
+ input,
+ LOCALE_ID,
+ model,
+ ModelSignal,
+ numberAttribute,
+ Output,
+ ViewChild,
+} from '@angular/core';
+import { RepeatOnHoldDirective } from './repeat-on-hold.directive';
+import { PickerControlDirection } from './misc.utils';
+import { InputDirective } from '@lucca-front/ng/form-field';
+
+let nextId = 0;
+
+@Component({
+ selector: 'lu-time-picker-part',
+ standalone: true,
+ imports: [RepeatOnHoldDirective, DecimalPipe, InputDirective],
+ templateUrl: './time-picker-part.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class TimePickerPartComponent {
+ label = input('');
+
+ decimalConf = input('2.0-0');
+
+ value: ModelSignal = model('--');
+
+ max = input(0, {
+ transform: numberAttribute,
+ });
+
+ displayArrows = input(false, {
+ transform: booleanAttribute,
+ });
+
+ isReadonly = input(false, {
+ transform: booleanAttribute,
+ });
+
+ hideValue = input(false, {
+ transform: booleanAttribute,
+ });
+
+ disabled = input(false, {
+ transform: booleanAttribute,
+ });
+
+ focused = input(false, {
+ transform: booleanAttribute,
+ });
+
+ @Output() prevRequest = new EventEmitter();
+ @Output() nextRequest = new EventEmitter();
+ @Output() inputControlClick = new EventEmitter();
+ @Output() touched = new EventEmitter();
+
+ @ViewChild('timePickerInput') timePickerInput?: ElementRef;
+
+ valueLabel = computed(() => {
+ if (this.hideValue()) {
+ return ' ';
+ }
+ const value = this.value();
+ if (value === '--') {
+ return value;
+ }
+ return formatNumber(value, this.locale, this.decimalConf());
+ });
+
+ protected inputId = `time-picker-part-${nextId++}`;
+
+ constructor(@Inject(LOCALE_ID) private locale: string) {}
+
+ arrowKeyPressed(event: KeyboardEvent, isUpArrow: boolean): void {
+ event.preventDefault();
+ this.inputControlClick.emit(isUpArrow ? 'up' : 'down');
+ }
+
+ keysInputHandler(event: Event): void {
+ event.preventDefault();
+
+ if (!(event.target instanceof HTMLInputElement) || !(event instanceof InputEvent)) {
+ return;
+ }
+
+ if (event.data && /\D+/.test(event.data)) {
+ event.target.value = String(this.value());
+ return;
+ }
+
+ const value = event.target.value;
+
+ let val = value.slice(-2) || '00';
+
+ if (this.max() && Number(val) * 10 > this.max()) {
+ this.moveRequest(event, 'next');
+ }
+
+ if (this.max() && Number(val) > this.max()) {
+ val = value.slice(-1);
+ }
+
+ event.target.value = val;
+ this.value.set(Number(val));
+ }
+
+ clearField(event: Event): void {
+ if (event instanceof KeyboardEvent) {
+ event.preventDefault();
+ }
+
+ this.value.set(0);
+ }
+
+ clickHandler(event: MouseEvent) {
+ event.preventDefault();
+ }
+
+ up(): void {
+ this.inputControlClick.emit('up');
+ }
+
+ down(): void {
+ this.inputControlClick.emit('down');
+ }
+
+ keydownHandler(event: KeyboardEvent): void {
+ switch (event.key) {
+ case 'ArrowLeft':
+ this.moveRequest(event, 'prev');
+ break;
+ case 'H':
+ case 'h':
+ case ':':
+ case 'ArrowRight':
+ this.moveRequest(event, 'next');
+ break;
+ case 'Delete':
+ case 'Backspace':
+ this.clearField(event);
+ break;
+ case 'ArrowUp':
+ this.arrowKeyPressed(event, true);
+ break;
+ case 'ArrowDown':
+ this.arrowKeyPressed(event, false);
+ break;
+ }
+ }
+
+ moveRequest(event: Event, direction: 'prev' | 'next'): void {
+ event.preventDefault();
+
+ if (direction === 'prev') {
+ this.prevRequest.emit();
+ } else {
+ this.nextRequest.emit();
+ }
+ }
+
+ focus(): void {
+ if (this.timePickerInput) {
+ this.timePickerInput.nativeElement.focus();
+ }
+ }
+}
diff --git a/packages/ng/time/duration-picker/duration-picker.component.html b/packages/ng/time/duration-picker/duration-picker.component.html
new file mode 100644
index 0000000000..22a9c333ef
--- /dev/null
+++ b/packages/ng/time/duration-picker/duration-picker.component.html
@@ -0,0 +1,32 @@
+
+
+
diff --git a/packages/ng/time/duration-picker/duration-picker.component.ts b/packages/ng/time/duration-picker/duration-picker.component.ts
new file mode 100644
index 0000000000..b75d7cc566
--- /dev/null
+++ b/packages/ng/time/duration-picker/duration-picker.component.ts
@@ -0,0 +1,224 @@
+import { NgClass } from '@angular/common';
+import { booleanAttribute, ChangeDetectionStrategy, Component, computed, forwardRef, Input, input, model, output } from '@angular/core';
+import { NG_VALUE_ACCESSOR } from '@angular/forms';
+import { getIntl } from '@lucca-front/ng/core';
+import { BasePickerComponent } from '../core/base-picker.component';
+import { ISO8601Duration } from '../core/date-primitives';
+import { createDurationFromHoursAndMinutes, getHoursPartFromDuration, getMinutesPartFromDuration, isISO8601Duration, isoDurationToDateFnsDuration, isoDurationToSeconds } from '../core/duration.utils';
+import { ceilToNearest, circularize, floorToNearest, roundToNearest } from '../core/math.utils';
+import { isNil, isNotNil, PickerControlDirection } from '../core/misc.utils';
+import { TimePickerPartComponent } from '../core/time-picker-part.component';
+import { DEFAULT_TIME_DECIMAL_PIPE_FORMAT, DurationChangeEvent } from './duration-picker.model';
+import { LU_DURATION_PICKER_TRANSLATIONS } from './duration-picker.translate';
+
+@Component({
+ selector: 'lu-duration-picker',
+ standalone: true,
+ imports: [TimePickerPartComponent, NgClass],
+ templateUrl: './duration-picker.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => DurationPickerComponent),
+ multi: true,
+ },
+ ],
+})
+export class DurationPickerComponent extends BasePickerComponent {
+ protected intl = getIntl(LU_DURATION_PICKER_TRANSLATIONS);
+
+ value = model('PT0S');
+ max = input('PT99H');
+
+ displayArrows = input(false, { transform: booleanAttribute });
+
+ @Input() label: string;
+
+ hideZeroValue = input(false, { transform: booleanAttribute });
+
+ durationChange = output();
+
+ protected hours = computed(() => getHoursPartFromDuration(this.value()));
+ protected minutes = computed(() => getMinutesPartFromDuration(this.value()));
+ protected shouldHideValue = computed(() => this.hideZeroValue() && this.hours() === 0 && this.minutes() === 0);
+
+ protected pickerClasses = computed(() => {
+ return {
+ timePicker: true,
+ 'mod-stepper': this.displayArrows(),
+ 'mod-stepperHover': this.displayArrows(),
+ [`mod-${this.size()}`]: Boolean(this.size()),
+ };
+ });
+ protected fieldsetSuffixClasses = computed(() => {
+ return {
+ 'timePicker-fieldset-groupSeparator': true,
+ 'u-visibilityHidden': this.shouldHideValue(),
+ };
+ });
+ protected separator = this.intl.timePickerTimeSeparator;
+
+ protected hoursDecimalConf = DEFAULT_TIME_DECIMAL_PIPE_FORMAT;
+
+ writeValue(value: ISO8601Duration): void {
+ this.value.set(value || 'PT0S');
+ }
+
+ setDisabledState?(isDisabled: boolean): void {
+ this.disabled.set(isDisabled);
+ }
+
+ protected hoursInputHandler(value: number): void {
+ this.setTime({
+ previousValue: this.value(),
+ value: createDurationFromHoursAndMinutes(value, this.minutes()),
+ source: 'input',
+ });
+ }
+
+ protected minutesInputHandler(value: number): void {
+ this.setTime({
+ previousValue: this.value(),
+ value: createDurationFromHoursAndMinutes(this.hours(), value),
+ source: 'input',
+ });
+ }
+
+ protected copyHandler(event: ClipboardEvent): void {
+ event.preventDefault();
+ // write value to clipboard
+ const value = this.value();
+ if (isNotNil(value)) {
+ event.clipboardData?.setData('text/plain', value);
+ }
+ }
+
+ protected pasteHandler(event: ClipboardEvent): void {
+ event.preventDefault();
+ const pastedValue = event.clipboardData?.getData('text/plain');
+ if (isNotNil(pastedValue)) {
+ let value: ISO8601Duration;
+ // If it's an iso duration, handle as-is
+ if (isISO8601Duration(pastedValue)) {
+ value = pastedValue;
+ }
+ if (/\d?\dh\d\d/.test(pastedValue)) {
+ const split = pastedValue.split('h');
+ value = createDurationFromHoursAndMinutes(+split[0], +split[1]);
+ }
+ if (/\d?\d:\d\d/.test(pastedValue)) {
+ const split = pastedValue.split(':');
+ value = createDurationFromHoursAndMinutes(+split[0], +split[1]);
+ }
+ if (value) {
+ this.durationChange.emit({
+ previousValue: this.value(),
+ source: 'paste',
+ value,
+ });
+ this.value.set(value);
+ this.onChange?.(value);
+ }
+ }
+ }
+
+ override getHoursIncrement(): number {
+ const step = this.step();
+ if (isNil(step)) {
+ return 1;
+ }
+
+ const { hours } = isoDurationToDateFnsDuration(step);
+
+ if (hours === 0) {
+ return 1;
+ }
+
+ return 24 % hours === 0 ? hours : 1;
+ }
+
+ override getMinutesIncrement(): number | null {
+ const step = this.step();
+ if (isNil(step)) {
+ return 1;
+ }
+
+ const { minutes } = isoDurationToDateFnsDuration(step);
+
+ if (minutes === 0) {
+ return null;
+ }
+
+ return 60 % minutes === 0 ? minutes : 1;
+ }
+
+ private setTime(protoEvent: DurationChangeEvent): void {
+ let hoursPart = getHoursPartFromDuration(protoEvent.value);
+ const minutesPart = getMinutesPartFromDuration(protoEvent.value);
+
+ if (hoursPart < 0) {
+ hoursPart = getHoursPartFromDuration(this.max());
+ if (isoDurationToSeconds(createDurationFromHoursAndMinutes(hoursPart, minutesPart)) > isoDurationToSeconds(this.max())) {
+ // If current value with minutes is > max, decrement hours again
+ hoursPart--;
+ }
+ } else if (hoursPart > getHoursPartFromDuration(this.max())) {
+ hoursPart = 0;
+ }
+
+ const max = isoDurationToSeconds(this.max());
+
+ const candidateTimeAsSeconds = hoursPart * 3600 + minutesPart * 60;
+
+ const seconds = roundToNearest(circularize(candidateTimeAsSeconds, max), 60);
+
+ const hours = Math.floor(seconds / 3600);
+ const minutes = Math.floor((seconds % 3600) / 60);
+
+ const result = createDurationFromHoursAndMinutes(hours, minutes);
+
+ this.value.set(result);
+ this.onChange?.(result);
+ this.durationChange.emit({
+ ...protoEvent,
+ value: result,
+ });
+ }
+
+ protected inputControlClickHandler(part: 'hours' | 'minutes', direction: PickerControlDirection): void {
+ const val = this.value() ?? 'PT0S';
+ const hoursPart = getHoursPartFromDuration(val);
+ const minutesPart = getMinutesPartFromDuration(val);
+
+ const selectedPart = part === 'hours' ? hoursPart : minutesPart;
+ const selectedIncrement = part === 'hours' ? this.hoursIncrement() : this.minutesIncrement();
+ let modifiedVal: number;
+
+ if (selectedIncrement === null) {
+ return;
+ }
+
+ if (direction === 'up') {
+ modifiedVal = floorToNearest(selectedPart + selectedIncrement, selectedIncrement);
+ } else {
+ modifiedVal = ceilToNearest(selectedPart - selectedIncrement, selectedIncrement);
+ }
+
+ const newTime = part === 'hours' ? createDurationFromHoursAndMinutes(modifiedVal, minutesPart) : createDurationFromHoursAndMinutes(hoursPart, modifiedVal);
+
+ this.setTime({
+ source: 'control',
+ previousValue: this.value(),
+ value: newTime,
+ part,
+ direction,
+ });
+ }
+
+ public focus() {
+ if (isNotNil(this.hoursPart)) {
+ this.focusPart('hours');
+ }
+ }
+}
diff --git a/packages/ng/time/duration-picker/duration-picker.model.ts b/packages/ng/time/duration-picker/duration-picker.model.ts
new file mode 100644
index 0000000000..b214e75d69
--- /dev/null
+++ b/packages/ng/time/duration-picker/duration-picker.model.ts
@@ -0,0 +1,13 @@
+import { ISO8601Duration } from '../core/date-primitives';
+
+export type DurationChangeEvent = {
+ previousValue: ISO8601Duration | null;
+ value: ISO8601Duration;
+} & (
+ | {
+ source: 'input' | 'paste';
+ }
+ | { source: 'control'; part: 'minutes' | 'hours'; direction: 'up' | 'down' }
+);
+
+export const DEFAULT_TIME_DECIMAL_PIPE_FORMAT = '2.0-0';
diff --git a/packages/ng/time/duration-picker/duration-picker.translate.ts b/packages/ng/time/duration-picker/duration-picker.translate.ts
new file mode 100644
index 0000000000..3d3107041c
--- /dev/null
+++ b/packages/ng/time/duration-picker/duration-picker.translate.ts
@@ -0,0 +1,25 @@
+import { InjectionToken } from '@angular/core';
+import { ILuTranslation } from '../../core/translate';
+
+export const LU_DURATION_PICKER_TRANSLATIONS = new InjectionToken('LuDurationPickerTranslations', {
+ factory: () => luDurationPickerTranslations,
+});
+
+export type DurationPickerTranslations = {
+ timePickerHours: string;
+ timePickerTimeSeparator: string;
+ timePickerMinutes: string;
+};
+
+export const luDurationPickerTranslations: ILuTranslation = {
+ en: {
+ timePickerHours: 'hours',
+ timePickerTimeSeparator: 'h',
+ timePickerMinutes: 'minutes',
+ },
+ fr: {
+ timePickerHours: 'heures',
+ timePickerTimeSeparator: 'h',
+ timePickerMinutes: 'minutes',
+ },
+};
diff --git a/packages/ng/time/ng-package.json b/packages/ng/time/ng-package.json
new file mode 100644
index 0000000000..68facef35b
--- /dev/null
+++ b/packages/ng/time/ng-package.json
@@ -0,0 +1,7 @@
+{
+ "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
+ "lib": {
+ "entryFile": "public-api.ts",
+ "styleIncludePaths": ["../styles"]
+ }
+}
diff --git a/packages/ng/time/public-api.ts b/packages/ng/time/public-api.ts
new file mode 100644
index 0000000000..9471acc98e
--- /dev/null
+++ b/packages/ng/time/public-api.ts
@@ -0,0 +1 @@
+export * from './time-picker/time-picker.component';
diff --git a/packages/ng/time/time-picker/time-picker.component.html b/packages/ng/time/time-picker/time-picker.component.html
new file mode 100644
index 0000000000..1578d09064
--- /dev/null
+++ b/packages/ng/time/time-picker/time-picker.component.html
@@ -0,0 +1,33 @@
+
+
+
diff --git a/packages/ng/time/time-picker/time-picker.component.ts b/packages/ng/time/time-picker/time-picker.component.ts
new file mode 100644
index 0000000000..b28e704f29
--- /dev/null
+++ b/packages/ng/time/time-picker/time-picker.component.ts
@@ -0,0 +1,212 @@
+import { NgClass } from '@angular/common';
+import { booleanAttribute, ChangeDetectionStrategy, Component, computed, forwardRef, Input, input, model, output } from '@angular/core';
+import { NG_VALUE_ACCESSOR } from '@angular/forms';
+import { getIntl } from '@lucca-front/ng/core';
+import { BasePickerComponent } from '../core/base-picker.component';
+import { ISO8601Time } from '../core/date-primitives';
+import {
+ convertStringToIsoTime,
+ createIsoTimeFromHoursAndMinutes,
+ getHoursDisplayPartFromIsoTime,
+ getHoursPartFromIsoTime,
+ getMinutesDisplayPartFromIsoTime,
+ getMinutesPartFromIsoTime,
+ isoTimeToSeconds,
+} from '../core/date.utils';
+import { isoDurationToDateFnsDuration } from '../core/duration.utils';
+import { ceilToNearest, circularize, floorToNearest, roundToNearest } from '../core/math.utils';
+import { isNil, isNotNil, PickerControlDirection } from '../core/misc.utils';
+import { TimePickerPartComponent } from '../core/time-picker-part.component';
+import { DEFAULT_MIN_TIME, DEFAULT_TIME_DECIMAL_PIPE_FORMAT, TimeChangeEvent } from './time-picker.model';
+import { LU_TIME_PICKER_TRANSLATIONS } from './time-picker.translate';
+
+@Component({
+ selector: 'lu-time-picker',
+ standalone: true,
+ imports: [TimePickerPartComponent, NgClass],
+ templateUrl: './time-picker.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => TimePickerComponent),
+ multi: true,
+ },
+ ],
+})
+export class TimePickerComponent extends BasePickerComponent {
+ protected intl = getIntl(LU_TIME_PICKER_TRANSLATIONS);
+
+ value = model('--:--:--');
+ max = input(null);
+
+ displayArrows = input(false, { transform: booleanAttribute });
+
+ @Input() label: string;
+
+ timeChange = output();
+
+ protected hoursDisplay = computed(() => getHoursDisplayPartFromIsoTime(this.value()));
+ protected minutesDisplay = computed(() => getMinutesDisplayPartFromIsoTime(this.value()));
+
+ protected hours = computed(() => getHoursPartFromIsoTime(this.value()));
+ protected minutes = computed(() => getMinutesPartFromIsoTime(this.value()));
+ protected pickerClasses = computed(() => {
+ return {
+ timePicker: true,
+ 'mod-stepper': this.displayArrows(),
+ 'mod-stepperHover': this.displayArrows(),
+ [`mod-${this.size()}`]: Boolean(this.size()),
+ };
+ });
+ protected separator = this.intl.timePickerTimeSeparator;
+
+ protected hoursDecimalConf = DEFAULT_TIME_DECIMAL_PIPE_FORMAT;
+
+ protected maxHours = computed(() => {
+ return getHoursPartFromIsoTime(this.max() ?? '23:59:59');
+ });
+
+ writeValue(value: ISO8601Time): void {
+ this.value.set(value || '--:--:--');
+ }
+
+ setDisabledState?(isDisabled: boolean): void {
+ this.disabled.set(isDisabled);
+ }
+
+ protected hoursInputHandler(value: number | '--'): void {
+ this.setTime({
+ previousValue: this.value(),
+ // Mostly for compiler because we never set the time to -- with input handler
+ value: createIsoTimeFromHoursAndMinutes(+value, this.minutes()),
+ source: 'input',
+ });
+ }
+
+ protected minutesInputHandler(value: number | '--'): void {
+ this.setTime({
+ previousValue: this.value(),
+ // Mostly for compiler because we never set the time to -- with input handler
+ value: createIsoTimeFromHoursAndMinutes(this.hours(), +value),
+ source: 'input',
+ });
+ }
+
+ protected copyHandler(event: ClipboardEvent): void {
+ event.preventDefault();
+ // write value to clipboard
+ const value = this.value();
+ if (isNotNil(value)) {
+ event.clipboardData?.setData('text/plain', value);
+ }
+ }
+
+ protected pasteHandler(event: ClipboardEvent): void {
+ event.preventDefault();
+ const pastedValue = event.clipboardData?.getData('text/plain');
+ if (isNotNil(pastedValue)) {
+ try {
+ const value = convertStringToIsoTime(pastedValue);
+ this.timeChange.emit({
+ previousValue: this.value(),
+ source: 'paste',
+ value,
+ });
+ this.value.set(value);
+ this.onChange?.(value);
+ } catch (e) {
+ // do nothing
+ }
+ }
+ }
+
+ override getHoursIncrement(): number {
+ const step = this.step();
+ if (isNil(step)) {
+ return 1;
+ }
+
+ const { hours } = isoDurationToDateFnsDuration(step);
+
+ if (hours === 0) {
+ return 1;
+ }
+
+ return 24 % hours === 0 ? hours : 1;
+ }
+
+ override getMinutesIncrement(): number | null {
+ const step = this.step();
+ if (isNil(step)) {
+ return 1;
+ }
+
+ const { minutes } = isoDurationToDateFnsDuration(step);
+
+ if (minutes === 0) {
+ return null;
+ }
+
+ return 60 % minutes === 0 ? minutes : 1;
+ }
+
+ private setTime(protoEvent: TimeChangeEvent): void {
+ const hoursPart = getHoursPartFromIsoTime(protoEvent.value);
+ const minutesPart = getMinutesPartFromIsoTime(protoEvent.value);
+
+ const max = isoTimeToSeconds(this.max());
+
+ const candidateTimeAsSeconds = hoursPart * 3600 + minutesPart * 60;
+
+ const seconds = roundToNearest(circularize(candidateTimeAsSeconds, max), 60);
+
+ const hours = Math.floor(seconds / 3600);
+ const minutes = Math.floor((seconds % 3600) / 60);
+
+ const result = createIsoTimeFromHoursAndMinutes(hours, minutes);
+
+ this.value.set(result);
+ this.onChange?.(result);
+ this.timeChange.emit({
+ ...protoEvent,
+ value: result,
+ });
+ }
+
+ protected inputControlClickHandler(part: 'hours' | 'minutes', direction: PickerControlDirection): void {
+ const val = this.value() ?? DEFAULT_MIN_TIME;
+ const hoursPart = getHoursPartFromIsoTime(val);
+ const minutesPart = getMinutesPartFromIsoTime(val);
+
+ const selectedPart = part === 'hours' ? hoursPart : minutesPart;
+ const selectedIncrement = part === 'hours' ? this.hoursIncrement() : this.minutesIncrement();
+ let modifiedVal: number;
+
+ if (selectedIncrement === null) {
+ return;
+ }
+
+ if (direction === 'up') {
+ modifiedVal = floorToNearest(selectedPart + selectedIncrement, selectedIncrement);
+ } else {
+ modifiedVal = ceilToNearest(selectedPart - selectedIncrement, selectedIncrement);
+ }
+
+ const newTime = part === 'hours' ? createIsoTimeFromHoursAndMinutes(modifiedVal, minutesPart) : createIsoTimeFromHoursAndMinutes(hoursPart, modifiedVal);
+
+ this.setTime({
+ source: 'control',
+ previousValue: this.value(),
+ value: newTime,
+ part,
+ direction,
+ });
+ }
+
+ public focus() {
+ if (isNotNil(this.hoursPart)) {
+ this.focusPart('hours');
+ }
+ }
+}
diff --git a/packages/ng/time/time-picker/time-picker.model.ts b/packages/ng/time/time-picker/time-picker.model.ts
new file mode 100644
index 0000000000..4f554d792a
--- /dev/null
+++ b/packages/ng/time/time-picker/time-picker.model.ts
@@ -0,0 +1,14 @@
+import { ISO8601Time } from '../core/date-primitives';
+
+export type TimeChangeEvent = {
+ previousValue: ISO8601Time | null;
+ value: ISO8601Time;
+} & (
+ | {
+ source: 'input' | 'paste';
+ }
+ | { source: 'control'; part: 'minutes' | 'hours'; direction: 'up' | 'down' }
+);
+export const DEFAULT_MIN_TIME: ISO8601Time = '--:--:--';
+
+export const DEFAULT_TIME_DECIMAL_PIPE_FORMAT = '2.0-0';
diff --git a/packages/ng/time/time-picker/time-picker.translate.ts b/packages/ng/time/time-picker/time-picker.translate.ts
new file mode 100644
index 0000000000..4572696202
--- /dev/null
+++ b/packages/ng/time/time-picker/time-picker.translate.ts
@@ -0,0 +1,25 @@
+import { InjectionToken } from '@angular/core';
+import { ILuTranslation } from '@lucca-front/ng/core';
+
+export const LU_TIME_PICKER_TRANSLATIONS = new InjectionToken('LuTimePickerTranslations', {
+ factory: () => luTimePickerTranslations,
+});
+
+export type TimePickerTranslations = {
+ timePickerHours: string;
+ timePickerTimeSeparator: string;
+ timePickerMinutes: string;
+};
+
+export const luTimePickerTranslations: ILuTranslation = {
+ en: {
+ timePickerHours: 'hours',
+ timePickerTimeSeparator: ':',
+ timePickerMinutes: 'minutes',
+ },
+ fr: {
+ timePickerHours: 'heures',
+ timePickerTimeSeparator: ':',
+ timePickerMinutes: 'minutes',
+ },
+};
diff --git a/packages/ng/toast/toasts.component.html b/packages/ng/toast/toasts.component.html
index 875fb7d366..79a3fc260c 100644
--- a/packages/ng/toast/toasts.component.html
+++ b/packages/ng/toast/toasts.component.html
@@ -1,17 +1,22 @@
-
+
{{ toast.title }}
+ @if (isStringPortalContent(toast.message)) {
+ } @else {
+
+ }
-
diff --git a/packages/ng/toast/toasts.component.ts b/packages/ng/toast/toasts.component.ts
index 4baab85f52..7b7dbf559d 100644
--- a/packages/ng/toast/toasts.component.ts
+++ b/packages/ng/toast/toasts.component.ts
@@ -1,6 +1,6 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, Input, OnDestroy } from '@angular/core';
-import { getIntl } from '@lucca-front/ng/core';
+import { getIntl, PortalContent, PortalDirective } from '@lucca-front/ng/core';
import { merge, Observable, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { LuToast, LuToastInput, LuToastType } from './toasts.model';
@@ -12,7 +12,7 @@ import { LU_TOAST_TRANSLATIONS } from './toasts.translate';
templateUrl: './toasts.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
- imports: [CommonModule],
+ imports: [CommonModule, PortalDirective],
})
export class LuToastsComponent implements OnDestroy {
@Input() public bottom = false;
@@ -49,6 +49,10 @@ export class LuToastsComponent implements OnDestroy {
Warning: 'palette-warning',
};
+ public isStringPortalContent(message: PortalContent): message is string {
+ return typeof message === 'string';
+ }
+
public removeToast(toast: LuToast): void {
this.toastsService.removeToast(toast);
}
diff --git a/packages/ng/toast/toasts.model.ts b/packages/ng/toast/toasts.model.ts
index 237c7dd098..c39b7cc108 100644
--- a/packages/ng/toast/toasts.model.ts
+++ b/packages/ng/toast/toasts.model.ts
@@ -1,3 +1,5 @@
+import { PortalContent } from '@lucca-front/ng/core';
+
export type LuToastType = 'Info' | 'Error' | 'Success' | 'Warning';
export const defaultToastDuration = 5000;
@@ -6,7 +8,7 @@ export interface LuToastInput {
/**
* InnerHTML supported.
*/
- message: string;
+ message: PortalContent;
/**
* Bold title.
* InnerHTML not supported.
diff --git a/packages/ng/tooltip/panel/tooltip-panel.component.html b/packages/ng/tooltip/panel/tooltip-panel.component.html
index cb157bfdf5..fd3c3570e2 100644
--- a/packages/ng/tooltip/panel/tooltip-panel.component.html
+++ b/packages/ng/tooltip/panel/tooltip-panel.component.html
@@ -1,11 +1 @@
-
+
diff --git a/packages/ng/tooltip/panel/tooltip-panel.component.scss b/packages/ng/tooltip/panel/tooltip-panel.component.scss
index e232ed1c40..ded932376d 100644
--- a/packages/ng/tooltip/panel/tooltip-panel.component.scss
+++ b/packages/ng/tooltip/panel/tooltip-panel.component.scss
@@ -1,2 +1 @@
-@import '_definitions';
-@include tooltipStyle;
+@use '@lucca-front/scss/src/components/tooltip';
diff --git a/packages/ng/tooltip/panel/tooltip-panel.component.ts b/packages/ng/tooltip/panel/tooltip-panel.component.ts
index 5897215097..9a79226022 100644
--- a/packages/ng/tooltip/panel/tooltip-panel.component.ts
+++ b/packages/ng/tooltip/panel/tooltip-panel.component.ts
@@ -1,54 +1,50 @@
-import { OverlayModule } from '@angular/cdk/overlay';
-import { CommonModule } from '@angular/common';
-import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, HostBinding, Output, TemplateRef, ViewChild } from '@angular/core';
+import { HorizontalConnectionPos, VerticalConnectionPos } from '@angular/cdk/overlay';
+import { ChangeDetectionStrategy, Component, DestroyRef, HostBinding, HostListener, inject } from '@angular/core';
import { SafeHtml } from '@angular/platform-browser';
-import { ALuPopoverPanel, ILuPopoverPanel } from '@lucca-front/ng/popover';
-import { luTransformTooltip } from '../animation/index';
+import { Subject } from 'rxjs';
+import { NgClass } from '@angular/common';
@Component({
selector: 'lu-tooltip-panel',
templateUrl: './tooltip-panel.component.html',
styleUrls: ['./tooltip-panel.component.scss'],
- animations: [luTransformTooltip],
standalone: true,
- imports: [CommonModule, OverlayModule],
+ host: {
+ role: 'tooltip',
+ },
changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [NgClass],
})
-export class LuTooltipPanelComponent extends ALuPopoverPanel implements ILuPopoverPanel {
- @HostBinding('@transformTooltip') animationState = 'enter';
+export class LuTooltipPanelComponent {
+ destroyRef = inject(DestroyRef);
- private _content: string | SafeHtml;
- get content() {
- return this._content;
- }
- set content(c) {
- this._content = c;
- this._changeDetectorRef.markForCheck();
- }
+ mouseEnter$ = new Subject
();
- //FIXME output event
- // eslint-disable-next-line @angular-eslint/no-output-native
- @Output() override close = new EventEmitter();
- // eslint-disable-next-line @angular-eslint/no-output-native
- @Output() override open = new EventEmitter();
- @Output() override hovered = new EventEmitter();
- @ViewChild(TemplateRef, { static: true })
- set vcTemplateRef(tr: TemplateRef) {
- this.templateRef = tr;
- }
- constructor(private _changeDetectorRef: ChangeDetectorRef) {
- super();
- this.scrollStrategy = 'close';
+ @HostListener('mouseenter')
+ mouseEnter(): void {
+ this.mouseEnter$.next();
}
- _emitCloseEvent(): void {
- this.close.emit();
- }
+ mouseLeave$ = new Subject();
- _emitOpenEvent(): void {
- this.open.emit();
+ @HostListener('mouseleave')
+ mouseLeave(): void {
+ this.mouseLeave$.next();
}
- _emitHoveredEvent(hovered: boolean): void {
- this.hovered.emit(hovered);
+
+ content: string | SafeHtml;
+
+ @HostBinding('attr.id')
+ id: string;
+
+ contentPositionClasses: Record = {};
+
+ setPanelPosition(posX: HorizontalConnectionPos, posY: VerticalConnectionPos): void {
+ this.contentPositionClasses = {
+ 'is-before': posX === 'end',
+ 'is-after': posX === 'start',
+ 'is-above': posY === 'bottom',
+ 'is-below': posY === 'top',
+ };
}
}
diff --git a/packages/ng/tooltip/trigger/tooltip-trigger.directive.spec.ts b/packages/ng/tooltip/trigger/tooltip-trigger.directive.spec.ts
deleted file mode 100644
index b28a646881..0000000000
--- a/packages/ng/tooltip/trigger/tooltip-trigger.directive.spec.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-import { Component } from '@angular/core';
-import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
-import { By, DomSanitizer, SafeHtml } from '@angular/platform-browser';
-import { NoopAnimationsModule } from '@angular/platform-browser/animations';
-import { LuTooltipModule } from '../tooltip.module';
-import { LuTooltipTriggerDirective } from './tooltip-trigger.directive';
-
-@Component({
- standalone: true,
- imports: [LuTooltipModule],
- selector: 'lu-test',
- template: ``,
-})
-export class LuTestComponent {
- content: string | SafeHtml = '';
-}
-
-describe('LuTooltipTriggerDirective', () => {
- let component: LuTestComponent;
- let fixture: ComponentFixture;
- let tooltipTrigger: LuTooltipTriggerDirective;
-
- beforeEach(async () => {
- await TestBed.configureTestingModule({
- imports: [LuTestComponent, NoopAnimationsModule],
- }).compileComponents();
- });
-
- beforeEach(() => {
- fixture = TestBed.createComponent(LuTestComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
-
- tooltipTrigger = fixture.debugElement.query(By.directive(LuTooltipTriggerDirective)).injector.get(LuTooltipTriggerDirective);
- });
-
- it('should not append unsafe html in dom', fakeAsync(() => {
- // Arrange
- component.content = '';
-
- // Act
- tooltipTrigger.onMouseEnter();
- tick(tooltipTrigger.enterDelay);
- fixture.detectChanges();
-
- // Assert
- const images = fixture.debugElement.queryAll(By.css('img')).map((el) => (el.nativeElement instanceof HTMLElement ? el.nativeElement : null));
-
- expect(images.length).toBe(1);
- expect(images[0].getAttribute('onerror')).toBe(null);
- }));
-
- it('should allow trusted html', fakeAsync(() => {
- // Arrange
- const sanitizer = TestBed.inject(DomSanitizer);
- component.content = sanitizer.bypassSecurityTrustHtml('');
-
- // Act
- tooltipTrigger.onMouseEnter();
- tick(tooltipTrigger.enterDelay);
- fixture.detectChanges();
-
- // Assert
- const images = fixture.debugElement.queryAll(By.css('img')).map((el) => (el.nativeElement instanceof HTMLElement ? el.nativeElement : null));
-
- expect(images.length).toBe(1);
- expect(images[0].getAttribute('onerror')).toBe('hack()');
- }));
-});
diff --git a/packages/ng/tooltip/trigger/tooltip-trigger.directive.ts b/packages/ng/tooltip/trigger/tooltip-trigger.directive.ts
index bfad9d1b5d..2c6e53f890 100644
--- a/packages/ng/tooltip/trigger/tooltip-trigger.directive.ts
+++ b/packages/ng/tooltip/trigger/tooltip-trigger.directive.ts
@@ -1,160 +1,350 @@
-import { FlexibleConnectedPositionStrategy, Overlay, OverlayRef } from '@angular/cdk/overlay';
+import { FlexibleConnectedPositionStrategy, HorizontalConnectionPos, OriginConnectionPosition, Overlay, OverlayConnectionPosition, OverlayRef, VerticalConnectionPos } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
-import { AfterViewInit, Directive, ElementRef, EventEmitter, HostBinding, HostListener, Input, OnDestroy, Output, ViewContainerRef } from '@angular/core';
+import { AfterContentInit, ChangeDetectorRef, DestroyRef, Directive, ElementRef, HostBinding, HostListener, Input, Renderer2, booleanAttribute, inject, numberAttribute } from '@angular/core';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { SafeHtml } from '@angular/platform-browser';
-import { ALuPopoverTrigger, LuPopoverPosition, LuPopoverScrollStrategy, LuPopoverTarget } from '@lucca-front/ng/popover';
-import { LuTooltipPanelComponent } from '../panel/tooltip-panel.component';
+import { LuPopoverPosition } from '@lucca-front/ng/popover';
+import { BehaviorSubject, Observable, Subject, combineLatest, merge, startWith, switchMap, timer } from 'rxjs';
+import { debounce, debounceTime, filter, map } from 'rxjs/operators';
+import { LuTooltipPanelComponent } from '../panel';
+
+let nextId = 0;
@Directive({
selector: '[luTooltip]',
standalone: true,
})
-export class LuTooltipTriggerDirective extends ALuPopoverTrigger implements AfterViewInit, OnDestroy {
- @Input('luTooltip') set tooltipContent(c: string | SafeHtml) {
- if (this.panel) {
- this.panel.content = c;
- }
+export class LuTooltipTriggerDirective implements AfterContentInit {
+ #overlay = inject(Overlay);
- this._tooltipContent = c;
- }
- /** when trigger = hover, delay before the popover panel appears, default 300ms */
- @Input('luTooltipEnterDelay') set inputEnterDelay(d: number) {
- this.enterDelay = d;
+ #host = inject>(ElementRef);
+
+ #renderer = inject(Renderer2);
+
+ #destroyRef = inject(DestroyRef);
+
+ #cdr = inject(ChangeDetectorRef);
+
+ @Input()
+ luTooltip: string | SafeHtml;
+
+ #openDelay$ = new BehaviorSubject(300);
+
+ @Input({ transform: numberAttribute })
+ set luTooltipEnterDelay(delay: number) {
+ this.#openDelay$.next(delay);
}
- /** when trigger = hover, delay before the popover panel disappears, default 100ms */
- @Input('luTooltipLeaveDelay') set inputLeaveDelay(d: number) {
- this.leaveDelay = d;
+
+ #closeDelay$ = new BehaviorSubject(100);
+
+ @Input({ transform: numberAttribute })
+ set luTooltipLeaveDelay(delay: number) {
+ this.#closeDelay$.next(delay);
}
- /** disable popover apparition */
- @Input('luTooltipDisabled') set inputDisabled(d: boolean) {
- this.disabled = d;
- if (this._handleTabindex) {
- this._setTabindex(d ? null : 0);
+
+ #disabled = false;
+
+ @Input({ transform: booleanAttribute })
+ set luTooltipDisabled(disabled: boolean) {
+ this.#disabled = disabled;
+ if (disabled) {
+ this.setAccessibilityProperties(null);
}
}
- @Input('luTooltipPosition') set inputPosition(pos: LuPopoverPosition) {
- this.target.position = pos;
- }
+ @Input()
+ luTooltipPosition: LuPopoverPosition = 'above';
- @Input('luTooltipWhenEllipsis') public set inputWhenEllipsis(we: boolean) {
- this.whenEllipsis = we;
- }
+ @Input({ transform: booleanAttribute })
+ luTooltipWhenEllipsis = false;
+
+ resize$ = new Observable((observer) => {
+ const resizeObserver = new ResizeObserver(() => {
+ observer.next();
+ });
+ resizeObserver.observe(this.#host.nativeElement);
+ return () => {
+ resizeObserver.disconnect();
+ };
+ });
- // FIXME output native
- /** Event emitted when the associated popover is opened. */
- // eslint-disable-next-line @angular-eslint/no-output-on-prefix
- @Output('luTooltipOnOpen') onOpen = new EventEmitter();
- /** Event emitted when the associated popover is closed. */
- // eslint-disable-next-line @angular-eslint/no-output-on-prefix
- @Output('luTooltipOnClose') onClose = new EventEmitter();
+ open$ = new Subject();
+
+ close$ = new Subject();
@HostListener('mouseenter')
- override onMouseEnter() {
- super.onMouseEnter();
+ onMouseEnter() {
+ this.open$.next();
}
+
@HostListener('mouseleave')
- override onMouseLeave() {
- super.onMouseLeave();
+ onMouseLeave() {
+ this.close$.next();
}
+
@HostListener('focus')
- override onFocus() {
- super.onFocus();
+ onFocus() {
+ this.open$.next();
}
+
@HostListener('blur')
- override onBlur() {
- super.onBlur();
+ onBlur() {
+ this.close$.next();
}
- private _handleTabindex = false;
- // @HostBinding('attr.tabindex') tabindex;
- // private set tabindex(i: number = null) {
- // }
+ #generatedId = `${this.#host.nativeElement.tagName.toLowerCase()}-tooltip-${nextId++}`;
- /** accessibility attribute - dont override */
- @HostBinding('attr.id') get _attrId() {
- return this._triggerId;
- }
- /** accessibility attribute - dont override */
- @HostBinding('attr.aria-describedby') get _attrAriaDescribedBy() {
- return this._panelId;
+ @HostBinding('attr.id')
+ _id: string;
+
+ @HostBinding('attr.aria-describedby')
+ get ariaDescribedBy() {
+ if (this.#disabled || this.luTooltipWhenEllipsis) {
+ return null;
+ }
+ return `${this.#generatedId}-panel`;
}
- protected override _portal: ComponentPortal;
- protected _tooltipContent: string | SafeHtml = '';
+ overlayRef?: OverlayRef;
- constructor(
- protected override _overlay: Overlay,
- protected override _elementRef: ElementRef,
- protected override _viewContainerRef: ViewContainerRef,
- ) {
- super(_overlay, _elementRef, _viewContainerRef);
- this.target = new LuPopoverTarget();
- this.target.elementRef = this._elementRef;
- this._triggerId = this._elementRef.nativeElement.getAttribute('id') || this._triggerId;
- this.triggerEvent = 'hover';
- this.target.position = 'above';
- this.enterDelay = 300;
- this.leaveDelay = 100;
+ constructor() {
+ combineLatest([this.#openDelay$, this.#closeDelay$])
+ .pipe(
+ switchMap(([openDelay, closeDelay]) => {
+ // We only filter open events because even if it's disabled while opened,
+ // we want the tooltip to be able to close itself no matter what
+ const openEvents$ = this.open$.pipe(
+ filter(() => {
+ if (this.#disabled) {
+ return false;
+ }
+ // If not disabled, let's check for ellipsis if needed
+ if (this.luTooltipWhenEllipsis) {
+ return this.hasEllipsis();
+ }
+ // If it's not disabled and is not triggered based on ellipsis, just return true
+ return true;
+ }),
+ map(() => 'open'),
+ );
+ return merge(openEvents$, this.close$.pipe(map(() => 'close'))).pipe(
+ debounce((event) => {
+ return timer(event === 'open' ? openDelay : closeDelay);
+ }),
+ );
+ }),
+ takeUntilDestroyed(this.#destroyRef),
+ )
+ .subscribe((event) => {
+ if (event === 'open') {
+ this.openTooltip();
+ } else {
+ this.closeTooltip();
+ }
+ });
- this._handleTabindex = this._shouldHandleTabindex();
+ this.resize$.pipe(takeUntilDestroyed(this.#destroyRef), debounceTime(150)).subscribe(() => {
+ this.setAccessibilityProperties(0);
+ this.#cdr.markForCheck();
+ });
+ }
- if (this._handleTabindex) {
- this._setTabindex(0);
+ private openTooltip(): void {
+ // If tooltip is already opened, don't do anything
+ if (this.overlayRef) {
+ return;
}
+ const position = this.legacyPositionBuilder();
+ this.overlayRef = this.#overlay.create({
+ positionStrategy: position,
+ scrollStrategy: this.#overlay.scrollStrategies.close(),
+ disposeOnNavigation: true,
+ });
+ const portal = new ComponentPortal(LuTooltipPanelComponent);
+ const ref = this.overlayRef.attach(portal);
+ position.positionChanges
+ .pipe(
+ takeUntilDestroyed(this.#destroyRef),
+ map(({ connectionPair }) => connectionPair),
+ startWith(position.positions[0]),
+ )
+ .subscribe(({ overlayX, overlayY }) => {
+ ref.instance.setPanelPosition(overlayX, overlayY);
+ });
+ if (this.luTooltip) {
+ ref.instance.content = this.luTooltip;
+ } else if (this.luTooltipWhenEllipsis) {
+ ref.instance.content = this.#host.nativeElement.innerText;
+ }
+ ref.instance.id = this.ariaDescribedBy;
+ // On tooltip leave => trigger close
+ ref.instance.mouseLeave$.pipe(takeUntilDestroyed(ref.instance.destroyRef)).subscribe(() => this.close$.next());
+ // On tooltip enter => trigger open to keep it opened
+ ref.instance.mouseEnter$.pipe(takeUntilDestroyed(ref.instance.destroyRef)).subscribe(() => this.open$.next());
}
- ngAfterViewInit() {
- this._checkTarget();
- }
- ngOnDestroy() {
- this._cleanUpSubscriptions();
- if (this._popoverOpen) {
- this.closePopover();
+ private closeTooltip(): void {
+ if (this.overlayRef) {
+ this.overlayRef.detach();
+ delete this.overlayRef;
}
- this.destroyPopover();
- }
- protected _emitOpen(): void {
- this.onOpen.emit();
- }
- protected _emitClose(): void {
- this.onClose.emit();
}
- protected override _createOverlay(): OverlayRef {
- if (!this._overlayRef) {
- this._portal = new ComponentPortal(LuTooltipPanelComponent, this._viewContainerRef);
- const config = this._getOverlayConfig();
- this._subscribeToPositions(config.positionStrategy as FlexibleConnectedPositionStrategy);
- this._overlayRef = this._overlay.create(config);
+ private setAccessibilityProperties(tabindex: number | null): void {
+ if (this.#disabled || (this.luTooltipWhenEllipsis && !this.hasEllipsis())) {
+ this.#renderer.removeAttribute(this.#host.nativeElement, 'tabindex');
+ return;
}
- return this._overlayRef;
+ const tag = this.#host.nativeElement.tagName.toLowerCase();
+ const nativelyFocusableTags = ['a', 'button', 'input', 'select', 'textarea'];
+ const isNatevelyFocusableTag = nativelyFocusableTags.includes(tag);
+
+ const hasATabIndex = this.#host.nativeElement.getAttribute('tabindex') !== null;
+
+ if (!isNatevelyFocusableTag && !hasATabIndex) {
+ this.#renderer.setAttribute(this.#host.nativeElement, 'tabindex', tabindex.toString());
+ }
}
- protected override _attachPortalToOverlay(): void {
- const componentRef = this._overlayRef.attach(this._portal);
- this._panel = componentRef.instance;
- this._panel.content = this._tooltipContent;
+ /**
+ * Hacky af but let's explain everything
+ * This method checks for ellipsis by cloning the node and checking its width against original element.
+ *
+ * We used to do this using scrollWidth but the thing is, it's a rounded value. Sometimes,
+ * you'd get true while it should be false and vice-versa, because of rounding.
+ *
+ * We could also use getBoundingClientRect() but it only considers text content, meaning that if ellipsis is caused by
+ * any margin or padding, it won't be detected
+ *
+ * @private
+ */
+ private hasEllipsis(): boolean {
+ if (window.getComputedStyle(this.#host.nativeElement).textOverflow !== 'ellipsis') {
+ return false;
+ }
+
+ const mask = this.#renderer.createElement('div') as HTMLDivElement;
+ const clone = this.#host.nativeElement.cloneNode(true) as HTMLElement;
+
+ this.#renderer.setStyle(clone, 'position', 'fixed');
+ this.#renderer.setStyle(clone, 'overflow', 'visible');
+ this.#renderer.setStyle(clone, 'white-space', 'nowrap');
+ this.#renderer.setStyle(clone, 'visibility', 'hidden');
+ this.#renderer.setStyle(clone, 'width', 'fit-content');
+
+ this.#renderer.addClass(mask, 'u-mask');
+ this.#renderer.setAttribute(mask, 'aria-hidden', 'true');
+ this.#renderer.appendChild(mask, clone);
+
+ this.#renderer.appendChild(this.#host.nativeElement.parentElement, mask);
+ try {
+ const fullWidth = clone.getBoundingClientRect().width;
+ const displayWidth = this.#host.nativeElement.getBoundingClientRect().width;
+
+ return fullWidth > displayWidth;
+ } catch (e) {
+ return false;
+ } finally {
+ mask.remove();
+ }
}
- protected override _getPanelScrollStrategy(): LuPopoverScrollStrategy {
- return 'close';
+ ngAfterContentInit(): void {
+ this.setAccessibilityProperties(0);
+ this._id = this.#host.nativeElement.id || this.#generatedId;
}
- private _shouldHandleTabindex(): boolean {
- const tag = this._elementRef.nativeElement.tagName?.toLowerCase();
- // https://allyjs.io/data-tables/focusable.html
- // i'm choosing to not support area and iframe, dont @ me
- const nativelyFocusableTags = ['a', 'button', 'input', 'select', 'textarea'];
- const isNatevelyFocusableTag = nativelyFocusableTags.includes(tag);
+ /**********************
+ *
+ * LEGACY STUFF TO HANDLE EXISTING POSITIONS
+ *
+ ***********************/
+
+ private legacyPositionBuilder(): FlexibleConnectedPositionStrategy {
+ const connectionPosition: OriginConnectionPosition = {
+ originX: 'start',
+ originY: 'top',
+ };
+
+ // Position
+ const position = this.luTooltipPosition;
+ if (position === 'above') {
+ connectionPosition.originY = 'top';
+ } else if (position === 'below') {
+ connectionPosition.originY = 'bottom';
+ } else if (position === 'before') {
+ connectionPosition.originX = 'start';
+ } else if (position === 'after') {
+ connectionPosition.originX = 'end';
+ }
+
+ // Alignment
+ if (position === 'above' || position === 'below') {
+ connectionPosition.originX = 'center';
+ } else {
+ connectionPosition.originY = 'center';
+ }
- const hasATabIndex = this._elementRef.nativeElement.getAttribute('tabindex') !== null;
+ const overlayPosition: OverlayConnectionPosition = {
+ overlayX: 'start',
+ overlayY: 'top',
+ };
- return !isNatevelyFocusableTag && !hasATabIndex;
+ if (position === 'above' || position === 'below') {
+ overlayPosition.overlayX = connectionPosition.originX;
+ overlayPosition.overlayY = position === 'above' ? 'bottom' : 'top';
+ } else {
+ overlayPosition.overlayX = position === 'before' ? 'end' : 'start';
+ overlayPosition.overlayY = connectionPosition.originY;
+ }
+
+ return this.#overlay
+ .position()
+ .flexibleConnectedTo(this.#host)
+ .withPositions([
+ {
+ originX: connectionPosition.originX,
+ originY: connectionPosition.originY,
+ overlayX: overlayPosition.overlayX,
+ overlayY: overlayPosition.overlayY,
+ },
+ {
+ originX: connectionPosition.originX,
+ originY: this.invertVerticalPos(connectionPosition.originY),
+ overlayX: overlayPosition.overlayX,
+ overlayY: this.invertVerticalPos(overlayPosition.overlayY),
+ },
+ {
+ originX: this.invertHorizontalPos(connectionPosition.originX),
+ originY: connectionPosition.originY,
+ overlayX: this.invertHorizontalPos(overlayPosition.overlayX),
+ overlayY: overlayPosition.overlayY,
+ },
+ {
+ originX: this.invertHorizontalPos(connectionPosition.originX),
+ originY: this.invertVerticalPos(connectionPosition.originY),
+ overlayX: this.invertHorizontalPos(overlayPosition.overlayX),
+ overlayY: this.invertVerticalPos(overlayPosition.overlayY),
+ },
+ ]);
+ }
+
+ private invertVerticalPos(y: VerticalConnectionPos): VerticalConnectionPos {
+ if (y === 'top') {
+ return 'bottom';
+ } else if (y === 'bottom') {
+ return 'top';
+ }
+ return y;
}
- private _setTabindex(i: number = null): void {
- this._elementRef.nativeElement.setAttribute('tabindex', `${i}`);
+ private invertHorizontalPos(x: HorizontalConnectionPos): HorizontalConnectionPos {
+ if (x === 'end') {
+ return 'start';
+ } else if (x === 'start') {
+ return 'end';
+ }
+ return x;
}
}
diff --git a/packages/ng/user/display/display-format.model.ts b/packages/ng/user/display/display-format.model.ts
index 39cac4d499..786c185983 100644
--- a/packages/ng/user/display/display-format.model.ts
+++ b/packages/ng/user/display/display-format.model.ts
@@ -1,4 +1,5 @@
import { InjectionToken } from '@angular/core';
+import { EnumValue } from '@lucca-front/ng/core';
export enum LuDisplayFullname {
firstlast = 'fl',
@@ -21,7 +22,7 @@ export enum LuDisplayHybrid {
lastFullfirstI = 'lF',
}
-export type LuDisplayFormat = LuDisplayFullname | LuDisplayInitials | LuDisplayHybrid;
+export type LuDisplayFormat = EnumValue | EnumValue | EnumValue;
/** Injection token that can be used to change the default displayed user format. */
export const LU_DEFAULT_DISPLAY_POLICY = new InjectionToken('LuDisplayFormat', { factory: () => LuDisplayFullname.lastfirst });
diff --git a/packages/ng/user/display/user-display.pipe.spec.ts b/packages/ng/user/display/user-display.pipe.spec.ts
index 4f022d36c6..213fe88c2d 100644
--- a/packages/ng/user/display/user-display.pipe.spec.ts
+++ b/packages/ng/user/display/user-display.pipe.spec.ts
@@ -1,17 +1,16 @@
import { createPipeFactory, SpectatorPipe } from '@ngneat/spectator/jest';
-import { ILuUser } from '../user.model';
-import { LuDisplayFullname, LuDisplayHybrid, LuDisplayInitials, LU_DEFAULT_DISPLAY_POLICY } from './display-format.model';
+import { LU_DEFAULT_DISPLAY_POLICY, LuDisplayFullname, LuDisplayHybrid, LuDisplayInitials } from './display-format.model';
import { luUserDisplay, LuUserDisplayPipe } from './user-display.pipe';
-describe('UserNamePipe', () => {
- let user: ILuUser;
- let userFirst: ILuUser;
- let userLast: ILuUser;
- beforeEach(() => {
- user = { firstName: 'John', lastName: 'Doe' };
- userFirst = { firstName: 'John', lastName: '' };
- userLast = { firstName: '', lastName: 'Doe' };
- });
+describe(LuUserDisplayPipe.name, () => {
+ const users = [
+ { firstName: 'John', lastName: 'Doe' },
+ { firstName: 'Michael', lastName: 'Scott' },
+ { firstName: 'Dwight', lastName: 'Schrute' },
+ ];
+ const user = users[0];
+ const userFirst = { firstName: user.firstName, lastName: '' };
+ const userLast = { firstName: '', lastName: user.lastName };
describe('luUserDisplay()', () => {
it("should return the right value with 'lf' format", () => {
@@ -91,7 +90,7 @@ describe('UserNamePipe', () => {
let spectator: SpectatorPipe;
const createPipe = createPipeFactory(LuUserDisplayPipe);
- it(`should return the right value with 'lf' format`, () => {
+ it(`should return the right single value with default 'lf' format`, () => {
spectator = createPipe(`{{ user | luUserDisplay }}`, {
hostProps: {
user,
@@ -100,7 +99,7 @@ describe('UserNamePipe', () => {
expect(spectator.element).toHaveText('Doe John');
});
- it(`should return the right value with 'fl' format`, () => {
+ it(`should return the right single value with provide 'fl' format`, () => {
const provider = { provide: LU_DEFAULT_DISPLAY_POLICY, useValue: LuDisplayFullname.firstlast };
spectator = createPipe(`{{ user | luUserDisplay }}`, {
hostProps: {
@@ -110,5 +109,61 @@ describe('UserNamePipe', () => {
});
expect(spectator.element).toHaveText('John Doe');
});
+
+ it(`should return the right single value with specify 'FL' format`, () => {
+ spectator = createPipe(`{{ user | luUserDisplay:'FL' }}`, {
+ hostProps: {
+ user,
+ },
+ });
+ expect(spectator.element).toHaveText('JD');
+ });
+
+ it(`should return the right multiple value with default 'lf' format and default ', ' separator`, () => {
+ spectator = createPipe(`{{ users | luUserDisplay }}`, {
+ hostProps: {
+ users,
+ },
+ });
+ expect(spectator.element).toHaveText('Doe John, Scott Michael, Schrute Dwight');
+ });
+
+ it(`should return the right multiple value with default 'lf' format and '; ' separator`, () => {
+ spectator = createPipe(`{{ users | luUserDisplay:{ separator: '; ' } }}`, {
+ hostProps: {
+ users,
+ },
+ });
+ expect(spectator.element).toHaveText('Doe John; Scott Michael; Schrute Dwight');
+ });
+
+ it(`should return the right multiple value with default 'Lf' format and default ', ' separator`, () => {
+ spectator = createPipe(`{{ users | luUserDisplay:{ format: 'Lf' } }}`, {
+ hostProps: {
+ users,
+ },
+ });
+ expect(spectator.element).toHaveText('D. John, S. Michael, S. Dwight');
+ });
+
+ it(`should return the right multiple value with specify 'Fl' format and ' ' separator`, () => {
+ spectator = createPipe(`{{ users | luUserDisplay:{ format: 'Fl', separator: ' ' } }}`, {
+ hostProps: {
+ users,
+ },
+ });
+ expect(spectator.element).toHaveText('J. Doe M. Scott D. Schrute');
+ });
+
+ it(`should return the right multiple value with specify 'fL' format and formatter`, () => {
+ const formatter = new Intl.ListFormat('en', { style: 'long', type: 'conjunction' });
+ spectator = createPipe(`{{ users | luUserDisplay:{ format: 'fL', formatter } }}`, {
+ hostProps: {
+ users,
+ formatter,
+ },
+ });
+ expect(spectator.element).toHaveText('John D., Michael S., and Dwight S.');
+ });
});
});
diff --git a/packages/ng/user/display/user-display.pipe.ts b/packages/ng/user/display/user-display.pipe.ts
index 689964ecc7..8c59695ef4 100644
--- a/packages/ng/user/display/user-display.pipe.ts
+++ b/packages/ng/user/display/user-display.pipe.ts
@@ -1,62 +1,58 @@
import { inject, Pipe, PipeTransform } from '@angular/core';
import { LU_DEFAULT_DISPLAY_POLICY, LuDisplayFormat, LuDisplayFullname, LuDisplayHybrid, LuDisplayInitials } from './display-format.model';
+function getFirstCharacter([firstCharacter]: string): string {
+ return firstCharacter ?? '';
+}
+
+function isNotEmptyString(value: string): boolean {
+ return value.length > 0;
+}
+
export interface LuUserDisplayInput {
firstName: string;
lastName: string;
}
+const formatUser: Record string> = {
+ [LuDisplayFullname.lastfirst]: ({ firstName, lastName }) => [lastName, firstName].filter(isNotEmptyString).join(' '),
+ [LuDisplayFullname.firstlast]: ({ firstName, lastName }) => [firstName, lastName].filter(isNotEmptyString).join(' '),
+ [LuDisplayFullname.first]: ({ firstName }) => firstName,
+ [LuDisplayFullname.last]: ({ lastName }) => lastName,
+ [LuDisplayInitials.lastfirst]: ({ firstName, lastName }) => [getFirstCharacter(lastName), getFirstCharacter(firstName)].filter(isNotEmptyString).join(''),
+ [LuDisplayInitials.firstlast]: ({ firstName, lastName }) => [getFirstCharacter(firstName), getFirstCharacter(lastName)].filter(isNotEmptyString).join(''),
+ [LuDisplayInitials.first]: ({ firstName }) => getFirstCharacter(firstName),
+ [LuDisplayInitials.last]: ({ lastName }) => getFirstCharacter(lastName),
+ [LuDisplayHybrid.lastIfirstFull]: ({ firstName, lastName }) => [isNotEmptyString(lastName) ? getFirstCharacter(lastName) + '.' : '', firstName].filter(isNotEmptyString).join(' '),
+ [LuDisplayHybrid.firstIlastFull]: ({ firstName, lastName }) => [isNotEmptyString(firstName) ? getFirstCharacter(firstName) + '.' : '', lastName].filter(isNotEmptyString).join(' '),
+ [LuDisplayHybrid.lastFullfirstI]: ({ firstName, lastName }) => [lastName, firstName ? getFirstCharacter(firstName) + '.' : ''].filter(isNotEmptyString).join(' '),
+ [LuDisplayHybrid.firstFulllastI]: ({ firstName, lastName }) => [firstName, lastName ? getFirstCharacter(lastName) + '.' : ''].filter(isNotEmptyString).join(' '),
+};
+
/**
* Displays a user name according to specified format. Supported formats: f for first name,
* F for first initial, l for last name, L for last initial.
*/
export function luUserDisplay(user: LuUserDisplayInput, format: LuDisplayFormat = LuDisplayFullname.lastfirst): string {
- let result = '';
- if (user) {
- switch (format) {
- case LuDisplayFullname.lastfirst:
- result = [user.lastName, user.firstName].filter((v) => !!v).join(' ');
- break;
- case LuDisplayFullname.firstlast:
- result = [user.firstName, user.lastName].filter((v) => !!v).join(' ');
- break;
- case LuDisplayFullname.first:
- result = user.firstName;
- break;
- case LuDisplayFullname.last:
- result = user.lastName;
- break;
- case LuDisplayInitials.lastfirst:
- result = [user.lastName?.charAt(0), user.firstName?.charAt(0)].filter((v) => !!v).join('');
- break;
- case LuDisplayInitials.firstlast:
- result = [user.firstName?.charAt(0), user.lastName?.charAt(0)].filter((v) => !!v).join('');
- break;
- case LuDisplayInitials.first:
- result = user.firstName?.charAt(0) ?? '';
- break;
- case LuDisplayInitials.last:
- result = user.lastName?.charAt(0) ?? '';
- break;
- case LuDisplayHybrid.firstIlastFull:
- result = [user.firstName ? user.firstName.charAt(0) + '.' : '', user.lastName].filter((v) => !!v).join(' ');
- break;
- case LuDisplayHybrid.lastIfirstFull:
- result = [user.lastName ? user.lastName.charAt(0) + '.' : '', user.firstName].filter((v) => !!v).join(' ');
- break;
- case LuDisplayHybrid.lastFullfirstI:
- result = [user.lastName, user.firstName ? user.firstName.charAt(0) + '.' : ''].filter((v) => !!v).join(' ');
- break;
- case LuDisplayHybrid.firstFulllastI:
- result = [user.firstName, user.lastName ? user.lastName.charAt(0) + '.' : ''].filter((v) => !!v).join(' ');
- break;
- default:
- break;
- }
+ return formatUser[format](user);
+}
+
+/**
+ * Displays a user name according to specified format. Supported formats: f for first name,
+ * F for first initial, l for last name, L for last initial.
+ */
+export function luUsersDisplay(users: LuUserDisplayInput[], options: LuUserDisplayMultipleOptions): string {
+ const usersStringified = users.map((u) => luUserDisplay(u, options.format));
+ if ('separator' in options) {
+ return usersStringified.join(options.separator);
}
- return result;
+ return options.formatter.format(usersStringified);
}
+export type LuUserDisplaySingleOptions = LuDisplayFormat | { format: LuDisplayFormat };
+
+export type LuUserDisplayMultipleOptions = { format: LuDisplayFormat; separator: string } | { format: LuDisplayFormat; formatter: Intl.ListFormat };
+
/**
* Displays a user name according to specified format. Supported formats: f for first name,
* F for first initial, l for last name, L for last initial.
@@ -65,7 +61,25 @@ export function luUserDisplay(user: LuUserDisplayInput, format: LuDisplayFormat
export class LuUserDisplayPipe implements PipeTransform {
private readonly defaultFormat = inject(LU_DEFAULT_DISPLAY_POLICY);
- public transform(user: LuUserDisplayInput, format: LuDisplayFormat = this.defaultFormat): string {
- return luUserDisplay(user, format);
+ public transform(user: T, options?: Partial): string;
+ public transform(users: T[], options?: Partial): string;
+ public transform(userOrUsers: T | T[], options?: Partial | Partial): string {
+ if (userOrUsers == null) {
+ throw new Error("Parameter 'userOrUsers' must be a user or a user array");
+ }
+
+ options = typeof options === 'string' ? { format: options } : options || {};
+
+ const format = options.format ?? this.defaultFormat;
+
+ if (Array.isArray(userOrUsers)) {
+ if ('formatter' in options) {
+ return luUsersDisplay(userOrUsers, { format, formatter: options.formatter });
+ }
+ const separator = ('separator' in options ? options.separator : undefined) ?? ', ';
+ return luUsersDisplay(userOrUsers, { format, separator });
+ }
+
+ return luUserDisplay(userOrUsers, format);
}
}
diff --git a/packages/ng/user/select/input/user-select-input.translate.ts b/packages/ng/user/select/input/user-select-input.translate.ts
index 7df9069988..a59da20d28 100644
--- a/packages/ng/user/select/input/user-select-input.translate.ts
+++ b/packages/ng/user/select/input/user-select-input.translate.ts
@@ -23,4 +23,8 @@ export const luUserSelectInputTranslations: ILuTranslation =
es: {
includeFormerEmployees: 'Incluir a los antiguos empleados',
},
+ de: {
+ includeFormerEmployees: 'Ehemalige Mitarbeiter einbeziehen',
+ },
};
diff --git a/packages/scss/src/commons/base.scss b/packages/scss/src/commons/base.scss
index a95ed99795..aa3db5e7f9 100644
--- a/packages/scss/src/commons/base.scss
+++ b/packages/scss/src/commons/base.scss
@@ -6,40 +6,78 @@
@mixin base($atRoot: 'without: rule') {
@at-root ($atRoot) {
- @font-face {
- font-family: 'Source Sans Pro';
- src: url('//cdn.lucca.fr/fonts/SourceSans/sourcesanspro-regular.woff2') format('woff2'),
- url('//cdn.lucca.fr/fonts/SourceSans/sourcesanspro-regular.woff') format('woff');
- font-weight: 400;
- font-style: normal;
- font-display: swap;
- }
+ @if config.$fontFamily != 'Source Sans Pro' {
+ @font-face {
+ font-family: '#{config.$fontFamily}';
+ src: url('//cdn.lucca.fr/fonts/#{config.$fontFamily}/regular.woff2') format('woff2'),
+ url('//cdn.lucca.fr/fonts/#{config.$fontFamily}/regular.woff') format('woff');
+ font-weight: 400;
+ font-style: normal;
+ font-display: swap;
+ }
- @font-face {
- font-family: 'Source Sans Pro';
- src: url('//cdn.lucca.fr/fonts/SourceSans/sourcesanspro-semibold.woff2') format('woff2'),
- url('//cdn.lucca.fr/fonts/SourceSans/sourcesanspro-semibold.woff') format('woff');
- font-weight: 600;
- font-style: normal;
- font-display: swap;
- }
+ @font-face {
+ font-family: '#{config.$fontFamily}';
+ src: url('//cdn.lucca.fr/fonts/#{config.$fontFamily}/semibold.woff2') format('woff2'),
+ url('//cdn.lucca.fr/fonts/#{config.$fontFamily}/semibold.woff') format('woff');
+ font-weight: 600;
+ font-style: normal;
+ font-display: swap;
+ }
- @font-face {
- font-family: 'Source Sans Pro';
- src: url('//cdn.lucca.fr/fonts/SourceSans/sourcesanspro-bold.woff2') format('woff2'),
- url('//cdn.lucca.fr/fonts/SourceSans/sourcesanspro-bold.woff') format('woff');
- font-weight: 700;
- font-style: normal;
- font-display: swap;
- }
+ @font-face {
+ font-family: '#{config.$fontFamily}';
+ src: url('//cdn.lucca.fr/fonts/#{config.$fontFamily}/bold.woff2') format('woff2'),
+ url('//cdn.lucca.fr/fonts/#{config.$fontFamily}/bold.woff') format('woff');
+ font-weight: 700;
+ font-style: normal;
+ font-display: swap;
+ }
- @font-face {
- font-family: 'Source Sans Pro';
- src: url('//cdn.lucca.fr/fonts/SourceSans/sourcesanspro-black.woff2') format('woff2'),
- url('//cdn.lucca.fr/fonts/SourceSans/sourcesanspro-black.woff') format('woff');
- font-weight: 900;
- font-style: normal;
- font-display: swap;
+ @font-face {
+ font-family: '#{config.$fontFamily}';
+ src: url('//cdn.lucca.fr/fonts/#{config.$fontFamily}/black.woff2') format('woff2'),
+ url('//cdn.lucca.fr/fonts/#{config.$fontFamily}/black.woff') format('woff');
+ font-weight: 900;
+ font-style: normal;
+ font-display: swap;
+ }
+ } @else {
+ @font-face {
+ font-family: 'Source Sans Pro';
+ src: url('//cdn.lucca.fr/fonts/SourceSans/sourcesanspro-regular.woff2') format('woff2'),
+ url('//cdn.lucca.fr/fonts/SourceSans/sourcesanspro-regular.woff') format('woff');
+ font-weight: 400;
+ font-style: normal;
+ font-display: swap;
+ }
+
+ @font-face {
+ font-family: 'Source Sans Pro';
+ src: url('//cdn.lucca.fr/fonts/SourceSans/sourcesanspro-semibold.woff2') format('woff2'),
+ url('//cdn.lucca.fr/fonts/SourceSans/sourcesanspro-semibold.woff') format('woff');
+ font-weight: 600;
+ font-style: normal;
+ font-display: swap;
+ }
+
+ @font-face {
+ font-family: 'Source Sans Pro';
+ src: url('//cdn.lucca.fr/fonts/SourceSans/sourcesanspro-bold.woff2') format('woff2'),
+ url('//cdn.lucca.fr/fonts/SourceSans/sourcesanspro-bold.woff') format('woff');
+ font-weight: 700;
+ font-style: normal;
+ font-display: swap;
+ }
+
+ @font-face {
+ font-family: 'Source Sans Pro';
+ src: url('//cdn.lucca.fr/fonts/SourceSans/sourcesanspro-black.woff2') format('woff2'),
+ url('//cdn.lucca.fr/fonts/SourceSans/sourcesanspro-black.woff') format('woff');
+ font-weight: 900;
+ font-style: normal;
+ font-display: swap;
+ }
}
*,
diff --git a/packages/scss/src/commons/config.scss b/packages/scss/src/commons/config.scss
index 0b315bf55c..ec7fdaf0dd 100644
--- a/packages/scss/src/commons/config.scss
+++ b/packages/scss/src/commons/config.scss
@@ -3,6 +3,7 @@
$importDeprecatedSpacings: true !default;
+$fontFamily: 'Source Sans Pro' !default;
$product: 'brand' !default;
$palettesShades: text, 25, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900;
$palettesStates: 'critical', 'error', 'warning', 'success';
@@ -469,6 +470,7 @@ $sizes: (
$borderRadius: (
'M': 4px,
'L': 8px,
+ 'XL': 12px,
'full': 9999px,
) !default;
diff --git a/packages/scss/src/commons/core.scss b/packages/scss/src/commons/core.scss
index fedbf8517a..1c8e206545 100644
--- a/packages/scss/src/commons/core.scss
+++ b/packages/scss/src/commons/core.scss
@@ -120,3 +120,20 @@ $overflow: 'hidden', 'auto', 'visible', 'scroll';
}
}
}
+
+@mixin rosetta($before, $after, $iterations) {
+ @if type-of($iterations) == map {
+ @each $iterationBefore, $iterationAfter in $iterations {
+ @if type-of($iterationAfter) == string {
+ #{$before}-#{$iterationBefore}: var(#{$after}-#{$iterationAfter});
+ } @else {
+ #{$before}-#{$iterationBefore}: #{$iterationAfter};
+ }
+ }
+ }
+ @else {
+ @each $iteration in $iterations {
+ #{$before}-#{$iteration}: var(#{$after}-#{$iteration});
+ }
+ }
+}
diff --git a/packages/scss/src/commons/utils/index.scss b/packages/scss/src/commons/utils/index.scss
index 693fd717c6..648a793455 100644
--- a/packages/scss/src/commons/utils/index.scss
+++ b/packages/scss/src/commons/utils/index.scss
@@ -323,12 +323,20 @@
.u-insetReset {
inset: 0 !important;
}
- } @else {
+ } @else if $direction != 'block' and $direction != 'inline' {
// .u-#{$direction}Reset is deprecated
.u-#{$direction}0,
.u-#{$direction}Reset {
#{$direction}: 0 !important;
}
+ } @else {
+ @each $boxModel in core.$boxModel {
+ @if $boxModel != 'border' {
+ .u-#{$boxModel}#{transform.capitalize($direction)}0 {
+ #{$boxModel}-#{$direction}: 0 !important;
+ }
+ }
+ }
}
}
diff --git a/packages/scss/src/commons/utils/media.scss b/packages/scss/src/commons/utils/media.scss
index 489f228b98..6b10811235 100644
--- a/packages/scss/src/commons/utils/media.scss
+++ b/packages/scss/src/commons/utils/media.scss
@@ -18,6 +18,10 @@
@if $max {
$reversed: 'not all and';
+
+ @if $at == 'container' {
+ $reversed: 'not';
+ }
}
@if map.get(config.$breakpoints, $breakpoint) {
@@ -32,6 +36,10 @@
@mixin queries($breakpoint1, $breakpoint2, $property: 'width', $at: 'media', $name: '') {
$reversed: 'not all and';
+ @if $at == 'container' {
+ $reversed: 'not';
+ }
+
@if map.get(config.$breakpoints, $breakpoint1) {
$breakpoint1: pxToEm(map.get(config.$breakpoints, $breakpoint1));
}
diff --git a/packages/scss/src/commons/vars.scss b/packages/scss/src/commons/vars.scss
index 3c5a00ccf1..224e308738 100644
--- a/packages/scss/src/commons/vars.scss
+++ b/packages/scss/src/commons/vars.scss
@@ -5,7 +5,6 @@
@use '@lucca-front/scss/src/commons/core';
@mixin vars {
-
// TOKENS
@each $key, $map in config.$elevation {
@@ -44,9 +43,8 @@
@include core.cssvars('colors', config.$colors, '-color');
@include core.cssvars('colors', config.$colorsRgb, '-rgb');
-
--commons-banner-height: 50px;
- --commons-font-family: 'Source Sans Pro', Tahoma, sans-serif;
+ --commons-font-family: '#{config.$fontFamily}', Tahoma, sans-serif;
--commons-divider-width: 1px;
--commons-divider-style: solid;
--commons-divider-border: var(--commons-divider-width) var(--commons-divider-style) var(--commons-divider-color);
diff --git a/packages/scss/src/components/avatar/mods.scss b/packages/scss/src/components/avatar/mods.scss
index 71a4541759..6cc81fd2e7 100644
--- a/packages/scss/src/components/avatar/mods.scss
+++ b/packages/scss/src/components/avatar/mods.scss
@@ -1,7 +1,6 @@
@mixin XL {
- --components-avatar-size: 4.5rem;
+ --components-avatar-size: 4rem;
--components-avatar-fontSize: var(--sizes-XL-fontSize);
- --components-avatar-border: 4px;
}
@mixin L {
diff --git a/packages/scss/src/components/breadcrumbs/component.scss b/packages/scss/src/components/breadcrumbs/component.scss
index 4e8048b15f..a01ff3a870 100644
--- a/packages/scss/src/components/breadcrumbs/component.scss
+++ b/packages/scss/src/components/breadcrumbs/component.scss
@@ -21,7 +21,7 @@
&::before {
@include icon.generate('arrow_chevron_right');
- color: var(--palettes-neutral-600);
+ color: var(--palettes-neutral-700);
font-size: var(--sizes-XS-lineHeight);
padding: 0 var(--pr-t-spacings-50);
}
@@ -29,7 +29,7 @@
}
.breadcrumbs-list-item-action {
- color: var(--palettes-neutral-600);
+ color: var(--palettes-neutral-700);
transition-duration: var(--commons-animations-durations-fast);
transition-property: color;
text-decoration: none;
@@ -40,7 +40,7 @@
cursor: pointer;
&:hover {
- color: var(--palettes-neutral-600);
+ color: var(--palettes-neutral-700);
text-decoration: underline;
}
diff --git a/packages/scss/src/components/breadcrumbs/mods.scss b/packages/scss/src/components/breadcrumbs/mods.scss
index b3c187d5b5..b437163a94 100644
--- a/packages/scss/src/components/breadcrumbs/mods.scss
+++ b/packages/scss/src/components/breadcrumbs/mods.scss
@@ -12,7 +12,7 @@
&:first-child {
.breadcrumbs-list-item-action {
&::before {
- @include icon.generate('format_corner_up_left');
+ @include icon.generate('arrow_left');
padding-right: var(--pr-t-spacings-50);
font-size: var(--sizes-XS-lineHeight);
diff --git a/packages/scss/src/components/breadcrumbs/states.scss b/packages/scss/src/components/breadcrumbs/states.scss
index e019538036..7cc6d0c19e 100644
--- a/packages/scss/src/components/breadcrumbs/states.scss
+++ b/packages/scss/src/components/breadcrumbs/states.scss
@@ -11,6 +11,6 @@
&:hover,
&:focus-visible {
outline: none;
- color: var(--palettes-neutral-600);
+ color: var(--palettes-neutral-700);
}
}
diff --git a/packages/scss/src/components/button/index.scss b/packages/scss/src/components/button/index.scss
index 527f623d6d..4f0620927d 100644
--- a/packages/scss/src/components/button/index.scss
+++ b/packages/scss/src/components/button/index.scss
@@ -44,19 +44,6 @@
}
}
- // .mod-counter deprecated
- &.mod-counter {
- @include counter;
-
- &.mod-S {
- @include counterS;
- }
-
- &.mod-XS {
- @include counterXS;
- }
- }
-
// .mod-icon is deprecated
&.mod-withIcon,
&.mod-icon {
diff --git a/packages/scss/src/components/button/mods.scss b/packages/scss/src/components/button/mods.scss
index 2ddbdff345..9ee3122d5d 100644
--- a/packages/scss/src/components/button/mods.scss
+++ b/packages/scss/src/components/button/mods.scss
@@ -92,12 +92,6 @@
--components-button-color: var(--palettes-700, var(--palettes-neutral-700));
--components-button-boxShadow: 0 0 0 var(--commons-divider-width) var(--palettes-400, var(--palettes-neutral-400));
- // deprecated
- .button-counter {
- color: var(--palettes-800, var(--palettes-neutral-800));
- background-color: var(--palettes-300, var(--palettes-neutral-300));
- }
-
&:hover {
--components-button-color: var(--palettes-700, var(--palettes-neutral-700));
--components-button-backgroundColor: var(--palettes-100, var(--palettes-neutral-100));
@@ -122,11 +116,11 @@
&:hover,
&:focus-visible {
--components-button-color: var(--colors-white-color);
- --components-button-backgroundColor: var(--palettes-800, var(--palettes-neutral-800));
+ --components-button-backgroundColor: var(--palettes-neutral-900);
}
&:active {
- --components-button-backgroundColor: var(--palettes-700, var(--palettes-neutral-700));
+ --components-button-backgroundColor: var(--palettes-neutral-700);
}
}
@@ -192,46 +186,3 @@
--components-button-backgroundColor: var(--palettes-error-200);
}
}
-
-// deprecated
-@mixin counter {
- --components-button-padding: var(--pr-t-spacings-100) var(--pr-t-spacings-150) var(--pr-t-spacings-100) var(--pr-t-spacings-200);
-
- .button-counter {
- background-color: var(--palettes-600, var(--palettes-product-600));
- border-radius: 1rem;
- display: flex;
- font-size: var(--sizes-XS-fontSize);
- height: 1.5rem;
- min-width: 1.5rem;
- align-items: center;
- justify-content: center;
- transition: background-color var(--commons-animations-durations-fast) ease;
- }
-
- &:hover {
- .button-counter {
- --components-button-backgroundColor: var(--palettes-500, var(--palettes-product-500));
- }
- }
-}
-
-// deprecated
-@mixin counterS {
- --components-button-padding: var(--pr-t-spacings-75) var(--pr-t-spacings-150) var(--pr-t-spacings-75) var(--pr-t-spacings-200);
-
- .button-counter {
- height: 1.25rem;
- min-width: 1.25rem;
- }
-}
-
-// deprecated
-@mixin counterXS {
- --components-button-padding: var(--pr-t-spacings-50) var(--pr-t-spacings-100) var(--pr-t-spacings-50) var(--pr-t-spacings-150);
-
- .button-counter {
- height: 1rem;
- min-width: 1rem;
- }
-}
diff --git a/packages/scss/src/components/callout/component.scss b/packages/scss/src/components/callout/component.scss
index b1dd08e8c9..5385a47e86 100644
--- a/packages/scss/src/components/callout/component.scss
+++ b/packages/scss/src/components/callout/component.scss
@@ -1,6 +1,7 @@
@use '@lucca-front/icons/src/commons/utils/icon';
@use '@lucca-front/scss/src/commons/utils/reset';
@use '@lucca-front/scss/src/commons/utils/a11y';
+@use '@lucca-front/scss/src/components/button/exports' as button;
@mixin component($atRoot: 'without: rule') {
align-items: flex-start;
@@ -36,6 +37,17 @@
font-weight: 600;
}
+ .callout-content-description-actions {
+ display: flex;
+ gap: var(--pr-t-spacings-75);
+ margin-top: var(--pr-t-spacings-50);
+ padding: var(--pr-t-spacings-50) 0;
+
+ .button {
+ @include button.S;
+ }
+ }
+
.callout-icon {
display: inline-flex;
color: var(--palettes-700, var(--palettes-neutral-700));
diff --git a/packages/scss/src/components/callout/index.scss b/packages/scss/src/components/callout/index.scss
index 4860ffea58..7801796ee8 100644
--- a/packages/scss/src/components/callout/index.scss
+++ b/packages/scss/src/components/callout/index.scss
@@ -7,8 +7,4 @@
&.mod-S {
@include S;
}
-
- &.mod-tiny {
- @include tiny;
- }
}
diff --git a/packages/scss/src/components/callout/mods.scss b/packages/scss/src/components/callout/mods.scss
index 7f2aeec639..b92c99e4b4 100644
--- a/packages/scss/src/components/callout/mods.scss
+++ b/packages/scss/src/components/callout/mods.scss
@@ -1,4 +1,5 @@
@use '@lucca-front/icons/src/icon/exports' as icon;
+@use '@lucca-front/scss/src/components/button/exports' as button;
@mixin S {
--components-callout-gap: var(--pr-t-spacings-100);
@@ -8,6 +9,12 @@
.callout-icon {
@include icon.S;
}
+
+ .callout-content-description-actions {
+ .button {
+ @include button.XS;
+ }
+ }
}
@mixin tiny {
diff --git a/packages/scss/src/components/comment/component.scss b/packages/scss/src/components/comment/component.scss
new file mode 100644
index 0000000000..0fc246305c
--- /dev/null
+++ b/packages/scss/src/components/comment/component.scss
@@ -0,0 +1,90 @@
+@mixin component($atRoot: 'without: rule') {
+ container: comment / inline-size;
+ display: flex;
+ flex-direction: column;
+ gap: var(--pr-t-spacings-100);
+ max-width: 40rem;
+
+ @at-root ($atRoot) {
+ .comment-infos {
+ display: flex;
+ font-style: normal;
+ gap: var(--pr-t-spacings-100);
+ justify-content: flex-start;
+ align-items: flex-start;
+ }
+
+ .comment-infos-content {
+ flex-direction: column;
+ display: var(--components-comment-info-content-display);
+ font-size: var(--components-comment-info-fontSize);
+ line-height: var(--components-comment-info-lineHeight);
+ margin-top: var(--components-comment-info-content-marginTop);
+ }
+
+ .comment-infos-name {
+ & + .comment-infos-date {
+ &::before {
+ content: var(--components-comment-info-separator);
+ color: var(--palettes-grey-400);
+ padding-inline: 1ch;
+
+ @supports (content: '*' / '') {
+ content: var(--components-comment-info-separator) / '';
+ }
+ }
+ }
+ }
+
+ .comment-content {
+ background: var(--palettes-grey-50);
+ border-radius: var(--commons-borderRadius-M) var(--commons-borderRadius-L) var(--commons-borderRadius-L);
+ display: flex;
+ flex-direction: column;
+ gap: var(--pr-t-spacings-150);
+ margin: 0 0 0 var(--components-comment-content-margin);
+ max-width: fit-content;
+ padding: var(--pr-t-spacings-100) var(--pr-t-spacings-150);
+ }
+
+ .comment-content-text {
+ margin-bottom: 0;
+ }
+
+ .comment-content-textContainer {
+ display: flex;
+ flex-direction: column;
+ gap: var(--pr-t-spacings-100);
+
+ > * {
+ margin: 0;
+ }
+ }
+
+ .comment-content-text {
+ font-size: var(--components-comment-text-fontSize);
+ line-height: var(--components-comment-text-lineHeight);
+ }
+
+ .comment-content-textContainer {
+ font-size: var(--components-comment-text-fontSize);
+ line-height: var(--components-comment-text-lineHeight);
+ }
+
+ .commentWrapper {
+ display: flex;
+ flex-direction: column;
+ gap: var(--pr-t-spacings-100);
+ margin: 0;
+ padding: 0;
+ }
+
+ .commentWrapper-item {
+ list-style: none;
+
+ &.mod-WrapperAvatar ~ &:not(.mod-WrapperAvatar) {
+ padding-left: var(--components-comment-content-margin);
+ }
+ }
+ }
+}
diff --git a/packages/scss/src/components/status/exports.scss b/packages/scss/src/components/comment/exports.scss
similarity index 100%
rename from packages/scss/src/components/status/exports.scss
rename to packages/scss/src/components/comment/exports.scss
diff --git a/packages/scss/src/components/comment/index.scss b/packages/scss/src/components/comment/index.scss
new file mode 100644
index 0000000000..cfafb3758f
--- /dev/null
+++ b/packages/scss/src/components/comment/index.scss
@@ -0,0 +1,28 @@
+@use '@lucca-front/scss/src/commons/utils/container';
+
+@use 'exports' as *;
+
+.comment {
+ @include vars;
+ @include component;
+
+ &.mod-S {
+ @include S;
+ }
+
+ &.mod-noAvatar {
+ @include noAvatar;
+ }
+
+ @include container.max(25rem, $name: 'comment') {
+ @include narrow;
+ }
+}
+
+.commentWrapper {
+ @include vars;
+
+ &.mod-compact {
+ @include wrapperCompact;
+ }
+}
diff --git a/packages/scss/src/components/comment/mods.scss b/packages/scss/src/components/comment/mods.scss
new file mode 100644
index 0000000000..20f328e606
--- /dev/null
+++ b/packages/scss/src/components/comment/mods.scss
@@ -0,0 +1,37 @@
+@use '@lucca-front/scss/src/commons/utils/a11y';
+
+@mixin S {
+ --components-comment-text-fontSize: var(--sizes-S-fontSize);
+ --components-comment-text-lineHeight: var(--sizes-S-lineHeight);
+ --components-comment-info-fontSize: var(--sizes-XS-fontSize);
+ --components-comment-info-lineHeight: var(--sizes-XS-lineHeight);
+ --components-comment-info-content-marginTop: var(--pr-t-spacings-50);
+}
+
+@mixin noAvatar {
+ --components-comment-content-margin: 0;
+}
+
+@mixin narrow {
+ @at-root {
+ .comment-infos-content {
+ --components-comment-info-content-display: flex;
+ }
+
+ .comment-infos-name + .comment-infos-date {
+ &::before {
+ --components-comment-info-separator: none;
+ }
+ }
+ }
+}
+
+@mixin wrapperCompact {
+ .commentWrapper-item {
+ &:not(:first-child) {
+ .comment-infos {
+ @include a11y.mask;
+ }
+ }
+ }
+}
diff --git a/packages/scss/src/components/status/mods.scss b/packages/scss/src/components/comment/states.scss
similarity index 100%
rename from packages/scss/src/components/status/mods.scss
rename to packages/scss/src/components/comment/states.scss
diff --git a/packages/scss/src/components/comment/vars.scss b/packages/scss/src/components/comment/vars.scss
new file mode 100644
index 0000000000..0330c5ac4d
--- /dev/null
+++ b/packages/scss/src/components/comment/vars.scss
@@ -0,0 +1,10 @@
+@mixin vars {
+ --components-comment-text-fontSize: var(--sizes-M-fontSize);
+ --components-comment-text-lineHeight: var(--sizes-M-lineHeight);
+ --components-comment-info-fontSize: var(--sizes-S-fontSize);
+ --components-comment-info-lineHeight: var(--sizes-S-lineHeight);
+ --components-comment-content-margin: calc(1.5rem + var(--pr-t-spacings-100)); // 1.5rem for the width of the avatar and 0.5rem for the gap
+ --components-comment-info-separator: '•';
+ --components-comment-info-content-display: block;
+ --components-comment-info-content-marginTop: var(--pr-t-spacings-25);
+}
diff --git a/packages/scss/src/components/dialog/component.scss b/packages/scss/src/components/dialog/component.scss
index cb3e79ab18..00568752a5 100644
--- a/packages/scss/src/components/dialog/component.scss
+++ b/packages/scss/src/components/dialog/component.scss
@@ -6,6 +6,10 @@
@mixin component($atRoot: 'without: rule') {
@include keyframe.scaleIn;
+ @supports not (height: 1dvh) {
+ --components-dialog-maxHeight: var(--components-dialog-maxHeightFallback);
+ }
+
animation-name: var(--components-dialog-animationOpening);
animation-duration: var(--commons-animations-durations-standard);
inset: var(--components-dialog-inset);
diff --git a/packages/scss/src/components/dialog/mods.scss b/packages/scss/src/components/dialog/mods.scss
index 4d0b967381..4f5b3e8bfe 100644
--- a/packages/scss/src/components/dialog/mods.scss
+++ b/packages/scss/src/components/dialog/mods.scss
@@ -27,6 +27,7 @@
--components-dialog-animationOpening: slideFromRight;
--components-dialog-maxHeight: none;
+ --components-dialog-maxHeightFallback: var(--components-dialog-maxHeight);
--components-dialog-height: 100%;
--components-dialog-maxWidth: calc(100vw - var(--pr-t-spacings-200));
--components-dialog-borderRadius: var(--commons-borderRadius-L) 0 0 var(--commons-borderRadius-L);
@@ -38,6 +39,7 @@
--components-dialog-animationOpening: slideFromBottom;
--components-dialog-maxHeight: calc(100dvh - var(--pr-t-spacings-200));
+ --components-dialog-maxHeightFallback: calc(100vh - var(--pr-t-spacings-200));
--components-dialog-maxWidth: none;
--components-dialog-inset: auto 0 0 0;
--components-dialog-borderRadius: var(--commons-borderRadius-L) var(--commons-borderRadius-L) 0 0;
@@ -56,5 +58,6 @@
--components-dialog-size: 100%;
--components-dialog-maxWidth: none;
--components-dialog-maxHeight: none;
+ --components-dialog-maxHeightFallback: var(--components-dialog-maxHeight);
--components-dialog-borderRadius: 0;
}
diff --git a/packages/scss/src/components/dialog/vars.scss b/packages/scss/src/components/dialog/vars.scss
index f9375a4931..e8c57f29e1 100644
--- a/packages/scss/src/components/dialog/vars.scss
+++ b/packages/scss/src/components/dialog/vars.scss
@@ -4,6 +4,7 @@
--components-dialog-height: fit-content;
--components-dialog-maxWidth: calc(100vw - (var(--pr-t-spacings-200) * 2));
--components-dialog-maxHeight: calc(100dvh - (var(--pr-t-spacings-200) * 2));
+ --components-dialog-maxHeightFallback: calc(100vh - (var(--pr-t-spacings-200) * 2));
--components-dialog-borderRadius: var(--commons-borderRadius-L);
--components-dialog-inset: 0;
--components-dialog-animationOpening: scaleIn;
diff --git a/packages/scss/src/components/emptyState/component.scss b/packages/scss/src/components/emptyState/component.scss
index 8491552ac0..e9a23c54b2 100644
--- a/packages/scss/src/components/emptyState/component.scss
+++ b/packages/scss/src/components/emptyState/component.scss
@@ -1,3 +1,5 @@
+@use '@lucca-front/scss/src/components/title/exports' as title;
+
@mixin component($atRoot: 'without: rule') {
display: flex;
flex-direction: column;
@@ -32,12 +34,20 @@
}
.emptyState-content-heading {
+ @include title.component;
+ @include title.h3;
+
margin-bottom: 0;
}
+ .emptyState-content-description {
+ margin: 0;
+ }
+
.emptyState-actions {
display: flex;
flex-wrap: wrap;
+ margin-top: var(--pr-t-spacings-200);
gap: var(--pr-t-spacings-100);
.button {
diff --git a/packages/scss/src/components/emptyState/mods.scss b/packages/scss/src/components/emptyState/mods.scss
index a39183f6fa..6530e02a21 100644
--- a/packages/scss/src/components/emptyState/mods.scss
+++ b/packages/scss/src/components/emptyState/mods.scss
@@ -21,6 +21,8 @@
}
.emptyState-content-heading {
+ @include title.h1;
+
@include media.max('XXS') {
@include title.h2;
}
diff --git a/packages/scss/src/components/errorPage/component.scss b/packages/scss/src/components/errorPage/component.scss
index 68be549c33..d5f41f026e 100644
--- a/packages/scss/src/components/errorPage/component.scss
+++ b/packages/scss/src/components/errorPage/component.scss
@@ -1,3 +1,5 @@
+@use '@lucca-front/scss/src/components/title/exports' as title;
+
@mixin component {
background-color: var(--components-errorPage-background);
height: 100vh;
@@ -24,12 +26,13 @@
}
.errorPage-section-info-title {
- color: var(--components-errorPage-header-color);
- font-size: 2.5rem;
+ @include title.component;
+ @include title.XXXL;
}
.errorPage-section-info-text {
- font-size: 1.5rem;
+ font-size: var(--sizes-L-fontSize);
+ line-height: var(--sizes-L-lineHeight);
}
.errorPage-section-image {
diff --git a/packages/scss/src/components/errorPage/vars.scss b/packages/scss/src/components/errorPage/vars.scss
index 8c4f6ed9d5..e02e11bc8c 100644
--- a/packages/scss/src/components/errorPage/vars.scss
+++ b/packages/scss/src/components/errorPage/vars.scss
@@ -1,4 +1,3 @@
@mixin vars {
--components-errorPage-background: var(--pr-t-elevation-surface-raised);
- --components-errorPage-header-color: #ff7a1a;
}
diff --git a/packages/scss/src/components/footer/component.scss b/packages/scss/src/components/footer/component.scss
index 41f1e1cf62..a7b2a2652d 100644
--- a/packages/scss/src/components/footer/component.scss
+++ b/packages/scss/src/components/footer/component.scss
@@ -1,37 +1,37 @@
-@use '@lucca-front/scss/src/commons/utils/media';
-@use '@lucca-front/scss/src/components/button/exports' as button;
+@use '@lucca-front/scss/src/components/container/exports' as container;
@mixin component($atRoot: 'without: rule') {
background-color: var(--pr-t-elevation-surface-raised);
padding: var(--pr-t-spacings-200) var(--pr-t-spacings-300);
display: flex;
+ justify-content: space-between;
gap: var(--pr-t-spacings-200);
- align-items: center;
+ align-items: var(--components-footer-alignItems);
box-shadow: var(--pr-t-elevation-shadow-overflow);
-
- @include media.max('XXS') {
- flex-direction: column;
- }
+ bottom: 0;
+ position: var(--components-footer-position);
+ flex-direction: var(--components-footer-direction);
@at-root ($atRoot) {
+ .footer-content {
+ flex-grow: 1;
+ }
+
.footer-actions {
display: flex;
- margin-left: auto;
gap: var(--pr-t-spacings-200);
+ flex-direction: var(--components-footer-direction);
+ }
- .button {
- margin: 0;
- }
-
- @include media.max('XXS') {
- flex-direction: column;
- margin-left: inherit;
- width: 100%;
+ .footer-containerOptional {
+ --components-container-padding: 0;
- .button {
- @include button.block;
- }
- }
+ display: flex;
+ gap: var(--pr-t-spacings-200);
+ align-items: var(--components-footer-alignItems);
+ flex-grow: 1;
+ justify-content: space-between;
+ flex-direction: var(--components-footer-direction);
}
}
}
diff --git a/packages/scss/src/components/footer/index.scss b/packages/scss/src/components/footer/index.scss
index bc73251f0d..809d230508 100644
--- a/packages/scss/src/components/footer/index.scss
+++ b/packages/scss/src/components/footer/index.scss
@@ -1,4 +1,5 @@
@use 'exports' as *;
+@use '@lucca-front/scss/src/commons/utils/media';
.footer {
@include vars;
@@ -7,4 +8,29 @@
&.mod-sticky {
@include sticky;
}
+
+ &:not([class*='mod-narrow']),
+ &.mod-narrowAtMediaMaxXXS {
+ @include media.max('XXS') {
+ @include narrow;
+ }
+ }
+
+ &.mod-narrowAtMediaMaxXS {
+ @include media.max('XS') {
+ @include narrow;
+ }
+ }
+
+ &.mod-narrowAtMediaMaxS {
+ @include media.max('S') {
+ @include narrow;
+ }
+ }
+
+ &.mod-narrowAtMediaMaxM {
+ @include media.max('M') {
+ @include narrow;
+ }
+ }
}
diff --git a/packages/scss/src/components/footer/mods.scss b/packages/scss/src/components/footer/mods.scss
index 48dbd14489..262518d549 100644
--- a/packages/scss/src/components/footer/mods.scss
+++ b/packages/scss/src/components/footer/mods.scss
@@ -1,4 +1,8 @@
@mixin sticky {
- position: sticky;
- bottom: 0;
+ --components-footer-position: sticky;
+}
+
+@mixin narrow {
+ --components-footer-direction: column;
+ --components-footer-alignItems: stretch;
}
diff --git a/packages/scss/src/components/footer/vars.scss b/packages/scss/src/components/footer/vars.scss
index 50f7fd0a14..7eae1c8bd7 100644
--- a/packages/scss/src/components/footer/vars.scss
+++ b/packages/scss/src/components/footer/vars.scss
@@ -1,2 +1,5 @@
@mixin vars {
+ --components-footer-position: static;
+ --components-footer-direction: row;
+ --components-footer-alignItems: center;
}
diff --git a/packages/scss/src/components/form/component.scss b/packages/scss/src/components/form/component.scss
index e420203160..d18885c0b4 100644
--- a/packages/scss/src/components/form/component.scss
+++ b/packages/scss/src/components/form/component.scss
@@ -19,6 +19,25 @@
position: relative;
@at-root ($atRoot) {
+ .form-header {
+ margin-bottom: var(--spacings-M);
+ }
+
+ .form-header-title {
+ margin: 0;
+ padding: 0;
+ }
+
+ .form-header-mandatory {
+ font-size: var(--sizes-S-fontSize);
+ line-height: var(--sizes-S-lineHeight);
+ color: var(--palettes-grey-700);
+ }
+
+ .form-header-mandatory-asterisk {
+ color: var(--palettes-error-700);
+ }
+
.form-field {
position: relative;
display: flex;
diff --git a/packages/scss/src/components/form/index.scss b/packages/scss/src/components/form/index.scss
index 39aec6e47a..e54b5a6498 100644
--- a/packages/scss/src/components/form/index.scss
+++ b/packages/scss/src/components/form/index.scss
@@ -6,6 +6,10 @@
.form {
@include component;
+
+ &.mod-maxWidth {
+ @include maxWidth;
+ }
}
.form-fieldset {
@@ -38,7 +42,7 @@
@include checkable;
}
- &:has(.textField-input-value[aria-invalid='true']) {
+ &:has(.textField-input-value[aria-invalid='true'], .timePicker-fieldset-group-textfield-input[aria-invalid='true']) {
@include invalid;
}
diff --git a/packages/scss/src/components/form/mods.scss b/packages/scss/src/components/form/mods.scss
index 3e3a623107..82f975aa72 100644
--- a/packages/scss/src/components/form/mods.scss
+++ b/packages/scss/src/components/form/mods.scss
@@ -7,11 +7,16 @@
@use '@lucca-front/scss/src/components/textField/exports' as textField;
@use '@lucca-front/scss/src/components/switchField/exports' as switchField;
@use '@lucca-front/scss/src/components/checkboxField/exports' as checkboxField;
+@use '@lucca-front/scss/src/components/radioField/exports' as radioField;
@use '@lucca-front/scss/src/components/simpleSelect/exports' as simpleSelect;
@use '@lucca-front/scss/src/components/multiSelect/exports' as multiSelect;
-@use '@lucca-front/scss/src/components/radioField/exports' as radioField;
+@use '@lucca-front/scss/src/components/timepicker/exports' as timepicker;
@use '@lucca-front/scss/src/components/box/exports' as box;
+@mixin maxWidth {
+ max-width: var(--components-form-maxWidth);
+}
+
@mixin S {
.formLabel {
@include formLabel.S;
@@ -40,6 +45,10 @@
.multiSelect {
@include multiSelect.S;
}
+
+ .timePicker {
+ @include timepicker.S;
+ }
}
@mixin XS {
@@ -279,12 +288,6 @@
color: var(--palettes-error-700);
display: inline-block;
margin-left: 0.2em;
-
- @supports (content: '*' / '') {
- content: '*' / '';
- }
-
- @supports not (content: '*' / '') {
- content: '*';
- }
+ content: '*';
+ content: '*' / '';
}
diff --git a/packages/scss/src/components/form/vars.scss b/packages/scss/src/components/form/vars.scss
index 90e94e18f8..2db17beed5 100644
--- a/packages/scss/src/components/form/vars.scss
+++ b/packages/scss/src/components/form/vars.scss
@@ -1,11 +1,9 @@
@mixin vars {
+ --components-form-maxWidth: 40rem;
--components-form-group-margin-bottom: 1.2rem;
-
--components-form-field-margin-bottom: var(--pr-t-spacings-200);
-
--components-form-label-font-size: var(--sizes-M-fontSize);
--components-form-label-margin-bottom: var(--pr-t-spacings-50);
-
--components-field-framed-side-padding: var(--pr-t-spacings-200);
--components-field-framed-top-padding: var(--pr-t-spacings-400);
--components-field-framed-bottom-padding: var(--pr-t-spacings-200);
diff --git a/packages/scss/src/components/formLabel/component.scss b/packages/scss/src/components/formLabel/component.scss
index eb5e1da65a..0c4a690289 100644
--- a/packages/scss/src/components/formLabel/component.scss
+++ b/packages/scss/src/components/formLabel/component.scss
@@ -1,3 +1,5 @@
+@use '@lucca-front/scss/src/commons/utils/a11y';
+
@mixin component($atRoot: 'without: rule') {
color: var(--components-formLabel-color);
display: flex;
@@ -27,6 +29,20 @@
top: 0;
}
+ .formLabel-info {
+ &:focus-visible {
+ outline: none;
+
+ .lucca-icon {
+ &::before {
+ border-radius: 50%;
+
+ @include a11y.focusVisible($offset: 0);
+ }
+ }
+ }
+ }
+
.formLabel-counter {
margin-left: auto;
margin-bottom: var(--pr-t-spacings-25);
diff --git a/packages/scss/src/components/index.scss b/packages/scss/src/components/index.scss
index 0c2d97d462..37886f2409 100644
--- a/packages/scss/src/components/index.scss
+++ b/packages/scss/src/components/index.scss
@@ -7,7 +7,6 @@
@forward 'chip'; // 1 Ko
@forward 'title'; // 1 Ko
@forward 'label'; // 1 Ko
-@forward 'status'; // 1 Ko
@forward 'filterBar'; // 1 Ko
@forward 'filters'; // 1 Ko
@forward 'divider'; // 1 Ko
@@ -36,7 +35,6 @@
@forward 'switch'; // 4 Ko
@forward 'switchField'; // new component for switch
@forward 'file'; // 5 Ko
-@forward 'toast'; // 5 Ko
@forward 'layout'; // 5 Ko
@forward 'radioButtons'; // 5 Ko
@forward 'table'; // 7 Ko
@@ -49,9 +47,9 @@
@forward 'textField'; // new component for checkbox
@forward 'navside'; // 15 Ko
@forward 'form'; // 25 Ko
-@forward 'tableFixed'; // 33 Ko
+@forward 'tableFixed'; // 2 Ko
@forward 'grid';
-@forward 'tableSticked'; // 67 Ko
+@forward 'tableSticked'; // 8 Ko
@forward 'timepicker'; //
@forward 'notchBox';
@forward 'statusBadge';
@@ -77,8 +75,16 @@
@forward 'avatar';
@forward 'indexTable';
@forward 'indexTableSorted';
+@forward 'tooltip';
@forward 'userPopover';
+@forward 'scrollBox';
+@forward 'comment';
+@forward 'toast'; // 5 Ko
+@forward 'popover';
// Deprecated CSS components
// @forward 'gridLegacy'; // 40 Ko
// @forward 'emptyStateDeprecated';
+
+// @forward 'tableFixedDeprecated'; // 33 Ko
+// @forward 'tableStickedDeprecated'; // 67 Ko
diff --git a/packages/scss/src/components/loading/component.scss b/packages/scss/src/components/loading/component.scss
index e5afec1834..b5ca2adac6 100644
--- a/packages/scss/src/components/loading/component.scss
+++ b/packages/scss/src/components/loading/component.scss
@@ -3,13 +3,27 @@
@mixin component {
@include loading.spinner();
- min-width: 1.5rem;
- min-height: 1.5rem;
- padding-left: var(--pr-t-spacings-400);
- display: inline-block;
+ @supports not (height: 1dvh) {
+ --components-loading-transform: var(--components-loading-transformFallback);
+ }
+
+ min-height: var(--components-loading-size);
+ padding: var(--components-loading-padding);
+ margin: var(--components-loading-margin);
+ display: var(--components-loading-display);
+ transform: var(--components-loading-transform);
+ color: var(--components-loading-color);
position: relative;
+ text-align: center;
+ vertical-align: top;
+
+ &:not(:empty) {
+ --components-loading-padding: 0 0 0 var(--pr-t-spacings-400);
+ }
&::after {
- margin: 0;
+ width: var(--components-loading-size);
+ height: var(--components-loading-size);
+ margin: var(--components-loading-spinnerMargin);
}
}
diff --git a/packages/scss/src/components/loading/index.scss b/packages/scss/src/components/loading/index.scss
index 025b9dfcec..733900051f 100644
--- a/packages/scss/src/components/loading/index.scss
+++ b/packages/scss/src/components/loading/index.scss
@@ -12,16 +12,20 @@
@include block;
}
- &.mod-fullPage {
+ &[class~='mod-fullPage' i] {
@include fullPage;
}
- &.mod-sidePanel {
- @include sidePanel;
+ // .mod-dialog is deprecated
+ &.mod-popin,
+ &.mod-dialog {
+ @include popin;
}
- &.mod-dialog {
- @include dialog;
+ // .mod-sidePanel is deprecated
+ &.mod-drawer,
+ &.mod-sidePanel {
+ @include drawer;
}
&.mod-invert {
diff --git a/packages/scss/src/components/loading/mods.scss b/packages/scss/src/components/loading/mods.scss
index e138a65bb2..2cc40270df 100644
--- a/packages/scss/src/components/loading/mods.scss
+++ b/packages/scss/src/components/loading/mods.scss
@@ -1,80 +1,51 @@
@use '@lucca-front/scss/src/commons/utils/color';
-@mixin L {
- height: var(--components-loading-size-big);
- padding: var(--components-loading-size-big) 0 0;
- text-align: center;
- width: 100%;
+@mixin block {
+ --components-loading-display: block;
+ --components-loading-margin: 0 auto;
+ --components-loading-spinnerMargin: 0 auto;
- &::after {
- margin: 0 auto;
- width: var(--components-loading-size-big);
- height: var(--components-loading-size-big);
+ &:not(:empty) {
+ --components-loading-padding: var(--components-loading-size) 0 0;
}
}
-@mixin block {
- display: block;
- margin-bottom: var(--pr-t-spacings-400);
- padding: var(--pr-t-spacings-600) 0 0;
- text-align: center;
- width: 100%;
+@mixin L {
+ @include block;
- &::after {
- margin: auto;
- }
+ --components-loading-size: var(--components-loading-size-big);
}
@mixin fullPage {
- display: block;
- margin: 0 auto;
- transform: translateY(30vh);
+ @include block;
+ @include L;
- &::after {
- margin: 0 auto;
- width: var(--components-loading-size-big);
- height: var(--components-loading-size-big);
- }
+ --components-loading-transform: translateY(calc((100dvh - var(--commons-banner-height)) / 2));
+ --components-loading-transformFallback: translateY(calc((100vh - var(--commons-banner-height)) / 2));
}
-@mixin dialog {
- display: block;
- text-align: center;
- position: relative;
- padding: 8rem 0 0;
- margin: 0 var(--pr-t-spacings-400) var(--pr-t-spacings-400);
+@mixin invert {
+ --components-loading-color: var(--colors-white-color);
&::after {
- margin: auto;
- width: var(--components-loading-size-big);
- height: var(--components-loading-size-big);
- left: 0;
- right: 0;
+ --commons-loading-frontground: color.transparentize(var(--colors-white-color), 0.66);
}
}
-@mixin sidePanel {
- display: block;
- text-align: center;
- position: relative;
- padding: 8rem 0 0;
- margin: 0 var(--pr-t-spacings-400) var(--pr-t-spacings-400);
- top: 35vh;
+@mixin popin {
+ @include block;
+ @include L;
- &::after {
- margin: auto;
- width: var(--components-loading-size-big);
- height: var(--components-loading-size-big);
- left: 0;
- right: 0;
+ &,
+ &:not(:empty) {
+ --components-loading-padding: 8rem 0 0;
}
-}
-@mixin invert {
- color: var(--colors-white-color);
+ --components-loading-margin: 0 var(--pr-t-spacings-400) var(--pr-t-spacings-400);
+ --components-loading-spinnerMargin: auto;
+}
- &::after {
- border-color: color.transparentize(var(--colors-white-color), 0.66);
- border-top-color: transparent;
- }
+@mixin drawer {
+ @include fullPage;
+ @include popin;
}
diff --git a/packages/scss/src/components/loading/vars.scss b/packages/scss/src/components/loading/vars.scss
index db12370918..55fdc3adf3 100644
--- a/packages/scss/src/components/loading/vars.scss
+++ b/packages/scss/src/components/loading/vars.scss
@@ -1,3 +1,11 @@
@mixin vars {
- --components-loading-size-big: 3rem;
+ --components-loading-display: inline-block;
+ --components-loading-size: var(--pr-t-spacings-300);
+ --components-loading-size-big: var(--pr-t-spacings-600);
+ --components-loading-padding: 0 0 0 var(--components-loading-size);
+ --components-loading-margin: 0;
+ --components-loading-spinnerMargin: 0;
+ --components-loading-color: currentColor;
+ --components-loading-transform: none;
+ --components-loading-transformFallback: var(--components-loading-transform);
}
diff --git a/packages/scss/src/components/menu/component.scss b/packages/scss/src/components/menu/component.scss
index 2cf63b3864..6a21216691 100644
--- a/packages/scss/src/components/menu/component.scss
+++ b/packages/scss/src/components/menu/component.scss
@@ -1,28 +1,25 @@
@use '@lucca-front/scss/src/commons/utils/reset';
@mixin component($atRoot: 'without: rule') {
+ position: relative;
column-gap: var(--pr-t-spacings-400);
align-items: center;
display: flex;
position: relative;
flex-wrap: wrap;
- &:not(.mod-noBorder) {
- &::after {
- border-bottom-width: var(--commons-divider-width);
- border-bottom-color: var(--commons-divider-color);
- border-bottom-style: solid;
- position: absolute;
- height: 1px;
- bottom: 0;
- left: 0;
- right: 0;
- content: '';
- }
+ &::after {
+ content: var(--components-menu-borderContent);
+ border-bottom-width: var(--commons-divider-width);
+ border-bottom-color: var(--commons-divider-color);
+ border-bottom-style: solid;
+ position: absolute;
+ height: 1px;
+ inset: auto 0 0;
}
+ // .label is deprecated
.label {
- // deprecated
margin-right: 0;
background-color: var(--palettes-neutral-100);
color: var(--palettes-neutral-700);
@@ -32,41 +29,48 @@
.menu-list {
@include reset.list;
- align-items: flex-end;
- column-gap: var(--pr-t-spacings-400);
+ align-items: var(--components-menu-listAlign);
+ gap: var(--components-menu-listGap);
+ flex-direction: var(--components-menu-listDirection);
display: flex;
+ flex-grow: 1;
flex-wrap: wrap;
+ padding: var(--components-menu-listPadding);
}
- .menu-link, // legacy syntax
- .menu-list-item-action {
+ // .menu-link is deprecated
+ .menu-list-item-action,
+ .menu-link {
@include reset.button;
- padding: var(--components-menu-padding);
+
+ font-size: var(--components-menu-listItemActionFontSize);
+ line-height: var(--components-menu-listItemActionLineHeight);
+ padding: var(--components-menu-listItemActionPadding);
border-radius: var(--commons-borderRadius-M);
- color: var(--palettes-neutral-800);
- display: inline-flex;
- align-items: center;
- text-align: center;
- gap: var(--pr-t-spacings-100);
+ color: var(--components-menu-listItemActionColor);
+ display: var(--components-menu-listItemActionDisplay);
+ text-align: var(--components-menu-listItemActionAlign);
transition-duration: var(--commons-animations-durations-fast);
+ gap: var(--pr-t-spacings-100);
+ align-items: center;
transition-property: color;
position: relative;
text-decoration: none;
width: auto;
z-index: 1;
+ scroll-margin-inline: var(--spacings-S);
&::after {
background-color: var(--palettes-700, var(--palettes-product-700));
- border-radius: var(--commons-borderRadius-M) var(--commons-borderRadius-M) 0 0;
+ border-radius: var(--components-menu-listItemActionRadius);
transition-duration: var(--commons-animations-durations-fast);
+ height: var(--components-menu-listItemActionRadiusHeight);
+ width: var(--components-menu-listItemActionRadiusWidth);
+ transform: var(--components-menu-listItemActionTransform);
+ inset: var(--components-menu-listItemActionInset);
transition-property: transform;
display: block;
position: absolute;
- bottom: 0;
- height: 2px;
- left: 0;
- right: 0;
- transform: scale(0, 1);
z-index: 1;
content: '';
}
@@ -77,14 +81,14 @@
color: var(--palettes-neutral-900);
}
+ // .label is deprecated
.label {
- // deprecated
background-color: var(--palettes-100, var(--palettes-product-100));
color: var(--palettes-700, var(--palettes-product-700));
}
&::after {
- transform: scale(0.75, 1);
+ --components-menu-listItemActionTransform: scale(0.75, 1);
}
}
}
diff --git a/packages/scss/src/components/menu/index.scss b/packages/scss/src/components/menu/index.scss
index c0a7b52580..dfcaa6f606 100644
--- a/packages/scss/src/components/menu/index.scss
+++ b/packages/scss/src/components/menu/index.scss
@@ -11,16 +11,42 @@
&.mod-S {
@include S;
}
+
+ &.mod-noBorder {
+ @include noBorder;
+ }
+
+ &.mod-vertical {
+ @include vertical;
+
+ .menu-list-item-action {
+ &[aria-current='page'],
+ &.is-active {
+ @include activeVertical;
+ }
+ }
+
+ &.mod-S {
+ @include verticalS;
+ }
+ }
}
-// legacy syntax
-.menu-link,
-.menu-list-item-action {
- &:is(.is-active, .active, [aria-current='page']) {
+// .menu-link is deprecated
+.menu-list-item-action,
+.menu-link {
+ // .active is deprecated
+ &[aria-current='page'],
+ &.is-active,
+ &.active {
@include active;
}
- &:is(.is-disabled, .disabled, &[disabled]) {
+ // .disabled is deprecated
+ // [disabled] is deprecated
+ &.is-disabled,
+ &.disabled,
+ &[disabled] {
@include disabled;
}
}
diff --git a/packages/scss/src/components/menu/mods.scss b/packages/scss/src/components/menu/mods.scss
index 622b63c2ee..54796da097 100644
--- a/packages/scss/src/components/menu/mods.scss
+++ b/packages/scss/src/components/menu/mods.scss
@@ -1,25 +1,77 @@
+@use '@lucca-front/scss/src/components/numericBadge/exports' as numericBadge;
+
@mixin header {
- padding: 0 2.5rem;
+ --components-menu-listPadding: 0 var(--pr-t-spacings-500);
}
@mixin S {
- // legacy syntax
- .menu-link,
- .menu-list-item-action {
- font-size: var(--sizes-S-fontSize);
- line-height: var(--sizes-S-lineHeight);
- padding: var(--pr-t-spacings-150) 0;
+ // .menu-link is deprecated
+ .menu-list-item-action,
+ .menu-link {
+ --components-menu-listItemActionFontSize: var(--sizes-S-fontSize);
+ --components-menu-listItemActionLineHeight: var(--sizes-S-lineHeight);
+ --components-menu-listItemActionPadding: var(--pr-t-spacings-150) 0;
}
- .label { // deprecated
+ // .menu-link is deprecated
+ .menu-link {
+ margin-right: var(--pr-t-spacings-300);
+ }
+
+ // .label is deprecated
+ .label {
height: var(--sizes-S-lineHeight);
min-width: var(--sizes-S-lineHeight);
line-height: var(--sizes-S-lineHeight);
padding: 0;
}
- // legacy syntax
- .menu-link {
- margin-right: var(--pr-t-spacings-300);
+ .numericBadge {
+ @include numericBadge.S;
+ }
+}
+
+@mixin noBorder {
+ &::after {
+ --components-menu-borderContent: none;
+ }
+}
+
+@mixin vertical {
+ @include noBorder;
+
+ .menu-list {
+ --components-menu-listDirection: column;
+ --components-menu-listGap: 0;
+ --components-menu-listAlign: stretch;
+ }
+
+ .menu-list-item-action {
+ --components-menu-listItemActionPadding: var(--pr-t-spacings-50) var(--pr-t-spacings-200);
+ --components-menu-listItemActionAlign: left;
+ --components-menu-listItemActionDisplay: block;
+
+ &::after {
+ --components-menu-listItemActionTransform: scale(1, 0);
+ --components-menu-listItemActionInset: 0 auto 0 0;
+ --components-menu-listItemActionRadius: 0 var(--commons-borderRadius-M) var(--commons-borderRadius-M) 0;
+ --components-menu-listItemActionRadiusWidth: var(--components-menu-listItemActionSize);
+ --components-menu-listItemActionRadiusHeight: auto;
+ }
+
+ &,
+ &[aria-current='page'] {
+ &:hover {
+ &::after {
+ --components-menu-listItemActionTransform: scale(1, 0.75);
+ }
+ }
+ }
+ }
+}
+
+@mixin verticalS {
+ .menu-list-item-action {
+ --components-menu-listItemActionPadding: var(--pr-t-spacings-50) var(--pr-t-spacings-150);
}
}
diff --git a/packages/scss/src/components/menu/states.scss b/packages/scss/src/components/menu/states.scss
index 44549a918d..fa049010bd 100644
--- a/packages/scss/src/components/menu/states.scss
+++ b/packages/scss/src/components/menu/states.scss
@@ -1,22 +1,21 @@
@use '@lucca-front/scss/src/components/numericBadge/exports' as numericBadge;
@mixin active {
- color: var(--palettes-700, var(--palettes-product-700));
+ --components-menu-listItemActionColor: var(--palettes-700, var(--palettes-product-700));
+ // .label is deprecated
.label {
- // deprecated
background-color: var(--palettes-100, var(--palettes-product-100));
color: var(--palettes-700, var(--palettes-product-700));
}
&::after {
- background-color: var(--palettes-700, var(--palettes-product-700));
- transform: scale(1);
+ --components-menu-listItemActionTransform: scale(1);
}
&:focus-visible {
&::after {
- transform: scale(0.75, 1);
+ --components-menu-listItemActionTransform: scale(0.75, 1);
}
}
@@ -26,9 +25,9 @@
}
@mixin disabled {
+ // .label is deprecated
&,
.label {
- // deprecated
color: var(--palettes-neutral-500);
pointer-events: none;
}
@@ -37,3 +36,9 @@
@include numericBadge.disabled;
}
}
+
+@mixin activeVertical {
+ &::after {
+ --components-menu-listItemActionTransform: scale(1);
+ }
+}
diff --git a/packages/scss/src/components/menu/vars.scss b/packages/scss/src/components/menu/vars.scss
index 9fb0c24a8b..4d4e46d4c1 100644
--- a/packages/scss/src/components/menu/vars.scss
+++ b/packages/scss/src/components/menu/vars.scss
@@ -1,3 +1,19 @@
@mixin vars {
- --components-menu-padding: var(--pr-t-spacings-200) 0;
+ --components-menu-borderContent: '';
+ --components-menu-listPadding: 0;
+ --components-menu-listItemActionPadding: var(--pr-t-spacings-200) 0;
+ --components-menu-listGap: 0 var(--pr-t-spacings-400);
+ --components-menu-listDirection: row;
+ --components-menu-listAlign: flex-end;
+ --components-menu-listItemActionAlign: center;
+ --components-menu-listItemActionDisplay: inline-flex;
+ --components-menu-listItemActionTransform: scale(0, 1);
+ --components-menu-listItemActionColor: var(--palettes-grey-800);
+ --components-menu-listItemActionSize: 2px;
+ --components-menu-listItemActionFontSize: inherit;
+ --components-menu-listItemActionLineHeight: inherit;
+ --components-menu-listItemActionInset: auto 0 0 0;
+ --components-menu-listItemActionRadius: var(--commons-borderRadius-M) var(--commons-borderRadius-M) 0 0;
+ --components-menu-listItemActionRadiusWidth: auto;
+ --components-menu-listItemActionRadiusHeight: var(--components-menu-listItemActionSize);
}
diff --git a/packages/scss/src/components/navside/component.scss b/packages/scss/src/components/navside/component.scss
index 88bd44c46a..56610aa037 100644
--- a/packages/scss/src/components/navside/component.scss
+++ b/packages/scss/src/components/navside/component.scss
@@ -110,25 +110,6 @@
font-weight: 600;
}
- // .navSide-item-alert is deprecated
- .navSide-item-alert {
- font-size: var(--sizes-XS-fontSize);
- font-weight: 600;
- background-color: var(--components-navSide-fullwidth-palette-alert-color);
- color: var(--components-navSide-fullwidth-palette-alert-text);
- transition-duration: var(--commons-animations-durations-standard);
- transition-property: background-color;
- margin-left: auto;
- border-radius: 6px;
- height: var(--sizes-S-lineHeight);
- line-height: var(--sizes-S-lineHeight);
- margin-right: 0;
- padding: 0 var(--pr-t-spacings-50);
- min-width: 1.25rem;
- text-align: center;
- flex-shrink: 0;
- }
-
.navSide-item-arrow {
@include icon.S;
@@ -204,12 +185,6 @@
color: var(--components-navSide-bottom-section-palette-hovered-text);
}
}
-
- // .navSide-item-alert is deprecated
- .navSide-item-alert {
- background-color: var(--components-navSide-bottom-section-palette-alert-color);
- color: var(--components-navSide-bottom-section-palette-alert-text);
- }
}
.navSide-item-placeholder {
diff --git a/packages/scss/src/components/navside/mods.scss b/packages/scss/src/components/navside/mods.scss
index 8ec68d7a51..c0fbbd0baf 100644
--- a/packages/scss/src/components/navside/mods.scss
+++ b/packages/scss/src/components/navside/mods.scss
@@ -30,15 +30,6 @@
margin-left: inherit;
}
- .navSide-item-alert {
- // deprecated
- background-color: var(--components-navSide-compact-palette-alert-color);
- color: var(--components-navSide-compact-palette-alert-text);
- padding: 0 var(--pr-t-spacings-100);
- margin: auto;
- position: relative;
- }
-
.navSide-item-placeholder {
flex-direction: column;
@@ -83,12 +74,6 @@
background-color: var(--components-navSide-compact-palette-selected-bg);
color: var(--components-navSide-compact-palette-selected-text);
opacity: 1;
-
- .navSide-item-alert {
- // deprecated
- background-color: var(--components-navSide-compact-palette-selected-alert-color);
- color: var(--components-navSide-compact-palette-selected-alert-text);
- }
}
@mixin banner {
diff --git a/packages/scss/src/components/navside/states.scss b/packages/scss/src/components/navside/states.scss
index 30febb5d32..eb57a59049 100644
--- a/packages/scss/src/components/navside/states.scss
+++ b/packages/scss/src/components/navside/states.scss
@@ -2,12 +2,6 @@
background-color: var(--components-navSide-fullwidth-palette-selected-bg);
color: var(--components-navSide-fullwidth-palette-selected-text);
opacity: 1;
-
- .navSide-item-alert {
- // deprecated
- background-color: var(--components-navSide-fullwidth-palette-selected-alert-color);
- color: var(--components-navSide-fullwidth-palette-selected-alert-text);
- }
}
@mixin expanded {
diff --git a/packages/scss/src/components/popover/component.scss b/packages/scss/src/components/popover/component.scss
new file mode 100644
index 0000000000..52ec1a38c1
--- /dev/null
+++ b/packages/scss/src/components/popover/component.scss
@@ -0,0 +1,50 @@
+@use '@lucca-front/scss/src/components/button/exports' as button;
+@use '@lucca-front/scss/src/commons/utils/a11y';
+
+@mixin component($atRoot: 'without: rule') {
+ display: block;
+ background-color: var(--pr-t-elevation-surface-raised);
+ box-shadow: var(--pr-t-elevation-shadow-overlay);
+ border-radius: var(--commons-borderRadius-L);
+ position: relative;
+ min-height: var(--pr-t-spacings-500);
+ min-width: var(--pr-t-spacings-500);
+ animation: popup var(--commons-animations-durations-fast) ease 1 forwards;
+
+ // need of a higher specificity
+ .popover-close {
+ --components-button-padding: var(--pr-t-spacings-50);
+ }
+
+ @at-root {
+ .popover-contentOptional {
+ padding: var(--pr-t-spacings-100) var(--pr-t-spacings-150);
+ }
+
+ .popover-close {
+ @include button.text;
+ @include button.XS;
+ @include button.onlyIconXS;
+
+ position: absolute;
+ right: var(--pr-t-spacings-150);
+ top: var(--pr-t-spacings-100);
+ z-index: 2;
+
+ &:not(:focus-visible) {
+ @include a11y.mask;
+ }
+ }
+ }
+
+ @keyframes popup {
+ from {
+ transform: scale(0.95);
+ opacity: 0.5;
+ }
+ to {
+ transform: scale(1);
+ opacity: 1;
+ }
+ }
+}
diff --git a/packages/scss/src/components/popover/exports.scss b/packages/scss/src/components/popover/exports.scss
new file mode 100644
index 0000000000..2c2986a26b
--- /dev/null
+++ b/packages/scss/src/components/popover/exports.scss
@@ -0,0 +1,4 @@
+@forward 'vars';
+@forward 'mods';
+@forward 'states';
+@forward 'component';
diff --git a/packages/scss/src/components/popover/index.scss b/packages/scss/src/components/popover/index.scss
new file mode 100644
index 0000000000..05914c52b2
--- /dev/null
+++ b/packages/scss/src/components/popover/index.scss
@@ -0,0 +1,6 @@
+@use 'exports' as *;
+
+.popover {
+ @include vars;
+ @include component;
+}
diff --git a/packages/scss/src/components/popover/mods.scss b/packages/scss/src/components/popover/mods.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/scss/src/components/popover/states.scss b/packages/scss/src/components/popover/states.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/scss/src/components/status/vars.scss b/packages/scss/src/components/popover/vars.scss
similarity index 100%
rename from packages/scss/src/components/status/vars.scss
rename to packages/scss/src/components/popover/vars.scss
diff --git a/packages/scss/src/components/scrollBox/component.scss b/packages/scss/src/components/scrollBox/component.scss
new file mode 100644
index 0000000000..fec153ac20
--- /dev/null
+++ b/packages/scss/src/components/scrollBox/component.scss
@@ -0,0 +1,114 @@
+@use '@lucca-front/scss/src/commons/utils/color';
+
+@mixin component($atRoot: 'without: rule') {
+ background-color: var(--components-scrollBox-backgroundColor);
+ position: relative;
+ display: flex;
+ overflow: auto;
+ scrollbar-width: thin;
+
+ @media (hover: none) {
+ scrollbar-width: none;
+
+ &::-webkit-scrollbar {
+ display: none;
+ }
+ }
+
+ &::before,
+ &::after {
+ content: '';
+ pointer-events: none;
+ position: sticky;
+ flex-shrink: 0;
+ width: var(--components-scrollBox-shadowWidth);
+ top: 0;
+ bottom: 0;
+ background-repeat: no-repeat;
+ background-size: 75% 100%, 25% 100%, 1px 100%;
+ }
+
+ &::before {
+ left: 0;
+ background-position: 0% 50%;
+ background-image: radial-gradient(
+ farthest-side at 0% 50%,
+ color.transparentize(var(--components-scrollBox-shadowColor), 0.24),
+ color.transparentize(var(--colors-black-color), 0)
+ ),
+ radial-gradient(
+ farthest-side at 0% 50%,
+ color.transparentize(var(--components-scrollBox-shadowColor), 0.32) 50%,
+ color.transparentize(var(--colors-black-color), 0)
+ ),
+ radial-gradient(
+ farthest-side at 0% 50%,
+ color.transparentize(var(--components-scrollBox-shadowColor), 1) calc(100% - 1px),
+ color.transparentize(var(--colors-black-color), 0)
+ );
+ }
+
+ &::after {
+ right: 0;
+ background-position: 100% 50%;
+ background-image: radial-gradient(
+ farthest-side at 100% 50%,
+ color.transparentize(var(--components-scrollBox-shadowColor), 0.24),
+ color.transparentize(var(--colors-black-color), 0)
+ ),
+ radial-gradient(
+ farthest-side at 100% 50%,
+ color.transparentize(var(--components-scrollBox-shadowColor), 0.32) 50%,
+ color.transparentize(var(--colors-black-color), 0)
+ ),
+ radial-gradient(
+ farthest-side at 100% 50%,
+ color.transparentize(var(--components-scrollBox-shadowColor), 1) calc(100% - 1px),
+ color.transparentize(var(--colors-black-color), 0)
+ );
+ }
+
+ @at-root ($atRoot) {
+ .scrollBox-inner {
+ flex-shrink: 0;
+ position: relative;
+
+ &::before,
+ &::after {
+ content: '';
+ position: absolute;
+ z-index: 1;
+ display: block;
+ top: 1px;
+ bottom: 1px;
+ width: calc(var(--components-scrollBox-shadowWidth) * 2);
+ pointer-events: none;
+ }
+
+ &::before {
+ left: calc(var(--components-scrollBox-shadowWidth) * -1);
+ background-image: linear-gradient(
+ to right,
+ var(--components-scrollBox-backgroundColor) var(--components-scrollBox-shadowWidth),
+ color.transparentize(var(--colors-black-color), 0)
+ );
+ }
+
+ &::after {
+ right: calc(var(--components-scrollBox-shadowWidth) * -1);
+ background-image: linear-gradient(
+ to left,
+ var(--components-scrollBox-backgroundColor) var(--components-scrollBox-shadowWidth),
+ color.transparentize(var(--colors-black-color), 0)
+ );
+ }
+ }
+
+ .scrollBox-inner-content {
+ margin-left: calc(var(--components-scrollBox-shadowWidth) * -1);
+ margin-right: calc(var(--components-scrollBox-shadowWidth) * -1);
+ position: relative;
+ z-index: 2;
+ }
+ }
+}
diff --git a/packages/scss/src/components/scrollBox/exports.scss b/packages/scss/src/components/scrollBox/exports.scss
new file mode 100644
index 0000000000..2c2986a26b
--- /dev/null
+++ b/packages/scss/src/components/scrollBox/exports.scss
@@ -0,0 +1,4 @@
+@forward 'vars';
+@forward 'mods';
+@forward 'states';
+@forward 'component';
diff --git a/packages/scss/src/components/scrollBox/index.scss b/packages/scss/src/components/scrollBox/index.scss
new file mode 100644
index 0000000000..2f932cada5
--- /dev/null
+++ b/packages/scss/src/components/scrollBox/index.scss
@@ -0,0 +1,6 @@
+@use 'exports' as *;
+
+.scrollBox {
+ @include vars;
+ @include component;
+}
diff --git a/packages/scss/src/components/scrollBox/mods.scss b/packages/scss/src/components/scrollBox/mods.scss
new file mode 100644
index 0000000000..b1d475da82
--- /dev/null
+++ b/packages/scss/src/components/scrollBox/mods.scss
@@ -0,0 +1,2 @@
+@mixin mod {
+}
diff --git a/packages/scss/src/components/scrollBox/states.scss b/packages/scss/src/components/scrollBox/states.scss
new file mode 100644
index 0000000000..c643d3a117
--- /dev/null
+++ b/packages/scss/src/components/scrollBox/states.scss
@@ -0,0 +1,2 @@
+@mixin state {
+}
diff --git a/packages/scss/src/components/scrollBox/vars.scss b/packages/scss/src/components/scrollBox/vars.scss
new file mode 100644
index 0000000000..5748cdd5e8
--- /dev/null
+++ b/packages/scss/src/components/scrollBox/vars.scss
@@ -0,0 +1,5 @@
+@mixin vars {
+ --components-scrollBox-backgroundColor: var(--pr-t-elevation-surface-default);
+ --components-scrollBox-shadowColor: var(--palettes-neutral-400);
+ --components-scrollBox-shadowWidth: var(--pr-t-spacings-200);
+}
diff --git a/packages/scss/src/components/status/component.scss b/packages/scss/src/components/status/component.scss
deleted file mode 100644
index 38b83da0d2..0000000000
--- a/packages/scss/src/components/status/component.scss
+++ /dev/null
@@ -1,47 +0,0 @@
-@mixin component($atRoot: 'without: rule') {
- display: inline-flex;
- align-items: center;
- white-space: nowrap;
-
- @at-root ($atRoot) {
- .status-dot {
- aspect-ratio: 1;
- width: var(--pr-t-spacings-100);
- border-radius: var(--commons-borderRadius-full);
- background-color: currentColor;
- margin: calc(var(--pr-t-spacings-50) / 2);
- margin-right: var(--pr-t-spacings-100);
- position: relative;
- z-index: 1;
- display: inline-flex;
- }
-
- .status-dot-important {
- @keyframes status {
- 0% {
- transform: scale(1);
- opacity: 1;
- }
- 70% {
- transform: scale(2.25);
- opacity: 0;
- }
- 100% {
- transform: scale(2.25);
- opacity: 0;
- }
- }
-
- inset: 0;
- position: absolute;
- border-radius: var(--commons-borderRadius-full);
- animation: status 2s infinite;
- background-color: currentColor;
- }
-
- .status-label {
- font-size: var(--sizes-S-fontSize);
- color: var(--palettes-neutral-800);
- }
- }
-}
diff --git a/packages/scss/src/components/status/index.scss b/packages/scss/src/components/status/index.scss
deleted file mode 100644
index 75c9f2d5eb..0000000000
--- a/packages/scss/src/components/status/index.scss
+++ /dev/null
@@ -1,18 +0,0 @@
-@use 'exports' as *;
-
-.status {
- @include vars;
- @include component;
-
- &:is(.success, .is-success) {
- @include success;
- }
-
- &:is(.warning, .is-warning) {
- @include warning;
- }
-
- &:is(.error, .is-error) {
- @include error;
- }
-}
diff --git a/packages/scss/src/components/status/states.scss b/packages/scss/src/components/status/states.scss
deleted file mode 100644
index 066d99296d..0000000000
--- a/packages/scss/src/components/status/states.scss
+++ /dev/null
@@ -1,17 +0,0 @@
-@mixin success {
- .status-dot {
- color: var(--palettes-success-700);
- }
-}
-
-@mixin error {
- .status-dot {
- color: var(--palettes-error-700);
- }
-}
-
-@mixin warning {
- .status-dot {
- color: var(--palettes-warning-700);
- }
-}
diff --git a/packages/scss/src/components/table/index.scss b/packages/scss/src/components/table/index.scss
index 4866f767a2..f6db6961ec 100644
--- a/packages/scss/src/components/table/index.scss
+++ b/packages/scss/src/components/table/index.scss
@@ -32,10 +32,6 @@
@include S;
}
- &.mod-layoutFixed {
- @include layoutFixed;
- }
-
&.mod-noOffset {
@include noOffset;
}
diff --git a/packages/scss/src/components/table/mods.scss b/packages/scss/src/components/table/mods.scss
index dc4c6e05f3..dce0250654 100644
--- a/packages/scss/src/components/table/mods.scss
+++ b/packages/scss/src/components/table/mods.scss
@@ -347,10 +347,6 @@
text-align: right;
}
-@mixin layoutFixed {
- table-layout: fixed;
-}
-
@mixin noOffset {
.table-head-row-cell,
.table-body-row-cell,
diff --git a/packages/scss/src/components/tableFixed/index.scss b/packages/scss/src/components/tableFixed/index.scss
index 8279c50229..785ffdf6e6 100644
--- a/packages/scss/src/components/tableFixed/index.scss
+++ b/packages/scss/src/components/tableFixed/index.scss
@@ -1,21 +1,18 @@
-@use '@lucca-front/scss/src/commons/config';
-@use '@lucca-front/scss/src/commons/utils/media';
@use 'exports' as *;
-.table-head-row-cell,
-.table-body-row-cell,
-.table-foot-row-cell {
- @for $i from 2 through 20 {
- &.mod-layoutFixed-#{$i} {
- @include layoutFixed($i);
- }
+.table {
+ // 1 - Layout fixed
+ &.mod-layoutFixed {
+ @include layoutFixed;
+ }
+
+ // 2 - Layout fixed starting at breakpoint
+ &[class*='mod-layoutFixedAtMediaMin'] {
+ @include layoutFixedWithBreakpoint;
+ }
- @each $breakpoint, $value in config.$breakpoints {
- @include media.min($breakpoint) {
- &.mod-layoutFixed-#{$i}\@mediaMin#{$breakpoint} {
- @include layoutFixed($i);
- }
- }
- }
+ // Cells management for 1 & 2
+ &[class*='mod-layoutFixed'] {
+ @include layoutFixedCells;
}
}
diff --git a/packages/scss/src/components/tableFixed/mods.scss b/packages/scss/src/components/tableFixed/mods.scss
index 8d5a9b86bd..fa82712014 100644
--- a/packages/scss/src/components/tableFixed/mods.scss
+++ b/packages/scss/src/components/tableFixed/mods.scss
@@ -1,5 +1,49 @@
-@mixin layoutFixed($i) {
- min-width: $i * 1rem;
- max-width: $i * 1rem;
- width: $i * 1rem;
+@use '@lucca-front/scss/src/commons/config';
+@use '@lucca-front/scss/src/commons/utils/media';
+
+@mixin layoutFixed {
+ table-layout: fixed;
+}
+
+@mixin layoutFixedWithBreakpoint {
+ @each $breakpoint, $value in config.$breakpoints {
+ @include media.min($breakpoint) {
+ &.mod-layoutFixedAtMediaMin#{$breakpoint} {
+ table-layout: fixed;
+ }
+ }
+ }
+}
+
+@mixin layoutFixedCells {
+ .table-head-row-cell,
+ .table-body-row-cell,
+ .table-foot-row-cell {
+ --cell-width: var(--table-layoutFixed-width);
+ }
+
+ &.mod-layoutFixed {
+ .table-head-row-cell,
+ .table-body-row-cell,
+ .table-foot-row-cell {
+ @include cellFixedWidth;
+ }
+ }
+
+ @each $breakpoint, $value in config.$breakpoints {
+ @include media.min($breakpoint) {
+ &.mod-layoutFixedAtMediaMin#{$breakpoint} {
+ .table-head-row-cell,
+ .table-body-row-cell,
+ .table-foot-row-cell {
+ @include cellFixedWidth;
+ }
+ }
+ }
+ }
+}
+
+@mixin cellFixedWidth {
+ min-width: var(--cell-width, auto);
+ width: var(--cell-width, auto);
}
diff --git a/packages/scss/src/components/tableFixed/vars.scss b/packages/scss/src/components/tableFixed/vars.scss
index e69de29bb2..18998b2cf0 100644
--- a/packages/scss/src/components/tableFixed/vars.scss
+++ b/packages/scss/src/components/tableFixed/vars.scss
@@ -0,0 +1,3 @@
+@mixin vars {
+
+}
diff --git a/packages/scss/src/components/tableFixedDeprecated/component.scss b/packages/scss/src/components/tableFixedDeprecated/component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/scss/src/components/tableFixedDeprecated/exports.scss b/packages/scss/src/components/tableFixedDeprecated/exports.scss
new file mode 100644
index 0000000000..2c2986a26b
--- /dev/null
+++ b/packages/scss/src/components/tableFixedDeprecated/exports.scss
@@ -0,0 +1,4 @@
+@forward 'vars';
+@forward 'mods';
+@forward 'states';
+@forward 'component';
diff --git a/packages/scss/src/components/tableFixedDeprecated/index.scss b/packages/scss/src/components/tableFixedDeprecated/index.scss
new file mode 100644
index 0000000000..8279c50229
--- /dev/null
+++ b/packages/scss/src/components/tableFixedDeprecated/index.scss
@@ -0,0 +1,21 @@
+@use '@lucca-front/scss/src/commons/config';
+@use '@lucca-front/scss/src/commons/utils/media';
+@use 'exports' as *;
+
+.table-head-row-cell,
+.table-body-row-cell,
+.table-foot-row-cell {
+ @for $i from 2 through 20 {
+ &.mod-layoutFixed-#{$i} {
+ @include layoutFixed($i);
+ }
+
+ @each $breakpoint, $value in config.$breakpoints {
+ @include media.min($breakpoint) {
+ &.mod-layoutFixed-#{$i}\@mediaMin#{$breakpoint} {
+ @include layoutFixed($i);
+ }
+ }
+ }
+ }
+}
diff --git a/packages/scss/src/components/tableFixedDeprecated/mods.scss b/packages/scss/src/components/tableFixedDeprecated/mods.scss
new file mode 100644
index 0000000000..8d5a9b86bd
--- /dev/null
+++ b/packages/scss/src/components/tableFixedDeprecated/mods.scss
@@ -0,0 +1,5 @@
+@mixin layoutFixed($i) {
+ min-width: $i * 1rem;
+ max-width: $i * 1rem;
+ width: $i * 1rem;
+}
diff --git a/packages/scss/src/components/tableFixedDeprecated/states.scss b/packages/scss/src/components/tableFixedDeprecated/states.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/scss/src/components/tableFixedDeprecated/vars.scss b/packages/scss/src/components/tableFixedDeprecated/vars.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/scss/src/components/tableSticked/index.scss b/packages/scss/src/components/tableSticked/index.scss
index 883952365a..25489d63f4 100644
--- a/packages/scss/src/components/tableSticked/index.scss
+++ b/packages/scss/src/components/tableSticked/index.scss
@@ -5,51 +5,29 @@
.table {
@include vars;
- &.mod-stickyColumn {
+ //For both mod-stickyColumn + responsive variant
+ &[class*='mod-stickyColumn'] {
@include stickyColumn;
}
- @each $breakpoint, $value in config.$breakpoints {
- @include media.min($breakpoint) {
- &.mod-stickyColumn\@mediaMin#{$breakpoint} {
- @include stickyColumn;
- }
- }
- }
-
- &.mod-stickyColumn-shadow {
- @include shadow;
+ &.mod-stickyColumn {
+ @include stickyColumnOffset;
}
- &[class*='mod-stickyColumn'] {
- .table-head-row-cell,
- .table-body-row-cell,
- .table-foot-row-cell {
- @for $i from 0 through 50 {
- &.mod-stickyColumn-leftOffset#{$i} {
- @include leftOffset($i);
- }
-
- &.mod-stickyColumn-rightOffset#{$i} {
- @include rightOffset($i);
- }
+ @each $breakpoint, $value in config.$breakpoints {
+ @include media.min($breakpoint) {
+ &.mod-stickyColumnAtMediaMin#{$breakpoint} {
+ @include stickyColumnOffset;
}
}
}
- [class*='sticky'][class*='shadow-wrapper'] {
- @include wrapper;
- }
-
+ //For both stickyHeader + -withBanner variant
&[class*='mod-stickyHeader'] {
@include stickyHeaderCommon;
- @each $breakpoint, $value in config.$breakpoints {
- @include media.max($breakpoint) {
- &.mod-stickyColumn\@mediaMin#{$breakpoint} {
- @include stickyColumnBreakpoint;
- }
- }
+ .mod-stickyHeader-shadow {
+ @include stickyHeaderShadow;
}
}
@@ -61,10 +39,3 @@
@include stickyHeaderBanner;
}
}
-
-.table-body-row,
-.table-foot-row {
- &.mod-stickyHeader-shadow {
- @include stickyHeaderShadow;
- }
-}
diff --git a/packages/scss/src/components/tableSticked/mods.scss b/packages/scss/src/components/tableSticked/mods.scss
index 5bf7047453..f3ed618de7 100644
--- a/packages/scss/src/components/tableSticked/mods.scss
+++ b/packages/scss/src/components/tableSticked/mods.scss
@@ -1,212 +1,171 @@
@use 'sass:color';
-
+@use '@lucca-front/scss/src/commons/utils/media';
+@use '@lucca-front/scss/src/commons/config';
@use '@lucca-front/icons/src/commons/utils/icon';
@use '@lucca-front/scss/src/commons/utils/reset';
-@mixin stickyColumn($shadowColor: #2a3551) {
+@mixin stickyColumn {
width: auto;
min-width: 100%;
- background-color: var(--pr-t-elevation-surface-raised);
+ background-color: var(--colors-white-color);
- .table-head-row-cell,
- .table-body-row-cell,
- .table-foot-row-cell {
- &[class*='mod-stickyColumn-'] {
- position: sticky;
- background-color: var(--pr-t-elevation-surface-raised);
- z-index: 3;
- }
-
- &.mod-stickyColumn-shadow {
- z-index: 1;
- min-width: var(--components-table-fixed-column-sticky-shadow-width);
- max-width: var(--components-table-fixed-column-sticky-shadow-width);
- width: var(--components-table-fixed-column-sticky-shadow-width);
- padding: 0;
- background: transparent;
- }
-
- .stickyColumn-shadow-wrapper {
- display: flex;
- }
+ //All stickies columns
+ [class*='mod-stickyColumn-'] {
+ background-color: var(--colors-white-color);
+ z-index: 3;
}
+ //Left sticked columns
[class*='mod-stickyColumn-left'] {
- .stickyColumn-shadow-wrapper {
- left: calc(var(--components-table-fixed-column-sticky-shadow-width) * -1);
+ left: var(--components-tableSticked-column-sticky-offset);
- &::after {
- background-image: linear-gradient(to right, color.adjust($shadowColor, $alpha: -0.75), color.adjust($shadowColor, $alpha: -1));
- }
+ //left sticked columns shadow
+ &.mod-stickyColumn-shadow,
+ .stickyColumn-shadow-wrapper::before {
+ left: calc(var(--components-tableSticked-column-sticky-offset) - var(--components-tableSticked-column-sticky-shadow-width));
+ }
+ .stickyColumn-shadow-wrapper::after {
+ left: var(--components-tableSticked-column-sticky-offset);
+ background-image: linear-gradient(to right, var(--components-tableSticked-column-sticky-shadow-color), transparent);
}
}
+ //Right sticked columns
[class*='mod-stickyColumn-right'] {
- .stickyColumn-shadow-wrapper {
- right: calc(var(--components-table-fixed-column-sticky-shadow-width) * -1);
+ right: var(--components-tableSticked-column-sticky-offset);
- &::after {
- background-image: linear-gradient(to left, color.adjust($shadowColor, $alpha: -0.75), color.adjust($shadowColor, $alpha: -1));
- }
+ //right sticked columns shadow
+ .stickyColumn-shadow-wrapper {
+ justify-items: end;
+ width: calc(var(--components-tableSticked-column-sticky-shadow-width) * 3);
+ right: calc(var(--components-tableSticked-column-sticky-shadow-width) * -1);
+ }
+ &.mod-stickyColumn-shadow,
+ .stickyColumn-shadow-wrapper::before {
+ right: calc(var(--components-tableSticked-column-sticky-offset) - var(--components-tableSticked-column-sticky-shadow-width));
+ }
+ .stickyColumn-shadow-wrapper::after {
+ right: var(--components-tableSticked-column-sticky-offset);
+ background-image: linear-gradient(to left, var(--components-tableSticked-column-sticky-shadow-color), transparent);
}
}
- &[class*='mod-stickyHeader'] {
- &[class*='mod-stickyColumn'] {
- .table-head-row-cell {
- &[class*='mod-stickyColumn'] {
- z-index: 6;
-
- &:not(.mod-stickyColumn-shadow) {
- z-index: 7;
- }
- }
-
- &.mod-columnSticky-shadowMask {
- &::before {
- width: var(--components-table-fixed-column-sticky-shadow-width);
- left: calc(var(--components-table-fixed-column-sticky-shadow-width) * -1);
- background: var(--pr-t-elevation-surface-raised);
- top: 0;
- bottom: 0;
- z-index: 4;
- position: absolute;
- content: '';
- }
- }
-
- &:not(.mod-columnSticky-shadowMask) {
- &:not(.mod-stickyColumn-shadow) {
- + .mod-columnSticky-shadowMask {
- &::before {
- left: auto;
- right: calc(var(--components-table-fixed-column-sticky-shadow-width) * -1);
- }
- }
- }
- }
- }
- }
+ //Sticky columns drop shadow
+ .mod-stickyColumn-shadow {
+ display: none;
+ width: 0;
+ min-width: 0;
+ padding: 0;
}
-}
-@mixin shadow {
- width: 0;
- position: static;
+ .stickyColumn-shadow-wrapper {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ display: grid;
+ grid-template-columns: calc(var(--components-tableSticked-column-sticky-shadow-width) * 2);
+ grid-template-areas: 'cell';
+ width: calc(var(--components-tableSticked-column-sticky-shadow-width) * 2);
+
+ &::before,
+ &::after {
+ content: '';
+ position: sticky;
+ grid-area: cell;
+ }
+ &::after {
+ width: var(--components-tableSticked-column-sticky-shadow-width);
+ }
+ &::before {
+ z-index: 1;
+ width: var(--components-tableSticked-column-sticky-shadow-width);
+ background-color: var(--colors-white-color);
+ }
+ }
}
-@mixin leftOffset($i) {
- left: calc(#{$i} * var(--pr-t-spacings-200));
+@mixin stickyColumnOffset {
+ [class*='mod-stickyColumn-'] {
+ --components-tableSticked-column-sticky-offset: var(--table-stickyColumn-offset, 0rem);
- &.mod-stickyColumn-shadow,
- .stickyColumn-shadow-wrapper::after {
- left: calc(#{$i} * var(--pr-t-spacings-200) + var(--components-table-fixed-column-sticky-shadow-width));
+ position: sticky;
+ }
+ .mod-stickyColumn-shadow {
+ display: table-cell;
}
}
-@mixin rightOffset($i) {
- right: calc(#{$i} * var(--pr-t-spacings-200));
+/*****
+ Sticky header
+****/
- &.mod-stickyColumn-shadow,
- .stickyColumn-shadow-wrapper::after {
- right: calc(#{$i} * var(--pr-t-spacings-200) + var(--components-table-fixed-column-sticky-shadow-width));
+@mixin stickyHeader {
+ .table-head-row-cell {
+ top: 0;
+ }
+ .mod-stickyHeader-shadow .table-body-row-cell {
+ top: calc(var(--table-stickyHeader-shadow-offset) + var(--components-tableSticked-column-sticky-shadow-width));
}
}
-@mixin wrapper {
- bottom: calc(var(--commons-divider-width) * -1);
- width: var(--components-table-fixed-column-sticky-shadow-width);
- border-bottom-width: var(--commons-divider-width);
- border-bottom-color: var(--commons-divider-color);
- border-bottom-style: solid;
- display: flex;
- position: absolute;
- top: 0;
-
- &::after {
- width: var(--components-table-fixed-column-sticky-shadow-width);
- position: sticky;
- display: block;
- background-color: transparent;
- height: 100%;
- content: '';
+@mixin stickyHeaderBanner {
+ .table-head-row-cell {
+ top: var(--commons-banner-height);
+ }
+ .mod-stickyHeader-shadow .table-body-row-cell {
+ top: calc(
+ var(--table-stickyHeader-shadow-offset) + var(--commons-banner-height) + var(--components-tableSticked-column-sticky-shadow-width)
+ );
}
}
@mixin stickyHeaderCommon {
+ margin-top: var(--components-tableSticked-column-sticky-shadow-width);
.table-head-row-cell {
- background-color: var(--pr-t-elevation-surface-raised);
+ background-color: var(--colors-white-color);
position: sticky;
z-index: 5;
+ &[class*='mod-stickyColumn'] {
+ z-index: 7;
+ }
}
}
-@mixin stickyColumnBreakpoint {
- .table-head-row-cell {
- left: auto !important;
- right: auto !important;
- }
-}
+/*****
+ Sticky header drop shadow
+****/
-@mixin stickyHeaderShadow($shadowColor: #2a3551) {
- .table-body-row-cell,
- .table-foot-row-cell {
- top: calc(var(--sticky-header-shadow-offset-top) + var(--components-table-fixed-column-sticky-shadow-width));
+@mixin stickyHeaderShadow {
+ [class*='row-cell'] {
+ position: sticky;
+ top: var(--table-stickyHeader-shadow-offset);
z-index: 4;
height: 0;
padding: 0;
border: 0;
- position: sticky;
background: transparent;
}
.stickyHeader-shadow-wrapper {
- top: calc(var(--components-table-fixed-column-sticky-shadow-width) * -1);
+ position: absolute;
+ top: calc(var(--components-tableSticked-column-sticky-shadow-width) * -1);
width: 100%;
height: 0;
border: 0;
&::after {
- top: calc(var(--sticky-header-shadow-offset-top) + var(--components-table-fixed-column-sticky-shadow-width));
- height: var(--components-table-fixed-column-sticky-shadow-width);
- background-image: linear-gradient(to bottom, color.adjust($shadowColor, $alpha: -0.75), color.adjust($shadowColor, $alpha: -1));
+ content: '';
+ display: block;
+ height: var(--components-tableSticked-column-sticky-shadow-width);
width: 100%;
- opacity: 0.5;
+ background-image: linear-gradient(to bottom, var(--components-tableSticked-column-sticky-shadow-color), transparent);
}
}
+ .table-body-row,
+ .table-foot-row {
- .table-body-row-cell,
- .table-foot-row-cell {
+ [class*='row-cell'] {
border-top: 0;
}
}
}
-
-@mixin stickyHeader {
- .table-head-row-cell {
- top: 0;
- }
-}
-
-@mixin stickyHeaderBanner {
- .table-head-row-cell {
- top: var(commons-banner-height);
- }
-
- .table-body-row-cell,
- .table-foot-row-cell {
- top: calc(
- var(commons-banner-height) + var(--sticky-header-shadow-offset-top) + var(--components-table-fixed-column-sticky-shadow-width)
- );
-
- .stickyHeader-shadow-wrapper {
- &::after {
- top: calc(
- var(commons-banner-height) + var(--sticky-header-shadow-offset-top) + var(--components-table-fixed-column-sticky-shadow-width)
- );
- }
- }
- }
-}
diff --git a/packages/scss/src/components/tableSticked/vars.scss b/packages/scss/src/components/tableSticked/vars.scss
index c293870b74..a04113e515 100644
--- a/packages/scss/src/components/tableSticked/vars.scss
+++ b/packages/scss/src/components/tableSticked/vars.scss
@@ -1,8 +1,6 @@
-@mixin vars {
- // --components-table-sticky-column-max-offset: 50;
- // --components-table-fixed-column-min-col-width: 2;
- // --components-table-fixed-column-max-col-width: 20;
- // --components-table-fixed-column-sticky-shadow-color: #2a3551;
+@use '@lucca-front/scss/src/commons/utils/color';
- --components-table-fixed-column-sticky-shadow-width: 0.625rem;
+@mixin vars {
+ --components-tableSticked-column-sticky-shadow-width: 0.5rem;
+ --components-tableSticked-column-sticky-shadow-color: #{color.transparentize(var(--palettes-neutral-400), 0.24)};
}
diff --git a/packages/scss/src/components/tableStickedDeprecated/component.scss b/packages/scss/src/components/tableStickedDeprecated/component.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/scss/src/components/tableStickedDeprecated/exports.scss b/packages/scss/src/components/tableStickedDeprecated/exports.scss
new file mode 100644
index 0000000000..2c2986a26b
--- /dev/null
+++ b/packages/scss/src/components/tableStickedDeprecated/exports.scss
@@ -0,0 +1,4 @@
+@forward 'vars';
+@forward 'mods';
+@forward 'states';
+@forward 'component';
diff --git a/packages/scss/src/components/tableStickedDeprecated/index.scss b/packages/scss/src/components/tableStickedDeprecated/index.scss
new file mode 100644
index 0000000000..883952365a
--- /dev/null
+++ b/packages/scss/src/components/tableStickedDeprecated/index.scss
@@ -0,0 +1,70 @@
+@use '@lucca-front/scss/src/commons/config';
+@use '@lucca-front/scss/src/commons/utils/media';
+@use 'exports' as *;
+
+.table {
+ @include vars;
+
+ &.mod-stickyColumn {
+ @include stickyColumn;
+ }
+
+ @each $breakpoint, $value in config.$breakpoints {
+ @include media.min($breakpoint) {
+ &.mod-stickyColumn\@mediaMin#{$breakpoint} {
+ @include stickyColumn;
+ }
+ }
+ }
+
+ &.mod-stickyColumn-shadow {
+ @include shadow;
+ }
+
+ &[class*='mod-stickyColumn'] {
+ .table-head-row-cell,
+ .table-body-row-cell,
+ .table-foot-row-cell {
+ @for $i from 0 through 50 {
+ &.mod-stickyColumn-leftOffset#{$i} {
+ @include leftOffset($i);
+ }
+
+ &.mod-stickyColumn-rightOffset#{$i} {
+ @include rightOffset($i);
+ }
+ }
+ }
+ }
+
+ [class*='sticky'][class*='shadow-wrapper'] {
+ @include wrapper;
+ }
+
+ &[class*='mod-stickyHeader'] {
+ @include stickyHeaderCommon;
+
+ @each $breakpoint, $value in config.$breakpoints {
+ @include media.max($breakpoint) {
+ &.mod-stickyColumn\@mediaMin#{$breakpoint} {
+ @include stickyColumnBreakpoint;
+ }
+ }
+ }
+ }
+
+ &.mod-stickyHeader {
+ @include stickyHeader;
+ }
+
+ &.mod-stickyHeader-withBanner {
+ @include stickyHeaderBanner;
+ }
+}
+
+.table-body-row,
+.table-foot-row {
+ &.mod-stickyHeader-shadow {
+ @include stickyHeaderShadow;
+ }
+}
diff --git a/packages/scss/src/components/tableStickedDeprecated/mods.scss b/packages/scss/src/components/tableStickedDeprecated/mods.scss
new file mode 100644
index 0000000000..de9471f6c0
--- /dev/null
+++ b/packages/scss/src/components/tableStickedDeprecated/mods.scss
@@ -0,0 +1,212 @@
+@use 'sass:color';
+
+@use '@lucca-front/icons/src/commons/utils/icon';
+@use '@lucca-front/scss/src/commons/utils/reset';
+
+@mixin stickyColumn($shadowColor: #2a3551) {
+ width: auto;
+ min-width: 100%;
+ background-color: var(--pr-t-elevation-surface-raised);
+
+ .table-head-row-cell,
+ .table-body-row-cell,
+ .table-foot-row-cell {
+ &[class*='mod-stickyColumn-'] {
+ position: sticky;
+ background-color: var(--pr-t-elevation-surface-raised);
+ z-index: 3;
+ }
+
+ &.mod-stickyColumn-shadow {
+ z-index: 1;
+ min-width: var(--components-tableFixed-column-sticky-shadow-width);
+ max-width: var(--components-tableFixed-column-sticky-shadow-width);
+ width: var(--components-tableFixed-column-sticky-shadow-width);
+ padding: 0;
+ background: transparent;
+ }
+
+ .stickyColumn-shadow-wrapper {
+ display: flex;
+ }
+ }
+
+ [class*='mod-stickyColumn-left'] {
+ .stickyColumn-shadow-wrapper {
+ left: calc(var(--components-tableFixed-column-sticky-shadow-width) * -1);
+
+ &::after {
+ background-image: linear-gradient(to right, color.adjust($shadowColor, $alpha: -0.75), color.adjust($shadowColor, $alpha: -1));
+ }
+ }
+ }
+
+ [class*='mod-stickyColumn-right'] {
+ .stickyColumn-shadow-wrapper {
+ right: calc(var(--components-tableFixed-column-sticky-shadow-width) * -1);
+
+ &::after {
+ background-image: linear-gradient(to left, color.adjust($shadowColor, $alpha: -0.75), color.adjust($shadowColor, $alpha: -1));
+ }
+ }
+ }
+
+ &[class*='mod-stickyHeader'] {
+ &[class*='mod-stickyColumn'] {
+ .table-head-row-cell {
+ &[class*='mod-stickyColumn'] {
+ z-index: 6;
+
+ &:not(.mod-stickyColumn-shadow) {
+ z-index: 7;
+ }
+ }
+
+ &.mod-columnSticky-shadowMask {
+ &::before {
+ width: var(--components-tableFixed-column-sticky-shadow-width);
+ left: calc(var(--components-tableFixed-column-sticky-shadow-width) * -1);
+ background: var(--pr-t-elevation-surface-raised);
+ top: 0;
+ bottom: 0;
+ z-index: 4;
+ position: absolute;
+ content: '';
+ }
+ }
+
+ &:not(.mod-columnSticky-shadowMask) {
+ &:not(.mod-stickyColumn-shadow) {
+ + .mod-columnSticky-shadowMask {
+ &::before {
+ left: auto;
+ right: calc(var(--components-tableFixed-column-sticky-shadow-width) * -1);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@mixin shadow {
+ width: 0;
+ position: static;
+}
+
+@mixin leftOffset($i) {
+ left: calc(#{$i} * var(--pr-t-spacings-200));
+
+ &.mod-stickyColumn-shadow,
+ .stickyColumn-shadow-wrapper::after {
+ left: calc(#{$i} * var(--pr-t-spacings-200) + var(--components-tableFixed-column-sticky-shadow-width));
+ }
+}
+
+@mixin rightOffset($i) {
+ right: calc(#{$i} * var(--pr-t-spacings-200));
+
+ &.mod-stickyColumn-shadow,
+ .stickyColumn-shadow-wrapper::after {
+ right: calc(#{$i} * var(--pr-t-spacings-200) + var(--components-tableFixed-column-sticky-shadow-width));
+ }
+}
+
+@mixin wrapper {
+ bottom: calc(var(--commons-divider-width) * -1);
+ width: var(--components-tableFixed-column-sticky-shadow-width);
+ border-bottom-width: var(--commons-divider-width);
+ border-bottom-color: var(--commons-divider-color);
+ border-bottom-style: solid;
+ display: flex;
+ position: absolute;
+ top: 0;
+
+ &::after {
+ width: var(--components-tableFixed-column-sticky-shadow-width);
+ position: sticky;
+ display: block;
+ background-color: transparent;
+ height: 100%;
+ content: '';
+ }
+}
+
+@mixin stickyHeaderCommon {
+ .table-head-row-cell {
+ background-color: var(--pr-t-elevation-surface-raised);
+ position: sticky;
+ z-index: 5;
+ }
+}
+
+@mixin stickyColumnBreakpoint {
+ .table-head-row-cell {
+ left: auto !important;
+ right: auto !important;
+ }
+}
+
+@mixin stickyHeaderShadow($shadowColor: #2a3551) {
+ .table-body-row-cell,
+ .table-foot-row-cell {
+ top: calc(var(--sticky-header-shadow-offset-top) + var(--components-tableFixed-column-sticky-shadow-width));
+ z-index: 4;
+ height: 0;
+ padding: 0;
+ border: 0;
+ position: sticky;
+ background: transparent;
+ }
+
+ .stickyHeader-shadow-wrapper {
+ top: calc(var(--components-tableFixed-column-sticky-shadow-width) * -1);
+ width: 100%;
+ height: 0;
+ border: 0;
+
+ &::after {
+ top: calc(var(--sticky-header-shadow-offset-top) + var(--components-tableFixed-column-sticky-shadow-width));
+ height: var(--components-tableFixed-column-sticky-shadow-width);
+ background-image: linear-gradient(to bottom, color.adjust($shadowColor, $alpha: -0.75), color.adjust($shadowColor, $alpha: -1));
+ width: 100%;
+ opacity: 0.5;
+ }
+ }
+
+ + .table-body-row,
+ + .table-foot-row {
+ .table-body-row-cell,
+ .table-foot-row-cell {
+ border-top: 0;
+ }
+ }
+}
+
+@mixin stickyHeader {
+ .table-head-row-cell {
+ top: 0;
+ }
+}
+
+@mixin stickyHeaderBanner {
+ .table-head-row-cell {
+ top: var(commons-banner-height);
+ }
+
+ .table-body-row-cell,
+ .table-foot-row-cell {
+ top: calc(
+ var(commons-banner-height) + var(--sticky-header-shadow-offset-top) + var(--components-tableFixed-column-sticky-shadow-width)
+ );
+
+ .stickyHeader-shadow-wrapper {
+ &::after {
+ top: calc(
+ var(commons-banner-height) + var(--sticky-header-shadow-offset-top) + var(--components-tableFixed-column-sticky-shadow-width)
+ );
+ }
+ }
+ }
+}
diff --git a/packages/scss/src/components/tableStickedDeprecated/states.scss b/packages/scss/src/components/tableStickedDeprecated/states.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/scss/src/components/tableStickedDeprecated/vars.scss b/packages/scss/src/components/tableStickedDeprecated/vars.scss
new file mode 100644
index 0000000000..e1437ec436
--- /dev/null
+++ b/packages/scss/src/components/tableStickedDeprecated/vars.scss
@@ -0,0 +1,8 @@
+@mixin vars {
+ // --components-table-sticky-column-max-offset: 50;
+ // --components-tableFixed-column-min-col-width: 2;
+ // --components-tableFixed-column-max-col-width: 20;
+ // --components-tableFixed-column-sticky-shadow-color: #2a3551;
+
+ --components-tableFixed-column-sticky-shadow-width: 0.625rem;
+}
diff --git a/packages/scss/src/components/textField/component.scss b/packages/scss/src/components/textField/component.scss
index 8c580535ec..9bdddb7d80 100644
--- a/packages/scss/src/components/textField/component.scss
+++ b/packages/scss/src/components/textField/component.scss
@@ -8,6 +8,7 @@
align-items: stretch;
border-radius: var(--commons-borderRadius-M);
background-color: var(--component-textField-background);
+ box-shadow: 0 0 0 1px var(--component-textField-border);
&:has(.textField-input-value:focus-visible) {
@include a11y.focusVisible($offset: 3px);
@@ -18,7 +19,6 @@
display: flex;
align-items: center;
width: 100%;
- box-shadow: 0 0 0 1px var(--component-textField-border);
border-radius: var(--commons-borderRadius-M);
background-color: var(--component-textField-background);
position: relative;
@@ -66,7 +66,10 @@
&:is(textarea) {
resize: vertical;
min-height: calc(2lh + var(--component-textField-padding) * 2);
- height: calc(3lh + var(--component-textField-padding) * 2);
+
+ &:not([rows]) {
+ height: calc(3lh + var(--component-textField-padding) * 2);
+ }
}
}
@@ -97,12 +100,11 @@
.textField-prefix {
display: flex;
- padding: 0 var(--component-textField-padding);
+ padding-left: var(--component-textField-padding);
align-items: center;
color: var(--component-textField-prefix-color);
line-height: var(--component-textField-lineHeight);
font-size: var(--component-textField-fontSize);
- box-shadow: 0 0 0 1px var(--component-textField-border);
border-top-left-radius: var(--commons-borderRadius-M);
border-bottom-left-radius: var(--commons-borderRadius-M);
@@ -114,12 +116,11 @@
.textField-suffix {
display: flex;
- padding: 0 var(--component-textField-padding);
+ padding-right: var(--component-textField-padding);
align-items: center;
color: var(--component-textField-prefix-color);
line-height: var(--component-textField-lineHeight);
font-size: var(--component-textField-fontSize);
- box-shadow: 0 0 0 1px var(--component-textField-border);
border-top-right-radius: var(--commons-borderRadius-M);
border-bottom-right-radius: var(--commons-borderRadius-M);
order: 1;
diff --git a/packages/scss/src/components/textField/index.scss b/packages/scss/src/components/textField/index.scss
index b24cc9ab2d..3ba2234d8c 100644
--- a/packages/scss/src/components/textField/index.scss
+++ b/packages/scss/src/components/textField/index.scss
@@ -12,11 +12,17 @@
@include XS;
}
- &.is-invalid, &:has(.textField-input-value[aria-invalid='true']) {
+ &.mod-valueAlignRight {
+ @include valueAlignRight;
+ }
+
+ &.is-invalid,
+ &:has(.textField-input-value[aria-invalid='true']) {
@include invalid;
}
- &.is-disabled, &:has(.textField-input-value:disabled) {
+ &.is-disabled,
+ &:has(.textField-input-value:disabled) {
@include disabled;
}
}
diff --git a/packages/scss/src/components/textField/mods.scss b/packages/scss/src/components/textField/mods.scss
index 3649aa1db9..c6fefb597c 100644
--- a/packages/scss/src/components/textField/mods.scss
+++ b/packages/scss/src/components/textField/mods.scss
@@ -4,7 +4,7 @@
@mixin S {
--component-textField-fontSize: var(--sizes-S-fontSize);
--component-textField-lineHeight: var(--sizes-S-lineHeight);
- --component-textField-padding: var(--pr-t-spacings-75);
+ --component-textField-padding: var(--pr-t-spacings-75);
.textField-input-affix-clear {
@include clear.S;
@@ -18,9 +18,10 @@
@mixin XS {
--component-textField-fontSize: var(--sizes-XS-fontSize);
--component-textField-lineHeight: var(--sizes-XS-lineHeight);
- --component-textField-padding: var(--pr-t-spacings-50);
+ --component-textField-padding: var(--pr-t-spacings-50);
- .textField-prefix, .textField-suffix {
+ .textField-prefix,
+ .textField-suffix {
@include icon.XS;
}
@@ -32,3 +33,9 @@
@include clear.S;
}
}
+
+@mixin valueAlignRight {
+ .textField-input-value {
+ text-align: right;
+ }
+}
diff --git a/packages/scss/src/components/textfields/mods.scss b/packages/scss/src/components/textfields/mods.scss
index f6a5b87bcb..74053bf570 100644
--- a/packages/scss/src/components/textfields/mods.scss
+++ b/packages/scss/src/components/textfields/mods.scss
@@ -32,29 +32,6 @@
bottom: var(--pr-t-spacings-150);
right: var(--pr-t-spacings-100);
}
-
- .textfield-actionClear {
- // deprecated
- text-align: center;
- position: absolute;
- bottom: var(--pr-t-spacings-150);
- right: var(--pr-t-spacings-100);
- width: 1rem;
- height: 1rem;
- padding: 0;
- line-height: 0;
- border-radius: var(--commons-borderRadius-full);
- background-color: var(--palettes-neutral-700);
-
- &:hover {
- background-color: var(--palettes-neutral-600);
- }
-
- .lucca-icon {
- font-size: var(--sizes-XS-lineHeight);
- color: white;
- }
- }
}
@mixin clearableS {
@@ -62,12 +39,6 @@
bottom: var(--pr-t-spacings-100);
right: var(--pr-t-spacings-50);
}
-
- .textfield-actionClear {
- // deprecated
- bottom: var(--pr-t-spacings-50);
- right: var(--pr-t-spacings-50);
- }
}
@mixin clearableXS {
@@ -75,29 +46,12 @@
bottom: var(--pr-t-spacings-50);
right: var(--pr-t-spacings-50);
}
-
- .textfield-actionClear {
- // deprecated
- bottom: var(--pr-t-spacings-50);
- right: var(--pr-t-spacings-50);
- height: 1rem;
- width: 1rem;
-
- .lucca-icon {
- font-size: 1rem;
- }
- }
}
@mixin suffix {
.textfield-input {
padding-right: var(--components-textfield-suffix-padding-right);
}
-
- .textfield-actionClear {
- // deprecated
- right: 2rem;
- }
}
@mixin noLabel {
@@ -236,11 +190,6 @@
}
}
}
-
- .textfield-actionClear {
- // deprecated
- right: 2.5rem;
- }
}
@mixin searchClearable {
@@ -251,11 +200,6 @@
.textfield-clear {
right: 2.5rem;
}
-
- .textfield-actionClear {
- // deprecated
- right: 2.5rem;
- }
}
@mixin searchS {
@@ -269,18 +213,6 @@
.textfield-input {
padding-right: 2rem;
}
-
- .textfield-actionClear {
- // deprecated
- right: 2.125rem;
- bottom: 0.625rem;
- width: 0.75rem;
- height: 0.75rem;
-
- .lucca-icon {
- font-size: 0.75rem;
- }
- }
}
@mixin searchClearableS {
@@ -294,12 +226,6 @@
right: 2.125rem;
bottom: 0.625rem;
}
-
- .textfield-actionClear {
- // deprecated
- right: 2.125rem;
- bottom: 0.625rem;
- }
}
@mixin searchXS {
@@ -313,18 +239,6 @@
.textfield-input {
padding-right: 1.5rem;
}
-
- .textfield-actionClear {
- // deprecated
- right: 1.75rem;
- bottom: 0.375rem;
- width: 0.75rem;
- height: 0.75rem;
-
- .lucca-icon {
- font-size: 0.75rem;
- }
- }
}
@mixin searchClearableXS {
@@ -338,18 +252,6 @@
right: 1.75rem;
bottom: var(--pr-t-spacings-75);
}
-
- .textfield-actionClear {
- // deprecated
- right: 1.75rem;
- bottom: 0.375rem;
- width: 0.75rem;
- height: 0.75rem;
-
- .lucca-icon {
- font-size: 0.75rem;
- }
- }
}
@mixin radio {
diff --git a/packages/scss/src/components/textfields/states.scss b/packages/scss/src/components/textfields/states.scss
index 5b682a8db0..ca14582166 100644
--- a/packages/scss/src/components/textfields/states.scss
+++ b/packages/scss/src/components/textfields/states.scss
@@ -24,12 +24,6 @@
--components-clear-cross-color: var(--palettes-neutral-500) !important;
pointer-events: none;
}
-
- ~ .textfield-actionClear {
- // deprecated
- color: var(--palettes-neutral-500) !important;
- pointer-events: none;
- }
}
@mixin filterHover {
diff --git a/packages/scss/src/components/timepicker/component.scss b/packages/scss/src/components/timepicker/component.scss
index 952d1cbb01..1225073a5c 100644
--- a/packages/scss/src/components/timepicker/component.scss
+++ b/packages/scss/src/components/timepicker/component.scss
@@ -1,91 +1,141 @@
@use '@lucca-front/scss/src/commons/utils/a11y';
+@use '@lucca-front/icons/src/icon/exports' as icons;
@mixin component($atRoot: 'without: rule') {
padding: var(--components-timepicker-padding);
- border: 0;
- border-radius: var(--commons-borderRadius-M);
- box-shadow: 0 0 0 1px var(--palettes-neutral-300);
- display: inline-flex;
- background-color: var(--pr-t-elevation-surface-raised);
- transition: box-shadow var(--commons-animations-durations-fast);
-
- &:focus-within {
- @include a11y.focusVisible($offset: 3px);
- background-color: transparent;
- }
+ width: fit-content;
@at-root ($atRoot) {
- .timepicker-field {
+ .timePicker-fieldset {
+ display: flex;
+ align-items: center;
+ box-shadow: 0 0 0 1px var(--components-timepicker-border);
+ border-radius: var(--commons-borderRadius-M);
+ padding: 0;
+ border: 0;
+ margin: 0;
+ background-color: var(--components-timepicker-background);
+ color: var(--components-timepicker-color);
+ font-size: var(--components-timepicker-fontSize);
+ line-height: var(--components-timepicker-lineHeight);
position: relative;
- display: inline-block;
+ cursor: text;
+
+ &:hover {
+ --components-timepicker-border: var(--palettes-neutral-400);
+ }
+
+ &:focus-within {
+ @include a11y.focusVisible($offset: 3px);
+ }
}
- .timepicker-field-input {
+ .timePicker-fieldset-groupSeparator {
+ text-align: center;
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ display: grid;
+ place-items: center;
+ }
+
+ .timePicker-fieldset-group {
+ position: relative;
+ }
+
+ .timePicker-fieldset-group-textfield {
background-color: transparent;
- border-radius: var(--commons-borderRadius-M);
+ }
+
+ .timePicker-fieldset-group-textfield-input {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: 0;
text-align: center;
- color: var(--palettes-neutral-800);
- width: var(--components-timepicker-input-width);
height: var(--components-timepicker-input-height);
- border: 0;
- padding: 0;
- -moz-appearance: textfield;
+ width: var(--components-timepicker-input-width);
+ outline: none;
+ color: inherit;
+ background-color: transparent;
+ text-align: center;
+ padding: var(--components-timepicker-paddingInput);
+ box-sizing: content-box;
- &::-webkit-outer-spin-button,
- &::-webkit-inner-spin-button {
- -webkit-appearance: none;
- margin: 0;
+ //&:has(+ .timePicker-fieldset-group-textfield-display) {
+ opacity: 0.0001;
+ //}
+
+ /*
+ &[type='number'] {
+ -moz-appearance: textfield;
+
+ &::-webkit-outer-spin-button,
+ &::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ }
+ }
+ */
+
+ &::placeholder {
+ color: var(--component-textField-placeholder);
}
- &:hover,
- &:focus,
&:focus-visible {
- background-color: var(--palettes-100, var(--palettes-product-100));
- outline: none;
+ & + .timePicker-fieldset-group-textfield-display {
+ background-color: var(--palettes-primary-100);
+ }
}
}
- .timepicker-field-increment {
- cursor: pointer;
+ .timePicker-fieldset-group-textfield-display {
+ position: absolute;
+ inset: var(--components-timepicker-paddingInput);
+ border-radius: var(--commons-borderRadius-M);
+ pointer-events: none;
+ display: grid;
+ place-items: center;
+ }
+
+ .timePicker-fieldset-group-stepper {
position: absolute;
- bottom: calc(100% + 0.5rem + 1px);
- left: 0;
+ bottom: calc(100% + var(--pr-t-spacings-100) + 1px);
+ left: var(--pr-t-spacings-100);
+ right: var(--pr-t-spacings-100);
border: 0;
- height: 1.25rem;
+ padding: 0;
+ height: var(--pr-t-spacings-200);
background-color: transparent;
color: var(--palettes-neutral-600);
display: inline-flex;
justify-content: center;
align-items: center;
border-radius: var(--commons-borderRadius-M);
- width: var(--components-timepicker-input-width);
- padding: 0;
-
- &:last-child {
- top: calc(100% + 0.5rem + 1px);
- bottom: auto;
- }
+ outline: none;
+ opacity: 1;
+ transition-property: opacity;
+ transition-duration: var(--commons-animations-durations-fast);
+ cursor: pointer;
&:hover {
background-color: var(--palettes-neutral-50);
color: var(--palettes-neutral-800);
}
- &:focus {
- background-color: var(--palettes-neutral-100);
- color: var(--palettes-neutral-800);
+ &:disabled {
+ cursor: default;
+ color: var(--palettes-neutral-500);
+ pointer-events: none;
}
.lucca-icon {
- font-size: 1rem;
+ @include icons.XS;
}
- }
- .timepicker-separator {
- margin: 0 var(--pr-t-spacings-50);
- text-align: center;
- width: 0.5rem;
- line-height: var(--components-timepicker-input-height);
+ + .timePicker-fieldset-group-stepper {
+ top: calc(100% + var(--pr-t-spacings-100) + 1px);
+ bottom: auto;
+ }
}
}
}
diff --git a/packages/scss/src/components/timepicker/index.scss b/packages/scss/src/components/timepicker/index.scss
index 9b1422669d..2b272f202d 100644
--- a/packages/scss/src/components/timepicker/index.scss
+++ b/packages/scss/src/components/timepicker/index.scss
@@ -1,6 +1,7 @@
@use 'exports' as *;
+@use '@lucca-front/scss/src/commons/utils/a11y';
-.timepicker {
+.timePicker {
@include vars;
@include component;
@@ -8,7 +9,19 @@
@include S;
}
- &:is(.is-disabled, .disabled, [disabled]) {
+ &.mod-stepper {
+ @include stepper;
+ }
+
+ &.mod-stepperHover {
+ @include stepperHover;
+ }
+
+ &:has([aria-invalid='true']) {
+ @include invalid;
+ }
+
+ &:has([disabled]) {
@include disabled;
}
}
diff --git a/packages/scss/src/components/timepicker/mods.scss b/packages/scss/src/components/timepicker/mods.scss
index 6c200147fa..09e53f3221 100644
--- a/packages/scss/src/components/timepicker/mods.scss
+++ b/packages/scss/src/components/timepicker/mods.scss
@@ -1,4 +1,21 @@
+@use '@lucca-front/scss/src/commons/utils/a11y';
+
@mixin S {
- --components-timepicker-input-height: 1.5rem;
- --components-timepicker-padding: var(--pr-t-spacings-50);
+ --components-timepicker-fontSize: var(--sizes-S-fontSize);
+ --components-timepicker-lineHeight: var(--sizes-S-lineHeight);
+ --components-timepicker-paddingInput: var(--pr-t-spacings-25) var(--pr-t-spacings-75);
+ --components-timepicker-input-height: 1.75rem;
+ --components-timepicker-input-width: 1.25rem;
+}
+
+@mixin stepper {
+ --components-timepicker-padding: var(--pr-t-spacings-300) 0;
+}
+
+@mixin stepperHover {
+ &:not(:hover, :focus-within) {
+ .timePicker-fieldset-group-stepper {
+ opacity: 0;
+ }
+ }
}
diff --git a/packages/scss/src/components/timepicker/states.scss b/packages/scss/src/components/timepicker/states.scss
index d52ffd02f2..e203ac77b2 100644
--- a/packages/scss/src/components/timepicker/states.scss
+++ b/packages/scss/src/components/timepicker/states.scss
@@ -1,13 +1,16 @@
-@mixin disabled {
- background-color: var(--palettes-neutral-100);
- color: var(--palettes-neutral-600);
+@use '@lucca-front/scss/src/commons/utils/a11y';
- .timepicker-field-input {
- background: transparent;
- color: var(--palettes-neutral-600);
- }
+@mixin invalid {
+ --components-timepicker-background: var(--palettes-error-50);
+ --components-timepicker-border: var(--palettes-error-400);
- .timepicker-field-increment {
- display: none;
+ &:hover {
+ --components-timepicker-border: var(--palettes-error-600);
}
}
+
+@mixin disabled {
+ --components-timepicker-background: var(--palettes-neutral-100);
+ --components-timepicker-border: var(--palettes-neutral-400);
+ --components-timepicker-color: var(--palettes-neutral-600);
+}
diff --git a/packages/scss/src/components/timepicker/vars.scss b/packages/scss/src/components/timepicker/vars.scss
index a1b74197ff..756d13f682 100644
--- a/packages/scss/src/components/timepicker/vars.scss
+++ b/packages/scss/src/components/timepicker/vars.scss
@@ -1,5 +1,11 @@
@mixin vars {
- --components-timepicker-input-width: 1.25rem;
- --components-timepicker-input-height: 2rem;
- --components-timepicker-padding: var(--pr-t-spacings-50) var(--pr-t-spacings-100);
+ --components-timepicker-border: var(--palettes-neutral-300);
+ --components-timepicker-background: var(--colors-white-color);
+ --components-timepicker-color: var(--palettes-neutral-800);
+ --components-timepicker-fontSize: var(--sizes-M-fontSize);
+ --components-timepicker-lineHeight: var(--sizes-M-lineHeight);
+ --components-timepicker-input-height: 2rem;
+ --components-timepicker-input-width: 1.5rem;
+ --components-timepicker-padding: 0;
+ --components-timepicker-paddingInput: var(--pr-t-spacings-50) var(--pr-t-spacings-100);
}
diff --git a/packages/scss/src/components/timepickerDeprecated/component.scss b/packages/scss/src/components/timepickerDeprecated/component.scss
new file mode 100644
index 0000000000..952d1cbb01
--- /dev/null
+++ b/packages/scss/src/components/timepickerDeprecated/component.scss
@@ -0,0 +1,91 @@
+@use '@lucca-front/scss/src/commons/utils/a11y';
+
+@mixin component($atRoot: 'without: rule') {
+ padding: var(--components-timepicker-padding);
+ border: 0;
+ border-radius: var(--commons-borderRadius-M);
+ box-shadow: 0 0 0 1px var(--palettes-neutral-300);
+ display: inline-flex;
+ background-color: var(--pr-t-elevation-surface-raised);
+ transition: box-shadow var(--commons-animations-durations-fast);
+
+ &:focus-within {
+ @include a11y.focusVisible($offset: 3px);
+ background-color: transparent;
+ }
+
+ @at-root ($atRoot) {
+ .timepicker-field {
+ position: relative;
+ display: inline-block;
+ }
+
+ .timepicker-field-input {
+ background-color: transparent;
+ border-radius: var(--commons-borderRadius-M);
+ text-align: center;
+ color: var(--palettes-neutral-800);
+ width: var(--components-timepicker-input-width);
+ height: var(--components-timepicker-input-height);
+ border: 0;
+ padding: 0;
+ -moz-appearance: textfield;
+
+ &::-webkit-outer-spin-button,
+ &::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+ }
+
+ &:hover,
+ &:focus,
+ &:focus-visible {
+ background-color: var(--palettes-100, var(--palettes-product-100));
+ outline: none;
+ }
+ }
+
+ .timepicker-field-increment {
+ cursor: pointer;
+ position: absolute;
+ bottom: calc(100% + 0.5rem + 1px);
+ left: 0;
+ border: 0;
+ height: 1.25rem;
+ background-color: transparent;
+ color: var(--palettes-neutral-600);
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+ border-radius: var(--commons-borderRadius-M);
+ width: var(--components-timepicker-input-width);
+ padding: 0;
+
+ &:last-child {
+ top: calc(100% + 0.5rem + 1px);
+ bottom: auto;
+ }
+
+ &:hover {
+ background-color: var(--palettes-neutral-50);
+ color: var(--palettes-neutral-800);
+ }
+
+ &:focus {
+ background-color: var(--palettes-neutral-100);
+ color: var(--palettes-neutral-800);
+ }
+
+ .lucca-icon {
+ font-size: 1rem;
+ }
+ }
+
+ .timepicker-separator {
+ margin: 0 var(--pr-t-spacings-50);
+ text-align: center;
+ width: 0.5rem;
+ line-height: var(--components-timepicker-input-height);
+ }
+ }
+}
diff --git a/packages/scss/src/components/timepickerDeprecated/exports.scss b/packages/scss/src/components/timepickerDeprecated/exports.scss
new file mode 100644
index 0000000000..150ea9d703
--- /dev/null
+++ b/packages/scss/src/components/timepickerDeprecated/exports.scss
@@ -0,0 +1,5 @@
+@forward 'vars';
+@forward 'mods';
+@forward 'states';
+@forward 'component';
+@forward 'todo-new-timepicker';
diff --git a/packages/scss/src/components/timepickerDeprecated/index.scss b/packages/scss/src/components/timepickerDeprecated/index.scss
new file mode 100644
index 0000000000..9b1422669d
--- /dev/null
+++ b/packages/scss/src/components/timepickerDeprecated/index.scss
@@ -0,0 +1,14 @@
+@use 'exports' as *;
+
+.timepicker {
+ @include vars;
+ @include component;
+
+ &.mod-S {
+ @include S;
+ }
+
+ &:is(.is-disabled, .disabled, [disabled]) {
+ @include disabled;
+ }
+}
diff --git a/packages/scss/src/components/timepickerDeprecated/mods.scss b/packages/scss/src/components/timepickerDeprecated/mods.scss
new file mode 100644
index 0000000000..5eb0116a74
--- /dev/null
+++ b/packages/scss/src/components/timepickerDeprecated/mods.scss
@@ -0,0 +1,4 @@
+@mixin S {
+ --components-timepicker-input-height: 1.5rem;
+ --components-timepicker-padding: var(--pr-t-spacings-50);
+}
diff --git a/packages/scss/src/components/timepickerDeprecated/states.scss b/packages/scss/src/components/timepickerDeprecated/states.scss
new file mode 100644
index 0000000000..d52ffd02f2
--- /dev/null
+++ b/packages/scss/src/components/timepickerDeprecated/states.scss
@@ -0,0 +1,13 @@
+@mixin disabled {
+ background-color: var(--palettes-neutral-100);
+ color: var(--palettes-neutral-600);
+
+ .timepicker-field-input {
+ background: transparent;
+ color: var(--palettes-neutral-600);
+ }
+
+ .timepicker-field-increment {
+ display: none;
+ }
+}
diff --git a/packages/scss/src/components/timepickerDeprecated/vars.scss b/packages/scss/src/components/timepickerDeprecated/vars.scss
new file mode 100644
index 0000000000..a1b74197ff
--- /dev/null
+++ b/packages/scss/src/components/timepickerDeprecated/vars.scss
@@ -0,0 +1,5 @@
+@mixin vars {
+ --components-timepicker-input-width: 1.25rem;
+ --components-timepicker-input-height: 2rem;
+ --components-timepicker-padding: var(--pr-t-spacings-50) var(--pr-t-spacings-100);
+}
diff --git a/packages/scss/src/components/toast/component.scss b/packages/scss/src/components/toast/component.scss
index 56461cd814..54930348a1 100644
--- a/packages/scss/src/components/toast/component.scss
+++ b/packages/scss/src/components/toast/component.scss
@@ -1,76 +1,77 @@
-@use '@lucca-front/scss/src/commons/utils/reset';
-@use '@lucca-front/icons/src/commons/utils/icon';
+@use '@lucca-front/scss/src/components/button/exports' as button;
@mixin component($atRoot: 'without: rule') {
- @keyframes toast {
- 0% {
- transform: translateY(var(--pr-t-spacings-200));
- opacity: 0;
- }
-
- 100% {
- opacity: 1;
- }
- }
-
- right: var(--components-toasts-right);
- top: var(--components-toasts-top);
max-width: var(--components-toasts-maxwidth);
display: flex;
flex-direction: column;
align-items: flex-end;
position: fixed;
z-index: 9999;
+ inset: var(--components-toasts-inset);
@at-root ($atRoot) {
.toasts-item {
display: flex;
- gap: var(--pr-t-spacings-150);
+ gap: var(--pr-t-spacings-50);
color: var(--components-toasts-color);
- padding: var(--components-toasts-padding);
+ padding: var(--pr-t-spacings-50);
margin-bottom: var(--components-toasts-margin-bottom);
- background-color: var(--palettes-800, var(--components-toasts-background));
- animation-name: toast;
+ background-color: var(--palettes-neutral-800);
+ animation-name: toastsItem;
animation-duration: var(--commons-animations-durations-standard);
animation-iteration-count: 1;
- border-radius: var(--commons-borderRadius-M);
+ border-radius: var(--commons-borderRadius-XL);
overflow: hidden;
position: relative;
transform-origin: top;
+
+ @keyframes toastsItem {
+ 0% {
+ transform: translateY(var(--pr-t-spacings-200));
+ opacity: 0;
+ }
+
+ 100% {
+ opacity: 1;
+ }
+ }
+
+ &:hover,
+ &:focus-within {
+ .toasts-item-kill {
+ animation-play-state: paused;
+ }
+ }
}
.toast-item-icon {
- margin-top: 2px;
+ background: var(--palettes-700, var(--palettes-product-700));
+ border-radius: var(--commons-borderRadius-L);
+ padding: var(--pr-t-spacings-100) var(--pr-t-spacings-50);
}
.toast-item-content {
- flex-grow: 1;
display: flex;
+ flex-grow: 1;
flex-direction: column;
- gap: var(--pr-t-spacings-100);
+ gap: var(--pr-t-spacings-50);
+ padding: var(--pr-t-spacings-100) 0 var(--pr-t-spacings-100) var(--pr-t-spacings-100);
}
.toasts-item-kill {
- @include reset.button;
- width: auto;
- color: var(--colors-white-color);
- transition-property: opacity;
- transition-duration: var(--commons-animations-durations-fast);
- height: 1.25rem;
- min-width: 1.25rem;
- cursor: pointer;
- position: relative;
- border: 0;
- background: transparent;
- margin-top: 2px;
+ // the button class should be added to the component, but in the meantime we initialize the component here
+ @include button.vars;
+ @include button.component;
- &:hover {
- opacity: 0.66;
- }
+ @include button.onlyIcon;
+ @include button.text;
+ @include button.inverted;
- &::after {
- @include icon.generate('sign_close');
- }
+ @keyframes timer {}
+
+ align-self: flex-start;
+ border-radius: var(--commons-borderRadius-L);
+ animation-name: timer;
}
}
}
diff --git a/packages/scss/src/components/toast/index.scss b/packages/scss/src/components/toast/index.scss
index 199b2c6a6b..6bde5b2fe4 100644
--- a/packages/scss/src/components/toast/index.scss
+++ b/packages/scss/src/components/toast/index.scss
@@ -7,8 +7,4 @@
&.mod-bottom {
@include bottom;
}
-
- &.mod-withCircularGauge {
- @include circularGauge;
- }
}
diff --git a/packages/scss/src/components/toast/mods.scss b/packages/scss/src/components/toast/mods.scss
index 680617f69e..41ddc35e8d 100644
--- a/packages/scss/src/components/toast/mods.scss
+++ b/packages/scss/src/components/toast/mods.scss
@@ -1,95 +1,3 @@
@mixin bottom {
- top: auto;
- bottom: var(--pr-t-spacings-300);
-}
-
-@mixin circularGauge {
- @keyframes stroke {
- 0% {
- stroke-dashoffset: 100.5;
- }
-
- 100% {
- stroke-dashoffset: 0;
- }
- }
-
- &:hover {
- .circularGauge {
- circle {
- animation-play-state: paused;
- }
- }
- }
-
- &:focus-within {
- .circularGauge {
- circle {
- animation-play-state: paused;
- }
- }
- }
-
- .toasts-item-kill {
- transition: transform 100ms, opacity 100ms;
-
- .lucca-icon {
- position: absolute;
- inset: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 2;
- }
-
- &::after {
- content: none;
- }
-
- &:hover,
- &:focus {
- outline: 0;
- opacity: 0.66;
- }
-
- .circularGauge {
- color: transparent;
- height: 1.25rem;
- pointer-events: none;
- position: absolute;
- top: 0;
- left: 0;
- width: 1.25rem;
-
- &::after {
- position: absolute;
- background-color: transparent;
- left: 2px;
- top: 2px;
- width: calc(100% - 4px);
- height: calc(100% - 4px);
- z-index: 1;
- border-radius: var(--commons-borderRadius-full);
- content: '';
- }
-
- svg {
- transform: rotate(-90deg);
- border-radius: var(--commons-borderRadius-full);
- width: 100%;
- height: auto;
- display: block;
- }
-
- circle {
- stroke-width: 20%;
- stroke-dasharray: 100.5, 100.5;
- stroke: var(--colors-white-color);
- fill: currentColor;
- animation-name: stroke;
- animation-timing-function: linear;
- animation-fill-mode: forwards;
- }
- }
- }
+ --components-toasts-inset: auto var(--components-toasts-right) var(--components-toasts-bottom) auto;
}
diff --git a/packages/scss/src/components/toast/vars.scss b/packages/scss/src/components/toast/vars.scss
index e72ea8b43e..8ae82837a7 100644
--- a/packages/scss/src/components/toast/vars.scss
+++ b/packages/scss/src/components/toast/vars.scss
@@ -1,11 +1,14 @@
@mixin vars {
- --components-toasts-background: var(--palettes-neutral-900);
--components-toasts-color: var(--colors-white-color);
--components-toasts-top: var(--pr-t-spacings-300);
--components-toasts-right: var(--pr-t-spacings-300);
--components-toasts-left: var(--pr-t-spacings-300);
--components-toasts-bottom: var(--pr-t-spacings-300);
- --components-toasts-margin-bottom: var(--pr-t-spacings-100);
+ --components-toasts-margin-bottom: var(--pr-t-spacings-50);
--components-toasts-maxwidth: 22.5rem;
+ --components-toasts-inset: var(--components-toasts-top) var(--components-toasts-right) auto auto;
+
+ // Deprecated
+ --components-toasts-background: var(--palettes-neutral-800);
--components-toasts-padding: var(--pr-t-spacings-100) var(--pr-t-spacings-200);
}
diff --git a/packages/scss/src/components/tooltip/component.scss b/packages/scss/src/components/tooltip/component.scss
new file mode 100644
index 0000000000..319c62c0b0
--- /dev/null
+++ b/packages/scss/src/components/tooltip/component.scss
@@ -0,0 +1,24 @@
+@use '@lucca-front/scss/src/commons/utils/keyframe';
+
+@mixin component($atRoot: 'without: rule') {
+ @include keyframe.scaleIn;
+
+ background-color: var(--components-tooltip-background-color);
+ color: var(--components-tooltip-color);
+ padding: var(--pr-t-spacings-50) var(--pr-t-spacings-100);
+ max-width: var(--components-tooltip-max-width);
+ border-radius: var(--commons-borderRadius-M);
+ font-size: var(--sizes-XS-fontSize);
+ line-height: var(--sizes-XS-lineHeight);
+ transform-origin: var(--components-tooltip-transformOrigin);
+ margin: var(--components-tooltip-margin);
+ text-align: center;
+ width: fit-content;
+ animation-name: scaleIn;
+ animation-duration: var(--commons-animations-durations-fast);
+ animation-iteration-count: 1;
+
+ &:empty {
+ display: none;
+ }
+}
diff --git a/packages/scss/src/components/tooltip/exports.scss b/packages/scss/src/components/tooltip/exports.scss
new file mode 100644
index 0000000000..2c2986a26b
--- /dev/null
+++ b/packages/scss/src/components/tooltip/exports.scss
@@ -0,0 +1,4 @@
+@forward 'vars';
+@forward 'mods';
+@forward 'states';
+@forward 'component';
diff --git a/packages/scss/src/components/tooltip/index.scss b/packages/scss/src/components/tooltip/index.scss
new file mode 100644
index 0000000000..cda170225a
--- /dev/null
+++ b/packages/scss/src/components/tooltip/index.scss
@@ -0,0 +1,38 @@
+@use 'exports' as *;
+
+.tooltip {
+ @include vars;
+ @include component;
+
+ &.is-above {
+ @include above;
+ }
+
+ &.is-below {
+ @include below;
+ }
+
+ &.is-before {
+ @include before;
+
+ &.is-above {
+ @include beforeAbove;
+ }
+
+ &.is-below {
+ @include beforeBelow;
+ }
+ }
+
+ &.is-after {
+ @include after;
+
+ &.is-above {
+ @include afterAbove;
+ }
+
+ &.is-below {
+ @include afterBelow;
+ }
+ }
+}
diff --git a/packages/scss/src/components/tooltip/mods.scss b/packages/scss/src/components/tooltip/mods.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/scss/src/components/tooltip/states.scss b/packages/scss/src/components/tooltip/states.scss
new file mode 100644
index 0000000000..8bfe288dce
--- /dev/null
+++ b/packages/scss/src/components/tooltip/states.scss
@@ -0,0 +1,31 @@
+@mixin above {
+ --components-tooltip-transformOrigin: bottom center;
+}
+
+@mixin below {
+ --components-tooltip-transformOrigin: top center;
+}
+
+@mixin before {
+ --components-tooltip-transformOrigin: center right;
+}
+
+@mixin after {
+ --components-tooltip-transformOrigin: center left;
+}
+
+@mixin beforeAbove {
+ --components-tooltip-transformOrigin: bottom right;
+}
+
+@mixin beforeBelow {
+ --components-tooltip-transformOrigin: top right;
+}
+
+@mixin afterAbove {
+ --components-tooltip-transformOrigin: bottom left;
+}
+
+@mixin afterBelow {
+ --components-tooltip-transformOrigin: top left;
+}
diff --git a/packages/scss/src/components/tooltip/vars.scss b/packages/scss/src/components/tooltip/vars.scss
new file mode 100644
index 0000000000..90d56d764a
--- /dev/null
+++ b/packages/scss/src/components/tooltip/vars.scss
@@ -0,0 +1,7 @@
+@mixin vars {
+ --components-tooltip-background-color: var(--palettes-neutral-900);
+ --components-tooltip-color: var(--colors-white-color);
+ --components-tooltip-max-width: 15rem;
+ --components-tooltip-transformOrigin: center;
+ --components-tooltip-margin: 0;
+}
diff --git a/packages/scss/src/components/userPopover/component.scss b/packages/scss/src/components/userPopover/component.scss
index e142d82e35..96823b9bc2 100644
--- a/packages/scss/src/components/userPopover/component.scss
+++ b/packages/scss/src/components/userPopover/component.scss
@@ -1,48 +1,44 @@
-@use '@lucca-front/scss/src/commons/utils/text';
-
@mixin component($atRoot: 'without: rule') {
- width: 22.5rem;
+ width: 23.5rem;
max-width: calc(100vw - var(--spacings-S) * 2);
padding: var(--spacings-S);
@at-root ($atRoot) {
.userPopover-details {
- --components-user-picture-image-size: 5.25rem;
+ --components-user-picture-image-size: var(--components-userPicture-XXXL-image);
+
display: flex;
- align-items: center;
+ align-items: flex-start;
gap: var(--spacings-S);
}
.userPopover-details-avatar {
- --components-user-picture-font-size: var(--sizes-XXL-fontSize);
+ --components-user-picture-font-size: var(--components-userPicture-XXXL-fontSize);
margin: var(--spacings-XXS) 0;
}
.userPopover-details-info {
- width: calc(100% - var(--components-user-picture-image-size) - var(--spacings-S));
+ min-width: 0;
}
.userPopover-details-info-name {
- @include text.ellipsis;
-
- margin: 0;
- padding: 0;
- }
-
- .userPopover-details-info-name-link {
- color: var(--palettes-grey-900);
+ margin: calc(var(--pr-t-spacings-50) * -1);
+ padding: var(--pr-t-spacings-50);
font-size: var(--sizes-L-fontSize);
line-height: var(--sizes-L-lineHeight);
font-weight: 700;
+ color: currentColor;
+ }
+
+ .userPopover-details-info-name-linkOptional {
+ color: currentColor;
text-decoration: underline;
text-decoration-thickness: 0.75px;
text-underline-offset: 3px;
- &:hover,
- &:active,
- &:focus {
- color: var(--palettes-grey-900);
+ &:hover {
+ color: currentColor;
text-decoration-thickness: 1.5px;
}
}
@@ -55,19 +51,15 @@
.userPopover-details-info-detail-workplace {
display: flex;
- align-items: center;
+ align-items: flex-start;
gap: var(--spacings-XXS);
- margin-top: var(--spacings-XS);
- color: var(--palettes-grey-800);
+ color: currentColor;
text-decoration: none;
- &.mod-link {
- text-decoration: none;
-
+ &:is(a, button) {
&:hover,
- &:active,
&:focus {
- color: var(--palettes-grey-800);
+ color: currentColor;
.userPopover-details-info-detail-workplace-state {
text-decoration: underline;
@@ -75,10 +67,5 @@
}
}
}
-
- .userPopover-details-info-detail,
- .userPopover-details-info-detail-link-state {
- @include text.ellipsis;
- }
}
}
diff --git a/stories/documentation/actions/button/angular/button-basic.stories.ts b/stories/documentation/actions/button/angular/button-basic.stories.ts
index 5f42dc42f1..adf5eac30b 100644
--- a/stories/documentation/actions/button/angular/button-basic.stories.ts
+++ b/stories/documentation/actions/button/angular/button-basic.stories.ts
@@ -7,7 +7,7 @@ export default {
component: ButtonComponent,
render: ({ luButton, ...inputs }, { argTypes }) => {
return {
- template: `Button`,
};
},
diff --git a/stories/documentation/feedback/callout/html&css/callout-tiny.stories.ts b/stories/documentation/feedback/callout/html&css/callout-tiny.stories.ts
deleted file mode 100644
index 33e7aa9292..0000000000
--- a/stories/documentation/feedback/callout/html&css/callout-tiny.stories.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import { Meta, StoryFn } from '@storybook/angular';
-
-interface CalloutTinyStory {
- s: boolean;
- palette: string;
- icon: string;
-}
-
-export default {
- title: 'Documentation/Feedback/Callout/HTML & CSS/Tiny',
- argTypes: {
- s: {
- control: {
- type: 'boolean',
- },
- description: 'Taille : Small',
- },
- palette: {
- options: ['', 'palette-success', 'palette-warning', 'palette-error'],
- control: {
- type: 'select',
- },
- },
- icon: {
- options: ['icon-signHelp', 'icon-signSuccess', 'icon-signWarning', 'icon-signError'],
- control: {
- type: 'select',
- },
- },
- },
-} as Meta;
-
-function getTemplate(args: CalloutTinyStory): string {
- const s = args.s ? ` mod-S` : '';
- let palette = args.palette;
- const icon = args.icon ? ' ' + args.icon : '';
- palette = ' ' + palette;
- return ``;
-}
-
-const Template: StoryFn = (args) => ({
- props: args,
- template: getTemplate(args),
-});
-
-export const Tiny = Template.bind({});
-Tiny.args = { s: false, icon: 'icon-signHelp', palette: '' };
diff --git a/stories/documentation/feedback/empty-state/angular/empty-state-page.stories.ts b/stories/documentation/feedback/empty-state/angular/empty-state-page.stories.ts
index ae7b633174..e8acaf9f2f 100644
--- a/stories/documentation/feedback/empty-state/angular/empty-state-page.stories.ts
+++ b/stories/documentation/feedback/empty-state/angular/empty-state-page.stories.ts
@@ -12,7 +12,7 @@ export default {
}),
],
render: (args: EmptyStatePageComponent) => {
- const { title, description, icon, topRightBackground, topRightForeground, bottomLeftBackground, bottomLeftForeground, contentBackgroundColor } = args;
+ const { title, description, icon, topRightBackground, topRightForeground, bottomLeftBackground, bottomLeftForeground, contentBackgroundColor, hx } = args;
const paramIcon = args.icon === '' ? '' : 'icon="' + args.icon + '"';
return {
@@ -34,6 +34,7 @@ export default {
bottomLeftBackground="${bottomLeftBackground}"
bottomLeftForeground="${bottomLeftForeground}"
contentBackgroundColor="${contentBackgroundColor}"
+ hx="${hx}"
>
Button
Button
@@ -133,6 +134,13 @@ export default {
type: 'text',
},
},
+ hx: {
+ control: {
+ type: 'number',
+ min: 1,
+ max: 6,
+ },
+ },
},
} as Meta;
@@ -146,5 +154,6 @@ export const Page: StoryObj = {
bottomLeftBackground: 'https://cdn.lucca.fr/lucca-front/assets/empty-states/poplee/bubbles-bottom-left-01.svg',
bottomLeftForeground: 'https://cdn.lucca.fr/lucca-front/assets/empty-states/poplee/core-hr-01.svg',
contentBackgroundColor: 'var(--pr-t-elevation-surface-default)',
+ hx: 1,
},
};
diff --git a/stories/documentation/feedback/empty-state/angular/empty-state-section.stories.ts b/stories/documentation/feedback/empty-state/angular/empty-state-section.stories.ts
index 1bf4265197..0bcc4f83f9 100644
--- a/stories/documentation/feedback/empty-state/angular/empty-state-section.stories.ts
+++ b/stories/documentation/feedback/empty-state/angular/empty-state-section.stories.ts
@@ -13,10 +13,10 @@ export default {
}),
],
render: (args: EmptyStateSectionComponent) => {
- const { title, description, center, palette, icon } = args;
+ const { title, description, center, palette, hx, icon } = args;
const paramIcon = args.icon === '' ? '' : 'icon="' + args.icon + '"';
return {
-template: `
+ template: `
Button
Button
`,
@@ -24,6 +24,13 @@ template: `
Empty State
- Flatus obsequiorum potest inanes pomerium obsequiorum credi homines vero caelibes orbos potest vile diversitate flatus.
+ Flatus obsequiorum potest inanes pomerium obsequiorum credi homines vero caelibes orbos potest vile diversitate flatus.
Button
Button
diff --git a/stories/documentation/feedback/empty-state/html&css/empty-state-section-center.stories.ts b/stories/documentation/feedback/empty-state/html&css/empty-state-section-center.stories.ts
index 2e6145a9eb..6f3f641f17 100644
--- a/stories/documentation/feedback/empty-state/html&css/empty-state-section-center.stories.ts
+++ b/stories/documentation/feedback/empty-state/html&css/empty-state-section-center.stories.ts
@@ -27,7 +27,7 @@ function getTemplate(args: EmptyStateSectionCenterStory): string {
>
Empty State
-
Flatus obsequiorum potest inanes pomerium obsequiorum credi homines vero caelibes orbos potest vile diversitate flatus.
+
Flatus obsequiorum potest inanes pomerium obsequiorum credi homines vero caelibes orbos potest vile diversitate flatus.
Button
Button
diff --git a/stories/documentation/feedback/empty-state/html&css/empty-state-section-palette.stories.ts b/stories/documentation/feedback/empty-state/html&css/empty-state-section-palette.stories.ts
index e13878ccc4..0972c617cb 100644
--- a/stories/documentation/feedback/empty-state/html&css/empty-state-section-palette.stories.ts
+++ b/stories/documentation/feedback/empty-state/html&css/empty-state-section-palette.stories.ts
@@ -27,7 +27,7 @@ function getTemplate(args: EmptyStateSectionPaletteStory): string {
>
Empty State
-
Flatus obsequiorum potest inanes pomerium obsequiorum credi homines vero caelibes orbos potest vile diversitate flatus.
+
Flatus obsequiorum potest inanes pomerium obsequiorum credi homines vero caelibes orbos potest vile diversitate flatus.
Button
Button
diff --git a/stories/documentation/feedback/empty-state/html&css/empty-state-section.stories.ts b/stories/documentation/feedback/empty-state/html&css/empty-state-section.stories.ts
index ea1a352a29..bbc3c38e58 100644
--- a/stories/documentation/feedback/empty-state/html&css/empty-state-section.stories.ts
+++ b/stories/documentation/feedback/empty-state/html&css/empty-state-section.stories.ts
@@ -1,9 +1,8 @@
-import { Meta, moduleMetadata, StoryFn } from '@storybook/angular';
+import { HttpClientModule } from '@angular/common/http';
import { LuSafeExternalSvgPipe } from '@lucca-front/ng/safe-content';
-import { HttpClientModule } from "@angular/common/http";
+import { Meta, moduleMetadata, StoryFn } from '@storybook/angular';
-interface EmptyStateSectionBasicStory {
-}
+interface EmptyStateSectionBasicStory {}
export default {
title: 'Documentation/Feedback/Empty State/HTML&CSS/Section',
@@ -12,8 +11,7 @@ export default {
imports: [LuSafeExternalSvgPipe, HttpClientModule],
}),
],
- argTypes: {
- },
+ argTypes: {},
} as Meta;
function getTemplate(args: EmptyStateSectionBasicStory): string {
@@ -27,7 +25,7 @@ function getTemplate(args: EmptyStateSectionBasicStory): string {
>
Empty State
-
Flatus obsequiorum potest inanes pomerium obsequiorum credi homines vero caelibes orbos potest vile diversitate flatus.
+
Flatus obsequiorum potest inanes pomerium obsequiorum credi homines vero caelibes orbos potest vile diversitate flatus.
Button
Button
@@ -44,4 +42,4 @@ const Template: StoryFn
= (args: EmptyStateSectionB
});
export const Section = Template.bind({});
-Section.args = { };
+Section.args = {};
diff --git a/stories/documentation/feedback/error-page/error-page-basic.stories.ts b/stories/documentation/feedback/error-page/error-page-basic.stories.ts
index 9269bd3fbd..a3199c756d 100644
--- a/stories/documentation/feedback/error-page/error-page-basic.stories.ts
+++ b/stories/documentation/feedback/error-page/error-page-basic.stories.ts
@@ -11,8 +11,8 @@ function getTemplate(args: ErrorBasicStory): string {
-
Titre de l'erreur
-
Vous n'êtes pas autorisé à consulter cette page ou cette ressource
+
Titre de l’erreur
+
Vous n’êtes pas autorisé à consulter cette page ou cette ressource
Revenir à la page précédente
diff --git a/stories/documentation/forms/checkbox/checkbox-basic.stories.ts b/stories/documentation/forms/checkbox/checkbox-basic.stories.ts
index dd0a11e617..8c8c2b9196 100644
--- a/stories/documentation/forms/checkbox/checkbox-basic.stories.ts
+++ b/stories/documentation/forms/checkbox/checkbox-basic.stories.ts
@@ -92,7 +92,7 @@ function getTemplate(args: CheckboxBasicStory): string {
return ``;
}
-const Template: StoryFn = (args) => ({
+const Template: StoryFn = (args) => ({
props: args,
template: getTemplate(args),
});
-export const Invlid = Template.bind({});
-Invlid.args = {};
+export const Invalid = Template.bind({});
+Invalid.args = {};
diff --git a/stories/documentation/forms/fields/text/angular/textfield.stories.ts b/stories/documentation/forms/fields/text/angular/textfield.stories.ts
index a095277e9e..13b32feb69 100644
--- a/stories/documentation/forms/fields/text/angular/textfield.stories.ts
+++ b/stories/documentation/forms/fields/text/angular/textfield.stories.ts
@@ -2,7 +2,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FormFieldComponent } from '@lucca-front/ng/form-field';
import { TextInputComponent } from '@lucca-front/ng/forms';
-import { Meta, moduleMetadata, StoryObj } from '@storybook/angular';
+import { Meta, StoryObj, moduleMetadata } from '@storybook/angular';
import { cleanupTemplate, generateInputs } from 'stories/helpers/stories';
export default {
@@ -84,8 +84,9 @@ export const Basic: StoryObj
diff --git a/stories/documentation/forms/fields/text/html&css/textfield-valueAlignRight.stories.ts b/stories/documentation/forms/fields/text/html&css/textfield-valueAlignRight.stories.ts
new file mode 100644
index 0000000000..e482510183
--- /dev/null
+++ b/stories/documentation/forms/fields/text/html&css/textfield-valueAlignRight.stories.ts
@@ -0,0 +1,28 @@
+import { Meta, StoryFn } from '@storybook/angular';
+
+interface TextfieldValueAlignRightStory {}
+
+export default {
+ title: 'Documentation/Forms/Fields/TextField/HTML&CSS',
+ argTypes: {},
+} as Meta;
+
+function getTemplate(args: TextfieldValueAlignRightStory): string {
+ return `
`;
+}
+
+const Template: StoryFn
= (args) => ({
+ props: args,
+ template: getTemplate(args),
+});
+
+export const ValueAlignRight = Template.bind({});
+ValueAlignRight.args = {};
diff --git a/stories/documentation/forms/fields/textarea/angular/textarea.stories.ts b/stories/documentation/forms/fields/textarea/angular/textarea.stories.ts
index 9392952c25..feaacdec1e 100644
--- a/stories/documentation/forms/fields/textarea/angular/textarea.stories.ts
+++ b/stories/documentation/forms/fields/textarea/angular/textarea.stories.ts
@@ -1,9 +1,9 @@
-import { TextareaInputComponent } from '@lucca-front/ng/forms';
-import { Meta, moduleMetadata, StoryObj } from '@storybook/angular';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
-import { cleanupTemplate, generateInputs } from 'stories/helpers/stories';
import { FormFieldComponent } from '@lucca-front/ng/form-field';
+import { TextareaInputComponent } from '@lucca-front/ng/forms';
+import { Meta, StoryObj, moduleMetadata } from '@storybook/angular';
+import { cleanupTemplate, generateInputs } from 'stories/helpers/stories';
export default {
title: 'Documentation/Forms/Fields/TextAreaField/Angular',
@@ -28,6 +28,9 @@ export default {
type: 'select',
},
},
+ rows: {
+ control: { type: 'number', min: 2 },
+ },
counter: {
description: '[v17.4]',
},
@@ -73,7 +76,8 @@ export const Basic: StoryObjLabel
Helper text
diff --git a/stories/documentation/forms/form-label/form-label-basic.stories.ts b/stories/documentation/forms/form-label/form-label-basic.stories.ts
index 13dabd9bfb..2eca699e26 100644
--- a/stories/documentation/forms/form-label/form-label-basic.stories.ts
+++ b/stories/documentation/forms/form-label/form-label-basic.stories.ts
@@ -1,16 +1,14 @@
import { Meta, StoryFn } from '@storybook/angular';
-interface FormLabelBasicStory {
-}
+interface FormLabelBasicStory {}
export default {
title: 'Documentation/Forms/Form Label Basic',
- argTypes: {
- },
+ argTypes: {},
} as Meta;
function getTemplate(args: FormLabelBasicStory): string {
- return ``;
+ return ``;
}
const Template: StoryFn = (args) => ({
diff --git a/stories/documentation/forms/form-label/form-label-counter.stories.ts b/stories/documentation/forms/form-label/form-label-counter.stories.ts
index 62645e73c1..39e9f641d7 100644
--- a/stories/documentation/forms/form-label/form-label-counter.stories.ts
+++ b/stories/documentation/forms/form-label/form-label-counter.stories.ts
@@ -1,17 +1,15 @@
import { Meta, StoryFn } from '@storybook/angular';
-interface FormLabelCounterStory {
-}
+interface FormLabelCounterStory {}
export default {
title: 'Documentation/Forms/Form Label Counter',
- argTypes: {
- },
+ argTypes: {},
} as Meta;
function getTemplate(args: FormLabelCounterStory): string {
return `
Empty State
-
Flatus obsequiorum potest inanes pomerium obsequiorum credi homines vero caelibes orbos potest vile diversitate flatus.
+
Flatus obsequiorum potest inanes pomerium obsequiorum credi homines vero caelibes orbos potest vile diversitate flatus.
diff --git a/stories/documentation/listings/table-legacy/table-sticky-columns-breakpoints.stories.ts b/stories/documentation/listings/table-legacy/table-sticky-columns-breakpoints.stories.ts
new file mode 100644
index 0000000000..e698727910
--- /dev/null
+++ b/stories/documentation/listings/table-legacy/table-sticky-columns-breakpoints.stories.ts
@@ -0,0 +1,155 @@
+import { Meta, StoryFn } from '@storybook/angular';
+
+interface TableStickyColumnsAndHeaderWithBreakpointsStory {}
+
+export default {
+ title: 'Documentation/Listings/Table/Legacy/Sticky Columns And Header With Breakpoints',
+ argTypes: {},
+} as Meta;
+
+function getTemplate(args: TableStickyColumnsAndHeaderWithBreakpointsStory): string {
+ return `
+
+ `;
+}
+
+const Template: StoryFn = (args) => ({
+ props: args,
+ template: getTemplate(args),
+ styles: [`.demo-wrapper {overflow: auto; height: 10rem;}`],
+});
+
+export const StickyColumnsAndHeaderWithBreakpoints = Template.bind({});
+StickyColumnsAndHeaderWithBreakpoints.args = {};
diff --git a/stories/documentation/listings/table/table-sticky-columns.stories.ts b/stories/documentation/listings/table-legacy/table-sticky-columns.stories.ts
similarity index 98%
rename from stories/documentation/listings/table/table-sticky-columns.stories.ts
rename to stories/documentation/listings/table-legacy/table-sticky-columns.stories.ts
index 26b5928589..a532dcfc30 100644
--- a/stories/documentation/listings/table/table-sticky-columns.stories.ts
+++ b/stories/documentation/listings/table-legacy/table-sticky-columns.stories.ts
@@ -3,7 +3,7 @@ import { Meta, StoryFn } from '@storybook/angular';
interface TableStickyColumnsStory {}
export default {
- title: 'Documentation/Listings/Table/Sticky Columns',
+ title: 'Documentation/Listings/Table/Legacy/Sticky Columns',
argTypes: {},
} as Meta;
diff --git a/stories/documentation/listings/table-legacy/table-sticky-header.stories.ts b/stories/documentation/listings/table-legacy/table-sticky-header.stories.ts
new file mode 100644
index 0000000000..086f29b793
--- /dev/null
+++ b/stories/documentation/listings/table-legacy/table-sticky-header.stories.ts
@@ -0,0 +1,65 @@
+import { Meta, StoryFn } from '@storybook/angular';
+
+interface TableStickyHeaderStory {}
+
+export default {
+ title: 'Documentation/Listings/Table/Legacy/Sticky Header',
+ argTypes: {},
+} as Meta;
+
+function getTemplate(args: TableStickyHeaderStory): string {
+ return `
+
+
+
+ `;
+}
+
+const Template: StoryFn = (args) => ({
+ props: args,
+ template: getTemplate(args),
+ styles: [`.demo-wrapper {height: 10rem; overflow: auto;}`],
+});
+
+export const StickyHeader = Template.bind({});
+StickyHeader.args = {};
diff --git a/stories/documentation/listings/table/table-sticky-columns-breakpoints.stories.ts b/stories/documentation/listings/table/table-sticky-columns-breakpoints.stories.ts
index 5260ca83f1..35be47b609 100644
--- a/stories/documentation/listings/table/table-sticky-columns-breakpoints.stories.ts
+++ b/stories/documentation/listings/table/table-sticky-columns-breakpoints.stories.ts
@@ -3,29 +3,34 @@ import { Meta, StoryFn } from '@storybook/angular';
interface TableStickyColumnsAndHeaderWithBreakpointsStory {}
export default {
- title: 'Documentation/Listings/Table/Sticky Columns And Header With Breakpoints',
+ title: 'Documentation/Listings/Table/Sticky Columns And Header With Breakpoints ',
argTypes: {},
} as Meta;
function getTemplate(args: TableStickyColumnsAndHeaderWithBreakpointsStory): string {
- return `
-