Getting started โ
Mono UI is a small set of Lit-powered web components โ <mono-button>, <mono-input>, <mono-card>, etc. They're standard custom elements, so they work in Vue, React, Svelte, plain HTML, or anywhere custom elements run.
Install โ
Place below code inside your package.json after that just install with pnpm i. You'll need a GitHub token, lit is a peer dependency โ it ships separately so you can share one copy across the components and your own code.
json
{
"devDependencies": {
"lit": "^3.3.2",
"mono-helper": "git+https://<GITHUB_CLASSIC_TOKEN>@github.com/EJI-ICT/libs#path:/packages/mono-helper"
}
}Load the styles โ
Import the global stylesheet once, near the top of your app entry. It registers the CSS variables every component reads (colors, spacing, density, light/dark) and the scoped rules that paint each element.
ts
import 'mono-helper/ui/index.css'Tree-shakeable imports โ
Each component lives at its own subpath. Import only the ones you use โ the rest never enter your bundle.
ts
import 'mono-helper/ui/button'
import 'mono-helper/ui/input'
import 'mono-helper/ui/card'Importing the module is enough โ the file calls customElements.define('mono-โฆ', โฆ) as a side effect, and the tag becomes available everywhere.
Use in Vue โ
Two pieces of one-time setup are needed in vite.config.ts (or your VitePress config). Tell Vue to skip its component resolver for mono-* tags so it leaves them alone for the browser to handle:
ts
import vue from '@vitejs/plugin-vue'
export default {
plugins: [
vue({
template: {
compilerOptions: {
isCustomElement: (tag) => tag.startsWith('mono-'),
},
},
}),
],
}Then any component renders just like an HTML element:
vue
<script setup>
import { ref } from 'vue'
import 'mono-helper/ui/input'
import 'mono-helper/ui/button'
const name = ref('')
</script>
<template>
<mono-input
label="Name"
:model-value="name"
@mno-input="name = $event.detail.modelValue"
></mono-input>
<mono-button color="primary" @click="alert(`Hello ${name}`)">
Say hi
</mono-button>
</template>TypeScript suggestions โ
Every <mono-*> tag is typed for Vue โ start typing inside one and your editor lists its real props with their real types:
vue
<mono-button coโถ
โ color ButtonColor
โ circle boolean
โ css-class ButtonCssClassThis is automatic. mono-helper ships a generated GlobalComponents augmentation (dist/vue.d.ts) that maps all 77 registered tags โ light and mono-shadow-* โ to the prop interfaces the components already declare, and the package's types entry references it. Importing anything from mono-helper activates it; there is nothing to install or configure. It covers native attributes (id, class, style, data-*), directives, key/ref, and .prop bindings.
To also turn a typo into an error, switch on Volar's strict template checking โ without it you get completions but unknown attributes pass silently:
jsonc
// tsconfig.json
{
"vueCompilerOptions": { "strictTemplates": true }
}vue
<mono-button color="primary" id="save" /> <!-- โ -->
<mono-button colr="primary" /> <!-- โ Did you mean to write 'color'? -->Non-Vue projects
vue is an optional peer dependency, so nothing breaks if you don't use Vue. If you'd rather wire the types explicitly than have them arrive with the main import, mono-helper/vue exposes the same augmentation: import type {} from 'mono-helper/vue'.
No suggestions? Check for two copies of vue
The augmentation is declare module 'vue', so it only applies to the exact vue package mono-helper resolves. If your app has a second copy โ common with pnpm, a monorepo, or a git/file: dependency where mono-helper is symlinked outside your node_modules โ the augmentation lands on the other copy and GlobalComponents stays empty, with no error to tell you.
Check with pnpm why vue (or npm ls vue); if more than one version shows up, dedupe to a single one. That was exactly the symptom in this docs site until mono-helper and the demo were pinned to the same vue.
Typed events โ
@mno-* handlers are typed too, including $event.detail:
vue
<!-- detail is InputModelEventDetail โ modelValue completes -->
<mono-input @mno-input="name = $event.detail.modelValue" />
<!-- multi-word names work; so does the camel spelling @mnoLoadingChange -->
<mono-button @mno-loading-change="e => console.log(e.detail.phase)" />
<mono-input @mno-inputt="โฆ" /> <!-- โ no such event -->
<mono-button @mno-change="โฆ" /> <!-- โ mono-button doesn't emit it -->
<mono-input @mno-input="e => e.detail.modelValu" /> <!-- โ Did you mean 'modelValue'? -->Each component's events come from its *Events interface (ButtonEvents, InputEvents, โฆ), so a component that declares one is typed automatically. 45 of 77 tags carry events today; the rest โ the mono-table-* controls, chart and dropdown-table โ emit nothing at all, because they're driven through their controller rather than DOM events. Their props are still fully typed.
A few Vue-specific gotchas:
v-modelon Lit elements silently misses the dispatched events. Bind:model-valueand listen for@mno-change/@mno-inputinstead, reading from$event.detail.modelValue.- For object/array props (
mono-menuitems,mono-selectoptions,cssClass), use the.propmodifier::items.prop="x". A plain:items="x"only sets a stringified attribute that Lit ignores. - For named slots, write
<el slot="name">โฆ</el>. Vue's<template #name>shorthand crashes the compiler on custom elements.