Clean code.

This commit is contained in:
yuanhau 2025-05-10 22:05:10 +08:00
parent bf357f1c84
commit 5d58016b1d
39 changed files with 481 additions and 381 deletions

View file

@ -1,30 +1,33 @@
<script setup lang="ts">
import type { ListboxRootEmits, ListboxRootProps } from 'reka-ui'
import { cn } from '@/lib/utils'
import { ListboxRoot, useFilter, useForwardPropsEmits } from 'reka-ui'
import { computed, type HTMLAttributes, reactive, ref, watch } from 'vue'
import { provideCommandContext } from '.'
import type { ListboxRootEmits, ListboxRootProps } from "reka-ui";
import { cn } from "@/lib/utils";
import { ListboxRoot, useFilter, useForwardPropsEmits } from "reka-ui";
import { computed, type HTMLAttributes, reactive, ref, watch } from "vue";
import { provideCommandContext } from ".";
const props = withDefaults(defineProps<ListboxRootProps & { class?: HTMLAttributes['class'] }>(), {
modelValue: '',
})
const props = withDefaults(
defineProps<ListboxRootProps & { class?: HTMLAttributes["class"] }>(),
{
modelValue: "",
},
);
const emits = defineEmits<ListboxRootEmits>()
const emits = defineEmits<ListboxRootEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
const { class: _, ...delegated } = props;
return delegated
})
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits)
const forwarded = useForwardPropsEmits(delegatedProps, emits);
const allItems = ref<Map<string, string>>(new Map())
const allGroups = ref<Map<string, Set<string>>>(new Map())
const allItems = ref<Map<string, string>>(new Map());
const allGroups = ref<Map<string, Set<string>>>(new Map());
const { contains } = useFilter({ sensitivity: 'base' })
const { contains } = useFilter({ sensitivity: "base" });
const filterState = reactive({
search: '',
search: "",
filtered: {
/** The count of all visible items. */
count: 0,
@ -33,59 +36,66 @@ const filterState = reactive({
/** Set of groups with at least one visible item. */
groups: new Set() as Set<string>,
},
})
});
function filterItems() {
if (!filterState.search) {
filterState.filtered.count = allItems.value.size
filterState.filtered.count = allItems.value.size;
// Do nothing, each item will know to show itself because search is empty
return
return;
}
// Reset the groups
filterState.filtered.groups = new Set()
let itemCount = 0
filterState.filtered.groups = new Set();
let itemCount = 0;
// Check which items should be included
for (const [id, value] of allItems.value) {
const score = contains(value, filterState.search)
filterState.filtered.items.set(id, score ? 1 : 0)
if (score)
itemCount++
const score = contains(value, filterState.search);
filterState.filtered.items.set(id, score ? 1 : 0);
if (score) itemCount++;
}
// Check which groups have at least 1 item shown
for (const [groupId, group] of allGroups.value) {
for (const itemId of group) {
if (filterState.filtered.items.get(itemId)! > 0) {
filterState.filtered.groups.add(groupId)
break
filterState.filtered.groups.add(groupId);
break;
}
}
}
filterState.filtered.count = itemCount
filterState.filtered.count = itemCount;
}
function handleSelect() {
filterState.search = ''
filterState.search = "";
}
watch(() => filterState.search, () => {
filterItems()
})
watch(
() => filterState.search,
() => {
filterItems();
},
);
provideCommandContext({
allItems,
allGroups,
filterState,
})
});
</script>
<template>
<ListboxRoot
v-bind="forwarded"
:class="cn('flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground', props.class)"
:class="
cn(
'flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground',
props.class,
)
"
>
<slot />
</ListboxRoot>

View file

@ -1,19 +1,21 @@
<script setup lang="ts">
import type { DialogRootEmits, DialogRootProps } from 'reka-ui'
import { Dialog, DialogContent } from '@/components/ui/dialog'
import { useForwardPropsEmits } from 'reka-ui'
import Command from './Command.vue'
import type { DialogRootEmits, DialogRootProps } from "reka-ui";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { useForwardPropsEmits } from "reka-ui";
import Command from "./Command.vue";
const props = defineProps<DialogRootProps>()
const emits = defineEmits<DialogRootEmits>()
const props = defineProps<DialogRootProps>();
const emits = defineEmits<DialogRootEmits>();
const forwarded = useForwardPropsEmits(props, emits)
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<Dialog v-bind="forwarded">
<DialogContent class="overflow-hidden p-0 shadow-lg">
<Command class="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
<Command
class="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"
>
<slot />
</Command>
</DialogContent>

View file

@ -1,25 +1,32 @@
<script setup lang="ts">
import type { PrimitiveProps } from 'reka-ui'
import { cn } from '@/lib/utils'
import { Primitive } from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
import { useCommand } from '.'
import type { PrimitiveProps } from "reka-ui";
import { cn } from "@/lib/utils";
import { Primitive } from "reka-ui";
import { computed, type HTMLAttributes } from "vue";
import { useCommand } from ".";
const props = defineProps<PrimitiveProps & { class?: HTMLAttributes['class'] }>()
const props = defineProps<
PrimitiveProps & { class?: HTMLAttributes["class"] }
>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
const { class: _, ...delegated } = props;
return delegated
})
return delegated;
});
const { filterState } = useCommand()
const isRender = computed(() => !!filterState.search && filterState.filtered.count === 0,
)
const { filterState } = useCommand();
const isRender = computed(
() => !!filterState.search && filterState.filtered.count === 0,
);
</script>
<template>
<Primitive v-if="isRender" v-bind="delegatedProps" :class="cn('py-6 text-center text-sm', props.class)">
<Primitive
v-if="isRender"
v-bind="delegatedProps"
:class="cn('py-6 text-center text-sm', props.class)"
>
<slot />
</Primitive>
</template>

View file

@ -1,44 +1,55 @@
<script setup lang="ts">
import type { ListboxGroupProps } from 'reka-ui'
import { cn } from '@/lib/utils'
import { ListboxGroup, ListboxGroupLabel, useId } from 'reka-ui'
import { computed, type HTMLAttributes, onMounted, onUnmounted } from 'vue'
import { provideCommandGroupContext, useCommand } from '.'
import type { ListboxGroupProps } from "reka-ui";
import { cn } from "@/lib/utils";
import { ListboxGroup, ListboxGroupLabel, useId } from "reka-ui";
import { computed, type HTMLAttributes, onMounted, onUnmounted } from "vue";
import { provideCommandGroupContext, useCommand } from ".";
const props = defineProps<ListboxGroupProps & {
class?: HTMLAttributes['class']
heading?: string
}>()
const props = defineProps<
ListboxGroupProps & {
class?: HTMLAttributes["class"];
heading?: string;
}
>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
const { class: _, ...delegated } = props;
return delegated
})
return delegated;
});
const { allGroups, filterState } = useCommand()
const id = useId()
const { allGroups, filterState } = useCommand();
const id = useId();
const isRender = computed(() => !filterState.search ? true : filterState.filtered.groups.has(id))
const isRender = computed(() =>
!filterState.search ? true : filterState.filtered.groups.has(id),
);
provideCommandGroupContext({ id })
provideCommandGroupContext({ id });
onMounted(() => {
if (!allGroups.value.has(id))
allGroups.value.set(id, new Set())
})
if (!allGroups.value.has(id)) allGroups.value.set(id, new Set());
});
onUnmounted(() => {
allGroups.value.delete(id)
})
allGroups.value.delete(id);
});
</script>
<template>
<ListboxGroup
v-bind="delegatedProps"
:id="id"
:class="cn('overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground', props.class)"
:class="
cn(
'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground',
props.class,
)
"
:hidden="isRender ? undefined : true"
>
<ListboxGroupLabel v-if="heading" class="px-2 py-1.5 text-xs font-medium text-muted-foreground">
<ListboxGroupLabel
v-if="heading"
class="px-2 py-1.5 text-xs font-medium text-muted-foreground"
>
{{ heading }}
</ListboxGroupLabel>
<slot />

View file

@ -1,27 +1,33 @@
<script setup lang="ts">
import { cn } from '@/lib/utils'
import { Search } from 'lucide-vue-next'
import { ListboxFilter, type ListboxFilterProps, useForwardProps } from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
import { useCommand } from '.'
import { cn } from "@/lib/utils";
import { Search } from "lucide-vue-next";
import {
ListboxFilter,
type ListboxFilterProps,
useForwardProps,
} from "reka-ui";
import { computed, type HTMLAttributes } from "vue";
import { useCommand } from ".";
defineOptions({
inheritAttrs: false,
})
});
const props = defineProps<ListboxFilterProps & {
class?: HTMLAttributes['class']
}>()
const props = defineProps<
ListboxFilterProps & {
class?: HTMLAttributes["class"];
}
>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
const { class: _, ...delegated } = props;
return delegated
})
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps)
const forwardedProps = useForwardProps(delegatedProps);
const { filterState } = useCommand()
const { filterState } = useCommand();
</script>
<template>
@ -31,7 +37,12 @@ const { filterState } = useCommand()
v-bind="{ ...forwardedProps, ...$attrs }"
v-model="filterState.search"
auto-focus
:class="cn('flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50', props.class)"
:class="
cn(
'flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50',
props.class,
)
"
/>
</div>
</template>

View file

@ -1,32 +1,39 @@
<script setup lang="ts">
import type { ListboxItemEmits, ListboxItemProps } from 'reka-ui'
import { cn } from '@/lib/utils'
import { useCurrentElement } from '@vueuse/core'
import { ListboxItem, useForwardPropsEmits, useId } from 'reka-ui'
import { computed, type HTMLAttributes, onMounted, onUnmounted, ref } from 'vue'
import { useCommand, useCommandGroup } from '.'
import type { ListboxItemEmits, ListboxItemProps } from "reka-ui";
import { cn } from "@/lib/utils";
import { useCurrentElement } from "@vueuse/core";
import { ListboxItem, useForwardPropsEmits, useId } from "reka-ui";
import {
computed,
type HTMLAttributes,
onMounted,
onUnmounted,
ref,
} from "vue";
import { useCommand, useCommandGroup } from ".";
const props = defineProps<ListboxItemProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<ListboxItemEmits>()
const props = defineProps<
ListboxItemProps & { class?: HTMLAttributes["class"] }
>();
const emits = defineEmits<ListboxItemEmits>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
const { class: _, ...delegated } = props;
return delegated
})
return delegated;
});
const forwarded = useForwardPropsEmits(delegatedProps, emits)
const forwarded = useForwardPropsEmits(delegatedProps, emits);
const id = useId()
const { filterState, allItems, allGroups } = useCommand()
const groupContext = useCommandGroup()
const id = useId();
const { filterState, allItems, allGroups } = useCommand();
const groupContext = useCommandGroup();
const isRender = computed(() => {
if (!filterState.search) {
return true;
}
else {
const filteredCurrentItem = filterState.filtered.items.get(id)
} else {
const filteredCurrentItem = filterState.filtered.items.get(id);
// If the filtered items is undefined means not in the all times map yet
// Do the first render to add into the map
if (filteredCurrentItem === undefined) {
@ -36,30 +43,31 @@ const isRender = computed(() => {
// Check with filter
return filteredCurrentItem > 0;
}
})
});
const itemRef = ref()
const currentElement = useCurrentElement(itemRef)
const itemRef = ref();
const currentElement = useCurrentElement(itemRef);
onMounted(() => {
if (!(currentElement.value instanceof HTMLElement))
return
if (!(currentElement.value instanceof HTMLElement)) return;
// textValue to perform filter
allItems.value.set(id, currentElement.value.textContent ?? props.value.toString())
allItems.value.set(
id,
currentElement.value.textContent ?? props.value.toString(),
);
const groupId = groupContext?.id
const groupId = groupContext?.id;
if (groupId) {
if (!allGroups.value.has(groupId)) {
allGroups.value.set(groupId, new Set([id]))
}
else {
allGroups.value.get(groupId)?.add(id)
allGroups.value.set(groupId, new Set([id]));
} else {
allGroups.value.get(groupId)?.add(id);
}
}
})
});
onUnmounted(() => {
allItems.value.delete(id)
})
allItems.value.delete(id);
});
</script>
<template>
@ -68,10 +76,17 @@ onUnmounted(() => {
v-bind="forwarded"
:id="id"
ref="itemRef"
:class="cn('relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0', props.class)"
@select="() => {
filterState.search = ''
}"
:class="
cn(
'relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0',
props.class,
)
"
@select="
() => {
filterState.search = '';
}
"
>
<slot />
</ListboxItem>

View file

@ -1,22 +1,27 @@
<script setup lang="ts">
import type { ListboxContentProps } from 'reka-ui'
import { cn } from '@/lib/utils'
import { ListboxContent, useForwardProps } from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
import type { ListboxContentProps } from "reka-ui";
import { cn } from "@/lib/utils";
import { ListboxContent, useForwardProps } from "reka-ui";
import { computed, type HTMLAttributes } from "vue";
const props = defineProps<ListboxContentProps & { class?: HTMLAttributes['class'] }>()
const props = defineProps<
ListboxContentProps & { class?: HTMLAttributes["class"] }
>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
const { class: _, ...delegated } = props;
return delegated
})
return delegated;
});
const forwarded = useForwardProps(delegatedProps)
const forwarded = useForwardProps(delegatedProps);
</script>
<template>
<ListboxContent v-bind="forwarded" :class="cn('max-h-[300px] overflow-y-auto overflow-x-hidden', props.class)">
<ListboxContent
v-bind="forwarded"
:class="cn('max-h-[300px] overflow-y-auto overflow-x-hidden', props.class)"
>
<div role="presentation">
<slot />
</div>

View file

@ -1,16 +1,18 @@
<script setup lang="ts">
import type { SeparatorProps } from 'reka-ui'
import { cn } from '@/lib/utils'
import { Separator } from 'reka-ui'
import { computed, type HTMLAttributes } from 'vue'
import type { SeparatorProps } from "reka-ui";
import { cn } from "@/lib/utils";
import { Separator } from "reka-ui";
import { computed, type HTMLAttributes } from "vue";
const props = defineProps<SeparatorProps & { class?: HTMLAttributes['class'] }>()
const props = defineProps<
SeparatorProps & { class?: HTMLAttributes["class"] }
>();
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
const { class: _, ...delegated } = props;
return delegated
})
return delegated;
});
</script>
<template>

View file

@ -1,14 +1,18 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
class?: HTMLAttributes["class"];
}>();
</script>
<template>
<span :class="cn('ml-auto text-xs tracking-widest text-muted-foreground', props.class)">
<span
:class="
cn('ml-auto text-xs tracking-widest text-muted-foreground', props.class)
"
>
<slot />
</span>
</template>

View file

@ -1,25 +1,29 @@
import type { Ref } from 'vue'
import { createContext } from 'reka-ui'
import type { Ref } from "vue";
import { createContext } from "reka-ui";
export { default as Command } from './Command.vue'
export { default as CommandDialog } from './CommandDialog.vue'
export { default as CommandEmpty } from './CommandEmpty.vue'
export { default as CommandGroup } from './CommandGroup.vue'
export { default as CommandInput } from './CommandInput.vue'
export { default as CommandItem } from './CommandItem.vue'
export { default as CommandList } from './CommandList.vue'
export { default as CommandSeparator } from './CommandSeparator.vue'
export { default as CommandShortcut } from './CommandShortcut.vue'
export { default as Command } from "./Command.vue";
export { default as CommandDialog } from "./CommandDialog.vue";
export { default as CommandEmpty } from "./CommandEmpty.vue";
export { default as CommandGroup } from "./CommandGroup.vue";
export { default as CommandInput } from "./CommandInput.vue";
export { default as CommandItem } from "./CommandItem.vue";
export { default as CommandList } from "./CommandList.vue";
export { default as CommandSeparator } from "./CommandSeparator.vue";
export { default as CommandShortcut } from "./CommandShortcut.vue";
export const [useCommand, provideCommandContext] = createContext<{
allItems: Ref<Map<string, string>>
allGroups: Ref<Map<string, Set<string>>>
allItems: Ref<Map<string, string>>;
allGroups: Ref<Map<string, Set<string>>>;
filterState: {
search: string
filtered: { count: number, items: Map<string, number>, groups: Set<string> }
}
}>('Command')
search: string;
filtered: {
count: number;
items: Map<string, number>;
groups: Set<string>;
};
};
}>("Command");
export const [useCommandGroup, provideCommandGroupContext] = createContext<{
id?: string
}>('CommandGroup')
id?: string;
}>("CommandGroup");