Creation modal quality of life changes (#467)

Co-authored-by: Matt Kilgore <tankerkiller125@users.noreply.github.com>
Co-authored-by: Tonya <tonya@tokia.dev>
This commit is contained in:
Corknut
2025-01-26 07:43:45 -05:00
committed by GitHub
parent 96d88c5728
commit fca7d24268
8 changed files with 139 additions and 55 deletions

View File

@@ -1,7 +1,11 @@
<template>
<div class="z-[999]">
<input :id="modalId" v-model="modal" type="checkbox" class="modal-toggle" />
<div class="modal modal-bottom overflow-visible sm:modal-middle">
<div
class="modal overflow-visible sm:modal-middle"
:class="{ 'modal-bottom': !props.modalTop }"
:modal-top="props.modalTop"
>
<div ref="modalBox" class="modal-box relative overflow-visible">
<button
v-if="props.showCloseButton"
@@ -41,6 +45,14 @@
type: Boolean,
default: true,
},
clickOutsideToClose: {
type: Boolean,
default: false,
},
modalTop: {
type: Boolean,
default: false,
},
});
const modalBox = ref();
@@ -51,9 +63,11 @@
}
}
if (props.clickOutsideToClose) {
onClickOutside(modalBox, () => {
close();
});
}
function close() {
if (props.readonly) {
@@ -74,3 +88,23 @@
}
});
</script>
<style lang="css" scoped>
@media (max-width: 640px) {
.modal[modal-top=true] {
align-items: start;
}
.modal[modal-top=true] :where(.modal-box) {
max-width: none;
--tw-translate-y: 2.5rem /* 40px */;
--tw-scale-x: 1;
--tw-scale-y: 1;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate))
skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
width: 100%;
border-top-left-radius: 0px;
border-top-right-radius: 0px;
}
}
</style>

View File

@@ -4,7 +4,7 @@
<span class="label-text">{{ label }}</span>
</label>
<div class="dropdown dropdown-top sm:dropdown-end">
<div tabindex="0" class="flex min-h-[48px] w-full flex-wrap gap-2 rounded-lg border border-gray-400 p-4">
<div tabindex="0" class="flex min-h-[48px] w-full flex-wrap gap-2 rounded-lg border border-base-content/20 p-4">
<span v-for="itm in value" :key="itm.id" class="badge">
{{ itm.name }}
</span>
@@ -20,7 +20,7 @@
<div
tabindex="0"
style="display: inline"
class="dropdown-content menu z-[9999] mb-1 w-full rounded border border-gray-400 bg-base-100 shadow"
class="dropdown-content menu z-[9999] mb-1 w-full rounded border border-base-content/20 bg-base-100 shadow"
>
<div class="m-2">
<input v-model="search" placeholder="Search…" class="input input-bordered input-sm w-full" />

View File

@@ -141,10 +141,13 @@
}
}
whenever(
watch(
() => modal.value,
() => {
open => {
if (open) {
useTimeoutFn(() => {
focused.value = true;
}, 50);
if (locationId.value) {
const found = locations.value.find(l => l.id === locationId.value);
@@ -156,6 +159,9 @@
if (labelId.value) {
form.labels = labels.value.filter(l => l.id === labelId.value);
}
} else {
focused.value = false;
}
}
);

View File

@@ -64,10 +64,14 @@
loading.value = false;
}
whenever(
watch(
() => modal.value,
() => {
open => {
if (open)
useTimeoutFn(() => {
focused.value = true;
}, 50);
else focused.value = false;
}
);

View File

@@ -2,6 +2,7 @@
<BaseModal v-model="modal">
<template #title>{{ $t("components.location.create_modal.title") }}</template>
<form @submit.prevent="create()">
<LocationSelector v-model="form.parent" />
<FormTextField
ref="locationNameRef"
v-model="form.name"
@@ -17,7 +18,6 @@
:label="$t('components.location.create_modal.location_description')"
:max-length="1000"
/>
<LocationSelector v-model="form.parent" />
<div class="modal-action">
<div class="flex justify-center">
<BaseButton class="rounded-r-none" type="submit" :loading="loading">{{ $t("global.create") }}</BaseButton>
@@ -59,10 +59,23 @@
parent: null as LocationSummary | null,
});
whenever(
watch(
() => modal.value,
() => {
open => {
if (open) {
useTimeoutFn(() => {
focused.value = true;
}, 50);
if (locationId.value) {
const found = locations.value.find(l => l.id === locationId.value);
if (found) {
form.parent = found;
}
}
} else {
focused.value = false;
}
}
);
@@ -77,8 +90,20 @@
const api = useUserApi();
const toast = useNotifier();
const locationsStore = useLocationStore();
const locations = computed(() => locationsStore.allLocations);
const route = useRoute();
const { shift } = useMagicKeys();
const locationId = computed(() => {
if (route.fullPath.includes("/location/")) {
return route.params.id;
}
return null;
});
async function create(close = true) {
if (loading.value) {
toast.error("Already creating a location");

View File

@@ -1,5 +1,5 @@
<template>
<Combobox v-model="selectedAction">
<Combobox v-model="selectedAction" :nullable="true">
<ComboboxInput
ref="inputBox"
class="input input-bordered mt-2 w-full"
@@ -7,6 +7,7 @@
></ComboboxInput>
<ComboboxOptions
class="card dropdown-content absolute max-h-48 w-full overflow-y-scroll rounded-lg border border-base-300 bg-base-100"
:unmount="false"
>
<ComboboxOption
v-for="(action, idx) in filteredActions"
@@ -68,21 +69,20 @@
},
});
const selectedAction = useVModel(props, "modelValue");
const emit = defineEmits(["update:modelValue", "actionSelected"]);
const selectedAction = ref(null);
const inputValue = ref("");
const inputBox = ref();
const inputBoxButton = ref();
const { focused: inputBoxFocused } = useFocus(inputBox);
const emit = defineEmits(["update:modelValue", "quickSelect"]);
const revealActions = () => {
unrefElement(inputBoxButton).click();
};
watch(inputBoxFocused, () => {
if (inputBoxFocused.value) revealActions();
watch(inputBoxFocused, val => {
if (val) revealActions();
else inputValue.value = "";
});
@@ -90,11 +90,19 @@
if (!oldVal) {
const action = props.actions?.find(v => v.shortcut === val);
if (action) {
emit("quickSelect", action);
emit("actionSelected", action);
inputBoxFocused.value = false;
}
}
});
watch(selectedAction, val => {
if (val) {
emit("actionSelected", val);
selectedAction.value = null;
}
});
const filteredActions = computed(() => {
const searchTerm = inputValue.value.toLowerCase();
return (props.actions || []).filter(action => {

View File

@@ -1,13 +1,14 @@
<template>
<BaseModal v-model="modal" :show-close-button="false">
<BaseModal
v-model="modal"
:show-close-button="false"
:click-outside-to-close="true"
:modal-top="true"
:class="{ 'self-start': true }"
>
<div class="relative">
<span class="text-neutral-400">{{ $t("components.quick_menu.shortcut_hint") }}</span>
<QuickMenuInput
ref="inputBox"
v-model="selectedAction"
:actions="props.actions || []"
@quick-select="invokeAction"
></QuickMenuInput>
<QuickMenuInput ref="inputBox" :actions="props.actions || []" @action-selected="invokeAction"></QuickMenuInput>
</div>
</BaseModal>
</template>
@@ -28,7 +29,6 @@
});
const modal = useVModel(props, "modelValue");
const selectedAction = ref<QuickMenuAction>();
const inputBox = ref<QuickMenuInputData>({ focused: false, revealActions: () => {} });
@@ -37,7 +37,6 @@
}, 50).start;
const onModalClose = () => {
selectedAction.value = undefined;
inputBox.value.focused = false;
};
@@ -51,8 +50,4 @@
modal.value = false;
useTimeoutFn(action.action, 100).start();
}
watch(selectedAction, action => {
if (action) invokeAction(action);
});
</script>

View File

@@ -35,7 +35,9 @@
<p class="text-center text-sm">
<a href="https://github.com/sysadminsmedia/homebox/releases/tag/{{ status.build.version }}" target="_blank">
{{ $t("global.version", { version: status.build.version }) }} ~
{{ $t("global.build", { build: status.build.commit }) }}</a> ~
{{ $t("global.build", { build: status.build.commit }) }}</a
>
~
<a href="https://homebox.software/en/api.html" target="_blank">API</a>
</p>
</footer>
@@ -70,9 +72,11 @@
<button class="group" @click="btn.action">
{{ btn.name.value }}
<kbd v-if="btn.shortcut" class="ml-auto hidden text-neutral-400 group-hover:inline">{{
btn.shortcut
}}</kbd>
<kbd
v-if="btn.shortcut"
class="kbd kbd-sm ml-auto hidden text-neutral-400 group-hover:inline"
>{{ btn.shortcut.replaceAll("Shift+", "⇧") }}</kbd
>
</button>
</li>
</ul>
@@ -145,9 +149,14 @@
const displayOutdatedWarning = computed(() => Boolean(!isDev.value && !hasHiddenLatest.value && isOutdated.value));
const activeElement = useActiveElement();
const keys = useMagicKeys({
aliasMap: {
"⌃": "control_",
"Shift+": "ShiftLeft_",
"1": "digit1",
"2": "digit2",
"3": "digit3",
},
});
@@ -173,7 +182,7 @@
{
id: 0,
name: computed(() => t("menu.create_item")),
shortcut: "1",
shortcut: "Shift+1",
action: () => {
modals.item = true;
},
@@ -181,7 +190,7 @@
{
id: 1,
name: computed(() => t("menu.create_location")),
shortcut: "2",
shortcut: "Shift+2",
action: () => {
modals.location = true;
},
@@ -189,7 +198,7 @@
{
id: 2,
name: computed(() => t("menu.create_label")),
shortcut: "3",
shortcut: "Shift+3",
action: () => {
modals.label = true;
},
@@ -197,9 +206,12 @@
];
dropdown.forEach(option => {
if (option.shortcut) {
whenever(keys[option.shortcut], () => {
if (option?.shortcut) {
const shortcutKeycode = option.shortcut.replace(/([0-9])/, "digit$&");
whenever(keys[shortcutKeycode], () => {
if (activeElement.value?.tagName !== "INPUT") {
option.action();
}
});
}
});
@@ -267,7 +279,7 @@
modals.import = false;
});
const quickMenuActions = ref(
const quickMenuActions = reactive(
[
{
text: computed(() => `${t("global.create")}: ${t("menu.create_item")}`),