WrocTypeScript / talks

What would you like to share with the community?
11 stars 1 forks source link

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

Open kaaboaye opened 1 year ago

kaaboaye commented 1 year ago

\</h1> <p>Design constraints with css classes and typescript validation and auto-completion</p> <p>Title is WIP</p> <h2>The talk</h2> <h3>One-sentence summary</h3> <p>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.</p> <h3>What's the format — is it a case study, a live coding session, a workshop or something else?</h3> <p>case study</p> <h3>Tell us more about the talk</h3> <p>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.</p> <pre><code class="language-ts">// 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 */</code></pre> <p>And explain how we've got from the code above the following results</p> <img width="822" alt="image" src="https://user-images.githubusercontent.com/12039839/208700568-baf65871-f9db-4d96-8d3d-85a204388114.png"> <img width="516" alt="image" src="https://user-images.githubusercontent.com/12039839/208700643-695b5bf1-c7da-4a24-a495-7e0918db06a3.png"> <h2>You</h2> <p>Co-Funder and CTO at Surfer Local. <a rel="noreferrer nofollow" target="_blank" href="https://surferlocal.com">https://surferlocal.com</a></p> <h3>A few words about yourself</h3> <p><a rel="noreferrer nofollow" target="_blank" href="https://www.linkedin.com/in/mieszko-wawrzyniak/">https://www.linkedin.com/in/mieszko-wawrzyniak/</a></p> <p>I'll work up something later if my talk is accepted.</p> <!-- That's what we're going to use on social media, and to introduce you during the event. --> <h3>How can we find you on social media?</h3> <p>It's best to email me.</p> <h3>Would you be willing to have a Q/A session after the talk?</h3> <p>Sure</p> <h3>Do you mind if we record the event?</h3> <p>Nope, go ahead</p> <h3>Is there anything we can help you with?</h3> <p>Nope</p> <p>I was invited to post my proposal by @hasparus </p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/welcome[bot]"><img src="https://avatars.githubusercontent.com/in/4141?v=4" />welcome[bot]</a> commented <strong> 1 year ago</strong> </div> <div class="markdown-body"> <p>Thanks for your contribution! One of the @WrocTypeScript/organisers will go back to you as soon as possible.</p> </div> </div> <div class="comment"> <div class="user"> <a rel="noreferrer nofollow" target="_blank" href="https://github.com/karol-majewski"><img src="https://avatars.githubusercontent.com/u/20233319?v=4" />karol-majewski</a> commented <strong> 1 year ago</strong> </div> <div class="markdown-body"> <p>Nice proposal @kaaboaye! Looking forward to this one.</p> </div> </div> <div class="page-bar-simple"> </div> <div class="footer"> <ul class="body"> <li>© <script> document.write(new Date().getFullYear()) </script> Githubissues.</li> <li>Githubissues is a development platform for aggregating issues.</li> </ul> </div> <script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js"></script> <script src="/githubissues/assets/js.js"></script> <script src="/githubissues/assets/markdown.js"></script> <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.4.0/build/highlight.min.js"></script> <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.4.0/build/languages/go.min.js"></script> <script> hljs.highlightAll(); </script> </body> </html>