From f56da68b950a2d684a6da9f5d4910e745e2a6800 Mon Sep 17 00:00:00 2001 From: elhombretecla Date: Tue, 27 Oct 2026 15:59:14 +0100 Subject: [PATCH] :sparkles: Add change review components and icons to the DS Add four product components that the branch review dialogs build on: - tag*: a short status word, as a small code-font label or a rounded pill, in neutral or one of six tones, with an optional icon. - filter-chip*: a rounded toggle that filters a list by category. A selected chip draws its outline, text and icon in its tone, or in the accent colour when neutral. - change-row*: one entry of a change list, a short status bar beside a card with an icon, a title over a summary, and a slot at each end. - preview-card*: a framed preview of one side of a comparison (base, main, branch or result) over a transparency checkerboard. A container can size its stage through two custom properties. Each one ships with its Storybook story and docs, and is exported from the DS entry point. Add three icons: side-by-side and collapse for the comparison views, and git-compare-arrows for the compare action. --- frontend/resources/images/icons/collapse.svg | 3 + .../images/icons/git-compare-arrows.svg | 3 + .../resources/images/icons/side-by-side.svg | 3 + frontend/src/app/main/ui/ds.cljs | 8 ++ .../main/ui/ds/foundations/assets/icon.cljs | 3 + .../app/main/ui/ds/product/change_row.cljs | 73 +++++++++++ .../src/app/main/ui/ds/product/change_row.mdx | 44 +++++++ .../app/main/ui/ds/product/change_row.scss | 119 ++++++++++++++++++ .../main/ui/ds/product/change_row.stories.jsx | 67 ++++++++++ .../app/main/ui/ds/product/filter_chip.cljs | 47 +++++++ .../app/main/ui/ds/product/filter_chip.mdx | 43 +++++++ .../app/main/ui/ds/product/filter_chip.scss | 90 +++++++++++++ .../ui/ds/product/filter_chip.stories.jsx | 50 ++++++++ .../app/main/ui/ds/product/preview_card.cljs | 53 ++++++++ .../app/main/ui/ds/product/preview_card.mdx | 49 ++++++++ .../app/main/ui/ds/product/preview_card.scss | 85 +++++++++++++ .../ui/ds/product/preview_card.stories.jsx | 83 ++++++++++++ frontend/src/app/main/ui/ds/product/tag.cljs | 47 +++++++ frontend/src/app/main/ui/ds/product/tag.mdx | 47 +++++++ frontend/src/app/main/ui/ds/product/tag.scss | 71 +++++++++++ .../app/main/ui/ds/product/tag.stories.jsx | 57 +++++++++ 21 files changed, 1045 insertions(+) create mode 100644 frontend/resources/images/icons/collapse.svg create mode 100644 frontend/resources/images/icons/git-compare-arrows.svg create mode 100644 frontend/resources/images/icons/side-by-side.svg create mode 100644 frontend/src/app/main/ui/ds/product/change_row.cljs create mode 100644 frontend/src/app/main/ui/ds/product/change_row.mdx create mode 100644 frontend/src/app/main/ui/ds/product/change_row.scss create mode 100644 frontend/src/app/main/ui/ds/product/change_row.stories.jsx create mode 100644 frontend/src/app/main/ui/ds/product/filter_chip.cljs create mode 100644 frontend/src/app/main/ui/ds/product/filter_chip.mdx create mode 100644 frontend/src/app/main/ui/ds/product/filter_chip.scss create mode 100644 frontend/src/app/main/ui/ds/product/filter_chip.stories.jsx create mode 100644 frontend/src/app/main/ui/ds/product/preview_card.cljs create mode 100644 frontend/src/app/main/ui/ds/product/preview_card.mdx create mode 100644 frontend/src/app/main/ui/ds/product/preview_card.scss create mode 100644 frontend/src/app/main/ui/ds/product/preview_card.stories.jsx create mode 100644 frontend/src/app/main/ui/ds/product/tag.cljs create mode 100644 frontend/src/app/main/ui/ds/product/tag.mdx create mode 100644 frontend/src/app/main/ui/ds/product/tag.scss create mode 100644 frontend/src/app/main/ui/ds/product/tag.stories.jsx diff --git a/frontend/resources/images/icons/collapse.svg b/frontend/resources/images/icons/collapse.svg new file mode 100644 index 0000000000..34d2071c12 --- /dev/null +++ b/frontend/resources/images/icons/collapse.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/git-compare-arrows.svg b/frontend/resources/images/icons/git-compare-arrows.svg new file mode 100644 index 0000000000..9ed02d600a --- /dev/null +++ b/frontend/resources/images/icons/git-compare-arrows.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/side-by-side.svg b/frontend/resources/images/icons/side-by-side.svg new file mode 100644 index 0000000000..0f425d8d59 --- /dev/null +++ b/frontend/resources/images/icons/side-by-side.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/app/main/ui/ds.cljs b/frontend/src/app/main/ui/ds.cljs index f65ed4f5ce..162d2877a5 100644 --- a/frontend/src/app/main/ui/ds.cljs +++ b/frontend/src/app/main/ui/ds.cljs @@ -37,14 +37,18 @@ [app.main.ui.ds.product.branch-pill :refer [branch-pill*]] [app.main.ui.ds.product.branch-row :refer [branch-row*]] [app.main.ui.ds.product.branch-status :refer [branch-status*]] + [app.main.ui.ds.product.change-row :refer [change-row*]] [app.main.ui.ds.product.cta :refer [cta*]] [app.main.ui.ds.product.empty-placeholder :refer [empty-placeholder*]] [app.main.ui.ds.product.empty-state :refer [empty-state*]] + [app.main.ui.ds.product.filter-chip :refer [filter-chip*]] [app.main.ui.ds.product.input-with-meta :refer [input-with-meta*]] [app.main.ui.ds.product.loader :refer [loader*]] [app.main.ui.ds.product.milestone :refer [milestone*]] [app.main.ui.ds.product.milestone-group :refer [milestone-group*]] [app.main.ui.ds.product.panel-title :refer [panel-title*]] + [app.main.ui.ds.product.preview-card :refer [preview-card*]] + [app.main.ui.ds.product.tag :refer [tag*]] [app.main.ui.ds.storybook :as sb] [app.main.ui.ds.tooltip.tooltip :refer [tooltip*]] [app.main.ui.ds.utilities.date :refer [date*]] @@ -89,6 +93,10 @@ :BranchPill branch-pill* :BranchRow branch-row* :BranchStatus branch-status* + :ChangeRow change-row* + :FilterChip filter-chip* + :PreviewCard preview-card* + :Tag tag* :Milestone milestone* :MilestoneGroup milestone-group* :Date date* diff --git a/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs b/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs index c3f17bce70..7dad14efd3 100644 --- a/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs +++ b/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs @@ -100,6 +100,7 @@ (def ^:icon-id close "close") (def ^:icon-id close-small "close-small") (def ^:icon-id code "code") +(def ^:icon-id collapse "collapse") (def ^:icon-id column "column") (def ^:icon-id column-reverse "column-reverse") (def ^:icon-id comments "comments") @@ -158,6 +159,7 @@ (def ^:icon-id git-branch-plus "git-branch-plus") (def ^:icon-id git-commit "git-commit") (def ^:icon-id git-commit-vertical "git-commit-vertical") +(def ^:icon-id git-compare-arrows "git-compare-arrows") (def ^:icon-id git-merge "git-merge") (def ^:icon-id git-pull-request-arrow "git-pull-request-arrow") (def ^:icon-id graphics "graphics") @@ -246,6 +248,7 @@ (def ^:icon-id separate-nodes "separate-nodes") (def ^:icon-id settings "settings") (def ^:icon-id shown "shown") +(def ^:icon-id side-by-side "side-by-side") (def ^:icon-id size-horizontal "size-horizontal") (def ^:icon-id size-vertical "size-vertical") (def ^:icon-id snap-nodes "snap-nodes") diff --git a/frontend/src/app/main/ui/ds/product/change_row.cljs b/frontend/src/app/main/ui/ds/product/change_row.cljs new file mode 100644 index 0000000000..c3e0c4de65 --- /dev/null +++ b/frontend/src/app/main/ui/ds/product/change_row.cljs @@ -0,0 +1,73 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS SUBSIDIARY SL + +(ns app.main.ui.ds.product.change-row + (:require-macros + [app.main.style :as stl]) + (:require + [app.common.data :as d] + [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] + [app.main.ui.ds.foundations.typography :as t] + [app.main.ui.ds.foundations.typography.text :refer [text*]] + [app.main.ui.ds.product.tag :as tag] + [app.util.keyboard :as kbd] + [rumext.v2 :as mf])) + +(def ^:private schema:change-row + [:map + [:class {:optional true} [:maybe :string]] + [:title :string] + [:subtitle {:optional true} [:maybe :string]] + [:icon {:optional true} [:maybe [:and :string [:fn #(contains? i/icon-list %)]]]] + [:tone {:optional true} [:maybe (into [:enum] tag/tones)]] + [:is-selected {:optional true} [:maybe :boolean]] + [:is-muted {:optional true} [:maybe :boolean]] + [:on-click {:optional true} [:maybe fn?]]]) + +(mf/defc change-row* + "One entry of a change list: a short bar in the tone of its status + beside a card holding an optional icon, a title over a one-line summary, and a slot at + each end (`start` for a checkbox or a chevron, `end` for a tag). A + muted row reads as left out." + {::mf/schema schema:change-row} + [{:keys [class title subtitle icon tone is-selected is-muted start end on-click] :rest props}] + (let [tone (d/nilv tone "neutral") + is-selected (d/nilv is-selected false) + is-muted (d/nilv is-muted false) + on-key-down (mf/use-fn + (mf/deps on-click) + (fn [event] + (when (and on-click (or (kbd/enter? event) (kbd/space? event))) + (.preventDefault event) + (on-click event)))) + props (mf/spread-props props + {:class [class (stl/css-case :change-row true + :is-selected is-selected + :is-muted is-muted + :tone-accent (= tone "accent") + :tone-info (= tone "info") + :tone-warning (= tone "warning") + :tone-error (= tone "error") + :tone-success (= tone "success") + :tone-purple (= tone "purple"))] + :role "button" + :tab-index 0 + :aria-pressed is-selected + :on-click on-click + :on-key-down on-key-down})] + [:> :div props + [:span {:class (stl/css :bar) :aria-hidden true}] + [:div {:class (stl/css :card)} + (when start + [:div {:class (stl/css :start)} start]) + (when icon + [:> icon* {:icon-id icon :size "s" :class (stl/css :icon)}]) + [:div {:class (stl/css :body)} + [:> text* {:as "span" :typography t/body-small :class (stl/css :title)} title] + (when subtitle + [:> text* {:as "span" :typography t/body-small :class (stl/css :subtitle)} subtitle])] + (when end + [:div {:class (stl/css :end)} end])]])) diff --git a/frontend/src/app/main/ui/ds/product/change_row.mdx b/frontend/src/app/main/ui/ds/product/change_row.mdx new file mode 100644 index 0000000000..87940f9b96 --- /dev/null +++ b/frontend/src/app/main/ui/ds/product/change_row.mdx @@ -0,0 +1,44 @@ +{ /* This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + + Copyright (c) KALEIDOS SUBSIDIARY SL */ } + +import { Canvas, Meta } from '@storybook/addon-docs/blocks'; +import * as ChangeRow from "./change_row.stories"; + + + +# ChangeRow + +One entry of a change list: a coloured bar for its status, an icon, a +title over a one-line summary, and a slot at each end. + + + +## Variants + +`end` takes a tag or a count. + + + +`start` takes a checkbox or a chevron. `is-muted` dims a row that is +left out. + + + +## Technical notes + +The row acts as a button (`role="button"`, Enter and Space call +`on-click`). Controls in the slots must stop the click from reaching +the row. + +```clj +[:> change-row* {:title (:label item) + :subtitle summary + :icon i/board + :tone "info" + :is-selected selected? + :end (mf/html [:> tag* {:variant "pill"} "Added"]) + :on-click on-select}] +``` diff --git a/frontend/src/app/main/ui/ds/product/change_row.scss b/frontend/src/app/main/ui/ds/product/change_row.scss new file mode 100644 index 0000000000..ecb868c1ff --- /dev/null +++ b/frontend/src/app/main/ui/ds/product/change_row.scss @@ -0,0 +1,119 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS SUBSIDIARY SL + +@use "ds/_borders.scss" as *; +@use "ds/_utils.scss" as *; +@use "ds/mixins.scss" as *; + +.change-row { + --row-bar-color: var(--color-foreground-secondary); + --row-bg-color: var(--color-background-tertiary); + --row-title-color: var(--color-foreground-primary); + + display: flex; + align-items: center; + gap: var(--sp-xxs); + cursor: pointer; + + &:hover, + &.is-selected { + --row-bg-color: var(--color-background-quaternary); + } + + &:focus-visible { + outline: none; + } + + &:focus-visible .card { + outline: $b-1 solid var(--color-accent-primary); + } + + &.is-muted { + --row-title-color: var(--color-foreground-secondary); + } + + &.tone-accent { + --row-bar-color: var(--color-accent-primary); + } + + &.tone-info { + --row-bar-color: var(--color-accent-info); + } + + &.tone-warning { + --row-bar-color: var(--color-accent-warning); + } + + &.tone-error { + --row-bar-color: var(--color-accent-error); + } + + &.tone-success { + --row-bar-color: var(--color-accent-success); + } + + &.tone-purple { + --row-bar-color: var(--color-accent-secondary); + } +} + +.bar { + flex-shrink: 0; + inline-size: px2rem(3); + block-size: px2rem(30); + border-radius: px2rem(2); + background-color: var(--row-bar-color); +} + +.card { + display: flex; + flex: 1; + align-items: center; + gap: var(--sp-s); + min-inline-size: 0; + min-block-size: px2rem(46); + padding-block: var(--sp-xs); + padding-inline: var(--sp-s); + border-radius: $br-8; + background-color: var(--row-bg-color); +} + +.start { + display: flex; + flex-shrink: 0; + align-items: center; +} + +.icon { + flex-shrink: 0; + color: var(--color-foreground-secondary); +} + +.body { + display: flex; + flex: 1; + flex-direction: column; + min-inline-size: 0; +} + +.title { + @include text-ellipsis; + + color: var(--row-title-color); +} + +.subtitle { + @include two-line-text-ellipsis; + + color: var(--color-foreground-secondary); +} + +.end { + display: flex; + flex-shrink: 0; + align-items: center; + gap: var(--sp-xs); +} diff --git a/frontend/src/app/main/ui/ds/product/change_row.stories.jsx b/frontend/src/app/main/ui/ds/product/change_row.stories.jsx new file mode 100644 index 0000000000..b91b179bfc --- /dev/null +++ b/frontend/src/app/main/ui/ds/product/change_row.stories.jsx @@ -0,0 +1,67 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS SUBSIDIARY SL + +import * as React from "react"; +import Components from "@target/components"; + +const { ChangeRow, Tag, Checkbox } = Components; +const tones = [ + "neutral", + "accent", + "info", + "warning", + "error", + "success", + "purple", +]; + +export default { + title: "Product/Branching/ChangeRow", + component: ChangeRow, + argTypes: { + tone: { options: tones, control: { type: "select" } }, + isSelected: { control: { type: "boolean" } }, + isMuted: { control: { type: "boolean" } }, + }, + args: { + title: "Plans / row", + subtitle: "Spaced out: gap 8 → 24", + icon: "board", + tone: "info", + isSelected: false, + isMuted: false, + }, + render: ({ ...args }) => ( +
+ +
+ ), +}; + +export const Default = {}; + +export const WithTag = { + args: { + title: "Card / Promo", + subtitle: "Corner radius - both changed", + tone: "warning", + end: ( + + Choose + + ), + }, +}; + +export const WithCheckbox = { + args: { + title: "Promo banner", + subtitle: "New board - left out", + tone: "success", + isMuted: true, + start: , + }, +}; diff --git a/frontend/src/app/main/ui/ds/product/filter_chip.cljs b/frontend/src/app/main/ui/ds/product/filter_chip.cljs new file mode 100644 index 0000000000..c727dbd376 --- /dev/null +++ b/frontend/src/app/main/ui/ds/product/filter_chip.cljs @@ -0,0 +1,47 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS SUBSIDIARY SL + +(ns app.main.ui.ds.product.filter-chip + (:require-macros + [app.main.style :as stl]) + (:require + [app.common.data :as d] + [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] + [app.main.ui.ds.product.tag :as tag] + [rumext.v2 :as mf])) + +(def ^:private schema:filter-chip + [:map + [:class {:optional true} [:maybe :string]] + [:label :string] + [:icon {:optional true} [:maybe [:and :string [:fn #(contains? i/icon-list %)]]]] + [:tone {:optional true} [:maybe (into [:enum] tag/tones)]] + [:is-selected {:optional true} [:maybe :boolean]]]) + +(mf/defc filter-chip* + "A rounded toggle that filters a list by one category, with an + optional icon before its label. Selected, a toned chip draws its + outline, text and icon in its tone; a neutral one in the accent + colour." + {::mf/schema schema:filter-chip} + [{:keys [class label icon tone is-selected] :rest props}] + (let [tone (d/nilv tone "neutral") + is-selected (d/nilv is-selected false) + props (mf/spread-props props + {:class [class (stl/css-case :filter-chip true + :is-selected is-selected + :tone-accent (= tone "accent") + :tone-info (= tone "info") + :tone-warning (= tone "warning") + :tone-error (= tone "error") + :tone-success (= tone "success") + :tone-purple (= tone "purple"))] + :type "button" + :aria-pressed is-selected})] + [:> :button props + (when icon + [:> icon* {:icon-id icon :size "s" :class (stl/css :icon)}]) + label])) diff --git a/frontend/src/app/main/ui/ds/product/filter_chip.mdx b/frontend/src/app/main/ui/ds/product/filter_chip.mdx new file mode 100644 index 0000000000..f2025f0afe --- /dev/null +++ b/frontend/src/app/main/ui/ds/product/filter_chip.mdx @@ -0,0 +1,43 @@ +{ /* This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + + Copyright (c) KALEIDOS SUBSIDIARY SL */ } + +import { Canvas, Meta } from '@storybook/addon-docs/blocks'; +import * as FilterChip from "./filter_chip.stories"; + + + +# FilterChip + +A rounded toggle that filters a list by one category. + + + +## Variants + +The selected chip draws its outline, text and icon in one colour: its +tone when it has one, the accent colour when it is neutral. + + + +`icon` goes before the label, to tell neutral chips apart without +colour. + + + +A row of chips filters the same list. + + + +## Technical notes + +It renders a `button` with `aria-pressed`. `label` is mandatory; any +other prop goes to the button. + +```clj +[:> filter-chip* {:label "Copy (1)" :icon i/text + :is-selected (= filter :copy) + :on-click on-filter}] +``` diff --git a/frontend/src/app/main/ui/ds/product/filter_chip.scss b/frontend/src/app/main/ui/ds/product/filter_chip.scss new file mode 100644 index 0000000000..c90d1aac5f --- /dev/null +++ b/frontend/src/app/main/ui/ds/product/filter_chip.scss @@ -0,0 +1,90 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS SUBSIDIARY SL + +@use "ds/_borders.scss" as *; +@use "ds/_sizes.scss" as *; +@use "ds/typography.scss" as *; + +.filter-chip { + --chip-fg-color: var(--color-foreground-secondary); + --chip-bg-color: var(--color-background-quaternary); + --chip-border-color: transparent; + --chip-selected-color: var(--color-accent-primary); + + @include use-typography("body-small"); + + display: inline-flex; + flex-shrink: 0; + align-items: center; + gap: var(--sp-xs); + + // the box of a small DS icon (16) plus the padding and the border, so + // chips with and without an icon share one height + min-block-size: calc(#{$sz-16} + var(--sp-xxs) * 2 + #{$b-1} * 2); + padding: var(--sp-xxs) var(--sp-s); + border: $b-1 solid var(--chip-border-color); + border-radius: $br-12; + background-color: var(--chip-bg-color); + color: var(--chip-fg-color); + white-space: nowrap; + cursor: pointer; + + &:hover { + --chip-border-color: var(--color-foreground-secondary); + } + + &:focus-visible { + outline: $b-1 solid var(--color-accent-primary); + } + + &.tone-accent { + --chip-fg-color: var(--color-accent-primary); + --chip-selected-color: var(--color-accent-primary); + --chip-bg-color: var(--color-accent-primary-muted); + } + + &.tone-info { + --chip-fg-color: var(--color-accent-info); + --chip-selected-color: var(--color-accent-info); + --chip-bg-color: var(--color-background-info); + } + + &.tone-warning { + --chip-fg-color: var(--color-accent-warning); + --chip-selected-color: var(--color-accent-warning); + --chip-bg-color: var(--color-background-warning); + } + + &.tone-error { + --chip-fg-color: var(--color-accent-error); + --chip-selected-color: var(--color-accent-error); + --chip-bg-color: var(--color-background-error); + } + + &.tone-success { + --chip-fg-color: var(--color-accent-success); + --chip-selected-color: var(--color-accent-success); + --chip-bg-color: var(--color-background-success); + } + + &.tone-purple { + --chip-fg-color: var(--color-accent-secondary); + --chip-selected-color: var(--color-accent-secondary); + + // TODO: no DS background token pairs with the secondary accent + --chip-bg-color: color-mix(in srgb, var(--color-accent-secondary) 18%, transparent); + } + + // the selected chip reads in its outline colour, text and icon too + &.is-selected { + --chip-border-color: var(--chip-selected-color); + --chip-fg-color: var(--chip-selected-color); + } +} + +.icon { + flex-shrink: 0; +} diff --git a/frontend/src/app/main/ui/ds/product/filter_chip.stories.jsx b/frontend/src/app/main/ui/ds/product/filter_chip.stories.jsx new file mode 100644 index 0000000000..0c1a85f27e --- /dev/null +++ b/frontend/src/app/main/ui/ds/product/filter_chip.stories.jsx @@ -0,0 +1,50 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS SUBSIDIARY SL + +import * as React from "react"; +import Components from "@target/components"; + +const { FilterChip } = Components; +const tones = [ + "neutral", + "accent", + "info", + "warning", + "error", + "success", + "purple", +]; + +export default { + title: "Product/Branching/FilterChip", + component: FilterChip, + argTypes: { + tone: { options: tones, control: { type: "select" } }, + isSelected: { control: { type: "boolean" } }, + }, + args: { label: "Decisions (2)", tone: "warning", isSelected: false }, + render: ({ ...args }) => , +}; + +export const Default = {}; + +export const Selected = { args: { isSelected: true } }; + +export const WithIcon = { + args: { label: "Copy (1)", icon: "text", tone: "neutral" }, +}; + +export const Bar = { + render: () => ( +
+ + + + + +
+ ), +}; diff --git a/frontend/src/app/main/ui/ds/product/preview_card.cljs b/frontend/src/app/main/ui/ds/product/preview_card.cljs new file mode 100644 index 0000000000..19d82d786f --- /dev/null +++ b/frontend/src/app/main/ui/ds/product/preview_card.cljs @@ -0,0 +1,53 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS SUBSIDIARY SL + +(ns app.main.ui.ds.product.preview-card + (:require-macros + [app.main.style :as stl]) + (:require + [app.common.data :as d] + [app.main.ui.ds.foundations.typography :as t] + [app.main.ui.ds.foundations.typography.text :refer [text*]] + [rumext.v2 :as mf])) + +(def ^:private schema:preview-card + [:map + [:class {:optional true} [:maybe :string]] + [:label :string] + [:side {:optional true} [:maybe [:enum "base" "main" "branch" "result"]]] + [:detail {:optional true} [:maybe :string]] + [:caption {:optional true} [:maybe :string]] + [:is-warning {:optional true} [:maybe :boolean]] + [:is-large {:optional true} [:maybe :boolean]]]) + +(mf/defc preview-card* + "A framed preview of one side of a comparison. The side picks the + colour of the frame and of the label: base is neutral, + main purple, the branch the accent and the merged result green. + `detail` follows the label (who or when); `caption` sits under the + preview, in the warning colour when `is-warning` says so. `is-large` + gives the preview a taller stage, for a view that has the room." + {::mf/schema schema:preview-card} + [{:keys [class label side detail caption is-warning is-large children] :rest props}] + (let [side (d/nilv side "base") + is-warning (d/nilv is-warning false) + is-large (d/nilv is-large false) + props (mf/spread-props props + {:class [class (stl/css-case :preview-card true + :side-main (= side "main") + :side-branch (= side "branch") + :side-result (= side "result"))]})] + [:> :div props + [:div {:class (stl/css :head)} + [:> text* {:as "span" :typography t/headline-small :class (stl/css :label)} label] + (when detail + [:> text* {:as "span" :typography t/body-small :class (stl/css :detail)} detail])] + [:div {:class (stl/css-case :stage true :is-large is-large)} children] + (when caption + [:> text* {:as "span" + :typography t/body-small + :class (stl/css-case :caption true :is-warning is-warning)} + caption])])) diff --git a/frontend/src/app/main/ui/ds/product/preview_card.mdx b/frontend/src/app/main/ui/ds/product/preview_card.mdx new file mode 100644 index 0000000000..c685bfee9e --- /dev/null +++ b/frontend/src/app/main/ui/ds/product/preview_card.mdx @@ -0,0 +1,49 @@ +{ /* This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + + Copyright (c) KALEIDOS SUBSIDIARY SL */ } + +import { Canvas, Meta } from '@storybook/addon-docs/blocks'; +import * as PreviewCard from "./preview_card.stories"; + + + +# PreviewCard + +A framed preview of one side of a comparison, labelled with the side +it shows. + + + +## Variants + +The side colours the frame and the label: base neutral, main purple, the +branch the accent, the merged result green. + + + +`caption` sits under the preview; `is-warning` draws it in the error +colour. + + + +`is-large` gives the preview a taller stage, for a view with room to +spare (the expanded comparison). + + + +## Technical notes + +The stage is 100 px high, 320 px with `is-large`. A container that +knows how much room it has can set `--preview-card-stage-block-size` +and `--preview-card-stage-large-block-size` to change them. + +The children fill the dark stage in the middle, centred. + +```clj +[:> preview-card* {:label (tr "workspace.branches.review.side.main") + :side "main" + :detail main-ago} + [:> entity-preview* {:kind :shape :value main}]] +``` diff --git a/frontend/src/app/main/ui/ds/product/preview_card.scss b/frontend/src/app/main/ui/ds/product/preview_card.scss new file mode 100644 index 0000000000..8e9c86836a --- /dev/null +++ b/frontend/src/app/main/ui/ds/product/preview_card.scss @@ -0,0 +1,85 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS SUBSIDIARY SL + +@use "ds/_borders.scss" as *; +@use "ds/_sizes.scss" as *; +@use "ds/_utils.scss" as *; +@use "ds/mixins.scss" as *; + +.preview-card { + --card-side-color: var(--color-foreground-secondary); + + display: flex; + flex-direction: column; + gap: var(--sp-s); + min-inline-size: 0; + padding: var(--sp-m); + border: $b-1 solid var(--card-side-color); + border-radius: $br-8; + background-color: var(--color-background-tertiary); + + &.side-main { + --card-side-color: var(--color-accent-secondary); + } + + &.side-branch { + --card-side-color: var(--color-accent-primary); + } + + &.side-result { + --card-side-color: var(--color-accent-success); + } +} + +.head { + display: flex; + align-items: center; + gap: var(--sp-xs); + min-inline-size: 0; +} + +.label { + flex-shrink: 0; + color: var(--card-side-color); +} + +.detail { + @include text-ellipsis; + + color: var(--color-foreground-secondary); +} + +.stage { + // the checkerboard of a transparent background, the same in both + // themes (the DS swatch draws its transparency the same way), so + // white, black and grey shapes all stand out from it + --stage-check-color: color-mix(in srgb, var(--color-canvas) 70%, var(--color-static-white)); + + display: flex; + align-items: center; + justify-content: center; + + // a container that knows its room sets these two + block-size: var(--preview-card-stage-block-size, #{px2rem(100)}); + padding: var(--sp-s); + overflow: hidden; + border-radius: $br-6; + background-color: var(--color-static-white); + background-image: repeating-conic-gradient(var(--stage-check-color) 0% 25%, transparent 0% 50%); + background-size: px2rem(20) px2rem(20); + + &.is-large { + block-size: var(--preview-card-stage-large-block-size, #{px2rem(320)}); + } +} + +.caption { + color: var(--color-foreground-secondary); + + &.is-warning { + color: var(--color-accent-error); + } +} diff --git a/frontend/src/app/main/ui/ds/product/preview_card.stories.jsx b/frontend/src/app/main/ui/ds/product/preview_card.stories.jsx new file mode 100644 index 0000000000..bddddcb2c6 --- /dev/null +++ b/frontend/src/app/main/ui/ds/product/preview_card.stories.jsx @@ -0,0 +1,83 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS SUBSIDIARY SL + +import * as React from "react"; +import Components from "@target/components"; + +const { PreviewCard } = Components; + +const Sample = ({ text }) => ( +
+ {text} +
+); + +export default { + title: "Product/Branching/PreviewCard", + component: PreviewCard, + argTypes: { + side: { + options: ["base", "main", "branch", "result"], + control: { type: "radio" }, + }, + isWarning: { control: { type: "boolean" } }, + isLarge: { control: { type: "boolean" } }, + }, + args: { label: "Branch", side: "branch", detail: "You", isWarning: false }, + render: ({ ...args }) => ( +
+ + + +
+ ), +}; + +export const Default = {}; + +export const Sides = { + render: () => ( +
+ + + + + + + + + +
+ ), +}; + +export const Large = { + args: { isLarge: true }, +}; + +export const WithCaption = { + args: { + label: "Result", + side: "result", + detail: "Merged as stored", + caption: "Card 4 from main lands on card 3.", + isWarning: true, + }, +}; diff --git a/frontend/src/app/main/ui/ds/product/tag.cljs b/frontend/src/app/main/ui/ds/product/tag.cljs new file mode 100644 index 0000000000..3eb7fb7fdb --- /dev/null +++ b/frontend/src/app/main/ui/ds/product/tag.cljs @@ -0,0 +1,47 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS SUBSIDIARY SL + +(ns app.main.ui.ds.product.tag + (:require-macros + [app.main.style :as stl]) + (:require + [app.common.data :as d] + [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] + [rumext.v2 :as mf])) + +(def tones + #{"neutral" "accent" "info" "warning" "error" "success" "purple"}) + +(def ^:private schema:tag + [:map + [:class {:optional true} [:maybe :string]] + [:icon {:optional true} [:maybe [:and :string [:fn #(contains? i/icon-list %)]]]] + [:tone {:optional true} [:maybe (into [:enum] tones)]] + [:variant {:optional true} [:maybe [:enum "label" "pill"]]]]) + +(mf/defc tag* + "A short, static status word. The `label` variant is a small code-font + tag that names what something is (Board, Verified, Clash); + the `pill` variant is a rounded word or count that sits at the end of a + row (Choose, Decided, 3 edits). The tone colours both; `icon` goes + before the text." + {::mf/schema schema:tag} + [{:keys [class icon tone variant children] :rest props}] + (let [tone (d/nilv tone "neutral") + variant (d/nilv variant "label") + props (mf/spread-props props + {:class [class (stl/css-case :tag true + :is-pill (= variant "pill") + :tone-accent (= tone "accent") + :tone-info (= tone "info") + :tone-warning (= tone "warning") + :tone-error (= tone "error") + :tone-success (= tone "success") + :tone-purple (= tone "purple"))]})] + [:> :span props + (when icon + [:> icon* {:icon-id icon :size "s"}]) + children])) diff --git a/frontend/src/app/main/ui/ds/product/tag.mdx b/frontend/src/app/main/ui/ds/product/tag.mdx new file mode 100644 index 0000000000..f54fba84f9 --- /dev/null +++ b/frontend/src/app/main/ui/ds/product/tag.mdx @@ -0,0 +1,47 @@ +{ /* This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + + Copyright (c) KALEIDOS SUBSIDIARY SL */ } + +import { Canvas, Meta } from '@storybook/addon-docs/blocks'; +import * as Tag from "./tag.stories"; + + + +# Tag + +A short, static status word that says what something is or where it +stands. + + + +## Variants + +`label` (the default) is a small code-font tag that names a type or a +state: Board, Main component, Verified, Clash. + +`pill` is a rounded word or count at the end of a row: Choose, Decided, +3 edits, 14 shapes. + + + +The tone colours the text and its background. Text keeps the case it +is written in; the tag never turns it uppercase. + + + +`icon` goes before the text. + + + +## Technical notes + +```clj +[:> tag* {:tone "warning"} (tr "workspace.branches.review.tag.clash")] +[:> tag* {:tone "success" :variant "pill"} (tr "workspace.branches.review.decided")] +``` + +## Usage guidelines (design) + +Use it for a word that does not act. For a filter, use `FilterChip`. diff --git a/frontend/src/app/main/ui/ds/product/tag.scss b/frontend/src/app/main/ui/ds/product/tag.scss new file mode 100644 index 0000000000..3be7be8190 --- /dev/null +++ b/frontend/src/app/main/ui/ds/product/tag.scss @@ -0,0 +1,71 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS SUBSIDIARY SL + +@use "ds/_borders.scss" as *; +@use "ds/_sizes.scss" as *; +@use "ds/_utils.scss" as *; +@use "ds/typography.scss" as *; + +.tag { + --tag-fg-color: var(--color-foreground-secondary); + --tag-bg-color: var(--color-background-quaternary); + + @include use-typography("code-font"); + + display: inline-flex; + flex-shrink: 0; + align-items: center; + gap: var(--sp-xxs); + + // the box of a small DS icon (16) plus the padding, so a tag with an + // icon and one without stand the same height side by side + min-block-size: calc(#{$sz-16} + var(--sp-xxs) * 2); + padding: var(--sp-xxs) var(--sp-xs); + border-radius: $br-4; + background-color: var(--tag-bg-color); + color: var(--tag-fg-color); + font-size: px2rem(10); + white-space: nowrap; + + &.is-pill { + @include use-typography("body-small"); + + padding: var(--sp-xxs) var(--sp-s); + border-radius: $br-12; + } + + &.tone-accent { + --tag-fg-color: var(--color-accent-primary); + --tag-bg-color: var(--color-accent-primary-muted); + } + + &.tone-info { + --tag-fg-color: var(--color-accent-info); + --tag-bg-color: var(--color-background-info); + } + + &.tone-warning { + --tag-fg-color: var(--color-accent-warning); + --tag-bg-color: var(--color-background-warning); + } + + &.tone-error { + --tag-fg-color: var(--color-accent-error); + --tag-bg-color: var(--color-background-error); + } + + &.tone-success { + --tag-fg-color: var(--color-accent-success); + --tag-bg-color: var(--color-background-success); + } + + &.tone-purple { + --tag-fg-color: var(--color-accent-secondary); + + // TODO: no DS background token pairs with the secondary accent + --tag-bg-color: color-mix(in srgb, var(--color-accent-secondary) 18%, transparent); + } +} diff --git a/frontend/src/app/main/ui/ds/product/tag.stories.jsx b/frontend/src/app/main/ui/ds/product/tag.stories.jsx new file mode 100644 index 0000000000..5a005c2b1b --- /dev/null +++ b/frontend/src/app/main/ui/ds/product/tag.stories.jsx @@ -0,0 +1,57 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS SUBSIDIARY SL + +import * as React from "react"; +import Components from "@target/components"; + +const { Tag } = Components; +const tones = [ + "neutral", + "accent", + "info", + "warning", + "error", + "success", + "purple", +]; + +export default { + title: "Product/Branching/Tag", + component: Tag, + argTypes: { + tone: { options: tones, control: { type: "select" } }, + variant: { options: ["label", "pill"], control: { type: "radio" } }, + }, + args: { tone: "neutral", variant: "label", children: "Board" }, + render: ({ ...args }) => , +}; + +export const Default = {}; + +export const Pill = { + args: { variant: "pill", tone: "warning", children: "Choose" }, +}; + +export const WithIcon = { + args: { icon: "component", children: "Components" }, +}; + +export const Tones = { + render: () => ( +
+ {tones.map((tone) => ( + + {tone} + + ))} + {tones.map((tone) => ( + + {tone} + + ))} +
+ ), +};