From 1c45d4afbede05ef311fae4b41770949b5654b53 Mon Sep 17 00:00:00 2001 From: Samuel Hobl Date: Sun, 25 Apr 2021 18:24:58 +0200 Subject: [PATCH] push --- .eslintrc.js | 1 + package.json | 2 + packages/interpolate-new/package.json | 35 +++ packages/interpolate-new/readme.md | 12 + packages/interpolate-new/src/index.ts | 2 + packages/interpolate-new/src/interpolate.ts | 178 +++++++++++++++ .../src/test/interpolate.test.ts | 175 ++++++++++++++ packages/interpolate-new/src/test/theme.ts | 39 ++++ packages/interpolate-new/src/types.ts | 211 +++++++++++++++++ packages/interpolate-new/theme.d.ts | 37 +++ packages/interpolate-new/theme.dev.d.ts | 7 + packages/interpolate-new/tsconfig.json | 4 + packages/jsx/package.json | 5 +- packages/jsx/src/Gumption.ts | 46 ---- packages/jsx/src/gumption-element.ts | 69 ++++++ packages/jsx/src/index.ts | 31 +-- packages/jsx/src/jsx-dev-runtime.ts | 38 +--- packages/jsx/src/jsx-namespace.ts | 18 +- packages/jsx/src/jsx-runtime.ts | 51 ++--- packages/jsx/src/parseProps.ts | 42 ---- .../test/__snapshots__/index.test.tsx.snap | 21 +- packages/jsx/src/test/index.test.tsx | 25 +- packages/jsx/src/types.ts | 12 +- packages/kwark/src/kwark.ts | 1 + .../test/__snapshots__/index.test.tsx.snap | 29 +-- packages/kwark/src/test/index.test.tsx | 12 +- packages/otion/.eslintrc.js | 7 + packages/otion/jsx-dev-runtime/package.json | 16 ++ packages/otion/jsx-runtime/package.json | 16 ++ packages/otion/package.json | 49 ++++ packages/otion/readme.md | 13 ++ packages/otion/src/index.ts | 38 ++++ packages/otion/src/jsx-dev-runtime.ts | 34 +++ packages/otion/src/jsx-namespace.ts | 33 +++ packages/otion/src/jsx-runtime.ts | 29 +++ packages/otion/src/parseProps.ts | 108 +++++++++ .../src/test/__snapshots__/jsx.test.tsx.snap | 69 ++++++ packages/otion/src/test/interpolate.test.ts | 131 +++++++++++ packages/otion/src/test/jsx.test.tsx | 213 ++++++++++++++++++ packages/otion/src/types.ts | 133 +++++++++++ packages/otion/src/useStyleConfig.ts | 18 ++ packages/{jsx => otion}/src/utils.ts | 0 packages/otion/theme.d.ts | 7 + packages/otion/tsconfig.json | 4 + packages/quark/src/quark.tsx | 1 + packages/theme-base/index.ts | 21 +- packages/theme-base/package.json | 3 - packages/utils/src/index.ts | 10 +- packages/utils/src/types.ts | 4 +- yarn.lock | 5 - 50 files changed, 1808 insertions(+), 257 deletions(-) create mode 100644 packages/interpolate-new/package.json create mode 100644 packages/interpolate-new/readme.md create mode 100644 packages/interpolate-new/src/index.ts create mode 100644 packages/interpolate-new/src/interpolate.ts create mode 100644 packages/interpolate-new/src/test/interpolate.test.ts create mode 100644 packages/interpolate-new/src/test/theme.ts create mode 100644 packages/interpolate-new/src/types.ts create mode 100644 packages/interpolate-new/theme.d.ts create mode 100644 packages/interpolate-new/theme.dev.d.ts create mode 100644 packages/interpolate-new/tsconfig.json delete mode 100644 packages/jsx/src/Gumption.ts create mode 100644 packages/jsx/src/gumption-element.ts delete mode 100644 packages/jsx/src/parseProps.ts create mode 100644 packages/otion/.eslintrc.js create mode 100644 packages/otion/jsx-dev-runtime/package.json create mode 100644 packages/otion/jsx-runtime/package.json create mode 100644 packages/otion/package.json create mode 100644 packages/otion/readme.md create mode 100644 packages/otion/src/index.ts create mode 100644 packages/otion/src/jsx-dev-runtime.ts create mode 100644 packages/otion/src/jsx-namespace.ts create mode 100644 packages/otion/src/jsx-runtime.ts create mode 100644 packages/otion/src/parseProps.ts create mode 100644 packages/otion/src/test/__snapshots__/jsx.test.tsx.snap create mode 100644 packages/otion/src/test/interpolate.test.ts create mode 100644 packages/otion/src/test/jsx.test.tsx create mode 100644 packages/otion/src/types.ts create mode 100644 packages/otion/src/useStyleConfig.ts rename packages/{jsx => otion}/src/utils.ts (100%) create mode 100644 packages/otion/theme.d.ts create mode 100644 packages/otion/tsconfig.json diff --git a/.eslintrc.js b/.eslintrc.js index 26d1dcf..9084100 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -36,6 +36,7 @@ module.exports = { '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/naming-convention': 'off', + '@typescript-eslint/no-unnecessary-condition': ['error'], 'react/destructuring-assignment': 'off', 'react/prop-types': 'off', 'react/require-default-props': 'off', diff --git a/package.json b/package.json index 3b3f369..288409c 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,10 @@ "workspaces": [ "packages/utils", "packages/interpolate", + "packages/interpolate-new", "packages/integral", "packages/jsx", + "packages/otion", "packages/quark", "packages/kwark", "packages/theme-base", diff --git a/packages/interpolate-new/package.json b/packages/interpolate-new/package.json new file mode 100644 index 0000000..ac05719 --- /dev/null +++ b/packages/interpolate-new/package.json @@ -0,0 +1,35 @@ +{ + "name": "@gumption-ui/interpolate-new", + "version": "0.0.1", + "private": true, + "license": "MIT", + "main": "./dist/index.js", + "module": "./dist/index.esm.js", + "source": "./src/index.ts", + "types": "./dist/index.d.ts", + "files": [ + "dist/", + "src/", + "!*.test.*" + ], + "scripts": { + "build": "microbundle --no-compress --tsconfig tsconfig.json --globals @gumption-ui/utils=utils", + "develop": "microbundle watch --no-compress --tsconfig tsconfig.json --globals @gumption-ui/utils=utils", + "typecheck": "tsc --noEmit", + "test": "jest" + }, + "jest": { + "modulePathIgnorePatterns": [ + "dist" + ], + "preset": "ts-jest", + "testEnvironment": "node" + }, + "dependencies": { + "@gumption-ui/utils": "0.0.1", + "csstype": "^3.0.6" + }, + "devDependencies": { + "@gumption-ui/theme-base": "0.0.1" + } +} diff --git a/packages/interpolate-new/readme.md b/packages/interpolate-new/readme.md new file mode 100644 index 0000000..7c1ebf0 --- /dev/null +++ b/packages/interpolate-new/readme.md @@ -0,0 +1,12 @@ +# Interpolate + +## Installation + +Install `@gumption-ui/interpolate-new` + +# Inspiration + +- https://github.com/system-ui/theme-ui +- https://github.com/styled-system/styled-system +- https://github.com/kripod/glaze +- https://github.com/jamesknelson/use-sx diff --git a/packages/interpolate-new/src/index.ts b/packages/interpolate-new/src/index.ts new file mode 100644 index 0000000..f79c888 --- /dev/null +++ b/packages/interpolate-new/src/index.ts @@ -0,0 +1,2 @@ +export * from './interpolate'; +export * from './types'; diff --git a/packages/interpolate-new/src/interpolate.ts b/packages/interpolate-new/src/interpolate.ts new file mode 100644 index 0000000..f04fbc4 --- /dev/null +++ b/packages/interpolate-new/src/interpolate.ts @@ -0,0 +1,178 @@ +import { get, isFunction } from '@gumption-ui/utils'; +import { + Shorthands, + Aliases, + Theme, + GumptionUIStyleObject, + GumptionUICSSObject, + CSSObject, +} from './types'; + +const transforms = [ + 'margin', + 'marginTop', + 'marginRight', + 'marginBottom', + 'marginLeft', + 'marginX', + 'marginY', + 'top', + 'bottom', + 'left', + 'right', +].reduce( + (acc, curr) => ({ + ...acc, + [curr]: positiveOrNegative, + }), + {}, +); + +type CSSPropsArgument = { theme: Theme } | Theme; + +type FuncsArg> = { + styles: T; + theme: T; +}; +type Funcs = Array<(funcsArgs: FuncsArg) => FuncsArg>; + +export const interpolate = < + T extends Record = GumptionUIStyleObject, + R = CSSObject +>( + ...funcs: Funcs +) => (args?: T) => (props: CSSPropsArgument = {}): R => { + const theme = 'theme' in props ? props.theme : props; + const result: any = {}; + + funcs.push(getObjectWithVariants); + + const { styles } = pipe(...funcs)({ + styles: isFunction(args) ? args(theme) : args || {}, + theme, + }); + + // eslint-disable-next-line guard-for-in, no-restricted-syntax + for (const alias in styles) { + const value = styles[alias as keyof GumptionUICSSObject]; + + if (value != null) { + const { aliases, shorthands } = theme; + + const shorthand = + aliases && alias in aliases + ? aliases[alias as Aliases] + : (alias as Exclude); + + const properties = + shorthands && shorthand in shorthands + ? shorthands[shorthand as Shorthands] + : [shorthand as Exclude]; + + // eslint-disable-next-line no-plusplus + for (let i = 0, len = properties.length; i < len; i++) { + const property = properties[i]; + + if (typeof value === 'object') { + result[property] = interpolate(...funcs)( + (value as unknown) as GumptionUICSSObject, + )(theme); + } else { + const { matchers = {}, scales = {} } = theme; + const scaleName = get(matchers, property); + const scale = get(scales, scaleName); + + if (!scale) { + result[property] = value; + continue; // eslint-disable-line no-continue + } + + const transform = get(transforms, property, get); + + /* + * `value` can be: + * a) a function + * b) reference to another token value + * c) scale value + * d) css value + */ + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore: TODO ThemedStyle currently does not support functions as value + let val = isFunction(value) ? value(theme) : value; + const scaleValue = + typeof val === 'number' ? scale[val] : get(scale, val as string); + + const refrenceScaleValue = get(scale, scaleValue || ''); + + val = refrenceScaleValue ?? scaleValue ?? val; + result[property] = transform(scale, val, val); + } + } + } + } + return result; +}; + +function getObjectWithVariants({ + styles, + theme, +}: { + styles: GumptionUICSSObject; + theme: Theme; +}) { + if (styles.variant) { + const { variant, ...restStyle } = styles; + let next: any = restStyle; + + const variants = + typeof variant === 'string' ? variant.split(' ') : [variant]; + + for (let i = 0, len = variants.length; i < len; i += 1) { + const variant = variants[i]; // eslint-disable-line @typescript-eslint/no-shadow + + const { styles: variantStyles } = getObjectWithVariants({ + styles: get(theme, `variants.${variant as string}`, {}), + theme, + }); + + next = { ...next, ...variantStyles }; + } + + return { + styles: next, + theme, + }; + } + + return { + styles, + theme, + }; +} + +function positiveOrNegative( + scale: Record, + value: string | number, +): string | number { + if (typeof value !== 'number' || value >= 0) { + if (typeof value === 'string' && value.startsWith('-')) { + const valueWithoutMinus = value.substring(1); + const n = get(scale, valueWithoutMinus, valueWithoutMinus); + if (typeof n === 'number') return Number(n) * -1; + return `-${n}`; + } + return get(scale, value, value); + } + const absolute = Math.abs(value); + const n = get(scale, absolute, absolute); + if (typeof n === 'string') return `-${n}`; + return Number(n) * -1; +} + +function pipe(...fns: Array<(a: R) => R>) { + return fns.reduce( + (f, g) => (...value) => f(g(...value)), + (value) => value, + ); +} diff --git a/packages/interpolate-new/src/test/interpolate.test.ts b/packages/interpolate-new/src/test/interpolate.test.ts new file mode 100644 index 0000000..f96fac1 --- /dev/null +++ b/packages/interpolate-new/src/test/interpolate.test.ts @@ -0,0 +1,175 @@ +import { get } from '@gumption-ui/utils'; +import { theme } from './theme'; +import { interpolate as createInterpolate } from '..'; + +const interpolate = createInterpolate(); + +describe('interpolate', () => { + test('returns an object', () => { + const result = interpolate()(); + expect(typeof result).toBe('object'); + }); + + test('returns styles', () => { + const result = interpolate({ + fontSize: 32, + color: 'blue', + borderRadius: 4, + })(); + expect(result).toEqual({ + fontSize: 32, + color: 'blue', + borderRadius: 4, + }); + }); + + test('returns interpolated styles', () => { + const result = interpolate({ + color: 'primary', + })(theme); + expect(result).toEqual({ + color: 'tomato', + }); + }); + + test('returns interpolated styles from function', () => { + const result = interpolate((t) => ({ + color: t.scales?.colors.primary, + }))(theme); + expect(result).toEqual({ + color: 'tomato', + }); + }); + + test('returns interpolated styles as function', () => { + const result = interpolate({ + color: (t) => t.scales?.colors.primary, + })(theme); + expect(result).toEqual({ + color: 'tomato', + }); + }); + + test('returns variants from theme', () => { + const result = interpolate({ + variant: 'button.primary', + })(theme); + expect(result).toEqual({ + backgroundColor: 'tomato', + }); + }); + + test('returns nested variants from theme', () => { + const result = interpolate({ + variant: 'button.round', + })(theme); + expect(result).toEqual({ + fontSize: 16, + overflow: 'hidden', + borderRadius: '50%', + }); + }); + + test('returns nested interpolated styles', () => { + const result = interpolate({ + ':hover': { + color: 'primary', + }, + })(theme); + expect(result).toEqual({ + ':hover': { + color: 'tomato', + }, + }); + }); + + test('returns multiple variants from theme', () => { + const result = interpolate({ + variant: 'button.primary button.sm', + })(theme); + expect(result).toEqual({ + backgroundColor: 'tomato', + fontSize: 16, + }); + }); + + test('returns pseudo selectors interpolated styles', () => { + const result = interpolate({ + ':hover': { + color: 'primary', + }, + })({ theme }); + expect(result).toEqual({ + ':hover': { + color: 'tomato', + }, + }); + }); + + test('handles aliases and shorthands', () => { + const result = interpolate({ + px: 'small', + })(theme); + expect(result).toEqual({ + paddingLeft: 16, + paddingRight: 16, + }); + }); + + test('handles negative margins from scale', () => { + const result = interpolate({ + mt: '-medium', + mx: '-large', + })(theme); + expect(result).toEqual({ + marginTop: -24, + marginLeft: -32, + marginRight: -32, + }); + }); + + test('handles negative top, left, bottom, and right from scale', () => { + const result = interpolate({ + top: '-x-small', + right: '-large', + bottom: '-medium', + left: '-small', + })(theme); + expect(result).toEqual({ + top: -8, + right: -32, + bottom: -24, + left: -16, + }); + }); + + test('handles negative margins from scale that is an object', () => { + const result = interpolate({ + mt: '-small', + mx: '-large', + })(theme); + expect(result).toEqual({ + marginTop: -16, + marginLeft: -32, + marginRight: -32, + }); + }); + + test('value as a function', () => { + const result = interpolate({ + color: (t) => get(t, 'scales.colors.gray.0'), + })(theme); + expect(result).toEqual({ + color: '#F8F9F9', + }); + }); + + test('nested scale tokens', () => { + const result = interpolate({ + color: 'text.subtle', + })(theme); + expect(result).toEqual({ + color: '#F8F9F9', + }); + }); +}); diff --git a/packages/interpolate-new/src/test/theme.ts b/packages/interpolate-new/src/test/theme.ts new file mode 100644 index 0000000..356c313 --- /dev/null +++ b/packages/interpolate-new/src/test/theme.ts @@ -0,0 +1,39 @@ +import { theme as baseTheme } from '@gumption-ui/theme-base'; + +const variants = { + text: { + caps: { + fontSize: ['small', 'medium'], + textDecoration: 'uppercase', + }, + }, + + button: { + primary: { + bg: 'primary', + }, + + secondary: { + bg: 'secondary', + }, + + lg: { + fontSize: 'x-large', + }, + + sm: { + fontSize: 'x-small', + }, + + round: { + variant: 'button.sm', + overflow: 'hidden', + borderRadius: '50%', + }, + }, +}; + +export const theme = { + ...baseTheme, + variants, +} as const; diff --git a/packages/interpolate-new/src/types.ts b/packages/interpolate-new/src/types.ts new file mode 100644 index 0000000..51f08ce --- /dev/null +++ b/packages/interpolate-new/src/types.ts @@ -0,0 +1,211 @@ +import * as CSS from 'csstype'; +import type { ThemeOrAny } from '@gumption-ui/interpolate-new/theme'; +import { LiteralUnion, ValueOf, Empty } from '@gumption-ui/utils'; + +type Modify = Omit & R; + +export type Theme = Partial; + +export type Tokens = Extract< + keyof ThemeOrAny[T], + string | number +>; +export type Matchers = Tokens<'matchers'>; +export type Shorthands = Tokens<'shorthands'>; +export type Aliases = Tokens<'aliases'>; +export type Variants = LiteralUnion, string>; + +type ResolveShorthand = ValueOf< + ThemeOrAny['shorthands'][T], + number +>; + +type ResolveAlias< + T extends Aliases +> = ThemeOrAny['aliases'][T] extends Shorthands + ? ResolveShorthand + : ThemeOrAny['aliases'][T]; + +type ScaleKeys = LiteralUnion< + Extract< + keyof ThemeOrAny['scales'][ThemeOrAny['matchers'][Extract< + Property, + Matchers + >]], + ValueOf + >, + ValueOf +>; + +export type StylePropertyValue = T | Empty | ((theme: Theme) => T | Empty); + +// --- CSSProperties + +export type CSSProperties = CSS.StandardProperties & + CSS.SvgProperties & + CSS.VendorProperties; + +// --- CSSObject + +type CSSOthersObjectForCSSObject = { + [name: string]: undefined | number | string | CSSObject; +}; + +/** + * CSS as POJO that is compatible with CSS-in-JS libaries. + * Used as the return type of `interpolate` function + */ +export type CSSObject = CSSProperties & CSSOthersObjectForCSSObject; + +/** + * Example: + +const cssObject: CSSObject = { + // boxSizing: 'sdf', + boxSizing: 'border-box', + display: 'flex', + 'pseudo|selector|mq|support': { + color: 'primary', + boxSizing: 'border-box', + }, + selectors: { + // Always start with "&", representing the parent rule + // See: https://drafts.csswg.org/css-nesting/#nest-selector + '& > * + *': { + marginLeft: 16, + }, + + // In a comma-separated list, each individual selector shall start with "&" + '&:focus, &:active': { + outline: 'solid', + }, + + // Self-references are also supported + '& + &': { + color: 'green', + }, + }, + '@media': { + '(min-width: 600px)': { + color: 'rebeccapurple', + ':hover': { + background: 'papayawhip', + }, + }, + '(min-width: 1000px)': { + color: 'teal', + }, + }, +}; +*/ + +// --- GumptionUICSSProperties + +export type ThemedCSSObject = { [key in Matchers]?: ScaleKeys } & + { [key in Shorthands]?: ScaleKeys> } & + { [key in Aliases]?: ScaleKeys> }; + +export type GumptionUIExtendedCSSProperties = Modify< + CSSProperties, + ThemedCSSObject +>; + +export type GumptionUICSSProperties = { + [key in keyof GumptionUIExtendedCSSProperties]: StylePropertyValue< + GumptionUIExtendedCSSProperties[key] + >; +}; + +/** + * Example: + +const gumptionUICSSProperties: GumptionUICSSProperties = { + color: 'primary', + bottom: 'small', + direction: 'ltr', + boxSizing: 'border-box', +}; + +*/ + +// --- GumptionUICSSObject + +export type VariantProperty = { + variant?: Variants; +}; + +type CSSOthersObject = { + [name: string]: StylePropertyValue; +}; + +export type GumptionUICSSObject = GumptionUICSSProperties & + VariantProperty & + CSSOthersObject; + +/** + * Example: + +const gumptionUICSSObject: GumptionUICSSObject = { + bottom: 'small', + direction: 'ltr', + variant: 'text', + ':after': () => ({ + width: 100, + color: 'secondary', + }), + ':hover': { + boxSizing: () => 'border-box', + }, + + property1: undefined, + property2: null, + property3: false, + property4: 'value', + property5: {}, + property6: () => undefined, + property7: () => null, + property8: () => false, + property9: () => 'value', + property10: () => ({}), + property13: { + property1: undefined, + property2: null, + property3: false, + property4: 'value', + property5: {}, + property6: () => undefined, + property7: () => null, + property8: () => false, + property9: () => 'value', + property10: () => ({}), + property13: { + color: 'primary', + bottom: 'small', + direction: 'ltr', + variant: 'text', + + boxSizing: () => 'border-box', + ':hover': { + color: 'secondary', + }, + property1: undefined, + property2: null, + property3: false, + property4: 'value', + property5: {}, + property6: () => undefined, + property7: () => null, + property8: () => false, + property9: () => 'value', + property10: () => ({}), + }, + }, +}; + +*/ + +// --- GumptionUIStyleObject + +type ThemeDerivedStyles = (theme: Theme) => GumptionUICSSObject; + +export type GumptionUIStyleObject = GumptionUICSSObject | ThemeDerivedStyles; diff --git a/packages/interpolate-new/theme.d.ts b/packages/interpolate-new/theme.d.ts new file mode 100644 index 0000000..1dd03f8 --- /dev/null +++ b/packages/interpolate-new/theme.d.ts @@ -0,0 +1,37 @@ +type AnyIfEmpty = keyof T extends never ? any : T; // eslint-disable-line @typescript-eslint/ban-types + +// type ItselfIfEmpty = + +declare module '@gumption-ui/interpolate-new/theme' { + // eslint-disable-next-line @typescript-eslint/no-empty-interface + export interface Theme {} + export type ThemeOrAny = AnyIfEmpty; + + type X = T; + type Y = string; + + export interface StylePropertyValueEnhancer { + (theme: Theme): T>; + } + + export interface GenericIdentityFn { + (arg: Type): Type; + } + + // interface XRegistry { + // // A: string; + // // B: number; + // } + + // export type StylePropertyValueEnhancer = XRegistry[keyof XRegistry]; + + // type X= XRegistry[keyof XRegistry]; + + // //... And in the other file + // declare module "someModule" { + // interface XRegistry { D:D } + // } + + // export interface StylePropertyValueEnhancer T + // export type StylePropertyValueEnhancer = ItselfIfEmpty<>; +} diff --git a/packages/interpolate-new/theme.dev.d.ts b/packages/interpolate-new/theme.dev.d.ts new file mode 100644 index 0000000..8c5b32c --- /dev/null +++ b/packages/interpolate-new/theme.dev.d.ts @@ -0,0 +1,7 @@ +import { theme } from './src/test/theme'; + +declare module '@gumption-ui/interpolate-new/theme' { + type Tokens = typeof theme; + // eslint-disable-next-line @typescript-eslint/no-empty-interface + export interface Theme extends Tokens {} +} diff --git a/packages/interpolate-new/tsconfig.json b/packages/interpolate-new/tsconfig.json new file mode 100644 index 0000000..4bcd1e8 --- /dev/null +++ b/packages/interpolate-new/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src", "*.d.ts"] +} diff --git a/packages/jsx/package.json b/packages/jsx/package.json index 79ee1dc..13511d2 100644 --- a/packages/jsx/package.json +++ b/packages/jsx/package.json @@ -30,10 +30,7 @@ }, "dependencies": { "@gumption-ui/integral": "0.0.1", - "@gumption-ui/interpolate": "0.0.1", - "@gumption-ui/utils": "0.0.1", - "classcat": "^5.0.3", - "otion": "^0.6.2" + "@gumption-ui/utils": "0.0.1" }, "devDependencies": { "@testing-library/jest-dom": "^5.11.0", diff --git a/packages/jsx/src/Gumption.ts b/packages/jsx/src/Gumption.ts deleted file mode 100644 index 7c52603..0000000 --- a/packages/jsx/src/Gumption.ts +++ /dev/null @@ -1,46 +0,0 @@ -import * as React from 'react'; -import { - As, - isRenderProp, - isFunction, - isEmptyObject, -} from '@gumption-ui/utils'; -import cc from 'classcat'; -import { css as toClassname } from 'otion'; -import { interpolate } from '@gumption-ui/interpolate'; -import { useTheme } from '@gumption-ui/integral'; - -export const Gumption = ( - props: Record & { - typePropName: T; - }, -): JSX.Element => { - const theme = useTheme(); - - const { typePropName: type, css = {}, children, ...htmlProps } = props; - - let newProps = { ...htmlProps }; - - const themedStyle = isFunction(css) ? css(theme) : css; - - if (!isEmptyObject(themedStyle)) { - newProps = { - ...newProps, - className: cc([ - newProps.className, - toClassname(interpolate(themedStyle)(theme)), - ]), - }; - } - - if (typeof type === 'string' && isRenderProp(children)) { - const { children: _, ...rest } = newProps; - return children(rest); - } - - return React.createElement(type, newProps, children); -}; - -if (process.env.NODE_ENV !== 'production') { - Gumption.displayName = 'GumptionStylePropsInternal'; -} diff --git a/packages/jsx/src/gumption-element.ts b/packages/jsx/src/gumption-element.ts new file mode 100644 index 0000000..ec15273 --- /dev/null +++ b/packages/jsx/src/gumption-element.ts @@ -0,0 +1,69 @@ +import * as React from 'react'; +import { As, isRenderProp, isObject } from '@gumption-ui/utils'; +import { useTheme } from '@gumption-ui/integral'; +import { ParseProps } from './types'; + +export const TYPE_PROP_NAME = '__GUMPTION_TYPE_PLEASE_DO_NOT_USE__'; + +type PropsWithoutGumption = { + [key: string]: any; +}; + +type PropsWithGumption = { + [key: string]: any; + __GUMPTION_TYPE_PLEASE_DO_NOT_USE__: [ + T, + boolean, + ParseProps['compileStyles'], + ]; +}; + +export function hasGumptionProps( + value: unknown, +): value is PropsWithGumption { + return isObject(value) && TYPE_PROP_NAME in value; +} + +export function parseProps( + type: T, + props?: ParseProps, +): PropsWithGumption | PropsWithoutGumption | null { + if (!props) return null; + + const renderProp = typeof type === 'string' && isRenderProp(props.children); + + const { compileStyles, ...restProps } = props; + + const gumptionElement = compileStyles || renderProp; + + return gumptionElement + ? { + [TYPE_PROP_NAME]: [type, renderProp, compileStyles], + ...restProps, + } + : restProps; +} + +export const Gumption = ( + props: PropsWithGumption, +): JSX.Element => { + const theme = useTheme(); + + const { + [TYPE_PROP_NAME]: [type, renderProp, compileStyles], + children, + ...restProps + } = props; + + const newProps = compileStyles?.(restProps, theme); + + if (renderProp) { + return children(newProps); + } + + return React.createElement(type, newProps, children); +}; + +if (process.env.NODE_ENV !== 'production') { + Gumption.displayName = 'GumptionStylePropsInternal'; +} diff --git a/packages/jsx/src/index.ts b/packages/jsx/src/index.ts index 54af88f..5e36929 100644 --- a/packages/jsx/src/index.ts +++ b/packages/jsx/src/index.ts @@ -1,35 +1,24 @@ import * as React from 'react'; -import { As, isRenderProp } from '@gumption-ui/utils'; +import { As } from '@gumption-ui/utils'; import { GumptionJSX } from './jsx-namespace'; -import { Gumption } from './Gumption'; -import { hasOwnProperty } from './utils'; -import { parseProps } from './parseProps'; +import { Gumption, parseProps, hasGumptionProps } from './gumption-element'; +import { ParseProps } from './types'; export type { GumptionJSX } from './jsx-namespace'; -export type { CssProp } from './types'; +export type { ParseProps } from './types'; /* eslint-disable-next-line import/export -- intentionally exporting jsx functin and namespace with the same name */ export function jsx( type: T, - props: Record, + props: ParseProps>, ...children: React.ReactNode[] ): JSX.Element { - // TODO strings in `css` are not allowed https://github.com/emotion-js/emotion/blob/master/packages/react/src/emotion-element.js#L25 - - // eslint-disable-next-line prefer-rest-params -- in this case the parameters fit 100% - const args: any = arguments; - const nextProps = parseProps(type, props); - - if (nextProps == null || !hasOwnProperty.call(nextProps, 'css')) { - if (typeof type === 'string' && isRenderProp(children)) { - return children(nextProps || {}); - } - - return React.createElement.apply(undefined, args); // eslint-disable-line prefer-spread - } - - return React.createElement(Gumption, nextProps, children); + return React.createElement( + hasGumptionProps(nextProps) ? Gumption : type, + nextProps, + children, + ); } /* eslint-disable-next-line import/export -- intentionally exporting jsx functin and namespace with the same name */ diff --git a/packages/jsx/src/jsx-dev-runtime.ts b/packages/jsx/src/jsx-dev-runtime.ts index 8bd02f9..a05a876 100644 --- a/packages/jsx/src/jsx-dev-runtime.ts +++ b/packages/jsx/src/jsx-dev-runtime.ts @@ -6,18 +6,17 @@ import { // @ts-ignore jsxDEV as reactJsxDEV, } from 'react/jsx-dev-runtime'; -import { As, isRenderProp } from '@gumption-ui/utils'; -import { Gumption } from './Gumption'; +import { As } from '@gumption-ui/utils'; +import { Gumption, parseProps, hasGumptionProps } from './gumption-element'; import type { GumptionJSX } from './jsx-namespace'; -import { hasOwnProperty } from './utils'; -import { parseProps } from './parseProps'; +import { ParseProps } from './types'; export type { GumptionJSX as JSX } from './jsx-namespace'; export { Fragment }; export const jsxDEV = ( type: T, - props: Record, + props: ParseProps>, key: string | undefined, isStaticChildren: boolean, source: { @@ -28,25 +27,12 @@ export const jsxDEV = ( self: any, // eslint-disable-line @typescript-eslint/explicit-module-boundary-types ): GumptionJSX.Element => { const nextProps = parseProps(type, props); - - if (nextProps == null || !hasOwnProperty.call(nextProps, 'css')) { - if ( - typeof type === 'string' && - nextProps && - isRenderProp(nextProps.children) - ) { - const { children, ...rest } = nextProps; - return children(rest); - } - return reactJsxDEV( - type, - nextProps || props, - key, - isStaticChildren, - source, - self, - ); - } - - return reactJsxDEV(Gumption, nextProps, key, isStaticChildren, source, self); + return reactJsxDEV( + hasGumptionProps(nextProps) ? Gumption : type, + nextProps, + key, + isStaticChildren, + source, + self, + ); }; diff --git a/packages/jsx/src/jsx-namespace.ts b/packages/jsx/src/jsx-namespace.ts index 82b02e7..b2296a4 100644 --- a/packages/jsx/src/jsx-namespace.ts +++ b/packages/jsx/src/jsx-namespace.ts @@ -1,17 +1,11 @@ +import { ParseProps } from './types'; + /** * Imitation of: * https://github.com/system-ui/theme-ui/blob/fe978473039652bc685302c3b076ce8aa283d1d8/packages/core/src/jsx-namespace.ts * https://github.com/emotion-js/emotion/blob/418daad9f7ac0eac88f206e3c4aee4e7aca7deb4/packages/react/types/jsx-namespace.d.ts */ -import { CssProp } from './types'; - -type WithConditionalSxProp

= 'className' extends keyof P - ? string extends P['className'] - ? P & CssProp - : P - : P; - // unpack all here to avoid infinite self-referencing when defining our own JSX namespace type ReactJSXElement = JSX.Element; type ReactJSXElementClass = JSX.ElementClass; @@ -32,13 +26,15 @@ export namespace GumptionJSX { extends ReactJSXElementAttributesProperty {} export interface ElementChildrenAttribute extends ReactJSXElementChildrenAttribute {} - export type LibraryManagedAttributes = WithConditionalSxProp

& - ReactJSXLibraryManagedAttributes; + export type LibraryManagedAttributes = ReactJSXLibraryManagedAttributes< + C, + P + >; export interface IntrinsicAttributes extends ReactJSXIntrinsicAttributes {} export interface IntrinsicClassAttributes extends ReactJSXIntrinsicClassAttributes {} export type IntrinsicElements = { [K in keyof ReactJSXIntrinsicElements]: ReactJSXIntrinsicElements[K] & - CssProp; + ParseProps; }; } diff --git a/packages/jsx/src/jsx-runtime.ts b/packages/jsx/src/jsx-runtime.ts index 12531ca..5069c26 100644 --- a/packages/jsx/src/jsx-runtime.ts +++ b/packages/jsx/src/jsx-runtime.ts @@ -8,57 +8,36 @@ import { // @ts-ignore jsxs as reactJsxs, } from 'react/jsx-runtime'; -import { As, isRenderProp } from '@gumption-ui/utils'; -import { Gumption } from './Gumption'; +import { As } from '@gumption-ui/utils'; +import { Gumption, parseProps, hasGumptionProps } from './gumption-element'; import type { GumptionJSX } from './jsx-namespace'; -import { hasOwnProperty } from './utils'; -import { parseProps } from './parseProps'; +import { ParseProps } from './types'; export type { GumptionJSX as JSX } from './jsx-namespace'; export { Fragment }; export function jsx( type: T, - props: Record, + props: ParseProps>, key: string | number, ): GumptionJSX.Element { const nextProps = parseProps(type, props); - - if (nextProps == null || !hasOwnProperty.call(nextProps, 'css')) { - if ( - typeof type === 'string' && - nextProps && - isRenderProp(nextProps.children) - ) { - const { children, ...rest } = nextProps; - return children(rest); - } - - return reactJsx(type, nextProps || props, key); - } - - return reactJsx(Gumption, nextProps, key); + return reactJsx( + hasGumptionProps(nextProps) ? Gumption : type, + nextProps, + key, + ); } export function jsxs( type: T, - props: Record, + props: ParseProps>, key: string | number, ): GumptionJSX.Element { const nextProps = parseProps(type, props); - - if (nextProps == null || !hasOwnProperty.call(nextProps, 'css')) { - if ( - typeof type === 'string' && - nextProps != null && - isRenderProp(nextProps.children) - ) { - const { children, ...rest } = nextProps; - return children(rest); - } - - return reactJsxs(type, nextProps || props, key); - } - - return reactJsxs(Gumption, nextProps, key); + return reactJsxs( + hasGumptionProps(nextProps) ? Gumption : type, + nextProps, + key, + ); } diff --git a/packages/jsx/src/parseProps.ts b/packages/jsx/src/parseProps.ts deleted file mode 100644 index 3da61ed..0000000 --- a/packages/jsx/src/parseProps.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { As } from '@gumption-ui/utils'; - -type ParsePropsReturnType = Record & { - typePropName: T; -}; - -export function parseProps( - type: T, - props?: Record | null, -): ParsePropsReturnType | null { - let next: Record = { - typePropName: type, - }; - - if (!props) { - return (next as unknown) as ParsePropsReturnType; - } - - const { css, variant, ...restProps } = props; - - next = { - ...next, - ...restProps, - }; - - // Fix for React.Fragment. - if (typeof type === 'symbol') { - return null; - } - - if (!variant) { - next.css = css; - return (next as unknown) as ParsePropsReturnType; - } - - next.css = { - ...css, - variant: css.variant ? `${variant} ${css.variant}` : variant, - }; - - return (next as unknown) as ParsePropsReturnType; -} diff --git a/packages/jsx/src/test/__snapshots__/index.test.tsx.snap b/packages/jsx/src/test/__snapshots__/index.test.tsx.snap index 267c27a..45bf7fb 100644 --- a/packages/jsx/src/test/__snapshots__/index.test.tsx.snap +++ b/packages/jsx/src/test/__snapshots__/index.test.tsx.snap @@ -1,39 +1,40 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`kwark render \`style\` attribute 1`] = ` +exports[`otion interpret \`compileStyles\` attribute 1`] = `

- Hello + Hello World
`; -exports[`kwark render with \`css\` prop 1`] = ` +exports[`otion render \`class\` attribute 1`] = `
- Hello + World
`; -exports[`kwark render with merged className 1`] = ` +exports[`otion render \`style\` attribute 1`] = `
- Hello + Hello World
`; -exports[`kwark renders 1`] = ` +exports[`otion renders 1`] = `
- Hello + Hello World
`; diff --git a/packages/jsx/src/test/index.test.tsx b/packages/jsx/src/test/index.test.tsx index 1fe1dde..dd55ca1 100644 --- a/packages/jsx/src/test/index.test.tsx +++ b/packages/jsx/src/test/index.test.tsx @@ -6,26 +6,35 @@ import { jsx } from '..'; afterEach(cleanup); -describe('kwark', () => { +describe('otion', () => { test('renders', () => { - const { asFragment } = render(
Hello
); + const { asFragment } = render(
Hello World
); expect(asFragment()).toMatchSnapshot(); }); test('render `style` attribute', () => { - const { asFragment } = render(
Hello
); + const { asFragment } = render( +
Hello World
, + ); expect(asFragment()).toMatchSnapshot(); }); - test('render with `css` prop', () => { - const { asFragment } = render(
Hello
); + test('render `class` attribute', () => { + const { asFragment } = render(
World
); expect(asFragment()).toMatchSnapshot(); }); - test('render with merged className', () => { + test('interpret `compileStyles` attribute', () => { const { asFragment } = render( -
- Hello +
({ + ...props, + className: `${props.className} come to you`, + })} + > + Hello World
, ); expect(asFragment()).toMatchSnapshot(); diff --git a/packages/jsx/src/types.ts b/packages/jsx/src/types.ts index a2be1c4..b9b31db 100644 --- a/packages/jsx/src/types.ts +++ b/packages/jsx/src/types.ts @@ -1,6 +1,6 @@ -import { ThemedStyle, Variants } from '@gumption-ui/interpolate'; - -export interface CssProp { - css?: ThemedStyle; - variant?: Variants; -} +export type ParseProps< + P = Record, + Theme = Record +> = P & { + compileStyles?: (props: P, theme?: Theme) => React.HTMLAttributes; +}; diff --git a/packages/kwark/src/kwark.ts b/packages/kwark/src/kwark.ts index 969ae92..917cf22 100644 --- a/packages/kwark/src/kwark.ts +++ b/packages/kwark/src/kwark.ts @@ -43,6 +43,7 @@ function styled( config?: Config, ) { const name = + // @ts-expect-error TODO: remove comment when upgrading to typescript 4.3 (isObject(component) ? component.displayName : component) || 'Kwark'; const useKwark = createHook({}); diff --git a/packages/kwark/src/test/__snapshots__/index.test.tsx.snap b/packages/kwark/src/test/__snapshots__/index.test.tsx.snap index 92fef0c..ad2f63f 100644 --- a/packages/kwark/src/test/__snapshots__/index.test.tsx.snap +++ b/packages/kwark/src/test/__snapshots__/index.test.tsx.snap @@ -3,29 +3,20 @@ exports[`kwark render \`style\` attribute 1`] = `
- Hello + Hello World
`; -exports[`kwark render with \`css\` prop 1`] = ` - -
- Hello -
-
-`; - -exports[`kwark render with \`useHook\` prop 1`] = ` +exports[`kwark render wrapElement using \`useHook\` prop 1`] = ` :
Hello
@@ -34,20 +25,10 @@ exports[`kwark render with \`useHook\` prop 1`] = `
`; -exports[`kwark render with merged className 1`] = ` - -
- Hello -
-
-`; - exports[`kwark renders 1`] = `
- Hello + Hello World
`; diff --git a/packages/kwark/src/test/index.test.tsx b/packages/kwark/src/test/index.test.tsx index 7b9c669..45381a5 100644 --- a/packages/kwark/src/test/index.test.tsx +++ b/packages/kwark/src/test/index.test.tsx @@ -8,19 +8,21 @@ afterEach(cleanup); describe('kwark', () => { test('renders', () => { const Kwark = kwark('div'); - const { asFragment } = render(Hello); + const { asFragment } = render(Hello World); expect(asFragment()).toMatchSnapshot(); }); test('render `style` attribute', () => { const Kwark = kwark('div'); const { asFragment } = render( - Hello, + + Hello World + , ); expect(asFragment()).toMatchSnapshot(); }); - test('render with `useHook` prop', () => { + test('render wrapElement using `useHook` prop', () => { const Anchor = kwark('a'); type AnchorHTMLProps = React.HTMLAttributes & React.AnchorHTMLAttributes & { @@ -40,7 +42,9 @@ describe('kwark', () => { const Kwark = kwark('div', { useHook, }); - const { asFragment } = render(Hello); + const { asFragment } = render( + Hello, + ); expect(asFragment()).toMatchSnapshot(); }); }); diff --git a/packages/otion/.eslintrc.js b/packages/otion/.eslintrc.js new file mode 100644 index 0000000..a7aea9a --- /dev/null +++ b/packages/otion/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['../../.eslintrc'], + rules: { + '@typescript-eslint/no-namespace': 'off', + '@typescript-eslint/no-empty-interface': 'off', + }, +}; diff --git a/packages/otion/jsx-dev-runtime/package.json b/packages/otion/jsx-dev-runtime/package.json new file mode 100644 index 0000000..314ea3d --- /dev/null +++ b/packages/otion/jsx-dev-runtime/package.json @@ -0,0 +1,16 @@ +{ + "name": "@gumption-ui/otion-jsx-dev-runtime", + "main": "dist/index.js", + "module": "dist/index.esm.js", + "source": "../src/jsx-dev-runtime.ts", + "types": "dist/jsx-dev-runtime.d.ts", + "dependencies": { + "@gumption-ui/integral": "*", + "@gumption-ui/interpolate": "*", + "@gumption-ui/jsx": "*", + "@gumption-ui/utils": "*", + "classcat": "*", + "otion": "*", + "react": "*" + } +} diff --git a/packages/otion/jsx-runtime/package.json b/packages/otion/jsx-runtime/package.json new file mode 100644 index 0000000..02c484d --- /dev/null +++ b/packages/otion/jsx-runtime/package.json @@ -0,0 +1,16 @@ +{ + "name": "@gumption-ui/otion-jsx-runtime", + "main": "dist/index.js", + "module": "dist/index.esm.js", + "source": "../src/jsx-runtime.ts", + "types": "dist/jsx-runtime.d.ts", + "dependencies": { + "@gumption-ui/integral": "*", + "@gumption-ui/interpolate": "*", + "@gumption-ui/jsx": "*", + "@gumption-ui/utils": "*", + "classcat": "*", + "otion": "*", + "react": "*" + } +} diff --git a/packages/otion/package.json b/packages/otion/package.json new file mode 100644 index 0000000..00475cc --- /dev/null +++ b/packages/otion/package.json @@ -0,0 +1,49 @@ +{ + "name": "@gumption-ui/otion", + "version": "0.0.1", + "private": true, + "license": "MIT", + "main": "./dist/index.js", + "module": "./dist/index.esm.js", + "source": "./src/index.ts", + "types": "./dist/index.d.ts", + "files": [ + "dist/", + "src/", + "!*.test.*" + ], + "scripts": { + "build": "yarn build:index && yarn build:jsx-runtime && yarn build:jsx-dev-runtime", + "build:index": "microbundle --no-compress --tsconfig tsconfig.json --globals @gumption-ui/utils=utils,@gumption-ui/interpolate-new=interpolate,@gumption-ui/integral=integral,@gumption-ui/jsx=jsx", + "build:jsx-runtime": "microbundle --no-compress --tsconfig tsconfig.json --globals @gumption-ui/utils=utils,@gumption-ui/interpolate-new=interpolate,@gumption-ui/integral=integral,@gumption-ui/jsx/jsx-runtime=internalJsxRuntime --cwd jsx-runtime", + "build:jsx-dev-runtime": "microbundle --no-compress --tsconfig tsconfig.json --globals @gumption-ui/utils=utils,@gumption-ui/interpolate-new=interpolate,@gumption-ui/integral=integral,@gumption-ui/jsx/jsx-dev-runtime=internalJsxDevRuntime --cwd jsx-dev-runtime", + "develop": "microbundle watch --no-compress --tsconfig tsconfig.json --globals @gumption-ui/utils=utils,@gumption-ui/interpolate-new=interpolate,@gumption-ui/integral=integral", + "test": "jest --passWithNoTests", + "typecheck": "tsc --noEmit" + }, + "jest": { + "modulePathIgnorePatterns": [ + "dist" + ], + "preset": "ts-jest", + "testEnvironment": "jsdom" + }, + "dependencies": { + "@gumption-ui/integral": "0.0.1", + "@gumption-ui/interpolate-new": "0.0.1", + "@gumption-ui/jsx": "0.0.1", + "@gumption-ui/utils": "0.0.1", + "classcat": "^5.0.3", + "csstype": "^3.0.6", + "otion": "^0.6.2" + }, + "devDependencies": { + "@gumption-ui/theme-base": "0.0.1", + "@testing-library/jest-dom": "^5.11.0", + "@testing-library/react": "^11.2.5", + "@types/react": "^17.0.0" + }, + "peerDependencies": { + "react": "^17.0.0" + } +} diff --git a/packages/otion/readme.md b/packages/otion/readme.md new file mode 100644 index 0000000..c2ca6c9 --- /dev/null +++ b/packages/otion/readme.md @@ -0,0 +1,13 @@ +# Links + +- https://www.typescriptlang.org/docs/handbook/jsx.html +- https://github.com/microsoft/TypeScript/issues/8757 +- https://fettblog.eu/typescript-react-extending-jsx-elements/ +- https://stackoverflow.com/questions/48450470/how-can-i-extend-the-attributes-of-jsx-elements-in-typescript-tsx-code +- https://github.com/system-ui/theme-ui/tree/develop/packages/core +- https://github.com/system-ui/theme-ui/blob/v0.4.0-rc.14/packages/core/src/react-jsx.ts +- https://dev.to/segunadebayo/migrating-to-react-17-and-fixing-the-jsx-runtime-error-with-emotion-l4n +- https://babeljs.io/docs/en/babel-preset-react#importsource +- https://github.com/reactjs/rfcs/blob/createlement-rfc/text/0000-create-element-changes.md#motivation +- https://www.google.com/search?q=pragma+and+pragmaFrag+cannot+be+set+when+runtime+is+automatic +- https://github.com/emotion-js/emotion/tree/master/packages/react diff --git a/packages/otion/src/index.ts b/packages/otion/src/index.ts new file mode 100644 index 0000000..6b8262d --- /dev/null +++ b/packages/otion/src/index.ts @@ -0,0 +1,38 @@ +import * as React from 'react'; +import { As } from '@gumption-ui/utils'; +import { jsx as internalJsx } from '@gumption-ui/jsx'; +import { GumptionOtionJSX } from './jsx-namespace'; +import { parseProps } from './parseProps'; + +export type { GumptionOtionJSX } from './jsx-namespace'; +export type { CssProp } from './types'; + +/* eslint-disable-next-line import/export -- intentionally exporting jsx functin and namespace with the same name */ +export function jsx( + type: T, + props: Record, + ...children: React.ReactNode[] +): JSX.Element { + return internalJsx(type, parseProps(type, props), children); +} + +/* eslint-disable-next-line import/export -- intentionally exporting jsx function and namespace with the same name */ +export declare namespace jsx { + export namespace JSX { + export interface Element extends GumptionOtionJSX.Element {} + export interface ElementClass extends GumptionOtionJSX.ElementClass {} + export interface ElementAttributesProperty + extends GumptionOtionJSX.ElementAttributesProperty {} + export interface ElementChildrenAttribute + extends GumptionOtionJSX.ElementChildrenAttribute {} + export type LibraryManagedAttributes< + C, + P + > = GumptionOtionJSX.LibraryManagedAttributes; + export interface IntrinsicAttributes + extends GumptionOtionJSX.IntrinsicAttributes {} + export interface IntrinsicClassAttributes + extends GumptionOtionJSX.IntrinsicClassAttributes {} + export type IntrinsicElements = GumptionOtionJSX.IntrinsicElements; + } +} diff --git a/packages/otion/src/jsx-dev-runtime.ts b/packages/otion/src/jsx-dev-runtime.ts new file mode 100644 index 0000000..d111ac3 --- /dev/null +++ b/packages/otion/src/jsx-dev-runtime.ts @@ -0,0 +1,34 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ + +import { As } from '@gumption-ui/utils'; +import { + jsxDEV as internalJsxDEV, + Fragment, +} from '@gumption-ui/jsx/jsx-dev-runtime'; +import type { GumptionOtionJSX } from './jsx-namespace'; +import { parseProps } from './parseProps'; + +export type { GumptionOtionJSX as JSX } from './jsx-namespace'; +export { Fragment }; + +export function jsxDEV( + type: T, + props: Record, + key: string | undefined, + isStaticChildren: boolean, + source: { + filename: string; + lineNumber: number; + columnNumber: number; + }, + self: any, +): GumptionOtionJSX.Element { + return internalJsxDEV( + type, + parseProps(type, props), + key, + isStaticChildren, + source, + self, + ); +} diff --git a/packages/otion/src/jsx-namespace.ts b/packages/otion/src/jsx-namespace.ts new file mode 100644 index 0000000..10716cf --- /dev/null +++ b/packages/otion/src/jsx-namespace.ts @@ -0,0 +1,33 @@ +/** + * Imitation of: + * https://github.com/system-ui/theme-ui/blob/fe978473039652bc685302c3b076ce8aa283d1d8/packages/core/src/jsx-namespace.ts + * https://github.com/emotion-js/emotion/blob/418daad9f7ac0eac88f206e3c4aee4e7aca7deb4/packages/react/types/jsx-namespace.d.ts + */ + +import { GumptionJSX } from '@gumption-ui/jsx'; +import { CssProp } from './types'; + +type WidthCOnditionalCssProp

= 'className' extends keyof P + ? string extends P['className'] + ? P & CssProp + : P + : P; + +export namespace GumptionOtionJSX { + export interface Element extends GumptionJSX.Element {} + export interface ElementClass extends GumptionJSX.ElementClass {} + export interface ElementAttributesProperty + extends GumptionJSX.ElementAttributesProperty {} + export interface ElementChildrenAttribute + extends GumptionJSX.ElementChildrenAttribute {} + export type LibraryManagedAttributes = WidthCOnditionalCssProp

& + GumptionJSX.LibraryManagedAttributes; + export interface IntrinsicAttributes + extends GumptionJSX.IntrinsicAttributes {} + export interface IntrinsicClassAttributes + extends GumptionJSX.IntrinsicClassAttributes {} + export type IntrinsicElements = { + [K in keyof GumptionJSX.IntrinsicElements]: GumptionJSX.IntrinsicElements[K] & + CssProp; + }; +} diff --git a/packages/otion/src/jsx-runtime.ts b/packages/otion/src/jsx-runtime.ts new file mode 100644 index 0000000..6b4796b --- /dev/null +++ b/packages/otion/src/jsx-runtime.ts @@ -0,0 +1,29 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ + +import { As } from '@gumption-ui/utils'; +import { + jsx as internalJsx, + jsxs as internalJsxs, + Fragment, +} from '@gumption-ui/jsx/jsx-runtime'; +import type { GumptionOtionJSX } from './jsx-namespace'; +import { parseProps } from './parseProps'; + +export type { GumptionOtionJSX as JSX } from './jsx-namespace'; +export { Fragment }; + +export function jsx( + type: T, + props: Record, + key: string | number, +): GumptionOtionJSX.Element { + return internalJsx(type, parseProps(type, props), key); +} + +export function jsxs( + type: T, + props: Record, + key: string | number, +): GumptionOtionJSX.Element { + return internalJsxs(type, parseProps(type, props), key); +} diff --git a/packages/otion/src/parseProps.ts b/packages/otion/src/parseProps.ts new file mode 100644 index 0000000..b23175b --- /dev/null +++ b/packages/otion/src/parseProps.ts @@ -0,0 +1,108 @@ +import { As, isEmptyObject, isFunction, isObject } from '@gumption-ui/utils'; +import cc from 'classcat'; +import { css as toClassname } from 'otion'; +import { + interpolate as createInterpolate, + Theme, +} from '@gumption-ui/interpolate-new'; +import { ParseProps } from '@gumption-ui/jsx'; +import { ScopedCSSRules } from 'otion'; +import { CssProp, GumptionUICSSObject, GumptionUIStyleObject } from './types'; + +const responsive = ({ + styles, + theme, +}: { + styles: GumptionUICSSObject; + theme: Theme; +}) => { + const next: any = {}; + const breakpoints = (theme.breakpoints ?? []) as Array; + const mediaQueries = [null, ...breakpoints.map((n) => `(min-width: ${n}px)`)]; + + // eslint-disable-next-line guard-for-in, no-restricted-syntax + for (const key in styles) { + /* eslint-disable no-continue */ + const valuePossiblyFunction = styles[key as keyof GumptionUICSSObject]; + const value = isFunction(valuePossiblyFunction) + ? valuePossiblyFunction(theme) + : valuePossiblyFunction; + + if (!Array.isArray(value)) { + next[key] = value; + continue; + } + // eslint-disable-next-line no-plusplus + for (let i = 0; i < value.slice(0, mediaQueries.length).length; i++) { + const media = mediaQueries[i]; + if (!media) { + next[key] = value[i]; + continue; + } + next['@media'] = next['@media'] || {}; + if (value[i] == null) continue; + next['@media'][media] = next['@media'][media] || {}; + next['@media'][media][key] = value[i]; + } + /* eslint-enable no-continue */ + } + return { + styles: next, + theme, + }; +}; + +export const interpolate = createInterpolate< + GumptionUIStyleObject, + ScopedCSSRules +>(responsive); + +export const parseProps = ( + _: T, + props: Record | null, +): ParseProps => { + const { variant, ...nextProps } = props ?? {}; + + if ( + !variant && + (!nextProps.css || + (isObject(nextProps.css) && Object.keys(nextProps.css).length === 0)) + ) { + return nextProps; + } + + // either `variant` or `css` exist, therefore styles that need to be compiled + const next: ParseProps< + Omit & { + [name: string]: unknown; + className?: string; + } + > = { + ...nextProps, + compileStyles: ({ css, ...nextNextProps }, theme) => { + const styles = interpolate(css)(theme); + if (!isEmptyObject(styles)) { + return { + ...nextNextProps, + className: cc([nextNextProps.className, toClassname(styles)]), + }; + } + + return nextNextProps; + }, + }; + + if (variant) { + next.css = (theme) => { + const themedStyle = isFunction(nextProps.css) + ? nextProps.css(theme) + : nextProps.css ?? {}; + return { + ...themedStyle, + variant: `${variant} ${themedStyle.variant}`, + }; + }; + } + + return next; +}; diff --git a/packages/otion/src/test/__snapshots__/jsx.test.tsx.snap b/packages/otion/src/test/__snapshots__/jsx.test.tsx.snap new file mode 100644 index 0000000..bdc1b90 --- /dev/null +++ b/packages/otion/src/test/__snapshots__/jsx.test.tsx.snap @@ -0,0 +1,69 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`otion functional values can return responsive arrays 1`] = ` + +

+ Responsive styles +
+ +`; + +exports[`otion render \`style\` attribute 1`] = ` + +
+ Hello +
+
+`; + +exports[`otion render with \`css\` prop 1`] = ` + +
+ Hello +
+
+`; + +exports[`otion render with merged className 1`] = ` + +
+ Hello +
+
+`; + +exports[`otion renders 1`] = ` + +
+ Hello +
+
+`; + +exports[`otion returns responsive interpolated styles 1`] = ` + +
+ Responsive stles +
+
+`; + +exports[`otion returns selectors interpolated styles 1`] = ` + +
+ selectors styles +
+
+`; diff --git a/packages/otion/src/test/interpolate.test.ts b/packages/otion/src/test/interpolate.test.ts new file mode 100644 index 0000000..b79ec7e --- /dev/null +++ b/packages/otion/src/test/interpolate.test.ts @@ -0,0 +1,131 @@ +import { theme } from '@gumption-ui/theme-base'; +import { interpolate } from '../parseProps'; + +describe('interpolate(responsive) transform', () => { + test('responsive value', () => { + const result = interpolate({ + color: ['primary', 'secondary'], + })(theme); + expect(result).toEqual({ + color: 'tomato', + '@media': { + '(min-width: 640px)': { + color: 'cyan', + }, + }, + }); + }); + + test('responsive value as function', () => { + const result = interpolate({ + color: (t) => [t.scales?.colors.primary, t.scales?.colors.secondary], + })(theme); + expect(result).toEqual({ + color: 'tomato', + '@media': { + '(min-width: 640px)': { + color: 'cyan', + }, + }, + }); + }); + + test('responsive value as object', () => { + const result = interpolate({ + selectors: { + selector1: { + color: 'primary', + }, + selector2: { + backgroundColor: ['primary', 'secondary'], + }, + }, + })(theme); + expect(result).toEqual({ + selectors: { + selector1: { + color: 'tomato', + }, + selector2: { + '@media': { + '(min-width: 640px)': { + backgroundColor: 'cyan', + }, + }, + backgroundColor: 'tomato', + }, + }, + }); + }); + + test('responsive styles', () => { + const result = interpolate({ + color: 'primary', + padding: ['small', 'medium', 'large'], + margin: [undefined, 'medium', undefined], + ':hover': [{ width: 'small' }, { width: 'medium' }], + })(theme); + expect(result).toEqual({ + color: 'tomato', + padding: 16, + ':hover': { + width: 16, + }, + '@media': { + '(min-width: 640px)': { + ':hover': { + width: 24, + }, + margin: 24, + padding: 24, + }, + '(min-width: 768px)': { + padding: 32, + }, + }, + }); + }); + + test('responsive value as complex object', () => { + const result = interpolate({ + selectors: { + '& > * + *': { + color: 'primary', + }, + '&:focus, &:active': { + color: 'primary', + }, + }, + })(theme); + expect(result).toEqual({ + selectors: { + '& > * + *': { + color: 'tomato', + }, + '&:focus, &:active': { + color: 'tomato', + }, + }, + }); + }); + + test('returns variants from theme', () => { + const result = interpolate({ + variant: 'buttons.primary', + })(theme); + expect(result).toEqual({ + backgroundColor: 'tomato', + color: 'white', + }); + }); + + // test('returns multiple variants from theme', () => { + // const result = interpolate({ + // variant: 'button.primary button.secondary', + // })(theme); + // expect(result).toEqual({ + // backgroundColor: 'tomato', + // fontSize: 16, + // }); + // }); +}); diff --git a/packages/otion/src/test/jsx.test.tsx b/packages/otion/src/test/jsx.test.tsx new file mode 100644 index 0000000..f7e4024 --- /dev/null +++ b/packages/otion/src/test/jsx.test.tsx @@ -0,0 +1,213 @@ +/** @jsx jsx */ + +import { render, cleanup } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { jsx } from '..'; + +afterEach(cleanup); + +describe('otion', () => { + test('renders', () => { + const { asFragment } = render(
Hello
); + expect(asFragment()).toMatchSnapshot(); + }); + + test('render `style` attribute', () => { + const { asFragment } = render(
Hello
); + expect(asFragment()).toMatchSnapshot(); + }); + + test('render with `css` prop', () => { + const { asFragment } = render(
Hello
); + expect(asFragment()).toMatchSnapshot(); + }); + + test('render with merged className', () => { + const { asFragment } = render( +
+ Hello +
, + ); + expect(asFragment()).toMatchSnapshot(); + }); + + test('returns responsive interpolated styles', () => { + const { asFragment } = render( +
+ Responsive stles +
, + ); + expect(asFragment()).toMatchSnapshot(); + }); + + test('functional values can return responsive arrays', () => { + const { asFragment } = render( +
[t.scales?.colors.primary, t.scales?.colors.secondary], + }} + className="satori" + > + Responsive styles +
, + ); + expect(asFragment()).toMatchSnapshot(); + }); + + test('returns selectors interpolated styles', () => { + const { asFragment } = render( +
* + *': { + color: 'primary', + }, + '&:focus, &:active': { + color: 'primary', + }, + }, + }} + className="satori" + > + selectors styles +
, + ); + expect(asFragment()).toMatchSnapshot(); + + // const result = interpolate({ + // selectors: { + // '& > * + *': { + // color: 'primary', + // }, + // '&:focus, &:active': { + // color: 'primary', + // }, + // }, + // })({ theme }); + // expect(result).toEqual({ + // selectors: { + // '& > * + *': { + // color: 'tomato', + // }, + // '&:focus, &:active': { + // color: 'tomato', + // }, + // }, + // }); + }); + + // test('returns responsive selectors interpolated styles', () => { + // const result = interpolate({ + // selectors: [ + // { + // '& > * + *': { + // color: 'primary', + // }, + // '&:focus, &:active': { + // color: 'primary', + // }, + // }, + // { + // '& > * + *': { + // color: 'secondary', + // }, + // '&:focus, &:active': { + // color: 'secondary', + // }, + // }, + // ], + // })({ theme }); + // expect(result).toEqual({ + // selectors: { + // '& > * + *': { + // color: 'tomato', + // }, + // '&:focus, &:active': { + // color: 'tomato', + // }, + // }, + // '@media': { + // '(min-width: 640px)': { + // selectors: { + // '& > * + *': { + // color: 'cyan', + // }, + // '&:focus, &:active': { + // color: 'cyan', + // }, + // }, + // }, + // }, + // }); + // }); + + // test('returns at-rule interpolated styles', () => { + // const result = interpolate({ + // '@media': { + // '(min-width: 600px)': { + // color: 'primary', + // }, + // }, + // '@supports': { + // '(display: grid)': { + // color: 'primary', + // }, + // }, + // })(theme); + // expect(result).toEqual({ + // '@media': { + // '(min-width: 600px)': { + // color: 'tomato', + // }, + // }, + // '@supports': { + // '(display: grid)': { + // color: 'tomato', + // }, + // }, + // }); + // }); + + // test('returns at-rule interpolated styles', () => { + // const result = interpolate({ + // '@media': { + // '(min-width: 600px)': { + // ':hover': { + // color: 'primary', + // }, + // }, + // }, + // '@supports': { + // '(display: grid)': { + // ':hover': { + // color: 'primary', + // }, + // }, + // }, + // })(theme); + // expect(result).toEqual({ + // '@media': { + // '(min-width: 600px)': { + // ':hover': { + // color: 'tomato', + // }, + // }, + // }, + // '@supports': { + // '(display: grid)': { + // ':hover': { + // color: 'tomato', + // }, + // }, + // }, + // }); + // }); +}); diff --git a/packages/otion/src/types.ts b/packages/otion/src/types.ts new file mode 100644 index 0000000..73477fe --- /dev/null +++ b/packages/otion/src/types.ts @@ -0,0 +1,133 @@ +import { + GumptionUIExtendedCSSProperties, + VariantProperty, + StylePropertyValue, + Theme, +} from '@gumption-ui/interpolate-new'; +import { ResponsiveStyleValue } from '@gumption-ui/utils'; +import * as CSS from 'csstype'; + +export type CSSStyleRules

> = P & + { + [pseudo in CSS.SimplePseudos]?: StylePropertyValue>; + } & { + selectors?: StylePropertyValue<{ [selector: string]: P }>; + }; + +export interface CSSGroupingRules

> { + '@media'?: { + [conditionText: string]: CSSRules

; + }; + '@supports'?: { + [conditionText: string]: CSSRules

; + }; +} + +export type CSSRules

> = CSSStyleRules

& + CSSGroupingRules

; + +export type GumptionUICSSProperties = { + [key in keyof GumptionUIExtendedCSSProperties]: StylePropertyValue< + ResponsiveStyleValue + >; +}; + +export type GumptionUICSSObject = CSSRules & + VariantProperty; + +type ThemeDerivedStyles = (theme: Theme) => GumptionUICSSObject; + +export type GumptionUIStyleObject = GumptionUICSSObject | ThemeDerivedStyles; + +/** + * Example: + */ +export const themedCSSObjecta: GumptionUIStyleObject = { + bottom: 'small', + direction: 'ltr', + variant: 'text', + + color: (t) => [t.scales?.colors.primary, t.scales?.colors.secondary], + + boxSizing: () => ['border-box'], + ':hover': [{ width: 'small' }, { width: 'medium' }], + display: ['flex'], + ':after': () => ({ + width: 100, + background: ['red', 'green'], + color: () => ['red', 'green', 'blue'], + display: ['flex'], + boxSizing: () => 'border-box', + }), + + selectors: () => ({ + property: { + background: ['red', 'green'], + boxSizing: () => ['border-box'], + }, + selector1: { + color: 'tomato', + }, + selector2: { + '@media': { + '(min-width: 640px)': { + backgroundColor: 'cyan', + }, + }, + backgroundColor: 'tomato', + }, + }), + + // property1: undefined, + // property2: null, + // property3: false, + // property4: 'value', + // property5: {}, + // property6: () => undefined, + // property7: () => null, + // property8: () => false, + // property9: () => 'value', + // property10: () => ({}), + // property11: () => ['border-box', 'value', {}], + // property12: ['border-box', 'value', {}], + property13: { + property1: undefined, + property2: null, + property3: false, + property4: 'value', + property5: {}, + property6: () => undefined, + property7: () => null, + property8: () => false, + property9: () => 'value', + property10: () => ({}), + property11: () => ['border-box', 'value', {}], + property12: ['border-box', 'value', {}], + property13: { + color: 'primary', + bottom: 'small', + direction: 'ltr', + variant: 'text', + boxSizing: () => 'border-box', + ':hover': { + color: 'secondary', + }, + property1: undefined, + property2: null, + property3: false, + property4: 'value', + property5: {}, + property6: () => undefined, + property7: () => null, + property8: () => false, + property9: () => 'value', + property10: () => ({}), + property11: () => ['border-box', 'value', {}], + property12: ['border-box', 'value', {}], + }, + }, +}; + +export interface CssProp extends VariantProperty { + css?: GumptionUIStyleObject; +} diff --git a/packages/otion/src/useStyleConfig.ts b/packages/otion/src/useStyleConfig.ts new file mode 100644 index 0000000..b50fca3 --- /dev/null +++ b/packages/otion/src/useStyleConfig.ts @@ -0,0 +1,18 @@ +// const useStyleConfig = createHook({ +// keys: ['variant', 'size', 'themeKey'], +// useProps: (options, { css, ...htmlProps }) => { +// const theme = useTheme(); +// const optionsWithTheme = { ...options, theme }; +// const computedStyles: ThemedStyle = { +// ...baseStyles(optionsWithTheme), +// ...modifierStyle(optionsWithTheme), +// ...useSlotStyles(name), +// ...css, +// }; +// +// return { +// ...htmlProps, +// css: computedStyles, +// }; +// }, +// }); diff --git a/packages/jsx/src/utils.ts b/packages/otion/src/utils.ts similarity index 100% rename from packages/jsx/src/utils.ts rename to packages/otion/src/utils.ts diff --git a/packages/otion/theme.d.ts b/packages/otion/theme.d.ts new file mode 100644 index 0000000..67274c2 --- /dev/null +++ b/packages/otion/theme.d.ts @@ -0,0 +1,7 @@ +import { theme } from '@gumption-ui/theme-base'; + +declare module '@gumption-ui/interpolate-new/theme' { + type Tokens = typeof theme; + // eslint-disable-next-line @typescript-eslint/no-empty-interface + export interface Theme extends Tokens {} +} diff --git a/packages/otion/tsconfig.json b/packages/otion/tsconfig.json new file mode 100644 index 0000000..4bcd1e8 --- /dev/null +++ b/packages/otion/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src", "*.d.ts"] +} diff --git a/packages/quark/src/quark.tsx b/packages/quark/src/quark.tsx index 39da6ab..8cff13f 100644 --- a/packages/quark/src/quark.tsx +++ b/packages/quark/src/quark.tsx @@ -66,6 +66,7 @@ function styled( const name = subComponentName || componentName || + // @ts-expect-error TODO: remove comment when upgrading to typescript 4.3 (isObject(component) ? component.displayName : component) || 'Quark'; diff --git a/packages/theme-base/index.ts b/packages/theme-base/index.ts index 37bc360..4b22aef 100644 --- a/packages/theme-base/index.ts +++ b/packages/theme-base/index.ts @@ -1,5 +1,3 @@ -import 'typeface-inter'; - const grid = 8; const numberToPx = (value: number) => `${value}px`; @@ -14,7 +12,7 @@ const space = { 'xx-large': 48, }; -export default { +export const theme = { breakpoints: [640, 768, 1024, 1280], scales: { space, @@ -181,6 +179,23 @@ export default { // zIndex: ' zIndices', }, + variants: { + colors: { + primary: '#07c', + secondary: '#639', + }, + buttons: { + primary: { + color: 'white', + bg: 'primary', + }, + secondary: { + color: 'white', + bg: 'secondary', + }, + }, + }, + // components: { // root: {}, // a: {}, diff --git a/packages/theme-base/package.json b/packages/theme-base/package.json index a47b497..a5901b5 100644 --- a/packages/theme-base/package.json +++ b/packages/theme-base/package.json @@ -17,8 +17,5 @@ "develop": "microbundle watch --no-compress --tsconfig tsconfig.json", "typecheck": "tsc --noEmit", "test": "jest --passWithNoTests" - }, - "dependencies": { - "typeface-inter": "^3.12.0" } } diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 1ff69ea..bd6b59f 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,4 +1,4 @@ -import { toArray, RenderProp } from 'reakit-utils'; +import { toArray, RenderProp, isObject } from 'reakit-utils'; import merge from 'deepmerge'; import mergeProps from 'merge-props'; import { Dict } from './types'; @@ -35,7 +35,7 @@ export function isRenderProp(children: any): children is RenderProp { return typeof children === 'function'; } -export { merge, toArray, mergeProps }; +export { merge, toArray, mergeProps, isObject }; // Assertions @@ -44,11 +44,7 @@ export function isFunction(value: unknown): value is Function { return typeof value === 'function'; } -export function isObject(value: unknown): value is Dict { - return typeof value === 'object'; -} - -export function isEmptyObject(value: unknown): boolean { +export function isEmptyObject(value: unknown): value is Dict { return isObject(value) && Object.keys(value).length === 0; } diff --git a/packages/utils/src/types.ts b/packages/utils/src/types.ts index 1dc25fa..5ff2212 100644 --- a/packages/utils/src/types.ts +++ b/packages/utils/src/types.ts @@ -2,6 +2,8 @@ import * as React from 'react'; import { As, PropsWithAs, RenderProp } from 'reakit-utils/types'; import { LiteralUnion, ValueOf } from 'type-fest'; +export type Empty = undefined | null | false; + export type Dict = Record; export type FirstParameter = F extends (arg: infer T) => any ? T : never; @@ -12,6 +14,6 @@ export type UnionStringArray> = T[number]; export type PropsOf = React.ComponentPropsWithRef; -export type ResponsiveStyleValue = T | Array; +export type ResponsiveStyleValue = T | Empty | Array; export type { As, LiteralUnion, ValueOf, PropsWithAs, RenderProp }; diff --git a/yarn.lock b/yarn.lock index 07d759e..88fc860 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8132,11 +8132,6 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" -typeface-inter@^3.12.0: - version "3.15.1" - resolved "https://registry.yarnpkg.com/typeface-inter/-/typeface-inter-3.15.1.tgz#bbd39cc26ed1c7b3a2bc797996a9d053b507e727" - integrity sha512-xlEvjfaTvFnr3ZmwM6fs5/KOKLpAuNksAUxiYfa6dKqZham2oTGm0gI1j/tcgxh1reihhrKbTFfFjtCV9wwxXA== - typescript@^4.1.3, typescript@^4.1.5: version "4.2.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.2.tgz#1450f020618f872db0ea17317d16d8da8ddb8c4c"