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

@jelinek/ui

v0.9.4

Published

JELÍNEK Svelte 5 component kit, generated from the brand guide

Readme

@jelinek/ui

Svelte 5 + Tailwind v4 component kit generated from the JELÍNEK brand guide.

The design authority is assets/css/jelinek.css in the root of this repository (the brand guide site), not this package. This kit reimplements a subset of that CSS as Svelte components; when the two disagree, the guide is right and the kit needs fixing. See "Sync contract" below for how (and how incompletely) that is enforced.

Install

Published public on npmjs.com as @jelinek/ui — no token, no .npmrc, nothing to log into:

pnpm add @jelinek/ui

Up to 0.2.0 it was @jelinek-nabytek-a-matrace/ui on GitHub Packages, which required a personal access token even for public packages, and consuming apps therefore aliased it to the short name. Both the alias and the .npmrc lines are now dead weight — delete them, or pnpm install will keep asking GitHub Packages for a package that is no longer published there:

{
  "dependencies": {
    "@jelinek/ui": "^0.9.2"
  }
}

The package ships component sources and CSS only. It carries no font files — fonts.css merely references /assets/fonts/*.woff2, which the consuming app must serve itself. The fonts are licensed and stay out of the published tarball.

import { Button } from '@jelinek/ui';

Setup in a consuming app

The package's exports map (from package.json):

{
  ".": {
    "types": "./dist/index.d.ts",
    "svelte": "./dist/index.js",
    "default": "./dist/index.js"
  },
  "./theme.css": "./dist/theme/tokens.css",
  "./extras.css": "./dist/theme/extras.css",
  "./base.css": "./dist/theme/base.css",
  "./fonts.css": "./dist/theme/fonts.css"
}

In your app's global CSS entry point:

@import "tailwindcss";
@import "@jelinek/ui/theme.css";
/* @import "@jelinek/ui/extras.css"; -- optional, hand-written theme additions, import after theme.css */
/* @import "@jelinek/ui/base.css"; -- optional but recommended, import after theme.css (and extras.css, if used) */

@source "../node_modules/@jelinek/ui/dist";

theme.css (dist/theme/tokens.css) is generated — see "Sync contract" below — and defines the semantic color/spacing/radius/shadow/typography variables plus the @theme inline block that maps them onto Tailwind utilities. The @source line is required: without it, Tailwind never scans the kit's own compiled class names and utilities the kit relies on (e.g. rounded-(--radius-button)) can be missing from your build. Adjust the relative path to match where node_modules actually sits from your CSS file — the path above assumes a typical src/app.css one level under the app root.

Consumer implication — theme.css pins Tailwind's --spacing to a fixed 4px. Tailwind v4's own default, --spacing: 0.25rem, is rem-relative, so it tracks whatever root font-size is in play. That root is a flat 16px today (see below), which makes 0.25rem and 4px agree — but the agreement is a property of the current root value, not of the system, and before this kit's type-scale rework the root was clamp(13px, 8px + 1.4vw, 16px) and the unpinned default shrank every p-*/px-*/m-*/gap-*/w-* utility, kit-wide, by up to 19% below roughly 571px viewport width. Type scales with the viewport; spacing does not, and the two must not share one lever. This import is required (unlike base.css/extras.css below), which is deliberate: components reproduce guide padding/margin/gap values as bare Tailwind spacing utilities (Card's p-6 for the guide's padding: 24px, for example) and rely on this pin to stay correct at every width, so it has to live in the one file every consumer of the components necessarily imports, not an optional one. If your app sets its own --spacing after this import, components using the bare numeric scale will resize with it — same tradeoff as overriding any other token here.

@jelinek/ui/base.css (dist/theme/base.css) is optional but recommended. Without it, this kit gives you JELÍNEK colours and components but plain markup — a bare <h2>, a <p>, an <a> — still looks generic, because theme.css only defines custom properties, never element defaults. It reproduces the guide's global element layer (*, html, body, main, h1-h6, p, a, img, code, plus the .muted/.muted2/.serif/.small/.tiny/.sub text-role utilities) inside @layer base — the same layer Tailwind's own preflight populates — so import it after tailwindcss (same layer, later source wins the preflight h1-h6 reset) and it will still lose to any Tailwind utility class you apply (text-step2, font-bold, …), since utilities is a later layer than base and layer order always beats specificity. Position relative to theme.css/extras.css doesn't affect that layering — those two are unlayered token files — but importing it after them means every token it references (--font-primary, --text-black, the --step* scale, …) is already defined, including any you've added yourself in extras.css.

Consumer implication — a fixed 16px root font size, with the fluidity on the steps. html's font-size is a flat 16px. It used to be clamp(13px, 8px + 1.4vw, 16px), which made the root — and with it every rem in the system — shrink to 13px on the narrowest phones; the guide now keeps the root fixed so body copy is 16px at every viewport width, and carries the responsiveness on the individual scale steps instead. Every --stepN token (theme.css) is its own clamp() interpolating over 320–1024px viewport width, so headings still grow with the viewport (--step6, i.e. h1, runs 33px → 48px) while --step0 (body) stays 16px and --step-down1/--step-down2 (.small/.tiny) are fixed at 15px/13px. Importing base.css therefore gives plain unstyled markup the same type behaviour as brand.jlnk.cz.

If your app needs the root to follow the reader's own browser font-size setting, override html's font-size (e.g. to 100%) in your own CSS after this import. Be aware of what that does and does not carry: each step's min, max and intercept are in rem and scale with the new root, but its slope is in vw and does not. The interpolation window therefore moves outward in proportion to the root: at a 24px root the 320–1024px range becomes 480–1536px, so a step reaches its (now larger) maximum at a wider viewport, not a narrower one, and the ramp between min and max is no longer a clean scale-up of the 16px curve — still monotonic, still bounded by the scaled min/max, just a different shape in between. The guide keeps 16px for now for a different reason (--header-h/--subnav-h are hand-measured px while the header's own text is rem, so an unpinned root exposes that seam); if you take the override, sanity-check your own layout at a raised browser default rather than assuming. Note that seam is now a floating-header-only problem: since Task 26 the guide's phone header is position: sticky and reserves no space through those constants at all, and re-measuring the raised-root sweep confirms it — at a 24px browser default with font-size: 100% the two glass bars still meet with a 0px seam at every width below 640px, and the overlap above it is a flat 6.5px (measured at 640, 700, 768, 1024, 1440 and 1920px: the fixed header grows to 101.5px against the hand-measured --header-h: 94px). Task 30's move to the designer's 640px ladder removed the anomaly this note used to call out — the old 621px boundary produced a much larger overlap in a narrow band right at the switch, and with header and chapter-nav now flipping on the same 640px query there is no band left. At the pinned 16px root the seam is 0px at every width, 320 to 1920.

Consumer implication — box-sizing: border-box on every element. This matches Tailwind's own preflight exactly, so it changes nothing for an app that already runs preflight; it only matters if you import base.css without Tailwind preflight (or without Tailwind at all), in which case your elements now size padding/border inward instead of the browser's default content-box behaviour.

Not included — scroll-behavior: smooth. The guide sets this on html, but base.css deliberately leaves it out: it's a global scroll behaviour, not a visual default, it isn't load-bearing for anything the guide itself does (its own chapter navigation is cross-page links, not in-page anchors), it can conflict with an app's own scroll management (virtualised lists, scroll-linked animation, router-driven scroll restoration), and the guide doesn't gate it behind prefers-reduced-motion. Add html { scroll-behavior: smooth } yourself, after this import, if you want it.

html also gets a static background-color: var(--bg-white) — the canvas colour the guide reveals on elastic overscroll (rubber-banding past the top/bottom of the page). The guide swaps this dynamically by scroll position via its own <script> (assets/js/jelinek.js); base.css ships only the static starting colour, not that JS behaviour.

body's background-color/color now transition (0.2s ease): if you use this kit's ThemeToggle, flipping data-theme animates instead of snapping. Override body's transition after import if you don't want that.

Consumer implication — this assumes a page shell. body is min-height: 100%; display: flex; flex-direction: column and main is flex: 1 0 auto (html gets height: 100% to make the percentage mean anything) — the guide's sticky-footer trick, so a page with little content still holds its footer to the bottom of the viewport instead of leaving a white gap below it. That means base.css expects your page's direct body children to be exactly: a header, zero or more chrome elements, one <main>, and a footer — the same shape this package's own showcase uses (src/routes/+layout.svelte: SiteHeader, optionally ChapterNav, <main>, SiteFooter, with nothing wrapping them so they're real flex children of body). If your app already owns a different page shell (its own scroll container, a fixed-height app frame, more than one <main>, …), either don't import base.css and take its other rules (typography, links, …) from your own reset, or import it and override body's display/flex-direction/min-height afterwards in your own CSS — everything else base.css provides (headings, links, the text utilities) is independent of this and keeps working.

@jelinek/ui/fonts.css (dist/theme/fonts.css) is optional. It declares @font-face rules assuming the .woff2 files are served from /assets/fonts/ in your app (the same absolute path the brand guide site itself uses). If you host fonts elsewhere, skip this import and declare your own @font-face rules — the kit only depends on the --font-primary/--font-secondary/--font-serif variables, which all have fallbacks.

This package's own showcase is the example of hosting fonts elsewhere-but-really-the-same-place, and it needs two different answers because it's served two ways:

  • Deployed behind the brand Worker at /ui/, /assets/fonts/... already resolves on the same origin for free — wrangler.jsonc serves the whole repo root as static assets, and assets/fonts/*.woff2 (the one canonical source, per assets/fonts/README.md) already lives there. Nothing to configure.
  • Standalone pnpm dev / pnpm preview only know about the svelte/ project, so vite.config.ts adds a small dev-only middleware that reads /assets/fonts/*.woff2 straight out of the repo root's assets/fonts/ and serves it at that same unprefixed path. A static/ symlink was tried first and rejected: this project sets paths.base: '/ui' (it's deployed at /ui/, not the site root), and SvelteKit's dev server serves everything under static/ — symlinked or not — prefixed by that base, so a symlink would only ever be reachable at /ui/assets/fonts/..., never at the unprefixed path fonts.css actually requests. The middleware reproduces the literal production URL instead. Either way, the licensed font files themselves are never copied into svelte/.

Use with htmx / server-rendered HTML

The kit is a Svelte library, but a consuming app doesn't have to be a Svelte app. An htmx project (or any server-rendered site) can use it on two levels; both need one small asset build, because the package ships uncompiled .svelte source (that's what svelte-package produces — see dist/), and someone has to run the Svelte compiler and Tailwind. In an htmx project that someone is a tiny Vite build producing one JS and one CSS file that your server-rendered pages link.

Level 1 — CSS layers only. Import the CSS entry points from "Setup in a consuming app" above (theme.css, optionally extras.css/base.css/ fonts.css) into a stylesheet compiled by Tailwind, and add a @source line pointing at your own template directory so utilities you use in server-rendered markup get generated:

@import "tailwindcss";
@import "@jelinek/ui/theme.css";
@import "@jelinek/ui/base.css";

@source "../node_modules/@jelinek/ui/dist";
@source "../templates";  /* your server's HTML templates */

This gives server-rendered markup the brand's tokens, element defaults (headings, links, body copy) and the full utility vocabulary. It does not give you the components themselves — hand-copying a component's class string into a template will drift the first time the component changes. For anything that looks like a component, use level 2.

Level 2 — components as islands. Interactive components mount into server-rendered pages as Svelte islands. Author each island as a small .svelte wrapper file in your asset bundle — not by constructing props from plain JS — because many components take Snippet props (Button's children, for example), and snippets are markup, which you want to write in a .svelte file anyway:

<!-- islands/ProductTabs.svelte -->
<script>
	import { Tab, Tabs } from '@jelinek/ui';

	let { value = 'popis' } = $props();
</script>

<Tabs bind:value>
	<Tab value="popis">Popis</Tab>
	<Tab value="parametry">Parametry</Tab>
</Tabs>

The entry point mounts islands on load and after every htmx swap, and unmounts them when htmx removes their element — without the cleanup listener, every swap leaks the previous instance:

// islands.js — the single <script type="module"> your layout links
import { mount, unmount } from 'svelte';
import './app.css'; // the stylesheet from level 1
import ProductTabs from './islands/ProductTabs.svelte';

const REGISTRY = { ProductTabs };
const mounted = new WeakMap();

function hydrate(root) {
	const targets = [root, ...root.querySelectorAll('[data-island]')];
	for (const el of targets) {
		if (!el.matches?.('[data-island]') || mounted.has(el)) continue;
		const Component = REGISTRY[el.dataset.island];
		if (!Component) continue;
		const props = el.dataset.props ? JSON.parse(el.dataset.props) : {};
		mounted.set(el, mount(Component, { target: el, props }));
	}
}

// Runs once on page load and again for every htmx swap.
htmx.onLoad(hydrate);

// htmx removes swapped-out elements itself; Svelte has to be told.
document.body.addEventListener('htmx:beforeCleanupElement', (e) => {
	const app = mounted.get(e.target);
	if (app) {
		unmount(app);
		mounted.delete(e.target);
	}
});

Server-rendered pages then place islands declaratively; htmx attributes and islands coexist freely:

<div data-island="ProductTabs" data-props='{"value":"parametry"}'></div>

The Vite project around this is three files (package.json with @jelinek/ui, svelte, vite, @sveltejs/vite-plugin-svelte, @tailwindcss/vite; a vite.config.js with the two plugins and build.rollupOptions.input: 'islands.js'; the islands.js above). Point build.outDir wherever your server serves static assets from.

Two caveats, both inherited from the setup section above: the @source line for the kit's dist/ is still required (island components' own classes live there), and fonts.css still assumes /assets/fonts/ on your origin — host the .woff2 files there or write your own @font-face. And a boundary worth knowing before you commit to it: paired components that talk through Svelte context (Tabs → Tab, RadioGroup → Radio, Container → PageHero) must live inside one island — context does not cross island boundaries, so a Tab in one data-island cannot report to a Tabs in another.

Dark mode and divisions

  • Dark mode: set data-theme="dark" on the document element (or add class .dark — both are supported so pim/admin's existing .dark convention keeps working).
  • Division re-theme: data-division="mattress" swaps the brand's brown accent for magenta (Zdravý spánek / mattress division). data-theme="mattress" is kept as an alias for the same effect, for apps whose existing markup already sets it that way.

ThemeToggle's theme prop defaults to undefined, not 'light'. When left unbound, it seeds itself from whatever is already on the document — document.documentElement.dataset.theme if you've already set data-theme yourself (a persisted choice from localStorage, a cookie, an SSR-rendered attribute), falling back to prefers-color-scheme only if nothing is set — instead of writing a hardcoded default back onto <html> on mount and clobbering whatever you'd already put there. If you do bind theme yourself (bind:theme={yourState}), that binding is authoritative as before; the DOM-seeding only applies when it's left unbound.

Components

Alert, Breadcrumbs, Button, Card, CartDrawer, CartLineItem, ChapterNav, Checkbox, CheckoutProgress, CmsButton, CmsColumns, CmsCover, CmsHeading, CmsImage, CmsMediaText, CmsText, CodeCopy, ConfigSection, Container, ContentLogo, Demo, DimensionSlider, Drawer, EmptyState, Eyebrow, FilterChip, FreeShippingBar, Gallery, Glass, Grid, Icon, Label, ListingToolbar, Modal, OptionTile, OrderSummary, PageHero, Pager, Pagination, Panel, PhotoTile, PriceDisplay, ProductCard, ProductHeader, ProductStickyBar, ProductTabs, QuantityStepper, Radio, RadioGroup, RatingStars, SearchField, Select, SideToggle, SiteFooter, SiteHeader, Skeleton, Slider, StickyBar, StockBadge, Swatch, Switch, Tab, Tabs, Tag, TextField, Textarea, ThemeToggle, Tile, TileScroller, Toaster, TrustBadges.

See src/lib/index.ts for the exact export list and src/lib/components/ for source. Per-component contracts, including the two below, are printed at /ui/api.

Two constraints worth knowing before you use them

Tabs — the scroll arrows are pinned to the WRAPPER, not to the row of pills. When the segments overflow, Tabs grows a wrapper with a back/forward arrow on each side, and those arrows sit at the wrapper's edges. If the row is narrower than its wrapper — which is what happens the moment you constrain it with w-3/4, max-w-* or a mx-auto width — the arrows detach from the pills and point at empty space. Measured: 439px adrift with a 200px row inside a 1082px wrapper, and already 68px off at this showcase's own min-[640px]:w-3/4 call site (invisible there only because nothing overflows at that width). Keep Tabs at the full width of its wrapper anywhere the segments can overflow, or constrain the wrapper rather than the component.

SiteHeader floats at every width, so the page must reserve its height. The bar is absolute below 640px and fixed from 640px up; it never adds padding to a sibling. Reserve --header-h (70px phone, 94px from 640px) plus --subnav-h (58px) when a ChapterNav is present — PageHero and this showcase's +layout.svelte both model it. And below 640px ChapterNav renders nothing at all: pass the same chapter list to SiteHeader's chapterItems so a phone reader can still reach the chapters from the menu panel.

SiteHeader — the way back to the company portal is homeHref, not a button in actions. Company apps built from the templates link back to the rozcestník. Pass its URL as homeHref and the header renders a home icon (from the kit's icon set, no text) as the first item of the bar, at the far left before the logo, at every width including phones. It is a 44px square — the burger's size — rendered through the kit's own ghost/black Button, so hover, transition and the global focus-visible ring match the nav links. homeLabel (default 'Zpět na rozcestník') is its aria-label and title. Without homeHref the header is unchanged. Do not put a "Zpět na rozcestník" Button into actions any more: it sat on the right next to the nav links at a different size (owner's decision, 15. 9. 2026).

<SiteHeader subtitle="Objednávky" items={menu} homeHref="https://jlnk.cz/" />

Sync contract — and its limits

Two automated checks keep this kit from silently drifting away from the guide. Both run in CI (see below) and both are honest about what they do not catch.

pnpm check:tokens (scripts/gen-tokens.js --check) regenerates src/lib/theme/tokens.css from the guide's :root / :root[data-theme="dark"] blocks and fails if the committed file is stale. It also fails the build outright — gen-tokens.js, unchecked — if the guide defines a :root custom property that is in neither TOKEN_MAP nor PASSTHROUGH in scripts/token-map.js. That's deliberate: a brand-new design token (a new color, spacing step, radius, etc.) must become a visible task for someone to map, not a silent omission. tokens.css is generated — never hand-edit it; run pnpm gen:tokens and commit the result. Hand-written theme additions belong in src/lib/theme/extras.css, which the generator never touches.

pnpm check:icons (scripts/gen-icons.js --check) does the same for the interface icon set: src/lib/icons/material-icons.generated.ts is generated from assets/icons/material/ (Google icons as per-icon SVGs plus our own drawings in vlastni/) and fails if it is stale. The generator also refuses any SVG without fill="currentColor" on its root or carrying its own colours — an icon that would not re-theme. The module is generated — never hand-edit it; add or remove SVG files, run pnpm gen:icons, commit. It exists so that <Icon name="…" /> works from the installed package: the earlier import.meta.glob reached outside dist/ and only worked inside this repo (review, 1. 9. 2026). The whole set is ~18 kB gzipped and ships with the package.

pnpm check:css-lock (scripts/check-css-lock.js) hashes the guide's CSS rule groups for two fixed, hardcoded allow-lists in that file: GUARDED — class names (btn, tag, alert, tile, panel, input, grid, site-header, and so on, one entry per class a component in this kit reimplements — including the six text-role utility classes .muted, .muted2, .serif, .small, .tiny, .sub, whose "component" is src/lib/theme/base.css itself) — and GUARDED_ELEMENTS — bare type selectors (*, html, body, main, h1-h6, p, a, img, code) that base.css also reproduces and that have no class for GUARDED to match. Both are compared against component-css.lock.json. If a guarded name's rules change in the guide, the check fails and names which component needs review (.grid rules changed for a class, body rules changed for an element — no leading dot); --write refreshes the lock once you've handled it.

The gap, stated plainly: check:css-lock only re-checks names that are already on GUARDED or GUARDED_ELEMENTS. It cannot detect that a brand-new component-shaped class — or bare element rule — was added to the guide — say, a designer adds .badge-new-something { ... } to jelinek.css for a component this kit doesn't have yet, or a new blockquote { ... } that base.css doesn't reproduce. That rule gets no bucket in the hash and the check reports "no drift", exit 0, with nothing in the kit reviewing it. This was verified by executing exactly that experiment during the final branch review. It is not a bug: reliably telling "this new CSS rule is a component that needs tracking" from "this is a one-off utility" cannot be inferred from selector shape alone, so GUARDED and GUARDED_ELEMENTS stay manual allow-lists rather than something auto-detected. In practice this means: when you add a new component to this kit (or extend base.css), add its guide class name to GUARDED — or its bare element name to GUARDED_ELEMENTS — in scripts/check-css-lock.js and run --write — don't rely on the check to remind you it's missing. See the header comment in that file for the full reasoning.

CI

.github/workflows/ui.yml runs on pull requests touching svelte/** or assets/css/jelinek.css, and on push to master. It does not run on a bare feature-branch push (no PR yet) — only PR and master events trigger the verify job: token drift, CSS-lock drift, check, test, build, build:showcase (the only step that actually compiles Tailwind and prerenders — see its own comment in the workflow for why it's separate from build), and finally a Playwright e2e suite (e2e/theme-division.spec.ts) run against that showcase build — browser- level regression coverage for the theme × division custom-property cascade that pnpm test's jsdom environment structurally cannot check (jsdom never resolves a CSS custom-property cascade at all).

The publish job is separate and gated on a tag matching ui-v* — it does not run on ordinary merges to master, only when someone pushes a ui-v* tag, and it also depends on verify passing first. It publishes to npmjs.org with --access public.

Releasing

  1. Bump version in svelte/package.json, commit, push master.
  2. git tag ui-v<version> && git push origin ui-v<version>.
  3. verify runs, then publish — nothing else to do.

The tag is what records which commit a version came from, so push it even if a release has to be finished by hand.

How the publish authenticates

Trusted publishing (GitHub Actions OIDC), no secret. The job asks GitHub for an OIDC token and npm exchanges it for a short-lived publish credential. Registered on npmjs under the package's Trusted publisher: org JELINEK-nabytek-a-matrace, repo brand-guide, workflow filename ui.yml, environment left empty. All three are checked on every publish — renaming ui.yml, or adding an environment: to the publish job, breaks releases until the publisher entry is edited to match.

Two consequences worth knowing:

  • The job runs npm publish, not pnpm publish. The OIDC exchange lives in the npm CLI (11.5.1+, which the job installs explicitly); pnpm would look for an auth token, find none, and fail.
  • Provenance stays off (NPM_CONFIG_PROVENANCE=false). npm can only attest a build from a public repository and this one is private (licensed fonts). Trusted publishing does not require provenance.

There is no NPM_TOKEN secret any more. It never worked: npm requires a second factor for every publish, and a granular token only satisfies that if "Bypass two-factor authentication (2FA)" was ticked when the token was created — a setting that cannot be added afterwards, on a mechanism npm is retiring outright (account changes Aug 2026, direct publishing Jan 2027). Without it the registry answered a PUT with a misleading E404 Not Found in CI. 0.3.0 through 0.5.0 were released by hand because of it.

0.5.1 was the first release published by CI, which settles the doubt this section used to carry: a private repository does not block trusted publishing. Only the provenance attestation needs a public repo, and that is opt-out, not a precondition.

Fallback: publishing by hand

If the job fails, the release is not stuck — publish from a machine that can reach a browser:

cd svelte && npm publish --access public --auth-type=web

That prints a npmjs.com/auth/cli/… URL; approving it there with a passkey (or an authenticator app) completes the publish. --otp=<code> works too, but only with an authenticator app — a passkey produces no code.

Known design debt

The guide does not yet define everything this kit's consumers need. See:

  • /audit.html#plan-prace — the single work plan (formerly the two backlog documents): missing interface components, the CMS content blocks still to be designed, admin components, with priority and deadline.

Five components in this kit have no corresponding CSS in the guide and were necessarily invented rather than reimplemented: Checkbox, Radio, RadioGroup, Slider, and ProductCard. Treat their current appearance as a placeholder, not as an extension of the brand guide's authority — they should be revisited once the guide actually specifies them.