Files
task-stream/libs/web/tracker/presenters/src/lib/pages/issue.page.scss
Josep Piguillem 93d028e870 feat: ask a teammate to review a ticket
«Pedir revisión» on a ticket asks a member (with an optional note): they
are notified and see it under «Te piden revisión» in my work until they
answer «Todo bien» or «Pedir cambios» (with what to change) from the
ticket's Revisiones box; whoever asked hears the answer and can withdraw
an open request. Events of the ticket (requested, answered, cancelled),
so they show in its history and the activity feed. Viewers can be asked
and answer; not oneself, nor twice the same person with one pending.

API: requestReview / answerReview / cancelReview, Issue.reviews,
myIssues(reviewing); POST /api/issues/{issue}/reviews,
POST …/reviews/{id}/answer, DELETE …/reviews/{id}. MCP: request_review,
answer_review, my_issues reviewing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 20:38:30 +02:00

929 lines
17 KiB
SCSS

.ts-back {
display: inline-block;
margin-bottom: 1.25rem;
font-family: var(--ts-font-mono);
font-size: 0.8125rem;
color: var(--ts-muted);
text-decoration: none;
&:hover {
color: var(--ts-ink);
}
}
.ts-issue {
display: grid;
grid-template-columns: minmax(0, 1fr) 20rem;
gap: 2.5rem;
align-items: start;
@media (max-width: 960px) {
grid-template-columns: minmax(0, 1fr);
}
}
.ts-issue__main {
display: grid;
// Without it the column takes the width of its widest field: on a phone,
// wider than the screen.
grid-template-columns: minmax(0, 1fr);
gap: 2rem;
min-width: 0;
}
// Header: key · type · status, then the title.
.ts-issue__meta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
}
.ts-type {
display: inline-flex;
align-items: center;
gap: 0.3125rem;
font-size: 0.8125rem;
font-weight: 600;
color: var(--tone);
}
.ts-issue__title-row {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 0.75rem 1rem;
margin-top: 0.75rem;
h1 {
flex: 1 1 20rem;
min-width: 0;
margin: 0;
font-size: clamp(1.625rem, 1.2rem + 1.4vw, 2.375rem);
font-weight: 700;
line-height: 1.1;
text-wrap: balance;
}
}
// Follow and edit, together, level with the title's first line.
.ts-issue__actions {
display: flex;
flex-shrink: 0;
gap: 0.5rem;
align-items: center;
margin-top: 0.25rem;
}
// Workflow: one button per allowed destination, in that status' colour.
.ts-transitions {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
padding: 0.75rem 0.875rem;
border: 1px dashed var(--ts-line-strong);
border-radius: 0.75rem;
.ts-eyebrow {
margin-right: 0.25rem;
}
}
.ts-move {
--tone: var(--ts-muted);
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.4375rem 0.75rem 0.4375rem 0.625rem;
border: 1px solid color-mix(in srgb, var(--tone) 35%, var(--ts-line));
border-radius: 999px;
background: var(--ts-surface);
color: var(--ts-ink);
font: inherit;
font-size: 0.8125rem;
font-weight: 600;
cursor: pointer;
transition:
background-color 0.15s ease,
border-color 0.15s ease;
&::before {
content: '';
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
background: var(--tone);
}
span {
color: var(--tone);
transition: transform 0.15s ease;
}
&:hover {
background: color-mix(in srgb, var(--tone) 10%, var(--ts-surface));
border-color: var(--tone);
span {
transform: translateX(2px);
}
}
&:focus-visible {
outline: 2px solid var(--tone);
outline-offset: 2px;
}
}
// Sections: small uppercase label over a rule (pireon.dev's eyebrow, in grey).
.ts-section {
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0 0 0.875rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--ts-line);
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--ts-muted);
.ts-mono {
color: var(--ts-ink-soft);
}
}
.ts-block > p {
margin: 0;
line-height: 1.6;
}
.ts-figma {
display: inline-flex;
align-items: center;
gap: 0.375rem;
margin-top: 0.75rem;
padding: 0.375rem 0.75rem;
border-radius: 999px;
background: color-mix(in srgb, var(--ts-t-ui) 10%, var(--ts-surface));
color: color-mix(in srgb, var(--ts-t-ui) 80%, var(--ts-ink));
font-size: 0.8125rem;
font-weight: 600;
text-decoration: none;
}
.ts-link {
display: flex;
align-items: center;
gap: 0.625rem;
padding: 0.375rem 0;
}
.ts-link__kind {
min-width: 9rem;
font-size: 0.875rem;
color: var(--ts-muted);
}
a.ts-key {
text-decoration: none;
&:hover {
border-color: var(--ts-brand);
color: var(--ts-brand);
}
}
.ts-inline-form {
display: grid;
grid-template-columns: 12rem minmax(0, 1fr) auto;
gap: 0.5rem;
align-items: center;
margin-top: 0.875rem;
@media (max-width: 640px) {
grid-template-columns: minmax(0, 1fr);
}
}
.ts-inline-form--two {
grid-template-columns: minmax(0, 1fr) auto;
}
// Breakdown: the ticket it hangs from, its checklist and its subtasks.
.ts-parent {
display: inline-flex;
align-items: center;
gap: 0.375rem;
min-width: 0;
color: var(--ts-muted);
font-size: 0.8125rem;
text-decoration: none;
&:hover {
color: var(--ts-brand);
}
}
.ts-parent__title {
overflow: hidden;
max-width: 18rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.ts-checklist__rule {
margin: 0 0 0.5rem;
color: var(--ts-muted);
font-size: 0.8125rem;
}
.ts-checklist,
.ts-children {
display: grid;
gap: 0.25rem;
margin: 0;
padding: 0;
list-style: none;
}
.ts-checklist__item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
&.is-done {
color: var(--ts-muted);
text-decoration: line-through;
}
}
.ts-children {
margin-top: 0.75rem;
}
.ts-child {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto auto;
gap: 0.625rem;
align-items: center;
padding: 0.375rem 0;
border-top: 1px solid var(--ts-line);
.ts-comment__avatar {
width: 1.5rem;
height: 1.5rem;
font-size: 0.625rem;
}
}
.ts-child__title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ts-parent-form {
display: flex;
align-items: center;
gap: 0.75rem;
.ts-inline-form {
flex: 1;
}
> .ts-muted {
margin-top: 0.875rem;
}
}
// Comments: a thread with initials on a rail.
.ts-comment {
display: grid;
grid-template-columns: 2rem minmax(0, 1fr);
gap: 0.75rem;
padding: 0.875rem 0;
border-bottom: 1px solid var(--ts-line);
}
.ts-comment__avatar {
display: inline-grid;
place-items: center;
width: 2rem;
height: 2rem;
border-radius: 50%;
background: color-mix(in srgb, var(--ts-brand) 16%, var(--ts-surface));
color: color-mix(in srgb, var(--ts-brand) 70%, var(--ts-ink));
font-family: var(--ts-font-mono);
font-size: 0.6875rem;
font-weight: 600;
}
.ts-comment__body {
min-width: 0;
p {
margin: 0.25rem 0 0;
line-height: 1.55;
}
}
/* A decision: marked on its comment, and listed above the ticket. */
.ts-comment.is-decision {
margin-inline: -0.75rem;
padding-inline: 0.75rem;
border-left: 3px solid var(--ts-c-green);
background: color-mix(in srgb, var(--ts-c-green) 6%, transparent);
}
.ts-comment.is-pointed {
animation: ts-pointed 2.4s ease-out;
}
@keyframes ts-pointed {
0%,
40% {
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ts-brand) 45%, transparent);
}
100% {
box-shadow: 0 0 0 3px transparent;
}
}
.ts-comment__decision {
padding: 0.0625rem 0.5rem;
border-radius: 999px;
background: color-mix(in srgb, var(--ts-c-green) 16%, transparent);
color: var(--ts-c-green);
font-size: 0.6875rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
}
/* A plain icon button, not @pi/ui's sized one. */
.ts-comment__flag {
display: inline-grid;
place-items: center;
width: 2rem !important;
min-width: 0 !important;
height: 2rem !important;
padding: 0 !important;
border: 0;
border-radius: 0.5rem;
background: transparent;
color: var(--ts-muted);
cursor: pointer;
&:hover,
&:focus-visible {
background: var(--ts-sunken);
color: var(--ts-ink);
}
&.is-on {
color: var(--ts-c-green);
}
}
.ts-decisions {
ol {
display: grid;
gap: 0.875rem;
margin: 0;
padding: 0;
list-style: none;
}
li {
padding: 0.75rem 1rem;
border-left: 3px solid var(--ts-c-green);
border-radius: 0 0.5rem 0.5rem 0;
background: color-mix(in srgb, var(--ts-c-green) 7%, var(--ts-surface));
}
.ts-section pi-icon {
color: var(--ts-c-green);
}
}
.ts-decisions__meta {
margin: 0.375rem 0 0;
color: var(--ts-muted);
font-size: 0.8125rem;
}
/* Looks like a link: not @pi/ui's sized button. */
.ts-decisions__go {
display: inline !important;
min-width: 0 !important;
height: auto !important;
padding: 0 !important;
border: 0;
background: none !important;
color: var(--ts-brand);
font: inherit;
cursor: pointer;
&:hover,
&:focus-visible {
text-decoration: underline;
}
}
.ts-comment__head {
display: flex;
align-items: center;
gap: 0.5rem;
min-height: 2rem;
}
.ts-comment__when {
font-family: var(--ts-font-mono);
font-size: 0.75rem;
color: var(--ts-muted);
}
.ts-spacer {
flex: 1;
}
// History as a log: time · what · how long ago.
.ts-log {
margin: 0;
padding: 0;
list-style: none;
li {
display: grid;
grid-template-columns: 3.75rem minmax(0, 1fr) auto;
gap: 0.875rem;
align-items: baseline;
padding: 0.5rem 0;
border-bottom: 1px dashed var(--ts-line);
font-size: 0.875rem;
&:last-child {
border-bottom: 0;
}
}
}
.ts-log__when,
.ts-log__ago {
font-family: var(--ts-font-mono);
font-size: 0.75rem;
color: var(--ts-muted);
}
.ts-log__what {
color: var(--ts-ink-soft);
}
@media (max-width: 640px) {
.ts-log li {
grid-template-columns: 3.25rem minmax(0, 1fr);
}
.ts-log__ago {
display: none;
}
}
// Side: the ticket's sheet.
.ts-issue__side {
display: grid;
gap: 1.375rem;
> .ts-eyebrow {
margin-bottom: 0.125rem;
}
position: sticky;
top: 5.5rem;
padding: 1.5rem;
@media (max-width: 960px) {
position: static;
}
}
.ts-facts {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.625rem 1rem;
margin: 0.25rem 0 0;
padding-top: 1rem;
border-top: 1px dashed var(--ts-line);
font-size: 0.875rem;
dt {
font-family: var(--ts-font-mono);
font-size: 0.6875rem;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--ts-muted);
align-self: center;
}
dd {
margin: 0;
}
.ts-mono {
font-size: 0.8125rem;
}
}
.ts-labels {
display: flex;
flex-wrap: wrap;
gap: 0.25rem 0.5rem;
}
.ts-label {
font-family: var(--ts-font-mono);
font-size: 0.8125rem;
color: var(--ts-brand);
}
.ts-attachments {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
gap: 0.5rem;
margin: 0.5rem 0;
}
// Code that mentions the ticket (from the Git integration).
.ts-dev {
display: grid;
gap: 0.25rem;
margin: 0;
padding: 0;
list-style: none;
}
.ts-dev__item {
display: grid;
grid-template-columns: 4rem minmax(0, 1fr) auto auto auto;
gap: 0.625rem;
align-items: center;
padding: 0.375rem 0;
border-top: 1px solid var(--ts-line);
@media (max-width: 640px) {
grid-template-columns: 4rem minmax(0, 1fr);
.ts-dev__ref,
.ts-dev__when {
display: none;
}
}
.ts-pill[data-state='MERGED'] {
--tone: var(--ts-c-violet, var(--ts-brand));
}
.ts-pill[data-state='OPEN'] {
--tone: var(--ts-c-green, var(--ts-brand));
}
}
.ts-dev__kind {
color: var(--ts-muted);
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.ts-dev__title {
overflow: hidden;
color: inherit;
text-overflow: ellipsis;
white-space: nowrap;
}
.ts-dev__when {
font-size: 0.8125rem;
}
.ts-trashed {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
align-items: center;
margin-bottom: 1rem;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
background: color-mix(in srgb, var(--ts-c-red, #dc2626) 12%, transparent);
}
// One level of replies under each comment.
.ts-replies {
display: grid;
gap: 0.75rem;
margin: -0.25rem 0 0.5rem 2.75rem;
padding-left: 0.875rem;
border-left: 2px solid var(--ts-line);
&:empty {
display: none;
}
}
.ts-comment__gone {
margin: 0;
font-style: italic;
}
// A text link, past @pi/ui's global button look: as a 40 px pill it sat on
// the line under the comment.
.ts-thread__actions {
display: flex;
flex-wrap: wrap;
gap: 0.25rem 1rem;
margin-top: 0.375rem;
}
/* Small text buttons, not @pi/ui's sized ones. */
.ts-thread__action {
flex: none;
height: auto !important;
min-width: 0 !important;
padding: 0.125rem 0 !important;
border: 0;
background: none !important;
color: var(--ts-brand);
font: inherit;
font-size: 0.8125rem;
font-weight: 600;
cursor: pointer;
&:hover,
&:focus-visible {
text-decoration: underline;
}
}
/* A settled conversation, folded to one line. */
.ts-thread-done {
display: flex;
align-items: center;
gap: 0.75rem;
margin: 0.5rem 0;
padding: 0.625rem 0.875rem;
border: 1px solid var(--ts-line);
border-radius: 0.625rem;
background: var(--ts-sunken);
}
.ts-thread-done__check {
flex: none;
display: inline-grid;
place-items: center;
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
background: color-mix(in srgb, var(--ts-c-green) 18%, transparent);
color: var(--ts-c-green);
font-weight: 700;
}
.ts-thread-done__text {
display: grid;
flex: 1;
min-width: 0;
font-size: 0.875rem;
> span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.ts-thread-done__topic {
font-weight: 600;
}
.ts-open-threads {
margin-left: auto;
padding: 0.0625rem 0.5rem;
border-radius: 999px;
background: color-mix(in srgb, var(--ts-c-amber) 18%, transparent);
color: var(--ts-ink);
font-size: 0.6875rem;
letter-spacing: 0.04em;
}
.ts-typing {
margin: 0;
color: var(--ts-muted);
font-size: 0.8125rem;
font-style: italic;
}
.ts-resolve__label {
display: grid;
gap: 0.375rem;
color: var(--ts-muted);
font-size: 0.8125rem;
}
.ts-resolve__text {
font: inherit;
color: var(--ts-ink);
resize: vertical;
}
.ts-reply__actions {
display: flex;
gap: 0.75rem;
align-items: center;
}
.ts-votes {
padding: 0.25rem 0.625rem;
border: 1px solid var(--ts-line);
border-radius: 999px;
color: var(--ts-muted);
font-size: 0.8125rem;
}
.ts-presence {
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0.375rem 0 0.25rem;
color: var(--ts-ink-soft);
font-size: 0.8125rem;
}
.ts-presence__dot {
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
background: var(--ts-c-green, #2e9e6a);
}
/* My own reminder on the ticket. */
.ts-reminder {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.25rem 0.625rem;
margin: 0;
padding: 0.5rem 0.875rem;
border-radius: 0.625rem;
background: color-mix(in srgb, var(--ts-c-amber) 14%, transparent);
font-size: 0.875rem;
}
/* Small text buttons, not @pi/ui's sized ones. */
.ts-reminder__action {
height: auto !important;
min-width: 0 !important;
padding: 0 !important;
border: 0;
background: none !important;
color: var(--ts-brand);
font: inherit;
font-weight: 600;
cursor: pointer;
&:hover,
&:focus-visible {
text-decoration: underline;
}
}
.ts-reminder__choices {
display: grid;
gap: 0.375rem;
}
.ts-reminder__choice {
display: flex;
justify-content: space-between;
gap: 1rem;
height: auto !important;
min-width: 0 !important;
padding: 0.625rem 0.875rem !important;
border: 1px solid var(--ts-line);
border-radius: 0.5rem;
background: var(--ts-surface);
color: var(--ts-ink);
font: inherit;
text-align: left;
cursor: pointer;
&:hover,
&:focus-visible {
border-color: var(--ts-brand);
}
}
.ts-reminder__field {
display: grid;
gap: 0.375rem;
color: var(--ts-muted);
font-size: 0.8125rem;
.ts-input {
font: inherit;
color: var(--ts-ink);
}
}
/* Reviews asked on the ticket. */
.ts-reviews {
ul {
display: grid;
gap: 0.625rem;
margin: 0;
padding: 0;
list-style: none;
}
li {
padding: 0.625rem 0.875rem;
border-left: 3px solid var(--ts-c-amber);
border-radius: 0 0.5rem 0.5rem 0;
background: color-mix(in srgb, var(--ts-c-amber) 8%, var(--ts-surface));
&.is-approved {
border-left-color: var(--ts-c-green);
background: color-mix(in srgb, var(--ts-c-green) 7%, var(--ts-surface));
}
&.is-changes {
border-left-color: var(--ts-c-red);
background: color-mix(in srgb, var(--ts-c-red) 6%, var(--ts-surface));
}
}
p {
margin: 0;
font-size: 0.875rem;
}
}
.ts-reviews__note {
margin-top: 0.25rem !important;
color: var(--ts-ink-soft);
}
.ts-reviews__actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem 0.75rem;
margin-top: 0.5rem;
&:empty {
display: none;
}
}
/* Small buttons, not @pi/ui's sized ones. */
.ts-reviews__button {
height: auto !important;
min-width: 0 !important;
padding: 0.25rem 0.75rem !important;
border: 1px solid var(--ts-line-strong) !important;
border-radius: 999px !important;
background: var(--ts-surface) !important;
color: var(--ts-ink);
font: inherit;
font-size: 0.8125rem;
font-weight: 600;
cursor: pointer;
&.is-ok {
border-color: transparent !important;
background: var(--ts-c-green) !important;
color: #fff;
}
&:hover,
&:focus-visible {
filter: brightness(1.05);
}
}
.ts-reviews__link {
height: auto !important;
min-width: 0 !important;
padding: 0 !important;
border: 0;
background: none !important;
color: var(--ts-brand);
font: inherit;
font-size: 0.8125rem;
font-weight: 600;
cursor: pointer;
}