diff --git a/app/assets/scss/_resources.scss b/app/assets/scss/_resources.scss index 0482f321..f75ff78a 100644 --- a/app/assets/scss/_resources.scss +++ b/app/assets/scss/_resources.scss @@ -3,15 +3,16 @@ // Include only SASS variables / mixins / functions. // functions -@forward "./functions/px-to-rem"; -@forward "./functions/px-to-em"; -@forward "./functions/ease"; -@forward "./functions/flex-grid"; +@forward "assets/scss/functions/px-to-rem"; +@forward "assets/scss/functions/px-to-em"; +@forward "assets/scss/functions/ease"; +@forward "assets/scss/functions/flex-grid"; // mixins -@forward './mixins/include-media' show media; -@forward './mixins/typography'; -@forward './mixins/form'; +@forward 'assets/scss/mixins/include-media' show media; +@forward 'assets/scss/mixins/typography'; +@forward 'assets/scss/mixins/form'; +@forward 'assets/scss/mixins/theme'; // components -@forward './components/v-button'; +@forward 'assets/scss/components/v-button'; diff --git a/app/assets/scss/components/_v-button-outlined.scss b/app/assets/scss/components/_v-button-outlined.scss new file mode 100644 index 00000000..e39f437d --- /dev/null +++ b/app/assets/scss/components/_v-button-outlined.scss @@ -0,0 +1,90 @@ +@use 'assets/scss/mixins/css-vars-map'; +@use 'assets/scss/mixins/sizes' as *; +@use 'assets/scss/functions/ease' as *; + +$vars: ( + (vars: (xs: (padding: 8px 12px, + gap: 6px, + ), + sm: (padding: 10px 16px, + gap: 8px, + ), + md: (padding: 16px 20px, + gap: 12px, + ), + lg: (padding: 18px 28px, + gap: 12px, + ), + ), + prefix: '', + ), + (vars: (xs: (font-size: 14px, + ), + sm: (font-size: 16px, + ), + md: (font-size: 18px, + ), + lg: (font-size: 20px, + ), + ), + prefix: 'label', + ), + (vars: (xs: (font-size: 16px, + ), + sm: (font-size: 20px, + ), + md: (font-size: 24px, + ), + lg: (font-size: 32px, + ), + ), + prefix: 'icon', + ), +); +$component: 'v-button'; + +// Outputs CSS property declarations using the CSS variables set by `size()`, with sm values as fallbacks. +// Usage: @include v-button.apply('') or @include v-button.apply('label') +@mixin apply($prefix: '') { + @if $prefix == '' { + border: initial; + background: transparent; + outline: 1px solid var(--v-button-outline-color, var(--button-label-on-light-outline-border, #010101)); + transition: color 0.3s ease(out-quad), background-color 0.3s ease(out-quad); + + &:where([inert], #{&}--disabled, [disabled]) { + color: var(--button-label-on-light-outline-content-disabled, #bdbdbd); + outline-color: var(--button-label-on-light-outline-border-disabled, rgba(1, 1, 1, 0.3)); + } + + @media (hover: hover) { + &:hover { + background-color: var(--button-label-on-light-outline-bg, #010101); + color: var(--button-label-on-light-outline-content-hover, #FFF); + } + } + } + + @include css-vars-map.apply($vars, $component, $prefix, $fallback-size: 'sm'); +} + +// Sets the CSS variables for a given size (xs, sm, md, lg) from $vars. +// Call this on the component element to drive properties() declarations elsewhere. +// eg: @include v-button-solid.size('md') generates: +// --v-button-solid-min-height: 56px; +// --v-button-solid-padding-inline: 20px; +// --v-button-solid-label-font-size: 18px; +// --v-button-solid-icon-font-size: 24px; etc. +@mixin size($size) { + @include css-vars-map.deep-get($vars, $size, $component); +} + +// Generates a BEM modifier class for each size in $vars. +// Call this on the component root to enable size variants via CSS class. +// eg: @include v-button-solid.size-classes() generates: +// &--size-xs { --v-button-solid-min-height: 32px; … } +// &--size-sm { --v-button-solid-min-height: 40px; … } +// &--size-md { --v-button-solid-min-height: 56px; … } etc. +@mixin size-classes() { + @include sizes($vars, $component); +} diff --git a/app/assets/scss/components/_v-button.scss b/app/assets/scss/components/_v-button.scss index 0718921a..a512573e 100644 --- a/app/assets/scss/components/_v-button.scss +++ b/app/assets/scss/components/_v-button.scss @@ -1,4 +1,5 @@ -@use '../mixins/css-vars-map'; +@use 'assets/scss/mixins/css-vars-map'; +@use 'assets/scss/mixins/sizes' as *; // $example-sizes-map: ( // ( @@ -25,22 +26,69 @@ // ), // ); $vars: ( - ( - vars: ( - sm: ( - font-size: 14px, + (vars: (xs: (padding: 8px 12px, + gap: 6px, ), - md: ( - font-size: 16px, + sm: (padding: 10px 16px, + gap: 8px, ), - lg: ( - font-size: 18px, + md: (padding: 16px 20px, + gap: 12px, + ), + lg: (padding: 18px 28px, + gap: 12px, ), ), - prefix: 'label' + prefix: '', + ), + (vars: (xs: (font-size: 14px, + ), + sm: (font-size: 16px, + ), + md: (font-size: 18px, + ), + lg: (font-size: 20px, + ), + ), + prefix: 'label', + ), + (vars: (xs: (font-size: 16px, + ), + sm: (font-size: 20px, + ), + md: (font-size: 24px, + ), + lg: (font-size: 32px, + ), + ), + prefix: 'icon', ), ); +$component: 'v-button'; +// Outputs CSS property declarations using the CSS variables set by `size()`, with sm values as fallbacks. +// Usage: @include v-button.apply('') or @include v-button.apply('label') +@mixin apply($prefix: '') { + @include css-vars-map.apply($vars, $component, $prefix, $fallback-size: 'sm'); +} + +// Sets the CSS variables for a given size (xs, sm, md, lg) from $vars. +// Call this on the component element to drive properties() declarations elsewhere. +// eg: @include v-button-solid.size('md') generates: +// --v-button-solid-min-height: 56px; +// --v-button-solid-padding-inline: 20px; +// --v-button-solid-label-font-size: 18px; +// --v-button-solid-icon-font-size: 24px; etc. @mixin size($size) { - @include css-vars-map.deep-get($vars, $size, 'v-button'); + @include css-vars-map.deep-get($vars, $size, $component); +} + +// Generates a BEM modifier class for each size in $vars. +// Call this on the component root to enable size variants via CSS class. +// eg: @include v-button-solid.size-classes() generates: +// &--size-xs { --v-button-solid-min-height: 32px; … } +// &--size-sm { --v-button-solid-min-height: 40px; … } +// &--size-md { --v-button-solid-min-height: 56px; … } etc. +@mixin size-classes() { + @include sizes($vars, $component); } diff --git a/app/assets/scss/functions/_list.scss b/app/assets/scss/functions/_list.scss index 57099da7..b05b316c 100644 --- a/app/assets/scss/functions/_list.scss +++ b/app/assets/scss/functions/_list.scss @@ -14,8 +14,8 @@ $piece: list.nth($pieces, $i); @if meta.type-of($piece) == list { - $result: string.unquote("#{$result}#{$glue}#{implode($piece, $glue)}"); - } @else { + $result: string.unquote("#{$result}#{$glue}#{list-implode($piece, $glue)}"); + } @else if $piece != null and $piece != '' { $result: string.unquote("#{$result}#{$glue}#{$piece}"); } } diff --git a/app/assets/scss/mixins/_css-vars-map.scss b/app/assets/scss/mixins/_css-vars-map.scss index b83594d3..2e197a6b 100644 --- a/app/assets/scss/mixins/_css-vars-map.scss +++ b/app/assets/scss/mixins/_css-vars-map.scss @@ -1,5 +1,5 @@ @use 'sass:map'; -@use '../functions/list' as *; +@use 'assets/scss/functions/list'; // Get CSS variables from a map // Parameters: @@ -26,7 +26,7 @@ // } @mixin get($map, $name) { @each $key, $value in $map { - --#{list-implode(($name, $key), '-')}: #{$value}; + --#{list.list-implode(($name, $key), '-')}: #{$value}; } } @@ -68,11 +68,44 @@ // --example-margin: 8px; // --example-label-padding: 40px; // } +// Outputs CSS property declarations using the CSS variables set by `deep-get()`, with fallback values. +// Parameters: +// - $map: The map containing nested variables (same structure as deep-get) +// - $name: The base component name used for CSS variable names +// - $prefix: The entry prefix to target ('' for root, 'label' for label entries, etc.) +// - $fallback-size: The size key to use as fallback values +// +// Example usage: +// .example-component { +// @include css-vars-map.apply($vars, 'example-component', 'label', 'sm'); +// } +// +// This will generate: +// .example-component { +// font-size: var(--example-component-label-font-size, 14px); +// padding: var(--example-component-label-padding, 4px 8px); +// } +@mixin apply($map, $name, $prefix: '', $fallback-size: 'sm') { + $var-prefix: list.list-implode(($name, $prefix), '-'); + + @each $entry in $map { + @if map.get($entry, 'prefix') == $prefix { + $entry-vars: map.get($entry, 'vars'); + + @if map.has-key($entry-vars, $fallback-size) { + @each $property, $default-value in map.get($entry-vars, $fallback-size) { + #{$property}: var(--#{$var-prefix}-#{$property}, #{$default-value}); + } + } + } + } +} + @mixin deep-get($map, $key, $name) { @each $value in $map { @if map.has-key($value, 'vars') { $map: map.get($value, 'vars'); - $prefix: list-implode(($name, map.get($value, 'prefix')), '-'); + $prefix: list.list-implode(($name, map.get($value, 'prefix')), '-'); @if map.has-key($map, $key) { $key-vars: map.get($map, $key); diff --git a/app/components/VButton.vue b/app/components/VButton.vue index a403c023..9d8a72db 100644 --- a/app/components/VButton.vue +++ b/app/components/VButton.vue @@ -4,6 +4,9 @@ import type { RouteLocationRaw } from 'vue-router' import { NuxtLink } from '#components' import type { Theme } from '~~/types/theme' +export const vButtonVariant = ['outlined'] as const +export type VButtonVariant = (typeof vButtonVariant)[number] + export const vButtonSizes = ['xs', 'sm', 'md', 'lg'] as const export type VButtonSize = (typeof vButtonSizes)[number] @@ -19,6 +22,7 @@ export const vButtonProps = { disabled: Boolean, size: [String, Boolean] as PropType, theme: [String, Boolean] as PropType, + variant: [String, Boolean] as PropType, } export default defineComponent({ @@ -51,6 +55,7 @@ export default defineComponent({ hasIcon.value && $style['root--has-icon'], props.disabled && $style['root--disabled'], typeof props.size === 'string' && $style[`root--size-${props.size}`], + typeof props.variant === 'string' && $style[`root--variant-${props.variant}`], themeClass.value, ] }) @@ -78,6 +83,7 @@ export default defineComponent({ > @use 'assets/scss/components/v-button'; -@use 'assets/scss/mixins/theme' as *; -@use 'assets/scss/mixins/sizes' as *; +@use 'assets/scss/components/v-button-outlined'; .root { display: var(--v-button-display, inline-flex); align-items: var(--v-button-align-items, center); justify-content: var(--v-button-justify-content, center); - padding: var(--v-button-padding, initial); - border: var(--v-button-border, initial); - background-color: var(--v-button-background-color, initial); font-family: var(--v-button-font-family, inherit); - text-decoration: var(--v-button-text-decoration, none); - user-select: none; - @include theme-variants('button'); - @include sizes(v-button.$vars, 'v-button'); + @include theme-variants('button-'); + + // Default styles + @include v-button.apply; + @include v-button.size-classes; - // PROPS STYLE &--icon-last { flex-direction: row-reverse; } @@ -139,21 +141,37 @@ export default defineComponent({ // remove the user agent style, but without specificity (i.e. :where()) for overriding it easily :where(button#{&}){ text-align: inherit; + user-select: none; } } + + &--variant-outlined { + @include v-button-outlined.apply; + } + + @include v-button-outlined.size-classes; } -// be aware than all nested svg are styled -.root svg, .icon { flex-shrink: var(--v-button-icon-flex-shrink, 0); color: var(--v-button-icon-color, currentColor); + + @include v-button.apply('icon'); + + .root--variant-outlined & { + @include v-button-outlined.apply('icon') + } } .label { overflow: var(--v-button-label-overflow, hidden); - font-size: var(--v-button-label-font-size); text-overflow: var(--v-button-label-text-overflow, ellipsis); white-space: var(--v-button-label-white-space, nowrap); + + @include v-button.apply('label'); + + .root--variant-outlined & { + @include v-button-outlined.apply('label') + } } diff --git a/stories/app/components/VButton/CssSize.stories.vue b/stories/app/components/VButton/CssSize.stories.vue index 1da9e0df..c11867d7 100644 --- a/stories/app/components/VButton/CssSize.stories.vue +++ b/stories/app/components/VButton/CssSize.stories.vue @@ -11,6 +11,11 @@ :class="[$style.button, $style['button--responsive']]" icon-name="check" /> + diff --git a/stories/app/components/VButton/WithSlots.stories.vue b/stories/app/components/VButton/WithSlots.stories.vue index fcb4c429..9858c388 100644 --- a/stories/app/components/VButton/WithSlots.stories.vue +++ b/stories/app/components/VButton/WithSlots.stories.vue @@ -3,10 +3,31 @@