feat(ui): add layout and spacing options

This commit is contained in:
upsiflu 2024-12-15 15:06:27 +01:00
parent 7f293d4143
commit 3865cbac93
9 changed files with 346 additions and 83 deletions

View File

@ -88,6 +88,7 @@ const isExternalLink = computed(() => {
&.is-category>.title {
font-size: 1.75em;
padding-bottom: .25em;
}
>.alert {

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref } from 'vue'
const { icon, placeholder } = defineProps<{ icon?: string, placeholder:string }>()
const { icon, placeholder } = defineProps<{ icon?: string, placeholder?:string }>()
const model = defineModel<string|number>()
@ -33,5 +33,9 @@ const input = ref()
</template>
<style lang="scss">
@import './input.scss'
@import './input.scss';
input[type=number]::-webkit-inner-spin-button {
opacity: 1;
}
</style>

View File

@ -1,14 +1,15 @@
<script setup lang="ts">
import { useCssModule } from 'vue'
const props = defineProps<{ [P in "stack" | "grid" | "flex"]?: true } & { columnWidth?: number }>()
const props = defineProps<{ [P in "stack" | "grid" | "flex"]?: true } & { columnWidth?: number, noGap?:true }>()
const classes = useCssModule()
const columnWidth = props.columnWidth ?? 320
</script>
<template>
<div :class="[
classes.gap,
classes.layout,
noGap || classes.gap,
props.grid ? classes.grid
: props.flex ? classes.flex
: classes.stack
@ -18,63 +19,76 @@ const columnWidth = props.columnWidth ?? 320
</template>
<style module>
/* Override --gap with your preferred value */
.gap {
gap: var(--gap, 32px);
}
.layout{
transition:gap .15s;
/* Override --gap with your preferred value */
&.gap {
gap: var(--gap, 32px);
}
&:not(.gap) {
gap: 0;
}
/* Growth */
&:has(:global(>.grow)){
>:not(:global(.grow)){
flex-grow:0;
}
}
&.grid {
display: grid;
grid-template-columns:
repeat(auto-fit, minmax(calc(v-bind(columnWidth) * 1px), min-content));
grid-auto-flow: row dense;
.grid {
display: grid;
grid-template-columns:
repeat(auto-fit, minmax(calc(v-bind(columnWidth) * 1px), min-content));
grid-auto-flow: row dense;
:global(>.span-2-rows) {
grid-row: span 2;
height: auto;
--height: auto;
}
:global(>.span-2-rows) {
grid-row: span 2;
height: auto;
--height: auto;
:global(>.span-3-rows) {
grid-row: span 3;
height: auto;
--height: auto;
}
:global(>.span-4-rows) {
grid-row: span 4;
height: auto;
--height: auto;
}
:global(>.span-2-columns) {
grid-column: span 2;
width: auto;
--width: auto;
}
:global(>.span-3-columns) {
grid-column: span 3;
width: auto;
--width: auto;
}
:global(>.span-4-columns) {
grid-column: span 4;
width: auto;
--width: auto;
}
}
:global(>.span-3-rows) {
grid-row: span 3;
height: auto;
--height: auto;
&.stack {
display: flex;
flex-direction: column;
height:100%;
}
:global(>.span-4-rows) {
grid-row: span 4;
height: auto;
--height: auto;
&.flex {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
:global(>.span-2-columns) {
grid-column: span 2;
width: auto;
--width: auto;
}
:global(>.span-3-columns) {
grid-column: span 3;
width: auto;
--width: auto;
}
:global(>.span-4-columns) {
grid-column: span 4;
width: auto;
--width: auto;
}
}
.stack {
display: flex;
flex-direction: column;
}
.flex {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
</style>

View File

@ -2,9 +2,9 @@
import { computed } from 'vue';
import { type RouterLinkProps, RouterLink } from 'vue-router';
import { type ColorProps, useColor } from '~/composables/colors';
const { to, icon, color } = defineProps<RouterLinkProps & ColorProps & {
const { to, icon, color, inline } = defineProps<RouterLinkProps & ColorProps & {
icon?: string;
variant?: 'solid' | 'outline' | 'ghost'
inline?: true
}>()
const colorClass = useColor(() => color)
@ -15,10 +15,10 @@ const isExternalLink = computed(() => {
</script>
<template>
<a v-if="isExternalLink" :class="[$style.external, colorClass, 'is-colored']" :href="to?.toString()" target="_blank">
<a v-if="isExternalLink" :class="[$style.external, colorClass, color && 'is-colored', inline && $style.inline]" :href="to?.toString()" target="_blank">
<slot />
</a>
<RouterLink v-if="to && !isExternalLink" :to="to" :class="[colorClass, 'is-colored']">
<RouterLink v-if="to && !isExternalLink" :to="to" :class="[colorClass, color && 'is-colored', inline && $style.inline]">
<i v-if="icon" :class="['bi', icon]" />
<slot />
</RouterLink>
@ -27,12 +27,12 @@ const isExternalLink = computed(() => {
<style module lang="scss">
.active { outline: 3px solid red; }
.external { outline: 3px dotted blue; }
.inline { display:inline-flex; }
a {
background-color: var(--fw-bg-color);
color: var(--fw-text-color);
border: 1px solid var(--fw-bg-color);
position: relative;
display: flex;
align-items: center;
white-space: nowrap;
@ -49,7 +49,7 @@ const isExternalLink = computed(() => {
margin: 0 0.5ch;
transform: translateX(var(--fw-translate-x)) translateY(var(--fw-translate-y)) scale(var(--fw-scale));
transition: all .2s ease;
transition:background-color .3s, border-color .2s;
i {
margin-right:1ch;
@ -61,10 +61,5 @@ const isExternalLink = computed(() => {
background-color:transparent;
border-color:transparent;
}
transition:background-color .3s, border-color .2s;
}
</style>

View File

@ -74,6 +74,7 @@
align-items: center;
font-size: 14px;
pointer-events: none;
writing-mode: horizontal-tb;
}
.prefix {

View File

@ -1,18 +1,54 @@
<script setup lang="ts">
import { useCssModule } from 'vue'
import { ref, watchEffect } from 'vue';
const classes = useCssModule()
const { grow, shrink, title, size = 16 } = defineProps<{ grow?:true, shrink?:true, title?:string, size?:number }>()
const minSize = 32
const measure = ref()
watchEffect(() => { measure.value = {
size: `${Math.max(size, minSize)}px`,
margin: `${Math.min(size/2-minSize/2, 0)}px`
}
})
</script>
<template>
<div :class="classes.spacer"> 
<div :class="[$style.spacer, grow && 'grow', title && $style['has-title']]">
<slot />
</div>
</template>
<style module>
<style module lang="scss">
.spacer {
--size: var(--size, 32px);
width: var(--size);
height: var(--size);
width: v-bind('measure.size');
height: v-bind('measure.size');
margin: v-bind('measure.margin');
flex-grow:v-bind('grow ? 1 : 0');
flex-shrink:v-bind('shrink ? 1 : 0');
transition:flex-grow .2s, flex-shrink .2s;
position: relative;
&.has-title::after {
position:absolute;
inset:calc(50% - 1em);
content:v-bind('`"${title}"`')
}
@if $docs {
animation: blink .7s 1;
@keyframes blink { 50% {
outline: 2px dashed var(--fw-secondary);
outline-offset: v-bind('measure.margin');
} }
&:hover {
animation-iteration-count: infinite;
}
}
@else {
pointer-events: none;
}
}
</style>

View File

@ -6,13 +6,11 @@ import Input from "~/components/ui/Input.vue"
Inputs are areas in which users can enter information. In Funkwhale, these mostly take the form of search fields.
| Prop | Data type | Required? | Description |
| ---- | --------- | --------- | ----------- |
|
| `placeholder` | String | No | The placeholder text that appears when the input is empty. |
| `icon` | String | No | The [Bootstrap icon](https://icons.getbootstrap.com/) to show on the input. |
| `v-model:value` | String | Yes | The text entered in the input. |
| Prop | Data type | Required? | Description |
| --------------- | --------- | --------- | --------------------------------------------------------------------------- |
| `placeholder` | String | No | The placeholder text that appears when the input is empty. |
| `icon` | String | No | The [Bootstrap icon](https://icons.getbootstrap.com/) to show on the input. |
| `v-model:value` | String | Yes | The text entered in the input. |
## Input model

View File

@ -2,8 +2,14 @@
import Card from '~/components/ui/Card.vue'
import Alert from '~/components/ui/Alert.vue'
import Layout from '~/components/ui/Layout.vue'
import Spacer from '~/components/ui/layout/Spacer.vue'
import Tab from '~/components/ui/Tab.vue'
import Tabs from '~/components/ui/Tabs.vue'
import Toggle from '~/components/ui/Toggle.vue'
import { ref } from 'vue'
const isGrowing = ref(true)
const noGap = ref(true)
</script>
# Layout
@ -145,3 +151,129 @@ Add space between vertically stacked items
</Tab>
</Tabs>
## Common props
### `no-gap`: Remove the gap between items
<Layout flex>
```vue
<script setup>
const noGap = ref(true);
</script>
<template>
<Toggle v-model="noGap" />
<Layout flex :no-gap="noGap || undefined">
<Card title="A" style="width:100px; min-width:100px" />
<Card title="B" />
<Card title="C" style="width:100px; min-width:100px" />
<Card title="D" />
</Layout>
</template>
```
<div class="preview">
<Toggle v-model="noGap" /> {{ noGap ? 'no-gap' : '-' }}
---
<Layout flex :no-gap="noGap || undefined">
<Card title="A" style="width:100px; min-width:100px" />
<Card title="B" />
<Card title="C" style="width:100px; min-width:100px" />
<Card title="D" />
</Layout>
</div>
</Layout>
### Add fixed or flexible Spacers
If you add a spacer with attribute `grow`, it will push the other item until the Layout fills the available space. This only works if the parent element itself grows beyond its minimal contents.
<Layout flex>
```vue
<script setup>
const isGrowing = ref(true);
</script>
<template>
<Toggle v-model="isGrowing" />
<div style="height:30em; /* Space to grow into */">
<Layout stack>
<Alert>A</Alert>
<Alert>B</Alert>
<Spacer :grow="isGrowing || undefined" />
<Alert>C (footer)</Alert>
</Layout>
</div>
</template>
```
<div class="preview">
<Toggle v-model="isGrowing" /> {{ isGrowing ? 'grow' : '-' }}
---
<div style="height:30em; background:rgba(255,250,20,.3)">
<Layout stack>
<Alert>A</Alert>
<Alert>B</Alert>
<Spacer :grow="isGrowing || undefined" />
<Alert>C (footer)</Alert>
</Layout>
</div>
</div>
</Layout>
Multiple spacers will distribute their growth evenly.
Note that you can set the minimum space occupied by the `Spacer` with its `size` prop [(docs)](layout/spacer). Negative values can offset the gap of the `Layout`:
<Layout flex>
```vue
<script setup>
const isGrowing = ref(true);
</script>
<template>
<Toggle v-model="isGrowing" />
<div style="height:20em; /* Space to grow into */">
<Layout>
<Alert>A</Alert>
<Spacer :size="-32" :grow="isGrowing || undefined" />
<Alert>B1</Alert>
<Alert>B2</Alert>
<Spacer :size="-32" :grow="isGrowing || undefined" />
<Alert>C (footer)</Alert>
</Layout>
</div>
</template>
```
<div class="preview" style="width:0">
<Toggle v-model="isGrowing" />
---
<div style="height:20em;background:rgba(255,250,20,.3)">
<Layout>
<Alert>A</Alert>
<Spacer :size="-32" :grow="isGrowing || undefined" />
<Alert>B1</Alert>
<Alert>B2</Alert>
<Spacer :size="-32" :grow="isGrowing || undefined" />
<Alert>C (footer)</Alert>
</Layout>
</div>
</div>
</Layout>

View File

@ -1,7 +1,12 @@
<script setup lang="ts">
import { ref } from 'vue';
import Alert from '~/components/ui/Alert.vue'
import Layout from '~/components/ui/Layout.vue'
import Spacer from '~/components/ui/layout/Spacer.vue'
import Input from '~/components/ui/Input.vue'
import Card from '~/components/ui/Card.vue'
const size = ref(32)
</script>
<style>
@ -57,10 +62,10 @@ Add a 16px gap between adjacent items.
```vue-html{4}
<Layout flex>
<Alert color="blue">A</Alert>
<Alert color="green">A</Alert>
<Spacer/>
<Alert color="red">B</Alert>
<Alert color="blue">A</Alert>
<Alert color="green">A</Alert>
<Spacer/>
<Alert color="red">B</Alert>
</Layout>
```
@ -73,3 +78,80 @@ Add a 16px gap between adjacent items.
</Layout>
</div>
</Layout>
## Modify the size of a Spacer
<Layout flex>
```vue
<script setup>
const size = ref(1);
</script>
<template>
<Input v-model="size" type="range" />
<Alert color="blue">A</Alert>
<Alert color="green">A</Alert>
<Spacer :size="size" />
<Alert color="red">B</Alert>
</template>
```
<div class="preview">
<Input v-model="size" type="range" style="writing-mode: vertical-lr; direction: rtl"/>
{{ size }}px
</div>
<div class="preview">
<Alert color="blue">A</Alert>
<Spacer :size="size" />
<Alert color="red">B</Alert>
</div>
</Layout>
## Make the Spacer elastic
<Layout flex>
```vue-html
<Layout flex style="height:30em;">
<Input v-model="size"
type="range"
style="writing-mode: vertical-lr; height:100%"
/>
<Layout stack no-gap
:style="{ height: size + '%' }"
>
<Alert>A</Alert>
<Spacer grow title="grow" />
<Alert>B</Alert>
<Alert>C</Alert>
<Spacer shrink title="shrink" />
<Alert>D</Alert>
<Spacer grow shrink title="grow shrink" />
<Alert>E</Alert>
</Layout>
</Layout>
```
<div class="preview" style="flex-wrap:no-wrap">
<Layout flex style="height:30em">
<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>
<Spacer grow title="grow" />
<Alert>B</Alert>
<Alert>C</Alert>
<Spacer shrink title="shrink" />
<Alert>D</Alert>
<Spacer grow shrink title="grow shrink" />
<Alert>E</Alert>
</Layout>
</Layout>
</div>
</Layout>