diff --git a/alfa-client/libs/design-system/src/index.ts b/alfa-client/libs/design-system/src/index.ts index 0c83b3d1ba9df2ab88f88227a9197f681970ca18..16c66be88076bf07e9c5bf7ec2b4052fa5a20d89 100644 --- a/alfa-client/libs/design-system/src/index.ts +++ b/alfa-client/libs/design-system/src/index.ts @@ -26,4 +26,6 @@ export * from './lib/icons/spinner-icon/spinner-icon.component'; export * from './lib/icons/stamp-icon/stamp-icon.component'; export * from './lib/instant-search/instant-search/instant-search.component'; export * from './lib/instant-search/instant-search/instant-search.model'; +export * from './lib/popup/popup-list-item/popup-list-item.component'; +export * from './lib/popup/popup/popup.component'; export * from './lib/testbtn/testbtn.component'; diff --git a/alfa-client/libs/design-system/src/lib/icons/iconVariants.ts b/alfa-client/libs/design-system/src/lib/icons/iconVariants.ts index 6ea2dff309ce50e62416e5dd898bcc4771563625..e13cd3b4f155db048c8aab75887fa4f8f13172da 100644 --- a/alfa-client/libs/design-system/src/lib/icons/iconVariants.ts +++ b/alfa-client/libs/design-system/src/lib/icons/iconVariants.ts @@ -8,6 +8,7 @@ export const iconVariants = cva('', { medium: 'size-6', large: 'size-8', 'extra-large': 'size-10', + xxl: 'size-12', }, }, }); diff --git a/alfa-client/libs/design-system/src/lib/icons/office-icon/office-icon.stories.ts b/alfa-client/libs/design-system/src/lib/icons/office-icon/office-icon.stories.ts index 8a54d90c40add3636c0f41c9748ab529f09fc253..46b9e413bf6ac9da265c12d440201cf12017da9e 100644 --- a/alfa-client/libs/design-system/src/lib/icons/office-icon/office-icon.stories.ts +++ b/alfa-client/libs/design-system/src/lib/icons/office-icon/office-icon.stories.ts @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/angular'; import { OfficeIconComponent } from './office-icon.component'; const meta: Meta<OfficeIconComponent> = { - title: 'Icons/Save icon', + title: 'Icons/Office icon', component: OfficeIconComponent, excludeStories: /.*Data$/, tags: ['autodocs'], diff --git a/alfa-client/libs/design-system/src/lib/icons/user-icon/user-icon.component.spec.ts b/alfa-client/libs/design-system/src/lib/icons/user-icon/user-icon.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..6e80ce433f6db593962497c6e2c92386745a5045 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/icons/user-icon/user-icon.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { UserIconComponent } from './user-icon.component'; + +describe('UserIconComponent', () => { + let component: UserIconComponent; + let fixture: ComponentFixture<UserIconComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [UserIconComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(UserIconComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alfa-client/libs/design-system/src/lib/icons/user-icon/user-icon.component.ts b/alfa-client/libs/design-system/src/lib/icons/user-icon/user-icon.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..90c01e353f6029d459584e6fcdafa425c6e6d9be --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/icons/user-icon/user-icon.component.ts @@ -0,0 +1,31 @@ +import { CommonModule } from '@angular/common'; +import { Component, Input } from '@angular/core'; +import { twMerge } from 'tailwind-merge'; +import { ExclamationIconComponent } from '../exclamation-icon/exclamation-icon.component'; +import { IconVariants, iconVariants } from '../iconVariants'; + +@Component({ + selector: 'ods-user-icon', + standalone: true, + imports: [CommonModule, ExclamationIconComponent], + template: ` + <svg + viewBox="0 0 47 47" + fill="none" + xmlns="http://www.w3.org/2000/svg" + [ngClass]="[twMerge(iconVariants({ size }), 'fill-ozggray-300', class)]" + > + <path + d="M23.5 3.91663C12.69 3.91663 3.91669 12.69 3.91669 23.5C3.91669 34.31 12.69 43.0833 23.5 43.0833C34.31 43.0833 43.0834 34.31 43.0834 23.5C43.0834 12.69 34.31 3.91663 23.5 3.91663ZM23.5 9.79163C26.7509 9.79163 29.375 12.4158 29.375 15.6666C29.375 18.9175 26.7509 21.5416 23.5 21.5416C20.2492 21.5416 17.625 18.9175 17.625 15.6666C17.625 12.4158 20.2492 9.79163 23.5 9.79163ZM23.5 37.6C18.6042 37.6 14.2763 35.0933 11.75 31.2941C11.8088 27.397 19.5834 25.2625 23.5 25.2625C27.3971 25.2625 35.1913 27.397 35.25 31.2941C32.7238 35.0933 28.3959 37.6 23.5 37.6Z" + /> + </svg> + `, +}) +export class UserIconComponent { + @Input() variant: 'user' | 'initials' = 'user'; + @Input() size: IconVariants['size'] = 'xxl'; + @Input() class: string = undefined; + + iconVariants = iconVariants; + twMerge = twMerge; +} diff --git a/alfa-client/libs/design-system/src/lib/icons/user-icon/user-icon.stories.ts b/alfa-client/libs/design-system/src/lib/icons/user-icon/user-icon.stories.ts new file mode 100644 index 0000000000000000000000000000000000000000..cf801c263a4b51d9dc560f937de4047b07da27a2 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/icons/user-icon/user-icon.stories.ts @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/angular'; + +import { UserIconComponent } from './user-icon.component'; + +const meta: Meta<UserIconComponent> = { + title: 'Icons/User icon', + component: UserIconComponent, + excludeStories: /.*Data$/, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj<UserIconComponent>; + +export const Default: Story = { + args: { size: 'xxl' }, + argTypes: { + size: { + control: 'select', + options: ['small', 'medium', 'large', 'extra-large', 'xxl', 'full'], + description: 'Size of icon. Property "full" means 100%', + table: { + defaultValue: { summary: 'xxl' }, + }, + }, + }, +}; diff --git a/alfa-client/libs/design-system/src/lib/instant-search/instant-search/instant-search.stories.ts b/alfa-client/libs/design-system/src/lib/instant-search/instant-search/instant-search.stories.ts index bfe67d23232b417e6cddd3b33f53245a793b161e..522bc7f81a9e3f677d55c9ac8066632c02f15247 100644 --- a/alfa-client/libs/design-system/src/lib/instant-search/instant-search/instant-search.stories.ts +++ b/alfa-client/libs/design-system/src/lib/instant-search/instant-search/instant-search.stories.ts @@ -26,14 +26,12 @@ export const SearchResults: Story = { headerText: 'In der OZG-Cloud', searchResults: [ { - text: 'Landeshauptstadt Kiel - Ordnungsamt, Gewerbe- und Schornsteinfegeraufsicht', - subText: 'Fabrikstraße 8-10, 24103 Kiel', - onClick: () => undefined, + title: 'Landeshauptstadt Kiel - Ordnungsamt, Gewerbe- und Schornsteinfegeraufsicht', + description: 'Fabrikstraße 8-10, 24103 Kiel', }, { - text: 'Amt für Digitalisierung, Breitband und Vermessung Nürnberg Außenstelle Hersbruck', - subText: 'Rathausmarkt 7, Hersbruck', - onClick: () => undefined, + title: 'Amt für Digitalisierung, Breitband und Vermessung Nürnberg Außenstelle Hersbruck', + description: 'Rathausmarkt 7, Hersbruck', }, ], }, diff --git a/alfa-client/libs/design-system/src/lib/popup/popup-list-item/popup-list-item.component.spec.ts b/alfa-client/libs/design-system/src/lib/popup/popup-list-item/popup-list-item.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..3d624f9674b26a1f03a2e024f0139f9eb8230746 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/popup/popup-list-item/popup-list-item.component.spec.ts @@ -0,0 +1,32 @@ +import { dispatchEventFromFixture } from '@alfa-client/test-utils'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { PopupListItemComponent } from './popup-list-item.component'; + +describe('PopupListItemComponent', () => { + let component: PopupListItemComponent; + let fixture: ComponentFixture<PopupListItemComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PopupListItemComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(PopupListItemComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('itemClicked emitter', () => { + it('should emit itemClicked', () => { + component.itemClicked.emit = jest.fn(); + + dispatchEventFromFixture(fixture, 'button', 'click'); + + expect(component.itemClicked.emit).toHaveBeenCalled(); + }); + }); +}); diff --git a/alfa-client/libs/design-system/src/lib/popup/popup-list-item/popup-list-item.component.ts b/alfa-client/libs/design-system/src/lib/popup/popup-list-item/popup-list-item.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..3bc88c4c30db6091ccf72a4e3658972fe4f117b6 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/popup/popup-list-item/popup-list-item.component.ts @@ -0,0 +1,21 @@ +import { CommonModule } from '@angular/common'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; + +@Component({ + selector: 'ods-popup-list-item', + standalone: true, + imports: [CommonModule], + template: `<button + class="flex min-h-12 w-full items-center gap-4 border-2 border-transparent bg-whitetext px-4 py-3 text-start outline-none hover:border-primary focus-visible:border-focus" + role="listitem" + (click)="itemClicked.emit()" + > + <ng-content select="[icon]" /> + <p class="text-text">{{ caption }}</p> + </button>`, +}) +export class PopupListItemComponent { + @Input({ required: true }) caption!: string; + + @Output() itemClicked: EventEmitter<MouseEvent> = new EventEmitter(); +} diff --git a/alfa-client/libs/design-system/src/lib/popup/popup-list-item/popup-list-item.stories.ts b/alfa-client/libs/design-system/src/lib/popup/popup-list-item/popup-list-item.stories.ts new file mode 100644 index 0000000000000000000000000000000000000000..a28201314e94cc47b348fd9082f158ec06326174 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/popup/popup-list-item/popup-list-item.stories.ts @@ -0,0 +1,24 @@ +import { moduleMetadata, type Meta, type StoryObj } from '@storybook/angular'; + +import { PopupListItemComponent } from './popup-list-item.component'; + +const meta: Meta<PopupListItemComponent> = { + title: 'Popup/Popup list item', + component: PopupListItemComponent, + decorators: [ + moduleMetadata({ + imports: [PopupListItemComponent], + }), + ], + excludeStories: /.*Data$/, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj<PopupListItemComponent>; + +export const Default: Story = { + args: { + caption: 'List item', + }, +}; diff --git a/alfa-client/libs/design-system/src/lib/popup/popup/popup.component.spec.ts b/alfa-client/libs/design-system/src/lib/popup/popup/popup.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..8d03abd239d9a8054e0efed06259e41bffb8d516 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/popup/popup/popup.component.spec.ts @@ -0,0 +1,212 @@ +import { getElementFromFixture } from '@alfa-client/test-utils'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; +import { PopupComponent } from './popup.component'; + +describe('PopupComponent', () => { + let component: PopupComponent; + let fixture: ComponentFixture<PopupComponent>; + const popupButton: string = getDataTestIdOf('popup-button'); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PopupComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(PopupComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('togglePopup', () => { + it('should change false to true', () => { + component.isPopupOpen = false; + + component.togglePopup(); + + expect(component.isPopupOpen).toBe(true); + }); + + it('should change true to false', () => { + component.isPopupOpen = true; + + component.togglePopup(); + + expect(component.isPopupOpen).toBe(false); + }); + }); + + describe('aria-expanded', () => { + it('should be true if popup is open', () => { + component.isPopupOpen = true; + fixture.detectChanges(); + + const buttonElement: HTMLElement = getElementFromFixture(fixture, popupButton); + + expect(buttonElement.getAttribute('aria-expanded')).toBe('true'); + }); + + it('should be false if popup is closed', () => { + component.isPopupOpen = false; + fixture.detectChanges(); + + const buttonElement: HTMLElement = getElementFromFixture(fixture, popupButton); + + expect(buttonElement.getAttribute('aria-expanded')).toBe('false'); + }); + }); + + describe('isEscapeKey', () => { + it('should return true', () => { + const escapeKeyEvent: KeyboardEvent = { + ...new KeyboardEvent('esc'), + key: 'Escape', + }; + + const result: boolean = component.isEscapeKey(escapeKeyEvent); + + expect(result).toBe(true); + }); + + it('should return false', () => { + const keyEvent: KeyboardEvent = new KeyboardEvent('whatever'); + + const result: boolean = component.isEscapeKey(keyEvent); + + expect(result).toBe(false); + }); + }); + + describe('closePopupAndFocusButton', () => { + beforeEach(() => { + component.isPopupOpen = true; + jest.spyOn(component.buttonRef.nativeElement, 'focus'); + }); + it('should close popup', () => { + component.closePopupAndFocusButton(); + + expect(component.isPopupOpen).toBe(false); + }); + + it('should focus button', () => { + component.closePopupAndFocusButton(); + + expect(component.buttonRef.nativeElement.focus).toHaveBeenCalled(); + }); + }); + + describe('isPopupClosed', () => { + it('should return true', () => { + component.isPopupOpen = false; + + const result: boolean = component.isPopupClosed(); + + expect(result).toBe(true); + }); + + it('should return false', () => { + component.isPopupOpen = true; + + const result: boolean = component.isPopupClosed(); + + expect(result).toBe(false); + }); + }); + + describe('onKeydownHandler', () => { + const e: KeyboardEvent = new KeyboardEvent('test'); + + beforeEach(() => { + component.closePopupAndFocusButton = jest.fn(); + component.isEscapeKey = jest.fn(); + }); + + describe('popup is closed', () => { + beforeEach(() => { + component.isPopupClosed = jest.fn().mockReturnValue(true); + }); + + it('should not check for escape key', () => { + component.onKeydownHandler(e); + + expect(component.isEscapeKey).not.toHaveBeenCalled(); + }); + }); + + describe('popup is open', () => { + beforeEach(() => { + component.isPopupClosed = jest.fn().mockReturnValue(false); + }); + + it('should check for escape key', () => { + component.onKeydownHandler(e); + + expect(component.isEscapeKey).toHaveBeenCalled(); + }); + + it('should handle escape key', () => { + component.isEscapeKey = jest.fn().mockReturnValue(true); + + component.onKeydownHandler(e); + + expect(component.closePopupAndFocusButton).toHaveBeenCalled(); + }); + + it('should not handle escape key', () => { + component.onKeydownHandler(e); + + expect(component.closePopupAndFocusButton).not.toHaveBeenCalled(); + }); + }); + }); + describe('onClickHandler', () => { + const e: MouseEvent = new MouseEvent('test'); + + beforeEach(() => { + component.closePopupAndFocusButton = jest.fn(); + component.buttonRef.nativeElement.contains = jest.fn(); + }); + + describe('popup is closed', () => { + beforeEach(() => { + component.isPopupClosed = jest.fn().mockReturnValue(true); + }); + + it('should not check for button containing event target', () => { + component.onClickHandler(e); + + expect(component.buttonRef.nativeElement.contains).not.toHaveBeenCalled(); + }); + }); + + describe('popup is open', () => { + beforeEach(() => { + component.isPopupClosed = jest.fn().mockReturnValue(false); + }); + + it('should check for button containing event target', () => { + component.onClickHandler(e); + + expect(component.buttonRef.nativeElement.contains).toHaveBeenCalled(); + }); + + it('should handle click', () => { + component.onClickHandler(e); + + expect(component.closePopupAndFocusButton).toHaveBeenCalled(); + }); + + it('should not handle click', () => { + component.buttonRef.nativeElement.contains = jest.fn().mockReturnValue(true); + + component.onClickHandler(e); + + expect(component.closePopupAndFocusButton).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/alfa-client/libs/design-system/src/lib/popup/popup/popup.component.ts b/alfa-client/libs/design-system/src/lib/popup/popup/popup.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..a4901d657b0d4d7246dcff422553a17fa60a31df --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/popup/popup/popup.component.ts @@ -0,0 +1,76 @@ +import { CdkTrapFocus } from '@angular/cdk/a11y'; +import { CommonModule } from '@angular/common'; +import { Component, ElementRef, HostListener, Input, ViewChild } from '@angular/core'; +import { twMerge } from 'tailwind-merge'; + +@Component({ + selector: 'ods-popup', + standalone: true, + imports: [CommonModule, CdkTrapFocus], + template: `<div class="relative w-fit"> + <button + class="w-fit outline-2 outline-offset-2 outline-focus" + [ngClass]="[twMerge('w-fit outline-2 outline-offset-2 outline-focus', buttonClass)]" + (click)="togglePopup()" + [attr.aria-expanded]="isPopupOpen" + aria-haspopup="true" + [attr.aria-label]="label" + data-test-id="popup-button" + #button + > + <ng-content select="[button]" /> + </button> + <ul + *ngIf="isPopupOpen" + class="max-h-120 animate-fadeIn absolute min-w-44 max-w-80 overflow-y-auto rounded shadow-lg shadow-grayborder focus:outline-none" + [ngClass]="alignTo === 'left' ? 'right-0' : 'left-0'" + role="dialog" + aria-modal="true" + cdkTrapFocus + [cdkTrapFocusAutoCapture]="true" + > + <ng-content /> + </ul> + </div>`, +}) +export class PopupComponent { + @Input() alignTo: 'left' | 'right' = 'left'; + @Input() label: string = ''; + @Input() buttonClass: string = ''; + + isPopupOpen: boolean = false; + twMerge = twMerge; + + @ViewChild('button') buttonRef: ElementRef<HTMLButtonElement>; + + @HostListener('document:keydown', ['$event']) + onKeydownHandler(e: KeyboardEvent): void { + if (this.isPopupClosed()) return; + if (this.isEscapeKey(e)) this.closePopupAndFocusButton(); + } + + @HostListener('document:click', ['$event']) + onClickHandler(e: MouseEvent): void { + if (this.isPopupClosed()) return; + if (!this.buttonRef.nativeElement.contains(e.target as HTMLElement)) { + this.closePopupAndFocusButton(); + } + } + + togglePopup(): void { + this.isPopupOpen = !this.isPopupOpen; + } + + closePopupAndFocusButton(): void { + this.isPopupOpen = false; + this.buttonRef.nativeElement.focus(); + } + + isEscapeKey(e: KeyboardEvent): boolean { + return e.key === 'Escape'; + } + + isPopupClosed(): boolean { + return !this.isPopupOpen; + } +} diff --git a/alfa-client/libs/design-system/src/lib/popup/popup/popup.stories.ts b/alfa-client/libs/design-system/src/lib/popup/popup/popup.stories.ts new file mode 100644 index 0000000000000000000000000000000000000000..ecde2c16145d2e95ddcd3327d51e003d27f721f6 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/popup/popup/popup.stories.ts @@ -0,0 +1,78 @@ +import { + argsToTemplate, + componentWrapperDecorator, + moduleMetadata, + type Meta, + type StoryObj, +} from '@storybook/angular'; + +import { SaveIconComponent } from '../../icons/save-icon/save-icon.component'; +import { UserIconComponent } from '../../icons/user-icon/user-icon.component'; +import { PopupListItemComponent } from '../popup-list-item/popup-list-item.component'; +import { PopupComponent } from './popup.component'; + +const meta: Meta<PopupComponent> = { + title: 'Popup/Popup', + component: PopupComponent, + decorators: [ + moduleMetadata({ + imports: [PopupComponent, PopupListItemComponent, SaveIconComponent, UserIconComponent], + }), + componentWrapperDecorator((story) => `<div class="flex justify-center mb-32">${story}</div>`), + ], + excludeStories: /.*Data$/, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj<PopupComponent>; + +export const Default: Story = { + args: { alignTo: 'left', label: '', buttonClass: '' }, + argTypes: { + alignTo: { + control: 'select', + options: ['left', 'right'], + table: { + defaultValue: { summary: 'left' }, + }, + }, + buttonClass: { description: 'Tailwind class for button' }, + label: { description: 'Aria-label for button' }, + }, + render: (args) => ({ + props: args, + template: `<ods-popup ${argsToTemplate(args)}> + <ods-user-icon button /> + <ods-popup-list-item caption="Lorem" /> + <ods-popup-list-item caption="Ipsum" /> + <ods-popup-list-item caption="Dolor" /> + </ods-popup>`, + }), +}; + +export const LongText: Story = { + render: (args) => ({ + props: args, + template: `<ods-popup ${argsToTemplate(args)}> + <p button>Trigger popup</p> + <ods-popup-list-item caption="Lorem" /> + <ods-popup-list-item caption="Lorem ipsum dolor sit amet" /> + </ods-popup>`, + }), +}; + +export const ItemsWithIcons: Story = { + render: (args) => ({ + props: args, + template: `<ods-popup ${argsToTemplate(args)}> + <p button>Trigger popup</p> + <ods-popup-list-item caption="Lorem"> + <ods-save-icon icon size="small" /> + </ods-popup-list-item> + <ods-popup-list-item caption="Lorem ipsum dolor sit amet"> + <ods-save-icon icon size="small" /> + </ods-popup-list-item> + </ods-popup>`, + }), +}; diff --git a/alfa-client/libs/design-system/src/lib/tailwind-preset/tailwind.config.js b/alfa-client/libs/design-system/src/lib/tailwind-preset/tailwind.config.js index 2fe29cc4a320a6a10b8dec75884f5f423b549e1d..b17ef3c855216e3e735314a6e0b17984d4b3068f 100644 --- a/alfa-client/libs/design-system/src/lib/tailwind-preset/tailwind.config.js +++ b/alfa-client/libs/design-system/src/lib/tailwind-preset/tailwind.config.js @@ -13,7 +13,11 @@ module.exports = { darkMode: 'class', theme: { extend: { - animation: { dash: 'dash 1.5s ease-in-out infinite', 'spin-slow': 'spin 2s linear infinite' }, + animation: { + dash: 'dash 1.5s ease-in-out infinite', + 'spin-slow': 'spin 2s linear infinite', + fadeIn: 'fade-in 0.2s ease-in-out 1', + }, keyframes: { dash: { from: { @@ -29,10 +33,21 @@ module.exports = { 'stroke-dashoffset': '-49', }, }, + 'fade-in': { + '0%': { + opacity: 0, + }, + '100%': { + opacity: 1, + }, + }, }, borderWidth: { 3: '3px', }, + maxHeight: { + 120: '480px', + }, colors: { ozgblue: { 50: 'hsl(200, 100%, 96%)',