UNPKG

cirrus-ui

Version:

A lightweight UI framework written in SCSS

377 lines (334 loc) 12.8 kB
@use 'sass:list'; @use 'sass:map'; @use 'sass:meta'; @use 'sass:selector'; @use 'sass:string'; @use 'config'; @use 'size'; @use 'mixins'; $default-prefix: 'u'; $default-override: ''; $default-delimiter: '-'; $default-variant-delimiter: '\\:'; /* Main utility for generating utility classes with different viewports, delimiters, base class names, etc. Example: .sm\:hover\:u-text-blue:hover { ... } @class-prefix {string} [config.$utility-prefix] - prefix used for generated classes. This is the first section of the class name. Can be empty. @delimiter {string} [config.$delimiter] - delimiter used in class name body but not for separating psuedos. Can be empty. @base-class-name {string} - the root of the class name. For the utility class above, 'blue' is the base class name @class-value-pairs {map<string, any>[]} - list of mappings that maps the variant name (e.g. 'blue') to a map of CSS properties to values @variants {string[]} [()] - list of strings specifying which variants to generate styles for. Possible values: - 'responsive', - 'dark', 'light', - 'reduce-motion', - 'first-of-type', - 'last-of-type', - 'portrait', 'landscape', - 'hover', 'group-hover', - 'focus', 'group-focus', 'focus-visible', 'focus-within', - 'active', - 'visited', - 'checked', - 'disabled' @variant-delimiter {string} [config.$variant-delimiter] - delimiter used to separate the variant portion of the class. Can be empty but not advisable. @override {string} [config.$override] - override for CSS properties, like '!important' */ @mixin utility( $class-prefix: config.$utility-prefix, $delimiter: config.$delimiter, $base-class-name, $class-value-pairs, $variants: (), $variant-delimiter: config.$variant-delimiter, $override: config.$override ) { $config: ( class-prefix: $class-prefix, delimiter: $delimiter, variant-delimiter: $variant-delimiter, override: $override, ); @include generate-rules-pseudos-selectors( $config: $config, $variants: $variants, $context: (), ) using ($props...) { @include kv-class-generator($base-class-name, $class-value-pairs, $props...) using ($key, $value, $props...) { // Set $props... so we don't crash in the event of extra params (can happen due to very recursive nature of this generator) @include mixins.explode-properties($value, $override); } } } @mixin generate-rules-pseudos-selectors($config, $variants: (), $context: ()) { $rules: (); // Media queries, after responsive classes $pseudos: (); // Selectors like :focus // Iterate over all variants and split between rules and pseudos @each $variant in $variants { // Check for get-rules @if meta.function-exists('get-#{$variant}') { $func: meta.get-function('get-#{$variant}'); $rule-map: meta.call($func); $rules: map.set($rules, $variant, $rule-map); // ( 'dark': '@media (prefers-color-scheme: dark)',) } @else { $pseudos: list.append($pseudos, $variant); } } // Generate classes without rules @include generate-variants($config, $pseudos, $context) using ($props...) { @content ($props...); } // Generate with rules // Rules for responsiveness and light/dark mode are roots of separate classes, they never appear together @each $rule in map.values($rules) { @each $key, $value in $rule { $current-context: map.merge( $context, ( rule: $key, ) ); @if string.index($value, '@media') { $value: string.slice($value, 7); @media #{$value} { @include generate-variants($config, $pseudos, $current-context) using ($props...) { @content ($props...); } } } } } } @mixin generate-variants($config, $variants: (), $context: ()) { // Get classes without pseudos, only rules $base-class-name: map.get($context, base-class-name); @if $base-class-name != null { // There is a parent selector. This is true if we are generating classes for the following scenarios: // - Classes with .group as parent // - Classes that have the main class name constructed. This is a recursive call to add in all the rules and variants to the class naming (e.g. text-blue and we want to add 'md:' and ':hover') @include get-base-class($config, $context) using ($props...) { @content ($props...); } @each $variant in $variants { $current-context: map.merge( $context, ( variant: $variant, pseudo: $variant, ) ); @if string.index($variant, 'group-') { $current-context: map.merge( $current-context, ( pseudo: string.slice($variant, 7), scope: '.group', ) ); } @include get-base-class($config, $current-context) using ($props...) { @content ($props...); } } } @else { @content ($config, $variants, $context); } } @mixin get-base-class($config, $context) { $base: (); $delimiter: map.get( $map: $config, $key: delimiter, ); $class-prefix: map.get($config, class-prefix); $base-class-name: map.get($context, base-class-name); @if $base-class-name != null { // Required to be comma separated since it will be used in generate-class() and treated like a selector $new-base: ''; @if $class-prefix != null and string.length($class-prefix) > 0 { // If there is a prefix defined, use it in the class name $new-base: $class-prefix + $delimiter; } $new-base: $new-base + $base-class-name; $base: list.append($base, $new-base, comma); } @include generate-class($config, $base, $context...) using ($props...) { @content ($props...); } } @mixin generate-class($config, $base, $rule: null, $variant: null, $pseudo: null, $scope: null, $base-class-name: '') { $variant-delimiter: map.get( $map: $config, $key: variant-delimiter, ); @if $variant { $base: selector.append('#{$variant}#{$variant-delimiter}', $base); } @if $rule { $base: selector.append('#{$rule}#{$variant-delimiter}', $base); } @if $scope and $pseudo { // For group related variants (e.g. .group:hover) $parent-selector: selector.append($scope, ':#{$pseudo}'); $base: selector.nest($parent-selector, string-to-class($base)); // Nest class below the parent selector } @else if $scope { // Contains parent selector $base: selector.nest($scope, string-to-class($base)); } @else if $pseudo { // Contains pseudo $base: selector.append(string-to-class($base), ':#{$pseudo}'); } @else { // Contains nothing $base: string-to-class($base); } // Generate the class #{$base} { @content ($config); } } @mixin kv-class-generator($base-class-name, $class-value-pairs, $config: (), $variants: (), $context: ()) { @if $base-class-name != null { $new-base-class-name: ''; @if string.length($base-class-name) > 0 { $new-base-class-name: $base-class-name + map.get($config, delimiter); } @each $key, $value in $class-value-pairs { $context: map.merge( $context, ( base-class-name: $new-base-class-name + $key, ) ); @include generate-rules-pseudos-selectors($config, $variants, $context) using ($props...) { @content ($key, $value, $props...); } } } @else { @include generate-rules-pseudos-selectors($config, $variants, $context) using ($props...) { @each $key, $value in $class-value-pairs { .#{$key} { @include generate-rules-pseudos-selectors($config, $variants, $context) using ($props...) { @content ($key, $value, $props...); } } } } } } @mixin inline-class-generator($base-class-name, $config: (), $variants: (), $context: ()) { @if $base-class-name == null { @error 'Base class name must be provided'; } $context: map.merge( $context, ( base-class-name: $base-class-name, ) ); @include generate-rules-pseudos-selectors($config, $variants, $context) using ($props...) { @content ($props...); } } @function get-responsive() { $result: (); @each $key, $value in config.$breakpoint-pairs { $result: map.set($result, $key, '@media screen and (min-width: #{map.get(config.$breakpoints, $value)})'); } @return $result; } @function get-dark() { @return ('dark': '@media (prefers-color-scheme: dark)'); } @function get-light() { @return ('light': '@media (prefers-color-scheme: light)'); } @function get-reduce-motion() { @return ('reduce-motion': '@media (prefers-reduced-motion: reduce)'); } @function get-portrait() { @return ('portrait': '@media (orientation: portrait)'); } @function get-landscape() { @return ('landscape': '@media (orientation: landscape)'); } @function class-value-map-with-single-property($property, $property-values) { $result: (); @each $key, $value in $property-values { $result: map.set( $result, $key, ( $property: $value, ) ); } @return $result; } /** * Convert a string to a proper class selector. * @param: {String} $selector * @return: {Selector} Returns the class selector. */ @function string-to-class($selector) { $separator: list.separator($selector); $result: (); @each $s in $selector { $selector-str: '#{$s}'; @if $selector-str != '' { $result: list.append($result, '.#{$selector-str}', $separator); } } @return $result; } @function class-value-to-property-map($class-value-map, $property) { $result: (); @each $class, $value in $class-value-map { $result: map.set( $result, $class, ( $property: $value, ) ); } @return $result; } /* Utility to use when generating classes with your own series of SCSS rules. This is typically used with the inline-class-generator mixin. @delimiter {string} [config.$delimiter] - delimiter used in class name body but not for separating psuedos. Can be empty. @variants {string[]} [()] - list of strings specifying which variants to generate styles for. Possible values: - 'responsive', - 'dark', 'light', - 'reduce-motion', - 'first-of-type', - 'last-of-type', - 'portrait', 'landscape', - 'hover', 'group-hover', - 'focus', 'group-focus', 'focus-visible', 'focus-within', - 'active', - 'visited', - 'checked', - 'disabled' @variant-delimiter {string} [config.$variant-delimiter] - delimiter used to separate the variant portion of the class. Can be empty but not advisable. @override {string} [config.$override] - override for CSS properties, like '!important' */ @mixin utility-with-body( $delimiter: config.$delimiter, $variants: (), $variant-delimiter: config.$variant-delimiter, $override: config.$override ) { $config: ( delimiter: $delimiter, variant-delimiter: $variant-delimiter, override: $override, ); @include generate-rules-pseudos-selectors( $config: $config, $variants: $variants, $context: (), ) using ($props...) { @content ($props...); } }