diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 77ebe93..3262a54 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -14,6 +14,10 @@ const ctx = await esbuild.context( bundle: true, minify: doMinify, sourcemap: false, + define: { + '__VUE_OPTIONS_API__': 'true', + '__VUE_PROD_DEVTOOLS__': 'false', + }, outdir: "server/ui/dist/", plugins: [pluginVue(), sassPlugin()], loader: { diff --git a/server/apiv1/api.go b/server/apiv1/api.go index 0dfbed7..ed1128f 100644 --- a/server/apiv1/api.go +++ b/server/apiv1/api.go @@ -304,7 +304,7 @@ func DownloadRaw(w http.ResponseWriter, r *http.Request) { return } - w.Header().Set("Content-Type", "text/plain") + w.Header().Set("Content-Type", "text/plain; charset=utf-8") if dl == "1" { w.Header().Set("Content-Disposition", "attachment; filename=\""+id+".eml\"") } @@ -495,7 +495,7 @@ func SetTags(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("ok")) } -// ReleaseMessage (method: POST) will release a message via a preconfigured external SMTP server. +// ReleaseMessage (method: POST) will release a message via a pre-configured external SMTP server. // If no IDs are provided then all messages are updated. func ReleaseMessage(w http.ResponseWriter, r *http.Request) { // swagger:route POST /api/v1/message/{ID}/release message Release diff --git a/server/ui-src/App.vue b/server/ui-src/App.vue index 68a270a..2f720bd 100644 --- a/server/ui-src/App.vue +++ b/server/ui-src/App.vue @@ -4,6 +4,7 @@ import Message from './templates/Message.vue' import MessageSummary from './templates/MessageSummary.vue' import MessageRelease from './templates/MessageRelease.vue' import MessageToast from './templates/MessageToast.vue' +import ThemeToggle from './templates/ThemeToggle.vue' import moment from 'moment' import Tinycon from 'tinycon' @@ -14,7 +15,8 @@ export default { Message, MessageSummary, MessageRelease, - MessageToast + MessageToast, + ThemeToggle, }, data() { @@ -50,55 +52,55 @@ export default { watch: { currentPath(v, old) { if (v && v.match(/^[a-z0-9]+-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+-[a-z0-9]+$/)) { - this.openMessage(); + this.openMessage() } else { - this.message = false; + this.message = false } }, unread(v, old) { if (v == this.tcStatus) { - return; + return } - this.tcStatus = v; + this.tcStatus = v if (v == 0) { - Tinycon.reset(); + Tinycon.reset() } else { - Tinycon.setBubble(v); + Tinycon.setBubble(v) } } }, computed: { canPrev: function () { - return this.start > 0; + return this.start > 0 }, canNext: function () { - return this.total > (this.start + this.count); + return this.total > (this.start + this.count) }, unreadInSearch: function () { if (!this.searching) { - return false; + return false } - return this.items.filter(i => !i.Read).length; + return this.items.filter(i => !i.Read).length } }, mounted() { - this.currentPath = window.location.hash.slice(1); + this.currentPath = window.location.hash.slice(1) window.addEventListener('hashchange', () => { - this.currentPath = window.location.hash.slice(1); - }); + this.currentPath = window.location.hash.slice(1) + }) this.notificationsSupported = window.isSecureContext - && ("Notification" in window && Notification.permission !== "denied"); - this.notificationsEnabled = this.notificationsSupported && Notification.permission == "granted"; + && ("Notification" in window && Notification.permission !== "denied") + this.notificationsEnabled = this.notificationsSupported && Notification.permission == "granted" Tinycon.setOptions({ height: 11, background: '#dd0000', fallback: false - }); + }) moment.updateLocale('en', { relativeTime: { @@ -119,11 +121,11 @@ export default { y: "a year", yy: "%d years" } - }); + }) - this.connect(); - this.getUISettings(); - this.loadMessages(); + this.connect() + this.getUISettings() + this.loadMessages() }, methods: { @@ -131,143 +133,143 @@ export default { let now = Date.now() // prevent double loading when UI loads & websocket connects if (this.lastLoaded && now - this.lastLoaded < 250) { - return; + return } if (this.start == 0) { - this.lastLoaded = now; + this.lastLoaded = now } - let self = this; - let params = {}; - self.selected = []; + let self = this + let params = {} + self.selected = [] - let uri = 'api/v1/messages'; + let uri = 'api/v1/messages' if (self.search) { - self.searching = true; - self.items = []; + self.searching = true + self.items = [] uri = 'api/v1/search' - self.start = 0; // search is displayed on one page - params['query'] = self.search; - params['limit'] = 200; + self.start = 0 // search is displayed on one page + params['query'] = self.search + params['limit'] = 200 } else { - self.searching = false; - params['limit'] = self.limit; + self.searching = false + params['limit'] = self.limit if (self.start > 0) { - params['start'] = self.start; + params['start'] = self.start } } self.get(uri, params, function (response) { - self.total = response.data.total; - self.unread = response.data.unread; - self.count = response.data.count; - self.start = response.data.start; - self.items = response.data.messages; - self.tags = response.data.tags; - self.existingTags = JSON.parse(JSON.stringify(self.tags)); + self.total = response.data.total + self.unread = response.data.unread + self.count = response.data.count + self.start = response.data.start + self.items = response.data.messages + self.tags = response.data.tags + self.existingTags = JSON.parse(JSON.stringify(self.tags)) // if pagination > 0 && results == 0 reload first page (prune) if (response.data.count == 0 && response.data.start > 0) { - self.start = 0; - return self.loadMessages(); + self.start = 0 + return self.loadMessages() } if (!self.scrollInPlace) { - let mp = document.getElementById('message-page'); + let mp = document.getElementById('message-page') if (mp) { - mp.scrollTop = 0; + mp.scrollTop = 0 } } - self.scrollInPlace = false; - }); + self.scrollInPlace = false + }) }, getUISettings: function () { - let self = this; + let self = this self.get('api/v1/webui', null, function (response) { - self.relayConfig = response.data; - }); + self.relayConfig = response.data + }) }, doSearch: function (e) { - e.preventDefault(); - this.loadMessages(); + e.preventDefault() + this.loadMessages() }, tagSearch: function (e, tag) { - e.preventDefault(); + e.preventDefault() if (tag.match(/ /)) { - tag = '"' + tag + '"'; + tag = '"' + tag + '"' } - this.search = 'tag:' + tag; - window.location.hash = ""; - this.loadMessages(); + this.search = 'tag:' + tag + window.location.hash = "" + this.loadMessages() }, resetSearch: function (e) { - e.preventDefault(); - this.search = ''; - this.scrollInPlace = true; - this.loadMessages(); + e.preventDefault() + this.search = '' + this.scrollInPlace = true + this.loadMessages() }, reloadMessages: function () { - this.search = ""; - this.start = 0; - this.loadMessages(); + this.search = "" + this.start = 0 + this.loadMessages() }, viewNext: function () { - this.start = parseInt(this.start, 10) + parseInt(this.limit, 10); - this.loadMessages(); + this.start = parseInt(this.start, 10) + parseInt(this.limit, 10) + this.loadMessages() }, viewPrev: function () { - let s = this.start - this.limit; + let s = this.start - this.limit if (s < 0) { - s = 0; + s = 0 } - this.start = s; - this.loadMessages(); + this.start = s + this.loadMessages() }, openMessage: function (id) { - let self = this; - self.selected = []; - self.releaseAddresses = false; - self.toastMessage = false; - self.existingTags = JSON.parse(JSON.stringify(self.tags)); + let self = this + self.selected = [] + self.releaseAddresses = false + self.toastMessage = false + self.existingTags = JSON.parse(JSON.stringify(self.tags)) let uri = 'api/v1/message/' + self.currentPath self.get(uri, false, function (response) { for (let i in self.items) { if (self.items[i].ID == self.currentPath) { if (!self.items[i].Read) { - self.items[i].Read = true; - self.unread--; + self.items[i].Read = true + self.unread-- } } } - let d = response.data; + let d = response.data // replace inline images embedded as inline attachments if (d.HTML && d.Inline) { for (let i in d.Inline) { - let a = d.Inline[i]; + let a = d.Inline[i] if (a.ContentID != '') { d.HTML = d.HTML.replace( new RegExp('(=["\']?)(cid:' + a.ContentID + ')(["|\'|\\s|\\/|>|;])', 'g'), '$1' + window.location.origin + window.location.pathname + 'api/v1/message/' + d.ID + '/part/' + a.PartID + '$3' - ); + ) } if (a.FileName.match(/^[a-zA-Z0-9\_\-\.]+$/)) { // some old email clients use the filename d.HTML = d.HTML.replace( new RegExp('(=["\']?)(' + a.FileName + ')(["|\'|\\s|\\/|>|;])', 'g'), '$1' + window.location.origin + window.location.pathname + 'api/v1/message/' + d.ID + '/part/' + a.PartID + '$3' - ); + ) } } } @@ -275,381 +277,381 @@ export default { // replace inline images embedded as regular attachments if (d.HTML && d.Attachments) { for (let i in d.Attachments) { - let a = d.Attachments[i]; + let a = d.Attachments[i] if (a.ContentID != '') { d.HTML = d.HTML.replace( new RegExp('(=["\']?)(cid:' + a.ContentID + ')(["|\'|\\s|\\/|>|;])', 'g'), '$1' + window.location.origin + window.location.pathname + 'api/v1/message/' + d.ID + '/part/' + a.PartID + '$3' - ); + ) } if (a.FileName.match(/^[a-zA-Z0-9\_\-\.]+$/)) { // some old email clients use the filename d.HTML = d.HTML.replace( new RegExp('(=["\']?)(' + a.FileName + ')(["|\'|\\s|\\/|>|;])', 'g'), '$1' + window.location.origin + window.location.pathname + 'api/v1/message/' + d.ID + '/part/' + a.PartID + '$3' - ); + ) } } } - self.message = d; + self.message = d // generate the prev/next links based on current message list - self.messagePrev = false; - self.messageNext = false; - let found = false; + self.messagePrev = false + self.messageNext = false + let found = false for (let i in self.items) { if (self.items[i].ID == self.message.ID) { - found = true; + found = true } else if (found && !self.messageNext) { - self.messageNext = self.items[i].ID; - break; + self.messageNext = self.items[i].ID + break } else { - self.messagePrev = self.items[i].ID; + self.messagePrev = self.items[i].ID } } - }); + }) }, // universal handler to delete current or selected messages deleteMessages: function () { - let ids = []; - let self = this; + let ids = [] + let self = this if (self.message) { - ids.push(self.message.ID); + ids.push(self.message.ID) } else { - ids = JSON.parse(JSON.stringify(self.selected)); + ids = JSON.parse(JSON.stringify(self.selected)) } if (!ids.length) { - return false; + return false } - let uri = 'api/v1/messages'; + let uri = 'api/v1/messages' self.delete(uri, { 'ids': ids }, function (response) { - window.location.hash = ""; - self.scrollInPlace = true; - self.loadMessages(); - }); + window.location.hash = "" + self.scrollInPlace = true + self.loadMessages() + }) }, // delete messages displayed in current search deleteSearch: function () { - let ids = this.items.map(item => item.ID); + let ids = this.items.map(item => item.ID) if (!ids.length) { - return false; + return false } - let self = this; - let uri = 'api/v1/messages'; + let self = this + let uri = 'api/v1/messages' self.delete(uri, { 'ids': ids }, function (response) { - window.location.hash = ""; - self.scrollInPlace = true; - self.loadMessages(); - }); + window.location.hash = "" + self.scrollInPlace = true + self.loadMessages() + }) }, // delete all messages from mailbox deleteAll: function () { - let self = this; - let uri = 'api/v1/messages'; + let self = this + let uri = 'api/v1/messages' self.delete(uri, false, function (response) { - window.location.hash = ""; - self.reloadMessages(); - }); + window.location.hash = "" + self.reloadMessages() + }) }, // mark current message as read markUnread: function () { - let self = this; + let self = this if (!self.message) { - return false; + return false } - let uri = 'api/v1/messages'; + let uri = 'api/v1/messages' self.put(uri, { 'read': false, 'ids': [self.message.ID] }, function (response) { - window.location.hash = ""; - self.scrollInPlace = true; - self.loadMessages(); - }); + window.location.hash = "" + self.scrollInPlace = true + self.loadMessages() + }) }, // mark all messages in mailbox as read markAllRead: function () { - let self = this; + let self = this let uri = 'api/v1/messages' self.put(uri, { 'read': true }, function (response) { - window.location.hash = ""; - self.scrollInPlace = true; - self.loadMessages(); - }); + window.location.hash = "" + self.scrollInPlace = true + self.loadMessages() + }) }, // mark messages in current search as read markSearchRead: function () { - let ids = this.items.map(item => item.ID); + let ids = this.items.map(item => item.ID) if (!ids.length) { - return false; + return false } - let self = this; - let uri = 'api/v1/messages'; + let self = this + let uri = 'api/v1/messages' self.put(uri, { 'read': true, 'ids': ids }, function (response) { - window.location.hash = ""; - self.scrollInPlace = true; - self.loadMessages(); - }); + window.location.hash = "" + self.scrollInPlace = true + self.loadMessages() + }) }, // mark selected messages as read markSelectedRead: function () { - let self = this; + let self = this if (!self.selected.length) { - return false; + return false } - let uri = 'api/v1/messages'; + let uri = 'api/v1/messages' self.put(uri, { 'read': true, 'ids': self.selected }, function (response) { - window.location.hash = ""; - self.scrollInPlace = true; - self.loadMessages(); - }); + window.location.hash = "" + self.scrollInPlace = true + self.loadMessages() + }) }, // mark selected messages as unread markSelectedUnread: function () { - let self = this; + let self = this if (!self.selected.length) { - return false; + return false } - let uri = 'api/v1/messages'; + let uri = 'api/v1/messages' self.put(uri, { 'read': false, 'ids': self.selected }, function (response) { - window.location.hash = ""; - self.scrollInPlace = true; - self.loadMessages(); - }); + window.location.hash = "" + self.scrollInPlace = true + self.loadMessages() + }) }, // test if any selected emails are unread selectedHasUnread: function () { if (!this.selected.length) { - return false; + return false } for (let i in this.items) { if (this.isSelected(this.items[i].ID) && !this.items[i].Read) { - return true; + return true } } - return false; + return false }, // test of any selected emails are read selectedHasRead: function () { if (!this.selected.length) { - return false; + return false } for (let i in this.items) { if (this.isSelected(this.items[i].ID) && this.items[i].Read) { - return true; + return true } } - return false; + return false }, // websocket connect connect: function () { - let wsproto = location.protocol == 'https:' ? 'wss' : 'ws'; + let wsproto = location.protocol == 'https:' ? 'wss' : 'ws' let ws = new WebSocket( wsproto + "://" + document.location.host + document.location.pathname + "api/events" - ); - let self = this; + ) + let self = this ws.onmessage = function (e) { - let response = JSON.parse(e.data); + let response = JSON.parse(e.data) if (!response) { - return; + return } // new messages if (response.Type == "new" && response.Data) { if (!self.searching) { if (self.start < 1) { // first page - self.items.unshift(response.Data); + self.items.unshift(response.Data) if (self.items.length > self.limit) { - self.items.pop(); + self.items.pop() } // first message was open, set messagePrev if (!self.messagePrev) { - self.messagePrev = response.Data.ID; + self.messagePrev = response.Data.ID } } else { - self.start++; + self.start++ } } - self.total++; - self.unread++; + self.total++ + self.unread++ for (let i in response.Data.Tags) { if (self.tags.indexOf(response.Data.Tags[i]) < 0) { - self.tags.push(response.Data.Tags[i]); - self.tags.sort(); + self.tags.push(response.Data.Tags[i]) + self.tags.sort() } } - let from = response.Data.From != null ? response.Data.From.Address : '[unknown]'; - self.browserNotify("New mail from: " + from, response.Data.Subject); - self.setMessageToast(response.Data); + let from = response.Data.From != null ? response.Data.From.Address : '[unknown]' + self.browserNotify("New mail from: " + from, response.Data.Subject) + self.setMessageToast(response.Data) } else if (response.Type == "prune") { // messages have been deleted, reload messages to adjust - self.scrollInPlace = true; - self.loadMessages(); + self.scrollInPlace = true + self.loadMessages() } } ws.onopen = function () { - self.isConnected = true; - self.loadMessages(); + self.isConnected = true + self.loadMessages() } ws.onclose = function (e) { - self.isConnected = false; + self.isConnected = false setTimeout(function () { - self.connect(); // reconnect - }, 1000); + self.connect() // reconnect + }, 1000) } ws.onerror = function (err) { - ws.close(); + ws.close() } }, getPrimaryEmailTo: function (message) { for (let i in message.To) { - return message.To[i].Address; + return message.To[i].Address } - return '[ Undisclosed recipients ]'; + return '[ Undisclosed recipients ]' }, getRelativeCreated: function (message) { let d = new Date(message.Created) - return moment(d).fromNow().toString(); + return moment(d).fromNow().toString() }, browserNotify: function (title, message) { if (!("Notification" in window)) { - return; + return } if (Notification.permission === "granted") { - let b = message.Subject; + let b = message.Subject let options = { body: message, icon: 'notification.png' } - new Notification(title, options); + new Notification(title, options) } }, requestNotifications: function () { // check if the browser supports notifications if (!("Notification" in window)) { - alert("This browser does not support desktop notification"); + alert("This browser does not support desktop notification") } // we need to ask the user for permission else if (Notification.permission !== "denied") { - let self = this; + let self = this Notification.requestPermission().then(function (permission) { // if the user accepts, let's create a notification if (permission === "granted") { - self.browserNotify("Notifications enabled", "You will receive notifications when new mails are received."); - self.notificationsEnabled = true; + self.browserNotify("Notifications enabled", "You will receive notifications when new mails are received.") + self.notificationsEnabled = true } - }); + }) } }, toggleSelected: function (e, id) { - e.preventDefault(); + e.preventDefault() if (this.isSelected(id)) { this.selected = this.selected.filter(function (ele) { - return ele != id; - }); + return ele != id + }) } else { - this.selected.push(id); + this.selected.push(id) } }, selectRange: function (e, id) { - e.preventDefault(); + e.preventDefault() - let selecting = false; - let lastSelected = this.selected.length > 0 && this.selected[this.selected.length - 1]; + let selecting = false + let lastSelected = this.selected.length > 0 && this.selected[this.selected.length - 1] if (lastSelected == id) { this.selected = this.selected.filter(function (ele) { - return ele != id; - }); - return; + return ele != id + }) + return } if (lastSelected === false) { - this.selected.push(id); - return; + this.selected.push(id) + return } for (let d of this.items) { if (selecting) { if (!this.isSelected(d.ID)) { - this.selected.push(d.ID); + this.selected.push(d.ID) } if (d.ID == lastSelected || d.ID == id) { // reached backwards select - break; + break } } else if (d.ID == id || d.ID == lastSelected) { if (!this.isSelected(d.ID)) { - this.selected.push(d.ID); + this.selected.push(d.ID) } - selecting = true; + selecting = true } } }, isSelected: function (id) { - return this.selected.indexOf(id) != -1; + return this.selected.indexOf(id) != -1 }, inSearch: function (tag) { - tag = tag.toLowerCase(); + tag = tag.toLowerCase() if (tag.match(/ /)) { - tag = '"' + tag + '"'; + tag = '"' + tag + '"' } - return this.search.toLowerCase().indexOf('tag:' + tag) > -1; + return this.search.toLowerCase().indexOf('tag:' + tag) > -1 }, loadInfo: function (e) { - e.preventDefault(); - let self = this; + e.preventDefault() + let self = this self.get('api/v1/info', false, function (response) { - self.appInfo = response.data; - self.modal('AppInfoModal').show(); - }); + self.appInfo = response.data + self.modal('AppInfoModal').show() + }) }, downloadMessageBody: function (str, ext) { - let dl = document.createElement('a'); - dl.href = "data:text/plain," + encodeURIComponent(str); - dl.target = '_blank'; - dl.download = this.message.ID + '.' + ext; - dl.click(); + let dl = document.createElement('a') + dl.href = "data:text/plain," + encodeURIComponent(str) + dl.target = '_blank' + dl.download = this.message.ID + '.' + ext + dl.click() }, initReleaseModal: function () { - this.releaseAddresses = false; - let addresses = []; + this.releaseAddresses = false + let addresses = [] for (let i in this.message.To) { addresses.push(this.message.To[i].Address) } @@ -661,31 +663,31 @@ export default { } // include only unique email addresses, regardless of casing - let uAddresses = new Map(addresses.map(a => [a.toLowerCase(), a])); - this.releaseAddresses = [...uAddresses.values()]; + let uAddresses = new Map(addresses.map(a => [a.toLowerCase(), a])) + this.releaseAddresses = [...uAddresses.values()] - let self = this; + let self = this window.setTimeout(function () { // delay to allow elements to load - self.modal('ReleaseModal').show(); + self.modal('ReleaseModal').show() window.setTimeout(function () { document.querySelector('#ReleaseModal input[role="combobox"]').focus() - }, 500); - }, 300); + }, 500) + }, 300) }, setMessageToast: function (m) { // don't display if browser notifications are enabled, or a toast is already displayed if (this.notificationsEnabled || this.toastMessage) { - return; + return } - this.toastMessage = m; + this.toastMessage = m }, clearMessageToast: function () { - this.toastMessage = false; + this.toastMessage = false } } } @@ -775,13 +777,13 @@ export default { Mailpit Mailpit -
+
-
@@ -807,8 +809,8 @@ export default { - @@ -920,7 +922,7 @@ export default { -
+
- - - - - - - - - - - - - - - - - - - - - diff --git a/server/ui-src/app.js b/server/ui-src/app.js index dddf71b..4861314 100644 --- a/server/ui-src/app.js +++ b/server/ui-src/app.js @@ -1,8 +1,7 @@ import { createApp } from 'vue'; import App from './App.vue'; import "./assets/styles.scss"; -import "../../node_modules/bootstrap-icons/font/bootstrap-icons.scss"; +import "bootstrap-icons/font/bootstrap-icons.scss"; import "bootstrap"; -import "./color-modes"; createApp(App).mount('#app'); diff --git a/server/ui-src/assets/_bootstrap_variables.scss b/server/ui-src/assets/_bootstrap_variables.scss index 9f0ec33..f4ac6df 100644 --- a/server/ui-src/assets/_bootstrap_variables.scss +++ b/server/ui-src/assets/_bootstrap_variables.scss @@ -6,3 +6,4 @@ $link-decoration: none; $primary: #2c3e50; $list-group-disabled-color: #adb5bd; $enable-negative-margins: true; +$body-color-dark: #e7eaed; diff --git a/server/ui-src/assets/styles.scss b/server/ui-src/assets/styles.scss index f278445..85de0a6 100644 --- a/server/ui-src/assets/styles.scss +++ b/server/ui-src/assets/styles.scss @@ -56,8 +56,17 @@ z-index: 1500; } -.message.read:not(.active):not(.selected) { - color: $gray-500; +.message { + &.read { + color: $text-muted; + + b { + font-weight: normal; + } + } + &.selected { + background: var(--bs-primary-bg-subtle); + } } #nav-plain-text .text-view, @@ -180,20 +189,6 @@ border-top: 0; } -.message.selected { - background: $gray-300; - - .text-muted { - color: $body-color !important; - } - - &.read { - b { - font-weight: normal; - } - } -} - body.blur { .privacy { filter: blur(3px); @@ -280,8 +275,8 @@ body.blur { https://prismjs.com/download.html#themes=prism-coy&languages=markup+css */ code[class*="language-"], pre[class*="language-"] { - color: #000; - background: 0 0; + // color: #000; + // background: 0 0; font-size: 0.85em; text-align: left; white-space: pre; @@ -314,7 +309,7 @@ code[class*="language-"] { } :not(pre) > code[class*="language-"], pre[class*="language-"] { - background-color: #fdfdfd; + // background-color: #fdfdfd; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; @@ -364,7 +359,7 @@ pre[class*="language-"] { .token.url, .token.variable { color: #a67f59; - background: rgba(255, 255, 255, 0.5); + // background: rgba(255, 255, 255, 0.5); } .token.atrule, .token.attr-value, @@ -379,7 +374,7 @@ pre[class*="language-"] { .language-css .token.string, .style .token.string { color: #a67f59; - background: rgba(255, 255, 255, 0.5); + // background: rgba(255, 255, 255, 0.5); } .token.important { font-weight: 400; @@ -390,9 +385,9 @@ pre[class*="language-"] { .token.italic { font-style: italic; } -.token.entity { - cursor: help; -} +// .token.entity { +// cursor: help; +// } .token.namespace { opacity: 0.7; } diff --git a/server/ui-src/color-modes.js b/server/ui-src/color-modes.js deleted file mode 100644 index f9c437e..0000000 --- a/server/ui-src/color-modes.js +++ /dev/null @@ -1,94 +0,0 @@ -/*! - * Color mode toggler for Bootstrap's docs (https://getbootstrap.com/) - * Copyright 2011-2023 The Bootstrap Authors - * Licensed under the Creative Commons Attribution 3.0 Unported License. - */ - -(() => { - 'use strict'; - - const getStoredTheme = () => localStorage.getItem('theme'); - const setStoredTheme = (theme) => localStorage.setItem('theme', theme); - - const getPreferredTheme = () => { - const storedTheme = getStoredTheme(); - if (storedTheme) { - return storedTheme; - } - - return window.matchMedia('(prefers-color-scheme: dark)').matches - ? 'dark' - : 'light'; - }; - - const setTheme = (theme) => { - if ( - theme === 'auto' && - window.matchMedia('(prefers-color-scheme: dark)').matches - ) { - document.documentElement.setAttribute('data-bs-theme', 'dark'); - } else { - document.documentElement.setAttribute('data-bs-theme', theme); - } - }; - - setTheme(getPreferredTheme()); - - const showActiveTheme = (theme, focus = false) => { - const themeSwitcher = document.querySelector('#bd-theme'); - - if (!themeSwitcher) { - return; - } - - const themeSwitcherText = document.querySelector('#bd-theme-text'); - const activeThemeIcon = document.querySelector('.theme-icon-active use'); - const btnToActive = document.querySelector( - `[data-bs-theme-value="${theme}"]` - ); - const svgOfActiveBtn = btnToActive - .querySelector('svg use') - .getAttribute('href'); - - document.querySelectorAll('[data-bs-theme-value]').forEach((element) => { - element.classList.remove('active'); - element.setAttribute('aria-pressed', 'false'); - }); - - btnToActive.classList.add('active'); - btnToActive.setAttribute('aria-pressed', 'true'); - activeThemeIcon.setAttribute('href', svgOfActiveBtn); - const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})`; - themeSwitcher.setAttribute('aria-label', themeSwitcherLabel); - - if (focus) { - themeSwitcher.focus(); - } - }; - - window - .matchMedia('(prefers-color-scheme: dark)') - .addEventListener('change', () => { - const storedTheme = getStoredTheme(); - if (storedTheme !== 'light' && storedTheme !== 'dark') { - setTheme(getPreferredTheme()); - } - }); - - window.addEventListener('DOMContentLoaded', () => { - showActiveTheme(getPreferredTheme()); - - document.querySelectorAll('[data-bs-theme-value]').forEach((toggle) => { - toggle.addEventListener('click', () => { - const theme = toggle.getAttribute('data-bs-theme-value'); - setStoredTheme(theme); - setTheme(theme); - showActiveTheme(theme, true); - }); - }); - }); -})(); - -document.querySelectorAll('[data-bs-toggle="popover"]').forEach((popover) => { - new bootstrap.Popover(popover); -}); diff --git a/server/ui-src/templates/Attachments.vue b/server/ui-src/templates/Attachments.vue index a13cf68..c4457ee 100644 --- a/server/ui-src/templates/Attachments.vue +++ b/server/ui-src/templates/Attachments.vue @@ -14,14 +14,18 @@ export default {