Repository: melt-ui/melt-ui (ts)

Issue:
🐛Bug: Calendar builder generates duplicate dates on some months when fixedWeeks is set to true
**Describe the bug**
Hello, 

I've encountered this bug on the calendar builder (while using Bits UI which implements Melt UI) where the dates are being duplicated on some months (e.g. January 2024, September 2021). I've attached some screenshots outlining the issue (2 from Melt UI, one from Bits UI).

**To Reproduce**
Steps to reproduce the behavior:
1. Go to [https://melt-ui.com/docs/builders/calendar](https://melt-ui.com/docs/builders/calendar)
3. Scroll down to Fixed Weeks
4. See error (as outlined in the screenshots)

**Expected behavior**
Dates should not be duplicated

**Screenshots**
<img width="569" alt="Screenshot 2024-01-01 at 19 22 07" src="https://github.com/melt-ui/melt-ui/assets/17836403/ed419f8e-9e0e-4671-b3f2-794d9aad2d1f">
<img width="1205" alt="Screenshot 2024-01-01 at 10 12 54" src="https://github.com/melt-ui/melt-ui/assets/17836403/b41631d0-5e0b-407a-9e56-5e060b9d97e1">
<img width="1210" alt="Screenshot 2024-01-01 at 10 14 18" src="https://github.com/melt-ui/melt-ui/assets/17836403/95c318ea-d3cc-4422-b547-1e84c2bb0e55">

**Desktop (please complete the following information):**
 - OS: MacOS 14.2.1
 - Browser: Arc
 - Version: 1.21.2

**Additional context**
I'll also open a pull request fixing this issue, plus a couple of tests on the createMonths function to check for this particular bug.


Pull request:
Fix(calendar): extraDays array should add days starting from 1
Fixes #868.

I've updated the extraDaysArray variable to add days starting from 1, not 0 which was causing the duplication outlined in the issue and I've added some tests for this particular edge case.

I've also added some defaults for some DatePicker tests which were breaking when I first forked the repository without making any modifications in the code.

Let me know if I need to make any other changes. Thanks!

Patch:
diff --git a/.changeset/loud-readers-count.md b/.changeset/loud-readers-count.md
new file mode 100644
index 00000000..6f17b0eb
--- /dev/null
+++ b/.changeset/loud-readers-count.md
@@ -0,0 +1,5 @@
+---
+"@melt-ui/svelte": patch
+---
+
+chore: upgrade nanoid to v5
diff --git a/.changeset/quiet-ads-allow.md b/.changeset/quiet-ads-allow.md
new file mode 100644
index 00000000..e6cd4a02
--- /dev/null
+++ b/.changeset/quiet-ads-allow.md
@@ -0,0 +1,5 @@
+---
+"@melt-ui/svelte": patch
+---
+
+Fix(calendar): extraDays array should add days starting from 1
diff --git a/.changeset/tricky-cougars-burn.md b/.changeset/tricky-cougars-burn.md
new file mode 100644
index 00000000..f8561883
--- /dev/null
+++ b/.changeset/tricky-cougars-burn.md
@@ -0,0 +1,5 @@
+---
+"@melt-ui/svelte": patch
+---
+
+Progress: allow `null` value for indeterminate state
diff --git a/package.json b/package.json
index 8dac44af..162ae406 100644
--- a/package.json
+++ b/package.json
@@ -67,7 +67,7 @@
 		"@internationalized/date": "^3.5.0",
 		"dequal": "^2.0.3",
 		"focus-trap": "^7.5.2",
-		"nanoid": "^4.0.2"
+		"nanoid": "^5.0.4"
 	},
 	"devDependencies": {
 		"@changesets/cli": "^2.26.2",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d492c258..29bba330 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -21,8 +21,8 @@ dependencies:
     specifier: ^7.5.2
     version: 7.5.2
   nanoid:
-    specifier: ^4.0.2
-    version: 4.0.2
+    specifier: ^5.0.4
+    version: 5.0.4
 
 devDependencies:
   '@changesets/cli':
@@ -45,7 +45,7 @@ devDependencies:
     version: 1.1.57
   '@melt-ui/pp':
     specifier: 0.1.0
-    version: 0.1.0(@melt-ui/svelte@0.63.1)(svelte@4.0.0)
+    version: 0.1.0(@melt-ui/svelte@0.66.2)(svelte@4.0.0)
   '@playwright/test':
     specifier: ^1.39.0
     version: 1.39.0
@@ -2389,6 +2389,12 @@ packages:
       '@floating-ui/utils': 0.1.6
     dev: true
 
+  /@floating-ui/core@1.5.2:
+    resolution: {integrity: sha512-Ii3MrfY/GAIN3OhXNzpCKaLxHQfJF9qvwq/kEJYdqDxeIHa01K8sldugal6TmeeXl+WMvhv9cnVzUTaFFJF09A==}
+    dependencies:
+      '@floating-ui/utils': 0.1.6
+    dev: true
+
   /@floating-ui/dom@1.4.5:
     resolution: {integrity: sha512-96KnRWkRnuBSSFbj0sFGwwOUd8EkiecINVl0O9wiZlZ64EkpyAOG3Xc2vKKNJmru0Z7RqWNymA+6b8OZqjgyyw==}
     dependencies:
@@ -2496,6 +2502,13 @@ packages:
     resolution: {integrity: sha512-nw0Q+oRkizBWMioseI8+2TeUPEyopJVz5YxoYVzR0W1v+2YytiYah7s/ot35F149q/xAg4F1gT/6eTd+tsUpFQ==}
     dependencies:
       '@swc/helpers': 0.5.3
+    dev: false
+
+  /@internationalized/date@3.5.1:
+    resolution: {integrity: sha512-LUQIfwU9e+Fmutc/DpRTGXSdgYZLBegi4wygCWDSVmUdLTaMHsQyASDiJtREwanwKuQLq0hY76fCJ9J/9I2xOQ==}
+    dependencies:
+      '@swc/helpers': 0.5.3
+    dev: true
 
   /@isaacs/cliui@8.0.2:
     resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
@@ -2690,25 +2703,25 @@ packages:
       react: 18.2.0
     dev: true
 
-  /@melt-ui/pp@0.1.0(@melt-ui/svelte@0.63.1)(svelte@4.0.0):
+  /@melt-ui/pp@0.1.0(@melt-ui/svelte@0.66.2)(svelte@4.0.0):
     resolution: {integrity: sha512-GbzqhLDjfxh3BwC0It7kVUHvx05f+IdBad+mXWMO76xgfdqBkAO/pS6Q+tAEntKglQvxXugV3gHAyEJgSI0AlA==}
     engines: {pnpm: '>=8.6.3'}
     peerDependencies:
       '@melt-ui/svelte': '>= 0.29.0'
       svelte: ^3.55.0 || ^4.0.0
     dependencies:
-      '@melt-ui/svelte': 0.63.1(svelte@4.0.0)
+      '@melt-ui/svelte': 0.66.2(svelte@4.0.0)
       svelte: 4.0.0
     dev: true
 
-  /@melt-ui/svelte@0.63.1(svelte@4.0.0):
-    resolution: {integrity: sha512-WzuI4uePPsHMlk1aq95UAgb32gXYRjBSi8DmXApWuM9CpJgcvUt2YcQ5+BPSOyrDOWVvPJd9gL7QA0NPDSCtNw==}
+  /@melt-ui/svelte@0.66.2(svelte@4.0.0):
+    resolution: {integrity: sha512-ufIhgOYP11A/G3AvW+2Qw74UGudMBJQ2wK+sETpU51VkC63/5D2sctKgXzGl0OUEJBlPHku6LRSry9rIeAVkNw==}
     peerDependencies:
       svelte: '>=3 <5'
     dependencies:
-      '@floating-ui/core': 1.5.0
+      '@floating-ui/core': 1.5.2
       '@floating-ui/dom': 1.5.3
-      '@internationalized/date': 3.5.0
+      '@internationalized/date': 3.5.1
       dequal: 2.0.3
       focus-trap: 7.5.4
       nanoid: 4.0.2
@@ -4783,12 +4796,12 @@ packages:
   /@types/eslint-scope@3.7.7:
     resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==}
     dependencies:
-      '@types/eslint': 8.44.8
+      '@types/eslint': 8.56.0
       '@types/estree': 1.0.5
     dev: true
 
-  /@types/eslint@8.44.8:
-    resolution: {integrity: sha512-4K8GavROwhrYl2QXDXm0Rv9epkA8GBFu0EI+XrrnnuCl7u8CWBRusX7fXJfanhZTDWSAL24gDI/UqXyUM0Injw==}
+  /@types/eslint@8.56.0:
+    resolution: {integrity: sha512-FlsN0p4FhuYRjIxpbdXovvHQhtlG05O1GG/RNWvdAxTboR438IOTwmrY/vLA+Xfgg06BTkP045M3vpFwTMv1dg==}
     dependencies:
       '@types/estree': 1.0.5
       '@types/json-schema': 7.0.15
@@ -4937,6 +4950,12 @@ packages:
       undici-types: 5.26.5
     dev: true
 
+  /@types/node@20.10.5:
+    resolution: {integrity: sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==}
+    dependencies:
+      undici-types: 5.26.5
+    dev: true
+
   /@types/node@20.3.1:
     resolution: {integrity: sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==}
     dev: true
@@ -6544,19 +6563,6 @@ packages:
       ms: 2.0.0
     dev: true
 
-  /debug@3.2.7:
-    resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
-    requiresBuild: true
-    peerDependencies:
-      supports-color: '*'
-    peerDependenciesMeta:
-      supports-color:
-        optional: true
-    dependencies:
-      ms: 2.1.3
-    dev: true
-    optional: true
-
   /debug@4.3.4:
     resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
     engines: {node: '>=6.0'}
@@ -8893,7 +8899,7 @@ packages:
     resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==}
     engines: {node: '>= 10.13.0'}
     dependencies:
-      '@types/node': 20.10.1
+      '@types/node': 20.10.5
       merge-stream: 2.0.0
       supports-color: 8.1.1
     dev: true
@@ -9110,10 +9116,8 @@ packages:
       image-size: 0.5.5
       make-dir: 2.1.0
       mime: 1.6.0
-      needle: 3.2.0
+      needle: 3.3.1
       source-map: 0.6.1
-    transitivePeerDependencies:
-      - supports-color
     dev: true
 
   /leven@3.1.0:
@@ -9966,8 +9970,8 @@ packages:
       thenify-all: 1.6.0
     dev: true
 
-  /nanoid@3.3.6:
-    resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==}
+  /nanoid@3.3.7:
+    resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
     engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
     hasBin: true
     dev: true
@@ -9976,6 +9980,13 @@ packages:
     resolution: {integrity: sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==}
     engines: {node: ^14 || ^16 || >=18}
     hasBin: true
+    dev: true
+
+  /nanoid@5.0.4:
+    resolution: {integrity: sha512-vAjmBf13gsmhXSgBrtIclinISzFFy22WwCYoyilZlsrRXNIHSwgFQ1bEdjRwMT3aoadeIF6HMuDRlOxzfXV8ig==}
+    engines: {node: ^18 || >=20}
+    hasBin: true
+    dev: false
 
   /natural-compare-lite@1.4.0:
     resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==}
@@ -9985,17 +9996,14 @@ packages:
     resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
     dev: true
 
-  /needle@3.2.0:
-    resolution: {integrity: sha512-oUvzXnyLiVyVGoianLijF9O/RecZUf7TkBfimjGrLM4eQhXyeJwM6GeAWccwfQ9aa4gMCZKqhAOuLaMIcQxajQ==}
+  /needle@3.3.1:
+    resolution: {integrity: sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==}
     engines: {node: '>= 4.4.x'}
     hasBin: true
     requiresBuild: true
     dependencies:
-      debug: 3.2.7
       iconv-lite: 0.6.3
       sax: 1.3.0
-    transitivePeerDependencies:
-      - supports-color
     dev: true
     optional: true
 
@@ -10709,7 +10717,7 @@ packages:
     resolution: {integrity: sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==}
     engines: {node: ^10 || ^12 || >=14}
     dependencies:
-      nanoid: 3.3.6
+      nanoid: 3.3.7
       picocolors: 1.0.0
       source-map-js: 1.0.2
     dev: true
@@ -10718,7 +10726,7 @@ packages:
     resolution: {integrity: sha512-Z7V5j0cq8oEKyejIKfpD8b4eBy9cwW2JWPk0+fB1HOAMsfHbnAXLLS+PfVWlzMSLQaWttKDt607I0XHmpE67Vw==}
     engines: {node: ^10 || ^12 || >=14}
     dependencies:
-      nanoid: 3.3.6
+      nanoid: 3.3.7
       picocolors: 1.0.0
       source-map-js: 1.0.2
     dev: true
@@ -12446,12 +12454,12 @@ packages:
       jest-worker: 27.5.1
       schema-utils: 3.3.0
       serialize-javascript: 6.0.1
-      terser: 5.24.0
+      terser: 5.26.0
       webpack: 5.89.0(esbuild@0.18.17)
     dev: true
 
-  /terser@5.24.0:
-    resolution: {integrity: sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==}
+  /terser@5.26.0:
+    resolution: {integrity: sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ==}
     engines: {node: '>=10'}
     hasBin: true
     dependencies:
diff --git a/src/lib/builders/progress/types.ts b/src/lib/builders/progress/types.ts
index 9113037c..e840e18e 100644
--- a/src/lib/builders/progress/types.ts
+++ b/src/lib/builders/progress/types.ts
@@ -9,7 +9,7 @@ export type CreateProgressProps = {
 	 *
 	 * @default 0
 	 */
-	defaultValue?: number;
+	defaultValue?: number | null;
 
 	/**
 	 * The controlled value store for the radio group.
@@ -17,14 +17,14 @@ export type CreateProgressProps = {
 	 *
 	 * @see https://melt-ui.com/docs/controlled#bring-your-own-store
 	 */
-	value?: Writable<number>;
+	value?: Writable<number | null>;
 
 	/**
 	 * The callback invoked when the value store of the progress changes.
 	 *
 	 * @see https://melt-ui.com/docs/controlled#change-functions
 	 */
-	onValueChange?: ChangeFn<number>;
+	onValueChange?: ChangeFn<number | null>;
 
 	/**
 	 * The maximum value of the progress.
diff --git a/src/lib/internal/helpers/date/calendar.ts b/src/lib/internal/helpers/date/calendar.ts
index 4d595534..4e056364 100644
--- a/src/lib/internal/helpers/date/calendar.ts
+++ b/src/lib/internal/helpers/date/calendar.ts
@@ -98,7 +98,7 @@ function createMonth(props: CreateMonthProps): Month<DateValue> {
 		}
 
 		const extraDaysArray = Array.from({ length: extraDays }, (_, i) => {
-			const incr = i;
+			const incr = i + 1;
 			return startFrom.add({ days: incr });
 		});
 		nextMonthDays.push(...extraDaysArray);


Test patch:
diff --git a/src/lib/internal/helpers/date/tests/calendar.spec.ts b/src/lib/internal/helpers/date/tests/calendar.spec.ts
new file mode 100644
index 00000000..85ad4c4b
--- /dev/null
+++ b/src/lib/internal/helpers/date/tests/calendar.spec.ts
@@ -0,0 +1,34 @@
+import { describe, expect, it } from 'vitest';
+import { createMonths } from '../calendar.js';
+import { CalendarDate } from '@internationalized/date';
+
+const calendarDate = new CalendarDate(2024, 1, 1);
+
+describe('Calendar', () => {
+	it('should create a month with the given locale, no fixed weeks and week starting on 0', () => {
+		const month = createMonths({
+			dateObj: calendarDate,
+			weekStartsOn: 0,
+			fixedWeeks: false,
+			locale: 'en-US',
+			numberOfMonths: 1,
+		});
+		expect(month[0].value.toString()).toBe(calendarDate.toString());
+	});
+
+	it.each([0, 1, 2, 3, 4, 5, 6])(
+		'should create a month not containing duplicate dates when fixed weeks is true',
+		(i) => {
+			const month = createMonths({
+				dateObj: calendarDate,
+				weekStartsOn: i,
+				fixedWeeks: true,
+				locale: 'en-US',
+				numberOfMonths: 1,
+			});
+			const dateStringArray = month[0].dates.map((date) => date.toString());
+			const dateStringSet = new Set(dateStringArray);
+			expect(dateStringArray.length).toBe(dateStringSet.size);
+		}
+	);
+});
diff --git a/src/tests/date-picker/DatePicker.spec.ts b/src/tests/date-picker/DatePicker.spec.ts
index cdbe3698..4b269a8f 100644
--- a/src/tests/date-picker/DatePicker.spec.ts
+++ b/src/tests/date-picker/DatePicker.spec.ts
@@ -293,7 +293,7 @@ describe('DatePicker', () => {
 			}
 		});
 
-		test('increments segment on arrow down', async () => {
+		test('decrements segment on arrow down', async () => {
 			const user = userEvent.setup();
 			const { getByTestId } = setup({
 				defaultPlaceholder: calendarDateToday,
@@ -308,24 +308,12 @@ describe('DatePicker', () => {
 			expect(firstSegment).toHaveTextContent(String(currentMonth));
 
 			await user.keyboard(kbd.ARROW_DOWN);
-			expect(firstSegment).toHaveTextContent(String(calendarDateToday.month - 1));
-		});
-
-		test('increments segment on arrow down', async () => {
-			const { getByTestId, user } = setup({
-				defaultPlaceholder: calendarDateToday,
-			});
-
-			const firstSegment = getByTestId('month');
-			await user.click(firstSegment);
 
-			await user.keyboard(kbd.ARROW_DOWN);
-
-			const currentMonth = calendarDateToday.month;
-			expect(firstSegment).toHaveTextContent(String(currentMonth));
-
-			await user.keyboard(kbd.ARROW_DOWN);
-			expect(firstSegment).toHaveTextContent(String(calendarDateToday.month - 1));
+			if (calendarDateToday.month === 1) {
+				expect(firstSegment).toHaveTextContent(String(12));
+			} else {
+				expect(firstSegment).toHaveTextContent(String(calendarDateToday.month - 1));
+			}
 		});
 
 		test('navigates segments using arrow keys', async () => {
@@ -377,6 +365,7 @@ describe('DatePicker', () => {
 		test('when no time selected, selecting', async () => {
 			const { getByTestId, queryByTestId, user, trigger } = setup({
 				granularity: 'minute',
+				defaultValue: new CalendarDateTime(2021, 1, 1),
 			});
 
 			await user.click(trigger);
@@ -393,7 +382,7 @@ describe('DatePicker', () => {
 			const minuteSegment = getByTestId('minute');
 			expect(minuteSegment).not.toHaveTextContent(String(undefined));
 
-			const firstDayInMonth = getByTestId('month-1-date-1');
+			const firstDayInMonth = getByTestId('month-2-date-1');
 			await user.click(firstDayInMonth);
 
 			await tick();