Stash
Interactive building blocks for creating user interfaces.
Stash is LeafLink's design system: Vue components (@leaflink/stash-vue) and design tokens and Tailwind v4 theme (@leaflink/stash-theme). It is intended for use across LeafLink's digital product portfolio.
Quick Start
The stash-vue package requires Vue 3 and Tailwind CSS 4. To get started, install the package and its peer dependencies. Apps using @leaflink/stash-vue should import @leaflink/stash-theme/tailwind-base and @leaflink/stash-vue/styles/components-base.css in the base layer (after Sofia font if you load it)—global resets plus shared tooltip/transition helpers and other utilities Stash Vue components expect:
npm install @leaflink/stash-theme @leaflink/stash-vue tailwindcss@4 @tailwindcss/postcss@4Configure PostCSS to use @tailwindcss/postcss (see Tailwind v4 Migration Guide Step 3). Then, import the package and its styles in your app. Design tokens and the Tailwind v4 theme come from @leaflink/stash-theme. Import the separate files in order (tokens → tailwind → tailwind-layer → base) as shown in the Tailwind section.
import { createApp } from 'vue';
import stash from '@leaflink/stash-vue';
// app.css contains the Stash theme stack (see Tailwind section) and your app styles
import './app.css';
import '@leaflink/stash-vue/components.css' layer(components);
const app = createApp(App);
app.use(stash);Your app.css must include the Stash theme stack in the order below, then your app @source and any app-specific CSS. Load Stash styles before your overrides.
Tailwind v4: Stash uses Tailwind v4 with CSS-first configuration. Load tokens and the Tailwind theme before the Stash layer in this order:
@layer theme, 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/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(components);
@import "tailwindcss/utilities.css" layer(utilities) source(none);
/* Scan your app files for Tailwind classes */
@source "./src/**/*.{vue,ts,js}";No tailwind.config.ts needed! See the Tailwind section and Tailwind v4 Migration Guide for migration details. For a full list of breaking and recommended changes when upgrading, see the theme migration guide in the architecture docs.
If you want the tokens and Tailwind theme without Vue, import @leaflink/stash-theme directly in your own stack:
@layer theme, 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-theme/sofia-font" layer(base);
@import "@leaflink/stash-theme/tailwind-base" layer(base);
@import "tailwindcss/utilities.css" layer(utilities) source(none);
@source "./src/**/*.{vue,ts,js}";INFO
Fonts: The default sans font is Sofia (loaded via @leaflink/stash-theme/sofia-font when needed). Stash stacks are standardized on Sofia.
INFO
Using a third-party component library? Follow that library's theming and preset documentation to integrate it with your design tokens and Tailwind setup.
Also, if you still need legacy Stash sass variables, functions, and mixins in your components, you can configure Vite to import them:
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. 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, toasts, and other features that require some setup that's normally done for you by the Stash plugin.
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:
// 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 (this repo’s package manager). Scripts are defined in the root and package package.json files.
Common commands (from repo root):
pnpm docs— start the Vitepress dev server on port 5180pnpm lint— ESLint for JS/TS;pnpm lint:css— Stylelint for CSS/Vue. Usepnpm lint:fix/pnpm lint:fix:cssto auto-fixpnpm test— run tests for all projects;pnpm test:vuefor the Vue package only. Frompackages/vue,pnpm test <file>orpnpm test:watchfor Vitestpnpm test:ci— defined inpackages/vue(Vitest with coverage)pnpm type-check— type-check all projects;pnpm type-check:vuefor Vue onlypnpm build— build all projects
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.
/* legacy stash styles - not required for greenfield projects */
@import '@leaflink/stash-vue/styles/backwards-compat.css' layer(base);Tailwind
@leaflink/stash-vue uses Tailwind behind the scene to style its components. It's currently required to run this library downstream in order to generate styles for stash's tailwind classes used. This helps avoiding issues with duplications & css ordering.
import Button from '@leaflink/stash-vue/Button.vue';
import IconLabel from '@leaflink/stash-vue/IconLabel.vue';
<Button icon-label class="text-blue-500 ml-3">
<IconLabel icon="user-add" title="Add Recipient" size="dense" stacked>Add Recipient</IconLabel>
</Button>Configuration
Tailwind v4: Stash uses CSS-first configuration. Import the separate files in this order (tokens and @leaflink/stash-theme/tailwind-theme must load before tailwind-layer.css):
/* app.css */
@layer theme, 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/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(components);
@import "tailwindcss/utilities.css" layer(utilities) source(none);
/* Scan your app files */
@source "./src/**/*.{vue,ts,js}";See the Tailwind v4 Migration Guide for complete instructions and customization options.
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: Standalone constants in
@leaflink/stash-constants(e.g. color-name mappings) - directives: Vue directives
- plugins: Vue plugins
- styles: SCSS, CSS, style utils, etc.
- types: Standalone types in
@leaflink/stash-types(e.g.StashColorNamesWithShades) - utils: Standalone helpers in
@leaflink/stash-utils(e.g.getColorScheme)
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.
// Component.vue
import Select from '@leaflink/stash-vue/Select.vue';
<Select></Select>// 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:
postcss-preset-env: Used for transforming CSS with PostCSS. Required by Tailwind. Required compatibility with this package on version 9.x.tailwindcss: Our utility-first CSS framework used for building our responsive and customizable components. Required compatibility with this package on version ^4.x (use with@tailwindcss/postcss@4).typescript: Adds static type-checking to JavaScript. Required compatibility with this package on version ^5.x or higher.vue-router: The official router for Vue.js applications. Required compatibility with this package on version ^4.x or higher.
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, please refer to this contributing testing section for more details on how to properly test your changes.
To run tests (use pnpm from the repo root or from packages/vue):
- From root:
pnpm testruns tests for all projects;pnpm test:vueruns only the Vue package. - From packages/vue:
pnpm testruns Vitest once;pnpm test:watchfor watch mode;pnpm test <file>for matching specs;pnpm test:cifor 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. This limit can be adjusted with the DEBUG_PRINT_LIMIT environment variable, e.g. DEBUG_PRINT_LIMIT=100000 pnpm test (or add export DEBUG_PRINT_LIMIT=100000 to your shell profile). For more on debugging with Testing Library, see official documentation.
Coverage HTML reports are written to ./coverage.
Run open ./coverage/index.html from the root of the repo to pop open the report in your browser of choice.
To test @leaflink/stash-vue components, it's necessary to expose stash as a plugin on the global config test object.
// setup-env.ts
import stash from '@leaflink/stash-vue';
import { config, flushPromises } from '@vue/test-utils';
config.global.plugins = [[stash, { googleMapsApiKey: 'my-key' }]];For example: if you are using the AddressSelect component, you need to mock the useGoogleMaps composable.
// 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' }]);
},
};
}It's also encouraged the use of @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 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.
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:
npm install -D rollup-plugin-copyimport 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 }) => {
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.
- If your work includes a new illustration, add it here in Stash: https://github.com/LeafLink/stash/tree/main/assets/illustrations
- Import the component:js
import Illustration from '@leaflink/stash-vue/Illustration.vue'; import Icon from '@leaflink/stash-vue/Icon.vue'; - Use it in your template:html
<Illustration name="your-illustration-name" /> <Icon name="your-icon-name" /> - 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
<!-- SomeComponent.vue -->
<Icon v-if="someCondition" data-test="delete-adjustment-icon" name="trashcan" />// ❌ 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 for guidelines about how to proceed.
Reach out in slack if you have other questions.
Architecture
If you are wanting to understand the how or why behind what is built, see the ARCHITECTURE.md doc.