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

@vueuse/skills

v14.2.0

Published

Agent Skills for VueUse

Downloads

56

Readme

VueUse Skills

Agent Skills for VueUse — a collection of essential Vue Composition Utilities.

[!IMPORTANT] Experimental Project: Aims to help AI agents use libraries more accurately with fewer tokens. Feedback is welcome.

  • 🪜 Progressive disclosure: send VueUse function overviews first, then load detailed usage and type declarations on demand
  • 💰 Minimal token usage: provide only necessary information to reduce token consumption
  • 📵 Offline-first design: works without internet access or additional agent permissions
  • ⚙️ Customizable policies: users can override function invocation rules in prompts or AGENTS.md
  • 💉 Reduced hallucinations: precise usage references help prevent invented APIs

Installation

Install via skills-npm

@vueuse/skills needs to be used together with skills-npm to install agent skills.

First, add a prepare script to your package.json so the skills are symlinked automatically for your agent whenever you install dependencies:

{
  "scripts": {
    "prepare": "skills-npm"
  }
}

Then, install both skills-npm and @vueuse/skills:

npm i -D @vueuse/skills skills-npm

Install via skills

npx skills add vueuse/vueuse

Install skills via skills you need to be careful about the potential version mismatch between the skill and your local VueUse version.

Example Usage

Install VueUse in your Vue or Nuxt project, then instruct the agent. It will automatically leverage VueUse to assist development.

Example prompt:

create a todo app with the following features:
- save todos to local storage
- show remains todo count on browser title
- add a copy button for each todo items
- infinite scrolling for this todo list
- dark / light mode
<script setup lang="ts">
import {
  useClipboard,
  useColorMode,
  useInfiniteScroll,
  useLocalStorage,
  useTitle,
} from '@vueuse/core'
import { computed, ref } from 'vue'

interface Todo {
  id: number
  text: string
  done: boolean
}

const seedTexts = [
  'Review project goals',
  'Plan the next sprint',
  'Reply to client email',
]

const defaultTodos = Array.from({ length: 36 }, (_, index) => ({
  id: index + 1,
  text:
    seedTexts[index % seedTexts.length]
    + (index >= seedTexts.length ? ` #${index + 1}` : ''),
  done: index % 7 === 0,
}))

const todos = useLocalStorage<Todo[]>('focus-flow-todos', defaultTodos)
const nextId = ref(
  todos.value.reduce((max, todo) => Math.max(max, todo.id), 0) + 1,
)
const newTodo = ref('')

const totalCount = computed(() => todos.value.length)
const remainingCount = computed(() =>
  todos.value.filter(todo => !todo.done).length,
)
const completedCount = computed(
  () => totalCount.value - remainingCount.value,
)

useTitle(computed(() => `Todos (${remainingCount.value})`))

const mode = useColorMode({
  attribute: 'data-theme',
  disableTransition: false,
})
const isDark = computed(() => mode.value === 'dark')

function toggleMode() {
  mode.value = isDark.value ? 'light' : 'dark'
}

const { copy, copied, isSupported } = useClipboard()
const lastCopiedId = ref<number | null>(null)

async function handleCopy(todo: Todo) {
  await copy(todo.text)
  lastCopiedId.value = todo.id
}

const pageSize = 8
const visibleCount = ref(Math.min(pageSize, todos.value.length))
const visibleTodos = computed(() => todos.value.slice(0, visibleCount.value))

const listRef = ref<HTMLElement | null>(null)
const { isLoading } = useInfiniteScroll(
  listRef,
  () => {
    if (visibleCount.value < todos.value.length) {
      visibleCount.value = Math.min(
        visibleCount.value + pageSize,
        todos.value.length,
      )
    }
  },
  {
    distance: 120,
    canLoadMore: () => visibleCount.value < todos.value.length,
  },
)

function syncVisibleCount() {
  if (todos.value.length <= pageSize) {
    visibleCount.value = todos.value.length
    return
  }

  if (visibleCount.value === 0) {
    visibleCount.value = pageSize
    return
  }

  if (visibleCount.value > todos.value.length) {
    visibleCount.value = todos.value.length
  }
}

function addTodo() {
  const value = newTodo.value.trim()
  if (!value)
    return

  todos.value.unshift({
    id: nextId.value++,
    text: value,
    done: false,
  })
  newTodo.value = ''
  syncVisibleCount()
}

function removeTodo(id: number) {
  todos.value = todos.value.filter(todo => todo.id !== id)
  syncVisibleCount()
}
</script>

<template>
  <div class="page">
    <div class="shell">
      <header class="header">
        <div>
          <p class="eyebrow">
            Minimal todo tracker
          </p>
          <h1>Focus Flow</h1>
          <p class="subtitle">
            Keep a lightweight list, copy tasks with a click, and scroll as the
            list grows.
          </p>
        </div>
        <button class="btn ghost mode-toggle" type="button" @click="toggleMode">
          <span class="mode-dot" :class="{ dark: isDark }" />
          <span>{{ isDark ? 'Dark' : 'Light' }} mode</span>
        </button>
      </header>

      <form class="composer" @submit.prevent="addTodo">
        <div class="input-wrap">
          <input
            v-model="newTodo"
            type="text"
            maxlength="120"
            placeholder="Add a new task"
            aria-label="Add a new task"
          >
          <button class="btn primary" type="submit" :disabled="!newTodo.trim()">
            Add task
          </button>
        </div>
        <div class="stats">
          <div class="stat">
            <span>Total</span>
            <strong>{{ totalCount }}</strong>
          </div>
          <div class="stat">
            <span>Remaining</span>
            <strong>{{ remainingCount }}</strong>
          </div>
          <div v-if="completedCount" class="stat">
            <span>Done</span>
            <strong>{{ completedCount }}</strong>
          </div>
        </div>
      </form>

      <section class="list-card">
        <div class="list-head">
          <h2>Todo list</h2>
          <span class="list-hint">
            {{ visibleTodos.length }} / {{ totalCount }} shown
          </span>
        </div>
        <div ref="listRef" class="todo-list" aria-live="polite">
          <article
            v-for="(todo, index) in visibleTodos"
            :key="todo.id"
            class="todo-item"
            :class="{ done: todo.done }"
            :style="{ animationDelay: `${index * 0.03}s` }"
          >
            <label class="todo-check">
              <input v-model="todo.done" type="checkbox">
              <span class="checkmark" />
            </label>
            <p class="todo-text">
              {{ todo.text }}
            </p>
            <div class="todo-actions">
              <button
                class="btn ghost"
                type="button"
                :disabled="!isSupported"
                :title="isSupported ? 'Copy task text' : 'Clipboard not supported'"
                @click="handleCopy(todo)"
              >
                {{ copied && lastCopiedId === todo.id ? 'Copied' : 'Copy' }}
              </button>
              <button class="btn danger" type="button" @click="removeTodo(todo.id)">
                Remove
              </button>
            </div>
          </article>

          <p v-if="!visibleTodos.length" class="empty">
            No tasks yet. Add your first todo above.
          </p>

          <div v-if="visibleTodos.length" class="list-footer">
            <span class="footer-status">
              <span v-if="isLoading">Loading more...</span>
              <span v-else-if="visibleTodos.length < totalCount">
                Scroll to load more
              </span>
              <span v-else>All caught up</span>
            </span>
          </div>
        </div>
      </section>
    </div>
  </div>
</template>

<style>
@import url('https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700&family=Space+Mono:wght@400;700&display=swap');

:root {
  color-scheme: light;
  --bg: #f6f7fb;
  --bg-spot: rgba(59, 130, 246, 0.18);
  --bg-spot-2: rgba(34, 197, 94, 0.18);
  --card: rgba(255, 255, 255, 0.92);
  --surface: rgba(255, 255, 255, 0.84);
  --border: rgba(148, 163, 184, 0.35);
  --text: #0f172a;
  --muted: #64748b;
  --accent: #2563eb;
  --accent-strong: #1d4ed8;
  --accent-soft: rgba(37, 99, 235, 0.18);
  --danger: #ef4444;
  --danger-soft: rgba(239, 68, 68, 0.15);
  --shadow: 0 24px 60px rgba(15, 23, 42, 0.12);
  --radius: 22px;
}

:root[data-theme='dark'] {
  color-scheme: dark;
  --bg: #0b1222;
  --bg-spot: rgba(56, 189, 248, 0.18);
  --bg-spot-2: rgba(16, 185, 129, 0.16);
  --card: rgba(15, 23, 42, 0.86);
  --surface: rgba(15, 23, 42, 0.7);
  --border: rgba(148, 163, 184, 0.25);
  --text: #f8fafc;
  --muted: #94a3b8;
  --accent: #38bdf8;
  --accent-strong: #0ea5e9;
  --accent-soft: rgba(56, 189, 248, 0.2);
  --danger: #f87171;
  --danger-soft: rgba(248, 113, 113, 0.2);
  --shadow: 0 26px 70px rgba(2, 6, 23, 0.55);
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-height: 100vh;
  font-family:
    'Sora',
    system-ui,
    -apple-system,
    sans-serif;
  color: var(--text);
  background:
    radial-gradient(900px circle at top left, var(--bg-spot), transparent 55%),
    radial-gradient(700px circle at bottom right, var(--bg-spot-2), transparent 50%), var(--bg);
  transition:
    background 0.4s ease,
    color 0.4s ease;
}

#app {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(20px, 4vw, 48px);
}

.page {
  width: min(980px, 100%);
}

.shell {
  display: grid;
  gap: clamp(20px, 3vw, 28px);
  padding: clamp(20px, 4vw, 36px);
  border-radius: var(--radius);
  background: var(--card);
  border: 1px solid var(--border);
  box-shadow: var(--shadow);
  backdrop-filter: blur(18px);
}

.header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
}

.eyebrow {
  text-transform: uppercase;
  letter-spacing: 0.2em;
  font-size: 0.72rem;
  color: var(--muted);
  margin: 0 0 10px;
}

h1 {
  margin: 0;
  font-size: clamp(2rem, 3vw, 2.6rem);
}

.subtitle {
  margin: 10px 0 0;
  color: var(--muted);
  max-width: 520px;
}

.composer {
  display: grid;
  gap: 16px;
}

.input-wrap {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 12px;
}

input[type='text'] {
  padding: 12px 14px;
  border-radius: 14px;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  font-size: 1rem;
  transition:
    border 0.2s ease,
    box-shadow 0.2s ease;
}

input[type='text']:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

.stats {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

.stat {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  border-radius: 999px;
  background: var(--surface);
  border: 1px solid var(--border);
  font-size: 0.9rem;
  color: var(--muted);
}

.stat strong {
  font-family: 'Space Mono', ui-monospace, SFMono-Regular, monospace;
  color: var(--text);
  font-size: 0.95rem;
}

.list-card {
  display: grid;
  gap: 16px;
}

.list-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}

.list-head h2 {
  margin: 0;
  font-size: 1.2rem;
}

.list-hint {
  font-size: 0.85rem;
  color: var(--muted);
  font-family: 'Space Mono', ui-monospace, SFMono-Regular, monospace;
}

.todo-list {
  max-height: clamp(320px, 55vh, 520px);
  overflow-y: auto;
  display: grid;
  gap: 12px;
  padding: 6px;
  margin: -6px;
}

.todo-item {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: 12px;
  align-items: center;
  padding: 14px 16px;
  border-radius: 16px;
  background: var(--surface);
  border: 1px solid var(--border);
  animation: fadeUp 0.4s ease both;
}

.todo-item.done {
  opacity: 0.7;
}

.todo-text {
  margin: 0;
  font-size: 0.98rem;
}

.todo-item.done .todo-text {
  text-decoration: line-through;
  color: var(--muted);
}

.todo-check {
  display: inline-flex;
  align-items: center;
}

.todo-check input {
  width: 18px;
  height: 18px;
  accent-color: var(--accent);
}

.checkmark {
  display: none;
}

.todo-actions {
  display: inline-flex;
  gap: 8px;
  flex-wrap: wrap;
}

.btn {
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text);
  padding: 8px 14px;
  border-radius: 999px;
  font-size: 0.88rem;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  transition: all 0.2s ease;
}

.btn:disabled {
  cursor: not-allowed;
  opacity: 0.6;
}

.btn.primary {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
  font-weight: 600;
}

.btn.primary:hover:not(:disabled) {
  background: var(--accent-strong);
  border-color: var(--accent-strong);
}

.btn.ghost:hover:not(:disabled) {
  border-color: var(--accent);
  color: var(--accent);
}

.btn.danger {
  border-color: transparent;
  color: var(--danger);
  background: var(--danger-soft);
}

.mode-toggle {
  white-space: nowrap;
}

.mode-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: #facc15;
  box-shadow: 0 0 0 3px rgba(250, 204, 21, 0.2);
}

.mode-dot.dark {
  background: #38bdf8;
  box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.2);
}

.empty {
  text-align: center;
  padding: 32px 12px;
  color: var(--muted);
  border-radius: 16px;
  border: 1px dashed var(--border);
}

.list-footer {
  text-align: center;
  font-size: 0.85rem;
  color: var(--muted);
  padding: 8px 0 12px;
}

@keyframes fadeUp {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@media (max-width: 820px) {
  .header {
    flex-direction: column;
    align-items: flex-start;
  }

  .input-wrap {
    grid-template-columns: 1fr;
  }

  .todo-item {
    grid-template-columns: auto 1fr;
  }

  .todo-actions {
    grid-column: 1 / -1;
    justify-content: flex-end;
  }
}
</style>

License

MIT