tooltips; css polishing

3clyp50 committed Jan 2, 2026 at 13:07 UTC 67e5783c3c20ecb3e0c847dfcb2ee57a4fcc82b7
7 files changed +224 -13
webui/components/chat/input/chat-bar-input.html
+2 -9
@@ -9,15 +9,13 @@
9 <body>
10 <div class="input-row">
11 <!-- Attachment icon with tooltip -->
12 - <div class="attachment-wrapper" x-data="{ showTooltip: false }">
13 - <label for="file-input" class="attachment-icon" @mouseover="showTooltip = true" @mouseleave="showTooltip = false">
12 + <div class="attachment-wrapper">
13 + <label for="file-input" class="attachment-icon" title="Add attachments to the message" data-bs-placement="top">
14 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
15 <path d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5c0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5c0 1.38 1.12 2.5 2.5 2.5s2.5-1.12 2.5-2.5V5c0-2.21-1.79-4-4-4S7 2.79 7 5v12.5c0 3.04 2.46 5.5 5.5 5.5s5.5-2.46 5.5-5.5V6h-1.5z" />
16 </svg>
17 </label>
18 <input type="file" id="file-input" accept="*" multiple style="display: none" @change="$store.chatAttachments.handleFileUpload($event)">
19 -
20 - <div x-show="showTooltip" class="tooltip">Add attachments to the message</div>
19 </div>
20
21 <!-- Container for textarea and expand button -->
@@ -64,8 +62,6 @@
62 .attachment-icon { cursor: pointer; color: var(--color-text); opacity: 0.7; transition: opacity 0.2s ease; display: flex; align-items: center; }
63 .attachment-icon:hover { opacity: 1; }
64 .attachment-icon:active { opacity: 0.5; }
67 - .tooltip { position: absolute; bottom: 100%; left: 50%; transform: translateX(0%); padding: 8px; background-color: var(--color-secondary); color: var(--color-text); border-radius: 4px; font-size: 12px; white-space: nowrap; z-index: 1002; }
68 -
65 /* Text input */
66 #chat-input-container {
67 position: relative;
@@ -117,8 +113,6 @@
113 .chat-button svg { width: 1.5rem; height: 1.5rem; }
114 /* Microphone button */
115 .chat-button.mic-inactive svg { /* Add specific styles if needed */ }
120 - /* Tooltip */
121 - .tooltip { position: absolute; bottom: 100%; left: 50%; transform: translateX(0%); padding: 8px; background-color: var(--color-secondary); color: var(--color-text); border-radius: 4px; font-size: 12px; white-space: nowrap; z-index: 1002; }
116 /* Responsive tweaks */
117 @media (max-width: 640px) {
118 #chat-input { min-height: 5.3rem; align-content: start; }
@@ -128,4 +122,3 @@
122 </style>
123 </body>
124 </html>
131 -
webui/components/sidebar/chats/chats-list.html
+1 -1
@@ -13,7 +13,7 @@
13 <div class="section-header-row">
14 <h3 class="section-header">Chats</h3>
15 <!-- New Chat Button -->
16 - <button id="newChat" @click="$store.chats.newChat()" aria-label="New Chat">
16 + <button id="newChat" @click="$store.chats.newChat()" aria-label="New Chat" title="New Chat" data-bs-placement="top">
17 <svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 110 135" width="32" height="32">
18 <path d="m38.3 81.3 3.2-9.1 3.1-9v-.1c0-.2.1-.3.2-.4.1-.2.2-.3.3-.4L82.4 25c1.6-1.6 3.7-2.4 5.7-2.4h.2c2 0 4 .8 5.6 2.4l3.7 3.7.1.2c1.5 1.6 2.2 3.6 2.2 5.6v.2c0 2-.8 4-2.4 5.6l-.1.1-3.8 3.6-.1.1-.1.1-32.9 33-.1.1q-.45.45-.9.6l-9.1 3.2h-.1l-9.1 3.1c-1.2.4-2.5-.2-2.9-1.4q-.3-.75 0-1.5m7.5-7.6-1.6 4.6 4.6-1.6 5.7-2-3.4-3.4-3.3-3.3zm4.2-9.9 4.3 4.3 4.3 4.3 30-30-8.6-8.5zm35.7-35.7-2.5 2.5 8.6 8.6 2.4-2.4.1-.1c.6-.6 1-1.5 1-2.3v-.1c0-.9-.3-1.8-1-2.4l-3.7-3.7c-.6-.6-1.5-1-2.3-1h-.1c-.9-.1-1.8.3-2.5.9m.4 44.1c0-1.3 1-2.3 2.3-2.3s2.3 1 2.3 2.3V101c0 3.2-1.3 6.1-3.4 8.2s-5 3.4-8.2 3.4H21.6c-3.2 0-6.1-1.3-8.2-3.4s-3.4-5-3.4-8.2V43.4c0-3.2 1.3-6.1 3.4-8.2s5-3.4 8.2-3.4h28.8c1.3 0 2.3 1 2.3 2.3s-1 2.3-2.3 2.3H21.6c-1.9 0-3.7.8-4.9 2-1.3 1.3-2 3-2 4.9v57.6c0 1.9.8 3.6 2 4.9 1.3 1.3 3 2 4.9 2h57.6c1.9 0 3.7-.8 4.9-2 1.3-1.3 2-3 2-4.9z" style="fill:currentColor"></path>
19 </svg>
webui/components/sidebar/top-section/quick-actions.html
+1 -1
@@ -31,7 +31,7 @@
31 </button>
32
33 <!-- Dropdown Toggle -->
34 - <button class="config-button dropdown-toggle" @click="dropdownOpen = !dropdownOpen" title="More options">
34 + <button class="config-button dropdown-toggle" @click="$store.tooltips.hideAll(); dropdownOpen = !dropdownOpen" title="More options">
35 <span class="material-symbols-outlined" :class="dropdownOpen ? 'rotated' : ''">expand_more</span>
36 </button>
37 </div>
webui/components/tooltips/tooltip-store.js new
+112
@@ -0,0 +1,112 @@
1 +import { createStore } from "/js/AlpineStore.js";
2 +
3 +let bootstrapTooltipObserver = null;
4 +
5 +function ensureBootstrapTooltip(element) {
6 + if (!element || !(element instanceof Element)) return;
7 +
8 + const bs = globalThis.bootstrap;
9 + if (!bs?.Tooltip) return;
10 +
11 + const existing = bs.Tooltip.getInstance(element);
12 + const title = element.getAttribute("title") || element.getAttribute("data-bs-original-title");
13 +
14 + if (!title) return;
15 +
16 + if (existing) {
17 + if (element.getAttribute("title")) {
18 + element.setAttribute("data-bs-original-title", title);
19 + element.removeAttribute("title");
20 + }
21 + existing.setContent({ ".tooltip-inner": title });
22 + return;
23 + }
24 +
25 + if (element.getAttribute("title")) {
26 + element.setAttribute("data-bs-original-title", title);
27 + element.removeAttribute("title");
28 + }
29 +
30 + element.setAttribute("data-bs-toggle", "tooltip");
31 + element.setAttribute("data-bs-tooltip-initialized", "true");
32 + new bs.Tooltip(element, {
33 + delay: { show: 0, hide: 0 },
34 + trigger: "hover focus",
35 + });
36 +}
37 +
38 +function initBootstrapTooltips(root = document) {
39 + if (!globalThis.bootstrap?.Tooltip) return;
40 + const tooltipTargets = root.querySelectorAll(
41 + "[title]:not([data-bs-tooltip-initialized]), [data-bs-original-title]:not([data-bs-tooltip-initialized])"
42 + );
43 + tooltipTargets.forEach((element) => ensureBootstrapTooltip(element));
44 +}
45 +
46 +function observeBootstrapTooltips() {
47 + if (!globalThis.bootstrap?.Tooltip) return;
48 +
49 + // Prevent multiple observers
50 + if (bootstrapTooltipObserver) return;
51 +
52 + bootstrapTooltipObserver = new MutationObserver((mutations) => {
53 + mutations.forEach((mutation) => {
54 + if (mutation.type === "attributes" && mutation.attributeName === "title") {
55 + ensureBootstrapTooltip(mutation.target);
56 + return;
57 + }
58 +
59 + if (mutation.type === "childList") {
60 + mutation.addedNodes.forEach((node) => {
61 + if (!(node instanceof Element)) return;
62 + if (node.matches("[title]") || node.querySelector("[title]")) {
63 + initBootstrapTooltips(node);
64 + }
65 + });
66 + }
67 + });
68 + });
69 +
70 + bootstrapTooltipObserver.observe(document.body, {
71 + childList: true,
72 + subtree: true,
73 + attributes: true,
74 + attributeFilter: ["title"],
75 + });
76 +}
77 +
78 +function cleanupTooltipObserver() {
79 + if (bootstrapTooltipObserver) {
80 + bootstrapTooltipObserver.disconnect();
81 + bootstrapTooltipObserver = null;
82 + }
83 +}
84 +
85 +function hideAllTooltips() {
86 + const Tooltip = globalThis.bootstrap?.Tooltip;
87 + if (!Tooltip) return;
88 +
89 + document
90 + .querySelectorAll('[data-bs-tooltip-initialized="true"]')
91 + .forEach((element) => {
92 + try {
93 + const instance = Tooltip.getInstance(element);
94 + if (instance) {
95 + instance.hide();
96 + }
97 + } catch (error) {
98 + console.warn("Error hiding tooltip:", error);
99 + }
100 + });
101 +}
102 +
103 +export const store = createStore("tooltips", {
104 + hideAll: hideAllTooltips,
105 +
106 + init() {
107 + initBootstrapTooltips();
108 + observeBootstrapTooltips();
109 + },
110 +
111 + cleanup: cleanupTooltipObserver,
112 +});
webui/css/settings.css
-2
@@ -280,12 +280,10 @@ nav ul li a img {
280
281 .settings-tabs::before {
282 left: 0;
283 - background: linear-gradient(to right, var(--color-panel), transparent);
283 }
284
285 .settings-tabs::after {
286 right: 0;
288 - background: linear-gradient(to left, var(--color-panel), transparent);
287 }
288
289 .settings-tab {
webui/index.css
+105
@@ -129,6 +129,111 @@ body,
129 color: var(--color-text);
130 }
131
132 +/* Bootstrap-like tooltips */
133 +.tooltip {
134 + position: absolute;
135 + z-index: 1080;
136 + display: block;
137 + margin: 0;
138 + font-family: var(--font-family-main);
139 + font-style: normal;
140 + font-weight: 400;
141 + line-height: 1.2;
142 + text-align: left;
143 + text-decoration: none;
144 + text-shadow: none;
145 + text-transform: none;
146 + letter-spacing: normal;
147 + word-break: normal;
148 + white-space: normal;
149 + opacity: 0;
150 + pointer-events: none;
151 + transition: opacity 0.08s ease-in-out;
152 +}
153 +
154 +.tooltip.show {
155 + opacity: 1;
156 +}
157 +
158 +.tooltip .tooltip-inner {
159 + max-width: 240px;
160 + padding: 0.35rem 0.6rem;
161 + color: var(--color-text);
162 + background-color: var(--color-secondary);
163 + border-radius: 0.375rem;
164 + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
165 + font-size: 0.75rem;
166 +}
167 +
168 +.tooltip .tooltip-arrow {
169 + position: absolute;
170 + width: 0.8rem;
171 + height: 0.4rem;
172 +}
173 +
174 +.tooltip .tooltip-arrow::before {
175 + content: "";
176 + position: absolute;
177 + border-color: transparent;
178 + border-style: solid;
179 +}
180 +
181 +.bs-tooltip-top .tooltip-arrow {
182 + bottom: 0;
183 + left: 50%;
184 + transform: translateX(-50%);
185 +}
186 +
187 +.bs-tooltip-top .tooltip-arrow::before {
188 + border-width: 0.4rem 0.4rem 0;
189 + border-top-color: var(--color-secondary);
190 + top: 0;
191 +}
192 +
193 +.bs-tooltip-bottom .tooltip-arrow {
194 + top: 0;
195 + left: 50%;
196 + transform: translateX(-50%);
197 +}
198 +
199 +.bs-tooltip-bottom .tooltip-arrow::before {
200 + border-width: 0 0.4rem 0.4rem;
201 + border-bottom-color: var(--color-secondary);
202 + top: 0;
203 +}
204 +
205 +.bs-tooltip-start .tooltip-arrow,
206 +.bs-tooltip-left .tooltip-arrow {
207 + right: 0;
208 + top: 50%;
209 + width: 0.4rem;
210 + height: 0.8rem;
211 + transform: translateY(-50%);
212 +}
213 +
214 +.bs-tooltip-start .tooltip-arrow::before,
215 +.bs-tooltip-left .tooltip-arrow::before {
216 + border-width: 0.4rem 0 0.4rem 0.4rem;
217 + border-left-color: var(--color-secondary);
218 + left: 0;
219 +}
220 +
221 +.bs-tooltip-end .tooltip-arrow,
222 +.bs-tooltip-right .tooltip-arrow {
223 + left: 0;
224 + top: 50%;
225 + width: 0.4rem;
226 + height: 0.8rem;
227 + transform: translateY(-50%);
228 +}
229 +
230 +.bs-tooltip-end .tooltip-arrow::before,
231 +.bs-tooltip-right .tooltip-arrow::before {
232 + border-width: 0.4rem 0.4rem 0.4rem 0;
233 + border-right-color: var(--color-secondary);
234 + right: 0;
235 +}
236 +
237 img {
238 user-drag: none; /* Safari & old Chrome */
239 -webkit-user-drag: none;
webui/js/modals.js
+3
@@ -1,5 +1,6 @@
1 // Import the component loader and page utilities
2 import { importComponent } from "/js/components.js";
3 +import { store as tooltipsStore } from "/components/tooltips/tooltip-store.js";
4
5 // Modal functionality
6 const modalStack = [];
@@ -106,6 +107,8 @@ function createModalElement(path) {
107 export function openModal(modalPath) {
108 return new Promise((resolve) => {
109 try {
110 + tooltipsStore.hideAll();
111 +
112 // Create new modal instance
113 const modal = createModalElement(modalPath);
114