diff --git a/lookbook/docs/components/border-box-list.md.erb b/lookbook/docs/components/border-box-list.md.erb new file mode 100644 index 000000000000..c72cf4c8d23f --- /dev/null +++ b/lookbook/docs/components/border-box-list.md.erb @@ -0,0 +1,191 @@ +The `BorderBoxListComponent` displays compact, structured list items with +optional section headers, item actions, empty states, and a footer. + +## Overview + +<%= embed OpenProject::Common::BorderBoxListComponentPreview, :default %> + +## Anatomy + +The list is structured in up to four parts: + +**Header** *(optional)* +Names the list section. It can show a count, description, action buttons, an +action menu, and an optional collapse toggle. In static headers, the description +renders below the heading line. In collapsible headers, the description is part +of the collapsible header and is hidden when the list is collapsed. + +**Items** +The list rows. Items can render generic row content or a work package card. + +**Empty state** *(optional)* +Shown when the list has no items. + +**Footer** *(optional)* +Shown below the items. Use it for summary or follow-up content. + +If a page shows multiple Border Box Lists and one list has a header, all +comparable lists should use headers. + +## Parameters + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `container` | `String`, `Symbol`, `Class`, `Object` | required | Seed used to derive stable DOM ids for the list, header, and footer | +| `scheme` | `Symbol` | `:default` | Visual scheme. Use `:transparent` for list containers that should blend into their surrounding surface | +| `padding` | `Symbol` | `:default` | Row padding forwarded to the underlying `Primer::Beta::BorderBox`. Supports Primer BorderBox padding values such as `:condensed`, `:default`, and `:spacious` | +| `header_padding` | `Symbol` | `:inherit` | Header-only vertical padding override. Supports `:inherit`, `:condensed`, `:default`, and `:spacious` | +| `interactive` | `Boolean` | `false` | Enables polite ARIA live-region announcements for counter and configured empty-state updates | +| `collapsible` | `Boolean` | `false` | Renders the header as a collapsible toggle when a header is present | +| `current_user` | `User` | `User.current` | User context forwarded to work package item rows | +| `system_arguments` | `Hash` | `{}` | Forwarded to the underlying `Primer::Beta::BorderBox` | + +## Slots + +| Slot | Description | +|---|---| +| `with_header` | Optional section header. See [Header parameters](#header-parameters) and [Header slots](#header-slots) below | +| `with_item` | Generic list row that renders the provided block content | +| `with_work_package_item` | Work package row that renders a work package card | +| `with_empty_state` | Empty-state content rendered when no items are present | +| `with_footer` | Optional footer row below the list items | + +## Header + +### Header parameters + +| Parameter | Type | Default | Description | +|---|---|---|---| +| `title` | `String` | required | Header title | +| `title_tag` | `Symbol` | `:h4` | HTML tag for the title heading | +| `title_arguments` | `Hash` | `{}` | Forwarded to the title heading element | +| `count` | `Integer`, `Boolean`, `nil` | `nil` | Pass `true` to infer the rendered item count, an integer for an explicit value, or `nil`/`false` to hide it | +| `count_arguments` | `Hash` | `{}` | Forwarded to `Primer::Beta::Counter`. Merged over defaults (`scheme: :primary`, `hide_if_zero: true`) | +| `collapsed` | `Boolean` | `false` | Whether a collapsible header starts closed | + +### Header slots + +| Slot | Description | +|---|---| +| `with_description` | Secondary content below the title, wrapped in `Primer::Beta::Text` with muted color by default. Accepts Primer system arguments | +| `with_action_button` | Button rendered in the header actions area | +| `with_menu` | Action menu rendered in the header actions area. Pass `button_aria_label:` for the trigger button | + +### Collapsible headers + +Collapsible headers add: + +- an **arrow indicator** next to the title to show whether the section is + collapsed or expanded. +- an optional **header description** that gives more context to the section and + is hidden when the list is collapsed. +- an optional **counter** that displays the number of items in the section. + +When collapsed, the bottom border is slightly thicker to indicate that content +is hidden. + +Both static and collapsible headers can include additional actions. For example, +a menu button can offer contextual actions such as edit, delete, move up, or +move down. + +<%= embed OpenProject::Common::BorderBoxListComponentPreview, :default, params: { collapsible: true } %> + +## Variants + +<%= embed OpenProject::Common::BorderBoxListComponentPreview, :playground, panels: %i[params source] %> + +### Transparent scheme + +Use the transparent scheme for list containers that should not render a visible +header background or separator, such as Backlogs sprint and inbox containers. + +<%= embed OpenProject::Common::BorderBoxListComponentPreview, :transparent, panels: %i[params source] %> + +### Work package items + +Use `with_work_package_item` for rows that should render a work package card. +It renders `OpenProject::Common::WorkPackageCardComponent` by default. See the +[Work Package Card documentation](/lookbook/pages/components/work_packages/card) +for card-specific layout, parameters, and slots. + +<%= embed OpenProject::Common::BorderBoxListComponentPreview, :with_work_package_items, panels: %i[source] %> + +### Empty state + +Use `with_empty_state` when the list can be rendered without items. It renders a +`Primer::Beta::Blankslate` under the hood, accepting `title:`, `description:`, +and `icon:` keywords. + +<%= embed OpenProject::Common::BorderBoxListComponentPreview, :empty_state, panels: %i[source] %> + +## Usage guidelines + +Use the Border Box List for compact item collections such as: + +- meeting agenda items. +- project attributes in project settings. +- configured OAuth applications. +- work package lists with card-style rows. + +If the content needs table structure with columns and column headers, use the +[Border Box Table](./tables/border_box_table) component instead. + +### Best practices + +**Do** + +- Use section headers when the items have logical categories, such as open and + planned meetings. +- Use the built-in header actions and item menus instead of composing raw + BorderBox rows by hand. +- Make headers and items draggable when users need to reorder sections or move + items between sections. + +**Don't** + +- Don't mix sections with and without headers. If one section has a header, they must all have headers. +- Don't make sections collapsible when users benefit from seeing all content at + once. +- Don't use the Border Box List for data that needs column alignment, sorting, + or column headers. + +## Technical notes + +Use `OpenProject::Common::BorderBoxListComponent` instead of composing +`Primer::Beta::BorderBox` directly when rendering OpenProject lists. +Header descriptions are wrapped in `Primer::Beta::Text` with muted text color by +default, and can receive Primer system arguments through `with_description`. + +## Code structure + +```ruby +render OpenProject::Common::BorderBoxListComponent.new( + container: "project-attributes", + scheme: :transparent, + header_padding: :default, + padding: :condensed, + collapsible: true +) do |list| + list.with_header(title: "Project attributes", count: true) do |header| + header.with_description(id: "project-attributes-description", font_size: :small) do + "Visible in the project overview" + end + header.with_menu(button_aria_label: "Project attribute actions") do |menu| + menu.with_item(label: "Edit", href: edit_project_path(@project)) + end + end + + @project_attributes.each do |attribute| + list.with_item { attribute.name } + end + + list.with_empty_state( + title: "No attributes yet", + description: "Add attributes to describe this project." + ) + + list.with_footer { "Last updated: #{format_date(@project.updated_at)}" } +end +``` + +For lower-level details, see the [Primer BorderBox preview examples](/lookbook/inspect/primer/beta/border_box/playground). diff --git a/lookbook/docs/components/border-box.md.erb b/lookbook/docs/components/border-box.md.erb deleted file mode 100644 index 6cbd9852c16d..000000000000 --- a/lookbook/docs/components/border-box.md.erb +++ /dev/null @@ -1,56 +0,0 @@ -The Border Box element is used to display structured items in a list, each with a set of actions. - -<%= embed OpPrimer::BorderBoxComponentPreview, :default %> - -## Anatomy - -The Border Box element is made up of - -1. The **header**, which acts as a container for the items below and helps distinguish between sections. It is optional. -2. Individual **items** below can vary in what information they show. - -If a page uses a Border Box with a header, all sections must use headers. - -## Header - -The header can either be static or collapsible. - -The CollapsibleHeader brings additional functionality: - -- an **arrow indicator** next to the title indicates if the section is collapsed or expanded. -- a **header description** (optional) to give additional context to the content of that section. Hidden when collapsed. -- a **counter** (optional) displays the number of items that that particular section contains. - -When collapsed, the bottom border is slightly thicker to indicate something is hidden. - -Both the collapsible and static headers can accept additional actions. For example, a **more button** can offer additional contextual actions such as edit, delete and move (move up, move to top, move down, move to the end). - -<%= embed OpPrimer::BorderBoxComponentPreview, :collapsible %> - -## Uses - -Some places in which we already use the Border Box: - -- to display individual meeting agenda items in a meeting -- to display the a list of project attributes in Project settings -- to display a list of configured OAuth applications - -If the content needs to be more structured (with columns and column headers), please use the [Border Box Table](./tables/border_box_table) component instead. - -## Best practices - -**Do** - -- Use sections (with headers) to better organise content if they have logical categories (eg. Open and Planned meetings). -- Do make the header and items draggable if the user should be able to drag individual items up and down and into other sections. The sections themselves can also be dragged up and down to change order. - -**Don't** - -- Don't mix sections with and without headers. If one section has a header, they must all have headers. -- Don't make sections collapsible if they do not need to be. - -## Technical notes - -<%= embed OpPrimer::BorderBoxComponentPreview, :collapsible, panels: %i[source] %> - -For detailed examples have a look at the other [border box preview examples](/lookbook/inspect/primer/beta/border_box/playground) or the [collapsible header preview examples](/lookbook/inspect/primer/open_project/border_box/collapsible_header/playground). diff --git a/lookbook/docs/components/inset-box.md.erb b/lookbook/docs/components/inset-box.md.erb index b78f65b95d60..a359b7b74691 100644 --- a/lookbook/docs/components/inset-box.md.erb +++ b/lookbook/docs/components/inset-box.md.erb @@ -38,7 +38,8 @@ Some places where we already use the Inset Box: It is useful whenever content should stand out from the page background while staying lightweight. -If content needs more structure (e.g., with headers, actions, or multiple items), consider using the Border Box component instead. +If content needs more structure, such as headers, actions, or multiple items, +consider using the Border Box List component instead. ## Best practices @@ -51,7 +52,8 @@ If content needs more structure (e.g., with headers, actions, or multiple items) - Don’t apply arbitrary custom background colors in CSS — use the component’s options. - Don’t duplicate "grey box" styles manually; always use the Inset Box. -- Don’t use Inset Box if a more structured component (like Border Box) is better suited. +- Don’t use Inset Box if a more structured component, such as Border Box List, + is better suited. ## Technical notes diff --git a/lookbook/docs/components/tables/border-box-table.md.erb b/lookbook/docs/components/tables/border-box-table.md.erb index 43bf49740bd4..a45846deec8d 100644 --- a/lookbook/docs/components/tables/border-box-table.md.erb +++ b/lookbook/docs/components/tables/border-box-table.md.erb @@ -31,7 +31,8 @@ Some examples of where we already use the Border Box table: - to display a list of configured OpenID providers - to display a list of configured SCIM clients -If the content does not need a table structure with column headers and grids, please use the simpler [Border Box](../border_box) component instead. +If the content does not need a table structure with column headers and grids, +please use the simpler [Border Box List](../border_box_list) component instead. ## Technical notes diff --git a/lookbook/previews/op_primer/border_box_component_preview.rb b/lookbook/previews/op_primer/border_box_component_preview.rb deleted file mode 100644 index 66aea786f708..000000000000 --- a/lookbook/previews/op_primer/border_box_component_preview.rb +++ /dev/null @@ -1,50 +0,0 @@ -# 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 OpPrimer - # @hidden - # @display min_height 400px - class BorderBoxComponentPreview < Lookbook::Preview - def default - render(Primer::Beta::BorderBox.new) do |component| - component.with_header { "Header" } - component.with_body { "Body" } - component.with_row { "Row one" } - component.with_row { "Row two" } - component.with_row { "Row three" } - component.with_footer { "Footer" } - end - end - - def collapsible - render_with_template - end - end -end diff --git a/lookbook/previews/op_primer/border_box_component_preview/collapsible.html.erb b/lookbook/previews/op_primer/border_box_component_preview/collapsible.html.erb deleted file mode 100644 index 6ce78e50a6df..000000000000 --- a/lookbook/previews/op_primer/border_box_component_preview/collapsible.html.erb +++ /dev/null @@ -1,64 +0,0 @@ -<%= render(Primer::Beta::BorderBox.new(list_id: "border-box-list")) do |component| %> - <% component.with_header do %> - <%= render(Primer::OpenProject::FlexLayout.new(align_items: :flex_start)) do |flex| %> - <%= flex.with_column(flex: 1, pt: 1) do %> - <%= render(Primer::OpenProject::BorderBox::CollapsibleHeader.new(collapsible_id: "border-box-list")) do |header| - header.with_count(count: 3) - header.with_title { "Collapsible title" } - header.with_description { "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor" } - end %> - <% end %> - <%= flex.with_column do %> - <%= render(Primer::Alpha::ActionMenu.new) do |menu| - menu.with_show_button(icon: "kebab-horizontal", scheme: :invisible, "aria-label": "Header actions") - menu.with_item( - tag: :a, - label: "Edit", - href: "#" - ) do |item| - item.with_leading_visual_icon(icon: :pencil) - "foo" - end - end %> - <% end %> - <% end %> - <% end %> - <% component.with_body { "Body" } %> - <% component.with_row do %> - <%= render(Primer::OpenProject::FlexLayout.new(justify_content: :space_between, align_items: :center)) do |flex| %> - <%= flex.with_column { "Row 1" } %> - <%= flex.with_column do %> - <%= render(Primer::Alpha::ActionMenu.new) do |menu| - menu.with_show_button(icon: "kebab-horizontal", "aria-label": "More", scheme: :invisible) - menu.with_item( - tag: :a, - label: "Edit", - href: "#" - ) do |item| - item.with_leading_visual_icon(icon: :pencil) - "foo" - end - end %> - <% end %> - <% end %> - <% end %> - <% component.with_row do %> - <%= render(Primer::OpenProject::FlexLayout.new(justify_content: :space_between, align_items: :center)) do |flex| %> - <%= flex.with_column { "Row 2" } %> - <%= flex.with_column do %> - <%= render(Primer::Alpha::ActionMenu.new) do |menu| - menu.with_show_button(icon: "kebab-horizontal", "aria-label": "More", scheme: :invisible) - menu.with_item( - tag: :a, - label: "Edit", - href: "#" - ) do |item| - item.with_leading_visual_icon(icon: :pencil) - "foo" - end - end %> - <% end %> - <% end %> - <% end %> - <% component.with_footer { "Footer" } %> -<% end %> diff --git a/lookbook/previews/open_project/common/border_box_list_component_preview.rb b/lookbook/previews/open_project/common/border_box_list_component_preview.rb index a8cdee75ff47..0c96017149f0 100644 --- a/lookbook/previews/open_project/common/border_box_list_component_preview.rb +++ b/lookbook/previews/open_project/common/border_box_list_component_preview.rb @@ -31,6 +31,7 @@ module OpenProject module Common # @logical_path OpenProject/Common + # @display min_height 400px class BorderBoxListComponentPreview < ViewComponent::Preview DEFAULT_DESCRIPTION = "Coordinate launch work and keep stakeholders aligned." TRANSPARENT_DESCRIPTION = "Sprint goals, scope, and timing for the next iteration." @@ -128,7 +129,7 @@ def with_work_package_items( interactive: false, collapsible: false ) - work_packages = WorkPackage.includes(:project).limit(2).to_a + work_packages = WorkPackage.includes(:project).limit(3).to_a return preview_message("No work packages in the database.") if work_packages.empty? render OpenProject::Common::BorderBoxListComponent.new(