1<template>
2 <div class="flex flex-col gap-4">
3 <div class="flex flex-col">
4 <h3 class="text-base-medium text-ink-gray-8">{{ __("Banner") }}</h3>
5 <span class="mt-1 text-base leading-5 text-ink-gray-6">{{
6 __(
7 "Choose a color, pattern or image to show behind the search bar to customize your knowledge base home page. For custom images recommended image size is minimum 1440x240 px in PNG or JPG."
8 )
9 }}</span>
10 </div>
11 <div
12 class="flex h-28 items-center justify-center rounded-6 border border-outline-gray-2 px-4"
13 :style="{ background: previewBackground }"
14 >
15 <div
16 class="flex h-8 w-full max-w-sm items-center gap-2 rounded-4 border border-outline-gray-1 bg-surface-base px-2.5 text-base text-ink-gray-4 shadow-sm"
17 >
18 <LucideSearch class="size-4 shrink-0" />
19 {{ __("Search articles...") }}
20 </div>
21 </div>
22 <div class="flex flex-wrap items-center justify-between gap-3">
23 <div class="flex items-center gap-2" role="radiogroup">
24 <Tooltip :text="__('None')">
25 <button
26 type="button"
27 role="radio"
28 :aria-label="__('None')"
29 :aria-checked="!image && !preset"
30 :class="[swatch, ring(!image && !preset)]"
31 class="flex items-center justify-center bg-surface-base text-ink-gray-4"
32 @click="save('', '')"
33 >
34 <LucideBan class="size-4" />
35 </button>
36 </Tooltip>
37 <Tooltip
38 v-for="option in BANNER_PRESETS"
39 :key="option.name"
40 :text="presetLabel(option.name)"
41 >
42 <button
43 type="button"
44 role="radio"
45 :aria-label="presetLabel(option.name)"
46 :aria-checked="!image && preset === option.name"
47 :class="[swatch, ring(!image && preset === option.name)]"
48 :style="{ background: option.background }"
49 @click="save('', option.name)"
50 />
51 </Tooltip>
52 <!-- flex: a block wrapper adds a line box, 6px taller than the swatches -->
53 <div v-if="uploaded" class="relative flex">
54 <Tooltip :text="__('Custom image')">
55 <button
56 type="button"
57 role="radio"
58 :aria-label="__('Custom image')"
59 :aria-checked="Boolean(image)"
60 :class="[swatch, ring(Boolean(image))]"
61 :style="{ background: imageBackground(uploaded) }"
62 @click="save(uploaded, '')"
63 />
64 </Tooltip>
65 <Tooltip :text="__('Remove image')">
66 <button
67 type="button"
68 :aria-label="__('Remove image')"
69 class="absolute -right-2 -top-2 flex size-4 items-center justify-center rounded-full border-[1.5px] border-[--surface-base] bg-[--ink-gray-9] text-[--surface-base]"
70 @click="removeImage"
71 >
72 <LucideX class="size-2.5" />
73 </button>
74 </Tooltip>
75 </div>
76 </div>
77 <FileUploader
78 :fileTypes="['image/*']"
79 :private="false"
80 @success="(file) => save(file.file_url, '')"
81 >
82 <template #default="{ progress, uploading, openFileSelector }">
83 <Button
84 variant="subtle"
85 icon-left="lucide-image-up"
86 :label="uploadLabel(uploading, progress)"
87 :loading="uploading"
88 @click="openFileSelector"
89 />
90 </template>
91 </FileUploader>
92 </div>
93 </div>
94</template>
95
96<script setup lang="ts">
97import { __ } from "../translation";
98import { BANNER_PRESETS, findBannerPreset } from "../knowledgeBaseBanner";
99import { Button, FileUploader, Tooltip } from "frappe-ui";
100import { computed, ref, watch } from "vue";
101import LucideBan from "~icons/lucide/ban";
102import LucideSearch from "~icons/lucide/search";
103import LucideX from "~icons/lucide/x";
104
105const props = defineProps<{ image: string; preset: string }>();
106const emit = defineEmits<{
107 change: [value: { banner_image: string; banner_preset: string }];
108}>();
109
110// Keep the upload's swatch after picking another, to re-pick it.
111const uploaded = ref(props.image);
112watch(
113 () => props.image,
114 (url) => url && (uploaded.value = url)
115);
116
117const swatch =
118 "size-8 shrink-0 rounded-[8px] border [--banner-pattern-scale:0.4] border-outline-gray-2 outline outline-[1.5px] outline-offset-2 transition-[outline-color]";
119
120function ring(selected: boolean) {
121 return selected
122 ? "outline-[--outline-gray-5]"
123 : "outline-transparent hover:outline-[--outline-gray-2]";
124}
125
126const previewBackground = computed(() =>
127 props.image
128 ? imageBackground(props.image)
129 : findBannerPreset(props.preset)?.background || ""
130);
131
132function imageBackground(url: string) {
133 return `center / cover url('${url}')`;
134}
135
136function uploadLabel(uploading: boolean, progress: number) {
137 if (uploading) return __("Uploading {0}%", [progress]);
138 return uploaded.value ? __("Replace image") : __("Upload image");
139}
140
141function presetLabel(name: string) {
142 return {
143 Gray: __("Gray"),
144 Blue: __("Blue"),
145 Violet: __("Violet"),
146 Lines: __("Lines"),
147 }[name];
148}
149
150function removeImage() {
151 uploaded.value = "";
152 if (props.image) save("", "");
153}
154
155function save(image: string, preset: string) {
156 if (image === props.image && preset === props.preset) return;
157 emit("change", { banner_image: image, banner_preset: preset });
158}
159</script>
160