Handle color modes in status button by passing __hl classes

This commit is contained in:
Oliver Günther
2025-03-06 13:21:09 +01:00
parent ed8f401ace
commit 990335dfed
10 changed files with 174 additions and 98 deletions
@@ -14,17 +14,8 @@
item.with_leading_visual_icon(icon: option.icon) if option.icon
item.with_description.with_content(option.description) if option.description
flex_layout(align_items: :center) do |flex|
if option.icon.nil? && option.color
flex.with_column(mr: 1) do
helpers.icon_for_color(option.color, style: "display: block")
end
end
flex.with_column do
render(Primer::Beta::Text.new) { option.name }
end
end
classes = option.colored? ? highlight_class_name(option, :inline) : ""
render(Primer::Beta::Text.new(classes:)) { option.name }
end
end
end
@@ -31,6 +31,7 @@
module OpPrimer
class StatusButtonComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
include Primer::ClassNameHelper
def initialize(current_status:, items:, readonly: false, disabled: false, button_arguments: {}, menu_arguments: {})
super
@@ -67,12 +68,21 @@ module OpPrimer
{
title:,
classes: class_names(@button_arguments[:classes], highlight_class_name(@current_status, :background)),
disabled: disabled?,
aria: {
label: title
},
style: @current_status.color&.color_styles_css
}
}.compact.deep_merge(@button_arguments)
end
def highlight_class_name(status, style)
case style
when :inline
helpers.hl_inline_class(status.color_namespace, status.color_ref)
when :background
helpers.hl_background_class(status.color_namespace, status.color_ref)
end
end
end
end
@@ -30,14 +30,20 @@
module OpPrimer
class StatusButtonOption # rubocop:disable OpenProject/AddPreviewForViewComponent
attr_reader :name, :color, :icon, :item_arguments, :description
attr_reader :name, :icon, :item_arguments, :description, :color_namespace, :color_ref
def initialize(name:, color: nil, icon: nil, description: nil, **item_arguments)
def initialize(name:, color_ref: nil, color_namespace: nil, icon: nil, description: nil, **item_arguments)
@name = name
@color = color
@icon = icon
@description = description
@item_arguments = item_arguments
@color_ref = color_ref
@color_namespace = color_namespace
end
def colored?
!icon && color_ref && color_namespace
end
def to_s
@@ -1,19 +1,40 @@
# frozen_string_literal: true
class WorkPackages::StatusButtonComponent < OpPrimer::StatusButtonComponent
include Primer::ClassNameHelper
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
class WorkPackages::StatusButtonComponent < OpPrimer::StatusButtonComponent
attr_reader :work_package, :user
def initialize(work_package:, user:, readonly: false, button_arguments: {}, menu_arguments: {})
@work_package = work_package
@user = user
button_arguments[:classes] = class_names(
button_arguments[:classes],
"__hl_background_status_#{work_package.status.id}"
)
super(
current_status: map_status(work_package.status),
items: available_statuses,
@@ -40,6 +61,6 @@ class WorkPackages::StatusButtonComponent < OpPrimer::StatusButtonComponent
def map_status(status)
icon = status.is_readonly? ? :lock : nil
OpPrimer::StatusButtonOption.new(name: status.name, color: status.color, icon:)
OpPrimer::StatusButtonOption.new(name: status.name, color_namespace: "status", color_ref: status.id, icon:)
end
end
+32 -22
View File
@@ -1,3 +1,5 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
@@ -59,31 +61,37 @@ module ColorsHelper
#
# Styles to display the color of attributes (type, status etc.) for example in the WP view
##
def resource_color_css(name, scope, inline_foreground: false)
def resources_scope_color_css(name, scope, inline_foreground: false)
scope.includes(:color).find_each do |entry|
color = entry.color
if color.nil?
concat ".#{hl_inline_class(name, entry)}::before { display: none }\n"
next
end
if inline_foreground
set_foreground_colors_for(class_name: ".#{hl_inline_class(name, entry)}", color:)
else
set_background_colors_for(class_name: ".#{hl_inline_class(name, entry)}::before", color:)
end
set_background_colors_for(class_name: ".#{hl_background_class(name, entry)}", color:)
resource_color_css(name, entry, inline_foreground: inline_foreground)
end
end
def resource_color_css(name, entry, inline_foreground: false)
color = entry.color
if color.nil?
concat ".#{hl_inline_class(name, entry)}::before { display: none }\n"
return
end
if inline_foreground
set_foreground_colors_for(class_name: ".#{hl_inline_class(name, entry)}", color:)
else
set_background_colors_for(class_name: ".#{hl_inline_class(name, entry)}::before", color:)
end
set_background_colors_for(class_name: ".#{hl_background_class(name, entry)}", color:)
end
def hl_inline_class(name, model)
"__hl_inline_#{name}_#{model.id}"
id = model.respond_to?(:id) ? model.id : model
"__hl_inline_#{name}_#{id}"
end
def hl_background_class(name, model)
"__hl_background_#{name}_#{model.id}"
id = model.respond_to?(:id) ? model.id : model
"__hl_background_#{name}_#{id}"
end
def icon_for_color(color, options = {})
@@ -170,11 +178,12 @@ module ColorsHelper
background: rgb(var(--color-r), var(--color-g), var(--color-b)) !important;"
mode = User.current.pref.theme
style += if mode == "light_high_contrast"
"border: 1px solid hsla(var(--color-h), calc(var(--color-s) * 1%), calc((var(--color-l) - 75) * 1%), 1) !important;"
else
"border: 1px solid hsl(var(--color-h), calc(var(--color-s) * 1%), calc((var(--color-l) - 15) * 1%)) !important;"
end
style +=
if mode == "light_high_contrast"
"border: 1px solid hsla(var(--color-h), calc(var(--color-s) * 1%), calc((var(--color-l) - 75) * 1%), 1) !important;"
else
"border: 1px solid hsl(var(--color-h), calc(var(--color-s) * 1%), calc((var(--color-l) - 15) * 1%)) !important;"
end
style
end
@@ -192,5 +201,6 @@ module ColorsHelper
"color: hsla(var(--color-h), calc(var(--color-s) * 1%), calc((var(--color-l) - (var(--color-l) * 0.22)) * 1%), 1) !important;"
end
end
# rubocop:enable Layout/LineLength
end
+8 -4
View File
@@ -1,9 +1,13 @@
<%# Highlightable resources %>
<%= resource_color_css("status", ::Status) %>
<%= resource_color_css("priority", ::IssuePriority) %>
<%= resource_color_css("type", ::Type, inline_foreground: true) %>
<%= resources_scope_color_css("status", ::Status) %>
<%= resources_scope_color_css("priority", ::IssuePriority) %>
<%= resources_scope_color_css("type", ::Type, inline_foreground: true) %>
<%# Color coded icons %>
<%= resource_color_css("life_cycle_step_definition", Project::LifeCycleStepDefinition, inline_foreground: true) %>
<%= resources_scope_color_css("life_cycle_step_definition", Project::LifeCycleStepDefinition, inline_foreground: true) %>
<% Meetings::Statuses::AVAILABLE.each do |meeting_status| %>
<%= resource_color_css("meeting_status", meeting_status) %>
<% end %>
<%= color_css %>
@@ -10,21 +10,26 @@ module OpPrimer
# @param size [Symbol] select [small, medium, large]
def playground(readonly: false, disabled: false, size: :medium)
status = OpPrimer::StatusButtonOption.new(name: "Open",
color: FactoryBot.build(:color_maroon),
tag: :a,
color_ref: :open,
color_namespace: :meeting_status,
href: "/some/test")
items = [
status,
OpPrimer::StatusButtonOption.new(name: "Closed",
color: FactoryBot.build(:color_green),
tag: :a,
color_ref: :closed,
color_namespace: :meeting_status,
href: "/some/other/action")
]
component = OpPrimer::StatusButtonComponent.new(current_status: status,
items:,
readonly:,
disabled:,
button_arguments: { title: "Edit", size: })
button_arguments: {
title: "Edit",
size:
})
render(component)
end
@@ -32,11 +37,17 @@ module OpPrimer
# See the [component documentation](/lookbook/pages/components/status_button) for more details.
# @display min_height 200px
def with_icon(size: :medium)
status = OpPrimer::StatusButtonOption.new(name: "Open", icon: :unlock, color: Color.new(hexcode: "#FF0000"))
status = OpPrimer::StatusButtonOption.new(name: "Open",
color_ref: :open,
color_namespace: :meeting_status,
icon: :unlock)
items = [
status,
OpPrimer::StatusButtonOption.new(name: "Closed", icon: :lock, color: Color.new(hexcode: "#00FF00"))
OpPrimer::StatusButtonOption.new(name: "Closed",
color_ref: :closed,
color_namespace: :meeting_status,
icon: :lock)
]
component = OpPrimer::StatusButtonComponent.new(current_status: status,
@@ -52,15 +63,13 @@ module OpPrimer
def with_description(size: :medium)
status = OpPrimer::StatusButtonOption.new(name: "Open",
icon: :unlock,
description: "The status is open",
color: Color.new(hexcode: "#FF0000"))
description: "The status is open")
items = [
status,
OpPrimer::StatusButtonOption.new(name: "Closed",
icon: :lock,
description: "The status is closed",
color: Color.new(hexcode: "#00FF00"))
description: "The status is closed")
]
component = OpPrimer::StatusButtonComponent.new(current_status: status,
@@ -43,12 +43,19 @@ module Meetings
end
def call
render(OpPrimer::StatusButtonComponent.new(current_status: current_status,
items: [open_status, in_progress_status, closed_status],
readonly: !edit_enabled?,
disabled: !edit_enabled?,
button_arguments: { title: t("label_meeting_state"), size: @size },
menu_arguments: { size: :small }))
render(
OpPrimer::StatusButtonComponent.new(
current_status: current_status,
items: [open_status, in_progress_status, closed_status],
readonly: !edit_enabled?,
disabled: !edit_enabled?,
button_arguments: {
title: t("label_meeting_state"),
size: @size
},
menu_arguments: { size: :small }
)
)
end
private
@@ -70,7 +77,8 @@ module Meetings
def open_status
OpPrimer::StatusButtonOption.new(name: t("label_meeting_state_open"),
color: Color.new(hexcode: "#1F883D"),
color_ref: Meetings::Statuses::OPEN.id,
color_namespace: :meeting_status,
icon: :"issue-opened",
tag: :a,
description: t("text_meeting_open_dropdown_description"),
@@ -82,7 +90,8 @@ module Meetings
def in_progress_status
OpPrimer::StatusButtonOption.new(name: t("label_meeting_state_in_progress"),
color: Color.new(hexcode: "#9A6700"),
color_ref: Meetings::Statuses::IN_PROGRESS.id,
color_namespace: :meeting_status,
icon: :play,
tag: :a,
description: t("text_meeting_in_progress_dropdown_description"),
@@ -94,7 +103,8 @@ module Meetings
def closed_status
OpPrimer::StatusButtonOption.new(name: t("label_meeting_state_closed"),
color: Color.new(hexcode: "#6E7781 "),
color_ref: Meetings::Statuses::CLOSED.id,
color_namespace: :meeting_status,
icon: :"issue-closed",
tag: :a,
description: t("text_meeting_closed_dropdown_description"),
@@ -0,0 +1,45 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
module Meetings
module Statuses
RECORD = Struct.new(:id, :color, keyword_init: true)
OPEN = RECORD.new(id: "open", color: Color.new(hexcode: "#1F883D"))
IN_PROGRESS = RECORD.new(id: "in_progress", color: Color.new(hexcode: "#9A6700"))
CLOSED = RECORD.new(id: "closed", color: Color.new(hexcode: "#565c63"))
AVAILABLE = [
OPEN,
IN_PROGRESS,
CLOSED
].freeze
end
end
@@ -85,36 +85,6 @@ RSpec.describe OpPrimer::StatusButtonComponent, type: :component do
end
end
context "when rendering with color status" do
let(:current_status) do
OpPrimer::StatusButtonOption.new(
name: "High Priority",
icon: nil,
color: Color.new(hexcode: "#FF0000")
)
end
let(:items) do
[
OpPrimer::StatusButtonOption.new(
name: "Low Priority",
icon: nil,
color: Color.new(hexcode: "#00FF00")
)
]
end
it "renders the status with color styles" do
render_inline(component)
expect(page).to have_css("[style*='background-color: #FF0000']")
expect(page).to have_text("High Priority")
expect(page).to have_css("[style*='background-color: #00FF00']", visible: :all)
expect(page).to have_text("Low Priority")
end
end
context "when readonly" do
let(:readonly) { true }