refactor(ui): [WIP] allow user to add, edit and remove custom pills

This commit is contained in:
upsiflu 2025-01-29 09:52:52 +01:00
parent 97ff6c56aa
commit a54a80fd02
4 changed files with 180 additions and 50 deletions

View File

@ -8,24 +8,34 @@ const handleClick = (event: MouseEvent) => {
emit('click', event)
}
const props = defineProps<{ noUnderline?:true } & (PastelProps | ColorProps) & VariantProps & RaisedProps>()
const model = defineModel<string>()
</script>
<template>
<button
<component
class="funkwhale pill"
:class="props.noUnderline && 'no-underline'"
:is="model ? 'label' : 'button'"
v-bind="color(props, ['interactive', 'outline'])()"
@click.stop="handleClick"
>
<div v-if="!!$slots.image" class="pill-image">
<slot name="image" />
</div>
<div class="pill-content">
<input v-if="model" class="pill-content" v-model="model" style="border-radius:16px; min-width: max-content; width:min-content;" />
<div class="pill-content" v-if="!!$slots.default">
<slot />
</div>
</button>
</component>
</template>
<style lang="scss">
@import './pill.scss';
label.funkwhale.pill.pill {
height: 28px;
>input {
// Note that <Input>s can't be styled with `min-width` or `width` directly.
width: 120px;
}
}
</style>

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, ref, watch, watchEffect } from 'vue'
import { ref, watchEffect, computed } from 'vue'
import { color } from '~/composables/color';
@ -10,23 +10,27 @@ const props = defineProps<{
icon?: string,
placeholder?: string,
label?: string,
customOptions?: true,
autofocus?: boolean,
}>();
const customOptions = ref<string[]>([]);
const model = defineModel<{
current : string[],
others?: string[]
}>({required:true})
current: string[],
others?: string[],
custom?: string[],
}>({ required: true });
const whenInteractive = (then:() => void) => {
if(!model.value.others) return; then();
}
const editingValue = ref('      ');
const isCustom = (value:string) =>
model.value.current.includes(value)
const selectedLabel = ref("+");
whenInteractive(()=>
watchEffect(() => {
if (!model.value.others) return
@ -39,14 +43,19 @@ whenInteractive(()=>
}
})
)
const deselectPill = (value:string) =>
whenInteractive(()=>
model.value =
{...model.value,
current : model.value.current.filter(v=>v!==value),
others : [value, ...(model.value.others || [])]
}
)
const pillClicked = (value: string) =>
model.value.custom?.includes(value) ? edit(value) : unselect(value)
const unselect = (value: string) =>
whenInteractive(() => model.value =
{...model.value,
current : model.value.current.filter(v=>v!==value),
others : [value, ...(model.value.others || [])]
}
)
const edit = (value: string) =>
whenInteractive(()=> editingValue.value = value)
</script>
@ -67,23 +76,8 @@ const deselectPill = (value:string) =>
v-bind="color({}, ['solid', 'default', 'secondary'])()"
:class="$style.list"
>
<Pill outline raised
v-for="value in model.current"
v-if="!model.others"
:class="$style.pill"
>
<span>{{ value }}</span>
</Pill>
<Pill outline raised
no-underline @click="deselectPill(value)"
v-for="value in model.current"
v-if="model.others"
:class="$style.pill"
>
<span :class="$style['pill-content']">{{ value }}</span>   ×
</Pill>
<!-- Add more pills -->
<!-- Add predefined or previously unselected pill -->
<select
v-if="model.others"
@ -97,6 +91,45 @@ const deselectPill = (value:string) =>
{{ value }}
</option>
</select>
<!-- Static pills -->
<Pill outline raised
v-if="!model.others"
v-for="value in model.current"
:class="$style.pill"
>
<span>{{ value }}</span>
</Pill>
<!-- Dynamic pills -->
<template v-for="value in model.current">
<Pill outline raised no-underline
v-if="value !== editingValue"
@click="pillClicked(value)"
:class="$style.pill"
>
<span :class="$style['pill-content']">{{ value }}</span>
{{ isCustom(value) ? '   ×' : ''}}
</Pill>
<Pill outline raised no-underline
v-else
v-model="editingValue"
@click="pillClicked(value)"
:class="$style.pill"
/>
</template>
<!-- Add custom pill -->
<Pill outline raised no-underline
v-if="model.custom"
:class="$style.pill"
v-model="editingValue"
style="margin-right: 40px; height:32px;"
>
</Pill>
</Layout>
</Layout>
</template>
@ -110,6 +143,8 @@ const deselectPill = (value:string) =>
font-weight: 600;
}
>.list {
position: relative;
padding:4px;
border-radius: 24px;
@ -124,9 +159,11 @@ const deselectPill = (value:string) =>
padding: 2px;
}
>.dropdown{
position: absolute;
inset: 0;
border-radius: 15px;
padding: 2px 11.25px;
flex-grow: 1;
text-align: right;
background: transparent;
appearance: auto;
@ -137,10 +174,10 @@ const deselectPill = (value:string) =>
color: inherit;
}
}
&:hover>.list {
&:hover:has(select)>.list {
box-shadow: inset 0 0 0 4px var(--border-color)
}
:has(>.dropdown:focus) {
:has(>select:focus) {
box-shadow: inset 0 0 0 4px var(--focus-ring-color)
}
}

View File

@ -1,16 +1,22 @@
<script setup>
import { computed, ref } from 'vue'
import Pill from '~/components/ui/Pill.vue'
import Button from '~/components/ui/Button.vue'
const customTag=ref('Custom Tag')
const isDeleted = computed(() => customTag.value==='' ? 'Pill was deleted' : '')
</script>
```ts
import Pill from "~/components/ui/Pill.vue"
import Pill from "~/components/ui/Pill.vue";
```
# Pill
Pills are decorative elements that display information about content they attach to. They can be links to other content or simple colored labels.
You can add text to pills by adding it between the `<Pill>` tags.
You can add text to pills by adding it between the `<Pill>` tags. Alternatively, you can set `v-model` and [make the pill editable](#editable-pill).
| Prop | Data type | Required? | Default | Description |
| ------- | ----------------------------------------------------------------------------------------------- | --------- | ----------- | ---------------------- |
@ -128,14 +134,14 @@ Funkwhale pills support a range of pastel colors to create visually appealing in
Yellow pill
</Pill>
## Image pills
## Image pill
Image pills contain a small circular image on their left. These can be used for decorative links such as artist links. To created an image pill, insert a link to the image between the pill tags as a `<template>`.
```vue-html{2-4}
<Pill>
<template #image>
<img src="/images/awesome-artist.png" />
<div style="background-color: #0004" />
</template>
Awesome artist
</Pill>
@ -147,3 +153,27 @@ Image pills contain a small circular image on their left. These can be used for
</template>
Awesome artist
</Pill>
## Editable pill
Add `v-model="..."` to link the pill content to a `ref`. Note that the pill is not rendered if v-model is ''.
```ts
import { computed, ref } from "vue";
const customTag = ref(" ");
const isDeleted = computed(() => customTag.value === "");
```
```vue-html
<Pill v-model="customTag" />
<Button primary v-if="isDeleted" :onClick="() => customTag=' '">New Pill</Button>
```
<Pill v-model="customTag">
<template #image>
<div style="background-color: #0004" />
</template>
</Pill>
<Button primary v-if="isDeleted" :onClick="() => customTag=' '">New Pill</Button>

View File

@ -7,25 +7,63 @@ import Input from '~/components/ui/Input.vue';
const nullModel = ref({
current: [],
others: []
});
const staticModel = ref({
current: ["#Noise", "#FieldRecording", "#Experiment"]
current: ["#Noise", "#FieldRecording", "#Experiment"],
});
const interactiveModel = ref({
current: ["#Noise", "#FieldRecording", "#Experiment"],
others: ["#Melody", "#Rhythm"]
others: ["#Melody", "#Rhythm"],
});
const customModel = ref({
current: ["#Noise", "#FieldRecording", "#Experiment"],
others: ["#Melody", "#Rhythm"],
custom: ["#Noise"],
});
</script>
```ts
import Pills from "~/components/ui/Pills.vue"
import Pills from "~/components/ui/Pills.vue";
```
# Pills
Show a dense list of pills representing tags, categories or options.
Users can select a subset of given options and create new ones.
The model you provide will be mutated by this component:
- `current`: these pills are currently selected
- `others`: these pills are currently not selected (but can be selected by the user). This prop is optional. By adding it, you allow users to change the selection.
- `custom`: these pills were created by the user. This prop is optional. Users can edit, add and remove any pill defined in this array. Note that the `custom` array should only contain pills that are either in `current` or in `others`.
## No pills
```ts
const nullModel = ref({
current: [],
});
```
```vue-html
<Pills v-model="nullModel" />
```
<Layout class="preview" style="padding:16px">
<Pills v-model="nullModel" />
</Layout>
## Predefined list of pills
```ts
const staticModel = ref({
current: ["#Noise", "#FieldRecording", "#Experiment"],
});
```
```vue-html
<Pills v-model="staticModel" label="Tags" />
```
@ -34,24 +72,39 @@ import Pills from "~/components/ui/Pills.vue"
<Pills v-model="staticModel" label="Tags" />
</Layout>
## Let users select and unselect pills
Select a set of pills from presets, and add and remove custom ones
```ts
const interactiveModel = ref({
current: ["#Noise", "#FieldRecording", "#Experiment"],
others: ["#Melody", "#Rhythm"],
});
```
```vue-html
<Pills v-model="interactiveModel" label="Tags" />
<Input label="Label" placeholder="Placeholder"></Input>
```
<Layout class="preview" style="padding:16px">
<Pills v-model="interactiveModel" label="Tags" />
<Input label="Label" placeholder="Placeholder"></Input>
</Layout>
## No pills
## Let users add, remove and edit custom pills
```ts
const customModel = ref({
current: ["#Noise", "#FieldRecording", "#Experiment"],
others: ["#Melody", "#Rhythm"],
custom: ["#Noise"],
});
```
```vue-html
<Pills v-model="nullModel" />
<Pills v-model="customModel" label="Custom Tags" can-add-pills />
```
<Layout class="preview" style="padding:16px">
<Pills v-model="nullModel" />
<Pills v-model="customModel" label="Custom Tags" />
</Layout>