Skip to content

Instantly share code, notes, and snippets.

@enten
Last active June 30, 2018 20:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save enten/cf9455a4a256ca8db03f945ac01f155f to your computer and use it in GitHub Desktop.
Save enten/cf9455a4a256ca8db03f945ac01f155f to your computer and use it in GitHub Desktop.
atomik: responsive design in an atomic way
//
// atomik: responsive design in an atomic way.
//
// When responsive design must be or becomes an atomic job:
// let's atomik do the dirty work with a pretty-sugar api.
//
//
// 0. CORE
//
@mixin css-props($props) {
@each $name, $value in $props {
#{$name}: $value;
}
@content;
}
// TODO remove it by hardcoded $A_BASE values.
@function rem($multiplier) {
$font-size: 10px;
@return $multiplier * $font-size;
}
//
// 1. CONSTANTS
//
//
// Default first screen.
//
// whe `mobile`? because we live in mobile-first world!
//
$A_FIRST_SCREEN: mobile !default;
//
// Default first level.
//
// why `4`? because levels 1-2-3 are for highlighting
// and levels 4-5-6 for lowering (and level 7 is for moron _\*laught\*_)
//
$A_FIRST_LEVEL: 4 !default;
//
// Map of screen media-query features.
//
$A_BREAKING: (
mobile: ( max-width: 899px ),
desktop: ( min-width: 900px )
) !default;
//
// Map of sizing used to compute value for each screen resolution.
//
$A_SIZING: (
border-radius: (
mobile: ( 0, rem(2), rem(1), rem(0.5), rem(0.25), rem(0.125), rem(0.1), 0 ),
desktop: ( 0, rem(4), rem(2), rem(1), rem(0.5), rem(0.25), rem(0.125), 0 )
),
border-width: (
mobile: ( 0, rem(2), rem(1), rem(0.5), rem(0.25), rem(0.125), rem(0.1), 0 ),
desktop: ( 0, rem(4), rem(2), rem(1), rem(0.5), rem(0.25), rem(0.125), 0 )
),
font-size: (
mobile: ( 0, rem(3), rem(2.5), rem(2), rem(1.5), rem(1.25), rem(1), rem(0.75) ),
desktop: ( 0, rem(6), rem(5), rem(4), rem(3), rem(1.5), rem(1.25), rem(1) )
),
font-weight: (
mobile: ( 100, bold, 800, 700, 500, 300, 200, 100 ),
desktop: ( 100, bold, 900, 800, 700, 500, 300, 200 )
),
letter-spacing: (
mobile: ( 0, rem(2.5), rem(2), rem(1.5), rem(1), rem(0.75), rem(0.5), 0 ),
desktop: ( 0, rem(3), rem(2.5), rem(2), rem(1.5), rem(1), rem(0.75), rem(0.5) )
),
line-height: (
mobile: ( 0, rem(3), rem(2.5), rem(2), rem(1.5), rem(1.25), rem(1), rem(0.75) ),
desktop: ( 0, rem(6), rem(5), rem(4), rem(3), rem(1.5), rem(1.25), rem(1) )
),
// spacing: (
// mobile: ( none, 32px, 16px, 8px, 4px, 2px, 1px, 0 ),
// desktop: ( none, 128px, 64px, 32px, 16px, 8px, 4px, 2px )
// ),
margin: (
mobile: ( none, 32px, 16px, 8px, 4px, 2px, 1px, 0 ),
desktop: ( none, 128px, 64px, 32px, 16px, 8px, 4px, 2px )
),
padding: (
mobile: ( none, 32px, 16px, 8px, 4px, 2px, 1px, 0 ),
desktop: ( none, 128px, 64px, 32px, 16px, 8px, 4px, 2px )
),
height: (
mobile: ( none, rem(48), rem(32), rem(16), rem(8), rem(4), rem(2), 0 ),
desktop: ( none, rem(96), rem(48), rem(32), rem(16), rem(8), rem(4), rem(2) )
),
width: (
mobile: ( none, rem(48), rem(32), rem(16), rem(8), rem(4), rem(2), 0 ),
desktop: ( none, rem(96), rem(48), rem(32), rem(16), rem(8), rem(4), rem(2) )
)
) !default;
//
// Map of styling used to resolve sizing.
//
// Use String to reference a sizing.
// Use List to derive/compose or alias/shorthand styling.
//
$A_STYLING: (
// border styling
border: border,
border-top: border,
border-right: border,
border-bottom: border,
border-left: border,
border-top-left: ( border-top, border-left ),
border-top-right: ( border-top, border-right ),
border-bottom-left: ( border-bottom, border-left ),
border-bottom-right: ( border-bottom, border-right ),
border-vertical: ( border-top, border-bottom ),
border-horizontal: ( border-top, border-bottom ),
// border-width styling
border-width: border-width,
border-width-top: border-width,
border-width-right: border-width,
border-width-bottom: border-width,
border-width-left: border-width,
border-width-top-left: ( border-width-top, border-width-left ),
border-width-top-right: ( border-width-top, border-width-right ),
border-width-bottom-left: ( border-width-bottom, border-width-left ),
border-width-bottom-right: ( border-width-bottom, border-width-right ),
border-width-vertical: ( border-width-top, border-width-bottom ),
border-width-horizontal: ( border-width-top, border-width-bottom ),
// border-style styling
border-style: border-style,
border-style-top: border-style,
border-style-right: border-style,
border-style-bottom: border-style,
border-style-left: border-style,
border-style-top-left: ( border-style-top, border-style-left ),
border-style-top-right: ( border-style-top, border-style-right ),
border-style-bottom-left: ( border-style-bottom, border-style-left ),
border-style-bottom-right: ( border-style-bottom, border-style-right ),
border-style-vertical: ( border-style-top, border-style-bottom ),
border-style-horizontal: ( border-style-top, border-style-bottom ),
// border-color styling
border-color: border-color,
border-color-top: border-color,
border-color-right: border-color,
border-color-bottom: border-color,
border-color-left: border-color,
border-color-top-left: ( border-color-top, border-color-left ),
border-color-top-right: ( border-color-top, border-color-right ),
border-color-bottom-left: ( border-color-bottom, border-color-left ),
border-color-bottom-right: ( border-color-bottom, border-color-right ),
border-color-vertical: ( border-color-top, border-color-bottom ),
border-color-horizontal: ( border-color-top, border-color-bottom ),
// border-radius styling
border-radius: border-radius,
border-top-left-radius: border-radius,
border-top-right-radius: border-radius,
border-bottom-left-radius: border-radius,
border-bottom-right-radius: border-radius,
border-top-radius: ( border-top-left-radius, border-top-right-radius ),
border-bottom-radius: ( border-bottom-left-radius, border-bottom-right-radius ),
// font styling
font-size: font-size,
font-weight: font-weight,
letter-spacing: letter-spacing,
line-height: line-height,
// margin styling
margin: margin,
margin-top: margin,
margin-right: margin,
margin-bottom: margin,
margin-left: margin,
margin-top-left: ( margin-top, margin-left ),
margin-top-right: ( margin-top, margin-right ),
margin-bottom-left: ( margin-bottom, margin-left ),
margin-bottom-right: ( margin-bottom, margin-right ),
margin-vertical: ( margin-top, margin-bottom ),
margin-horizontal: ( margin-top, margin-bottom ),
// padding styling
padding: padding,
padding-top: padding,
padding-right: padding,
padding-bottom: padding,
padding-left: padding,
padding-top-left: ( padding-top, padding-left ),
padding-top-right: ( padding-top, padding-right ),
padding-bottom-left: ( padding-bottom, padding-left ),
padding-bottom-right: ( padding-bottom, padding-right ),
padding-vertical: ( padding-top, padding-bottom ),
padding-horizontal: ( padding-left, padding-right ),
// width styling
width: width,
max-width: width,
min-width: width,
// height styling
height: height,
max-height: height,
min-height: height,
// font shorthand-styling
fs: ( font-size, ),
fw: ( font-weight, ),
ls: ( letter-spacing, ),
lh: ( line-height, ),
// margin shorthand-styling
ma: ( margin, ),
mt: ( margin-top, ),
mr: ( margin-right, ),
mb: ( margin-bottom, ),
ml: ( margin-left, ),
mtl: ( margin-top-left, ),
mtr: ( margin-top-right, ),
mbl: ( margin-bottom-left, ),
mbr: ( margin-bottom-right, ),
mv: ( margin-vertical, ),
mh: ( margin-horizontal, ),
// padding shorthand-styling
pa: ( padding, ),
pt: ( padding-top, ),
pr: ( padding-right, ),
pb: ( padding-bottom, ),
pl: ( padding-left, ),
ptl: ( padding-top-left, ),
ptr: ( padding-top-right, ),
pbl: ( padding-bottom-left, ),
pbr: ( padding-bottom-right, ),
pv: ( padding-vertical, ),
ph: ( padding-horizontal, ),
// width shorthand-styling
w: ( width, ),
max-w: ( max-width, ),
min-w: ( min-width, ),
// height shorthand-styling
h: ( height, ),
max-h: ( max-height, ),
min-h: ( min-height, )
) !default;
//
// Default atomik [BluePrint].
//
$A_BASE: (
first-screen: $A_FIRST_SCREEN,
first-level: $A_FIRST_LEVEL,
breaking: $A_BREAKING,
sizing: $A_SIZING,
styling: $A_STYLING
) !default;
//
// 2. FUNCTIONS
//
//
// Creates new blueprint based on $A_BASE.
//
// If args are given, every arg must be a [BluePrint]
// which will be merge into the base blueprint.
//
// A blueprint instance is the only required variable
// to work with the entire API functions and mixins.
//
// @param $user-a [Array<BluePrint>]
// @returns [BluePrint] A new blueprint.
//
@function a-core($user-a...) {
$a: map-merge((), $A_BASE);
@if length($user-a) < 1 {
@return $a;
}
@for $index from 1 through length($user-a) {
$b: nth($user-a, $index);
$a: a-core-merge($a, $b);
}
@return $a;
}
//
// Merges two blueprints into a new [BluePrint].
// Values in `$b` will take precedence over values in `$a`.
//
// This is the best way to merging blueprints.
//
// Only [BluePrint] propertes are merged.
//
// @param $a [BluePrint]
// @param $b [BluePrint]
// @returns [BluePrint]
//
@function a-core-merge($a, $b) {
// support user first-level
@if map-has-key($b, first-level) {
$a: map-merge($a, ( first-level: map-get($b, first-level) ));
}
// support user first-screen
@if map-has-key($b, first-screen) {
$a: map-merge($a, ( first-screen: map-get($b, first-screen) ));
}
// support user breaking
//
// devnote: breaking is an advanced option
// it can't be merged (it doesn't make sense)
@if map-has-key($b, breaking) {
$a: map-merge($a, ( breaking: map-get($b, breaking) ));
}
// support user styling
//
// devnote: merging map-merge($a.styling, $b.styling)
@if map-has-key($b, styling) {
$a: map-merge($a, ( styling: map-merge(map-get($a, styling), map-get($b, styling)) ));
}
// support user sizing
//
// devnote: dvance merge because sizing is the
// more dfficult option to extend
@if map-has-key($b, sizing) {
$screens-keys: map-keys(map-get($a, breaking));
$screens-len: length($screens-keys);
$a-sizing: map-get($a, sizing);
$b-sizing: map-get($b, sizing);
// loop on each user sizing
@each $name, $b-screens in $b-sizing {
$a-screens: map-get($a-sizing, $name);
@if not $a-screens {
$a-screens: ();
}
$prev-screen: null;
// loop on each $a.breaking key
@for $index from 1 through $screens-len {
$screen: nth($screens-keys, $index);
// @warn('cp' $a-sizing);
$a-screen: map-get($a-screens, $screen);
$b-screen: map-get($b-screens, $screen);
$a-screen-len: 0;
$b-screen-len: 0;
@if $a-screen {
$a-screen-len: length($a-screen);
}
@if $b-screen {
$b-screen-len: length($b-screen);
}
$sizes: ();
$sizes-len: $a-screen-len;
@if $b-screen-len > $sizes-len {
$sizes-len: $b-screen-len;
}
// loop to deep level
@for $level from 1 through $sizes-len {
$size: null;
@if $level <= $a-screen-len {
$size: nth($a-screen, $level);
}
@if $level <= $b-screen-len {
$size: nth($b-screen, $level);
}
// support fallback placeholder with previous screen values
@if $size == _ {
$size: null;
@if $prev-screen {
$size: nth($prev-screen, $level);
}
}
$sizes: append($sizes, $size);
}
$prev-screen: $sizes;
// override screen sizes for $screen resolution
$a-screens: map-merge($a-screens, ( $screen: $sizes ));
}
// override sizing styling $name
$a-sizing: map-merge($a-sizing, ( $name: $a-screens ));
}
// finaly, set merged sizing
$a: map-merge($a, ( sizing: $a-sizing ));
}
// breaking cohercion (only if needed)
//
// devnote: required to avoid blinking with
// new narrow-screens (with min/max like tablet)
@if map-has-key($b, breaking) {
$screens-keys: map-keys(map-get($a, breaking));
$screens-len: length($screens-keys);
$a-sizing: map-get($a, sizing);
$b-sizing: map-get($b, sizing);
// loop on each names
@each $name, $a-screens in $a-sizing {
// filter name contains in $b-sizing
@if not map-has-key($b-sizing, $name) {
$prev-screen: null;
// loop on each screen
@for $index from 1 through $screens-len {
$screen: nth($screens-keys, $index);
$a-screen: map-get($a-screens, $screen);
// fallback with previous screen
@if not $a-screen and $prev-screen {
$a-screen: $prev-screen
}
// TODO inspect and comment
@if $a-screen {
$prev-screen: $a-screen;
$a-screens: map-merge($a-screens, ( $screen: $a-screen ));
} @else {
$a-screens: map-merge($a-screens, ( $screen: () ));
}
}
$a-sizing: map-merge($a-sizing, ( $name: $a-screens ));
}
}
// finaly, set coherced sizing
$a: map-merge($a, ( sizing: $a-sizing ));
}
@return $a;
}
//
// Returns a map of `( css-prop: sizing-key )`for a given styling.
//
// @param $a [BluePrint]
// @param $name [String]
// @returns [Map<String, String>]
//
@function a-styling-sizing-map($a, $name) {
$map: ();
// support of $name as list of styling names
@if type-of($name) == "list" {
@for $index from 1 through length($name) {
$map: map-merge($map, a-styling-sizing-map($a, nth($name, $index)));
}
@return $map;
}
$styling: map-get(map-get($a, styling), $name);
// support of $styling as list of styling names
// (allow of derive/compose/alias/shorthand styling)
@if type-of($styling) == "list" {
@for $index from 1 through length($styling) {
$map: map-merge($map, a-styling-sizing-map($a, nth($styling, $index)));
}
@return $map;
}
$sizing: map-get(map-get($a, sizing) , $styling);
@if $styling and $sizing {
$map: map-merge($map, ( $name: $styling ));
} @else if not $styling {
@warn("[atomik] StylingNotFoundWarn: #{$name}");
} @else if not $sizing {
@warn("[atomik] SizingNotFoundWarn: #{$styling}");
}
@return $map;
}
//
// Returns a map of styling map sized by screens.
//
// @param $a [BluePrint]
// @param $name [String]
// @param $level? [Number]
// @param $screens? [String|List<String>]
// @returns [Map<String, Map<String, Object>>]
//
@function a-styling-map($a, $name, $level: null, $screens: null) {
@if $level and type-of($level) != "number" {
$screens: $level;
$level: null;
}
@if not $level {
$level: map-get($a, first-level);
}
@if not $screens or $screens == all {
$screens: map-keys(map-get($a, breaking))
} @else if type-of($screens == "string") {
$screens: ( $screens, );
}
$styling-map: ();
$styling-sizing-map: a-styling-sizing-map($a, $name);
// about level incrementation line below:
// * list first index is 1 in SCSS (_\*private joke\*_) ;
// * remember that first index is reserved for `none` value ;
// * the first index 1 is the level 0.
$level: $level + 1;
// compute $styling-map for each screen
@for $index from 1 through length($screens) {
$screen: nth($screens, $index);
$styling: ();
// compute $styling for each style only if a sizing value exists
@each $styling-key, $sizing-key in $styling-sizing-map {
$sizing: map-get(map-get($a, sizing), $sizing-key);
@if $sizing and map-has-key($sizing, $screen) {
// avoid index out of bounds exception
// when $sizing doesn't contains the except level
// for the current $screen
@if $level <= length(map-get($sizing, $screen)) {
$value: nth(map-get($sizing, $screen), $level);
// avoid merge bad $value into $styling
@if $value {
$styling: map-merge($styling, ( $styling-key: $value ));
}
}
}
}
$styling-map: map-merge($styling-map, ( $screen: $styling ));
}
@return $styling-map;
}
//
// 3. MIXINS
//
//
// Mixin style inside a screen media-query.
//
// @param $a [BluePrint]
// @param $screen [String]
// @param $user-media-query? [Number] Additional media query features
//
@mixin a-screen($a, $screen, $user-media-query: null) {
$media-query: map-get(map-get($a, breaking), $screen);
@if $media-query {
@if $user-media-query {
@media only screen and ($media-query) and ($user-media-query) {
/* #{$screen} */
@content;
}
} @else {
@media only screen and ($media-query) {
/* #{$screen} */
@content;
}
}
}
}
//
// Mixin a style for a level.
//
// It will be responsive by default (when not $screens).
//
// @param $a [BluePrint]
// @param $name [String]
// @param $level? [Number]
// @param $screens? [String|List<String>]
//
@mixin a-style($a, $name, $level: null, $screens: null) {
@if $level and type-of($level) != "number" {
$screens: $level;
$level: null;
}
$first-screen: map-get($a, first-screen);
$styling-map: a-styling-map($a, $name, $level, $screens);
@each $screen, $props in $styling-map {
// when not $screens: make first-screen outside media-query block
// (mixin first-screen by default)
@if not $screens and $screen == $first-screen {
@include css-props(map-get($styling-map, $first-screen)) {
@content;
}
} @else {
@include a-screen($a, $screen) {
@include css-props(map-get($styling-map, $screen)) {
@content;
}
}
}
}
}
//
// Mixin a style for a level as like a screen resolution.
//
// @param $a [BluePrint]
// @param $name [String]
// @param $level? [Number]
// @param $screen [String]
//
@mixin a-style-like($a, $name, $level: null, $screen: null) {
@if $level and type-of($level) != "number" {
$screen: $level;
$level: null;
}
@if not $level {
$level: map-get($a, first-level);
}
@if not $screen {
@error("InvalidArgumentException: missing $screen argument");
}
@if type-of($screen) != "string" {
@error("InvalidArgumentException: invalid $screen argument `#{$screen}`");
}
$styling-map: a-styling-map($a, $name, $level, $screen);
@include css-props(map-get($styling-map, $screen)) {
@content;
}
}
//
// 4. Documentation
//
//
// Getting start guide
//
//
// 1. create blueprint
//
// A blueprint is a huge nested map.
//
// At its root level we have keys below.
//
// * first-level: number which indicates the first-level (`4` by default)
// * first-screen: string which is the key of the main breaking point (`mobile` as default)
// * breaking: map of screens and its breaking point features
// * sizing: all sizes by screen and level (he first level 0 is reserved for `none` value)
// * styling: like graphql resolver but here we resolved sizing
//
// @import '~atomik';
// 1.1 basic setup: new default-based blueprint
$a: a-core();
// 1.2 advanced setup: new blueprint with some overrides
$a: a-core((
first-level: 4, // default level value
first-screen: mobile, // default screen value
// breaking points screen
breaking: (
mobile: ( max-width: 899px ),
desktop: ( min-width: 900px )
),
// sizing groupe by name, screen and level
// the first index of screen list values is the level 0
sizing: (
font-size: (
mobile: ( 0, 30px, 25px, 20px, 15px, 12px, 10px, 8px ),
desktop: ( 0, 60px, 50px, 40px, 30px, 15px, 12px, 10px )
),
spacing: (
mobile: ( none, 32px, 16px, 8px, 4px, 2px, 1px, 0 ),
desktop: ( none, 128px, 64px, 32px, 16px, 8px, 4px, 2px )
)
),
// styling resolution
// when value is a string: it's a reference to a sizing key
// when value is a list: it's a derived/composed styling
styling: (
margin: spacing,
padding: spacing,
box: ( margin, padding )
)
));
//
// 2. Done
//
// Now, the setup is done! The only required thing
// to use every functions API is a blueprint instance.
//
// Now, we can do awesome responsive things without efforts.
//
// Three main mixins are declare by atomik scss import:
//
// * a-screen: mixin style inside a screen media-query
// * a-style: mixin a style for a level (responsive atomic way)
// * a-style-like: mixin a style for a level as like a screen resolution
//
// Let's use them!
//
.my-header {
@include a-style($a, ( box, font-size ), 3);
.title {
@include a-style($a, font-size, 2);
}
@include a-screen($a, desktop) {
@include a-style-like($a, min-height, 2, desktop);
}
}
//
// That will output:
//
// ```css
// .my-header {
// margin: 8px;
// padding: 8px;
// font-size: 20px; }
// @media only screen and ((min-width: 900px)) {
// .my-header {
// /* desktop */
// margin: 32px;
// padding: 32px;
// font-size: 40px; } }
// .my-header .title {
// font-size: 25px; }
// @media only screen and ((min-width: 900px)) {
// .my-header .title {
// /* desktop */
// font-size: 50px; } }
// @media only screen and ((min-width: 900px)) {
// .my-header {
// /* desktop */
// min-height: 480px; } }
// ```
//
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment