-
Notifications
You must be signed in to change notification settings - Fork 1
Open
Labels
Description
- 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
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
karol-majewski