diff --git a/.changeset/light-ways-dress.md b/.changeset/light-ways-dress.md new file mode 100644 index 000000000..1e024620a --- /dev/null +++ b/.changeset/light-ways-dress.md @@ -0,0 +1,5 @@ +--- +'svelte-ux': minor +--- + +BREAKING: removed `dateDisplay()` in favor of `format()` diff --git a/.changeset/ninety-carpets-cross.md b/.changeset/ninety-carpets-cross.md new file mode 100644 index 000000000..04b6556c5 --- /dev/null +++ b/.changeset/ninety-carpets-cross.md @@ -0,0 +1,5 @@ +--- +'svelte-ux': patch +--- + +add locale management of date leveraging intl diff --git a/.gitignore b/.gitignore index d3f118eb4..5535d8867 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,6 @@ coverage/ .idea/ .svelte-kit/ .env +.DS_Store test-* \ No newline at end of file diff --git a/packages/svelte-ux/package.json b/packages/svelte-ux/package.json index 71504b720..7681685e0 100644 --- a/packages/svelte-ux/package.json +++ b/packages/svelte-ux/package.json @@ -13,7 +13,7 @@ "prepublishOnly": "svelte-package", "check": "svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-check --tsconfig ./tsconfig.json --watch", - "test:unit": "vitest", + "test:unit": "TZ=UTC+4 vitest --coverage", "lint": "prettier --ignore-path ../../.gitignore --check .", "format": "prettier --ignore-path ../../.gitignore --write .", "typedoc": "typedoc", @@ -29,6 +29,7 @@ "@types/lodash-es": "^4.17.11", "@types/marked": "^6.0.0", "@types/prismjs": "^1.26.3", + "@vitest/coverage-v8": "^0.34.6", "autoprefixer": "^10.4.16", "execa": "^8.0.1", "marked": "^10.0.0", @@ -56,8 +57,7 @@ "clsx": "^2.0.0", "d3-array": "^3.2.4", "d3-scale": "^4.0.2", - "d3-time": "^3.1.0", - "date-fns": "^2.30.0", + "date-fns": "^3.0.5", "immer": "^10.0.3", "lodash-es": "^4.17.21", "posthog-js": "^1.95.1", diff --git a/packages/svelte-ux/src/lib/components/DateButton.svelte b/packages/svelte-ux/src/lib/components/DateButton.svelte index 7a8b35aa5..2ec7be7cc 100644 --- a/packages/svelte-ux/src/lib/components/DateButton.svelte +++ b/packages/svelte-ux/src/lib/components/DateButton.svelte @@ -1,12 +1,13 @@ {#if value?.from} - {dateDisplay(value.from, { periodType, format, variant, utc })} + {format_ux(value.from, getPeriodType(value), { variant: 'long' })} {:else}
 
{/if} {#if value?.to && showToValue} - - {dateDisplay(value.to, { periodType, format, variant, utc })} + {format_ux(value.to, getPeriodType(value), { variant: 'long' })} {/if} diff --git a/packages/svelte-ux/src/lib/components/DateRangeField.svelte b/packages/svelte-ux/src/lib/components/DateRangeField.svelte index 573c9c0bb..0c8306442 100644 --- a/packages/svelte-ux/src/lib/components/DateRangeField.svelte +++ b/packages/svelte-ux/src/lib/components/DateRangeField.svelte @@ -53,7 +53,6 @@ export let icon: string | null = null; let open: boolean = false; - let format: string = undefined; let currentValue = value; @@ -104,7 +103,7 @@ on:click={() => (open = true)} {id} > - +
diff --git a/packages/svelte-ux/src/lib/components/Month.svelte b/packages/svelte-ux/src/lib/components/Month.svelte index 1348874f9..3175243ae 100644 --- a/packages/svelte-ux/src/lib/components/Month.svelte +++ b/packages/svelte-ux/src/lib/components/Month.svelte @@ -6,7 +6,6 @@ endOfDay as endOfDayFunc, startOfMonth as startOfMonthFunc, endOfMonth as endOfMonthFunc, - format, addMonths, isSameDay, isWithinInterval, @@ -16,10 +15,12 @@ import { getMonthDaysByWeek, PeriodType } from '../utils/date'; import type { SelectedDate } from '../utils/date'; + import { format } from '../utils'; import { hasKeyOf } from '../types/typeGuards'; import Button from './Button.svelte'; import DateButton from './DateButton.svelte'; + import { getSettings } from '.'; export let selected: SelectedDate | undefined = undefined; @@ -33,7 +34,7 @@ startOfMonthFunc(new Date()); $: endOfMonth = endOfMonthFunc(startOfMonth); - $: monthDaysByWeek = getMonthDaysByWeek(startOfMonth); + $: monthDaysByWeek = getMonthDaysByWeek(startOfMonth, getSettings().formats?.dates?.weekStartsOn); /** * Hide controls and date. Useful to control externally @@ -54,15 +55,15 @@ return disabledDays instanceof Function ? disabledDays(date) : disabledDays instanceof Date - ? isSameDay(date, disabledDays) - : disabledDays instanceof Array - ? disabledDays.some((d) => isSameDay(date, d)) - : disabledDays instanceof Object - ? isWithinInterval(date, { - start: startOfDayFunc(disabledDays.from), - end: endOfDayFunc(disabledDays.to || disabledDays.from), - }) - : false; + ? isSameDay(date, disabledDays) + : disabledDays instanceof Array + ? disabledDays.some((d) => isSameDay(date, d)) + : disabledDays instanceof Object + ? isWithinInterval(date, { + start: startOfDayFunc(disabledDays.from), + end: endOfDayFunc(disabledDays.to || disabledDays.from), + }) + : false; }; $: isDayHidden = (day: Date) => { @@ -91,7 +92,7 @@ />
- {format(startOfMonth, 'MMMM yyyy')} + {format(startOfMonth, PeriodType.MonthYear)}
diff --git a/packages/svelte-ux/src/lib/components/index.ts b/packages/svelte-ux/src/lib/components/index.ts index 2db586d8c..c1fbbdb81 100644 --- a/packages/svelte-ux/src/lib/components/index.ts +++ b/packages/svelte-ux/src/lib/components/index.ts @@ -92,5 +92,5 @@ export { default as TreeList } from './TreeList.svelte'; export { default as TweenedValue } from './TweenedValue.svelte'; export { default as ViewportCenter } from './ViewportCenter.svelte'; export { default as YearList } from './YearList.svelte'; -export { settings, getFormatNumberOptions, getSettings } from './settings'; +export { settings, getFormatNumber, getSettings, getDictionary } from './settings'; export { getTheme, getComponentTheme } from './theme'; diff --git a/packages/svelte-ux/src/lib/components/settings.ts b/packages/svelte-ux/src/lib/components/settings.ts index fd943e754..c6d862db4 100644 --- a/packages/svelte-ux/src/lib/components/settings.ts +++ b/packages/svelte-ux/src/lib/components/settings.ts @@ -2,6 +2,15 @@ import type { FormatNumberOptions, FormatNumberStyle } from '$lib/utils/number'; import { getContext, setContext } from 'svelte'; import type { Theme } from './theme'; import type { Prettify } from '$lib/types/typeHelpers'; +import { + type FormatDateOptions, + DayOfWeek, + type DateFormatVariant, + type CustomIntlDateTimeFormatOptions, + type OrdinalSuffixes, + DateToken, +} from '$lib/utils/date'; +import type { DictionaryMessages, DictionaryMessagesOptions } from '$lib/utils/dictionary'; type ExcludeNone = T extends 'none' ? never : T; export type Settings = { @@ -13,7 +22,9 @@ export type Settings = { [key in ExcludeNone]?: FormatNumberOptions; } >; + dates?: FormatDateOptions; }; + dictionary?: DictionaryMessagesOptions; theme?: Theme; }; @@ -32,7 +43,7 @@ export function getSettings() { } } -export function getFormatNumberOptions(style?: FormatNumberStyle) { +export function getFormatNumber(style?: FormatNumberStyle) { let toRet = { locales: 'en', currency: 'USD', @@ -49,3 +60,226 @@ export function getFormatNumberOptions(style?: FormatNumberStyle) { return toRet; } + +export function getFormatDate(options?: FormatDateOptions) { + // if custom is set && variant is not set, let's put custom as variant + const variant: FormatDateOptions['variant'] = + options?.custom && options?.variant === undefined ? 'custom' : options?.variant ?? 'default'; + + const settings = getSettings(); + + const baseParsing = options?.baseParsing ?? settings.formats?.dates?.baseParsing ?? 'MM/dd/yyyy'; + + const custom = options?.custom ?? ''; + + let toRet: { + locales: string; + baseParsing: string; + weekStartsOn: DayOfWeek; + variant: DateFormatVariant; + custom: CustomIntlDateTimeFormatOptions; + presets: { + day: Record; + dayTime: Record; + timeOnly: Record; + week: Record; + month: Record; + monthYear: Record; + year: Record; + }; + ordinalSuffixes: Record; + dictionaryDate: DictionaryMessages['Date']; + } = { + locales: options?.locales ?? settings.formats?.dates?.locales ?? 'en', + baseParsing, + weekStartsOn: + options?.weekStartsOn ?? settings.formats?.dates?.weekStartsOn ?? DayOfWeek.Sunday, + variant, + custom, + + // keep always the en fallback + ordinalSuffixes: { + en: { + one: 'st', + two: 'nd', + few: 'rd', + other: 'th', + }, + ...settings.formats?.dates?.ordinalSuffixes, + ...options?.ordinalSuffixes, + }, + + presets: { + day: { + short: options?.presets?.day?.short ?? + settings.formats?.dates?.presets?.day?.short ?? [ + DateToken.DayOfMonth_numeric, + DateToken.Month_numeric, + ], + default: options?.presets?.day?.default ?? + settings.formats?.dates?.presets?.day?.default ?? [ + DateToken.DayOfMonth_numeric, + DateToken.Month_numeric, + DateToken.Year_numeric, + ], + long: options?.presets?.day?.long ?? + settings.formats?.dates?.presets?.day?.long ?? [ + DateToken.DayOfMonth_numeric, + DateToken.Month_short, + DateToken.Year_numeric, + ], + custom, + }, + dayTime: { + short: options?.presets?.dayTime?.short ?? + settings.formats?.dates?.presets?.dayTime?.short ?? [ + DateToken.DayOfMonth_numeric, + DateToken.Month_numeric, + DateToken.Year_numeric, + DateToken.Hour_numeric, + DateToken.Minute_numeric, + ], + default: options?.presets?.dayTime?.default ?? + settings.formats?.dates?.presets?.dayTime?.default ?? [ + DateToken.DayOfMonth_numeric, + DateToken.Month_numeric, + DateToken.Year_numeric, + DateToken.Hour_2Digit, + DateToken.Minute_2Digit, + ], + long: options?.presets?.dayTime?.long ?? + settings.formats?.dates?.presets?.dayTime?.long ?? [ + DateToken.DayOfMonth_numeric, + DateToken.Month_numeric, + DateToken.Year_numeric, + DateToken.Hour_2Digit, + DateToken.Minute_2Digit, + DateToken.Second_2Digit, + ], + custom, + }, + + timeOnly: { + short: options?.presets?.timeOnly?.short ?? + settings.formats?.dates?.presets?.timeOnly?.short ?? [ + DateToken.Hour_numeric, + DateToken.Minute_numeric, + ], + default: options?.presets?.timeOnly?.default ?? + settings.formats?.dates?.presets?.timeOnly?.default ?? [ + DateToken.Hour_2Digit, + DateToken.Minute_2Digit, + DateToken.Second_2Digit, + ], + long: options?.presets?.timeOnly?.long ?? + settings.formats?.dates?.presets?.timeOnly?.long ?? [ + DateToken.Hour_2Digit, + DateToken.Minute_2Digit, + DateToken.Second_2Digit, + DateToken.MiliSecond_3, + ], + custom, + }, + + week: { + short: options?.presets?.week?.short ?? + settings.formats?.dates?.presets?.week?.short ?? [ + DateToken.DayOfMonth_numeric, + DateToken.Month_numeric, + ], + default: options?.presets?.week?.default ?? + settings.formats?.dates?.presets?.week?.default ?? [ + DateToken.DayOfMonth_numeric, + DateToken.Month_numeric, + DateToken.Year_numeric, + ], + long: options?.presets?.week?.long ?? + settings.formats?.dates?.presets?.week?.long ?? [ + DateToken.DayOfMonth_numeric, + DateToken.Month_numeric, + DateToken.Year_numeric, + ], + custom, + }, + month: { + short: + options?.presets?.month?.short ?? + settings.formats?.dates?.presets?.month?.short ?? + DateToken.Month_short, + default: + options?.presets?.month?.default ?? + settings.formats?.dates?.presets?.month?.default ?? + DateToken.Month_short, + long: + options?.presets?.month?.long ?? + settings.formats?.dates?.presets?.month?.long ?? + DateToken.Month_long, + custom, + }, + monthYear: { + short: options?.presets?.monthsYear?.short ?? + settings.formats?.dates?.presets?.monthsYear?.short ?? [ + DateToken.Month_short, + DateToken.Year_2Digit, + ], + default: options?.presets?.monthsYear?.default ?? + settings.formats?.dates?.presets?.monthsYear?.default ?? [ + DateToken.Month_long, + DateToken.Year_numeric, + ], + long: options?.presets?.monthsYear?.long ?? + settings.formats?.dates?.presets?.monthsYear?.long ?? [ + DateToken.Month_long, + DateToken.Year_numeric, + ], + custom, + }, + year: { + short: + options?.presets?.year?.short ?? + settings.formats?.dates?.presets?.year?.short ?? + DateToken.Year_2Digit, + default: + options?.presets?.year?.default ?? + settings.formats?.dates?.presets?.year?.default ?? + DateToken.Year_numeric, + long: + options?.presets?.year?.long ?? + settings.formats?.dates?.presets?.year?.long ?? + DateToken.Year_numeric, + custom, + }, + }, + + // dico + dictionaryDate: getDictionary().Date, + }; + + return toRet; +} + +export function getDictionary(options?: DictionaryMessagesOptions) { + // if custom is set && variant is not set, let's put custom as variant + const settings = getSettings(); + + let toRet: DictionaryMessages = { + Ok: options?.Ok ?? settings.dictionary?.Ok ?? 'Ok', + Cancel: options?.Cancel ?? settings.dictionary?.Cancel ?? 'Cancel', + + Date: { + Day: options?.Date?.Day ?? settings.dictionary?.Date?.Day ?? 'Day', + Week: options?.Date?.Week ?? settings.dictionary?.Date?.Week ?? 'Week', + BiWeek: options?.Date?.BiWeek ?? settings.dictionary?.Date?.BiWeek ?? 'Bi-Week', + Month: options?.Date?.Month ?? settings.dictionary?.Date?.Month ?? 'Month', + Quarter: options?.Date?.Quarter ?? settings.dictionary?.Date?.Quarter ?? 'Quarter', + CalendarYear: + options?.Date?.CalendarYear ?? settings.dictionary?.Date?.CalendarYear ?? 'Calendar Year', + FiscalYearOct: + options?.Date?.FiscalYearOct ?? + settings.dictionary?.Date?.FiscalYearOct ?? + 'Fiscal Year (Oct)', + }, + }; + + return toRet; +} diff --git a/packages/svelte-ux/src/lib/index.ts b/packages/svelte-ux/src/lib/index.ts index 180dd4045..3e5b71788 100644 --- a/packages/svelte-ux/src/lib/index.ts +++ b/packages/svelte-ux/src/lib/index.ts @@ -2,4 +2,5 @@ export * from './actions'; export * from './components'; export * from './stores'; export * from './types'; +// TODO: Conflic Duration Component & Type export * from './utils'; diff --git a/packages/svelte-ux/src/lib/utils/date.test.ts b/packages/svelte-ux/src/lib/utils/date.test.ts new file mode 100644 index 000000000..09e8447d1 --- /dev/null +++ b/packages/svelte-ux/src/lib/utils/date.test.ts @@ -0,0 +1,483 @@ +import { describe, it, expect } from 'vitest'; +import { + PeriodType, + formatDate, + getMonthDaysByWeek, + localToUtcDate, + utcToLocalDate, + DayOfWeek, + formatIntl, + type CustomIntlDateTimeFormatOptions, + type FormatDateOptions, + DateToken, +} from './date'; +import { format } from '.'; + +const DATE = '2023-11-21'; // "good" default date as the day (21) is bigger than 12 (number of months). And november is a good month1 (because why not?) +const dt_2M_2d = new Date(2023, 10, 21); +const dt_2M_1d = new Date(2023, 10, 7); +const dt_1M_1d = new Date(2023, 2, 7); + +const dt_1M_1d_time_pm = new Date(2023, 2, 7, 14, 2, 3, 4); +const dt_1M_1d_time_am = new Date(2023, 2, 7, 1, 2, 3, 4); + +const fr: FormatDateOptions = { + locales: 'fr', + ordinalSuffixes: { + fr: { + one: 'er', + two: '', + few: '', + other: '', + }, + }, +}; + +describe('formatDate()', () => { + it('should return empty string for null or undefined date', () => { + expect(formatDate(null)).equal(''); + expect(formatDate(undefined)).equal(''); + }); + + it('should return empty string for invalid date', () => { + expect(formatDate('invalid date')).equal(''); + }); + + describe('should format date for PeriodType.Day', () => { + const localDate = new Date(2023, 10, 21); + const combi = [ + ['short', undefined, '11/21'], + ['short', 'fr', '21/11'], + ['long', undefined, 'Nov 21, 2023'], + ['long', 'fr', '21 nov. 2023'], + ] as const; + + for (const c of combi) { + const [variant, locales, expected] = c; + it(c.toString(), () => { + expect(formatDate(localDate, PeriodType.Day, { variant, locales })).equal(expected); + }); + } + }); + + describe('should format date string for PeriodType.Day', () => { + const combi = [ + ['short', undefined, '11/21'], + ['short', 'fr', '21/11'], + ['long', undefined, 'Nov 21, 2023'], + ['long', 'fr', '21 nov. 2023'], + ] as const; + + for (const c of combi) { + const [variant, locales, expected] = c; + it(c.toString(), () => { + expect(formatDate(DATE, PeriodType.Day, { variant, locales })).equal(expected); + }); + } + }); + + describe('should format date string for DayTime, TimeOnly', () => { + const combi: [Date, PeriodType, FormatDateOptions, string[]][] = [ + [ + dt_1M_1d_time_pm, + PeriodType.DayTime, + { variant: 'short' }, + ['3/7/2023, 2:02 PM', '07/03/2023 14:02'], + ], + [ + dt_1M_1d_time_pm, + PeriodType.DayTime, + { variant: 'default' }, + ['3/7/2023, 02:02 PM', '07/03/2023 14:02'], + ], + [ + dt_1M_1d_time_pm, + PeriodType.DayTime, + { variant: 'long' }, + ['3/7/2023, 02:02:03 PM', '07/03/2023 14:02:03'], + ], + [dt_1M_1d_time_pm, PeriodType.TimeOnly, { variant: 'short' }, ['2:02 PM', '14:02']], + [dt_1M_1d_time_pm, PeriodType.TimeOnly, { variant: 'default' }, ['02:02:03 PM', '14:02:03']], + [ + dt_1M_1d_time_pm, + PeriodType.TimeOnly, + { variant: 'long' }, + ['02:02:03.004 PM', '14:02:03,004'], + ], + ]; + + for (const c of combi) { + const [date, periodType, options, [expected_default, expected_fr]] = c; + it(c.toString(), () => { + expect(format(date, periodType, options)).equal(expected_default); + }); + + it(c.toString() + 'fr', () => { + expect(format(date, periodType, { ...options, ...fr })).equal(expected_fr); + }); + } + }); + + describe('should format date for PeriodType.WeekSun / Mon', () => { + const combi = [ + [PeriodType.WeekSun, 'short', undefined, '11/19 - 11/25'], + [PeriodType.WeekSun, 'short', 'fr', '19/11 - 25/11'], + [PeriodType.WeekSun, 'long', undefined, '11/19/2023 - 11/25/2023'], + [PeriodType.WeekSun, 'long', 'fr', '19/11/2023 - 25/11/2023'], + [PeriodType.WeekMon, 'long', undefined, '11/20/2023 - 11/26/2023'], + [PeriodType.WeekMon, 'long', 'fr', '20/11/2023 - 26/11/2023'], + ] as const; + + for (const c of combi) { + const [periodType, variant, locales, expected] = c; + it(c.toString(), () => { + expect(formatDate(DATE, periodType, { variant, locales })).equal(expected); + }); + } + }); + + describe('should format date for PeriodType.Week', () => { + const combi = [ + [PeriodType.Week, 'short', undefined, DayOfWeek.Sunday, '11/19 - 11/25'], + [PeriodType.Week, 'short', 'fr', DayOfWeek.Sunday, '19/11 - 25/11'], + [PeriodType.Week, 'long', undefined, DayOfWeek.Sunday, '11/19/2023 - 11/25/2023'], + [PeriodType.Week, 'long', 'fr', DayOfWeek.Sunday, '19/11/2023 - 25/11/2023'], + + [PeriodType.Week, 'short', undefined, DayOfWeek.Monday, '11/20 - 11/26'], + [PeriodType.Week, 'short', 'fr', DayOfWeek.Monday, '20/11 - 26/11'], + [PeriodType.Week, 'long', undefined, DayOfWeek.Monday, '11/20/2023 - 11/26/2023'], + [PeriodType.Week, 'long', 'fr', DayOfWeek.Monday, '20/11/2023 - 26/11/2023'], + ] as const; + + for (const c of combi) { + const [periodType, variant, locales, weekStartsOn, expected] = c; + it(c.toString(), () => { + expect(formatDate(DATE, periodType, { variant, locales, weekStartsOn })).equal(expected); + }); + } + }); + + describe('should format date for PeriodType.Month', () => { + const combi = [ + ['short', undefined, 'Nov'], + ['short', 'fr', 'nov.'], + ['long', undefined, 'November'], + ['long', 'fr', 'novembre'], + ] as const; + + for (const c of combi) { + const [variant, locales, expected] = c; + it(c.toString(), () => { + expect(formatDate(DATE, PeriodType.Month, { variant, locales })).equal(expected); + }); + } + }); + + describe('should format date for PeriodType.MonthYear', () => { + const combi = [ + ['short', undefined, 'Nov 23'], + ['short', 'fr', 'nov. 23'], + ['long', undefined, 'November 2023'], + ['long', 'fr', 'novembre 2023'], + ] as const; + + for (const c of combi) { + const [variant, locales, expected] = c; + it(c.toString(), () => { + expect(formatDate(DATE, PeriodType.MonthYear, { variant, locales })).equal(expected); + }); + } + }); + + describe('should format date for PeriodType.Quarter', () => { + const combi = [ + ['short', undefined, 'Oct - Dec 23'], + ['short', 'fr', 'oct. - déc. 23'], + ['long', undefined, 'October - December 2023'], + ['long', 'fr', 'octobre - décembre 2023'], + ] as const; + + for (const c of combi) { + const [variant, locales, expected] = c; + it(c.toString(), () => { + expect(formatDate(DATE, PeriodType.Quarter, { variant, locales })).equal(expected); + }); + } + }); + + describe('should format date for PeriodType.CalendarYear', () => { + const combi = [ + ['short', undefined, '23'], + ['short', 'fr', '23'], + ['long', undefined, '2023'], + ['long', 'fr', '2023'], + ] as const; + + for (const c of combi) { + const [variant, locales, expected] = c; + it(c.toString(), () => { + expect(formatDate(DATE, PeriodType.CalendarYear, { variant, locales })).equal(expected); + }); + } + }); + + describe('should format date for PeriodType.FiscalYearOctober', () => { + const combi = [ + ['short', undefined, '24'], + ['short', 'fr', '24'], + ['long', undefined, '2024'], + ['long', 'fr', '2024'], + ] as const; + + for (const c of combi) { + const [variant, locales, expected] = c; + it(c.toString(), () => { + expect(formatDate(DATE, PeriodType.FiscalYearOctober, { variant, locales })).equal( + expected + ); + }); + } + }); + + describe('should format date for PeriodType.BiWeek1Sun', () => { + const combi = [ + ['short', undefined, '11/12 - 11/25'], + ['short', 'fr', '12/11 - 25/11'], + ['long', undefined, '11/12/2023 - 11/25/2023'], + ['long', 'fr', '12/11/2023 - 25/11/2023'], + ] as const; + + for (const c of combi) { + const [variant, locales, expected] = c; + it(c.toString(), () => { + expect(formatDate(DATE, PeriodType.BiWeek1Sun, { variant, locales })).equal(expected); + }); + } + }); + + describe('should format date for PeriodType.undefined', () => { + const expected = '2023-11-21T00:00:00-04:00'; + const combi = [ + ['short', undefined], + ['short', 'fr'], + ['long', undefined], + ['long', 'fr'], + ] as const; + + for (const c of combi) { + const [variant, locales] = c; + it(c.toString(), () => { + expect(formatDate(DATE, undefined, { variant, locales })).equal(expected); + }); + } + }); +}); + +describe('formatIntl() tokens', () => { + const combi: [Date, CustomIntlDateTimeFormatOptions, string[]][] = [ + [dt_1M_1d, 'MM/dd/yyyy', ['03/07/2023', '07/03/2023']], + [dt_2M_2d, 'M/d/yyyy', ['11/21/2023', '21/11/2023']], + [dt_2M_1d, 'M/d/yyyy', ['11/7/2023', '07/11/2023']], + [dt_2M_1d, 'M/dd/yyyy', ['11/07/2023', '07/11/2023']], + [dt_1M_1d, 'M/d/yyyy', ['3/7/2023', '07/03/2023']], + [dt_1M_1d, 'MM/d/yyyy', ['03/7/2023', '7/03/2023']], + [dt_2M_2d, 'M/d', ['11/21', '21/11']], + [dt_2M_2d, 'MMM d, yyyy', ['Nov 21, 2023', '21 nov. 2023']], + [dt_2M_1d, 'MMM d, yyyy', ['Nov 7, 2023', '7 nov. 2023']], + [dt_2M_1d, 'MMM do, yyyy', ['Nov 7th, 2023', '7 nov. 2023']], + [dt_2M_2d, 'MMM', ['Nov', 'nov.']], + [dt_2M_2d, 'MMMM', ['November', 'novembre']], + [dt_2M_2d, 'MMM yy', ['Nov 23', 'nov. 23']], + [dt_2M_2d, 'MMMM yyyy', ['November 2023', 'novembre 2023']], + [dt_2M_2d, 'yy', ['23', '23']], + [dt_2M_2d, 'yyyy', ['2023', '2023']], + [dt_2M_2d, { dateStyle: 'full' }, ['Tuesday, November 21, 2023', 'mardi 21 novembre 2023']], + [dt_2M_2d, { dateStyle: 'long' }, ['November 21, 2023', '21 novembre 2023']], + [dt_2M_2d, { dateStyle: 'medium' }, ['Nov 21, 2023', '21 nov. 2023']], + [dt_2M_2d, { dateStyle: 'medium', withOrdinal: true }, ['Nov 21st, 2023', '21 nov. 2023']], + [dt_2M_2d, { dateStyle: 'short' }, ['11/21/23', '21/11/2023']], + [dt_1M_1d, { dateStyle: 'short' }, ['3/7/23', '07/03/2023']], + + // time + [dt_1M_1d_time_pm, [DateToken.Hour_numeric, DateToken.Minute_numeric], ['2:02 PM', '14:02']], + [dt_1M_1d_time_am, [DateToken.Hour_numeric, DateToken.Minute_numeric], ['1:02 AM', '01:02']], + [ + dt_1M_1d_time_am, + [DateToken.Hour_numeric, DateToken.Minute_numeric, DateToken.Hour_wAMPM], + ['1:02 AM', '1:02 AM'], + ], + [ + dt_1M_1d_time_am, + [DateToken.Hour_2Digit, DateToken.Minute_2Digit, DateToken.Hour_woAMPM], + ['01:02', '01:02'], + ], + [ + dt_1M_1d_time_am, + [DateToken.Hour_numeric, DateToken.Minute_numeric, DateToken.Second_numeric], + ['1:02:03 AM', '01:02:03'], + ], + [ + dt_1M_1d_time_am, + [ + DateToken.Hour_numeric, + DateToken.Minute_numeric, + DateToken.Second_numeric, + DateToken.MiliSecond_3, + ], + ['1:02:03.004 AM', '01:02:03,004'], + ], + ]; + + for (const c of combi) { + const [date, tokens, [expected_default, expected_fr]] = c; + it(c.toString(), () => { + expect(formatIntl(date, tokens)).equal(expected_default); + }); + + it(c.toString() + 'fr', () => { + expect(formatIntl(date, tokens, fr)).equal(expected_fr); + }); + } +}); + +describe('utcToLocalDate()', () => { + it('in with offset -00 => local', () => { + const utcDate = '2023-11-21T00:00:00-00:00'; + const localDate = utcToLocalDate(utcDate); + expect(localDate.toISOString()).equal('2023-11-21T04:00:00.000Z'); + }); + + it('in without offset, the utc is already +4, to local: another +4', () => { + const utcDate = '2023-11-21T00:00:00'; + const localDate = utcToLocalDate(utcDate); + expect(localDate.toISOString()).equal('2023-11-21T08:00:00.000Z'); + }); +}); + +describe('localToUtcDate()', () => { + it('in with offset -04 => UTC', () => { + const localDate = '2023-11-21T00:00:00-04:00'; + const utcDate = localToUtcDate(localDate); + expect(utcDate.toISOString()).equal('2023-11-21T00:00:00.000Z'); + }); + + it('in with offset -00 => UTC', () => { + const localDate = '2023-11-21T04:00:00-00:00'; + const utcDate = localToUtcDate(localDate); + expect(utcDate.toISOString()).equal('2023-11-21T00:00:00.000Z'); + }); + + it('in without offset == UTC', () => { + const localDate = '2023-11-21T04:00:00'; + const utcDate = localToUtcDate(localDate); + expect(utcDate.toISOString()).equal('2023-11-21T04:00:00.000Z'); + }); +}); + +describe('getMonthDaysByWeek()', () => { + it('default starting Week: Sunday', () => { + const dates = getMonthDaysByWeek(new Date(DATE)); + expect(dates).toMatchInlineSnapshot(` + [ + [ + 2023-10-29T04:00:00.000Z, + 2023-10-30T04:00:00.000Z, + 2023-10-31T04:00:00.000Z, + 2023-11-01T04:00:00.000Z, + 2023-11-02T04:00:00.000Z, + 2023-11-03T04:00:00.000Z, + 2023-11-04T04:00:00.000Z, + ], + [ + 2023-11-05T04:00:00.000Z, + 2023-11-06T04:00:00.000Z, + 2023-11-07T04:00:00.000Z, + 2023-11-08T04:00:00.000Z, + 2023-11-09T04:00:00.000Z, + 2023-11-10T04:00:00.000Z, + 2023-11-11T04:00:00.000Z, + ], + [ + 2023-11-12T04:00:00.000Z, + 2023-11-13T04:00:00.000Z, + 2023-11-14T04:00:00.000Z, + 2023-11-15T04:00:00.000Z, + 2023-11-16T04:00:00.000Z, + 2023-11-17T04:00:00.000Z, + 2023-11-18T04:00:00.000Z, + ], + [ + 2023-11-19T04:00:00.000Z, + 2023-11-20T04:00:00.000Z, + 2023-11-21T04:00:00.000Z, + 2023-11-22T04:00:00.000Z, + 2023-11-23T04:00:00.000Z, + 2023-11-24T04:00:00.000Z, + 2023-11-25T04:00:00.000Z, + ], + [ + 2023-11-26T04:00:00.000Z, + 2023-11-27T04:00:00.000Z, + 2023-11-28T04:00:00.000Z, + 2023-11-29T04:00:00.000Z, + 2023-11-30T04:00:00.000Z, + 2023-12-01T04:00:00.000Z, + 2023-12-02T04:00:00.000Z, + ], + ] + `); + }); + + it('Starting Week: Monday', () => { + const dates = getMonthDaysByWeek(new Date(DATE), 1); + expect(dates).toMatchInlineSnapshot(` + [ + [ + 2023-10-30T04:00:00.000Z, + 2023-10-31T04:00:00.000Z, + 2023-11-01T04:00:00.000Z, + 2023-11-02T04:00:00.000Z, + 2023-11-03T04:00:00.000Z, + 2023-11-04T04:00:00.000Z, + 2023-11-05T04:00:00.000Z, + ], + [ + 2023-11-06T04:00:00.000Z, + 2023-11-07T04:00:00.000Z, + 2023-11-08T04:00:00.000Z, + 2023-11-09T04:00:00.000Z, + 2023-11-10T04:00:00.000Z, + 2023-11-11T04:00:00.000Z, + 2023-11-12T04:00:00.000Z, + ], + [ + 2023-11-13T04:00:00.000Z, + 2023-11-14T04:00:00.000Z, + 2023-11-15T04:00:00.000Z, + 2023-11-16T04:00:00.000Z, + 2023-11-17T04:00:00.000Z, + 2023-11-18T04:00:00.000Z, + 2023-11-19T04:00:00.000Z, + ], + [ + 2023-11-20T04:00:00.000Z, + 2023-11-21T04:00:00.000Z, + 2023-11-22T04:00:00.000Z, + 2023-11-23T04:00:00.000Z, + 2023-11-24T04:00:00.000Z, + 2023-11-25T04:00:00.000Z, + 2023-11-26T04:00:00.000Z, + ], + [ + 2023-11-27T04:00:00.000Z, + 2023-11-28T04:00:00.000Z, + 2023-11-29T04:00:00.000Z, + 2023-11-30T04:00:00.000Z, + 2023-12-01T04:00:00.000Z, + 2023-12-02T04:00:00.000Z, + 2023-12-03T04:00:00.000Z, + ], + ] + `); + }); +}); diff --git a/packages/svelte-ux/src/lib/utils/date.ts b/packages/svelte-ux/src/lib/utils/date.ts index 581354e18..37277dc92 100644 --- a/packages/svelte-ux/src/lib/utils/date.ts +++ b/packages/svelte-ux/src/lib/utils/date.ts @@ -1,5 +1,4 @@ import { - format, startOfDay, endOfDay, startOfWeek, @@ -31,11 +30,10 @@ import { formatISO, } from 'date-fns'; -import { timeDays } from 'd3-time'; - import { hasKeyOf } from '../types/typeGuards'; import { chunk } from './array'; import type { DateRange } from './dateRange'; +import { getFormatDate } from '$lib/components/settings'; export type SelectedDate = Date | Date[] | DateRange | null; @@ -46,7 +44,11 @@ export type Period = { }; export enum PeriodType { + Custom = 1, + Day = 10, + DayTime = 11, + TimeOnly = 15, WeekSun = 20, WeekMon = 21, @@ -55,8 +57,10 @@ export enum PeriodType { WeekThu = 24, WeekFri = 25, WeekSat = 26, + Week = 27, // will be replaced by WeekSun, WeekMon, etc depending on weekStartsOn Month = 30, + MonthYear = 31, Quarter = 40, CalendarYear = 50, FiscalYearOctober = 60, @@ -68,6 +72,7 @@ export enum PeriodType { BiWeek1Thu = 74, BiWeek1Fri = 75, BiWeek1Sat = 76, + BiWeek1 = 77, // will be replaced by BiWeek1Sun, BiWeek1Mon, etc depending on weekStartsOn BiWeek2Sun = 80, BiWeek2Mon = 81, @@ -76,76 +81,89 @@ export enum PeriodType { BiWeek2Thu = 84, BiWeek2Fri = 85, BiWeek2Sat = 86, + BiWeek2 = 87, // will be replaced by BiWeek2Sun, BiWeek2Mon, etc depending on weekStartsOn } export enum DayOfWeek { - SUN, - MON, - TUE, - WED, - THU, - FRI, - SAT, + Sunday = 0, + Monday = 1, + Tuesday = 2, + Wednesday = 3, + Thursday = 4, + Friday = 5, + Saturday = 6, } -export function getPeriodTypeName(periodType: PeriodType) { +export function getDayOfWeekName(weekStartsOn: DayOfWeek, locales: string) { + // Create a date object for a specific day (0 = Sunday, 1 = Monday, etc.) + // And "7 of Jan 2024" is a Sunday + const date = new Date(2024, 0, 7 + weekStartsOn); + const formatter = new Intl.DateTimeFormat(locales, { weekday: 'short' }); + return formatter.format(date); +} + +export function getPeriodTypeName(periodType: PeriodType, options: FormatDateOptions = {}) { + const { locales, dictionaryDate: dico } = getFormatDate(options); + switch (periodType) { case PeriodType.Day: - return 'Day'; + return dico.Day; case PeriodType.WeekSun: - return 'Week (Sun)'; + return `${dico.Week} (${getDayOfWeekName(DayOfWeek.Sunday, locales)})`; case PeriodType.WeekMon: - return 'Week (Mon)'; + return `${dico.Week} (${getDayOfWeekName(1, locales)})`; case PeriodType.WeekTue: - return 'Week (Tue)'; + return `${dico.Week} (${getDayOfWeekName(2, locales)})`; case PeriodType.WeekWed: - return 'Week (Wed)'; + return `${dico.Week} (${getDayOfWeekName(3, locales)})`; case PeriodType.WeekThu: - return 'Week (Thu)'; + return `${dico.Week} (${getDayOfWeekName(4, locales)})`; case PeriodType.WeekFri: - return 'Week (Fri)'; + return `${dico.Week} (${getDayOfWeekName(5, locales)})`; case PeriodType.WeekSat: - return 'Week (Sat)'; + return `${dico.Week} (${getDayOfWeekName(6, locales)})`; case PeriodType.Month: - return 'Month'; + return dico.Month; + case PeriodType.MonthYear: + return dico.Month; case PeriodType.Quarter: - return 'Quarter'; + return dico.Quarter; case PeriodType.CalendarYear: - return 'Calendar Year'; + return dico.CalendarYear; case PeriodType.FiscalYearOctober: - return 'Fiscal Year (Oct)'; + return dico.FiscalYearOct; case PeriodType.BiWeek1Sun: - return 'Bi-Week (Sun)'; + return `${dico.BiWeek} (${getDayOfWeekName(0, locales)})`; case PeriodType.BiWeek1Mon: - return 'Bi-Week (Mon)'; + return `${dico.BiWeek} (${getDayOfWeekName(1, locales)})`; case PeriodType.BiWeek1Tue: - return 'Bi-Week (Tue)'; + return `${dico.BiWeek} (${getDayOfWeekName(2, locales)})`; case PeriodType.BiWeek1Wed: - return 'Bi-Week (Wed)'; + return `${dico.BiWeek} (${getDayOfWeekName(3, locales)})`; case PeriodType.BiWeek1Thu: - return 'Bi-Week (Thu)'; + return `${dico.BiWeek} (${getDayOfWeekName(4, locales)})`; case PeriodType.BiWeek1Fri: - return 'Bi-Week (Fri)'; + return `${dico.BiWeek} (${getDayOfWeekName(5, locales)})`; case PeriodType.BiWeek1Sat: - return 'Bi-Week (Sat)'; + return `${dico.BiWeek} (${getDayOfWeekName(6, locales)})`; case PeriodType.BiWeek2Sun: - return 'Bi-Week 2 (Sun)'; + return `${dico.BiWeek} 2 (${getDayOfWeekName(0, locales)})`; case PeriodType.BiWeek2Mon: - return 'Bi-Week 2 (Mon)'; + return `${dico.BiWeek} 2 (${getDayOfWeekName(1, locales)})`; case PeriodType.BiWeek2Tue: - return 'Bi-Week 2 (Tue)'; + return `${dico.BiWeek} 2 (${getDayOfWeekName(2, locales)})`; case PeriodType.BiWeek2Wed: - return 'Bi-Week 2 (Wed)'; + return `${dico.BiWeek} 2 (${getDayOfWeekName(3, locales)})`; case PeriodType.BiWeek2Thu: - return 'Bi-Week 2 (Thu)'; + return `${dico.BiWeek} 2 (${getDayOfWeekName(4, locales)})`; case PeriodType.BiWeek2Fri: - return 'Bi-Week 2 (Fri)'; + return `${dico.BiWeek} 2 (${getDayOfWeekName(5, locales)})`; case PeriodType.BiWeek2Sat: - return 'Bi-Week 2 (Sat)'; + return `${dico.BiWeek} 2 (${getDayOfWeekName(6, locales)})`; default: return 'Unknown'; @@ -292,12 +310,22 @@ export function getMonths(year = new Date().getFullYear()) { return Array.from({ length: 12 }, (_, i) => new Date(year, i, 1)); } -export function getMonthDaysByWeek(startOfMonth: Date): Date[][] { - const startOfFirstWeek = startOfWeek(startOfMonth); - const endOfLastWeek = endOfWeek(endOfMonth(startOfMonth)); - const monthDaysByWeek = chunk(timeDays(startOfFirstWeek, endOfLastWeek), 7); +export function getMonthDaysByWeek( + dateInTheMonth: Date, + weekStartsOn: DayOfWeek = DayOfWeek.Sunday +): Date[][] { + const startOfFirstWeek = startOfWeek(startOfMonth(dateInTheMonth), { weekStartsOn }); + const endOfLastWeek = endOfWeek(endOfMonth(dateInTheMonth), { weekStartsOn }); - return monthDaysByWeek; + const list = []; + + let valueToAdd = startOfFirstWeek; + while (valueToAdd <= endOfLastWeek) { + list.push(valueToAdd); + valueToAdd = addDays(valueToAdd, 1); + } + + return chunk(list, 7) as Date[][]; } export function getMinSelectedDate(date: SelectedDate | null | undefined) { @@ -549,11 +577,223 @@ export function formatISODate( return formatISO(date, { representation }); } +export enum DateToken { + /** `1982, 1986, 2024` */ + Year_numeric = 'yyy', + /** `82, 86, 24` */ + Year_2Digit = 'yy', + + /** `January, February, ..., December` */ + Month_long = 'MMMM', + /** `Jan, Feb, ..., Dec` */ + Month_short = 'MMM', + /** `01, 02, ..., 12` */ + Month_2Digit = 'MM', + /** `1, 2, ..., 12` */ + Month_numeric = 'M', + + /** `1, 2, ..., 11, 12` */ + Hour_numeric = 'h', + /** `01, 02, ..., 11, 12` */ + Hour_2Digit = 'hh', + /** You should probably not use this. Force with AM/PM (and the good locale), not specifying this will automatically take the good local */ + Hour_wAMPM = 'a', + /** You should probably not use this. Force without AM/PM (and the good locale), not specifying this will automatically take the good local */ + Hour_woAMPM = 'aaaaaa', + + /** `0, 1, ..., 59` */ + Minute_numeric = 'm', + /** `00, 01, ..., 59` */ + Minute_2Digit = 'mm', + + /** `0, 1, ..., 59` */ + Second_numeric = 's', + /** `00, 01, ..., 59` */ + Second_2Digit = 'ss', + + /** `000, 001, ..., 999` */ + MiliSecond_3 = 'SSS', + + /** Minimize digit: `1, 2, 11, ...` */ + DayOfMonth_numeric = 'd', + /** `01, 02, 11, ...` */ + DayOfMonth_2Digit = 'dd', + /** `1st, 2nd, 11th, ...` You can have your local ordinal by passing `ordinalSuffixes` in options / settings */ + DayOfMonth_withOrdinal = 'do', + + /** `M, T, W, T, F, S, S` */ + DayOfWeek_narrow = 'eeeee', + /** `Monday, Tuesday, ..., Sunday` */ + DayOfWeek_long = 'eeee', + /** `Mon, Tue, Wed, ..., Sun` */ + DayOfWeek_short = 'eee', +} + +export function formatIntl( + dt: Date, + tokens_or_intlOptions: CustomIntlDateTimeFormatOptions, + options: FormatDateOptions = {} +) { + const { locales, ordinalSuffixes } = getFormatDate(options); + + function formatIntlOrdinal(formatter: Intl.DateTimeFormat, with_ordinal = false) { + if (with_ordinal) { + const suffixes = ordinalSuffixes[locales] ?? ordinalSuffixes['en']; + const rules = new Intl.PluralRules(locales, { type: 'ordinal' }); + + const splited = formatter.formatToParts(dt); + return splited + .map((c) => { + if (c.type === 'day') { + const ordinal = rules.select(parseInt(c.value, 10)); + const suffix = suffixes[ordinal]; + return `${c.value}${suffix}`; + } + return c.value; + }) + .join(''); + } + + return formatter.format(dt); + } + + if (typeof tokens_or_intlOptions !== 'string' && !Array.isArray(tokens_or_intlOptions)) { + return formatIntlOrdinal( + new Intl.DateTimeFormat(locales, tokens_or_intlOptions), + tokens_or_intlOptions.withOrdinal + ); + } + + const tokens = Array.isArray(tokens_or_intlOptions) + ? tokens_or_intlOptions.join('') + : tokens_or_intlOptions; + + // Order of includes check is important! (longest first) + const formatter = new Intl.DateTimeFormat(locales, { + year: tokens.includes(DateToken.Year_numeric) + ? 'numeric' + : tokens.includes(DateToken.Year_2Digit) + ? '2-digit' + : undefined, + + month: tokens.includes(DateToken.Month_long) + ? 'long' + : tokens.includes(DateToken.Month_short) + ? 'short' + : tokens.includes(DateToken.Month_2Digit) + ? '2-digit' + : tokens.includes(DateToken.Month_numeric) + ? 'numeric' + : undefined, + + day: tokens.includes(DateToken.DayOfMonth_2Digit) + ? '2-digit' + : tokens.includes(DateToken.DayOfMonth_numeric) + ? 'numeric' + : undefined, + + hour: tokens.includes(DateToken.Hour_2Digit) + ? '2-digit' + : tokens.includes(DateToken.Hour_numeric) + ? 'numeric' + : undefined, + hour12: tokens.includes(DateToken.Hour_woAMPM) + ? false + : tokens.includes(DateToken.Hour_wAMPM) + ? true + : undefined, + + minute: tokens.includes(DateToken.Minute_2Digit) + ? '2-digit' + : tokens.includes(DateToken.Minute_numeric) + ? 'numeric' + : undefined, + + second: tokens.includes(DateToken.Second_2Digit) + ? '2-digit' + : tokens.includes(DateToken.Second_numeric) + ? 'numeric' + : undefined, + + fractionalSecondDigits: tokens.includes(DateToken.MiliSecond_3) ? 3 : undefined, + + weekday: tokens.includes(DateToken.DayOfWeek_narrow) + ? 'narrow' + : tokens.includes(DateToken.DayOfWeek_long) + ? 'long' + : tokens.includes(DateToken.DayOfWeek_short) + ? 'short' + : undefined, + }); + + return formatIntlOrdinal(formatter, tokens.includes(DateToken.DayOfMonth_withOrdinal)); +} + +function range( + date: Date, + weekStartsOn: DayOfWeek, + options: FormatDateOptions, + formats: Record, + variant: DateFormatVariant, + biWeek: undefined | 1 | 2 = undefined // undefined means that it's not a bi-week +) { + const start = + biWeek === undefined + ? startOfWeek(date, { weekStartsOn }) + : startOfBiWeek(date, biWeek, weekStartsOn); + const end = + biWeek === undefined + ? endOfWeek(date, { weekStartsOn }) + : endOfBiWeek(date, biWeek, weekStartsOn); + + const formatToUse = formats[variant]; + + return formatIntl(start, formatToUse, options) + ' - ' + formatIntl(end, formatToUse, options); +} + +export type OrdinalSuffixes = { + one: string; + two: string; + few: string; + other: string; + zero?: string; + many?: string; +}; +export type DateFormatVariant = 'short' | 'default' | 'long' | 'custom'; +type DateFormatVariantPreset = { + short?: CustomIntlDateTimeFormatOptions; + default?: CustomIntlDateTimeFormatOptions; + long?: CustomIntlDateTimeFormatOptions; +}; +export type CustomIntlDateTimeFormatOptions = + | string + | string[] + | (Intl.DateTimeFormatOptions & { withOrdinal?: boolean }); +export type FormatDateOptions = { + locales?: string | undefined; + baseParsing?: string; + weekStartsOn?: DayOfWeek; + variant?: DateFormatVariant; + custom?: CustomIntlDateTimeFormatOptions; + presets?: { + day?: DateFormatVariantPreset; + dayTime?: DateFormatVariantPreset; + timeOnly?: DateFormatVariantPreset; + week?: DateFormatVariantPreset; + month?: DateFormatVariantPreset; + monthsYear?: DateFormatVariantPreset; + year?: DateFormatVariantPreset; + }; + ordinalSuffixes?: Record; +}; + export function formatDate( date: Date | string | null | undefined, periodType?: PeriodType | null | undefined, - variant?: 'short' | 'long' // TODO: Support x-long, etc (maybe call it sm, md, lg, xl, etc) -) { + options: FormatDateOptions = {} +): string { + periodType = periodType ?? undefined; + if (typeof date === 'string') { date = parseISO(date); } @@ -564,49 +804,118 @@ export function formatDate( return ''; } + const { variant, weekStartsOn, custom, presets } = getFormatDate(options); + const { day, dayTime, timeOnly, week, month, monthYear, year } = presets; + + if (periodType === PeriodType.Week) { + periodType = [ + PeriodType.WeekSun, + PeriodType.WeekMon, + PeriodType.WeekTue, + PeriodType.WeekWed, + PeriodType.WeekThu, + PeriodType.WeekFri, + PeriodType.WeekSat, + ][weekStartsOn]; + } else if (periodType === PeriodType.BiWeek1) { + periodType = [ + PeriodType.BiWeek1Sun, + PeriodType.BiWeek1Mon, + PeriodType.BiWeek1Tue, + PeriodType.BiWeek1Wed, + PeriodType.BiWeek1Thu, + PeriodType.BiWeek1Fri, + PeriodType.BiWeek1Sat, + ][weekStartsOn]; + } else if (periodType === PeriodType.BiWeek2) { + periodType = [ + PeriodType.BiWeek2Sun, + PeriodType.BiWeek2Mon, + PeriodType.BiWeek2Tue, + PeriodType.BiWeek2Wed, + PeriodType.BiWeek2Thu, + PeriodType.BiWeek2Fri, + PeriodType.BiWeek2Sat, + ][weekStartsOn]; + } + switch (periodType) { + case PeriodType.Custom: + return formatIntl(date, custom, options); + case PeriodType.Day: - return variant === 'short' ? format(date, 'M/d') : format(date, 'MMM d, yyyy'); + return formatIntl(date, day[variant], options); + + case PeriodType.DayTime: + return formatIntl(date, dayTime[variant], options); + + case PeriodType.TimeOnly: + return formatIntl(date, timeOnly[variant], options); case PeriodType.WeekSun: + return range(date, 0, options, week, variant); case PeriodType.WeekMon: + return range(date, 1, options, week, variant); case PeriodType.WeekTue: + return range(date, 2, options, week, variant); case PeriodType.WeekWed: + return range(date, 3, options, week, variant); case PeriodType.WeekThu: + return range(date, 4, options, week, variant); case PeriodType.WeekFri: + return range(date, 5, options, week, variant); case PeriodType.WeekSat: - return variant === 'short' - ? format(date, 'M/d') + ' - ' + format(addDays(date, 6), 'M/d') - : format(date, 'M/d/yyyy') + ' - ' + format(addDays(date, 6), 'M/d/yyyy'); + return range(date, 6, options, week, variant); case PeriodType.Month: - return variant === 'short' ? format(date, 'MMM yyyy') : format(date, 'MMMM yyyy'); + return formatIntl(date, month[variant], options); + + case PeriodType.MonthYear: + return formatIntl(date, monthYear[variant], options); + case PeriodType.Quarter: - return variant === 'short' - ? format(date, 'MMM') + ' - ' + format(addMonths(date, 2), 'MMM yyyy') - : format(date, 'MMMM') + ' - ' + format(addMonths(date, 2), 'MMMM yyyy'); + return [ + formatIntl(startOfQuarter(date), month[variant], options), + formatIntl(endOfQuarter(date), monthYear[variant], options), + ].join(' - '); + case PeriodType.CalendarYear: - return variant === 'short' ? format(date, 'yy') : format(date, 'yyyy'); + return formatIntl(date, year[variant], options); + case PeriodType.FiscalYearOctober: - return variant === 'short' ? `${getFiscalYear(date)}`.substring(2) : `${getFiscalYear(date)}`; + const fDate = new Date(getFiscalYear(date), 0, 1); + return formatIntl(fDate, year[variant], options); case PeriodType.BiWeek1Sun: + return range(date, 0, options, week, variant, 1); case PeriodType.BiWeek1Mon: + return range(date, 1, options, week, variant, 1); case PeriodType.BiWeek1Tue: + return range(date, 2, options, week, variant, 1); case PeriodType.BiWeek1Wed: + return range(date, 3, options, week, variant, 1); case PeriodType.BiWeek1Thu: + return range(date, 4, options, week, variant, 1); case PeriodType.BiWeek1Fri: + return range(date, 5, options, week, variant, 1); case PeriodType.BiWeek1Sat: + return range(date, 6, options, week, variant, 1); + case PeriodType.BiWeek2Sun: + return range(date, 0, options, week, variant, 2); case PeriodType.BiWeek2Mon: + return range(date, 1, options, week, variant, 2); case PeriodType.BiWeek2Tue: + return range(date, 2, options, week, variant, 2); case PeriodType.BiWeek2Wed: + return range(date, 3, options, week, variant, 2); case PeriodType.BiWeek2Thu: + return range(date, 4, options, week, variant, 2); case PeriodType.BiWeek2Fri: + return range(date, 5, options, week, variant, 2); case PeriodType.BiWeek2Sat: - return variant === 'short' - ? format(date, 'M/d') + ' - ' + format(addDays(date, 13), 'M/d') - : format(date, 'M/d/yyyy') + ' - ' + format(addDays(date, 13), 'M/d/yyyy'); + return range(date, 6, options, week, variant, 2); + default: return formatISO(date); } @@ -620,9 +929,6 @@ export function utcToLocalDate(date: Date | string | null | undefined) { // https://github.com/date-fns/date-fns/issues/376#issuecomment-454163253 // return new Date(date.getTime() + date.getTimezoneOffset() * 60 * 1000); - - // This approach seems to work more reliably with dates before 11/18/1883 @ 12:00 - // https://github.com/d3/d3-time/issues/29#issuecomment-396415951 const d = new Date( date.getUTCFullYear(), date.getUTCMonth(), diff --git a/packages/svelte-ux/src/lib/utils/dateDisplay.ts b/packages/svelte-ux/src/lib/utils/dateDisplay.ts deleted file mode 100644 index 6ca714f8b..000000000 --- a/packages/svelte-ux/src/lib/utils/dateDisplay.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { format as dateFormat } from 'date-fns'; -import { formatDate, utcToLocalDate, PeriodType } from './date'; - -export type DateDisplayOptions = { - periodType?: PeriodType | null; - variant?: Parameters[2]; - format?: string; - utc?: boolean; -}; - -export function dateDisplay( - value: Date | string | number | null | undefined, - options?: DateDisplayOptions -) { - let date = value != null ? (value instanceof Date ? value : new Date(value)) : null; - - // Offset for UTC - if (options?.utc) { - date = utcToLocalDate(date); - } - - let formattedDate = ''; - if (date) { - if (options?.format) { - formattedDate = dateFormat(date, options?.format); - } else if (options?.periodType) { - formattedDate = formatDate(date, options?.periodType, options?.variant); - } else { - formattedDate = date.toLocaleString(); - } - } - - return formattedDate; -} diff --git a/packages/svelte-ux/src/lib/utils/dictionary.ts b/packages/svelte-ux/src/lib/utils/dictionary.ts new file mode 100644 index 000000000..ad59883e0 --- /dev/null +++ b/packages/svelte-ux/src/lib/utils/dictionary.ts @@ -0,0 +1,20 @@ +export type DictionaryMessagesOptions = { + Ok?: string; + Cancel?: string; + + Date?: { + Day?: string; + Week?: string; + BiWeek?: string; + Month?: string; + Quarter?: string; + CalendarYear?: string; + FiscalYearOct?: string; + }; +}; + +type DeepRequired = Required<{ + [K in keyof T]: T[K] extends Required ? Required : DeepRequired; +}>; + +export type DictionaryMessages = DeepRequired; diff --git a/packages/svelte-ux/src/lib/utils/format.ts b/packages/svelte-ux/src/lib/utils/format.ts index e066c986c..6a2c2a157 100644 --- a/packages/svelte-ux/src/lib/utils/format.ts +++ b/packages/svelte-ux/src/lib/utils/format.ts @@ -1,6 +1,6 @@ import { isFunction } from 'lodash-es'; -import { formatDate, PeriodType } from './date'; +import { formatDate, PeriodType, type FormatDateOptions } from './date'; import { formatNumber } from './number'; import type { FormatNumberOptions, FormatNumberStyle } from './number'; @@ -14,15 +14,19 @@ export type FormatType = */ export function format( value: null | undefined, - format?: FormatNumberStyle, - extraFuncArgs?: FormatNumberOptions + format?: FormatNumberStyle | PeriodType, + extraFuncArgs?: FormatNumberOptions | FormatDateOptions ): string; export function format( value: number, format?: FormatNumberStyle, extraFuncArgs?: FormatNumberOptions ): string; -export function format(value: string | Date, format?: PeriodType, ...extraFuncArgs: any[]): string; +export function format( + value: string | Date, + format?: PeriodType, + extraFuncArgs?: FormatDateOptions +): string; export function format(value: any, format?: FormatType, ...extraFuncArgs: any[]): any { let formattedValue = value ?? ''; // Do not render `null` @@ -30,7 +34,11 @@ export function format(value: any, format?: FormatType, ...extraFuncArgs: any[]) if (isFunction(format)) { formattedValue = format(value, ...extraFuncArgs); } else if (format in PeriodType) { - formattedValue = formatDate(value, format as PeriodType, ...extraFuncArgs); + formattedValue = formatDate( + value, + format as PeriodType, + extraFuncArgs.length > 0 ? extraFuncArgs[0] : undefined + ); } else if (typeof value === 'number') { formattedValue = formatNumber(value, { style: format, diff --git a/packages/svelte-ux/src/lib/utils/index.ts b/packages/svelte-ux/src/lib/utils/index.ts index 991bf1d6a..6429b6577 100644 --- a/packages/svelte-ux/src/lib/utils/index.ts +++ b/packages/svelte-ux/src/lib/utils/index.ts @@ -1,6 +1,5 @@ // top-level exports -export { formatDate, PeriodType } from './date'; -export * from './dateDisplay'; +export { formatDate, PeriodType, DayOfWeek, DateToken } from './date'; export * from './duration'; export * from './file'; export * from './format'; diff --git a/packages/svelte-ux/src/lib/utils/number.ts b/packages/svelte-ux/src/lib/utils/number.ts index e2be0ec1d..a91b026f1 100644 --- a/packages/svelte-ux/src/lib/utils/number.ts +++ b/packages/svelte-ux/src/lib/utils/number.ts @@ -1,4 +1,4 @@ -import { getFormatNumberOptions } from '$lib/components/settings'; +import { getFormatNumber } from '$lib/components/settings'; export type FormatNumberStyle = | 'decimal' // from Intl.NumberFormat options.style NumberFormatOptions @@ -32,7 +32,7 @@ export function formatNumber(number: number | null | undefined, options: FormatN return `${number}`; } - const defaults = getFormatNumberOptions(options.style); + const defaults = getFormatNumber(options.style); const formatter = Intl.NumberFormat(options.locales ?? defaults.locales ?? undefined, { // Let's always starts with all defaults diff --git a/packages/svelte-ux/src/routes/+layout.svelte b/packages/svelte-ux/src/routes/+layout.svelte index 4c102bbb6..40b717317 100644 --- a/packages/svelte-ux/src/routes/+layout.svelte +++ b/packages/svelte-ux/src/routes/+layout.svelte @@ -17,6 +17,7 @@ import { settings } from '$lib'; import type { PageData } from './$types'; + import { DateToken } from '$lib/utils/date'; export let data: PageData; @@ -26,6 +27,7 @@ $: title = data.pr_id ? `🚧 (pr:${data.pr_id}) - ${baseTitle}` : baseTitle; settings({ + // Usefull to test different locales with the docs // formats: { // numbers: { // defaults: { @@ -33,6 +35,32 @@ // currency: 'EUR', // }, // }, + // dates: { + // locales: 'fr', + // weekStartsOn: 1, + // presets: { + // days: { + // long: { dateStyle: 'full' }, + // }, + // months: { + // default: [DateToken.Month_long], + // }, + // }, + // ordinalSuffixes: { + // fr: { + // one: 'er', + // two: '', + // few: '', + // other: '', + // }, + // }, + // }, + // }, + // dictionary: { + // Cancel: 'Annuler', + // Date: { + // Day: 'Jour', + // }, // }, // theme: { // AppBar: 'bg-red-500 text-white shadow-md', diff --git a/packages/svelte-ux/src/routes/+page.md b/packages/svelte-ux/src/routes/+page.md index a433e7ef9..2fc78b552 100644 --- a/packages/svelte-ux/src/routes/+page.md +++ b/packages/svelte-ux/src/routes/+page.md @@ -110,12 +110,4 @@ Using `components`, `actions`, or `stores` is as simple as importing from `svelt ``` -Currently, `utils` are not exposed as top-level exports to not polute the namespace, although this may change in the future. For now, you can import them using the full path. - -```js -import { dateDisplay } from 'svelte-ux/utils/dateDisplay'; -``` - -See each component page for detailed usage examples. - diff --git a/packages/svelte-ux/src/routes/_NavMenu.svelte b/packages/svelte-ux/src/routes/_NavMenu.svelte index 8104a66d6..1636ffa41 100644 --- a/packages/svelte-ux/src/routes/_NavMenu.svelte +++ b/packages/svelte-ux/src/routes/_NavMenu.svelte @@ -52,7 +52,6 @@ ], Feedback: ['Badge', 'Progress', 'ProgressCircle'], Date: [ - 'dateDisplay', 'DateField', 'DatePickerField', 'DateRange', diff --git a/packages/svelte-ux/src/routes/customization/+page.md b/packages/svelte-ux/src/routes/customization/+page.md index 4cb1e15f9..c20c68d58 100644 --- a/packages/svelte-ux/src/routes/customization/+page.md +++ b/packages/svelte-ux/src/routes/customization/+page.md @@ -80,6 +80,39 @@ settings({ fractionDigits: 4, }, }, + + dates: { + // This is the default, but you can override it here for your app + locales: 'en', + weekStartsOn: DayOfWeek.Sunday, + + presets: { + day: { + long: { dateStyle: 'full' }, + }, + month: { + default: [DateToken.Month_long], + }, + }, + + ordinalSuffixes: { + en: { + one: 'st', + two: 'nd', + few: 'rd', + }, + }, + + dico: { + Day: 'Day', + Week: 'Week', + BiWeek: 'Bi-Week', + Month: 'Month', + Quarter: 'Quarter', + CalendarYear: 'Calendar Year', + FiscalYearOct: 'Fiscal Year (Oct)', + }, + }, }, }); ``` diff --git a/packages/svelte-ux/src/routes/docs/components/dateDisplay/+page.svelte b/packages/svelte-ux/src/routes/docs/components/dateDisplay/+page.svelte deleted file mode 100644 index f151cfe41..000000000 --- a/packages/svelte-ux/src/routes/docs/components/dateDisplay/+page.svelte +++ /dev/null @@ -1,139 +0,0 @@ - - -

Examples

- -

No format

- - - {dateDisplay(new Date('1982-03-30T00:00:00'))} - - -

Custom format

- - - {dateDisplay(new Date('1982-03-30T00:00:00'), { format: 'EEE, MMMM do' })} - - -

PeriodType Day w/ long (default)

- - - {dateDisplay(new Date('1982-03-30T00:00:00'), { - periodType: PeriodType.Day, - })} - - -

short

- - - {dateDisplay(new Date('1982-03-30T00:00:00'), { - periodType: PeriodType.Day, - variant: 'short', - })} - - -

PeriodType WeekSun w/ long (default)

- - - {dateDisplay(new Date('1982-03-30T00:00:00'), { - periodType: PeriodType.WeekSun, - })} - - -

short

- - - {dateDisplay(new Date('1982-03-30T00:00:00'), { - periodType: PeriodType.WeekSun, - variant: 'short', - })} - - -

PeriodType BiWeek1Sun w/ long (default)

- - - {dateDisplay(new Date('1982-03-30T00:00:00'), { - periodType: PeriodType.BiWeek1Sun, - })} - - -

short

- - - {dateDisplay(new Date('1982-03-30T00:00:00'), { - periodType: PeriodType.BiWeek1Sun, - variant: 'short', - })} - - -

PeriodType Month w/ long (default)

- - - {dateDisplay(new Date('1982-03-30T00:00:00'), { - periodType: PeriodType.Month, - })} - - -

short

- - - {dateDisplay(new Date('1982-03-30T00:00:00'), { - periodType: PeriodType.Month, - variant: 'short', - })} - - -

PeriodType Quarter w/ long (default)

- - - {dateDisplay(new Date('1982-03-30T00:00:00'), { - periodType: PeriodType.Quarter, - })} - - -

short

- - - {dateDisplay(new Date('1982-03-30T00:00:00'), { - periodType: PeriodType.Quarter, - variant: 'short', - })} - - -

PeriodType CalendarYear w/ long (default)

- - - {dateDisplay(new Date('1982-03-30T00:00:00'), { - periodType: PeriodType.CalendarYear, - })} - - -

short

- - - {dateDisplay(new Date('1982-03-30T00:00:00'), { - periodType: PeriodType.CalendarYear, - variant: 'short', - })} - - -

PeriodType FiscalYearOctober w/ long (default)

- - - {dateDisplay(new Date('1982-03-30T00:00:00'), { - periodType: PeriodType.FiscalYearOctober, - })} - - -

short

- - - {dateDisplay(new Date('1982-03-30T00:00:00'), { - periodType: PeriodType.FiscalYearOctober, - variant: 'short', - })} - diff --git a/packages/svelte-ux/src/routes/docs/components/dateDisplay/+page.ts b/packages/svelte-ux/src/routes/docs/components/dateDisplay/+page.ts deleted file mode 100644 index 6d4abe1dd..000000000 --- a/packages/svelte-ux/src/routes/docs/components/dateDisplay/+page.ts +++ /dev/null @@ -1,16 +0,0 @@ -import source from '$lib/utils/dateDisplay.ts?raw'; -import pageSource from './+page.svelte?raw'; - -export async function load() { - return { - meta: { - source, - pageSource, - features: [ - 'Pass `periodType` along with `variant` for quick formatting', - 'Pass `format` for greater control', - 'By default, will be formatted using `date.toLocaleString()`', - ], - }, - }; -} diff --git a/packages/svelte-ux/src/routes/docs/utils/format/+page.svelte b/packages/svelte-ux/src/routes/docs/utils/format/+page.svelte index e1839e59e..d5379abe3 100644 --- a/packages/svelte-ux/src/routes/docs/utils/format/+page.svelte +++ b/packages/svelte-ux/src/routes/docs/utils/format/+page.svelte @@ -1,30 +1,37 @@

Usage

-

Examples

- -

Playground

+

Playgrounds

+

Playground numbers

- + + + ({ label: value, value }))} + /> - ({ label: value, value }))} - /> - {format(value, style, { locales, currency })}
+

Playground dates

+ +
+ + + ({ label: value, value }))} + /> +
+ + +
{format(myDate, PeriodType.Day, { locales })}
+
+ +

Numbers

+

number formats (defaut settings)

@@ -83,13 +102,323 @@
{format(0.5678, 'percent', { locales: 'fr', fractionDigits: 1 })}
-

Period formats

+

Dates

+ +

Custom format

+ +
+
+

With random string

+ + {format(myDate, PeriodType.Custom, { + custom: 'eee, MMMM do', + })} + +
+
+

With descriptive tokens

+ + {format(myDate, PeriodType.Custom, { + custom: [DateToken.DayOfWeek_short, DateToken.Month_long, DateToken.DayOfMonth_withOrdinal], + })} + +
+
+

With full intl

+ + {format(myDate, PeriodType.Custom, { + custom: { weekday: 'short', month: 'long', day: 'numeric', withOrdinal: true }, + })} + +
+
+ +

PeriodType Day

+ +
+
+

short

+ + {format(myDate, PeriodType.Day, { + variant: 'short', + })} + +
+
+

default

+ + {format(myDate, PeriodType.Day, { + // variant: 'default', + })} + +
+
+

long

+ + {format(myDate, PeriodType.Day, { + variant: 'long', + })} + +
+
+ +

PeriodType DayTime

+ +
+
+

short

+ + {format(myDate, PeriodType.DayTime, { + variant: 'short', + })} + +
+
+

default

+ + {format(myDate, PeriodType.DayTime, { + // variant: 'default', + })} + +
+
+

long

+ + {format(myDate, PeriodType.DayTime, { + variant: 'long', + })} + +
+
+ +

PeriodType TimeOnly

+ +
+
+

short

+ + {format(myDate, PeriodType.TimeOnly, { + variant: 'short', + })} + +
+
+

default

+ + {format(myDate, PeriodType.TimeOnly, { + // variant: 'default', + })} + +
+
+

long

+ + {format(myDate, PeriodType.TimeOnly, { + variant: 'long', + })} + +
+
+ +

PeriodType Week

+ + It will take your default weekStartsOn + settings, if you want to be + specific, you can also use + PeriodType.WeekSun + +
+
+

short

+ + {format(myDate, PeriodType.Week, { + variant: 'short', + })} + +
+
+

default

+ + {format(myDate, PeriodType.Week, { + // variant: 'default', + })} + +
+
+

long

+ + {format(myDate, PeriodType.Week, { + variant: 'long', + })} + +
+
+ +

PeriodType BiWeek1

+ + It will take your default weekStartsOn + settings, if you want to be + specific, you can also use + PeriodType.BiWeek1Sun + +
+
+

short

+ + {format(myDate, PeriodType.BiWeek1, { + variant: 'short', + })} + +
+
+

default

+ + {format(myDate, PeriodType.BiWeek1, { + // variant: 'default', + })} + +
+
+

long

+ + {format(myDate, PeriodType.BiWeek1, { + variant: 'long', + })} + +
+
+ +

PeriodType Month

+ +
+
+

short

+ + {format(myDate, PeriodType.Month, { + variant: 'short', + })} + +
+
+

default

+ + {format(myDate, PeriodType.Month, { + // variant: 'default', + })} + +
+
+

long

+ + {format(myDate, PeriodType.Month, { + variant: 'long', + })} + +
+
+ +

PeriodType Quarter

+ +
+
+

short

+ + {format(myDate, PeriodType.Quarter, { + variant: 'short', + })} + +
+
+

default

+ + {format(myDate, PeriodType.Quarter, { + // variant: 'default', + })} + +
+
+

long

+ + {format(myDate, PeriodType.Quarter, { + variant: 'long', + })} + +
+
+ +

PeriodType CalendarYear

+ +
+
+

short

+ + {format(myDate, PeriodType.CalendarYear, { + variant: 'short', + })} + +
+
+

default

+ + {format(myDate, PeriodType.CalendarYear, { + // variant: 'default', + })} + +
+
+

long

+ + {format(myDate, PeriodType.CalendarYear, { + variant: 'long', + })} + +
+
+ +

PeriodType FiscalYearOctober

+ +
+
+

short

+ + {format(myDate, PeriodType.FiscalYearOctober, { + variant: 'short', + })} + +
+
+

default

+ + {format(myDate, PeriodType.FiscalYearOctober, { + // variant: 'default', + })} + +
+
+

long

+ + {format(myDate, PeriodType.FiscalYearOctober, { + variant: 'long', + })} + +
+
+ +

Date / Period formats (local settings)

+ + + You can customize numbers with the 3rd arg. You can pass for example locales like fr, + de, ... You can also to that globally in the + Settings. + -
{format(date, PeriodType.Day)}
-
{format(date, PeriodType.Month)}
-
{format(date, PeriodType.CalendarYear)}
-
{format(date, PeriodType.Day, 'short')}
-
{format(date, PeriodType.Month, 'short')}
-
{format(date, PeriodType.CalendarYear, 'short')}
+
{format(myDate, PeriodType.Day, { locales: 'fr' })}
+
{format(myDate, PeriodType.Month, { locales: 'fr' })}
+
{format(myDate, PeriodType.CalendarYear, { locales: 'fr' })}
+
{format(myDate, PeriodType.Day, { variant: 'short', locales: 'fr' })}
+
{format(myDate, PeriodType.Month, { variant: 'short', locales: 'fr' })}
+
+ {format(myDate, PeriodType.CalendarYear, { variant: 'short', locales: 'fr' })} +
diff --git a/packages/svelte-ux/src/routes/docs/utils/format/+page.ts b/packages/svelte-ux/src/routes/docs/utils/format/+page.ts index 74db8cf11..7c3cef134 100644 --- a/packages/svelte-ux/src/routes/docs/utils/format/+page.ts +++ b/packages/svelte-ux/src/routes/docs/utils/format/+page.ts @@ -6,7 +6,12 @@ export async function load() { meta: { source, pageSource, - description: 'Easily format numbers and dates to a variety of formats', + description: 'Easily format numbers and dates to a variety of formats and locales', + features: [ + 'Number: Pass `style` for quick formatting', + 'Date: Pass `periodType` along with `variant` for quick formatting', + 'Date: Pass `custom` for greater control', + ], }, }; } diff --git a/packages/svelte-ux/vite.config.js b/packages/svelte-ux/vite.config.js index 97ce538e7..578e7fdd5 100644 --- a/packages/svelte-ux/vite.config.js +++ b/packages/svelte-ux/vite.config.js @@ -6,5 +6,8 @@ export default defineConfig({ plugins: [sveltekit(), sveld()], test: { include: ['src/**/*.{test,spec}.{js,ts}'], + coverage: { + reporter: ['html'], + }, }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 59820acc7..82d550621 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -66,12 +66,9 @@ importers: d3-scale: specifier: ^4.0.2 version: 4.0.2 - d3-time: - specifier: ^3.1.0 - version: 3.1.0 date-fns: - specifier: ^2.30.0 - version: 2.30.0 + specifier: ^3.0.5 + version: 3.0.5 immer: specifier: ^10.0.3 version: 10.0.3 @@ -80,7 +77,7 @@ importers: version: 4.17.21 posthog-js: specifier: ^1.95.1 - version: 1.95.1 + version: 1.96.1 prism-svelte: specifier: ^0.5.0 version: 0.5.0 @@ -127,6 +124,9 @@ importers: '@types/prismjs': specifier: ^1.26.3 version: 1.26.3 + '@vitest/coverage-v8': + specifier: ^0.34.6 + version: 0.34.6(vitest@0.33.0) autoprefixer: specifier: ^10.4.16 version: 10.4.16(postcss@8.4.31) @@ -229,6 +229,10 @@ packages: dependencies: regenerator-runtime: 0.14.0 + /@bcoe/v8-coverage@0.2.3: + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + dev: true + /@changesets/apply-release-plan@6.1.4: resolution: {integrity: sha512-FMpKF1fRlJyCZVYHr3CbinpZZ+6MwvOtWUuO8uo+svcATEoc1zRDcj23pAurJ2TZ/uVz1wFHH6K3NlACy0PLew==} dependencies: @@ -689,6 +693,11 @@ packages: wrap-ansi-cjs: /wrap-ansi@7.0.0 dev: true + /@istanbuljs/schema@0.1.3: + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + dev: true + /@jest/schemas@29.6.3: resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -984,6 +993,10 @@ packages: dependencies: ci-info: 3.9.0 + /@types/istanbul-lib-coverage@2.0.6: + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + dev: true + /@types/lodash-es@4.17.11: resolution: {integrity: sha512-eCw8FYAWHt2DDl77s+AMLLzPn310LKohruumpucZI4oOFJkIgnlaJcy23OKMJxx4r9PeTF13Gv6w+jqjWQaYUg==} dependencies: @@ -1040,6 +1053,27 @@ packages: /@types/unist@3.0.2: resolution: {integrity: sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==} + /@vitest/coverage-v8@0.34.6(vitest@0.33.0): + resolution: {integrity: sha512-fivy/OK2d/EsJFoEoxHFEnNGTg+MmdZBAVK9Ka4qhXR2K3J0DS08vcGVwzDtXSuUMabLv4KtPcpSKkcMXFDViw==} + peerDependencies: + vitest: '>=0.32.0 <1' + dependencies: + '@ampproject/remapping': 2.2.1 + '@bcoe/v8-coverage': 0.2.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.6 + magic-string: 0.30.5 + picocolors: 1.0.0 + std-env: 3.4.3 + test-exclude: 6.0.0 + v8-to-istanbul: 9.1.3 + vitest: 0.33.0 + transitivePeerDependencies: + - supports-color + dev: true + /@vitest/expect@0.33.0: resolution: {integrity: sha512-sVNf+Gla3mhTCxNJx+wJLDPp/WcstOe0Ksqz4Vec51MmgMth/ia0MGFEkIZmVGeTL5HtjYR4Wl/ZxBxBXZJTzQ==} dependencies: @@ -1450,6 +1484,10 @@ packages: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: true + /convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + dev: true + /cookie@0.5.0: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} engines: {node: '>= 0.6'} @@ -1556,11 +1594,8 @@ packages: resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} dev: true - /date-fns@2.30.0: - resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} - engines: {node: '>=0.11'} - dependencies: - '@babel/runtime': 7.23.2 + /date-fns@3.0.5: + resolution: {integrity: sha512-Q4Tq5c5s/Zl/zbgdWf6pejn9ru7UwdIlLfvEEg1hVsQNQ7LKt76qIduagIT9OPK7+JCv1mAKherdU6bOqGYDnw==} dev: false /debug@4.3.4: @@ -2118,6 +2153,10 @@ packages: /hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + /html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + dev: true + /human-id@1.0.2: resolution: {integrity: sha512-UNopramDEhHJD+VR+ehk8rOslwSfByxPIZyJRfV739NDhN5LF1fa1MqnzKm2lGTQRjNrjK19Q5fhkgIfjlVUKw==} @@ -2335,6 +2374,39 @@ packages: /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + /istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + dev: true + + /istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + dev: true + + /istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + dependencies: + debug: 4.3.4 + istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + dev: true + + /istanbul-reports@3.1.6: + resolution: {integrity: sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==} + engines: {node: '>=8'} + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + dev: true + /jackspeak@2.3.6: resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} engines: {node: '>=14'} @@ -2490,6 +2562,13 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true + /make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + dependencies: + semver: 7.5.4 + dev: true + /map-obj@1.0.1: resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} engines: {node: '>=0.10.0'} @@ -2969,8 +3048,8 @@ packages: source-map-js: 1.0.2 dev: true - /posthog-js@1.95.1: - resolution: {integrity: sha512-79HPLoBqENBEEGFhn+hueKliYH66Qbu4WcRTEd8WaqtvqHrK9qAQkcrShZNkg1V5vM4kHp0iMIkJYBXg1sq06Q==} + /posthog-js@1.96.1: + resolution: {integrity: sha512-kv1vQqYMt2BV3YHS+wxsbGuP+tz+M3y1AzNhz8TfkpY1HT8W/ONT0i0eQpeRr9Y+d4x/fZ6M4cXG5GMvi9lRCA==} dependencies: fflate: 0.4.8 dev: false @@ -3357,6 +3436,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: true + /spawndamnit@2.0.0: resolution: {integrity: sha512-j4JKEcncSjFlqIwU5L/rp2N5SIPsdxaRsIv678+TZxZ0SRDJTm8JrxJMjE/XuiEZNEir3S8l0Fa3Ke339WI4qA==} dependencies: @@ -3682,6 +3766,15 @@ packages: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} + /test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + dev: true + /thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -3914,6 +4007,15 @@ packages: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: true + /v8-to-istanbul@9.1.3: + resolution: {integrity: sha512-9lDD+EVI2fjFsMWXc6dy5JJzBsVTcQ2fVkfBvncZ6xJWG9wtBhOldG+mHkSL0+V1K/xgZz0JDO5UT5hFwHUghg==} + engines: {node: '>=10.12.0'} + dependencies: + '@jridgewell/trace-mapping': 0.3.20 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + dev: true + /validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} dependencies: