feat(ui): color (see "using color" in dev:ui-docs)

This commit is contained in:
upsiflu 2024-12-16 00:07:09 +01:00
parent 925d2db0a6
commit e583c51a54
22 changed files with 932 additions and 297 deletions

View File

@ -52,6 +52,7 @@
"standardized-audio-context": "25.3.60", "standardized-audio-context": "25.3.60",
"text-clipper": "2.2.0", "text-clipper": "2.2.0",
"transliteration": "2.3.5", "transliteration": "2.3.5",
"type-fest": "4.30.1",
"universal-cookie": "4.0.4", "universal-cookie": "4.0.4",
"vite-plugin-pwa": "0.14.4", "vite-plugin-pwa": "0.14.4",
"vue": "3.5.13", "vue": "3.5.13",

View File

@ -1,14 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { useColorOrPastel, type PastelProps } from '~/composables/colors' import { type PastelProps, propsToColor } from '~/composables/colors';
const props = defineProps<PastelProps>() type Props = PastelProps
const color = useColorOrPastel(() => props.color, 'blue')
const props = defineProps<Props>()
</script> </script>
<template> <template>
<div <div
class="funkwhale is-colored alert" class="funkwhale is-colored solid alert"
:class="[color]" v-bind="propsToColor(props)"
> >
<slot /> <slot />

View File

@ -1,11 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, useSlots, onMounted } from 'vue' import { ref, computed, useSlots, onMounted } from 'vue'
import { type ColorProps, useColor } from '~/composables/colors' import { type ColorProps, type VariantProps, propsToColor } from '~/composables/colors';
import Loader from '~/components/ui/Loader.vue' import Loader from '~/components/ui/Loader.vue'
interface Props { type Props = {
variant?: 'solid' | 'outline' | 'ghost'
width?: 'standard' | 'auto' | 'full' width?: 'standard' | 'auto' | 'full'
alignText?: 'left' | 'center' | 'right' alignText?: 'left' | 'center' | 'right'
@ -19,14 +18,12 @@ interface Props {
onClick?: (...args: any[]) => void | Promise<void> onClick?: (...args: any[]) => void | Promise<void>
autofocus? : boolean autofocus? : boolean
} } & ColorProps & VariantProps
const props = defineProps<Props & ColorProps>() const props = defineProps<Props>()
const color = useColor(() => props.color)
const slots = useSlots() const slots = useSlots()
const iconOnly = computed(() => !!props.icon && !slots.default) const isIconOnly = computed(() => !!props.icon && !slots.default)
const internalLoader = ref(false) const internalLoader = ref(false)
const isLoading = computed(() => props.isLoading || internalLoader.value) const isLoading = computed(() => props.isLoading || internalLoader.value)
@ -48,20 +45,22 @@ onMounted(() => {
</script> </script>
<template> <template>
<button ref="button" class="funkwhale is-colored button" :class="[ <button ref="button"
color, v-bind="propsToColor({...props, interactive:true})"
'is-' + (variant ?? 'solid'), class="funkwhale is-colored button"
'is-' + (width ?? 'standard'), :class="[
'is-aligned-' + (alignText ?? 'center'), 'is-' + (width ?? 'standard'),
{ 'is-aligned-' + (alignText ?? 'center'),
'is-active': isActive, {
'is-loading': isLoading, 'is-active': isActive,
'icon-only': iconOnly, 'is-loading': isLoading,
'has-icon': !!icon, 'is-icon-only': isIconOnly,
'is-round': round, 'has-icon': !!icon,
'is-shadow': shadow 'is-round': round,
} 'is-shadow': shadow
]" @click="click"> }
]" @click="click"
>
<i v-if="icon" :class="['bi', icon]" /> <i v-if="icon" :class="['bi', icon]" />
<span> <span>
@ -73,5 +72,81 @@ onMounted(() => {
</template> </template>
<style lang="scss"> <style lang="scss">
@import './button.scss' .funkwhale {
&.button {
position: relative;
display: inline-flex;
align-items: center;
white-space: nowrap;
font-family: $font-main;
font-weight: 900;
font-size: 0.875em;
line-height: 1em;
padding: 0.642857142857em 0.714em 0.714em 0.714em;
&.is-icon-only {
padding: 0.675em 0.714em 0.678em 0.714em;
}
border-radius: var(--fw-border-radius);
margin: 0 0.5ch;
transform: translateX(var(--fw-translate-x)) translateY(var(--fw-translate-y)) scale(var(--fw-scale));
transition: all .2s ease;
&.is-aligned-center {
justify-content: center;
}
&.is-aligned-left {
justify-content: flex-start;
}
&.is-aligned-right {
justify-content: flex-end;
}
&.is-shadow {
box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.2);
}
&:not(.is-icon-only):not(.is-auto) {
min-width: 8.5rem;
}
&.is-full {
display: block;
}
&.is-round {
border-radius: 100vh;
}
&[disabled] {
font-weight: normal;
pointer-events: none;
}
&.is-loading {
@extend .is-active;
> span {
opacity: 0;
}
}
i.bi {
font-size: 1.2rem;
}
i.bi + span:not(:empty) {
margin-left: 1ch;
}
}
}
</style> </style>

View File

@ -1,9 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { useCssModule } from 'vue'
import { computed } from 'vue' import { computed } from 'vue'
import { type RouterLinkProps, RouterLink } from 'vue-router'; import { type RouterLinkProps, RouterLink } from 'vue-router';
import { type Pastel } from '~/composables/colors'; import { type ColorProps, type PastelProps, type RaisedProps, type VariantProps, propsToColor } from '~/composables/colors';
import Pill from './Pill.vue' import Pill from './Pill.vue'
import Alert from './Alert.vue' import Alert from './Alert.vue'
@ -13,12 +12,11 @@ import Spacer from './layout/Spacer.vue';
interface Props extends Partial<RouterLinkProps> { interface Props extends Partial<RouterLinkProps> {
title: string title: string
category?: true | "h1" | "h2" | "h3" | "h4" | "h5" category?: true | "h1" | "h2" | "h3" | "h4" | "h5"
color?: Pastel
image?: string | { src: string, style?: "withPadding" } image?: string | { src: string, style?: "withPadding" }
tags?: string[] tags?: string[]
} }
const props = defineProps<Props>() const props = defineProps<Props & (PastelProps | ColorProps) & RaisedProps & VariantProps>()
const image = typeof props.image === 'string' ? { src: props.image } : props.image const image = typeof props.image === 'string' ? { src: props.image } : props.image
@ -146,7 +144,11 @@ const isExternalLink = computed(() => {
</style> </style>
<template> <template>
<Layout stack :class="{ [$style.card]: true, [$style['is-category']]: category }" style="--gap:16px"> <Layout stack
:class="{ [$style.card]: true, [$style['is-category']]: category }"
style="--gap:16px"
v-bind="propsToColor({...(props.to? { interactive: true, solid: true, default: true } : {}), ...props})"
>
<!-- Link --> <!-- Link -->
<a v-if="props.to && isExternalLink" :class="$style.covering" :href="to?.toString()" target="_blank" /> <a v-if="props.to && isExternalLink" :class="$style.covering" :href="to?.toString()" target="_blank" />
@ -164,7 +166,7 @@ const isExternalLink = computed(() => {
<!-- Content --> <!-- Content -->
<component :class="$style.title" :is="typeof category === 'string' ? category : 'h6'">{{ title }}</component> <component :class="$style.title" :is="typeof category === 'string' ? category : 'h6'">{{ title }}</component>
<Alert v-if="$slots.alert" :class="$style.alert"> <Alert blue v-if="$slots.alert" :class="$style.alert">
<slot name="alert" /> <slot name="alert" />
</Alert> </Alert>

View File

@ -1,24 +1,34 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import { type RouterLinkProps, RouterLink } from 'vue-router'; import { type RouterLinkProps, RouterLink } from 'vue-router';
import { type ColorProps, useColor } from '~/composables/colors'; import { type ColorProps, propsToColor } from '~/composables/colors';
const { to, icon, color, inline } = defineProps<RouterLinkProps & ColorProps & { const { to, icon, inline, ...otherProps } = defineProps<RouterLinkProps
& ColorProps
& {
icon?: string; icon?: string;
inline?: true inline?: true
}>() }>()
const colorClass = useColor(() => color)
const isExternalLink = computed(() => { const isExternalLink = computed(() => {
return typeof to === 'string' && to.startsWith('http') return typeof to === 'string' && to.startsWith('http')
}) })
</script> </script>
<template> <template>
<a v-if="isExternalLink" :class="[$style.external, colorClass, color && 'is-colored', inline && $style.inline]" :href="to?.toString()" target="_blank"> <a v-if="isExternalLink"
<slot /> :v-bind="propsToColor(otherProps)"
:class="[$style.link, $style.external, inline && $style.inline]"
:href="to?.toString()"
target="_blank"
>
<i v-if="icon" :class="['bi', icon]" />
<slot />
</a> </a>
<RouterLink v-if="to && !isExternalLink" :to="to" :class="[colorClass, color && 'is-colored', inline && $style.inline]"> <RouterLink v-else
:to="to"
:v-bind="propsToColor(otherProps)"
:class="[$style.link, inline && $style.inline]"
>
<i v-if="icon" :class="['bi', icon]" /> <i v-if="icon" :class="['bi', icon]" />
<slot /> <slot />
</RouterLink> </RouterLink>
@ -28,7 +38,7 @@ const isExternalLink = computed(() => {
.active { outline: 3px solid red; } .active { outline: 3px solid red; }
.external { outline: 3px dotted blue; } .external { outline: 3px dotted blue; }
.inline { display:inline-flex; } .inline { display:inline-flex; }
a { .link {
background-color: var(--fw-bg-color); background-color: var(--fw-bg-color);
color: var(--fw-text-color); color: var(--fw-text-color);
border: 1px solid var(--fw-bg-color); border: 1px solid var(--fw-bg-color);

View File

@ -17,7 +17,7 @@ const isOpen = defineModel<boolean>({ default:false })
<div @click.stop class="funkwhale modal" :class="$slots.alert && 'has-alert'" > <div @click.stop class="funkwhale modal" :class="$slots.alert && 'has-alert'" >
<h2> <h2>
{{ title }} {{ title }}
<Button icon="bi-x-lg" color="secondary" variant="ghost" @click="isOpen = false" /> <Button icon="bi-x-lg" ghost @click="isOpen = false" />
</h2> </h2>
<div class="modal-content"> <div class="modal-content">

View File

@ -1,21 +1,20 @@
<script setup lang="ts"> <script setup lang="ts">
import { useColorOrPastel, type ColorProps, type PastelProps } from '~/composables/colors' import { type ColorProps, type PastelProps, propsToColor } from '~/composables/colors';
const props = defineProps<ColorProps | PastelProps>()
const color = useColorOrPastel(() => props.color, 'secondary')
const emit = defineEmits<{ const emit = defineEmits<{
click: [event: MouseEvent] click: [event: MouseEvent]
}>() }>()
const handleClick = (event: MouseEvent) => { const handleClick = (event: MouseEvent) => {
emit('click', event) emit('click', event)
} }
const props = defineProps<PastelProps | ColorProps>()
</script> </script>
<template> <template>
<button <button
type="button" type="button"
class="funkwhale is-colored pill" class="funkwhale is-colored pill"
:class="[color]" v-bind="propsToColor({...props, interactive:true})"
@click.stop="handleClick" @click.stop="handleClick"
> >
<div v-if="!!$slots.image" class="pill-image"> <div v-if="!!$slots.image" class="pill-image">

View File

@ -186,17 +186,17 @@ const focus = () => textarea.value.focus()
@keydown.ctrl.shift.x.exact.prevent="strikethrough" @keydown.ctrl.k.exact.prevent="link" :maxlength="max" @keydown.ctrl.shift.x.exact.prevent="strikethrough" @keydown.ctrl.k.exact.prevent="link" :maxlength="max"
:placeholder="placeholder" v-model="model" id="textarea_id" /> :placeholder="placeholder" v-model="model" id="textarea_id" />
<div class="textarea-buttons"> <div class="textarea-buttons">
<Button @click="preview = !preview" icon="bi-eye" color="secondary" :is-active="preview" /> <Button @click="preview = !preview" icon="bi-eye" color="secondary" :aria-pressed="preview" />
<div class="separator" /> <div class="separator" />
<Button @click="heading1" icon="bi-type-h1" color="secondary" :is-active="isHeading1" :disabled="preview" /> <Button @click="heading1" icon="bi-type-h1" color="secondary" :aria-pressed="isHeading1" :disabled="preview" />
<Button @click="heading2" icon="bi-type-h2" color="secondary" :is-active="isHeading2" :disabled="preview" /> <Button @click="heading2" icon="bi-type-h2" color="secondary" :aria-pressed="isHeading2" :disabled="preview" />
<Button @click="paragraph" icon="bi-paragraph" color="secondary" :is-active="isParagraph" :disabled="preview" /> <Button @click="paragraph" icon="bi-paragraph" color="secondary" :aria-pressed="isParagraph" :disabled="preview" />
<Button @click="quote" icon="bi-quote" color="secondary" :is-active="isQuote" :disabled="preview" /> <Button @click="quote" icon="bi-quote" color="secondary" :aria-pressed="isQuote" :disabled="preview" />
<Button @click="orderedList" icon="bi-list-ol" color="secondary" :is-active="isOrderedList" <Button @click="orderedList" icon="bi-list-ol" color="secondary" :aria-pressed="isOrderedList"
:disabled="preview" /> :disabled="preview" />
<Button @click="unorderedList" icon="bi-list-ul" color="secondary" :is-active="isUnorderedList" <Button @click="unorderedList" icon="bi-list-ul" color="secondary" :aria-pressed="isUnorderedList"
:disabled="preview" /> :disabled="preview" />
<div class="separator" /> <div class="separator" />

View File

@ -1,37 +1,37 @@
.funkwhale.alert { .funkwhale.alert {
color: var(--fw-gray-900); // color: var(--fw-gray-900);
@include light-theme { // @include light-theme {
background-color: var(--fw-pastel-1, var(--fw-bg-color)); // background-color: var(--fw-pastel-1, var(--fw-bg-color));
> .actions .funkwhale.button { // > .actions .funkwhale.button {
--fw-bg-color: var(--fw-pastel-2); // --fw-bg-color: var(--fw-pastel-2);
&:hover, &.is-hovered { // &:hover, &.is-hovered {
--fw-bg-color: var(--fw-pastel-3); // --fw-bg-color: var(--fw-pastel-3);
} // }
&:active, &.is-active { // &:active, &.is-active {
--fw-bg-color: var(--fw-pastel-4); // --fw-bg-color: var(--fw-pastel-4);
} // }
} // }
} // }
@include dark-theme { // @include dark-theme {
background-color: var(--fw-pastel-3, var(--fw-bg-color)); // background-color: var(--fw-pastel-3, var(--fw-bg-color));
> .actions .funkwhale.button { // > .actions .funkwhale.button {
--fw-bg-color: var(--fw-pastel-4); // --fw-bg-color: var(--fw-pastel-4);
&:hover, &.is-hovered { // &:hover, &.is-hovered {
--fw-bg-color: var(--fw-pastel-2); // --fw-bg-color: var(--fw-pastel-2);
} // }
&:active, &.is-active { // &:active, &.is-active {
--fw-bg-color: var(--fw-pastel-1); // --fw-bg-color: var(--fw-pastel-1);
} // }
} // }
} // }
padding: 0.625rem 2rem; padding: 0.625rem 2rem;
line-height: 1.2; line-height: 1.2;

View File

@ -2,14 +2,10 @@
&.pill { &.pill {
color: var(--fw-text-color); color: var(--fw-text-color);
@include light-theme { // @include dark-theme {
background-color: var(--fw-pastel-2, var(--fw-bg-color)); // --fw-darken-pastel: color-mix(in srgb, var(--fw-pastel-4) 90%, black);
} // background-color: var(--fw-darken-pastel, var(--fw-bg-color));
// }
@include dark-theme {
--fw-darken-pastel: color-mix(in srgb, var(--fw-pastel-4) 90%, black);
background-color: var(--fw-darken-pastel, var(--fw-bg-color));
}
position: relative; position: relative;
display: inline-flex; display: inline-flex;

View File

@ -1,24 +1,61 @@
import { toValue, type MaybeRefOrGetter } from "@vueuse/core" import { toValue, type MaybeRefOrGetter } from "@vueuse/core"
import type { Entry, Join, KeysOfUnion, RequireExactlyOne, RequireOneOrNone, Simplify, SingleKeyObject, UnionToIntersection } from "type-fest"
import { computed } from 'vue' import { computed } from 'vue'
export function useColor(color: MaybeRefOrGetter<Color | undefined>, defaultColor: Color = 'primary') { export type DefaultProps =
return computed(() => `is-${toValue(color) ?? defaultColor}`) | { default?: true }
} export type Default = KeysOfUnion<DefaultProps>
export function usePastel(color: MaybeRefOrGetter<Pastel | undefined>, defaultColor: Pastel = 'blue') { export type ColorProps =
return computed(() => `is-${toValue(color) ?? defaultColor}`) | { primary?: true}
} | { secondary?: true }
| { destructive?: true }
export type Color = KeysOfUnion<ColorProps>
export function useColorOrPastel<T extends Color | Pastel>(color: MaybeRefOrGetter<T | undefined>, defaultColor: T) { export type PastelProps =
return computed(() => `is-${toValue(color) ?? defaultColor}`) | { red?:true }
} | { blue?:true }
| { purple?:true }
| { green?: true }
| { yellow?:true }
export type Pastel = KeysOfUnion<PastelProps>
export type Color = 'primary' | 'secondary' | 'destructive' export type VariantProps =
export interface ColorProps { | { solid?:true }
color?: Color | { outline?:true }
} | { ghost?:true }
export type Variant = KeysOfUnion<VariantProps>
export type Pastel = 'red' | 'blue' | 'purple' | 'green' | 'yellow' export type InteractiveProps =
export interface PastelProps { | { interactive?: true }
color?: Pastel export type Interactive = KeysOfUnion<DefaultProps>
}
export type RaisedProps =
| { raised?: true }
export type Raised = KeysOfUnion<RaisedProps>
export type Props =
DefaultProps & ColorProps & PastelProps & VariantProps & InteractiveProps & RaisedProps
export type ColorSelector =
`${Color | Pastel | Default}${'' | ` ${Variant}${'' | ' interactive'}${'' | ' raised'}`}`
/**
* Apply a color class for setting `color`, `background-color` and `border` styles.
* You can override it with `style="..."`.
*
* @param color Choose a semantic color or the default page background.
* This will affect the text color.
* To add an outline or filled background, add a `Variant`: `'solid' | 'outline'`.
* If the surface should stand out, add `raised`.
* If the surface reacts to mouse input, add `interactive`.
* @returns the corresponding `class` object
*
* Note: Make sure to implement the necessary classes in `colors.scss`!
*/
export const color = (color: ColorSelector) =>
({ class: color })
// Color from Props
export const propsToColor = (props: Partial<Props>) =>
({ class: Object.entries(props).filter(([key, value])=>value && key).map(([key,_])=>key).join(" ") })

View File

@ -87,7 +87,7 @@
--fw-pastel-yellow-3: #fed100; --fw-pastel-yellow-3: #fed100;
--fw-pastel-yellow-4: #efa300; --fw-pastel-yellow-4: #efa300;
// Override Bulma // Same in light and dark theme
--fw-primary: var(--fw-blue-500); --fw-primary: var(--fw-blue-500);
--fw-secondary: #ff6600; --fw-secondary: #ff6600;
--fw-destructive: var(--fw-red-500); --fw-destructive: var(--fw-red-500);
@ -96,83 +96,215 @@
--fw-page-bg-color: var(--fw-gray-960); --fw-page-bg-color: var(--fw-gray-960);
} }
.funkwhale { :is(.VPDoc .vp-doc, .funkwhale){
:is(button, input):focus-visible {
outline: 3px solid var(--fw-secondary);
outline-offset: 2px;
}
// Variants
.solid, .alert>.actions>button, button:not(:is(.ghost,.outline,.tabs-item)) {
color: var(--color);
background-color:var(--background-color);
border: 1px solid var(--background-color);
&.interactive {
&[aria-pressed=true] {
color: var(--pressed-color, var(--active-color));
background-color: var(--pressed-background-color, var(--active-background-color));
border-color: var(--pressed-background-color, var(--active-background-color));
}
&:hover{
color:var(--hover-color);
background-color:var(--hover-background-color);
border-color: var(--hover-background-color);
}
&:active{
color:var(--active-color);
background-color:var(--active-background-color);
border-color: var(--active-background-color);
}
&[disabled] {
color: var(--disabled-color);
border-color: var(--disabled-border-color);
background-color:var(--disabled-background-color);
}
}
}
.ghost {
color: var(--color);
border: 1px solid transparent;
&.interactive{
&:hover{
border: 1px solid var(--hover-background-color);
}
&:active{
border: 1px solid var(--active-background-color);
&.router-link-exact-active {
border: 1px solid var(--exact-active-background-color);
}
}
&[disabled] {
opacity:.5;
}
}
}
.outline {
color: var(--color);
border: 1px solid var(--border-color);
&.interactive{
&:hover{
border: 1px solid var(--hover-background-color);
}
&:active{
border: 1px solid var(--active-background-color);
&.router-link-exact-active {
background: 1px solid var(--exact-active-background-color);
}
}
}
}
}
:root{
@include light-theme { @include light-theme {
.is-primary { --fw-page-bg-color: var(--fw-beige-100);
--fw-bg-color: var(--fw-blue-400);
--fw-text-color: var(--fw-blue-010);
&.is-colored { .default {
&[disabled] { --color: var(--fw-gray-900);
--fw-bg-color: var(--fw-blue-100) !important; --background-color: var(--fw-beige-100);
--fw-text-color: var(--fw-blue-900) !important; --border-color:var(--fw-gray-300);
}
&.is-hovered, --hover-color:var(--fw-gray-800);
&:hover { --hover-background-color:var(--fw-beige-200);
--fw-bg-color: var(--fw-blue-500); --hover-border-color:var(--fw-gray-800);
}
&.is-active, --active-color:var(--fw-red-40);
&:active { --active-background-color:var(--fw-beige-400);
--fw-bg-color: var(--fw-blue-600); --active-border-color:var(--fw-gray-600);
&.router-link-exact-active {
--fw-bg-color: var(--fw-blue-700); --pressed-color:var(--fw-red-40);
} --pressed-background-color:var(--fw-gray-900);
}
--disabled-color:var(--fw-gray-500);
--disabled-background-color:var(--fw-beige-100);
--disabled-border-color:var(--fw-beige-100);
&.raised{
--background-color:var(--fw-beige-300);
--border-color:var(--fw-beige-400);
} }
} }
.is-secondary { .primary {
--fw-bg-color: var(--fw-gray-200); --color: var(--fw-blue-010);
--fw-text-color: var(--fw-gray-900); --background-color:var(--fw-blue-400);
--border-color:var(--fw-blue-010);
&.is-colored { --hover-color: var(--fw-blue-010);
&[disabled] { --hover-background-color:var(--fw-blue-500);
--fw-bg-color: var(--fw-gray-100) !important;
}
&.is-hovered, --active-color: var(--fw-blue-010);
&:hover { --active-background-color:var(--fw-blue-600);
--fw-bg-color: var(--fw-gray-200);
}
&.is-active, --pressed-color:var(--fw-blue-010);
&.active, --pressed-background-color:var(--fw-blue-800);
&:active {
--fw-bg-color: var(--fw-gray-300); --disabled-color:var(--fw-blue-900);
&.router-link-exact-active { --disabled-background-color:var(--fw-blue-100);
--fw-bg-color: var(--fw-gray-500); --disabled-border-color:var(--fw-blue-100);
}
} &.raised {
--background-color:var(--fw-blue-500);
--hover-background-color:var(--fw-blue-600);
--active-background-color:var(--fw-blue-700);
} }
} }
.is-destructive { .secondary, button {
--fw-bg-color: var(--fw-red-400); --color: var(--fw-gray-700);
--fw-text-color: var(--fw-red-010); --background-color: var(--fw-gray-200);
--border-color:var(--fw-gray-700);
&.is-colored { --hover-color:var(--fw-gray-800);
&[disabled] { --hover-background-color:var(--fw-gray-300);
--fw-bg-color: var(--fw-red-100) !important; --hover-border-color:var(--fw-gray-800);
--fw-text-color: var(--fw-blue-900) !important;
}
&.is-hovered, --active-color:var(--fw-gray-970);
&:hover { --active-background-color:var(--fw-gray-400);
--fw-bg-color: var(--fw-red-600); --active-border-color:var(--fw-gray-400);
}
&.is-active, --pressed-color:var(--fw-beige-200);
&.active, --pressed-background-color:var(--fw-gray-900);
&:active {
--fw-bg-color: var(--fw-red-700); --disabled-color:var(--fw-gray-500);
&.router-link-exact-active { --disabled-background-color:var(--fw-gray-100);
--fw-bg-color: var(--fw-red-800); --disabled-border-color:var(--fw-gray-100);
}
} &.raised {
--background-color:var(--fw-gray-300);
--border-color:var(--fw-gray-300);
--hover-background-color:var(--fw-gray-400);
--active-background-color:var(--fw-gray-500);
} }
} }
.destructive {
--color: var(--fw-red-010);
--background-color: var(--fw-red-500);
--border-color:var(--fw-red-500);
--hover-color:var(--fw-gray-100);
--hover-background-color:var(--fw-red-600);
--hover-border-color:var(--fw-red-600);
--active-color:var(--fw-gray-100);
--active-background-color:var(--fw-red-700);
--active-border-color:var(--fw-red-700);
--disabled-color:var(--fw-gray-500);
--disabled-background-color:var(--fw-gray-100);
--disabled-border-color:var(--fw-gray-100);
}
.blue {
--color: var(--fw-blue-900);
--background-color: var(--fw-pastel-blue-1);
.raised, button {
--background-color: var(--fw-pastel-blue-2);
--hover-background-color: var(--fw-pastel-blue-3);
--active-background-color: var(--fw-pastel-blue-4);
--disabled-color: var(--fw-gray-700);
--disabled-border-color: var(--fw-gray-400);
--disabled-background-color: transparent;
}
}
.red {
--color: var(--fw-red-900);
--background-color: var(--fw-pastel-red-2);
}
.purple {
--color: var(--fw-gray-970);
--background-color: var(--fw-pastel-purple-1);
}
.green {
--color: var(--fw-gray-900);
--background-color: var(--fw-pastel-green-1);
}
.yellow {
--color: var(--fw-gray-900);
--background-color: var(--fw-pastel-yellow-1);
}
} }
@include dark-theme { @include dark-theme {
@ -209,7 +341,7 @@
&.is-colored { &.is-colored {
&.is-hovered, &.is-hovered,
&:hover { &:hover {
--fw-bg-color: var(--fw-gray-800); --fw-bg-color: var(--fw-gray-950);
} }
&.is-active, &.is-active,
@ -217,7 +349,7 @@
&:active { &:active {
--fw-bg-color: var(--fw-gray-900); --fw-bg-color: var(--fw-gray-900);
&.router-link-exact-active { &.router-link-exact-active {
--fw-bg-color: var(--fw-gray-950); --fw-bg-color: var(--fw-gray-850);
} }
} }
} }
@ -252,6 +384,7 @@
} }
} }
.funkwhale { .funkwhale {
@each $pastel in ("blue", "red", "green", "purple", "yellow") { @each $pastel in ("blue", "red", "green", "purple", "yellow") {
&.is-#{$pastel} { &.is-#{$pastel} {

View File

@ -3,6 +3,7 @@ import { ref, onMounted } from 'vue'
import { useUploadsStore } from '../stores/upload' import { useUploadsStore } from '../stores/upload'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useStore } from '~/store' import { useStore } from '~/store'
import { color } from '~/composables/colors'
import Input from '~/components/ui/Input.vue' import Input from '~/components/ui/Input.vue'
import Link from '~/components/ui/Link.vue' import Link from '~/components/ui/Link.vue'
@ -23,7 +24,7 @@ const uploads = useUploadsStore()
</script> </script>
<template> <template>
<aside :class="[$style.sidebar, $style['sticky-content']]"> <aside :class="[$style.sidebar, $style['sticky-content']]" v-bind="color('default solid raised')">
<nav :class="$style['quick-actions']"> <nav :class="$style['quick-actions']">
<Link to="/"> <Link to="/">
<img <img
@ -135,8 +136,6 @@ const uploads = useUploadsStore()
<style module lang="scss"> <style module lang="scss">
.sidebar { .sidebar {
background-color: var(--fw-bg-raised)
height: 100%; height: 100%;
display:flex; display:flex;
flex-direction:column; flex-direction:column;

View File

@ -80,7 +80,7 @@ const currentFilter = ref(filterItems[0])
title="Upload music to library" title="Upload music to library"
> >
<template #alert="closeAlert"> <template #alert="closeAlert">
<Alert> <Alert yellow>
Before uploading, please ensure your files are tagged properly. Before uploading, please ensure your files are tagged properly.
We recommend using Picard for that purpose. We recommend using Picard for that purpose.

View File

@ -13,91 +13,83 @@ import Button from "~/components/ui/Button.vue"
Funkwhale alerts support a range of pastel colors for visual appeal. Funkwhale alerts support a range of pastel colors for visual appeal.
::: details Colors
- Red
- Blue
- Purple
- Green
- Yellow
:::
### Blue ### Blue
```vue-html ```vue-html
<Alert color="blue"> <Alert blue>
Blue alert Blue alert
</Alert> </Alert>
``` ```
<Alert color="blue"> <Alert blue>
Blue alert Blue alert
</Alert> </Alert>
### Red ### Red
```vue-html ```vue-html
<Alert color="red"> <Alert red>
Red alert Red alert
</Alert> </Alert>
``` ```
<Alert color="red"> <Alert red>
Red alert Red alert
</Alert> </Alert>
### Purple ### Purple
```vue-html ```vue-html
<Alert color="purple"> <Alert purple>
Purple alert Purple alert
</Alert> </Alert>
``` ```
<Alert color="purple"> <Alert purple>
Purple alert Purple burglar alert
</Alert> </Alert>
### Green ### Green
```vue-html ```vue-html
<Alert color="green"> <Alert green>
Green alert Green alert
</Alert> </Alert>
``` ```
<Alert color="green"> <Alert green>
Green alert Green alert
</Alert> </Alert>
### Yellow ### Yellow
```vue-html ```vue-html
<Alert color="yellow"> <Alert yellow>
Yellow alert Yellow alert
</Alert> </Alert>
``` ```
<Alert color="yellow"> <Alert yellow>
Yellow alert Yellow alert
</Alert> </Alert>
## Alert actions ## Alert actions
```vue-html{2-4} ```vue-html{2-4}
<Alert> <Alert blue>
Awesome artist Awesome artist
<template #actions> <template #actions>
<Button disabled>Deny</Button>
<Button>Got it</Button> <Button>Got it</Button>
</template> </template>
</Alert> </Alert>
``` ```
<Alert> <Alert blue>
Awesome artist Awesome artist
<template #actions> <template #actions>
<Button disabled>Deny</Button>
<Button>Got it</Button> <Button>Got it</Button>
</template> </template>
</Alert> </Alert>

View File

@ -8,15 +8,15 @@ const click = () => new Promise(resolve => setTimeout(resolve, 1000))
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. Funkwhale uses buttons in many contexts.
| Prop | Data type | Required? | Default | Description | | Prop | Data type | Required? | Default | Description |
| ------------ | ----------------------------------------- | --------- | --------- | ------------------------------------------------------------------ | | -------------- | --------- | --------- | ------- | ---------------------------------------------- |
| `variant` | `solid` \| `outline` \| `ghost` | No | `solid` | Whether to render the button as an solid, outline or ghost button. | | `shadow` | Boolean | No | `false` | Whether to render the button with a shadow |
| `shadow` | Boolean | No | `false` | Whether to render the button with a shadow | | `round` | Boolean | No | `false` | Whether to render the button as a round button |
| `round` | Boolean | No | `false` | Whether to render the button as a round button | | `icon` | String | No | | The icon attached to the button |
| `icon` | String | No | | The icon attached to the button | | `aria-pressed` | Boolean | No | `false` | Whether the button is in an active state |
| `is-active` | Boolean | No | `false` | Whether the button is in an active state | | `is-loading` | Boolean | No | `false` | Whether the button is in a loading state |
| `is-loading` | Boolean | No | `false` | Whether the button is in a loading state |
| `color` | `primary` \| `secondary` \| `destructive` | No | `primary` | Renders a colored button | In addition, use [Colors] and [Variants]
## Button colors ## Button colors
@ -31,12 +31,12 @@ This is the default type. If you don't specify a type, a primary button is rende
::: :::
```vue-html ```vue-html
<Button> <Button primary>
Primary button Primary button
</Button> </Button>
``` ```
<Button> <Button primary>
Primary button Primary button
</Button> </Button>
@ -45,12 +45,12 @@ This is the default type. If you don't specify a type, a primary button is rende
Secondary buttons represent **neutral** actions such as cancelling a change or dismissing a notification. Secondary buttons represent **neutral** actions such as cancelling a change or dismissing a notification.
```vue-html ```vue-html
<Button color="secondary"> <Button secondary>
Secondary button Secondary button
</Button> </Button>
``` ```
<Button color="secondary"> <Button secondary>
Secondary button Secondary button
</Button> </Button>
@ -59,12 +59,12 @@ Secondary buttons represent **neutral** actions such as cancelling a change or d
Desctrutive buttons represent **dangerous** actions including deleting items or purging domain information. Desctrutive buttons represent **dangerous** actions including deleting items or purging domain information.
```vue-html ```vue-html
<Button color="destructive"> <Button destructive>
Destructive button Destructive button
</Button> </Button>
``` ```
<Button color="destructive"> <Button destructive>
Destructive button Destructive button
</Button> </Button>
@ -84,23 +84,31 @@ This is the default style. If you don't specify a style, a solid button is rende
<Button> <Button>
Filled button Filled button
</Button> </Button>
<Button solid>
Also filled button
</Button>
``` ```
<Button> <Button>
Filled button Filled button
</Button> </Button>
<Button solid>
Also filled button
</Button>
### Outline ### Outline
Outline buttons have a transparent background. Use these to deemphasize the action the button performs. Outline buttons have a transparent background. Use these to deemphasize the action the button performs.
```vue-html ```vue-html
<Button variant="outline" color="secondary"> <Button outline secondary>
Outline button Outline button
</Button> </Button>
``` ```
<Button variant="outline" color="secondary"> <Button outline secondary>
Outline button Outline button
</Button> </Button>
@ -109,12 +117,12 @@ Outline buttons have a transparent background. Use these to deemphasize the acti
Ghost buttons have a transparent background and border. Use these to deemphasize the action the button performs. Ghost buttons have a transparent background and border. Use these to deemphasize the action the button performs.
```vue-html ```vue-html
<Button variant="ghost" color="secondary"> <Button ghost secondary>
Ghost button Ghost button
</Button> </Button>
``` ```
<Button variant="ghost" color="secondary"> <Button ghost secondary>
Ghost button Ghost button
</Button> </Button>
@ -176,15 +184,35 @@ You can pass a state to indicate whether a user can interact with a button.
### Active ### Active
A button is active when clicked by a user. You can force an active state by passing an `is-active` prop. 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 ```vue-html
<Button is-active> <Button aria-pressed>
Active button Active button
</Button> </Button>
``` ```
<Button is-active> **Secondary (default):**
<Button>
Inactive button
</Button>
<Button aria-pressed>
Active button
</Button>
---
**Primary:**
<Button primary>
Inactive button
</Button>
<Button primary aria-pressed>
Active button Active button
</Button> </Button>
@ -270,9 +298,9 @@ Icon buttons shrink down to the icon size if you don't pass any content. If you
</Button> </Button>
``` ```
<Button color="secondary" icon="bi-three-dots-vertical" /> <Button icon="bi-three-dots-vertical" />
<Button color="secondary" round icon="bi-x" /> <Button round icon="bi-x" />
<Button icon="bi-save">&nbsp;</Button> <Button primary icon="bi-save">&nbsp;</Button>
<Button color="destructive" icon="bi-trash"> <Button destructive icon="bi-trash">
Delete Delete
</Button> </Button>

View File

@ -25,14 +25,14 @@ Add a 16px gap between adjacent items.
<Layout flex> <Layout flex>
```vue-html ```vue-html
<Alert color="green">A</Alert> <Alert green">A</Alert>
<Alert color="red">B</Alert> <Alert red">B</Alert>
``` ```
<div class="preview"> <div class="preview">
<Alert color="green">A</Alert> <Alert green>A</Alert>
<Alert color="red">B</Alert> <Alert purple>B</Alert>
</div> </div>
@ -43,15 +43,15 @@ Add a 16px gap between adjacent items.
<Layout flex> <Layout flex>
```vue-html{2} ```vue-html{2}
<Alert color="green">A</Alert> <Alert green">A</Alert>
<Spacer/> <Spacer/>
<Alert color="red">B</Alert> <Alert red">B</Alert>
``` ```
<div class="preview"> <div class="preview">
<Alert color="green">A</Alert> <Alert green>A</Alert>
<Spacer/> <Spacer/>
<Alert color="red">B</Alert> <Alert purple>B</Alert>
</div> </div>
</Layout> </Layout>
@ -62,19 +62,19 @@ Add a 16px gap between adjacent items.
```vue-html{4} ```vue-html{4}
<Layout flex> <Layout flex>
<Alert color="blue">A</Alert> <Alert blue">A</Alert>
<Alert color="green">A</Alert> <Alert yellow">B</Alert>
<Spacer/> <Spacer/>
<Alert color="red">B</Alert> <Alert red">C</Alert>
</Layout> </Layout>
``` ```
<div class="preview"> <div class="preview">
<Layout flex> <Layout flex>
<Alert color="blue">A</Alert> <Alert blue>A</Alert>
<Alert color="green">A</Alert> <Alert yellow>B</Alert>
<Spacer/> <Spacer/>
<Alert color="red">B</Alert> <Alert red>C</Alert>
</Layout> </Layout>
</div> </div>
</Layout> </Layout>
@ -91,10 +91,9 @@ const size = ref(1);
<template> <template>
<Input v-model="size" type="range" /> <Input v-model="size" type="range" />
<Alert color="blue">A</Alert> <Alert yellow>A</Alert>
<Alert color="green">A</Alert>
<Spacer :size="size" /> <Spacer :size="size" />
<Alert color="red">B</Alert> <Alert purple>B</Alert>
</template> </template>
``` ```
@ -103,9 +102,9 @@ const size = ref(1);
{{ size }}px {{ size }}px
</div> </div>
<div class="preview"> <div class="preview">
<Alert color="blue">A</Alert> <Alert yellow>A</Alert>
<Spacer :size="size" /> <Spacer :size="size" />
<Alert color="red">B</Alert> <Alert purple>B</Alert>
</div> </div>
</Layout> </Layout>
@ -123,14 +122,14 @@ const size = ref(1);
<Layout stack no-gap <Layout stack no-gap
:style="{ height: size + '%' }" :style="{ height: size + '%' }"
> >
<Alert>A</Alert> <Alert blue>A</Alert>
<Spacer grow title="grow" /> <Spacer grow title="grow" />
<Alert>B</Alert> <Alert red>B</Alert>
<Alert>C</Alert> <Alert green>C</Alert>
<Spacer shrink title="shrink" /> <Spacer shrink title="shrink" />
<Alert>D</Alert> <Alert purple>D</Alert>
<Spacer grow shrink title="grow shrink" /> <Spacer grow shrink title="grow shrink" />
<Alert>E</Alert> <Alert yellow>E</Alert>
</Layout> </Layout>
</Layout> </Layout>
``` ```
@ -141,14 +140,14 @@ const size = ref(1);
<Input v-model="size" type="range" style="writing-mode: vertical-lr; height:100%"><template #input-right>{{ size }}%</template></Input> <Input v-model="size" type="range" style="writing-mode: vertical-lr; height:100%"><template #input-right>{{ size }}%</template></Input>
<Layout stack no-gap :style="{ height: size + '%'}"> <Layout stack no-gap :style="{ height: size + '%'}">
<Alert>A</Alert> <Alert blue>A</Alert>
<Spacer grow title="grow" /> <Spacer grow title="grow" />
<Alert>B</Alert> <Alert red>B</Alert>
<Alert>C</Alert> <Alert green>C</Alert>
<Spacer shrink title="shrink" /> <Spacer shrink title="shrink" />
<Alert>D</Alert> <Alert purple>D</Alert>
<Spacer grow shrink title="grow shrink" /> <Spacer grow shrink title="grow shrink" />
<Alert>E</Alert> <Alert yellow>E</Alert>
</Layout> </Layout>
</Layout> </Layout>

View File

@ -70,7 +70,7 @@ Make sure to add `autofocus` to the preferred button.
Modal content Modal content
<template #actions> <template #actions>
<Button @click="isOpen = false" color="secondary"> <Button @click="isOpen = false">
Cancel Cancel
</Button> </Button>
@ -88,7 +88,7 @@ Make sure to add `autofocus` to the preferred button.
<Modal v-model="isOpen2" title="My modal"> <Modal v-model="isOpen2" title="My modal">
Modal content Modal content
<template #actions> <template #actions>
<Button @click="isOpen2 = false" color="secondary"> <Button @click="isOpen2 = false">
Cancel Cancel
</Button> </Button>
<Button autofocus @click="isOpen2 = false"> <Button autofocus @click="isOpen2 = false">
@ -120,13 +120,13 @@ Note that confirmation dialogs interrupt the user's workflow. Consider adding a
::: :::
```vue-html ```vue-html
<Button @click="isOpen = true" color="destructive"> <Button @click="isOpen = true" destructive>
Delete my account ... Delete my account ...
</Button> </Button>
<Modal v-model="isOpen" title="Delete account?"> <Modal v-model="isOpen" title="Delete account?">
<template #alert> <template #alert>
<Alert color="red"> <Alert red>
1 082 music files that you uploaded will be deleted.<br /> 1 082 music files that you uploaded will be deleted.<br />
7 879 items in your collections will be unlinked. 7 879 items in your collections will be unlinked.
</Alert> </Alert>
@ -135,22 +135,22 @@ Do you want to delete your account forever?
You will not be able to restore your account. You will not be able to restore your account.
<template #actions> <template #actions>
<Button autofocus @click="isOpen = false" color="secondary"> <Button autofocus @click="isOpen = false" >
Keep my account Keep my account
</Button> </Button>
<Button color="destructive" @click="isOpen = false"> <Button destructive @click="isOpen = false">
I understand. Delete my account now! I understand. Delete my account now!
</Button> </Button>
</template> </template>
</Modal> </Modal>
``` ```
<Button @click="isOpen6 = true" color="destructive"> <Button @click="isOpen6 = true" destructive>
Delete my account ... Delete my account ...
</Button> </Button>
<Modal v-model="isOpen6" title="Delete account?"> <Modal v-model="isOpen6" title="Delete account?">
<template #alert> <template #alert>
<Alert color="red"> <Alert red>
1 082 music files that you uploaded will be deleted.<br /> 1 082 music files that you uploaded will be deleted.<br />
7 879 items in your collections will be unlinked. 7 879 items in your collections will be unlinked.
</Alert> </Alert>
@ -159,10 +159,10 @@ Do you want to delete your account forever?
You will not be able to restore your account. You will not be able to restore your account.
<template #actions> <template #actions>
<Button autofocus @click="isOpen6 = false" color="secondary"> <Button autofocus @click="isOpen6 = false">
Keep my account Keep my account
</Button> </Button>
<Button color="destructive" @click="isOpen6 = false"> <Button destructive @click="isOpen6 = false">
I understand. Delete my account now! I understand. Delete my account now!
</Button> </Button>
</template> </template>
@ -239,7 +239,7 @@ You can nest [Funkwhale alerts](./alert) to visually highlight content within th
<Modal v-model="isOpen3" title="My modal"> <Modal v-model="isOpen3" title="My modal">
Modal content Modal content
<template #alert v-if="alertOpen"> <template #alert v-if="alertOpen">
<Alert> <Alert blue>
Alert content Alert content
<template #actions> <template #actions>
<Button autofocus @click="alertOpen = false">Close alert</Button> <Button autofocus @click="alertOpen = false">Close alert</Button>
@ -250,7 +250,7 @@ You can nest [Funkwhale alerts](./alert) to visually highlight content within th
<Button @click="isOpen3 = false" color="secondary"> <Button @click="isOpen3 = false" color="secondary">
Cancel Cancel
</Button> </Button>
<Button @click="isOpen3 = false"> <Button primary @click="isOpen3 = false">
Ok Ok
</Button> </Button>
</template> </template>

View File

@ -12,29 +12,17 @@ You can add text to pills by adding it between the `<Pill>` tags.
| ------- | ----------------------------------------------------------------------------------------------- | --------- | ----------- | ---------------------- | | ------- | ----------------------------------------------------------------------------------------------- | --------- | ----------- | ---------------------- |
| `color` | `primary` \| `secondary` \| `destructive` \| `blue` \| `red` \| `purple` \| `green` \| `yellow` | No | `secondary` | Renders a colored pill | | `color` | `primary` \| `secondary` \| `destructive` \| `blue` \| `red` \| `purple` \| `green` \| `yellow` | No | `secondary` | Renders a colored pill |
## Pill types
You can assign a type to your pill to indicate what kind of information it conveys.
::: details Types
- Primary
- Secondary
- Destructive
:::
### Primary ### Primary
Primary pills convey **positive** information. Primary pills convey **positive** information.
```vue-html ```vue-html
<Pill color="primary"> <Pill primary>
Primary pill Primary pill
</Pill> </Pill>
``` ```
<Pill color="primary"> <Pill primary>
Primary pill Primary pill
</Pill> </Pill>
@ -61,12 +49,12 @@ This is the default type for pills. If you don't specify a type, a **secondary**
Destructive pills convey **destructive** or **negative** information. Use these to indicate that information could cause issues such as data loss. Destructive pills convey **destructive** or **negative** information. Use these to indicate that information could cause issues such as data loss.
```vue-html ```vue-html
<Pill color="destructive"> <Pill destructive>
Destructive pill Destructive pill
</Pill> </Pill>
``` ```
<Pill color="destructive"> <Pill destructive>
Destructive pill Destructive pill
</Pill> </Pill>
@ -74,73 +62,63 @@ Destructive pills convey **destructive** or **negative** information. Use these
Funkwhale pills support a range of pastel colors to create visually appealing interfaces. Funkwhale pills support a range of pastel colors to create visually appealing interfaces.
::: details Colors
- Red
- Blue
- Purple
- Green
- Yellow
:::
### Blue ### Blue
```vue-html ```vue-html
<Pill color="blue"> <Pill blue>
Blue pill Blue pill
</Pill> </Pill>
``` ```
<Pill color="blue"> <Pill blue>
Blue pill Blue pill
</Pill> </Pill>
### Red ### Red
```vue-html ```vue-html
<Pill color="red"> <Pill red>
Red pill Red pill
</Pill> </Pill>
``` ```
<Pill color="red"> <Pill red>
Red pill Red pill
</Pill> </Pill>
### Purple ### Purple
```vue-html ```vue-html
<Pill color="purple"> <Pill purple>
Purple pill Purple pill
</Pill> </Pill>
``` ```
<Pill color="purple"> <Pill purple>
Purple pill Purple pill
</Pill> </Pill>
### Green ### Green
```vue-html ```vue-html
<Pill color="green"> <Pill green>
Green pill Green pill
</Pill> </Pill>
``` ```
<Pill color="green"> <Pill green>
Green pill Green pill
</Pill> </Pill>
### Yellow ### Yellow
```vue-html ```vue-html
<Pill color="yellow"> <Pill yellow>
Yellow pill Yellow pill
</Pill> </Pill>
``` ```
<Pill color="yellow"> <Pill yellow>
Yellow pill Yellow pill
</Pill> </Pill>

View File

@ -1 +1,292 @@
<script setup>
import { color } from "~/composables/colors.ts"
import Button from "~/components/ui/Button.vue"
import Card from "~/components/ui/Card.vue"
</script>
# Using Color # Using Color
## Add color via props
[Alerts](components/ui/alert) support [Pastel](#pastel) attributes. [Buttons](components/ui/button) accept [Color](#color) and [Variant](#variant) attributes. [Cards](components/ui/card) accept [Pastel](#pastel), [Variant](#variant), [Neutral](#neutral) and [Raised](#raised) attributes.
```vue-html
<Card title="solid red" solid red />
```
<Card title="solid red" solid red />
## Add color to a any component or Html tag
1. Choose a
- [base color](#colors) (`primary | secondary | destructive`) or
- [pastel color](#pastel) (`blue | red | green | yellow`) or
- [neutral beige or gray](#neutral) (`default`) for surfaces
2. Choose a [variant](#color-variants) (`solid | ghost | outline`)
3. Add [interactivity and raise the surface](#interactive-andor-raised)
```vue
<script setup>
import { color } from "~/composables/colors.ts";
</script>
<template>
<div v-bind="color('primary solid interactive raised')" />
</template>
```
<div :class="$style.swatch" v-bind="color('primary solid interactive raised')" />
## Base colors
### Neutral
<div :class="$style.swatch" v-bind="color('default solid')" />
<div :class="$style.swatch" v-bind="color('default solid interactive')" />
<div :class="$style.swatch" v-bind="color('default solid raised')" />
<div :class="$style.swatch" v-bind="color('default solid interactive')" />
### Color
Primary
<div :class="$style.swatch" v-bind="color('primary solid')" />
<div :class="$style.swatch" v-bind="color('primary solid interactive')" />
<div :class="$style.swatch" v-bind="color('primary solid raised')" />
<div :class="$style.swatch" v-bind="color('primary solid interactive')" />
Secondary
<div :class="$style.swatch" v-bind="color('secondary solid')" />
<div :class="$style.swatch" v-bind="color('secondary solid interactive')" />
<div :class="$style.swatch" v-bind="color('secondary solid raised')" />
<div :class="$style.swatch" v-bind="color('secondary solid interactive')" />
Destructive
<div :class="$style.swatch" v-bind="color('destructive solid')" />
<div :class="$style.swatch" v-bind="color('destructive solid interactive')" />
<div :class="$style.swatch" v-bind="color('destructive solid raised')" />
<div :class="$style.swatch" v-bind="color('destructive solid interactive')" />
### Pastel
Blue, Red, Purple, Green, Yellow
<div :class="$style.swatch" v-bind="color('blue solid interactive')" />
<div :class="$style.swatch" v-bind="color('red solid interactive')" />
<div :class="$style.swatch" v-bind="color('purple solid interactive')" />
<div :class="$style.swatch" v-bind="color('green solid interactive')" />
<div :class="$style.swatch" v-bind="color('yellow solid interactive')" />
---
### Variant
Solid (default), Ghost, Outline
<Button round shadow icon="bi-x" solid />
<div :class="$style.swatch" v-bind="color('solid raised')">
<Button round icon="bi-x" ghost />
<Button round icon="bi-x" outline />
</div>
<br/>
<Button round shadow icon="bi-x" primary solid />
<div :class="$style.swatch" v-bind="color('primary solid')">
<Button round icon="bi-x" primary ghost />
<Button round icon="bi-x" primary outline />
</div>
---
### Interactive and/or Raised
<div v-bind="color('default solid')" style="display:inline-flex;">
<div :class="$style.swatch" v-bind="color('solid')" />
<div :class="$style.swatch" v-bind="color('solid interactive')" />
</div>
<div v-bind="color('secondary solid')" style="display:inline-flex;">
<div :class="$style.swatch" v-bind="color('solid')" />
<div :class="$style.swatch" v-bind="color('solid interactive')" />
</div>
<div v-bind="color('primary solid')" style="display:inline-flex;">
<div :class="$style.swatch" v-bind="color('solid')" />
<div :class="$style.swatch" v-bind="color('solid interactive')" />
</div>
<br/>
<div v-bind="color('default raised solid')" style="display:inline-flex;">
<div :class="$style.swatch" v-bind="color('solid raised')" />
<div :class="$style.swatch" v-bind="color('solid interactive raised')" />
</div>
<div v-bind="color('secondary raised solid')" style="display:inline-flex;">
<div :class="$style.swatch" v-bind="color('solid raised')" />
<div :class="$style.swatch" v-bind="color('solid interactive raised')" />
</div>
<div v-bind="color('primary raised solid')" style="display:inline-flex;">
<div :class="$style.swatch" v-bind="color('solid raised')" />
<div :class="$style.swatch" v-bind="color('solid interactive raised')" />
</div>
## Palette
The color palette consists of Blues, Reds, Grays, Beiges, as well as pastel blue/red/green/yellow.
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-blue-010)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-blue-100)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-blue-400)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-blue-500)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-blue-600)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-blue-700)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-blue-800)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-blue-900)" />
<br/>
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-red-010)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-red-100)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-red-400)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-red-500)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-red-600)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-red-700)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-red-800)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-red-900)" />
<br/>
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-gray-100)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-gray-200)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-gray-300)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-gray-400)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-gray-500)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-gray-600)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-gray-700)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-gray-800)" />
<div :class="[$style.swatch, $style.tiny]" style="background:var(--fw-gray-850)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-gray-900)" />
<div :class="[$style.swatch, $style.tiny]" style="background:var(--fw-gray-950)" />
<div :class="[$style.swatch, $style.tiny]" style="background:var(--fw-gray-960)" />
<div :class="[$style.swatch, $style.tiny]" style="background:var(--fw-gray-970)" />
<br/>
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-beige-100)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-beige-200)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-beige-300)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-beige-400)" />
---
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-pastel-blue-1)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-pastel-blue-2)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-pastel-blue-3)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-pastel-blue-4)" />
<br/>
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-pastel-red-1)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-pastel-red-2)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-pastel-red-3)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-pastel-red-4)" />
<br/>
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-pastel-purple-1)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-pastel-purple-2)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-pastel-purple-3)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-pastel-purple-4)" />
<br/>
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-pastel-green-1)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-pastel-green-2)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-pastel-green-3)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-pastel-green-4)" />
<br/>
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-pastel-yellow-1)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-pastel-yellow-2)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-pastel-yellow-3)" />
<div :class="[$style.swatch, $style.small]" style="background:var(--fw-pastel-yellow-4)" />
In addition, we have a single shade of orange used for secondary indicators such as active tabs:
<div :class="$style.swatch" style="background:var(--fw-secondary)" />
## Theme
Many browsers automatically turn on "night mode" to lower the light emission of the screen. Users can override the browser theme in their settings menu.
In both "dark mode" and "light mode", the colors must provide adequate contrast and consistency.
## Semantic colors
We use semantic color variables that can mean a different shade depending on the currently chosen theme
- primary
- secondary (default)
- destructive
## Color variants
For each semantic color, we set a foreground and a background. In addition, we need to check context: A button should have a stronger shade when the mouse is hovering over it. When a surface is raised over another surface, it should have a stronger shade, too.
- ghost (default for most other things)
- text color
- outline
- border color
- solid (default for buttons)
- bg color
- border color
Variants can be made interactive and/or raised:
- no alteration (default)
- raised
- text color
- border color
- bg color
- interactive
- can be disabled
- can be active
- can be exact-active
- can be hovering
- interactive-raised
- combine `raised` and `interactive`
<style module>
.swatch {
border-radius: 2em;
min-width: 3.2em;
min-height: 3.2em;
margin: 1ch;
display: inline-flex;
box-shadow: 1px 2px 7px #0003, 0px 0px 1px #0009;
align-items:center;
justify-items:center;
}
.small{
margin: .25rem;
min-width: 1.6em;
min-height: 1.6em;
box-shadow: 1px 2px 4px #0002, 0px 0px 1px #0007;
}
.tiny{
margin: .5rem .25rem;
min-width: 1em;
min-height: 1em;
box-shadow: 1px 2px 4px #0002, 0px 0px 1px #0007;
}
</style>

View File

@ -71,7 +71,96 @@ import Button from "~/components/ui/Button.vue";
<style module></style> <style module></style>
<template> <template>
<Alert /> <Alert yellow />
<Button /> <Button />
</template> </template>
``` ```
## Limitations of component props
While Vue can infer props based on a type, it will fail with mysterious errors if the type is an exclusive union or has union types as keys.
I hope this will be resolved soon so we can use this more elegant way of injecting non-trivial props with full autocomplete, 21st century style:
```vue
<script setup>
type A = 'either' | 'or'
type B = 'definitely'
type Props = { [k in `${A}-${B}`]?: true }
</script>
<template>
<Component either-definitely />
<Component or-definitely />
<Component />
{{ Error: <Component either /> }} {{ Error: <Component definitely /> }}
</template>
```
::: details Example
````ts
// Color from props
type SingleOrNoProp<T extends string> = RequireOneOrNone<Record<T, true>, T>;
type SingleProp<T extends string> = RequireExactlyOne<Record<T, true>, T>;
export type Props = Simplify<
SingleProp<Color | Default | Pastel> &
SingleOrNoProp<Variant> &
SingleOrNoProp<"interactive"> &
SingleOrNoProp<"raised">
>;
// Limit the choices:
export type ColorProps = Simplify<
SingleProp<Color> & SingleOrNoProp<Variant> & SingleOrNoProp<"raised">
>;
export type PastelProps = Simplify<
SingleProp<Pastel> & SingleOrNoProp<"raised">
>;
// Note that as of now, Vue does not support unions of props.
// So instead, we give it a single string:
export type ColorProp = Simplify<`${Color}${
| ""
| `-${Variant}${"" | "-raised"}`}`>;
export type PastelProp = Simplify<`${Pastel}${"" | "-raised"}`>;
// Using like this:
// type Props = {...} & { [k in ColorProp]? : true }
// This will also lead to runtime errors. Why?
export const isColorProp = (k: string) =>
!![...colors, ...defaults, ...pastels].find(k.startsWith);
console.log(true, isColorProp("primary"));
console.log(true, isColorProp("secondary"));
console.log(true, isColorProp("red"));
console.log(false, isColorProp("Jes"));
console.log(false, isColorProp("raised"));
/**
* Convenience function in case you want to hand over the props in the form
* ```
* <Component primary solid interactive raised >...</Component>
* ```
*
* @param props Any superset of type `Props`
* @returns the corresponding `class` object
*
* Note: Make sure to implement the necessary classes in `colors.scss`!
*/
export const colorFromProps = (props: Record<string, unknown>) =>
color(
Object.keys(props)
.filter(isColorProp)
.join(" ")
.replace("-", " ") as ColorSelector,
);
````
:::

View File

@ -10668,6 +10668,11 @@ type-detect@^4.0.0, type-detect@^4.1.0:
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.1.0.tgz#deb2453e8f08dcae7ae98c626b13dddb0155906c" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.1.0.tgz#deb2453e8f08dcae7ae98c626b13dddb0155906c"
integrity sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw== integrity sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==
type-fest@4.30.1:
version "4.30.1"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.30.1.tgz#120b9e15177310ec4e9d5d6f187d86c0f4b55e0e"
integrity sha512-ojFL7eDMX2NF0xMbDwPZJ8sb7ckqtlAi1GsmgsFXvErT9kFTk1r0DuQKvrCh73M6D4nngeHJmvogF9OluXs7Hw==
type-fest@^0.16.0: type-fest@^0.16.0:
version "0.16.0" version "0.16.0"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.16.0.tgz#3240b891a78b0deae910dbeb86553e552a148860" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.16.0.tgz#3240b891a78b0deae910dbeb86553e552a148860"