Full Code of nuxt-modules/ionic for AI

main 1f0892393015 cached
85 files
130.3 KB
36.6k tokens
9 symbols
1 requests
Download .txt
Repository: nuxt-modules/ionic
Branch: main
Commit: 1f0892393015
Files: 85
Total size: 130.3 KB

Directory structure:
gitextract_uaky4g3w/

├── .editorconfig
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug-report.yml
│   │   ├── config.yml
│   │   ├── documentation.yml
│   │   ├── feature-suggestion.yml
│   │   └── help-wanted.yml
│   └── workflows/
│       ├── ci.yml
│       ├── provenance.yml
│       └── release.yml
├── .gitignore
├── CODEOWNERS
├── CODE_OF_CONDUCT.md
├── LICENCE
├── README.md
├── build.config.ts
├── docs/
│   ├── .gitignore
│   ├── app.config.ts
│   ├── assets/
│   │   └── css/
│   │       └── main.css
│   ├── components/
│   │   └── AppHeaderLogo.vue
│   ├── content/
│   │   ├── 1.get-started/
│   │   │   ├── .navigation.yml
│   │   │   ├── 1.introduction.md
│   │   │   ├── 2.installation.md
│   │   │   ├── 3.configuration.md
│   │   │   ├── 4.enabling-capacitor.md
│   │   │   └── 5.watch-outs.md
│   │   ├── 2.overview/
│   │   │   ├── .navigation.yml
│   │   │   ├── 1.routing.md
│   │   │   ├── 2.theming.md
│   │   │   ├── 3.ionic-auto-imports.md
│   │   │   ├── 4.module-utilities.md
│   │   │   ├── 5.icons.md
│   │   │   └── 6.deployment.md
│   │   ├── 3.cookbook/
│   │   │   ├── .navigation.yml
│   │   │   ├── 1.customising-app-vue.md
│   │   │   ├── 2.local-development.md
│   │   │   ├── 3.app-tabs.md
│   │   │   ├── 4.page-metadata.md
│   │   │   ├── 5.creating-ios-android-apps.md
│   │   │   ├── 6.web-and-device.md
│   │   │   └── 7.live-updates.md
│   │   └── index.md
│   ├── nuxt.config.ts
│   ├── package.json
│   ├── tokens.config.ts
│   └── tsconfig.json
├── eslint.config.js
├── package.json
├── playground/
│   ├── assets/
│   │   └── css/
│   │       └── ionic.css
│   ├── capacitor.config.ts
│   ├── components/
│   │   └── ExploreContainer.vue
│   ├── composables/
│   │   └── usePhotoGallery.ts
│   ├── middleware/
│   │   └── auth.global.ts
│   ├── nuxt.config.ts
│   ├── package.json
│   ├── pages/
│   │   ├── overlap.vue
│   │   ├── tabs/
│   │   │   ├── tab1/
│   │   │   │   └── index.vue
│   │   │   ├── tab2/
│   │   │   │   └── index.vue
│   │   │   ├── tab3/
│   │   │   │   ├── index.vue
│   │   │   │   └── page-two.vue
│   │   │   └── tab4/
│   │   │       └── index.vue
│   │   └── tabs.vue
│   └── tsconfig.json
├── pnpm-workspace.yaml
├── renovate.json
├── src/
│   ├── imports.ts
│   ├── module.ts
│   ├── parts/
│   │   ├── capacitor.ts
│   │   ├── components.ts
│   │   ├── css.ts
│   │   ├── icons.ts
│   │   ├── meta.ts
│   │   └── router.ts
│   ├── runtime/
│   │   ├── app.vue
│   │   ├── components/
│   │   │   └── IonAnimation.vue
│   │   ├── composables/
│   │   │   └── head.ts
│   │   └── plugins/
│   │       ├── ionic.ts
│   │       └── router.ts
│   └── utils.ts
├── test/
│   ├── e2e/
│   │   ├── ion-head.spec.ts
│   │   └── ssr.spec.ts
│   └── unit/
│       ├── capacitor.spec.ts
│       └── imports.spec.ts
├── tsconfig.json
└── vitest.config.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
root = true

[*]
indent_size = 2
indent_style = space
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false


================================================
FILE: .github/FUNDING.yml
================================================
github: [danielroe]


================================================
FILE: .github/ISSUE_TEMPLATE/bug-report.yml
================================================
name: "\U0001F41B Bug report"
description: Something's not working
labels: ["bug"]
body:
  - type: textarea
    validations:
      required: true
    attributes:
      label: 🐛 The bug
      description: What isn't working? Describe what the bug is.
  - type: input
    validations:
      required: true
    attributes:
      label: 🛠️ To reproduce
      description: A reproduction of the bug via https://stackblitz.com/github/nuxt-modules/ionic/tree/main/playground
      placeholder: https://stackblitz.com/[...]
  - type: textarea
    validations:
      required: true
    attributes:
      label: 🌈 Expected behaviour
      description: What did you expect to happen? Is there a section in the docs about this?
  - type: textarea
    attributes:
      label: ℹ️ Additional context
      description: Add any other context about the problem here.


================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
contact_links:
  - name: Nuxt Community Discord
    url: https://discord.nuxtjs.org/
    about: Consider asking questions about the module here.


================================================
FILE: .github/ISSUE_TEMPLATE/documentation.yml
================================================
name: "\U0001F4DA Documentation"
description: How do I ... ?
labels: ["documentation"]
body:
  - type: textarea
    validations:
      required: true
    attributes:
      label: 📚 Is your documentation request related to a problem?
      description: A clear and concise description of what the problem is.
      placeholder: I feel I should be able to [...] but I can't see how to do it from the docs.
  - type: textarea
    attributes:
      label: 🔍 Where should you find it?
      description: What page of the docs do you expect this information to be found on?
  - type: textarea
    attributes:
      label: ℹ️ Additional context
      description: Add any other context or information.


================================================
FILE: .github/ISSUE_TEMPLATE/feature-suggestion.yml
================================================
name: "\U0001F195 Feature suggestion"
description: Suggest an idea
labels: ["enhancement"]
body:
  - type: textarea
    validations:
      required: true
    attributes:
      label: 🆒 Your use case
      description: Add a description of your use case, and how this feature would help you.
      placeholder: When I do [...] I would expect to be able to do [...]
  - type: textarea
    validations:
      required: true
    attributes:
      label: 🆕 The solution you'd like
      description: Describe what you want to happen.
  - type: textarea
    attributes:
      label: 🔍 Alternatives you've considered
      description: Have you considered any alternative solutions or features?
  - type: textarea
    attributes:
      label: ℹ️ Additional info
      description: Is there any other context you think would be helpful to know?


================================================
FILE: .github/ISSUE_TEMPLATE/help-wanted.yml
================================================
name: "\U0001F198 Help"
description: I need help with ...
labels: ["help"]
body:
  - type: textarea
    validations:
      required: true
    attributes:
      label: 📚 What are you trying to do?
      description: A clear and concise description of your objective.
      placeholder: I'm not sure how to [...].
  - type: textarea
    attributes:
      label: 🔍 What have you tried?
      description: Have you looked through the docs? Tried different approaches? The more detail the better.
  - type: textarea
    attributes:
      label: ℹ️ Additional context
      description: Add any other context or information.


================================================
FILE: .github/workflows/ci.yml
================================================
name: ci

on:
  pull_request:
    branches:
      - main
      - renovate/*
  push:
    branches:
      - main

jobs:
  lint:
    if: github.event_name == 'pull_request' || github.event_name == 'push' && github.ref != 'refs/heads/renovate/*' || github.event_name == 'push' && github.ref == 'refs/heads/renovate/*' && github.event.pull_request == null
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
      - run: corepack enable
      - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
        with:
          node-version: lts/*
          cache: 'pnpm'

      - name: 📦 Install dependencies
        run: pnpm install --frozen-lockfile

      - name: 🚧 Set up project
        run: pnpm dev:prepare

      - name: 🔠 Lint project
        run: pnpm run lint

  test:
    if: github.event_name == 'pull_request' || github.event_name == 'push' && github.ref != 'refs/heads/renovate/*' || github.event_name == 'push' && github.ref == 'refs/heads/renovate/*' && github.event.pull_request == null
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest]
    runs-on: ${{ matrix.os }}

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
      - run: corepack enable
      - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
        with:
          node-version: lts/-1
          cache: 'pnpm'

      - name: 📦 Install dependencies
        run: pnpm install --frozen-lockfile

      - name: 🚧 Set up project
        run: pnpm dev:prepare

      - name: 🎭 Set up playwright
        run: pnpm playwright-core install

      - name: 🧪 Test project
        run: pnpm test

      - name: 💪 Test types
        run: pnpm test:types

      - name: 🛠 Build project
        run: pnpm build

      - name: 🟩 Coverage
        if: matrix.os != 'windows-latest'
        uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6
        env:
          CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}


================================================
FILE: .github/workflows/provenance.yml
================================================
name: ci

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main
permissions:
  contents: read
jobs:
  check-provenance:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
        with:
          fetch-depth: 0
      - name: Check provenance downgrades
        uses: danielroe/provenance-action@41bcc969e579d9e29af08ba44fcbfdf95cee6e6c # v0.1.1
        with:
          fail-on-provenance-change: true


================================================
FILE: .github/workflows/release.yml
================================================
name: Release

permissions:
  contents: write

on:
  push:
    tags:
      - 'v*'
  workflow_dispatch:

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
      - run: corepack enable
      - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
        with:
          node-version: lts/*
          cache: 'pnpm'

      - run: npx changelogithub
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .gitignore
================================================
# Dependencies
node_modules

# Capacitor projects
playground/android
playground/ios

# Logs
*.log*

# Temp directories
.temp
.tmp
.cache

# Yarn
**/.yarn/cache
**/.yarn/*state*

# Generated dirs
dist

# Nuxt
.nuxt
.output
.vercel_build_output
.build-*
.env
.netlify

# Env
.env

# Testing
reports
coverage
*.lcov
.nyc_output

# VSCode
.vscode

# Intellij idea
*.iml
.idea

# Zed
.zed

# OSX
.DS_Store
.AppleDouble
.LSOverride
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

ionic.config.json


================================================
FILE: CODEOWNERS
================================================
* @danielroe


================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct

## Our pledge

In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.

## Our standards

Examples of behavior that contributes to creating a positive environment include:

- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members

Examples of unacceptable behavior by participants include:

- The use of sexualized language or imagery and unwelcome sexual attention or advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting

## Our responsibilities

Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.

Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.

## Scope

This Code of Conduct applies within all project spaces, and it also applies when an individual is representing the project or its community in public spaces. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [daniel@roe.dev](mailto:daniel@roe.dev). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.

Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html

[homepage]: https://www.contributor-covenant.org

For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq


================================================
FILE: LICENCE
================================================
MIT License

Copyright (c) 2022 Daniel Roe

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
[![@nuxtjs/ionic](./docs/public/cover.jpg)](https://ionic.nuxtjs.org)

# Nuxt Ionic

[![npm version][npm-version-src]][npm-version-href]
[![npm downloads][npm-downloads-src]][npm-downloads-href]
[![Github Actions][github-actions-src]][github-actions-href]
[![Codecov][codecov-src]][codecov-href]
[![nuxt.care health](https://img.shields.io/endpoint?url=https://nuxt.care/api/badge/ionic)](https://nuxt.care/?search=ionic)

> [Ionic](https://ionicframework.com/docs/) integration for [Nuxt](https://nuxtjs.org)

- [✨  Changelog](https://github.com/nuxt-modules/ionic/blob/main/CHANGELOG.md)
- [📖  Read the documentation](https://ionic.nuxtjs.org)
- [▶️  Online playground](https://stackblitz.com/github/nuxt-modules/ionic/tree/main/playground)

## Features

- Zero-config required
- Auto-import Ionic components, composables and icons
- Ionic Router integration
- Pre-render routes
- Mobile meta tags
- Works out-of-the-box with [Capacitor](https://capacitorjs.com/) to build mobile apps

**In progress**

- [ ] PWA Elements [#14](https://github.com/nuxt-modules/ionic/issues/14)

## Usage

👉 Check out https://ionic.nuxtjs.org.

## 💻 Development

- Clone this repository
- Enable [Corepack](https://github.com/nodejs/corepack) using `corepack enable`
- Install dependencies using `pnpm install`
- Stub module with `pnpm dev:prepare`
- Run `pnpm dev` to start [playground](./playground) in development mode

## License

Made with ❤️

Published under the [MIT License](./LICENCE).

<!-- Badges -->

[npm-version-src]: https://npmx.dev/api/registry/badge/version/@nuxtjs/ionic
[npm-version-href]: https://npmx.dev/package/@nuxtjs/ionic
[npm-downloads-src]: https://npmx.dev/api/registry/badge/downloads/@nuxtjs/ionic
[npm-downloads-href]: https://npm.chart.dev/@nuxtjs/ionic
[github-actions-src]: https://img.shields.io/github/actions/workflow/status/nuxt-modules/ionic/ci.yml?style=flat-square&branch=main
[github-actions-href]: https://github.com/nuxt-modules/ionic/actions?query=workflow%3Aci
[codecov-src]: https://img.shields.io/codecov/c/gh/nuxt-modules/ionic/main?style=flat-square
[codecov-href]: https://codecov.io/gh/nuxt-modules/ionic


================================================
FILE: build.config.ts
================================================
import { defineBuildConfig } from 'unbuild'

export default defineBuildConfig({
  // TODO: fix in unbuild
  externals: ['node:fs'],
})


================================================
FILE: docs/.gitignore
================================================
node_modules
*.iml
.idea
*.log*
.nuxt
.vscode
.DS_Store
coverage
dist
sw.*
.env
.output
.data

================================================
FILE: docs/app.config.ts
================================================
export default defineAppConfig({
  ui: {
    colors: {
      primary: 'blue',
      neutral: 'zinc',
    },
  },
  docus: {
    title: 'nuxt/ionic',
    url: 'https://ionic.nuxtjs.org/',
    description: 'Batteries-included Ionic integration for Nuxt.',
    socials: {
      twitter: 'danielcroe',
      github: 'nuxt-modules/ionic',
    },
    cover: {
      src: '/cover.jpg',
      alt: 'Nuxt Ionic module.',
    },
    github: {
      owner: 'nuxt-modules',
      repo: 'ionic',
      branch: 'main',
      root: 'docs/content',
      edit: true,
    },
    header: {
      logo: true,
    },
    footer: {
      credits: {
        icon: 'IconDocus',
        text: 'Powered by Docus',
        href: 'https://docus.com',
      },
      iconLinks: [
        {
          label: 'NuxtJS',
          href: 'https://nuxtjs.org',
          icon: 'IconNuxt',
        },
      ],
    },
  },
})


================================================
FILE: docs/assets/css/main.css
================================================
@import "tailwindcss";
@import "@nuxt/ui";


:root {
--color-blue: oklch(66.85% 0.17582 260.7);
--color-blue-50: oklch(100% 0 none);
--color-blue-100: oklch(98.081% 0.00906 258.34);
--color-blue-200: oklch(89.967% 0.0486 260.89);
--color-blue-300: oklch(82.018% 0.08975 261.51);
--color-blue-400: oklch(74.222% 0.13277 261.06);
--color-blue-500: oklch(66.85% 0.17582 260.7);
--color-blue-600: oklch(57.957% 0.23039 261.17);
--color-blue-700: oklch(49.935% 0.22723 261.78);
--color-blue-800: oklch(40.624% 0.18173 261.53);
--color-blue-900: oklch(30.965% 0.13179 260.75);
--color-blue-950: oklch(25.869% 0.105 259.91);

}

================================================
FILE: docs/components/AppHeaderLogo.vue
================================================
<template>
  <div class="logo">
    <svg
      width="32"
      height="32"
      viewBox="0 0 32 32"
      fill="none"
      xmlns="http://www.w3.org/2000/svg"
    >
      <path
        fill-rule="evenodd"
        clip-rule="evenodd"
        d="M30.4327 9.05578L30.5633 9.36054C31.5211 11.4721 32 13.6925 32 16C32 24.8163 24.8163 32 16 32C7.18367 32 0 24.8163 0 16C0 7.18367 7.18367 0 16 0C18.5905 0 21.0503 0.609524 23.3143 1.7415L23.619 1.89388L23.3578 2.11156C22.7048 2.63401 22.2041 3.28707 21.8776 4.04898L21.7905 4.26667L21.5946 4.17959C19.8313 3.35238 17.9592 2.91701 16 2.91701C8.77279 2.91701 2.91701 8.77279 2.91701 16C2.91701 23.2272 8.77279 29.083 16 29.083C23.2272 29.083 29.083 23.2054 29.083 16C29.083 14.2803 28.7565 12.5823 28.0816 10.9932L27.9946 10.7755L28.2122 10.6884C28.9741 10.4054 29.6707 9.92653 30.215 9.31701L30.4327 9.05578ZM26.4707 9.36057C28.3102 9.36057 29.8014 7.8694 29.8014 6.02996C29.8014 4.19051 28.3102 2.69934 26.4707 2.69934C24.6313 2.69934 23.1401 4.19051 23.1401 6.02996C23.1401 7.8694 24.6313 9.36057 26.4707 9.36057ZM15.9999 8.70754C11.9727 8.70754 8.7074 11.9728 8.7074 16.0001C8.7074 20.0273 11.9727 23.2926 15.9999 23.2926C20.0271 23.2926 23.2924 20.0273 23.2924 16.0001C23.2924 11.9728 20.0271 8.70754 15.9999 8.70754Z"
        fill="var(--color-blue-500)"
      />
    </svg>
    <span>
      Nuxt <span style="color: var(--color-blue-500)">Ionic</span>
    </span>
  </div>
</template>

<style scoped>
  .logo {
    display: flex;
    flex-direction: row;
    align-items: center;
    gap: 0.5rem;
    font-weight: 800;
  }
</style>


================================================
FILE: docs/content/1.get-started/.navigation.yml
================================================
icon: heroicons-outline:star
navigation.redirect: /get-started/introduction


================================================
FILE: docs/content/1.get-started/1.introduction.md
================================================
---
navigation.icon: uil:info-circle
description: 'Batteries-included, zero-config needed, Ionic integration for Nuxt'
---

`@nuxtjs/ionic` provides a batteries-included, zero-config needed, Ionic integration for Nuxt.

Supercharge your Ionic apps with the power of Nuxt, giving you the tremendous development experience you're used to
when using Nuxt for web.

Follow the installation guide to get going immediately, or continue reading to find out more about what this module provides.

::callout{color="info" icon="i-lucide-info"}
Get started with our [Installation guide](/get-started/installation).
::

## What is Ionic?

[The Ionic SDK](https://ionicframework.com/) is an open-source UI toolkit for building modern, cross-platform mobile apps from a single codebase. It deeply integrates with Vue for a delightful mobile dev experience.

::callout{color="info" icon="i-lucide-info"}
Read the [Ionic documentation for Vue](https://ionicframework.com/docs/vue/overview) to learn more about Vue development with Ionic.
::

## Features

This module attempts to get you going with Nuxt + Ionic quickly, providing sane defaults for ionic and helpful components and utilities. The configuration file can still customize and override its default behaviors and provide full customisation of ionic.

::list{type=success}
- **Ionic router integration:** continue defining routes based on the structure of your `~/pages` directory and using page-level utilities such as `definePageMeta()`.
- **Auto-imports**: Ionic components, composables and icons are all [auto-imported](https://nuxt.com/docs/guide/concepts/auto-imports) for ease of use.
- **Helpful components and utilities**: This module provides components and utilities to accomplish common tasks more easily.
- **Pre-render routes**
- **Mobile meta tags**
- **Capacitor integration**: works out-of-the-box with [Capacitor](https://capacitorjs.com/) for shipping to native platforms like iOS and Android.
::

::callout{color="info" icon="i-lucide-info"}
Read more about [our features](/overview).
::

## Cookbook

Our cookbook documentation provides common use-cases with code examples to get you going as fast as possible. And if you create something useful, come back and submit a PR to have it added to our cookbook.

::callout{color="info" icon="i-lucide-info"}
Discover common recipes in [our cookbook](/cookbook).
::


================================================
FILE: docs/content/1.get-started/2.installation.md
================================================
---
navigation.icon: uil:play-circle
description: 'Get started quickly by installing and setting up this module with the following instructions.'
---

## Prerequisites

- A fresh or existing Nuxt project
- [Nuxt's prerequisites](https://nuxt.com/docs/getting-started/installation#prerequisites)
- [Ionic's environment setup](https://ionicframework.com/docs/intro/environment)
- [Capacitor's enviroment setup](https://capacitorjs.com/docs/getting-started/environment-setup) if using iOS or Android

## Installation

::steps{level=3}

### Add module

Add `@nuxtjs/ionic` to your project's dev dependencies:
```bash [Terminal]
npx nuxi@latest module add ionic
```

### Nuxt Module

Next, add the module to your Nuxt configuration in `nuxt.config.ts`.

```js{}[nuxt.config.ts]
export default defineNuxtConfig({
  modules: ['@nuxtjs/ionic'],
  ssr: false,
})
```

::callout{color="warning" icon="i-lucide-alert-triangle"}
If deploying to iOS or Android, be aware the app must be able to run completely client-side. We recommend setting `ssr: false` in your nuxt config. Find out more in [deploying to both web and device](/cookbook/web-and-device).
::

Finally, either remove your `app.vue` file or [replace it with a custom one](/cookbook/customising-app-vue).

### Development Server

Now you'll be able to start your Nuxt app in development mode as usual:

::code-group

```bash [yarn]
yarn dev -o
```

```bash [npm]
npm run dev -- -o
```

```bash [pnpm]
pnpm dev -o
```

::
::callout{color="success" icon="i-lucide-check-circle" .font-bold}
Well done! A browser window should automatically open for <http://localhost:3000>.
::


::callout{color="info" icon="i-lucide-info"}
The first time you start a Nuxt project with `@nuxtjs/ionic` enabled, a `ionic.config.json` file will be created if it doesn't already exist.
::

::


================================================
FILE: docs/content/1.get-started/3.configuration.md
================================================
---
title: Configuration
description: >
  This module provides configuration options for itself, as well as passing through configuration for ionic.
  Configuration for capacitor is set in the usual way, via capacitor.config.ts.
navigation.icon: uil:wrench
---



### Module Config

This module exposes three keys for configuration: `integrations`, `css` and `config`:

```js [nuxt.config.ts]
export default defineNuxtConfig({
  modules: ['@nuxtjs/ionic'],
  ionic: {
    integrations: {
      //
    },
    css: {
      //
    },
    config: {
      //
    }
  },
})
```

#### `integrations`

Integrations control which other modules this module should enable and setup from the list below. Disabling them allows you to remove them, or gives you the option to set them up yourself.

- **meta**

  Default: `true`  
  Disable to take full control of meta tags.

- **router**

  Default: `true`  
  Disable to configure Ionic Router yourself.

- **icons**

  Default: `true`  
  Disable to stop icons from being auto-imported.

#### `css`

Configure which Ionic stylesheets are automatically added to your application. For more information about
these stylesheets, [see the Ionic Documentation for Stylesheets](https://ionicframework.com/docs/layout/global-stylesheets).

- **core**

  Default: `true`  
  Disable to import these CSS files manually:

  - `@ionic/vue/css/core.css`

- **basic**

  Default: `true`  
  Disable to import these CSS files manually:

  - `@ionic/vue/css/normalize.css`
  - `@ionic/vue/css/structure.css`
  - `@ionic/vue/css/typography.css`

- **utilities**

  Default: `false`  
  Enable to add extra Ionic CSS utilities:

  - `@ionic/vue/css/padding.css`
  - `@ionic/vue/css/float-elements.css`
  - `@ionic/vue/css/text-alignment.css`
  - `@ionic/vue/css/text-transformation.css`
  - `@ionic/vue/css/flex-utils.css`
  - `@ionic/vue/css/display.css`

#### `config`

Configure Ionic components globally across your app, such as app mode, tab button layout, etc. For example:

```js [nuxt.config.ts]
export default defineNuxtConfig({
  ionic: {
    config: {
      rippleEffect: false,
      mode: 'md',
      // ...
    },
  },
})
```

::callout{color="info" icon="i-lucide-info"}
Please see the [Ionic Config Options](https://ionicframework.com/docs/vue/config#config-options) for available keys, values
and examples of how they work.
::

### Capacitor Config

Capacitor is configured via the `capacitor.config.ts` file - this is only required if you are targeting native devices such as iOS or Android.

::callout{color="info" icon="i-lucide-info"}
Please see the [Capacitor Config docs](https://capacitorjs.com/docs/config) for more information.
::


================================================
FILE: docs/content/1.get-started/4.enabling-capacitor.md
================================================
---
title: Enabling Capacitor
description: ""
navigation.icon: nonicons:capacitor-16
---

[Capacitor](https://capacitorjs.com/) is a powerful tool for shipping to native platforms like iOS and Android, separate from or alongside your web app.

The good news is that it's installed by default with `@nuxtjs/ionic`. You just need to enable it in your ionic app, and choose what platforms you want to support.

> The Ionic CLI is available via `npx` or can be installed globally with `npm install -g @ionic/cli` or `yarn global add @ionic/cli` or `pnpm add -g @ionic/cli`.

::code-group

```bash [npx]
npx @ionic/cli integrations enable capacitor
npx @ionic/cli capacitor add ios
npx @ionic/cli capacitor add android
```

```bash [npm]
# ionic config set -g npmClient npm

ionic integrations enable capacitor
ionic capacitor add ios
ionic capacitor add android
```

```bash [yarn]
ionic config set -g npmClient yarn

ionic integrations enable capacitor
ionic capacitor add ios
ionic capacitor add android
```

```bash [pnpm]
ionic config set -g npmClient pnpm

ionic integrations enable capacitor
ionic capacitor add ios
ionic capacitor add android
```

::

::callout{color="info" icon="i-lucide-info"}
Read more about [creating for iOS and Android here](/cookbook/creating-ios-android-apps).
::

#### Run on iOS or Android

Once an Android or iOS project is added with Capacitor, you can run your app on an iOS or Android emulator.

::callout
Android Studio and SDK (for Android projects) or XCode (for iOS projects) are required to use the `npx cap open` or `npx cap run` command. See the [Capacitor Environment Setup docs](https://capacitorjs.com/docs/getting-started/environment-setup) for details.
::

To build, sync, and run your app:

1. Create a web build with `npx nuxt generate` or `npx nuxt build`.
2. Run `npx cap sync` to update your Capacitor project directories with your latest app build.
3. Run `npx cap run android` or `npx cap run ios` to run the app from the command line using an installed device **OR**
4. _(Optional)_ Run `npx cap open android` or `npx cap open ios` to open the project in Android Studio or XCode, respectively.

> Remember to run `npx cap sync` after every new build to ensure your Android and/or iOS project is up-to-date.

::callout{color="info" icon="i-lucide-info"}
Read more about [local development for iOS and Android here](/cookbook/local-development).
::


================================================
FILE: docs/content/1.get-started/5.watch-outs.md
================================================
---
title: Watchouts
description: ""
navigation.icon: uil:exclamation-triangle
---

This page aims to succinctly mention things to watch out for when building your Nuxt-powered Ionic application, particularly for those familiar with regular Nuxt and Vue applications.

## Pages and Navigation

::list{type="warning"}
- Avoid using `<NuxtPage>`, `<NuxtLayout>` or `<NuxtLink>`. These are currently not integrated with this module.
::

Instead, Ionic expects `<ion-router-outlet>` to show the route component, and `useIonRouter()` or the `router-link` property on any `ion-` component to change page.

```vue [app.vue]
<template>
  <ion-app>
    <ion-router-outlet />
  </ion-app>
</template>
```

::list{type="warning"}
- The root element of every page must always be `<ion-page>`. 
::

This is required by Ionic to set up page transitions and stack navigation. Each view that is navigated to using the router must include an `<ion-page>` component.

::list{type="warning"}
- When navigating from a tabbed route to a non-tabbed route, `route.params` from the auto-imported `useRoute()` will always be an empty object.
::

A current workaround is to `import { useRoute } from 'vue-router';`.

::callout{color="info" icon="i-lucide-info"}
Read more [about routing here](/overview/routing).
::

## Lifecycle Hooks

Ionic handles lifecycle events slightly differently to Vue, as it persists some components in the DOM when Vue would usually unmount them.

This means the various mounted hooks, e.g. `onBeforeMount`, may not be called when you would expect them to.

To help with this, Ionic has added extra lifecycle hooks which behave how you may have expected the mounted hooks to behave.

::callout{color="info" icon="i-lucide-info"}
Read about the [Ionic Vue lifecycle hooks here](https://ionicframework.com/docs/vue/lifecycle).
::

Because of this, some expected functionality from Nuxt or other modules may not work or may require changes to get functioning:

::list{type="warning"}
- **The composable `useHead()` will not work out of the box**.  
  See [our cookbook page](/cookbook/page-metadata) for how to continue using `useHead()`
- **Certain Vue Router components should not be used**.  
  This includes `<keep-alive>`, `<transition>`, and `<router-view>` - [read more here](https://ionicframework.com/docs/vue/lifecycle#how-ionic-framework-handles-the-life-of-a-page).
::

## No serverside rendering

::list{type="warning"}
- The application code cannot contain any serverside rendering.
::

When targeting iOS or Android devices, the build must be able to run completely on the clientside. We discuss [solutions for if you're targeting both web and device here](/cookbook/web-and-device).

::callout{color="info" icon="i-lucide-info"}
Learn more about [building for native devices here](/cookbook/creating-ios-android-apps).
::


================================================
FILE: docs/content/2.overview/.navigation.yml
================================================
icon: heroicons-outline:sparkles
navigation.redirect: /overview/routing


================================================
FILE: docs/content/2.overview/1.routing.md
================================================
---
title: Routing
description: Routing within your Nuxt Ionic application will feel very similar, but with a couple of differences.
navigation.icon: uil:sign-alt
---

By default, this module sets up the Ionic Router, or `IonRouter`. This router is built on top of `vue-router`, but with some changed functionality specifically for making it work better for mobile applications.

::list{type="success"}
- Enables [non-linear routing](https://ionicframework.com/docs/vue/navigation#non-linear-routing), e.g. for application tabs
- Separate navigation stacks for each tab of your application
- Rich page transitions out-of-the-box, appropriate for mobile
- Simple API for custom animations and direction control when navigating via links 
::

## Pages

Just like regular Nuxt, you can start building your Ionic application by creating routes within the `~/pages` directory. Every Vue file inside this directory will generate a route that displays the contents of the file. Read more about [Nuxt file-based routing](https://nuxt.com/docs/getting-started/routing) here.

::list{type="warning"}
- The root component of every page must always be the `<ion-page>` component.
::

This is required by Ionic to set up page transitions and stack navigation. Each view that is navigated to using the router must include an `<ion-page>` component.

```vue [~/pages/home.vue]
<template>
  <ion-page>
    <ion-header>
      <ion-toolbar>
        <ion-title>Home</ion-title>
      </ion-toolbar>
    </ion-header>
    <ion-content class="ion-padding">Hello World</ion-content>
  </ion-page>
</template>
```

::callout{color="info" icon="i-lucide-info"}
Read more about [`<ion-page>` here](https://ionicframework.com/docs/vue/navigation#ionpage).
::

### Defining page meta

Nuxt utilities like `definePageMeta` are fully functional. However, you should avoid using `<NuxtPage>` or `<NuxtLayout>` as these will not function correctly, due to Ionic requiring an `<ion-router-outlet>` instead.

::callout{color="info" icon="i-lucide-info"}
Read more about [`definePageMeta` here](https://nuxt.com/docs/api/utils/define-page-meta).
::

### Define a root alias if there's no index.vue page

Usually applications with tab bars will not have a `~/pages/index.vue` file, as it's not needed. However, this will mean opening the app will return a 404, e.g. on localhost:3000.

To solve this, if you do not have a `~/pages/index.vue` file in your project, you should add the following code to the page that you want to be displayed on your app's root page:

```ts [pages/tabs.vue]
definePageMeta({
  alias: ['/'],
})
```

It will now be used as the root path, and will be routed to by default when visiting e.g. localhost:3000.


## Navigation

Navigation can be done in several ways, using the `IonRouter` or certain `ion-` components.

::callout{color="info" icon="i-lucide-info"}
Read about navigation in [Ionic's Vue Navigation docs](https://ionicframework.com/docs/vue/navigation).
::

### IonRouter

To access the router, use the composable `useIonRouter()`. This should always be used instead of the regular `useRouter()` (which is auto-imported from `vue-router` by Nuxt). This ensures the benefits of the Ion Router are always provided.

This module auto-imports `useIonRouter()` so it should be available automatically. If this is disabled, you can import it from `@ionic/vue`.

It is best used when you wish to control navigation programmatically and have full control over the page transitions.

```vue
<script setup lang="ts">
import { customAnimation } from '~/animations/customAnimation';

const router = useIonRouter();

const goHome = () => router.push('/home');
const goBack = () => router.back();
const replaceRoute = () => router.replace({ name: 'newRoute' })
const customAnimatedNavigation = () => router.navigate('/page2', 'forward', 'replace', customAnimation);
</script>
```

::callout{color="info" icon="i-lucide-info"}
Read more about [`useIonRouter() here`](https://ionicframework.com/docs/vue/navigation#navigating-using-useionrouter).
::

### Navigating with Components

All Ionic components expose a `router-link` attribute. When set, the router will navigate to the specified route when the component is clicked. It accepts strings as well as named routes.

`router-direction` and `router-animation` are also available for further control.

It's best used when simple routing is required, without any programmatic logic before or after.

```vue
<template>
  <ion-button router-link="{ name: 'myPage' }">Click Me</ion-button>
  <ion-button 
    router-link="/page2" 
    router-direction="back" 
    :router-animation="myAnimation"
  >
    Click Me
  </ion-button>
</template>
```


::callout{color="info" icon="i-lucide-info"}
Read about [using the `router-link` attribute here](https://ionicframework.com/docs/vue/navigation#navigating-using-router-link).
::

::callout{color="warning" icon="i-lucide-alert-triangle"}
Avoid using `<NuxtLink>` for now as it currently is not integrated with the Ionic Router.
::

## Route Parameters

When accessing route parameters, `useRoute()` should continue to be used, just like regular Nuxt.

```vue [pages/posts/[id}.vue]
<script setup lang="ts">
const route = useRoute()

// When accessing /posts/1, route.params.id will be 1
console.log(route.params.id)
</script>
```

::callout{color="info" icon="i-lucide-info"}
Read more about [Nuxt route parmeters here](https://nuxt.com/docs/getting-started/routing#route-parameters). 
::

## Route Middleware

Nuxt's Route Middleware is also integrated and available, just like regular Nuxt.

::code-group

```ts [middleware/auth.ts]
export default defineNuxtRouteMiddleware((to, from) => {
  // isAuthenticated() is an example method verifying if a user is authenticated
  if (isAuthenticated() === false) {
    return showLoginModal(); // showLoginModal() is an example of opening a modal flow for authentication
  }
})
```

```html [pages/tabs/feed.vue]
<script setup lang="ts">
definePageMeta({
  middleware: 'auth'
})
</script>

<template>
  <h1>Welcome to your feed</h1>
</template>
```

::

::callout{color="info" icon="i-lucide-info"}
Read more about [Nuxt route middleware here](https://nuxt.com/docs/getting-started/routing#route-middleware).
::


================================================
FILE: docs/content/2.overview/2.theming.md
================================================
---
title: Theming
description: ""
navigation.icon: uil:palette
---

Ionic provides many css variables with which their components derive css styles. These variables can be overridden to customise the theme of your app. 

In its most simple form, you can create a `~/assets/css/ionic.css` file and add it to the `css` property in your `nuxt.config.ts` file. You can then add css variables that you'd like to override within this file, under the `:root` selector:

::code-group

```css [assets/css/ionic.css]
:root {
  --ion-color-primary: #57e389;
  --ion-color-primary-rgb: 87, 227, 137;
  --ion-color-primary-contrast: #000000;
  --ion-color-primary-contrast-rgb: 0, 0, 0;
  --ion-color-primary-shade: #4dc879;
  --ion-color-primary-tint: #68e695;

  /* And so on... */
}
```

```ts [nuxt.config.ts]
export default defineNuxtConfig({
  css: ['~/assets/css/ionic.css'],
})
```

::

To learn more about theming and which variables to override, check out Ionic's [official theming documentation](https://ionicframework.com/docs/theming/basics).


================================================
FILE: docs/content/2.overview/3.ionic-auto-imports.md
================================================
---
title: Ionic Auto-Imports
navigation.icon: uil:channel
description:
---

Ionic provides various components and helper functions for use in your application.

This module automatically auto-imports them throughout your app, so you do not need to import them manually.

::callout{color="info" icon="i-lucide-info"}
Read more about [how component auto-imports work](https://v3.nuxtjs.org/guide/directory-structure/components#components-directory).
::

## Ionic Components

All Ionic Vue components should be auto-imported throughout your app. Although your IDE should be aware of these components everywhere, they are not globally registered and are only imported within the components that use them.

For a list of all Ionic Vue components, please refer to the ionic component documentation: https://ionicframework.com/docs/components

::callout{color="info" icon="i-lucide-info"}
If you find a component that isn't being auto-imported, please [open an issue](https://github.com/nuxt-modules/ionic/issues/new/choose) or [a pull request](https://github.com/nuxt-modules/ionic/compare).
::

## Ionic Helper Functions

A number of Ionic hooks/composables are also imported by Nuxt via auto-imports within your app:

- [`getPlatforms`](https://ionicframework.com/docs/vue/platform#getplatforms)
- [`isPlatform`](https://ionicframework.com/docs/vue/platform#isplatform)
- [`menuController`](https://ionicframework.com/docs/api/menu)
- [`onIonViewWillEnter`](https://ionicframework.com/docs/vue/lifecycle#ionic-framework-lifecycle-methods)
- [`onIonViewDidEnter`](https://ionicframework.com/docs/vue/lifecycle#ionic-framework-lifecycle-methods)
- [`onIonViewWillLeave`](https://ionicframework.com/docs/vue/lifecycle#ionic-framework-lifecycle-methods)
- [`onIonViewDidLeave`](https://ionicframework.com/docs/vue/lifecycle#ionic-framework-lifecycle-methods)
- [`useBackButton`](https://ionicframework.com/docs/vue/utility-functions#hardware-back-button)
- [`useKeyboard`](https://ionicframework.com/docs/vue/utility-functions#keyboard)
- [`useIonRouter`](https://ionicframework.com/docs/vue/utility-functions#router)
- [`createAnimation`](https://ionicframework.com/docs/utilities/animations)
- [`createGesture`](https://ionicframework.com/docs/utilities/gestures)
- [`getTimeGivenProgression`](https://github.com/ionic-team/ionic-framework/blob/main/core/src/utils/animation/cubic-bezier.ts#L20)
- [`iosTransitionAnimation`](https://github.com/ionic-team/ionic-framework/blob/main/core/src/utils/transition/ios.transition.ts#L267)
- [`mdTransitionAnimation`](https://github.com/ionic-team/ionic-framework/blob/main/core/src/utils/transition/md.transition.ts#L6)

::callout{color="info" icon="i-lucide-info"}
Read more about these [helper functions in Ionic's docs](https://ionicframework.com/docs/).
::



================================================
FILE: docs/content/2.overview/4.module-utilities.md
================================================
---
title: Module Utilities
navigation.icon: uil:layer-group
description:
---

This modules aims to provide a few components and helpers for an easier and more seamless integration of Ionic's composables or functions in your app. We currently have the one `IonAnimation` component.

## Components

### `IonAnimation` component

This component makes using Ionic's `createAnimation` easier. It matches the majority of props directly to the usual Ionic animation object, to make adoption easier.

For more information, see [official Ionic docs](https://ionicframework.com/docs/utilities/animations) or check out the [playground examples](https://github.com/nuxt-modules/ionic/blob/main/playground/pages/tabs/tab4.vue)

You can see how it replaces usage of the regular [`createAnimation`](https://ionicframework.com/docs/utilities/animations#installation) function in the code example below:

::code-group

```vue [IonAnimation]
<template>
  <IonAnimation
    :duration="3000"
    :iterations="Infinity"
    :keyframes="[
      { offset: 0, background: 'red' },
      { offset: 0.72, background: 'var(--background)' },
      { offset: 1, background: 'green' },
    ]"
    playOnMount
  >
    <!-- Content to animate -->
  </IonAnimation>
</template>
```

```vue [Manual usage]
<script setup lang="ts">
// Template ref of your element
const squareRef = ref()

// Your animation object
const animation = createAnimation()
  .addElement(squareRef.value)
  .duration(3000)
  .iterations(Infinity)
  .keyframes([
    { offset: 0, background: 'red' },
    { offset: 0.72, background: 'var(--background)' },
    { offset: 1, background: 'green' },
  ])

onMounted(() => {
  animation.play()
})
</script>
```

::

::callout{color="info" icon="i-lucide-info"}
Currently component doesn't support grouped and chained animations. For that usage we still recommend using `createAnimation` by itself
::


================================================
FILE: docs/content/2.overview/5.icons.md
================================================
---
navigation.icon: uil:illustration
description: ""
---

Icons are auto-imported from [`ionicons/icons`](https://github.com/ionic-team/ionicons) by default, following the pattern of camel case naming with `ionicons` in front of the original icon name, that you can find on the [official ionicons website](https://ionic.io/ionicons).

::code-group

```vue [Auto-imported icons]
<template>
  <ion-icon :icon="ioniconsImage"></ion-icon>
  <ion-icon :md="ioniconsSquareSharp" :ios="ioniconsTriangleOutline"></ion-icon>
</template>
```

```vue [Manual imports]
<script setup lang="ts">
import { image, squareSharp, triangleOutline } from 'ionicons/icons'
</script>

<template>
  <ion-icon :icon="image"></ion-icon>
  <ion-icon :md="squareSharp" :ios="triangleOutline"></ion-icon>
</template>
```

::

You can opt-out of auto-importing icons by setting the `integrations.icons` module options in your `nuxt.config.ts` to `false`.

```js
export default defineNuxtConfig({
  ionic: {
    integrations: {
      icons: false,
    },
  },
})
```


================================================
FILE: docs/content/2.overview/6.deployment.md
================================================
---
navigation.icon: uil:rocket
---

## Web

Deployment on the web is the same as any Nuxt project. You can find more information for this in Nuxt's deployment documentation.

::callout{color="info" icon="i-lucide-info"}
Read more about [deploying to the web](https://nuxt.com/docs/getting-started/deployment).
::


## iOS and Android

For iOS and Android, it's a little more involved. We recommend looking at the deployment documentation from Ionic for these platforms: 

### iOS

https://ionicframework.com/docs/deployment/app-store

### Android

https://ionicframework.com/docs/deployment/play-store


================================================
FILE: docs/content/3.cookbook/.navigation.yml
================================================
icon: heroicons-outline:bookmark-alt
navigation.redirect: /cookbook/customising-app-vue


================================================
FILE: docs/content/3.cookbook/1.customising-app-vue.md
================================================
---
title: Customising app.vue
description: ""
---

This module provides a default `app.vue` file for when one is not otherwise provided by your app.

If you need to customize this `app.vue` file, you can create a new one in your project's source directory, based off the default:

```vue [app.vue]
<template>
  <ion-app>
    <ion-router-outlet />
  </ion-app>
</template>
```

This module will then stop providing one, so that your project's `app.vue` is used instead.

## Necessary ion tags

It's necessary that `<ion-app>` and `<ion-router-outlet>` are provided in your `app.vue` for Ionic to function.

`<ion-app>` is the container element of Ionic - there should be only one per project - and is required for certain behaviours to work. Please see the [Ionic ion-app documentation](https://ionicframework.com/docs/api/app) for more info.

Equally, `<ion-router-outlet>` provides a mounting point for the route component. In regular Nuxt applications,
this would be `<NuxtPage />`, but as Ionic Router is handling our routing we must use the `<ion-router-outlet>`.

::callout{color="warning" icon="i-lucide-alert-triangle"}
Remember that app.vue acts as the main component of your Nuxt application. Anything you add to it (JS and CSS) will be global and included in every page. Read more about app.vue [in the Nuxt app.vue docs](https://nuxt.com/docs/guide/directory-structure/app).
::


================================================
FILE: docs/content/3.cookbook/2.local-development.md
================================================
---
description: ""
---

::callout{color="info" icon="i-lucide-info"}
You may find the Ionic docs on developing [for iOS](https://ionicframework.com/docs/developing/ios) and [for Android](https://ionicframework.com/docs/developing/android) helpful before continuing.
::

When using Ionic just for the web, local development is as easy as running the `dev` script provided by Nuxt:

::code-group

```bash [yarn]
yarn dev -o
```

```bash [npm]
npm run dev -- -o
```

```bash [pnpm]
pnpm dev -o
```

::


But when working with iOS and Android, we're required to sync our built project to XCode and Android Studio, [using `npx cap sync`](https://capacitorjs.com/docs/cli/commands/sync). 

This manual process can be tedious, but capacitor provides a way to livereload in development mode. This allows you to test on a native device or a device simulator and maintain the hot module replacement or livereload functionality that you enjoy already with Nuxt on the web.

## Native device or simulator

First, ensure your Nuxt development build is created and the development server is running:

::code-group

```bash [yarn]
yarn dev -o
```

```bash [npm]
npm run dev -- -o
```

```bash [pnpm]
pnpm dev -o
```

::

Then, in a separate tab, [sync the build](https://ionicframework.com/docs/cli/commands/capacitor-sync) to ios or android, based on the device you wish to run it on (or both). This copies the web build and capacitor configuration file into the native platform project, then updates the native plugins referenced in `package.json`, and installs any discovered capacitor or cordova plugins.

::code-group

```bash [ios]
npx @ionic/cli capacitor sync ios --no-build
```

```bash [android]
npx @ionic/cli capacitor sync android --no-build
```

::

Then to deploy this to a native device or device simulator with livereload, you can [ask Capacitor to run the build](https://ionicframework.com/docs/cli/commands/capacitor-run). Ensure your native device is plugged in so that it displays in your list.

::code-group

```bash [ios]
npx @ionic/cli capacitor run ios --livereload-url=http://${LIP}:3000  --external --mode development
```

```bash [android]
npx @ionic/cli capacitor run android --livereload-url=http://${LIP}:3000  --external --mode development
```

::

The app will then open on the chosen native device or device simulator. 

## Automating on-device dev

We recommend putting this into a script in `scripts/` that you can run more easily via `yarn run` or `pnpm run`. For example:

::code-group

```bash [scripts/android.sh]
#!/bin/bash
LIP=$(ipconfig getifaddr en0)

echo "🍦 Starting local development to android device - ensure local dev server is running already"
echo "🏗️ Type checking and building for development..."
pnpm run build:dev
echo "🔃 Capacitor installation, podfile installation, sync and copy to app distribution folders..."
npx @ionic/cli capacitor sync android --no-build
echo "🏃 Select an Android device to run the build at local ip address ${LIP} on..."
eval "npx @ionic/cli capacitor run android --livereload-url=http://${LIP}:3000  --external --mode development"
```

```bash [scripts/ios.sh]
#!/bin/bash
LIP=$(ipconfig getifaddr en0)

echo "🍦 Starting local development to ios device - ensure local dev server is running already"
echo "🏗️ Type checking and building for development..."
pnpm run build:dev
echo "🔃 Capacitor installation, podfile installation, sync and copy to app distribution folders..."
npx @ionic/cli capacitor sync ios --no-build
echo "🏃 Select an iOS device to run the build at local ip address ${LIP} on..."
eval "npx @ionic/cli capacitor run ios --livereload-url=http://${LIP}:3000  --external --mode development"
```

::

```json [package.json]
{
  "scripts": {
    "android": "bash ./scripts/android.sh",
    "ios": "bash ./scripts/ios.sh"
  }
}
```

::code-group

```bash [yarn]
yarn ios
yarn android
```

```bash [npm]
npm run ios
npm run android
```

```bash [pnpm]
pnpm run ios
pnpm run android
```

::


::callout{color="warning" icon="i-lucide-alert-triangle"}
If you're having trouble with this step, please check the Ionic guides for [deploying to iOS](https://ionicframework.com/docs/developing/ios) and [deploying to android](https://ionicframework.com/docs/developing/android) for more information.
::


================================================
FILE: docs/content/3.cookbook/3.app-tabs.md
================================================
---
title: App Tabs
description: ""
---

It's common for mobile apps to come with tabs at the bottom of the screen. These tabs act as separate routing stacks, so should remember where they were when a user navigates away and back to a tab.

Ionic provides several components to provide App Tabs out of the box, with a deep integration with the Ionic Router for them.

::callout{color="info" icon="i-lucide-info"}
Read more about [Ionic's `ion-tabs` here](https://ionicframework.com/docs/api/tabs).
::

## Setup an application with tabs

> See the example in [the playground](https://github.com/nuxt-modules/ionic/blob/main/playground) for a working demo of an app with tabs.

Tabs require a main tab component, and then child components to be rendered in the tab view.

Your file structure should look like this:

```text [pages/ directory]
pages/
--| tabs.vue
--| tabs/
----| tab1.vue
----| tab2.vue
----| tab3.vue
----| tab4.vue
```

These tabs should then have a similar code structure as shown below. Remember, all pages must start with an `ion-page` component.

::code-group

```vue [pages/tabs.vue]
<template>
  <ion-page>
    <ion-content>
      <ion-tabs>
        <ion-router-outlet />
        
        <ion-tab-bar slot="bottom">
          <ion-tab-button tab="tab1" href="/tabs/tab1">
            <ion-icon :icon="ioniconsHomeOutline" />
            <ion-label>Tab 1</ion-label>
          </ion-tab-button>

          <ion-tab-button tab="tab2" href="/tabs/tab2">
            <ion-icon :icon="ioniconsImagesOutline" />
            <ion-label>Photos</ion-label>
          </ion-tab-button>

          <ion-tab-button tab="tab3" href="/tabs/tab3">
            <ion-icon :icon="ioniconsBulbOutline" />
            <ion-label>Tab 3</ion-label>
          </ion-tab-button>

          <ion-tab-button tab="tab4" href="/tabs/tab4">
            <ion-icon :icon="ioniconsAccessibilityOutline" />
            <ion-label>Animation examples</ion-label>
          </ion-tab-button>
        </ion-tab-bar>
      </ion-tabs>
    </ion-content>
  </ion-page>
</template>
```

```vue [pages/tabs/tab1.vue]
<template>
  <ion-page>
    <ion-header>
      <ion-toolbar>
        <ion-title>Tab 1</ion-title>
      </ion-toolbar>
    </ion-header>
    <ion-content>
      Tab 1 content
    </ion-content>
  </ion-page>
</template>
```

```vue [pages/tabs/tab2.vue]
<template>
  <ion-page>
    <ion-header>
      <ion-toolbar>
        <ion-title>Tab 2</ion-title>
      </ion-toolbar>
    </ion-header>
    <ion-content>
      Tab 2 content
    </ion-content>
  </ion-page>
</template>
```

::


All pages that should show inside these tabs must be **sibling routes** of these initial tab views, but with the name of the tab in its prefix.

The simplest way to manage this is for all pages being within this `pages/tabs/` directory, with a directory per tab, like so:

```text [pages/ directory]
pages/
--| tabs.vue
--| tabs/
----| tab1/
------| index.vue
------| a-page.vue
----| tab2/
------| index.vue
----| tab3/
------| index.vue
----| tab4.vue
------| index.vue
------| another-page.vue
```

If a particular tab only has one route component, you don't explicitly need the directory for it with the index.vue inside
of it, but we find it's a neater approach this way.

Using these directories avoids the routes becoming children routes of the tab by accident. The following folder structure is incorrect and would result in children routes, which IonRouter will not serve correctly:

::list{type="danger"}
- An example of incorrect routing (do not copy):
::

```text [pages/ directory]
pages/
--| tabs.vue
--| tabs/
----| tab1.vue
----| tab1/
------| a-page.vue
----| tab2.vue
----| tab3.vue
----| tab4.vue
----| tab4/
------| another-page.vue
```

::callout{color="success" icon="i-lucide-check-circle"}
Ionic provides an article on [best practices for using tabs in your application](https://ionicframework.com/docs/vue/navigation#working-with-tabs).
::

## Routing to pages that shouldn't be displayed in the tabs

If you'd like to surface a page on-top of the tabs, rather than inside one of them, you can include the page component outside
of the tabs directory.

```text [pages/ directory]
pages/
--| tabs.vue
--| tabs/
----| tab1/
------| index.vue
------| a-page.vue
----| tab2/
------| index.vue
--| settings.vue
```

::list{type="danger"}
- When navigating from a tabbed route to a non-tabbed route, `route.params` from the auto-imported `useRoute()` will always be an empty object. A current workaround is to `import { useRoute } from 'vue-router';`.
::

## Reusing views across tabs

Some apps may require showing the same component in different tabs. For instance, Spotify will allow you to view an album from both the Home and the Search tab.

To best accomplish this with Nuxt's file-based routing, create the routes using vue components in `pages/tabs`, and have them include the same component.

::code-group
```text [pages/ directory]
pages/
--| tabs.vue
--| tabs/
----| home/
------| index.vue
------| album-[id].vue
----| search/
------| index.vue
------| album.vue
----| library/
------| index.vue
```

```vue [pages/home/album-{id}.vue]
<template>
  <SingleAlbumView />
</template>
```

```vue [pages/search/album-{id}.vue]
<template>
  <SingleAlbumView />
</template>
```
::

::callout{color="info" icon="i-lucide-info"}
Read more about [reusing views across tabs in the Ionic docs](https://ionicframework.com/docs/vue/navigation#switching-between-tabs).
::


================================================
FILE: docs/content/3.cookbook/4.page-metadata.md
================================================
---
title: useHead / Page Meta
description: ""
---

::callout{color="warning" icon="i-lucide-alert-triangle"}
⚠️ This page is a stub and needs further information.
::

The composable `useHead()` will not work out of the box.

Please see this issue for reference: https://github.com/nuxt-modules/ionic/issues/6

Also see the documentation for use-head: https://nuxt.com/docs/api/composables/use-head


================================================
FILE: docs/content/3.cookbook/5.creating-ios-android-apps.md
================================================
---
title: iOS and Android Apps
---


::callout{color="warning" icon="i-lucide-alert-triangle"}
⚠️ This page is a stub and needs further information.
::

When building for iOS and Android, we recommend setting `ssr: false` in your nuxt config

```js [nuxt.config.ts]
export default defineNuxtConfig({
  modules: ['@nuxtjs/ionic'],
  ssr: false,
})
```

Also see:

- https://ionicframework.com/docs/developing/ios
- https://ionicframework.com/docs/developing/android
- https://ionicframework.com/docs/deployment/app-store
- https://ionicframework.com/docs/deployment/play-store

Please also see our page on (building for both Web and Device together)(/cookbook/web-and-device)


================================================
FILE: docs/content/3.cookbook/6.web-and-device.md
================================================
---
description: ""
---
Here we talk a little about some differences in deploying to native devices over the web, and what is required from the codebase to do so. We then explore some potential solutions to solve these issues. 

## Building for native devices

One main caveat of building for native devices is that the final build needs to be able to run completely
clientside. Another is that deploying a new build requires submitting to the app stores and being manually reviewed and approved.

This makes building for devices more cumbersome than deploying to the web, and means the following must be followed when deploying:

::list{type=danger}
- **No serverside rendering in the codebase**  
  As the build must be able to run completely clientside, we cannot have serverside rendering in the codebase.

- **Generating new builds are not quickly deployed**  
  A common paradigm on the web is to use serverside generation to build an app from stable data, then push the bundle to the web. This is then re-generated based on CMS changes or other triggers to ensure a static site that can stay up-to-date.

  As deploying to the app stores is much slower than to the web, this approach likely will not work anymore.
::

This means, in the codebase that will be deployed to the devices, we recommend using `ssr: false`, and not using serverside rendering at all.

If serverside rendering is required on the web, the simplest solution is to create two Nuxt projects: one targeting the web with SSR/SSG, and one targeting devices.

## Further Solutions

Having two completely separate codebases to target web and device is a little cumbersome, so there are some potential solutions you could explore.

These solutions are outside of the scope of this module, but are provided as guidance on improving your own DX in these cases. We'd love to hear about if you implement these successfully.

### A single codebase

It may be possible to have a single codebase with both SSR and a fully static application in-tandem, with code-switching based on configuration. E.G. when building for the web, setting `ssr: true`, and when building for devices, setting `ssr: false`.

You may need further wrapping around other SSR-aware code and utilities, such as `useAsyncData()`.

### A monorepo

You would likely be able to reduce the burden of two separate Nuxt apps by utilising a monorepo. The majority of your shared code could exist within a core package, while having two Nuxt apps, one to target Web with `ssr: true` and one to target devices with `ssr: false`.

A possible directory structure for this may look like:

```
- apps
  - nuxt-web
    - ...
    - nuxt.config.ts
  - nuxt-device
    - ...
    - nuxt.config.ts
- packages
  - core
    - components
    - composables
    - ...
```




================================================
FILE: docs/content/3.cookbook/7.live-updates.md
================================================
---
title: Live Updates
description: ""
---

Live Updates, also known as Over-the-Air (OTA) or hot code updates, are a way to push updates to your Android or iOS app without going through the app store review process. This is particularly useful for bug fixes or minor updates that don't require a full app release. 

There are several providers that offer live update services, including:
- [Capawesome Cloud](https://cloud.capawesome.io/)
- [CodePush (standalone)](https://github.com/microsoft/code-push-server)

## Capawesome Cloud

This guide demonstrates how to implement live updates using Capawesome Cloud.

### Installation

To enable Live Updates with Capawesome Cloud, you need to install the `@capawesome/capacitor-live-update` plugin:

```bash
npm install --save @capawesome/capacitor-live-update
```

After that, you need to sync the changes with your native projects:

```bash
npx cap sync
```

### Configuration

Next, you need to configure the plugin to work with [Capawesome Cloud](https://cloud.capawesome.io/).

#### App ID

In order for your app to identify itself to Capawesome Cloud, you need to set the `appId` in your `capacitor.config` file. For this, you need to create an app on the [Capawesome Cloud Console](https://console.cloud.capawesome.io/) and get the App ID.

```json
{
  "plugins": {
    "LiveUpdate": {
      "appId": "00000000-0000-0000-0000-000000000000"
    }
  }
}
```

Replace `00000000-0000-0000-0000-000000000000` with your actual App ID from the Capawesome Cloud Console.

After configuring the App ID, sync your Capacitor project again:

```bash
npx cap sync
```

### Usage

The most basic usage of the Live Update plugin is to call the [`sync(...)`](https://capawesome.io/plugins/live-update/#sync) method when the app starts. This method checks for updates, downloads them if available, and sets them as the next bundle to be applied. You can then call the [`reload()`](https://capawesome.io/plugins/live-update/#reload) method to apply the update immediately. If the [`reload()`](https://capawesome.io/plugins/live-update/#reload) method is not called, the new bundle will be used on the next app start.

```js
import { LiveUpdate } from "@capawesome/capacitor-live-update"

const sync = async () => {
  const result = await LiveUpdate.sync()
  if (result.nextBundleId) {
    await LiveUpdate.reload()
  }
}
```

### Publishing updates

To publish your first update, you need to [create a bundle](https://capawesome.io/cloud/live-updates/bundles/#create-a-bundle) on Capawesome Cloud. For this, you need a bundle artifact. A bundle artifact is the build output of your web app. In Nuxt, this is the `dist` folder. You can create a bundle artifact by running the following command:

#### Building your app

Generate the production build of your Nuxt app:

```bash
npx nuxt generate
```

This creates a `dist` folder with the build output of your web app.

#### Publishing with Capawesome CLI

To install the Capawesome CLI,run the following command:

```bash
npm i -g @capawesome/cli
```

After installing the Capawesome CLI, you need to log in to your Capawesome Cloud account. Run the following command and follow the instructions:

```bash
npx capawesome login
```

Once you are logged in, you can create a bundle by running the following command:

```bash
npx capawesome apps:bundles:create --path dist
```

Congratulations! You have successfully published your first live update. You can now test it by running your app on a device or emulator. The app will check for updates and apply them if available.
Feel free to check out the [documentation](https://capawesome.io/plugins/live-update/) of the Live Update plugin to see what else you can do with it.

## Other providers

For other live update providers, consult their respective documentation for the specific upload process and tooling requirements.

::callout{color="info" icon="i-lucide-info"}
We'd welcome PRs to add examples for other providers.
::



================================================
FILE: docs/content/index.md
================================================
---
title: 'Get Started'
navigation: false
layout: page
---

::u-page-hero{orientation="horizontal"}
#title
Nuxt [Ionic]{style="color:var(--color-primary-500);"}

#description
Batteries-included [Ionic](https://ionicframework.com/) integration for Nuxt.

- Zero-config required
- Auto-import Ionic components, composables and icons
- Ionic Router integration
- Pre-render routes
- Mobile meta tags
- Works out of the box with [Capacitor](https://capacitorjs.com/) to build mobile apps

#links
  :::u-button
  ---
  size: xl
  to: /get-started/introduction
  trailing-icon: i-lucide-arrow-right
  ---
  Get started
  :::

  :::u-button
  ---
  color: neutral
  icon: simple-icons-github
  size: xl
  to: https://github.com/nuxt-modules/ionic
  variant: outline
  ---
  Star on GitHub
  :::

#default
```bash [Terminal]
npx nuxi@latest module add ionic
```

::



================================================
FILE: docs/nuxt.config.ts
================================================
export default defineNuxtConfig({
  extends: ['docus'],
  modules: ['@nuxtjs/plausible'],
  css: ['~/assets/css/main.css'],
  site: {
    name: 'Nuxt Ionic',
  },
  colorMode: {
    preference: 'dark',
  },
  routeRules: {
    '/overview': { redirect: '/overview/routing' },
    '/cookbook': { redirect: '/cookbook/customising-app-vue' },
  },
  plausible: {
    domain: 'ionic.nuxtjs.org',
    apiHost: 'https://v.roe.dev',
  },
})


================================================
FILE: docs/package.json
================================================
{
  "name": "nuxt-ionic-docs",
  "description": "Batteries-included Ionic integration for Nuxt.",
  "homepage": "https://github.com/nuxt-modules/ionic/",
  "scripts": {
    "dev": "nuxt dev",
    "build": "nuxt build",
    "generate": "nuxt generate",
    "preview": "nuxt preview"
  },
  "dependencies": {
    "@nuxt/content": "3.12.0",
    "@nuxt/ui": "4.6.0",
    "@nuxtjs/plausible": "3.0.2",
    "better-sqlite3": "12.8.0",
    "docus": "5.8.1",
    "nuxt": "4.3.1",
    "tailwindcss": "4.2.2"
  }
}


================================================
FILE: docs/tokens.config.ts
================================================
import { defineTheme } from 'pinceau'

export default defineTheme({
  color: {
    primary: {
      50: '#84c3ff',
      100: '#7ab9ff',
      200: '#70afff',
      300: '#66a5ff',
      400: '#5c9bff',
      500: '#5291ff',
      600: '#4887f5',
      700: '#3e7deb',
      800: '#3473e1',
      900: '#2a69d7',
    },
  },
})


================================================
FILE: docs/tsconfig.json
================================================
{
  "extends": "./.nuxt/tsconfig.json"
}


================================================
FILE: eslint.config.js
================================================
// @ts-check
import { createConfigForNuxt } from '@nuxt/eslint-config/flat'

export default createConfigForNuxt({
  features: {
    tooling: true,
    stylistic: true,
  },
  dirs: {
    src: [
      './playground',
      './docs',
    ],
  },
}).append(
  {
    files: ['test/**'],
    rules: {
      '@typescript-eslint/no-explicit-any': 'off',
    },
  },
  {
    files: ['docs/**'],
    rules: {
      'vue/multi-word-component-names': 'off',
    },
  },
  {
    files: ['playground/**'],
    rules: {
      'vue/no-deprecated-slot-attribute': 'off',
    },
  },
)


================================================
FILE: package.json
================================================
{
  "name": "@nuxtjs/ionic",
  "version": "1.0.2",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/nuxt-modules/ionic.git"
  },
  "description": "Ionic integration for Nuxt",
  "keywords": [
    "nuxt",
    "module",
    "nuxt-module",
    "ionic",
    "ionic-framework",
    "web-components",
    "native",
    "android",
    "ios"
  ],
  "author": {
    "name": "Daniel Roe",
    "email": "daniel@roe.dev",
    "url": "https://github.com/danielroe"
  },
  "type": "module",
  "exports": {
    ".": {
      "types": "./dist/types.d.mts",
      "import": "./dist/module.mjs"
    }
  },
  "main": "./dist/module.mjs",
  "typesVersions": {
    "*": {
      ".": [
        "./dist/types.d.mts"
      ]
    }
  },
  "files": [
    "dist"
  ],
  "scripts": {
    "build": "pnpm dev:prepare && nuxt-module-build build",
    "dev": "nuxt dev playground",
    "dev:build": "nuxt build playground",
    "dev:prepare": "pnpm nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground",
    "docs:dev": "pnpm --filter 'nuxt-ionic-docs' dev",
    "docs:build": "pnpm --filter 'nuxt-ionic-docs' generate",
    "lint": "eslint",
    "prepack": "pnpm build",
    "prepare": "simple-git-hooks",
    "prepublishOnly": "pnpm lint && pnpm test",
    "release": "bumpp && npm publish && git push --follow-tags",
    "test": "vitest run --coverage",
    "test:types": "tsc --noEmit"
  },
  "dependencies": {
    "@capacitor/cli": "^8.0.0",
    "@capacitor/core": "^8.0.0",
    "@ionic/cli": "^7.2.1",
    "@ionic/vue": "^8.7.3",
    "@ionic/vue-router": "^8.7.3",
    "@nuxt/kit": "^4.1.2",
    "@unhead/vue": "^2.0.14",
    "ionicons": "^8.0.13",
    "jiti": "^2.6.0",
    "pathe": "^2.0.3",
    "pkg-types": "^2.3.0",
    "std-env": "^4.0.0",
    "ufo": "^1.6.1",
    "unimport": "^6.0.0"
  },
  "devDependencies": {
    "@ionic/core": "8.8.2",
    "@nuxt/eslint-config": "1.15.2",
    "@nuxt/module-builder": "1.0.2",
    "@nuxt/schema": "4.3.1",
    "@nuxt/test-utils": "4.0.0",
    "@types/node": "24.12.0",
    "@vitest/coverage-v8": "4.1.5",
    "@vue/test-utils": "2.4.6",
    "bumpp": "11.0.1",
    "eslint": "10.2.1",
    "expect-type": "1.3.0",
    "happy-dom": "20.9.0",
    "husky": "9.1.7",
    "lint-staged": "16.4.0",
    "nuxt": "4.3.1",
    "playwright-core": "1.58.2",
    "simple-git-hooks": "2.13.1",
    "typescript": "6.0.2",
    "unbuild": "3.6.1",
    "unhead": "2.1.13",
    "vitest": "4.1.5",
    "vue": "3.5.30",
    "vue-tsc": "3.2.6"
  },
  "lint-staged": {
    "*.{md,js,ts,mjs,cjs,json,.*rc}": [
      "npx eslint --fix"
    ]
  },
  "simple-git-hooks": {
    "pre-commit": "npx lint-staged"
  },
  "resolutions": {
    "@nuxt/kit": "^4.1.2",
    "@nuxt/schema": "4.3.1",
    "@nuxtjs/ionic": "link:.",
    "consola": "^3.4.2",
    "nuxt-component-meta": ">=0.14.0"
  },
  "packageManager": "pnpm@10.33.0"
}


================================================
FILE: playground/assets/css/ionic.css
================================================
:root {
  --ion-color-primary: #6030ff;
  --ion-color-primary-rgb: 96, 48, 255;
  --ion-color-primary-contrast: #ffffff;
  --ion-color-primary-contrast-rgb: 255, 255, 255;
  --ion-color-primary-shade: #542ae0;
  --ion-color-primary-tint: #7045ff;
}


================================================
FILE: playground/capacitor.config.ts
================================================
import type { CapacitorConfig } from '@capacitor/cli'

const config: CapacitorConfig = {
  appId: 'io.ionic.starter',
  appName: 'nuxt-ionic-playground',
  webDir: 'dist',
}

export default config


================================================
FILE: playground/components/ExploreContainer.vue
================================================
<script setup lang="ts">
const props = defineProps({ name: String })
useHead({
  title: `Explore Container - ${props.name}`,
})
</script>

<template>
  <div id="container">
    <strong>{{ name }}</strong>
    <p>
      Explore <a
        target="_blank"
        rel="noopener noreferrer"
        href="https://ionicframework.com/docs/components"
      >UI Components</a>
    </p>
  </div>
</template>

<style scoped>
#container {
  text-align: center;
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
}

#container strong {
  font-size: 20px;
  line-height: 26px;
}

#container p {
  font-size: 16px;
  line-height: 22px;
  color: #8c8c8c;
  margin: 0;
}

#container a {
  text-decoration: none;
}
</style>


================================================
FILE: playground/composables/usePhotoGallery.ts
================================================
import { Capacitor } from '@capacitor/core'
import type { Photo } from '@capacitor/camera'
import { Camera, CameraSource, CameraResultType } from '@capacitor/camera'
import { Filesystem, Directory } from '@capacitor/filesystem'
import { Preferences } from '@capacitor/preferences'

export function usePhotoGallery() {
  const photos = ref<UserPhoto[]>([])
  const PHOTO_STORAGE = 'photos'

  const loadSaved = async () => {
    const photoList = await Preferences.get({ key: PHOTO_STORAGE })
    const photosInStorage = photoList.value ? JSON.parse(photoList.value) : []

    // If running on the web...
    if (!isPlatform('hybrid')) {
      for (const photo of photosInStorage) {
        const file = await Filesystem.readFile({
          path: photo.filepath,
          directory: Directory.Data,
        })
        // Web platform only: Load the photo as base64 data
        photo.webviewPath = `data:image/jpeg;base64,${file.data}`
      }
    }

    photos.value = photosInStorage
  }

  const convertBlobToBase64 = (blob: Blob) =>
    new Promise((resolve, reject) => {
      const reader = new FileReader()
      reader.onerror = reject
      reader.onload = () => {
        resolve(reader.result)
      }
      reader.readAsDataURL(blob)
    })

  const savePicture = async (photo: Photo, fileName: string): Promise<UserPhoto> => {
    let base64Data: string | Blob
    // "hybrid" will detect Cordova or Capacitor;
    if (isPlatform('hybrid')) {
      const file = await Filesystem.readFile({

        path: photo.path!,
      })
      base64Data = file.data
    }
    else {
      // Fetch the photo, read as a blob, then convert to base64 format

      const response = await fetch(photo.webPath!)
      const blob = await response.blob()
      base64Data = (await convertBlobToBase64(blob)) as string
    }
    const savedFile = await Filesystem.writeFile({
      path: fileName,
      data: base64Data,
      directory: Directory.Data,
    })

    if (isPlatform('hybrid')) {
      // Display the new image by rewriting the 'file://' path to HTTP
      // Details: https://ionicframework.com/docs/building/webview#file-protocol
      return {
        filepath: savedFile.uri,
        webviewPath: Capacitor.convertFileSrc(savedFile.uri),
      }
    }
    else {
      // Use webPath to display the new image instead of base64 since it's
      // already loaded into memory
      return {
        filepath: fileName,
        webviewPath: photo.webPath,
      }
    }
  }

  const takePhoto = async () => {
    const photo = await Camera.getPhoto({
      resultType: CameraResultType.Uri,
      source: CameraSource.Camera,
      quality: 100,
    })
    const fileName = new Date().getTime() + '.jpeg'
    const savedFileImage = await savePicture(photo, fileName)

    photos.value = [savedFileImage, ...photos.value]
  }

  const deletePhoto = async (photo: UserPhoto) => {
    // Remove this photo from the Photos reference data array
    photos.value = photos.value.filter(p => p.filepath !== photo.filepath)

    // delete photo file from filesystem
    const filename = photo.filepath.substr(photo.filepath.lastIndexOf('/') + 1)
    await Filesystem.deleteFile({
      path: filename,
      directory: Directory.Data,
    })
  }

  const cachePhotos = () => {
    Preferences.set({
      key: PHOTO_STORAGE,
      value: JSON.stringify(photos.value),
    })
  }

  onMounted(loadSaved)

  watch(photos, cachePhotos)

  return {
    photos,
    takePhoto,
    deletePhoto,
  }
}

export interface UserPhoto {
  filepath: string
  webviewPath?: string
}


================================================
FILE: playground/middleware/auth.global.ts
================================================
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export default defineNuxtRouteMiddleware((to) => {
  console.log('ran middleware')
})


================================================
FILE: playground/nuxt.config.ts
================================================
export default defineNuxtConfig({
  modules: ['@nuxtjs/ionic'],
  css: ['~/assets/css/ionic.css'],
  compatibilityDate: '2024-08-19',
  ionic: {
    // integrations: {
    //   icons: true,
    //   meta: true,
    //   pwa: true,
    //   router: true,
    // },
    // css: {
    //   core: true,
    //   basic: true,
    //   utilities: false,
    // },
  },
})


================================================
FILE: playground/package.json
================================================
{
  "private": true,
  "name": "nuxt-ionic-playground",
  "type": "module",
  "scripts": {
    "dev": "nuxi dev",
    "build": "nuxi generate",
    "ionic:build": "pnpm build"
  },
  "devDependencies": {
    "@capacitor/camera": "8.0.2",
    "@capacitor/filesystem": "8.1.2",
    "@capacitor/preferences": "8.0.1",
    "@nuxtjs/ionic": "latest",
    "nuxt": "4.3.1"
  }
}


================================================
FILE: playground/pages/overlap.vue
================================================
<script setup lang="ts">
useHead({
  title: 'Overlapping - no tabs',
})
const isExploreEnabled = ref(true)
</script>

<template>
  <ion-page>
    <ion-header>
      <ion-toolbar>
        <ion-buttons slot="start">
          <ion-back-button default-href="/tabs/tab3" />
        </ion-buttons>
        <ion-title>Overlapping - no tabs</ion-title>
      </ion-toolbar>
    </ion-header>
    <ion-content :fullscreen="true">
      <ExploreContainer
        v-if="isExploreEnabled"
        name="Overlap Page"
      />
      <p style="text-align: center;">
        <ion-button
          class="explorer-toggle-op"
          fill="solid"
          color="primary"
          strong
          @click="isExploreEnabled = !isExploreEnabled"
        >
          Toggle Overlap Explore Container
        </ion-button>
      </p>
    </ion-content>
  </ion-page>
</template>


================================================
FILE: playground/pages/tabs/tab1/index.vue
================================================
<script setup lang="ts">
definePageMeta({
  alias: ['/', '/tabs'],
})
useHead({
  title: 'Tab 1',
})
const isExploreEnabled = ref(true)
</script>

<template>
  <ion-page>
    <ion-header translucent>
      <ion-toolbar>
        <ion-thumbnail slot="start">
          <ion-img src="/icon.png" />
        </ion-thumbnail>
        <ion-title> Nuxt Ionic </ion-title>
      </ion-toolbar>
    </ion-header>
    <ion-content :fullscreen="true">
      <ExploreContainer
        v-if="isExploreEnabled"
        name="Tab 1"
      />
      <ion-header collapse="condense">
        <ion-toolbar>
          <ion-title size="large">
            Tab 1
          </ion-title>
        </ion-toolbar>
      </ion-header>

      <ion-list>
        <ion-item>
          <ion-checkbox
            slot="start"
            aria-label="Mark idea complete"
          />
          <ion-label>
            <h1>Create Idea</h1>
            <ion-note>Run Idea By Brandy</ion-note>
          </ion-label>
          <ion-badge
            slot="end"
            color="success"
          >
            5 Days
          </ion-badge>
        </ion-item>
      </ion-list>

      <p style="text-align: center;">
        <ion-button
          class="explorer-toggle-1"
          fill="solid"
          color="primary"
          strong
          @click="isExploreEnabled = !isExploreEnabled"
        >
          Toggle Explore Container - Tab 1
        </ion-button>
      </p>
    </ion-content>
  </ion-page>
</template>


================================================
FILE: playground/pages/tabs/tab2/index.vue
================================================
<script setup lang="ts">
import { actionSheetController } from '@ionic/vue'
import type { UserPhoto } from '~/composables/usePhotoGallery'

useHead({
  title: 'Tab 2 - Photos',
})

const { photos, takePhoto, deletePhoto } = usePhotoGallery()

const showActionSheet = async (photo: UserPhoto) => {
  const actionSheet = await actionSheetController.create({
    header: 'Photos',
    buttons: [
      {
        text: 'Delete',
        role: 'destructive',
        icon: ioniconsTrash,
        handler: () => {
          deletePhoto(photo)
        },
      },
      {
        text: 'Cancel',
        icon: ioniconsClose,
        role: 'cancel',
        handler: () => {
          // Nothing to do, action sheet is automatically closed
        },
      },
    ],
  })
  await actionSheet.present()
}
</script>

<template>
  <ion-page>
    <ion-header>
      <ion-toolbar>
        <ion-title>Photo Gallery</ion-title>
      </ion-toolbar>
    </ion-header>
    <ion-content :fullscreen="true">
      <ion-header collapse="condense">
        <ion-toolbar>
          <ion-title size="large">
            Photo Gallery
          </ion-title>
        </ion-toolbar>
      </ion-header>
      <ion-grid>
        <ion-row>
          <ion-col
            v-for="photo in photos"
            :key="photo.filepath"
            size="6"
          >
            <ion-img
              :src="photo.webviewPath"
              @click="showActionSheet(photo)"
            />
          </ion-col>
        </ion-row>
      </ion-grid>

      <ion-fab
        slot="fixed"
        vertical="bottom"
        horizontal="center"
      >
        <ion-fab-button @click="takePhoto()">
          <ion-icon :icon="ioniconsCamera" />
        </ion-fab-button>
      </ion-fab>
    </ion-content>
  </ion-page>
</template>


================================================
FILE: playground/pages/tabs/tab3/index.vue
================================================
<script setup lang="ts">
useHead({
  title: 'Tab 3',
})
const isExploreEnabled = ref(true)
</script>

<template>
  <ion-page>
    <ion-header>
      <ion-toolbar>
        <ion-title>Tab 3</ion-title>
      </ion-toolbar>
    </ion-header>
    <ion-content :fullscreen="true">
      <ion-header collapse="condense">
        <ion-toolbar>
          <ion-title size="large">
            Tab 3
          </ion-title>
        </ion-toolbar>
      </ion-header>
      <ion-button router-link="/tabs/tab3/page-two">
        Go to page two
      </ion-button>
      <ion-button router-link="/overlap">
        Go to overlapping page
      </ion-button>
      <ExploreContainer
        v-if="isExploreEnabled"
        name="Tab 3"
      />
      <p style="text-align: center;">
        <ion-button
          class="explorer-toggle-3"
          fill="solid"
          color="primary"
          strong
          @click="isExploreEnabled = !isExploreEnabled"
        >
          Toggle Explore Container - Tab 3
        </ion-button>
      </p>
    </ion-content>
  </ion-page>
</template>


================================================
FILE: playground/pages/tabs/tab3/page-two.vue
================================================
<script setup lang="ts">
useHead({
  title: 'Page Two - Tab 3',
})
const isExploreEnabled = ref(true)
</script>

<template>
  <ion-page>
    <ion-header>
      <ion-toolbar>
        <ion-buttons slot="start">
          <ion-back-button default-href="/tabs/tab3" />
        </ion-buttons>
        <ion-title>Tab 3 - Page 2</ion-title>
      </ion-toolbar>
    </ion-header>
    <ion-content>
      <ExploreContainer
        v-if="isExploreEnabled"
        name="Tab 3 - Page Two"
      />
      <p style="text-align: center;">
        <ion-button
          class="explorer-toggle-p2"
          fill="solid"
          color="primary"
          strong
          @click="isExploreEnabled = !isExploreEnabled"
        >
          Toggle Explore Container - Tab 3 / Page Two
        </ion-button>
      </p>
    </ion-content>
  </ion-page>
</template>


================================================
FILE: playground/pages/tabs/tab4/index.vue
================================================
<script setup lang="ts">
useHead({
  title: 'Tab 4',
})
</script>

<template>
  <ion-page>
    <ion-header>
      <ion-toolbar>
        <ion-title>Animation examples</ion-title>
      </ion-toolbar>
    </ion-header>
    <ion-content :fullscreen="true">
      <ion-header collapse="condense">
        <ion-toolbar>
          <ion-title size="large">
            Animation examples
          </ion-title>
        </ion-toolbar>
      </ion-header>

      <div class="animations-grid">
        <section>
          <IonLabel color="primary">
            <strong>Basic animation</strong>
          </IonLabel>
          <IonAnimation
            id="animation1"
            v-slot="{ animation }"
            :duration="2000"
            :from-to="[
              { property: 'opacity', fromValue: '1', toValue: '0.2' },
              { property: 'transform', fromValue: 'translateX(0px)', toValue: 'translateX(-50px)' },
            ]"
            fill="forwards"
          >
            <div class="red-square" />

            <div class="buttons">
              <IonButton @click="animation?.play()">
                Play
              </IonButton>
              <IonButton @click="animation?.pause()">
                Pause
              </IonButton>
              <IonButton @click="animation?.stop()">
                Stop
              </IonButton>
            </div>
          </IonAnimation>
        </section>

        <section>
          <IonLabel color="primary">
            <strong>Keyframes animation</strong>
          </IonLabel>
          <IonAnimation
            id="animation2"
            v-slot="{ animation }"
            :duration="3000"
            :keyframes="[
              { offset: 0, transform: 'scale(1) translate(0, 0)' },
              { offset: 0.4, transform: 'scale(1.05) translate(15px, 15px)' },
              { offset: 0.6, transform: 'scale(1.05) translate(-30px, 15px)' },
              { offset: 1, transform: 'scale(1) translate(0, 0)' },
            ]"
            fill="none"
          >
            <div class="blue-square" />

            <div class="buttons">
              <IonButton @click="animation?.play()">
                Play
              </IonButton>
              <IonButton @click="animation?.pause()">
                Pause
              </IonButton>
              <IonButton @click="animation?.stop()">
                Stop
              </IonButton>
            </div>
          </IonAnimation>
        </section>

        <section>
          <IonLabel color="primary">
            <strong>Animation that repeats forever</strong>
          </IonLabel>
          <IonAnimation
            id="animation3"
            v-slot="{ animation }"
            :duration="1000"
            :keyframes="[
              { offset: 0, transform: 'scale(1)' },
              { offset: 0.75, transform: 'scale(1.05)' },
              { offset: 1, transform: 'scale(1)' },
            ]"
            :iterations="Infinity"
          >
            <div class="green-square" />

            <div class="buttons">
              <IonButton @click="animation?.play()">
                Play
              </IonButton>
              <IonButton @click="animation?.pause()">
                Pause
              </IonButton>
              <IonButton @click="animation?.stop()">
                Stop
              </IonButton>
            </div>
          </IonAnimation>
        </section>

        <section>
          <IonLabel color="primary">
            <strong>Animation with style hooks</strong>
          </IonLabel>
          <IonAnimation
            id="animation4"
            v-slot="{ animation }"
            :duration="2000"
            :keyframes="[
              { offset: 0, transform: 'scale(1)' },
              { offset: 0.75, transform: 'scale(1.1)' },
              { offset: 1, transform: 'scale(1)' },
            ]"
            :before-styles="{
              opacity: 0.5,
            }"
            :after-clear-styles="['opacity']"
            :after-styles="{
              transform: 'scale(0.9)',
            }"
            :before-clear-styles="['transform']"
            fill="none"
          >
            <div class="red-square" />

            <div class="buttons">
              <IonButton @click="animation?.play()">
                Play
              </IonButton>
              <IonButton @click="animation?.pause()">
                Pause
              </IonButton>
              <IonButton @click="animation?.stop()">
                Stop
              </IonButton>
            </div>
          </IonAnimation>
        </section>

        <section>
          <IonLabel color="primary">
            <strong>Animation with specific easing</strong>
          </IonLabel>
          <IonAnimation
            id="animation5"
            v-slot="{ animation }"
            :duration="2000"
            :keyframes="[
              {
                offset: 0,
                transform: 'rotate(0)',
              },
              {
                offset: 0.5,
                transform: 'rotate(60deg)',
              },
              {
                offset: 1,
                transform: 'rotate(0)',
              },
            ]"
            easing="cubic-bezier(.7,.55,0,1.15)"
          >
            <div class="blue-square" />

            <div class="buttons">
              <IonButton @click="animation?.play()">
                Play
              </IonButton>
              <IonButton @click="animation?.pause()">
                Pause
              </IonButton>
              <IonButton @click="animation?.stop()">
                Stop
              </IonButton>
            </div>
          </IonAnimation>
        </section>

        <section>
          <IonLabel color="primary">
            <strong>Reversed animation direction</strong>
          </IonLabel>
          <IonAnimation
            id="animation6"
            v-slot="{ animation }"
            :duration="3000"
            :keyframes="[
              { offset: 0, transform: 'scale(1)' },
              { offset: 0.3, transform: 'scale(1.2)' },
              { offset: 0.6, transform: 'scale(1.05)' },
              { offset: 0.9, transform: 'scale(0.8)' },
              { offset: 1, transform: 'scale(1)' },
            ]"
            direction="reverse"
            easing="ease-in"
          >
            <div class="green-square" />

            <div class="buttons">
              <IonButton @click="animation?.play()">
                Play
              </IonButton>
              <IonButton @click="animation?.pause()">
                Pause
              </IonButton>
              <IonButton @click="animation?.stop()">
                Stop
              </IonButton>
            </div>
          </IonAnimation>
        </section>
      </div>
    </ion-content>
  </ion-page>
</template>

<style scoped>
.animations-grid {
  padding: 3em;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(460px, 1fr));
  grid-auto-flow: row;
  row-gap: 4em;
  align-items: center;
  justify-items: center;
  text-align: center;
}

.animations-grid > *:nth-child(2) {
  margin-left: auto;
  margin-right: auto;
}

.buttons {
  width: 100%;
  display: flex;
  justify-content: space-around;
  align-items: center;
}

.red-square {
  width: 250px;
  height: 250px;
  background-color: red;
  border-radius: 1em;
}

.blue-square {
  width: 250px;
  height: 250px;
  background-color: blue;
  border-radius: 1em;
}

.green-square {
  width: 250px;
  height: 250px;
  background-color: green;
  border-radius: 1em;
}
</style>


================================================
FILE: playground/pages/tabs.vue
================================================
<script setup lang="ts">
useHead({
  title: 'House Tabs',
})
</script>

<template>
  <ion-page>
    <ion-content>
      <ion-tabs>
        <ion-router-outlet />
        <ion-tab-bar slot="bottom">
          <ion-tab-button
            tab="tab1"
            href="/tabs/tab1"
          >
            <ion-icon :icon="ioniconsHomeOutline" />
            <ion-label>Tab 1</ion-label>
          </ion-tab-button>

          <ion-tab-button
            tab="tab2"
            href="/tabs/tab2"
          >
            <ion-icon :icon="ioniconsImagesOutline" />
            <ion-label>Photos</ion-label>
          </ion-tab-button>

          <ion-tab-button
            tab="tab3"
            href="/tabs/tab3"
          >
            <ion-icon :icon="ioniconsBulbOutline" />
            <ion-label>Tab 3</ion-label>
          </ion-tab-button>

          <ion-tab-button
            tab="tab4"
            href="/tabs/tab4"
          >
            <ion-icon :icon="ioniconsAccessibilityOutline" />
            <ion-label>Animation examples</ion-label>
          </ion-tab-button>
        </ion-tab-bar>
      </ion-tabs>
    </ion-content>
  </ion-page>
</template>


================================================
FILE: playground/tsconfig.json
================================================
{
  "extends": "./.nuxt/tsconfig.json",
}


================================================
FILE: pnpm-workspace.yaml
================================================
packages:
  - playground
  - docs

onlyBuiltDependencies:
  - '@parcel/watcher'
  - '@tailwindcss/oxide'
  - better-sqlite3
  - esbuild
  - sharp
  - simple-git-hooks
  - unrs-resolver
  - vue-demi



================================================
FILE: renovate.json
================================================
{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": [
    "github>danielroe/renovate"
  ]
}


================================================
FILE: src/imports.ts
================================================
/* Ionic Hooks and components */

// If you are about to add a hook or component to one of the array below, please do so
// in alphabetical order. This makes it easier to find and check if certain hooks & components are there

export const IonicHooks = [
  'createAnimation',
  'createGesture',
  'getIonPageElement',
  'getPlatforms',
  'getTimeGivenProgression',
  'iosTransitionAnimation',
  'isPlatform',
  'mdTransitionAnimation',
  'menuController',
  'modalController',
  'popoverController',
  'alertController',
  'actionSheetController',
  'loadingController',
  'pickerController',
  'toastController',
  'onIonViewDidEnter',
  'onIonViewDidLeave',
  'onIonViewWillEnter',
  'onIonViewWillLeave',
  'openURL',
  'useBackButton',
  'useIonRouter',
  'useKeyboard',
]

export const IonicBuiltInComponents = [
  'IonAccordion',
  'IonAccordionGroup',
  'IonActionSheet',
  'IonAlert',
  'IonApp',
  'IonAvatar',
  'IonBackButton',
  'IonBackdrop',
  'IonBadge',
  'IonBreadcrumb',
  'IonBreadcrumbs',
  'IonButton',
  'IonButtons',
  'IonCard',
  'IonCardContent',
  'IonCardHeader',
  'IonCardSubtitle',
  'IonCardTitle',
  'IonCheckbox',
  'IonChip',
  'IonCol',
  'IonContent',
  'IonDatetime',
  'IonDatetimeButton',
  'IonFab',
  'IonFabButton',
  'IonFabList',
  'IonFooter',
  'IonGrid',
  'IonHeader',
  'IonIcon',
  'IonImg',
  'IonInfiniteScroll',
  'IonInfiniteScrollContent',
  'IonInput',
  'IonInputOtp',
  'IonInputPasswordToggle',
  'IonItem',
  'IonItemDivider',
  'IonItemGroup',
  'IonItemOption',
  'IonItemOptions',
  'IonItemSliding',
  'IonLabel',
  'IonList',
  'IonListHeader',
  'IonLoading',
  'IonMenu',
  'IonMenuButton',
  'IonMenuToggle',
  'IonModal',
  'IonNav',
  'IonNavLink',
  'IonNote',
  'IonPage',
  'IonPicker',
  'IonPickerColumn',
  'IonPickerColumnOption',
  'IonPickerLegacy',
  'IonPopover',
  'IonProgressBar',
  'IonRadio',
  'IonRadioGroup',
  'IonRange',
  'IonRefresher',
  'IonRefresherContent',
  'IonReorder',
  'IonReorderGroup',
  'IonRippleEffect',
  'IonRouterOutlet',
  'IonRow',
  'IonSearchbar',
  'IonSegment',
  'IonSegmentButton',
  'IonSegmentContent',
  'IonSegmentView',
  'IonSelect',
  'IonSelectModal',
  'IonSelectOption',
  'IonSkeletonText',
  'IonSpinner',
  'IonSplitPane',
  'IonTab',
  'IonTabs',
  'IonTabBar',
  'IonTabButton',
  'IonText',
  'IonTextarea',
  'IonThumbnail',
  'IonTitle',
  'IonToast',
  'IonToggle',
  'IonToolbar',
]


================================================
FILE: src/module.ts
================================================
import { existsSync, promises as fsp } from 'node:fs'

import {
  defineNuxtModule,
  addComponent,
  addPlugin,
  addTemplate,
  addImportsSources,
} from '@nuxt/kit'
import { join, resolve } from 'pathe'
import { readPackageJSON } from 'pkg-types'
import { defineUnimportPreset } from 'unimport'

import type { AnimationBuilder, SpinnerTypes, PlatformConfig } from '@ionic/vue'
import { runtimeDir } from './utils'
import { IonicBuiltInComponents, IonicHooks } from './imports'

import { setupUtilityComponents } from './parts/components'
import { useCSSSetup } from './parts/css'
import { setupIcons } from './parts/icons'
import { setupMeta } from './parts/meta'
import { setupRouter } from './parts/router'
import { setupCapacitor } from './parts/capacitor'

export interface ModuleOptions {
  integrations?: {
    router?: boolean
    meta?: boolean
    icons?: boolean
  }
  css?: {
    core?: boolean
    basic?: boolean
    utilities?: boolean
  }
  config?: {
    actionSheetEnter?: AnimationBuilder
    actionSheetLeave?: AnimationBuilder
    alertEnter?: AnimationBuilder
    alertLeave?: AnimationBuilder
    animated?: boolean
    backButtonDefaultHref?: string
    backButtonIcon?: string
    backButtonText?: string
    innerHTMLTemplatesEnabled?: boolean
    hardwareBackButton?: boolean
    infiniteLoadingSpinner?: SpinnerTypes
    loadingEnter?: AnimationBuilder
    loadingLeave?: AnimationBuilder
    loadingSpinner?: SpinnerTypes
    menuIcon?: string
    menuType?: string
    modalEnter?: AnimationBuilder
    modalLeave?: AnimationBuilder
    mode?: 'ios' | 'md'
    navAnimation?: AnimationBuilder
    pickerEnter?: AnimationBuilder
    pickerLeave?: AnimationBuilder
    platform?: PlatformConfig
    popoverEnter?: AnimationBuilder
    popoverLeave?: AnimationBuilder
    refreshingIcon?: string
    refreshingSpinner?: SpinnerTypes
    sanitizerEnabled?: boolean
    spinner?: SpinnerTypes
    statusTap?: boolean
    swipeBackEnabled?: boolean
    tabButtonLayout?:
      | 'icon-top'
      | 'icon-start'
      | 'icon-end'
      | 'icon-bottom'
      | 'icon-hide'
      | 'label-hide'
    toastDuration?: number
    toastEnter?: AnimationBuilder
    toastLeave?: AnimationBuilder
    toggleOnOffLabels?: boolean
  }
}

export default defineNuxtModule<ModuleOptions>({
  meta: {
    name: '@nuxtjs/ionic',
    configKey: 'ionic',
    compatibility: {
      nuxt: '>=3.0.0-rc.12',
    },
  },
  defaults: {
    integrations: {
      meta: true,
      router: true,
      icons: true,
    },
    css: {
      core: true,
      basic: true,
      utilities: false,
    },
    config: {},
  },
  async setup(options, nuxt) {
    nuxt.options.build.transpile.push(runtimeDir)
    nuxt.options.build.transpile.push(/@ionic/, /@stencil/)

    // Inject options for the Ionic Vue plugin as a virtual module
    addTemplate({
      filename: 'ionic/vue-config.mjs',
      getContents: () => `export default ${JSON.stringify(options.config)}`,
    })

    // Create an Ionic config file if it doesn't exist yet
    const ionicConfigPath = join(nuxt.options.rootDir, 'ionic.config.json')
    if (!existsSync(ionicConfigPath)) {
      await fsp.writeFile(
        ionicConfigPath,
        JSON.stringify(
          {
            name: await readPackageJSON(nuxt.options.rootDir).then(
              ({ name }) => name || 'nuxt-ionic-project',
            ),
            integrations: {},
            type: 'vue',
          },
          null,
          2,
        ),
      )
    }

    // Set up Ionic Core
    addPlugin(resolve(runtimeDir, 'plugins/ionic'))

    // Add Nuxt Vue custom utility components
    setupUtilityComponents()

    // Ensure `@ionic/vue` types flow through
    nuxt.options.typescript.hoist ||= []
    nuxt.options.typescript.hoist.push('@ionic/vue')

    // add capacitor integration
    const { excludeNativeFolders, findCapacitorConfig, parseCapacitorConfig } = setupCapacitor()

    // add the `android` and `ios` folders to the TypeScript config exclude list if capacitor is enabled
    // this is to prevent TypeScript from trying to resolve the Capacitor native code
    const capacitorConfigPath = await findCapacitorConfig()
    if (capacitorConfigPath) {
      const { androidPath, iosPath } = await parseCapacitorConfig(capacitorConfigPath)
      excludeNativeFolders(androidPath, iosPath)
    }

    // Add auto-imported components
    IonicBuiltInComponents.map(name =>
      addComponent({
        name,
        export: name,
        filePath: '@ionic/vue',
      }),
    )

    // Add auto-imported composables
    addImportsSources([
      defineUnimportPreset({
        from: '@ionic/vue',
        imports: [...IonicHooks],
      }),
      defineUnimportPreset({
        from: resolve(runtimeDir, 'composables/head'),
        imports: ['useHead'],
        priority: 2,
      }),
    ])

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    if (nuxt.options.nitro.static || (nuxt.options as any)._generate /* TODO: remove in future */) {
      nuxt.hook('nitro:config', async (config) => {
        config.prerender ||= {}
        config.prerender.routes ||= []
        config.prerender.routes.push('/200.html')

        config.output ||= {}
        if (!config.output.publicDir) {
          const distDir = resolve(nuxt.options.rootDir, 'dist')
          const stats = await fsp.lstat(distDir).catch(() => null)
          if (!stats || !stats.isSymbolicLink()) {
            config.output.publicDir = distDir
            if (!existsSync(distDir)) {
              await fsp.mkdir(distDir, { recursive: true })
            }
          }
        }
      })

      // Ensure there is an index.html file present when doing static file generation
      let publicFolder: string
      nuxt.hook('nitro:init', (nitro) => {
        publicFolder = nitro.options.output.publicDir
      })

      nuxt.hook('close', async () => {
        const indexFile = join(publicFolder, 'index.html')
        const fallbackFile = join(publicFolder, '200.html')

        if (!existsSync(indexFile) && existsSync(fallbackFile)) {
          await fsp.copyFile(fallbackFile, indexFile)
        }
      })
    }

    const { setupBasic, setupCore, setupUtilities } = useCSSSetup()

    // Add Ionic Core CSS
    if (options.css?.core) {
      await setupCore()
    }

    if (options.css?.basic) {
      await setupBasic()
    }

    if (options.css?.utilities) {
      await setupUtilities()
    }

    // Add auto-imported icons
    if (options.integrations?.icons) {
      await setupIcons()
    }

    if (options.integrations?.meta) {
      await setupMeta()
    }

    // @ts-expect-error removed module option
    if (options.integrations?.pwa) {
      console.log('PWA integration is has been removed from @nuxtjs/ionic. It is recommended to install and configure @vite-pwa/nuxt instead following the instructions in https://vite-pwa-org.netlify.app/frameworks/nuxt.html.')
    }

    // Set up Ionic Router integration
    if (options.integrations?.router) {
      await setupRouter()
    }
  },
})


================================================
FILE: src/parts/capacitor.ts
================================================
import type { CapacitorConfig } from '@capacitor/cli'
import { findPath, useNuxt } from '@nuxt/kit'
import { join } from 'pathe'
import { pathToFileURL } from 'node:url'
import { isWindows } from 'std-env'
import { createJiti, type Jiti } from 'jiti'

export const setupCapacitor = () => {
  const nuxt = useNuxt()
  let jiti: Jiti

  /** Find the path to capacitor configuration file (if it exists) */
  const findCapacitorConfig = async () => {
    const path = await findPath(
      'capacitor.config',
      {
        extensions: ['ts', 'json'],
        virtual: false,
      },
      'file',
    )

    return path
  }

  const parseCapacitorConfig = async (path: string | null): Promise<{
    androidPath: string | null
    iosPath: string | null
  }> => {
    if (!path) {
      return {
        androidPath: null,
        iosPath: null,
      }
    }

    jiti ||= createJiti(import.meta.url)

    const capacitorConfig = await jiti.import<CapacitorConfig>(isWindows ? pathToFileURL(path).href : path)

    return {
      androidPath: capacitorConfig.android?.path || null,
      iosPath: capacitorConfig.ios?.path || null,
    }
  }

  /** Exclude native folder paths from type checking by excluding them in tsconfig */
  const excludeNativeFolders = (androidPath: string | null, iosPath: string | null) => {
    nuxt.hook('prepare:types', (ctx) => {
      const paths = [
        join('..', androidPath ?? 'android'),
        join('..', iosPath ?? 'ios'),
      ]

      for (const key of ['tsConfig', 'nodeTsConfig', 'sharedTsConfig'] as const) {
        if (ctx[key]) {
          ctx[key].exclude ||= []
          ctx[key].exclude.push(...paths)
        }
      }
    })

    nuxt.options.ignore.push(
      join(androidPath ?? 'android'),
      join(iosPath ?? 'ios'),
    )
  }

  return {
    excludeNativeFolders,
    findCapacitorConfig,
    parseCapacitorConfig,
  }
}


================================================
FILE: src/parts/components.ts
================================================
import { resolve } from 'node:path'
import { addComponent } from '@nuxt/kit'
import { runtimeDir } from '../utils'

export const setupUtilityComponents = () => {
  addComponent({
    name: 'IonAnimation',
    filePath: resolve(runtimeDir, 'components', 'IonAnimation.vue'),
  })
}


================================================
FILE: src/parts/css.ts
================================================
import { useNuxt } from '@nuxt/kit'

export const useCSSSetup = () => {
  const nuxt = useNuxt()

  const setupCore = () => {
    // Core CSS required for Ionic components to work properly
    nuxt.options.css.unshift('@ionic/vue/css/core.css')
  }

  const setupBasic = () => {
    // Basic CSS for apps built with Ionic
    nuxt.options.css.unshift(
      '@ionic/vue/css/normalize.css',
      '@ionic/vue/css/structure.css',
      '@ionic/vue/css/typography.css',
    )
  }

  const setupUtilities = () => {
    // Optional CSS utils that can be commented out
    nuxt.options.css.unshift(
      '@ionic/vue/css/padding.css',
      '@ionic/vue/css/float-elements.css',
      '@ionic/vue/css/text-alignment.css',
      '@ionic/vue/css/text-transformation.css',
      '@ionic/vue/css/flex-utils.css',
      '@ionic/vue/css/display.css',
    )
  }

  return { setupCore, setupBasic, setupUtilities }
}


================================================
FILE: src/parts/icons.ts
================================================
import { useNuxt, addImportsSources } from '@nuxt/kit'
import { defineUnimportPreset } from 'unimport'
import * as _icons from 'ionicons/icons'

const icons = _icons as typeof import('ionicons/icons')

const iconsPreset = defineUnimportPreset({
  from: 'ionicons/icons',
  imports: Object.keys(icons).map(name => ({
    name,
    as: 'ionicons' + name[0]!.toUpperCase() + name.slice(1),
  })),
})

export const setupIcons = () => {
  const nuxt = useNuxt()

  nuxt.options.build.transpile.push(/ionicons/)

  addImportsSources(iconsPreset)
}


================================================
FILE: src/parts/meta.ts
================================================
import { useNuxt } from '@nuxt/kit'

export const setupMeta = () => {
  const nuxt = useNuxt()

  const metaDefaults = [
    { name: 'color-scheme', content: 'light dark' },
    { name: 'format-detection', content: 'telephone: no' },
    { name: 'msapplication-tap-highlight', content: 'no' },
  ]

  nuxt.options.app.head.meta = nuxt.options.app.head.meta || []
  for (const meta of metaDefaults) {
    if (!nuxt.options.app.head.meta.some(i => i.name === meta.name)) {
      nuxt.options.app.head.meta.unshift(meta)
    }
  }
  const viewport = nuxt.options.app.head.meta.find(i => i.name === 'viewport')
  if (viewport?.content === 'width=device-width, initial-scale=1') {
    viewport.content
      = 'viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no'
  }
}


================================================
FILE: src/parts/router.ts
================================================
import { existsSync } from 'node:fs'
import { useNuxt, useLogger } from '@nuxt/kit'
import { join, resolve } from 'pathe'
import { runtimeDir } from '../utils'

export const setupRouter = () => {
  const nuxt = useNuxt()
  const logger = useLogger()

  const pagesDirs = nuxt.options._layers.map(layer =>
    resolve(layer.config?.srcDir || layer.cwd!, layer.config?.dir?.pages || 'pages'),
  )

  // Disable module (and use universal router) if pages dir do not exists or user has disabled it
  if (
    nuxt.options.pages === false
    || (nuxt.options.pages !== true && !pagesDirs.some(dir => existsSync(dir)))
  ) {
    logger.info('Disabling Ionic Router integration as pages dir does not exist.')
    return
  }

  const ROUTER_PLUGIN_RE = /nuxt(?:3|-nightly)?\/dist\/(?:app\/plugins|pages\/runtime)\/(?:plugins\/)?router/
  const PAGE_USAGE_PLUGIN_RE = /nuxt(?:3|-nightly)?\/dist\/(?:app\/plugins|pages\/runtime)\/(?:plugins\/)?check-if-page-unused/
  const ionicRouterPlugin = {
    src: resolve(runtimeDir, 'plugins/router'),
    mode: 'all',
  } as const

  nuxt.hook('modules:done', () => {
    nuxt.hook('app:resolve', (app) => {
      app.plugins = app.plugins.filter(p => !PAGE_USAGE_PLUGIN_RE.test(p.src))
      const routerPlugin = app.plugins.findIndex(p => ROUTER_PLUGIN_RE.test(p.src))
      if (routerPlugin !== -1) {
        app.plugins.splice(routerPlugin, 1, ionicRouterPlugin)
      }
      else {
        app.plugins.unshift(ionicRouterPlugin)
      }
    })
  })

  // Add default ionic root layout
  nuxt.hook('app:resolve', (app) => {
    if (
      !app.mainComponent
      || app.mainComponent.includes('@nuxt/ui-templates')
      || app.mainComponent.match(/nuxt3?\/dist/)
    ) {
      app.mainComponent = join(runtimeDir, 'app.vue')
    }
  })
}


================================================
FILE: src/runtime/app.vue
================================================
<template>
  <ion-app>
    <ion-router-outlet />
  </ion-app>
</template>

<script setup>
import { IonApp, IonRouterOutlet } from '@ionic/vue'
</script>


================================================
FILE: src/runtime/components/IonAnimation.vue
================================================
<script setup lang="ts">
import { onMounted, onBeforeUnmount, ref } from 'vue'

import type {} from '@ionic/core'
import { createAnimation } from '@ionic/vue'
import type {
  Animation,
  AnimationDirection,
  AnimationFill,
  AnimationKeyFrames,
} from '@ionic/vue'

interface AnimationFromObject {
  property: string
  fromValue: string
}

interface AnimationFromToObject {
  property: string
  fromValue: string
  toValue: string
}

type AnimationStyles = {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  [key: string]: any
}

interface AnimationOptions {
  id?: string
  duration?: number
  iterations?: number
  easing?: string
  fill?: AnimationFill
  direction?: AnimationDirection
  from?: AnimationFromObject | AnimationFromObject[] | null
  fromTo?: AnimationFromToObject | AnimationFromToObject[] | null
  keyframes?: AnimationKeyFrames | null
  playOnMount?: boolean
  playOnVisible?: boolean
  beforeStyles?: AnimationStyles | null
  beforeAddClass?: string | string[] | null
  beforeClearStyles?: string[] | null
  afterStyles?: AnimationStyles | null
  afterAddClass?: string | string[] | null
  afterClearStyles?: string[] | null
}

const props = withDefaults(defineProps<AnimationOptions>(), {
  id: '',
  duration: 1000,
  iterations: 1,
  easing: 'linear',
  fill: 'auto',
  direction: 'normal',
  from: null,
  fromTo: null,
  keyframes: null,
  playOnMount: false,
  playOnVisible: false,
  beforeStyles: null,
  beforeAddClass: null,
  beforeClearStyles: null,
  afterStyles: null,
  afterAddClass: null,
  afterClearStyles: null,
})

const element = ref<HTMLDivElement | null>(null)

const animation = ref<Animation | null>(null)

let observer: IntersectionObserver

onMounted(() => {
  animation.value = createAnimation(props.id)
    .addElement(element.value!)
    .duration(props.duration)
    .iterations(props.iterations)
    .easing(props.easing)
    .fill(props.fill)
    .direction(props.direction)
    // Animation Hooks
    .beforeStyles(props.beforeStyles ?? {})
    .beforeAddClass(props.beforeAddClass ?? [])
    .beforeClearStyles(props.beforeClearStyles ?? [])
    .afterStyles(props.afterStyles ?? {})
    .afterAddClass(props.afterAddClass ?? [])
    .afterClearStyles(props.afterClearStyles ?? [])

  const hasKeyframes = Array.isArray(props.keyframes) && props.keyframes.length > 0

  if (hasKeyframes) {
    animation.value.keyframes(props.keyframes!)
  }

  // From
  if (props.from !== null && !hasKeyframes) {
    if (Array.isArray(props.from)) {
      props.from.forEach(({ property, fromValue }) => {
        animation.value!.from(property, fromValue)
      })
    }
    else {
      animation.value.from(props.from.property, props.from.fromValue)
    }
  }

  // From-to
  if (props.fromTo !== null && !hasKeyframes) {
    if (Array.isArray(props.fromTo)) {
      props.fromTo.forEach(({ property, fromValue, toValue }) => {
        animation.value!.fromTo(property, fromValue, toValue)
      })
    }
    else {
      animation.value.fromTo(props.fromTo.property, props.fromTo.fromValue, props.fromTo.toValue)
    }
  }

  if (props.playOnVisible && !props.playOnMount) {
    observer = new IntersectionObserver(
      () => {
        // Play animation
        animation.value!.play()
        // Disconnect observer - making animation always trigger ONLY ONCE
        observer.disconnect()
      },
      {
        // Use viewport as root element
        root: null,
        rootMargin: '0px',
        threshold: 0.5,
      },
    )
    // Start observing for animation element
    observer.observe(element.value!)
  }
  else if (props.playOnMount) animation.value.play()
})
onBeforeUnmount(() => {
  // Destroy animation and disconnect observer when component is about to be unmounted if it is defined
  animation.value?.destroy()
  if (observer) observer.disconnect()
})
</script>

<template>
  <div ref="element">
    <slot :animation="animation" />
  </div>
</template>


================================================
FILE: src/runtime/composables/head.ts
================================================
import { onIonViewDidEnter, onIonViewDidLeave } from '@ionic/vue'
import type { ActiveHeadEntry, UseHeadInput, UseHeadOptions } from '@unhead/vue/types'
import type { useHead as _useHead } from '@unhead/vue'
import { getCurrentInstance, onBeforeUnmount } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { injectHead } from '#imports'

// This is used to store the active head for each path as long as the path's page is still in the DOM
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const headMap = new Map<string, Array<[UseHeadInput<any>, ActiveHeadEntry<UseHeadInput<any>>]>>()

let beforeHook: (() => void) | undefined
let afterHook: (() => void) | undefined
let currPath: string
let prevPath: string

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function useHead<T extends Record<string, any>>(obj: UseHeadInput<T>, _?: UseHeadOptions) {
  const instance = getCurrentInstance()
  const activeHead = injectHead()

  // vue-router composables require being called in setup
  const currentPath = (instance && useRoute().path) || ''

  let innerObj = obj
  const __returned: Omit<ActiveHeadEntry<UseHeadInput<T>>, '_poll'> = {
    dispose() {
      // Can just easily mutate the array instead of wasting little CPU to slice/spread it :P
      const headArr = [...headMap.get(currentPath)!]
      const headArrIndex = headArr.findIndex(headVal => headVal[0] === innerObj)
      if (headArrIndex === -1) return
      const headToDispose = headArr[headArrIndex]![1]
      headToDispose?.dispose()
      headArr.splice(headArrIndex, 1)
      headMap.set(currentPath, headArr)
    },
    patch(newObj) {
      // Can just easily mutate the array instead of wasting little CPU to slice/spread it :P
      const headArr = [...headMap.get(currentPath)!]
      const headArrIndex = headArr.findIndex(headVal => headVal[0] === innerObj)
      if (headArrIndex === -1) return
      const [, headToPatch] = headArr[headArrIndex]!
      innerObj = newObj
      headToPatch?.patch(innerObj)
      headArr.splice(headArrIndex, 1, [innerObj, headToPatch])
      headMap.set(currentPath, headArr)
    },
  }

  /* Initially assign the head to the respected slots in the map
     because Ionic components don't unmount the way we expect them to */
  if (!headMap.has(currentPath)) {
    const headObj = activeHead?.push(obj)
    headMap.set(currentPath, [[obj, headObj]])
  }
  else {
    const headObj = activeHead?.push(obj)
    const metaArr = headMap.get(currentPath) || []
    headMap.set(currentPath, [...metaArr, [obj, headObj]])
  }

  // Only use lifecycle hooks if called inside component setup
  if (instance) {
    const router = useRouter()
    const currentRoute = router!.currentRoute

    /* Clear any reference to the input Object and the bound head object before unmounting the component */
    onBeforeUnmount(__returned.dispose)

    if (!beforeHook) {
      beforeHook = router?.beforeEach(() => {
        prevPath = currentRoute.value.path
      })
    }
    if (!afterHook) {
      afterHook = router?.afterEach(() => {
        currPath = currentRoute.value.path
      })
    }

    let hasReallyLeft = false
    onIonViewDidLeave(() => {
      let headArr = headMap.get(prevPath)
      if (headArr) {
        headArr = headArr.map(([obj, head]) => {
          head?.dispose()
          return [obj, head]
        })
        headMap.set(prevPath, headArr)
      }
      hasReallyLeft = true
    })

    onIonViewDidEnter(() => {
      if (hasReallyLeft) {
        let headArr = headMap.get(currPath)
        if (headArr) {
          headArr = headArr.map(([obj, head]) => {
            head?.dispose()
            const newHead = activeHead?.push(obj)
            return [obj, newHead]
          })
          headMap.set(currPath, headArr)
        }
      }
    })
  }

  return __returned
}


================================================
FILE: src/runtime/plugins/ionic.ts
================================================
import { IonicVue } from '@ionic/vue'
import { defineNuxtPlugin } from '#imports'
import ionicVueConfig from '#build/ionic/vue-config.mjs'
import type {} from 'nuxt/app'

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.use(IonicVue, ionicVueConfig)
})


================================================
FILE: src/runtime/plugins/router.ts
================================================
import {
  createMemoryHistory,
  createRouter,
  createWebHashHistory,
  createWebHistory,
} from '@ionic/vue-router'

import { isReadonly, reactive, shallowReactive, shallowRef } from 'vue'
import type { Ref } from 'vue'
import type { RouteLocationNormalizedLoaded, Router } from 'vue-router'
import { createError } from 'h3'
import { isEqual, withoutBase } from 'ufo'

import type { PageMeta, Plugin, RouteMiddleware } from '#app'
import { getRouteRules } from '#app/composables/manifest'
import { defineNuxtPlugin, useRuntimeConfig } from '#app/nuxt'
import { clearError, showError, useError } from '#app/composables/error'
import { onNuxtReady } from '#app/composables/ready'
import { navigateTo } from '#app/composables/router'

// @ts-expect-error virtual file
import { globalMiddleware, namedMiddleware } from '#build/middleware'
import { appManifest as isAppManifestEnabled } from '#build/nuxt.config.mjs'
// @ts-expect-error virtual file
import routerOptions from '#build/router.options'
// @ts-expect-error virtual file
import _routes from '#build/routes'

const plugin: Plugin<{ router: Router }> = defineNuxtPlugin({
  name: 'nuxt-ionic:router',
  enforce: 'pre',
  async setup(nuxtApp) {
    let routerBase = useRuntimeConfig().app.baseURL
    if (routerOptions.hashMode && !routerBase.includes('#')) {
      // allow the user to provide a `#` in the middle: `/base/#/app`
      routerBase += '#'
    }

    const history
      = routerOptions.history?.(routerBase)
        ?? (import.meta.client
          ? routerOptions.hashMode
            ? createWebHashHistory(routerBase)
            : createWebHistory(routerBase)
          : createMemoryHistory(routerBase))

    const routes = routerOptions.routes?.(_routes) ?? _routes

    const router = createRouter({
      ...routerOptions,
      history,
      routes,
    })

    if (import.meta.client && 'scrollRestoration' in window.history) {
      window.history.scrollRestoration = 'auto'
    }
    nuxtApp.vueApp.use(router)

    const previousRoute = shallowRef(router.currentRoute.value)
    router.afterEach((_to, from) => {
      previousRoute.value = from
    })

    Object.defineProperty(nuxtApp.vueApp.config.globalProperties, 'previousRoute', {
      get: () => previousRoute.value,
    })

    const initialURL = import.meta.server
      ? nuxtApp.ssrContext!.url
      : createCurrentLocation(routerBase, window.location, nuxtApp.payload.path)

    // Allows suspending the route object until page navigation completes
    const _route = shallowRef(router.currentRoute.value)
    const syncCurrentRoute = () => {
      _route.value = router.currentRoute.value
    }
    nuxtApp.hook('page:finish', syncCurrentRoute)
    router.afterEach((to, from) => {
      // We won't trigger suspense if the component is reused between routes
      // so we need to update the route manually
      if (to.matched[0]?.components?.default === from.matched[0]?.components?.default) {
        syncCurrentRoute()
      }
    })

    // https://github.com/vuejs/router/blob/main/packages/router/src/router.ts#L1225-L1233
    const route = {} as RouteLocationNormalizedLoaded
    for (const key in _route.value) {
      Object.defineProperty(route, key, {
        get: () => _route.value[key as keyof RouteLocationNormalizedLoaded],
      })
    }

    nuxtApp._route = shallowReactive(route)

    nuxtApp._middleware = nuxtApp._middleware || {
      global: [],
      named: {},
    }

    const error = useError()
    if (import.meta.client || !nuxtApp.ssrContext?.islandContext) {
      router.afterEach(async (to, _from, failure) => {
        delete nuxtApp._processingMiddleware

        if (import.meta.client && !nuxtApp.isHydrating && error.value) {
          // Clear any existing errors
          await nuxtApp.runWithContext(clearError)
        }
        if (failure) {
          await nuxtApp.callHook('page:loading:end')
        }
        if (import.meta.server && failure?.type === 4 /* ErrorTypes.NAVIGATION_ABORTED */) {
          return
        }
        if (to.matched.length === 0) {
          await nuxtApp.runWithContext(() => showError(createError({
            statusCode: 404,
            fatal: false,
            statusMessage: `Page not found: ${to.fullPath}`,
            data: {
              path: to.fullPath,
            },
          })))
        }
        else if (import.meta.server && to.redirectedFrom && to.fullPath !== initialURL) {
          await nuxtApp.runWithContext(() => navigateTo(to.fullPath || '/'))
        }
      })
    }

    const resolvedInitialRoute = import.meta.client && initialURL !== router.currentRoute.value.fullPath
      ? router.resolve(initialURL)
      : router.currentRoute.value
    syncCurrentRoute()

    if (import.meta.server && nuxtApp.ssrContext?.islandContext) {
      // We're in an island context, and don't need to handle middleware or redirections
      return { provide: { router } }
    }

    const initialLayout = nuxtApp.payload.state._layout
    router.beforeEach(async (to, from) => {
      await nuxtApp.callHook('page:loading:start')
      to.meta = reactive(to.meta)
      if (nuxtApp.isHydrating && initialLayout && !isReadonly(to.meta.layout)) {
        to.meta.layout = initialLayout as Exclude<PageMeta['layout'], Ref | false>
      }
      nuxtApp._processingMiddleware = true

      if (import.meta.client || !nuxtApp.ssrContext?.islandContext) {
        type MiddlewareDef = string | RouteMiddleware
        const middlewareEntries = new Set<MiddlewareDef>([...globalMiddleware, ...nuxtApp._middleware.global])
        for (const component of to.matched) {
          const componentMiddleware = component.meta.middleware as MiddlewareDef | MiddlewareDef[]
          if (!componentMiddleware) {
            continue
          }
          for (const entry of toArray(componentMiddleware)) {
            middlewareEntries.add(entry)
          }
        }

        if (isAppManifestEnabled) {
          const routeRules = await nuxtApp.runWithContext(() => getRouteRules(to.path))

          if (routeRules.appMiddleware) {
            for (const key in routeRules.appMiddleware) {
              if (routeRules.appMiddleware[key]) {
                middlewareEntries.add(key)
              }
              else {
                middlewareEntries.delete(key)
              }
            }
          }
        }

        for (const entry of middlewareEntries) {
          const middleware
            = typeof entry === 'string'
              ? nuxtApp._middleware.named[entry]
              // eslint-disable-next-line @typescript-eslint/no-explicit-any
              || await namedMiddleware[entry]?.().then((r: any) => r.default || r)
              : entry

          if (!middleware) {
            if (import.meta.dev) {
              throw new Error(`Unknown route middleware: '${entry}'. Valid middleware: ${Object.keys(namedMiddleware).map(mw => `'${mw}'`).join(', ')}.`)
            }
            throw new Error(`Unknown route middleware: '${entry}'.`)
          }

          const result = await nuxtApp.runWithContext(() => middleware(to, from))
          if (import.meta.server || (!nuxtApp.payload.serverRendered && nuxtApp.isHydrating)) {
            if (result === false || result instanceof Error) {
              const error = result || createError({
                statusCode: 404,
                statusMessage: `Page Not Found: ${initialURL}`,
              })
              await nuxtApp.runWithContext(() => showError(error))
              return false
            }
          }

          if (result === true) {
            continue
          }
          if (result || result === false) {
            return result
          }
        }
      }
    })

    router.onError(async () => {
      delete nuxtApp._processingMiddleware
      await nuxtApp.callHook('page:loading:end')
    })

    nuxtApp.hooks.hookOnce('app:created', async () => {
      delete nuxtApp._processingMiddleware
    })

    onNuxtReady(async () => {
      try {
        if (import.meta.client) {
          // #4920, #4982
          if ('name' in resolvedInitialRoute) {
            resolvedInitialRoute.name = undefined
          }
          await router.replace({
            ...resolvedInitialRoute,
            force: true,
          })
        }
      }
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      catch (error: any) {
        // We'll catch middleware errors or deliberate exceptions here
        await nuxtApp.runWithContext(() => showError(error))
      }
    })

    return { provide: { router } }
  },
})

// https://github.com/vuejs/router/blob/4a0cc8b9c1e642cdf47cc007fa5bbebde70afc66/packages/router/src/history/html5.ts#L37
function createCurrentLocation(base: string, location: Location, renderedPath?: string): string {
  const { pathname, search, hash } = location
  // allows hash bases like #, /#, #/, #!, #!/, /#!/, or even /folder#end
  const hashPos = base.indexOf('#')
  if (hashPos > -1) {
    const slicePos = hash.includes(base.slice(hashPos)) ? base.slice(hashPos).length : 1
    let pathFromHash = hash.slice(slicePos)
    // prepend the starting slash to hash so the url starts with /#
    if (pathFromHash[0] !== '/') {
      pathFromHash = '/' + pathFromHash
    }
    return withoutBase(pathFromHash, '')
  }
  const displayedPath = withoutBase(pathname, base)
  const path = !renderedPath || isEqual(displayedPath, renderedPath, { trailingSlash: true }) ? displayedPath : renderedPath
  return path + (path.includes('?') ? '' : search) + hash
}

function toArray<T>(value: T | T[]): T[] {
  return Array.isArray(value) ? value : [value]
}

export default plugin


================================================
FILE: src/utils.ts
================================================
import { fileURLToPath } from 'node:url'

export const runtimeDir = fileURLToPath(new URL('./runtime', import.meta.url))


================================================
FILE: test/e2e/ion-head.spec.ts
================================================
import { fileURLToPath } from 'node:url'
import { setup, createPage, url } from '@nuxt/test-utils/e2e'
import { describe, it } from 'vitest'
import type { Page } from 'playwright-core'

function expectTitleToBe(page: Page, title: string) {
  return page.waitForFunction(title => (document.querySelector('title') as HTMLTitleElement)?.textContent?.trim() === title, title)
}

describe('Nuxt Ionic useHead', async () => {
  await setup({
    server: true,
    browser: true,
    rootDir: fileURLToPath(new URL('../../playground', import.meta.url)),
  })

  it('useHead should work with navigation', { timeout: 120_000 }, async () => {
    const page = await createPage()

    await page.goto(url('/'), { waitUntil: 'hydration' })
    await expectTitleToBe(page, 'Explore Container - Tab 1')

    await page.click('.explorer-toggle-1')
    await expectTitleToBe(page, 'Tab 1')

    await page.click('.explorer-toggle-1')
    await expectTitleToBe(page, 'Explore Container - Tab 1')

    // Navigate to /tabs/tab2
    await page.click('#tab-button-tab2')
    await expectTitleToBe(page, 'Tab 2 - Photos')

    // Navigate to /tabs/tab3
    await page.click('#tab-button-tab3')
    await expectTitleToBe(page, 'Explore Container - Tab 3')

    await page.click('.explorer-toggle-3')
    await expectTitleToBe(page, 'Tab 3')

    await page.click('.explorer-toggle-3')
    await expectTitleToBe(page, 'Explore Container - Tab 3')

    // Navigate to /tabs/tab4
    await page.click('#tab-button-tab4')
    await expectTitleToBe(page, 'Tab 4')

    // Navigate back to /tabs/tab3
    await page.goBack()
    await expectTitleToBe(page, 'Explore Container - Tab 3')

    // Navigate to tabs/tab3/page-two
    await page.click('[routerlink="/tabs/tab3/page-two"]')
    await expectTitleToBe(page, 'Explore Container - Tab 3 - Page Two')

    await page.click('.explorer-toggle-p2')
    await expectTitleToBe(page, 'Page Two - Tab 3')

    await page.click('.explorer-toggle-p2')
    await expectTitleToBe(page, 'Explore Container - Tab 3 - Page Two')

    // Navigate to tabs/tab3/overlap
    await page.goBack()
    await page.click('[routerlink="/overlap"]')
    await expectTitleToBe(page, 'Explore Container - Overlap Page')

    await page.click('.explorer-toggle-op')
    await expectTitleToBe(page, 'Overlapping - no tabs')

    await page.click('.explorer-toggle-op')
    await expectTitleToBe(page, 'Explore Container - Overlap Page')
    await page.goBack()

    // Navigate to tabs/tab1
    await page.click('#tab-button-tab1')
    await expectTitleToBe(page, 'Explore Container - Tab 1')

    await page.close()
  })
})


================================================
FILE: test/e2e/ssr.spec.ts
================================================
/* @vitest-environment node */
import { fileURLToPath } from 'node:url'
import { setup, $fetch, createPage, url } from '@nuxt/test-utils'
import { describe, expect, it } from 'vitest'

describe('nuxt ionic', async () => {
  await setup({
    server: true,
    browser: true,
    rootDir: fileURLToPath(new URL('../../playground', import.meta.url)),
  })

  it('renders web components', async () => {
    const html = await $fetch('/')
    expect(html).toContain(
      '<ion-app name="IonApp"><!--[--><ion-router-outlet></ion-router-outlet><!--]--></ion-app>',
    )
  })

  it('renders correct viewport tags', async () => {
    const html = await $fetch('/')
    expect(html).toContain(
      '<meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">',
    )
  })

  it('runs middleware on client-side', async () => {
    const logs: string[] = []
    const page = await createPage()
    page.on('console', (msg) => {
      logs.push(msg.text())
    })
    await page.goto(url('/tabs/tab1'))
    await page.waitForLoadState('networkidle')
    expect(logs).toContain('ran middleware')

    await page.close()
  })
})


================================================
FILE: test/unit/capacitor.spec.ts
================================================
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { useNuxt, findPath } from '@nuxt/kit'
import { setupCapacitor } from '../../src/parts/capacitor'
import { pathToFileURL } from 'node:url'
import { isWindows } from 'std-env'

// Mock @nuxt/kit
vi.mock('@nuxt/kit', () => ({
  findPath: vi.fn(),
  useNuxt: vi.fn(),
}))

// Mock jiti
const mockJitiImport = vi.fn()
vi.mock('jiti', () => ({
  createJiti: vi.fn(() => ({
    import: mockJitiImport,
  })),
}))

describe('useCapacitor', () => {
  const mockNuxt = {
    hook: vi.fn(),
    options: {
      ignore: [],
    },
  }

  beforeEach(() => {
    vi.clearAllMocks()
    vi.mocked(useNuxt).mockReturnValue(mockNuxt as any)
    mockNuxt.options.ignore = []
    mockJitiImport.mockClear()
  })

  describe('findCapacitorConfig', () => {
    it('should find capacitor.config.ts', async () => {
      const mockPath = '/project/capacitor.config.ts'
      vi.mocked(findPath).mockResolvedValue(mockPath)

      const { findCapacitorConfig } = setupCapacitor()
      const result = await findCapacitorConfig()

      expect(result).toBe(mockPath)
    })

    it('should return null when no config found', async () => {
      vi.mocked(findPath).mockResolvedValue(null)

      const { findCapacitorConfig } = setupCapacitor()
      const result = await findCapacitorConfig()

      expect(result).toBeNull()
    })
  })

  describe('parseCapacitorConfig', () => {
    it('should return null paths when no config path provided', async () => {
      const { parseCapacitorConfig } = setupCapacitor()
      const result = await parseCapacitorConfig(null)

      expect(result).toEqual({
        androidPath: null,
        iosPath: null,
      })
    })

    it('should parse capacitor config with custom paths', async () => {
      const configPath = './capacitor.config.ts'
      const mockConfig = {
        android: { path: 'custom-android' },
        ios: { path: 'custom-ios' },
      }

      mockJitiImport.mockResolvedValue(mockConfig)

      const { parseCapacitorConfig } = setupCapacitor()
      const result = await parseCapacitorConfig(configPath)

      const expectedPath = isWindows ? pathToFileURL(configPath).href : configPath
      expect(mockJitiImport).toHaveBeenCalledWith(expectedPath)
      expect(result).toEqual({
        androidPath: 'custom-android',
        iosPath: 'custom-ios',
      })
    })

    it('should handle config without android/ios paths', async () => {
      const configPath = './capacitor.config.ts'
      const mockConfig = {
        android: undefined,
        ios: undefined,
      }

      mockJitiImport.mockResolvedValue(mockConfig)

      const { parseCapacitorConfig } = setupCapacitor()
      const result = await parseCapacitorConfig(configPath)

      const expectedPath = isWindows ? pathToFileURL(configPath).href : configPath
      expect(mockJitiImport).toHaveBeenCalledWith(expectedPath)
      expect(result).toEqual({
        androidPath: null,
        iosPath: null,
      })
    })
  })

  describe('excludeNativeFolders', () => {
    it('should register prepare:types hook and add native folders to ignore', () => {
      const { excludeNativeFolders } = setupCapacitor()
      excludeNativeFolders('android', 'ios')

      expect(mockNuxt.hook).toHaveBeenCalledWith('prepare:types', expect.any(Function))
      expect(mockNuxt.options.ignore).toContain('android')
      expect(mockNuxt.options.ignore).toContain('ios')
    })

    it('should handle null paths with defaults', () => {
      const { excludeNativeFolders } = setupCapacitor()
      excludeNativeFolders(null, null)

      expect(mockNuxt.hook).toHaveBeenCalledWith('prepare:types', expect.any(Function))
      expect(mockNuxt.options.ignore).toContain('android')
      expect(mockNuxt.options.ignore).toContain('ios')
    })

    it('should modify typescript configs in prepare:types hook', () => {
      const { excludeNativeFolders } = setupCapacitor()
      excludeNativeFolders('custom-android', 'custom-ios')

      // Get the hook callback that was registered
      const hookCallback = mockNuxt.hook.mock.calls.find(call => call[0] === 'prepare:types')?.[1]
      expect(hookCallback).toBeDefined()

      // Mock typescript context
      const mockCtx = {
        tsConfig: { exclude: [] },
        nodeTsConfig: { exclude: [] },
        sharedTsConfig: { exclude: [] },
      }

      // Call the hook callback
      hookCallback(mockCtx)

      // Verify all configs were updated
      expect(mockCtx.tsConfig.exclude).toContain('../custom-android')
      expect(mockCtx.tsConfig.exclude).toContain('../custom-ios')
      expect(mockCtx.nodeTsConfig.exclude).toContain('../custom-android')
      expect(mockCtx.nodeTsConfig.exclude).toContain('../custom-ios')
      expect(mockCtx.sharedTsConfig.exclude).toContain('../custom-android')
      expect(mockCtx.sharedTsConfig.exclude).toContain('../custom-ios')
    })

    it('should initialize exclude arrays if not present in typescript configs', () => {
      const { excludeNativeFolders } = setupCapacitor()
      excludeNativeFolders('android', 'ios')

      const hookCallback = mockNuxt.hook.mock.calls.find(call => call[0] === 'prepare:types')?.[1]

      // Mock context without exclude arrays
      const mockCtx = {
        tsConfig: {} as any,
        nodeTsConfig: {} as any,
        sharedTsConfig: {} as any,
      }

      hookCallback(mockCtx)

      expect(mockCtx.tsConfig.exclude).toEqual(['../android', '../ios'])
      expect(mockCtx.nodeTsConfig.exclude).toEqual(['../android', '../ios'])
      expect(mockCtx.sharedTsConfig.exclude).toEqual(['../android', '../ios'])
    })

    it('should handle missing typescript configs gracefully', () => {
      const { excludeNativeFolders } = setupCapacitor()
      excludeNativeFolders('android', 'ios')

      const hookCallback = mockNuxt.hook.mock.calls.find(call => call[0] === 'prepare:types')?.[1]

      // Mock context with only some configs present
      const mockCtx = {
        tsConfig: { exclude: [] },
        // nodeTsConfig and sharedTsConfig are undefined
      }

      expect(() => hookCallback(mockCtx)).not.toThrow()
      expect(mockCtx.tsConfig.exclude).toContain('../android')
      expect(mockCtx.tsConfig.exclude).toContain('../ios')
    })
  })
})


================================================
FILE: test/unit/imports.spec.ts
================================================
import { expect, describe, it } from 'vitest'
import * as ionicVue from '@ionic/vue'
import { IonicBuiltInComponents, IonicHooks } from '../../src/imports'

const ExportedHelpers = Object.keys(ionicVue) as Array<keyof typeof ionicVue>
const RegisteredHelpers = [...IonicBuiltInComponents, ...IonicHooks]

const ExcludedHelpers: Array<keyof typeof ionicVue> = [
  'IonicSafeString',
  'IonicSlides',
  'IonicVue',
  'actionSheetController',
  'alertController',
  'loadingController',
  'modalController',
  'pickerController',
  'popoverController',
  'toastController',
]

describe('imports:ionic', () => {
  it('should not register anything that is not exported', () => {
    for (const helper of RegisteredHelpers) {
      expect(ExportedHelpers).toContain(helper)
    }
  })
  it('should register everything that is exported', () => {
    for (const helper of ExportedHelpers) {
      if (ExcludedHelpers.includes(helper)) continue
      expect(RegisteredHelpers).toContain(helper)
    }
  })
})


================================================
FILE: tsconfig.json
================================================
{
  "extends": "./.nuxt/tsconfig.json",
  "exclude": [
    "node_modules",
    "docs",
    "playground"
  ]
}


================================================
FILE: vitest.config.ts
================================================
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    coverage: {
      include: ['src/**/*.ts'],
      reporter: ['text', 'json', 'html'],
    },
  },
})
Download .txt
gitextract_uaky4g3w/

├── .editorconfig
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug-report.yml
│   │   ├── config.yml
│   │   ├── documentation.yml
│   │   ├── feature-suggestion.yml
│   │   └── help-wanted.yml
│   └── workflows/
│       ├── ci.yml
│       ├── provenance.yml
│       └── release.yml
├── .gitignore
├── CODEOWNERS
├── CODE_OF_CONDUCT.md
├── LICENCE
├── README.md
├── build.config.ts
├── docs/
│   ├── .gitignore
│   ├── app.config.ts
│   ├── assets/
│   │   └── css/
│   │       └── main.css
│   ├── components/
│   │   └── AppHeaderLogo.vue
│   ├── content/
│   │   ├── 1.get-started/
│   │   │   ├── .navigation.yml
│   │   │   ├── 1.introduction.md
│   │   │   ├── 2.installation.md
│   │   │   ├── 3.configuration.md
│   │   │   ├── 4.enabling-capacitor.md
│   │   │   └── 5.watch-outs.md
│   │   ├── 2.overview/
│   │   │   ├── .navigation.yml
│   │   │   ├── 1.routing.md
│   │   │   ├── 2.theming.md
│   │   │   ├── 3.ionic-auto-imports.md
│   │   │   ├── 4.module-utilities.md
│   │   │   ├── 5.icons.md
│   │   │   └── 6.deployment.md
│   │   ├── 3.cookbook/
│   │   │   ├── .navigation.yml
│   │   │   ├── 1.customising-app-vue.md
│   │   │   ├── 2.local-development.md
│   │   │   ├── 3.app-tabs.md
│   │   │   ├── 4.page-metadata.md
│   │   │   ├── 5.creating-ios-android-apps.md
│   │   │   ├── 6.web-and-device.md
│   │   │   └── 7.live-updates.md
│   │   └── index.md
│   ├── nuxt.config.ts
│   ├── package.json
│   ├── tokens.config.ts
│   └── tsconfig.json
├── eslint.config.js
├── package.json
├── playground/
│   ├── assets/
│   │   └── css/
│   │       └── ionic.css
│   ├── capacitor.config.ts
│   ├── components/
│   │   └── ExploreContainer.vue
│   ├── composables/
│   │   └── usePhotoGallery.ts
│   ├── middleware/
│   │   └── auth.global.ts
│   ├── nuxt.config.ts
│   ├── package.json
│   ├── pages/
│   │   ├── overlap.vue
│   │   ├── tabs/
│   │   │   ├── tab1/
│   │   │   │   └── index.vue
│   │   │   ├── tab2/
│   │   │   │   └── index.vue
│   │   │   ├── tab3/
│   │   │   │   ├── index.vue
│   │   │   │   └── page-two.vue
│   │   │   └── tab4/
│   │   │       └── index.vue
│   │   └── tabs.vue
│   └── tsconfig.json
├── pnpm-workspace.yaml
├── renovate.json
├── src/
│   ├── imports.ts
│   ├── module.ts
│   ├── parts/
│   │   ├── capacitor.ts
│   │   ├── components.ts
│   │   ├── css.ts
│   │   ├── icons.ts
│   │   ├── meta.ts
│   │   └── router.ts
│   ├── runtime/
│   │   ├── app.vue
│   │   ├── components/
│   │   │   └── IonAnimation.vue
│   │   ├── composables/
│   │   │   └── head.ts
│   │   └── plugins/
│   │       ├── ionic.ts
│   │       └── router.ts
│   └── utils.ts
├── test/
│   ├── e2e/
│   │   ├── ion-head.spec.ts
│   │   └── ssr.spec.ts
│   └── unit/
│       ├── capacitor.spec.ts
│       └── imports.spec.ts
├── tsconfig.json
└── vitest.config.ts
Download .txt
SYMBOL INDEX (9 symbols across 5 files)

FILE: playground/composables/usePhotoGallery.ts
  function usePhotoGallery (line 7) | function usePhotoGallery() {
  type UserPhoto (line 123) | interface UserPhoto {

FILE: src/module.ts
  type ModuleOptions (line 25) | interface ModuleOptions {
  method setup (line 103) | async setup(options, nuxt) {

FILE: src/runtime/composables/head.ts
  function useHead (line 18) | function useHead<T extends Record<string, any>>(obj: UseHeadInput<T>, _?...

FILE: src/runtime/plugins/router.ts
  method setup (line 32) | async setup(nuxtApp) {
  function createCurrentLocation (line 251) | function createCurrentLocation(base: string, location: Location, rendere...
  function toArray (line 269) | function toArray<T>(value: T | T[]): T[] {

FILE: test/e2e/ion-head.spec.ts
  function expectTitleToBe (line 6) | function expectTitleToBe(page: Page, title: string) {
Condensed preview — 85 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (144K chars).
[
  {
    "path": ".editorconfig",
    "chars": 188,
    "preview": "root = true\n\n[*]\nindent_size = 2\nindent_style = space\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ni"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 20,
    "preview": "github: [danielroe]\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.yml",
    "chars": 851,
    "preview": "name: \"\\U0001F41B Bug report\"\ndescription: Something's not working\nlabels: [\"bug\"]\nbody:\n  - type: textarea\n    validati"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "chars": 145,
    "preview": "contact_links:\n  - name: Nuxt Community Discord\n    url: https://discord.nuxtjs.org/\n    about: Consider asking question"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/documentation.yml",
    "chars": 695,
    "preview": "name: \"\\U0001F4DA Documentation\"\ndescription: How do I ... ?\nlabels: [\"documentation\"]\nbody:\n  - type: textarea\n    vali"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature-suggestion.yml",
    "chars": 837,
    "preview": "name: \"\\U0001F195 Feature suggestion\"\ndescription: Suggest an idea\nlabels: [\"enhancement\"]\nbody:\n  - type: textarea\n    "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/help-wanted.yml",
    "chars": 619,
    "preview": "name: \"\\U0001F198 Help\"\ndescription: I need help with ...\nlabels: [\"help\"]\nbody:\n  - type: textarea\n    validations:\n   "
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 2050,
    "preview": "name: ci\n\non:\n  pull_request:\n    branches:\n      - main\n      - renovate/*\n  push:\n    branches:\n      - main\n\njobs:\n  "
  },
  {
    "path": ".github/workflows/provenance.yml",
    "chars": 492,
    "preview": "name: ci\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n    branches:\n      - main\npermissions:\n  contents: rea"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 510,
    "preview": "name: Release\n\npermissions:\n  contents: write\n\non:\n  push:\n    tags:\n      - 'v*'\n  workflow_dispatch:\n\njobs:\n  release:"
  },
  {
    "path": ".gitignore",
    "chars": 513,
    "preview": "# Dependencies\nnode_modules\n\n# Capacitor projects\nplayground/android\nplayground/ios\n\n# Logs\n*.log*\n\n# Temp directories\n."
  },
  {
    "path": "CODEOWNERS",
    "chars": 13,
    "preview": "* @danielroe\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 3382,
    "preview": "# Contributor Covenant Code of Conduct\n\n## Our pledge\n\nIn the interest of fostering an open and welcoming environment, w"
  },
  {
    "path": "LICENCE",
    "chars": 1067,
    "preview": "MIT License\n\nCopyright (c) 2022 Daniel Roe\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
  },
  {
    "path": "README.md",
    "chars": 2158,
    "preview": "[![@nuxtjs/ionic](./docs/public/cover.jpg)](https://ionic.nuxtjs.org)\n\n# Nuxt Ionic\n\n[![npm version][npm-version-src]][n"
  },
  {
    "path": "build.config.ts",
    "chars": 135,
    "preview": "import { defineBuildConfig } from 'unbuild'\n\nexport default defineBuildConfig({\n  // TODO: fix in unbuild\n  externals: ["
  },
  {
    "path": "docs/.gitignore",
    "chars": 93,
    "preview": "node_modules\n*.iml\n.idea\n*.log*\n.nuxt\n.vscode\n.DS_Store\ncoverage\ndist\nsw.*\n.env\n.output\n.data"
  },
  {
    "path": "docs/app.config.ts",
    "chars": 890,
    "preview": "export default defineAppConfig({\n  ui: {\n    colors: {\n      primary: 'blue',\n      neutral: 'zinc',\n    },\n  },\n  docus"
  },
  {
    "path": "docs/assets/css/main.css",
    "chars": 620,
    "preview": "@import \"tailwindcss\";\n@import \"@nuxt/ui\";\n\n\n:root {\n--color-blue: oklch(66.85% 0.17582 260.7);\n--color-blue-50: oklch(1"
  },
  {
    "path": "docs/components/AppHeaderLogo.vue",
    "chars": 1583,
    "preview": "<template>\n  <div class=\"logo\">\n    <svg\n      width=\"32\"\n      height=\"32\"\n      viewBox=\"0 0 32 32\"\n      fill=\"none\"\n"
  },
  {
    "path": "docs/content/1.get-started/.navigation.yml",
    "chars": 76,
    "preview": "icon: heroicons-outline:star\nnavigation.redirect: /get-started/introduction\n"
  },
  {
    "path": "docs/content/1.get-started/1.introduction.md",
    "chars": 2375,
    "preview": "---\nnavigation.icon: uil:info-circle\ndescription: 'Batteries-included, zero-config needed, Ionic integration for Nuxt'\n-"
  },
  {
    "path": "docs/content/1.get-started/2.installation.md",
    "chars": 1822,
    "preview": "---\nnavigation.icon: uil:play-circle\ndescription: 'Get started quickly by installing and setting up this module with the"
  },
  {
    "path": "docs/content/1.get-started/3.configuration.md",
    "chars": 2678,
    "preview": "---\ntitle: Configuration\ndescription: >\n  This module provides configuration options for itself, as well as passing thro"
  },
  {
    "path": "docs/content/1.get-started/4.enabling-capacitor.md",
    "chars": 2402,
    "preview": "---\ntitle: Enabling Capacitor\ndescription: \"\"\nnavigation.icon: nonicons:capacitor-16\n---\n\n[Capacitor](https://capacitorj"
  },
  {
    "path": "docs/content/1.get-started/5.watch-outs.md",
    "chars": 2838,
    "preview": "---\ntitle: Watchouts\ndescription: \"\"\nnavigation.icon: uil:exclamation-triangle\n---\n\nThis page aims to succinctly mention"
  },
  {
    "path": "docs/content/2.overview/.navigation.yml",
    "chars": 72,
    "preview": "icon: heroicons-outline:sparkles\nnavigation.redirect: /overview/routing\n"
  },
  {
    "path": "docs/content/2.overview/1.routing.md",
    "chars": 6242,
    "preview": "---\ntitle: Routing\ndescription: Routing within your Nuxt Ionic application will feel very similar, but with a couple of "
  },
  {
    "path": "docs/content/2.overview/2.theming.md",
    "chars": 1044,
    "preview": "---\ntitle: Theming\ndescription: \"\"\nnavigation.icon: uil:palette\n---\n\nIonic provides many css variables with which their "
  },
  {
    "path": "docs/content/2.overview/3.ionic-auto-imports.md",
    "chars": 2799,
    "preview": "---\ntitle: Ionic Auto-Imports\nnavigation.icon: uil:channel\ndescription:\n---\n\nIonic provides various components and helpe"
  },
  {
    "path": "docs/content/2.overview/4.module-utilities.md",
    "chars": 1886,
    "preview": "---\ntitle: Module Utilities\nnavigation.icon: uil:layer-group\ndescription:\n---\n\nThis modules aims to provide a few compon"
  },
  {
    "path": "docs/content/2.overview/5.icons.md",
    "chars": 1037,
    "preview": "---\nnavigation.icon: uil:illustration\ndescription: \"\"\n---\n\nIcons are auto-imported from [`ionicons/icons`](https://githu"
  },
  {
    "path": "docs/content/2.overview/6.deployment.md",
    "chars": 603,
    "preview": "---\nnavigation.icon: uil:rocket\n---\n\n## Web\n\nDeployment on the web is the same as any Nuxt project. You can find more in"
  },
  {
    "path": "docs/content/3.cookbook/.navigation.yml",
    "chars": 88,
    "preview": "icon: heroicons-outline:bookmark-alt\nnavigation.redirect: /cookbook/customising-app-vue\n"
  },
  {
    "path": "docs/content/3.cookbook/1.customising-app-vue.md",
    "chars": 1390,
    "preview": "---\ntitle: Customising app.vue\ndescription: \"\"\n---\n\nThis module provides a default `app.vue` file for when one is not ot"
  },
  {
    "path": "docs/content/3.cookbook/2.local-development.md",
    "chars": 4277,
    "preview": "---\ndescription: \"\"\n---\n\n::callout{color=\"info\" icon=\"i-lucide-info\"}\nYou may find the Ionic docs on developing [for iOS"
  },
  {
    "path": "docs/content/3.cookbook/3.app-tabs.md",
    "chars": 5473,
    "preview": "---\ntitle: App Tabs\ndescription: \"\"\n---\n\nIt's common for mobile apps to come with tabs at the bottom of the screen. Thes"
  },
  {
    "path": "docs/content/3.cookbook/4.page-metadata.md",
    "chars": 399,
    "preview": "---\ntitle: useHead / Page Meta\ndescription: \"\"\n---\n\n::callout{color=\"warning\" icon=\"i-lucide-alert-triangle\"}\n⚠️ This pa"
  },
  {
    "path": "docs/content/3.cookbook/5.creating-ios-android-apps.md",
    "chars": 676,
    "preview": "---\ntitle: iOS and Android Apps\n---\n\n\n::callout{color=\"warning\" icon=\"i-lucide-alert-triangle\"}\n⚠️ This page is a stub a"
  },
  {
    "path": "docs/content/3.cookbook/6.web-and-device.md",
    "chars": 2787,
    "preview": "---\ndescription: \"\"\n---\nHere we talk a little about some differences in deploying to native devices over the web, and wh"
  },
  {
    "path": "docs/content/3.cookbook/7.live-updates.md",
    "chars": 3964,
    "preview": "---\ntitle: Live Updates\ndescription: \"\"\n---\n\nLive Updates, also known as Over-the-Air (OTA) or hot code updates, are a w"
  },
  {
    "path": "docs/content/index.md",
    "chars": 860,
    "preview": "---\ntitle: 'Get Started'\nnavigation: false\nlayout: page\n---\n\n::u-page-hero{orientation=\"horizontal\"}\n#title\nNuxt [Ionic]"
  },
  {
    "path": "docs/nuxt.config.ts",
    "chars": 433,
    "preview": "export default defineNuxtConfig({\n  extends: ['docus'],\n  modules: ['@nuxtjs/plausible'],\n  css: ['~/assets/css/main.css"
  },
  {
    "path": "docs/package.json",
    "chars": 505,
    "preview": "{\n  \"name\": \"nuxt-ionic-docs\",\n  \"description\": \"Batteries-included Ionic integration for Nuxt.\",\n  \"homepage\": \"https:/"
  },
  {
    "path": "docs/tokens.config.ts",
    "chars": 328,
    "preview": "import { defineTheme } from 'pinceau'\n\nexport default defineTheme({\n  color: {\n    primary: {\n      50: '#84c3ff',\n     "
  },
  {
    "path": "docs/tsconfig.json",
    "chars": 41,
    "preview": "{\n  \"extends\": \"./.nuxt/tsconfig.json\"\n}\n"
  },
  {
    "path": "eslint.config.js",
    "chars": 569,
    "preview": "// @ts-check\nimport { createConfigForNuxt } from '@nuxt/eslint-config/flat'\n\nexport default createConfigForNuxt({\n  feat"
  },
  {
    "path": "package.json",
    "chars": 2891,
    "preview": "{\n  \"name\": \"@nuxtjs/ionic\",\n  \"version\": \"1.0.2\",\n  \"license\": \"MIT\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \""
  },
  {
    "path": "playground/assets/css/ionic.css",
    "chars": 249,
    "preview": ":root {\n  --ion-color-primary: #6030ff;\n  --ion-color-primary-rgb: 96, 48, 255;\n  --ion-color-primary-contrast: #ffffff;"
  },
  {
    "path": "playground/capacitor.config.ts",
    "chars": 197,
    "preview": "import type { CapacitorConfig } from '@capacitor/cli'\n\nconst config: CapacitorConfig = {\n  appId: 'io.ionic.starter',\n  "
  },
  {
    "path": "playground/components/ExploreContainer.vue",
    "chars": 746,
    "preview": "<script setup lang=\"ts\">\nconst props = defineProps({ name: String })\nuseHead({\n  title: `Explore Container - ${props.nam"
  },
  {
    "path": "playground/composables/usePhotoGallery.ts",
    "chars": 3572,
    "preview": "import { Capacitor } from '@capacitor/core'\nimport type { Photo } from '@capacitor/camera'\nimport { Camera, CameraSource"
  },
  {
    "path": "playground/middleware/auth.global.ts",
    "chars": 148,
    "preview": "// eslint-disable-next-line @typescript-eslint/no-unused-vars\nexport default defineNuxtRouteMiddleware((to) => {\n  conso"
  },
  {
    "path": "playground/nuxt.config.ts",
    "chars": 366,
    "preview": "export default defineNuxtConfig({\n  modules: ['@nuxtjs/ionic'],\n  css: ['~/assets/css/ionic.css'],\n  compatibilityDate: "
  },
  {
    "path": "playground/package.json",
    "chars": 372,
    "preview": "{\n  \"private\": true,\n  \"name\": \"nuxt-ionic-playground\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"nuxi dev\",\n    \"b"
  },
  {
    "path": "playground/pages/overlap.vue",
    "chars": 863,
    "preview": "<script setup lang=\"ts\">\nuseHead({\n  title: 'Overlapping - no tabs',\n})\nconst isExploreEnabled = ref(true)\n</script>\n\n<t"
  },
  {
    "path": "playground/pages/tabs/tab1/index.vue",
    "chars": 1491,
    "preview": "<script setup lang=\"ts\">\ndefinePageMeta({\n  alias: ['/', '/tabs'],\n})\nuseHead({\n  title: 'Tab 1',\n})\nconst isExploreEnab"
  },
  {
    "path": "playground/pages/tabs/tab2/index.vue",
    "chars": 1792,
    "preview": "<script setup lang=\"ts\">\nimport { actionSheetController } from '@ionic/vue'\nimport type { UserPhoto } from '~/composable"
  },
  {
    "path": "playground/pages/tabs/tab3/index.vue",
    "chars": 1078,
    "preview": "<script setup lang=\"ts\">\nuseHead({\n  title: 'Tab 3',\n})\nconst isExploreEnabled = ref(true)\n</script>\n\n<template>\n  <ion-"
  },
  {
    "path": "playground/pages/tabs/tab3/page-two.vue",
    "chars": 847,
    "preview": "<script setup lang=\"ts\">\nuseHead({\n  title: 'Page Two - Tab 3',\n})\nconst isExploreEnabled = ref(true)\n</script>\n\n<templa"
  },
  {
    "path": "playground/pages/tabs/tab4/index.vue",
    "chars": 7587,
    "preview": "<script setup lang=\"ts\">\nuseHead({\n  title: 'Tab 4',\n})\n</script>\n\n<template>\n  <ion-page>\n    <ion-header>\n      <ion-t"
  },
  {
    "path": "playground/pages/tabs.vue",
    "chars": 1163,
    "preview": "<script setup lang=\"ts\">\nuseHead({\n  title: 'House Tabs',\n})\n</script>\n\n<template>\n  <ion-page>\n    <ion-content>\n      "
  },
  {
    "path": "playground/tsconfig.json",
    "chars": 42,
    "preview": "{\n  \"extends\": \"./.nuxt/tsconfig.json\",\n}\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "chars": 199,
    "preview": "packages:\n  - playground\n  - docs\n\nonlyBuiltDependencies:\n  - '@parcel/watcher'\n  - '@tailwindcss/oxide'\n  - better-sqli"
  },
  {
    "path": "renovate.json",
    "chars": 121,
    "preview": "{\n  \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n  \"extends\": [\n    \"github>danielroe/renovate\"\n  ]\n}"
  },
  {
    "path": "src/imports.ts",
    "chars": 2424,
    "preview": "/* Ionic Hooks and components */\n\n// If you are about to add a hook or component to one of the array below, please do so"
  },
  {
    "path": "src/module.ts",
    "chars": 7058,
    "preview": "import { existsSync, promises as fsp } from 'node:fs'\n\nimport {\n  defineNuxtModule,\n  addComponent,\n  addPlugin,\n  addTe"
  },
  {
    "path": "src/parts/capacitor.ts",
    "chars": 1887,
    "preview": "import type { CapacitorConfig } from '@capacitor/cli'\nimport { findPath, useNuxt } from '@nuxt/kit'\nimport { join } from"
  },
  {
    "path": "src/parts/components.ts",
    "chars": 281,
    "preview": "import { resolve } from 'node:path'\nimport { addComponent } from '@nuxt/kit'\nimport { runtimeDir } from '../utils'\n\nexpo"
  },
  {
    "path": "src/parts/css.ts",
    "chars": 902,
    "preview": "import { useNuxt } from '@nuxt/kit'\n\nexport const useCSSSetup = () => {\n  const nuxt = useNuxt()\n\n  const setupCore = ()"
  },
  {
    "path": "src/parts/icons.ts",
    "chars": 542,
    "preview": "import { useNuxt, addImportsSources } from '@nuxt/kit'\nimport { defineUnimportPreset } from 'unimport'\nimport * as _icon"
  },
  {
    "path": "src/parts/meta.ts",
    "chars": 827,
    "preview": "import { useNuxt } from '@nuxt/kit'\n\nexport const setupMeta = () => {\n  const nuxt = useNuxt()\n\n  const metaDefaults = ["
  },
  {
    "path": "src/parts/router.ts",
    "chars": 1779,
    "preview": "import { existsSync } from 'node:fs'\nimport { useNuxt, useLogger } from '@nuxt/kit'\nimport { join, resolve } from 'pathe"
  },
  {
    "path": "src/runtime/app.vue",
    "chars": 153,
    "preview": "<template>\n  <ion-app>\n    <ion-router-outlet />\n  </ion-app>\n</template>\n\n<script setup>\nimport { IonApp, IonRouterOutl"
  },
  {
    "path": "src/runtime/components/IonAnimation.vue",
    "chars": 3944,
    "preview": "<script setup lang=\"ts\">\nimport { onMounted, onBeforeUnmount, ref } from 'vue'\n\nimport type {} from '@ionic/core'\nimport"
  },
  {
    "path": "src/runtime/composables/head.ts",
    "chars": 3859,
    "preview": "import { onIonViewDidEnter, onIonViewDidLeave } from '@ionic/vue'\nimport type { ActiveHeadEntry, UseHeadInput, UseHeadOp"
  },
  {
    "path": "src/runtime/plugins/ionic.ts",
    "chars": 268,
    "preview": "import { IonicVue } from '@ionic/vue'\nimport { defineNuxtPlugin } from '#imports'\nimport ionicVueConfig from '#build/ion"
  },
  {
    "path": "src/runtime/plugins/router.ts",
    "chars": 9676,
    "preview": "import {\n  createMemoryHistory,\n  createRouter,\n  createWebHashHistory,\n  createWebHistory,\n} from '@ionic/vue-router'\n\n"
  },
  {
    "path": "src/utils.ts",
    "chars": 121,
    "preview": "import { fileURLToPath } from 'node:url'\n\nexport const runtimeDir = fileURLToPath(new URL('./runtime', import.meta.url))"
  },
  {
    "path": "test/e2e/ion-head.spec.ts",
    "chars": 2623,
    "preview": "import { fileURLToPath } from 'node:url'\nimport { setup, createPage, url } from '@nuxt/test-utils/e2e'\nimport { describe"
  },
  {
    "path": "test/e2e/ssr.spec.ts",
    "chars": 1207,
    "preview": "/* @vitest-environment node */\nimport { fileURLToPath } from 'node:url'\nimport { setup, $fetch, createPage, url } from '"
  },
  {
    "path": "test/unit/capacitor.spec.ts",
    "chars": 6294,
    "preview": "import { describe, it, expect, vi, beforeEach } from 'vitest'\nimport { useNuxt, findPath } from '@nuxt/kit'\nimport { set"
  },
  {
    "path": "test/unit/imports.spec.ts",
    "chars": 1000,
    "preview": "import { expect, describe, it } from 'vitest'\nimport * as ionicVue from '@ionic/vue'\nimport { IonicBuiltInComponents, Io"
  },
  {
    "path": "tsconfig.json",
    "chars": 110,
    "preview": "{\n  \"extends\": \"./.nuxt/tsconfig.json\",\n  \"exclude\": [\n    \"node_modules\",\n    \"docs\",\n    \"playground\"\n  ]\n}\n"
  },
  {
    "path": "vitest.config.ts",
    "chars": 191,
    "preview": "import { defineConfig } from 'vitest/config'\n\nexport default defineConfig({\n  test: {\n    coverage: {\n      include: ['s"
  }
]

About this extraction

This page contains the full source code of the nuxt-modules/ionic GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 85 files (130.3 KB), approximately 36.6k tokens, and a symbol index with 9 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!