refactor(ui): add Slider component
This commit is contained in:
parent
a2d79aa6bc
commit
337373ff75
|
@ -0,0 +1,107 @@
|
|||
<script setup lang="ts" generic="T extends string">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
|
||||
import Layout from "~/components/ui/Layout.vue"
|
||||
import Spacer from "~/components/ui/Spacer.vue"
|
||||
|
||||
const props = defineProps<{
|
||||
label?: string,
|
||||
options: Record<T, string>,
|
||||
autofocus?: true
|
||||
}>();
|
||||
|
||||
const keys = computed(() => Object.keys(props.options) as T[])
|
||||
|
||||
const model = defineModel<T>({ required: true });
|
||||
|
||||
const index = computed({
|
||||
get() {
|
||||
return keys.value.indexOf(model.value)
|
||||
},
|
||||
set(newIndex) {
|
||||
console.log("NEW", newIndex)
|
||||
model.value = keys.value[newIndex]
|
||||
console.log("=", keys.value[newIndex])
|
||||
}
|
||||
})
|
||||
|
||||
const input = ref()
|
||||
|
||||
onMounted(() => {
|
||||
if (props.autofocus) input.value.focus();
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Layout stack no-gap :style="`
|
||||
--step-size: calc(100% / ${keys.length + 2});
|
||||
--slider-width: calc(var(--step-size) * ${keys.length - 1} + 16px);
|
||||
--current-step: ${index};
|
||||
`">
|
||||
|
||||
<!-- Label -->
|
||||
|
||||
<label v-if="$slots['label']" :class="$style.label">
|
||||
<slot name="label" />
|
||||
</label>
|
||||
<label v-if="label" :class="$style.label">
|
||||
{{ label }}
|
||||
</label>
|
||||
|
||||
<!-- Model indicator -->
|
||||
|
||||
<Layout flex no-gap>
|
||||
<button v-for="key in keys" :class="[$style.key, {[$style.current]: key===model}]"
|
||||
style="flex-basis:var(--step-size)"
|
||||
type="button"
|
||||
@click="() => { model = key; input.focus(); }"
|
||||
tabindex="-1"
|
||||
>{{ key }}</button>
|
||||
</Layout>
|
||||
<Spacer size-8 />
|
||||
|
||||
<!-- Slider -->
|
||||
|
||||
<input ref="input" type="range"
|
||||
style="width: var(--slider-width);"
|
||||
:max="keys.length - 1"
|
||||
v-model="index"
|
||||
/>
|
||||
<Spacer size-8/>
|
||||
|
||||
<!-- Description of current option -->
|
||||
<span style="position: relative;">
|
||||
<span style="display: inline-flex; margin-right: -100%; width: 100%; visibility: hidden;" >
|
||||
<span v-for="key in keys" :class="$style.description" :style="`margin-right: -20%; --current-step: 0; color: magenta;`">{{ options[key] }}</span>
|
||||
</span>
|
||||
<span style="position: absolute;" :class="$style.description">{{ options[model] }}</span>
|
||||
</span>
|
||||
</Layout>
|
||||
</template>
|
||||
|
||||
|
||||
<style module lang="scss">
|
||||
.label {
|
||||
margin-top: -18px;
|
||||
padding-bottom: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.key {
|
||||
all: unset;
|
||||
opacity: .7;
|
||||
&.current {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.description {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
width: 11rem;
|
||||
overflow: visible;
|
||||
transition: margin .2s;
|
||||
--inset: calc(var(--step-size) * var(--current-step));
|
||||
margin-left: var(--inset);
|
||||
margin-right: calc(0px - var(--inset));
|
||||
}
|
||||
</style>
|
|
@ -42,6 +42,7 @@ export default defineConfig({
|
|||
],
|
||||
},
|
||||
{ text: 'Input', link: '/components/ui/input' },
|
||||
{ text: 'Slider', link: '/components/ui/slider' },
|
||||
{ text: 'Popover (Dropdown Menu)', link: '/components/ui/popover' },
|
||||
{ text: 'Textarea', link: '/components/ui/textarea' },
|
||||
{ text: 'Toggle', link: '/components/ui/toggle' },
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
import Slider from '~/components/ui/Slider.vue'
|
||||
|
||||
const options = {
|
||||
me: "Only I can find and edit this",
|
||||
pod: "Me and other users on the instance can find and edit this",
|
||||
everyone: "Everyone can find and edit this"
|
||||
} as const
|
||||
|
||||
const option = ref<keyof typeof options>('me')
|
||||
</script>
|
||||
|
||||
```ts
|
||||
import Slider from "~/components/ui/Slider.vue";
|
||||
```
|
||||
|
||||
# Slider
|
||||
|
||||
Let a user select a value along a gradient.
|
||||
|
||||
The model (required) is the current value.
|
||||
|
||||
```ts
|
||||
const options = {
|
||||
me: "Only I can find and edit this",
|
||||
pod: "Me and other users on the instance can find and edit this",
|
||||
everyone: "Everyone can find and edit this",
|
||||
} as const;
|
||||
|
||||
const option = ref<keyof typeof options>("me");
|
||||
```
|
||||
|
||||
```vue-html
|
||||
<Slider :options="options" v-model="option" label="Privacy" />
|
||||
```
|
||||
|
||||
<Slider :options="options" v-model="option" label="Privacy" />
|
||||
|
||||
Functionality:
|
||||
|
||||
- Define a list of `[value:string, description:markdown]` pairs (props)
|
||||
- Select one value as active (model)
|
||||
|
||||
User interaction:
|
||||
|
||||
- It mimics the functionality of a single `range` input:
|
||||
- to be operated with arrow keys or mouse
|
||||
- focus is indicated
|
||||
- ticks are indicated
|
||||
|
||||
Design:
|
||||
|
||||
- A pin (same as in the toggle component)
|
||||
- a range (very faint)
|
||||
- Ticks
|
||||
|
||||
- Not to be confused with a pearls navigation patterns (list of dots; indicates temporal range)
|
Loading…
Reference in New Issue