@@ -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" | |||
} | |||
} |