New:Microsoft Teams Notifications Are Now Available in Socket.Learn more
Get Started

@pepitahq/uikit

Package Overview
Dependencies
Maintainers
1
Versions
20
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@pepitahq/uikit

pepita's shared UI kit — design tokens, CSS primitives, and Svelte 5 components.

latest
Source
npmnpm
Version
0.11.0
Version published
Weekly downloads
46
31.43%
Maintainers
1
Weekly downloads
 
Created
Source

@pepitahq/uikit

pepita's shared UI kit — design tokens, CSS primitives, and presentational Svelte 5 components, extracted from the pepita editor chrome so the app, the marketing landing, and future surfaces share one visual language.

Three CSS layers, each usable on its own, plus the component library:

  • tokens.csstypography only: the font custom properties (--f-mono / --f-serif / --f-sans and --font), the data-font variant blocks, and the base body + box-sizing reset. Colors are deliberately NOT here — they come from a theme (below).
  • A theme — the color layer: --bg, --ink, --ink-soft, --ink-faint, --accent, --on-accent, --surface, --surface-raised, --code-bg, --rule, --widget-border, --success, --warning, --error. Ships as a set of curated, runtime-swappable theme files.
  • primitives.css — reusable global classes: .cta (+ .cta-google / .cta-new / .cta-create), .g-badge, .hdr-btn, .pp-toggle, .dialog-btn, .field / .input-wrap / .input-prefix.
  • Svelte 5 components — presentational (props + snippets + callbacks; no app logic).

Theming

Colors live in curated theme files, one per theme, listed in a registry:

idlabel
rose-pineRosé Pine (default)
githubGitHub
ethereal-lemonsEthereal Lemons

Each theme file sets the color tokens on :root, with a @media (prefers-color-scheme: dark) block so the OS drives light/dark automatically; [data-theme="light"] / [data-theme="dark"] on the root forces one. Themes are runtime-swappable: the host loads exactly one via a <link> whose href it can swap at runtime (e.g. a Settings → theme picker). The component barrel does not bundle any theme, so the loaded link is the single source of truth — nothing overrides it.

<!-- typography + the chosen theme; data-font optionally overrides the typeface -->
<link rel="stylesheet" href="/uikit/tokens.css" />
<link rel="stylesheet" href="/uikit/styles/themes/rose-pine.css" id="theme" /> <!-- swap href to re-theme -->
<html data-font="mono">

The registry (default + the list above) is published as JSON so you can build a picker:

import registry from '@pepitahq/uikit/styles/themes/themes.json';
// { default: 'rose-pine', themes: [{ id, label }, …] }

Export paths:

  • @pepitahq/uikit/tokens.css — typography layer
  • @pepitahq/uikit/primitives.css — global classes
  • @pepitahq/uikit/theme.css — the default theme (Rosé Pine) as a single file
  • @pepitahq/uikit/styles/themes/<id>.css — an individual swappable theme file
  • @pepitahq/uikit/styles/themes/themes.json — the theme registry

Requirements

The kit assumes the host provides two things it deliberately does not bundle:

  • Fonts — IBM Plex Mono / Serif / Sans (e.g. from Google Fonts). tokens.css references them but does not @import them, so the host controls loading (and there's no double-load).
  • Phosphor icons (CDN) — the ph-* classes, for icon snippets you pass into primitives (Button / HeaderButton / TextInput). Components that own their icons (SplitActionButton, TabBar, IconButton) bundle them via the phosphor-svelte dependency, so those need nothing from the host.

Usage

Importing the barrel pulls in the tokens + primitives CSS automatically — but not a theme; load one yourself (see Theming):

<script>
  import { Button, AuthButton, SplitActionButton } from '@pepitahq/uikit';
</script>

<Button variant="primary" onclick={save}>Save</Button>
<AuthButton href="/auth/google" />

Plain-HTML surfaces (the landing, served user sites) can consume just the CSS layers:

import '@pepitahq/uikit/tokens.css';
import '@pepitahq/uikit/theme.css';        // or a specific styles/themes/<id>.css
import '@pepitahq/uikit/primitives.css';
<button class="cta">Save</button>
<button class="cta cta-google"><span class="g-badge"><!-- G mark --></span> Continue with Google</button>

Components

All components are presentational — you pass data + callbacks; you wire the behavior.

ComponentKey props (→ snippets)
Buttonvariant: 'primary'|'google'|'new'|'create', disabled, type, href, title, onclickchildren, icon?
AuthButtonhref, label? ('Continue with Google'), disabled? — renders the branded Google button with an inlined mark
HeaderButtonactive?, disabled?, title?, onclick?icon?, children
IconButtonlabel (feeds BOTH aria-label and the built-in tooltip), icon (snippet), active? (undefined = action button, boolean = toggle with pressed look), disabled?, onclick?, type?, anchor mode via href?/download?/target?/rel?, size? (22), placement?, tip? (true), class? — square icon control with a built-in IconTip
TogglePillactive?, disabled?, title?, onclick?children
TextInputvalue (bindable), label?, placeholder?, help?, disabled?, id?, oninput?prefix?
SplitActionButtonkind: 'draft'|'live', primaryLabel, onPrimary, busy?, primaryTitle?, downloadHref?, downloadLabel?, urlsHeading?, urls?: string[]primaryIcon?
TabBartabs: TabItem[], activeId, onSelect(id), onClose?(id)
Tablabel, active?, closable?, onSelect?, onClose? (the single-tab primitive TabBar composes)
InputBracketdisabled?input, controls?, action? — a focus-aware frame; any <textarea>/<input> in the input slot is reset borderless

TabItem is { id: string; label: string; closable?: boolean }.

Example — InputBracket (chat-style composer frame)

<script>
  import { InputBracket, TogglePill, Button } from '@pepitahq/uikit';
</script>

<InputBracket>
  {#snippet input()}<textarea rows="3" placeholder="Describe the edit…"></textarea>{/snippet}
  {#snippet controls()}<TogglePill>Select</TogglePill>{/snippet}
  {#snippet action()}<Button variant="new">Send</Button>{/snippet}
</InputBracket>

Overlays & dialogs

ComponentKey props (→ snippets)
Popoveropen (bindable), placement, offset, anchor?: {x,y}, anchorEl?: HTMLElementcontent. Controlled floating panel; you render your own trigger.
Menuitems: MenuEntry[] ({label, icon?, shortcut?, danger?, disabled?, onSelect} | {separator:true}), onClose? — arrow-key nav
ContextMenuopen (bindable), x, y, items — a Menu in a Popover at a point
Modalopen (bindable), title?, width?, height?, dismissable? (default true; set false to veto Escape/backdrop close while an op is in flight), onClose?icon?, tabs?, children (body), footer?. Focus-trap + Escape/backdrop close.
UnderlineTabBartabs: {id,label,icon?}[], activeId, onSelect(id) — the modal-style tabs
Dialog + dialogimperative dialog.alert/confirm/prompt; mount <Dialog /> once at the root
Toaster + toasttransient notifications: toast.info/success/error(msg, { duration? }); mount <Toaster /> once

Layering: Modal (z 50) < Popover/Menu (z 60) < Dialog/ContextMenu (z 100).

Example — Dialog

Mount <Dialog /> once at the root, then call it imperatively anywhere:

<script>
  import { Dialog, dialog } from '@pepitahq/uikit';
</script>

<Dialog />
await dialog.alert('Saved.');
if (!(await dialog.confirm('Delete site?', { destructive: true }))) return;
const name = await dialog.prompt('Rename to:', { initialValue: current });

// `note` — a highlighted caveat under the message (info icon + a --info tint),
// for something the reader needs BEFORE deciding. Keep it out of `message`:
// at the same weight the eye goes to the buttons and skips it. `link` is its
// quiet sibling — a footer link, same slot, lowest emphasis.
await dialog.confirm('Delete this video?', {
  destructive: true,
  note: 'This one is shared — it is only detached from your site.'
});

Example — Popover (controlled)

<script>
  import { Popover } from '@pepitahq/uikit';
  let open = $state(false);
  let btn = $state<HTMLElement>();
</script>

<button bind:this={btn} onclick={() => (open = !open)}>Menu</button>
<Popover bind:open anchorEl={btn} placement="bottom-end">
  {#snippet content()}<div class="my-panel">…</div>{/snippet}
</Popover>

Feedback & controls

ComponentKey props (→ snippets)
SegmentedControloptions: {value,label,disabled?}[], value (bindable), onChange? — pill-group toggle
Selectoptions: {value,label,description?,disabled?}[], value (bindable), onChange?, disabled?, placeholder?, size: 'sm'|'md', variant: 'field'|'pill' (default field; pill is the chip shape, for an inline choice rather than a form row), placement? (default 'bottom-start'; use a top-* value where the trigger sits near the bottom of its pane), fullWidth?, ariaLabel? — themeable <select> replacement (Popover + listbox, keyboard nav + type-ahead)
Badgevariant: 'neutral'|'success'|'warning'|'error', icon?children — status pill
IconActionPillstate: 'ok'|'info'|'alert', label (required — the pill has no visible text), detail? (hover-panel body via Tooltip), actions?: {label, onSelect}[] (caret menu, alert only), busy? — compact icon-only status pill for a table cell; ok/info are inert, alert optionally offers one action
Tooltiptext? or tip? (snippet), placement, width?, delay?children (focusable trigger); hover/focus reveal. HOVER IS DELAYED BY DEFAULT (delay, 500 ms) — these triggers are small controls in dense rows (a section header's ⓘ, a badge in a file tree), so a cursor crossing one without meaning to is the common case, and a panel that opens on the way past sits over whatever was actually being reached for. Leaving before it elapses cancels the open, so a pass-over never opens it at all. Keyboard focus is NEVER delayed whatever delay says: tabbing to a control is deliberate, so there is no accidental arrival to guard against. delay={0} opens on hover at once. width is capped at min(90vw, 22rem) — a wider value is silently clamped, so keep it under the cap. placement is a preference, not a promise: the panel is kept inside the viewport — pushed back horizontally by exactly the overflow (so it stays as near the trigger as the screen allows) and flipped to the other side when the preferred one has no room. The panel is hoverable, so tip may contain a link: hide() is delayed 160ms to cover the 8px gap between trigger and panel, and re-entering cancels it.
IconTiptext? (compact one-line) or tip? (snippet), placement: 'top'|'bottom', delay? (300 ms) → children (focusable trigger) — a position: fixed tooltip that escapes ancestor overflow/clipping; what IconButton uses internally
ChartTooltipx, y, visiblecontent — dark surface positioned at a point (chart hovers)
CollapsibleSectionlabel, open (bindable, default closed), count?, icon?, afterLabel?, actions?children — uppercase disclosure header with rotating caret + optional right-aligned count; the expand/collapse pattern (Assets panel, new-site advanced settings). icon (optional snippet) — rendered between the caret and the label, inside the toggle button (pass a phosphor icon sized ~14). afterLabel (optional snippet) — rendered immediately AFTER the label and outside the toggle button, for a control that belongs to the label rather than to the section (an info tooltip). Passing it shrinks the toggle button to its content so the snippet sits beside the text instead of at the far right — the one trade-off: the whole header row is no longer clickable. actions (optional snippet) — right-aligned header actions (e.g. buttons), rendered outside the toggle button so clicking them never toggles the section.

Development

pnpm -F @pepitahq/uikit dev       # preview gallery (all components × themes)
pnpm -F @pepitahq/uikit test      # vitest (@testing-library/svelte + jsdom)
pnpm -F @pepitahq/uikit check     # svelte-check
pnpm -F @pepitahq/uikit gen:theme # regenerate the theme CSS files from source
pnpm -F @pepitahq/uikit package   # svelte-package → dist/ (+ publint)

Built with @sveltejs/package. dist/ (the publishable output) is generated, not committed.

FAQs

Package last updated on 02 Aug 2026

Related posts