From 441ea68fd7018d6dc92253dd945fefd58b1990e7 Mon Sep 17 00:00:00 2001 From: kuu7o Date: Wed, 19 Nov 2025 21:06:36 -0300 Subject: [PATCH] chore+feat(css): added styling for follow button and new search in navbar --- src/sass/general.scss | 49 +++++++++++++++++++ src/sass/include/_mixins.css | 17 +------ src/sass/navbar.scss | 12 ++++- src/sass/search.scss | 92 +++++++++++++++++++++++++++++------- src/views/general.nim | 9 ++-- src/views/search.nim | 70 ++------------------------- src/views/search_panel.nim | 58 +++++++++++++++++++++++ 7 files changed, 202 insertions(+), 105 deletions(-) create mode 100644 src/views/search_panel.nim diff --git a/src/sass/general.scss b/src/sass/general.scss index 9feb3d3..6cefd56 100644 --- a/src/sass/general.scss +++ b/src/sass/general.scss @@ -37,3 +37,52 @@ height: unset; } } + +.follow-form { + display: inline-block; + pointer-events: all; +} + +.follow-button { + background-color: transparent; + color: var(--accent); + border: 1px solid var(--accent); + border-radius: 50%; + padding: 4px 12px; + font-weight: 700; + font-size: 14px; + cursor: pointer; + line-height: 1.2; + transition: background-color 0.2s; + outline: none; + + &:hover { + background-color: var(--bg_hover); + } + + &.unfollow { + color: #e0245e; + border-color: #e0245e; + + &:hover { + background-color: rgba(224, 36, 94, 0.1); + } + } +} + +.profile-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + flex: 1; + min-width: 0; +} + +.tweet-name-row .follow-form { + margin-left: 10px; +} + +.tweet-name-row .follow-button { + padding: 2px 10px; + font-size: 13px; +} diff --git a/src/sass/include/_mixins.css b/src/sass/include/_mixins.css index 94e11ee..7264ad2 100644 --- a/src/sass/include/_mixins.css +++ b/src/sass/include/_mixins.css @@ -61,23 +61,8 @@ @mixin search-resize($width, $rows) { @media(max-width: $width) { - .search-toggles { - grid-template-columns: repeat($rows, auto); - } - #search-panel-toggle:checked ~ .search-panel { - @if $rows == 6 { - max-height: 200px !important; - } - @if $rows == 5 { - max-height: 300px !important; - } - @if $rows == 4 { - max-height: 300px !important; - } - @if $rows == 3 { - max-height: 365px !important; - } + max-height: 80vh !important; } } } diff --git a/src/sass/navbar.scss b/src/sass/navbar.scss index 47a8765..4b48533 100644 --- a/src/sass/navbar.scss +++ b/src/sass/navbar.scss @@ -25,6 +25,14 @@ nav { align-items: center; flex-basis: 920px; height: 50px; + gap: 15px; +} + +.inner-nav .search-field { + flex: 1; + margin: 0 auto; + max-width: 350px; + min-width: 0; } .site-name { @@ -46,11 +54,11 @@ nav { .nav-item { display: flex; - flex: 1; + flex: 0 0 auto; line-height: 50px; height: 50px; overflow: hidden; - flex-wrap: wrap; + flex-wrap: nowrap; align-items: center; &.right { diff --git a/src/sass/search.scss b/src/sass/search.scss index f70f7ea..8afc930 100644 --- a/src/sass/search.scss +++ b/src/sass/search.scss @@ -9,19 +9,22 @@ .search-field { display: flex; - flex-wrap: wrap; + flex-wrap: nowrap; + position: relative; button { margin: 0 2px 0 0; height: 23px; display: flex; align-items: center; + flex: 0 0 auto; } .pref-input { margin: 0 4px 0 0; flex-grow: 1; height: 23px; + min-width: 0; } input[type="text"] { @@ -30,49 +33,86 @@ } > label { - display: inline; + display: flex; + align-items: center; + height: 23px; background-color: var(--bg_elements); color: var(--fg_color); border: 1px solid var(--accent_border); - padding: 1px 6px 2px 6px; + padding: 0 6px; font-size: 14px; cursor: pointer; - margin-bottom: 2px; + margin-bottom: 0; + box-sizing: border-box; @include input-colors; } - @include create-toggle(search-panel, 200px); + @include create-toggle(search-panel, 600px); } .search-panel { width: 100%; + margin-top: 5px; + border: 1px solid var(--accent_border); max-height: 0; - overflow: hidden; + overflow-y: auto; + overflow-x: hidden; transition: max-height 0.4s; + + position: absolute; + top: 100%; + left: 0; + background-color: var(--bg_overlays); + box-shadow: 0 4px 6px rgba(0,0,0,0.3); + border-radius: 0 0 4px 4px; + z-index: 2000; + padding: 5px 10px; + box-sizing: border-box; + + opacity: 0; + visibility: hidden; + transition: max-height 0.4s, opacity 0.4s, visibility 0.4s; - flex-grow: 1; font-weight: initial; text-align: left; + font-size: 13px; > div { - line-height: 1.7em; + line-height: 1.4em; + margin-bottom: 4px; } .checkbox-container { - display: inline; + display: inline-flex; + align-items: center; padding-right: unset; - margin-bottom: unset; - margin-left: 23px; + margin-bottom: 2px; + margin-left: 18px; + margin-right: 0; + white-space: nowrap; } .checkbox { right: unset; - left: -22px; + left: -18px; + width: 15px; + height: 15px; } .checkbox-container .checkbox:after { - top: -4px; + top: 0; + left: 1px; + bottom: unset; + font-size: 13px; + width: auto; + height: auto; + } + + .search-title { + font-size: 13px; + margin-top: 2px; + margin-bottom: 2px; } } @@ -92,11 +132,11 @@ .pref-input { display: block; - padding-bottom: 5px; + padding-bottom: 2px; input { height: 21px; - margin-top: 1px; + margin-top: 0; } } } @@ -104,8 +144,13 @@ .search-toggles { flex-grow: 1; display: grid; - grid-template-columns: repeat(6, auto); - grid-column-gap: 10px; + grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); + gap: 5px 10px; +} + +#search-panel-toggle:checked ~ .search-panel { + opacity: 1; + visibility: visible; } .profile-tabs { @@ -120,3 +165,16 @@ @include search-resize(560px, 5); @include search-resize(480px, 4); @include search-resize(410px, 3); + +@media(max-width: 600px) { + .search-panel { + position: fixed; + top: 50px; + left: 0; + right: 0; + width: 100%; + border-radius: 0; + border-top: 1px solid var(--accent_border); + box-shadow: 0 4px 6px rgba(0,0,0,0.3); + } +} diff --git a/src/views/general.nim b/src/views/general.nim index 8bc0719..4f52b1a 100644 --- a/src/views/general.nim +++ b/src/views/general.nim @@ -2,8 +2,8 @@ import uri, strutils, strformat import karax/[karaxdsl, vdom] -import renderutils -import ../utils, ../types, ../prefs, ../formatters +import renderutils, search_panel +import ../utils, ../types, ../prefs, ../formatters, ../query, tables import jester @@ -20,14 +20,17 @@ proc renderNavbar(cfg: Config; req: Request; rss, canonical: string): VNode = path = $(parseUri(req.path) ? filterParams(req.params)) if "/status/" in path: path.add "#m" + let query = initQuery(req.params) + buildHtml(nav): tdiv(class="inner-nav"): tdiv(class="nav-item"): a(class="site-name", href="/"): text cfg.title a(href="/about"): text "(donate)" + renderSearchPanel(query) + tdiv(class="nav-item right"): - icon "search", title="Search", href="/search" if cfg.enableRss and rss.len > 0: icon "rss", title="RSS Feed", href=rss icon "bird", title="Open in Twitter", href=canonical diff --git a/src/views/search.nim b/src/views/search.nim index e6c22c0..c2d39f5 100644 --- a/src/views/search.nim +++ b/src/views/search.nim @@ -1,33 +1,13 @@ # SPDX-License-Identifier: AGPL-3.0-only -import strutils, strformat, sequtils, unicode, tables, options +import strutils, options import karax/[karaxdsl, vdom] import renderutils, timeline import ".."/[types, query] -const toggles = { - "nativeretweets": "Retweets", - "media": "Media", - "videos": "Videos", - "news": "News", - "verified": "Verified", - "native_video": "Native videos", - "replies": "Replies", - "links": "Links", - "images": "Images", - "safe": "Safe", - "quote": "Quotes", - "pro_video": "Pro videos" -}.toOrderedTable - proc renderSearch*(): VNode = buildHtml(tdiv(class="panel-container")): - tdiv(class="search-bar"): - form(`method`="get", action="/search", autocomplete="off"): - hiddenField("f", "tweets") - input(`type`="text", name="q", autofocus="", - placeholder="Search...", dir="auto") - button(`type`="submit"): icon "search" + discard proc renderProfileTabs*(query: Query; username: string): VNode = let link = "/" & username @@ -51,43 +31,6 @@ proc renderSearchTabs*(query: Query): VNode = q.kind = users a(href=("?" & genQueryUrl(q))): text "Users" -proc isPanelOpen(q: Query): bool = - q.fromUser.len == 0 and (q.filters.len > 0 or q.excludes.len > 0 or - @[q.near, q.until, q.since].anyIt(it.len > 0)) - -proc renderSearchPanel*(query: Query): VNode = - let user = query.fromUser.join(",") - let action = if user.len > 0: &"/{user}/search" else: "/search" - buildHtml(form(`method`="get", action=action, - class="search-field", autocomplete="off")): - hiddenField("f", "tweets") - genInput("q", "", query.text, "Enter search...", class="pref-inline") - button(`type`="submit"): icon "search" - - input(id="search-panel-toggle", `type`="checkbox", checked=isPanelOpen(query)) - label(`for`="search-panel-toggle"): icon "down" - - tdiv(class="search-panel"): - for f in @["filter", "exclude"]: - span(class="search-title"): text capitalize(f) - tdiv(class="search-toggles"): - for k, v in toggles: - let state = - if f == "filter": k in query.filters - else: k in query.excludes - genCheckbox(&"{f[0]}-{k}", v, state) - - tdiv(class="search-row"): - tdiv: - span(class="search-title"): text "Time range" - tdiv(class="date-range"): - genDate("since", query.since) - span(class="search-title"): text "-" - genDate("until", query.until) - tdiv: - span(class="search-title"): text "Near" - genInput("near", "", query.near, "Location...", autofocus=false) - proc renderTweetSearch*(results: Timeline; prefs: Prefs; path: string; pinned=none(Tweet)): VNode = let query = results.query @@ -100,8 +43,7 @@ proc renderTweetSearch*(results: Timeline; prefs: Prefs; path: string; renderProfileTabs(query, query.fromUser.join(",")) if query.fromUser.len == 0 or query.kind == tweets: - tdiv(class="timeline-header"): - renderSearchPanel(query) + discard if query.fromUser.len == 0: renderSearchTabs(query) @@ -110,11 +52,5 @@ proc renderTweetSearch*(results: Timeline; prefs: Prefs; path: string; proc renderUserSearch*(results: Result[User]; prefs: Prefs; path: string): VNode = buildHtml(tdiv(class="timeline-container")): - tdiv(class="timeline-header"): - form(`method`="get", action="/search", class="search-field", autocomplete="off"): - hiddenField("f", "users") - genInput("q", "", results.query.text, "Enter username...", class="pref-inline") - button(`type`="submit"): icon "search" - renderSearchTabs(results.query) renderTimelineUsers(results, prefs, path) diff --git a/src/views/search_panel.nim b/src/views/search_panel.nim new file mode 100644 index 0000000..ee32cdb --- /dev/null +++ b/src/views/search_panel.nim @@ -0,0 +1,58 @@ +# SPDX-License-Identifier: AGPL-3.0-only +import strutils, strformat, sequtils, unicode, tables +import karax/[karaxdsl, vdom] + +import renderutils +import ".."/[types] + +const toggles = { + "nativeretweets": "Retweets", + "media": "Media", + "videos": "Videos", + "news": "News", + "verified": "Verified", + "native_video": "Native videos", + "replies": "Replies", + "links": "Links", + "images": "Images", + "safe": "Safe", + "quote": "Quotes", + "pro_video": "Pro videos" +}.toOrderedTable + +proc isPanelOpen(q: Query): bool = + q.fromUser.len == 0 and (q.filters.len > 0 or q.excludes.len > 0 or + @[q.near, q.until, q.since].anyIt(it.len > 0)) + +proc renderSearchPanel*(query: Query): VNode = + let user = query.fromUser.join(",") + let action = if user.len > 0: &"/{user}/search" else: "/search" + buildHtml(form(`method`="get", action=action, + class="search-field", autocomplete="off")): + hiddenField("f", "tweets") + genInput("q", "", query.text, "Enter search...", class="pref-inline") + button(`type`="submit"): icon "search" + + input(id="search-panel-toggle", `type`="checkbox", checked=isPanelOpen(query)) + label(`for`="search-panel-toggle"): icon "down" + + tdiv(class="search-panel"): + for f in @["filter", "exclude"]: + span(class="search-title"): text capitalize(f) + tdiv(class="search-toggles"): + for k, v in toggles: + let state = + if f == "filter": k in query.filters + else: k in query.excludes + genCheckbox(&"{f[0]}-{k}", v, state) + + tdiv(class="search-row"): + tdiv: + span(class="search-title"): text "Time range" + tdiv(class="date-range"): + genDate("since", query.since) + span(class="search-title"): text "-" + genDate("until", query.until) + tdiv: + span(class="search-title"): text "Near" + genInput("near", "", query.near, "Location...", autofocus=false)