# @leaflink/stash-vue First-party Vue components and Utils

> Interactive building blocks for creating user interfaces.

[![version](https://img.shields.io/npm/v/@leaflink/stash-vue.svg?color=%230072f0)](https://www.npmjs.com/package/@leaflink/stash-vue)
[![downloads](https://img.shields.io/npm/dm/@leaflink/stash-vue.svg)](http://npm-stat.com/charts.html?package=@leaflink/stash-vue&from=2015-08-01)
[![Contact Us](https://img.shields.io/badge/slack-%23stash-pink.svg?logo=slack)](https://leaflink.slack.com/archives/C012WHER0R0)
[![Nx release](https://img.shields.io/badge/release-Nx_Release-143055?logo=nx&logoColor=white)](https://nx.dev/docs/features/manage-releases)
[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)

Stash is a collection of primitive, product-agnostic elements that help encapsulate LeafLink's look and feel at base
level. This project is intended to be used across our digital product portfolio.

> **Upgrading from `@leaflink/stash`?** The Vue library is now published as `@leaflink/stash-vue`. Install
> `@leaflink/stash-vue` and update all imports and dependency references from `@leaflink/stash` to
> `@leaflink/stash-vue`.

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
## Table of Contents

- [Development Setup](#development-setup)
  - [Installing pnpm](#installing-pnpm)
  - [Installing Dependencies](#installing-dependencies)
- [Quick Start](#quick-start)
- [Usage](#usage)
  - [Example](#example)
  - [npm scripts](#npm-scripts)
- [Legacy Styles](#legacy-styles)
- [Tailwind](#tailwind)
  - [Configuration](#configuration)
    - [Override behavior](#override-behavior)
  - [VSCode](#vscode)
- [Resources](#resources)
- [Core files & Entry Points](#core-files--entry-points)
- [à la carte](#%C3%A0-la-carte)
- [Peer dependencies](#peer-dependencies)
- [Testing](#testing)
  - [Mocking Google Maps API when testing AddressSelect](#mocking-google-maps-api-when-testing-addressselect)
- [Assets](#assets)
- [Illustrations and Icons](#illustrations-and-icons)
  - [Testing `Icon`'s and `Illustration`'s](#testing-icons-and-illustrations)
- [Contributing](#contributing)
- [Architecture](#architecture)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## Development Setup

This project uses **pnpm** as the package manager. Please ensure you have pnpm installed before contributing.

### Installing pnpm

```bash
# Using npm
npm install -g pnpm

# Using Homebrew (macOS)
brew install pnpm

# Using curl
curl -fsSL https://get.pnpm.io/install.sh | sh -
```

### Installing Dependencies

```bash
pnpm install
```

> ⚠️ **Important**: This project enforces pnpm usage. Other package managers (npm, yarn) will be blocked by the
> preinstall script.

## Quick Start

Stash requires Vue 3 and Tailwind CSS v4. To get started, install both packages and peer dependencies:

```sh
npm install @leaflink/stash-theme @leaflink/stash-vue tailwindcss@4 @tailwindcss/postcss@4
```

Alternatively, to install only peer dependencies (you must add `@leaflink/stash-theme` separately):

```sh
npx install-peerdeps @leaflink/stash-vue
```

Then, import the package and its styles in your app. Your main CSS file must include the Stash theme stack in order (see
below); load it before your app overrides. **Whenever you use stash-vue components, import both**
`@leaflink/stash-theme/tailwind-reset` **in `layer(reset)`**, then `@leaflink/stash-theme/tailwind-base` **and**
`@leaflink/stash-vue/styles/components-base.css` **in the `base` layer** (after Sofia font if you load it).

```ts filename="main.ts"
import { createApp } from 'vue';
import stash from '@leaflink/stash-vue';

// app.css contains the Stash theme stack (see below) and your app styles
import './app.css';
import '@leaflink/stash-vue/components.css' layer(utilities);

const app = createApp(App);

app.use(stash);
```

Your `app.css` must include the Stash theme stack in **this exact order** (wrong order can break styles). Tailwind v4
uses CSS-first configuration:

```css filename="app.css"
@layer theme, reset, base, components, utilities;
@import 'tailwindcss/theme.css' layer(theme);
@import '@leaflink/stash-theme/tokens' layer(theme);
@import '@leaflink/stash-theme/tailwind-theme' layer(theme);
@import '@leaflink/stash-vue/styles/tailwind-layer.css';
@import '@leaflink/stash-theme/tailwind-reset' layer(reset);
@import '@leaflink/stash-theme/sofia-font' layer(base);
@import '@leaflink/stash-theme/tailwind-base' layer(base);
@import '@leaflink/stash-vue/styles/components-base.css' layer(base);
@import '@leaflink/stash-vue/styles/backwards-compat.css' layer(base); /* optional */
@import '@leaflink/stash-vue/components.css' layer(utilities);
@import 'tailwindcss/utilities.css' layer(utilities) source(none);

/* Scan your app files and stash's dist for Tailwind classes */
@source "./src/**/*.{vue,ts,js}";
@source "./node_modules/@leaflink/stash-vue/dist/*.js";
```

No `tailwind.config.ts` needed! See the [Tailwind](#tailwind) section and
[Tailwind v4 Migration Guide](../../docs/architecture/TAILWIND_V4_MIGRATION.md) for more details.

**Font:** The default sans font is **Sofia**, wired through the theme tokens (via `--stash-font-sans`). To load the font
files, import `@leaflink/stash-theme/sofia-font` once in your app's main CSS (before or where components-base.css is
applied). See
[docs/architecture/FONT-CHANGE-SENSITIVE-STYLES.md](../../docs/architecture/FONT-CHANGE-SENSITIVE-STYLES.md) for layout
considerations when working with typography-sensitive UIs.

> [!NOTE] For apps still requiring deprecated css & utility classes, you can include the backwards compat styles from
> Stash:

```ts filename="main.ts"
import '@leaflink/stash-vue/styles/backwards-compat.css'; // Before the theme stack
import './app.css'; // app.css contains the Stash theme stack in order (see Tailwind section)
import '@leaflink/stash-vue/components.css' layer(utilities);
```

Also, if you still need legacy Stash sass variables, functions, and mixins in your components, you can configure Vite to
import them:

```ts filename="vite.config.ts"
export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd(), '');

  return {
    css: {
      preprocessorOptions: {
        scss: {
          additionalData: '@import "@leaflink/stash-vue/styles/core";',
        },
      },
    },
  };
});
```

## Usage

`@leaflink/stash-vue` is a Vue component library that implements
[Leaflink's Stash Design System](https://stash.leaflink.com). So every one of LeafLink's colors, typography, shadows,
etc. can be accessible via tailwind utility classes like `text-blue-500 text-sm`.

Stash is a Vue plugin that can be installed in your app. You **do not need to install the plugin in order to use the
components**, but it is required if you need to configure the framework to suit your specific needs.

There are several options to configure the framework to suit your specific needs, and they are all optional. Any options
you pass to the plugin will be merged with the default options & applied to the entire framework.

> [!WARNING] If you don't install the plugin in your app, you will need to manually setup
> [modals](https://stash.leaflink.com/guides/modals.html#installation),
> [toasts](https://stash.leaflink.com/guides/toasts.html#installation), and other features that require some setup
> that's normally done for you by the Stash plugin.

```ts
interface StashPluginOptions {
  /**
   * Translation options, language and locale
   */
  i18n?: I18nPlugin;

  /**
   * Setup methods to persist user-settings. There are several LocalStorage helpers which may be overridden
   */
  storage?: {
    set: <T = unknown>(name: string, data: T, options?: { [key: string]: unknown }) => void;
    get: <T = unknown>(name: string, options?: { [key: string]: unknown }) => T;
  };

  /**
   * Path to static assets such as icons and illustrations
   */
  staticPath?: string;

  /**
   * Image options
   */
  images?: StashOptionImages;

  /**
   * Google Maps API key
   */
  googleMapsApiKey?: string;

  /**
   * Modals options
   */
  modals?: false | ModalsPluginOptions;

  /**
   * Toasts options
   */
  toasts?: false | ToastsPluginOptions;
}

interface StashOptionImages {
  provider: StashImageProviders; // 'cloudinary' | 'static'
}

interface ModalsPluginOptions {
  mountNodeClass?: string;
  mountNodeId?: string;
}

interface ToastsPluginOptions {
  mountNodeClass?: string;
  mountNodeId?: string;
}
```

### Example

A sample configuration might look something like:

```ts
// src/main.ts
import { createApp } from 'vue';
import stash from '@leaflink/stash-vue';
import i18n, { locale } from 'path/to/i18n';

const app = createApp(App);

app.use(stash, {
  i18n: {
    locale,
    t: (key, value) => i18n.t(key, value),
  },
  googleMapsApiKey: import.meta.env.VITE_GOOGLE_MAPS_API,
});
```

This example will load the core i18n options and Google Maps api key.

### npm scripts

Most operations are run using **pnpm** and are defined in the root and package `package.json` files. From the repo root,
run `pnpm dev:help` to list dev commands.

A few commonly used commands are:

- **From repo root:** `pnpm lint` (ESLint) and `pnpm lint:css` (Stylelint); `pnpm lint:fix` / `pnpm lint:fix:css` to
  auto-fix. `pnpm test` runs tests for all projects; `pnpm test:vue` runs only this package. `pnpm type-check` and
  `pnpm type-check:vue`; `pnpm build` builds all (or use `nx build vue` for this package).
- **From this package (`packages/vue`):** `pnpm test` runs Vitest once; `pnpm test:watch` for watch mode;
  `pnpm test <file>` to run matching specs; `pnpm test:ci` for coverage. `pnpm type-check` and `pnpm build` for this
  package only.

## Legacy Styles

`@leaflink/stash-vue` exposes a stylesheet for backwards compatibility with legacy stash utilities. This stylesheet
includes styles for components that have been deprecated or removed from the Stash Design System. It is not required for
greenfield projects.

```ts filename="main.ts"
/* legacy stash styles - not required for greenfield projects */
import '@leaflink/stash-vue/styles/backwards-compat.css';

/* theme stack in order (see Tailwind section) + components */
import './app.css';
import '@leaflink/stash-vue/components.css' layer(utilities);
```

## Tailwind

`@leaflink/stash-vue` uses [Tailwind](https://tailwindcss.com/) behind the scene to style its components. It's currently
required to **run this library downstream in order to avoid issues with css duplication & ordering**.

```jsx
import Button from '@leaflink/stash-vue/Button.vue';
import IconLabel from '@leaflink/stash-vue/IconLabel.vue';

<Button icon-label class="hidden md:inline ml-3">
  <IconLabel icon="user-add" title="Add Recipient" size="dense" stacked>
    Add Recipient
  </IconLabel>
</Button>;
```

### Configuration

**Tailwind v4 Configuration:**

Stash uses Tailwind v4 with CSS-first configuration. Import Stash's base styles in your main CSS file. For setup details
and migration guidance, see the Tailwind v4 migration guide in the docs.

```css
/* app.css */
@layer theme, reset, base, components, utilities;
@import 'tailwindcss/theme.css' layer(theme);
@import '@leaflink/stash-theme/tokens' layer(theme);
@import '@leaflink/stash-theme/tailwind-theme' layer(theme);
@import '@leaflink/stash-vue/styles/tailwind-layer.css';
@import '@leaflink/stash-theme/tailwind-reset' layer(reset);
@import '@leaflink/stash-theme/sofia-font' layer(base);
@import '@leaflink/stash-theme/tailwind-base' layer(base);
@import '@leaflink/stash-vue/styles/components-base.css' layer(base);
@import '@leaflink/stash-vue/styles/backwards-compat.css' layer(base); /* optional */
@import '@leaflink/stash-vue/components.css' layer(utilities);
@import 'tailwindcss/utilities.css' layer(utilities) source(none);

/* Scan your app files and stash's dist for Tailwind classes */
@source "./src/**/*.{vue,ts,js}";
@source "./node_modules/@leaflink/stash-vue/dist/*.js";
```

#### Override behavior

Stash module CSS, the Tailwind utilities stash templates use, and your app's Tailwind utilities all live together in the
`utilities` layer. Within one layer, normal CSS rules apply: higher specificity wins, ties go to whichever rule appears
last in source order.

- Apply a Tailwind utility to a stash component (e.g. `<Button class="rounded-full" />`) — your `.rounded-full` ties on
  specificity with stash's module rule and wins on source order because your CSS loads after stash's.
- Stash module rules with higher specificity (e.g. `:last-of-type:not(...)` selectors) are intentional visual invariants
  and beat single-class consumer utilities. Override those with Tailwind v4 important syntax (`pr-2!`) or a more
  specific selector when needed.

> **Migrating from an earlier version:** `components.css` is now imported with `layer(utilities)` instead of
> `layer(components)`. That's the only change — keep your existing
> `@source "../../node_modules/@leaflink/stash-vue/dist/*.js";` directive so Tailwind still emits the classes stash
> templates use.

### VSCode

To avoid warnings from VSCode (and Cursor) saying "Unknown at rule" for `@reference`, `@apply`, and others from
tailwind:

1. Create a `.vscode/` directory (it is gitignored)
2. Add a `settings.json` file inside of it unless one already exists
3. Create a `custom-css.json` file in the `.vscode` directory
4. In the `custom-css.json` file, copy-paste its content from
   [here](https://github.com/tailwindlabs/tailwindcss/discussions/5258#discussioncomment-13239940)

## Resources

- **index.js**: This is the "install" entry point, for use with `app.use(...)`.
- **components**: All components
- **composables**: Similar to mixins or React's "Hooks", but for a Vue component
- **constants**: LeafLink global constants
- **directives**: [Vue directives](https://vuejs.org/guide/reusability/custom-directives#custom-directives)
- **plugins**: [Vue plugins](https://vuejs.org/guide/reusability/plugins.html#plugins)
- **styles**: SCSS, CSS, style utils, etc.
- **types**: TypeScript type declarations
- **utils**: Includes various helpers for internal and external use

## Core files & Entry Points

`index.js` is used as the main entry point to the framework. It also exports each component individually, for an _à la
carte_ build. You may pull in the default export directly and `app.use` it (to quickly get up and running w/ all
components and features); or, you may wish configure it with particular options, components, or features.

## à la carte

`@leaflink/stash-vue` serves its components and directives _à la carte_, which means that instead of importing the
entire library, you selectively import only the specific components and directives that you need for your project. This
approach helps reduce the bundle size of your application, resulting in faster load times and improved performance.

```js
// Component.vue

import Select from '@leaflink/stash-vue/Select.vue';

<Select></Select>;
```

```js
// Component.vue

import vAutofocus from '@leaflink/stash-vue/directives/autofocus';

<button v-autofocus>Click</button>;
```

## Peer dependencies

Peer dependencies are specific dependencies that a package requires to work correctly, but expects the consumer of the
package to provide. In other words, they are dependencies that the package relies on, but are not bundled with the
package itself.

`@leaflink/stash-vue` project requires some peer dependencies:

- `lodash-es`: The utility library is required as a peer dependency as an optimization to reduce the bundle size.
  Required compatibility with this package on version **^4.x**.

- `tailwindcss`: Our utility-first CSS framework used for building our responsive and customizable components. Required
  compatibility with this package on version **^4.x.x**.

- `vue`: **^3.5.28** or higher. Older Vue versions (e.g. 3.5.13) can cause type errors when used with this package and
  other dependencies; consumers should use 3.5.28+ so the whole dependency tree uses a single compatible Vue.

- `vue-router`: The official router for Vue.js applications. Required **^4.6.4** or higher. Older versions (4.0–4.5)
  ship Vue types that conflict with Vue 3.5.28+ and cause TypeScript errors.

These peer dependencies need to be installed separately by the consumer of the package, ensuring that the correct
versions are used to maintain compatibility and avoid conflicts with other dependencies in the project.

## Testing

> [!TIP] If you are contributing to `@leaflink/stash-vue`, see the
> [Contributing guide](https://github.com/LeafLink/stash/blob/main/CONTRIBUTING.md#testing) (testing section) for how to
> run and write tests.

To run tests (use **pnpm**; from repo root or from `packages/vue`):

- **From root:** `pnpm test` runs tests for all projects (once); `pnpm test:vue` runs only this package.
- **From this package:** `pnpm test` runs Vitest once; `pnpm test:watch` for watch mode; `pnpm test <file>` for matching
  specs; `pnpm test:ci` for coverage (used in CI).

You can pass Vitest options, e.g. `pnpm test -- --silent`.

Testing Library truncates the output from tests, which can cut off large DOM elements logged to the console. Adjust the
limit with the `DEBUG_PRINT_LIMIT` environment variable, e.g. `DEBUG_PRINT_LIMIT=100000 pnpm test` or
`export DEBUG_PRINT_LIMIT=100000` in your shell profile. See
[Testing Library debugging](https://testing-library.com/docs/dom-testing-library/api-debugging).

Coverage HTML reports are written to `packages/vue/coverage` when you run `pnpm test:ci` from that package (or
`nx test vue --coverage` from root). Open `packages/vue/coverage/index.html` in your browser to view the report.

To test `@leaflink/stash-vue` components, it's necessary to expose stash as a plugin on the global config test object.

```ts
// setup-env.ts

import stash from '@leaflink/stash-vue';
import { config, flushPromises } from '@vue/test-utils';

config.global.plugins = [[stash, { googleMapsApiKey: 'my-key' }]];
```

### Mocking Google Maps API when testing AddressSelect

When testing components that use the `AddressSelect` component or `useGoogleMaps` composable, it's necessary to mock it.
This is because the `useGoogleMaps` composable uses the `google.maps` global object, which is not available in the
testing environment.

The easiest way to do this is to mock the useGoogleMaps composable and avoid trying to mock the Google Maps API
directly.

Create a file in the `__mocks__` directory of the `@leaflink/stash-vue` package, and mock the `useGoogleMaps`
composable.

```ts
/* __mocks__/@leaflink/stash-vue/useGoogleMaps.js */
export default function () {
  return {
    getPlaceDetails: vi.fn().mockResolvedValue({
      street_address: '123 Main St',
      extended_address: 'ap 802',
      city: 'New York',
      state: 'NY',
      postal_code: '10001',
      country: 'US',
    }),
    getPlacePredictions: () => {
      return Promise.resolve([{ id: '1', name: '123 Main St, ap 802, New York, US' }]);
    },
  };
}

/* tests/setup-env.ts */
import '@leaflink/dom-testing-utils/setup-env'; // to ensure lodash-es/debounce is mocked properly
vi.mock('@leaflink/stash-vue/useGoogleMaps');

/* src/components/YourComponent.spec.ts */
const user = userEvent.setup();
// Start typing in the AddressSelect select input to trigger the useGoogleMaps mock response
await user.type(screen.getByPlaceholderText('Search'), 'type anything');
// The mock response will be used to populate the options - so it will always be the same
// (id of 1 from the getPlacePredictions mock)
await user.selectOptions(screen.getByLabelText('Bank address'), ['1']);
```

It's also encouraged the use of [@leaflink/dom-testing-utils](https://github.com/LeafLink/dom-testing-utils) for testing
utilities like global and local test setup, mocking endpoints, clean up components, get selected options and more.
Checkout the [documention](https://github.com/LeafLink/dom-testing-utils) for learning more about this package.

## Assets

When using Stash, a collection of assets are available to use, such as icons and illustrations.

In order to configure the assets path for your project, you can do it via the `staticPath` option. By default, this
property is set to the `/assets` path.

```ts
import { createApp } from 'vue';
import stash from '@leaflink/stash-vue';

const app = createApp(App);

app.use(stash, {
  staticPath: '/my-assets-path',
});
```

Usually you will want to copy assets from the package installed in your `node_modules` folder to your application.

For projects using Vite, you can do it using the `copy` rollup plugin and adding to your plugins array:

```sh
pnpm add -D rollup-plugin-copy
```

```ts
import path from 'node:path';

import vue from '@vitejs/plugin-vue';
import copy from 'rollup-plugin-copy';
import { defineConfig, loadEnv } from 'vite';

export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd(), '');

  return {
    plugins: [
      vue(),
      copy({
        targets: [
          {
            src: path.resolve(__dirname, 'node_modules/@leaflink/stash-vue/assets/spritesheet.svg'),
            dest: 'public/static',
          },
          {
            src: path.resolve(__dirname, 'node_modules/@leaflink/stash-vue/assets/illustrations'),
            dest: 'public/static',
          },
        ],
        hook: 'buildStart',
      }),
    ],
  };
});
```

## Illustrations and Icons

It's encouraged to use Stash's `Illustration` and `Icon` components for these kind of data.

1. If your work includes a new illustration, add it here in Stash:
   <https://github.com/LeafLink/stash/tree/main/assets/illustrations>
2. Import the component:
   ```js
   import Illustration from '@leaflink/stash-vue/Illustration.vue';
   import Icon from '@leaflink/stash-vue/Icon.vue';
   ```
3. Use it in your template:
   ```html
   <Illustration name="your-illustration-name" /> <Icon name="your-icon-name" />
   ```
4. Customize however you like: i.e:
   ```html
   <Illustration name="your-illustration-name" :size="58" /> <Icon name="your-icon-name" :size="58" />
   ```

If you're working on existing templates that use `SvgIcon` using one of the newer illustrations and you feel inclined to
migrate it over to Stash, that would be helpful!

### Testing `Icon`'s and `Illustration`'s

The `Icon` and `Illustration` components from Stash now loads SVG's asyncronously. This is fine for tests unless you're
actually looking to query for an SVG. In that event, you will just need to be sure to `await findBy...` the icon before
asserting on or interacting with it.

**Example**

```html
<!-- SomeComponent.vue -->
<Icon v-if="someCondition" data-test="delete-adjustment-icon" name="trashcan" />
```

```ts
// ❌ Fails
renderAccountingAmounts();
expect(screen.getByTestId('delete-adjustment-icon')).toBeInTheDocument();

// ❌ Possible false-positives
renderAccountingAmounts();
expect(screen.queryByTestId('delete-adjustment-icon')).not.toBeInTheDocument();

// ✅ Passes
renderAccountingAmounts();
expect(await screen.findByTestId('delete-adjustment-icon')).toBeInTheDocument();

// ✅ Passes
import { flushPromises } from '@vue/test-utils';

renderAccountingAmounts();
await flushPromises();
expect(screen.queryByTestId('delete-adjustment-icon')).not.toBeInTheDocument();
```

**Details**

- Slack thread: https://leaflink.slack.com/archives/C012WHER0R0/p1688063341166259
- Follow up Slack thread: https://leaflink.slack.com/archives/C012WHER0R0/p1688660541670479
- PR: https://github.com/LeafLink/stash/pull/1037

## Contributing

Anyone can contribute to `@leaflink/stash-vue`! Please check out the
[Contribution guide](https://github.com/LeafLink/stash/blob/main/CONTRIBUTING.md) for guidelines about how to proceed.

Reach out in [slack](https://leaflink.slack.com/archives/C012WHER0R0) if you have other questions.

## Architecture

If you are wanting to understand the how or why behind what is built, see the
[Architecture doc](https://github.com/LeafLink/stash/blob/main/ARCHITECTURE.md).
