mirror of
https://github.com/sysadminsmedia/homebox.git
synced 2025-12-21 21:33:02 +01:00
* feat: migrate tools page and label generator to shadcn * chore: lint issues * feat: also do profile page * feat: shadcn 404 page * feat: login page shadcn * fix: daisyui ironically breaks the z height for the login page * feat: componentise the language selector and add it to the login page * feat: use nuxtlink * feat: card and table made more shadcn * feat: shadcn statscard * chore: lint * feat: shadcn labelchip and locationcard * feat: shadcn locations page * refactor: remove unused new item page * chore: lint * feat: shadcn item card * fix: wrapping of location and lint * feat: ctrl enter in text area in form submits form * feat: begin shadcn locations page and remove pageqrcode comp in favour of integrating it into labelmaker * chore: lint + remove unused code * fix: remove uneeded margin * feat: shadcn labels page and fix some issues with location * feat: shadcn scanner * chore: lint * feat: begin shadcning item pages * feat: shadcn maintenance page * feat: begin shadcn search page * fix: quick switch blurry text and crashing page when switching + incorrect z height for create menu * feat: finish shadcn search page * chore: lint * feat: shadcn edit item page * fix: quickmenumodal bug * feat: shadcn item details page * feat: remove all non-color related daisyui classes * fix: type error * fix: quick menu modal again :(
140 lines
4.3 KiB
Vue
140 lines
4.3 KiB
Vue
<template>
|
|
<div class="flex flex-col gap-1">
|
|
<Label :for="id" class="px-1">
|
|
{{ label }}
|
|
</Label>
|
|
<Popover v-model:open="open">
|
|
<PopoverTrigger as-child>
|
|
<Button :id="id" variant="outline" role="combobox" :aria-expanded="open" class="w-full justify-between">
|
|
<span>
|
|
<slot name="display" v-bind="{ item: value }">
|
|
{{ displayValue(value) || placeholder }}
|
|
</slot>
|
|
</span>
|
|
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent class="w-[--reka-popper-anchor-width] p-0">
|
|
<Command :ignore-filter="true">
|
|
<CommandInput v-model="search" :placeholder="searchPlaceholder" :display-value="_ => ''" />
|
|
<CommandEmpty>
|
|
{{ noResultsText }}
|
|
</CommandEmpty>
|
|
<CommandList>
|
|
<CommandGroup>
|
|
<CommandItem v-for="item in filtered" :key="itemKey(item)" :value="itemKey(item)" @select="select(item)">
|
|
<Check :class="cn('mr-2 h-4 w-4', isSelected(item) ? 'opacity-100' : 'opacity-0')" />
|
|
<slot name="display" v-bind="{ item }">
|
|
{{ displayValue(item) }}
|
|
</slot>
|
|
</CommandItem>
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, watch } from "vue";
|
|
import { Check, ChevronsUpDown } from "lucide-vue-next";
|
|
import fuzzysort from "fuzzysort";
|
|
import { useVModel } from "@vueuse/core";
|
|
import { Button } from "~/components/ui/button";
|
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "~/components/ui/command";
|
|
import { Label } from "~/components/ui/label";
|
|
import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover";
|
|
import { cn } from "~/lib/utils";
|
|
import { useId } from "#imports";
|
|
|
|
type ItemsObject = {
|
|
[key: string]: unknown;
|
|
};
|
|
|
|
interface Props {
|
|
label: string;
|
|
modelValue: string | ItemsObject | null | undefined;
|
|
items: ItemsObject[] | string[];
|
|
itemText?: string;
|
|
itemValue?: string;
|
|
search?: string;
|
|
searchPlaceholder?: string;
|
|
noResultsText?: string;
|
|
placeholder?: string;
|
|
}
|
|
|
|
const emit = defineEmits(["update:modelValue", "update:search"]);
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
label: "",
|
|
modelValue: "",
|
|
items: () => [],
|
|
itemText: "text",
|
|
itemValue: "value",
|
|
search: "",
|
|
searchPlaceholder: "Type to search...",
|
|
noResultsText: "No Results Found",
|
|
placeholder: "Select...",
|
|
});
|
|
|
|
const id = useId();
|
|
const open = ref(false);
|
|
const search = ref(props.search);
|
|
const value = useVModel(props, "modelValue", emit);
|
|
|
|
watch(
|
|
() => props.search,
|
|
val => {
|
|
search.value = val;
|
|
}
|
|
);
|
|
|
|
watch(
|
|
() => search.value,
|
|
val => {
|
|
emit("update:search", val);
|
|
}
|
|
);
|
|
|
|
function isStrings(arr: string[] | ItemsObject[]): arr is string[] {
|
|
return typeof arr[0] === "string";
|
|
}
|
|
|
|
function displayValue(item: string | ItemsObject | null | undefined): string {
|
|
if (!item) return "";
|
|
if (typeof item === "string") return item;
|
|
return (item[props.itemText] as string) || "";
|
|
}
|
|
|
|
function itemKey(item: string | ItemsObject): string {
|
|
if (typeof item === "string") return item;
|
|
return (item[props.itemValue] as string) || displayValue(item);
|
|
}
|
|
|
|
function isSelected(item: string | ItemsObject): boolean {
|
|
if (!value.value) return false;
|
|
if (typeof item === "string") return value.value === item;
|
|
if (typeof value.value === "string") return itemKey(item) === value.value;
|
|
return itemKey(item) === itemKey(value.value);
|
|
}
|
|
|
|
function select(item: string | ItemsObject) {
|
|
if (isSelected(item)) {
|
|
value.value = null;
|
|
} else {
|
|
value.value = item;
|
|
}
|
|
open.value = false;
|
|
}
|
|
|
|
const filtered = computed(() => {
|
|
if (!search.value) return props.items;
|
|
if (isStrings(props.items)) {
|
|
return props.items.filter(item => item.toLowerCase().includes(search.value.toLowerCase()));
|
|
} else {
|
|
// Fuzzy search on itemText
|
|
return fuzzysort.go(search.value, props.items, { key: props.itemText, all: true }).map(i => i.obj);
|
|
}
|
|
});
|
|
</script>
|