✨ 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.
This commit is contained in:
elhombretecla 2026-10-27 15:59:14 +01:00
parent dcedd40a42
commit f56da68b95
21 changed files with 1045 additions and 0 deletions

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" stroke-linecap="round" stroke-linejoin="round">
<path d="M9.643 3.528v2.829h2.828m-9 3.286h2.829v2.829m6.886-9.658l-3.543 3.543m-6.858 6.858l3.543-3.543"/>
</svg>

After

Width:  |  Height:  |  Size: 227 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" stroke-linecap="round" stroke-linejoin="round">
<path d="M5.333 4c0 1.105-.895 2-2 2s-2-.895-2-2 .895-2 2-2 2 .895 2 2zm9.334 8c0 1.105-.896 2-2 2-1.105 0-2-.895-2-2s.895-2 2-2c1.104 0 2 .895 2 2zm-6.667-8h3.333c.737 0 1.334.597 1.334 1.333v4.667m-2.667-4-2-2 2-2m-2 10h-3.333c-.737 0-1.334-.597-1.334-1.333v-4.667m2.667 4 2 2-2 2"/>
</svg>

After

Width:  |  Height:  |  Size: 405 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" stroke-linecap="round" stroke-linejoin="round">
<path d="M3.333 2h9.334c.736 0 1.333.597 1.333 1.333v9.334c0 .736-.597 1.333-1.333 1.333h-9.334c-.736 0-1.333-.597-1.333-1.333v-9.334c0-.736.597-1.333 1.333-1.333zm4.667 2.333v1m0 2.167v1m0 2.167v1"/>
</svg>

After

Width:  |  Height:  |  Size: 320 B

View File

@ -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*

View File

@ -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")

View File

@ -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])]]))

View File

@ -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";
<Meta title="Product/Branching/ChangeRow" />
# 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.
<Canvas of={ChangeRow.Default} />
## Variants
`end` takes a tag or a count.
<Canvas of={ChangeRow.WithTag} />
`start` takes a checkbox or a chevron. `is-muted` dims a row that is
left out.
<Canvas of={ChangeRow.WithCheckbox} />
## 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}]
```

View File

@ -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);
}

View File

@ -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 }) => (
<div style={{ width: "290px" }}>
<ChangeRow {...args} />
</div>
),
};
export const Default = {};
export const WithTag = {
args: {
title: "Card / Promo",
subtitle: "Corner radius - both changed",
tone: "warning",
end: (
<Tag tone="warning" variant="pill">
Choose
</Tag>
),
},
};
export const WithCheckbox = {
args: {
title: "Promo banner",
subtitle: "New board - left out",
tone: "success",
isMuted: true,
start: <Checkbox id="row-check" checked={false} />,
},
};

View File

@ -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]))

View File

@ -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";
<Meta title="Product/Branching/FilterChip" />
# FilterChip
A rounded toggle that filters a list by one category.
<Canvas of={FilterChip.Default} />
## 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.
<Canvas of={FilterChip.Selected} />
`icon` goes before the label, to tell neutral chips apart without
colour.
<Canvas of={FilterChip.WithIcon} />
A row of chips filters the same list.
<Canvas of={FilterChip.Bar} />
## 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}]
```

View File

@ -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;
}

View File

@ -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 }) => <FilterChip {...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: () => (
<div style={{ display: "flex", gap: "8px" }}>
<FilterChip label="All (19)" isSelected={true} />
<FilterChip label="Decisions (2)" icon="triangle-alert" tone="warning" />
<FilterChip label="Copy (1)" icon="text" />
<FilterChip label="Color (1)" icon="swatches" />
<FilterChip label="Components (1)" icon="component" />
</div>
),
};

View File

@ -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])]))

View File

@ -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";
<Meta title="Product/Branching/PreviewCard" />
# PreviewCard
A framed preview of one side of a comparison, labelled with the side
it shows.
<Canvas of={PreviewCard.Default} />
## Variants
The side colours the frame and the label: base neutral, main purple, the
branch the accent, the merged result green.
<Canvas of={PreviewCard.Sides} />
`caption` sits under the preview; `is-warning` draws it in the error
colour.
<Canvas of={PreviewCard.WithCaption} />
`is-large` gives the preview a taller stage, for a view with room to
spare (the expanded comparison).
<Canvas of={PreviewCard.Large} />
## 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}]]
```

View File

@ -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);
}
}

View File

@ -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 }) => (
<div
style={{
padding: "12px",
borderRadius: "6px",
background: "#2e3434",
color: "#fff",
}}
>
{text}
</div>
);
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 }) => (
<div style={{ width: "340px" }}>
<PreviewCard {...args}>
<Sample text="Summer offer" />
</PreviewCard>
</div>
),
};
export const Default = {};
export const Sides = {
render: () => (
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(3, 1fr)",
gap: "12px",
width: "740px",
}}
>
<PreviewCard label="Main" side="main" detail="2 hours ago">
<Sample text="12" />
</PreviewCard>
<PreviewCard label="Branch" side="branch" detail="You">
<Sample text="4" />
</PreviewCard>
<PreviewCard label="Result" side="result">
<Sample text="4" />
</PreviewCard>
</div>
),
};
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,
},
};

View File

@ -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]))

View File

@ -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";
<Meta title="Product/Branching/Tag" />
# Tag
A short, static status word that says what something is or where it
stands.
<Canvas of={Tag.Default} />
## 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.
<Canvas of={Tag.Pill} />
The tone colours the text and its background. Text keeps the case it
is written in; the tag never turns it uppercase.
<Canvas of={Tag.Tones} />
`icon` goes before the text.
<Canvas of={Tag.WithIcon} />
## 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`.

View File

@ -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);
}
}

View File

@ -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 }) => <Tag {...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: () => (
<div style={{ display: "flex", flexWrap: "wrap", gap: "8px" }}>
{tones.map((tone) => (
<Tag key={tone} tone={tone}>
{tone}
</Tag>
))}
{tones.map((tone) => (
<Tag key={"pill-" + tone} tone={tone} variant="pill">
{tone}
</Tag>
))}
</div>
),
};