Repository: melt-ui/melt-ui
Pull request #1239 (2024-06-26 01:01:14)

Feat: add a pushState function as parameter to the table of content builder
Hello! This is a first attempt to provide the pushState function from SvelteKit to the Table of Contents builder. Using history.pushState not only throws a warning but also breaks some of SvelteKit's $page store state. I'd love to hear your comments and explore other possible implementation approaches.

changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: f1b8b773e3db6342243b38e37a14a2fe66942e80

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/Manuel-DaSilva/melt-ui/new/table-of-contents-dynamic-push-state?filename=.changeset/kind-files-grin.md&value=---%0A%22%40melt-ui%2Fsvelte%22%3A%20patch%0A---%0A%0AFeat%3A%20add%20a%20pushState%20function%20as%20parameter%20to%20the%20table%20of%20content%20builder%0A)



Patch:
diff --git a/.changeset/purple-eggs-flow.md b/.changeset/purple-eggs-flow.md
new file mode 100644
index 00000000..df499d81
--- /dev/null
+++ b/.changeset/purple-eggs-flow.md
@@ -0,0 +1,5 @@
+---
+"@melt-ui/svelte": minor
+---
+
+feat: add a pushState prop to createTableOfContents
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7036e4b0..5cc4b5ef 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,29 @@
 # @melt-ui/svelte
 
+## 0.83.0
+
+### Minor Changes
+
+- feat: add rootMargin to TableOfContents builder ([#1210](https://github.com/melt-ui/melt-ui/pull/1210))
+
+## 0.82.0
+
+### Minor Changes
+
+- Added prop to createToaster() to pause all timers on hover ([#1178](https://github.com/melt-ui/melt-ui/pull/1178))
+
+- feat(tags-input): split on paste ([#1229](https://github.com/melt-ui/melt-ui/pull/1229))
+
+- add selection helpers to range calendar ([`cf20fc72`](https://github.com/melt-ui/melt-ui/commit/cf20fc72bf367da13a17daaa9807bd430ff99688))
+
+- add `name` prop to radio group builder hidden input element ([#1237](https://github.com/melt-ui/melt-ui/pull/1237))
+
+### Patch Changes
+
+- fix(Calendar): sometimes first day of next month is missing ([#1236](https://github.com/melt-ui/melt-ui/pull/1236))
+
+- fix(slider): `onValueCommitted` called if the slider had an active state ([#1218](https://github.com/melt-ui/melt-ui/pull/1218))
+
 ## 0.81.0
 
 ### Minor Changes
diff --git a/package.json b/package.json
index 0eaaf289..83a5c7ef 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@melt-ui/svelte",
-	"version": "0.81.0",
+	"version": "0.83.0",
 	"license": "MIT",
 	"repository": "github:melt-ui/melt-ui",
 	"author": "Thomas G. Lopes",
@@ -8,6 +8,7 @@
 		"dev": "vite dev",
 		"build": "vite build && npm run package",
 		"deploy": "vite build",
+		"prepare": "svelte-kit sync",
 		"preview": "vite preview",
 		"package": "svelte-kit sync && svelte-package && publint",
 		"prepublishOnly": "npm run package",
diff --git a/src/docs/data/builders/combobox.ts b/src/docs/data/builders/combobox.ts
index 7e057994..39123596 100644
--- a/src/docs/data/builders/combobox.ts
+++ b/src/docs/data/builders/combobox.ts
@@ -31,6 +31,11 @@ const OPTION_PROPS = [
 		description:
 			'When true, hovering an option will update the `highlightedItem` store, and when the cursor leaves an option the store will be set to `null`',
 	},
+	{
+		name: 'name',
+		type: 'string',
+		description: 'The name to be used for the hidden input.',
+	},
 ];
 
 const BUILDER_NAME = 'combobox';
diff --git a/src/docs/data/builders/radio-group.ts b/src/docs/data/builders/radio-group.ts
index 66262751..9b9165cf 100644
--- a/src/docs/data/builders/radio-group.ts
+++ b/src/docs/data/builders/radio-group.ts
@@ -17,6 +17,11 @@ const OPTION_PROPS = [
 		default: '"vertical"',
 		description: 'The orientation of the radio group.',
 	},
+	{
+		name: 'name',
+		type: 'string',
+		description: 'The name to be used for the hidden input.',
+	},
 ];
 const BUILDER_NAME = 'radio group';
 
diff --git a/src/docs/data/builders/range-calendar.ts b/src/docs/data/builders/range-calendar.ts
index 3c749aae..d9f529f6 100644
--- a/src/docs/data/builders/range-calendar.ts
+++ b/src/docs/data/builders/range-calendar.ts
@@ -221,10 +221,20 @@ const builder = builderSchema(BUILDER_NAME, {
 			type: 'Readable<(date: DateValue) => boolean>',
 		},
 		{
-			name: 'isDateSelected',
+			name: 'isSelected',
 			description: 'A function that returns whether the given date is selected.',
 			type: 'Readable<(date: DateValue) => boolean>',
 		},
+		{
+			name: 'isSeletionStart',
+			description:
+				'A function that returns whether the given date is the date at the start of the selection range.',
+		},
+		{
+			name: 'isSeletionEnd',
+			description:
+				'A function that returns whether the given date is the date at the end of the selection range.',
+		},
 	],
 });
 
diff --git a/src/docs/data/builders/table-of-contents.ts b/src/docs/data/builders/table-of-contents.ts
index 7c6ab7b8..85bd77fa 100644
--- a/src/docs/data/builders/table-of-contents.ts
+++ b/src/docs/data/builders/table-of-contents.ts
@@ -35,6 +35,12 @@ const builder: APISchema = {
 `'highest'` means the opposite.\n\
 `'lowest-parents'` means that parents of the heading with the lowest visible content are also considered active, and the same goes for `'highest-parents'`.",
 		},
+		{
+			name: 'rootMargin',
+			type: 'string',
+			description:
+				'The root margin for the intersection observer. Refer to the [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/rootMargin) for more information.',
+		},
 		{
 			name: 'scrollOffset',
 			type: 'number',
@@ -60,6 +66,12 @@ const builder: APISchema = {
 			description:
 				'Allows you to overwrite the default scroll function with your own custom one. The scroll function gets the heading id passed to it.',
 		},
+		{
+			name: 'pushStateFn',
+			type: 'PushStateFn',
+			description:
+				"Allows you to overwrite the browser pushState function. The pushState function receives the fragment that corresponds to the heading id. It is intended to be used with SvelteKit's navigation module.",
+		},
 	],
 	builders: [
 		{
diff --git a/src/docs/data/builders/toast.ts b/src/docs/data/builders/toast.ts
index 04f445dd..0653a39a 100644
--- a/src/docs/data/builders/toast.ts
+++ b/src/docs/data/builders/toast.ts
@@ -20,6 +20,12 @@ const OPTION_PROPS = [
 		default: "'foreground'",
 		description: 'The sensitivity of the toast for accessibility purposes.',
 	},
+	{
+		name: 'hover',
+		type: ["'pause'", "'pause-all'", 'null'],
+		default: "'pause'",
+		description: 'The behavior when a toast is hovered.',
+	},
 ];
 
 const BUILDER_NAME = 'toast';
@@ -129,7 +135,7 @@ const keyboard: KeyboardSchema = [
 ];
 
 const schemas = [builder, content, title, description, close];
-const features = ['Automatically closes', 'Resets closing on hover'];
+const features = ['Automatically closes', 'Pause closing on hover (single, all, none)'];
 
 export const toastData: BuilderData = {
 	schemas,
diff --git a/src/docs/previews/table-of-contents/main/tailwind/index.svelte b/src/docs/previews/table-of-contents/main/tailwind/index.svelte
index fc556494..aa0c03c9 100644
--- a/src/docs/previews/table-of-contents/main/tailwind/index.svelte
+++ b/src/docs/previews/table-of-contents/main/tailwind/index.svelte
@@ -2,6 +2,7 @@
 	import { Bird } from '$icons/index.js';
 	import Tree from './tree.svelte';
 	import { createTableOfContents } from '$lib/index.js';
+	import { pushState } from '$app/navigation';
 
 	const {
 		elements: { item },
@@ -10,6 +11,11 @@
 		selector: '#toc-builder-preview',
 		exclude: ['h1', 'h4', 'h5', 'h6'],
 		activeType: 'all',
+		/**
+		 * Here we can optionally provide SvelteKit's `pushState` function.
+		 * This function preserve navigation state within the framework.
+		 */
+		pushStateFn: pushState,
 		headingFilterFn: (heading) => !heading.hasAttribute('data-toc-ignore'),
 		scrollFn: (id) => {
 			/**
diff --git a/src/docs/previews/toast/progress/tailwind/index.svelte b/src/docs/previews/toast/progress/tailwind/index.svelte
index e659ddda..58473fa8 100644
--- a/src/docs/previews/toast/progress/tailwind/index.svelte
+++ b/src/docs/previews/toast/progress/tailwind/index.svelte
@@ -7,16 +7,20 @@
 </script>
 
 <script lang="ts">
-	import { createToaster } from '$lib/index.js';
+	import { writable, type Writable } from 'svelte/store';
+	import { createToaster, type CreateToasterProps } from '$lib/index.js';
 	import { flip } from 'svelte/animate';
 	import Toast from './toast.svelte';
+	import SelectHover from './select-hover.svelte';
 
-	const {
+	const hover: Writable<CreateToasterProps['hover']> = writable('pause');
+
+	$: ({
 		elements,
 		helpers: { addToast },
 		states: { toasts },
 		actions: { portal },
-	} = createToaster<ToastData>();
+	} = createToaster<ToastData>({ hover: $hover }));
 
 	const toastData: ToastData[] = [
 		{
@@ -41,6 +45,8 @@
 	}
 </script>
 
+<SelectHover bind:value={$hover} />
+
 <button
 	class="inline-flex items-center justify-center rounded-xl bg-white px-4 py-3
 	font-medium leading-none text-magnum-700 shadow hover:opacity-75"
diff --git a/src/docs/previews/toast/progress/tailwind/select-hover.svelte b/src/docs/previews/toast/progress/tailwind/select-hover.svelte
new file mode 100644
index 00000000..104da266
--- /dev/null
+++ b/src/docs/previews/toast/progress/tailwind/select-hover.svelte
@@ -0,0 +1,82 @@
+<script lang="ts">
+	import { Check, ChevronDown } from '$icons/index.js';
+	import { createSelect, melt, type CreateToasterProps } from '$lib/index.js';
+	import { fade } from 'svelte/transition';
+
+	const options: { value: CreateToasterProps['hover']; label: string }[] = [
+		{ value: 'pause', label: 'pause' },
+		{ value: 'pause-all', label: 'pause-all' },
+		{ value: null, label: 'null' },
+	];
+
+	const {
+		elements: { trigger, menu, option, label },
+		states: { selected, selectedLabel, open },
+		helpers: { isSelected },
+	} = createSelect<CreateToasterProps['hover']>({
+		forceVisible: true,
+		positioning: {
+			placement: 'bottom',
+			fitViewport: true,
+			sameWidth: true,
+		},
+		defaultSelected: { value: 'pause', label: 'pause' },
+	});
+
+	export let value: CreateToasterProps['hover'];
+
+	$: value = $selected?.value;
+</script>
+
+<div class="absolute left-4 top-4">
+	<div class="flex flex-col gap-1">
+		<!-- svelte-ignore a11y-label-has-associated-control - $label contains the 'for' attribute -->
+		<label class="block text-white" use:melt={$label}> Hover behavior : </label>
+		<button
+			class="flex h-10 min-w-[220px] items-center justify-between rounded-lg bg-white px-3 py-2
+	text-magnum-700 shadow transition-opacity hover:opacity-90"
+			use:melt={$trigger}
+			aria-label="Hover behavior"
+		>
+			{$selectedLabel || 'Select hover behavior'}
+			<ChevronDown class="size-5" />
+		</button>
+		{#if $open}
+			<div
+				class="force-dark z-10 flex max-h-[300px] flex-col
+		overflow-y-auto rounded-lg bg-white p-1
+		shadow focus:!ring-0"
+				use:melt={$menu}
+				transition:fade={{ duration: 150 }}
+			>
+				{#each options as { value, label }}
+					<div
+						class="relative cursor-pointer rounded-lg py-1 pl-8 pr-4 text-neutral-800
+							hover:bg-magnum-100 focus:z-10
+							focus:text-magnum-700
+							data-[highlighted]:bg-magnum-200 data-[highlighted]:text-magnum-900
+							data-[disabled]:opacity-50"
+						use:melt={$option({ value, label })}
+					>
+						<div class="check {$isSelected(value) ? 'block' : 'hidden'}">
+							<Check class="size-4" />
+						</div>
+
+						{label}
+					</div>
+				{/each}
+			</div>
+		{/if}
+	</div>
+</div>
+
+<style lang="postcss">
+	.check {
+		position: absolute;
+		left: theme(spacing.2);
+		top: 50%;
+		z-index: theme(zIndex.20);
+		translate: 0 calc(-50% + 1px);
+		color: theme(colors.magnum.500);
+	}
+</style>
diff --git a/src/lib/builders/checkbox/create.ts b/src/lib/builders/checkbox/create.ts
index 2a394c76..323a27ef 100644
--- a/src/lib/builders/checkbox/create.ts
+++ b/src/lib/builders/checkbox/create.ts
@@ -6,11 +6,10 @@ import {
 	kbd,
 	omit,
 	overridable,
-	styleToString,
 	toWritableStores,
 } from '$lib/internal/helpers/index.js';
 import type { Defaults, MeltActionReturn } from '$lib/internal/types.js';
-import { derived, readable, writable } from 'svelte/store';
+import { derived, writable } from 'svelte/store';
 import type { CheckboxEvents } from './events.js';
 import type { CreateCheckboxProps } from './types.js';
 import { createHiddenInput } from '../hidden-input/create.js';
diff --git a/src/lib/builders/dialog/create.ts b/src/lib/builders/dialog/create.ts
index 6850a06c..06799baf 100644
--- a/src/lib/builders/dialog/create.ts
+++ b/src/lib/builders/dialog/create.ts
@@ -21,7 +21,6 @@ import {
 } from '$lib/internal/helpers/index.js';
 import { withGet } from '$lib/internal/helpers/withGet.js';
 import type { Defaults, MeltActionReturn } from '$lib/internal/types.js';
-import { tick } from 'svelte';
 import { derived, writable } from 'svelte/store';
 import type { DialogEvents } from './events.js';
 import type { CreateDialogProps } from './types.js';
diff --git a/src/lib/builders/menubar/create.ts b/src/lib/builders/menubar/create.ts
index 4e7f7fba..2c824d08 100644
--- a/src/lib/builders/menubar/create.ts
+++ b/src/lib/builders/menubar/create.ts
@@ -65,7 +65,7 @@ export function createMenubar(props?: CreateMenubarProps) {
 	const nextFocusable = withGet(writable<HTMLElement | null>(null));
 	const prevFocusable = withGet(writable<HTMLElement | null>(null));
 	const lastFocusedMenuTrigger = withGet(writable<HTMLElement | null>(null));
-	const closeTimer = withGet(writable(0));
+	// const closeTimer = withGet(writable(0));
 	let scrollRemoved = false;
 
 	const ids = toWritableStores({ ...generateIds(menubarIdParts), ...withDefaults.ids });
diff --git a/src/lib/builders/popover/create.ts b/src/lib/builders/popover/create.ts
index 3093dd14..e0135c6c 100644
--- a/src/lib/builders/popover/create.ts
+++ b/src/lib/builders/popover/create.ts
@@ -19,7 +19,6 @@ import {
 	portalAttr,
 	generateIds,
 	withGet,
-	sleep,
 } from '$lib/internal/helpers/index.js';
 
 import { usePopper, usePortal, type InteractOutsideEvent } from '$lib/internal/actions/index.js';
diff --git a/src/lib/builders/radio-group/create.ts b/src/lib/builders/radio-group/create.ts
index 331a5fdf..0b19c758 100644
--- a/src/lib/builders/radio-group/create.ts
+++ b/src/lib/builders/radio-group/create.ts
@@ -16,7 +16,7 @@ import {
 } from '$lib/internal/helpers/index.js';
 import { safeOnMount } from '$lib/internal/helpers/lifecycle.js';
 import type { Defaults, MeltActionReturn } from '$lib/internal/types.js';
-import { derived, writable } from 'svelte/store';
+import { derived, readonly, writable } from 'svelte/store';
 import { createHiddenInput } from '../hidden-input/create.js';
 import type { RadioGroupEvents } from './events.js';
 import type { CreateRadioGroupProps, RadioGroupItemProps } from './types.js';
@@ -27,6 +27,7 @@ const defaults = {
 	disabled: false,
 	required: false,
 	defaultValue: undefined,
+	name: undefined,
 } satisfies Defaults<CreateRadioGroupProps>;
 
 type RadioGroupParts = 'item' | 'hidden-input';
@@ -38,7 +39,7 @@ export function createRadioGroup(props?: CreateRadioGroupProps) {
 
 	// options
 	const options = toWritableStores(omit(withDefaults, 'value'));
-	const { disabled, required, loop, orientation } = options;
+	const { disabled, required, loop, orientation, name: nameProp } = options;
 
 	const valueWritable = withDefaults.value ?? writable(withDefaults.defaultValue);
 	const value = overridable(valueWritable, withDefaults?.onValueChange);
@@ -177,6 +178,7 @@ export function createRadioGroup(props?: CreateRadioGroupProps) {
 
 	const hiddenInput = createHiddenInput({
 		value,
+		name: readonly(nameProp),
 		disabled,
 		required,
 	});
diff --git a/src/lib/builders/radio-group/types.ts b/src/lib/builders/radio-group/types.ts
index f41ae499..6a743655 100644
--- a/src/lib/builders/radio-group/types.ts
+++ b/src/lib/builders/radio-group/types.ts
@@ -20,6 +20,8 @@ export type CreateRadioGroupProps = {
 	 */
 	required?: boolean;
 
+	name?: string;
+
 	/**
 	 * Whether or not the radio group should loop around when the end
 	 * is reached.
diff --git a/src/lib/builders/range-calendar/create.ts b/src/lib/builders/range-calendar/create.ts
index 1353d0b5..bcb34bd0 100644
--- a/src/lib/builders/range-calendar/create.ts
+++ b/src/lib/builders/range-calendar/create.ts
@@ -1096,6 +1096,9 @@ export function createRangeCalendar<T extends DateValue = DateValue>(
 			setMonth,
 			isDateDisabled: _isDateDisabled,
 			isDateUnavailable,
+			isSelectionStart,
+			isSelectionEnd,
+			isSelected,
 		},
 		options,
 		ids,
diff --git a/src/lib/builders/slider/create.ts b/src/lib/builders/slider/create.ts
index 459a9407..1046870f 100644
--- a/src/lib/builders/slider/create.ts
+++ b/src/lib/builders/slider/create.ts
@@ -521,11 +521,11 @@ export const createSlider = (props?: CreateSliderProps) => {
 			};
 
 			const pointerUp = () => {
-				isActive.set(false);
-
-				if (withDefaults?.onValueCommitted) {
+				if (withDefaults?.onValueCommitted && isActive.get()) {
 					withDefaults.onValueCommitted(value.get());
 				}
+
+				isActive.set(false);
 			};
 
 			const unsub = executeCallbacks(
diff --git a/src/lib/builders/table-of-contents/create.ts b/src/lib/builders/table-of-contents/create.ts
index 978d7345..91b5158d 100644
--- a/src/lib/builders/table-of-contents/create.ts
+++ b/src/lib/builders/table-of-contents/create.ts
@@ -32,10 +32,12 @@ export function createTableOfContents(args: CreateTableOfContentsArgs) {
 		selector,
 		exclude,
 		activeType,
+		rootMargin,
 		scrollBehaviour,
 		scrollOffset,
 		headingFilterFn,
 		scrollFn,
+		pushStateFn,
 	} = argsWithDefaults;
 
 	const { name } = createElHelpers('table-of-contents');
@@ -301,6 +303,7 @@ export function createTableOfContents(args: CreateTableOfContentsArgs) {
 			observer = new IntersectionObserver(handleElementObservation, {
 				root: null,
 				threshold: observer_threshold,
+				rootMargin,
 			});
 			elementsList.forEach((el) => observer?.observe(el));
 		}
@@ -373,7 +376,11 @@ export function createTableOfContents(args: CreateTableOfContentsArgs) {
 
 					// Add items hash to URL
 					if (id) {
-						history.pushState({}, '', `#${id}`);
+						if (pushStateFn) {
+							pushStateFn(`#${id}`, {});
+						} else {
+							history.pushState({}, '', `#${id}`);
+						}
 					}
 				})
 			);
diff --git a/src/lib/builders/table-of-contents/types.ts b/src/lib/builders/table-of-contents/types.ts
index 4a422a7e..e157ac1a 100644
--- a/src/lib/builders/table-of-contents/types.ts
+++ b/src/lib/builders/table-of-contents/types.ts
@@ -20,6 +20,8 @@ export type HeadingFilterFn = (heading: HTMLHeadingElement) => boolean;
 
 export type ScrollFn = (id: string) => void;
 
+export type PushStateFn = (url: string | URL, state: object) => void;
+
 export type CreateTableOfContentsArgs = {
 	/**
 	 * The ID of the container holding the page content.
@@ -48,6 +50,10 @@ export type CreateTableOfContentsArgs = {
 	 * - 'highest-parents': Parents of the heading with the highest visible content are also considered active.
 	 */
 	activeType?: ActiveType;
+	/**
+	 * The root margin for the intersection observer. Refer to the [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/rootMargin) for more information.
+	 */
+	rootMargin?: string;
 	/**
 	 * A custom filter function for headings.
 	 */
@@ -56,6 +62,11 @@ export type CreateTableOfContentsArgs = {
 	 * A custom scroll function.
 	 */
 	scrollFn?: ScrollFn;
+
+	/**
+	 * A custom pushState function, expected to be SvelteKit's pushState function.
+	 */
+	pushStateFn?: PushStateFn;
 };
 
 export type ElementHeadingLU = {
diff --git a/src/lib/builders/tags-input/create.ts b/src/lib/builders/tags-input/create.ts
index ec7b0ba4..85646946 100644
--- a/src/lib/builders/tags-input/create.ts
+++ b/src/lib/builders/tags-input/create.ts
@@ -339,11 +339,17 @@ export function createTagsInput(props?: CreateTagsInputProps) {
 					if (!addOnPaste.get()) return;
 					e.preventDefault();
 
-					// Update value with the pasted text or set invalid
-					if (isInputValid(pastedText) && (await addTag(pastedText))) {
-						node.value = '';
-					} else {
-						inputInvalid.set(true);
+					const newTags = pastedText.split(',');
+					addTag: for (let i = 0; i < newTags.length; i++) {
+						const newTag = newTags[i];
+						// Update value with the pasted tag or set invalid
+						if (isInputValid(newTag) && (await addTag(newTag))) {
+							continue addTag;
+						} else {
+							node.value = newTags.slice(i).join(',');
+							inputInvalid.set(true);
+							return;
+						}
 					}
 				}),
 				addMeltEventListener(node, 'keydown', async (e) => {
diff --git a/src/lib/builders/toast/create.ts b/src/lib/builders/toast/create.ts
index 2576e73a..640ce466 100644
--- a/src/lib/builders/toast/create.ts
+++ b/src/lib/builders/toast/create.ts
@@ -21,13 +21,14 @@ const { name } = createElHelpers<ToastParts>('toast');
 const defaults = {
 	closeDelay: 5000,
 	type: 'foreground',
+	hover: 'pause',
 } satisfies CreateToasterProps;
 
 export function createToaster<T = object>(props?: CreateToasterProps) {
 	const withDefaults = { ...defaults, ...props } satisfies CreateToasterProps;
 
 	const options = toWritableStores(withDefaults);
-	const { closeDelay, type } = options;
+	const { closeDelay, type, hover } = options;
 
 	const toastsMap = writable(new Map<string, Toast<T>>());
 
@@ -98,6 +99,23 @@ export function createToaster<T = object>(props?: CreateToasterProps) {
 		});
 	};
 
+	const pauseToastTimer = (currentToast: Toast<T>) => {
+		if (currentToast.timeout !== null) {
+			window.clearTimeout(currentToast.timeout);
+		}
+		currentToast.pausedAt = performance.now();
+	};
+	const restartToastTimer = (currentToast: Toast<T>) => {
+		const pausedAt = currentToast.pausedAt ?? currentToast.createdAt;
+		const elapsed = pausedAt - currentToast.createdAt - currentToast.pauseDuration;
+		const remaining = currentToast.closeDelay - elapsed;
+		currentToast.timeout = window.setTimeout(() => {
+			removeToast(currentToast.id);
+		}, remaining);
+		currentToast.pauseDuration += performance.now() - pausedAt;
+		currentToast.pausedAt = undefined;
+	};
+
 	const content = makeElement(name('content'), {
 		stores: toastsMap,
 		returned: ($toasts) => {
@@ -123,31 +141,42 @@ export function createToaster<T = object>(props?: CreateToasterProps) {
 				addMeltEventListener(node, 'pointerenter', (e) => {
 					if (isTouch(e)) return;
 					toastsMap.update((currentMap) => {
-						const currentToast = currentMap.get(node.id);
-						if (!currentToast || currentToast.closeDelay === 0) return currentMap;
-
-						if (currentToast.timeout !== null) {
-							window.clearTimeout(currentToast.timeout);
+						switch (hover.get()) {
+							case 'pause': {
+								const currentToast = currentMap.get(node.id);
+								if (!currentToast || currentToast.closeDelay === 0) return currentMap;
+								pauseToastTimer(currentToast);
+								break;
+							}
+							case 'pause-all':
+								for (const [, currentToast] of currentMap) {
+									if (!currentToast || currentToast.closeDelay === 0) continue;
+									pauseToastTimer(currentToast);
+								}
+								break;
 						}
-						currentToast.pausedAt = performance.now();
 						return new Map(currentMap);
 					});
 				}),
 				addMeltEventListener(node, 'pointerleave', (e) => {
 					if (isTouch(e)) return;
 					toastsMap.update((currentMap) => {
-						const currentToast = currentMap.get(node.id);
-						if (!currentToast || currentToast.closeDelay === 0) return currentMap;
-
-						const pausedAt = currentToast.pausedAt ?? currentToast.createdAt;
-						const elapsed = pausedAt - currentToast.createdAt - currentToast.pauseDuration;
-						const remaining = currentToast.closeDelay - elapsed;
-						currentToast.timeout = window.setTimeout(() => {
-							removeToast(node.id);
-						}, remaining);
-
-						currentToast.pauseDuration += performance.now() - pausedAt;
-						currentToast.pausedAt = undefined;
+						switch (hover.get()) {
+							case 'pause': {
+								const currentToast = currentMap.get(node.id);
+								if (!currentToast || currentToast.closeDelay === 0) return currentMap;
+								restartToastTimer(currentToast);
+								break;
+							}
+
+							case 'pause-all':
+								for (const [, currentToast] of currentMap) {
+									if (!currentToast || currentToast.closeDelay === 0) continue;
+									restartToastTimer(currentToast);
+								}
+								break;
+						}
+
 						return new Map(currentMap);
 					});
 				}),
diff --git a/src/lib/builders/toast/types.ts b/src/lib/builders/toast/types.ts
index 82af1ec8..fd56622b 100644
--- a/src/lib/builders/toast/types.ts
+++ b/src/lib/builders/toast/types.ts
@@ -8,9 +8,10 @@ export type CreateToasterProps = {
 	// If set to 0, the toast will not be automatically closed.
 	closeDelay?: number;
 	type?: 'foreground' | 'background';
+	hover?: 'pause' | 'pause-all' | null;
 };
 
-export type AddToastProps<T = object> = CreateToasterProps & {
+export type AddToastProps<T = object> = Omit<CreateToasterProps, 'hover'> & {
 	data: T;
 };
 
diff --git a/src/lib/internal/actions/floating/action.ts b/src/lib/internal/actions/floating/action.ts
index bda14424..4ba816f8 100644
--- a/src/lib/internal/actions/floating/action.ts
+++ b/src/lib/internal/actions/floating/action.ts
@@ -14,7 +14,7 @@ import {
 } from '@floating-ui/dom';
 import type { FloatingConfig } from './types.js';
 import { isHTMLElement, isObject, noop } from '$lib/internal/helpers/index.js';
-import type { Placement, VirtualElement, FlipOptions } from '@floating-ui/core';
+import type { Placement, VirtualElement } from '@floating-ui/core';
 
 const defaultConfig = {
 	strategy: 'absolute',
diff --git a/src/lib/internal/helpers/date/calendar.ts b/src/lib/internal/helpers/date/calendar.ts
index 4e056364..532cf6c8 100644
--- a/src/lib/internal/helpers/date/calendar.ts
+++ b/src/lib/internal/helpers/date/calendar.ts
@@ -94,7 +94,7 @@ function createMonth(props: CreateMonthProps): Month<DateValue> {
 		let startFrom = nextMonthDays[nextMonthDays.length - 1];
 
 		if (!startFrom) {
-			startFrom = dateObj.add({ months: 1 }).set({ day: 1 });
+			startFrom = endOfMonth(dateObj);
 		}
 
 		const extraDaysArray = Array.from({ length: extraDays }, (_, i) => {
diff --git a/src/lib/internal/helpers/object.ts b/src/lib/internal/helpers/object.ts
index 49add7e2..fffcc644 100644
--- a/src/lib/internal/helpers/object.ts
+++ b/src/lib/internal/helpers/object.ts
@@ -57,6 +57,7 @@ export function removeUndefined<T extends object>(
 	for (const key in obj) {
 		const value = obj[key];
 		if (value !== undefined) {
+			// eslint-disable-next-line @typescript-eslint/no-explicit-any
 			result[key] = value as any;
 		}
 	}
diff --git a/src/lib/internal/helpers/store/toReadableStores.ts b/src/lib/internal/helpers/store/toReadableStores.ts
index cfd5f0f0..bc7c7078 100644
--- a/src/lib/internal/helpers/store/toReadableStores.ts
+++ b/src/lib/internal/helpers/store/toReadableStores.ts
@@ -3,6 +3,7 @@ import { isReadable } from '../is.js';
 import { withGet, type WithGet } from '../withGet.js';
 import type { MaybeReadable } from '$lib/internal/types.js';
 
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
 type TODO = any;
 
 export type ToReadableStores<T extends Record<string, unknown>> = {
@@ -34,5 +35,5 @@ export function toReadableStores<T extends Record<string, unknown>>(
 		}
 	});
 
-	return result as any;
+	return result as TODO;
 }
diff --git a/src/routes/(landing-ui)/popover.svelte b/src/routes/(landing-ui)/popover.svelte
index cae33270..de6eb49b 100644
--- a/src/routes/(landing-ui)/popover.svelte
+++ b/src/routes/(landing-ui)/popover.svelte
@@ -25,8 +25,13 @@
 
 	export let contentClass = '';
 
+	let popoverButton: HTMLButtonElement;
+
 	onMount(() => {
 		open.set(true);
+		setTimeout(() => {
+			popoverButton.blur();
+		}, 100);
 	});
 </script>
 
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 1cf0bc1f..c9ed3fec 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -45,6 +45,15 @@
 
 <ModeWatcher defaultMode="dark" />
 
+<a
+	href="#main"
+	on:blur={() => (skipToContent = false)}
+	on:focus={() => (skipToContent = true)}
+	class="force-dark fixed top-[-20rem] z-0 ml-56 rounded-br-xl rounded-tl-xl bg-neutral-900 px-2 py-1 text-neutral-100 focus:top-4 focus:z-[1000] max-sm:right-[6]"
+	tabindex="0"
+>
+	Skip to main content
+</a>
 <div class="relative flex min-h-screen flex-col md:flex-col-reverse" id="page">
 	<div class="flex flex-1">
 		<slot />
diff --git a/src/routes/docs/+layout.svelte b/src/routes/docs/+layout.svelte
index 60e65eff..6a57c9d8 100644
--- a/src/routes/docs/+layout.svelte
+++ b/src/routes/docs/+layout.svelte
@@ -22,7 +22,7 @@
 			</Switch>
 		</div>
 	</aside>
-	<div class="mx-auto w-full min-w-0">
+	<div id="main" class="mx-auto w-full min-w-0">
 		<slot />
 	</div>
 </div>


Test patch:
diff --git a/src/tests/calendar/CalendarTest.svelte b/src/tests/calendar/CalendarTest.svelte
index 81b5246f..e3aaff46 100644
--- a/src/tests/calendar/CalendarTest.svelte
+++ b/src/tests/calendar/CalendarTest.svelte
@@ -32,6 +32,7 @@
 		states: { value: insideValue, months, headingValue, weekdays },
 		options: {
 			weekdayFormat: weekdayFormatOption,
+			// eslint-disable-next-line @typescript-eslint/no-unused-vars
 			numberOfMonths: numberOfMonthsOption,
 			fixedWeeks: fixedWeeksOption,
 			weekStartsOn: weekStartsOnOption,
diff --git a/src/tests/date-picker/DatePickerTest.svelte b/src/tests/date-picker/DatePickerTest.svelte
index 56a2ece7..e7beeb78 100644
--- a/src/tests/date-picker/DatePickerTest.svelte
+++ b/src/tests/date-picker/DatePickerTest.svelte
@@ -49,6 +49,7 @@
 		options: {
 			locale: insideLocale,
 			weekdayFormat: weekdayFormatOption,
+			// eslint-disable-next-line @typescript-eslint/no-unused-vars
 			numberOfMonths: numberOfMonthsOption,
 			fixedWeeks: fixedWeeksOption,
 			weekStartsOn: weekStartsOnOption,
diff --git a/src/tests/portal/PopoverSelect.svelte b/src/tests/portal/PopoverSelect.svelte
index 28020e73..893c6df7 100644
--- a/src/tests/portal/PopoverSelect.svelte
+++ b/src/tests/portal/PopoverSelect.svelte
@@ -1,5 +1,5 @@
 <script lang="ts">
-	import { createPopover, createSelect, melt, type CreateSelectProps } from '$lib/index.js';
+	import { createPopover, createSelect, melt } from '$lib/index.js';
 	import { Settings2 } from '$icons/index.js';
 	import type { PortalConfig } from '$lib/internal/actions/portal.js';
 
diff --git a/src/tests/radio-group/RadioGroup.spec.ts b/src/tests/radio-group/RadioGroup.spec.ts
index deab96dd..29018ee2 100644
--- a/src/tests/radio-group/RadioGroup.spec.ts
+++ b/src/tests/radio-group/RadioGroup.spec.ts
@@ -84,4 +84,9 @@ describe('Radio Group', () => {
 		expect(els[2]).toHaveFocus();
 		expect(els[1]).not.toHaveFocus();
 	});
+
+	it('has specified name on hidden input', async () => {
+		const { getByTestId } = setup({ name: 'name' });
+		expect(getByTestId('input')).toHaveAttribute('name', 'name');
+	});
 });
diff --git a/src/tests/radio-group/RadioGroupTest.svelte b/src/tests/radio-group/RadioGroupTest.svelte
index 111c1785..b1acb7ab 100644
--- a/src/tests/radio-group/RadioGroupTest.svelte
+++ b/src/tests/radio-group/RadioGroupTest.svelte
@@ -16,6 +16,7 @@
 	export let disabled: CreateRadioGroupProps['disabled'] = undefined;
 	export let onValueChange: CreateRadioGroupProps['onValueChange'] = undefined;
 	export let required: CreateRadioGroupProps['required'] = undefined;
+	export let name: CreateRadioGroupProps['name'] = undefined;
 	export let items: Item[] = [
 		{ value: 'a', disabled: false },
 		{ value: 'b', disabled: false },
@@ -24,7 +25,7 @@
 	];
 
 	const {
-		elements: { root, item },
+		elements: { root, item, hiddenInput },
 		states: { value: localValue },
 	} = createRadioGroup({
 		value,
@@ -32,6 +33,7 @@
 		onValueChange,
 		disabled,
 		required,
+		name,
 		...removeUndefined($$restProps),
 	});
 </script>
@@ -39,6 +41,7 @@
 <main>
 	<div data-testid="value">{$localValue}</div>
 	<form>
+		<input use:melt={$hiddenInput} data-testid="input" />
 		<label id="radio-group-label" for="radio-group" data-testid="label"> Airplane mode </label>
 		<div use:melt={$root} data-testid="root">
 			{#each items as radioItem}
diff --git a/src/tests/range-calendar/RangeCalendarTest.svelte b/src/tests/range-calendar/RangeCalendarTest.svelte
index 4961f63b..7c0a6a10 100644
--- a/src/tests/range-calendar/RangeCalendarTest.svelte
+++ b/src/tests/range-calendar/RangeCalendarTest.svelte
@@ -29,6 +29,7 @@
 		states: { value: insideValue, months, headingValue, weekdays },
 		options: {
 			weekdayFormat: weekdayFormatOption,
+			// eslint-disable-next-line @typescript-eslint/no-unused-vars
 			numberOfMonths: numberOfMonthsOption,
 			fixedWeeks: fixedWeeksOption,
 			locale: localeOption,