Appearance
Appearance
We distinguish between components that are coupled with funkwhale-specific datatypes and "pure" user interface components:
Activity or AlbumCard import from types.ts.src/components/ui) are independent from Funkwhale. Think of Button, Tabs or LayoutJump to chapter:
First, import vue features and external libraries. Add the sub-components you want to use last. Order each block of imports by alphabet to prevent commit diff noise.
Add a blank line between Imports and script. Use modern typescript-friendly features such as defineModel and defineProps as documented in the Vue Docs instead of Macros.
If you are new to Vue, read the docs, especially the chapter about Single-File Components, to get familiar.
Don't pollute the global namespace. Funkwhale compiles a single stylesheet (used in the app, the blog and the website). If you need specific styles in your component, use vue's SFC features such as module. Vue will give you a $style object containing all locally defined classes.
<script setup>
import { ref } from "vue";
const theme = ref({
color: "red"
});
</script>
<style module>
.content {
color: v-bind("theme.color");
}
</style>
<template>
<div :class="$style.content"></div>
</template>We have enabled the vite feature css.devSourcemap: true so that in your browser devtools, you can trace the code responsible for module styles:
What about the global style?
As of now, class and variable names from the global styles are not available as typescript objects. We should definitely add this feature at some point.
<script setup>
import Alert from "~/components/ui/Alert.vue";
import Button from "~/components/ui/Button.vue";
</script>
<style module></style>
<template>
<Alert yellow />
<Button />
</template>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:
<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>// 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
);