cirrus-ui
Version:
A lightweight UI framework written in SCSS
377 lines (334 loc) • 12.8 kB
Plain Text
$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'
*/
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,
);
generate-rules-pseudos-selectors(
$config: $config,
$variants: $variants,
$context: (),
)
using ($props...) {
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)
mixins.explode-properties($value, $override);
}
}
}
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
$variant in $variants {
// Check for get-rules
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)',)
} {
$pseudos: list.append($pseudos, $variant);
}
}
// Generate classes without rules
generate-variants($config, $pseudos, $context) using ($props...) {
($props...);
}
// Generate with rules
// Rules for responsiveness and light/dark mode are roots of separate classes, they never appear together
$rule in map.values($rules) {
$key, $value in $rule {
$current-context: map.merge(
$context,
(
rule: $key,
)
);
string.index($value, ' ') {
$value: string.slice($value, 7);
#{$value} {
generate-variants($config, $pseudos, $current-context) using ($props...) {
($props...);
}
}
}
}
}
}
generate-variants($config, $variants: (), $context: ()) {
// Get classes without pseudos, only rules
$base-class-name: map.get($context, base-class-name);
$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')
get-base-class($config, $context) using ($props...) {
($props...);
}
$variant in $variants {
$current-context: map.merge(
$context,
(
variant: $variant,
pseudo: $variant,
)
);
string.index($variant, 'group-') {
$current-context: map.merge(
$current-context,
(
pseudo: string.slice($variant, 7),
scope: '.group',
)
);
}
get-base-class($config, $current-context) using ($props...) {
($props...);
}
}
} {
($config, $variants, $context);
}
}
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);
$base-class-name != null {
// Required to be comma separated since it will be used in generate-class() and treated like a selector
$new-base: '';
$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);
}
generate-class($config, $base, $context...) using ($props...) {
($props...);
}
}
generate-class($config, $base, $rule: null, $variant: null, $pseudo: null, $scope: null, $base-class-name: '') {
$variant-delimiter: map.get(
$map: $config,
$key: variant-delimiter,
);
$variant {
$base: selector.append('#{$variant}#{$variant-delimiter}', $base);
}
$rule {
$base: selector.append('#{$rule}#{$variant-delimiter}', $base);
}
$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
} if $scope {
// Contains parent selector
$base: selector.nest($scope, string-to-class($base));
} if $pseudo {
// Contains pseudo
$base: selector.append(string-to-class($base), ':#{$pseudo}');
} {
// Contains nothing
$base: string-to-class($base);
}
// Generate the class
#{$base} {
($config);
}
}
kv-class-generator($base-class-name, $class-value-pairs, $config: (), $variants: (), $context: ()) {
$base-class-name != null {
$new-base-class-name: '';
string.length($base-class-name) > 0 {
$new-base-class-name: $base-class-name + map.get($config, delimiter);
}
$key, $value in $class-value-pairs {
$context: map.merge(
$context,
(
base-class-name: $new-base-class-name + $key,
)
);
generate-rules-pseudos-selectors($config, $variants, $context) using ($props...) {
($key, $value, $props...);
}
}
} {
generate-rules-pseudos-selectors($config, $variants, $context) using ($props...) {
$key, $value in $class-value-pairs {
.#{$key} {
generate-rules-pseudos-selectors($config, $variants, $context) using ($props...) {
($key, $value, $props...);
}
}
}
}
}
}
inline-class-generator($base-class-name, $config: (), $variants: (), $context: ()) {
$base-class-name == null {
'Base class name must be provided';
}
$context: map.merge(
$context,
(
base-class-name: $base-class-name,
)
);
generate-rules-pseudos-selectors($config, $variants, $context) using ($props...) {
($props...);
}
}
get-responsive() {
$result: ();
$key, $value in config.$breakpoint-pairs {
$result: map.set($result, $key, ' screen and (min-width: #{map.get(config.$breakpoints, $value)})');
}
$result;
}
get-dark() {
('dark': ' (prefers-color-scheme: dark)');
}
get-light() {
('light': ' (prefers-color-scheme: light)');
}
get-reduce-motion() {
('reduce-motion': ' (prefers-reduced-motion: reduce)');
}
get-portrait() {
('portrait': ' (orientation: portrait)');
}
get-landscape() {
('landscape': ' (orientation: landscape)');
}
class-value-map-with-single-property($property, $property-values) {
$result: ();
$key, $value in $property-values {
$result: map.set(
$result,
$key,
(
$property: $value,
)
);
}
$result;
}
/**
* Convert a string to a proper class selector.
* @param: {String} $selector
* @return: {Selector} Returns the class selector.
*/
string-to-class($selector) {
$separator: list.separator($selector);
$result: ();
$s in $selector {
$selector-str: '#{$s}';
$selector-str != '' {
$result: list.append($result, '.#{$selector-str}', $separator);
}
}
$result;
}
class-value-to-property-map($class-value-map, $property) {
$result: ();
$class, $value in $class-value-map {
$result: map.set(
$result,
$class,
(
$property: $value,
)
);
}
$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'
*/
utility-with-body(
$delimiter: config.$delimiter,
$variants: (),
$variant-delimiter: config.$variant-delimiter,
$override: config.$override
) {
$config: (
delimiter: $delimiter,
variant-delimiter: $variant-delimiter,
override: $override,
);
generate-rules-pseudos-selectors(
$config: $config,
$variants: $variants,
$context: (),
)
using ($props...) {
($props...);
}
}
'sass:list';
'sass:map';
'sass:meta';
'sass:selector';
'sass:string';
'config';
'size';
'mixins';