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
7 changes: 7 additions & 0 deletions .changeset/dull-bugs-send.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@mincho-js/css": minor
---


**theme**
- Add `theme()` `this.raw()` API
106 changes: 93 additions & 13 deletions packages/css/src/theme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type WithOptionalLayer<T extends Theme> = T & {

interface ThemeSubFunctions {
fallbackVar: typeof fallbackVar;
raw(varReference: unknown): string;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix return type to match actual implementation.

The return type is declared as string, but the implementation returns CSSVarValue, which can be string | number | boolean | undefined (as seen in line 610 where primitives are returned directly). This type mismatch undermines type safety.

Apply this diff to fix the type signature:

-  raw(varReference: unknown): string;
+  raw(varReference: unknown): CSSVarValue;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
raw(varReference: unknown): string;
raw(varReference: unknown): CSSVarValue;
🤖 Prompt for AI Agents
In packages/css/src/theme/index.ts around line 38, the raw(varReference:
unknown): string; signature is incorrect because the implementation returns
CSSVarValue (string | number | boolean | undefined); change the declared return
type to CSSVarValue to match the implementation and restore type safety (i.e.,
raw(varReference: unknown): CSSVarValue;).

}

type ResolveThemeOutput<T extends Theme> = Resolve<ResolveTheme<T>>;
Expand Down Expand Up @@ -132,6 +133,14 @@ function assignTokensWithPrefix<ThemeTokens extends Theme>(
writable: false
});

// Add raw utility to extract actual values instead of var() references
Object.defineProperty(resolvedTokens, "raw", {
value: createRawExtractor(vars),
enumerable: false,
configurable: false,
writable: false
});

// Execute two-pass token resolution:
// 1. Assign CSS variables to all tokens
// 2. Resolve semantic token references
Expand Down Expand Up @@ -475,6 +484,25 @@ function setByPath(
}
}

/**
* Creates a function that extracts raw CSS values from var() references.
* Used to "hardcode" values instead of using CSS variable references.
*/
function createRawExtractor(vars: AssignedVars) {
return function raw(varReference: unknown) {
if (typeof varReference === "string") {
// Use getVarName utility to extract the CSS variable name
const varName = getVarName(varReference);
// If it was a var() reference and we have the value, return it
if (varName !== varReference && varName in vars) {
return vars[varName];
}
}
// If not a var reference or lookup failed, return as-is
return varReference;
};
}

// == Token Value Extractors ==================================================
function extractFontFamilyValue(value: TokenFontFamilyValue): CSSVarValue {
if (Array.isArray(value)) {
Expand Down Expand Up @@ -579,7 +607,7 @@ function extractTokenDefinitionValue(definition: TokenDefinition): CSSVarValue {

function extractCSSValue(value: TokenValue): CSSVarValue {
if (isPrimitive(value)) {
return String(value) as CSSVarValue;
return value as CSSVarValue;
}
Comment on lines 608 to 611
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Verify type consistency with CSSVarValue definition.

Primitives are now returned directly instead of being stringified. However, CSSVarValue is defined as `${string | number}` (a template literal type that resolves to string), which is inconsistent with returning primitive number, boolean, or undefined values.

Consider one of these approaches:

  1. Update the CSSVarValue type definition in packages/transform-to-vanilla/src/types/style-rule.ts:
export type CSSVarValue = string | number | boolean | undefined;
  1. Or revert to stringifying primitives if CSS variable values must be strings:
-    return value as CSSVarValue;
+    return String(value) as CSSVarValue;

The first approach aligns with the test expectations and new behavior, while the second maintains type consistency with the current CSSVarValue definition.

🤖 Prompt for AI Agents
In packages/css/src/theme/index.ts around lines 608 to 611, extractCSSValue
currently returns primitive values (number/boolean/undefined) but CSSVarValue is
defined as a template literal (`${string | number}`) which resolves to string
and is therefore inconsistent; fix by updating the CSSVarValue type in
packages/transform-to-vanilla/src/types/style-rule.ts to be a union that allows
primitives (e.g., string | number | boolean | undefined), update any
imports/usages if necessary, run typecheck/tests, and remove any code that
previously stringified primitives if present.


if (isTokenUnitValue(value)) {
Expand Down Expand Up @@ -687,9 +715,9 @@ if (import.meta.vitest) {
// Compare normalized values (without hashes)
expect(normalizeVars(result.vars)).toEqual({
"--color": "red",
"--size": "16",
"--enabled": "true",
"--nothing": "undefined"
"--size": 16,
"--enabled": true,
"--nothing": undefined
});

expect(normalizeResolvedTokens(result.resolvedTokens)).toEqual({
Expand All @@ -712,7 +740,7 @@ if (import.meta.vitest) {
expect(normalizeVars(result.vars)).toEqual({
"--background-color": "white",
"--font-size": "16px",
"--line-height": "1.5"
"--line-height": 1.5
});

expect(normalizeResolvedTokens(result.resolvedTokens)).toEqual({
Expand All @@ -730,11 +758,11 @@ if (import.meta.vitest) {
validateHashFormat(result.vars);

expect(normalizeVars(result.vars)).toEqual({
"--space-0": "2",
"--space-1": "4",
"--space-2": "8",
"--space-3": "16",
"--space-4": "32"
"--space-0": 2,
"--space-1": 4,
"--space-2": 8,
"--space-3": 16,
"--space-4": 32
});

expect(normalizeResolvedTokens(result.resolvedTokens)).toEqual({
Expand Down Expand Up @@ -866,6 +894,58 @@ if (import.meta.vitest) {
);
});

it("handles raw value extraction in semantic tokens", () => {
const result = assignTokens(
composedValue({
color: {
base: {
blue: "#0000ff",
red: "#ff0000"
},
semantic: {
get primary(): string {
// Use raw to get the actual value instead of var() reference
return this.raw(this.color.base.blue);
},
get danger(): string {
// Test raw with another token
return this.raw(this.color.base.red);
},
get custom(): string {
// Test raw with a non-var value
return this.raw("#00ff00");
}
}
},
space: {
base: [2, 4, 8, 16, 32, 64],
semantic: {
get small(): string {
return this.raw(this.space.base[1]);
},
get medium(): string {
return this.raw(this.space.base[3]);
},
get large(): string {
return this.raw(this.space.base[5]);
}
}
}
})
);

validateHashFormat(result.vars);

const normalizedVars = normalizeVars(result.vars);
// The semantic tokens should contain the raw values, not var() references
expect(normalizedVars["--color-semantic-primary"]).toBe("#0000ff");
expect(normalizedVars["--color-semantic-danger"]).toBe("#ff0000");
expect(normalizedVars["--color-semantic-custom"]).toBe("#00ff00");
expect(normalizedVars["--space-semantic-small"]).toBe(4);
expect(normalizedVars["--space-semantic-medium"]).toBe(16);
expect(normalizedVars["--space-semantic-large"]).toBe(64);
});

it("handles TokenUnitValue", () => {
const result = assignTokens({
spacing: { value: 1.5, unit: "rem" },
Expand Down Expand Up @@ -1170,14 +1250,14 @@ if (import.meta.vitest) {
validateHashFormat(result.vars);
const normalized = normalizeVars(result.vars);

expect(normalized["--typography-heading-sizes-0"]).toBe("48");
expect(normalized["--typography-heading-sizes-4"]).toBe("16");
expect(normalized["--typography-heading-sizes-0"]).toBe(48);
expect(normalized["--typography-heading-sizes-4"]).toBe(16);
expect(normalized["--typography-heading-weight"]).toBe("700");
expect(normalized["--typography-heading-family"]).toBe(
"Helvetica, sans-serif"
);
expect(normalized["--typography-body-size"]).toBe("14px");
expect(normalized["--typography-body-line-height"]).toBe("1.5");
expect(normalized["--typography-body-line-height"]).toBe(1.5);
expect(normalized["--colors-primary"]).toBe("#007bff");
expect(normalized["--colors-secondary"]).toBe("#6c757d");
});
Expand Down