feat(desktop): Мастерская — Linear-style строки: плоские ID, фиксированные левые колонки, единый формат времени
EntityIdBadge получил flat-вариант (muted mono-текст без плашки, копи-иконка по hover) — серые коробки с двумя иконками убраны из строк. Левые колонки зафиксированы для всех уровней: [chevron/приоритет 28px | ID 96px | статус-иконка 22px | title] — заголовки уровня стартуют с одной координаты. Чип времени задачи прижат к правому краю своей колонки, формат часов синхронизирован («1ч / 4ч» и у проектов, и у задач), правый отступ строк выровнен (--p-3 у всех уровней).
This commit is contained in:
+4
-4
@@ -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 = (
|
||||
|
||||
+19
-13
@@ -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;
|
||||
}
|
||||
|
||||
// Вложенный уровень (задачи компонента) — отступ каскада
|
||||
|
||||
+21
-17
@@ -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 {
|
||||
|
||||
+21
-11
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user