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

@deutschemodelunitednations/munify-resolution-editor

v0.2.1

Published

UN Resolution Editor component for MUNify applications

Readme

@deutschemodelunitednations/munify-resolution-editor

A Svelte 5 component library for creating and editing UN-style resolutions. Built for Model United Nations conferences.

Features

  • Store-based architecture — editor components consume a ResolutionStore interface; swap between local-only JSON and Y.js-backed real-time collaboration without changing the UI
  • Full Resolution Editor — preamble + operative clauses, recursive sub-clauses up to 4 levels
  • Real-time co-editing (optional) — character-level collaborative typing via the /yjs subpath, including cursor-preserving CRDT bindings and remote-user awareness
  • Phrase Validation & Suggestions — validate clause openings against UN vocabulary, inline autocomplete
  • Import from Text — parse plain text or LLM-formatted resolutions
  • Preview & Print — official UN document format, page-broken via pagedjs
  • Customizable — i18n, custom phrases, snippet extension points
  • Type-safe — full TypeScript + Zod schema validation

Installation

bun add -d @deutschemodelunitednations/munify-resolution-editor

Note: install as a dev dependency — SvelteKit bundles components at build time.

Peer Dependencies

| Peer | Required for | Optional? | | --------------- | ------------------- | --------- | | svelte ^5.0.0 | always | no | | yjs ^13.6.0 | /yjs subpath only | yes | | y-protocols | /yjs subpath only | yes |

If you only use the native (JSON) store you do not need to install yjs / y-protocols.

Styling Setup

The library uses Tailwind CSS utilities. Tailwind needs to scan the library's components.

Tailwind v4 (recommended)

@import 'tailwindcss';
@import '@deutschemodelunitednations/munify-resolution-editor/tailwind.css';
@plugin "daisyui";

Tailwind v3

export default {
	content: [
		'./src/**/*.{html,js,svelte,ts}',
		'./node_modules/@deutschemodelunitednations/munify-resolution-editor/dist/**/*.svelte'
	]
};

Architecture

The editor is UI-only. Persistence and collaboration are concerns of the application that hosts it. Two store implementations bridge that gap:

┌──────────────────────────────────────────────────────────────┐
│   <ResolutionEditor store={...} presence={...} … />          │
│   ClauseEditor / OperativeClauseEditor / SubClauseEditor     │
│   (read store.snapshot, call store.addPreambleClause(), …)   │
└────────────────────────┬─────────────────────────────────────┘
                         │ ResolutionStore interface
            ┌────────────┴────────────┐
            ▼                         ▼
   createNativeStore           createYjsStore
   ─────────────────           ────────────────
   plain $state<Resolution>    Y.Doc + Y.Map + per-clause Y.Text
   onChange(snapshot)           Y.transact() on every mutator
   no peers                     awareness presence adapter

A ResolutionStore exposes:

  • snapshot: Resolution — reactive Svelte 5 $state value
  • typed mutators (addPreambleClause, updateTextBlock, indentSubClause, …)
  • getTextHandle(loc) — returns a TextHandle whose bindTextarea(el) is a no-op for the native store and a CRDT binding for the Y.js store
  • replaceResolution(next) — bulk replace, preserving clause ids where possible
  • destroy() — release subscriptions

When to use which store

| Use case | Store | | ------------------------------------------------------ | ------------------------------------------------------- | | Single-user editor, save-on-blur to your DB | createNativeStore | | Inline mini-editor in a modal (e.g. amendment compose) | createNativeStore | | Live multi-user co-editing of a working paper | createYjsStore | | Server-side mutations against a paper's canonical doc | createYjsStore (against the same Y.Doc you persist) |


Usage

Native (single-user)

<script lang="ts">
	import {
		ResolutionEditor,
		createNativeStore,
		createEmptyResolution,
		type Resolution
	} from '@deutschemodelunitednations/munify-resolution-editor';
	import { germanLabels } from '@deutschemodelunitednations/munify-resolution-editor/i18n/de';
	import {
		germanPreamblePhrases,
		germanOperativePhrases
	} from '@deutschemodelunitednations/munify-resolution-editor/phrases/de';

	let { initialContent }: { initialContent?: Resolution } = $props();

	const store = createNativeStore(initialContent ?? createEmptyResolution('General Assembly'), {
		onChange: (snapshot) => {
			// Persist to your backend, debounced if you like.
			void saveToServer(snapshot);
		}
	});
</script>

<ResolutionEditor
	{store}
	editable
	labels={germanLabels}
	preamblePhrases={germanPreamblePhrases}
	operativePhrases={germanOperativePhrases}
/>

The store owns a Svelte 5 $state, so store.snapshot is reactive — components re-render automatically. Don't keep a separate $state<Resolution> outside the store.

Y.js (real-time collaboration)

<script lang="ts">
	import * as Y from 'yjs';
	import { WebsocketProvider } from 'y-websocket';
	import { ResolutionEditor } from '@deutschemodelunitednations/munify-resolution-editor';
	import {
		createYjsStore,
		createAwarenessPresence
	} from '@deutschemodelunitednations/munify-resolution-editor/yjs';
	import type {
		ResolutionStore,
		PresenceAdapter
	} from '@deutschemodelunitednations/munify-resolution-editor';

	let { paperId, currentUser }: { paperId: string; currentUser: { id: string; name: string } } =
		$props();

	let store = $state<ResolutionStore | null>(null);
	let presence = $state<PresenceAdapter | null>(null);
	let synced = $state(false);

	$effect(() => {
		const doc = new Y.Doc();
		const provider = new WebsocketProvider(`wss://${location.host}/api/ws/yjs`, paperId, doc);

		const s = createYjsStore(doc); // no `seed` — server delivers initial state
		const p = createAwarenessPresence({ awareness: provider.awareness, user: currentUser });

		const onSynced = (v: boolean) => (synced = v);
		provider.on('synced', onSynced);
		store = s;
		presence = p;

		return () => {
			provider.off('synced', onSynced);
			s.destroy();
			provider.destroy();
			doc.destroy();
			store = null;
			presence = null;
			synced = false;
		};
	});
</script>

{#if !synced || !store}
	<div>Connecting…</div>
{:else}
	<ResolutionEditor {store} presence={presence ?? undefined} editable />
{/if}

Important: gate the editor on synced. Until the WS handshake completes the local Y.Doc has no root structure, and mutators that target it will silently no-op (e.g. addPreambleClause returns without inserting).

Preview only

<script lang="ts">
	import { ResolutionPreview } from '@deutschemodelunitednations/munify-resolution-editor';
</script>

<ResolutionPreview
	{resolution}
	headerData={{
		conferenceName: 'Model United Nations',
		committeeName: 'Security Council',
		topic: 'International Peace and Security',
		documentNumber: 'S/RES/2026/1'
	}}
/>

ResolutionPreview is a pure render — no store needed.


Server-side mutations (Y.js mode)

When the host application needs to mutate a paper from the server (e.g. applying an approved amendment, transitioning status), it should mutate the canonical Y.Doc directly, not the JSON projection. The library exports the conversion helpers:

import {
	jsonToYDoc,
	yDocToJson,
	replaceResolution as replaceYDocResolution
} from '@deutschemodelunitednations/munify-resolution-editor/yjs';

A typical server flow looks like:

import * as Y from 'yjs';
import {
	yDocToJson,
	replaceResolution
} from '@deutschemodelunitednations/munify-resolution-editor/yjs';

// inside an async server-side function with the doc in hand
doc.transact(() => {
	const before = yDocToJson(doc);
	const after = applyAmendment(before, amendment); // pure JSON transform
	replaceResolution(doc, after); // structural diff, preserves ids
}, 'server');

The 'server' origin tag lets WS sync-loop guards (which ignore their own echo) distinguish server mutations from peer broadcasts.

CHASE has a reference implementation at src/api/yjs/server.ts (ref-counted in-memory cache, debounced persistence to a bytea column, idle eviction). See MIGRATION.md for a porting checklist.


API Reference

Stores

import {
	createNativeStore,
	createEmptyNativeStore,
	type ResolutionStore,
	type TextHandle,
	type TextLocation,
	type ClausePath,
	type SubclausesBlockPath,
	type OutdentResult,
	type PresenceAdapter,
	type PresenceUser,
	type PresenceInfo,
	type NativeStoreOptions
} from '@deutschemodelunitednations/munify-resolution-editor';

import {
	createYjsStore,
	createAwarenessPresence,
	jsonToYDoc,
	yDocToJson,
	replaceResolution,
	bindYTextToTextarea,
	ROOT_KEY,
	type YjsStoreOptions,
	type AwarenessPresenceOptions
} from '@deutschemodelunitednations/munify-resolution-editor/yjs';

Components

import {
	ResolutionEditor,
	ResolutionPreview,
	ResolutionDocumentHeader,
	ResolutionDocumentFooter,
	ClauseEditor,
	OperativeClauseEditor,
	SubClauseEditor,
	PhraseLookupModal,
	PhraseSuggestions,
	ImportModal,
	ResolutionPrintPreview
} from '@deutschemodelunitednations/munify-resolution-editor';

Schema & Types

import {
	type Resolution,
	type PreambleClause,
	type OperativeClause,
	type SubClause,
	type ClauseBlock,
	type TextBlock,
	type SubclausesBlock,
	type ResolutionHeaderData,
	type AmendmentOverlay,
	ResolutionSchema,
	createEmptyResolution,
	createEmptyOperativeClause,
	createEmptyPreambleClause,
	createEmptySubClause,
	createTextBlock,
	createSubclausesBlock,
	getSubClauseLabel,
	isLegacyResolution,
	migrateResolution,
	validateResolution
} from '@deutschemodelunitednations/munify-resolution-editor/schema';

Phrases

import {
	germanPreamblePhrases,
	germanOperativePhrases,
	englishPreamblePhrases,
	englishOperativePhrases
} from '@deutschemodelunitednations/munify-resolution-editor/phrases';

i18n

import type { ResolutionEditorLabels } from '@deutschemodelunitednations/munify-resolution-editor/i18n';
import {
	germanLabels,
	englishLabels
} from '@deutschemodelunitednations/munify-resolution-editor/i18n/de';

Extension Points

Editor render slots use Svelte 5 snippets:

<ResolutionEditor {store}>
	{#snippet clauseToolbar({ clause, index })}
		<button onclick={() => addAmendment(clause)}>Add Amendment</button>
	{/snippet}

	{#snippet clauseAnnotations({ clause, index })}
		{#if hasAmendments(clause)}
			<div class="badge badge-warning">Has amendments</div>
		{/if}
	{/snippet}

	{#snippet previewHeader({ resolution, headerData })}
		<div class="custom-header">…</div>
	{/snippet}

	{#snippet previewFooter({ resolution })}
		<div class="signatures">…</div>
	{/snippet}
</ResolutionEditor>

Available snippets: clauseToolbar, preambleClauseToolbar, clauseAnnotations, preambleAnnotations, afterPreambleClause, afterOperativeClause, betweenOperativeClauses, previewHeader, previewFooter.


Migrating from 0.1.x

The 0.1 API took resolution + editable + onResolutionChange props directly on ResolutionEditor. In 0.2 the editor consumes a ResolutionStore and the consumer wires persistence into the store.

See MIGRATION.md for the full upgrade path including a removed-props table and search-and-replace recipes.


Development

bun install
bun run dev        # demo SvelteKit app
bun run package    # build the library
bun run check      # svelte-check
bun test

License

MIT — Deutsche Model United Nations e.V.