Title
-Label
-Slot start, complex label
+Label
+Label
-Slot end, complex label
+Label
+Label
-Slot start, complex label
+Label
+Label
-Slot end, complex label
+Label
+Label
-Slot start, complex label
+Label
+Label
-Slot end, complex label
+Label
++ {{ myAccountNumber /* e.g. { regNo: '987', accountNo: '1234567890' } */ | accountNumber }} +
`, + }, + }, + }, +}; diff --git a/libs/extensions/angular/localization/src/account-number/index.ts b/libs/extensions/angular/localization/src/account-number/index.ts new file mode 100644 index 0000000000..805d3c72d8 --- /dev/null +++ b/libs/extensions/angular/localization/src/account-number/index.ts @@ -0,0 +1,3 @@ +export * from './account-number.model'; +export * from './account-number.pipe'; +export * from './account-number-service-formatter'; diff --git a/libs/extensions/angular/localization/src/amount/amount-service-formatter.ts b/libs/extensions/angular/localization/src/amount/amount-service-formatter.ts new file mode 100644 index 0000000000..8e8403fad5 --- /dev/null +++ b/libs/extensions/angular/localization/src/amount/amount-service-formatter.ts @@ -0,0 +1,88 @@ +import { formatNumber } from '@angular/common'; + +import { Amount } from './amount.model'; + +export function formatAmount( + amount: Amount, + amountServiceConfiguration: AmountServiceConfiguration, + locale: string, + nativeCurrency: string +) { + const config = deriveConfiguration(amountServiceConfiguration); + + let formattedAmount = formatNumber(amount && amount.amount, locale, config.digitsInfo); + + if (config.stripSign) { + formattedAmount = formattedAmount.replace('-', '').trim(); + } + + const currencyCodeToAppend = deriveCurrencyCode(config, amount, nativeCurrency); + + if (!currencyCodeToAppend) { + return formattedAmount; + } + if (config.currencyCodePosition === 'postfix') { + return formattedAmount + ' ' + currencyCodeToAppend; + } else { + return currencyCodeToAppend + ' ' + formattedAmount; + } +} + +export function deriveCurrencyCode( + config: AmountServiceConfiguration, + amount: Amount, + nativeCurrency: string +) { + let currencyCodeToAppend; + + if (config.showCurrencyCode) { + if (config.showCurrencyCode === 'alwaysShowCurrency') { + currencyCodeToAppend = amount && amount.currencyCode; + } else if (config.showCurrencyCode === 'showForeignCurrency') { + currencyCodeToAppend = + amount && amount.currencyCode !== nativeCurrency ? amount.currencyCode : ''; + } + } + + return currencyCodeToAppend || ''; +} + +export function deriveConfiguration(configuration: AmountServiceConfiguration) { + const config: AmountServiceConfiguration = { + showCurrencyCode: '', + digitsInfo: '1.2-2', + stripSign: false, + }; + + return Object.assign({}, config, configuration); +} + +export type ShowCurrencyCode = '' | 'alwaysShowCurrency' | 'showForeignCurrency'; +export type CurrencyCodePosition = '' | 'prefix' | 'postfix'; + +export interface AmountServiceConfiguration { + /** + * - '' - don't output CurrencyCode + * - 'alwaysShowCurrency' - always shows CurrencyCode, regardless of presentation currency + * - 'showForeignCurrency' - only show CurrencyCode if it differs from the presentation currency + */ + showCurrencyCode: ShowCurrencyCode; + /** + * The position of the currency code in the formatted amount + * - 'postfix' - output CurrencyCode after the formatted amount, eg. 1.234,56 EUR + * - 'prefix' - output CurrencyCode before the formatted amount, eg. DKK 1.234,56 + */ + currencyCodePosition?: CurrencyCodePosition; + /** + * A string that represents the format of the number. Learn more about the format here: https://angular.io/api/common/DecimalPipe#parameters + */ + digitsInfo?: string; + /** + * Remove the minus sign from the formatted amount and trim any leading or trailing whitespace. + */ + stripSign?: boolean; + /** + * The string to return if the amount is empty + */ + returnValueOnEmptyAmount?: string; +} diff --git a/libs/extensions/angular/localization/src/amount/amount.model.ts b/libs/extensions/angular/localization/src/amount/amount.model.ts new file mode 100644 index 0000000000..7f184d578a --- /dev/null +++ b/libs/extensions/angular/localization/src/amount/amount.model.ts @@ -0,0 +1,15 @@ +export interface Amount { + /** + * The monetary value + * @min -999999999 + * @max 999999999 + * @example [100] + */ + amount: number; + + /** + * The currency in which the value is denominated + * @example [DKK] + */ + currencyCode: string; +} diff --git a/libs/extensions/angular/localization/src/amount/amount.pipe.spec.ts b/libs/extensions/angular/localization/src/amount/amount.pipe.spec.ts new file mode 100644 index 0000000000..70cb53cc5c --- /dev/null +++ b/libs/extensions/angular/localization/src/amount/amount.pipe.spec.ts @@ -0,0 +1,35 @@ +import { + createServiceFactory, + mockProvider, + SpectatorService, + SpyObject, +} from '@ngneat/spectator/jest'; + +import { AmountPipe } from './amount.pipe'; +import { AmountService } from './amount.service'; +import { AmountServiceConfiguration } from './amount-service-formatter'; + +describe('AmountPipe', () => { + let spectator: SpectatorService+ {{ myAmount /* e.g. { amount: 123456, currencyCode: 'DKK' } */ | amount: amountServiceConfiguration }} +
`, + }, + }, + }, + argTypes: { + myAmount: { + control: { + type: 'object', + }, + }, + }, +}; + +/** + * Here the amount is formatted with the US currency code USD, and the currency code is always shown. + * The `LOCALE_ID` of the example is `en`, so the amount is formatted with a period as the decimal separator and a comma as the thousand separator. + */ +export const AmountUSD: Story = { + args: { + myAmount: { amount: 123456, currencyCode: 'USD' }, + amountServiceConfiguration: { + showCurrencyCode: 'alwaysShowCurrency', + digitsInfo: '1.2-2', + stripSign: false, + currencyCodePosition: 'prefix', + }, + }, + parameters: { + docs: { + source: { + language: 'tsx', // Using tsx here to get better syntax highlighting + code: `+ {{ myAmount /* e.g. { amount: 123456, currencyCode: 'USD' } */ | amount: amountServiceConfiguration }} +
`, + }, + }, + }, + decorators: [ + moduleMetadata({ + providers: [ + { provide: LOCALE_ID, useValue: 'en' }, + { + provide: KIRBY_EXTENSIONS_LOCALIZATION_TOKEN, + useValue: { + nativeCurrency: 'USD', + }, + }, + ], + }), + ], +}; diff --git a/libs/extensions/angular/localization/src/amount/index.ts b/libs/extensions/angular/localization/src/amount/index.ts new file mode 100644 index 0000000000..c980dc0959 --- /dev/null +++ b/libs/extensions/angular/localization/src/amount/index.ts @@ -0,0 +1,4 @@ +export * from './amount.model'; +export * from './amount.pipe'; +export * from './amount.service'; +export * from './amount-service-formatter'; diff --git a/libs/extensions/angular/localization/src/date-time/abstract-timezone-compensating.pipe.ts b/libs/extensions/angular/localization/src/date-time/abstract-timezone-compensating.pipe.ts new file mode 100644 index 0000000000..9303ff0a1b --- /dev/null +++ b/libs/extensions/angular/localization/src/date-time/abstract-timezone-compensating.pipe.ts @@ -0,0 +1,85 @@ +import { inject, LOCALE_ID, PipeTransform } from '@angular/core'; +import { KIRBY_EXTENSIONS_LOCALIZATION_TOKEN } from '../di-tokens'; +import { DateFormats } from './date-formats'; + +/** + * Abstract implementation of pipe that should format dates, and compensate for time-zone offset. + * + * This class provides tools for formatting dates (and timestamps) in a time zone + */ +export abstract class AbstractTimezoneCompensatingPipe implements PipeTransform { + private config = inject(KIRBY_EXTENSIONS_LOCALIZATION_TOKEN); + private locale = inject(LOCALE_ID); + + abstract transform(value: unknown, ...args: unknown[]): unknown; + + protected format(time: number | Date, formatPattern: string): string { + if (!time) { + return ''; + } + + const date = typeof time === 'number' ? new Date(time) : time; + + const timeZone = this.config.timeZone; + const options = this.getIntlOptions(formatPattern); + + const formatter = new Intl.DateTimeFormat(this.locale, { ...options, timeZone }); + let formattedDate = formatter.format(date); + + // Capitalize month abbreviation and remove trailing period for `MEDIUM_LETTER_DATE_FORMAT` + if (formatPattern === DateFormats.MEDIUM_LETTER_DATE_FORMAT) { + formattedDate = formattedDate.replace( + /(\d{2}\.\s)(\w+)\.(\s\d{4})/, + (_, day, month, year) => { + return `${day}${month.charAt(0).toUpperCase()}${month.slice(1)}${year}`; + } + ); + } + + if ( + formatPattern === DateFormats.SHORT_TIME_FORMAT || + formatPattern === DateFormats.MEDIUM_TIME_FORMAT + ) { + formattedDate = formattedDate.replace(/\./g, ':'); + } + + return formattedDate; + } + + private getIntlOptions(formatPattern: string): Intl.DateTimeFormatOptions { + switch (formatPattern) { + case DateFormats.SHORT_DATE_FORMAT: + return { year: 'numeric', month: '2-digit', day: '2-digit' }; + + case DateFormats.MEDIUM_DATE_FORMAT: + return { year: 'numeric', month: 'long', day: 'numeric' }; + + case DateFormats.MEDIUM_LETTER_DATE_FORMAT: + return { + year: 'numeric', + month: 'short', + day: '2-digit', + }; + + case DateFormats.SHORT_TIME_FORMAT: + return { hour: '2-digit', minute: '2-digit', hour12: false }; + + case DateFormats.MEDIUM_TIME_FORMAT: + return { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }; + + case DateFormats.SHORT_DATE_MEDIUM_TIME_FORMAT: + return { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + }; + + default: + throw new Error(`Unsupported format pattern: ${formatPattern}`); + } + } +} diff --git a/libs/extensions/angular/localization/src/date-time/date-formats.ts b/libs/extensions/angular/localization/src/date-time/date-formats.ts new file mode 100644 index 0000000000..59acfac92b --- /dev/null +++ b/libs/extensions/angular/localization/src/date-time/date-formats.ts @@ -0,0 +1,8 @@ +export class DateFormats { + static readonly SHORT_DATE_FORMAT = 'dd.MM.yyyy'; + static readonly MEDIUM_DATE_FORMAT = 'd. MMMM y'; + static readonly MEDIUM_LETTER_DATE_FORMAT = 'dd. MMM yyyy'; + static readonly SHORT_TIME_FORMAT = 'HH:mm'; + static readonly MEDIUM_TIME_FORMAT = 'HH:mm:ss'; + static readonly SHORT_DATE_MEDIUM_TIME_FORMAT = `${DateFormats.SHORT_DATE_FORMAT} ${DateFormats.MEDIUM_TIME_FORMAT}`; +} diff --git a/libs/extensions/angular/localization/src/date-time/date-only/date-only.pipe.spec.ts b/libs/extensions/angular/localization/src/date-time/date-only/date-only.pipe.spec.ts new file mode 100644 index 0000000000..6173ab05b3 --- /dev/null +++ b/libs/extensions/angular/localization/src/date-time/date-only/date-only.pipe.spec.ts @@ -0,0 +1,43 @@ +import { createPipeFactory, SpectatorPipe } from '@ngneat/spectator/jest'; + +import { LOCALE_ID } from '@angular/core'; +import { KIRBY_EXTENSIONS_LOCALIZATION_TOKEN } from '../../di-tokens'; +import { DateOnlyPipe } from './date-only.pipe'; + +describe('DateOnlyPipe', () => { + let spectator: SpectatorPipeOriginal Date: {{ myDate | json }}
+Formatted Date: {{ myDate | dateOnly }}
+Original Date: {{ myDate | json }}
+Formatted Date: {{ myDate | dateOnly }}
+`, + }, + }, + }, +}; diff --git a/libs/extensions/angular/localization/src/date-time/index.ts b/libs/extensions/angular/localization/src/date-time/index.ts new file mode 100644 index 0000000000..050e7a2930 --- /dev/null +++ b/libs/extensions/angular/localization/src/date-time/index.ts @@ -0,0 +1,4 @@ +export * from './date-formats'; +export * from './date-only/date-only.pipe'; +export * from './time-only/time-only.pipe'; +export * from './time-or-date/time-or-date.pipe'; diff --git a/libs/extensions/angular/localization/src/date-time/time-only/time-only.pipe.spec.ts b/libs/extensions/angular/localization/src/date-time/time-only/time-only.pipe.spec.ts new file mode 100644 index 0000000000..2c4b03fedf --- /dev/null +++ b/libs/extensions/angular/localization/src/date-time/time-only/time-only.pipe.spec.ts @@ -0,0 +1,70 @@ +import { createPipeFactory, SpectatorPipe } from '@ngneat/spectator/jest'; + +import { LOCALE_ID } from '@angular/core'; +import { KIRBY_EXTENSIONS_LOCALIZATION_TOKEN } from '../../di-tokens'; +import { TimeOnlyPipe } from './time-only.pipe'; + +describe('TimeOnlyPipe', () => { + let spectator: SpectatorPipeOriginal Date: {{ myDate | json }}
+Formatted Time: {{ myDate | timeOnly: timeFormat }}
+Original Date: {{ myTime | json }}
+Formatted Time: {{ myTime | timeOnly: timeFormat /* timeFormat ('short' or 'medium') is optional */ }}
`, + }, + }, + }, +}; diff --git a/libs/extensions/angular/localization/src/date-time/time-or-date/time-or-date.pipe.spec.ts b/libs/extensions/angular/localization/src/date-time/time-or-date/time-or-date.pipe.spec.ts new file mode 100644 index 0000000000..530ff7a45d --- /dev/null +++ b/libs/extensions/angular/localization/src/date-time/time-or-date/time-or-date.pipe.spec.ts @@ -0,0 +1,101 @@ +import { createPipeFactory } from '@ngneat/spectator/jest'; +import { format, toZonedTime } from 'date-fns-tz'; + +import { LOCALE_ID } from '@angular/core'; +import { KIRBY_EXTENSIONS_LOCALIZATION_TOKEN } from '../../di-tokens'; +import { TimeOrDatePipe } from './time-or-date.pipe'; + +const timeZone = 'Europe/Copenhagen'; + +describe('TimeOrDatePipe', () => { + const createPipe = createPipeFactory({ + pipe: TimeOrDatePipe, + providers: [ + { provide: LOCALE_ID, useValue: 'da' }, + { + provide: KIRBY_EXTENSIONS_LOCALIZATION_TOKEN, + useValue: { + timeZone, + }, + }, + ], + }); + + it(`should create`, () => { + const date = new Date(); + date.setHours(9, 32, 55); + const spectator = createPipe('{{date | timeOrDate: true}}', { + hostProps: { + date, + }, + }); + + expect(spectator.element).toBeTruthy(); + }); + + it(`should format a time today as hour and minutes (HH:mm)`, () => { + const date = new Date(); + date.setHours(9, 32); + const spectator = createPipe('{{date | timeOrDate}}', { + hostProps: { + date, + }, + }); + + const timeZoneTime = toZonedTime(date.toISOString(), timeZone); + + const expected = format(timeZoneTime, 'HH:mm', { + timeZone, + }); + + expect(spectator.element).toHaveExactText(expected); + }); + + it(`should format a time today as hour, minutes and seconds (HH:mm:ss)`, () => { + const date = new Date(); + date.setHours(9, 32, 55); + const spectator = createPipe('{{date | timeOrDate: true}}', { + hostProps: { + date, + }, + }); + + const timeZoneTime = toZonedTime(date.toISOString(), timeZone); + + const expected = format(timeZoneTime, 'HH:mm:ss', { + timeZone, + }); + expect(spectator.element).toHaveExactText(expected); + }); + + it('should format a date different from "today" as day, month (in digits) and year (dd.MM.yyyy)', () => { + const date = new Date('1981-12-24'); + const spectator = createPipe('{{date | timeOrDate}}', { + hostProps: { + date, + }, + }); + expect(spectator.element).toHaveExactText('24.12.1981'); + }); + + it('should format a date different from "today" as day, month (in letters) and year (dd. MMM yyyy)', () => { + const date = new Date('1981-01-24'); + const spectator = createPipe('{{date | timeOrDate: false : "month-as-letters"}}', { + hostProps: { + date, + }, + }); + expect(spectator.element).toHaveExactText('24. Jan 1981'); + }); + + [null, undefined].forEach((absent) => { + it(`should produce an empty string ("") when an absent (${absent}) input value is passed to pipe`, () => { + const spectator = createPipe('{{date | timeOrDate}}', { + hostProps: { + date: absent, + }, + }); + expect(spectator.element).toHaveExactText(''); + }); + }); +}); diff --git a/libs/extensions/angular/localization/src/date-time/time-or-date/time-or-date.pipe.ts b/libs/extensions/angular/localization/src/date-time/time-or-date/time-or-date.pipe.ts new file mode 100644 index 0000000000..018d482fd9 --- /dev/null +++ b/libs/extensions/angular/localization/src/date-time/time-or-date/time-or-date.pipe.ts @@ -0,0 +1,49 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +import { AbstractTimezoneCompensatingPipe } from '../abstract-timezone-compensating.pipe'; +import { DateFormats } from '../date-formats'; + +/** + * Formats a given timestamp so that: + * - If timestamp is of "today", it's formatted as time with hours and minutes (eg. 23:56) + * - If timestamp is different from "today", it's formatted as date with "day of month", month and year (eg. 28.02.2020) + * + * All formatting and parsing is expect to be handled in "Europe/Copenhagen" time zone and with + * the locale provided by `LOCALE_ID`. + */ + +@Pipe({ + name: 'timeOrDate', + standalone: true, +}) +export class TimeOrDatePipe extends AbstractTimezoneCompensatingPipe implements PipeTransform { + transform( + time: number | Date, + showSeconds = false, + formatMonth: 'month-as-digits' | 'month-as-letters' = 'month-as-digits' + ): string { + if (!time) { + return ''; + } + + const date = typeof time === 'number' ? new Date(time) : time; + + const today = new Date(); + const sameDay = + date.getFullYear() === today.getFullYear() && + date.getMonth() === today.getMonth() && + date.getDate() === today.getDate(); + + let format = DateFormats.SHORT_DATE_FORMAT; + + if (formatMonth === 'month-as-letters') { + format = DateFormats.MEDIUM_LETTER_DATE_FORMAT; + } + + if (sameDay) { + format = showSeconds ? DateFormats.MEDIUM_TIME_FORMAT : DateFormats.SHORT_TIME_FORMAT; + } + + return this.format(date, format); + } +} diff --git a/libs/extensions/angular/localization/src/date-time/time-or-date/time-or-date.stories.ts b/libs/extensions/angular/localization/src/date-time/time-or-date/time-or-date.stories.ts new file mode 100644 index 0000000000..41aed21128 --- /dev/null +++ b/libs/extensions/angular/localization/src/date-time/time-or-date/time-or-date.stories.ts @@ -0,0 +1,100 @@ +import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; +import { TimeOrDatePipe } from '@kirbydesign/extensions-angular/localization'; + +import { Component, Input } from '@angular/core'; +import { JsonPipe } from '@angular/common'; + +@Component({ + template: ` +Original Timestamp: {{ myTimestamp | json }}
+Today, Formatted: {{ myTimestamp | timeOrDate: showSeconds : formatMonth }}
++ Tomorrow, formatted: + {{ tomorrowTimestamp | timeOrDate: showSeconds : formatMonth }} +
+Original Timestamp: {{ myTimestamp | json }}
+Today, formatted: {{ myTimestamp | timeOrDate: showSeconds : formatMonth }}
+Tomorrow, formatted: {{ tomorrowTimestamp | timeOrDate: showSeconds : formatMonth }}
`, + }, + }, + }, +}; diff --git a/libs/extensions/angular/localization/src/di-tokens.ts b/libs/extensions/angular/localization/src/di-tokens.ts new file mode 100644 index 0000000000..7c7f2ef09f --- /dev/null +++ b/libs/extensions/angular/localization/src/di-tokens.ts @@ -0,0 +1,35 @@ +import { InjectionToken } from '@angular/core'; + +export const KIRBY_EXTENSIONS_LOCALIZATION_TOKEN = + new InjectionToken+ {{ myNumber | formatNumber: { digitsInfo: '1.2-2' } /* config-object with digits info is optional */ }} +
`, + }, + }, + }, +}; diff --git a/libs/extensions/angular/localization/src/number/index.ts b/libs/extensions/angular/localization/src/number/index.ts new file mode 100644 index 0000000000..6d446e0882 --- /dev/null +++ b/libs/extensions/angular/localization/src/number/index.ts @@ -0,0 +1,2 @@ +export * from './format-number.pipe'; +export * from './format-number.service'; diff --git a/libs/extensions/angular/localization/src/phone-number/index.ts b/libs/extensions/angular/localization/src/phone-number/index.ts new file mode 100644 index 0000000000..3d47493c14 --- /dev/null +++ b/libs/extensions/angular/localization/src/phone-number/index.ts @@ -0,0 +1,3 @@ +export * from './phone-number'; +export * from './phone-number.pipe'; +export * from './phone-number.service'; diff --git a/libs/extensions/angular/localization/src/phone-number/phone-number.pipe.ts b/libs/extensions/angular/localization/src/phone-number/phone-number.pipe.ts new file mode 100644 index 0000000000..b3399c1056 --- /dev/null +++ b/libs/extensions/angular/localization/src/phone-number/phone-number.pipe.ts @@ -0,0 +1,27 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +import { PhoneNumber } from './phone-number'; +import { PhoneNumberService } from './phone-number.service'; + +@Pipe({ + name: 'phoneNumber', + standalone: true, +}) +export class PhoneNumberPipe implements PipeTransform { + constructor(private phoneNumberService: PhoneNumberService) {} + + /** + * Transforms a phone number, chunked up with spaces between the chunks and the country code in front, if desired. + * + * @param phoneNumber A PhoneNumber or a string representation of a phone number + * @param chunk The chunk size used to split up the phone number with spaces + * @param showCountryCode Show the country code in front of the phone number. If a string representation is supplied, the KIRBY_EXTENSIONS_LOCALIZATION_TOKEN.countryCode is used. + */ + transform( + phoneNumber: PhoneNumber | string, + chunk = 2, + showCountryCode?: boolean + ): string | undefined { + return this.phoneNumberService.formatPhoneNumber(phoneNumber, chunk, showCountryCode); + } +} diff --git a/libs/extensions/angular/localization/src/phone-number/phone-number.service.spec.ts b/libs/extensions/angular/localization/src/phone-number/phone-number.service.spec.ts new file mode 100644 index 0000000000..b9b26fa5d5 --- /dev/null +++ b/libs/extensions/angular/localization/src/phone-number/phone-number.service.spec.ts @@ -0,0 +1,97 @@ +import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; + +import { LOCALE_ID } from '@angular/core'; +import { KIRBY_EXTENSIONS_LOCALIZATION_TOKEN } from '../di-tokens'; +import { PhoneNumber } from './phone-number'; +import { PhoneNumberService } from './phone-number.service'; + +describe('PhoneNumberService', () => { + let spectator: SpectatorServiceOriginal Phone Number: {{ myPhoneNumber | json }}
+Formatted Phone Number: {{ myPhoneNumber | phoneNumber: chunk : showCountryCode }}
+Original Phone Number: {{ phoneNumber | json }}
+Formatted Phone Number: {{ phoneNumber | phoneNumber: chunk : showCountryCode }}
`, + }, + }, + }, +}; diff --git a/libs/extensions/angular/localization/src/phone-number/phone-number.ts b/libs/extensions/angular/localization/src/phone-number/phone-number.ts new file mode 100644 index 0000000000..232e2bae37 --- /dev/null +++ b/libs/extensions/angular/localization/src/phone-number/phone-number.ts @@ -0,0 +1,4 @@ +export interface PhoneNumber { + countryCode: string; + number: string; +} diff --git a/libs/extensions/angular/package.json b/libs/extensions/angular/package.json index fbc8cb296a..a2fa33fd7b 100644 --- a/libs/extensions/angular/package.json +++ b/libs/extensions/angular/package.json @@ -1,6 +1,6 @@ { "name": "@kirbydesign/extensions-angular", - "version": "1.2.0", + "version": "1.3.0", "peerDependencies": { "@angular/common": "^18.0.0 || ^19.0.0", "@angular/compiler": "^18.0.0 || ^19.0.0", @@ -35,6 +35,8 @@ "@repo/eslint-config": "*", "@storybook/angular": "^8.4.4", "@types/jest": "^29.4.0", + "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", "jest": "^29.4.1", "jest-environment-jsdom": "^29.4.1", "jest-preset-angular": "^14.5.0", diff --git a/libs/extensions/angular/skeleton-loader/ng-package.json b/libs/extensions/angular/skeleton-loader/ng-package.json new file mode 100644 index 0000000000..c781f0df46 --- /dev/null +++ b/libs/extensions/angular/skeleton-loader/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "src/index.ts" + } +} diff --git a/libs/extensions/angular/skeleton-loader/src/index.ts b/libs/extensions/angular/skeleton-loader/src/index.ts new file mode 100644 index 0000000000..8ea16d5b95 --- /dev/null +++ b/libs/extensions/angular/skeleton-loader/src/index.ts @@ -0,0 +1 @@ +export { SkeletonLoaderComponent } from './skeleton-loader.component'; diff --git a/libs/extensions/angular/skeleton-loader/src/skeleton-loader.component.html b/libs/extensions/angular/skeleton-loader/src/skeleton-loader.component.html new file mode 100644 index 0000000000..9b22fde9cc --- /dev/null +++ b/libs/extensions/angular/skeleton-loader/src/skeleton-loader.component.html @@ -0,0 +1 @@ +