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