Robert Helewka r

@heluca/svelte (0.3.0)

Published 2026-08-29 10:06:20 +00:00 by r

Installation

@heluca:registry=https://git.helu.ca/api/packages/r/npm/
npm install @heluca/svelte@0.3.0
"@heluca/svelte": "0.3.0"

About this package

Heluca Design System — Svelte 5 implementation: tokens, daisyUI bridge, Phosphor icons, and component primitives.

@heluca/svelte

The Heluca Design System implemented for Svelte 5 — brand tokens, the daisyUI bridge, Phosphor icons, and 15 component primitives.

This repo is maintained by hand. Its upstream, heluca-design-system, is a verbatim mirror of the design system as it exists in Claude Design and is never hand-edited. This repo is the translation of that into code your apps run. Tokens and theme.css are byte-identical copies from the export; the components and the Icon wrapper are written here.

src/
  tokens/*.css      verbatim copies from the export — do not hand-edit
  theme.css         verbatim copy from the export — do not hand-edit
                    the bridge: Heluca tokens -> daisyUI + light/dark semantics
  Icon.svelte       Phosphor icon wrapper
  components/       the 15 primitives, ported from the export's React .jsx
  index.js          package entry

Install

Published to the Gitea npm registry on git.helu.ca. The consuming app needs an .npmrc pointing the @heluca scope at it:

@heluca:registry=https://git.helu.ca/api/packages/r/npm/

No credential, and do not add one. The registry serves reads anonymously, the same way the Gitea PyPI registry does for pip. An _authToken line is worse than useless here: Gitea rejects a malformed credential rather than falling back to anonymous, and npm only omits the header when the referenced variable is set to the empty string — leave it unset and npm sends a literal Bearer ${VAR}, so every command 401s. A token is needed only to publish.

Then:

npm i @heluca/svelte @phosphor-icons/web

Peer dependencies — Svelte 5, Tailwind 4, daisyUI 5, @phosphor-icons/web — are expected to already be in the consuming app.

Why a registry rather than a git URL: Docker builds have no git binary and no SSH keys, so a git+ssh:// dependency fails inside a container. An anonymous HTTPS registry works everywhere the app builds.

Wire up the styling

Order matters. In your app's app.css:

@import 'tailwindcss';

/* 1. Tokens first — everything below references them. */
@import '@heluca/svelte/tokens/fonts.css';
@import '@heluca/svelte/tokens/colors.css';
@import '@heluca/svelte/tokens/typography.css';
@import '@heluca/svelte/tokens/spacing.css';
@import '@heluca/svelte/tokens/elevation.css';
@import '@heluca/svelte/tokens/motion.css';

/* 2. daisyUI. */
@plugin 'daisyui' {
	themes:
		light --default,
		dark --prefersdark;
}

/* 3. The bridge — must come after the plugin. */
@import '@heluca/svelte/theme.css';

/* 4. Phosphor. Only the three weights Heluca uses. */
@import '@phosphor-icons/web/regular';
@import '@phosphor-icons/web/fill';
@import '@phosphor-icons/web/bold';

Your app is responsible for setting data-theme="light" or "dark" on <html>theme.css keys off that attribute and defines nothing without it.

Use

<script>
	import { Button, Card, Badge, Icon } from '@heluca/svelte';
</script>

<Card>
	<Button variant="primary" icon="ph-fill ph-navigation-arrow">Get directions</Button>
	<Badge tone="success">Open</Badge>
	<Icon name="map-pin" class="h-4 w-4" />
</Card>

What theme.css does

Two jobs the export leaves to the consuming app:

  1. Theme semantics. The export declares dark and light neutrals side by side in :root and points its semantic aliases (--bg, --surface-card) at the dark set. It does not switch themes. theme.css re-points those per [data-theme] and adds resolved --t-* neutrals.
  2. daisyUI bridge. Mapping Heluca tokens onto daisyUI's --color-* slots means existing btn / input / card / badge markup inherits the brand with no per-component edits. This is the highest-leverage part of the package — it is why adopting Heluca in an existing daisyUI app is mostly a stylesheet change rather than a rewrite.

Prefer tokens over hex in app code: var(--orange), var(--blue), var(--success), var(--t-text-muted), var(--radius-card), var(--dur-fast).

Body text colour is var(--t-text), not var(--text-body). --text-body is the 16px body size, consistent with --text-heading, --text-label and --text-caption. It used to be defined twice — as that size in tokens/typography.css and as a colour in tokens/colors.css — which made font-size: var(--text-body) invalid at computed-value time everywhere. The 2026-08-29 export removed the colour alias, so the clash is fixed at source.

Migrating to 0.3.0

Additive. TextField and TextArea are new, and ListCard gains selected and size. Two things to know:

  • Svelte 5.20 is now the minimum (was 5.0). TextField and TextArea use $props.id() to generate the for / aria-describedby ids when you don't supply an id.
  • If you skipped 0.2.0, apply its one change: color: var(--text-body) -> color: var(--t-text). font-size: var(--text-body) needs no change.

Icons

Phosphor, per the Heluca spec. Weight carries state: regular = resting, fill = active/on/selected, bold = FAB and true emphasis only.

<Icon name="map-pin" class="h-4 w-4" />
<Icon name="star" weight="fill" class="h-5 w-5" />

Icons take the same Tailwind sizing utilities as everything else; the wrapper converts that box into a font-size, since Phosphor is an icon font.

Type: chrome vs content

The app's own words are Public Sans (--font-sans) — nav, buttons, labels, tables. Words the user typed are IBM Plex Sans (--font-work) — inputs, textareas, badges. theme.css wires this by default; the exception is a custom control that should opt into --font-work.

Product names and titles use Marcellus via .heluca-display, never below ~22px. Data and metrics use JetBrains Mono.

Deviations from the React sources

Faithful except where the source was demonstrably wrong for a real app. Each is commented at its site:

Component Deviation Why
TextField, TextArea label is a sibling; hint/error linked with aria-describedby the source nests them inside the <label>, so the hint is folded into the input's accessible name and an error replaces the label. Also adds aria-invalid, which the source signals by border colour alone
ListCard renders a real <button type="button"> when onclick is set the source is a <div onClick> — not focusable or keyboard-operable. Two literal elements rather than <svelte:element>, so no ARIA is needed to prop up what is already a button
all --t-* theme-aware neutrals, not the dark-only --text / --surface the sources are written against the dark theme; the ports have to work on paper too
all classes + <style> instead of React inline styles idiomatic Svelte, and inline styles cannot express :hover / :active / :focus-visible

The 2026-08-29 export corrected five earlier deviations at source — Checkbox and Radio real inputs, Slider's real range input, Switch's white knob, Button's disabled surface tone, DeviceTile's color-mix tints. Those rows are gone because the sources now match the ports.

DeviceTile comes from Hecate's device grid; its on/off/media/offline pattern suits any stateful tile.

Updating from upstream

When heluca-design-system gets a new export:

cp ~/git/heluca-design-system/tokens/*.css src/tokens/
cp ~/git/heluca-design-system/theme.css    src/

Both are verbatim, so that copy is the whole job for a token or bridge change. If the export's components/*.jsx or .d.ts contracts changed, the corresponding .svelte file here needs a matching update by hand.

Token files are deliberately excluded from Prettier (see .prettierignore) so they stay byte-identical and re-syncing is a clean cp.

Publishing a new version

Consumers resolve by semver from the registry, so a change is not live until it is published. Publishing is done by CI, not by hand — pushing a version tag runs .gitea/workflows/publish.yml, which checks the tag against package.json, verifies every exports target exists, and uploads the tarball:

npm version patch          # or minor / major — bumps package.json, tags vX.Y.Z
git push && git push --tags

Watch the run at https://git.helu.ca/r/heluca-svelte/actions. Then in each consuming app: npm update @heluca/svelte.

The workflow authenticates with the repo secret PACKAGE_TOKEN — a Gitea access token with read:package + write:package, set under Settings → Actions → Secrets. That is the same secret name the PyPI-publishing repos (themis, html2msoffice, pallas) use. The registry rejects re-publishing a version that already exists, so the tag is the release gate: bump first.

A manual workflow_dispatch run is the fallback — it publishes whatever version package.json declares on the chosen ref.

Consuming the package needs no credential at all — see Install. The publish token is the only one in play, and only CI holds it.

Consumers

  • periplus — first adopter
  • hecate, daedalus — not yet migrated

Dependencies

Peer Dependencies

ID Version
@phosphor-icons/web ^2.1.0
daisyui ^5.0.0
svelte ^5.20.0
tailwindcss ^4.0.0

Keywords

heluca design-system svelte
Details
npm
2026-08-29 10:06:20 +00:00
14
UNLICENSED
17 KiB
Assets (1)
Versions (5) View all
0.5.0 2026-08-29
0.4.0 2026-08-29
0.3.0 2026-08-29
0.2.0 2026-08-28
0.1.0 2026-08-28