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 {
|
||||
font-size: 1.75em;
|
||||
padding-bottom: .25em;
|
||||
}
|
||||
|
||||
>.alert {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -74,6 +74,7 @@
|
|||
align-items: center;
|
||||
font-size: 14px;
|
||||
pointer-events: none;
|
||||
writing-mode: horizontal-tb;
|
||||
}
|
||||
|
||||
.prefix {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue