commit d796ab2844d832ab4d5d0f7ef71e9c88bf5385ef Author: TheoryOfNekomata Date: Sun Nov 20 18:46:16 2022 +0800 Initial commit Add files from pridepack. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a33b78f --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +dist/ +node_modules/ +.idea/ diff --git a/package.json b/package.json new file mode 100644 index 0000000..9824f58 --- /dev/null +++ b/package.json @@ -0,0 +1,68 @@ +{ + "name": "@tesseract-design/css-utils", + "version": "0.0.0", + "files": [ + "dist", + "src" + ], + "engines": { + "node": ">=10" + }, + "license": "MIT", + "keywords": [ + "pridepack" + ], + "devDependencies": { + "@types/node": "^18.0.0", + "eslint": "^8.20.0", + "eslint-config-lxsmnsyc": "^0.4.7", + "pridepack": "2.2.1", + "tslib": "^2.4.0", + "typescript": "^4.7.4", + "vitest": "^0.19.1" + }, + "scripts": { + "prepublishOnly": "pridepack clean && pridepack build", + "build": "pridepack build", + "type-check": "pridepack check", + "lint": "pridepack lint", + "clean": "pridepack clean", + "watch": "pridepack watch", + "start": "pridepack start", + "dev": "pridepack dev", + "test": "vitest" + }, + "private": true, + "description": "CSS utilities powered by goober.", + "repository": { + "url": "", + "type": "git" + }, + "homepage": "", + "bugs": { + "url": "" + }, + "author": "TheoryOfNekomata ", + "publishConfig": { + "access": "restricted" + }, + "dependencies": { + "csstype": "^3.1.0", + "goober": "^2.1.10" + }, + "types": "./dist/types/index.d.ts", + "exports": { + ".": { + "development": { + "require": "./dist/cjs/development/index.js", + "import": "./dist/esm/development/index.js" + }, + "require": "./dist/cjs/production/index.js", + "import": "./dist/esm/production/index.js", + "types": "./dist/types/index.d.ts" + } + }, + "typesVersions": { + "*": {} + } +} diff --git a/pridepack.json b/pridepack.json new file mode 100644 index 0000000..1e3f72f --- /dev/null +++ b/pridepack.json @@ -0,0 +1,3 @@ +{ + "target": "es2017" +} \ No newline at end of file diff --git a/src/index.test.ts b/src/index.test.ts new file mode 100644 index 0000000..b8778a9 --- /dev/null +++ b/src/index.test.ts @@ -0,0 +1,143 @@ +import { css, CssIfStringImpl, CssStringImpl } from '.'; +import { + vi, + describe, + it, + expect, +} from 'vitest'; + +vi.mock('goober', () => { + return { + css: () => 'gooberClass', + }; +}); + +describe('css-utils', () => { + describe('css', () => { + it('should return CssString', () => { + const c = css` + background-color: white; + color: black; + ` + + expect(c).toBeInstanceOf(CssStringImpl); + expect(c.toString()).toBe('background-color:white;color:black;'); + }) + }) + + describe('css.if', () => { + it('should return CssString when the condition is true', () => { + const c = css.if(true)( + css` + background-color: white; + ` + ) + + expect(c).toBeInstanceOf(CssIfStringImpl) + expect(c.toString()).toBe('background-color:white;') + }) + + it('should return empty string when the condition is false', () => { + const c = css.if(false)( + css` + background-color: white; + ` + ) + + expect(c.toString()).toBe('') + }) + + it('should return CssString with .else when the if condition is false', () => { + const c = css.if(false)( + css` + background-color: white; + ` + ).else( + css` + background-color: black; + ` + ) + + expect(c.toString()).toBe('background-color:black;') + }) + + it('should return CssString with .else when the if condition is true', () => { + const c = css.if(true)( + css` + background-color: white; + ` + ).else( + css` + background-color: black; + ` + ) + + expect(c.toString()).toBe('background-color:white;') + }) + }) + + describe('css.nest', () => { + it('should add selector', () => { + const c = css.nest('> div')( + css` + background-color: white; + ` + ) + + expect(c.toString()).toBe('> div{background-color:white;}') + }) + }) + + describe('css.dynamic', () => { + it('should evaluate dynamic object', () => { + const randomNumber = Math.floor(Math.random() * 1000); + + const c = css.dynamic({ + 'line-height': randomNumber, + }) + + expect(c.toString()).toBe(`line-height:${randomNumber};`) + }) + }) + + describe('css.media', () => { + it('should accept raw queries', () => { + const c = css.media('only screen and (min-width: 720px)')( + css` + color: black; + background-color: white; + ` + ) + + expect(c.toString()).toBe('@media only screen and (min-width: 720px){color:black;background-color:white;}') + }) + }) + + describe('css.cx', () => { + it('should accept strings as classnames', () => { + expect(css.cx('class1', 'class2')).toBe('class1 class2'); + }) + + it('should accept CSS strings for classname generation', () => { + expect( + css.cx( + css` + color: white; + ` + ) + ).toBe('gooberClass'); + }) + + it('should accept mixed values', () => { + expect( + css.cx( + 'class1', + 'class2', + css` + color: white; + ` + ) + ).toBe('class1 class2 gooberClass'); + }) + }) +}) diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..7e839a2 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,143 @@ +import { css as gooberCss } from 'goober'; +import { PropertiesHyphenFallback } from 'csstype'; + +interface CssString { + // TODO stricter type checking + toString(): string +} + +export class CssStringImpl implements CssString { + private css: string + + constructor(s: TemplateStringsArray) { + this.css = s.raw[0] + .trim() + .replace(/[ ][ ]+/g, ' ') + .replace(/:[ ]/g, ':') + .replace(/\n/g, '') + .replace(/;[ ]/g, ';'); + } + + toString() { + return this.css + } +} + +interface CssIf { + (b: boolean): (...a: CssString[]) => CssIfString +} + +interface CssElse { + (...c: CssString[]): CssString + if: CssIf +} + +interface CssIfString extends CssString { + else: CssElse +} + +const cssIf: CssIf = (b: boolean) => (...a: CssString[]) => new CssIfStringImpl(b, ...a); + +export class CssIfStringImpl implements CssIfString { + readonly else: CssElse + private readonly cssStrings: CssString[] + + constructor(private readonly condition: boolean, ...cssStrings: CssString[]) { + this.cssStrings = cssStrings + + const elseFn = (...c: CssString[]) => { + if (this.condition) { + return { + toString: () => { + return this.cssStrings.map((c2) => c2.toString()).join(''); + } + } + } + return { + toString: () => { + return c.map((cc) => cc.toString()).join('') + } + }; + } + elseFn.if = cssIf + + this.else = elseFn + } + + toString() { + if (this.condition) { + return this.cssStrings.map((c2) => c2.toString()).join(''); + } + return ''; + } +} + +interface CssNest { + (selector: string): (...a: CssString[]) => CssString +} + +const cssNest: CssNest = (selector) => (...a) => { + return { + toString: () => `${selector}{${a.map(aa => aa.toString()).join('')}}` + } +} + +interface CssDynamic { + (a: PropertiesHyphenFallback): CssString +} + +const cssDynamic: CssDynamic = (a: PropertiesHyphenFallback) => { + return { + toString(): string { + return Object + .entries(a) + .map(([key, value]) => `${key}:${value.toString()};`) + .join('') + } + }; +}; + +interface CssMedia { + (raw: string): any +} + +const cssMedia: CssMedia = (arg1: string) => { + return (...body: CssString[]) => { + return { + toString(): string { + return `@media ${arg1}{${body.map(b => b.toString()).join('')}}` + } + } + } +} + +const cssCompile = (...strings: CssString[]) => { + return strings + .filter((s) => ['string', 'object'].includes(typeof s)) + .map((s) => { + if (typeof s === 'object') { + return gooberCss`${s.toString()}` + } + + return s + }) + .join(' ') +} + +interface Css { + (s: TemplateStringsArray): CssString + if: CssIf + nest: CssNest + dynamic: CssDynamic + media: CssMedia + cx(...strings: CssString[]): string +} + +const _css: Partial = (s: TemplateStringsArray) => new CssStringImpl(s); +_css.if = cssIf; +_css.nest = cssNest; +_css.dynamic = cssDynamic; +_css.media = cssMedia; +_css.cx = cssCompile; + +export const css = _css as Css; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..94556fd --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "exclude": ["node_modules"], + "extends": "../../../tsconfig.json", + "compilerOptions": { + "rootDir": "src" + } +}