Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Design constraints with css classes and typescript validation and auto-completion #87

Open
kaaboaye opened this issue Dec 20, 2022 · 2 comments
Labels

Comments

@kaaboaye
Copy link

  • Your full name: Mieszko Wawrzyniak
  • Target audience (beginner/intermediate/advanced/everyone): intermediate
  • Estimated duration: 20 min
  • Keywords:

<Title of your talk>

Design constraints with css classes and typescript validation and auto-completion

Title is WIP

The talk

One-sentence summary

I'll explain how you can leverage Typescript template literal types to create both run-time and compile time classes enabling your very own Tailwind-like developer experience.

What's the format — is it a case study, a live coding session, a workshop or something else?

case study

Tell us more about the talk

I'll most likely go through the following file and show step by step how Typescript's type system can infer types in various real time scenarios.

// register global styles
declare global {
  interface Window {
    sfrStyles?: HTMLStyleElement;
  }
}

const atomicClasses = (() => {
  const baseClasses = {
    hide: "display: none",
    block: "display: block",
    "inline-block": "display: inline-block",
    flex: "display: flex",
    "inline-flex": "display: inline-flex",

    "flex-wrap": "flex-wrap: wrap",

    flex1: "flex: 1",
    flex2: "flex: 2",
    flex3: "flex: 3",
    flex4: "flex: 4",
    flex5: "flex: 5",
    flex6: "flex: 6",

    capitalize: "text-transform: capitalize",
    lowercase: "text-transform: lowercase",
    uppercase: "text-transform: uppercase",

    "margin-0auto": "margin: 0 auto",
    "margin-left-auto": "margin-left: auto",
    "position-relative": "position: relative",
    "background-color-initial": "background-color: initial",
    "background-color-white": "background-color: white",
    "user-select-none": "user-select: none",
    "cursor-pointer": "cursor: pointer",
    "color-white": "color: white",

    "max-width400": "max-width: 400px",
    "max-width720": "max-width: 720px",

    "pointer-events-auto": "pointer-events: auto",

    "white-space-pre-wrap": "white-space: pre-wrap",
  } as const;

  function computeVariations<
    T1 extends string,
    T2 extends string,
    V1 extends string,
    V2 extends string
  >(
    [names, subNames]: readonly [readonly T1[], readonly T2[]],
    values: readonly (readonly [V1, V2])[]
  ) {
    return names.flatMap(
      (name) =>
        [
          ...values.map(
            ([valueName, value]) =>
              [`${name}${valueName}`, `${name}: ${value}`] as const
          ),
          ...subNames.flatMap((subName) =>
            values.map(
              ([valueName, value]) =>
                [
                  `${name}-${subName}${valueName}`,
                  `${name}-${subName}: ${value}`,
                ] as const
            )
          ),
        ] as const
    );
  }

  const sizes = [
    ["4", "4px"],
    ["6", "6px"],
    ["8", "8px"],
    ["12", "12px"],
    ["16", "16px"],
    ["18", "18px"],
    ["24", "24px"],
    ["32", "32px"],
    ["48", "48px"],
    ["64", "64px"],
  ] as const;

  const colors = (["blue", "green", "red", "gray"] as const).flatMap((color) =>
    (["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"] as const).flatMap(
      (step) => [`${color}-${step}`] as const
    )
  );

  const colorOptions = (
    [
      ["background", "background-color"],
      ["color", "color"],
    ] as const
  ).flatMap(([name, property]) =>
    colors.map(
      (color) => [`${name}-${color}`, `${property}: var(--${color})`] as const
    )
  );

  const sizeEntries = computeVariations(
    [
      ["width", "max-width", "min-width", "height", "max-height", "min-height"],
      [],
    ],
    [
      ...sizes,
      ["128", "128px"],
      ["100", "100%"],
      ["-min", "min-content"],
      ["-max", "max-content"],
    ]
  );

  const spacingEntries = computeVariations(
    [
      ["margin", "padding"],
      ["top", "bottom", "left", "right"],
    ],
    [["0", "0"], ...sizes]
  );

  const gapEntries = computeVariations(
    [["gap"], []],
    [...sizes, ["-items", "var(--gap-items)"], ["-cards", "var(--gap-cards)"]]
  );

  const sizedEntries = computeVariations(
    [["border-radius"], []],
    [
      ["6", "6px"],
      ["16", "16px"],
      ["100", "100%"],
    ]
  );

  const borderEntries = computeVariations(
    [["border"], ["right"]],
    [
      ["0", "0"],
      ["-none", "none"],
    ]
  );

  const flexDirectionEntries = computeVariations(
    [["flex-direction"], []],
    [
      ["-row", "row"],
      ["-column", "column"],
      ["-row-reverse", "row-reverse"],
      ["-column-reverse", "column-reverse"],
    ]
  );

  const contentEntries = computeVariations(
    [["justify-content"], []],
    [
      ["-space-between", "space-between"],
      ["-space-around", "space-around"],
      ["-center", "center"],
      ["-end", "end"],
      ["-flex-end", "flex-end"],
    ]
  );

  const itemsEntries = computeVariations(
    [["align-items", "align-self", "justify-items"], []],
    [
      ["-start", "start"],
      ["-center", "center"],
      ["-stretch", "stretch"],
      ["-end", "end"],
      ["-flex-end", "flex-end"],
    ]
  );

  const textAlignEntries = computeVariations(
    [["text-align"], []],
    [
      ["-left", "left"],
      ["-center", "center"],
      ["-right", "right"],
    ]
  );

  const overflowEntries = computeVariations(
    [["overflow"], ["x", "y"]],
    [
      ["-visible", "visible"],
      ["-hidden", "hidden"],
      ["-auto", "auto"],
    ]
  );

  const fontSizeEntries = computeVariations([["font-size"], []], sizes);

  type NameFromEntries<T extends readonly (readonly [string, string])[]> =
    T[number][0];

  const externalClasses = [
    "btn-hidable",
    "btn-icon-right",
    "pagination-hide-steps",
  ] as const;

  type Name =
    | keyof typeof baseClasses
    | NameFromEntries<typeof sizeEntries>
    | NameFromEntries<typeof colorOptions>
    | NameFromEntries<typeof spacingEntries>
    | NameFromEntries<typeof gapEntries>
    | NameFromEntries<typeof sizedEntries>
    | NameFromEntries<typeof flexDirectionEntries>
    | NameFromEntries<typeof contentEntries>
    | NameFromEntries<typeof itemsEntries>
    | NameFromEntries<typeof textAlignEntries>
    | NameFromEntries<typeof overflowEntries>
    | NameFromEntries<typeof borderEntries>
    | NameFromEntries<typeof fontSizeEntries>
    | typeof externalClasses[number];

  const classes = Object.freeze(
    Object.assign(
      Object.create(null) as object,
      baseClasses,
      Object.fromEntries(sizeEntries),
      Object.fromEntries(colorOptions),
      Object.fromEntries(spacingEntries),
      Object.fromEntries(gapEntries),
      Object.fromEntries(sizedEntries),
      Object.fromEntries(flexDirectionEntries),
      Object.fromEntries(contentEntries),
      Object.fromEntries(itemsEntries),
      Object.fromEntries(textAlignEntries),
      Object.fromEntries(overflowEntries),
      Object.fromEntries(borderEntries),
      Object.fromEntries(fontSizeEntries),
      Object.fromEntries(externalClasses.map((name) => [name, ""] as const))
    ) as { [key in Name]: string }
  );

  // register styles

  const styleContent = Object.entries(classes)
    .filter(([, style]) => style)
    .map(([className, style]) => `.sfr-${className} {${style} !important;}`)
    .join("\n");

  const textNode = document.createTextNode(styleContent);

  if (window.sfrStyles) {
    window.sfrStyles.firstChild?.remove();
    window.sfrStyles.appendChild(textNode);
  } else {
    window.sfrStyles = document.createElement("style");
    window.sfrStyles.appendChild(textNode);
    document.head.appendChild(window.sfrStyles);
  }

  return classes;
})();

type Name = keyof typeof atomicClasses;

// let's use arguments directly to prevent webpack from converting it to array
/* eslint-disable prefer-rest-params */
export const cssCommons = function cssCommons(): string {
  const parts: string[] = new Array(arguments.length * 2);
  let partsIdx = 0;

  // those loops aren't recommended because of readability but they are still the fastest way
  // to iterate over an array
  // eslint-disable-next-line no-plusplus
  for (let argumentsIdx = 0; argumentsIdx < arguments.length; argumentsIdx++) {
    // since app wont crash when unknown css classes are used we don't need this check
    // in production builds and webpack will remove this code when bundling
    if (process.env.NODE_ENV !== "production") {
      if (!((arguments[argumentsIdx] as Name) in atomicClasses)) {
        throw new Error(
          `Unknown atomic class. "${
            arguments[argumentsIdx] as Name
          }" is not available.`
        );
      }
    }

    // parts.push() is a no-go since we preallocate the array
    /* eslint-disable no-plusplus */
    parts[partsIdx++] = " sfr-";
    parts[partsIdx++] = arguments[argumentsIdx];
    /* eslint-enable no-plusplus */
  }

  // apply is faster then "".concat(...parts)
  return String.prototype.concat.apply("", parts);
} as (...classNames: readonly [Name, ...(readonly Name[])]) => string;
/* eslint-enable prefer-rest-params */

And explain how we've got from the code above the following results

image

image

You

Co-Funder and CTO at Surfer Local. https://surferlocal.com

A few words about yourself

https://www.linkedin.com/in/mieszko-wawrzyniak/

I'll work up something later if my talk is accepted.

How can we find you on social media?

It's best to email me.

Would you be willing to have a Q/A session after the talk?

Sure

Do you mind if we record the event?

Nope, go ahead

Is there anything we can help you with?

Nope

I was invited to post my proposal by @hasparus

@welcome
Copy link

welcome bot commented Dec 20, 2022

Thanks for your contribution! One of the @WrocTypeScript/organisers will go back to you as soon as possible.

@karol-majewski
Copy link
Member

Nice proposal @kaaboaye! Looking forward to this one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants