BlazeUI - Tailwind components for Meteor Blaze
🔥 UI components for Meteor-Blaze and TailwindCSS 🔥
About
This project is partially inspired by shacdn, radix-ui and headless ui and brings a set of opinionated, yet flexibly changeable UI components on the table.
- 🔥 awesome UI components out of the box
- 🔥 simple to get started
- 🔥 supports variants; allows for custom variants
- 🔥 performant reactive attribute compilation
- 🔥 register your own components
- 🔥 builtin light/dark theme support
BlazeUI is not related to https://www.blazeui.com/
Getting started
1. Add the package:
$ meteor add blazeui:components
2. Install tailwind and a few other little helpers:
$ meteor npm install --save \ tailwindcss \ autoprefixer \ postcss \ postcss-load-config \ class-variance-authority \ tailwindcss-animate \ clsx \ tailwind-merge \ @blazeui/theme-milkyway
This looks like a lot, so let's see what these packages are for:
- tailwindcss - the tailwind library
- autoprefixer - required to
- postcss - required to drop non-necessary css
- postcss-load-config - required to load config from tailwind config
- class-variance-authority - resolve variations of component styles
- tailwindcss-animate - more animation tools for tailwind
- clsx - assign classes with truthy/falsy values
- tailwind-merge - deduplicate class names for components
- @blazeui/theme-milkyway - the default blazeui theme
3. Create config files
You need to provide a tailwind.config.js
config file for your project.
As a staring point, you can use the config from the Milkyway theme:
1const { fontFamily } = require("tailwindcss/defaultTheme") 2const milkyway = require("@blazeui/theme-milkyway") 3 4/** @type {import('tailwindcss').Config} */ 5module.exports = { 6 ...milkyway, 7 content: [ 8 "./imports/ui/**/*.{js,jsx,ts,tsx,html}", 9 './client/*.{js,html}', 10 '.meteor/local/build/programs/web*/**/*.js', 11 './node_modules/@fortawesome/fontawesome-free/css/all.css', 12 ] 13}
Next, you need a postcss.config.js
config file. You can use this as a starter:
1module.exports = { 2 plugins: { 3 tailwindcss: {}, 4 autoprefixer: {}, 5 }, 6 excludedMeteorPackages: [] 7}
4. Import the library in your client code
For static import of all components, use:
1import 'meteor/blazeui:components/all'
this will instantly make all components available but also increases the bundle size.
If you need to be careful about bundle size, you may use the dynamic import way:
1const { BlazeUI } = await import('meteor/jkuester:blazeui/core/BlazeUI.js') 2const { Badge } = await import('meteor/jkuester:blazeui/components/badge/Badge.js') 3 4BlazeUI.register(Badge)
5. Import the theme CSS
BlazeUI provides a default theme, which you can
import in client/main.js
via
1import '@blazeui/theme-milkyway/milkyway.css'
This theme is zero config and looks great out of the box.
You also can also override the root variables
To do so, open your client/main.css
(or .scss) file and provide
the root variables for the variants of the components:
1@tailwind base; 2@tailwind components; 3@tailwind utilities; 4 5@layer base { 6 :root { 7 --background: 0 0% 100%; 8 --foreground: 222.2 47.4% 11.2%; 9 10 --muted: 210 40% 96.1%; 11 --muted-foreground: 215.4 16.3% 46.9%; 12 13 --popover: 0 0% 100%; 14 --popover-foreground: 222.2 47.4% 11.2%; 15 16 --border: 214.3 31.8% 91.4%; 17 --input: 214.3 31.8% 91.4%; 18 19 --card: 0 0% 100%; 20 --card-foreground: 222.2 47.4% 11.2%; 21 22 --primary: 222.2 47.4% 11.2%; 23 --primary-foreground: 210 40% 98%; 24 25 --secondary: 210 40% 96.1%; 26 --secondary-foreground: 222.2 47.4% 11.2%; 27 28 --accent: 210 40% 96.1%; 29 --accent-foreground: 222.2 47.4% 11.2%; 30 31 --destructive: 0 100% 50%; 32 --destructive-foreground: 210 40% 98%; 33 34 --ring: 215 20.2% 65.1%; 35 36 --radius: 0.5rem; 37 } 38 39 .dark { 40 --background: 224 71% 4%; 41 --foreground: 213 31% 91%; 42 43 --muted: 223 47% 11%; 44 --muted-foreground: 215.4 16.3% 56.9%; 45 46 --accent: 216 34% 17%; 47 --accent-foreground: 210 40% 98%; 48 49 --popover: 224 71% 4%; 50 --popover-foreground: 215 20.2% 65.1%; 51 52 --border: 216 34% 17%; 53 --input: 216 34% 17%; 54 55 --card: 224 71% 4%; 56 --card-foreground: 213 31% 91%; 57 58 --primary: 210 40% 98%; 59 --primary-foreground: 222.2 47.4% 1.2%; 60 61 --secondary: 222.2 47.4% 11.2%; 62 --secondary-foreground: 210 40% 98%; 63 64 --destructive: 0 63% 31%; 65 --destructive-foreground: 210 40% 98%; 66 67 --ring: 216 34% 17%; 68 69 --radius: 0.5rem; 70 } 71} 72 73@layer base { 74 * { 75 @apply border-border; 76 } 77 body { 78 @apply bg-background text-foreground; 79 font-feature-settings: "rlig" 1, "calt" 1; 80 font-family: Arial, Helvetica, sans-serif; 81 } 82}
Usage
Assuming your components are available you can use them by their respective names. Every component allows for a content block!
I'm a default badge I'm a badge with custom attributes I'm a badge with a custom variant!
Define custom variants
The library comes with default variants for the components. If that's not enough, you can easily extend variants:
1import { BlazeUI, Badge } from 'meteor/jkuester:blazeui/static' 2 3BlazeUI.variants({ 4 ctx: Badge, 5 type: 'size', 6 values: { 7 xs: "text-xs", 8 sm: "text-sm", 9 base: "text-base", 10 lg: "text-lg p-2", 11 xl: "text-xl p-3", 12 }, 13 default: 'xs' 14})
You can use this function also to change the default
for an
existing variant or extend/override the variant itself.
Register custom components
With BlazeUI you can easily create your own components. In fact, every of the provided core components are actually build using the same method.
1. Create a template
First of all, there needs to be a template that Blaze can create:
<template name="Loading"> <div > </div> </template>
Then you need to define the Template's functionality:
1import { BlazeUI } from 'meteor/jkuester:blazeui/static' 2import './Loading.html' 3 4export const Loading = { 5 name: 'Loading', // needs to be the exact template name! 6 7 attributes: { // optional 8 // this is a hypotehtical default attribute of the component 9 role: 'loading' 10 }, 11 class: "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", 12 variants: { // optional 13 variant: { 14 default: "bg-background text-foreground", 15 destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive" 16 } 17 }, 18 defaultVariants: { // optional 19 variant: "default" 20 } 21}
Now you can use it in your template like so:
<template name="myTemplate"> ...Loading </template>
Customizations
BlazeUI is flexible at its core, enabling you fine-grained customization.
Create custom components
First, you need to have a template defined for your component:
<template name="Hello"> <div > </div> </template>
In your js, you can then register it like this:
1import { ReactiveDict } from 'meteor/reactive-dict' 2import { BlazeUI } from 'meteor/blazeui:core' 3import './Hello.html' 4 5const Hello = { 6 /** 7 * Required, must exactly match the name of the Template! 8 */ 9 name: 'Hello', 10 11 /** 12 * Optional, the base class that is always applied. 13 * Can be overridden. 14 */ 15 class: 'p-1 bg-primary text-primary-foreground transition-all ease-in-out', 16 17 /** 18 * Optional, a RactiveDict that can be used to manage internal 19 * state. You can also use a custom reactive data source as long 20 * as implements the methods of ReactiveDict (get, set, all etc.). 21 */ 22 state: new ReactiveDict({ active: false }), 23 24 /** 25 * Optional function if you need to resolve attributes for the component 26 * with awareness of the state. 27 * Reactive: This method gets called, when props or state change. 28 * 29 * @param props {object} the object, returned by {Template.currentData()} 30 * @param state {object|undefined} present, when {state} is defined on the component. 31 * @param api {BlazeUI} the BlazeUI top-level api is always passed down to components. 32 * @return {{role: string, class: string}} 33 */ 34 attributes ({ props, state, api }) { 35 const { class:className, ...rest } = props 36 const { merge } = api.styles() 37 const { active } = state 38 39 return { 40 role: 'button', 41 class: merge( 42 Hello.class, 43 active ? 'text-4xl' : 'text-xs', 44 className 45 ), 46 ...rest 47 } 48 }, 49 /** 50 * This is passed to the Template's `onCreated` method. 51 * Note, that state is only passed, when being defined on this 52 * component. 53 * @param state {object?} 54 */ 55 onCreated ({ state }) { 56 const instance = this 57 instance.state = state 58 }, 59 helpers: { 60 active() { 61 return Template.instance().state.get('active') 62 } 63 }, 64 events: { 65 'click div' (e, t) { 66 // this results in 'attributes' being called right after 67 t.state.set('active', !t.state.get('active')) 68 } 69 } 70} 71 72BlazeUI.register(Hello)
Override components' default classes
You can change any component's default styles by simply overriding its class
property
or its variants.
Share state between components
BlazeUI is designed to relieve you from the burden of implementing state-sharing between components and their children.
Sometimes you can simply forward the state as props of the child, but this quickly results in so-called "prop-drilling", where a re prop is passed down multiple levels of children.
Instead you can use a context that parents share with their children.
The parent simply needs to use the blazeui_contetx
helper to provide
it to all children:
<template name="MyComponent"> <div > </div> </template> <template name="MyComponenTrigger"> <button > </button> </template>
A child component can use this shared state via the api
parameter in the state
function:
1export const MyComponent = { 2 name: 'MyComponent', 3 class: 'p-4 border rounded-md', 4 state: ({ instance }) => { 5 // attach state to the instance 6 // so the blazeui_context helper 7 // will pick it up. 8 // convention: it must be named "state" 9 // and exist as property of the instance 10 instance.state = new ReactiveDict({ 11 active: false 12 }) 13 return instance.state 14 }, 15 attributes ({ props, state, api }) { 16 const { merge } = api.styles() 17 const active = state.get('active') 18 return { 19 data-active: active, // this also updates when children change the state 20 class: merge(MyComponent.class, props.class) 21 } 22 } 23} 24 25export const MyComponentTrigger = { 26 name: 'MyComponentTrigger', 27 class: 'border bg-primary rounded-md font-semibold text-primary-foreground', 28 state: ({ instance, api }) => { 29 // this will automatically pick up the state from the parent 30 // no matter which level of depth this child is curently located. 31 const { useFromContext } = api.state() 32 return useFromContext({ instance, key: 'MyComponentContext' }) 33 }, 34 events: { 35 'click button' (e, t) { 36 // toggle active 37 t.state.set({ active: !t.state.get('active') }) 38 } 39 } 40}
License
MIT, see license file