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

@mark2form/catalog

v0.4.0

Published

De productcatalogus van Mark2Form: producten, modi, slugs, uitvoerformaten, staffels per modus, de btw-regel, de proefperiode, het jaarplan en de welkomstactie.

Readme

@mark2form/catalog

Wat Mark2Form verkoopt, als data: categorieën, producten, slugs, uitvoerformaten, de staffels per product, de btw-regel, de proefperiode, het jaarplan en de welkomstactie. Klein, nul dependencies, gepubliceerd op npm (public scope @mark2form).

Structuurplan §5.5. De staffels zelf zijn §9.1.

Waarom dit package bestaat

Een prijs staat op drie plekken: de prijspagina van de site, het portaal en straks de factuur. Drie plekken die hetzelfde moeten zeggen, dus één bron (§3 principe 3) — een prijs die op de site anders staat dan op de factuur is geen weergavefout maar een geschil.

Nul dependencies is een eis en geen toeval: mark2form-website importeert dit build-time om een prijstabel te renderen. Zo'n site hoort daarvoor geen schema-bibliotheek te installeren. Dit package hangt dus ook niet aan @mark2form/contracts.

De twee lagen

Eigenaarsbesluit Pascal, 21-9-2026:

Neon Studio            de CATEGORIE: hier beheer je je instellingen
├── Neon Logo          product, eigen staffel
└── Neon Tekst         product, eigen staffel

Die twee lagen zaten er al; ze heetten alleen anders. De bovenlaag heette "product" — PRODUCT_IDS, PRODUCTEN — en heet nu CATEGORIE_IDS / CATEGORIEEN; de oude namen blijven als @deprecated bestaan en zijn exact dezelfde bindingen. De onderlaag heet in code nog Modus, omdat dat de as is waarop gefactureerd wordt: STAFFELS is erop gesleuteld, DownloadCompleted.modus draagt de waarde over de lijn en download_ledger heeft er een kolom van. Dat woord hernoemen raakt een opgeslagen payload-veld op het geldpad en is dus een eigen besluit.

De ids blijven neon, logo en tekst. Een id staat in de database en in elke outbox-rij; een naam staat op een scherm. De categorie met id neon heet dus "Neon Studio", en dat is geen inconsistentie maar het verschil tussen een sleutel en een naam.

Een tweede categorie is een rij in CATEGORIEEN plus rijen in MODI en STAFFELS, geen herbouw. Eén regel daarbij: productids zijn globaal uniek, want de staffel en de factuurregel dragen alleen het product-id en niet de categorie. categorieVanModus() bewaakt dat elk product bij precies één categorie hoort.

Consumeren

// in de monorepo
{ "dependencies": { "@mark2form/catalog": "workspace:*" } }
# erbuiten (de marketingsite)
npm install @mark2form/catalog

In de werkkopie wijst package.json naar ./src/..., in de tarball via publishConfig naar ./dist/... (ESM + .d.ts, tsup). Smal importeren mag: @mark2form/catalog/staffels, /producten, /talen, /proef, /jaarplan, /welkomstactie.

import { BTW_REGEL, euroUitCenten, maandbedragCenten, staffelRijen } from "@mark2form/catalog";

maandbedragCenten("logo", 42);        // 10000
euroUitCenten(maandbedragCenten("logo", 42)); // "€ 100"
staffelRijen("tekst");                // [{ van: 1, tot: 10, bedragCenten: 500 }, …]
BTW_REGEL.percentage;                 // 21

De staffels

Eén ding telt: een gedownload productiebestand, per modus, per kalendermaand (Europe/Amsterdam). Herdownload van dezelfde ontwerprevisie in dezelfde maand telt niet opnieuw, ook niet in een ander formaat; een nieuwe revisie wel. Retries, mislukte downloads en demo's tellen nooit. Traces, vision-calls, adviesgesprekken en design-hulp tellen nooit voor de factuur.

| Downloads/maand | Neon Logo | Neon Tekst | |---|---|---| | 0 | € 0 | € 0 | | 1–10 | € 25 | € 5 | | 11–30 | € 50 | € 10 | | 31–75 | € 100 | € 20 | | 76–149 | € 175 | € 35 | | 150+ | € 250 | € 50 |

Volume, niet cumulatief: je betaalt per modus precies één bedrag voor de maand — dat van de staffel waarin je uitkomt. Boven 150 blijft dat bedrag gelijk; er is geen toeslag, en dat is een keuze: een maker die groeit moet niet bang hoeven zijn voor zijn eigen succes. Geen minimum, geen vaste maandfee.

Alle bedragen staan in centen en zijn exclusief btw. De btw-regel hoort één keer paginabreed te staan (§4.3), niet zes keer achter een prijs — daarvoor is BTW_REGEL.

staffelVoor en maandbedragCenten gooien op een onbekende modus of een onbruikbaar aantal. Dat is met opzet: een stille terugval zou een rekenfout stroomopwaarts als "€ 0" op de prijspagina zetten en niemand die het merkt. Liever een build die faalt.

De proefperiode

Besluit Pascal, 23-9-2026:

| Feit | Waarde | Betekenis | |---|---|---| | dagen | 7 | zo lang duurt de proef | | betaalmethodeNodig | false | de proef start zonder betaalgegevens | | downloads | false | productiebestanden downloaden kan alleen met een abonnement | | maxGebruikers | 1 | collega's uitnodigen kan pas met een betaald abonnement | | liveOpEigenSite | false | tijdens de proef alleen zelf testen in het portaal | | naAfloopZonderBetaling | "uit" | na de proef zonder betaling gaat de configurator helemaal uit |

import { PROEF } from "@mark2form/catalog/proef";

PROEF.dagen;                  // 7
PROEF.naAfloopZonderBetaling; // "uit"

Dit is de VORM van de proef, niet de stand van een klant. Of een bedrijf nu in zijn proef zit leidt het portaal af (toegang sinds + PROEF.dagen, en het abonnement); de runtime ziet dat als de claim download_blokkade in het embed-token (@mark2form/contracts/embed-jwt). De site, het portaal en de runtime lezen het aantal dagen hier — een site die iets anders belooft dan de grendel doet, is een klant die zich bedrogen voelt.

Het jaarplan

Besluit Pascal, 23-9-2026. Per modus een jaarplan naast het maandplan (M2F_PLANNEN = ["maand", "jaar"]):

| Feit | Waarde | Betekenis | |---|---|---| | kortingProcent | 10 | 10% korting op élke staffelprijs; je betaalt nog steeds naar gebruik | | looptijdMaanden | 12 | het jaar ligt 12 maanden vast | | betaling | "maandelijks" | per maand afgerekend, niet een jaar vooruit | | verlengen | "automatisch" | daarna telkens 12 maanden erbij; opzeggen = niet verlengen | | herinneringDagenVooraf | 30 | de herinneringsmail gaat 30 dagen vóór het einde uit | | minimumPerMaand | "geen" | nog open. "eerste-staffel": een maand zonder downloads kost de eerste staffel met korting | | naOpzeggen | "maandelijks" | nog open. na een opgezegd jaar door op de maandprijzen; "stoppen": beëindigen |

| Downloads/maand | Neon Logo (jaar) | Neon Tekst (jaar) | |---|---|---| | 1–10 | € 22,50 | € 4,50 | | 11–30 | € 45 | € 9 | | 31–75 | € 90 | € 18 | | 76–149 | € 157,50 | € 31,50 | | 150+ | € 225 | € 45 |

import { JAARPLAN, jaarMaandbedragCenten, staffelRijenVoorPlan } from "@mark2form/catalog/jaarplan";

jaarMaandbedragCenten("logo", 42);      // 9000
staffelRijenVoorPlan("tekst", "jaar");  // [{ van: 1, tot: 10, bedragCenten: 450 }, …]
staffelRijenVoorPlan("tekst", "maand"); // hetzelfde als staffelRijen("tekst")
JAARPLAN.minimumPerMaand;               // "geen"

De jaarbedragen worden afgeleid uit STAFFELS (jaarStaffels), niet overgetypt; een korting die geen hele centen oplevert gooit in plaats van af te ronden. jaarMaandbedragCenten en staffelsVoorPlan volgen dezelfde gooi-regels als staffelVoor, en een onbekend plan is een fout, nooit stil het maandplan. Of een klant NU een jaarplan heeft en tot wanneer, is toestand: dat staat op subscriptions (m2f_plan, looptijd_tot, verlengen, herinnerd_op) en wordt beheerd in @mark2form/billing.

De opzegregel staat hier wél, omdat het scherm, de opzegknop en de worker precies dezelfde moeten volgen. looptijd_tot is een EXCLUSIEF einde; opzeggen (of het terugdraaien) kan tot en met 23:59:59 Europe/Amsterdam van de kalenderdag ervóór:

import { laatsteWijzigdag, magVerlengenWijzigen } from "@mark2form/catalog/jaarplan";

const tot = new Date("2027-10-01T10:00:00Z");
laatsteWijzigdag(tot);                                         // "2027-09-30"
magVerlengenWijzigen(tot, new Date("2027-09-30T21:59:59Z"));   // true  (23:59:59 in Amsterdam)
magVerlengenWijzigen(tot, new Date("2027-09-30T22:00:00Z"));   // false (1 oktober 00:00)

De welkomstactie

Besluit Pascal, 23-9-2026 (WENS-9), letterlijk: "Nieuwe klanten die tijdens hun proefweek of tot 7 dagen erna een abonnement afsluiten, krijgen 20% korting op de eerste factuur, bij maandelijks én bij het jaarplan. Eén keer per bedrijf, niet te combineren met andere acties."

| Feit | Waarde | Betekenis | |---|---|---| | id | "welkom-20" | de sleutel in acties_gebruik (migratie 0084); niet hernoemen bij een nieuw percentage | | procent | 20 | korting op de eerste échte factuur (de eerste met een bedrag) | | vensterNaProefDagen | 7 | zo lang na de proef telt afsluiten nog | | eenmaligPerBedrijf | true | de database dwingt het ook af: unique (bedrijf_id, actie) | | combineerbaar | false | staat er al een andere korting op, dan vervalt de actie |

Het venster is start proef + PROEF.dagen + 7 dagen, in de Amsterdamse dag: de laatste dag is de Amsterdamse kalenderdag waarop gestart_op + 14 × 24 uur valt, en die dag telt helemaal mee. De proef telt in blokken van 24 uur, net als het portaal het einde van de proef uitrekent.

import { komtInAanmerking, welkomstDagenOver, welkomstVenster, WELKOMSTACTIE } from "@mark2form/catalog/welkomstactie";

const start = new Date("2026-10-01T13:00:00Z");          // 1 oktober, 15:00 in Amsterdam
welkomstVenster(start);                                   // { eersteDag: "2026-10-01", laatsteDag: "2026-10-15" }
welkomstDagenOver("2026-10-15", new Date("2026-10-11T10:00:00Z")); // 5 — "Nog 5 dagen"
komtInAanmerking({ proefGestartOp: start, nu: new Date("2026-10-15T21:59:59Z"), alGebruikt: false, andereActie: false }); // "ja"
komtInAanmerking({ proefGestartOp: start, nu: new Date("2026-10-15T22:00:00Z"), alGebruikt: false, andereActie: false }); // "buiten-venster"
WELKOMSTACTIE.procent;                                    // 20

komtInAanmerking geeft "ja" of de reden waarom niet, in deze vaste volgorde: geen-proef → buiten-venster → al-gebruikt → andere-actie. Hij gooit op een onbruikbare datum en op een stand die geen boolean is — een onbekende stand is geen "nee".

Of een bedrijf de actie al kreeg en op welke factuur, is toestand: dat staat in acties_gebruik en wordt beheerd in @mark2form/billing/welkomstactie. Daar staat ook HOE de korting op de factuur komt: niet als coupon op het abonnement (die zou op de startfactuur van € 0 opgaan), maar op de eerste échte factuur, bij de webhook invoice.created.

De talen

De eerste taalset is NL/EN/DE/FR/ES/IT/PL (besluit 13-9, §4.3), gebouwd in de volgorde NL/EN → DE/FR → ES/IT/PL. Een taal gaat pas open als de hele reis erin gecontroleerd is — site, auth, portaal, runtime, hulp, foutmeldingen én mails.

Alle zeven sleutels bestaan daarom vanaf dag één. Wat nog niet vertaald is draagt de Engelse tekst en staat in terugval, zodat een site kan tonen dat iets nog niet vertaald is in plaats van te doen alsof het dat wel is:

import { leesTekst, MODI } from "@mark2form/catalog";

leesTekst(MODI.tekst.naam, "nl"); // { waarde: "Neon Tekst", isTerugval: false }
leesTekst(MODI.tekst.naam, "pl"); // { waarde: "Neon Text",  isTerugval: true  }

Een eigennaam (Neon Studio, Neon Logo, DXF) staat nooit in terugval: die is in het Pools niet onvertaald, hij is gewoon hetzelfde woord. "Neon Tekst" is dat wél ("Neon Text", "Neon Texte") en draagt dus wel een terugval — die asymmetrie is eerlijkheid, geen slordigheid. Een echte vertaling toevoegen is één regel metVertalingen(...), en haalt die taal meteen uit de terugval.

Versiebeleid en publiceren

Semver via Changesets, net als @mark2form/contracts: een product, modus, formaat of taal erbij is een minor; een prijs die verandert, een id dat hernoemt of een veld dat verdwijnt is een major — daar hangt een factuur aan.

Publiceren gaat niet met de hand: merge naar main → de release-workflow opent "chore(release): versies bijwerken" → Pascal mergt die → dán pas publish.

pnpm --filter @mark2form/catalog test        # vitest
pnpm --filter @mark2form/catalog typecheck   # tsc --noEmit
pnpm --filter @mark2form/catalog build       # tsup → dist/