Skip to content
Merged
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
9 changes: 9 additions & 0 deletions .changeset/sharp-ears-wear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@mincho-js/css": patch
---

## New
- Add `VariantStyle` type for constrained variant styles

## Changes
- Allow nested selector in `globalCss` function
83 changes: 82 additions & 1 deletion packages/css/src/css/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,91 @@ import { className, getDebugName } from "../utils.js";

// == Global CSS ===============================================================
export function globalCss(selector: string, rule: GlobalCSSRule) {
gStyle(selector, transform(rule) as GlobalStyleRule);
const transformedStyle = transform({
selectors: {
[selector]: {
...rule
}
}
}) as CSSRule;

const { selectors, ...atRuleStyles } = transformedStyle;
if (selectors !== undefined) {
Object.entries(selectors).forEach(([selector, styles]) => {
gStyle(selector, styles as GlobalStyleRule);
});
}

if (atRuleStyles !== undefined) {
const otherStyles = hoistSelectors(atRuleStyles);
Object.entries(otherStyles.selectors).forEach(([atRule, atRuleStyles]) => {
gStyle(atRule, atRuleStyles as GlobalStyleRule);
});
}
}
export const globalStyle = globalCss;

// TODO: Make more type-safe
type UnknownObject = Record<string, unknown>;
type CSSRuleMap = Record<string, CSSRule>;
interface HoistResult {
selectors: CSSRuleMap;
}

function hoistSelectors(input: CSSRule): HoistResult {
const result: HoistResult = {
selectors: {}
};

function processAtRules(obj: UnknownObject, path: string[] = []) {
for (const key in obj) {
if (key === "selectors") {
// Hoist each selector when selectors are found
const selectors = obj[key] as CSSRuleMap;
for (const selector in selectors) {
if (!result.selectors[selector]) {
result.selectors[selector] = {};
}

// Create nested object structure based on current path
let current = result.selectors[selector] as UnknownObject;
for (let i = 0; i < path.length; i += 2) {
const atRule = path[i];
const condition = path[i + 1];

if (!current[atRule]) {
current[atRule] = {};
}
const atRuleObj = current[atRule] as UnknownObject;
if (!atRuleObj[condition]) {
atRuleObj[condition] = {};
}

current = atRuleObj[condition] as UnknownObject;
}

// Copy style properties
Object.assign(current, selectors[selector]);
}
} else if (typeof obj[key] === "object" && obj[key] !== null) {
// at-rule found (e.g: @media, @supports)
const atRules = obj[key] as UnknownObject;
for (const condition in atRules) {
// Add current at-rule and condition to path and recursively call
processAtRules(atRules[condition] as UnknownObject, [
...path,
key,
condition
]);
}
}
}
}

processAtRules(input as UnknownObject);
return result;
}

// == CSS ======================================================================
export function css(style: ComplexCSSRule, debugId?: string) {
return vStyle(transform(style), debugId);
Expand Down
1 change: 1 addition & 0 deletions packages/css/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export {
export { rules, recipe } from "./rules/index.js";
export { createRuntimeFn } from "./rules/createRuntimeFn.js";
export type {
VariantStyle,
RulesVariants,
RecipeVariants,
RuntimeFn,
Expand Down
9 changes: 5 additions & 4 deletions packages/css/src/rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ import type {
PropTarget,
PropVars,
Serializable,
VariantStringMap
VariantStringMap,
VariantStyle
} from "./types.js";
import {
mapValues,
Expand Down Expand Up @@ -264,16 +265,16 @@ if (import.meta.vitest) {
color: {
brand: { color: "#FFFFA0" },
accent: { color: "#FFE4B5" }
},
} satisfies VariantStyle<"brand" | "accent">,
size: {
small: { padding: 12 },
medium: { padding: 16 },
large: { padding: 24 }
},
} satisfies VariantStyle<"small" | "medium" | "large">,
outlined: {
true: { border: "1px solid black" },
false: { border: "1px solid transparent" }
}
} satisfies VariantStyle<"true" | "false">
}
} as const;
const result = rules(variants, debugId);
Expand Down
8 changes: 8 additions & 0 deletions packages/css/src/rules/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ export type Serializable =

type RecipeStyleRule = ComplexCSSRule | string;

export type VariantStyle<
VariantNames extends string,
CssRule extends RecipeStyleRule = RecipeStyleRule
> = {
[VariantName in VariantNames]: CssRule;
};

// Same of VariantMap but for fast type checking
export type VariantDefinitions = Record<string, RecipeStyleRule>;

type BooleanMap<T> = T extends "true" | "false" ? boolean : T;
Expand Down
7 changes: 7 additions & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from "vitest/config";

export default defineConfig({
test: {
projects: ["packages/*/vite.config.ts"],
}
});
3 changes: 0 additions & 3 deletions vitest.workspace.ts

This file was deleted.