next round of ux updates

This commit is contained in:
Armin Abaye
2026-04-24 21:24:03 -04:00
parent b49f8d9cda
commit 1d684827a3
7 changed files with 70 additions and 18 deletions
@@ -1,7 +1,7 @@
<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">
<span class="modal-title">{{ title() }}</span>
<span class="modal-title" id="modal-title">{{ title() }}</span>
<button class="close-btn" (click)="onClose()" aria-label="Close">&times;</button>
</div>
<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({
selector: 'app-modal-shell',
templateUrl: './modal-shell.html',
styleUrl: './modal-shell.scss',
})
export class ModalShell {
export class ModalShell implements OnInit, OnDestroy {
readonly title = input.required<string>();
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() {
this.closed.emit();
}
@@ -48,9 +48,9 @@
<div class="field">
<div class="field-label-row">
<label class="field-label" for="prompt">Prompt</label>
@if (promptSaved()) {
<span class="save-indicator">&#10003; Saved</span>
}
<span class="save-indicator" aria-live="polite" aria-atomic="true">
@if (promptSaved()) { &#10003; Saved }
</span>
</div>
<textarea
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) {
<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>
<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="6" y1="6" x2="18" y2="18"/>
</svg>
@@ -69,7 +69,7 @@
<h1 class="student-name">{{ student()!.identifier }}</h1>
<span class="student-iep">IEP {{ formatDate(student()!.nextIepDate) }}</span>
<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"/>
<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"/>
@@ -107,7 +107,7 @@
</span>
}
<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"/>
<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"/>
@@ -140,7 +140,7 @@
<app-edit-icon size="13" (click)="onEditBenchmark(b)" ariaLabel="Edit benchmark" />
<span class="benchmark-name">{{ b.shortName || b.benchmark }}</span>
<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"/>
<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"/>
@@ -167,7 +167,7 @@
<app-edit-icon size="13" (click)="onEditEvent(ev)" ariaLabel="Edit event" color="#bbb" />
<span class="event-date">{{ formatDate(ev.createdAt) }}</span>
<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"/>
<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"/>
@@ -19,7 +19,7 @@
<div class="sidebar-controls">
<span class="controls-label">My Students</span>
<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-thumb"></span>
</span>
@@ -34,7 +34,11 @@
}
@for (s of group.students; track 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-name" [class.bold]="selectedStudentId() === s.studentId">
{{ s.identifier }}
+28 -2
View File
@@ -11,8 +11,8 @@
--text-primary: #1a1a1a;
--text-secondary: #555;
--text-muted: #888;
--text-faint: #999;
--text-dim: #bbb;
--text-faint: #717171;
--text-dim: #767676;
--accent-indigo: #4338CA;
--accent-indigo-light: #6366F1;
--accent-indigo-dark: #3730A3;
@@ -72,6 +72,32 @@ select:focus {
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) ─── */
::-webkit-scrollbar {
width: 6px;