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

kystrap

v0.0.12

Published

A SvelteKit component library that turns Zod schemas into fully-wired CRUD interfaces — forms, tables, modals, and MongoDB server actions — with a single schema definition.

Readme

Kystrap

A SvelteKit component library that turns Zod schemas into fully-wired CRUD interfaces — forms, tables, modals, and MongoDB server actions — with a single schema definition.

How it works

Everything in Kystrap flows from one Zod schema. You define your data shape once, attach configuration through .meta(), and Kystrap generates the UI components and server actions automatically.

const UserSchema = zod.object({
  _id: zod.zObjectId(),
  name: zod.string().min(1),
  email: zod.string().email(),
  role: zod.enum(['admin', 'user', 'guest'])
}).meta({
  title: 'User',
  database: { collection: 'users' }
});

That single schema drives:

  • A form with correct input types and validation
  • A table with sortable, filterable columns
  • A modal for inline add/edit
  • Server actions for insert, update, and delete against MongoDB

Installation

pnpm add -D kystrap @iconify-json/tabler @iconify/tailwind4 tailwindcss mongodb daisyui bits-ui

Add to your root CSS file:

@plugin 'daisyui';
@plugin '@iconify/tailwind4' {
	prefix: 'iconify';
	prefixes: tabler;
	scale: 1.5;
}
@import '../../node_modules/kystrap/dist/kystrap.css';
@source '../../node_modules/kystrap/dist/';

For using ThemeToggler components, add the following to your root app.html file:

<script>
	(function () {
		try {
			var theme = localStorage.getItem('theme');
			if (theme === 'theme-dark' || theme === 'theme-light') {
				document.documentElement.setAttribute('data-theme', theme);
			}
		} catch (e) {}
	})();
</script>
%sveltekit.head%

Toast notifications (optional but recommended)

Kystrap ships a Toaster component and an initCookieToast helper that display toast notifications automatically. They integrate with InstantForm and other instants features out of the box, and support multi-page flash messages across redirects.

1. Set up the server-side flash handler in your root +layout.server.ts:

import { loadFlash } from 'kystrap/server';

export const load = loadFlash(() => ({}));

2. Render Toaster and initialize cookie toasts in your root +layout.svelte:

<script lang="ts">
	import { onMount } from 'svelte';
	import { Toaster, initCookieToast } from 'kystrap/functions';

	const { children } = $props();

	onMount(() => {
		initCookieToast();
	});
</script>

<Toaster richColors />
{@render children()}

That's it. Success and error messages returned from instant form actions (or any SvelteKit form action using flash messages) will automatically appear as toast notifications without any additional wiring.


The .meta() system

.meta() is the configuration layer that tells Kystrap how to render and handle each field. It can be applied at the field level or the schema level.

Schema-level meta

Set on the zod.object() itself. Controls the entity name, database target, and description.

const ProductSchema = zod.object({ ... }).meta({
  title: 'Product',              // Used in success messages ("Product created!")
  description: 'Store products', // Optional description
  database: {
    collection: 'products',      // MongoDB collection name (required for server functions)
    db: 'myapp'                  // Database name (optional, uses default if omitted)
  }
});

You can read these values back using the schema utility functions:

import { titleOf, descriptionOf, collectionOf, dbOf } from 'kystrap/schemas';

titleOf(ProductSchema)       // 'Product'
collectionOf(ProductSchema)  // 'products'
dbOf(ProductSchema)          // 'myapp'

Field-level meta

Applied to individual fields with .meta({}). Controls input type, form visibility, table columns, dropdown options, and more.

fieldType — input type override

By default Kystrap infers the input type from the Zod type. Use fieldType to override.

const schema = zod.object({
  _id:       zObjectId(),
  email:     zod.string().meta({ fieldType: 'email' }),
  password:  zod.string().meta({ fieldType: 'password' }),
  bio:       zod.string().meta({ fieldType: 'area' }),
  tags:      zod.array(zod.string()).meta({ fieldType: 'arrayText' }),
  color:     zod.string().meta({ fieldType: 'color' }),
  phone:     zod.string().meta({ fieldType: 'phone' }),
  active:    zod.boolean().meta({ fieldType: 'toggle' }),
  birthday:  zISOdate.meta({ fieldType: 'date' }),
  createdAt: zISODate.meta({ fieldType: 'datetime' }),
  avatar:    zod.string().meta({ fieldType: 'file' }),
  volume:    zod.number().min(0).max(100).default(50).meta({ fieldType: 'slider', sliderProps: { valuePostfix: '%' } }),
});

Available values: text, email, password, number, area, arrayText, color, phone, toggle, date, datetime, file, singleOption, multipleOption, parsedInput, slider

form — form visibility

const schema = zod.object({
  _id: zod.zObjectId().meta({
    form: { hidden: true }       // Never show in any form
  }),
  createdAt: zISODate.meta({
    form: {
      hideOnAdd: true,           // Hide when creating a new record
      hideOnEdit: false          // Show when editing an existing record
    }
  })
});

datatable — column display

const schema = zod.object({
  _id: zod.zObjectId().meta({
    datatable: { hidden: true }  // Hide from table
  }),
  name: zod.string().meta({
    datatable: {
      order: 1,                  // Column position (lower = leftmost)
      label: 'Full Name'         // Custom column header
    }
  }),
  createdAt: zod.date().meta({
    fieldType: 'date',          // Render as date picker in form
    order: 4,
    dateProps: {
      formatString: 'MMM dd, yyyy'  // Date display format in datatable
    }
  })
});

optionProps — dropdown and select options

Three ways to supply options to singleOption / multipleOption fields:

Static options — hardcoded list:

role: zod.enum(['admin', 'user', 'guest']).meta({
  optionProps: {
    staticOptions: [
      { value: 'admin', label: 'Administrator' },
      { value: 'user',  label: 'Regular User' },
      { value: 'guest', label: 'Guest' }
    ]
  }
})

Remote options — fetched from an API endpoint:

country: zod.string().meta({
  optionProps: {
    remoteOptions: {
      endpoint: '/api/countries',
      method: 'GET',
      headers: { 'Authorization': 'Bearer token' },
      responseSchema: zod.array(zod.object({
        code: zod.string(),
        name: zod.string()
      })).transform(opts => opts.map(o => ({ value: o.code, label: o.name })))
    }
  }
})

Load options — from SvelteKit page.data:

category: zod.string().meta({
  optionProps: {
    loadOptions: {
      key: 'categories',         // Reads from page.data.categories
      dataSchema: zod.array(zod.object({
        _id: zod.string(),
        name: zod.string()
      })).transform(opts => opts.map(o => ({ value: o._id, label: o.name })))
    },
    allowAdd: true,              // Let user add new options inline
    addSchema: zod.object({ name: zod.string() })
  }
})

toggleProps — toggle labels

notifications: zod.boolean().meta({
  toggleProps: {
    yes: 'Enabled',
    no: 'Disabled'
  }
})

Other field-level options

name: zod.string().meta({
  order: 1,              // Display order within the form
  alwaysRequired: true,  // Mark required even if schema has .optional()
  readonly: true,        // Render as read-only
  isLabelKey: true       // Use this field as the display label for the record
})

Complete example

Here is a full CRUD setup for a products entity — schema, server actions, and UI in one place.

Schema (shared)

// src/lib/schemas/product.ts
import { zod } from 'kystrap/schemas';

export const ProductSchema = zod.object({
  _id: zod.zObjectId().meta({
    form: { hidden: true },
    datatable: { hidden: true }
  }),
  name: zod.string().min(1).meta({
    order: 1,
    datatable: { order: 1, label: 'Product Name' }
  }),
  price: zod.number().min(0).meta({
    order: 2,
    datatable: { order: 2 }
  }),
  category: zod.string().meta({
    order: 3,
    fieldType: 'singleOption',
    datatable: { order: 3 },
    optionProps: {
      staticOptions: [
        { value: 'electronics', label: 'Electronics' },
        { value: 'clothing',    label: 'Clothing' },
        { value: 'food',        label: 'Food' }
      ]
    }
  }),
  rating: zod.number().min(0).max(5).default(0).meta({
    order: 3,
    fieldType: 'slider',
    sliderProps: {
      showTick: true,
      tickLabelSkips: 1,
      valuePostfix: ' stars'
    }
  }),
  inStock: zod.boolean().default(true).meta({
    order: 4,
    fieldType: 'toggle',
    toggleProps: { yes: 'In Stock', no: 'Out of Stock' },
    datatable: { order: 4 }
  }),
  createdAt: zod.coerce.date().default(new Date()).meta({
    fieldType: 'date',
    form: { hideOnAdd: true },
    dateProps: { formatString: 'MMM dd, yyyy' },
    datatable: { order: 5 }
  })
}).meta({
  title: 'Product',
  database: { collection: 'products' }
});

Server actions

// src/routes/products/+page.server.ts
import { collectionInstantAddEditRemove, collectionFind } from 'kystrap/server/mongodb';
import { ProductSchema } from '$lib/schemas/product';

export const load = async () => {
  const products = await collectionFind(ProductSchema);
  return { products };
};

export const actions = {
  ...collectionInstantAddEditRemove(ProductSchema)
};

Page component

<!-- src/routes/products/+page.svelte -->
<script lang="ts">
  import { InstantTable, InstantWindowForm } from 'kystrap/instants';
  import { ProductSchema } from '$lib/schemas/product';

  let isOpen = $state(false);
  let editData = $state<any>(null);
</script>

<button onclick={() => isOpen = true}>Add Product</button>

<InstantTable
  schema={ProductSchema}
/>

<InstantWindowForm
  schema={ProductSchema}
  bind:open={isOpen}
  data={editData}
/>

This gives you: a sortable, filterable table; an Add button that opens a modal; Edit and Delete per row; and all server actions wired to MongoDB — with no additional code.


Instant components

Which component to use

| Situation | Component | |-----------|-----------| | Standalone form on its own page | InstantForm | | Add record via a modal | InstantWindowForm + button | | Table + add/edit/delete in a modal | InstantTable + InstantWindowForm | | Read-only data display | InstantTable with no window form |

InstantForm

A standalone form rendered inline on the page. Good for dedicated add/edit pages.

<script lang="ts">
  import { InstantForm } from 'kystrap/instants';
  import { UserSchema } from '$lib/schemas/user';
</script>

<InstantForm schema={UserSchema} />
<!-- For editing: -->
<InstantForm schema={UserSchema} data={existingRecord} />

Props: schema (required), data (optional, pre-fills for editing), action (optional, form action name)

InstantWindowForm

A modal dialog that wraps InstantForm. Typically driven by InstantTable, but can be opened manually.

<script lang="ts">
  import { InstantWindowForm } from 'kystrap/instants';
  import { UserSchema } from '$lib/schemas/user';

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

<button onclick={() => isOpen = true}>Add User</button>

<InstantWindowForm
  schema={UserSchema}
  bind:open={isOpen}
/>

Props: schema (required), open (bindable), data (optional, pre-fills for editing), title (optional, overrides dialog title)

InstantTable

A full datatable with built-in add, edit, and delete. Columns are auto-generated from schema .meta({ datatable: { ... } }) configuration.

<InstantTable
  schema={UserSchema}
  bind:open={isOpen}
  bind:editData={editData}
/>

Props: schema (required), open (bindable, connects to InstantWindowForm), editData (bindable, carries row data to form), data (optional, override loaded data), columns (optional, override auto-generated columns), rowActions (optional, custom per-row actions)

What it provides automatically:

  • Add button → opens InstantWindowForm in add mode
  • Edit button per row → opens InstantWindowForm pre-filled with that row
  • Delete button per row → removes record with confirmation

Charts

Kystrap provides configurable chart components powered by LayerChart. Import from $lib/charts.

ChartContainer

A wrapper card with title, optional background icon (Tabler icons), responsive grid column span (1–12), and an optional info popover.

<script lang="ts">
  import { ChartContainer } from '$lib/charts';
  import { ChartPie } from '$lib/charts';
</script>

<ChartContainer title="Revenue" icon="tabler--currency-dollar" colSpan={2}>
  <ChartPie data={revenueData} angle="amount" label="source" type="donut" />
</ChartContainer>

| Prop | Type | Default | Description | |------|------|---------|-------------| | title | string | 'New Orders' | Card heading | | colSpan | number | 4 | Grid column span (1–12) | | icon | string | — | Tabler icon class for decorative background | | iconColor | IconColor | 'primary' | Icon tint: primary | secondary | success | warning | error | info | | children | Snippet | required | Chart or content to render inside the card | | popoverChildren | Snippet | — | Extra content for the info popover |

StatsWrapper

Displays a single numeric stat with optional subtitle and trend indicator.

<script lang="ts">
  import { StatsWrapper } from '$lib/charts';
</script>

<StatsWrapper title="$45,230" subtitle="Total Revenue" trend={{ value: 12.5, direction: 'up', suffix: '%', label: 'vs last month' }} />

| Prop | Type | Default | Description | |------|------|---------|-------------| | title | string | required | Stat value (formatted as you want it displayed) | | subtitle | string | — | Small label below the title | | trend | Trend | — | Trend indicator: { value, direction: 'up' \| 'down', suffix?, label? } | | class | string | — | Additional CSS classes |

ChartTimeline

Multi-purpose time-series chart supporting line, bar, and area chart types. Use the single-series shorthand for simple charts or the series prop for multi-series with mixed types, dual-axis, and bar stacking.

<script lang="ts">
  import { ChartTimeline } from '$lib/charts';
</script>

<!-- Single series: line chart -->
<ChartTimeline data={orders} x="date" y="revenue" />

<!-- Single series: bar chart -->
<ChartTimeline data={orders} x="date" y="revenue" type="bar" />

<!-- Multi-series with mixed types -->
<ChartTimeline data={monthlyData} x="month" series={[
  { type: 'bar', y: 'revenue', color: 'var(--color-primary)' },
  { type: 'line', y: 'profit', color: 'var(--color-success)', area: true },
]} />

| Prop | Type | Default | Description | |------|------|---------|-------------| | data | Record<string, unknown>[] | required | Chart data array | | x | string | required | Date/category field accessor for the x-axis | | y | string | — | Single-series shorthand — value accessor. Omit when using series | | type | 'line' \| 'bar' \| 'area' | 'line' | Single-series chart type | | color | string | 'var(--color-primary)' | Single-series color | | yAxis | 'left' \| 'right' | 'left' | Which y-axis this series uses (dual-axis only) | | curve | boolean | — | Smooth curve interpolation (line/area only) | | area | boolean | — | Fill area under the line (line only) | | series | TimelineSeries[] | — | Multi-series config. Omit y/type/color/curve/area when using this | | height | number | 256 | Chart height in pixels | | padding | { top?, right?, bottom?, left? } | — | Chart padding in pixels | | tooltipMode | 'bisect-x' \| 'band' \| 'bisect-band' | 'band' | Tooltip interaction mode | | tooltip | Snippet | — | Custom tooltip content (receives hovered data point) | | tooltipFormat | string \| object | — | Format for tooltip values ('currency', 'percent', etc.) | | showLabel | boolean | — | Show value labels on each data point | | labelFormat | string \| object | — | Format for value labels ('currency', 'percent', etc.) | | labelPlacement | 'outside' \| 'inside' \| 'center' \| 'smart' | 'outside' | Value label position | | showPoint | boolean | — | Show dot markers at data values (line/area only) | | pointRadius | number | 4 | Point marker radius in pixels | | pointStrokeWidth | number | 2 | Point marker stroke width | | barWidth | number | — | Fixed bar width in pixels (bar only) | | barVariant | 'fill' \| 'outline' \| 'both' | — | Bar visual style (bar only) | | barStrokeWidth | number | — | Bar stroke width (bar only) | | barFillOpacity | number | — | Bar fill opacity (bar only) | | barRadius | number | 6 | Bar corner radius in pixels (bar only) | | showLegend | boolean | — | Show series legend below chart | | legendProps | object | — | Props forwarded to the Legend component | | seriesLayout | 'overlap' \| 'stack' \| 'group' \| 'stackExpand' \| 'stackDiverging' | 'overlap' | Layout for multiple bar series | | xScale | AnyScale | scaleBand().padding(0.4) | X-axis scale factory | | yScale | AnyScale | auto | Y-axis scale factory | | xDomain | DomainType | auto | Explicit x-axis domain override | | yDomain | DomainType | auto | Explicit y-axis domain override | | xAxisProps | object | — | Props forwarded to the bottom (x) axis | | yAxisProps | object | — | Props forwarded to the left (y) axis | | yRightAxisProps | object | — | Props forwarded to the right axis (dual-axis only) | | class | string | — | Additional CSS classes | | style | string | — | Inline styles

ChartPie

Pie and donut chart for proportional data.

<script lang="ts">
  import { ChartPie } from '$lib/charts';
</script>

<ChartPie data={browserData} angle="share" label="browser" />

<ChartPie data={revenueData} angle="amount" label="source" type="donut" showLabel showLegend />

📌 Slice ordering: ChartPie renders slices in the order they appear in the data array — it does not sort internally. For consistent display, sort your data on the server before passing it to the component (e.g., by value descending for largest-to-smallest slices).

| Prop | Type | Default | Description | |------|------|---------|-------------| | data | Record<string, unknown>[] | required | Chart data array | | angle | string | required | Numeric field accessor for arc size | | label | string | required | Category/label field accessor for slice names | | type | 'pie' \| 'donut' | 'pie' | Chart style | | colors | string[] | theme colors | Slice color overrides | | innerRadius | number | 0.6 (donut) | Inner radius: pixel (≥1), percent (0–1), or offset (<0) | | outerRadius | number | auto (half height) | Outer radius in pixels | | padAngle | number | 0 | Angle between arcs in radians | | cornerRadius | number | 0 | Corner radius of arcs in pixels | | showLabel | boolean | false | Show value labels on each slice | | labelFormat | object | — | Arc label config (placement, format, fontSize, calloutLineLength, etc.) | | labelPlacement | 'centroid' \| 'callout' \| 'inner' \| 'middle' \| 'outer' | 'callout' | Label positioning strategy | | tooltipFormat | string \| object | — | Tooltip value format ('currency', 'percent', etc.) | | showLegend | boolean | false | Show legend below chart | | legendProps | object | — | Props forwarded to the Legend component (title, placement, classes, etc.) | | onSliceClick | function | — | Click handler: (e, { data, series }) => void | | height | number | 256 | Chart height in pixels | | class | string | — | Additional CSS classes | | style | string | — | Inline styles

TableMini

A compact mini-table widget for dashboards — ideal for displaying "Recent Orders", "Top Products", or "Latest Signups" inside a ChartContainer. It reads column configuration from your Zod schema's datatable meta, supports row click handlers, row actions snippets, and a "View All" footer link.

<script lang="ts">
  import { ChartContainer, TableMini } from '$lib/charts';
  import { OrderSchema } from './schema';

  let { data } = $props();
</script>

<ChartContainer title="Recent Orders" icon="tabler--shopping-cart">
  <TableMini
    data={data.orders}
    schema={OrderSchema}
    maxRows={5}
    viewAllHref="/orders"
  />
</ChartContainer>

| Prop | Type | Default | Description | |------|------|---------|-------------| | data | T[] | required | Array of typed records | | schema | ZodObject | required | Kystrap Zod schema | | columns | string[] | schema datatable order | Column keys to show (in order) | | labelMap | Record<string, string> | — | Override column labels | | maxRows | number | 5 | Max visible rows before scroll | | viewAllHref | string | — | "View All" link URL | | rowClick | (row) => void | — | Row click handler | | showRowNumber | boolean | false | Show row number column | | rowActions | Snippet<[row]> | — | Extra column with per-row action buttons | | rowActionsLabel | string | 'Actions' | Actions column header | | rowActionsWidth | string | 'w-20' | Actions column width | | locale | DateLocale | 'enGB' | Date formatting locale | | timezone | string | local | Date formatting timezone | | emptyMessage | string | 'No data' | Empty state message | | class | string | 'w-full' | Container CSS classes |

Tip: TableMini automatically resolves field types (date, toggle, singleOption, parsedInput) and datatable.displayParser from the schema meta. No manual column config needed for standard types.


InstantDashboard

Location: $lib/instants/InstantDashboard.svelte

Auto-generates a full dashboard page from your Zod schema's dashboard meta config. Pass one or more schemas — each becomes a titled section with stat cards, charts, and mini-tables defined in the schema's meta({ dashboard: {...} }).

Data is resolved server-side via collectionInstantDashboardDataMany() — define which fields to aggregate, which charts to render, and which tables to show.

<script lang="ts">
  import InstantDashboard from '$lib/instants/InstantDashboard.svelte';
  import { TransactionSchema } from '../schema.ts';

  let { data } = $props();
</script>

<InstantDashboard schemas={[TransactionSchema]} data={data.dashboardData} />

| Prop | Type | Default | Description | |------|------|---------|-------------| | schemas | zod.ZodObject[] | required | Zod schemas with dashboard meta — each becomes a section | | data | Record<string, DashboardWidgetData> | required | Widget data keyed by schema identifier | | locale | DateLocale | 'enGB' | Locale for date formatting in tables | | timezone | string | local tz | Timezone for date formatting in tables |

Server-side data resolution:

// +page.server.ts
import { collectionInstantDashboardDataMany } from '$lib/server/mongodb';

export const load = async () => {
  return {
    dashboardData: await collectionInstantDashboardDataMany([TransactionSchema])
  };
};

Multi-schema with different databases: Pass a second argument with collection → dbName overrides:

const data = await collectionInstantDashboardDataMany(
  [OrdersSchema, UsersSchema],
  { users: 'analytics-db' }
);

Dashboard meta reference

Configure widgets in your schema's .meta({ dashboard: {...} }):

export const TransactionSchema = zod.object({...}).meta({
  title: 'Transaction',
  database: { collection: 'transactions' },
  dashboard: {
    title: 'Transactions Insights',
    cols: 3,
    statCards: [
      {
        field: 'amount',
        label: 'Total Revenue',
        icon: 'tabler--currency-dollar',
        valueFormat: 'currency',
        query: { type: 'sum' }
      }
    ],
    charts: [
      {
        type: 'line',
        title: 'Revenue Over Time',
        xField: 'createdAt',
        yField: 'amount',
        colSpan: 2,
        timelineChartProps: { showPoint: true }
      },
      {
        type: 'pie',
        title: 'By Status',
        angleField: 'amount',
        labelField: 'status',
        pieChartProps: { showLegend: true }
      }
    ],
    tables: [
      {
        title: 'Recent Transactions',
        columns: ['customer', 'amount', 'status'],
        maxRows: 5,
        query: { sort: { createdAt: -1 } }
      }
    ]
  }
});

Stat card aggregation auto-inference: When query.type is omitted, the server infers it from the field's type — 'sum' for number/slider/parsedInput fields, 'count' for everything else.

| Property | Type | Default | Description | |----------|------|---------|-------------| | key | string | collection → title | Unique data lookup key for multi-schema | | title | string | schema's title | Section header | | cols | 2 \| 3 \| 4 | 3 | Grid columns | | statCards | DashboardStatCard[] | auto-discovered | Stat card widget definitions. Omit to auto-discover fields with statCard meta | | charts | DashboardChart[] | — | Chart definitions (timelineChartProps / pieChartProps for customization) | | tables | DashboardTable[] | — | Mini-table widget definitions |

Auto-discovery: If you don't define statCards, InstantDashboard auto-discovers fields with a statCard meta property and creates stat cards for them. The aggregation type is inferred from the field type.

Empty states: When a chart or stat card has no data, InstantDashboard gracefully shows "No data available" instead of a blank chart. Stat cards show when the aggregation returns no results.


Server / MongoDB

All server functions for MongoDB are imported from $lib/server/mongodb.ts.

Low-level collection functions

These functions give you direct, type-safe access to MongoDB collections using your Kystrap schema. The collection and database names are automatically resolved from the schema's .meta({ database: { db, collection } }).

import {
  collection,           // Get typed MongoDB Collection handle
  collectionFind,       // Find documents
  collectionInsertOne,  // Insert a document
  collectionUpdateOne,  // Update a document
  collectionRemoveOne,  // Delete a document
  collectionInstantDashboardData,  // Single-schema dashboard widget data
  collectionInstantDashboardDataMany, // Multi-schema dashboard widget data
} from '$lib/server/mongodb.ts';

collection(schema, [options])

Returns a typed MongoDB Collection handle. The collection is typed with the schema's output (with _id made optional), so all chain operations are fully type-safe.

const users = collection(userSchema);
const doc = await users.findOne({ _id: new ObjectId(id) });

| Param | Type | Description | |-------|------|-------------| | schema | ZodObject | The Kystrap schema describing the collection | | options.dbName | string | Database override. Falls back to MONGODB_DEFAULT_DB env, then 'default' |

Returns: Collection<OptionalId<zOutput<T>>>

collectionFind(schema, [options])

Finds all documents matching the filter and returns them as an array.

const admins = await collectionFind(userSchema, {
  filter: { role: 'admin' },
  findOptions: { limit: 10, sort: { name: 1 } }
});

| Param | Type | Default | Description | |-------|------|---------|-------------| | schema | ZodObject | — | The Kystrap schema | | options.filter | Filter | {} | MongoDB filter query, typed against the schema | | options.findOptions | FindOptions | {} | Projection, sort, limit, etc. | | options.dbName | string | — | Database override |

collectionInsertOne(schema, document, [options])

Inserts a single typed document.

const result = await collectionInsertOne(userSchema, {
  name: 'Alice',
  email: '[email protected]'
});

| Param | Type | Description | |-------|------|-------------| | schema | ZodObject | The Kystrap schema | | document | OptionalUnlessRequiredId<OptionalId<zOutput<T>>> | The document to insert | | options.dbName | string | Database override |

collectionUpdateOne(schema, updateQuery, [filter], [options])

Updates a single document matching the filter using MongoDB update operators.

const result = await collectionUpdateOne(
  userSchema,
  { $set: { name: 'Bob' } },
  { _id: new ObjectId(id) }
);

| Param | Type | Default | Description | |-------|------|---------|-------------| | schema | ZodObject | — | The Kystrap schema | | updateQuery | UpdateFilter | — | MongoDB update operators or aggregation pipeline | | filter | Filter | {} | Filter to match the document | | options.dbName | string | — | Database override | | options.updateOptions | UpdateOptions | {} | Upsert, arrayFilters, etc. |

collectionRemoveOne(schema, [filter], [options])

Deletes a single document matching the filter.

const result = await collectionRemoveOne(userSchema, { _id: new ObjectId(id) });

| Param | Type | Default | Description | |-------|------|---------|-------------| | schema | ZodObject | — | The Kystrap schema | | filter | Filter | {} | Filter to match the document to delete | | options.dbName | string | — | Database override | | options.deleteOptions | DeleteOptions | {} | Collation, hint, etc. |

Instant SvelteKit form actions

These functions generate auto-keyed SvelteKit form actions (e.g. add-users, edit-users, remove-users) that validate incoming data against the schema, perform the database operation, and return success/failure responses consumable by InstantForm or SvelteKit's $page.form.

import {
  collectionInstantAdd,        // SvelteKit form action - add
  collectionInstantEdit,       // SvelteKit form action - edit
  collectionInstantRemove,     // SvelteKit form action - remove
  collectionInstantAddEditRemove // All three combined
} from '$lib/server/mongodb.ts';

Lifecycle hooks (beforeAdd/beforeEdit/beforeRemove) receive an options object { data, event } giving you access to the validated data and the full RequestEvent (which includes event.locals, event.request.headers, event.cookies, etc.).

Aborting with before- hooks: Return a string from any before- hook to abort the operation and display that string as an error message on the form. This is the recommended pattern for authorization guards.

collectionInstantAdd(schema, [options])

Creates an add-{collectionName} form action. The action validates the form data against the schema (via InstantValidate), transforms string IDs to ObjectId, and inserts the document via collectionInsertOne.

export const actions = {
  ...collectionInstantAdd(userSchema, {
    successMessage: 'User created!',
    redirectTo: '/users',
    beforeAdd: async ({ data, event }) => {
      // Authorization guard: return a string to abort
      if (!event.locals.user) return 'You must be logged in';
      // Transform data if needed
    },
    afterAdd: async ({ data, event }) => {
      // Log or notify — runs only after successful insert
    }
  })
};

| Option | Type | Default | Description | |--------|------|---------|-------------| | dbName | string | — | Database override | | errorMessage | string | 'Invalid data' | Message on validation failure | | successMessage | string | '{Title} added!' | Message on success | | redirectTo | string | — | URL to redirect to on success | | redirectOnError | boolean | false | Redirect on validation failure | | beforeAdd | ({ data, event }) => Promise<string \| undefined> | — | Async hook before insert. Receives { data, event }. Return a string to abort and show an error | | afterAdd | ({ data, event }) => Promise<void> | — | Async hook after successful insert. Receives { data, event } |

collectionInstantEdit(schema, [options])

Creates an edit-{collectionName} form action. Automatically:

  • Validates the request is an edit (requires validator.isEdit and validator.dataId)
  • Strips the updateKey field from the data to avoid overwriting the identifier
  • Sets updated_at to the current date if the field exists in the schema
export const actions = {
  ...collectionInstantEdit(userSchema, {
    updateKey: '_id',
    updateKeyType: 'objectId',
    beforeEdit: async ({ data, event }) => {
      if (!event.locals.user?.roles?.includes('admin'))
        return 'Only admins can edit';
    },
    afterEdit: async ({ data, event }) => {
      console.log(`Edited by ${event.locals.user?.name}`);
    }
  })
};

| Option | Type | Default | Description | |--------|------|---------|-------------| | updateKey | string | '_id' | Field used to identify the document | | updateKeyType | 'string' \| 'number' \| 'objectId' | 'objectId' | Type of the identifier value | | dbName | string | — | Database override | | errorMessage | string | 'Invalid data' | Message on validation failure | | successMessage | string | '{Title} edited!' | Message on success | | redirectTo | string | — | URL to redirect to on success | | redirectOnError | boolean | false | Redirect on validation failure | | beforeEdit | ({ data, event }) => Promise<string \| undefined> | — | Async hook before update. Receives { data, event }. Return a string to abort and show an error | | afterEdit | ({ data, event }) => Promise<void> | — | Async hook after successful update. Receives { data, event } |

collectionInstantRemove(schema, [options])

Creates a remove-{collectionName} form action. Extracts the identifier value from the submitted data using removeKey and deletes via collectionRemoveOne.

export const actions = {
  ...collectionInstantRemove(userSchema, {
    beforeRemove: async ({ data, event }) => {
      if (!event.locals.user) return 'Login required to delete';
    },
    afterRemove: async ({ data, event }) => {
      await auditLog(event.locals.user.id, 'deleted');
    }
  })
};

| Option | Type | Default | Description | |--------|------|---------|-------------| | removeKey | string | '_id' | Field used to identify the document to delete | | removeKeyType | 'string' \| 'number' \| 'objectId' | 'objectId' | Type of the identifier value | | dbName | string | — | Database override | | errorMessage | string | 'Invalid data' | Message on validation failure | | successMessage | string | '{Title} removed!' | Message on success | | redirectTo | string | — | URL to redirect to on success | | redirectOnError | boolean | false | Redirect on validation failure | | beforeRemove | ({ data, event }) => Promise<string \| undefined> | — | Async hook before removal. Receives { data, event }. Return a string to abort and show an error | | afterRemove | ({ data, event }) => Promise<void> | — | Async hook after successful removal. Receives { data, event } |

collectionInstantAddEditRemove(schema, [options])

Combines all three CRUD actions into a single spreadable object — the easiest way to wire up full CRUD for a collection.

export const actions: Actions = {
  ...collectionInstantAddEditRemove(userSchema, {
    dbName: 'my-db',
    add: { redirectTo: '/users' },
    edit: { updateKey: '_id' },
    remove: {}
  })
};

| Option | Type | Description | |--------|------|-------------| | dbName | string | Database name shared across all three actions | | add | Omit<InstantAddOptions, 'dbName'> | Options forwarded to collectionInstantAdd | | edit | Omit<InstantEditOptions, 'dbName'> | Options forwarded to collectionInstantEdit | | remove | Omit<InstantRemoveOptions, 'dbName'> | Options forwarded to collectionInstantRemove |


Schema utilities

Imported from kystrap/schemas.

Custom Zod types

import { zod, zObjectId, zObjectIdNullable, zPhone } from 'kystrap/schemas';

zod.zObjectId()          // MongoDB ObjectId field
zod.zObjectIdNullable()  // Nullable ObjectId
zod.zPhone()             // Phone number with formatting

Schema helpers

import { WithObjectId, enumToOptions } from 'kystrap/schemas';

// Add _id: zObjectId() to any schema
const WithId = WithObjectId(UserSchema);

// Convert a zod.enum to an options array for selects
const options = enumToOptions(zod.enum(['admin', 'user']));
// → [{ value: 'admin', label: 'admin' }, { value: 'user', label: 'user' }]

Schema meta readers

import { titleOf, descriptionOf, collectionOf, dbOf } from 'kystrap/schemas';

titleOf(UserSchema)        // 'User'
descriptionOf(UserSchema)  // 'User account'
collectionOf(UserSchema)   // 'users'
dbOf(UserSchema)           // 'myapp'

ObjectId utilities

import { SingleObjectId, MultipleObjectId, isObjectId } from 'kystrap/schemas';

// Schemas for ID-only payloads (e.g. delete by ID)
SingleObjectId    // zod.object({ _id: zObjectId() })
MultipleObjectId  // zod.object({ ids: zod.array(zObjectId()) })

isObjectId('507f1f77bcf86cd799439011')  // true

Server-side schema transforms

Used in +page.server.ts to coerce string ObjectIds from form data to ObjectId instances.

import { TransformObjectId, TransformNullableObjectId, TransformAllObjectId } from 'kystrap/server/schemas';

Fields

Field components pair an input with a label, validation display, and error message. They read field type from the Zod schema and its .meta() configuration.

<script lang="ts">
  import { InputField, AreaField, SingleOptionField, ToggleField, SliderField, DateField } from 'kystrap/fields';
</script>

<InputField {schema} name="name" />
<AreaField {schema} name="bio" />
<SingleOptionField {schema} name="role" options={[{ value: 'admin', label: 'Admin' }]} />
<ToggleField {schema} name="active" />
<SliderField {schema} name="rating" />
<DateField {schema} name="birthday" />

All fields: InputField, AreaField, SingleOptionField, MultipleOptionField, ToggleField, TagField, HiddenField, DateField, SliderField, FieldPopover, SubmitButton


UI components

Imported from kystrap/components.

import { Alert, Dialog, Dropdown, Popover } from 'kystrap/components';

<Alert type="success">Saved!</Alert>
<Alert type="error">Something went wrong.</Alert>

<Dialog bind:open={isOpen} title="Confirm">
  Are you sure?
</Dialog>

Components: Alert, AlertAction, Dialog, Dropdown, Popover, Scrollable, Swaps, ToggleButton, ThemeToggler


ThemeToggler

Imported from kystrap/components.

The ThemeToggler toggles between light and dark themes by setting a data-theme attribute on <html> and persisting the choice in localStorage.

1. Add the init script to app.html

This prevents a flash of the wrong theme on first page load, before Svelte hydrates. Add this inside <head> before %sveltekit.head%:

<script>
	(function () {
		try {
			var theme = localStorage.getItem('theme');
			if (theme === 'theme-dark' || theme === 'theme-light') {
				document.documentElement.setAttribute('data-theme', theme);
			}
		} catch (e) {}
	})();
</script>
%sveltekit.head%

Already included in the Installation section above — make sure it's in your project.

2. Use the component

<script lang="ts">
  import { ThemeToggler } from 'kystrap/components';
</script>

<ThemeToggler />
<!-- Uses default theme names: 'theme-dark' and 'theme-light' -->

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | darkThemeName | string | 'theme-dark' | Theme name applied when dark mode is active | | lightThemeName | string | 'theme-light' | Theme name applied when light mode is active | | content | Snippet<[{ theme: string; isDark: boolean }]> | — | Custom snippet for the toggle icon/ content |

Custom theme names

If your DaisyUI config uses different theme names, pass them via props:

<ThemeToggler darkThemeName="dracula" lightThemeName="corporate" />

Custom toggle content

Use the content snippet prop to render your own icon or markup. It receives { theme, isDark } so you can react to the current state:

<script lang="ts">
  import { ThemeToggler } from 'kystrap/components';
</script>

<ThemeToggler>
  {#snippet content({ isDark })}
    <span class="iconify size-6 {isDark ? 'tabler--moon' : 'tabler--sun'}" />
  {/snippet}
</ThemeToggler>

When no content snippet is provided, the component renders a sun icon in dark mode and a moon icon in light mode (with svelte/transition:fade animation) as fallback.


Datatable

The raw Datatable component, for cases where you want full control over columns and data without the Instant layer.

<script lang="ts">
  import { Datatable } from 'kystrap/datatable';
  import type { ColumnDef } from 'kystrap/datatable';

  const data = [...];

  const columns: ColumnDef<any>[] = [
    { accessorKey: 'name', header: 'Name' },
    { accessorKey: 'email', header: 'Email' }
  ];
</script>

<Datatable {data} {columns} />

Types: ColumnDef, RowSelectionState, ColumnFiltersState, SortingState

Helpers: renderComponent, renderSnippet, dataTableSelectorColumn


Utility functions

Imported from kystrap/functions.

import { toast, debounce, formatDate, simpleDelay } from 'kystrap/functions';

toast.success('Saved!');
toast.error('Failed.');

const debouncedSearch = debounce(search, 300);

formatDate(new Date(), 'MMM dd, yyyy');  // 'Jun 09, 2026'

await simpleDelay(500);  // wait 500ms

Date utilities: getLocalTimeZone, today, todayLocal, fromToday, toToday, dateTimezoned, formatDate, formatRelativeDate, nowDate


Low-level inputs

Bare input components without label or validation wrapper. Use these when building custom form layouts.

import { Input, Select, Area, Toggle, TagInput, Combobox, DateInput } from 'kystrap/inputs';

<Input bind:value={name} placeholder="Name" />
<Select {options} bind:value={selected} />
<Toggle bind:checked={active} />

License

MIT