@@ -0,0 +1,3 @@ | |||||
dist/ | |||||
node_modules/ | |||||
.idea/ |
@@ -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 <allan.crisostomo@outlook.com>", | |||||
"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": { | |||||
"*": {} | |||||
} | |||||
} |
@@ -0,0 +1,3 @@ | |||||
{ | |||||
"target": "es2017" | |||||
} |
@@ -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'); | |||||
}) | |||||
}) | |||||
}) |
@@ -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<Css> = (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; |
@@ -0,0 +1,7 @@ | |||||
{ | |||||
"exclude": ["node_modules"], | |||||
"extends": "../../../tsconfig.json", | |||||
"compilerOptions": { | |||||
"rootDir": "src" | |||||
} | |||||
} |