Robert Helewka r

@heluca/svelte (0.1.0)

Published 2026-08-28 23:21:32 +00:00 by r

Installation

@heluca:registry=https://git.helu.ca/api/packages/r/npm/
npm install @heluca/svelte@0.1.0
"@heluca/svelte": "0.1.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 13 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 are byte-identical copies from the export; everything else is written here.

src/
  tokens/*.css      verbatim copies from the export — do not hand-edit
  theme.css         the bridge: Heluca tokens -> daisyUI + light/dark semantics
  Icon.svelte       Phosphor icon wrapper
  components/       the 13 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/
//git.helu.ca/api/packages/r/npm/:_authToken=${GITEA_NPM_TOKEN}

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 HTTPS registry with a token 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, --text-body) 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).

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
Checkbox, Radio real <input> behind the visual the source put onClick on an inner <span> — not keyboard- or screen-reader-accessible
Slider added a real <input type="range"> the source was display-only: no input, no handler
Switch knob is white when on, not --on-orange #1a1206 at 22px reads as a hole punched in the amber track
Button disabled drops to a surface tone, not opacity: .5 amber at 50% over the dark base muddies to brown and still reads as a filled action
DeviceTile color-mix against the surface, not fixed rgba() the source tints were tuned for the dark base and wash out on paper
all classes + <style> instead of React inline styles idiomatic Svelte, and inline styles cannot express :hover / :active / :focus-visible

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/

Tokens are verbatim, so that copy is the whole job for a token 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 only a read:package token, supplied as GITEA_NPM_TOKEN in the build environment. Keep that separate from the CI publish token; nothing but this workflow needs write access.

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.0.0
tailwindcss ^4.0.0

Keywords

heluca design-system svelte
Details
npm
2026-08-28 23:21:32 +00:00
6
UNLICENSED
14 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