QuikDim - Multidimensional Unit Converter

QuikDim - Multidimensional Unit Converter

Published
Updated
8 min read

Table of Contents

Just Another Unit Converter?

Let’s say you have two products, one measured in centimeters and the other in inches. Most unit converters you’ll find only allow you to convert one dimension at a time. QuikDim was designed to convert units of multiple dimensions simultaneously.

The idea came from a random redditor who pointed out there was no tool to do this. Additionally, I also wanted a tool to convert decimals to nearest tape measure fraction. My father is a wood worker and had been looking for a utility like this. Finally, I wanted this to be a mobile friendly PWA. So I got to work.

Stack

Let’s talk about some of the more notable tools in the package.json.

"dependencies": {
    "convert": "^4.9.0",
    "fraction.js": "^4.2.0",
    "lit": "^2.4.1"
  },
  "devDependencies": {
    "@typescript-eslint/eslint-plugin": "^5.47.1",
    "@typescript-eslint/parser": "^5.47.1",
    "autoprefixer": "^10.4.13",
    "eslint": "^8.31.0",
    "postcss": "^8.4.20",
    "prettier": "^2.8.1",
    "prettier-plugin-tailwindcss": "^0.2.1",
    "pwa-asset-generator": "^6.2.0",
    "tailwindcss": "^3.2.4",
    "terser": "^5.16.1",
    "typescript": "^4.9.3",
    "vite": "^4.0.0",
    "vite-plugin-pwa": "^0.14.0",
    "vite-plugin-sitemap": "^0.3.0",
  }

Vite

I really love this bundler. It’s super fast and has great plugins for creating the sitemap and PWA. The docs are pretty good and configuration isn’t a total nightmare. Plus the Lit TypeScript template is a decent starting point. Although the default configuration for a web component library that is easy enough to change.

Sources:

To create a new project use npm create vite@latest quikdim --template lit-ts

Then modify vite.config.ts for building the website:

export default defineConfig({
  build: {
    rollupOptions: {
      input: {
        main: './index.html',
      },
    },
  },
});

VS the original configuration for building a library:

export default defineConfig({
  build: {
    lib: {
      entry: 'src/my-element.ts',
      formats: ['es'],
    },
    rollupOptions: {
      external: /^lit/,
    },
  },
});

Additional you may remove these keys from package.json:

...
    "type": "module",
    "main": "dist/my-element.es.js",
    "exports": {
        ".": "./dist/my-element.es.js"
    },
    "types": "types/my-element.d.ts",
    "files": [
        "dist",
        "types"
    ],
...

Back at vite.config.ts, we can add the remaining tooling and configuration. First let’s install the packages for generating the sitemap, service worker and other PWA assets.

npm i -D vite-plugin-pwa vite-plugin-sitemap pwa-asset-generator

import { defineConfig } from 'vite';
import { VitePWA } from 'vite-plugin-pwa';
import Sitemap from 'vite-plugin-sitemap';

export default defineConfig({
  build: {
    rollupOptions: {
      input: {
        main: './index.html',
      },
    },
  },
  plugins: [
    Sitemap({ hostname: 'http://www.quikdim.com/' }),
    VitePWA({
      injectRegister: 'auto',
      registerType: 'autoUpdate',
      manifest: {
        name: 'QuikDim',
        short_name: 'QuikDim',
        start_url: 'index.html',
        scope: './',
        icons: [
          {
            src: 'splash_screens/manifest-icon-192.maskable.png',
            sizes: '192x192',
            type: 'image/png',
            purpose: 'any maskable',
          },
          {
            src: 'splash_screens/manifest-icon-512.maskable.png',
            sizes: '512x512',
            type: 'image/png',
            purpose: 'any maskable',
          },
        ],
        theme_color: '#FDE047',
        background_color: '#333',
        display: 'standalone',
      },
    }),
  ],
});

There are a lot of assets required to support a PWA. Especially if you want support IOS devices. I can get around raster and vector image manipulation programs, but that’s no fun. So what do you do?

PWA Asset Generator to the rescue! Just take the favicon and feed it into the asset generator with some options for dark and light theme. Then make sure all of the meta tags in your index.html are correct and off you go.

Light Mode

npx pwa-asset-generator public/favicon.svg public/splash_screens/ --index index.html --favicon .\public\favicon.svg --background "rgba(255, 255, 255, 1)"

Dark Mode

npx pwa-asset-generator public/favicon.svg public/splash_screens/ --index index.html --favicon .\public\favicon.svg --dark-mode --background "rgba(25, 25, 25, 1)"

Google Lit

Google Lit formerly known as Polymer, is a web framework created by Google that allows developers to build web applications using web components. Web components are a set of standardized APIs that allow developers to create reusable UI elements that can be used across different web applications.

Developers can create browser native web components using HTML, CSS, and TypeScript, and then use these components to build complex web applications. It provides a number of useful features, such as data binding, event handling, and a robust but minimal wrapper around web components.

I had been using Lit for work recently and wanted something quick and minimal to build components with. Honestly though, I may rebuild this in the future because it isn’t ideal for building websites with Tailwind / other global stylesheets.

Shadow vs Light DOM

To really understand this we first need to understand the difference between DOMs. This is one of those things that sounds complicated but it’s really not too bad.

Light DOM

Your standard DOM. Open your dev tools and the Light DOM is everything you see. In short, it’s like this:

meme_city

The Light DOM is great for developing web applications. Most frameworks render directly to the Light DOM. Here are some reasons you may want to stay in the Light.

  1. Developers can easily manipulate HTML elements and content without the constraints of encapsulation in a Shadow DOM. The Light DOM requires only basic HTML and CSS knowledge, making it easy to create and modify web pages.

  2. The Light DOM is typically faster and more Lightweight than a Shadow DOM, making web pages perform better, especially on slower devices. Although, in my experience, I would say this is minimal.

  3. Light DOM is more transparent than a Shadow DOM, since it is visible in the browser’s developer tools. This makes it easier for developers to debug and troubleshoot issues with the web page.

Long story short, if you want to use global styles you want Light DOM. If you want Tailwind you want Light DOM. More on that below.

Shadow DOM

By default Lit renders to the Shadow DOM. This is very helpful for web components because it allows the styles of the components to be isolated from the parent stylesheets. The Shadow DOM works by creating a separate DOM tree that is attached to a host element in the main document tree.

This separate tree is called the Shadow Tree and contains the encapsulated styles, HTML, and JavaScript code for the component. The Shadow Tree is then rendered inside the host element, which acts as a gateway to the Shadow Tree.

To simplify things a bit, Shadow DOM is just another DOM isolated in some element on your site. This is great for web components because it ensures your component styles are never overridden by the parent site.

This means Tailwind, or anything like it, is a non-starter when using shadow DOM. You’re often stuck writing native CSS in the component itself or a shared constructable style sheet.

For this project however I thought to play it safe and just use Tailwind and render my components to the Light DOM. Shadow DOM was completely unnecessary here and therefore I could have picked something other than Lit. It’s pretty easy to render Lit components to the Light DOM. Simply overwrite the createRenderRoot() lifecycle method.

@customElement('light-dom')
export class LightDOM extends LitElement {
  // forces component to render to light dom
  protected createRenderRoot() {
    return this;
  }
  render() {
    return html`
      <h1>I am the Light DOM</h1>
    `;
  }
}

//vs shadow dom default
@customElement('shadow-dom')
export class ShadowDOM extends LitElement {
  render() {
    return html`
      <h1>I am the Shadow DOM</h1>
    `;
  }
}

Tailwind / Twind

Tailwind CSS is a popular utility-first CSS framework that provides a set of pre-defined CSS classes to help developers rapidly build custom user interfaces. The framework is designed to be highly customizable and adaptable, allowing developers to easily create responsive, scalable, and maintainable front-end designs for web applications.

Twind is Tailwind but Just In Time instead of Preprocessed. Twind is directly interoperable with Tailwind, but styles are compiled at runtime and injected into a constructable stylesheet. Very very cool and, unlike Tailwind, this works perfectly with the Shadow DOM while still allowing you to use Tailwind classes.

Unfortunately Safari does not fully support the constructable stylesheet constructor and therefore does not support Twind. This has just entered the preview stages in iOS 16 ( MDN source )

I’ll write another article on this specific topic in the future. 👾

Math, but not really…

The true double edged sword of Node is that it has a package for everything. Therefore conversion between unit types or numerical representations is trivial.

To convert between different units I used convert which has very simple API for an easy one liner.

const result = convert(5, 'inches').to('centimeters');

There isn’t much to this. Just tied 5 to the component input and expose the various unit types to select boxes for user input.

Originally I thought tape measure fractions would be the difficult part but Fraction.js makes it very simple.

To extend the example above you can take the output of convert() and feed it directly into the Fraction type provided by Fraction.js. From there it’s a simple task to get the closest tape measure value.

const result = new Fraction(String(convert(10, 'centimeters').to('inches')));

// 32 is the equivalent of 1/32 fractions
const fraction = new Fraction(
  Math.round(32 * result.valueOf()),
  32
).toFraction();

const decimal = result.toString(3); // 3 decimal precision

Conclusion

Building QuikDim was fun weekend project that allowed me to explore a variety of tools and technologies. From using Tailwind CSS for rapid UI prototyping to leveraging Google’s Lit framework for creating web components, I was able to build a unique unit converter that meets the needs of users who require simultaneous unit conversions across multiple dimensions. Additionally, by integrating powerful libraries such as Convert and Fraction.js, QuikDim is able to provide accurate and reliable unit conversions, including decimal to nearest tape measure fraction conversions. All tied together as a mobile-friendly Progressive Web App (PWA). It is accessible on a wide range of devices, making QuikDim a valuable tool.