blazeui:core

v1.0.0Published last month

blazeui logo

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!

{{#Badge id="main" class="rounded-none"}}
 I'm a default badge
{{/Badge}}

{{#Badge id="badge-1" data-foo="bar"}}
    I'm a badge with custom attributes
{{/Badge}}

{{#Badge size="xl"}}
    I'm a badge with a custom variant!
{{/Badge}}

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 {{atts}}>
        {{> Template.contentBlock}}
    </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">
  {{#unless loadComplete}}
      {{#Loading}}
          ...Loading
      {{/Loading}}
  {{/unless}}
</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 {{blazeui_atts}}>
        {{> Template.contentBlock active=active}}
    </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 {{blazeui_atts}}>
   {{> Template.contentBlock context=(blazeui_context "MyComponent")}}
  </div>
</template>

<template name="MyComponenTrigger">
  <button {{blazeui_atts}}>
    {{> Template.contentBlock}}
  </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