mirror of
https://github.com/opelly27/WinStudentGoalTracker.git
synced 2026-05-20 01:47:41 +00:00
next round of ux updates
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
<div class="overlay" (click)="onOverlayClick()"></div>
|
<div class="overlay" (click)="onOverlayClick()"></div>
|
||||||
<div class="modal" (click)="$event.stopPropagation()">
|
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-title" (click)="$event.stopPropagation()">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<span class="modal-title">{{ title() }}</span>
|
<span class="modal-title" id="modal-title">{{ title() }}</span>
|
||||||
<button class="close-btn" (click)="onClose()" aria-label="Close">×</button>
|
<button class="close-btn" (click)="onClose()" aria-label="Close">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
|||||||
@@ -1,14 +1,36 @@
|
|||||||
import { Component, input, output } from '@angular/core';
|
import { Component, ElementRef, HostListener, inject, input, OnDestroy, OnInit, output } from '@angular/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-modal-shell',
|
selector: 'app-modal-shell',
|
||||||
templateUrl: './modal-shell.html',
|
templateUrl: './modal-shell.html',
|
||||||
styleUrl: './modal-shell.scss',
|
styleUrl: './modal-shell.scss',
|
||||||
})
|
})
|
||||||
export class ModalShell {
|
export class ModalShell implements OnInit, OnDestroy {
|
||||||
readonly title = input.required<string>();
|
readonly title = input.required<string>();
|
||||||
readonly closed = output<void>();
|
readonly closed = output<void>();
|
||||||
|
|
||||||
|
private readonly el = inject(ElementRef);
|
||||||
|
private previousFocus: HTMLElement | null = null;
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.previousFocus = document.activeElement as HTMLElement;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const focusable = this.el.nativeElement.querySelector<HTMLElement>(
|
||||||
|
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||||
|
);
|
||||||
|
focusable?.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.previousFocus?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('keydown.escape')
|
||||||
|
onEscape() {
|
||||||
|
this.closed.emit();
|
||||||
|
}
|
||||||
|
|
||||||
onOverlayClick() {
|
onOverlayClick() {
|
||||||
this.closed.emit();
|
this.closed.emit();
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -48,9 +48,9 @@
|
|||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="field-label-row">
|
<div class="field-label-row">
|
||||||
<label class="field-label" for="prompt">Prompt</label>
|
<label class="field-label" for="prompt">Prompt</label>
|
||||||
@if (promptSaved()) {
|
<span class="save-indicator" aria-live="polite" aria-atomic="true">
|
||||||
<span class="save-indicator">✓ Saved</span>
|
@if (promptSaved()) { ✓ Saved }
|
||||||
}
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
id="prompt"
|
id="prompt"
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<div class="toast-stack">
|
<div class="toast-stack" aria-live="polite" aria-atomic="false">
|
||||||
@for (t of toast.toasts(); track t.id) {
|
@for (t of toast.toasts(); track t.id) {
|
||||||
<div class="toast" [class]="'toast--' + t.type">
|
<div class="toast" [class]="'toast--' + t.type" [attr.role]="t.type === 'error' ? 'alert' : 'status'">
|
||||||
<span class="toast-msg">{{ t.message }}</span>
|
<span class="toast-msg">{{ t.message }}</span>
|
||||||
<button class="toast-close" (click)="toast.dismiss(t.id)" aria-label="Dismiss">
|
<button class="toast-close" (click)="toast.dismiss(t.id)" aria-label="Dismiss">
|
||||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
|
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" aria-hidden="true">
|
||||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
<h1 class="student-name">{{ student()!.identifier }}</h1>
|
<h1 class="student-name">{{ student()!.identifier }}</h1>
|
||||||
<span class="student-iep">IEP {{ formatDate(student()!.nextIepDate) }}</span>
|
<span class="student-iep">IEP {{ formatDate(student()!.nextIepDate) }}</span>
|
||||||
<button class="delete-student-btn" (click)="onDeleteStudent()" aria-label="Delete student">
|
<button class="delete-student-btn" (click)="onDeleteStudent()" aria-label="Delete student">
|
||||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
<polyline points="3 6 5 6 21 6"/>
|
<polyline points="3 6 5 6 21 6"/>
|
||||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||||
<line x1="10" y1="11" x2="10" y2="17"/>
|
<line x1="10" y1="11" x2="10" y2="17"/>
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
<button class="delete-goal-btn" (click)="onDeleteGoal()" aria-label="Delete goal" title="Delete goal">
|
<button class="delete-goal-btn" (click)="onDeleteGoal()" aria-label="Delete goal" title="Delete goal">
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
<polyline points="3 6 5 6 21 6"/>
|
<polyline points="3 6 5 6 21 6"/>
|
||||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||||
<line x1="10" y1="11" x2="10" y2="17"/>
|
<line x1="10" y1="11" x2="10" y2="17"/>
|
||||||
@@ -140,7 +140,7 @@
|
|||||||
<app-edit-icon size="13" (click)="onEditBenchmark(b)" ariaLabel="Edit benchmark" />
|
<app-edit-icon size="13" (click)="onEditBenchmark(b)" ariaLabel="Edit benchmark" />
|
||||||
<span class="benchmark-name">{{ b.shortName || b.benchmark }}</span>
|
<span class="benchmark-name">{{ b.shortName || b.benchmark }}</span>
|
||||||
<button class="delete-benchmark-btn" (click)="onDeleteBenchmark(b)" aria-label="Delete benchmark" title="Delete benchmark">
|
<button class="delete-benchmark-btn" (click)="onDeleteBenchmark(b)" aria-label="Delete benchmark" title="Delete benchmark">
|
||||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
<polyline points="3 6 5 6 21 6"/>
|
<polyline points="3 6 5 6 21 6"/>
|
||||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||||
<line x1="10" y1="11" x2="10" y2="17"/>
|
<line x1="10" y1="11" x2="10" y2="17"/>
|
||||||
@@ -167,7 +167,7 @@
|
|||||||
<app-edit-icon size="13" (click)="onEditEvent(ev)" ariaLabel="Edit event" color="#bbb" />
|
<app-edit-icon size="13" (click)="onEditEvent(ev)" ariaLabel="Edit event" color="#bbb" />
|
||||||
<span class="event-date">{{ formatDate(ev.createdAt) }}</span>
|
<span class="event-date">{{ formatDate(ev.createdAt) }}</span>
|
||||||
<button class="delete-event-btn" (click)="onDeleteEvent(ev)" aria-label="Delete event" title="Delete event">
|
<button class="delete-event-btn" (click)="onDeleteEvent(ev)" aria-label="Delete event" title="Delete event">
|
||||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
<polyline points="3 6 5 6 21 6"/>
|
<polyline points="3 6 5 6 21 6"/>
|
||||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||||
<line x1="10" y1="11" x2="10" y2="17"/>
|
<line x1="10" y1="11" x2="10" y2="17"/>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<div class="sidebar-controls">
|
<div class="sidebar-controls">
|
||||||
<span class="controls-label">My Students</span>
|
<span class="controls-label">My Students</span>
|
||||||
<label class="scope-toggle">
|
<label class="scope-toggle">
|
||||||
<input type="checkbox" [checked]="showAll()" (change)="onToggleScope()" hidden>
|
<input type="checkbox" class="sr-only" [checked]="showAll()" (change)="onToggleScope()" aria-label="Show all students">
|
||||||
<span class="toggle-track" [class.active]="showAll()">
|
<span class="toggle-track" [class.active]="showAll()">
|
||||||
<span class="toggle-thumb"></span>
|
<span class="toggle-thumb"></span>
|
||||||
</span>
|
</span>
|
||||||
@@ -34,7 +34,11 @@
|
|||||||
}
|
}
|
||||||
@for (s of group.students; track s.studentId) {
|
@for (s of group.students; track s.studentId) {
|
||||||
<div class="student-item" [class.active]="selectedStudentId() === s.studentId"
|
<div class="student-item" [class.active]="selectedStudentId() === s.studentId"
|
||||||
(click)="onSelectStudent(s)">
|
role="button" tabindex="0"
|
||||||
|
[attr.aria-label]="s.identifier"
|
||||||
|
(click)="onSelectStudent(s)"
|
||||||
|
(keydown.enter)="onSelectStudent(s)"
|
||||||
|
(keydown.space)="onSelectStudent(s); $event.preventDefault()">
|
||||||
<div class="student-item-info">
|
<div class="student-item-info">
|
||||||
<div class="student-item-name" [class.bold]="selectedStudentId() === s.studentId">
|
<div class="student-item-name" [class.bold]="selectedStudentId() === s.studentId">
|
||||||
{{ s.identifier }}
|
{{ s.identifier }}
|
||||||
|
|||||||
@@ -11,8 +11,8 @@
|
|||||||
--text-primary: #1a1a1a;
|
--text-primary: #1a1a1a;
|
||||||
--text-secondary: #555;
|
--text-secondary: #555;
|
||||||
--text-muted: #888;
|
--text-muted: #888;
|
||||||
--text-faint: #999;
|
--text-faint: #717171;
|
||||||
--text-dim: #bbb;
|
--text-dim: #767676;
|
||||||
--accent-indigo: #4338CA;
|
--accent-indigo: #4338CA;
|
||||||
--accent-indigo-light: #6366F1;
|
--accent-indigo-light: #6366F1;
|
||||||
--accent-indigo-dark: #3730A3;
|
--accent-indigo-dark: #3730A3;
|
||||||
@@ -72,6 +72,32 @@ select:focus {
|
|||||||
box-shadow: 0 0 0 2px rgba(67, 56, 202, 0.12);
|
box-shadow: 0 0 0 2px rgba(67, 56, 202, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Visible focus ring for all interactive elements (keyboard navigation) */
|
||||||
|
:focus-visible {
|
||||||
|
outline: 2px solid var(--accent-indigo);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Suppress outline for mouse/touch users on inputs (handled above) */
|
||||||
|
input:focus:not(:focus-visible),
|
||||||
|
textarea:focus:not(:focus-visible),
|
||||||
|
select:focus:not(:focus-visible) {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Screen reader only utility ─── */
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* ─── Scrollbar (subtle) ─── */
|
/* ─── Scrollbar (subtle) ─── */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
|
|||||||
Reference in New Issue
Block a user