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

Use template literal types for unit values #1

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 2 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ const styles = tacky(_ => [
_.fontSize(_.rem(fontSize)),
_.fontFamily("Times New Roman", "serif"),
_.display(display),
_.boxShadow(_.rem(2), _.rem(2), _.rem(2), _.rem(2), _.rgba(0, 0, 0, 0.5)),
_.media([_.media.screen(_.media.minWidth(_.rem(30)))],
_.boxShadow("2rem", "2rem", "2rem", "2rem", _.rgba(0, 0, 0, 0.5)),
_.media([_.media.screen(_.media.minWidth("300px"))],
_.color(_.rgb(0, 255, 0)),
),
]);
Expand Down Expand Up @@ -72,17 +72,6 @@ Tacky is a library inspired by
approach in order to provide safety that can't be guaranteed by a `Record`
interface alone.

### Why are values specified unit-first?

Standard object styles allow any string where scalar CSS values e.g. `"2rem"`,
`"5px"` are expected. It's not feasible to enumerate every possibility
of those values, and TypeScript's type system has no ability to interpolate
strings.

By expressing these as a "unit function" that receives magnitude as a
number, we can be much more strict about what values are allowed for a given
property.

### Why write styles as a list of function calls?

Representing complex CSS values safely in TypeScript warrants the use of a
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@
"@types/lodash-es": "^4.17.3",
"@types/react": "^16.9.46",
"@types/react-dom": "^16.9.8",
"@typescript-eslint/eslint-plugin": "^3.10.0",
"@typescript-eslint/parser": "^3.10.0",
"@typescript-eslint/eslint-plugin": "^4.1.0",
"@typescript-eslint/parser": "^4.1.0",
"babel-jest": "^26.3.0",
"babel-loader": "^8.1.0",
"babel-plugin-macros": "^2.8.0",
"clean-webpack-plugin": "^3.0.0",
"csstype": "^3.0.3",
"eslint": "^7.7.0",
"eslint": "^7.9.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-react": "^7.20.6",
Expand All @@ -50,7 +50,7 @@
"react": "^16.13.1",
"react-dom": "^16.13.1",
"tsconfig-paths-webpack-plugin": "^3.3.0",
"typescript": "^4.0.2",
"typescript": "^4.1.0-dev.20200911",
"webpack": "^4.44.1",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0"
Expand Down
2 changes: 1 addition & 1 deletion packages/tacky-css/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@
"babel-plugin-macros": "^2.8.0",
"csstype": "^3.0.3",
"nanoid": "^3.1.12",
"typescript": "^4.0.2"
"typescript": "^4.1.0-dev.20200911"
}
}
153 changes: 152 additions & 1 deletion packages/tacky-css/src/color.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,156 @@
import { TackyVariant } from "./types";

type NamedColor =
| "aliceblue"
| "antiquewhite"
| "aqua"
| "aquamarine"
| "azure"
| "beige"
| "bisque"
| "black"
| "blanchedalmond"
| "blue"
| "blueviolet"
| "brown"
| "burlywood"
| "cadetblue"
| "chartreuse"
| "chocolate"
| "coral"
| "cornflowerblue"
| "cornsilk"
| "crimson"
| "cyan"
| "darkblue"
| "darkcyan"
| "darkgoldenrod"
| "darkgray"
| "darkgreen"
| "darkgrey"
| "darkkhaki"
| "darkmagenta"
| "darkolivegreen"
| "darkorange"
| "darkorchid"
| "darkred"
| "darksalmon"
| "darkseagreen"
| "darkslateblue"
| "darkslategray"
| "darkslategrey"
| "darkturquoise"
| "darkviolet"
| "deeppink"
| "deepskyblue"
| "dimgray"
| "dimgrey"
| "dodgerblue"
| "firebrick"
| "floralwhite"
| "forestgreen"
| "fuchsia"
| "gainsboro"
| "ghostwhite"
| "gold"
| "goldenrod"
| "gray"
| "green"
| "greenyellow"
| "grey"
| "honeydew"
| "hotpink"
| "indianred"
| "indigo"
| "ivory"
| "khaki"
| "lavender"
| "lavenderblush"
| "lawngreen"
| "lemonchiffon"
| "lightblue"
| "lightcoral"
| "lightcyan"
| "lightgoldenrodyellow"
| "lightgray"
| "lightgreen"
| "lightgrey"
| "lightpink"
| "lightsalmon"
| "lightseagreen"
| "lightskyblue"
| "lightslategray"
| "lightslategrey"
| "lightsteelblue"
| "lightyellow"
| "lime"
| "limegreen"
| "linen"
| "magenta"
| "maroon"
| "mediumaquamarine"
| "mediumblue"
| "mediumorchid"
| "mediumpurple"
| "mediumseagreen"
| "mediumslateblue"
| "mediumspringgreen"
| "mediumturquoise"
| "mediumvioletred"
| "midnightblue"
| "mintcream"
| "mistyrose"
| "moccasin"
| "navajowhite"
| "navy"
| "oldlace"
| "olive"
| "olivedrab"
| "orange"
| "orangered"
| "orchid"
| "palegoldenrod"
| "palegreen"
| "paleturquoise"
| "palevioletred"
| "papayawhip"
| "peachpuff"
| "peru"
| "pink"
| "plum"
| "powderblue"
| "purple"
| "rebeccapurple"
| "red"
| "rosybrown"
| "royalblue"
| "saddlebrown"
| "salmon"
| "sandybrown"
| "seagreen"
| "seashell"
| "sienna"
| "silver"
| "skyblue"
| "slateblue"
| "slategray"
| "slategrey"
| "snow"
| "springgreen"
| "steelblue"
| "tan"
| "teal"
| "thistle"
| "tomato"
| "transparent"
| "turquoise"
| "violet"
| "wheat"
| "white"
| "whitesmoke"
| "yellow"
| "yellowgreen";

export type Rgb = TackyVariant<"rgb">;
export const rgb = (red: number, green: number, blue: number): Rgb =>
`rgb(${red}, ${green}, ${blue})` as Rgb;
Expand All @@ -12,4 +163,4 @@ export const rgba = (
alpha: number
): Rgba => `rgba(${red}, ${green}, ${blue}, ${alpha})` as Rgba;

export type CSSColor = Rgb | Rgba | "currentcolor" | "transparent";
export type CSSColor = Rgb | Rgba | "currentcolor" | NamedColor;
5 changes: 3 additions & 2 deletions packages/tacky-css/src/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ export type CSSURL = TackyVariant<"url">;
export const url = (url: URL): CSSURL => `url(${url})` as CSSURL;

export type FitContent = TackyVariant<"fitContent">;
export const fitContent = (arg: CSSLengthPercentage): FitContent =>
`fitContent(${arg})` as FitContent;
export const fitContent = <T extends string>(
arg: CSSLengthPercentage<T>
): FitContent => `fitContent(${arg})` as FitContent;
28 changes: 16 additions & 12 deletions packages/tacky-css/src/image.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BackgroundPositionArgs } from "./property";
import { CSSColor } from "./color";
import { CSSLengthPercentage, Percent, CSSAngle } from "./unit";
import { CSSLengthPercentage, CSSPercentage, CSSAngle } from "./unit";
import { TackyVariant } from "./types";
import { CSSURL } from "./function";

Expand All @@ -18,15 +18,17 @@ type SideOrCorner =
| "to right top"
| "to right bottom";

type InitialLinearColorStop = [
type InitialLinearColorStop<T extends string> = [
color: CSSColor,
stopStart?: CSSLengthPercentage,
stopEnd?: CSSLengthPercentage
stopStart?: CSSLengthPercentage<T>,
stopEnd?: CSSLengthPercentage<T>
];

type LinearColorStop = InitialLinearColorStop;
type ColorHint = Percent;
type LinearColorStopOrHint = LinearColorStop | [ColorHint, ...LinearColorStop];
type LinearColorStop<T extends string> = InitialLinearColorStop<T>;
type ColorHint<T extends string> = CSSPercentage<T>;
type LinearColorStopOrHint<T extends string> =
| LinearColorStop<T>
| [ColorHint<T>, ...LinearColorStop<T>];

// Ideally <color-hint> would be expressed in a way that matches the CSS syntax
// more closely, i.e.
Expand Down Expand Up @@ -58,11 +60,12 @@ type LinearColorStopOrHint = LinearColorStop | [ColorHint, ...LinearColorStop];
export interface LinearGradientFunction<Return> {
<
T extends [CSSAngle | SideOrCorner] | [],
V extends [LinearColorStopOrHint, ...LinearColorStopOrHint[]]
U extends string,
V extends [LinearColorStopOrHint<U>, ...LinearColorStopOrHint<U>[]]
>(
...args: [
...angle: T,
colorStop: InitialLinearColorStop,
colorStop: InitialLinearColorStop<U>,
...colorStopOrHint: V
]
): Return;
Expand Down Expand Up @@ -95,17 +98,18 @@ type RadialGradientExtentKeyword =
| "farthest-corner";
export interface RadialGradientFunction<Return> {
<
U extends string,
T extends
| [
RadialGradientShape | RadialGradientExtentKeyword,
...([] | ["at", ...BackgroundPositionArgs])
...([] | ["at", ...BackgroundPositionArgs<U>])
]
| [],
V extends [LinearColorStopOrHint, ...LinearColorStopOrHint[]]
V extends [LinearColorStopOrHint<U>, ...LinearColorStopOrHint<U>[]]
>(
...args: [
...shape: T,
colorStop: InitialLinearColorStop,
colorStop: InitialLinearColorStop<U>,
...colorStopOrHint: V
]
): Return;
Expand Down
2 changes: 1 addition & 1 deletion packages/tacky-css/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ type TackyArg = typeof tackyArg;

export const compile = (styles: TypedCSSArray): CSSObject =>
styles.reduce((acc, [key, value]) => {
// Investigate TS2590 without this cast
// TODO: Investigate TS2590 without this cast
acc[key as string] = Array.isArray(value) ? compile(value) : value;
return acc;
}, {} as CSSObject);
Expand Down
42 changes: 25 additions & 17 deletions packages/tacky-css/src/media/mediaFeatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,41 +17,48 @@ export const color = (bits?: number): MediaExpression =>
export const colorGamut = (gamut: "srgb" | "p3" | "rec2020"): MediaExpression =>
`(colorGamut: ${gamut})` as MediaExpression;

export const height = (value: CSSLength): MediaExpression =>
`(height: ${value})` as MediaExpression;
export const height = <T extends string>(
value: CSSLength<T>
): MediaExpression => `(height: ${value})` as MediaExpression;

export const hover = (support: "none" | "hover"): MediaExpression =>
`(hover: ${support})` as MediaExpression;

export const maxColor = (bits: number): MediaExpression =>
`(max-color: ${bits})` as MediaExpression;

export const maxHeight = (value: CSSLength): MediaExpression =>
`(max-height: ${value})` as MediaExpression;
export const maxHeight = <T extends string>(
value: CSSLength<T>
): MediaExpression => `(max-height: ${value})` as MediaExpression;

export const maxMonochrome = (bits: number): MediaExpression =>
`(max-monochrome: ${bits})` as MediaExpression;

export const maxResolution = (value: CSSResolution): MediaExpression =>
`(max-resolution: ${value})` as MediaExpression;
export const maxResolution = <T extends string>(
value: CSSResolution<T>
): MediaExpression => `(max-resolution: ${value})` as MediaExpression;

export const maxWidth = (value: CSSLength): MediaExpression =>
`(max-width: ${value})` as MediaExpression;
export const maxWidth = <T extends string>(
value: CSSLength<T>
): MediaExpression => `(max-width: ${value})` as MediaExpression;

export const minColor = (bits: number): MediaExpression =>
`(min-color: ${bits})` as MediaExpression;

export const minHeight = (value: CSSLength): MediaExpression =>
`(min-height: ${value})` as MediaExpression;
export const minHeight = <T extends string>(
value: CSSLength<T>
): MediaExpression => `(min-height: ${value})` as MediaExpression;

export const minMonochrome = (bits: number): MediaExpression =>
`(min-monochrome: ${bits})` as MediaExpression;

export const minResolution = (value: CSSResolution): MediaExpression =>
`(min-resolution: ${value})` as MediaExpression;
export const minResolution = <T extends string>(
value: CSSResolution<T>
): MediaExpression => `(min-resolution: ${value})` as MediaExpression;

export const minWidth = (value: CSSLength): MediaExpression =>
`(min-width: ${value})` as MediaExpression;
export const minWidth = <T extends string>(
value: CSSLength<T>
): MediaExpression => `(min-width: ${value})` as MediaExpression;

export const monochrome = (bits?: number): MediaExpression =>
`(monochrome${bits ? `: ${bits}` : ""})` as MediaExpression;
Expand All @@ -63,8 +70,9 @@ export const pointer = (
accuracy: "none" | "coarse" | "fine"
): MediaExpression => `(pointer: ${accuracy})` as MediaExpression;

export const resolution = (value: CSSResolution): MediaExpression =>
`(resolution: ${value})` as MediaExpression;
export const resolution = <T extends string>(
value: CSSResolution<T>
): MediaExpression => `(resolution: ${value})` as MediaExpression;

export const width = (value: CSSLength): MediaExpression =>
export const width = <T extends string>(value: CSSLength<T>): MediaExpression =>
`(width: ${value})` as MediaExpression;
Loading