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

@woptima/dirigo

v1.2.1

Published

Dirigo — configurable Vue 3 directive library (DOM-first, zero runtime dependencies): clipboard, sanitization, inputs, scroll, and more.

Readme

Dirigo

Configurable Vue 3 plugin that registers only the directives you choose. Zero runtime dependencies (peer: vue). UI-library agnostic (DOM APIs only).

Current release: 1.2.0 on npm · Source: github.com/bUxEE/dirigo

Install

npm install @woptima/dirigo

Quick start

import { createApp } from 'vue'
import App from './App.vue'
import { Dirigo } from '@woptima/dirigo'

const app = createApp(App)

app.use(Dirigo, {
  directives: 'all', // or e.g. ['click-copy', 'html-safe', 'case']
  prefix: '', // optional e.g. 'vd' → v-vd-click-copy
  directiveConfig: {
    imageOverlay: {
      // backdropStyle: 'rgba(0,0,0,0.1)',
      // component: MyImageModal, // props: value (image URL), onClose
    },
    clickCopy: {
      // toastComponent: MyCopyToast, // prop: message (label text)
      // duration: 2500,
    },
  },
})

app.mount('#app')

Local registration

import { clickCopyDirective } from '@woptima/dirigo'

app.directive('click-copy', clickCopyDirective)

Options

| Option | Type | Description | |--------|------|-------------| | directives | DirectiveName[] \| 'all' | Which directives to register globally. | | prefix | string? | Prepended to each name (prefix: 'vd'vd-click-copy). | | directiveConfig | DirigoDirectiveConfig? | Shared UI defaults (e.g. imageOverlay, clickCopy toast). |

Directives overview

| Name | Purpose | |------|---------| | size-vars | Publishes --{id}-width / --{id}-height on :root (element needs id). | | next-pad | Sets next sibling padding-top to this element’s height. | | case | Enforces 'uppercase' or 'lowercase' on <input> / <textarea> + syncs v-model (v-case="'uppercase'" / v-case="'lowercase'"). | | type-value | On blur, normalizes v-model (kind: text | number | integer | float); invalid numeric strings clear to ''. | | input-type | Key filter modes: integer, decimal, digits, phone, numpad, slug. | | transitioning | Adds transitioning class around CSS transitions (optional property filter). | | drop-file | onDrop(files) + drag-over class. | | paste-file | { onPaste(files) } for clipboard files (no custom DOM events). | | image-overlay | Full-screen image viewer on <img> or a wrapper: default modal, or { component, componentProps }. Optional callback-only: (src) => void or { onOpen } without component / overlay: true. | | click-copy | Copy on click + toast (value, label, optional toastComponent, duration). | | link-copy | Copy URL / href + toast (same toast options as click-copy). | | html-safe | Like v-html with default allowlist sanitizer; optional sanitize(html). | | sticky | Applies position: sticky + offsets (top, bottom, zIndex, …). | | strip-html-paste | Plain-text paste for inputs / contenteditable. | | scroll-lock | Locks body scroll (ref-count); optional { target }. | | scroll-spy | Scroll-linked “active” section: viewport = trigger-line box (default: host); root = scroll target (inferred: nearest vertical scroll container from that box when omitted). | | media-class | Adds classes for coarse, reducedMotion, dark media queries. | | hash-scroll | Scrolls to id when location.hash matches. | | select-on-focus | Select-all on focus. | | skeleton | Loading shimmer overlay; host keeps layout while content is visibility: hidden; optional theme, delay, custom component. | | debounce-input | Debounces input; listen for debounced-input custom event. | | view-classes | view-visible when ≥1px in view; directional classes (entering-top, entering-bottom, leaving-top, leaving-bottom) while scrolling and partially visible. | | more-text | Truncates text with Show more / Show less (maxLength, labels). | | is-scrolling | has-vertical-scroll / has-horizontal-scroll when overflow exists; is-scrolling-vertical / is-scrolling-horizontal when scrolled (scrollTop > 0 / scrollLeft > 0, LTR). | | page-visibility-class | Tab hidden/visible classes. | | img-fallback | Placeholder src on <img> error. | | keybind | Hotkey map (esc, ctrl+enter, …) → handlers; optional focusWithin / disabled. | | infinite-scroll | IntersectionObserver on scroll root: { load, disabled?, distance?, rootMargin?, throttleMs? }. | | longpress | Pointer long-press (handler, duration?, preventContextMenu?). | | truncate | Truncates element textContent to length (number or { length, ellipsis? }). | | drag-select | Click-drag box selection with { items, selected, key? }; children use data-{key} matching items (Ctrl/Meta toggles, Shift range). | | empty-state | When items is empty: hides default slot and shows empty UI (text, icon, component, ctaText / onCta). | | form-lock | When binding is true, disables inputs/buttons inside the host; restores prior disabled when unlocked. | | error-focus | On first invalid (HTML5 validation), focus + optional smooth scrollIntoView / shake; optional ariaInvalid / findFirstInvalid. | | float-to | Toggle: animate host to match a target element’s screen rect (fixed + optional transition); ResizeObserver keeps it aligned. Second click fades out and restores layout. |

Details & bindings

html-safe

  • Object: { html: string, sanitize?: (html: string) => string } or allowlist options for the built-in sanitizer.
  • String shorthand: same HTML string, default sanitizer.
  • Security: default sanitizer is best-effort; for untrusted HTML prefer server-side filtering and/or pass DOMPurify (your dependency) via sanitize.

image-overlay

  • Host: put v-image-overlay on the <img> (uses that image’s src / currentSrc) or on a wrapper (uses the first nested <img>). Optional binding { src: '…' } overrides the URL.
  • Modal (default): click opens a built-in full-screen overlay (backdrop, centered image constrained to the viewport, white × control top-right, Escape closes). Override globally with directiveConfig.imageOverlay (component, backdropStyle, padding) or per binding: { component: MyModal, componentProps?: { … } }. Custom components receive value (image URL string) and onClose.
  • Callback-only: v-image-overlay="(src) => …" or { onOpen } without component and without overlay: true — runs the handler instead of opening the modal.

click-copy / link-copy toast

  • Per binding: { value, label, toastComponent?, duration? }.
  • Global: directiveConfig.clickCopy.toastComponent (Vue component with prop message for the label) and duration (ms).

paste-file / drop-file

<input v-paste-file="{ onPaste: (files) => console.log(files) }" />
<div v-drop-file="{ onDrop: (files) => upload(files) }"></div>

case

Required binding: 'uppercase' or 'lowercase' (or a ref holding either). Invalid values log a dev warning and do not transform.

<input v-model="code" v-case="'uppercase'" />
<textarea v-model="note" v-case="'lowercase'" />

type-value / input-type

  • type-value: on blur, normalizes the value: integer only accepts a full signed integer string (12abc or asdasdas → cleared); float / number use Number() on the whole string (12abc → cleared). Empty input becomes ''. HTML inputs cannot bind NaN; in script use e.g. const n = num === '' ? NaN : Number(num) if you need it.
  • input-type: blocks disallowed keydown characters (optional; separate from type-value).
<input v-model="n" v-type-value="{ kind: 'float' }" />
<input v-input-type="{ mode: 'phone' }" />

more-text

<p v-more-text="{ maxLength: 120, moreLabel: 'Show more', lessLabel: 'Show less' }">Long text …</p>

is-scrolling

  • Overflow: has-vertical-scroll / has-horizontal-scroll when content overflows that axis.
  • Scrolled: is-scrolling-vertical when scrollTop > 0; is-scrolling-horizontal when scrollLeft > 0 (LTR origin). Optional overrides: { hasVertical, hasHorizontal, scrolledVertical, scrolledHorizontal }. Deprecated: vertical / horizontal map to the has- names.

keybind

<div v-keybind="{ esc: onClose, 'ctrl+enter': onSubmit, focusWithin: true }"></div>

Uses capture on window. Modifiers: ctrl, meta/cmd, alt, shift. Keys: esc, enter, single letters, etc.

infinite-scroll

Host must be the scroll container (overflow: auto / scroll). A sentinel is appended; when it intersects (near bottom), load runs. Set disabled: true while fetching.

longpress

v-longpress="onLongPress" or v-longpress="{ handler: onLongPress, duration: 600 }".

truncate

v-truncate="40" or v-truncate="{ length: 40, ellipsis: '…' }" (best with static text; reactive copy changes may need :key on the host).

float-to

  • Binding: { target, transition?, trigger? }. target: HTMLElement, Vue Ref<HTMLElement | null>, or a CSS selector (via document.querySelector). transition: CSS transition for top / left / width / height (default ~400ms cubic-bezier). trigger: selector within the host for the clickable node; omit to use the host as the click target.
  • Behavior: first click captures the host’s getBoundingClientRect() (viewport), moves the host to document.body for that flight, applies position: fixed with a high z-index, then animates top / left / width / height to the target’s viewport rect (same coordinate system). While flying and docked, the host has class floated-to (export FLOAT_TO_DOCKED_CLASS) for hooks or styling; it is removed when returning or unmounting. Without the body move, any ancestor transform / filter / perspective would make fixed use a different containing block than the viewport, so pixel rects would be wrong. After the return fade, the host is inserted back at its original DOM position. While docked, the host tracks the target via ResizeObserver (element box changes) plus window resize, document scroll (capture) so window resize and scroll (which move getBoundingClientRect() without firing RO) still sync; visualViewport resize / scroll are included when available. transitionend from descendants is ignored for the fade so nested controls do not reset the host early.
  • Templates: like v-scroll-spy, avoid v-float-to="{ target: myRef }" — Vue unwraps myRef in inline objects. Use computed(() => ({ target: myRef, … })) and v-float-to="opts".

error-focus

Place on a <form> (or an element inside one). Listens for the bubbling invalid event (HTML5 constraint validation: required, pattern, setCustomValidity, etc.).

  • scroll: true (default) — after focus(), calls scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' }). false — no scroll into view.
  • shake: optional short CSS shake on the focused field (default true).
  • ariaInvalid: if the form has no :invalid element but there is [aria-invalid="true"], focus that node instead of the event target. Useful when a library marks errors only with ARIA (some Vuetify setups) while the browser still emitted invalid from a related control.
  • findFirstInvalid(form): return the HTMLElement to focus yourself (VeeValidate, custom rules, errors on wrappers). Use this when validation is not expressed as native validity on the right node, or when you need a specific field order.

Vuetify / VeeValidate / other libs: There is no automatic “library detection”. The directive uses the DOM: the invalid event and optionally :invalid, [aria-invalid="true"], or your findFirstInvalid. Purely JS-driven forms that never call reportValidity() or never produce invalid events will not trigger this unless you wire validation so those events fire, or you use findFirstInvalid and ensure your submit path still surfaces an invalid event (e.g. hidden native inputs) — otherwise focus the field from your validator and skip this directive for that path.

skeleton

  • Binding: a Vue Component, or { loading?, component?, props?, theme?, delay?, minDuration?, debug? }. loading drives the overlay (boolean or ref; omitted means loading). Pass a component to replace the built-in shimmer; its props merge with props (and the built-in receives a theme prop when using the default skeleton).
  • theme: dark | light — palette for the built-in shimmer only (dark default: slate-style gradient; light: light gray for pale surfaces). Ignored if you pass a custom component (unless you forward theme yourself).
  • delay / minDuration: ms before showing the overlay after loading stays on, and minimum time the overlay stays visible (defaults 120 / 200). debug: log show/hide timing to the console.
  • Built-in shimmer animation runs on a ~2s cycle.

scroll-spy

  • Binding: { active, selector?, offset?, root?, viewport? }. active (required) is a Ref<string> updated with the id of the active block. Templates: do not write v-scroll-spy="{ active: myRef, … }" — Vue unwraps myRef inside the object. Use const opts = computed(() => ({ active: myRef, … })) and v-scroll-spy="opts". selector queries from the host (default :scope > *); only elements with an id participate. offset (default 0.4) places the trigger line at that fraction of the viewport box height. viewport: omithost (the element with the directive); 'body' → full-page visual viewport (window.innerHeight); otherwise a selector / element / ref. root: omitnearest vertical scroll container walking up from the resolved viewport node (same starting point as the default trigger box: the host); viewport: 'body'window; root: null → force window; or pass an element / ref. If the scrollable panel is only a child of the host, set viewport (or root) to that element — inference does not search down into descendants.
  • Logic: an element is “crossing” when rect.top <= trigger && rect.bottom > trigger (using getBoundingClientRect inside requestAnimationFrame). If several cross, scrolling down picks the last, scrolling up the first. If none cross, uses the block closest above the line, else the first block.
  • Listeners: scroll on root or window; resize on window to refresh the cached node list.

scroll-lock

Mounted element triggers body scroll lock until unmounted (nested modals: ref-counted).

Migration from legacy registerDirectives

| Old | New | |-----|-----| | vue-directives-plus (package) | @woptima/dirigo — plugin export Dirigo, createDirigo, types DirigoOptions, DirigoDirectiveConfig | | uppercase | v-case with 'uppercase' or 'lowercase' | | numberValue | v-type-value | | numberInput / phoneInput / … | v-input-type with mode | | v-transition | v-transitioning | | pasteFile | v-paste-file + { onPaste } (no custom DOM event) | | height-style | use v-size-vars + CSS var(--id-height) | | scroll-shadow* | handle in CSS / app | | scrolling-vertical / scrolling-horizontal (old is-scrolling) | has-vertical-scroll / has-horizontal-scroll + is-scrolling-vertical / is-scrolling-horizontal |

Development

npm install
npm run playground
npm test
npm run build

Marketing site (site/)

Not included in the npm package. Separate Vue app: home, playground (reuses playground/App.vue), docs/API table.

npm run site:dev
npm run site:build
npm run site:preview

See docs/REPOSITORY_LAYOUT.md for deploying to your domain and splitting public library vs private site across two repos.

Publish to npm

npm run release:npm:dry          # test + build + npm publish --dry-run (no version bump)
npm run release:npm              # patch version + publish (local git tag/commit from npm version)
RELEASE_NO_GIT=1 npm run release:npm   # same without git moves (CI-friendly)

License

MIT