diff --git a/src/app/components/edit-milestone/edit-milestone.component.html b/src/app/components/edit-milestone/edit-milestone.component.html index 807fe7ea..48a93daf 100644 --- a/src/app/components/edit-milestone/edit-milestone.component.html +++ b/src/app/components/edit-milestone/edit-milestone.component.html @@ -15,9 +15,11 @@
{{ 'OVERVIEW-GRAPH.NO-COMMITS-TIT
+ [class.no-milestones]="loading || !hasDisplayedMilestones()">
diff --git a/src/app/components/graphs/overview/overview.component.ts b/src/app/components/graphs/overview/overview.component.ts index c0aec96d..5b35d7b7 100644 --- a/src/app/components/graphs/overview/overview.component.ts +++ b/src/app/components/graphs/overview/overview.component.ts @@ -162,6 +162,13 @@ export class OverviewComponent hasMovedDuringDrag = false; dragScrollTimer: d3.Timer; dragTimeIndicator: d3.Selection; + milestoneLongPressTimer: any = null; + activeMilestoneBadge: d3.Selection = null; + activeMilestoneCleanupFn: (() => void) | null = null; + ignoreNextMilestoneClick = false; + readonly MILESTONE_HOLD_DURATION = 700; + readonly MILESTONE_DRAG_JITTER_TOLERANCE = 5; + readonly MILESTONE_CLICK_SUPPRESSION_DELAY = 300; //////////////////////// public modeHoverState: any = { @@ -330,6 +337,10 @@ export class OverviewComponent this.resizeObserver.disconnect(); } this.clearMilestoneHoverTimer(); + this.cancelMilestoneLongPress(); + this.stopDragScrollTimer(); + this.removeDragTimeIndicator(); + document.body.style.cursor = ""; this.unsubscribeAssignmentModified(this.assignmentsModified$); document.body.style.overscrollBehaviorX = "auto"; } @@ -444,14 +455,36 @@ export class OverviewComponent commit_date_format = Utils.COMMIT_DATE_FORMAT; - download() { - this.assignmentsService.exportAssignment(this.dataService.assignment); + hasDisplayedMilestones(): boolean { + if (!this.dataService) return false; + if (!this.showReviews && !this.showCorrections && !this.showOthers) { + return false; + } + const milestone_filter = (m: Milestone) => + (!this.dataService.groupFilter || + !m.tpGroup || + m.tpGroup === this.dataService.groupFilter) && + (!this.searchFilter?.length || + this.searchFilter.some((question) => m.questions?.includes(question))); + + return ( + (this.showReviews && !!this.dataService.reviews?.some(milestone_filter)) || + (this.showCorrections && !!this.dataService.corrections?.some(milestone_filter)) || + (this.showOthers && !!this.dataService.others?.some(milestone_filter)) + ); } updateVariableFromCss(): void { let chart_div = document.getElementById("chart"); if (!chart_div) return; + const noMilestones = this.loading || !this.hasDisplayedMilestones(); + if (noMilestones) { + chart_div.classList.add("no-milestones"); + } else { + chart_div.classList.remove("no-milestones"); + } + var style = getComputedStyle(chart_div); var css_var_number = (name: string, dash = true) => @@ -755,6 +788,46 @@ export class OverviewComponent .append("g") .attr("transform", "translate(" + translation + ")"); + if (this.hasDisplayedMilestones() && this.inner_margin.top > 0) { + this.chart_abs_g + .append("rect") + .attr("class", "milestone-strip-hitbox") + .attr("x", 0) + .attr("y", 0) + .attr("width", this.inner_width) + .attr("height", this.inner_margin.top) + .attr("opacity", "0") + .style("cursor", "default") + .style("pointer-events", "all") + .on("contextmenu", (event: MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + const rawDate = overview.getDateFromMouseEvent(event); + overview.openContextMenu(event.pageX, event.pageY, rawDate); + }) + .on("click", (event: MouseEvent) => { + event.stopPropagation(); + const rawDate = overview.getDateFromMouseEvent(event); + overview.openContextMenu(event.pageX, event.pageY, rawDate); + }) + .on("wheel", (event: WheelEvent) => { + if (event.shiftKey) return; + let dx = event.deltaX; + let dy = event.deltaY; + if (event.ctrlKey && Math.abs(dy) > 0 && Math.abs(dx) === 0) { + dx = dy; + dy = 0; + } + if (Math.abs(dx) > Math.abs(dy) || event.ctrlKey) { + event.preventDefault(); + event.stopPropagation(); + if (overview.zoom && overview.data_g) { + overview.data_g.call(overview.zoom.translateBy, -dx / (overview.current_zoom?.k || 1), 0); + } + } + }); + } + this.data_g .append("rect") .attr("id", "data") @@ -764,20 +837,12 @@ export class OverviewComponent .on("contextmenu", (event: MouseEvent) => { event.preventDefault(); event.stopPropagation(); - var rect = (event.target as any).getBoundingClientRect(); - var x = - ((event.clientX - rect.left) / (rect.right - rect.left)) * - overview.inner_width; - let rawDate = overview.x_scale_copy.invert(x); + const rawDate = this.getDateFromMouseEvent(event); this.openContextMenu(event.pageX, event.pageY, rawDate); }) .on("click", (event: MouseEvent) => { event.stopPropagation(); - var rect = (event.target as any).getBoundingClientRect(); - var x = - ((event.clientX - rect.left) / (rect.right - rect.left)) * - overview.inner_width; //x position within the element. - let rawDate = overview.x_scale_copy.invert(x); + const rawDate = this.getDateFromMouseEvent(event); this.openContextMenu(event.pageX, event.pageY, rawDate); }); @@ -902,6 +967,27 @@ export class OverviewComponent } } + getDateFromMouseEvent(event: MouseEvent): Date { + const dataElement = + (this.data_g?.select("#data")?.node() as SVGRectElement) || + (document.getElementById("data") as unknown as SVGRectElement); + let x = 0; + if (dataElement) { + const rect = dataElement.getBoundingClientRect(); + const width = rect.right - rect.left; + if (width > 0) { + x = ((event.clientX - rect.left) / width) * this.inner_width; + } + } else if (this.chart_svg && (d3 as any).pointer) { + const [px] = (d3 as any).pointer(event, this.chart_svg.node()); + x = px; + } + x = Math.max(0, Math.min(this.inner_width, x)); + return this.x_scale_copy + ? this.x_scale_copy.invert(x) + : (this.x_scale ? this.x_scale.invert(x) : new Date()); + } + onSaveMilestone(result: { oldMilestone: Milestone; newMilestone: Milestone; @@ -1043,20 +1129,22 @@ export class OverviewComponent .on("contextmenu", (e) => { e.preventDefault(); e.stopPropagation(); + const rawDate = overview.getDateFromMouseEvent(e); overview.openEditSessionContextMenu( session, e.pageX, e.pageY, - overview.x_scale.invert(e.pageX) + rawDate ); }) .on("click", (e) => { e.stopPropagation(); + const rawDate = overview.getDateFromMouseEvent(e); overview.openEditSessionContextMenu( session, e.pageX, e.pageY, - overview.x_scale.invert(e.pageX) + rawDate ); }); @@ -1156,6 +1244,10 @@ export class OverviewComponent box.attr("x", -bbox.width / 2); box.attr("y", bbox.y); + const badgeX = bbox.width / 2 - 2; + const badgeY = bbox.y + bbox.height - 2; + g.attr("data-badge-x", badgeX).attr("data-badge-y", badgeY); + // Hitbox (transparent, plus large pour faciliter le clic) g.append("rect") .attr("class", "hitbox") @@ -1163,24 +1255,242 @@ export class OverviewComponent .attr("height", bbox.height + 30) .attr("x", -(bbox.width + 30) / 2) .attr("y", bbox.y - 15) - .attr("style", "cursor: grab; pointer-events: all;"); + .attr("style", "cursor: pointer; pointer-events: all;"); + } + + private readonly MILESTONE_COLOR_MAP: Record = { + correction: "var(--color-danger)", + other: "var(--color-secondary)", + }; + + private getMilestoneColor(type: string): string { + return this.MILESTONE_COLOR_MAP[type] || "var(--color-primary)"; + } + + private getEuclideanDistance( + x1: number, + y1: number, + x2: number, + y2: number + ): number { + const dx = x2 - x1; + const dy = y2 - y1; + return Math.sqrt(dx * dx + dy * dy); } - private setupMilestoneDragBehavior(m: Milestone) { + private readonly FA_ARROWS_ALT_PATH = + "M352.201 425.775l-79.196 79.196c-9.373 9.373-24.568 9.373-33.941 0l-79.196-79.196c-15.119-15.119-4.411-40.971 16.971-40.97h51.162L228 284H127.196v51.162c0 21.382-25.851 32.09-40.971 16.971L7.029 272.937c-9.373-9.373-9.373-24.569 0-33.941L86.225 159.8c15.119-15.119 40.971-4.411 40.971 16.971V228H228V127.196h-51.23c-21.382 0-32.09-25.851-16.971-40.971l79.196-79.196c9.373-9.373 24.568-9.373 33.941 0l79.196 79.196c15.119 15.119 4.411 40.971-16.971 40.971h-51.162V228h100.804v-51.162c0-21.382 25.851-32.09 40.97-16.971l79.196 79.196c9.373 9.373 9.373 24.569 0 33.941L425.773 352.2c-15.119 15.119-40.971 4.411-40.97-16.971V284H284v100.804h51.23c21.382 0 32.09 25.851 16.971 40.971z"; + + private createMilestoneLongPressBadge( + g: d3.Selection, + m: Milestone + ): d3.Selection { + const badgeX = parseFloat(g.attr("data-badge-x") || "20"); + const badgeY = parseFloat(g.attr("data-badge-y") || "10"); + + const badge = g + .append("g") + .attr("class", "milestone-drag-badge") + .attr("transform", `translate(${badgeX}, ${badgeY})`); + + const badgeContent = badge.append("g").attr("class", "badge-content"); + + // Base background circle + badgeContent + .append("circle") + .attr("class", "badge-bg") + .attr("cx", 0) + .attr("cy", 0) + .attr("r", 10); + + // Background track ring + badgeContent + .append("circle") + .attr("class", "badge-track") + .attr("cx", 0) + .attr("cy", 0) + .attr("r", 8); + + // Progress circle (r = 8, circumference = 2 * PI * 8 ~= 50.265) + const circumference = 2 * Math.PI * 8; + const progressColor = this.getMilestoneColor(m.type); + + const progressCircle = badgeContent + .append("circle") + .attr("class", "badge-progress") + .attr("cx", 0) + .attr("cy", 0) + .attr("r", 8) + .attr("stroke", progressColor) + .attr("stroke-dasharray", circumference) + .attr("stroke-dashoffset", circumference) + .attr("transform", "rotate(-90)"); + + // 4-way move arrow icon in center (FontAwesome fa-arrows-alt) + badgeContent + .append("g") + .attr("class", "badge-icon-group") + .attr("transform", "translate(-4, -4) scale(0.015625)") + .append("path") + .attr("class", "badge-icon") + .attr("d", this.FA_ARROWS_ALT_PATH) + .attr("fill", "var(--color-text-primary)"); + + // Animate circular fill over 700ms using D3 transition + progressCircle + .transition() + .duration(this.MILESTONE_HOLD_DURATION) + .ease(d3.easeLinear) + .attr("stroke-dashoffset", 0); + + return badge; + } + + private cancelMilestoneLongPress() { + if (this.activeMilestoneCleanupFn) { + this.activeMilestoneCleanupFn(); + this.activeMilestoneCleanupFn = null; + } + if (this.milestoneLongPressTimer) { + clearTimeout(this.milestoneLongPressTimer); + this.milestoneLongPressTimer = null; + } + if (this.activeMilestoneBadge) { + this.activeMilestoneBadge.remove(); + this.activeMilestoneBadge = null; + } + if (this.chart_abs_g) { + this.chart_abs_g.selectAll(".milestone-drag-badge").remove(); + } + } + + private setupMilestoneInteractions( + g: d3.Selection, + m: Milestone + ) { const overview = this; - return d3.drag() - .on("start", function(event) { - overview.onMilestoneDragStart(event, d3.select(this)); - }) - .on("drag", function(event) { - overview.onMilestoneDrag(event, d3.select(this), m); - }) - .on("end", function() { - overview.onMilestoneDragEnd(d3.select(this)); - }); + + g.select(".hitbox").on("mousedown", (e: MouseEvent) => { + // Left click only + if (e.button !== 0) return; + if (overview.isDraggingMilestone) return; + + e.stopPropagation(); + e.preventDefault(); + + overview.clearMilestoneHoverTimer(); + if (overview.hovered_milestone) { + overview.hovered_milestone = undefined; + overview.tooltipService.hide(); + } + + const startClientX = e.clientX; + const startClientY = e.clientY; + const originalDate = new Date(m.date.getTime()); + let isLongPressFulfilled = false; + let hasMovedDuringDrag = false; + + overview.cancelMilestoneLongPress(); + + const badge = overview.createMilestoneLongPressBadge(g, m); + overview.activeMilestoneBadge = badge; + + overview.milestoneLongPressTimer = setTimeout(() => { + isLongPressFulfilled = true; + badge.classed("drag-ready", true); + document.body.style.cursor = "grabbing"; + + const currentMilestoneX = overview.xScaledTimeZoned(m.date); + overview.onMilestoneDragStart(currentMilestoneX, g); + }, overview.MILESTONE_HOLD_DURATION); + + const onWindowMouseMove = (moveEvent: MouseEvent) => { + const dist = overview.getEuclideanDistance( + startClientX, + startClientY, + moveEvent.clientX, + moveEvent.clientY + ); + + if (!isLongPressFulfilled) { + if (dist > overview.MILESTONE_DRAG_JITTER_TOLERANCE) { + overview.ignoreNextMilestoneClick = true; + setTimeout(() => { + overview.ignoreNextMilestoneClick = false; + }, overview.MILESTONE_CLICK_SUPPRESSION_DELAY); + overview.cancelMilestoneLongPress(); + } + } else { + hasMovedDuringDrag = true; + overview.onMilestoneDragCustom(moveEvent, g, m); + } + }; + + const onWindowMouseUp = (upEvent: MouseEvent) => { + const dist = overview.getEuclideanDistance( + startClientX, + startClientY, + upEvent.clientX, + upEvent.clientY + ); + + overview.cancelMilestoneLongPress(); + + if (!isLongPressFulfilled) { + if ( + dist <= overview.MILESTONE_DRAG_JITTER_TOLERANCE && + upEvent.button === 0 + ) { + overview.ignoreNextMilestoneClick = true; + setTimeout(() => { + overview.ignoreNextMilestoneClick = false; + }, overview.MILESTONE_CLICK_SUPPRESSION_DELAY); + const rawDate = overview.getDateFromMouseEvent(upEvent); + overview.openEditMilestoneContextMenu( + m, + upEvent.pageX, + upEvent.pageY, + rawDate + ); + } + } else { + overview.ignoreNextMilestoneClick = true; + setTimeout(() => { + overview.ignoreNextMilestoneClick = false; + }, overview.MILESTONE_CLICK_SUPPRESSION_DELAY); + overview.onMilestoneDragEndCustom(g, hasMovedDuringDrag); + } + }; + + const onWindowBlur = () => { + if (isLongPressFulfilled && !hasMovedDuringDrag) { + m.date = originalDate; + const restoredX = overview.xScaledTimeZoned(originalDate); + g.attr("transform", `translate(${restoredX}, ${overview.inner_margin.top})`); + } + overview.cancelMilestoneLongPress(); + if (isLongPressFulfilled) { + overview.onMilestoneDragEndCustom(g, false); + } + }; + + const cleanup = () => { + window.removeEventListener("mousemove", onWindowMouseMove); + window.removeEventListener("mouseup", onWindowMouseUp); + window.removeEventListener("blur", onWindowBlur); + }; + + overview.activeMilestoneCleanupFn = cleanup; + window.addEventListener("mousemove", onWindowMouseMove); + window.addEventListener("mouseup", onWindowMouseUp); + window.addEventListener("blur", onWindowBlur); + }); } - private onMilestoneDragStart(event: any, element: d3.Selection) { + private onMilestoneDragStart( + startX: number, + element: d3.Selection + ) { this.isDraggingMilestone = true; this.hasMovedDuringDrag = false; this.clearMilestoneHoverTimer(); @@ -1190,31 +1500,45 @@ export class OverviewComponent } element.raise(); element.select(".hitbox").attr("style", "cursor: grabbing; pointer-events: all;"); - this.createDragTimeIndicator(event.x); + this.createDragTimeIndicator(startX); } - private onMilestoneDrag(event: any, element: d3.Selection, m: Milestone) { + private onMilestoneDragCustom( + event: MouseEvent, + element: d3.Selection, + m: Milestone + ) { this.hasMovedDuringDrag = true; - let currentX = Math.max(0, Math.min(this.inner_width, event.x)); - + let currentX = 0; + if (this.chart_abs_g) { + const [px] = (d3 as any).pointer(event, this.chart_abs_g.node()); + currentX = px; + } else { + currentX = event.clientX; + } + currentX = Math.max(0, Math.min(this.inner_width, currentX)); + m.date = this.x_scale_copy.invert(currentX); element.attr("transform", `translate(${currentX}, ${this.inner_margin.top})`); - + this.updateDragTimeIndicator(currentX, m.date); this.handleDragEdgeScrolling(currentX, element, m); } - private onMilestoneDragEnd(element: d3.Selection) { + private onMilestoneDragEndCustom( + element: d3.Selection, + hasMoved: boolean + ) { this.isDraggingMilestone = false; - element.select(".hitbox").attr("style", "cursor: grab; pointer-events: all;"); - + document.body.style.cursor = ""; + element.select(".hitbox").attr("style", "cursor: pointer; pointer-events: all;"); + this.stopDragScrollTimer(); this.removeDragTimeIndicator(); - if (this.hasMovedDuringDrag) { + if (hasMoved) { this.saveData(); - // Optional: Update tooltip position or refresh - this.loadGraphDataAndRefresh(); // Force redraw properly to sync zoom/pan states if needed, but only if moved. + this.loadGraphDataAndRefresh(); } } @@ -1340,7 +1664,7 @@ export class OverviewComponent this.buildMilestoneGraphics(g, m, index); let x = this.xScaledTimeZoned(m.date); - const dragBehavior = this.setupMilestoneDragBehavior(m); + this.setupMilestoneInteractions(g, m); let lastX = 0; let lastY = 0; @@ -1348,7 +1672,6 @@ export class OverviewComponent return g .attr("transform", `translate(${x}, ${this.inner_margin.top})`) .call((g) => g.classed("hidden", x < 0 || x > overview.width)) - .call(dragBehavior) .on("mouseenter", (e) => { if (overview.isDraggingMilestone) return; lastX = e.clientX; @@ -1382,6 +1705,7 @@ export class OverviewComponent }) .on("contextmenu", (e) => { overview.clearMilestoneHoverTimer(); + overview.cancelMilestoneLongPress(); if (overview.hovered_milestone) { overview.hovered_milestone = undefined; overview.tooltipService.hide(); @@ -1389,19 +1713,24 @@ export class OverviewComponent if (overview.isDraggingMilestone) return; e.preventDefault(); e.stopPropagation(); - const rawDate = overview.x_scale.invert(e.pageX); + const rawDate = overview.getDateFromMouseEvent(e); overview.openEditMilestoneContextMenu(m, e.pageX, e.pageY, rawDate); }) .on("click", (e) => { + if (overview.ignoreNextMilestoneClick) { + overview.ignoreNextMilestoneClick = false; + e.stopPropagation(); + e.preventDefault(); + return; + } overview.clearMilestoneHoverTimer(); if (overview.hovered_milestone) { overview.hovered_milestone = undefined; overview.tooltipService.hide(); } if (overview.isDraggingMilestone) return; - if (e.defaultPrevented) return; // Ignore click triggered by drag e.stopPropagation(); - const rawDate = overview.x_scale.invert(e.pageX); + const rawDate = overview.getDateFromMouseEvent(e); overview.openEditMilestoneContextMenu(m, e.pageX, e.pageY, rawDate); }); } diff --git a/src/app/components/home/home.component.ts b/src/app/components/home/home.component.ts index 3f7f4067..954f9839 100644 --- a/src/app/components/home/home.component.ts +++ b/src/app/components/home/home.component.ts @@ -1,6 +1,5 @@ import { Component, OnInit } from "@angular/core"; import { AuthService } from "@services/auth.service"; -import { TourService } from "@services/tour.service"; import { environment } from "../../../environments/environment"; /** @@ -17,27 +16,14 @@ export class HomeComponent implements OnInit { * HomeComponent constructor * @param authService The service managing authentication */ - constructor( - public authService: AuthService, - private tourService: TourService - ) {} + constructor(public authService: AuthService) {} version = environment.version; - ngOnInit() { - // Small timeout to ensure DOM is ready - setTimeout(() => { - if (this.authService.isSignedIn() && this.tourService.shouldShowTour()) { - this.tourService.startTour(); - } - }, 500); - } + ngOnInit() {} async onSignInGithub() { await this.authService.signIn(); - if (this.authService.isSignedIn() && this.tourService.shouldShowTour()) { - setTimeout(() => this.tourService.startTour(), 500); - } } scroll(el: HTMLElement) { diff --git a/src/app/components/overview-graph-contextual-menu/overview-graph-contextual-menu.component.html b/src/app/components/overview-graph-contextual-menu/overview-graph-contextual-menu.component.html index ac9092a3..e8a8ef6a 100644 --- a/src/app/components/overview-graph-contextual-menu/overview-graph-contextual-menu.component.html +++ b/src/app/components/overview-graph-contextual-menu/overview-graph-contextual-menu.component.html @@ -1,5 +1,5 @@
-
+
-
+ -
diff --git a/src/app/components/overview-graph-contextual-menu/overview-graph-contextual-menu.component.scss b/src/app/components/overview-graph-contextual-menu/overview-graph-contextual-menu.component.scss index 2c196f9e..79b1b2a6 100644 --- a/src/app/components/overview-graph-contextual-menu/overview-graph-contextual-menu.component.scss +++ b/src/app/components/overview-graph-contextual-menu/overview-graph-contextual-menu.component.scss @@ -1,3 +1,6 @@ +@import 'variables'; + .contextMenu { position: fixed; + z-index: $zindex-context-menu; } diff --git a/src/app/services/tour.service.ts b/src/app/services/tour.service.ts index 4e9a76cd..99b3a284 100644 --- a/src/app/services/tour.service.ts +++ b/src/app/services/tour.service.ts @@ -1,4 +1,5 @@ import { Injectable } from "@angular/core"; +import { Router } from "@angular/router"; import { TranslateService } from "@ngx-translate/core"; import { driver } from "driver.js"; @@ -9,13 +10,17 @@ export class TourService { private hasSeenTourKey = "hasSeenTour"; private driverObj: any; - constructor(private translate: TranslateService) {} + constructor( + private translate: TranslateService, + private router: Router + ) {} /** * Check if the tour should be shown (first visit). + * Temporarily disabled. */ public shouldShowTour(): boolean { - return localStorage.getItem(this.hasSeenTourKey) === null; + return false; } /** @@ -28,7 +33,17 @@ export class TourService { /** * Start the interactive tour. */ - public startTour(): void { + public async startTour(): Promise { + if (this.driverObj && this.driverObj.isActive()) { + this.driverObj.destroy(); + } + + if (this.router.url !== "/home") { + await this.router.navigate(["/home"]); + // Allow component DOM to initialize after navigation + await new Promise((resolve) => setTimeout(resolve, 300)); + } + this.driverObj = driver({ showProgress: true, nextBtnText: this.translate.instant("TOUR.NEXT"), diff --git a/src/app/shared/ui/type-picker/type-picker.component.html b/src/app/shared/ui/type-picker/type-picker.component.html index ae710f5a..a38df88b 100644 --- a/src/app/shared/ui/type-picker/type-picker.component.html +++ b/src/app/shared/ui/type-picker/type-picker.component.html @@ -20,9 +20,5 @@ {{ option.label | translate }}
- -
- {{ errorMessage }} -
diff --git a/src/app/shared/ui/type-picker/type-picker.component.scss b/src/app/shared/ui/type-picker/type-picker.component.scss index 0d76bad7..559817f0 100644 --- a/src/app/shared/ui/type-picker/type-picker.component.scss +++ b/src/app/shared/ui/type-picker/type-picker.component.scss @@ -26,8 +26,7 @@ } &.is-invalid { - background: rgba(239, 68, 68, 0.05); - box-shadow: inset 0 0 0 1px var(--color-danger); + box-shadow: inset 0 0 0 1px var(--color-danger-subtle); } .buttons-row { @@ -48,23 +47,6 @@ } } - .error-msg { - width: 100%; - text-align: center; - color: var(--color-danger); - font-size: 0.85rem; - font-weight: 500; - margin-top: 6px; - padding-top: 8px; - border-top: 1px solid var(--color-border); - animation: slideDownFade 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); - } - - @keyframes slideDownFade { - from { opacity: 0; transform: translateY(-8px); } - to { opacity: 1; transform: translateY(0); } - } - .type-indicator { position: absolute; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); diff --git a/src/app/shared/ui/type-picker/type-picker.component.ts b/src/app/shared/ui/type-picker/type-picker.component.ts index e8d10d6c..2542b55d 100644 --- a/src/app/shared/ui/type-picker/type-picker.component.ts +++ b/src/app/shared/ui/type-picker/type-picker.component.ts @@ -23,7 +23,6 @@ export class TypePickerComponent implements ControlValueAccessor, AfterViewInit @Input() options: TypePickerOption[] = []; @Input() wrapContent: boolean = false; @Input() isInvalid: boolean = false; - @Input() errorMessage: string = ''; @ViewChildren('btn') buttons!: QueryList; diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 2ef17a3e..78980d2c 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -397,7 +397,7 @@ "HOME-OBJECTIVE-2": "Quickly see which questions are the least well answered in the group of students to identify problematic concepts and clarify them, or questions that are poorly worded.", "HOME-OBJECTIVE-3": "Follow the completion of the questions by the student group to plan the code review sessions and question corrections at the best possible time.", "GO-TO-GITHUB-TOOLTIP": "Go to our Github repository", - "NO-MILESTONE-TYPE-FEEDBACK": "A type of milestone must be choosen !", + "NO-MILESTONE-TYPE-FEEDBACK": "A type of milestone must be chosen", "MILESTONE-TYPE": "Milestone type", "MILESTONE-NOTES": "Personal notes", "REQUIRED-FIELDS-TEXT": "These fields must be filled in to validate the form.", diff --git a/src/assets/i18n/fr.json b/src/assets/i18n/fr.json index b4014825..15a171d7 100644 --- a/src/assets/i18n/fr.json +++ b/src/assets/i18n/fr.json @@ -397,7 +397,7 @@ "HOME-OBJECTIVE-2": "Voir rapidement quelles sont les questions qui posent le plus de difficultés au groupe d'étudiant·es pour identifier les notions problématiques et faire un point dessus, ou des ambiguïtés dans le sujet.", "HOME-OBJECTIVE-1": "Suivre le travail des étudiant·es, commit par commit, pour identifier rapidement et en amont les étudiant·es en difficulté et leur apporter l'aide nécessaire pour acquérir les connaissances associées.", "GO-TO-GITHUB-TOOLTIP": "Accédez au dépôt Github de Git4School", - "NO-MILESTONE-TYPE-FEEDBACK": "Un type de jalon doit être choisi !", + "NO-MILESTONE-TYPE-FEEDBACK": "Un type de jalon doit être choisi", "MILESTONE-TYPE": "Type de jalon", "MILESTONE-NOTES": "Notes personnelles", "REQUIRED-FIELDS-TEXT": "Ces champs doivent être remplis pour valider le formulaire.", diff --git a/src/assets/i18n/ru.json b/src/assets/i18n/ru.json index 7750960f..186a834f 100644 --- a/src/assets/i18n/ru.json +++ b/src/assets/i18n/ru.json @@ -394,7 +394,7 @@ "HOME-OBJECTIVE-2": "Быстро выявляйте вопросы, на которые хуже всего отвечают в группе студентов, чтобы определить проблемные концепции и прояснить их, или вопросы с плохой формулировкой.", "HOME-OBJECTIVE-3": "Отслеживайте выполнение вопросов группой студентов, чтобы планировать сеансы проверки кода и исправления вопросов в наилучшее время.", "GO-TO-GITHUB-TOOLTIP": "Перейти к нашему репозиторию Github", - "NO-MILESTONE-TYPE-FEEDBACK": "Необходимо выбрать тип этапа!", + "NO-MILESTONE-TYPE-FEEDBACK": "Необходимо выбрать тип этапа", "MILESTONE-TYPE": "Тип этапа", "MILESTONE-NOTES": "Личные заметки", "REQUIRED-FIELDS-TEXT": "Эти поля должны быть заполнены для подтверждения формы.", diff --git a/src/styles/_components.scss b/src/styles/_components.scss index 5acc1399..b3f3db9e 100644 --- a/src/styles/_components.scss +++ b/src/styles/_components.scss @@ -668,6 +668,23 @@ outline: none; font-weight: 500; } + + &.dropdown-item-danger { + color: var(--color-danger) !important; + + i { + color: var(--color-danger) !important; + } + + &:hover, &:focus { + color: var(--color-danger) !important; + background-color: var(--color-danger-bg) !important; + + i { + color: var(--color-danger) !important; + } + } + } } /* ========================================= @@ -856,6 +873,23 @@ color: var(--color-primary); font-weight: 600; } + + &.dropdown-item-danger { + color: var(--color-danger) !important; + + i { + color: var(--color-danger) !important; + } + + &:hover, &:focus { + color: var(--color-danger) !important; + background-color: var(--color-danger-bg) !important; + + i { + color: var(--color-danger) !important; + } + } + } } .dropdown-divider { diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss index 42cab327..12246836 100644 --- a/src/styles/_variables.scss +++ b/src/styles/_variables.scss @@ -24,6 +24,8 @@ $font-family-sans: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Robot --color-secondary: #64748b; // Slate for secondary actions --color-success: #059669; // Green: Restful, examples, success states --color-danger: #dc2626; // Red: Sparing use, errors, critical actions + --color-danger-subtle: rgba(220, 38, 38, 0.45); + --color-danger-bg: rgba(220, 38, 38, 0.1); --color-warning: #d97706; // Amber/Orange: Warnings, dates, highlights --color-info: #0ea5e9; // Light blue for info @@ -58,6 +60,16 @@ $font-family-sans: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Robot --glass-border: rgba(226, 232, 240, 0.6); --tooltip-glass-bg: rgba(255, 255, 255, 0.85); + + // Transitions + --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1); + --transition-normal: 200ms cubic-bezier(0.4, 0, 0.2, 1); + --transition-slow: 300ms cubic-bezier(0.4, 0, 0.2, 1); + --transition-bounce: 150ms cubic-bezier(0.175, 0.885, 0.32, 1.275); + + // Milestone Drag Badge + --badge-drag-shadow: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.15)); + --badge-drag-shadow-ready: drop-shadow(0 3px 6px rgba(0, 0, 0, 0.25)); } body.dark-theme { @@ -78,6 +90,8 @@ body.dark-theme { --color-secondary: #94a3b8; // Slate --color-success: #10b981; // Brighter green --color-danger: #ef4444; // Brighter red + --color-danger-subtle: rgba(239, 68, 68, 0.45); + --color-danger-bg: rgba(239, 68, 68, 0.15); --color-warning: #f59e0b; // Brighter amber --color-info: #38bdf8; // Light blue @@ -112,6 +126,10 @@ body.dark-theme { --glass-border: rgba(51, 65, 85, 0.5); --tooltip-glass-bg: rgba(30, 41, 59, 0.85); + + // Milestone Drag Badge (Dark mode) + --badge-drag-shadow: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.45)); + --badge-drag-shadow-ready: drop-shadow(0 3px 6px rgba(0, 0, 0, 0.6)); } // Map SCSS variables to CSS custom properties so existing code works @@ -128,6 +146,7 @@ $color-primary-hover: var(--color-primary-hover); $color-secondary: var(--color-secondary); $color-success: var(--color-success); $color-danger: var(--color-danger); +$color-danger-bg: var(--color-danger-bg); $color-warning: var(--color-warning); $color-info: var(--color-info); @@ -170,6 +189,7 @@ $transition-drawer-exit: 0.2s cubic-bezier(0.32, 0, 0.67, 0); $transition-drawer: $transition-drawer-enter; // Z-Index Layers +$zindex-context-menu: 1050; $zindex-typeahead-dropdown: 1060; $zindex-quick-help-popover: 1070;