npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@frontline-hq/tdc

v0.0.29

Published

This library is dedicated to Ivan Hover, with whom I had the honor to work with at inlang and from whom I learned a great deal. Some parts of this library contain code borrowed from inlang, which I am also grateful for.

Downloads

52

Readme

@frontline-hq/tdc

This library is dedicated to Ivan Hover, with whom I had the honor to work with at inlang and from whom I learned a great deal. Some parts of this library contain code borrowed from inlang, which I am also grateful for.

It's purpose is to minimize the CSS footprint of components with a lot of different design variants (e.g. components in component libraries)

This is accomplished by offering a component design registration standard.

Setup

  1. Install npm package @frontlinq-hq/tdc

  2. Add configuration file in the root directory of your vite project (named tdc.config.ts):

    // tdc.config.ts
    import type { LibraryConfig } from "@frontline-hq/tdc";
    
    export default {
        debug: false,
        /* Here go your component design registrations */
        registrations: [],
        tagNameDelimiter: "-",
        /* The path to your tailwind config relative to your projects root */
        tailwindConfigPath: "./tailwind.config.ts",
    } satisfies LibraryConfig;
  3. Add plugin to sveltekit vite config:

    // vite.config.ts
    import { defineConfig } from "vite";
    import { sveltekit } from "@sveltejs/kit/vite";
    import { plugin } from "@frontline-hq/tdc/plugin";
    
    export default defineConfig({
        plugins: [await plugin(), sveltekit()],
    });
  4. Add safelit & content configuration to tailwind config:

    // tailwind.config.ts
    import type { Config } from "tailwindcss";
    import { getDynamicSafelist } from "@frontline-hq/tdc/tailwind";
    
    export default {
        content: [
            "./src/**/*.{html,js,svelte,ts}",
            /* Exclude *.tdc.ts files from tailwind class detection */
            "!**/*.tdc.ts",
            /* Exclude tdc.config.ts file from tailwind class detection */
            "!tdc.config.ts",
        ],
        theme: {
            extend: {},
        },
        /* Add the safelist here */
        safelist: [...getDynamicSafelist()],
        plugins: [],
    } as Config;

Now you are ready to go and build optimized components!

Component registration

You can write registrations either

  • in the tdc.config.ts file
  • or in _name_.tdc.ts (and then import and add to registrations in the tdc.config.ts file)

Registrations are written as follows:

// icon.tdc.ts
export const iconRegistration = new Registration({
    identifier: "icon",
    props: { size: ["xl", "2xl"], destructive: ["true", "false"] },
    styles: s => ({
        c: `border-${s("size", { xl: "2", "2xl": "4" })} text-${s(
            "destructive",
            "red",
            {
                false: "green",
            }
        )}-400`,
    }),
    dependencies: {},
    mappings: {},
    importPath: "$lib/components/icon",
});

identifier

The identifier determines how the component that can be uses is called. In the above example: <tdc-icon></tdc-icon>.

importPath

The brilliant thing is that you don't need to import the components anymore into your .svelte code. Just use them!

<script>
    <!-- NO IMPORT -->
</script>

<tdc-icon />

The import is handled by the library depending on the importPath that you specify for the registration. Please make sure, that the import path is not relative, but absolute. E.g. use aliases like $lib, so that the import can be resolved from anywhere it will be used.

props

The props specified are available on the component tag as the tdc attribute:

<tdc-icon tdc={{size: "xl", destructive: "true"}}>
    <!-- Your markup -->
</tdc-icon>

This will inject the tdc icon component with the size xl and the destructive variant. You can also specify variants for any tailwind modifier:

<tdc-icon tdc={{ size: 'xl', destructive: { default: true, md: false } }} />

This will inject the xl component styles always. By default the destructive component styles will be used and on tailwind md: screens the non-destructive component.

styles

This is how you actually register the styles - as a function. The return value of the function will be available within your component definition as the tdc.styles property:

<!-- Icon.svelte definition -->
<script lang="ts">
	import type { TdcProp } from '@frontline-hq/tdc';
	import type { iconRegistration } from './icon.tdc.ts';

	interface $$Props extends HTMLElement {
		tdc: TdcProp<typeof iconRegistration>;
	}

	export let tdc: $$Props['tdc'];
</script>

<!-- Compiled result of tdc.styles.c is an array of strings -->
<div class={tdc.styles.c.join(' ')}>Hey there</div>

The compiled classes (depending on the used component variants) will be split into an array of strings. The parameter of the styles function takes two or three arguments:

2 Arguments:

(prop: PropsKey, styles: Record<PropsValue, string>)

  • PropsKey is the name of some component prop (e.g. destructive in this case).
  • PropsValue is an available value of this prop (e.g. true or false for destructive)

Note that the second argument needs to be a full mapping of all states. That means it has to be an object that describes the styles for every variant (true and false) of the specific prop you are targeting (destructive).

3 Arguments

(prop: PropsKey, defaultStyle: string, styles: Partial<Record<PropsValue, string>>)

Same as above, just that the second prop is the default value. This means you only need to give a partial mapping of component props to styles as the third argument.

The styles define what properties you will have available in the styles object after compilation. In this example, it is only the property c, but you can define styles for any key you want:

/* ... */
styles: s => ({
    c: `border-${s("size", { xl: "2", "2xl": "4" })} text-${s(
        "destructive",
        "red",
        {
            false: "green",
        }
    )}-400`,
    some-other-key: "... some styles ..."
});
/* ... */

dependencies

Now it gets interesting. You can even nest component insertion!

Surely you know the case where a specific component variant implies that usage of another specific component variant? Like a small button implies the usage of a small icon within?

Well this is called dependencies.

dependencies are defined as an object where you can just insert the registration of other components.

e.g. for a button:

const iconRegistration = new Registration({ identifier: "icon" /*  */ });
const buttonRegistration = new Registration({
    identifier: "button",
    /* ... */
    dependencies: {
        icon: iconRegistration,
    },
});

Now this also has implications for injection within .svelte files, as even the dependencies are detected:

<!-- Will injection the icon registration from button -->
<tdc-button-icon/>
<!-- Will inject the button component -->
<tdc-button/>

The generated styles for the icon registration will be available in the tdc.children.icon prop after injection.

mappings

Now the only thing left to specify is how the parent props are actually mapped to the dependency props! E.g. we want to map the buttons props to the icons props, more specifically we want:

  • The "sm" button to use a "md" icon and the "md" button to use a "lg" icon
  • The destructive properties of the components to match

The mappings are defined using a helper function, which again takes 2 or 3 arguments.

  1. The first argument is always the parent components prop name
  2. The second argument can either be a full mapping (object) of parent prop value -> dependency prop value, e.g. button: md -> icon: 2xl or just the default dependency prop value.
  3. The third argument is only available when the second is a default value (see destructive below) - it is a partial mapping of parent -> dependency mapping that extends the default dependency prop.
/* button registration ... */
mappings: {
		icon: {
			destructive: (m) => m('destructive', true, { false: 'false' }),
			size: (m) => m('scale', { md: '2xl', sm: 'xl' })
		}
	},
/* ... */

Usage

You can specify which version of a component you want to be rendered by passing them in the tdc prop. There are differences on how optimized the resulting CSS will be though.

This can be explained by how we built the safelisting. It searches files and analyzes the code withing the tdc={{}} brackets to generate the dynamic tailwind safelist. If there are variables used within these brackets, that are defined elsewhere, the plugin will try to guess all possible values this variable could take and generate the safelist accordingly (less optimized / more general)

  1. Fully optimized ✅

    Best specify all your props so that the information contained within the tdc={{}} brackets are complete.

    Example:

    <!-- +page.svelte -->
    <tdc-button tdc={{color: "green", size: {default: "sm", md: "md"}}}>
  2. Partially optimized ⚠️

    The tdc props are specified but not all information can be extracted by only looking within the tdc={{}} brackets.

    Example:

    <!-- +page.svelte -->
    <script>
        const color = "green"
        const defaultSize = "sm"
        const mdSize = "md"
    </script>
    <tdc-button tdc={{color: color, size: {default: defaultSize, md: mdSize}}}>
  3. Not optimizable ❌

    If specifying breakpoint dependent styles, you can't use variables for the object defining it. While the syntax in the example above works, the one below will render an error during runtime:

    Example:

    <!-- +page.svelte -->
    <script>
        const color = "green"
        const defaultSize = "sm"
        const mdSize = "md"
        const size = {default: defaultSize, md: mdSize}
    </script>
    <tdc-button tdc={{color: color, size: size}}>

    That is because we cannot guess which tailwind modifiers could be used in the tdc prop size.

In any case, you will get

  • a warning for unoptimized code
  • a runtime error for non-optimizable code