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: 'Input', link: '/components/ui/input' },
|
||||||
|
{ text: 'Slider', link: '/components/ui/slider' },
|
||||||
{ text: 'Popover (Dropdown Menu)', link: '/components/ui/popover' },
|
{ text: 'Popover (Dropdown Menu)', link: '/components/ui/popover' },
|
||||||
{ text: 'Textarea', link: '/components/ui/textarea' },
|
{ text: 'Textarea', link: '/components/ui/textarea' },
|
||||||
{ text: 'Toggle', link: '/components/ui/toggle' },
|
{ 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