Skip to content

Commit 6e259a9

Browse files
author
Brijesh Bittu
committed
Implement styled runtime
1 parent 631b7c0 commit 6e259a9

File tree

10 files changed

+259
-45
lines changed

10 files changed

+259
-45
lines changed

packages/pigment-css-react-new/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"dependencies": {
3434
"@babel/plugin-syntax-jsx": "^7.25.9",
3535
"@babel/types": "^7.25.8",
36+
"@emotion/is-prop-valid": "^1.3.1",
3637
"@pigment-css/core": "workspace:*",
3738
"@pigment-css/utils": "workspace:*",
3839
"@pigment-css/theme": "workspace:^",

packages/pigment-css-react-new/src/runtime/styled.js

Lines changed: 0 additions & 13 deletions
This file was deleted.
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { Primitive } from '@pigment-css/core';
2+
import { ClassInfo, css } from '@pigment-css/core/runtime';
3+
import * as React from 'react';
4+
import isPropValid from '@emotion/is-prop-valid';
5+
6+
type StyledInfo = ClassInfo & {
7+
displayName?: string;
8+
vars?: Record<string, [(...args: unknown[]) => Primitive, boolean]>;
9+
};
10+
11+
function defaultShouldForwardProp(propName: string): boolean {
12+
// if first character is $
13+
if (propName.charCodeAt(0) === 36) {
14+
return false;
15+
}
16+
if (propName === 'as') {
17+
return false;
18+
}
19+
return true;
20+
}
21+
22+
function shouldForwardProp(propName: string) {
23+
if (defaultShouldForwardProp(propName)) {
24+
return isPropValid(propName);
25+
}
26+
return false;
27+
}
28+
29+
function getStyle(props: ClassInfo['defaultVariants'], vars: StyledInfo['vars']) {
30+
const newStyle: Record<string, Primitive> = {};
31+
if (!props || !vars) {
32+
return newStyle;
33+
}
34+
// eslint-disable-next-line no-restricted-syntax
35+
for (const key in vars) {
36+
if (!vars.hasOwnProperty(key)) {
37+
continue;
38+
}
39+
const [variableFunction, isUnitLess] = vars[key];
40+
const value = variableFunction(props);
41+
if (typeof value === 'undefined') {
42+
continue;
43+
}
44+
if (typeof value === 'string' || isUnitLess) {
45+
newStyle[key] = value;
46+
} else {
47+
newStyle[key] = `${value}px`;
48+
}
49+
}
50+
return newStyle;
51+
}
52+
53+
export function styled<T extends React.ElementType>(tag: T) {
54+
if (process.env.NODE_ENV === 'development') {
55+
if (tag === undefined) {
56+
throw new Error(
57+
'You are trying to create a styled element with an undefined component.\nYou may have forgotten to import it.',
58+
);
59+
}
60+
}
61+
const shouldForwardPropLocal =
62+
typeof tag === 'string' ? shouldForwardProp : defaultShouldForwardProp;
63+
64+
function scopedStyled({
65+
classes,
66+
variants = [],
67+
defaultVariants = {},
68+
vars,
69+
displayName = '',
70+
}: StyledInfo) {
71+
const cssFn = css({
72+
classes,
73+
variants,
74+
defaultVariants,
75+
});
76+
const baseClasses = cssFn();
77+
78+
const StyledComponent = React.forwardRef<
79+
React.ComponentRef<T>,
80+
React.ComponentPropsWithoutRef<T> & {
81+
as?: React.ElementType;
82+
className?: string;
83+
style?: React.CSSProperties;
84+
}
85+
>(function render(props, ref) {
86+
const newProps: Record<string, unknown> = {};
87+
88+
// eslint-disable-next-line no-restricted-syntax
89+
for (const key in props) {
90+
// if first char is $
91+
if (shouldForwardPropLocal(key)) {
92+
newProps[key] = props[key];
93+
}
94+
}
95+
newProps.className = variants.length === 0 ? baseClasses : baseClasses;
96+
newProps.style = {
97+
...props.style,
98+
...getStyle(props, vars),
99+
};
100+
101+
const Component = props.as ?? tag;
102+
return <Component ref={ref} {...newProps} />;
103+
});
104+
105+
if (displayName) {
106+
StyledComponent.displayName = displayName;
107+
} else {
108+
StyledComponent.displayName = 'Styled(Pigment)';
109+
}
110+
111+
// @ts-expect-error No TS check required
112+
// eslint-disable-next-line no-underscore-dangle
113+
StyledComponent.__styled_by_pigment_css = true;
114+
115+
return StyledComponent;
116+
}
117+
118+
return scopedStyled;
119+
}
Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,75 @@
1-
export declare function styled(): void;
1+
import type * as React from 'react';
2+
import { Variants, BaseInterface, CssArg, VariantNames, Primitive } from '@pigment-css/core';
3+
4+
export type NoInfer<T> = [T][T extends any ? 0 : never];
5+
type FastOmit<T extends object, U extends string | number | symbol> = {
6+
[K in keyof T as K extends U ? never : K]: T[K];
7+
};
8+
9+
export type Substitute<A extends object, B extends object> = FastOmit<A, keyof B> & B;
10+
11+
interface RequiredProps {
12+
className?: string;
13+
style?: React.CSSProperties;
14+
}
15+
16+
export type PolymorphicComponentProps<
17+
Props extends {},
18+
AsTarget extends React.ElementType | undefined,
19+
AsTargetProps extends object = AsTarget extends React.ElementType
20+
? React.ComponentPropsWithRef<AsTarget>
21+
: {},
22+
> = NoInfer<Omit<Substitute<Props, AsTargetProps>, 'as'>> & {
23+
as?: AsTarget;
24+
children?: React.ReactNode;
25+
};
26+
27+
export interface PolymorphicComponent<Props extends {}>
28+
extends React.ForwardRefExoticComponent<Props> {
29+
<AsTarget extends React.ElementType | undefined = undefined>(
30+
props: PolymorphicComponentProps<Props, AsTarget>,
31+
): React.JSX.Element;
32+
}
33+
34+
type StyledArgument<V extends Variants> = CssArg<V>;
35+
36+
interface StyledComponent<Props extends {}> extends PolymorphicComponent<Props> {
37+
defaultProps?: Partial<Props> | undefined;
38+
toString: () => string;
39+
}
40+
41+
interface StyledOptions extends BaseInterface {}
42+
43+
export interface CreateStyledComponent<
44+
Component extends React.ElementType,
45+
OuterProps extends object,
46+
> {
47+
<Props extends {} = {}>(
48+
arg: TemplateStringsArray,
49+
...templateArgs: (Primitive | ((props: Props) => Primitive))[]
50+
): StyledComponent<Substitute<OuterProps, Props>> & (Component extends string ? {} : Component);
51+
52+
<V extends {}>(
53+
...styles: Array<StyledArgument<V>>
54+
): StyledComponent<Substitute<OuterProps, V extends Variants ? VariantNames<V> : V>> &
55+
(Component extends string ? {} : Component);
56+
}
57+
58+
export interface CreateStyled {
59+
<
60+
TagOrComponent extends React.ElementType,
61+
FinalProps extends {} = React.ComponentPropsWithRef<TagOrComponent>,
62+
>(
63+
tag: TagOrComponent,
64+
options?: StyledOptions,
65+
): CreateStyledComponent<TagOrComponent, FinalProps>;
66+
}
67+
68+
export type CreateStyledIndex = {
69+
[Key in keyof React.JSX.IntrinsicElements]: CreateStyledComponent<
70+
Key,
71+
React.JSX.IntrinsicElements[Key]
72+
>;
73+
};
74+
75+
export declare const styled: CreateStyled & CreateStyledIndex;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function TestComponent() {
2+
return <h1>Hello</h1>;
3+
}

packages/pigment-css-react-new/tests/styled/fixtures/styled.input.js

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { styled, keyframes, css } from '@pigment-css/react-new';
2+
import { TestComponent } from './dummy-component.fixture';
23

34
const cls1 = css({
45
color: 'red',
@@ -13,10 +14,6 @@ export const rotateKeyframe = keyframes({ className: 'rotate' })({
1314
},
1415
});
1516

16-
function TestComponent() {
17-
return <h1>Hello</h1>;
18-
}
19-
2017
const StyledTest = styled(TestComponent, {
2118
className: 'StyledTest',
2219
})({
@@ -27,9 +24,9 @@ const StyledTest = styled(TestComponent, {
2724
[`.${cls1}`]: {
2825
color: 'blue',
2926
},
30-
color(props) {
31-
return props.size === 'small' ? 'red' : 'blue';
32-
},
27+
// color(props) {
28+
// return props.size === 'small' ? 'red' : 'blue';
29+
// },
3330
variants: {
3431
size: {
3532
small: {

packages/pigment-css-react-new/tests/styled/fixtures/styled.output.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/pigment-css-react-new/tests/styled/fixtures/styled.output.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,8 @@ import {
88
styled as _styled6,
99
styled as _styled7,
1010
} from '@pigment-css/react-new/runtime';
11+
import { TestComponent } from './dummy-component.fixture';
1112
export const rotateKeyframe = 'rotate';
12-
function TestComponent() {
13-
return <h1>Hello</h1>;
14-
}
1513
const _exp4 = /*#__PURE__*/ () => TestComponent;
1614
const StyledTest = /*#__PURE__*/ _styled(_exp4())({
1715
classes: 'StyledTest',
Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,52 @@
1-
// import { styled } from '../../src/styled';
2-
3-
// const Button = styled('button')({
4-
// color: 'red',
5-
// variants: {
6-
// hue: {
7-
// primary: {
8-
// color: 'red',
9-
// backgroundColor: 'blue',
10-
// },
11-
// },
12-
// },
13-
// });
14-
15-
// // @ts-expect-error `download` does not exist on button
16-
// <Button download>Hello</Button>;
17-
18-
// <Button as="a" download>
19-
// Hello
20-
// </Button>;
1+
import { t } from '@pigment-css/theme';
2+
import { styled } from '../../src/styled';
3+
4+
declare module '@pigment-css/theme' {
5+
interface Theme {
6+
palette: {
7+
main: string;
8+
};
9+
}
10+
}
11+
12+
const Button = styled('button')({
13+
color: 'red',
14+
variants: {
15+
btnSize: {
16+
small: {
17+
padding: 0,
18+
},
19+
medium: {
20+
padding: '1rem',
21+
},
22+
large: {
23+
padding: '2rem',
24+
},
25+
},
26+
},
27+
});
28+
29+
const Div1 = styled.div<{ $size?: 'small' | 'medium' | 'large' }>`
30+
color: red;
31+
padding: ${({ $size }) => ($size === 'small' ? 2 : 4)};
32+
`;
33+
34+
<Div1 onClick={() => undefined}>
35+
<Button type="button" btnSize="medium" />;
36+
</Div1>;
37+
38+
const Button2 = styled('button')<{ $isRed: boolean }>({
39+
color: 'red',
40+
backgroundColor: 'red',
41+
});
42+
43+
<Button2 $isRed className="" />;
44+
45+
function TestComponent({}: { className?: string; style?: React.CSSProperties; hello: string }) {
46+
return <div>Hello</div>;
47+
}
48+
49+
styled(TestComponent)`
50+
color: red;
51+
background-color: ${t('$palette.main')};
52+
`;

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)