lucywheel
11/1/2016 - 4:27 PM

viewport.scss

$breakpoints: (
  s: 450px,
  m: 600px,
  l: 900px,
  xl: 1200px
) !default;

@function get-breakpoint($name, $breakpoints: $breakpoints) {
  $breakpoint: map-get($breakpoints, $name);
  @if $breakpoint == null {
    @error "Could not find breakpoint `#{$name}` in `#{$breakpoints}`.";
  }
  @return $breakpoint;
}

/// Viewport mixin to ergonomically generate media queries
///
/// # Parameters
///
/// - `$query`: The breakpoint constrait for this viewport. This is not
///   formatted like a CSS media query, please see the documentation below.
/// - `$breakpoints`: Optional, map of size names to breakpoints (e.g.,
///   `(phone: 600px, desktop: 1200px)`).
/// - `@content` block.
///
/// ## Query syntax
///
/// The `$query` paramter can be in one of the following forms:
///
/// - `$breakpoint`: Covers everything up to and including `$breakpoint`
/// - `below $breakpoint`: Covers everything up to but not including
///   `$breakpoint`
/// - `above $breakpoint`: Covers everything just above `$breakpoint`
/// - `from $narrow to $wide`: Covers everything from `$narrow` up to but not
///   including `$wide`. (I originally wanted to call this `between $x and $y`
///   but SCSS interprets the `and` as an operator and thus sees this as a tuple
///   with `between` and the result of `$x and $y` in it.)
///
/// # Examples
///
/// ```scss
/// @include viewport(s)               { /* */ }
/// @include viewport(above m)         { .t3 { content: "above m" } }
/// @include viewport(below xl)        { .t4 { content: "below xl" } }
/// @include viewport(from m to l)     { .t2 { content: "between m and l" } }
/// ```
@mixin viewport($query, $breakpoints: $breakpoints) {
  $parse-error: "Viewport mixin could not parse query `#{$query}`.";

  // Usage: `viewport($breakpoint)`
  @if length($query) == 1 {
    $breakpoint: get-breakpoint($query, $breakpoints);
    @media screen and (max-width: $breakpoint) {
      @content;
    }
  }
  // Usage: `viewport(above|below $breakpoint)`
  @else if length($query) == 2 {
    $direction: nth($query, 1);
    $breakpoint: get-breakpoint(nth($query, 2), $breakpoints);

    @if $direction == "above" {
      @media screen and (min-width: $breakpoint) {
        @content;
      }
    } @else if $direction == "below" {
      @media screen and (max-width: $breakpoint - 1px) {
        @content;
      }
    } @else {
      @error $parse-error;
    }
  }
  // Usage: `viewport(from $narrow to $wide)`
  @else if length($query) == 4 {
    @if (nth($query, 1) != "from") or (nth($query, 3) != "to") {
      @error $parse-error;
    }

    $narrow: get-breakpoint(nth($query, 2), $breakpoints);
    $wide: get-breakpoint(nth($query, 4), $breakpoints);

    @media screen and (min-width: $narrow) and (max-width: $wide - 1px) {
      @content;
    }
  } @else {
    @error $parse-error;
  }
}

// Tests

// - Compile success
@include viewport(s)               { .t1 { content: "s" } }
@include viewport(above m)         { .t3 { content: "above m" } }
@include viewport(below xl)        { .t4 { content: "below xl" } }
@include viewport(from m to l)     { .t2 { content: "between m and l" } }

// - Compile fail
// @include viewport() { /* the endless void */ }
// @include viewport(nope) { /* so sad */ }
// @include viewport(what is going on) { /* dafuq */ }