typestyle / typestyle

Making CSS Typesafe 🌹

Home Page:https://typestyle.github.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Proposal: stricter / app-specific CSSNestedProperties

silviogutierrez opened this issue · comments

First off: huge fan of this library. I can't praise it enough! So simple and really makes styling an app a pleasure.

TypeStyle already has a way to add properties that are missing, as documented here:

https://github.com/typestyle/typestyle.github.io/blob/source/src/docs/core.md#tip-declaring-new-css-stuff

But it lacks a way to further restrict properties. Why restrict them? Two use cases:

  • Forbid using specific properties
  • Only allow certain colors, fonts, weights, etc based on a style guide.

A lot of the above can be enforced procedurally through mixins, code review, etc. But why not do it at the compiler level?

So I actually got this to work with a few hacks, and it's 90% of the way there. But with a few tweaks it can be built-in to TypeStyle. And far simpler.

In the example below, as a POC, I wanted to limit font weights and font families only to what I provide.

// Put this in a file like client/style.ts
import * as typeCSSTips from "csstips";

import {
    media as typeMedia,
    style as typeStyle,
    types,
} from "typestyle";


type CSSTipNames = Extract<keyof typeof typeCSSTips, string>;

type HandleCSSTipMember<T> = T extends (... args: infer S) => types.NestedCSSProperties ? (...args: S) => JoyNestedCSSProperties : JoyNestedCSSProperties;

type JoyCSSTips = {
    [P in Exclude<CSSTipNames, 'padding' |' margin' | 'border'>]: HandleCSSTipMember<typeof typeCSSTips[P]>;
} & {
    [P in Extract<CSSTipNames, 'padding' |' margin' | 'border'>]: typeof typeCSSTips[P];
};

export const csstips = typeCSSTips as JoyCSSTips;

export interface JoyNestedCSSProperties
    extends Omit<types.NestedCSSProperties, "fontFamily" | "fontWeight"> {
    fontFamily?: "Montserrat";
    fontWeight?: 200 | 300 | 500 | 600;
   
    // this could be tighter but I just wanted to demonstrate 
    $nest?: {
        [selector: string]: JoyNestedCSSProperties | undefined;
    };
}
export function style(...objects: (JoyNestedCSSProperties | undefined)[]): string;
export function style(
    ...objects: (JoyNestedCSSProperties | null | false | undefined)[]
): string;
export function style() {
    return typeStyle.apply(undefined, arguments);
}

export const media = (
    mediaQuery: types.MediaQuery,
    ...objects: (JoyNestedCSSProperties | undefined | null | false)[]
): JoyNestedCSSProperties => {
    return typeMedia(mediaQuery, ...objects) as JoyNestedCSSProperties;
};

The main style function was super easy. But it could be even easier: just make typestyle.createTypeStyle(); take in a generic and pass it to a class. Like so:

declare function createTypeStyle<T = types.NestedCSSProperties>()

It still defaults for those who don't care.

We'd also need to move media under that factory and any related functions. But the above covered everything I used in my code base.

CSS Tips
Unfortunately, CSS tips were a little harder, since it's a collection of functions and objects that return CSSNestedProperties. Boxed types are a particularly troublesome one because argument inference in TS makes all of these required, which isn't true in the real source.

But again, if we make these into a factory and simply provide a default instance just like TypeStyle, it should be no problem.

Thanks again for a great library!