@uniweb/kit
v1.7.21
Published
Standard component library for Uniweb foundations
Downloads
9,193
Readme
@uniweb/kit
Standard component library for Uniweb foundations. Tree-shakeable utilities, components, and hooks for building foundation components.
Installation
npm install @uniweb/kitTree-Shaking Benefits
Kit is designed to be bundled into your foundation (not externalized like @uniweb/core). This means:
- Only what you use is bundled — Import 3 components? Only those 3 end up in your foundation
- No runtime overhead — Unused code is eliminated at build time
- Customizable — Override or extend any component without carrying dead code
- Small foundations — A minimal foundation using just
LinkanduseWebsitestays tiny
// vite.config.js - Kit is bundled, core is external
export default {
build: {
rollupOptions: {
external: ['react', 'react-dom', 'react-router-dom', '@uniweb/core']
// Note: @uniweb/kit is NOT in external — it gets tree-shaken
}
}
}Quick Start
import { Link, Image, useWebsite } from '@uniweb/kit'
function Hero({ content }) {
const { localize } = useWebsite()
return (
<div>
<Image src={content.images[0]?.url} alt="Hero" />
<h1>{localize({ en: 'Welcome', es: 'Bienvenido' })}</h1>
<Link to="/about">Learn More</Link>
</div>
)
}Components
Link
Smart link component with routing and downloads. A link that leaves the site, downloads a file, writes an email or places a call gets a tooltip saying so; an internal link gets none.
import { Link } from '@uniweb/kit'
<Link to="/about">About</Link>
<Link href="https://github.com">GitHub</Link>
<Link href="/files/report.pdf">Download Report</Link>
<Link href="mailto:[email protected]">Contact</Link>| Prop | Type | Description |
|------|------|-------------|
| to / href | string | Destination URL |
| title | string | Tooltip. If omitted, one is generated only where the link leaves the site, downloads, writes an email or places a call |
| target | string | Link target |
| download | boolean | Force download behavior |
Image
Versatile image component with filters and size presets.
import { Image } from '@uniweb/kit'
<Image src="/photo.jpg" alt="Photo" />
<Image src="/photo.jpg" filter={{ grayscale: 100 }} />
<Image src="/team/ada.jpg" alt="Ada" size="lg" rounded />| Prop | Type | Description |
|------|------|-------------|
| src / url | string | Image URL |
| alt | string | Alt text |
| size | string | Preset: xs, sm, md, lg, xl, 2xl, full |
| rounded | boolean\|string | Border radius |
| filter | object | CSS filters: blur, brightness, contrast, grayscale, saturate, sepia |
SafeHtml
Safely render HTML, resolving page: links and the base path.
import { SafeHtml } from '@uniweb/kit'
<SafeHtml value="<p>Hello <strong>World</strong></p>" />
<SafeHtml value='<a href="page:about">About</a>' />Icon
SVG icon component with built-in icons and URL loading.
import { Icon } from '@uniweb/kit'
<Icon name="check" size="24" color="green" />
<Icon url="/icons/custom.svg" />
<Icon svg="<svg>...</svg>" />Built-in: check, alert, user, heart, settings, star, close, menu, chevronDown, chevronRight, externalLink, download, play
A library icon's name (icon="lu-house", library + name) is for an icon content supplies at runtime, and its drawing is fetched. Import the icons your own code draws, so the page has them from its first render.
SocialIcon
Social media platform icons with automatic detection.
import { SocialIcon, getSocialPlatform, filterSocialLinks } from '@uniweb/kit'
<SocialIcon platform="twitter" size={24} />
<SocialIcon url="https://twitter.com/example" />
// Utilities
getSocialPlatform('https://linkedin.com/in/user') // 'linkedin'
filterSocialLinks(links) // Filter to only social linksSupported: facebook, twitter, x, linkedin, instagram, youtube, github, medium, pinterest, tiktok, discord, mastodon, bluesky, email, phone, orcid, researchgate, googlescholar
Typography
Smart typography components for rendering semantic parser output.
import { Text, H1, H2, P, PlainText } from '@uniweb/kit'
<H1 text="Main Title" />
<H2 text={["Multi-line", "Subtitle"]} />
<P text="A paragraph of content" />
<P text={["First paragraph", "Second paragraph"]} />
// Plain text (HTML shown as text)
<PlainText text="Show <strong>tags</strong> as text" />| Prop | Type | Description |
|------|------|-------------|
| text | string\|string[] | Content to render |
| as | string | Tag: 'h1'-'h6', 'p', 'div', 'span' |
| html | boolean | Render as HTML (default: true) |
| lineAs | string | Tag for array items |
Aliases: H1, H2, H3, H4, H5, H6, P, Span, Div, PlainText
Media
Video player for YouTube, Vimeo, and local videos.
import { Media } from '@uniweb/kit'
<Media src="https://youtube.com/watch?v=abc123" />
<Media src="/videos/intro.mp4" controls />
<Media src="https://youtube.com/..." thumbnail="/poster.jpg" facade />FileLogo
File type icons based on filename.
import { FileLogo } from '@uniweb/kit'
<FileLogo filename="report.pdf" size="32" />MediaIcon
Platform icons (YouTube, Vimeo, etc.).
import { MediaIcon } from '@uniweb/kit'
<MediaIcon type="youtube" size="24" />Asset
File preview with download functionality.
import { Asset } from '@uniweb/kit'
<Asset value="/files/document.pdf" />Hooks
useWebsite
Access website instance and utilities.
import { useWebsite } from '@uniweb/kit'
function MyComponent() {
const {
website, // Website instance
localize, // Localize multilingual values
makeHref, // Resolve page: references to routes
getLanguage, // Current language code
getLanguages // Available languages
} = useWebsite()
return <div>{localize({ en: 'Hello', fr: 'Bonjour' })}</div>
}useActiveRoute
Detect active navigation state.
import { useActiveRoute } from '@uniweb/kit'
function NavLink({ page }) {
const { isActive, isActiveOrAncestor } = useActiveRoute()
return (
<Link
to={page.route}
className={isActiveOrAncestor(page) ? 'font-bold' : ''}
>
{page.title}
</Link>
)
}useScrolled
Detect scroll position for sticky headers.
import { useScrolled } from '@uniweb/kit'
function Header() {
const scrolled = useScrolled(50) // Threshold in pixels
return (
<header className={scrolled ? 'shadow-md' : ''}>
...
</header>
)
}useMobileMenu
Mobile menu state management.
import { useMobileMenu } from '@uniweb/kit'
function Navbar() {
const { isOpen, toggle, close } = useMobileMenu()
return (
<>
<button onClick={toggle}>Menu</button>
{isOpen && <MobileMenu onClose={close} />}
</>
)
}useAccordion
Accordion/FAQ state management.
import { useAccordion } from '@uniweb/kit'
function FAQ({ items }) {
const { isOpen, toggle } = useAccordion()
return items.map((item, i) => (
<div key={i}>
<button onClick={() => toggle(i)}>{item.question}</button>
{isOpen(i) && <p>{item.answer}</p>}
</div>
))
}useInView
Viewport intersection detection for lazy loading and animations.
import { useInView, useIsInView } from '@uniweb/kit'
function AnimatedSection() {
const { ref, inView } = useInView({ threshold: 0.2, once: true })
return (
<div ref={ref} className={inView ? 'animate-fade-in' : 'opacity-0'}>
Content appears when scrolled into view
</div>
)
}
// Simple boolean version
function LazyImage({ src }) {
const [ref, isInView] = useIsInView()
return <div ref={ref}>{isInView && <img src={src} />}</div>
}useGridLayout
Responsive grid utilities.
import { useGridLayout, getGridClasses } from '@uniweb/kit'
function Gallery({ items }) {
const { columns } = useGridLayout(items.length, { maxColumns: 4 })
return (
<div className={getGridClasses(columns)}>
{items.map(item => <Card key={item.id} {...item} />)}
</div>
)
}Theme Hooks
Access site theming data at runtime.
import {
useThemeData,
useThemeColor,
useThemeColorVar,
useColorContext,
useAppearance
} from '@uniweb/kit'
function ThemedComponent({ block }) {
// Full theme access
const theme = useThemeData()
const palettes = theme?.getPaletteNames() // ['primary', 'secondary', ...]
// Get specific color
const primaryColor = useThemeColor('primary', 500) // '#3b82f6'
const primaryVar = useThemeColorVar('primary', 600) // 'var(--primary-600)'
// Context-aware (light/medium/dark sections)
const context = useColorContext(block) // 'light' | 'medium' | 'dark'
// Dark mode
const { scheme, toggle, canToggle } = useAppearance()
return (
<div style={{ color: primaryColor }}>
{canToggle && (
<button onClick={toggle}>
{scheme === 'dark' ? 'Light' : 'Dark'}
</button>
)}
</div>
)
}Search
Full-text search powered by Fuse.js. Requires fuse.js as a peer dependency in your foundation.
npm install fuse.jsuseSearch
Main search hook with debouncing and state management.
import { useSearch, useWebsite } from '@uniweb/kit'
function SearchComponent() {
const { website } = useWebsite()
const { query, results, isLoading, isEnabled, preload } = useSearch(website)
if (!isEnabled) return null
return (
<div>
<input onChange={e => query(e.target.value)} placeholder="Search..." />
{isLoading && <span>Searching...</span>}
{results.map(r => (
<a key={r.id} href={r.href}>{r.title}</a>
))}
</div>
)
}useSearchPrefetch
Prefetches the search index on hover, focus or touch instead of on page load.
import { useSearchPrefetch, useSearchShortcut } from '@uniweb/kit'
function SearchButton({ onClick }) {
const { website } = useWebsite()
const { triggerPreload, prefetchProps } = useSearchPrefetch(website)
// Cmd/Ctrl+K shortcut with preload
useSearchShortcut({
onOpen: onClick,
onPreload: triggerPreload,
})
return (
<button onClick={onClick} {...prefetchProps}>
Search
</button>
)
}A visitor who never searches never downloads the index. It fetches once.
useSearchShortcut
Keyboard shortcut for opening search.
import { useSearchShortcut } from '@uniweb/kit'
// Simple
useSearchShortcut(() => setSearchOpen(true))
// With preload on shortcut
useSearchShortcut({
onOpen: () => setSearchOpen(true),
onPreload: () => searchClient.preload()
})createSearchClient
Low-level search client for advanced use.
import { createSearchClient } from '@uniweb/kit'
const client = createSearchClient(website, {
fuseOptions: { threshold: 0.3 },
defaultLimit: 10
})
// Query — returns SearchResult[]
const results = await client.query('authentication', { limit: 5 })
// Same query, plus how many matched before `limit` — the 47 in "showing 10 of 47".
// `total` is null when the active provider cannot say (a deployment fact, not an
// error): the local index always knows it; an endpoint knows it only if it
// reports one. Render the count conditionally, the results unconditionally.
const { results: page, total } = await client.queryWithTotal('authentication', { limit: 5 })
// Preload index
await client.preload()
// Check status
client.isEnabled()
client.getIndexUrl()Styled Components
Pre-styled components with Tailwind CSS.
import { Section, SidebarLayout, Disclaimer } from '@uniweb/kit'
<Section width="lg" padding="md" className="bg-gray-50">
<h1>Welcome</h1>
</Section>
<SidebarLayout sidebar={<Nav />} sidebarPosition="left">
<main>Content</main>
</SidebarLayout>
<Disclaimer
title="Terms of Service"
content="<p>Please read our terms...</p>"
triggerText="View Terms"
/>Utilities
import { cn, stripTags, isExternalUrl, isFileUrl, detectMediaType } from '@uniweb/kit'
// Merge Tailwind classes (uses tailwind-merge)
cn('px-4 py-2', 'bg-blue-500', condition && 'opacity-50')
// Strip HTML tags
stripTags('<p>Hello</p>') // "Hello"
// URL utilities
isExternalUrl('https://google.com') // true
isFileUrl('/files/doc.pdf') // true
detectMediaType('https://youtube.com/...') // 'youtube'Architecture
┌─────────────────────────────────────────────────────────────┐
│ Foundation (your code) │
│ ├── imports @uniweb/kit (bundled, tree-shaken) │
│ └── @uniweb/core marked as external │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ @uniweb/runtime (browser) │
│ ├── Loads foundation dynamically │
│ ├── Provides @uniweb/core singleton │
│ └── Orchestrates React rendering │
└─────────────────────────────────────────────────────────────┘Why bundle kit but externalize core?
Kit: Different foundations may use different subsets of kit. Tree-shaking ensures each foundation only includes what it uses.
Core: Contains the Website, Page, and Block classes that must be singletons. The runtime provides these — foundations reference them via the external import.
License
Apache-2.0
