npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@rshval/svelte-components

v1.2.6

Published

Reusable Svelte 5 UI component library (forms, table, map, notifications, drawers and stores).

Readme

@rshval/svelte-components

Reusable UI library for Svelte 5.

Package includes:

  • base UI components (Button, InputField, Select, Modal, Toast, Alert);
  • composite components (Table, InputPhone, Drawer, Notifications);
  • map components built on mapbox-gl;
  • helpers and plugins (api, geoserviceApi, storage*);
  • ready-to-use Svelte stores for session, account, network, geolocation, and device info.

Project positioning

This library is primarily developed and used by the author in personal projects.

What this means in practice:

  • there is currently no goal to turn this package into a large universal UI library with a full standalone docs site in the near term;
  • API, component set, and dependency list may evolve as real product needs change;
  • the library is kept up to date for the author’s active projects and updated in a timely manner;
  • improvements, fixes, and suggestions are welcome via Pull Request.

About Storybook and examples

  • Storybook in this repository is still incomplete and needs further work.
  • For integration scenarios, prefer Svelte sandbox / a local SvelteKit sandbox.
  • The README captures working patterns and should be used as the primary reference.

Svelte Playground note

If you see Failed to import $app@latest, it means the example contains SvelteKit-only aliases ($app/*) but is opened in plain Svelte Playground.

Even with the newest Svelte version selected in Playground, $app/* remains unavailable there because it is provided by SvelteKit runtime, not by the core svelte package.

Use one of these options:

  • run the example in a SvelteKit project/sandbox;
  • replace $app/* imports with environment-agnostic alternatives before running in plain Svelte Playground.

Installation

npm i @rshval/svelte-components

Also make sure your project has compatible library dependencies installed:

  • @popperjs/core — runtime dependency (installed automatically with the package);
  • svelte, @tiptap/* — peer dependencies that must stay compatible in consumer apps.
  • @sveltejs/kit is optional and only needed in consumer apps that use SvelteKit-specific APIs.

Styling requirements (Tailwind + DaisyUI)

This library currently relies on utility and component classes from Tailwind CSS 4 and DaisyUI 5 (for example btn, btn-primary, input, drawer, modal).

If your app does not include these plugins, components will still render, but visual styles will be incomplete.

Minimum setup in consumer app styles:

@import 'tailwindcss';
@plugin 'daisyui';

Quick README navigation

Quick start

<script lang="ts">
	import { Button, InputField, Table } from '@rshval/svelte-components';

	let value = $state('');
	const rows = [{ id: '1', name: 'Alice' }];
	const columns = [{ id: 'name', title: 'Name' }];
</script>

<InputField bind:value label="Name" placeholder="Enter name" />
<Button>Save</Button>
<Table {rows} {columns} />

Component usage

Below are working patterns aligned with the current component implementations and exported API.

Modal

Use-case: confirmations, forms, and content cards with user actions.

For programmatic control (showModal() / close()), bind:element is required; otherwise consumers have no reference to the internal dialog element.

Props / Events / Bindings

| Type | Fields | | -------- | ---------------------------------------------------------------------------------------------- | | Props | title, noActions, btnDisabled, classBox, class, styleBox, noAutoClose, btnText | | Events | onclose | | Bindings | bind:element, bind:send |

Basic example

<script lang="ts">
	import { Modal } from '@rshval/svelte-components';

	let modalElem: HTMLDialogElement | null = null;
</script>

<button class="btn" onclick={() => modalElem?.showModal()}>Open</button>

<Modal bind:element={modalElem} title="Title" noActions>
	<div class="p-4">Modal content</div>
</Modal>

Programmatic control (production-like)

<script lang="ts">
	import { Modal, Button } from '@rshval/svelte-components';

	let modalElem: HTMLDialogElement | null = null;
</script>

<Button onclick={() => modalElem?.showModal()}>Open</Button>

<Modal bind:element={modalElem} title="Title" noActions btnDisabled classBox="max-w-xl">
	<div class="p-4">
		Modal content
		<div class="mt-4">
			<Button onclick={() => modalElem?.close()} class="btn-ghost">Close</Button>
		</div>
	</div>
</Modal>

Toast

Use-case: quick notifications for successful/error actions.

Props / Events / Bindings

| Type | Fields | | -------- | ---------------- | | Props | items, class | | Events | onclose(item) | | Bindings | none |

Input variants

  1. Current API: items array.
  2. Legacy pattern: { type, message } object mapped to items in caller code.

Current example

<script lang="ts">
	import { Toast } from '@rshval/svelte-components';

	let toast: { type: 'success' | 'info' | 'alert'; message: string } | null = {
		type: 'success',
		message: 'Saved'
	};
</script>

{#if toast}
	<Toast items={[toast]} onclose={() => (toast = null)} />
{/if}

Legacy-compatible example (type + message)

<script lang="ts">
	import { Toast } from '@rshval/svelte-components';

	let toast: { type: 'success' | 'info' | 'alert'; message: string } | null = {
		type: 'info',
		message: 'Done'
	};
</script>

{#if toast}
	<Toast items={[{ type: toast.type, message: toast.message }]} onclose={() => (toast = null)} />
{/if}

type and message as standalone props are not supported by current Toast; for backward compatibility map them into items.

Table stack: TableFilters + Table + TablePagination

Use-case: list pages (orders/events/customers) with filters and pagination.

Table

| Type | Fields | | -------- | ---------------------------------------------------------------------- | | Props | columns, rows, hover, zebra, class, selected, ths, trs | | Events | none (row selection is done via selected) | | Bindings | bind:rows, bind:columns, bind:selected |

TableFilters

| Type | Fields | | -------- | -------------------------------------- | | Props | title, count, bodyClass, class | | Events | none | | Snippets | #snippet actions() + children |

TablePagination

| Type | Fields | | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | Props | total, page, limit, limitOptions, onPrev, onNext, onLimitChange, canPrev, canNext, summary, pageLabel, showLimit, class | | Events | callback props | | Bindings | none |

End-to-end example

<script lang="ts">
	import { Table, TableFilters, TablePagination, Button } from '@rshval/svelte-components';
	import ActionButtons from '$lib/components/ActionButtons.svelte';

	let rows: any[] = [];

	function openEdit(id: string) {
		console.log('edit', id);
	}

	function doDelete(id: string) {
		console.log('delete', id);
	}

	const columns = [
		{ id: 'id', title: 'ID', tpl: (r: any) => r._id },
		{ id: 'title', title: 'Title', tpl: (r: any) => r.title },
		{
			id: 'actions',
			title: '',
			component: ActionButtons,
			props: {
				onEdit: openEdit,
				onDelete: doDelete
			},
			propsFn: (r: object) => {
				return {
					row: r
				};
			}
		}
	];

	let total = 0;
	let pageNumber = 1;
	let limit = 10;

	function resetFilters() {
		pageNumber = 1;
		load();
	}

	function load() {
		// load rows/total
	}
</script>

<TableFilters title="Filters" count={rows.length} bodyClass="grid grid-cols-1 gap-3 md:grid-cols-4">
	{#snippet actions()}
		<Button class="btn-ghost btn-sm" onclick={resetFilters}>Reset</Button>
	{/snippet}

	<input class="input-bordered input input-sm" placeholder="Search" />
	<Button class="btn-sm btn-primary" onclick={load}>Apply</Button>
</TableFilters>

<Table {columns} {rows} hover class="table-sm" />

<TablePagination
	{total}
	page={pageNumber}
	{limit}
	limitOptions={[10, 20, 50]}
	onLimitChange={(value) => {
		pageNumber = 1;
		limit = value;
		load();
	}}
	onPrev={() => {
		pageNumber -= 1;
		load();
	}}
	onNext={() => {
		pageNumber += 1;
		load();
	}}
	canPrev={pageNumber > 1}
	canNext={pageNumber * limit < total}
/>

Example action-cell component (ActionButtons.svelte)

<!-- Action component for table cell -->
<script lang="ts">
	import { Button } from '@rshval/svelte-components';
	import IconEdit from '@tabler/icons-svelte-runes/icons/edit';
	import IconTrash from '@tabler/icons-svelte-runes/icons/trash';

	let { onEdit, onDelete, row, propsFn } = $props();

	$effect(() => {
		console.log(propsFn, row);
	});

	function handleEdit() {
		onEdit(row._id);
	}

	function handleDelete() {
		onDelete(row._id);
	}
</script>

<div class="flex gap-2">
	<Button class="btn-sm" onclick={handleEdit}><IconEdit size={16} /></Button>
	<Button class="btn-sm btn-error" onclick={handleDelete}><IconTrash size={16} /></Button>
</div>

How it works:

  • props from the column definition are passed as regular component inputs (onEdit, onDelete).
  • propsFn(row) is executed for each row and adds dynamic inputs (for example, row).
  • Inside the component, values are received via let { ... } = $props().
  • As a result, ActionButtons receives both callbacks and current row data, then can call onEdit(row._id) and onDelete(row._id).

Form components

InputField

Use-case: text inputs and password fields with toggle.

| Type | Fields | | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | Props | value, label, type, passwordToggle, class, id + HTML attributes (placeholder, autocomplete, name, spellcheck, maxlength etc.) | | Events | oninput, onchange, onfocus | | Bindings | bind:value |

<InputField bind:value={title} placeholder="Title" />

<InputField
	value={smtpHostDraft}
	type="text"
	oninput={(e: any) => (smtpHostDraft = String(e.currentTarget?.value || ''))}
/>

InputPhone

Use-case: phone input with country selector.

| Type | Fields | | -------- | --------------------------------------------------------------------------------------- | | Props | value, inputId, inputClass, placeholder, disabledCountry, disabled, class | | Events | onInput | | Bindings | bind:value, bind:country, bind:valid, bind:element |

<InputPhone
	inputId="buyer-phone"
	inputClass="input-bordered input w-full"
	bind:value={buyerPhone}
/>

Switch

Use-case: binary flags in filters/settings.

| Type | Fields | | -------- | ------------------------------------------- | | Props | checked, styleType, class, disabled | | Events | onchange | | Bindings | bind:checked |

<Switch styleType="warning" bind:checked={isPrivate} />

<Switch
	checked={telegramEnabledDraft}
	onchange={(e: Event & { currentTarget: EventTarget & HTMLInputElement }) => {
		telegramEnabledDraft = Boolean(e.currentTarget?.checked);
	}}
/>

Select

Use-case: selecting a value from options.

| Type | Fields | | -------- | ---------------------------------------------------------------------------------------------------- | | Props | value, options: Array<{ value; label }>, label, placeholder, disabled, required, class | | Events | via standard <select> onchange | | Bindings | bind:value |

<Select
	label="Event"
	bind:value={selectedEvent}
	options={events.map((e) => ({ value: e._id, label: e.title }))}
	placeholder="No event"
/>

Other UI components

  • Button, BreadCrumbs, Loader, ImagesUploader, Editor, Popup, Alert, ThemeButton, Theme.

Minimal examples:

<Alert class="alert-info">Info message</Alert>
<Popup title="Hint">Popup text</Popup>
<ThemeButton />

Stores / API / Utils / Directives (non-UI exports)

Stores

Exports: sessionStore, accountStore, sessionIsInited, accountStoreInited, deviceInfoStore, networkStore, geolocationStore, geolocationIsInited.

import {
	sessionStore,
	accountStore,
	sessionIsInited,
	accountStoreInited
} from '@rshval/svelte-components';

if (!$sessionIsInited) {
	await sessionStore.initSession();
}
if (!$accountStoreInited) {
	await accountStore.initAccount(API_BASE);
}

API client

Exports: api.get/post/put/patch/del.

import { api, sessionStore } from '@rshval/svelte-components';
import { get } from 'svelte/store';

const session = get(sessionStore);
const res = await api.get(`${API_BASE}/events`, session?.dc1_auth_key);

Storage helpers

Exports: storageGet, storageSet, storageRemove (typically stores a JSON string).

import { storageGet, storageSet, storageRemove } from '@rshval/svelte-components';

await storageSet('cart', JSON.stringify(items));
const saved = await storageGet('cart');
await storageRemove('cart');

Utils

Exports: isObject, isValidEmail, patternPassword.

import { isObject, isValidEmail, patternPassword } from '@rshval/svelte-components';

Directive

Export: clickOutside.

<script lang="ts">
	import { clickOutside } from '@rshval/svelte-components';

	let isOpen = $state(true);
</script>

<div use:clickOutside={() => (isOpen = false)}>...</div>

Coverage of commonly used entities

UI

  • Modal
  • Button
  • BreadCrumbs
  • Table
  • TableFilters
  • TablePagination
  • InputField
  • InputPhone
  • Switch
  • Select
  • Toast
  • Loader
  • ImagesUploader
  • Editor
  • Popup
  • Alert
  • ThemeButton
  • Theme

Non-UI

  • api
  • sessionStore, sessionIsInited
  • accountStore, accountStoreInited
  • storageGet, storageSet, storageRemove
  • isObject, isValidEmail, patternPassword
  • clickOutside
  • deviceInfoStore
  • networkStore
  • geolocationStore, geolocationIsInited

Current vs legacy

  • Current: APIs listed in the tables above (including Modal with bind:element and Toast with items).
  • Legacy/compatibility: old patterns where toast is passed as { type, message } should be mapped to items on the consumer side.

Exports

Main export groups from src/lib/index.ts:

  • Components: Button, Badge, InputField, Textarea, Editor, Select, Loader, Modal, Switch, Alert, Popup, BreadCrumbs, Timer, Toast;
  • Complex components: InputPhone, Notifications, Notification, Table, Theme, ThemeButton, Drawer;
  • Map: Map, MapComponent, UiMap*, getGeolocation, mapbox event types;
  • Helpers: clickOutside, blurOnEscape, isValid*, patternPassword, getColorByValue, isObject;
  • Plugins: api, geoserviceApi, storageGet/storageSet/storageRemove;
  • Stores: accountStore, sessionStore, networkStore, deviceInfoStore, geolocationStore, screenOrientationStore, noScrollAppStore.

ImagesUploader

ImagesUploader now validates files on the client before upload starts.

  • Default supported formats: image/png, image/jpeg (.png, .jpg, .jpeg).
  • Invalid files are rejected before preview/upload.
  • Upload errors are returned through onerror.

Props

  • accept?: string | string[] - accepted file formats (default: ['image/png', 'image/jpeg']).
  • maxFileSizeMb?: number - optional max file size in MB.
  • validateFile?: (file: File) => string | null - custom validator, returns error text or null.
  • onerror?: (message: string, context?: { fileName?: string; code?: string }) => void - unified error callback for client/server upload errors.

Toast integration example

<script lang="ts">
	import { ImagesUploader, Toast } from '@rshval/svelte-components';

	let toastItems = $state([]);

	function handleUploadError(message: string, context?: { fileName?: string }) {
		toastItems = [
			...toastItems,
			{
				type: 'error',
				message: context?.fileName ? `${context.fileName}: ${message}` : message
			}
		];
	}
</script>

<ImagesUploader
	assetsGet="/api/assets"
	assetsPost="/api/assets"
	pathPrefix=""
	onerror={handleUploadError}
/>

<Toast items={toastItems} />

Development scripts

npm run dev
npm run check
npm run lint
npm run test
npm run build
npm run storybook
npm run build-storybook

Storybook

For isolated visual component checks:

npm run storybook

Build static Storybook:

npm run build-storybook

Publishing

Build package:

npm run prepack

Manual publish:

npm publish --access public

Automated release via Changesets:

  1. Add a changeset (npm run changeset) describing your changes.
  2. On main, workflow creates/updates a release PR with versions and changelog.
  3. After merging the release PR, the package is published automatically (npm run release).

Compatibility

| Package | Recommended version | | ---------------------------------------------- | ------------------- | | svelte (peer) | ^5.53.7 | | @sveltejs/kit (optional, for SvelteKit apps) | ^2.53.4 | | @tiptap/core and @tiptap/* (peer) | ^3.20.0 | | @popperjs/core (runtime) | ^2.11.8 |

SSR limitations and notes

  • Some components target browser-only environments (mapbox-gl, geolocation, Capacitor plugins) and should be used on client side only.
  • For SSR SvelteKit pages, wrap browser-only components in if (browser) checks or load them in onMount.
  • Map components require token setup and client-side initialization code.
  • Capacitor plugins assume native environment access; in regular browsers API limitations and fallback behavior may apply.

Breaking changes policy

  • Any removal/change of a public export (exports or named root export) is a breaking change and requires a major bump.
  • Changes to required component props and public store/helper API shape are also breaking changes.
  • Consumers should pin major version (^1.x) and read changelog before upgrading.

Detailed step-by-step extraction plan is in MIGRATION_TO_STANDALONE.md.

Migration checklist for extracting into a standalone repository

  1. Extract package into a separate git repository preserving history (git subtree split or history filtering).
  2. Configure CI checks on pull requests: check, prepack, build-storybook.
  3. Enable changesets workflow for semver and changelog management.
  4. Verify public API: keep exports contracts stable and do not remove them without a major bump.
  5. Add a consumer smoke example (minimal SvelteKit app) that installs package from tarball.

CI and smoke checks

Recommended CI checks before publishing:

npm run check
npm run prepack
npm run smoke:exports
npm run build-storybook

smoke:exports verifies that after prepack, all artifacts declared in exports are present in dist/.

QR scanner (event check-in, Web + Capacitor)

QrScanner gives a unified scanner API for browser and Capacitor apps.

  • Web mode: uses native BarcodeDetector, and falls back to @zxing/browser.
  • Capacitor mode: uses @capacitor-mlkit/barcode-scanning if installed.
  • Built-in dedupe (cooldownMs) prevents duplicate check-in actions for repeated scans.
  • Deep-link parser supports payload like /board/tickets/scanner?ticket=....
  • Visual scan feedback: green frame + check icon for success, red frame + cross icon for invalid scan.
<script lang="ts">
	import { QrScanner, type ParsedQrPayload } from '@rshval/svelte-components';

	type TicketStatus = 'idle' | 'loading' | 'valid' | 'used' | 'invalid';

	let scannerRef: { pause: () => void; resume: () => void } | null = null;
	let ticketNumber = '';
	let status: TicketStatus = 'idle';
	let message = 'Scan ticket QR to begin check-in';

	async function loadTicketStatus(scanned: ParsedQrPayload) {
		if (!scanned.ticketNumber) {
			status = 'invalid';
			message = 'QR does not contain a check-in ticket number';
			return;
		}

		ticketNumber = scanned.ticketNumber;
		status = 'loading';
		scannerRef?.pause();

		const result = await fetch(`/api/tickets/status?ticket=${ticketNumber}`).then((r) => r.json());
		status = result.status;
		message = result.message;
	}

	async function activateTicket() {
		await fetch('/api/tickets/activate', {
			method: 'POST',
			headers: { 'Content-Type': 'application/json' },
			body: JSON.stringify({ ticketNumber })
		});

		status = 'used';
		message = `Ticket ${ticketNumber} is activated`;
		scannerRef?.resume();
	}
</script>

<QrScanner
	bind:this={scannerRef}
	formats={['qr_code']}
	cooldownMs={2500}
	highlightFrame
	showScanResult
	scanResultDurationMs={1400}
	isSuccessfulScan={(payload) => payload.kind === 'check-in-link' && Boolean(payload.ticketNumber)}
	vibrateOnDetect
	onDetect={loadTicketStatus}
	onError={(error) => (message = error.message)}
/>

<p>{message}</p>

{#if status === 'valid'}
	<button class="btn btn-success" onclick={activateTicket}>Activate ticket</button>
{/if}

If you need lower-level control, use createQrScanner() directly:

import { createQrScanner } from '@rshval/svelte-components';

const scanner = createQrScanner({
	cooldownMs: 2000,
	formats: ['qr_code'],
	onDetect: (payload) => console.log(payload.ticketNumber)
});

await scanner.start();

QrScanner visual feedback props:

  • showScanResult (true by default) — show status icon in the center after each detection.
  • scanResultDurationMs (1400 by default) — how long success/error state is displayed.
  • isSuccessfulScan — custom predicate to mark a payload as successful; if returns false, error state is shown.