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

@sken-ds/vue

v0.10.0

Published

Vue 3 thin adapter over @sken-ds/primitives (Web Components).

Readme

@sken-ds/vue

Vue 3 thin adapter over @sken-ds/primitives. The adapter is glue: the contract lives in @sken-ds/contracts, the implementation lives in @sken-ds/primitives. This package only translates between the primitive's Web Component API and Vue's component API (props, emits, slots).

Why is this so small?

Every adapter SFC in this package is ~30 lines. The reason is that the component itself is not here — it is in @sken-ds/primitives, as a framework-ready Web Component. The adapter only translates between the Web Component API and Vue's component API:

  • Imports the primitive (side-effect: registers the custom element).
  • Re-exposes the contract types as Vue prop types.
  • Re-emits the primitive's sken-* CustomEvent as a Vue event.
  • Forwards the default slot to the primitive.

The translation is hand-written, not generated. Each decision (when to use kebab-case vs camelCase, how to handle v-model, how to map named slots, which defineEmits overload to use) is an opinion that would not survive an autogenerator without AI. A future packages/react/SkenButton.tsx will be the same shape and the same size — same contract, same primitive, same glue pattern, different idiom.

Why a per-framework adapter and not SFCs from scratch

We could write <SkenButton> as a 50-line Vue SFC and ship it in @sken-ds/vue. That is what M8 (now M9 first wave) was originally going to do. We changed the plan in 2026-07-15 after a review confirmed that several Legrand Care products already run on other frameworks and will adopt the DS as consumers.

The cost of two parallel implementations is paid forever; the cost of writing a thin adapter is paid once. Lit 3 (the primitive substrate) is ~5 KB and is already in the transitive deps of every Sken consumer, so the cost of "shipping a runtime" is already on the table.

Progressive Implementation (the architecture behind the adapter)

The adapter pattern is the visible half of a deeper architecture: Progressive Implementation Architecture (PIA). The full rationale lives in docs/architecture/progressive-implementation.md and the principle it expresses lives in docs/PRINCIPLES.md § 9. The short version:

  • The platform owns the contract (props, events, slots, ARIA semantics) of every component.
  • Each component picks the implementation that earns its place. Today most primitives are Lit 3 Web Components in @sken-ds/primitives. Tomorrow <sken-date-picker> may be PrimeVue, <sken-rich-editor> may be Tiptap, and <sken-button> will still be Lit because the trade-off says it should.
  • The consumer sees only the contract. The implementation is a private detail of the package that owns the component today.
  • Replacing an implementation is a silent refactor: the consumer's import does not change, the prop names do not change, the event names do not change, the tests do not change.

The only place where the implementation's vocabulary is allowed to leak into the consumer-facing surface is the mapper — one file per component, under packages/vue/src/_internal/, fully tested. The mapper is the boundary between "the library we use today" and "the contract we own forever". A worked example of a mapper is at the bottom of this README.

This is Replacement, not Migration. There is no big-bang event. There is no "we are switching the whole stack on Tuesday". There is a per-component RFC, a private refactor of the mapper, and a release note that says "implementation of <sken-X> changed internally; public API unchanged".

"Never copy the external API" (the most important rule)

A naive "wrap the library" looks like this:

// ❌ BAD — we copy Carbon's vocabulary because we wrap Carbon.
import '@carbon/web-components/es/components/checkbox/index.js'
export const SkenCheckbox = 'cds-checkbox' // ← dishonest

This is vendor lock-in with extra steps. The day we want to replace Carbon, the consumer's code (and ours) is full of Carbon's cds-* naming, its cds-checkbox-selected events, its attribute defaults. The wrapper added nothing.

The discipline is the inverse. We design our own contract first, then we map it to whichever implementation we picked today. For Carbon, the primitive is a Lit Web Component in @sken-ds/primitives that owns the contract; the Vue adapter is a thin SFC that translates Vue's prop/event surface to the primitive's HTMLElement surface:

<!-- ✅ GOOD — Vue adapter for the Sken primitive.
     The SFC imports the Lit primitive (side-effect: registers
     the custom element) and translates Vue props/emits to the
     Sken-shaped CustomEvent the primitive emits. -->
<script setup lang="ts">
import '@sken-ds/primitives/sken-checkbox'
import type { SkenCheckboxProps } from '@sken-ds/contracts'

const props = defineProps<SkenCheckboxProps>()
const emit = defineEmits<{
  (e: 'change', checked: boolean): void
}>()

function onChange(event: Event) {
  const detail = (event as CustomEvent<{ checked: boolean }>).detail
  emit('change', detail.checked)
}
</script>

<template>
  <sken-checkbox
    :checked="props.checked"
    :indeterminate="props.indeterminate"
    :disabled="props.disabled"
    @sken-change="onChange"
  >
    <slot />
  </sken-checkbox>
</template>

The adapter is a Vue-shaped wrapper around the Sken primitive which is itself a Lit wrapper around Carbon. The vocabulary Sken consumers see is sken-checkbox and sken-change; the Carbon vocabulary is the adapter's private concern. The day Carbon is replaced, only the Lit primitive changes; the Vue adapter and the contract stay where they are. the consumers' code, the Storybook stories — all of it stays the same.

A non-trivial example: mapping skenVariant (our vocabulary) to Carbon's kind (Carbon's vocabulary):

// packages/primitives/src/components/_internal/carbon/buttonVariants.ts
import type { SkenButtonVariant } from '@sken-ds/contracts'

const carbonButtonKinds = {
  primary: 'primary',
  secondary: 'secondary',
  tertiary: 'tertiary',
  quaternary: 'ghost',
} as const satisfies Record<SkenButtonVariant, 'primary' | 'secondary' | 'tertiary' | 'ghost'>

export function mapButtonVariant(skenVariant: SkenButtonVariant) {
  return carbonButtonKinds[skenVariant]
}

That is the kind of file the platform team owns. It is private to @sken-ds/primitives (the wrapping layer, not the framework adapter), it is tested, and it is the only place where external-API vocabulary is allowed to leak in. If we ever swap implementations, this file is the first thing that changes — and the last. Everything else is the contract.

The full worked example — including the "why object-constant over switch" rationale (one source of truth, compile-time exhaustiveness via as const satisfies, hashmap at runtime) and the naming convention (mapButtonVariant per component, never generic mapVariant) — lives in docs/architecture/progressive-implementation.md. The example above is a short pointer; the doc is the canonical reference.

Layer split

| Layer | Package | Imports | | --- | --- | --- | | Contract (types only) | @sken-ds/contracts | nothing | | Implementation | @sken-ds/primitives | @sken-ds/contracts, lit | | Vue adapter | @sken-ds/vue (this) | @sken-ds/contracts, @sken-ds/primitives, vue |

A contract change is a TypeScript error in every adapter at the same time. A primitive change ships to every framework at the same time. An adapter change is per-framework and is small.

"Framework-ready", not "framework-agnostic"

@sken-ds/primitives is framework-agnostic at the core (it is a W3C Custom Element, no framework dep beyond Lit). The package @sken-ds/vue is Vue-specific, and consumers import from it. We call the design "framework-ready" because the core ships ready to be wrapped by any framework — but the wrapping itself is intentional, not automatic.

A consumer on React opens packages/react/SkenButton.tsx (when we add it) and writes the same ~30 lines of glue. The component behavior is identical; only the API translation changes.

Adding a new component

  1. Define the contract in @sken-ds/contracts/src/index.ts: Props, Slots, Emits, *Aria.
  2. Implement the primitive in @sken-ds/primitives/src/components/<name>.ts (Lit 3 class extending LitElement, custom element tag sken-<name>, shadow DOM with token-driven styles, framework-portable sken-click (or component-specific) custom events).
  3. Re-export the primitive from @sken-ds/primitives/src/index.ts and add a subpath entry in its package.json.
  4. Write a thin adapter SFC in this package: @sken-ds/vue/src/<ComponentName>.vue (~30 lines).
  5. Re-export the adapter from @sken-ds/vue/src/index.ts.
  6. Write contract tests in @sken-ds/primitives/test/<name>.test.ts (not in the adapter — the adapter has no logic to test).

What this is NOT

  • Not a component library. There is no logic here, only translation.
  • Not framework-agnostic. It imports from vue. Consumers using another framework import from the matching @sken-ds/<framework> package, not from here.
  • Not a Figma replacement. The Figma library is the source of truth for the visual concept vocabulary; the contract types mirror that vocabulary in code.

See docs/adr/0004-vue-as-primary.md for why Vue is the first adapter, and docs/adr/0005-framework-evolution-roadmap.md for the multi-framework plan. The architecture that makes "implementation can be replaced" work is documented in docs/architecture/progressive-implementation.md and is the architectural expression of docs/PRINCIPLES.md § 9.

Required Vite config (custom elements)

<sken-button> and the other primitives are Web Components, not Vue components. Vue 3's template compiler treats every unknown tag as a component lookup, which crashes at runtime with Maximum call stack size exceeded if sken-* is not in the isCustomElement allowlist.

You must add this to your consumer app's vite.config.ts (or nuxt.config.ts for Nuxt):

vue({
  template: {
    compilerOptions: {
      isCustomElement: (tag) => tag.startsWith('sken-'),
    },
  },
})

The full rationale, a Nuxt example, and a 3-step test to verify the fix is wired correctly live in docs/custom-elements.md. The same config is in this repo at apps/storybook/vite.config.ts — that file is the canonical reference for any Vite + Vue 3 consumer.