# KunUI — full documentation for LLMs > Cross-framework component library — 88 Tailwind v4 Vue components, framework-agnostic design tokens + core, with a Nuxt layer. Published on npm under the @kungal scope; the four packages are versioned in lockstep. This file inlines the integration guide plus a component reference. For the compact index see llms.txt. --- # Critical rules ## HTML is never sanitized KunUI ships no sanitizer. KunContent (the `content` prop, rendered via v-html) and useKunMessage(msg, type, dur, richText=true) render raw HTML exactly as given. Sanitize untrusted/user HTML yourself (e.g. DOMPurify) BEFORE passing it in. Fully-trusted/author content may be passed directly. ## Icons are bundled, never fetched KunIcon renders inline SVG from an in-memory registry seeded with the icons the components need — it NEVER calls the Iconify API at runtime. Use bundled names like `lucide:check`. Register your own with registerKunIcon(name, {body}) / registerKunIcons({...}) from @kungal/ui-core at startup. Unknown names render the injected iconComponent fallback (the Nuxt layer wires @nuxt/icon; plain Vue renders nothing). ## The consumer owns the Tailwind entry KunUI does NOT ship one all-in-one stylesheet. Your app's CSS entry must contain: `@import 'tailwindcss';` then `@import '@kungal/ui-tokens';` then `@import '@kungal/ui-vue/style.css';` (scoped component styles) plus `@source` directives pointing at @kungal/ui-vue and @kungal/ui-core so Tailwind generates the utility classes the components use. Under pnpm, install @kungal/ui-vue and @kungal/ui-core as DIRECT deps so the @import/@source resolve. Dark mode = toggle the `kun-dark-mode` class on . --- # KunUI Integration Guide How to add KunUI to your own project and integrate it yourself. This guide is accurate to the **currently published** packages and is meant to be read by a consuming project (Nuxt or plain Vue) with **no changes required on the KunUI side**. > TL;DR — KunUI is Tailwind-v4-native. You install the packages, and **your app > owns its Tailwind entry stylesheet**. There are exactly three CSS lines you > must add (`tailwindcss`, `@kungal/ui-tokens`, `@kungal/ui-vue/style.css`) plus a > couple of `@source` directives so Tailwind generates the classes the > components use. Everything else is auto-wired. --- ## 1. Package map — what to install All four packages are published under the `@kungal` scope and **share one version** (lockstep semver — always install matching versions). | Package | What it is | Install it when | | --- | --- | --- | | `@kungal/ui-tokens` | Framework-agnostic Tailwind v4 theme: semantic colors, radius, z-index, animations, optional base layer. **Pure CSS.** | Always (you `@import` it in your stylesheet). | | `@kungal/ui-core` | Framework-agnostic TypeScript core: `cn()`, the variant/class matrix, the radius system, the bundled-icon registry, shared types (`KunUIColor`, `KunUser`, …). **No Vue/React.** | Pulled in automatically; you import from it only for icon registration or shared types. | | `@kungal/ui-vue` | The Vue 3 component layer (88 components). Works in **any** Vue 3.5+ app. Decoupled from Nuxt. | Plain Vue / Vite apps. | | `@kungal/ui-nuxt` | A **Nuxt 4 layer** that wraps `@kungal/ui-vue`: auto-imports all components & composables and injects `NuxtLink` + `@nuxt/icon` + `@nuxt/image`. | Nuxt apps. | `@kungal/ui-nuxt` depends on `@kungal/ui-vue` + `@kungal/ui-tokens`, and `@kungal/ui-vue` depends on `@kungal/ui-core`, so you never install the lower layers by hand — but you **do** add `@kungal/ui-tokens` explicitly because you reference it from your CSS. --- ## 2. Requirements - **Node** ≥ 20 - **Tailwind CSS v4** — required. KunUI components are styled with Tailwind utility classes that **your** Tailwind build generates (see §5). v3 will not work. - **Vue** ≥ 3.5 (peer dependency) - **Nuxt** ≥ 4 — only for the `@kungal/ui-nuxt` layer. --- ## 3. Quick start — Nuxt (recommended) ### 3.1 Install ```bash pnpm add @kungal/ui-nuxt @kungal/ui-vue @kungal/ui-core @kungal/ui-tokens tailwindcss @tailwindcss/vite ``` > Why install `@kungal/ui-vue` and `@kungal/ui-core` explicitly, when > `@kungal/ui-nuxt` already depends on them? Because your **CSS** references > them directly (`@import '@kungal/ui-vue/style.css'` and the `@source` lines in > §3.3). Under pnpm's strict `node_modules`, only your *direct* dependencies are > resolvable by name — so list them. (With npm/yarn flat installs this is > optional, but listing them is always safe and explicit.) ### 3.2 `nuxt.config.ts` ```ts import tailwindcss from '@tailwindcss/vite' export default defineNuxtConfig({ // Pull in the whole KunUI layer: component auto-imports, composable // auto-imports, and NuxtLink / @nuxt/icon / @nuxt/image wiring. extends: ['@kungal/ui-nuxt'], // Your app owns its Tailwind entry stylesheet (see 3.3). css: ['~/assets/css/main.css'], vite: { plugins: [tailwindcss()], }, compatibilityDate: '2025-01-01', }) ``` ### 3.3 `app/assets/css/main.css` ```css @import 'tailwindcss'; @import '@kungal/ui-tokens'; /* theme tokens + opinionated base layer */ @import '@kungal/ui-vue/style.css'; /* scoped component styles (transitions, etc.) */ /* * Tailwind must scan the KunUI packages so it generates the utility classes * the components reference (e.g. `inline-flex`, `bg-primary`, `rounded-kun-md`). * Paths are relative to THIS file — adjust the depth so they resolve to wherever * node_modules holds the packages (3 levels up for app/assets/css/*). */ @source '../../../node_modules/@kungal/ui-vue'; @source '../../../node_modules/@kungal/ui-core'; ``` That's it. ``, ``, `useKunMessage()`, etc. are now available everywhere with no per-file imports. Links render as `NuxtLink`, icons resolve through the bundled registry (and fall back to `@nuxt/icon`), images use `@nuxt/image`. ### 3.4 Mount the overlay providers once Toasts, confirm dialogs and the mascot need their host components mounted once near the app root (e.g. in `app.vue`): ```vue ``` (Only mount the ones you use — see §8.) --- ## 4. Quick start — plain Vue (Vite) ### 4.1 Install ```bash pnpm add @kungal/ui-vue @kungal/ui-core @kungal/ui-tokens tailwindcss @tailwindcss/vite ``` (`@kungal/ui-core` is listed explicitly because your CSS `@source`s it — see the note in §3.1.) ### 4.2 `vite.config.ts` ```ts import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import tailwindcss from '@tailwindcss/vite' export default defineConfig({ plugins: [vue(), tailwindcss()], }) ``` ### 4.3 `src/assets/main.css` ```css @import 'tailwindcss'; @import '@kungal/ui-tokens'; @import '@kungal/ui-vue/style.css'; /* Relative to this file. For src/assets/main.css node_modules is 2 levels up. */ @source '../../node_modules/@kungal/ui-vue'; @source '../../node_modules/@kungal/ui-core'; ``` ### 4.4 `src/main.ts` ```ts import { createApp } from 'vue' import { KunUI } from '@kungal/ui-vue' import App from './App.vue' import './assets/main.css' createApp(App) .use(KunUI) // registers every component globally under its Kun* name .mount('#app') ``` `app.use(KunUI)` is the plain-Vue parity for the Nuxt auto-import DX. Prefer tree-shaking instead? Skip the plugin and import components directly: ```ts import { KunButton, KunModal } from '@kungal/ui-vue' ``` ### 4.5 Wire links/navigation for a router app (optional) By default KunUI renders links as native `` and navigates with `window.location.assign`. In a `vue-router` app, inject `RouterLink` and `router.push` once (see §7): ```ts import { createApp } from 'vue' import { createRouter, createWebHistory, RouterLink } from 'vue-router' import { KunUI, installKunUIConfig } from '@kungal/ui-vue' import App from './App.vue' import './assets/main.css' const app = createApp(App) const router = createRouter({ history: createWebHistory(), routes: [/* … */] }) app.use(router).use(KunUI) installKunUIConfig(app, { linkComponent: RouterLink, navigate: (href) => router.push(href), }) app.mount('#app') ``` --- ## 5. CSS & theming (the part that matters) KunUI deliberately does **not** ship a single all-in-one stylesheet. The split exists so you stay in control of your Tailwind build and bundle size. What you import, and why: | Line | Why it's required | | --- | --- | | `@import 'tailwindcss';` | Your app owns the Tailwind entry. KunUI's CSS never imports Tailwind for you. Must come **first**. | | `@import '@kungal/ui-tokens';` | Registers the design tokens via Tailwind v4 `@theme` (semantic colors with 50–950 scales, `rounded-kun-*`, `z-kun-*`, animation keyframes, the `dark` variant) plus an opinionated base layer (page background, font stack, `.scrollbar-hide`/`.scrollbar-visible`). Must come **after** Tailwind. | | `@import '@kungal/ui-vue/style.css';` | The components' **scoped** styles (modal/drawer/lightbox transitions, spoiler blur, Loli mascot, toast list animations). ~6 KB. Without it those effects silently break. | | `@source '…/@kungal/ui-vue';`
`@source '…/@kungal/ui-core';` | Tailwind v4 ignores `node_modules` by default. These tell it to scan the compiled KunUI packages so it generates the utility classes the components reference. Without them components render **unstyled**. | ### Tokens-only (no base layer) If you don't want KunUI touching global element styles (page background, font stack), import the tokens without the base layer: ```css @import 'tailwindcss'; @import '@kungal/ui-tokens/css'; /* tokens only — no html/body/base resets */ @import '@kungal/ui-vue/style.css'; ``` Subpaths: `@kungal/ui-tokens` (tokens **+** base), `@kungal/ui-tokens/css` (tokens only), `@kungal/ui-tokens/base.css` (base only). ### Dark mode KunUI's `dark:` variant is wired to a class on a **parent** element: `@custom-variant dark (&:is(.kun-dark-mode *))`. Toggle the `kun-dark-mode` class on ``: ```ts document.documentElement.classList.toggle('kun-dark-mode', isDark) ``` Using `@nuxtjs/color-mode`? Point its `classSuffix`/class at `kun-dark-mode`, or mirror its state onto that class. ### Overridable knobs `@kungal/ui-tokens` exposes two `:root` custom properties you may override: ```css :root { --kun-global-opacity: 0.7; /* alpha of "glassy" surfaces (Card/Modal/topbar) */ --kun-background-blur: 0px; /* backdrop blur strength for those surfaces */ } ``` ### `--kun-scrollbar-width` — compensating your own fixed elements When a Modal / Drawer / Lightbox / CommandPalette opens, KunUI locks body scroll, which hides the page scrollbar. In-flow content is held still for you (KunUI pads `` by exactly the width that disappeared), but padding cannot reach a `position: fixed` element: fixed positioning resolves against the initial containing block, and *that* grows by the scrollbar width. So a fixed header, FAB or side rail of yours will slide sideways by ~15px unless you compensate it. KunUI publishes the width it removed on `` while a lock is active. It is `0px` at rest, and `0px` whenever nothing was actually removed — overlay scrollbars (macOS, iOS, most mobile), or a page that reserves its own `scrollbar-gutter: stable` — so one declaration is correct everywhere: ```css /* anchored to the right edge — take back all of it */ .my-fixed-toolbar { right: 1rem; margin-right: var(--kun-scrollbar-width, 0px); } /* centred against the viewport — take back half */ .my-fixed-centred { margin-left: calc(var(--kun-scrollbar-width, 0px) / -2); } ``` Two rules for using it. **Keep the `0px` fallback** — the variable is only guaranteed if you import `@kungal/ui-tokens`. And **compensate with a margin rather than folding it into `left`/`right`/`top`**: a declaration containing `var()` is only checked after substitution, so if the variable ever holds something that isn't a length the whole declaration becomes invalid at computed-value time — a bad `margin` falls back to `0` and you merely lose the compensation, a bad `left` falls back to `auto` and the element jumps to its static position. A full-bleed fixed element (`inset: 0`, or `left: 0; right: 0`) needs nothing for its own box — it simply spans the wider one. Its **children** may still need it: anything right-aligned or centred inside that box moves with the box's new width. KunUI's own toasts already compensate themselves. If you set `scrollbar-gutter: stable` on `` yourself, you are already immune and KunUI stays out of the way — it detects the reserved gutter and adds no padding at all. KunUI deliberately does not set `scrollbar-gutter` for you: a reserved gutter is outside the initial containing block, so every full-bleed backdrop would stop ~15px short of the screen edge with no way to paint the remainder. ### Scroll lock — known limitations - **A scroll container of your own is not locked.** The lock covers the viewport — `` and `` are both locked, so `html { overflow-y: scroll }` and `html { overflow-x: hidden }` are fine (they were a silent no-op before 2.31.0). An app that scrolls inside an element of its own (`#app { overflow: auto }` under a full-height html/body) is a different shape and KunUI cannot find that element: lock it yourself while an overlay is open. - **The width is measured once, when the overlay opens.** Zooming or resizing while it is open leaves both the body padding and `--kun-scrollbar-width` stale until the next open. - **Nothing can be opened on top of an open `KunLightbox`.** It is the one component that uses the native top layer (`.showModal()`), which makes everything outside its own subtree inert — including any KunUI overlay, which all teleport to ``. A Modal or CommandPalette opened over a Lightbox renders but cannot take focus or be dismissed with Escape. In development KunUI prints a named `console.warn` when you do it, instead of leaving you with a page that just looks frozen. ### Troubleshooting CSS - **Components look unstyled / layout is broken** → a `@source` path isn't resolving to `node_modules`. Fix the relative depth (it's relative to the CSS file), or point it at wherever your package manager places `@kungal/ui-vue` (in a pnpm monorepo deps may resolve at the workspace root). - **Transitions / spoilers / mascot missing** → you forgot `@import '@kungal/ui-vue/style.css';`. - **Dark mode doesn't switch** → ensure `@kungal/ui-tokens` is imported *after* `tailwindcss`, and that you toggle `.kun-dark-mode` on ``. --- ## 6. Icons — bundled, never fetched KunUI **never fetches icons from the Iconify API at runtime.** `` renders inline SVG from an in-memory registry that is seeded with the icons the components need. This is intentional: runtime icon fetches are fragile under SSR and add network dependencies. ```vue ``` ### Register your own icons Add icons to the registry once at startup (any Iconify-style `{ body }` payload, or your own SVG inner markup using `currentColor`): ```ts import { registerKunIcon, registerKunIcons } from '@kungal/ui-core' registerKunIcon('lucide:rocket', { body: '', width: 24, height: 24, }) // or in bulk (e.g. icons extracted from @iconify-json/* at build time): registerKunIcons({ 'lucide:star': { body: '' }, 'lucide:heart': { body: '' }, }) ``` In Nuxt, do this in a plugin (`app/plugins/kun-icons.ts`); in plain Vue, before `mount()`. ### Unknown icons If `` is asked for a name not in the registry, it renders the `iconComponent` fallback (see §7) — `null` by default in plain Vue (renders nothing). The Nuxt layer sets the fallback to an `@nuxt/icon` wrapper, so in Nuxt unknown names still resolve through `@nuxt/icon`. API: `getKunIcon(name)`, `hasKunIcon(name)`, `registerKunIcon(s)` — all from `@kungal/ui-core`. --- ## 7. Configuration — `KunUIConfig` (the decoupling slots) KunUI stays framework-agnostic by reading a small injectable config. Precedence for any component: ``` per-instance prop > KunUIConfig provider > built-in default ``` | Slot | Type | Default | Purpose | | --- | --- | --- | --- | | `rounded` | `KunUIRounded` | `'md'` | Default corner radius for components that don't pass `rounded`. | | `linkComponent` | `Component \| string` | `'a'` | What renders for navigational elements with an `href` (Button, Card…). Inject `RouterLink` / `NuxtLink`. | | `navigate` | `(href: string) => unknown` | `window.location.assign` | Imperative navigation (e.g. Tab with an `href`). Inject `router.push` / `navigateTo`. | | `imageComponent` | `Component \| string` | `'img'` | What `KunImage` renders. The Nuxt layer injects ``. | | `iconComponent` | `Component \| string \| null` | `null` | Fallback renderer for icons not in the registry. The Nuxt layer injects an `@nuxt/icon` wrapper. | | `userLinkTemplate` | `string` | `'/user/{id}/info'` | Path `KunAvatar` navigates to; `{id}` is replaced. | | `avatarFallbackPool` | `string[]` | `[]` | Images `KunAvatar` picks from for a user with no avatar. Empty → the bundled `KUN_AVATAR_FALLBACK` data URI for *every* such user (dev warning). See §7.1. | | `locale` | `KunLocale` | `zh-CN` | The strings KunUI renders on its own — accessible names for icon-only controls, empty/loading states, built-in button labels. See §7.2. | **In Nuxt** the first four are set for you by `@kungal/ui-nuxt` — you usually don't touch those. `installKunUIConfig` **merges** over whatever a previous call installed, so your own plugin can set `avatarFallbackPool` without resetting the layer's `linkComponent` / `imageComponent`, in either order. **In plain Vue** set them with either: - `installKunUIConfig(app, partialConfig)` — from `main.ts` (has the `app`, no active `setup()`), or - `provideKunUIConfig(partialConfig)` — inside a component `setup()` (e.g. your root component), scopes config to that subtree. ```ts import { provideKunUIConfig } from '@kungal/ui-vue' provideKunUIConfig({ rounded: 'lg', userLinkTemplate: '/u/{id}', }) ``` `useKunUIConfig()` reads the resolved config if you're building wrappers. ### 7.1 The avatar fallback pool Most accounts in a community site have no avatar image, so whatever renders in their place is, in practice, the site's default avatar — and one of the most requested images it serves. `avatarFallbackPool` is that set. Rules that matter more than they look: - **Supply absolute, immutable URLs.** KunUI never assembles one from a template. The predecessor of this API hardcoded a host and a `/stickers/{pack}/{n}.webp` path — an address for a *position in a mutable collection* — and every avatar-less account across six sites 404'd the day that collection moved. Content-addressed URLs (`…/_128.webp`) cannot rot that way and can be cached `immutable` for a year. - **The pool is an index space, not a ranking.** The pick is `hash(user.name) % pool.length`, so replacing one entry moves only the users who land on it, while changing the array's *length* re-assigns everybody. Edit slots; avoid resizing. - **Keep it small and fetch it server-side.** A pool of 64 at ~5 KB each is ~340 KB — the whole set a browser can ever download, once, if the images are served `immutable`. Fetch the list on your server (once an hour is plenty), never per render and never from the browser. - **An empty pool is not an error, it is a uniform site.** The default is `[]`, and `pickAvatarFallback` reads that as "no choice to make": it returns the single bundled `KUN_AVATAR_FALLBACK` data URI for every seed. Nothing throws and nothing 404s — every avatar-less user just renders the same picture. The KunUI docs site shipped exactly that for a release. Since 2.32.1 KunAvatar logs a dev-only warning when it happens; in production it stays silent, so do not rely on noticing it there. In the NextMoe ecosystem the list comes from `https://sticker.kungal.com/api/v1/avatar-pool`, which returns ready `_128` CDN URLs. **The payload is wrapped** — assigning the response itself to `avatarFallbackPool` hands KunUI an object where an array belongs, which lands you right back on the one repeated image: ```jsonc { "code": 0, "data": { "version": "cd41af02cb0050ea", // changes when the pool changes "variant": "128", "urls": ["https://image.kungal.iloveren.link/c6/97/c697…_128.webp", "…"] // 64 of them } } ``` Two files, and this is the whole recipe for an **SSR** app: ```ts // app/utils/avatarPool.ts interface KunAvatarPoolResponse { code: number data: { version: string; variant: string; urls: string[] } | null } // Baked snapshot, so a failed or offline fetch degrades to a STALE POOL rather // than to one repeated image. Keep it the same length as the live pool: the // pick is `hash(name) % pool.length`, so a shorter fallback re-assigns everyone. export const AVATAR_POOL_FALLBACK: string[] = [ 'https://image.kungal.iloveren.link/c6/97/c697…_128.webp', /* … the rest of the pool … */ ] // One fetch per process, NOT per render. Module scope is safe here precisely // because the list is immutable and not user-specific — module-scoped state that // IS user-visible is shared across SSR requests and will leak between users. let poolPromise: Promise | null = null export const fetchAvatarPool = (): Promise => (poolPromise ??= (async () => { try { const res = await $fetch( 'https://sticker.kungal.com/api/v1/avatar-pool', { timeout: 5000, retry: 1 } ) const urls = res?.data?.urls return Array.isArray(urls) && urls.length > 0 ? urls : AVATAR_POOL_FALLBACK } catch { // Never fail a build or a render over the default avatar. return AVATAR_POOL_FALLBACK } })()) ``` ```ts // app/plugins/kun-avatar-pool.ts import { installKunUIConfig } from '@kungal/ui-vue' export default defineNuxtPlugin(async (nuxtApp) => { const pool = useState('kun-avatar-pool', () => AVATAR_POOL_FALLBACK) if (import.meta.server) pool.value = await fetchAvatarPool() installKunUIConfig(nuxtApp.vueApp, { avatarFallbackPool: pool.value }) }) ``` `useState` is what keeps server and client picking identically: the list is serialized into the payload, so hydration sees the same array — and therefore the same `hash(name) % pool.length` — as the server did. Refetching on the client instead could return a pool of a different length and reshuffle every avatar on hydration. `installKunUIConfig` merges over whatever a previous call installed (§7), so this plugin and the `@kungal/ui-nuxt` layer's own config plugin can both run in either order. **A fully prerendered site should bundle the list instead.** A fetched value can reach the browser only through the payload, and since the config is installed app-wide, that payload rides on every page — on this repo's docs site the `/colors` page carried all 64 URLs without rendering a single avatar. Both versions were built and measured: fetch + `useState` put `/components/avatar` at 80797 B raw / 11922 B gzip, a bundled constant at 71494 B / 8567 B. A bundled constant costs nothing extra, because server and client already both have it and pick identically with no transfer: ```ts // app/plugins/kun-avatar-pool.ts — prerendered site export default defineNuxtPlugin((nuxtApp) => { installKunUIConfig(nuxtApp.vueApp, { avatarFallbackPool: KUN_AVATAR_POOL }) }) ``` That is what `apps/docs` in this repo does (`app/utils/avatarPool.ts`), and it is only safe because the URLs are content-addressed: a bundled snapshot of `_128.webp` cannot rot, so it never needs refreshing for correctness. --- ### 7.2 Locale Every string KunUI renders by itself lives in one `KunLocale` object. The built-in default is **zh-CN**, so an app that never touches this behaves exactly as it always has. ```ts import { installKunUIConfig } from '@kungal/ui-vue' import en from '@kungal/ui-vue/locale/en' installKunUIConfig(app, { locale: en }) ``` Only the locale you import is bundled — `zh-CN` ships inside the main entry, every other locale is its own entry point. The strings themselves live in `@kungal/ui-core` (`KUN_CATALOG_ZH_CN`, `@kungal/ui-core/locale/en`), which is what any non-Vue layer reads and what the `kun_ui_messages` pub package is generated from; `@kungal/ui-vue` re-exports everything you need, so a Vue app never has to import `@kungal/ui-core` for this. A per-instance prop still wins, so a single component can override one string without a custom locale: ```vue ``` Write your own by filling in `KunMessages` — `defineKunLocale` only gives you the type-check, so a missing key is a compile error rather than a `{key}` leaking into an aria-label: ```ts import { defineKunLocale } from '@kungal/ui-vue' export default defineKunLocale({ name: 'Nederlands', code: 'nl', messages: { modal: { close: 'Sluiten' }, /* …every other namespace… */ }, }) ``` Values interpolate with `{name}` placeholders, and the whole string is one template on purpose: a placeholder's position moves between languages (`Decrease {field}` vs `{field} verringern`), so it can never be assembled from fragments at the call site. **The calendar grid is dates, not strings.** `KunDatePicker`'s weekday and month names, and the full date each day cell announces, come from a date-fns locale, so `KunLocale` carries one alongside `messages`: ```ts import { enUS } from 'date-fns/locale' defineKunLocale({ name: 'English', code: 'en', dateLocale: enUS, messages: { … } }) ``` Omit it and KunUI falls back to `code` (it bundles `zh-CN` / `ja` / `en`), then to `en-US`. `KunDatePicker`'s own `locale` prop still wins over both, and changes only the grid — the buttons around it keep following `messages`. **SSR.** The locale rides on the config, which is provided per app — and so per request. It is deliberately *not* a module-level singleton: one would leak the language of whichever request rendered last into another request's HTML. Like the rest of `KunUIConfig` it is read once where it is used, so switching language at runtime means re-providing the config and re-keying the subtree. **Two escape hatches for code that runs outside `setup()`.** `useKunAlert` is a module-level store with no component to inject from, so leave `confirmText` / `cancelText` unset and `KunAlertProvider` resolves them against the active locale. `useKunCopy` runs from an event handler, so it takes its two toast strings as an argument — `KunCopy` passes the resolved ones; pass your own if you call it directly and are not on zh-CN. ## 8. Toasts, confirm dialogs & mascot These are **store + provider** systems: mount the provider once, trigger from anywhere (event handlers, stores, composables — no component instance needed). | Trigger | Provider to mount | Signature | | --- | --- | --- | | `useKunMessage()` | `` | `(message: string, type: 'warn'\|'success'\|'error'\|'info', duration = 3000, richText = false, position = 'top-center') => string` | | `useKunAlert()` | `` | `(opts?: { title?; message?; … }) => Promise` (resolves true on confirm) | | `useKunLoliInfo()` | `` | mascot popup (message, duration) | ```ts import { useKunMessage, useKunAlert } from '@kungal/ui-vue' // auto-imported in Nuxt useKunMessage('Saved!', 'success') const ok = await useKunAlert({ title: 'Delete?', message: 'This cannot be undone.' }) if (ok) { /* … */ } ``` > ⚠️ `useKunMessage(..., richText = true)` renders the message as **raw HTML**. > See §9 — sanitize first. --- ## 9. Security: KunUI never sanitizes HTML **This is a deliberate, load-bearing design decision. Read it.** KunUI does **not** sanitize any HTML, anywhere. Components that render raw HTML render *exactly* what you give them: - **``** — renders `html` via `v-html`. - **`useKunMessage(msg, type, dur, richText = true)`** — renders `msg` via `v-html`. If that HTML can contain anything user-controlled, **you must sanitize it yourself, in your app, before passing it in.** KunUI intentionally ships no sanitizer: a library-side one would have to run in the SSR render, where DOMPurify needs a server DOM (jsdom) that leaks memory badly under sustained load. Sanitization is the consuming app's responsibility — ideally server-side at write time, so you persist already-safe HTML and the render never sees raw input. ```ts import DOMPurify from 'dompurify' const safe = DOMPurify.sanitize(userProvidedHtml) ``` ```vue ``` If your content is fully trusted (authored by you, not user input), you can pass it directly — but make that an explicit decision, not an accident. --- ## 10. User model Components that show a user (`KunAvatar`, `KunAvatarGroup`, `KunUserChip`) accept a minimal `KunUser`: ```ts import type { KunUser } from '@kungal/ui-vue' // re-exported from @kungal/ui-core interface KunUser { id: number name: string avatar: string // URL; empty falls back to a deterministic pick from // config.avatarFallbackPool (§7.1) } ``` Your richer app `User` type is structurally compatible as long as it has these three fields. `KunAvatar` navigates to `userLinkTemplate` (§7) on click. --- ## 11. SSR notes - All components are SSR-safe and prerender cleanly. Because icons are bundled (§6), SSR never makes a network request to render an icon. - Mount the overlay providers (§8) at the app root; they are client-driven but SSR-safe to render. - Do sanitization (§9) on the consumer side, and prefer doing it **server-side at write time** (persist already-safe HTML) so the SSR render never touches raw HTML. Do **not** run DOMPurify with a server DOM (`jsdom`/`isomorphic-dompurify`) in the SSR render path — it leaks memory badly under sustained SSR load. --- ## 12. Component reference (88) **Layout & display:** `KunCard` · `KunDivider` · `KunHeader` · `KunInfo` · `KunBanner` · `KunProgress` · `KunBadge` · `KunChip` · `KunKbd` · `KunScrollShadow` · `KunSplitPane` · `KunFadeCard` · `KunNull` · `KunAccordion` · `KunAccordionItem` · `KunSkeleton` · `KunSteps` · `KunTimeline` · `KunTimelineItem` **Buttons & feedback:** `KunButton` · `KunButtonGroup` · `KunRipple` · `KunLoading` · `KunCopy` · `KunRating` · `KunReaction` · `KunShatter` **Media & content:** `KunImage` · `KunCarousel` · `KunCarouselItem` · `KunImageNative` · `KunIcon` · `KunBrand` · `KunContent` · `KunText` · `KunMarkdown` **Navigation & overlays:** `KunLink` · `KunNavItem` · `KunTab` · `KunTabPanel` · `KunTabPanels` · `KunModal` · `KunDrawer` · `KunTooltip` · `KunPopover` · `KunHoverCard` · `KunDropdown` · `KunContextMenu` · `KunCommandPalette` · `KunCommandSearch` · `KunPagination` · `KunLightbox` · `KunLightboxGallery` · `KunLightboxGalleryItem` **Chat:** `KunChatLayout` · `KunChatMessageList` · `KunChatBubble` · `KunChatText` · `KunChatComposer` · `KunChatConversationItem` · `KunChatHeader` · `KunChatPinnedBar` · `KunChatRequestBar` · `KunChatMessageMenu` · `KunChatReactionPicker` · `KunChatTyping` **Forms:** `KunInput` · `KunTextarea` · `KunNumberInput` · `KunPinInput` · `KunSelect` · `KunAutocomplete` · `KunSwitch` · `KunCheckBox` · `KunCheckBoxGroup` · `KunSlider` · `KunRadioGroup` · `KunDatePicker` · `KunFileInput` · `KunTagInput` · `KunUpload` **People:** `KunAvatar` · `KunAvatarGroup` · `KunUserChip` **Overlay providers (mount once near app root):** `KunMessageProvider` · `KunAlertProvider` · `KunLoliProvider` · `KunLoli` In Nuxt all are auto-imported. In plain Vue they're named exports of `@kungal/ui-vue` (or registered globally via `app.use(KunUI)`). --- ## 13. Composable reference Auto-imported by the Nuxt layer; named exports of `@kungal/ui-vue` otherwise. | Composable | Purpose | | --- | --- | | `useKunMessage` / `useKunMessageState` | Trigger / read toasts | | `useKunAlert` / `useKunAlertState` | Trigger / read confirm dialogs | | `useKunLoliInfo` / `useKunLoliState` | Trigger / read the mascot popup | | `useKunUIConfig` | Read the resolved `KunUIConfig` | | `provideKunUIConfig` / `installKunUIConfig` | Set `KunUIConfig` | | `useKunLocale` | Resolve KunUI's own strings — `{ locale, t }` (§7.2) | | `useKunCopy` | Copy-to-clipboard helper | | `useKunUniqueId` | SSR-safe unique id generation | | `useResolvedRounded` | Resolve a component's effective radius | | `useRipple` | Ripple effect directive/logic | | `useFilePicker` | Programmatic file selection | | `useContentLightbox` / `useSpoilerContent` | Enhancers used by `KunContent` | From `@kungal/ui-core` (framework-agnostic): `cn`, `registerKunIcon(s)`, `getKunIcon`, `hasKunIcon`, the variant class helpers (`kunBgClasses`, …), and the shared types (`KunUIColor`, `KunUISize`, `KunUIVariant`, `KunUIRounded`, `KunUser`). --- ## 14. Versioning The four packages are released together under one version (a Changesets `fixed` group). Always install matching versions, and treat the set as one unit when upgrading. Release process: see [`RELEASING.md`](./RELEASING.md). --- # Component reference ## Layout & display - **KunCard** — Container/panel with color + variant; renders as a link when given href/to. - **KunDivider** — Horizontal/vertical separator with an optional centered label. - **KunHeader** — Section heading (scale h1–h6) with brand styling. - **KunInfo** — Inline callout box (color, icon, title, description; solid/flat/bordered). Rich body content goes in the default slot — there is no `description` slot; the heading has a `title` slot. - **KunBanner** — Full-width one-sentence announcement strip for the top of a page, in normal flow above the sticky header (text or default slot, icon, `solid`/`flat`, `closable`; the `#actions` slot gets `{ dismiss }`). `size="sm"` is a 32px `text-xs` strip for a standing, non-dismissable notice that would otherwise wrap tall on a phone. Bind `v-model`, not `v-if`, so it can collapse on close. `storageKey` keeps a closed banner closed across reloads (localStorage, hidden before first paint under SSR by an inline script; pass `nonce` under a strict CSP); use a new key per announcement. - **KunProgress** — Progress bar: linear/striped/gradient/circle, indeterminate, optional label. - **KunBadge** — Count/dot badge wrapping a trigger (e.g. a button/icon); `max` caps the count. - **KunChip** — Small tag/pill with color + variant. - **KunKbd** — Keyboard shortcut badge: `keys="Mod+K"` renders ⌘K on Apple and Ctrl K elsewhere (server renders Ctrl, a Mac switches after mount); screen readers hear key names. `variant="plain"` is the one-line menu form (⇧⌘Z / Ctrl+Shift+Z). - **KunScrollShadow** — Horizontal scroll container with edge fade shadows. - **KunSplitPane** — Two panes with a draggable, keyboard-adjustable divider (#start / #end slots). `v-model:size` is the primary pane width in px (server-rendered); `resize-end` is the event to persist; `min-size` / `max-size`, `snap-points` (pulled within `snap-threshold`), `primary="end"` for an auxiliary right column. Stacks to one pane (`show-pane`) below `stack-below` of its OWN width (container query, default md = 48rem). - **KunFadeCard** — Fade + expand transition wrapper for conditional content. - **KunNull** — Empty-state placeholder with a bundled mascot image + caption (`description`, `src`, `isShowSticker`). - **KunAccordion** — Collapsible sections (v-model string | string[]; `multiple`, variants light/bordered/splitted). Wraps KunAccordionItem. - **KunAccordionItem** — One accordion section: `value` (required), `title`/`icon` or the `title` slot, `disabled`. - **KunSkeleton** — Loading placeholder (variant text/circle/rect, width/height); set `loaded` to swap in the real content. - **KunSteps** — Step / progress indicator (items array, `current` index; horizontal or vertical). Bind `v-model:current` to make steps clickable (`linear` keeps clicks backward-only); per-item `disabled` and `status: 'error'`. - **KunTimeline** — Vertical timeline container for KunTimelineItem. - **KunTimelineItem** — One timeline entry (title, time, icon, color + default slot for the body). ## Buttons & feedback - **KunButton** — Button: color/variant/size, loading state; renders as a link with href/to. - **KunButtonGroup** — Joins adjacent KunButtons into one segmented control (horizontal/vertical; merges the touching radii). - **KunRipple** — Material-style ripple effect. - **KunLoading** — Loading state: overlay over wrapped content (default slot) or standalone; bundled mascot image (`src`, `loading`, `description`). - **KunCopy** — Copy-to-clipboard button with feedback. - **KunRating** — Star rating input (v-model number). - **KunReaction** — Like / reaction toggle (v-model boolean) with an animated icon and an optional `count`. - **KunShatter** — Breaks its slot content into flying glass shards on click, and can fly them back (`pieces`, `gravity`, `reassemble`, `autoRestore`). Compositor-only (transform + opacity), zero deps. ## Media & content - **KunImage** — Image with skeleton/aspect/objectFit; uses the injected image component (NuxtImg in Nuxt). - **KunCarousel** — Swipeable carousel (arrows, indicators, `loop`, `autoplay` ms, `slidesPerView`). Wraps KunCarouselItem. - **KunCarouselItem** — One carousel slide. - **KunImageNative** — Bare with class merging (no optimization pipeline). - **KunIcon** — Inline SVG icon from the bundled registry; never fetches. `name` like `lucide:x`. Inherits text color. - **KunBrand** — Logo + name brand block, links home; optional badge. - **KunContent** — Renders HTML (`content`) via v-html with spoiler + inline-image lightbox. DOES NOT sanitize — caller must. - **KunText** — Safe-wrapping text block (handles long URLs/underscores). - **KunMarkdown** — The KunUI markdown glyph. ## Navigation & overlays - **KunLink** — Styled link; uses the injected link component (NuxtLink in Nuxt). - **KunNavItem** — One destination of an app shell's navigation: a full-width KunButton, `flat` in `color` and `aria-current="page"` while `current`, `light` otherwise. `stacked` puts the icon over the label (rail, bottom bar); the `#icon` slot takes a KunBadge-wrapped icon for an unread count. - **KunTab** — Tab bar (v-model value, items array; variants, orientation). Items with `href` render real
for crawlable tab-as-route. - **KunTabPanel** — One tab's content. SEO-first: mount=eager (default) SSRs every panel and hides inactive ones with hidden="until-found" (indexable + findable); mount=lazy/unmount opt out. Pairs with KunTab via `value`/`name`. - **KunTabPanels** — Optional wrapper sharing active (v-model) + mount/hiddenStrategy defaults across KunTabPanels, and auto-wiring find-in-page reveal. - **KunModal** — Dialog teleported to body, focus-trapped, body-scroll-locked, Escape to close (v-model boolean). - **KunDrawer** — Slide-in panel from any edge (placement, size; v-model boolean). - **KunTooltip** — Hover/focus tooltip (text + trigger slot); `shortcut="Mod+K"` appends KunKbd keys. - **KunPopover** — Floating popover anchored to a trigger slot. - **KunHoverCard** — Preview card for a link (a user card on an avatar): opens on mouse hover after `openDelay` or on keyboard `:focus-visible`, never on touch; a click is never intercepted — it follows the link and closes the card. The trigger wrapper is `display: contents`, so it does not change the trigger's layout; the card anchors to the trigger slot's first element. No role and no focus move: the link stays the accessible interface, so put nothing in the card that the destination page lacks. The card mounts only while open (fetch in its setup) and its default slot gets `{ close }`; `v-model:open` tracks it. `group` = one open at a time across a list, siblings switch instantly. `disabled` renders the trigger alone. - **KunDropdown** — Menu of items anchored to a trigger (items array: `{ type: 'separator' }` rows, per-item `shortcut`, and `children` for one level of submenu). - **KunContextMenu** — Right-click context menu (same items array as KunDropdown: separators, `shortcut`, one level of `children`). - **KunCommandPalette** — Command-palette SHELL (v-model:open + ⌘K/Ctrl-K, v-model:query): dialog, keyboard nav, grouped rendering, match highlighting, a11y. It does NOT search — YOU compute `items` (flat, or KunCommandGroup[]) from `query`; picking one emits @select. - **KunCommandSearch** — Top-bar search FIELD (v-model:query): always visible, opens a panel of the same KunCommandItem rows under it on focus (recent searches when empty, grouped results while typing). Nothing is highlighted by default: Enter emits @submit(query) for the full search page; arrow onto a row + Enter (or click) emits @select. `removable` rows emit @remove. No dialog, no focus trap. Below md pair it with KunCommandPalette via breakpoint classes. It does NOT search — YOU compute `items` from `query`. - **KunPagination** — Page navigation (v-model:current-page, total-page) with quick-jump. - **KunLightbox** — Image lightbox/viewer (v-model:is-open, images array). - **KunLightboxGallery** — Gallery wrapper; click an item to open the lightbox. - **KunLightboxGalleryItem** — A single gallery item (src + slot for the thumbnail). ## Chat - **KunChatLayout** — Responsive chat shell: conversation list and conversation side by side once the layout ITSELF is 48rem wide (container query, not the window), one pane at a time below (`show-conversation`). `resizable` + `v-model:sidebar-size` (px) make the list draggable via KunSplitPane. Slots #sidebar / default / #empty. - **KunChatMessageList** — Conversation scrollback for NextMoe `/v2/chat` messages (string ids): sender runs, sticky day pills, unread divider (opens there), older/newer paging without jumps, stick-to-bottom, scroll-down button with unread count, `scrollToSeq(seq)` + highlight, right-click / long-press menu, swipe-left to reply. Emits load-older, load-newer, read, jump, action, react, retry. - **KunChatBubble** — One message: own (right, primary tint) or theirs; reply / quote preview, photo or Telegram-mosaic album (lightbox), context card, reactions, time / edited / status (failed → `retry`). A service message renders as a centred pill. Photos show `media.url` unless `resolve-media-url` is passed. - **KunChatText** — Renders message `text` + `entities` (UTF-16 ranges): bold, italic, underline, strike, spoiler (click reveals all), code, pre (language + copy), blockquote, links, mentions. No v-html. `preview` = one-line form with masked spoilers. - **KunChatComposer** — Message input: auto-grow, Enter sends (touch: newline), composer markdown → `send({ text, entities })`, v-model:reply-to / quote / editing bars, attachments (paste / drop / pick → `attach(files)`), 4096 counter, throttled `typing`. - **KunChatConversationItem** — Conversation-list row: avatar, title, time + status, preview (typing › draft › last message), unread / @ badges, muted, pinned, selected; touch swipe actions. - **KunChatHeader** — Conversation top bar: back (mobile), avatar, title, subtitle that becomes "typing…"; #actions slot. - **KunChatPinnedBar** — Pinned-message bar: newest first, each click emits `jump(seq)` and steps to the next older one. - **KunChatRequestBar** — Message-request bar: accept / delete / block / report, with a per-action loading state. - **KunChatMessageMenu** — Message context menu at a point: quick-reaction row (expands to the full picker) + site-chosen actions (reply, quote, copy, edit, pin, unpin, delete, report, retry, or custom items). - **KunChatReactionPicker** — Reaction vocabulary grid (v-model key | null); animated `image_url` art or native emoji; one reaction per person. - **KunChatTyping** — Three-dot typing indicator + "X is typing…"; takes raw typing events and expires each after 6 s (useKunChatTyping). ## Forms - **KunInput** — Text input (v-model) with label/helper/error, sizes, variants. - **KunTextarea** — Multiline input (v-model) with optional counter. - **KunNumberInput** — Numeric input (v-model number|null) with steppers, min/max/step, precision, clamping. - **KunPinInput** — OTP / PIN segmented input (v-model string): per-cell entry, paste, masking, complete event. - **KunSelect** — Select dropdown (v-model, options array); single/multiple, searchable, clearable. Remote sources via manualFilter + @search + loading + debounce; maxVisibleTags collapses chips; fullWidth/icon/popupWidth/classNames shape it into a filter pill. - **KunAutocomplete** — Combobox: a text field (v-model string) with a filtered suggestion list; allowCustomValue / manualFilter. - **KunSwitch** — Toggle switch (v-model boolean). - **KunCheckBox** — Checkbox (v-model boolean). - **KunCheckBoxGroup** — Multi-select checkbox group (v-model array, options array; `max`, orientation, label/error). - **KunSlider** — Range slider (v-model number; min/max/step). - **KunRadioGroup** — Radio group (v-model, options array; card/classic variants). - **KunDatePicker** — Date / date-range picker (date-fns based; v-model). `precision` picks a day, month or year (value 'yyyy-MM-dd' / 'yyyy-MM' / 'yyyy'), orthogonal to `mode`; the panel header zooms out day → month → year. - **KunFileInput** — File picker button (v-model File | File[]); default trigger icon is bundled. - **KunTagInput** — Tag entry input (v-model string[]; split chars, validation, max). - **KunUpload** — Drag/drop image upload with crop (size, aspect). ## People - **KunAvatar** — User avatar (KunUser); navigates to userLinkTemplate on click; sticker fallback. - **KunAvatarGroup** — Stacked avatars with +N overflow. - **KunUserChip** — Avatar + name + description chip. ## Overlay providers (mount once near app root) - **KunMessageProvider** — Renders toasts triggered by useKunMessage(). Mount once. - **KunAlertProvider** — Renders confirm dialogs triggered by useKunAlert(). Mount once. - **KunLoliProvider** — Renders the mascot popup triggered by useKunLoliInfo(). Mount once. - **KunLoli** — The standalone mascot popup component. # Composable reference - **useKunMessage / useKunMessageState** — Trigger / read toasts. useKunMessage(message, type, duration=3000, richText=false, position="top-center"). - **useKunAlert / useKunAlertState** — Trigger / read confirm dialogs. useKunAlert(opts?) returns Promise. - **useKunLoliInfo / useKunLoliState** — Trigger / read the mascot popup. - **useKunUIConfig / provideKunUIConfig / installKunUIConfig** — Read/set KunUIConfig (rounded, linkComponent, navigate, imageComponent, iconComponent, userLinkTemplate). - **useKunCopy** — Copy-to-clipboard helper. - **useKunUniqueId** — SSR-safe unique id generation. - **useResolvedRounded** — Resolve a component's effective radius. - **useRipple** — Ripple effect logic. - **useFilePicker** — Programmatic file selection. - **useContentLightbox / useSpoilerContent** — Enhancers used by KunContent. From @kungal/ui-core (framework-agnostic): cn, registerKunIcon, registerKunIcons, getKunIcon, hasKunIcon, the variant class helpers (kunBgClasses, kunTextClasses, kunRingClasses, …), and types KunUIColor, KunUISize, KunUIVariant, KunUIRounded, KunUser.