frutjam
Version:
CSS-only Tailwind CSS v4 component library, also usable with UnoCSS or no build step at all. 65+ accessible UI components, zero JavaScript, WCAG AA guaranteed. Free MCP server for AI editors included.
78 lines (70 loc) • 3.23 kB
JavaScript
import { symbols } from "@unocss/core"
// Generated by scripts/build-unocss.mjs from the same objects the Tailwind
// plugin ships, so the two cannot describe different components.
import { rules as tokenRules, preflight, base, components } from "../../dist/unocss/map.js"
/**
* Frutjam as an UnoCSS preset.
*
* Every class the Tailwind plugin generates is available here too, so
* `class="btn btn-error"` works, and with presetAttributify so does
* `btn="error xl"`. Only what you use is emitted.
*
* Bodies go through symbols.body rather than a plain object, because several
* components nest, and symbols.body takes nested CSS while leaving variants
* intact. That is what keeps `md:btn-error` and `hover:card-primary` working.
*
* @param {object} [options]
* @param {boolean} [options.preflight=true] Emit Frutjam's tokens, reset and
* themes. Turn off if you already
* import frutjam/css/base yourself.
* @param {string} [options.prefix=''] Matches the Tailwind plugin's
* prefix option, so `fj` gives
* fj-btn and fj-btn="error".
* @param {string} [options.layer='frutjam'] Cascade layer for the components.
*/
export function presetFrutjam(options = {}) {
const { preflight: withPreflight = true, prefix = "", layer = "frutjam" } = options
const p = prefix ? `${prefix}-` : ""
const named = (token) => `${p}${token}`
const componentAttrs = new Set(components.map(named))
/*
* `btn="error xl"` expands to btn-error and btn-xl and never to a bare btn,
* so the component itself would be missing. Asking for it here, at extraction
* time, keeps it free of whatever variant the attribute carried: with
* `btn="md:error"` the button must exist at every width and only the colour
* belongs inside the media query.
*/
const attributeBaseExtractor = {
name: "frutjam-attribute-base",
order: -1,
extract({ code }) {
const found = new Set()
for (const match of code.matchAll(/(?:^|[\s"'`])([a-z][a-z0-9-]*)\s*=\s*["']/g)) {
if (componentAttrs.has(match[1])) found.add(match[1])
}
return found
},
}
// Static rules rather than one catch-all regex: UnoCSS can look these up
// directly instead of running a pattern against every token it sees.
const rules = Object.entries(tokenRules).map(([token, body]) => {
const css = { [symbols.body]: body }
// A component answers to its class and to its attribute. UnoCSS merges the
// two into one selector list when both appear, so this costs nothing.
if (componentAttrs.has(named(token))) {
css[symbols.selector] = (selector) => `${selector},[${named(token)}]`
}
return [named(token), css, { layer }]
})
const preflights = [{ layer, getCSS: () => preflight }]
if (withPreflight) preflights.unshift({ layer, getCSS: () => base })
return {
name: "frutjam",
layers: { [layer]: -1 },
extractors: [attributeBaseExtractor],
rules,
preflights,
autocomplete: { templates: Object.keys(tokenRules).map(named) },
}
}
export default presetFrutjam