feat(ui): add layout and spacing options
This commit is contained in:
parent
7f293d4143
commit
3865cbac93
|
@ -88,6 +88,7 @@ const isExternalLink = computed(() => {
|
||||||
|
|
||||||
&.is-category>.title {
|
&.is-category>.title {
|
||||||
font-size: 1.75em;
|
font-size: 1.75em;
|
||||||
|
padding-bottom: .25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
>.alert {
|
>.alert {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
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>()
|
const model = defineModel<string|number>()
|
||||||
|
|
||||||
|
@ -33,5 +33,9 @@ const input = ref()
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import './input.scss'
|
@import './input.scss';
|
||||||
|
|
||||||
|
input[type=number]::-webkit-inner-spin-button {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useCssModule } from 'vue'
|
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 classes = useCssModule()
|
||||||
const columnWidth = props.columnWidth ?? 320
|
const columnWidth = props.columnWidth ?? 320
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="[
|
<div :class="[
|
||||||
classes.gap,
|
classes.layout,
|
||||||
|
noGap || classes.gap,
|
||||||
props.grid ? classes.grid
|
props.grid ? classes.grid
|
||||||
: props.flex ? classes.flex
|
: props.flex ? classes.flex
|
||||||
: classes.stack
|
: classes.stack
|
||||||
|
@ -18,12 +19,22 @@ const columnWidth = props.columnWidth ?? 320
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style module>
|
<style module>
|
||||||
|
.layout{
|
||||||
|
transition:gap .15s;
|
||||||
/* Override --gap with your preferred value */
|
/* Override --gap with your preferred value */
|
||||||
.gap {
|
&.gap {
|
||||||
gap: var(--gap, 32px);
|
gap: var(--gap, 32px);
|
||||||
}
|
}
|
||||||
|
&:not(.gap) {
|
||||||
.grid {
|
gap: 0;
|
||||||
|
}
|
||||||
|
/* Growth */
|
||||||
|
&:has(:global(>.grow)){
|
||||||
|
>:not(:global(.grow)){
|
||||||
|
flex-grow:0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns:
|
grid-template-columns:
|
||||||
repeat(auto-fit, minmax(calc(v-bind(columnWidth) * 1px), min-content));
|
repeat(auto-fit, minmax(calc(v-bind(columnWidth) * 1px), min-content));
|
||||||
|
@ -67,14 +78,17 @@ const columnWidth = props.columnWidth ?? 320
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.stack {
|
&.stack {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
height:100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex {
|
&.flex {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
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, useColor } from '~/composables/colors';
|
||||||
const { to, icon, color } = defineProps<RouterLinkProps & ColorProps & {
|
const { to, icon, color, inline } = defineProps<RouterLinkProps & ColorProps & {
|
||||||
icon?: string;
|
icon?: string;
|
||||||
variant?: 'solid' | 'outline' | 'ghost'
|
inline?: true
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const colorClass = useColor(() => color)
|
const colorClass = useColor(() => color)
|
||||||
|
@ -15,10 +15,10 @@ const isExternalLink = computed(() => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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 />
|
<slot />
|
||||||
</a>
|
</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]" />
|
<i v-if="icon" :class="['bi', icon]" />
|
||||||
<slot />
|
<slot />
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
@ -27,12 +27,12 @@ const isExternalLink = computed(() => {
|
||||||
<style module lang="scss">
|
<style module lang="scss">
|
||||||
.active { outline: 3px solid red; }
|
.active { outline: 3px solid red; }
|
||||||
.external { outline: 3px dotted blue; }
|
.external { outline: 3px dotted blue; }
|
||||||
|
.inline { display:inline-flex; }
|
||||||
a {
|
a {
|
||||||
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);
|
||||||
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
@ -49,7 +49,7 @@ const isExternalLink = computed(() => {
|
||||||
margin: 0 0.5ch;
|
margin: 0 0.5ch;
|
||||||
|
|
||||||
transform: translateX(var(--fw-translate-x)) translateY(var(--fw-translate-y)) scale(var(--fw-scale));
|
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 {
|
i {
|
||||||
margin-right:1ch;
|
margin-right:1ch;
|
||||||
|
@ -61,10 +61,5 @@ const isExternalLink = computed(() => {
|
||||||
background-color:transparent;
|
background-color:transparent;
|
||||||
border-color:transparent;
|
border-color:transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
transition:background-color .3s, border-color .2s;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -74,6 +74,7 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
writing-mode: horizontal-tb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prefix {
|
.prefix {
|
||||||
|
|
|
@ -1,18 +1,54 @@
|
||||||
<script setup lang="ts">
|
<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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="classes.spacer">
|
<div :class="[$style.spacer, grow && 'grow', title && $style['has-title']]">
|
||||||
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style module>
|
<style module lang="scss">
|
||||||
.spacer {
|
.spacer {
|
||||||
--size: var(--size, 32px);
|
width: v-bind('measure.size');
|
||||||
width: var(--size);
|
height: v-bind('measure.size');
|
||||||
height: var(--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>
|
</style>
|
||||||
|
|
|
@ -7,9 +7,7 @@ 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.
|
Inputs are areas in which users can enter information. In Funkwhale, these mostly take the form of search fields.
|
||||||
|
|
||||||
| Prop | Data type | Required? | Description |
|
| Prop | Data type | Required? | Description |
|
||||||
| ---- | --------- | --------- | ----------- |
|
| --------------- | --------- | --------- | --------------------------------------------------------------------------- |
|
||||||
|
|
||||||
|
|
|
||||||
| `placeholder` | String | No | The placeholder text that appears when the input is empty. |
|
| `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. |
|
| `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. |
|
| `v-model:value` | String | Yes | The text entered in the input. |
|
||||||
|
|
|
@ -2,8 +2,14 @@
|
||||||
import Card from '~/components/ui/Card.vue'
|
import Card from '~/components/ui/Card.vue'
|
||||||
import Alert from '~/components/ui/Alert.vue'
|
import Alert from '~/components/ui/Alert.vue'
|
||||||
import Layout from '~/components/ui/Layout.vue'
|
import Layout from '~/components/ui/Layout.vue'
|
||||||
|
import Spacer from '~/components/ui/layout/Spacer.vue'
|
||||||
import Tab from '~/components/ui/Tab.vue'
|
import Tab from '~/components/ui/Tab.vue'
|
||||||
import Tabs from '~/components/ui/Tabs.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>
|
</script>
|
||||||
|
|
||||||
# Layout
|
# Layout
|
||||||
|
@ -145,3 +151,129 @@ Add space between vertically stacked items
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
</Tabs>
|
</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>
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
import Alert from '~/components/ui/Alert.vue'
|
import Alert from '~/components/ui/Alert.vue'
|
||||||
import Layout from '~/components/ui/Layout.vue'
|
import Layout from '~/components/ui/Layout.vue'
|
||||||
import Spacer from '~/components/ui/layout/Spacer.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>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -73,3 +78,80 @@ Add a 16px gap between adjacent items.
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</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>
|
||||||
|
|
Loading…
Reference in New Issue