diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fc19a8..1592bb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,14 @@ + +## Version 1.19.0 - 2026-01-03 + +update css, html, js files + +**Changes:** 4 files, 311 lines +**Languages:** CSS (137 lines), HTML (3 lines), JavaScript (171 lines) + ## Version 1.18.0 - 2026-01-03 update html, js files diff --git a/pyproject.toml b/pyproject.toml index 6fd6625..e7635a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "Snek" -version = "1.18.0" +version = "1.19.0" readme = "README.md" #license = { file = "LICENSE", content-type="text/markdown" } description = "Snek Chat Application by Molodetz" diff --git a/src/snek/static/base.css b/src/snek/static/base.css index cd91467..aead386 100644 --- a/src/snek/static/base.css +++ b/src/snek/static/base.css @@ -497,13 +497,14 @@ a { header { top: 0; left: 0; - text-overflow: ellipsis; width: 100%; display: flex; - flex-direction: column; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 8px; .logo { - display: block; flex: 1; text-overflow: ellipsis; white-space: nowrap; @@ -512,15 +513,10 @@ a { h2 { font-size: 14px; } - - text-align: center; } nav { - text-align: right; - flex: 1; - display: block; - width: 100%; + display: none; } } diff --git a/src/snek/static/channel-menu.css b/src/snek/static/channel-menu.css new file mode 100644 index 0000000..c14b2fb --- /dev/null +++ b/src/snek/static/channel-menu.css @@ -0,0 +1,123 @@ +/* retoor */ + +channel-menu { + display: none; + position: relative; +} + +@media (max-width: 768px) { + channel-menu { + display: inline-block; + } +} + +.channel-menu-toggle { + display: flex; + align-items: center; + justify-content: center; + padding: 4px 8px; + background: none; + color: #888; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 1.2em; + transition: all 0.2s ease; +} + +.channel-menu-toggle:hover { + color: #fff; + background-color: rgba(255, 255, 255, 0.05); +} + +channel-menu[open] .channel-menu-toggle { + color: #fff; + background-color: rgba(255, 255, 255, 0.05); +} + +.channel-menu-panel { + position: absolute; + top: 100%; + right: 0; + margin-top: 8px; + background-color: #111; + border: 1px solid #333; + border-radius: 8px; + padding: 8px 0; + display: none; + flex-direction: column; + min-width: 200px; + max-height: 60vh; + overflow-y: auto; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); + z-index: 1000; +} + +channel-menu[open] .channel-menu-panel { + display: flex; +} + +.channel-menu-section { + padding: 8px 16px 4px; + font-size: 0.75em; + color: #666; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.channel-menu-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 10px 16px; + color: #888; + text-decoration: none; + font-size: 0.95em; + transition: all 0.2s ease; + cursor: pointer; + border: none; + background: none; + width: 100%; + text-align: left; +} + +.channel-menu-item:hover { + color: #fff; + background-color: rgba(255, 255, 255, 0.05); +} + +.channel-menu-item.active { + color: #fff; + background-color: rgba(255, 255, 255, 0.1); +} + +.channel-menu-name { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.channel-menu-count { + background: #f05a28; + color: #fff; + border-radius: 10px; + padding: 2px 8px; + font-size: 0.8em; + min-width: 20px; + text-align: center; +} + +.channel-menu-empty { + padding: 16px; + color: #666; + font-size: 0.9em; + text-align: center; +} + +.channel-menu-divider { + height: 1px; + background: #333; + margin: 8px 0; +} diff --git a/src/snek/static/channel-menu.js b/src/snek/static/channel-menu.js new file mode 100644 index 0000000..5bd1bbb --- /dev/null +++ b/src/snek/static/channel-menu.js @@ -0,0 +1,171 @@ +// retoor + +import { app } from "./app.js"; + +class ChannelMenu extends HTMLElement { + constructor() { + super(); + this._isOpen = false; + this._channels = []; + this._boundClickOutside = this._handleClickOutside.bind(this); + + this._container = document.createElement('div'); + this._container.className = 'channel-menu-container'; + + this._toggleButton = document.createElement('button'); + this._toggleButton.className = 'channel-menu-toggle'; + this._toggleButton.setAttribute('aria-label', 'Toggle channel menu'); + this._toggleButton.innerHTML = '💬'; + this._toggleButton.addEventListener('click', (e) => { + e.stopPropagation(); + this._toggle(); + }); + + this._menuPanel = document.createElement('div'); + this._menuPanel.className = 'channel-menu-panel'; + + this._container.appendChild(this._toggleButton); + this._container.appendChild(this._menuPanel); + this.appendChild(this._container); + } + + async connectedCallback() { + await this._loadChannels(); + app.addEventListener('channel-message', (data) => { + if (data.is_final && data.channel_uid) { + this._incrementCount(data.channel_uid); + } + }); + } + + async _loadChannels() { + try { + this._channels = await app.rpc.getChannels(); + this._renderChannels(); + } catch (e) { + this._menuPanel.innerHTML = '
Failed to load channels
'; + } + } + + _renderChannels() { + this._menuPanel.innerHTML = ''; + + const publicChannels = this._channels.filter(c => !c.is_private); + const privateChannels = this._channels.filter(c => c.is_private); + + if (publicChannels.length > 0) { + const header = document.createElement('div'); + header.className = 'channel-menu-section'; + header.textContent = 'Channels'; + this._menuPanel.appendChild(header); + + publicChannels.forEach(channel => { + this._menuPanel.appendChild(this._createChannelItem(channel)); + }); + } + + if (privateChannels.length > 0) { + if (publicChannels.length > 0) { + const divider = document.createElement('div'); + divider.className = 'channel-menu-divider'; + this._menuPanel.appendChild(divider); + } + + const header = document.createElement('div'); + header.className = 'channel-menu-section'; + header.textContent = 'Private'; + this._menuPanel.appendChild(header); + + privateChannels.forEach(channel => { + this._menuPanel.appendChild(this._createChannelItem(channel)); + }); + } + + if (this._channels.length === 0) { + this._menuPanel.innerHTML = '
No channels available
'; + } + } + + _createChannelItem(channel) { + const item = document.createElement('a'); + item.className = 'channel-menu-item'; + item.href = `/channel/${channel.uid}.html`; + item.dataset.channelUid = channel.uid; + + if (window.location.pathname.includes(channel.uid)) { + item.classList.add('active'); + } + + const name = document.createElement('span'); + name.className = 'channel-menu-name'; + name.textContent = channel.name; + if (channel.color) { + name.style.color = channel.color; + } + item.appendChild(name); + + if (channel.new_count > 0) { + const count = document.createElement('span'); + count.className = 'channel-menu-count'; + count.textContent = channel.new_count; + item.appendChild(count); + } + + item.addEventListener('click', () => { + this._close(); + }); + + return item; + } + + _incrementCount(channelUid) { + const item = this._menuPanel.querySelector(`[data-channel-uid="${channelUid}"]`); + if (item && !item.classList.contains('active')) { + let countEl = item.querySelector('.channel-menu-count'); + if (!countEl) { + countEl = document.createElement('span'); + countEl.className = 'channel-menu-count'; + countEl.textContent = '1'; + item.appendChild(countEl); + } else { + const current = parseInt(countEl.textContent) || 0; + countEl.textContent = current + 1; + } + } + } + + _toggle() { + if (this._isOpen) { + this._close(); + } else { + this._open(); + } + } + + _open() { + this._isOpen = true; + this.setAttribute('open', ''); + this._loadChannels(); + setTimeout(() => { + document.addEventListener('click', this._boundClickOutside); + }, 0); + } + + _close() { + this._isOpen = false; + this.removeAttribute('open'); + document.removeEventListener('click', this._boundClickOutside); + } + + _handleClickOutside(event) { + if (!this.contains(event.target)) { + this._close(); + } + } + + disconnectedCallback() { + document.removeEventListener('click', this._boundClickOutside); + } +} + +customElements.define('channel-menu', ChannelMenu); diff --git a/src/snek/templates/app.html b/src/snek/templates/app.html index 5ceb4a2..96264e4 100644 --- a/src/snek/templates/app.html +++ b/src/snek/templates/app.html @@ -14,6 +14,7 @@ + @@ -35,12 +36,14 @@ +
+