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 🙏

© 2025 – Pkg Stats / Ryan Hefner

itc-otp-verification

v1.0.0

Published

A modern, customizable OTP (One-Time Password) verification component for Vue 3 + Quasar with auto-navigation, paste support, and timer functionality

Downloads

97

Readme

OTP Verification Component

A modern, customizable OTP (One-Time Password) verification dialog component for Vue 3 + Quasar applications with automatic field navigation, paste support, and countdown timer.

Features

  • Auto-focus & Navigation - Automatically focuses first field and moves between fields
  • Keyboard Support - Arrow keys, backspace navigation
  • Paste Support - Paste OTP from clipboard
  • Countdown Timer - Shows expiration time with resend option
  • Customizable Length - Support for 4, 6, or any digit OTP
  • Phone & Email - Supports both phone and email verification
  • Responsive - Works on desktop and mobile
  • Loading States - Built-in loading indicator for submit button
  • v-model Support - Easy dialog visibility control
  • TypeScript Ready - Full TypeScript support

Installation

npm install @itc/otp-verification
# or
pnpm install @itc/otp-verification

Requirements

  • Vue 3.x
  • Quasar 2.x

These are peer dependencies and must be installed in your project.

Usage

Basic Example

<script setup>
import { ref } from 'vue'
import { OtpVerification } from '@itc/otp-verification'

const showOtp = ref(false)
const contactInfo = ref({
  country_code: '+1',
  phone_mobile: '5551234567'
})
const otpTimer = ref('02:00')
const loading = ref(false)

const handleSubmit = (otp) => {
  console.log('OTP entered:', otp)
  loading.value = true
  // Your verification API call here
}

const handleResend = () => {
  console.log('Resend OTP')
  otpTimer.value = '02:00'
  // Your resend OTP API call here
}
</script>

<template>
  <div>
    <q-btn label="Verify Phone" @click="showOtp = true" />
    
    <OtpVerification
      v-model="showOtp"
      :contact-info="contactInfo"
      contact-type="phone"
      :otp-expire-counter="otpTimer"
      :loading="loading"
      @submit-otp="handleSubmit"
      @resend-otp="handleResend"
    />
  </div>
</template>

Email Verification

<script setup>
import { ref } from 'vue'
import { OtpVerification } from '@itc/otp-verification'

const showOtp = ref(false)
const contactInfo = ref({
  email: '[email protected]'
})
const otpTimer = ref('03:00')

const handleSubmit = (otp) => {
  // Verify email OTP
  console.log('Email OTP:', otp)
}
</script>

<template>
  <OtpVerification
    v-model="showOtp"
    :contact-info="contactInfo"
    contact-type="email"
    :otp-expire-counter="otpTimer"
    :otp-length="6"
    @submit-otp="handleSubmit"
    @resend-otp="handleResend"
  />
</template>

With Timer Management

<script setup>
import { ref, onUnmounted } from 'vue'
import { OtpVerification } from '@itc/otp-verification'

const showOtp = ref(false)
const contactInfo = ref({
  country_code: '+1',
  phone_mobile: '5551234567'
})
const otpTimer = ref('02:00')
const loading = ref(false)
let timerInterval = null

const startTimer = () => {
  let seconds = 120 // 2 minutes
  timerInterval = setInterval(() => {
    if (seconds <= 0) {
      clearInterval(timerInterval)
      otpTimer.value = '00:00'
      return
    }
    seconds--
    const mins = Math.floor(seconds / 60)
    const secs = seconds % 60
    otpTimer.value = `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`
  }, 1000)
}

const handleSubmit = async (otp) => {
  loading.value = true
  try {
    const response = await fetch('/api/verify-otp', {
      method: 'POST',
      body: JSON.stringify({ otp, phone: contactInfo.value.phone_mobile })
    })
    if (response.ok) {
      showOtp.value = false
      console.log('OTP verified successfully')
    }
  } finally {
    loading.value = false
  }
}

const handleResend = async () => {
  await fetch('/api/resend-otp', {
    method: 'POST',
    body: JSON.stringify({ phone: contactInfo.value.phone_mobile })
  })
  otpTimer.value = '02:00'
  startTimer()
}

const openOtpDialog = () => {
  showOtp.value = true
  startTimer()
}

onUnmounted(() => {
  if (timerInterval) clearInterval(timerInterval)
})
</script>

<template>
  <div>
    <q-btn label="Verify Phone" @click="openOtpDialog" />
    
    <OtpVerification
      v-model="showOtp"
      :contact-info="contactInfo"
      contact-type="phone"
      :otp-expire-counter="otpTimer"
      :loading="loading"
      @submit-otp="handleSubmit"
      @resend-otp="handleResend"
      @close="showOtp = false"
    />
  </div>
</template>

Props

| Prop | Type | Default | Required | Description | |------|------|---------|----------|-------------| | modelValue | Boolean | false | Yes | Controls dialog visibility (v-model) | | contactInfo | Object | {} | Yes | Contact information object | | contactType | String | 'phone' | No | Type of contact: 'phone' or 'email' | | otpLength | Number | 4 | No | Number of OTP digits (4, 6, etc.) | | otpExpireCounter | String\|Number | 0 | Yes | Timer display (e.g., '02:00') | | loading | Boolean | false | No | Loading state for submit button | | dialogTitle | String | 'OTP VERIFICATION' | No | Dialog header title | | continueLabel | String | 'Continue' | No | Submit button label | | persistent | Boolean | true | No | Prevents closing dialog on outside click |

contactInfo Object Structure

For phone verification:

{
  country_code: '+1',        // Optional
  phone_mobile: '5551234567' // Required
  // or
  phone: '5551234567'        // Alternative to phone_mobile
}

For email verification:

{
  email: '[email protected]' // Required
}

Events

| Event | Payload | Description | |-------|---------|-------------| | update:modelValue | Boolean | Emitted when dialog visibility changes | | submit-otp | String | Emitted with OTP value when submit button is clicked | | resend-otp | - | Emitted when "Resend OTP" link is clicked | | close | - | Emitted when dialog is closed |

Features in Detail

Auto-Navigation

The component automatically moves focus to the next field when a digit is entered, and to the previous field when backspace is pressed on an empty field.

Paste Support

Users can paste the entire OTP code, and it will be automatically distributed across all input fields.

Keyboard Navigation

  • Arrow Left/Right: Navigate between fields
  • Backspace: Clear current field and move to previous
  • Delete: Clear current field

Timer Display

Shows a countdown timer with the format MM:SS. When the timer reaches 00:00, the "Resend OTP" link appears automatically.

Responsive Design

The component adapts its input field width based on screen size:

  • Desktop: 6 characters width
  • Mobile: 4 characters width

Submit Button State

The submit button is automatically disabled until all OTP fields are filled.

Customization

Custom Styling

You can pass additional Quasar dialog props using v-bind="$attrs":

<OtpVerification
  v-model="showOtp"
  :contact-info="contactInfo"
  :otp-expire-counter="otpTimer"
  maximized
  transition-show="slide-up"
  transition-hide="slide-down"
  @submit-otp="handleSubmit"
/>

Custom Labels

<OtpVerification
  v-model="showOtp"
  :contact-info="contactInfo"
  :otp-expire-counter="otpTimer"
  dialog-title="Verify Your Phone"
  continue-label="Verify"
  @submit-otp="handleSubmit"
/>

Browser Support

  • Chrome/Edge (latest)
  • Firefox (latest)
  • Safari (latest)
  • Mobile browsers (iOS Safari, Chrome Mobile)

License

MIT

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Support

For issues or questions, please visit: GitHub Issues

Installation

npm install company-branding
# or
pnpm install company-branding

Usage

OtpVerification Component

A complete OTP verification dialog with field management, timer, and resend functionality.

Basic Usage

<script setup>
import { ref } from 'vue'
import { OtpVerification } from 'company-branding'

const showOtp = ref(false)
const contactInfo = ref({
  country_code: '+1',
  phone_mobile: '1234567890'
})
const otpTimer = ref('02:00')
const loading = ref(false)

const handleSubmit = (otp) => {
  console.log('OTP entered:', otp)
  loading.value = true
  // Your verification logic here
}

const handleResend = () => {
  // Your resend OTP logic here
  otpTimer.value = '02:00'
}
</script>

<template>
  <OtpVerification
    v-model="showOtp"
    :contact-info="contactInfo"
    contact-type="phone"
    :otp-expire-counter="otpTimer"
    :loading="loading"
    @submit-otp="handleSubmit"
    @resend-otp="handleResend"
    @close="showOtp = false"
  />
</template>

With Email Contact

<script setup>
import { ref } from 'vue'
import { OtpVerification } from 'company-branding'

const showOtp = ref(false)
const contactInfo = ref({
  email: '[email protected]'
})
const otpTimer = ref('03:00')

const handleSubmit = (otp) => {
  // Verify OTP
  console.log('OTP:', otp)
}
</script>

<template>
  <OtpVerification
    v-model="showOtp"
    :contact-info="contactInfo"
    contact-type="email"
    :otp-expire-counter="otpTimer"
    :otp-length="6"
    @submit-otp="handleSubmit"
    @resend-otp="handleResend"
  />
</template>

OtpVerification Props

| Prop | Type | Default | Required | Description | |------|------|---------|----------|-------------| | modelValue | Boolean | false | Yes | Controls dialog visibility (v-model) | | contactInfo | Object | {} | Yes | Contact information object | | contactType | String | 'phone' | No | Type of contact: 'phone' or 'email' | | otpLength | Number | 4 | No | Number of OTP digits | | otpExpireCounter | String\|Number | 0 | Yes | Timer display (e.g., '02:00') | | loading | Boolean | false | No | Loading state for submit button | | dialogTitle | String | 'OTP VERIFICATION' | No | Dialog header title | | continueLabel | String | 'Continue' | No | Submit button label | | persistent | Boolean | true | No | Prevents closing on click outside |

contactInfo Object Structure

For phone:

{
  country_code: '+1',      // Optional
  phone_mobile: '1234567890', // Required
  // or
  phone: '1234567890'      // Alternative to phone_mobile
}

For email:

{
  email: '[email protected]' // Required
}

OtpVerification Events

| Event | Payload | Description | |-------|---------|-------------| | update:modelValue | Boolean | Emitted when dialog visibility changes | | submit-otp | String | Emitted with OTP value when submit clicked | | resend-otp | - | Emitted when resend OTP is clicked | | close | - | Emitted when dialog is closed |

Features

  • Auto-focus - First field is auto-focused
  • Auto-navigation - Automatically moves to next field on input
  • Arrow key navigation - Use arrow keys to move between fields
  • Backspace support - Moves to previous field on backspace
  • Paste support - Paste OTP from clipboard
  • Timer display - Shows countdown with resend option
  • Responsive - Adapts to mobile and desktop
  • Validation - Submit button disabled until all fields filled

CompanyBranding Component

A dynamic logo component that loads brand-specific logos based on domain names.

Basic Usage

<script setup>
import { CompanyBranding } from 'company-branding'
</script>

<template>
  <CompanyBranding />
</template>

With Custom Fallback

⚠️ IMPORTANT: You MUST import images from your assets:

<script setup>
import { CompanyBranding } from 'company-branding'
import logoFallback from '@/assets/logos/logo-horizontal.png'
</script>

<template>
  <CompanyBranding :fallback="logoFallback" />
</template>

With Additional Props

<template>
  <CompanyBranding 
    :fallback="logoFallback"
    height="80px"
    width="200px"
    fit="contain"
    class="my-logo"
  />
</template>

How It Works

The component automatically:

  1. Reads window.location.hostname (e.g., dashboard.acme.com)
  2. Extracts the company name (second-to-last part: acme)
  3. Attempts to load /logo-horizontal-acme.png
  4. Falls back to provided fallback if not found

Domain Examples:

  • dashboard.acme.com/logo-horizontal-acme.png
  • portal.techcorp.io/logo-horizontal-techcorp.png
  • localhost → Uses fallback

CompanyBranding Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | fallback | String | Built-in SVG placeholder | Fallback image URL (must be imported) |

All other props are passed through to q-img.

Logo Naming Convention

Place logos in your app's public folder:

your-app/
└── public/
    ├── logo-horizontal-acme.png
    ├── logo-horizontal-techcorp.png
    └── logo-horizontal-clientname.png

Complete Example

<script setup>
import { ref, onMounted } from 'vue'
import { OtpVerification, CompanyBranding } from 'company-branding'
import defaultLogo from '@/assets/logos/logo-horizontal.png'

const showOtp = ref(false)
const contactInfo = ref({
  country_code: '+1',
  phone_mobile: '5551234567'
})
const otpTimer = ref('02:00')
const loading = ref(false)

let timerInterval = null

const startTimer = () => {
  let seconds = 120
  timerInterval = setInterval(() => {
    if (seconds <= 0) {
      clearInterval(timerInterval)
      otpTimer.value = '00:00'
      return
    }
    seconds--
    const mins = Math.floor(seconds / 60)
    const secs = seconds % 60
    otpTimer.value = `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`
  }, 1000)
}

const handleSubmit = async (otp) => {
  loading.value = true
  try {
    // Your API call to verify OTP
    const response = await fetch('/api/verify-otp', {
      method: 'POST',
      body: JSON.stringify({ otp, phone: contactInfo.value.phone_mobile })
    })
    if (response.ok) {
      showOtp.value = false
      // Success
    }
  } finally {
    loading.value = false
  }
}

const handleResend = async () => {
  // Your API call to resend OTP
  await fetch('/api/resend-otp', {
    method: 'POST',
    body: JSON.stringify({ phone: contactInfo.value.phone_mobile })
  })
  otpTimer.value = '02:00'
  startTimer()
}

const openOtpDialog = () => {
  showOtp.value = true
  startTimer()
}

onMounted(() => {
  // Cleanup on unmount
  return () => {
    if (timerInterval) clearInterval(timerInterval)
  }
})
</script>

<template>
  <div class="app">
    <header class="app-header">
      <CompanyBranding 
        :fallback="defaultLogo"
        height="60px"
        width="200px"
        class="header-logo"
      />
    </header>

    <main>
      <q-btn label="Verify Phone" @click="openOtpDialog" />
    </main>

    <OtpVerification
      v-model="showOtp"
      :contact-info="contactInfo"
      contact-type="phone"
      :otp-expire-counter="otpTimer"
      :otp-length="4"
      :loading="loading"
      @submit-otp="handleSubmit"
      @resend-otp="handleResend"
      @close="showOtp = false"
    />
  </div>
</template>

Requirements

  • Vue 3.x
  • Quasar 2.x

These are peer dependencies and must be installed in your project.

License

MIT

Getting Started

Prerequisites

  • Node.js 20.19+ or 22.12+
  • pnpm (recommended) or npm/yarn

Installation

  1. Clone or use this template:

    git clone <your-repo-url>
    cd <your-project-name>
  2. Install dependencies:

    pnpm install
    # or
    npm install
  3. Update package.json:

    • Change name to your package name
    • Update version, description, author, license, and keywords
    • Adjust dependencies and devDependencies as needed
  4. Update src/index.ts:

    • Export your main components/utilities
    • Example:
      export { default as MyComponent } from "./components/MyComponent.vue";
      export * from "./utils/helpers";

Usage

Path Aliases

This template supports importing files using the src/ prefix without relative paths:

// ✅ Good - Using path alias
import { API_BASE_URL } from "src/consts";
import MyComponent from "src/components/MyComponent.vue";
import { helper } from "src/utils/helpers";

// ❌ Avoid - Relative paths (still works, but not recommended)
import { API_BASE_URL } from "../consts";
import MyComponent from "./components/MyComponent.vue";

Adding More Path Aliases

To add additional path aliases (e.g., @/, @components/, etc.):

  1. Update tsconfig.json:

    {
      "compilerOptions": {
        "baseUrl": ".",
        "paths": {
          "src/*": ["./src/*"],
          "@/*": ["./src/*"],
          "@components/*": ["./src/components/*"],
          "@utils/*": ["./src/utils/*"]
        }
      }
    }
  2. Update vite.config.ts:

    resolve: {
      alias: {
        'src': resolve(__dirname, './src'),
        '@': resolve(__dirname, './src'),
        '@components': resolve(__dirname, './src/components'),
        '@utils': resolve(__dirname, './src/utils')
      }
    }

    The vite-tsconfig-paths plugin will automatically read from tsconfig.json, but explicit aliases in Vite config ensure build-time resolution works correctly.

  3. Restart your TypeScript server (in VS Code/Cursor: Cmd+Shift+P → "TypeScript: Restart TS Server")

Project Structure

├─ src/
│   ├─ components/          # Vue components
│   │   └─ ExampleComponent.vue
│   ├─ utils/              # Utility functions (optional)
│   ├─ types/              # TypeScript type definitions (optional)
│   ├─ consts.ts           # Constants
│   ├─ index.ts            # Main export file
│   └─ vue-shim.d.ts       # Vue type declarations
├─ dist/                   # Build output (generated)
├─ package.json
├─ tsconfig.json
└─ vite.config.ts

Building

Build for Production

pnpm run build
# or
npm run build

This will:

  • Compile TypeScript to JavaScript
  • Bundle Vue components
  • Generate both ES Modules (.js) and CommonJS (.cjs) formats
  • Output to dist/ directory with preserved src/ structure

Build Output Structure

dist/
└─ src/
   ├─ index.js              # ES Module entry
   ├─ index.cjs             # CommonJS entry
   ├─ components/
   │   └─ ...
   └─ ...

Testing Locally

Option 1: Using pnpm link (Recommended)

  1. In your plugin directory:

    pnpm link --global
  2. In your test project:

    pnpm link --global <your-package-name>
  3. Use in your project:

    <script setup>
    import { ExampleComponent } from "<your-package-name>";
    // or
    import { ExampleComponent } from "<your-package-name>/src/components/ExampleComponent.vue";
    </script>

Option 2: Using File Path

  1. In your test project's package.json:

    {
      "dependencies": {
        "<your-package-name>": "file:../path/to/your/plugin"
      }
    }
  2. Install:

    pnpm install
  3. Use in your project:

    <script setup>
    import { ExampleComponent } from "<your-package-name>";
    </script>

Unlinking After Local Development

When you're done with local development and want to switch back to the published version (or remove the link):

For Option 1 (pnpm/npm link):

  1. In your test project, unlink the package:

    # For pnpm
    pnpm unlink --global <your-package-name>
    
    # For npm
    npm unlink <your-package-name>
  2. Reinstall the package from npm (or your registry):

    pnpm install <your-package-name>
    # or
    npm install <your-package-name>
  3. Optionally, unlink from global (in your plugin directory):

    # For pnpm
    pnpm unlink --global
    
    # For npm
    npm unlink -g

    Note: This step is optional. The global link can remain for future development sessions.

For Option 2 (File Path):

  1. Remove the file path dependency from your test project's package.json:

    {
      "dependencies": {
        // Remove or comment out:
        // "<your-package-name>": "file:../path/to/your/plugin"
      }
    }
  2. Reinstall the package from npm:

    pnpm install <your-package-name>
    # or
    npm install <your-package-name>

Troubleshooting Unlinking

If you encounter issues after unlinking:

  • Clear node_modules and reinstall:

    rm -rf node_modules
    pnpm install
    # or
    npm install
  • Check for leftover symlinks:

    # Check if symlink still exists
    ls -la node_modules/<your-package-name>
    
    # If it's still a symlink, remove it manually
    rm node_modules/<your-package-name>
    pnpm install
  • Verify package source:

    # Check where the package is coming from
    pnpm why <your-package-name>
    # or
    npm ls <your-package-name>

Development Workflow

For active development with auto-rebuild:

Run in watch mode:

pnpm run dev

Changes will rebuild automatically. Restart your test project's dev server to pick up changes.

Publishing to npm

Before Publishing

  1. Update package.json:

    • Set correct name (must be unique on npm)
    • Update version (follow semver)
    • Add description, keywords, author, license
    • Verify files array includes only what should be published:
      {
        "files": ["dist"]
      }
  2. Build the package:

    pnpm run build
  3. Test the build locally (see Testing Locally section above)

Publishing Steps

  1. Login to npm:

    npm login
  2. Verify you're logged in:

    npm whoami
  3. Check what will be published:

    npm pack --dry-run
  4. Publish:

    npm publish

    For scoped packages (e.g., @your-org/package-name):

    npm publish --access public
  5. Verify on npm: Visit https://www.npmjs.com/package/<your-package-name>

Version Management

Use npm version commands to bump versions:

# Patch version (1.0.0 → 1.0.1)
npm version patch

# Minor version (1.0.0 → 1.1.0)
npm version minor

# Major version (1.0.0 → 2.0.0)
npm version major

Then publish:

npm publish

Configuration

External Dependencies

By default, vue and quasar are marked as external (not bundled). To add more:

Update vite.config.ts:

rollupOptions: {
  external: (id) => {
    return (
      id === "vue" ||
      id === "quasar" ||
      id.startsWith("@quasar/") ||
      id.startsWith("quasar/") ||
      id === "some-other-package"
    ); // Add here
  };
}

Build Formats

The template builds both ES Modules and CommonJS. To change formats:

Update vite.config.ts:

build: {
  lib: {
    formats: ["es", "cjs"]; // or ['es'], ['cjs'], etc.
  }
}

Handling External Dependencies

Using Quasar Extras and Icon Sets

If your plugin requires dependencies like @quasar/extras, FontAwesome icons, or other Quasar-related packages that may not be present in the target project, you should declare them as peer dependencies rather than regular dependencies.

Why Peer Dependencies?

  • Smaller bundle size: External dependencies aren't bundled into your package
  • Version control: Consumers control which versions to install
  • Avoid conflicts: Prevents duplicate packages in the consumer's project
  • Best practice: Standard approach for library/plugin packages

Step 1: Add Peer Dependencies

Update your package.json to include peer dependencies:

{
  "peerDependencies": {
    "quasar": "^2.18.6",
    "vue": "^3.5.25",
    "@quasar/extras": "^1.0.0"
  },
  "peerDependenciesMeta": {
    "@quasar/extras": {
      "optional": true
    }
  }
}

Notes:

  • peerDependencies: Required dependencies that consumers must install
  • peerDependenciesMeta: Mark dependencies as optional if they're not always needed
  • Keep quasar and vue in dependencies for development, but also list them in peerDependencies for consumers

Step 2: Ensure Externalization in Build Config

Your vite.config.ts should already externalize @quasar/ packages (this is already configured):

rollupOptions: {
  external: (id) => {
    return (
      id === "vue" ||
      id === "quasar" ||
      id.startsWith("@quasar/") || // ✅ Already covers @quasar/extras
      id.startsWith("quasar/")
    );
  };
}

If you need to externalize additional packages, add them to the external function:

rollupOptions: {
  external: (id) => {
    return (
      id === "vue" ||
      id === "quasar" ||
      id.startsWith("@quasar/") ||
      id.startsWith("quasar/") ||
      id === "some-other-package" || // Add specific packages
      id.startsWith("@some-scope/") // Or entire scopes
    );
  };
}

Step 3: Document for Consumers

Add installation instructions to your README for consumers:

## Installation

```bash
npm install your-package @quasar/extras
# or
pnpm install your-package @quasar/extras
```

Quasar Configuration

If you're using FontAwesome icons or other icon sets from @quasar/extras, configure them in your Quasar project:

For Quasar CLI projects (quasar.conf.js):

module.exports = function (ctx) {
  return {
    extras: [
      "fontawesome-v6", // or 'fontawesome-v5', 'material-icons', etc.
      "material-icons-outlined",
    ],
    // ... rest of config
  };
};

For Vite projects (quasar.config.js):

export default {
  extras: ["fontawesome-v6", "material-icons-outlined"],
  // ... rest of config
};

Step 4: Handle Missing Dependencies Gracefully (Optional)

If you want to provide fallbacks when dependencies aren't available, you can check for them:

<script setup>
import { computed } from "vue";

const props = defineProps({
  icon: { type: String, default: "close" },
});

// Example: Fallback to Material Icons if FontAwesome not available
const safeIcon = computed(() => {
  // If using FontAwesome icon (fa- prefix), ensure @quasar/extras is installed
  if (props.icon?.startsWith("fa-") || props.icon?.startsWith("fas ")) {
    // Document that @quasar/extras is required for FontAwesome icons
    return props.icon;
  }
  return props.icon;
});
</script>

Best Practices

  • Use peer dependencies for packages that should be provided by the consumer
  • Use regular dependencies only for packages that are internal to your plugin
  • Document all peer dependencies in your README
  • Use Material Icons by default (built into Quasar) when possible
  • Only require @quasar/extras when you specifically need FontAwesome, Material Symbols, etc.
  • Don't bundle large dependencies like icon sets into your package
  • Don't assume consumers have specific Quasar extras installed

Troubleshooting

TypeScript Errors

  • "Cannot find module 'src/...'": Restart TypeScript server
  • "Cannot find module 'path'": Ensure @types/node is installed
  • Path aliases not working: Check both tsconfig.json and vite.config.ts have matching aliases

Build Errors

  • "Rollup failed to resolve import": Add the package to external in vite.config.ts
  • "Preprocessor dependency not found": Install required preprocessors (e.g., sass-embedded for SCSS)

Import Errors in Test Project

  • Ensure the package is built (pnpm run build)
  • Check package.json exports are correct
  • Verify the import path matches your exports

License

ISC (or update to your preferred license)

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Build and test locally
  5. Submit a pull request

Happy coding! 🚀