feat(desktop): Мастерская — Linear-style строки: плоские ID, фиксированные левые колонки, единый формат времени
Typecheck / desktop (pull_request) Failing after 8m16s
Typecheck / controller (pull_request) Successful in 13m13s

EntityIdBadge получил flat-вариант (muted mono-текст без плашки,
копи-иконка по hover) — серые коробки с двумя иконками убраны из строк.
Левые колонки зафиксированы для всех уровней: [chevron/приоритет 28px |
ID 96px | статус-иконка 22px | title] — заголовки уровня стартуют с
одной координаты. Чип времени задачи прижат к правому краю своей
колонки, формат часов синхронизирован («1ч / 4ч» и у проектов, и у
задач), правый отступ строк выровнен (--p-3 у всех уровней).
This commit is contained in:
coopops
2026-06-10 16:44:51 +00:00
parent 112d13114c
commit 3b81f821c7
6 changed files with 94 additions and 48 deletions
@@ -118,7 +118,7 @@ function formatHours(h: number | null | undefined): string {
const inlineLabel = computed(() => {
if (hasFact.value && hasEstimate.value) {
return `${formatHours(props.fact)}/${formatHours(props.estimate)}`;
return `${formatHours(props.fact)} / ${formatHours(props.estimate)}`;
}
if (hasFact.value) return formatHours(props.fact);
if (hasEstimate.value) return formatHours(props.estimate);
@@ -181,15 +181,15 @@ const saveEstimate = async () => {
align-items: center;
}
// Триггер фиксированной ширины — title не прыгает между задачами
// с разными значениями времени.
// Триггер живёт в фиксированной колонке строки (.cell-time) и
// прижимается к её правому краю — время всех уровней в одной вертикали.
.time-trigger {
display: inline-flex;
align-items: center;
justify-content: flex-end;
gap: 3px;
padding: 2px 6px;
height: 22px;
width: 86px;
box-sizing: border-box;
border-radius: 4px;
font-size: 11px;
@@ -22,12 +22,13 @@ const hoursFormatter = new Intl.NumberFormat('ru-RU', {
maximumFractionDigits: 1,
});
/** Часы «факт / план ч» по пулам проекта */
/** Часы «фактч / планч» по пулам проекта — формат синхронен с
* инлайн-чипом времени задачи (IssueTimeChip) */
export const formatHoursFactPlan = (
fact?: string | number | null,
plan?: string | number | null,
): string =>
`${hoursFormatter.format(parseAssetAmount(fact))} / ${hoursFormatter.format(parseAssetAmount(plan))} ч`;
`${hoursFormatter.format(parseAssetAmount(fact))}ч / ${hoursFormatter.format(parseAssetAmount(plan))}ч`;
/** Инвестиции «факт / план» компактно */
export const formatInvestFactPlan = (
@@ -24,27 +24,29 @@ div
@click='handleToggleComponent(props.row.project_hash)'
)
// ID с иконкой типа внутри бейджа
// ID — плоский muted-текст фиксированной колонки (Linear-style)
.col-auto.component-row__id
EntityIdBadge(
v-if='props.row.id'
:raw-id='props.row.id'
copy-on-click
address-clipboard
flat
)
template(#prefix)
q-icon(name='code', size='xs')
// Title со статусом
// Статус — фиксированный слот, заголовки уровня стартуют с одной координаты
.col-auto.row-status
q-icon(
:name='getProjectStatusIcon(props.row.status)',
:color='getProjectStatusDotColor(props.row.status)',
size='xs'
)
// Title
.col.component-row__title-col
.list-item-title(
@click.stop='handleOpenComponent(props.row.project_hash)'
)
q-icon(
:name='getProjectStatusIcon(props.row.status)',
:color='getProjectStatusDotColor(props.row.status)',
size='xs'
).q-mr-sm
span {{ props.row.title }}
// Правая сетка строки: время | инвестиции | действие —
@@ -218,21 +220,25 @@ const columns = [
// уровня нет — каскадный отступ задаёт родительский виджет, чтобы при
// одиночном использовании список начинался от края
.component-row {
padding-right: var(--p-3);
min-height: 48px;
}
.component-row__toggle {
width: 35px;
width: 28px;
flex-shrink: 0;
}
.component-row__id {
width: 100px;
width: 96px;
flex-shrink: 0;
}
.component-row__title-col {
padding-left: var(--p-2);
.row-status {
width: 22px;
flex-shrink: 0;
display: flex;
align-items: center;
}
// Вложенный уровень (задачи компонента) — отступ каскада
@@ -1,20 +1,21 @@
<template lang="pug">
.issue-row(role='row')
// 1. Метаблок (горизонтально): приоритет → ID.
.meta-block
// 1. Левые колонки — та же сетка, что у проектов/компонентов:
// приоритет (на месте chevron) → плоский ID фиксированной ширины.
.row-lead
q-icon.priority-icon(
:name='priorityIcon'
:color='priorityColor'
size='18px'
)
q-tooltip(anchor='bottom middle', self='top middle') Приоритет: {{ priorityLabel }}
.row-id
EntityIdBadge(
:raw-id='issue.id'
copy-on-click
address-clipboard
flat
)
template(#prefix)
q-icon(name='task', size='xs')
// 2. Тайтл: занимает всё свободное место, переносится по словам, ellipsis по необходимости.
.title-block(@click.stop="onTitleClick")
@@ -89,20 +90,27 @@ const canChangeEstimate = computed(
// мигрирует на вторую строку, не клипается за правый край.
flex-wrap: wrap;
align-items: center;
gap: var(--p-2) var(--p-3);
row-gap: var(--p-2);
min-height: 48px;
padding: var(--p-2) var(--p-3);
padding: var(--p-2) var(--p-3) var(--p-2) 0;
width: 100%;
box-sizing: border-box;
}
// 1. Meta — горизонтальная цепочка приоритет → ID → время.
.meta-block {
display: flex;
flex-direction: row;
align-items: center;
// 1. Левые колонки — сетка, синхронная со строками проектов/компонентов.
.row-lead {
width: 28px;
flex-shrink: 0;
gap: var(--p-2);
display: flex;
align-items: center;
justify-content: center;
}
.row-id {
width: 96px;
flex-shrink: 0;
display: flex;
align-items: center;
}
.priority-icon {
@@ -180,11 +188,7 @@ const canChangeEstimate = computed(
@media (max-width: 640px) {
.issue-row {
flex-wrap: wrap;
row-gap: 6px;
}
.meta-block {
gap: 6px;
row-gap: var(--p-2);
}
.title-block {
@@ -35,27 +35,29 @@ div
@click='handleToggleExpand(props.row.project_hash)'
)
// ID с иконкой типа внутри бейджа
// ID — плоский muted-текст фиксированной колонки (Linear-style)
.col-auto.project-row__id
EntityIdBadge(
v-if='props.row.id'
:raw-id='props.row.id'
copy-on-click
address-clipboard
flat
)
template(#prefix)
q-icon(name='work', size='xs')
// Title со статусом
// Статус — фиксированный слот, заголовки уровня стартуют с одной координаты
.col-auto.row-status
q-icon(
:name='getProjectStatusIcon(props.row.status)',
:color='getProjectStatusDotColor(props.row.status)',
size='xs'
)
// Title
.col.project-row__title-col
.list-item-title(
@click.stop='handleOpenProject(props.row.project_hash)'
)
q-icon(
:name='getProjectStatusIcon(props.row.status)',
:color='getProjectStatusDotColor(props.row.status)',
size='xs'
).q-mr-sm
span {{ props.row.title }}
// Правая сетка строки: время | инвестиции | действие —
@@ -356,19 +358,27 @@ const columns = [
// virtual-scroll-item-size=48 — не spacing, токенам не подлежат)
.project-row {
padding-left: var(--p-3);
padding-right: var(--p-3);
min-height: 48px;
}
.project-row__toggle {
width: 35px;
width: 28px;
flex-shrink: 0;
}
.project-row__id {
width: 100px;
width: 96px;
flex-shrink: 0;
}
.row-status {
width: 22px;
flex-shrink: 0;
display: flex;
align-items: center;
}
// Вложенный уровень (компоненты проекта) — отступ каскада задаёт родитель,
// сами виджеты при одиночном использовании отступа не имеют
.project-row__nested {
@@ -1,6 +1,7 @@
<template lang="pug">
span.entity-id-badge__pill(
v-if="displayLabel"
:class='{ "entity-id-badge__pill--flat": flat }'
@click.stop="onActivate"
)
span.entity-id-badge__prefix(v-if="$slots.prefix")
@@ -31,8 +32,11 @@ const props = withDefaults(
/** Что именно класть в буфер (если отличается от отображаемого id —
например показываем короткий хеш, а копируем полный) */
copyValue?: string | null;
/** Плоский вариант для плотных строк списков: без плашки,
muted-текст, иконка копирования появляется по hover */
flat?: boolean;
}>(),
{ copyOnClick: false, addressClipboard: false, copyValue: null },
{ copyOnClick: false, addressClipboard: false, copyValue: null, flat: false },
);
const sessionStore = useSessionStore();
@@ -119,4 +123,25 @@ const onActivate = async (e: MouseEvent) => {
.entity-id-badge__pill:hover .entity-id-badge__copy {
color: var(--p-primary);
}
// Плоский вариант для плотных строк (Linear-style ID)
.entity-id-badge__pill--flat {
background: transparent;
padding: 2px 0;
color: var(--p-ink-3);
&:hover {
background: transparent;
color: var(--p-ink-1);
}
.entity-id-badge__copy {
opacity: 0;
transition: opacity var(--p-dur-fast, 120ms) ease;
}
&:hover .entity-id-badge__copy {
opacity: 1;
}
}
</style>