Skip to content

Commit

Permalink
Implement styled runtime
Browse files Browse the repository at this point in the history
  • Loading branch information
Brijesh Bittu committed Feb 3, 2025
1 parent 631b7c0 commit 6e259a9
Show file tree
Hide file tree
Showing 10 changed files with 259 additions and 45 deletions.
1 change: 1 addition & 0 deletions packages/pigment-css-react-new/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"dependencies": {
"@babel/plugin-syntax-jsx": "^7.25.9",
"@babel/types": "^7.25.8",
"@emotion/is-prop-valid": "^1.3.1",
"@pigment-css/core": "workspace:*",
"@pigment-css/utils": "workspace:*",
"@pigment-css/theme": "workspace:^",
Expand Down
13 changes: 0 additions & 13 deletions packages/pigment-css-react-new/src/runtime/styled.js

This file was deleted.

119 changes: 119 additions & 0 deletions packages/pigment-css-react-new/src/runtime/styled.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { Primitive } from '@pigment-css/core';
import { ClassInfo, css } from '@pigment-css/core/runtime';
import * as React from 'react';
import isPropValid from '@emotion/is-prop-valid';

type StyledInfo = ClassInfo & {
displayName?: string;
vars?: Record<string, [(...args: unknown[]) => Primitive, boolean]>;
};

function defaultShouldForwardProp(propName: string): boolean {
// if first character is $
if (propName.charCodeAt(0) === 36) {
return false;
}
if (propName === 'as') {
return false;
}
return true;
}

function shouldForwardProp(propName: string) {
if (defaultShouldForwardProp(propName)) {
return isPropValid(propName);
}
return false;
}

function getStyle(props: ClassInfo['defaultVariants'], vars: StyledInfo['vars']) {
const newStyle: Record<string, Primitive> = {};
if (!props || !vars) {
return newStyle;
}
// eslint-disable-next-line no-restricted-syntax
for (const key in vars) {
if (!vars.hasOwnProperty(key)) {
continue;
}
const [variableFunction, isUnitLess] = vars[key];
const value = variableFunction(props);
if (typeof value === 'undefined') {
continue;
}
if (typeof value === 'string' || isUnitLess) {
newStyle[key] = value;
} else {
newStyle[key] = `${value}px`;
}
}
return newStyle;
}

export function styled<T extends React.ElementType>(tag: T) {
if (process.env.NODE_ENV === 'development') {
if (tag === undefined) {
throw new Error(
'You are trying to create a styled element with an undefined component.\nYou may have forgotten to import it.',
);
}
}
const shouldForwardPropLocal =
typeof tag === 'string' ? shouldForwardProp : defaultShouldForwardProp;

function scopedStyled({
classes,
variants = [],
defaultVariants = {},
vars,
displayName = '',
}: StyledInfo) {
const cssFn = css({
classes,
variants,
defaultVariants,
});
const baseClasses = cssFn();

const StyledComponent = React.forwardRef<
React.ComponentRef<T>,
React.ComponentPropsWithoutRef<T> & {
as?: React.ElementType;
className?: string;
style?: React.CSSProperties;
}
>(function render(props, ref) {
const newProps: Record<string, unknown> = {};

// eslint-disable-next-line no-restricted-syntax
for (const key in props) {
// if first char is $
if (shouldForwardPropLocal(key)) {
newProps[key] = props[key];
}
}
newProps.className = variants.length === 0 ? baseClasses : baseClasses;
newProps.style = {
...props.style,
...getStyle(props, vars),
};

const Component = props.as ?? tag;
return <Component ref={ref} {...newProps} />;
});

if (displayName) {
StyledComponent.displayName = displayName;
} else {
StyledComponent.displayName = 'Styled(Pigment)';
}

// @ts-expect-error No TS check required
// eslint-disable-next-line no-underscore-dangle
StyledComponent.__styled_by_pigment_css = true;

return StyledComponent;
}

return scopedStyled;
}
76 changes: 75 additions & 1 deletion packages/pigment-css-react-new/src/styled.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,75 @@
export declare function styled(): void;
import type * as React from 'react';
import { Variants, BaseInterface, CssArg, VariantNames, Primitive } from '@pigment-css/core';

export type NoInfer<T> = [T][T extends any ? 0 : never];
type FastOmit<T extends object, U extends string | number | symbol> = {
[K in keyof T as K extends U ? never : K]: T[K];
};

export type Substitute<A extends object, B extends object> = FastOmit<A, keyof B> & B;

interface RequiredProps {
className?: string;
style?: React.CSSProperties;
}

export type PolymorphicComponentProps<
Props extends {},
AsTarget extends React.ElementType | undefined,
AsTargetProps extends object = AsTarget extends React.ElementType
? React.ComponentPropsWithRef<AsTarget>
: {},
> = NoInfer<Omit<Substitute<Props, AsTargetProps>, 'as'>> & {
as?: AsTarget;
children?: React.ReactNode;
};

export interface PolymorphicComponent<Props extends {}>
extends React.ForwardRefExoticComponent<Props> {
<AsTarget extends React.ElementType | undefined = undefined>(
props: PolymorphicComponentProps<Props, AsTarget>,
): React.JSX.Element;
}

type StyledArgument<V extends Variants> = CssArg<V>;

interface StyledComponent<Props extends {}> extends PolymorphicComponent<Props> {
defaultProps?: Partial<Props> | undefined;
toString: () => string;
}

interface StyledOptions extends BaseInterface {}

export interface CreateStyledComponent<
Component extends React.ElementType,
OuterProps extends object,
> {
<Props extends {} = {}>(
arg: TemplateStringsArray,
...templateArgs: (Primitive | ((props: Props) => Primitive))[]
): StyledComponent<Substitute<OuterProps, Props>> & (Component extends string ? {} : Component);

<V extends {}>(
...styles: Array<StyledArgument<V>>
): StyledComponent<Substitute<OuterProps, V extends Variants ? VariantNames<V> : V>> &
(Component extends string ? {} : Component);
}

export interface CreateStyled {
<
TagOrComponent extends React.ElementType,
FinalProps extends {} = React.ComponentPropsWithRef<TagOrComponent>,
>(
tag: TagOrComponent,
options?: StyledOptions,
): CreateStyledComponent<TagOrComponent, FinalProps>;
}

export type CreateStyledIndex = {
[Key in keyof React.JSX.IntrinsicElements]: CreateStyledComponent<
Key,
React.JSX.IntrinsicElements[Key]
>;
};

export declare const styled: CreateStyled & CreateStyledIndex;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function TestComponent() {
return <h1>Hello</h1>;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { styled, keyframes, css } from '@pigment-css/react-new';
import { TestComponent } from './dummy-component.fixture';

const cls1 = css({
color: 'red',
Expand All @@ -13,10 +14,6 @@ export const rotateKeyframe = keyframes({ className: 'rotate' })({
},
});

function TestComponent() {
return <h1>Hello</h1>;
}

const StyledTest = styled(TestComponent, {
className: 'StyledTest',
})({
Expand All @@ -27,9 +24,9 @@ const StyledTest = styled(TestComponent, {
[`.${cls1}`]: {
color: 'blue',
},
color(props) {
return props.size === 'small' ? 'red' : 'blue';
},
// color(props) {
// return props.size === 'small' ? 'red' : 'blue';
// },
variants: {
size: {
small: {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@ import {
styled as _styled6,
styled as _styled7,
} from '@pigment-css/react-new/runtime';
import { TestComponent } from './dummy-component.fixture';
export const rotateKeyframe = 'rotate';
function TestComponent() {
return <h1>Hello</h1>;
}
const _exp4 = /*#__PURE__*/ () => TestComponent;
const StyledTest = /*#__PURE__*/ _styled(_exp4())({
classes: 'StyledTest',
Expand Down
72 changes: 52 additions & 20 deletions packages/pigment-css-react-new/tests/styled/styled.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,52 @@
// import { styled } from '../../src/styled';

// const Button = styled('button')({
// color: 'red',
// variants: {
// hue: {
// primary: {
// color: 'red',
// backgroundColor: 'blue',
// },
// },
// },
// });

// // @ts-expect-error `download` does not exist on button
// <Button download>Hello</Button>;

// <Button as="a" download>
// Hello
// </Button>;
import { t } from '@pigment-css/theme';
import { styled } from '../../src/styled';

declare module '@pigment-css/theme' {
interface Theme {
palette: {
main: string;
};
}
}

const Button = styled('button')({
color: 'red',
variants: {
btnSize: {
small: {
padding: 0,
},
medium: {
padding: '1rem',
},
large: {
padding: '2rem',
},
},
},
});

const Div1 = styled.div<{ $size?: 'small' | 'medium' | 'large' }>`
color: red;
padding: ${({ $size }) => ($size === 'small' ? 2 : 4)};
`;

<Div1 onClick={() => undefined}>
<Button type="button" btnSize="medium" />;
</Div1>;

const Button2 = styled('button')<{ $isRed: boolean }>({
color: 'red',
backgroundColor: 'red',
});

<Button2 $isRed className="" />;

function TestComponent({}: { className?: string; style?: React.CSSProperties; hello: string }) {
return <div>Hello</div>;
}

styled(TestComponent)`
color: red;
background-color: ${t('$palette.main')};
`;
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 6e259a9

Please sign in to comment.