refactor(ui): factor out alignment (like width and color)

This commit is contained in:
upsiflu 2025-01-02 00:31:20 +01:00
parent c9f59cbd26
commit 8240856630
19 changed files with 379 additions and 266 deletions

View File

@ -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'

View File

@ -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 />

View File

@ -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;

View File

@ -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;

View File

@ -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"');

View File

@ -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>

View File

@ -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';

View File

@ -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">

View File

@ -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" />

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { color } from "~/composables/colors.ts";
import { color } from "~/composables/color";
const { big } = defineProps<{
big?: boolean

View File

@ -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;
}

View File

@ -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'}`}`

View File

@ -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))
)

View File

@ -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'

View File

@ -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'

View File

@ -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>

View File

@ -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>

View File

@ -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>
```

View File

@ -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