diff --git a/.changeset/calm-heads-care.md b/.changeset/calm-heads-care.md index 19e9cdea..bf9b06b5 100644 --- a/.changeset/calm-heads-care.md +++ b/.changeset/calm-heads-care.md @@ -3,4 +3,8 @@ "@mincho-js/css": minor --- -Separate vanilla extract API to `./compat` +**Compatibility** + +## Changes + +- Separate vanilla extract API to `./compat` diff --git a/.changeset/cold-mirrors-accept.md b/.changeset/cold-mirrors-accept.md new file mode 100644 index 00000000..a315b21f --- /dev/null +++ b/.changeset/cold-mirrors-accept.md @@ -0,0 +1,9 @@ +--- +"@mincho-js/css": minor +--- + +**css** + +## New + +- Add `css.with()` API \ No newline at end of file diff --git a/.changeset/sad-meals-rescue.md b/.changeset/sad-meals-rescue.md index 1361f6cf..9b920515 100644 --- a/.changeset/sad-meals-rescue.md +++ b/.changeset/sad-meals-rescue.md @@ -3,4 +3,6 @@ "@mincho-js/css": minor --- -css.multiple() API +## New + +- Add `css.multiple()` API diff --git a/.changeset/sharp-ears-wear.md b/.changeset/sharp-ears-wear.md index 4ad32178..8791ea0a 100644 --- a/.changeset/sharp-ears-wear.md +++ b/.changeset/sharp-ears-wear.md @@ -2,6 +2,8 @@ "@mincho-js/css": patch --- +**Types** + ## New - Add `VariantStyle` type for constrained variant styles diff --git a/packages/css/src/compat.ts b/packages/css/src/compat.ts index fe092080..d929131d 100644 --- a/packages/css/src/compat.ts +++ b/packages/css/src/compat.ts @@ -21,7 +21,7 @@ export { export { globalCss as globalStyle, css as style, - cssVariants as styleVariants + cssMultiple as styleVariants } from "./css/index.js"; export type { diff --git a/packages/css/src/css/index.ts b/packages/css/src/css/index.ts index 06a2b539..0cccea70 100644 --- a/packages/css/src/css/index.ts +++ b/packages/css/src/css/index.ts @@ -13,6 +13,7 @@ import { setFileScope } from "@vanilla-extract/css/fileScope"; import { style as vStyle, globalStyle as gStyle } from "@vanilla-extract/css"; import type { GlobalStyleRule } from "@vanilla-extract/css"; import { className, getDebugName } from "../utils.js"; +import type { RestrictCSSRule } from "./types.js"; // == Global CSS =============================================================== export function globalCss(selector: string, rule: GlobalCSSRule) { @@ -109,27 +110,80 @@ function cssRaw(style: ComplexCSSRule) { return style; } +function cssWith( + callback?: (style: RestrictCSSRule) => ComplexCSSRule +) { + type RestrictedCSSRule = RestrictCSSRule; + const cssFunction = callback ?? ((style: RestrictedCSSRule) => style); + + function cssWithImpl(style: RestrictedCSSRule, debugId?: string) { + return cssImpl(cssFunction(style), debugId); + } + function cssWithRaw(style: RestrictedCSSRule) { + return cssRaw(cssFunction(style)); + } + + function cssWithVariants< + StyleMap extends Record + >(styleMap: StyleMap, debugId?: string): Record; + function cssWithVariants< + Data extends Record, + Key extends keyof Data, + MapData extends (value: Data[Key], key: Key) => ComplexCSSRule + >(data: Data, mapData: MapData, debugId?: string): Record; + function cssWithVariants< + Data extends Record, + MapData extends ( + value: unknown, + key: string | number | symbol + ) => ComplexCSSRule + >( + styleMapOrData: Data, + mapDataOrDebugId?: MapData | string, + debugId?: string + ): Record { + if (isMapDataFunction(mapDataOrDebugId)) { + return cssMultiple( + styleMapOrData, + (value, key) => mapDataOrDebugId(cssFunction(value), key), + debugId + ); + } else { + return cssMultiple(styleMapOrData, cssFunction, mapDataOrDebugId); + } + } + + return Object.assign(cssWithImpl, { + raw: cssWithRaw, + multiple: cssWithVariants + }); +} + export const css = Object.assign(cssImpl, { raw: cssRaw, - multiple: cssVariants + multiple: cssMultiple, + with: cssWith }); // == CSS Variants ============================================================= // TODO: Need to optimize // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get#smart_self-overwriting_lazy_getters // https://github.com/vanilla-extract-css/vanilla-extract/blob/master/packages/css/src/style.ts -export function cssVariants< +export function cssMultiple< StyleMap extends Record >(styleMap: StyleMap, debugId?: string): Record; -export function cssVariants< +export function cssMultiple< Data extends Record, Key extends keyof Data, MapData extends (value: Data[Key], key: Key) => ComplexCSSRule >(data: Data, mapData: MapData, debugId?: string): Record; -export function cssVariants< +export function cssMultiple< StyleMap extends Record, Data extends Record, - MapData extends (value: unknown, key: string | number) => ComplexCSSRule + MapData extends ( + value: unknown, + key: string | number | symbol + ) => ComplexCSSRule >( styleMapOrData: StyleMap | Data, mapDataOrDebugId?: MapData | string, @@ -217,6 +271,23 @@ if (import.meta.vitest) { }); }); + describe.concurrent("css.raw()", () => { + it("handles simple CSS properties", () => { + const style = { + color: "red", + fontSize: 16, + padding: "10px" + }; + const result = css.raw(style); + + expect(result).toEqual({ + color: "red", + fontSize: 16, + padding: "10px" + }); + }); + }); + describe.concurrent("css.multiple()", () => { it("Variants", () => { const result = css.multiple( @@ -271,7 +342,105 @@ if (import.meta.vitest) { className(`${debugId}_secondary`, "base") ); }); + }); + + describe.concurrent("css.with()", () => { + it("css.with() with type restrictions", () => { + const myCss = css.with<{ + color: true; + background: "blue" | "grey"; + border: false; + }>(); + + myCss({ + color: "red", // Allow all properties + background: "blue", // Only some properties are allowed + // @ts-expect-error: border is not allowed + border: "none" + }); + myCss({ + // @ts-expect-error: background is allowed only "blue" or "grey" + background: "red" + }); + }); + + it("Basic callback transformation", () => { + const withRedBackground = css.with((style) => ({ + ...style, + backgroundColor: "red" + })); + + const result = withRedBackground({ color: "blue" }, debugId); + + assert.isString(result); + expect(result).toMatch(className(debugId)); + }); - // TODO: Mocking globalCSS() for Variant Reference + it("css.with().raw()", () => { + const withRedBackground = css.with((style) => ({ + ...style, + backgroundColor: "red" + })); + + const result = withRedBackground.raw({ color: "blue" }); + + expect(result).toEqual({ + color: "blue", + backgroundColor: "red" + }); + }); + + it("css.with().multiple()", () => { + const withRedBackground = css.with((style) => ({ + ...style, + backgroundColor: "red" + })); + + const result = withRedBackground.multiple( + { + primary: { color: "blue" }, + secondary: { color: "green" } + }, + debugId + ); + + assert.hasAllKeys(result, ["primary", "secondary"]); + expect(result.primary).toMatch(className(`${debugId}_primary`)); + expect(result.secondary).toMatch(className(`${debugId}_secondary`)); + }); + + it("css.with() with like mixin", () => { + const myCss = css.with<{ size: number; radius?: number }>( + ({ size, radius = 10 }) => { + const styles: CSSRule = { + width: size, + height: size + }; + + if (radius !== 0) { + styles.borderRadius = radius; + } + + return styles; + } + ); + + expect(myCss.raw({ size: 100 })).toStrictEqual({ + width: 100, + height: 100, + borderRadius: 10 + }); + expect(myCss.raw({ size: 100, radius: 0 })).toStrictEqual({ + width: 100, + height: 100 + }); + expect(myCss.raw({ size: 100, radius: 100 })).toStrictEqual({ + width: 100, + height: 100, + borderRadius: 100 + }); + }); }); + + // TODO: Mocking globalCSS() for Variant Reference } diff --git a/packages/css/src/css/types.ts b/packages/css/src/css/types.ts new file mode 100644 index 00000000..52377bf1 --- /dev/null +++ b/packages/css/src/css/types.ts @@ -0,0 +1,23 @@ +import type { CSSRule } from "@mincho-js/transform-to-vanilla"; +import type { Resolve } from "../types.js"; + +type IsOptional = + Record extends Pick ? true : false; +type IsRequired = + IsOptional extends true ? false : true; + +export type RestrictCSSRule = { + [K in keyof T as T[K] extends false ? never : K]: T[K] extends true + ? K extends keyof CSSRule + ? CSSRule[K] + : never + : T[K]; +} extends infer U + ? Resolve< + { + [K in keyof U as IsRequired extends true ? K : never]-?: U[K]; + } & { + [K in keyof U as IsOptional extends true ? K : never]?: U[K]; + } + > + : never; diff --git a/packages/css/src/rules/index.ts b/packages/css/src/rules/index.ts index 50561bf5..796bd7dd 100644 --- a/packages/css/src/rules/index.ts +++ b/packages/css/src/rules/index.ts @@ -8,7 +8,7 @@ import type { PureCSSVarKey } from "@mincho-js/transform-to-vanilla"; -import { css, cssVariants } from "../css/index.js"; +import { css } from "../css/index.js"; import { className, getDebugName, getVarName } from "../utils.js"; import { createRuntimeFn } from "./createRuntimeFn.js"; import type { @@ -110,7 +110,7 @@ export function rules< // @ts-expect-error - Temporarily ignoring the error as the PatternResult type is not fully defined const variantClassNames: PatternResult["variantClassNames"] = mapValues(mergedVariants, (variantGroup, variantGroupName) => - cssVariants( + css.multiple( variantGroup, (styleRule) => typeof styleRule === "string" diff --git a/packages/css/src/rules/types.ts b/packages/css/src/rules/types.ts index 23012747..bb7b7484 100644 --- a/packages/css/src/rules/types.ts +++ b/packages/css/src/rules/types.ts @@ -5,10 +5,8 @@ import type { ResolvedProperties, NonNullableString } from "@mincho-js/transform-to-vanilla"; +import type { Resolve } from "../types.js"; -type Resolve = { - [Key in keyof T]: T[Key]; -} & {}; export type ResolveComplex = T extends Array ? Array> : Resolve; type RemoveUndefined = T extends undefined ? never : T; diff --git a/packages/css/src/types.ts b/packages/css/src/types.ts new file mode 100644 index 00000000..6905d358 --- /dev/null +++ b/packages/css/src/types.ts @@ -0,0 +1,3 @@ +export type Resolve = { + [Key in keyof T]: T[Key]; +} & {}; diff --git a/packages/css/tsconfig.lib.json b/packages/css/tsconfig.lib.json index f07d1760..8a55fa3d 100644 --- a/packages/css/tsconfig.lib.json +++ b/packages/css/tsconfig.lib.json @@ -3,6 +3,9 @@ "compilerOptions": { "rootDir": "./src", "baseUrl": "./", + "paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */ + "@/*": ["src/*"] + }, "tsBuildInfoFile": "./.cache/typescript/tsbuildinfo-esm", "outDir": "./dist/esm", "declarationDir": "./dist/esm"