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",
"text-clipper": "2.2.0",
"transliteration": "2.3.5",
"type-fest": "4.30.1",
"universal-cookie": "4.0.4",
"vite-plugin-pwa": "0.14.4",
"vue": "3.5.13",

View File

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

View File

@ -1,11 +1,10 @@
<script setup lang="ts">
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'
interface Props {
variant?: 'solid' | 'outline' | 'ghost'
type Props = {
width?: 'standard' | 'auto' | 'full'
alignText?: 'left' | 'center' | 'right'
@ -19,14 +18,12 @@ interface Props {
onClick?: (...args: any[]) => void | Promise<void>
autofocus? : boolean
}
} & ColorProps & VariantProps
const props = defineProps<Props & ColorProps>()
const color = useColor(() => props.color)
const props = defineProps<Props>()
const slots = useSlots()
const iconOnly = computed(() => !!props.icon && !slots.default)
const isIconOnly = computed(() => !!props.icon && !slots.default)
const internalLoader = ref(false)
const isLoading = computed(() => props.isLoading || internalLoader.value)
@ -48,20 +45,22 @@ onMounted(() => {
</script>
<template>
<button ref="button" class="funkwhale is-colored button" :class="[
color,
'is-' + (variant ?? 'solid'),
'is-' + (width ?? 'standard'),
'is-aligned-' + (alignText ?? 'center'),
{
'is-active': isActive,
'is-loading': isLoading,
'icon-only': iconOnly,
'has-icon': !!icon,
'is-round': round,
'is-shadow': shadow
}
]" @click="click">
<button ref="button"
v-bind="propsToColor({...props, interactive:true})"
class="funkwhale is-colored button"
:class="[
'is-' + (width ?? 'standard'),
'is-aligned-' + (alignText ?? 'center'),
{
'is-active': isActive,
'is-loading': isLoading,
'is-icon-only': isIconOnly,
'has-icon': !!icon,
'is-round': round,
'is-shadow': shadow
}
]" @click="click"
>
<i v-if="icon" :class="['bi', icon]" />
<span>
@ -73,5 +72,81 @@ onMounted(() => {
</template>
<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>

View File

@ -1,9 +1,8 @@
<script setup lang="ts">
import { useCssModule } from 'vue'
import { computed } from 'vue'
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 Alert from './Alert.vue'
@ -13,12 +12,11 @@ import Spacer from './layout/Spacer.vue';
interface Props extends Partial<RouterLinkProps> {
title: string
category?: true | "h1" | "h2" | "h3" | "h4" | "h5"
color?: Pastel
image?: string | { src: string, style?: "withPadding" }
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
@ -146,7 +144,11 @@ const isExternalLink = computed(() => {
</style>
<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 -->
<a v-if="props.to && isExternalLink" :class="$style.covering" :href="to?.toString()" target="_blank" />
@ -164,7 +166,7 @@ const isExternalLink = computed(() => {
<!-- Content -->
<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" />
</Alert>

View File

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

View File

@ -1,21 +1,20 @@
<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<{
click: [event: MouseEvent]
}>()
const handleClick = (event: MouseEvent) => {
emit('click', event)
}
const props = defineProps<PastelProps | ColorProps>()
</script>
<template>
<button
type="button"
class="funkwhale is-colored pill"
:class="[color]"
v-bind="propsToColor({...props, interactive:true})"
@click.stop="handleClick"
>
<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"
:placeholder="placeholder" v-model="model" id="textarea_id" />
<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" />
<Button @click="heading1" icon="bi-type-h1" color="secondary" :is-active="isHeading1" :disabled="preview" />
<Button @click="heading2" icon="bi-type-h2" color="secondary" :is-active="isHeading2" :disabled="preview" />
<Button @click="paragraph" icon="bi-paragraph" color="secondary" :is-active="isParagraph" :disabled="preview" />
<Button @click="quote" icon="bi-quote" color="secondary" :is-active="isQuote" :disabled="preview" />
<Button @click="orderedList" icon="bi-list-ol" color="secondary" :is-active="isOrderedList"
<Button @click="heading1" icon="bi-type-h1" color="secondary" :aria-pressed="isHeading1" :disabled="preview" />
<Button @click="heading2" icon="bi-type-h2" color="secondary" :aria-pressed="isHeading2" :disabled="preview" />
<Button @click="paragraph" icon="bi-paragraph" color="secondary" :aria-pressed="isParagraph" :disabled="preview" />
<Button @click="quote" icon="bi-quote" color="secondary" :aria-pressed="isQuote" :disabled="preview" />
<Button @click="orderedList" icon="bi-list-ol" color="secondary" :aria-pressed="isOrderedList"
: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" />
<div class="separator" />

View File

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

View File

@ -2,14 +2,10 @@
&.pill {
color: var(--fw-text-color);
@include light-theme {
background-color: var(--fw-pastel-2, 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));
}
// @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;
display: inline-flex;

View File

@ -1,24 +1,61 @@
import { toValue, type MaybeRefOrGetter } from "@vueuse/core"
import type { Entry, Join, KeysOfUnion, RequireExactlyOne, RequireOneOrNone, Simplify, SingleKeyObject, UnionToIntersection } from "type-fest"
import { computed } from 'vue'
export function useColor(color: MaybeRefOrGetter<Color | undefined>, defaultColor: Color = 'primary') {
return computed(() => `is-${toValue(color) ?? defaultColor}`)
}
export type DefaultProps =
| { default?: true }
export type Default = KeysOfUnion<DefaultProps>
export function usePastel(color: MaybeRefOrGetter<Pastel | undefined>, defaultColor: Pastel = 'blue') {
return computed(() => `is-${toValue(color) ?? defaultColor}`)
}
export type ColorProps =
| { primary?: true}
| { secondary?: true }
| { destructive?: true }
export type Color = KeysOfUnion<ColorProps>
export function useColorOrPastel<T extends Color | Pastel>(color: MaybeRefOrGetter<T | undefined>, defaultColor: T) {
return computed(() => `is-${toValue(color) ?? defaultColor}`)
}
export type PastelProps =
| { red?:true }
| { blue?:true }
| { purple?:true }
| { green?: true }
| { yellow?:true }
export type Pastel = KeysOfUnion<PastelProps>
export type Color = 'primary' | 'secondary' | 'destructive'
export interface ColorProps {
color?: Color
}
export type VariantProps =
| { solid?:true }
| { outline?:true }
| { ghost?:true }
export type Variant = KeysOfUnion<VariantProps>
export type Pastel = 'red' | 'blue' | 'purple' | 'green' | 'yellow'
export interface PastelProps {
color?: Pastel
}
export type InteractiveProps =
| { interactive?: true }
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-4: #efa300;
// Override Bulma
// Same in light and dark theme
--fw-primary: var(--fw-blue-500);
--fw-secondary: #ff6600;
--fw-destructive: var(--fw-red-500);
@ -96,83 +96,215 @@
--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 {
.is-primary {
--fw-bg-color: var(--fw-blue-400);
--fw-text-color: var(--fw-blue-010);
--fw-page-bg-color: var(--fw-beige-100);
&.is-colored {
&[disabled] {
--fw-bg-color: var(--fw-blue-100) !important;
--fw-text-color: var(--fw-blue-900) !important;
}
.default {
--color: var(--fw-gray-900);
--background-color: var(--fw-beige-100);
--border-color:var(--fw-gray-300);
&.is-hovered,
&:hover {
--fw-bg-color: var(--fw-blue-500);
}
--hover-color:var(--fw-gray-800);
--hover-background-color:var(--fw-beige-200);
--hover-border-color:var(--fw-gray-800);
&.is-active,
&:active {
--fw-bg-color: var(--fw-blue-600);
&.router-link-exact-active {
--fw-bg-color: var(--fw-blue-700);
}
}
--active-color:var(--fw-red-40);
--active-background-color:var(--fw-beige-400);
--active-border-color:var(--fw-gray-600);
--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 {
--fw-bg-color: var(--fw-gray-200);
--fw-text-color: var(--fw-gray-900);
.primary {
--color: var(--fw-blue-010);
--background-color:var(--fw-blue-400);
--border-color:var(--fw-blue-010);
&.is-colored {
&[disabled] {
--fw-bg-color: var(--fw-gray-100) !important;
}
--hover-color: var(--fw-blue-010);
--hover-background-color:var(--fw-blue-500);
&.is-hovered,
&:hover {
--fw-bg-color: var(--fw-gray-200);
}
--active-color: var(--fw-blue-010);
--active-background-color:var(--fw-blue-600);
&.is-active,
&.active,
&:active {
--fw-bg-color: var(--fw-gray-300);
&.router-link-exact-active {
--fw-bg-color: var(--fw-gray-500);
}
}
--pressed-color:var(--fw-blue-010);
--pressed-background-color:var(--fw-blue-800);
--disabled-color:var(--fw-blue-900);
--disabled-background-color:var(--fw-blue-100);
--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 {
--fw-bg-color: var(--fw-red-400);
--fw-text-color: var(--fw-red-010);
.secondary, button {
--color: var(--fw-gray-700);
--background-color: var(--fw-gray-200);
--border-color:var(--fw-gray-700);
&.is-colored {
&[disabled] {
--fw-bg-color: var(--fw-red-100) !important;
--fw-text-color: var(--fw-blue-900) !important;
}
--hover-color:var(--fw-gray-800);
--hover-background-color:var(--fw-gray-300);
--hover-border-color:var(--fw-gray-800);
&.is-hovered,
&:hover {
--fw-bg-color: var(--fw-red-600);
}
--active-color:var(--fw-gray-970);
--active-background-color:var(--fw-gray-400);
--active-border-color:var(--fw-gray-400);
&.is-active,
&.active,
&:active {
--fw-bg-color: var(--fw-red-700);
&.router-link-exact-active {
--fw-bg-color: var(--fw-red-800);
}
}
--pressed-color:var(--fw-beige-200);
--pressed-background-color:var(--fw-gray-900);
--disabled-color:var(--fw-gray-500);
--disabled-background-color:var(--fw-gray-100);
--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 {
@ -209,7 +341,7 @@
&.is-colored {
&.is-hovered,
&:hover {
--fw-bg-color: var(--fw-gray-800);
--fw-bg-color: var(--fw-gray-950);
}
&.is-active,
@ -217,7 +349,7 @@
&:active {
--fw-bg-color: var(--fw-gray-900);
&.router-link-exact-active {
--fw-bg-color: var(--fw-gray-950);
--fw-bg-color: var(--fw-gray-850);
}
}
}
@ -252,6 +384,7 @@
}
}
.funkwhale {
@each $pastel in ("blue", "red", "green", "purple", "yellow") {
&.is-#{$pastel} {

View File

@ -3,6 +3,7 @@ import { ref, onMounted } from 'vue'
import { useUploadsStore } from '../stores/upload'
import { useI18n } from 'vue-i18n'
import { useStore } from '~/store'
import { color } from '~/composables/colors'
import Input from '~/components/ui/Input.vue'
import Link from '~/components/ui/Link.vue'
@ -23,7 +24,7 @@ const uploads = useUploadsStore()
</script>
<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']">
<Link to="/">
<img
@ -135,8 +136,6 @@ const uploads = useUploadsStore()
<style module lang="scss">
.sidebar {
background-color: var(--fw-bg-raised)
height: 100%;
display:flex;
flex-direction:column;

View File

@ -80,7 +80,7 @@ const currentFilter = ref(filterItems[0])
title="Upload music to library"
>
<template #alert="closeAlert">
<Alert>
<Alert yellow>
Before uploading, please ensure your files are tagged properly.
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.
::: details Colors
- Red
- Blue
- Purple
- Green
- Yellow
:::
### Blue
```vue-html
<Alert color="blue">
<Alert blue>
Blue alert
</Alert>
```
<Alert color="blue">
<Alert blue>
Blue alert
</Alert>
### Red
```vue-html
<Alert color="red">
<Alert red>
Red alert
</Alert>
```
<Alert color="red">
<Alert red>
Red alert
</Alert>
### Purple
```vue-html
<Alert color="purple">
<Alert purple>
Purple alert
</Alert>
```
<Alert color="purple">
Purple alert
<Alert purple>
Purple burglar alert
</Alert>
### Green
```vue-html
<Alert color="green">
<Alert green>
Green alert
</Alert>
```
<Alert color="green">
<Alert green>
Green alert
</Alert>
### Yellow
```vue-html
<Alert color="yellow">
<Alert yellow>
Yellow alert
</Alert>
```
<Alert color="yellow">
<Alert yellow>
Yellow alert
</Alert>
## Alert actions
```vue-html{2-4}
<Alert>
<Alert blue>
Awesome artist
<template #actions>
<Button disabled>Deny</Button>
<Button>Got it</Button>
</template>
</Alert>
```
<Alert>
<Alert blue>
Awesome artist
<template #actions>
<Button disabled>Deny</Button>
<Button>Got it</Button>
</template>
</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.
| 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 |
| `round` | Boolean | No | `false` | Whether to render the button as a round button |
| `icon` | String | No | | The icon attached to the button |
| `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 |
| `color` | `primary` \| `secondary` \| `destructive` | No | `primary` | Renders a colored button |
| Prop | Data type | Required? | Default | Description |
| -------------- | --------- | --------- | ------- | ---------------------------------------------- |
| `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 |
| `icon` | String | No | | The icon attached to the button |
| `aria-pressed` | Boolean | No | `false` | Whether the button is in an active state |
| `is-loading` | Boolean | No | `false` | Whether the button is in a loading state |
In addition, use [Colors] and [Variants]
## 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
<Button>
<Button primary>
Primary button
</Button>
```
<Button>
<Button primary>
Primary 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.
```vue-html
<Button color="secondary">
<Button secondary>
Secondary button
</Button>
```
<Button color="secondary">
<Button secondary>
Secondary 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.
```vue-html
<Button color="destructive">
<Button destructive>
Destructive button
</Button>
```
<Button color="destructive">
<Button destructive>
Destructive button
</Button>
@ -84,23 +84,31 @@ This is the default style. If you don't specify a style, a solid button is rende
<Button>
Filled button
</Button>
<Button solid>
Also filled button
</Button>
```
<Button>
Filled button
</Button>
<Button solid>
Also filled button
</Button>
### Outline
Outline buttons have a transparent background. Use these to deemphasize the action the button performs.
```vue-html
<Button variant="outline" color="secondary">
<Button outline secondary>
Outline button
</Button>
```
<Button variant="outline" color="secondary">
<Button outline secondary>
Outline 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.
```vue-html
<Button variant="ghost" color="secondary">
<Button ghost secondary>
Ghost button
</Button>
```
<Button variant="ghost" color="secondary">
<Button ghost secondary>
Ghost button
</Button>
@ -176,15 +184,35 @@ You can pass a state to indicate whether a user can interact with a button.
### 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
<Button is-active>
<Button aria-pressed>
Active 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
</Button>
@ -270,9 +298,9 @@ Icon buttons shrink down to the icon size if you don't pass any content. If you
</Button>
```
<Button color="secondary" icon="bi-three-dots-vertical" />
<Button color="secondary" round icon="bi-x" />
<Button icon="bi-save">&nbsp;</Button>
<Button color="destructive" icon="bi-trash">
<Button icon="bi-three-dots-vertical" />
<Button round icon="bi-x" />
<Button primary icon="bi-save">&nbsp;</Button>
<Button destructive icon="bi-trash">
Delete
</Button>

View File

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

View File

@ -70,7 +70,7 @@ Make sure to add `autofocus` to the preferred button.
Modal content
<template #actions>
<Button @click="isOpen = false" color="secondary">
<Button @click="isOpen = false">
Cancel
</Button>
@ -88,7 +88,7 @@ Make sure to add `autofocus` to the preferred button.
<Modal v-model="isOpen2" title="My modal">
Modal content
<template #actions>
<Button @click="isOpen2 = false" color="secondary">
<Button @click="isOpen2 = false">
Cancel
</Button>
<Button autofocus @click="isOpen2 = false">
@ -120,13 +120,13 @@ Note that confirmation dialogs interrupt the user's workflow. Consider adding a
:::
```vue-html
<Button @click="isOpen = true" color="destructive">
<Button @click="isOpen = true" destructive>
Delete my account ...
</Button>
<Modal v-model="isOpen" title="Delete account?">
<template #alert>
<Alert color="red">
<Alert red>
1 082 music files that you uploaded will be deleted.<br />
7 879 items in your collections will be unlinked.
</Alert>
@ -135,22 +135,22 @@ Do you want to delete your account forever?
You will not be able to restore your account.
<template #actions>
<Button autofocus @click="isOpen = false" color="secondary">
<Button autofocus @click="isOpen = false" >
Keep my account
</Button>
<Button color="destructive" @click="isOpen = false">
<Button destructive @click="isOpen = false">
I understand. Delete my account now!
</Button>
</template>
</Modal>
```
<Button @click="isOpen6 = true" color="destructive">
<Button @click="isOpen6 = true" destructive>
Delete my account ...
</Button>
<Modal v-model="isOpen6" title="Delete account?">
<template #alert>
<Alert color="red">
<Alert red>
1 082 music files that you uploaded will be deleted.<br />
7 879 items in your collections will be unlinked.
</Alert>
@ -159,10 +159,10 @@ Do you want to delete your account forever?
You will not be able to restore your account.
<template #actions>
<Button autofocus @click="isOpen6 = false" color="secondary">
<Button autofocus @click="isOpen6 = false">
Keep my account
</Button>
<Button color="destructive" @click="isOpen6 = false">
<Button destructive @click="isOpen6 = false">
I understand. Delete my account now!
</Button>
</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 content
<template #alert v-if="alertOpen">
<Alert>
<Alert blue>
Alert content
<template #actions>
<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">
Cancel
</Button>
<Button @click="isOpen3 = false">
<Button primary @click="isOpen3 = false">
Ok
</Button>
</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 |
## 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 pills convey **positive** information.
```vue-html
<Pill color="primary">
<Pill primary>
Primary pill
</Pill>
```
<Pill color="primary">
<Pill primary>
Primary 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.
```vue-html
<Pill color="destructive">
<Pill destructive>
Destructive pill
</Pill>
```
<Pill color="destructive">
<Pill destructive>
Destructive 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.
::: details Colors
- Red
- Blue
- Purple
- Green
- Yellow
:::
### Blue
```vue-html
<Pill color="blue">
<Pill blue>
Blue pill
</Pill>
```
<Pill color="blue">
<Pill blue>
Blue pill
</Pill>
### Red
```vue-html
<Pill color="red">
<Pill red>
Red pill
</Pill>
```
<Pill color="red">
<Pill red>
Red pill
</Pill>
### Purple
```vue-html
<Pill color="purple">
<Pill purple>
Purple pill
</Pill>
```
<Pill color="purple">
<Pill purple>
Purple pill
</Pill>
### Green
```vue-html
<Pill color="green">
<Pill green>
Green pill
</Pill>
```
<Pill color="green">
<Pill green>
Green pill
</Pill>
### Yellow
```vue-html
<Pill color="yellow">
<Pill yellow>
Yellow pill
</Pill>
```
<Pill color="yellow">
<Pill yellow>
Yellow 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
## 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>
<template>
<Alert />
<Alert yellow />
<Button />
</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"
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:
version "0.16.0"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.16.0.tgz#3240b891a78b0deae910dbeb86553e552a148860"