novacbn / kahi-ui

Straight-forward Svelte UI for the Web
https://kahi-ui.nbn.dev
MIT License
187 stars 5 forks source link

[1.0] Theming — Viewports #99

Closed novacbn closed 2 years ago

novacbn commented 2 years ago

Description

When the progressive introduction of SASS into the code base starts this month, I want to start converting common code into atoms, mixins, and functions. To reduce boilerplate, etc for viewport management.

Definition API

Ideally we should have a simple declarative API for defining the viewports which the Framework will dump into the theming output CSS else where. While we could use SASS Variables for this, I think an API based on SASS Mixins would be better due to being able to perform magic behind the scenes:

@include define-viewport($viewport: "mobile", $width: 640px);
@include define-viewport($viewport: "tablet", $width: 768px);
@include define-viewport($viewport: "desktop", $width: 1024px);
@include define-viewport($viewport: "widescreen", $width: 1280px);

Arguments are named, each viewport's breakpoint width is defined, and it's all easily readable. Ideally we would've used a SASS Function, but those cannot be used outside of rule declarations.

As far as the implementation goes, the requirements are:

$maximum: null;
$minimum: null;

$viewports: ();

@mixin define-viewport($viewport, $width) {
  @if (not $maximum or $width > $maximum) {
    $maximum: $width !global;
  }

  @if (not $minimum or $width < $minimum) {
    $minimum: $width !global;
  }

  $viewports: map.merge(
    $viewports,
    (
      $viewport: $width,
    )
  ) !global;
}

Media Query

Using the above cached viewport data, we should have an API surface for simplifying @media access to promote non-repeating code.

@include query-viewport("mobile") {
  .hero > header {
    font-size: ...;
  }
}

For this implementation we'll need a helper function get-viewport-range($viewport: str) first, to simplify Viewport calculations:

@function get-viewport-range($viewport) {
  $width: map.get($viewports, $viewport);

  @if ($width == $minimum) {
    @return ("maximum": $width, "minimum": 0px);
  }

  $closest-width: null;
  @each $target-viewport, $target-width in $viewports {
    @if (
      $target-viewport !=
        $viewport and
        $target-width <
        $width and
        (not $closest-width or $target-width > $closest-width)
    ) {
      $current-distance: if($closest-width, $width - $closest-width, null);
      $closest-distance: $width - $target-width;

      @if (not $current-distance or $current-distance > $closest-distance) {
        $closest-width: $target-width;
      }
    }
  }

  @if ($width == $maximum) {
    @return ("maximum": 99999999px, "minimum": $closest-width + 1px);
  }

  @return ("maximum": $width, "minimum": $closest-width + 1px);
}

Using that helper function, we can start with the query-viewport($viewport: string) mixin:

@mixin query-viewport($viewport) {
  $range: get-viewport-range($viewport);

  $maximum-width: map.get($range, "maximum");
  $minimum-width: map.get($range, "minimum");

  @if ($maximum-width == $minimum) {
    @media (max-width: $maximum-width) {
      @content ($viewport);
    }
  } @else if ($maximum-width == $maximum) {
    @media (min-width: $minimum-width) {
      @content ($viewport);
    }
  } @else {
    @media (min-width: #{$minimum-width}) and (max-width: #{$maximum-width}) {
      @content ($viewport);
    }
  }
}

Next we'll need a higher level multi-input mixin for accessing multiple Viewports at once, e.g.

@include query-viewports("mobile", "tablet", "desktop", "widescreen") using ($viewport) {
  [data-hidden~="#{$viewport}"] {
    display: none;
  }
}

So we need the following requirements:

@mixin query-viewports($viewports...) {
  @each $viewport, $index in $viewports {
    @include query-viewport($viewport) using ($viewport) {
      @content ($viewport);
    }
  }
}

CSS Variables

We need to dump all Viewport values as CSS Custom Properties. Ideally we would use these properties for media queries, but CSS doesn't support that. However they are still useful so the Javascript codebase doesn't have to hardcode the values.

:root {
  @include each-viewport() using ($viewport, $minimum-width, $maximum-width) {
    --viewport-#{$viewport}-max: #{$maximum-width};
    --viewport-#{$viewport}-min: #{$minimum-width};
  }
}

Which would have output similar to:

:root {
  --viewport-mobile-max: 640px;
  --viewport-mobile-min: 0px;
  --viewport-tablet-max: 768px;
  --viewport-tablet-min: 641px;
  --viewport-desktop-max: 1024px;
  --viewport-desktop-min: 769px;
  --viewport-widescreen-max: 99999999px;
  --viewport-widescreen-min: 1025px;
}

To accomplish this, we need the following implementation for the each-viewport($viewports...: str[]) mixin:

@mixin each-viewport() {
  @each $viewport, $width in $viewports {
    $range: get-viewport-range($viewport);

    $maximum-width: map.get($range, "maximum");
    $minimum-width: map.get($range, "minimum");

    @content ($viewport, $minimum-width, $maximum-width);
  }
}