diff --git a/assets/css/common/measurements.css b/assets/css/common/measurements.css index 03bbfc84..3ef1035e 100644 --- a/assets/css/common/measurements.css +++ b/assets/css/common/measurements.css @@ -4,6 +4,7 @@ $narrow-layout-width: 960px; $medium-layout-width: $min-desktop-width; $font-family-base: verdana, arial, helvetica, sans-serif; $font-family-monospace: "Droid Sans Mono", monospace; +$font-family-heading: $font-family-base; :root { /* Paddings */ @@ -84,6 +85,7 @@ $font-family-monospace: "Droid Sans Mono", monospace; /* Fonts */ --font-family: $font-family-base; --font-family-monospace: $font-family-monospace; + --font-family-heading: $font-family-heading; --font-size: 14px; --font-tiny-size: 12px; --font-micro-size: 10px; diff --git a/assets/css/elements/button.css b/assets/css/elements/button.css index 51aecd7e..644f2b8f 100644 --- a/assets/css/elements/button.css +++ b/assets/css/elements/button.css @@ -150,6 +150,10 @@ border-radius: var(--border-radius-inner); } +.button--transparent { + background: 0 !important; +} + @mixin button-type primary; @mixin button-type secondary; @mixin button-type success; diff --git a/assets/css/elements/flex.css b/assets/css/elements/flex.css index 77132562..75882c3d 100644 --- a/assets/css/elements/flex.css +++ b/assets/css/elements/flex.css @@ -91,6 +91,11 @@ flex: 1 0 auto; } +.flex__break { + flex-basis: 100%; + width: 0; +} + @mixin if-desktop { .flex--maybe-wrap { flex-wrap: nowrap; diff --git a/assets/css/elements/heading.css b/assets/css/elements/heading.css index 6e1ee4a4..34f61880 100644 --- a/assets/css/elements/heading.css +++ b/assets/css/elements/heading.css @@ -1,8 +1,8 @@ @define-mixin heading $el { $(el) { + font-family: var(--font-family-heading); font-size: var(--font-$(el)-size); - font-weight: bold; - font-variant: small-caps; + font-weight: normal; padding: var(--padding-small) 0; margin-top: 0; border-bottom: 1px solid var(--primary-border-color); @@ -30,12 +30,6 @@ @mixin heading h4; @mixin heading h5; @mixin heading h6; - -/* To differentiate between h5 and h6, simply make h6 not bold */ -h6 { - font-weight: normal; -} - @mixin heading-type success; @mixin heading-type warning; @mixin heading-type danger; diff --git a/assets/css/elements/layout.css b/assets/css/elements/layout.css index c2b7d24e..39b7f5d8 100644 --- a/assets/css/elements/layout.css +++ b/assets/css/elements/layout.css @@ -11,6 +11,7 @@ .column-layout__main { flex: 1 0 auto; + max-width: calc(100% - var(--column-left-width) - var(--padding-normal)); } @mixin if-mobile { @@ -21,6 +22,10 @@ .column-layout__left { margin-right: 0; } + + .column-layout__main { + max-width: 100%; + } } #container { diff --git a/assets/css/options/system-fonts.css b/assets/css/options/system-fonts.css new file mode 100644 index 00000000..279b2b6d --- /dev/null +++ b/assets/css/options/system-fonts.css @@ -0,0 +1,5 @@ +:root { + --font-family: sans-serif; + --font-family-monospace: monospace; + --font-family-heading: sans-serif; +} diff --git a/assets/css/views/communication.css b/assets/css/views/communication.css index bdb6f078..c50ea77d 100644 --- a/assets/css/views/communication.css +++ b/assets/css/views/communication.css @@ -80,6 +80,11 @@ padding: var(--padding-small); } +.communication__options { + display: flex; + flex-flow: row; +} + .communication__options__staff { display: flex; align-items: center; @@ -87,19 +92,40 @@ margin-top: var(--padding-small); } -.communication__options__post-time { +.communication__post-time { display: flex; align-items: center; gap: var(--padding-small); - flex-wrap: wrap; + padding: var(--padding-small); + flex-wrap: nowrap; } +.communication__post-time > time, .communication__options__edit-time { - width: 100%; - margin-bottom: var(--padding-small); + white-space: nowrap; } @mixin if-mobile { + .communication__options { + flex-flow: column; + } + + .communication__options__edit-time { + width: 100%; + } + + .communication__post-time > time, + .communication__options__edit-time { + white-space: normal; + } + + .communication__post-time { + border-radius: var(--border-radius-inner); + margin-top: var(--padding-small); + background: var(--primary-muted-color); + flex-wrap: wrap; + } + .communication__body__text { margin-top: var(--padding-normal); } diff --git a/assets/css/views/statistics.css b/assets/css/views/statistics.css index 7d90ffb4..7f6fd1b1 100644 --- a/assets/css/views/statistics.css +++ b/assets/css/views/statistics.css @@ -22,13 +22,33 @@ .sparkline { border-bottom: 1px solid var(--primary-border-color); display: flex; - height: var(--padding-normal); + height: var(--padding-large); + padding: var(--padding-tiny); + overflow: hidden; +} + +.sparkline svg { + overflow: visible; } .barline__bar { - fill: var(--primary-border-color); + fill: var(--primary-link-color); } .barline__bar:hover { fill: var(--danger-color); } + +.barline__dot { + fill: var(--primary-border-color); + stroke: var(--link-color); + stroke-width: 1px; +} + +.barline__dot:hover { + fill: var(--danger-color); +} + +.barline__dot[cy="21.25"] { + display: none; +} diff --git a/assets/js/graph.ts b/assets/js/graph.ts new file mode 100644 index 00000000..3a0049b7 --- /dev/null +++ b/assets/js/graph.ts @@ -0,0 +1,30 @@ +/** + * Graph Logic + * + * Scales graphs, makes graphs resizable, and stuff like that + */ + +import { $, $$ } from './utils/dom'; + +function resizeGraphs() { + $$('#js-sparkline-svg').forEach(el => { + const parent: HTMLElement | null = el.parentElement; + + if (parent) { + el.viewBox.baseVal.width = parent.clientWidth; + + const graph: SVGPathElement | null = $('#js-barline-graph', el); + + if (graph) { + graph.setAttribute('style', `transform: scale(${parent.clientWidth / 375});`); + } + } + }); +} + +function sizeGraphs() { + resizeGraphs(); + window.addEventListener('resize', resizeGraphs); +} + +export { sizeGraphs }; diff --git a/assets/js/when-ready.js b/assets/js/when-ready.js index 88476357..ba2e6640 100644 --- a/assets/js/when-ready.js +++ b/assets/js/when-ready.js @@ -35,6 +35,7 @@ import { hideStaffTools } from './staffhider'; import { pollOptionCreator } from './poll'; import { warnAboutPMs } from './pmwarning'; import { imageSourcesCreator } from './sources'; +import { sizeGraphs } from './graph'; whenReady(() => { @@ -68,5 +69,6 @@ whenReady(() => { pollOptionCreator(); warnAboutPMs(); imageSourcesCreator(); + sizeGraphs(); }); diff --git a/lib/philomena_web/templates/comment/_comment.html.slime b/lib/philomena_web/templates/comment/_comment.html.slime index 1dc80bfd..b0faefeb 100644 --- a/lib/philomena_web/templates/comment/_comment.html.slime +++ b/lib/philomena_web/templates/comment/_comment.html.slime @@ -1,3 +1,5 @@ +- options = render PhilomenaWeb.CommentView, "_comment_link.html", comment: @comment, conn: @conn + article.block.communication id="comment_#{@comment.id}" = if not @comment.approved and not @comment.hidden_from_users and (can?(@conn, :hide, @comment) or @comment.user_id == @conn.assigns.current_user.id) do .block__content @@ -22,13 +24,14 @@ article.block.communication id="comment_#{@comment.id}" = submit "Delete", class: "button" .block__content.flex.flex--no-wrap class=communication_body_class(@comment) - = render PhilomenaWeb.CommunicationView, "_body.html", object: @comment, body: @body, conn: @conn, name: "comment" + = render PhilomenaWeb.CommunicationView, "_body.html", object: @comment, body: @body, conn: @conn, name: "comment", options: options .block__content.communication__options .flex.flex--wrap.flex--spaced-out = render PhilomenaWeb.CommentView, "_comment_options.html", comment: @comment, conn: @conn = if can?(@conn, :hide, @comment) do + .flex__spacer .js-staff-action .communication__options__staff = cond do @@ -43,16 +46,16 @@ article.block.communication id="comment_#{@comment.id}" ' Delete Contents - not @comment.hidden_from_users and not @comment.destroyed_content -> - a.button.button--danger.togglable-delete-form-link href="#" data-click-toggle="#inline-del-form-comment-#{@comment.id}" + a.button.button--danger.button--transparent.togglable-delete-form-link href="#" data-click-toggle="#inline-del-form-comment-#{@comment.id}" i.fas.fa-times> ' Delete - true -> = if can?(@conn, :show, :ip_address) do - .button.button--information.js-staff-action + .button.button--warning.button--transparent.js-staff-action =<> link_to_ip(@conn, @comment.ip) - .button.button--information.js-staff-action + .button.button--warning.button--transparent.js-staff-action =<> link_to_fingerprint(@conn, @comment.fingerprint) = form_for :comment, Routes.image_comment_hide_path(@conn, :create, @comment.image_id, @comment), [class: "togglable-delete-form hidden flex", id: "inline-del-form-comment-#{@comment.id}"], fn f -> diff --git a/lib/philomena_web/templates/comment/_comment_link.html.slime b/lib/philomena_web/templates/comment/_comment_link.html.slime new file mode 100644 index 00000000..862dc21d --- /dev/null +++ b/lib/philomena_web/templates/comment/_comment_link.html.slime @@ -0,0 +1,18 @@ +- comment_link = Routes.image_path(@conn, :show, @comment.image) <> "#comment_#{@comment.id}" + +.flex__spacer.hidden--desktop +a.communication__interaction.hidden--desktop title="Link to comment" href=comment_link + i.fa.fa-link += if not is_nil(@comment.edited_at) and can?(@conn, :show, @comment) do + a.communication__options__edit-time href=Routes.image_comment_history_path(@conn, :index, @comment.image, @comment) + span.hidden--mobile> - edited + span.hidden--desktop> Edited + => pretty_time(@comment.edited_at) + + = if @comment.edit_reason not in [nil, ""] do + | ( + = @comment.edit_reason + | ) +.flex__spacer.hidden--mobile +a.communication__interaction.hidden--mobile title="Link to comment" href=comment_link + i.fa.fa-link diff --git a/lib/philomena_web/templates/comment/_comment_options.html.slime b/lib/philomena_web/templates/comment/_comment_options.html.slime index 459b0492..32683808 100644 --- a/lib/philomena_web/templates/comment/_comment_options.html.slime +++ b/lib/philomena_web/templates/comment/_comment_options.html.slime @@ -1,29 +1,11 @@ -.communication__options__post-time - span - ' Posted - => pretty_time(@comment.created_at) - - a.button.button--primary href=Routes.image_comment_report_path(@conn, :new, @comment.image, @comment) - i.fa.fa-flag> - ' Report - - = if not is_nil(@comment.edited_at) and can?(@conn, :show, @comment) do - a.communication__options__edit-time href=Routes.image_comment_history_path(@conn, :index, @comment.image, @comment) - ' Edited - => pretty_time(@comment.edited_at) - - = if @comment.edit_reason not in [nil, ""] do - ' because: - => @comment.edit_reason - .flex.flex--normal-gap.flex--centered - link_path = Routes.image_path(@conn, :show, @comment.image) <> "#comment_#{@comment.id}" - safe_author = PhilomenaWeb.PostView.markdown_safe_author(@comment) - quote_body = if @comment.hidden_from_users, do: "", else: @comment.body - a.button.button--primary title="Link to comment" href=link_path - i.fa.fa-link - ' Link + a.button.button--primary href=Routes.image_comment_report_path(@conn, :new, @comment.image, @comment) + i.fa.fa-flag> + ' Report a.button.button--primary.post-reply.post-reply-quote href=link_path data-reply-url=link_path data-author=safe_author data-post=quote_body i.fa.fa-quote-right diff --git a/lib/philomena_web/templates/comment/_comment_with_image.html.slime b/lib/philomena_web/templates/comment/_comment_with_image.html.slime index d84ab25a..0078f64f 100644 --- a/lib/philomena_web/templates/comment/_comment_with_image.html.slime +++ b/lib/philomena_web/templates/comment/_comment_with_image.html.slime @@ -1,6 +1,8 @@ +- options = render PhilomenaWeb.CommentView, "_comment_link.html", comment: @comment, conn: @conn + article.block.communication id="comment_#{@comment.id}" .block__content.flex.flex--no-wrap class=communication_body_class(@comment) - = render PhilomenaWeb.CommunicationView, "_body.html", object: @comment, image: @comment.image, body: @body, conn: @conn, name: "comment" + = render PhilomenaWeb.CommunicationView, "_body.html", object: @comment, image: @comment.image, body: @body, conn: @conn, name: "comment", options: options .block__content.communication__options .flex.flex--wrap.flex--spaced-out @@ -15,21 +17,21 @@ article.block.communication id="comment_#{@comment.id}" ' Restore = if can?(@conn, :delete, @comment) do - = link(to: Routes.image_comment_delete_path(@conn, :create, @comment.image_id, @comment), data: [confirm: "Are you sure?"], method: "post", class: "button button--danger") do + = link(to: Routes.image_comment_delete_path(@conn, :create, @comment.image_id, @comment), data: [confirm: "Are you sure?"], method: "post", class: "button button--danger button--transparent") do i.fas.fa-times> ' Delete Contents - not @comment.hidden_from_users and not @comment.destroyed_content -> - a.button.button--danger.togglable-delete-form-link href="#" data-click-toggle="#inline-del-form-comment-#{@comment.id}" + a.button.button--danger.button--transparent.togglable-delete-form-link href="#" data-click-toggle="#inline-del-form-comment-#{@comment.id}" i.fas.fa-times> ' Delete - true -> = if can?(@conn, :show, :ip_address) do - .button.button--warning.js-staff-action + .button.button--warning.button--transparent.js-staff-action =<> link_to_ip(@conn, @comment.ip) - .button.button--information.js-staff-action + .button.button--warning.button--transparent.js-staff-action =<> link_to_fingerprint(@conn, @comment.fingerprint) = form_for :comment, Routes.image_comment_hide_path(@conn, :create, @comment.image_id, @comment), [class: "togglable-delete-form hidden flex", id: "inline-del-form-comment-#{@comment.id}"], fn f -> diff --git a/lib/philomena_web/templates/communication/_body.html.slime b/lib/philomena_web/templates/communication/_body.html.slime index 1c1f0274..224d004d 100644 --- a/lib/philomena_web/templates/communication/_body.html.slime +++ b/lib/philomena_web/templates/communication/_body.html.slime @@ -1,4 +1,5 @@ - anon = is_nil(assigns[:noanon]) or @noanon == false +- options = if is_nil(assigns[:options]), do: "", else: @options - avatar = cond do - not is_nil(assigns[:image]) -> @@ -45,6 +46,11 @@ .communication__sender-block span.communication__sender-name = username + .flex__spacer + span.communication__post-time + ' Posted + =< pretty_time(@object.created_at) + = options = title .communication__body__text = contents @@ -55,6 +61,10 @@ span.communication__sender-name = username = title + span.communication__post-time + ' Posted + = pretty_time(@object.created_at) + = options .communication__body.communication__body__text = contents \ No newline at end of file diff --git a/lib/philomena_web/templates/post/_post.html.slime b/lib/philomena_web/templates/post/_post.html.slime index 1826face..42a0b046 100644 --- a/lib/philomena_web/templates/post/_post.html.slime +++ b/lib/philomena_web/templates/post/_post.html.slime @@ -1,3 +1,17 @@ +- post_link = Routes.forum_topic_path(@conn, :show, @post.topic.forum, @post.topic, post_id: @post.id) <> "#post_#{@post.id}" +- options = if true do + .flex__spacer.hidden--desktop + a.communication__interaction.hidden--desktop title="Link to post" href=post_link + i.fa.fa-link + = if not is_nil(@post.edited_at) and can?(@conn, :show, @post) do + a href=Routes.forum_topic_post_history_path(@conn, :index, @post.topic.forum, @post.topic, @post) + ' Edited + => pretty_time(@post.edited_at) + + = if @post.edit_reason not in [nil, ""] do + ' because: + => @post.edit_reason + article.block.communication id="post_#{@post.id}" = if not @post.approved and not @post.hidden_from_users and (can?(@conn, :hide, @post) or @post.user_id == @conn.assigns.current_user.id) do .block__content @@ -22,7 +36,7 @@ article.block.communication id="post_#{@post.id}" = submit "Delete", class: "button" .block__content.flex.flex--no-wrap class=communication_body_class(@post) - = render PhilomenaWeb.CommunicationView, "_body.html", object: @post, body: @body, conn: @conn, name: "post" + = render PhilomenaWeb.CommunicationView, "_body.html", object: @post, body: @body, conn: @conn, name: "post", options: options .block__content.communication__options .flex.flex--wrap.flex--spaced-out diff --git a/lib/philomena_web/templates/post/_post_options.html.slime b/lib/philomena_web/templates/post/_post_options.html.slime index 40f9d3f6..bd9aaf43 100644 --- a/lib/philomena_web/templates/post/_post_options.html.slime +++ b/lib/philomena_web/templates/post/_post_options.html.slime @@ -1,29 +1,11 @@ -div - ' Posted - => pretty_time(@post.created_at) - - a.communication__interaction href=Routes.forum_topic_post_report_path(@conn, :new, @post.topic.forum, @post.topic, @post) - i.fa.fa-flag> - ' Report - - = if not is_nil(@post.edited_at) and can?(@conn, :show, @post) do - br - a href=Routes.forum_topic_post_history_path(@conn, :index, @post.topic.forum, @post.topic, @post) - ' Edited - => pretty_time(@post.edited_at) - - = if @post.edit_reason not in [nil, ""] do - ' because: - => @post.edit_reason - div - link_path = Routes.forum_topic_path(@conn, :show, @post.topic.forum, @post.topic, post_id: @post.id) <> "#post_#{@post.id}" - safe_author = markdown_safe_author(@post) - quote_body = if @post.hidden_from_users, do: "", else: @post.body - a.communication__interaction title="Link to post" href=link_path - i.fa.fa-link> - ' Link + a.communication__interaction href=Routes.forum_topic_post_report_path(@conn, :new, @post.topic.forum, @post.topic, @post) + i.fa.fa-flag> + ' Report a.communication__interaction.post-reply.post-reply-quote href=link_path data-reply-url=link_path data-author=safe_author data-post=quote_body i.fa.fa-quote-right> diff --git a/lib/philomena_web/views/profile_view.ex b/lib/philomena_web/views/profile_view.ex index 2a9fe461..8fc93f7b 100644 --- a/lib/philomena_web/views/profile_view.ex +++ b/lib/philomena_web/views/profile_view.ex @@ -36,25 +36,53 @@ defmodule PhilomenaWeb.ProfileView do def commission_status(%{open: true}), do: "Open" def commission_status(_commission), do: "Closed" - def sparkline_data(data) do + defp sparkline_y(val, max) do + # Filter out negative values + calc = max(val, 0) + + # Lerp or 0 if not present + height = zero_div(calc * 20, max) + + # In SVG coords, y grows down + 20 - height + end + + def sparkline_data(data, width \\ 375, height \\ 20) do # Normalize range max = max(Enum.max(data), 0) + sx = width / 90 + sy = height / 20 + factor = 100 / 90 - content_tag :svg, width: "100%", preserveAspectRatio: "none", viewBox: "0 0 90 20" do - for {val, i} <- Enum.with_index(data) do - # Filter out negative values - calc = max(val, 0) + content_tag :svg, id: "js-sparkline-svg", width: "100%", preserveAspectRatio: "xMinYMin", viewBox: "0 0 #{width} #{height}" do + first = List.first(data) + last = List.last(data) + first_y = sparkline_y(first, max) * sy + last_y = sparkline_y(last, max) * sy + indexed_data = data + |> Enum.with_index() + points = + indexed_data + |> Enum.chunk_every(2, 1, :discard) + |> Enum.map(fn [{cv, ci}, {nv, ni}] -> + cy = sparkline_y(cv, max) + ny = sparkline_y(nv, max) - # Lerp or 0 if not present - height = zero_div(calc * 20, max) + "C #{(ci * sx) + 0.5 * sx},#{cy * sy} #{(ni * sx) - 0.5 * sx},#{ny * sy} #{ni * sx},#{ny * sy}" + end) + |> Enum.join("") - # In SVG coords, y grows down - y = 20 - height + circles = for {val, i} <- indexed_data do + y = sparkline_y(val, max) * sy - content_tag :rect, class: "barline__bar", x: i, y: y, width: 1, height: height do + content_tag :circle, class: "barline__dot", cx: "#{i * factor}%", cy: y * sy + 1.25, r: 2.5 do content_tag(:title, val) end end + + graph = content_tag :path, "", id: "js-barline-graph", class: "barline__bar", d: "M0,#{first_y}#{points}L#{width - sx},#{last_y}L#{width - sx},#{height}L0,#{height}L0,#{first_y}" + + [graph, circles] end end