refactor(ui): factor out alignment (like width and color)
This commit is contained in:
parent
c9f59cbd26
commit
8240856630
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { usePastel } from '~/composables/colors'
|
||||
import { usePastel } from '~/composables/color'
|
||||
import { FwCard, FwPlayButton } from '~/components'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { type PastelProps, color } from '~/composables/colors'
|
||||
import { type PastelProps, color } from '~/composables/color'
|
||||
|
||||
type Props = PastelProps
|
||||
|
||||
|
@ -9,7 +9,7 @@ const props = defineProps<Props>()
|
|||
<template>
|
||||
<div
|
||||
class="funkwhale is-colored solid alert"
|
||||
v-bind="color(props)"
|
||||
v-bind="color(props)()"
|
||||
>
|
||||
<slot />
|
||||
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed, useSlots, onMounted } from 'vue'
|
||||
|
||||
import { type ColorProps, type VariantProps, type DefaultProps, type RaisedProps, color } from '~/composables/colors';
|
||||
import { type WidthProps, width } from '~/composables/widths'
|
||||
import { type ColorProps, type VariantProps, type DefaultProps, type RaisedProps, color } from '~/composables/color';
|
||||
import { type WidthProps, width } from '~/composables/width'
|
||||
import { type AlignmentProps, align } from '~/composables/alignment'
|
||||
|
||||
import Loader from '~/components/ui/Loader.vue'
|
||||
|
||||
|
@ -25,7 +26,8 @@ const props = defineProps<{
|
|||
} & (ColorProps | DefaultProps)
|
||||
& VariantProps
|
||||
& RaisedProps
|
||||
& WidthProps>()
|
||||
& WidthProps
|
||||
& AlignmentProps>()
|
||||
|
||||
const slots = useSlots()
|
||||
const isIconOnly = computed(() => !!props.icon && !slots.default)
|
||||
|
@ -37,10 +39,11 @@ const fontWeight = props.thin ? 400 : 900
|
|||
|
||||
const button = ref()
|
||||
|
||||
const attributes = computed(() => ({
|
||||
...color(props, ['interactive']),
|
||||
...width(props, [isIconOnly.value ? 'minContent' : 'buttonWidth'])
|
||||
}))
|
||||
const attributes = computed(() =>
|
||||
color(props, ['interactive'])(
|
||||
width(props, [isIconOnly.value ? 'minContent' : 'buttonWidth'])(
|
||||
align(props, { alignSelf:'start', alignText:'center' })()
|
||||
)))
|
||||
|
||||
const click = async (...args: any[]) => {
|
||||
internalLoader.value = true
|
||||
|
@ -58,21 +61,18 @@ onMounted(() => {
|
|||
|
||||
<template>
|
||||
<button ref="button"
|
||||
:aria-pressed="props.ariaPressed"
|
||||
v-bind="attributes"
|
||||
class="funkwhale button"
|
||||
:class="[
|
||||
'is-text-aligned-' + (alignText ?? 'center'),
|
||||
'is-self-aligned-' + (alignSelf ?? 'start'),
|
||||
{
|
||||
:aria-pressed="props.ariaPressed"
|
||||
:class="{
|
||||
'is-active': isActive,
|
||||
'is-loading': isLoading,
|
||||
'is-icon-only': isIconOnly,
|
||||
'has-icon': !!icon,
|
||||
'is-round': round,
|
||||
'is-shadow': shadow
|
||||
}
|
||||
]" @click="click"
|
||||
}"
|
||||
@click="click"
|
||||
>
|
||||
<i v-if="icon" :class="['bi', icon]" />
|
||||
|
||||
|
@ -87,12 +87,22 @@ onMounted(() => {
|
|||
<style lang="scss">
|
||||
.funkwhale {
|
||||
&.button {
|
||||
|
||||
// Layout
|
||||
|
||||
--padding: 10px;
|
||||
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
justify-content: space-between;
|
||||
|
||||
padding: 9px var(--padding) 11px var(--padding);
|
||||
&.is-icon-only {
|
||||
padding: var(--padding);
|
||||
}
|
||||
|
||||
// Font
|
||||
|
||||
font-family: $font-main;
|
||||
font-weight: v-bind(fontWeight);
|
||||
|
@ -100,60 +110,22 @@ onMounted(() => {
|
|||
|
||||
line-height: 1em;
|
||||
|
||||
// Padding
|
||||
|
||||
padding: 9px var(--padding) 11px var(--padding);
|
||||
&.is-icon-only {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
border-radius: var(--fw-border-radius);
|
||||
// Decoration
|
||||
|
||||
transform: translateX(var(--fw-translate-x)) translateY(var(--fw-translate-y)) scale(var(--fw-scale));
|
||||
transition: all .2s ease;
|
||||
|
||||
&.is-text-aligned-center {
|
||||
justify-content: center
|
||||
}
|
||||
|
||||
&.is-text-aligned-left {
|
||||
justify-content: flex-start
|
||||
}
|
||||
|
||||
&.is-text-aligned-right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
&.is-shadow {
|
||||
box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
align-self: start;
|
||||
|
||||
&.is-self-aligned-start {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
&.is-self-aligned-center {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
&.is-self-aligned-end {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
&.is-self-aligned-baseline {
|
||||
align-self: baseline;
|
||||
}
|
||||
|
||||
&.is-full {
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
border-radius: var(--fw-border-radius);
|
||||
&.is-round {
|
||||
border-radius: 100vh;
|
||||
}
|
||||
|
||||
// States
|
||||
|
||||
&[disabled] {
|
||||
font-weight: normal;
|
||||
pointer-events: none;
|
||||
|
@ -167,6 +139,8 @@ onMounted(() => {
|
|||
}
|
||||
}
|
||||
|
||||
// Icon
|
||||
|
||||
i.bi {
|
||||
font-size: 18px;
|
||||
margin: -2px 0;
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
import { computed } from 'vue'
|
||||
|
||||
import { type RouterLinkProps, RouterLink } from 'vue-router';
|
||||
import { type ColorProps, type DefaultProps, type PastelProps, type RaisedProps, type VariantProps, color } from '~/composables/colors'
|
||||
import { type WidthProps, width } from '~/composables/widths'
|
||||
import { type ColorProps, type DefaultProps, type PastelProps, type RaisedProps, type VariantProps, color } from '~/composables/color'
|
||||
import { type WidthProps, width } from '~/composables/width'
|
||||
|
||||
import Pill from './Pill.vue'
|
||||
import Alert from './Alert.vue'
|
||||
|
@ -30,10 +30,10 @@ const isExternalLink = computed(() => {
|
|||
return typeof props.to === 'string' && props.to.startsWith('http')
|
||||
})
|
||||
|
||||
const attributes = computed(() => ({
|
||||
...color(props, props.to ? ['interactive', 'solid'] : []),
|
||||
...width(props, ['medium'])
|
||||
}))
|
||||
const attributes = computed(() =>
|
||||
color(props, props.to ? ['interactive', 'solid'] : [])(
|
||||
width(props, ['medium'])()
|
||||
))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -103,7 +103,7 @@ const attributes = computed(() => ({
|
|||
|
||||
<style module>
|
||||
.card {
|
||||
--fw-card-padding: v-bind("props.small ? '16px' : '24px'");
|
||||
--fw-card-padding: v-bind("'small' in props ? '16px' : '24px'");
|
||||
|
||||
position: relative;
|
||||
|
||||
|
|
|
@ -1,84 +1,97 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { type ColorProps, type DefaultProps, type PastelProps, type RaisedProps, type VariantProps, color } from '~/composables/color'
|
||||
import { type WidthProps, width } from '~/composables/width'
|
||||
|
||||
const props = defineProps<{
|
||||
columnWidth?: number,
|
||||
noGap?:true,
|
||||
noRule?:true,
|
||||
noWrap?:true
|
||||
columnWidth?: string,
|
||||
noGap?: true,
|
||||
noRule?: true,
|
||||
noWrap?: true
|
||||
}
|
||||
& { [P in "stack" | "grid" | "flex" | "columns" | "row" | "page"]?: true | string }
|
||||
& { [C in "nav" | "aside" | "header" | "footer" | "main" | "label" | "form" | "h1" | "h2" | "h3" | "h4" | "h5"]?:true }>()
|
||||
& { [C in "nav" | "aside" | "header" | "footer" | "main" | "label" | "form" | "h1" | "h2" | "h3" | "h4" | "h5"]?: true }
|
||||
& (PastelProps | ColorProps | DefaultProps)
|
||||
& RaisedProps
|
||||
& VariantProps
|
||||
& WidthProps>()
|
||||
|
||||
const columnWidth = props.columnWidth ?? 46
|
||||
const columnWidth = props.columnWidth ?? '46px'
|
||||
|
||||
const attributes = computed(() => ({
|
||||
...color(props)(width(props)()),
|
||||
layout:
|
||||
props.grid === true ? 'grid' :
|
||||
props.grid ? 'grid-custom' :
|
||||
props.flex ? 'flex' :
|
||||
props.columns ? 'columns' :
|
||||
'stack'
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="props.nav ? 'nav' : props.aside ? 'aside' : props.header ? 'header' : props.footer ? 'footer' : props.main ? 'main' : props.label ? 'label' : props.form ? 'form' : props.h1? 'h1' : props.h2? 'h2' : props.h3? 'h3' : props.h4? 'h4' : props.h5? 'h5' : 'div'"
|
||||
:class="[
|
||||
$style.layout,
|
||||
noGap || $style.gap,
|
||||
noWrap || $style.wrap,
|
||||
props.grid ? $style[props.grid===true ? 'grid' : 'grid-custom']
|
||||
: props.flex ? $style.flex
|
||||
: props.columns? $style.columns
|
||||
: $style.stack
|
||||
]">
|
||||
<slot />
|
||||
</component>
|
||||
<component
|
||||
:is="props.nav ? 'nav' : props.aside ? 'aside' : props.header ? 'header' : props.footer ? 'footer' : props.main ? 'main' : props.label ? 'label' : props.form ? 'form' : props.h1 ? 'h1' : props.h2 ? 'h2' : props.h3 ? 'h3' : props.h4 ? 'h4' : props.h5 ? 'h5' : 'div'"
|
||||
:class="[
|
||||
$style.layout,
|
||||
noGap || $style.gap,
|
||||
noWrap || $style.wrap,
|
||||
]" v-bind="attributes">
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<style module>
|
||||
.layout{
|
||||
transition:all .15s;
|
||||
.layout {
|
||||
transition: all .15s;
|
||||
|
||||
/* Override --gap with your preferred value */
|
||||
|
||||
&.gap {
|
||||
gap: var(--gap, 32px);
|
||||
}
|
||||
gap: var(--gap, 32px);
|
||||
&:not(.gap) {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
/* Growth */
|
||||
|
||||
&:has(:global(>.grow)){
|
||||
>:not(:global(.grow)){
|
||||
flex-grow:0;
|
||||
&:has(:global(>.grow)) {
|
||||
>:not(:global(.grow)) {
|
||||
flex-grow: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Layout strategy */
|
||||
|
||||
&.columns {
|
||||
&[layout=columns] {
|
||||
column-count: auto;
|
||||
column-width: calc(v-bind(columnWidth) * 1px);
|
||||
column-width: v-bind(columnWidth);
|
||||
display: block;
|
||||
column-rule: 1px solid v-bind("noRule ? 'transparent' : 'var(--border-color)'");
|
||||
}
|
||||
|
||||
&.grid {
|
||||
&[layout=grid] {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
repeat(auto-fit, calc(v-bind(columnWidth) * 1px));
|
||||
repeat(auto-fit, v-bind(columnWidth));
|
||||
grid-auto-flow: row dense;
|
||||
place-content: center; /* If the grid has a fixed size smaller than its container, center it */
|
||||
place-content: center;
|
||||
/* If the grid has a fixed size smaller than its container, center it */
|
||||
}
|
||||
|
||||
&.grid-custom {
|
||||
&[layout=grid-custom] {
|
||||
display: grid;
|
||||
grid: v-bind("props.grid");
|
||||
grid: v-bind(grid);
|
||||
grid-auto-flow: row dense;
|
||||
place-content: center; /* If the grid has a fixed size smaller than its container, center it */
|
||||
place-content: center;
|
||||
/* If the grid has a fixed size smaller than its container, center it */
|
||||
}
|
||||
|
||||
&.stack {
|
||||
&[layout=stack] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&.flex {
|
||||
&[layout=flex] {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: v-bind('props.noWrap ? "nowrap" : "wrap"');
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { type RouterLinkProps, RouterLink } from 'vue-router'
|
||||
import { type ColorProps, type DefaultProps, type VariantProps, color, isNoColors } from '~/composables/colors';
|
||||
import { type WidthProps, width } from '~/composables/widths'
|
||||
import { type RouterLinkProps } from 'vue-router'
|
||||
import { type ColorProps, type DefaultProps, type VariantProps, color, isNoColors } from '~/composables/color';
|
||||
import { type WidthProps, width } from '~/composables/width'
|
||||
import { type AlignmentProps, align } from '~/composables/alignment'
|
||||
|
||||
const props = defineProps<{
|
||||
alignText?: 'left' | 'center' | 'right' | 'stretch'
|
||||
alignSelf?: 'start' | 'center' | 'end'
|
||||
thickWhenActive?: true
|
||||
|
||||
thin?: true
|
||||
|
@ -17,41 +16,37 @@ const props = defineProps<{
|
|||
} & RouterLinkProps
|
||||
& (ColorProps | DefaultProps)
|
||||
& VariantProps
|
||||
& WidthProps>()
|
||||
& WidthProps
|
||||
& AlignmentProps>()
|
||||
|
||||
const isExternalLink = computed(() => {
|
||||
return typeof props.to === 'string' && props.to.startsWith('http')
|
||||
})
|
||||
const isExternalLink = computed(() =>
|
||||
typeof props.to === 'string' && props.to.startsWith('http')
|
||||
)
|
||||
|
||||
const [fontWeight, activeFontWeight] = props.thickWhenActive ? [600, 900] : [400, 400]
|
||||
|
||||
const isIconOnly = computed(() => !!props.icon)
|
||||
const isSimple = isNoColors(props)
|
||||
|
||||
const attributes = computed(() => ({
|
||||
...color(props, ['interactive']),
|
||||
...width(props, ['auto'])
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="isExternalLink ? 'a' : 'RouterLink'"
|
||||
v-bind="attributes"
|
||||
v-bind="color(props, ['interactive'])(
|
||||
width(props, ['auto'])(
|
||||
align(props, { alignText:'center' })
|
||||
()))"
|
||||
:class="[
|
||||
$style.link,
|
||||
$style['is-' + width],
|
||||
$style['is-text-aligned-' + (alignText ?? 'left')],
|
||||
$style['is-self-aligned-' + (alignSelf ?? 'auto')],
|
||||
round && $style['is-round'],
|
||||
isIconOnly && $style['is-icon-only'],
|
||||
isSimple && $style['force-underline'],
|
||||
isSimple && $style['no-spacing'],
|
||||
isNoColors(props) && $style['force-underline'],
|
||||
isNoColors(props) && $style['no-spacing'],
|
||||
]"
|
||||
:href="isExternalLink ? to.toString() : undefined"
|
||||
:to="isExternalLink ? undefined : to"
|
||||
:target="isExternalLink ? '_blank' : undefined"
|
||||
>
|
||||
<i v-if="icon" :class="['bi', icon]" />
|
||||
|
||||
<span>
|
||||
<slot />
|
||||
</span>
|
||||
|
@ -60,47 +55,16 @@ const attributes = computed(() => ({
|
|||
|
||||
<style module lang="scss">
|
||||
.link {
|
||||
|
||||
// Layout
|
||||
|
||||
--padding: 10px;
|
||||
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
white-space: nowrap;
|
||||
justify-content: space-between;
|
||||
|
||||
font-family: $font-main;
|
||||
font-weight: v-bind(fontWeight);
|
||||
|
||||
&:global(.router-link-exact-active) {
|
||||
font-weight: v-bind(activeFontWeight);
|
||||
}
|
||||
|
||||
font-size: 0.875em;
|
||||
|
||||
line-height: 1em;
|
||||
|
||||
transform: translateX(var(--fw-translate-x)) translateY(var(--fw-translate-y)) scale(var(--fw-scale));
|
||||
transition:background-color .2s, border-color .3s;
|
||||
|
||||
// Icon
|
||||
|
||||
i:global(.bi) {
|
||||
font-size: 1.2rem;
|
||||
&.large {
|
||||
font-size:2rem;
|
||||
}
|
||||
}
|
||||
|
||||
i:global(.bi) + span:not(:empty) {
|
||||
margin-left: 1ch;
|
||||
}
|
||||
|
||||
&:not(.force-underline) {
|
||||
text-decoration: none;
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
/* Shape */
|
||||
|
||||
border-radius: var(--fw-border-radius);
|
||||
margin: 0 0.5ch;
|
||||
|
||||
padding: 9px 10px 11px 10px;
|
||||
&.is-icon-only {
|
||||
padding: 10px;
|
||||
|
@ -111,54 +75,49 @@ const attributes = computed(() => ({
|
|||
font-size: 1em;
|
||||
}
|
||||
|
||||
// Font
|
||||
|
||||
font-family: $font-main;
|
||||
font-weight: v-bind(fontWeight);
|
||||
font-size: 14px;
|
||||
|
||||
line-height: 1em;
|
||||
|
||||
// Decoration
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
transform: translateX(var(--fw-translate-x)) translateY(var(--fw-translate-y)) scale(var(--fw-scale));
|
||||
transition:background-color .2s, border-color .3s;
|
||||
|
||||
&:not(.force-underline) {
|
||||
text-decoration: none;
|
||||
// background-color: transparent;
|
||||
// border-color: transparent;
|
||||
}
|
||||
|
||||
border-radius: var(--fw-border-radius);
|
||||
|
||||
&.is-round {
|
||||
border-radius: 100vh;
|
||||
}
|
||||
|
||||
/* Alignment */
|
||||
// States
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
|
||||
&.is-text-aligned-center {
|
||||
justify-content: center
|
||||
&:global(.router-link-exact-active) {
|
||||
font-weight: v-bind(activeFontWeight);
|
||||
}
|
||||
|
||||
&.is-text-aligned-left {
|
||||
justify-content: flex-start
|
||||
}
|
||||
// Icon
|
||||
|
||||
&.is-text-aligned-right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
&.is-text-aligned-stretch {
|
||||
justify-content: stretch;
|
||||
align-content: stretch;
|
||||
> span { width:100%; }
|
||||
}
|
||||
|
||||
&.is-self-aligned-start {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
&.is-self-aligned-center {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
&.is-self-aligned-auto {
|
||||
align-self: auto;
|
||||
}
|
||||
|
||||
&.is-self-aligned-end {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
/* Width */
|
||||
|
||||
&.is-full {
|
||||
align-self: stretch;
|
||||
> i:global(.bi) {
|
||||
font-size: 1.2rem;
|
||||
&.large {
|
||||
font-size:2rem;
|
||||
}
|
||||
&+span:not(:empty) {
|
||||
margin-left: 1ch;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { type ColorProps, type DefaultProps, color } from '~/composables/colors';
|
||||
import { type ColorProps, type DefaultProps, color } from '~/composables/color';
|
||||
import { watchEffect } from 'vue';
|
||||
import onKeyboardShortcut from '~/composables/onKeyboardShortcut';
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { type ColorProps, type PastelProps, color } from '~/composables/colors'
|
||||
import { type ColorProps, type PastelProps, color } from '~/composables/color'
|
||||
|
||||
const emit = defineEmits<{
|
||||
click: [event: MouseEvent]
|
||||
|
@ -13,7 +13,7 @@ const props = defineProps<PastelProps | ColorProps>()
|
|||
<template>
|
||||
<button
|
||||
class="funkwhale pill outline"
|
||||
v-bind="color(props, ['interactive'])"
|
||||
v-bind="color(props, ['interactive'])()"
|
||||
@click.stop="handleClick"
|
||||
>
|
||||
<div v-if="!!$slots.image" class="pill-image">
|
||||
|
|
|
@ -4,7 +4,7 @@ import { whenever, useElementBounding, onClickOutside } from '@vueuse/core'
|
|||
|
||||
import { isMobileView, useScreenSize } from '~/composables/screen'
|
||||
import { POPOVER_INJECTION_KEY, POPOVER_CONTEXT_INJECTION_KEY } from '~/injection-keys'
|
||||
import { type ColorProps, type DefaultProps, type RaisedProps, color } from '~/composables/colors';
|
||||
import { type ColorProps, type DefaultProps, type RaisedProps, color } from '~/composables/color';
|
||||
|
||||
/* TODO: Basic accessibility
|
||||
|
||||
|
@ -144,7 +144,7 @@ watch(open, (isOpen) => {
|
|||
:style="position"
|
||||
:class="{ 'is-mobile': isMobile }"
|
||||
class="funkwhale popover"
|
||||
v-bind="color(colorProps)"
|
||||
v-bind="color(colorProps)()"
|
||||
style="display:flex; flex-direction:column;"
|
||||
>
|
||||
<slot name="items" />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { color } from "~/composables/colors.ts";
|
||||
import { color } from "~/composables/color";
|
||||
|
||||
const { big } = defineProps<{
|
||||
big?: boolean
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
import type { Entries, Entry, KeysOfUnion } from "type-fest"
|
||||
import type { HTMLAttributes } from "vue";
|
||||
|
||||
export type AlignmentProps = {
|
||||
alignText?: 'left' | 'center' | 'right' | 'stretch' | 'space-out',
|
||||
alignSelf?: 'start' | 'center' | 'end' | 'auto' | 'baseline' | 'stretch'
|
||||
} & {
|
||||
[T in 'center' | 'stretch']?: true
|
||||
}
|
||||
export type Key = KeysOfUnion<AlignmentProps>
|
||||
|
||||
const styles = {
|
||||
center: 'place-content: center center; place-self: center center;',
|
||||
stretch: 'place-content: stretch stretch; place-self: stretch stretch;',
|
||||
alignText: (a:AlignmentProps['alignText'])=>({
|
||||
left: 'justify-content: flex-start;',
|
||||
center: 'justify-content: center;',
|
||||
baseline: 'align-items: baseline;',
|
||||
right: 'justify-content: flex-end;',
|
||||
stretch: 'place-content: stretch;',
|
||||
'space-out': 'place-content: space-between;'
|
||||
}[a!]),
|
||||
alignSelf: (a:AlignmentProps['alignSelf'])=> ({
|
||||
start: 'align-self: flex-start;',
|
||||
center: 'align-self: center;',
|
||||
end: 'align-self: flex-end;',
|
||||
auto: 'align-self: auto;',
|
||||
baseline: 'align-self: baseline;',
|
||||
stretch: 'align-self: stretch;'
|
||||
}[a!])
|
||||
} as const;
|
||||
|
||||
const getStyle = (props : Partial<AlignmentProps>) => ([key, value]: Entry<AlignmentProps>) =>
|
||||
(trace(`getStyle: key=${key}, value=${value}`),
|
||||
typeof styles[key] === 'function' ?
|
||||
(trace(`getStyle: key=${key}, value=${value}`), styles[key](
|
||||
// We know that props[key] is a value accepted by styles[key]. The ts compiler is not so smart.
|
||||
// @ts-ignore
|
||||
(key in props && props[key]) ?
|
||||
props[(trace(props[key]), trace(key))]
|
||||
: value
|
||||
))
|
||||
: styles[key]
|
||||
)
|
||||
|
||||
const merge = (rules: string[]) => (attributes: HTMLAttributes = {}) =>
|
||||
rules.length === 0 ? attributes : ({
|
||||
...attributes,
|
||||
style: rules.join(' ')+('style' in attributes ? attributes.style + ' ' : '')
|
||||
})
|
||||
|
||||
// All keys are exclusive
|
||||
const conflicts: Set<Key>[] = [
|
||||
new Set(['center', 'stretch']),
|
||||
new Set(['alignText']),
|
||||
new Set(['alignSelf']),
|
||||
]
|
||||
|
||||
/**
|
||||
* Add alignment styles to your component.
|
||||
* Alignments are designed to work both in a grid and flex contexts but may fail in normal context.
|
||||
*
|
||||
* (1) Add `& AlignmentProps` to your `Props` type
|
||||
* (2) Call `v-bind="alignment(props)"` on your component template
|
||||
* (3) Now your component accepts width props such as `align-text="center"`.
|
||||
*
|
||||
* @param props Your component's props (or ...rest props if you have destructured them already)
|
||||
* @param defaults These props are applied immediately and can be overridden by the user
|
||||
* @param attributes Optional: To compose width, color, alignment, etc.
|
||||
* @returns the corresponding `{ style }` object
|
||||
*/
|
||||
export const align = <TProps extends Partial<AlignmentProps>>(
|
||||
props: TProps,
|
||||
defaults: Partial<AlignmentProps> = {}
|
||||
) => merge (
|
||||
trace((Object.entries(props) as Entries<TProps>).reduce(
|
||||
((acc, [key, value]) =>
|
||||
value && key in styles ?
|
||||
acc.filter(([accKey, _]) => !conflicts.find(set => set.has(accKey) && set.has(key)))
|
||||
.concat([[key, value]])
|
||||
: acc
|
||||
),
|
||||
trace(Object.entries(defaults)) as Entries<Partial<AlignmentProps>>
|
||||
)).map(getStyle(props))
|
||||
)
|
||||
|
||||
const trace = <T extends unknown>(a:T):T => {
|
||||
console.log(a);
|
||||
return a;
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import type { KeysOfUnion } from "type-fest"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
|
||||
export type DefaultProps =
|
||||
| { default?: true }
|
||||
|
@ -68,7 +69,13 @@ const classes = {
|
|||
* @returns the number of actually applied classes (if there are no defaults)
|
||||
*/
|
||||
export const isNoColors = (props: Partial<Props>) =>
|
||||
color(props).class===''
|
||||
!color(props)().class
|
||||
|
||||
const merge = (classes: string[]) => (attributes: HTMLAttributes = {}) =>
|
||||
classes.length === 0 ? attributes : ({
|
||||
...attributes,
|
||||
class: classes.join(' ') + ('class' in attributes ? attributes.class + ' ' : '')
|
||||
})
|
||||
|
||||
/**
|
||||
* Add color classes to your component.
|
||||
|
@ -79,10 +86,12 @@ export const isNoColors = (props: Partial<Props>) =>
|
|||
* (3) Now your component accepts color props such as `secondary outline raised`.
|
||||
*
|
||||
* @param props Your component's props (or ...rest props if you have destructured them already)
|
||||
* @param defaults These props are applied immediately and can be overridden by the user
|
||||
* @param attributes Optional: To compose width, color, alignment, etc.
|
||||
* @returns the corresponding `class` object
|
||||
*/
|
||||
export const color = (props: Partial<Props>, defaults?: Key[]) => ({
|
||||
class:
|
||||
export const color = (props: Partial<Props>, defaults?: Key[]) =>
|
||||
merge(
|
||||
Object.entries(props).reduce(
|
||||
((acc, [key, value]) =>
|
||||
value && key in classes ?
|
||||
|
@ -91,8 +100,8 @@ export const color = (props: Partial<Props>, defaults?: Key[]) => ({
|
|||
: acc
|
||||
),
|
||||
defaults || []
|
||||
).join(' ')
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
type ColorSelector =
|
||||
`${Color | Pastel | Default}${'' | ` ${Variant}${'' | ' interactive'}${'' | ' raised'}`}`
|
|
@ -1,4 +1,5 @@
|
|||
import type { Entries, KeysOfUnion } from "type-fest"
|
||||
import type { HTMLAttributes } from "vue";
|
||||
|
||||
export type WidthProps =
|
||||
| { minContent?: true }
|
||||
|
@ -14,16 +15,17 @@ export type Key = KeysOfUnion<WidthProps>
|
|||
const styles = {
|
||||
minContent: 'width: min-content;',
|
||||
tiny: "width: 124px; grid-column: span 2;",
|
||||
buttonWidth: "width: 136px; grid-column: span 2; flex-grow: 0; align-self: start;",
|
||||
buttonWidth: "width: max(136px, min-content); grid-column: span 2; flex-grow: 0; align-self: start;",
|
||||
small: "width: 202px; grid-column: span 3;",
|
||||
medium: "width: 280px; grid-column: span 4;",
|
||||
auto: "width: auto;",
|
||||
full: "width: auto; grid-column: 1 / -1; align-self: auto; flex-grow:1;",
|
||||
full: "width: auto; grid-column: 1 / -1; place-self: stretch; flex-grow: 1;",
|
||||
width: (w:string)=>`width: ${w}; flex-grow:0;`,
|
||||
} as const satisfies Record<Key, string|((w:string)=>string)>;
|
||||
|
||||
const getStyle = (props : Partial<WidthProps>) => (key : Key) =>
|
||||
(typeof styles[key] !== 'string' && key in props) ?
|
||||
const getStyle = (props : Partial<WidthProps>) => (key : Key):string =>
|
||||
// @ts-ignore
|
||||
(typeof styles[key] === 'function' && key in props) ?
|
||||
styles[key](
|
||||
// TODO: Make the typescript compiler understand `key in props`
|
||||
// @ts-ignore
|
||||
|
@ -31,13 +33,17 @@ const getStyle = (props : Partial<WidthProps>) => (key : Key) =>
|
|||
)
|
||||
: styles[key]
|
||||
|
||||
|
||||
|
||||
// All keys are exclusive
|
||||
const conflicts: Set<Key>[] = [
|
||||
new Set(Object.keys(styles) as Key[])
|
||||
]
|
||||
|
||||
const merge = (rules: string[]) => (attributes: HTMLAttributes = {}) =>
|
||||
rules.length === 0 ? attributes : ({
|
||||
...attributes,
|
||||
style: rules.join(' ') + ('style' in attributes ? attributes.style + ' ' : '')
|
||||
})
|
||||
|
||||
/**
|
||||
* Add a width style to your component.
|
||||
* Widths are designed to work both in a page-grid context and in a flex or normal context.
|
||||
|
@ -47,21 +53,21 @@ const conflicts: Set<Key>[] = [
|
|||
* (3) Now your component accepts width props such as `small`, `medium`, `stretch`.
|
||||
*
|
||||
* @param props Your component's props (or ...rest props if you have destructured them already)
|
||||
* @param defaults These props are applied immediately and can be overridden by the user
|
||||
* @param attributes Optional: To compose width, color, alignment, etc.
|
||||
* @returns the corresponding `{ style }` object
|
||||
*/
|
||||
export const width = <TProps extends Partial<WidthProps>>(
|
||||
props: TProps,
|
||||
defaults: Key[] = []
|
||||
) => ({
|
||||
style:
|
||||
(Object.entries(props) as Entries<TProps>).reduce(
|
||||
((acc, [key, value]) =>
|
||||
value && key in styles ?
|
||||
acc.filter(accKey => !conflicts.find(set => set.has(accKey) && set.has(key)))
|
||||
.concat([key])
|
||||
: acc
|
||||
),
|
||||
defaults
|
||||
).map(getStyle(props))
|
||||
.join(' ')
|
||||
})
|
||||
) => merge (
|
||||
(Object.entries(props) as Entries<TProps>).reduce(
|
||||
((acc, [key, value]) =>
|
||||
value && key in styles ?
|
||||
acc.filter(accKey => !conflicts.find(set => set.has(accKey) && set.has(key)))
|
||||
.concat([key])
|
||||
: acc
|
||||
),
|
||||
defaults
|
||||
).map(getStyle(props))
|
||||
)
|
|
@ -1,6 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted, nextTick } from 'vue'
|
||||
import { color } from '~/composables/colors.ts';
|
||||
import { color } from '~/composables/color';
|
||||
|
||||
import Sidebar from '~/ui/components/Sidebar.vue'
|
||||
import ShortcutsModal from './modals/Shortcuts.vue'
|
||||
|
|
|
@ -3,7 +3,7 @@ import { ref, onMounted, watch, computed } from 'vue'
|
|||
import { useUploadsStore } from '../stores/upload'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useStore } from '~/store'
|
||||
import { color } from '~/composables/colors'
|
||||
import { color } from '~/composables/color'
|
||||
|
||||
import Logo from '~/components/Logo.vue'
|
||||
import Input from '~/components/ui/Input.vue'
|
||||
|
|
|
@ -7,12 +7,10 @@ const click = ():Promise<void> => new Promise(resolve => setTimeout(resolve, 100
|
|||
|
||||
# Button
|
||||
|
||||
Buttons are UI elements that users can interact with to perform actions. Funkwhale uses buttons in many contexts.
|
||||
Buttons are UI elements that users can interact with to perform actions and manipulate objects. They are distinct from [Links](link) and will not change the user's position.
|
||||
|
||||
```ts
|
||||
{
|
||||
alignText?: 'left' | 'center' | 'right'
|
||||
alignSelf?: 'start' | 'center' | 'end'
|
||||
thin?: true
|
||||
|
||||
isActive?: boolean
|
||||
|
@ -30,6 +28,7 @@ Buttons are UI elements that users can interact with to perform actions. Funkwha
|
|||
& VariantProps
|
||||
& RaisedProps
|
||||
& WidthProps
|
||||
& AlignmentProps
|
||||
```
|
||||
|
||||
## Button colors
|
||||
|
@ -240,26 +239,30 @@ Round buttons have fully rounded edges.
|
|||
|
||||
You can pass a state to indicate whether a user can interact with a button.
|
||||
|
||||
### Active
|
||||
### On/Off
|
||||
|
||||
You can force an active state by passing an `aria-pressed` prop.
|
||||
|
||||
This can be useful for toggle buttons (if you don't want to use a [Toggle component](toggle))
|
||||
|
||||
```vue-html
|
||||
<Button>
|
||||
Off
|
||||
</Button>
|
||||
|
||||
<Button aria-pressed>
|
||||
Active button
|
||||
On
|
||||
</Button>
|
||||
```
|
||||
|
||||
**Default:**
|
||||
|
||||
<Button>
|
||||
Inactive button
|
||||
Off
|
||||
</Button>
|
||||
|
||||
<Button aria-pressed>
|
||||
Active button
|
||||
On
|
||||
</Button>
|
||||
|
||||
---
|
||||
|
@ -267,11 +270,11 @@ This can be useful for toggle buttons (if you don't want to use a [Toggle compo
|
|||
**Secondary:**
|
||||
|
||||
<Button secondary>
|
||||
Inactive button
|
||||
Off
|
||||
</Button>
|
||||
|
||||
<Button secondary aria-pressed>
|
||||
Active button
|
||||
On
|
||||
</Button>
|
||||
|
||||
---
|
||||
|
@ -279,17 +282,25 @@ This can be useful for toggle buttons (if you don't want to use a [Toggle compo
|
|||
**Primary:**
|
||||
|
||||
<Button primary>
|
||||
Inactive button
|
||||
Off
|
||||
</Button>
|
||||
|
||||
<Button primary aria-pressed>
|
||||
Active button
|
||||
On
|
||||
</Button>
|
||||
|
||||
### Disabled
|
||||
|
||||
Disabled buttons are non-interactive and inherit a less bold color than the one provided. You can apply a disabled state by passing a `disabled` prop.
|
||||
|
||||
::: tip When should I use `disabled`?
|
||||
|
||||
Use the `disabled` property for buttons that the user expects at a certain position, for example in a toolbar or in a row of action buttons.
|
||||
|
||||
If there is just one button in a form and its action is disabled, you may instead just remove it.
|
||||
|
||||
:::
|
||||
|
||||
```vue-html
|
||||
<Button disabled>
|
||||
Disabled button
|
||||
|
@ -314,10 +325,6 @@ If a user can't interact with a button until something has finished loading, you
|
|||
Loading button
|
||||
</Button>
|
||||
|
||||
<Button primary is-loading>
|
||||
Loading button
|
||||
</Button>
|
||||
|
||||
### Promise handling in `@click`
|
||||
|
||||
When a function passed to `@click` returns a promise, the button automatically toggles a loading state on click. When the promise resolves or is rejected, the loading state turns off.
|
||||
|
@ -352,7 +359,7 @@ You can override the promise state by passing a false `is-loading` prop.
|
|||
Click me
|
||||
</Button>
|
||||
|
||||
## Icons
|
||||
## Add an icon
|
||||
|
||||
You can use [Bootstrap Icons](https://icons.getbootstrap.com/) in your button component
|
||||
|
||||
|
@ -378,7 +385,9 @@ Icon buttons shrink down to the icon size if you don't pass any content. If you
|
|||
</Button>
|
||||
</Layout>
|
||||
|
||||
## Width and alignment
|
||||
## Set width and alignment
|
||||
|
||||
See [Using width](/using-width) and [Using alignment](/using-alignment)
|
||||
|
||||
<Layout flex>
|
||||
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
<script setup>
|
||||
import Card from "~/components/ui/Card.vue"
|
||||
import Layout from "~/components/ui/Layout.vue"
|
||||
import Button from "~/components/ui/Button.vue"
|
||||
</script>
|
||||
|
||||
# Using alignments
|
||||
|
||||
You can align items inside `flex` and `grid` layouts.
|
||||
|
||||
## Align a component relative to its container with `align-self`
|
||||
|
||||
<Layout grid class="preview">
|
||||
|
||||
<template
|
||||
v-for="alignment in ['start', 'center', 'end', 'auto', 'baseline', 'stretch']"
|
||||
:key="alignment"
|
||||
|
||||
>
|
||||
|
||||
<div style="position:relative;place-self:stretch; grid-area: span 2 / span 2; min-height: 72px; border:.5px solid red; display:grid; grid: 1fr / 1fr; grid-auto-flow: column;"
|
||||
>
|
||||
<Button auto primary :align-self="alignment">🐌</Button>
|
||||
_
|
||||
<span style="position:absolute; right: 0; margin-left:-20px; bottom:0;">align-self={{ alignment }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template
|
||||
v-for="alignment in ['center', 'stretch']"
|
||||
:key="alignment"
|
||||
|
||||
>
|
||||
|
||||
<div style="position:relative;place-self:stretch; grid-area: span 2 / span 2; min-height: 72px; border:.5px solid red; display:grid; grid: 1fr / 1fr; grid-auto-flow: column;"
|
||||
>
|
||||
<Button auto primary v-bind="{[alignment]:true}">🐌</Button>
|
||||
_
|
||||
<span style="position:absolute; right: 0; bottom:0;">{{ alignment }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</Layout>
|
||||
|
||||
## Align the content of a component with `align-text`
|
||||
|
||||
<Layout flex class="preview">
|
||||
|
||||
```vue-html
|
||||
<Button align-text="left">🐌</Button>
|
||||
<Button align-text="center">🐌</Button>
|
||||
<Button align-text="right">🐌</Button>
|
||||
<Button icon="bi-star" align-text="stretch">🐌</Button>
|
||||
<Button icon="bi-star" align-text="space-out">🐌</Button>
|
||||
```
|
||||
|
||||
<Layout class="preview solid primary" stack no-gap>
|
||||
<Button align-text="left">🐌</Button>
|
||||
<Button align-text="center">🐌</Button>
|
||||
<Button align-text="right">🐌</Button>
|
||||
<Button icon="bi-star" align-text="stretch">🐌</Button>
|
||||
<Button icon="bi-star" align-text="space-out">🐌</Button>
|
||||
</Layout>
|
||||
</Layout>
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { color, setColors } from "~/composables/colors.ts"
|
||||
import { color, setColors } from "~/composables/color"
|
||||
import { useRoute } from "vue-router"
|
||||
|
||||
import Button from "~/components/ui/Button.vue"
|
||||
|
@ -59,12 +59,12 @@ import { color } from "~/composables/colors.ts";
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div v-bind="color({}, ['primary', 'solid', 'interactive', 'raised'])" />
|
||||
<div v-bind="color({}, ['primary', 'solid', 'interactive', 'raised'])()" />
|
||||
</template>
|
||||
```
|
||||
|
||||
<div class="preview">
|
||||
<div :class="$style.swatch" v-bind="color({}, ['primary', 'solid', 'interactive', 'raised'])" />
|
||||
<div :class="$style.swatch" v-bind="color({}, ['primary', 'solid', 'interactive', 'raised'])()" />
|
||||
</div>
|
||||
|
||||
</Layout>
|
||||
|
@ -86,7 +86,7 @@ const props = defineProps<{
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div v-bind="color(props, ['primary', 'solid'])" />
|
||||
<div v-bind="color(props, ['primary', 'solid'])()" />
|
||||
</template>
|
||||
```
|
||||
|
||||
|
|
|
@ -1,16 +1,6 @@
|
|||
<script setup>
|
||||
import { color, setColors } from "~/composables/colors.ts"
|
||||
import { useRoute } from "vue-router"
|
||||
|
||||
import Button from "~/components/ui/Button.vue"
|
||||
import Card from "~/components/ui/Card.vue"
|
||||
import Link from "~/components/ui/Link.vue"
|
||||
import Layout from "~/components/ui/Layout.vue"
|
||||
import Alert from "~/components/ui/Alert.vue"
|
||||
import Spacer from "~/components/ui/layout/Spacer.vue"
|
||||
|
||||
const route = useRoute();
|
||||
const here = route.path
|
||||
</script>
|
||||
|
||||
# Using widths
|
Loading…
Reference in New Issue