@heluca/svelte (0.1.0)
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/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:
- Theme semantics. The export declares dark and light neutrals side by
side in
:rootand points its semantic aliases (--bg,--surface-card,--text-body) at the dark set. It does not switch themes.theme.cssre-points those per[data-theme]and adds resolved--t-*neutrals. - daisyUI bridge. Mapping Heluca tokens onto daisyUI's
--color-*slots means existingbtn/input/card/badgemarkup 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 |