From 127838ea3c9a5238a7ff81c5f142d16ea106364a Mon Sep 17 00:00:00 2001 From: Mika Pons Date: Thu, 10 Sep 2026 10:34:21 +0200 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=9A=B8=20Improve=20assignment=20statu?= =?UTF-8?q?s=20badges=20and=20status=20filters=20interaction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../assignment-chooser.component.html | 68 +++++++--- .../assignment-chooser.component.scss | 119 ++++++++++++------ .../assignment-chooser.component.ts | 78 ++++++++++++ src/app/models/Assignment.model.ts | 2 + src/assets/i18n/en.json | 2 + src/assets/i18n/fr.json | 2 + src/assets/i18n/ru.json | 2 + src/styles/_components.scss | 99 +++++++++++++++ src/styles/_variables.scss | 12 ++ 9 files changed, 328 insertions(+), 56 deletions(-) diff --git a/src/app/components/home/assignment-chooser/assignment-chooser.component.html b/src/app/components/home/assignment-chooser/assignment-chooser.component.html index e7586b0f..9222ed29 100644 --- a/src/app/components/home/assignment-chooser/assignment-chooser.component.html +++ b/src/app/components/home/assignment-chooser/assignment-chooser.component.html @@ -95,25 +95,47 @@
{{ 'HOME.FILTER.STATUS' | translate }}
-
@@ -256,7 +278,8 @@
{{ 'HOME.FILTER.YEAR' | translate }}
'disabled-card': !authService.isSignedIn(), 'is-selected': isSelected(assignment.id), 'is-editing': editingAssignmentId === assignment.id, - 'is-shrunk': (hoveredAssignment === assignment.id || isSelected(assignment.id)) && editingAssignmentId !== assignment.id + 'is-shrunk': (hoveredAssignment === assignment.id || isSelected(assignment.id)) && editingAssignmentId !== assignment.id, + 'status-preview-dimmed': hoveredStatusPreview && assignment.id !== -1 && assignment.uiStatus !== hoveredStatusPreview }">
@@ -289,12 +312,27 @@
{{ 'HOME.FILTER.YEAR' | translate }}
translate) }} - - {{ 'HOME.STATUS.' + assignment.uiStatus.toUpperCase() | translate }} - - - {{ 'HOME.NEW' | translate }} - +
span { + display: inline-flex; + align-items: center; + gap: 6px; padding: 0.35rem 0.8rem; border-radius: 99px; - border: none; - background-color: var(--color-bg-body); - color: var(--color-text-secondary); + border: 1px dashed var(--color-border); + background-color: transparent; + color: var(--color-text-muted); font-size: 0.8rem; - font-weight: 500; - transition: all 0.15s ease; + font-weight: 400; + opacity: 0.4; + filter: grayscale(100%); + transition: all 0.2s cubic-bezier(0.23, 1, 0.32, 1); + user-select: none; + + .chip-icon { + flex-shrink: 0; + opacity: 0.7; + } + + .filter-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background-color: currentColor; + display: inline-block; + flex-shrink: 0; + opacity: 0.7; + } } + + // Hover when inactive: invite activation + &:hover > span { + opacity: 0.7; + filter: grayscale(40%); + background-color: var(--color-hover); + } + + // ACTIVE / CHECKED STATES (100% visible, fully colored) input[type="checkbox"]:checked + span { - background-color: var(--color-primary); - color: #ffffff; + opacity: 1; + filter: none; + border-style: solid; + + .chip-icon, + .filter-dot { + opacity: 1; + } + } + + &.chip-prepared input[type="checkbox"]:checked + span { + background-color: var(--badge-prepared-bg); + color: var(--badge-prepared-text); + border-color: var(--badge-prepared-border); + } + &.chip-ongoing input[type="checkbox"]:checked + span { + background-color: var(--badge-ongoing-bg); + color: var(--badge-ongoing-text); + border-color: var(--badge-ongoing-border); } - &:hover span { - filter: brightness(0.95); + &.chip-finished input[type="checkbox"]:checked + span { + background-color: var(--badge-finished-bg); + color: var(--badge-finished-text); + border-color: var(--badge-finished-border); + } + &.chip-default input[type="checkbox"]:checked + span { + background-color: var(--badge-default-bg); + color: var(--badge-default-text); + border-color: var(--badge-default-border); + } + + &:hover input[type="checkbox"]:checked + span { + filter: brightness(0.96); + opacity: 1; } } } @@ -277,6 +337,13 @@ margin: 0; position: relative; z-index: 1; + transition: opacity 200ms cubic-bezier(0.23, 1, 0.32, 1), filter 200ms cubic-bezier(0.23, 1, 0.32, 1); + + &.status-preview-dimmed { + opacity: 0.35; + filter: grayscale(80%); + pointer-events: none; + } &.is-editing { z-index: 10; @@ -409,36 +476,6 @@ max-width: 450px; } -.status-badge { - padding: 0.2rem 0.5rem; - border-radius: 6px; - font-size: 0.75rem; - font-weight: 500; - letter-spacing: 0; - display: inline-flex; - align-items: center; - text-transform: capitalize; - border: none; - - &.status-prepared { - background-color: var(--badge-prepared-bg); - color: var(--badge-prepared-text); - } - &.status-ongoing { - background-color: var(--badge-ongoing-bg); - color: var(--badge-ongoing-text); - } - &.status-finished { - background-color: var(--badge-finished-bg); - color: var(--badge-finished-text); - } - &.status-default { - background-color: var(--color-surface); - color: var(--color-text-secondary); - border: 1px solid var(--color-border); - } -} - .meta-info { margin-top: 2px; diff --git a/src/app/components/home/assignment-chooser/assignment-chooser.component.ts b/src/app/components/home/assignment-chooser/assignment-chooser.component.ts index ec674b92..4809cb3e 100644 --- a/src/app/components/home/assignment-chooser/assignment-chooser.component.ts +++ b/src/app/components/home/assignment-chooser/assignment-chooser.component.ts @@ -55,6 +55,10 @@ export class AssignmentChooserComponent implements OnInit, OnDestroy { selectedAssignments: Set = new Set(); hoveredAssignment: number | null = null; + // Status hover preview state + hoveredStatusPreview: string | null = null; + private statusPreviewTimeout: any = null; + // Inline edit state editingAssignmentId: number | null = null; isCreatingNew = false; @@ -155,6 +159,77 @@ export class AssignmentChooserComponent implements OnInit, OnDestroy { this.filterType = type; } + onStatusMouseEnter(status: string) { + if (this.statusPreviewTimeout) { + clearTimeout(this.statusPreviewTimeout); + } + this.statusPreviewTimeout = setTimeout(() => { + this.hoveredStatusPreview = status; + this.cdr.markForCheck(); + }, 180); + } + + onStatusMouseLeave() { + if (this.statusPreviewTimeout) { + clearTimeout(this.statusPreviewTimeout); + this.statusPreviewTimeout = null; + } + if (this.hoveredStatusPreview !== null) { + this.hoveredStatusPreview = null; + this.cdr.markForCheck(); + } + } + + isStatusIsolated(status: string): boolean { + const s = this.advancedFilters.status; + const allStatuses: Array = [ + "prepared", + "ongoing", + "finished", + "default", + ]; + return ( + s[status as keyof typeof s] === true && + allStatuses.filter((k) => k !== status).every((k) => !s[k]) + ); + } + + getStatusTooltip(status: string): string { + return this.isStatusIsolated(status) + ? "HOME.STATUS-TOOLTIP-RESET" + : "HOME.STATUS-TOOLTIP-ISOLATE"; + } + + toggleStatusFilterFromBadge( + status: "prepared" | "ongoing" | "finished" | "default", + event: MouseEvent + ) { + event.stopPropagation(); + if (this.statusPreviewTimeout) { + clearTimeout(this.statusPreviewTimeout); + this.statusPreviewTimeout = null; + } + this.hoveredStatusPreview = null; + + if (this.isStatusIsolated(status)) { + this.advancedFilters.status = { + prepared: true, + ongoing: true, + finished: true, + default: true, + }; + } else { + this.advancedFilters.status = { + prepared: status === "prepared", + ongoing: status === "ongoing", + finished: status === "finished", + default: status === "default", + }; + } + this.savePreferences(); + this.cdr.markForCheck(); + } + constructor( private databaseService: DatabaseService, private dataService: DataService, @@ -206,6 +281,9 @@ export class AssignmentChooserComponent implements OnInit, OnDestroy { if (this.dbSubscription) { this.dbSubscription.unsubscribe(); } + if (this.statusPreviewTimeout) { + clearTimeout(this.statusPreviewTimeout); + } } async loadAssignments() { diff --git a/src/app/models/Assignment.model.ts b/src/app/models/Assignment.model.ts index bfba5a8f..7ae658f9 100644 --- a/src/app/models/Assignment.model.ts +++ b/src/app/models/Assignment.model.ts @@ -11,6 +11,8 @@ import { Type } from "class-transformer"; */ export class Assignment { id: number; + uiStatus?: "prepared" | "ongoing" | "finished" | "default"; + uiType?: "github" | "gitlab"; @Type(() => Metadata) metadata: Metadata; diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 529be8f4..d4e39c20 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -148,6 +148,8 @@ "FINISHED": "Finished", "DEFAULT": "Not scheduled" }, + "STATUS-TOOLTIP-ISOLATE": "Click to isolate this status", + "STATUS-TOOLTIP-RESET": "Click to show all assignments", "NO-ASSIGNMENTS": "No assignments found", "SELECTED-ASSIGNMENTS": "assignment(s) selected", "SORT-BY": "Sort by", diff --git a/src/assets/i18n/fr.json b/src/assets/i18n/fr.json index a88a3c63..958f799d 100644 --- a/src/assets/i18n/fr.json +++ b/src/assets/i18n/fr.json @@ -147,6 +147,8 @@ "FINISHED": "Terminé", "DEFAULT": "Non programmé" }, + "STATUS-TOOLTIP-ISOLATE": "Cliquer pour isoler ce statut", + "STATUS-TOOLTIP-RESET": "Cliquer pour réafficher tous les devoirs", "NO-ASSIGNMENTS": "Aucun devoir trouvé", "SELECTED-ASSIGNMENTS": "devoir(s) sélectionné(s)", "SORT-BY": "Trier par", diff --git a/src/assets/i18n/ru.json b/src/assets/i18n/ru.json index bb1bcc62..aa1ec21d 100644 --- a/src/assets/i18n/ru.json +++ b/src/assets/i18n/ru.json @@ -146,6 +146,8 @@ "FINISHED": "Завершено", "DEFAULT": "Не запланировано" }, + "STATUS-TOOLTIP-ISOLATE": "Нажмите, чтобы отфильтровать по этому статусу", + "STATUS-TOOLTIP-RESET": "Нажмите, чтобы показать все задания", "NO-ASSIGNMENTS": "Задания не найдены", "SELECTED-ASSIGNMENTS": "заданий выбрано", "SORT-BY": "Сортировать по", diff --git a/src/styles/_components.scss b/src/styles/_components.scss index ffb2f8f2..e30b5446 100644 --- a/src/styles/_components.scss +++ b/src/styles/_components.scss @@ -2213,3 +2213,102 @@ body:has(app-custom-modal-container) ngb-typeahead-window { } } } + +/* ========================================= + STATUS BADGES (Linear / Modern Dev Style) + ========================================= */ +.status-badge { + padding: 0.2rem 0.55rem; + border-radius: 9999px; + font-size: 0.725rem; + font-weight: 400; + letter-spacing: 0; + display: inline-flex; + align-items: center; + gap: 5px; + text-transform: capitalize; + border: 1px solid transparent; + cursor: pointer; + user-select: none; + background: transparent; + outline: none; + line-height: 1.25; + transition: transform 0.16s cubic-bezier(0.23, 1, 0.32, 1), box-shadow 0.16s ease, filter 0.16s ease; + + &:hover { + transform: translateY(-1px); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06); + } + + &:active { + transform: scale(0.96); + } + + .status-text { + display: inline-block; + font-weight: 400; + } + + .status-icon { + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; + } + + .status-pulse-dot { + width: 5.5px; + height: 5.5px; + border-radius: 50%; + background-color: currentColor; + position: relative; + flex-shrink: 0; + + &::after { + content: ''; + position: absolute; + inset: 0; + border-radius: 50%; + background-color: currentColor; + opacity: 0.45; + animation: status-pulse 2s cubic-bezier(0.24, 0, 0.38, 1) infinite; + } + } + + &.status-prepared { + background-color: var(--badge-prepared-bg); + color: var(--badge-prepared-text); + border-color: var(--badge-prepared-border); + } + &.status-ongoing { + background-color: var(--badge-ongoing-bg); + color: var(--badge-ongoing-text); + border-color: var(--badge-ongoing-border); + } + &.status-finished { + background-color: var(--badge-finished-bg); + color: var(--badge-finished-text); + border-color: var(--badge-finished-border); + } + &.status-default { + background-color: var(--badge-default-bg); + color: var(--badge-default-text); + border-color: var(--badge-default-border); + } +} + +@keyframes status-pulse { + 0% { + transform: scale(1); + opacity: 0.45; + } + 60% { + transform: scale(1.8); + opacity: 0; + } + 100% { + transform: scale(1.8); + opacity: 0; + } +} + diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss index d2fa5666..a06d0a35 100644 --- a/src/styles/_variables.scss +++ b/src/styles/_variables.scss @@ -47,10 +47,16 @@ $font-family-sans: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Robot --badge-prepared-bg: #eff6ff; --badge-prepared-text: #1d4ed8; + --badge-prepared-border: rgba(59, 130, 246, 0.28); --badge-ongoing-bg: #ecfdf5; --badge-ongoing-text: #047857; + --badge-ongoing-border: rgba(16, 185, 129, 0.28); --badge-finished-bg: #f3f4f6; --badge-finished-text: #4b5563; + --badge-finished-border: rgba(148, 163, 184, 0.28); + --badge-default-bg: #f8fafc; + --badge-default-text: #64748b; + --badge-default-border: rgba(100, 116, 139, 0.28); --timeline-track-bg: #f3f4f6; --timeline-text: #6b7280; @@ -114,10 +120,16 @@ body.dark-theme { --badge-prepared-bg: rgba(59, 130, 246, 0.15); --badge-prepared-text: #60a5fa; + --badge-prepared-border: rgba(96, 165, 250, 0.3); --badge-ongoing-bg: rgba(16, 185, 129, 0.15); --badge-ongoing-text: #34d399; + --badge-ongoing-border: rgba(52, 211, 153, 0.3); --badge-finished-bg: rgba(148, 163, 184, 0.15); --badge-finished-text: #94a3b8; + --badge-finished-border: rgba(148, 163, 184, 0.25); + --badge-default-bg: rgba(100, 116, 139, 0.15); + --badge-default-text: #94a3b8; + --badge-default-border: rgba(148, 163, 184, 0.3); --timeline-track-bg: #334155; --timeline-text: #94a3b8; From 0123c65bdcede76b5c187d6bdcb9bd7ec55693d4 Mon Sep 17 00:00:00 2001 From: Mika Pons Date: Thu, 10 Sep 2026 13:32:02 +0200 Subject: [PATCH 2/3] =?UTF-8?q?=E2=9C=A8=20redesign=20milestone=20labels?= =?UTF-8?q?=20with=20chronological=20overlap=20masking?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/components/graphs/overview/chart.scss | 79 +++- .../graphs/overview/overview.component.html | 1 - .../graphs/overview/overview.component.ts | 416 +++++++++++++++--- 3 files changed, 412 insertions(+), 84 deletions(-) diff --git a/src/app/components/graphs/overview/chart.scss b/src/app/components/graphs/overview/chart.scss index 5cc22c24..53daf66f 100644 --- a/src/app/components/graphs/overview/chart.scss +++ b/src/app/components/graphs/overview/chart.scss @@ -133,6 +133,8 @@ body.dark-theme .commit { pointer-events: auto; overflow: hidden; cursor: default; + mask: url(#milestone-badges-cutout-mask); + -webkit-mask: url(#milestone-badges-cutout-mask); } .session-pill { @@ -211,56 +213,87 @@ body.dark-theme .commit { pointer-events: auto; cursor: pointer; - > rect:first-child { + .milestone-vertical-line { transition: width var(--transition-fast), transform var(--transition-fast); } - &:hover > rect:first-child { + &:hover .milestone-vertical-line { width: 2px; transform: translate(-1px, 0); } - > rect:nth-child(2) { - rx: 12px; - ry: 12px; - transition: filter var(--transition-normal), transform var(--transition-normal); - filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1)); + .milestone-label-group { + pointer-events: none; } - - &:hover > rect:nth-child(2) { - transform: translateY(-2px); - filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.15)); + + // --- Bottom Bar --- + .milestone-bottom-bar { + pointer-events: none; + transition: height var(--transition-fast), y var(--transition-fast), filter var(--transition-fast); } - > text { - fill: var(--color-surface); - user-select: none; - font-family: var(--font-family-sans); - font-size: 12px; - font-weight: 500; - transition: transform var(--transition-normal); + // Hover feedback: bar thickens upward (bottom stays at y=0, perfectly attached to line) + &:hover .milestone-bottom-bar { + y: -4.5px; + height: 4.5px; + filter: brightness(1.15); } - &:hover > text { - transform: translateY(-2px); + .milestone-text { + fill: var(--color-text-primary); + user-select: none; + pointer-events: none; + font-family: var(--font-family-sans); + font-size: 11px; + font-weight: 600; + line-height: 1; + transition: fill var(--transition-fast); } } .review { - > rect:first-child, > rect:nth-child(2) { fill: var(--color-primary); stroke: var(--color-primary); } + .milestone-vertical-line { + fill: var(--color-primary); + stroke: var(--color-primary); + } + .milestone-bottom-bar { + fill: var(--color-primary); + } + &:hover .milestone-text { + fill: var(--color-primary); + } .milestone-drag-badge .badge-progress { stroke: var(--color-primary); } } .correction { - > rect:first-child, > rect:nth-child(2) { fill: var(--color-danger); stroke: var(--color-danger); } + .milestone-vertical-line { + fill: var(--color-danger); + stroke: var(--color-danger); + } + .milestone-bottom-bar { + fill: var(--color-danger); + } + &:hover .milestone-text { + fill: var(--color-danger); + } .milestone-drag-badge .badge-progress { stroke: var(--color-danger); } } .other { - > rect:first-child, > rect:nth-child(2) { fill: var(--color-secondary); stroke: var(--color-secondary); } + .milestone-vertical-line { + fill: var(--color-secondary); + stroke: var(--color-secondary); + } + .milestone-bottom-bar { + fill: var(--color-secondary); + } + &:hover .milestone-text { + fill: var(--color-secondary); + } .milestone-drag-badge .badge-progress { stroke: var(--color-secondary); } } + .hitbox { fill: transparent !important; stroke: none !important; diff --git a/src/app/components/graphs/overview/overview.component.html b/src/app/components/graphs/overview/overview.component.html index f8eebed9..20eba8b7 100644 --- a/src/app/components/graphs/overview/overview.component.html +++ b/src/app/components/graphs/overview/overview.component.html @@ -489,4 +489,3 @@