Skip to content

Commit

Permalink
feat: screen directive (#93)
Browse files Browse the repository at this point in the history
  • Loading branch information
sastan committed Jan 22, 2021
1 parent 68189af commit cf37f6d
Show file tree
Hide file tree
Showing 4 changed files with 191 additions and 18 deletions.
47 changes: 45 additions & 2 deletions docs/css-in-js.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Sometimes you might find yourself wanting to write some arbitrary styles for an

- [CSS directive](#css-directive)
- [Accessing the theme](#accessing-the-theme)
- [Screen Directive](#screen-directive)
- [Animation Directive](#animation-directive)
- [Keyframes Helper](#keyframes-helper)

Expand All @@ -17,10 +18,10 @@ Sometimes you might find yourself wanting to write some arbitrary styles for an

## CSS directive

Essentially a CSS directive uses some CSS rules in object notation format to create a optimized [inline plugin]('./plugins.md#inline-plugin). Here you can use the `&` selector to target the current element much like in other CSS-in-JS libraries. In this way, it is possible to write styles that cannot be described using an inline style attribute alone; things like specific children selectors.
Essentially a CSS directive uses some CSS rules in object notation, array or template literal format to create a optimized [inline plugin](./plugins.md#inline-plugin). Here you can use the `&` selector to target the current element much like in other CSS-in-JS libraries. In this way, it is possible to write styles that cannot be described using an inline style attribute alone; things like specific children selectors.

```js
import { css } from 'twind/css'
import { tw, css } from 'twind/css'

tw(
css({
Expand Down Expand Up @@ -177,6 +178,48 @@ css({
})
```

## Screen Directive

The `screen` directive allows you to create media queries that reference your breakpoints by name instead of duplicating their values in your own CSS.

For example, say you have a `sm` breakpoint at `640px` and you need to write some custom CSS that references this breakpoint.

Instead of writing a raw media query that duplicates that value like this:

```js
css`
@media (min-width: 640px) {
/* ... */
}
`
```

...you can use the `screen` directive and reference the breakpoint by name:

```js
import { css, screen, apply } from 'twind/css'

// With template literal
css`
${screen('sm')} {
/* ... */
}
${screen('md', css` /* ... */ `)}
${screen('lg', css({ /* ... */ }))}
${screen('xl', { /* ... */ })}
${screen('2xl', apply` ... `)}
`

// With object notation
css(
screen('md', css` /* ... */ `),
screen('lg', css({ /* ... */ })),
screen('xl', { /* ... */ }),
screen('2xl', apply` ... `),
)
`
```

## Animation Directive

Custom animations are difficult to configure in Tailwind. During `setup` you need to add to the `theme.animation` section and the `theme.keyframes` section. This means all animations must known before hand and you can not use "one-off" animations.
Expand Down
63 changes: 62 additions & 1 deletion src/css/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Sometimes you might find yourself wanting to write some arbitrary styles for an

- [CSS directive](#css-directive)
- [Accessing the theme](#accessing-the-theme)
- [Screen Directive](#screen-directive)
- [Animation Directive](#animation-directive)
- [Keyframes Helper](#keyframes-helper)

Expand Down Expand Up @@ -90,6 +91,8 @@ const styles = css({
Tagged template literal syntax works like in emotion, goober or styled-components:

```js
import { css, apply, theme } from 'twind/css'

const style = css`
color: rebeccapurple;
background-color: ${theme('colors.gray.500')};
Expand Down Expand Up @@ -146,6 +149,10 @@ css`
p {
${apply('my-5')}
}
h1 {
${apply`text(black dark:white hover:purple-500)`}
}
`
```

Expand Down Expand Up @@ -174,6 +181,48 @@ css({
})
```

## Screen Directive

The `screen` directive allows you to create media queries that reference your breakpoints by name instead of duplicating their values in your own CSS.

For example, say you have a `sm` breakpoint at `640px` and you need to write some custom CSS that references this breakpoint.

Instead of writing a raw media query that duplicates that value like this:

```js
css`
@media (min-width: 640px) {
/* ... */
}
`
```

...you can use the `screen` directive and reference the breakpoint by name:

```js
import { css, screen, apply } from 'twind/css'

// With template literal
css`
${screen('sm')} {
/* ... */
}
${screen('md', css` /* ... */ `)}
${screen('lg', css({ /* ... */ }))}
${screen('xl', { /* ... */ })}
${screen('2xl', apply` ... `)}
`

// With object notation
css(
screen('md', css` /* ... */ `),
screen('lg', css({ /* ... */ })),
screen('xl', { /* ... */ }),
screen('2xl', apply` ... `),
)
`
```

## Animation Directive

Custom animations are difficult to configure in Tailwind. During {@link twind.setup} you need to add to the `theme.animation` section and the `theme.keyframes` section. This means all animations must known before hand and you can not use "one-off" animations.
Expand Down Expand Up @@ -249,7 +298,19 @@ const bounce = animation(
)
```

The second parameter are the waypoints of a [@keyframes](https://developer.mozilla.org/en-US/docs/Web/CSS/@keyframes) at-rule in CSS object format.
The second parameter are the waypoints of a [@keyframes](https://developer.mozilla.org/en-US/docs/Web/CSS/@keyframes) at-rule in CSS object format. The [keyframes helper](#keyframes-helper) can be used the create waypoints.

The result of `animation` can be used within `css`:

```js
css(bounce, {
/* other properties */
})
css`
${bounce}
`
```

## Keyframes Helper

Expand Down
75 changes: 60 additions & 15 deletions src/css/css.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,11 @@ import type { VirtualSheet } from '../sheets/index'

import { virtualSheet } from '../sheets/index'
import { create, strict } from '../index'
import { css, keyframes, animation, apply, theme } from './index'
import { css, keyframes, animation, apply, theme, screen } from './index'

const test = suite<{
sheet: VirtualSheet
tw: Instance['tw']
css: typeof css
keyframes: typeof keyframes
animation: typeof animation
}>('css')

test.before((context) => {
Expand All @@ -25,17 +22,13 @@ test.before((context) => {
prefix: false,
})
context.tw = instance.tw

context.css = css.bind(context.tw)
context.keyframes = keyframes.bind(context.tw)
context.animation = animation.bind(context.tw) as typeof animation
})

test.after.each(({ sheet }) => {
sheet.reset()
})

test('create css', ({ css, tw, sheet }) => {
test('create css', ({ tw, sheet }) => {
const style = css({
backgroundColor: 'hotpink',
'&:hover': {
Expand Down Expand Up @@ -110,7 +103,7 @@ test('can be used with variants', ({ tw, sheet }) => {
])
})

test('keyframes', ({ keyframes, css, tw, sheet }) => {
test('keyframes', ({ tw, sheet }) => {
const bounce = keyframes({
'from, 20%, 53%, 80%, to': {
transform: 'translate3d(0,0,0)',
Expand Down Expand Up @@ -143,7 +136,7 @@ test('keyframes', ({ keyframes, css, tw, sheet }) => {
])
})

test('keyframes lazy', ({ keyframes, css, tw, sheet }) => {
test('keyframes lazy', ({ tw, sheet }) => {
const bounce = keyframes({
'from, 20%, 53%, 80%, to': {
transform: 'translate3d(0,0,0)',
Expand Down Expand Up @@ -175,7 +168,7 @@ test('keyframes lazy', ({ keyframes, css, tw, sheet }) => {
])
})

test('animation', ({ animation, tw, sheet }) => {
test('animation', ({ tw, sheet }) => {
const bounce = animation('1s ease infinite', {
'from, 20%, 53%, 80%, to': {
transform: 'translate3d(0,0,0)',
Expand All @@ -201,7 +194,7 @@ test('animation', ({ animation, tw, sheet }) => {
])
})

test('animation with callback', ({ animation, tw, sheet }) => {
test('animation with callback', ({ tw, sheet }) => {
const slidein = animation(
({ theme }) => `${theme('durations', '500')} ${theme('transitionTimingFunction', 'in-out')}`,
{
Expand All @@ -224,7 +217,7 @@ test('animation with callback', ({ animation, tw, sheet }) => {
])
})

test('animation object notation', ({ animation, tw, sheet }) => {
test('animation object notation', ({ tw, sheet }) => {
const bounce = animation(
{
animationDuration: '1s',
Expand Down Expand Up @@ -257,7 +250,7 @@ test('animation object notation', ({ animation, tw, sheet }) => {
])
})

test('animation with variant', ({ animation, tw, sheet }) => {
test('animation with variant', ({ tw, sheet }) => {
const bounce = animation('1s ease infinite', {
'from, 20%, 53%, 80%, to': {
transform: 'translate3d(0,0,0)',
Expand Down Expand Up @@ -681,4 +674,56 @@ test('extending preflight styles', () => {
)
})

test('screen directive (template literal)', ({ tw, sheet }) => {
const style = css`
${screen('sm')} {
match: sm;
}
${screen(
'md',
css`
match: md;
`,
)}
${screen('lg', css({ match: 'md' }))}
${screen('xl', { match: 'xl' })}
${screen('2xl', apply`underline`)}
`

assert.equal(sheet.target, [])

assert.is(tw(style), 'tw-560pmr')
assert.equal(sheet.target, [
'@media (min-width: 640px){.tw-560pmr{match:sm}}',
'@media (min-width: 768px){.tw-560pmr{match:md}}',
'@media (min-width: 1024px){.tw-560pmr{match:md}}',
'@media (min-width: 1280px){.tw-560pmr{match:xl}}',
'@media (min-width: 1536px){.tw-560pmr{text-decoration:underline}}',
])
})

test('screen directive (object notation)', ({ tw, sheet }) => {
const style = css(
screen(
'md',
css`
match: md;
`,
),
screen('lg', css({ match: 'md' })),
screen('xl', { match: 'xl' }),
screen('2xl', apply`underline`),
)

assert.equal(sheet.target, [])

assert.is(tw(style), 'tw-uorso7')
assert.equal(sheet.target, [
'@media (min-width: 768px){.tw-uorso7{match:md}}',
'@media (min-width: 1024px){.tw-uorso7{match:md}}',
'@media (min-width: 1280px){.tw-uorso7{match:xl}}',
'@media (min-width: 1536px){.tw-uorso7{text-decoration:underline}}',
])
})

test.run()
24 changes: 24 additions & 0 deletions src/css/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,3 +234,27 @@ export const animation = ((
...(is.object(value) ? value : { animation: value }),
animationName: is.function(waypoints) ? waypoints : keyframes(waypoints),
})) as Animation

export interface ScreenDirective {
(context: Context): string
}
export interface Screen {
(size: string): ScreenDirective
(size: string, css: CSSDirective | MaybeArray<CSSRules | Falsy>): CSSDirective
}

const screenFactory = (
{ size, rules }: { size: string; rules?: CSSDirective | MaybeArray<CSSRules | Falsy> },
context: Context,
): string | CSSRules => {
const media = `@media (min-width: ${context.theme('screens', size)})`

return rules === undefined
? media
: {
[media]: is.function(rules) ? evalThunk(rules, context) : cssFactory([rules], context),
}
}

export const screen = ((size: string, rules?: CSSDirective | MaybeArray<CSSRules | Falsy>) =>
directive(screenFactory, { size, rules })) as Screen

0 comments on commit cf37f6d

Please sign in to comment.