fix(web): label interactive controls

This commit is contained in:
Saravana Achu Mac 2026-05-05 10:22:49 -07:00
parent 16dddcb728
commit 01c2d31514
8 changed files with 38 additions and 23 deletions

View File

@ -100,8 +100,8 @@ export default function SettingsPage() {
{error && <div style={{ color: "var(--nl-danger)", fontSize: "var(--nl-fs-sm)" }}>{error}</div>}
{success && <div style={{ color: "var(--nl-status-success)", fontSize: "var(--nl-fs-sm)" }}>{success}</div>}
<form onSubmit={handleChangePassword} style={{ display: "grid", gap: "var(--nl-space-3)" }}>
<input className="input-shell" type="password" placeholder="Current password" value={currentPassword} onChange={(e) => setCurrentPassword(e.target.value)} required />
<input className="input-shell" type="password" placeholder="New password" minLength={8} value={newPassword} onChange={(e) => setNewPassword(e.target.value)} required />
<input className="input-shell" type="password" placeholder="Current password" aria-label="Current password" value={currentPassword} onChange={(e) => setCurrentPassword(e.target.value)} required />
<input className="input-shell" type="password" placeholder="New password" aria-label="New password" minLength={8} value={newPassword} onChange={(e) => setNewPassword(e.target.value)} required />
<button type="submit" disabled={isLoading} style={{ padding: "8px 16px", background: "var(--nl-accent-primary)", color: "var(--nl-on-accent)", border: "none", borderRadius: "var(--nl-radius-sm)", fontWeight: 600, justifySelf: "start" }}>
{isLoading ? "Updating…" : "Update password"}
</button>
@ -176,15 +176,15 @@ MCP API base: ${MCP_SERVER_URL}
<section className="surface-card" style={{ padding: "var(--nl-space-5)", display: "grid", gap: "var(--nl-space-3)", marginTop: "var(--nl-space-4)" }}>
<strong>Send feedback</strong>
<div style={{ display: "grid", gridTemplateColumns: "140px 1fr", gap: "var(--nl-space-3)" }}>
<select className="input-shell" value={feedbackType} onChange={(e) => setFeedbackType(e.target.value as typeof feedbackType)}>
<select className="input-shell" value={feedbackType} onChange={(e) => setFeedbackType(e.target.value as typeof feedbackType)} aria-label="Feedback type">
<option value="bug">Bug</option>
<option value="feature">Feature request</option>
<option value="praise">Praise</option>
<option value="other">Other</option>
</select>
<input className="input-shell" placeholder="Title" value={feedbackTitle} onChange={(e) => setFeedbackTitle(e.target.value)} />
<input className="input-shell" placeholder="Title" aria-label="Feedback title" value={feedbackTitle} onChange={(e) => setFeedbackTitle(e.target.value)} />
</div>
<textarea className="input-shell" placeholder="Details (optional)" rows={3} value={feedbackBody} onChange={(e) => setFeedbackBody(e.target.value)} style={{ resize: "vertical" }} />
<textarea className="input-shell" placeholder="Details (optional)" aria-label="Feedback details" rows={3} value={feedbackBody} onChange={(e) => setFeedbackBody(e.target.value)} style={{ resize: "vertical" }} />
<button
onClick={handleSubmitFeedback}
disabled={submittingFeedback || !feedbackTitle.trim()}

View File

@ -99,6 +99,7 @@ export function ArtifactPanel({
value={title}
onChange={(event) => setTitle(event.target.value)}
placeholder="Artifact title"
aria-label="Artifact title"
/>
</div>
<div style={{ display: "grid", gridTemplateColumns: "minmax(140px, 180px) minmax(0, 1fr)", gap: "var(--nl-space-3)" }}>
@ -106,6 +107,7 @@ export function ArtifactPanel({
className="input-shell"
value={artifactType}
onChange={(event) => setArtifactType(event.target.value as "file" | "summary" | "extraction" | "citation" | "export")}
aria-label="Artifact type"
>
<option value="file">file</option>
<option value="summary">summary</option>
@ -118,6 +120,7 @@ export function ArtifactPanel({
value={blobPath}
onChange={(event) => setBlobPath(event.target.value)}
placeholder="Optional blob path"
aria-label="Artifact blob path"
/>
</div>
<textarea
@ -125,6 +128,7 @@ export function ArtifactPanel({
value={description}
onChange={(event) => setDescription(event.target.value)}
placeholder="Description"
aria-label="Artifact description"
style={{ minHeight: 96, resize: "vertical" }}
/>
<div style={{ display: "flex", justifyContent: "flex-end", gap: "var(--nl-space-2)" }}>
@ -173,6 +177,7 @@ export function ArtifactPanel({
void handleOpenArtifact(artifact);
}}
disabled={openingId === artifact.id}
aria-label={`Open artifact: ${artifact.name}`}
>
{openingId === artifact.id ? "Opening…" : "Open"}
</button>

View File

@ -94,6 +94,7 @@ export function CreateNoteModal({ workspaces, defaultWorkspaceId, onCreated, onC
<span style={{ fontWeight: 600, fontSize: "0.875rem" }}>Template</span>
<select
className="input"
aria-label="Note template"
value={templateId}
onChange={(e) => {
const v = e.target.value;
@ -115,7 +116,7 @@ export function CreateNoteModal({ workspaces, defaultWorkspaceId, onCreated, onC
<label style={{ display: "grid", gap: "var(--nl-space-1)" }}>
<span style={{ fontWeight: 600, fontSize: "0.875rem" }}>Workspace</span>
<select value={workspaceId} onChange={(e) => setWorkspaceId(e.target.value)} className="input">
<select value={workspaceId} onChange={(e) => setWorkspaceId(e.target.value)} className="input" aria-label="Workspace">
{workspaces.map((ws) => (
<option key={ws.id} value={ws.id}>
{ws.name}
@ -132,6 +133,7 @@ export function CreateNoteModal({ workspaces, defaultWorkspaceId, onCreated, onC
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Note title"
aria-label="Note title"
autoFocus
/>
</label>
@ -143,6 +145,7 @@ export function CreateNoteModal({ workspaces, defaultWorkspaceId, onCreated, onC
value={body}
onChange={(e) => setBody(e.target.value)}
placeholder="Note content..."
aria-label="Note body"
rows={6}
style={{ resize: "vertical" }}
/>
@ -156,6 +159,7 @@ export function CreateNoteModal({ workspaces, defaultWorkspaceId, onCreated, onC
value={tags}
onChange={(e) => setTags(e.target.value)}
placeholder="launch, meeting, review"
aria-label="Note tags"
/>
</label>

View File

@ -51,12 +51,12 @@ export function CreateWorkspaceModal({ onCreated, onClose }: Props) {
<label style={{ display: "grid", gap: 6 }}>
<span style={{ fontWeight: 600, fontSize: "var(--nl-fs-sm)" }}>Name</span>
<input className="input-shell" type="text" required value={name} onChange={(e) => setName(e.target.value)} autoFocus />
<input className="input-shell" type="text" required value={name} onChange={(e) => setName(e.target.value)} aria-label="Workspace name" autoFocus />
</label>
<label style={{ display: "grid", gap: 6 }}>
<span style={{ fontWeight: 600, fontSize: "var(--nl-fs-sm)" }}>Description</span>
<textarea className="input-shell" value={description} onChange={(e) => setDescription(e.target.value)} rows={3} style={{ resize: "vertical" }} />
<textarea className="input-shell" value={description} onChange={(e) => setDescription(e.target.value)} rows={3} style={{ resize: "vertical" }} aria-label="Workspace description" />
</label>
<div style={{ display: "flex", justifyContent: "flex-end", gap: "var(--nl-space-3)" }}>

View File

@ -97,6 +97,7 @@ export function LinkNoteModal({ noteId, workspaceId, existingLinkedIds, onLinked
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search notes..."
aria-label="Search notes to link"
style={{ flex: 1 }}
autoFocus
/>
@ -123,6 +124,7 @@ export function LinkNoteModal({ noteId, workspaceId, existingLinkedIds, onLinked
border: selectedId === note.id ? "2px solid var(--nl-accent-primary)" : "2px solid transparent",
}}
onClick={() => setSelectedId(note.id)}
aria-label={`Select note: ${note.title}`}
>
<div style={{ fontWeight: 600 }}>{note.title}</div>
<div style={{ color: "var(--nl-text-secondary)", fontSize: "0.875rem" }}>{note.excerpt}</div>
@ -134,7 +136,7 @@ export function LinkNoteModal({ noteId, workspaceId, existingLinkedIds, onLinked
{selectedId && (
<label style={{ display: "grid", gap: "var(--nl-space-1)" }}>
<span style={{ fontWeight: 600, fontSize: "0.875rem" }}>Relationship type</span>
<select value={relationshipType} onChange={(e) => setRelationshipType(e.target.value)} className="input">
<select value={relationshipType} onChange={(e) => setRelationshipType(e.target.value)} className="input" aria-label="Relationship type">
{RELATIONSHIP_TYPES.map((rt) => (
<option key={rt} value={rt}>
{rt.replace("_", " ")}

View File

@ -25,9 +25,9 @@ const TOOLBAR_BTN_ACTIVE: React.CSSProperties = {
color: "var(--nl-text-primary)",
};
function ToolbarButton({ active, onClick, label }: { active: boolean; onClick: () => void; label: string }) {
function ToolbarButton({ active, onClick, label, ariaLabel }: { active: boolean; onClick: () => void; label: string; ariaLabel: string }) {
return (
<button type="button" style={active ? TOOLBAR_BTN_ACTIVE : TOOLBAR_BTN} onClick={onClick} title={label}>
<button type="button" style={active ? TOOLBAR_BTN_ACTIVE : TOOLBAR_BTN} onClick={onClick} title={label} aria-label={ariaLabel}>
{label}
</button>
);
@ -194,18 +194,18 @@ export function NoteEditor({
{editor && (
<div style={{ display: "flex", gap: 4, flexWrap: "wrap", padding: "4px 0" }}>
<ToolbarButton active={editor.isActive("bold")} onClick={() => editor.chain().focus().toggleBold().run()} label="B" />
<ToolbarButton active={editor.isActive("italic")} onClick={() => editor.chain().focus().toggleItalic().run()} label="I" />
<ToolbarButton active={editor.isActive("strike")} onClick={() => editor.chain().focus().toggleStrike().run()} label="S" />
<ToolbarButton active={editor.isActive("bold")} onClick={() => editor.chain().focus().toggleBold().run()} label="B" ariaLabel="Bold" />
<ToolbarButton active={editor.isActive("italic")} onClick={() => editor.chain().focus().toggleItalic().run()} label="I" ariaLabel="Italic" />
<ToolbarButton active={editor.isActive("strike")} onClick={() => editor.chain().focus().toggleStrike().run()} label="S" ariaLabel="Strikethrough" />
<span style={{ width: 1, background: "var(--nl-border-default)", margin: "0 4px" }} />
<ToolbarButton active={editor.isActive("heading", { level: 1 })} onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()} label="H1" />
<ToolbarButton active={editor.isActive("heading", { level: 2 })} onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()} label="H2" />
<ToolbarButton active={editor.isActive("heading", { level: 3 })} onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()} label="H3" />
<ToolbarButton active={editor.isActive("heading", { level: 1 })} onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()} label="H1" ariaLabel="Heading level 1" />
<ToolbarButton active={editor.isActive("heading", { level: 2 })} onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()} label="H2" ariaLabel="Heading level 2" />
<ToolbarButton active={editor.isActive("heading", { level: 3 })} onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()} label="H3" ariaLabel="Heading level 3" />
<span style={{ width: 1, background: "var(--nl-border-default)", margin: "0 4px" }} />
<ToolbarButton active={editor.isActive("bulletList")} onClick={() => editor.chain().focus().toggleBulletList().run()} label="•" />
<ToolbarButton active={editor.isActive("orderedList")} onClick={() => editor.chain().focus().toggleOrderedList().run()} label="1." />
<ToolbarButton active={editor.isActive("codeBlock")} onClick={() => editor.chain().focus().toggleCodeBlock().run()} label="<>" />
<ToolbarButton active={editor.isActive("blockquote")} onClick={() => editor.chain().focus().toggleBlockquote().run()} label="❝" />
<ToolbarButton active={editor.isActive("bulletList")} onClick={() => editor.chain().focus().toggleBulletList().run()} label="•" ariaLabel="Bullet list" />
<ToolbarButton active={editor.isActive("orderedList")} onClick={() => editor.chain().focus().toggleOrderedList().run()} label="1." ariaLabel="Numbered list" />
<ToolbarButton active={editor.isActive("codeBlock")} onClick={() => editor.chain().focus().toggleCodeBlock().run()} label="<>" ariaLabel="Code block" />
<ToolbarButton active={editor.isActive("blockquote")} onClick={() => editor.chain().focus().toggleBlockquote().run()} label="❝" ariaLabel="Block quote" />
</div>
)}
@ -244,7 +244,7 @@ export function NoteEditor({
{explainResult && (
<div style={{ background: "var(--nl-surface-card)", border: "1px solid var(--nl-border-default)", borderRadius: "var(--nl-radius-md)", padding: "var(--nl-space-3)", fontSize: "var(--nl-fs-sm)", color: "var(--nl-text-secondary)", display: "flex", gap: 8, alignItems: "flex-start" }}>
<div style={{ flex: 1 }}>{explainResult}</div>
<button type="button" style={{ ...TOOLBAR_BTN, fontSize: 12 }} onClick={() => setExplainResult(null)}></button>
<button type="button" style={{ ...TOOLBAR_BTN, fontSize: 12 }} onClick={() => setExplainResult(null)} aria-label="Dismiss explanation"></button>
</div>
)}

View File

@ -84,7 +84,7 @@ export function SurveyBanner() {
<span><strong>{survey.title}</strong> Quick survey ({survey.questions.length} questions)</span>
<div style={{ display: "flex", gap: "var(--nl-space-3)" }}>
<button onClick={handleStart} style={{ background: "var(--nl-accent-primary)", color: "var(--nl-on-accent)", border: "none", borderRadius: "var(--nl-radius-sm)", padding: "4px 12px", fontWeight: 600, cursor: "pointer" }}>Start</button>
<button onClick={dismiss} style={{ background: "none", border: "none", color: "var(--nl-text-secondary)", cursor: "pointer" }}>&times;</button>
<button onClick={dismiss} aria-label="Dismiss survey" style={{ background: "none", border: "none", color: "var(--nl-text-secondary)", cursor: "pointer" }}>&times;</button>
</div>
</div>
);
@ -125,6 +125,7 @@ export function SurveyBanner() {
placeholder="Your answer…"
value={getTextValue()}
onChange={(e) => handleAnswer(question, e.target.value)}
aria-label="Survey answer"
/>
)}
@ -134,6 +135,7 @@ export function SurveyBanner() {
<button
key={n}
onClick={() => handleAnswer(question, String(n))}
aria-label={`Rate ${n}`}
style={{
width: 36, height: 36,
borderRadius: "var(--nl-radius-sm)",
@ -155,6 +157,7 @@ export function SurveyBanner() {
<button
key={opt.id}
onClick={() => handleAnswer(question, opt.id)}
aria-label={`Choose ${opt.text}`}
style={{
padding: "4px 12px",
borderRadius: "var(--nl-radius-sm)",

View File

@ -51,6 +51,7 @@ export function TaskReviewPanel({
value={description}
onChange={(event) => setDescription(event.target.value)}
placeholder="Description"
aria-label="Task description"
style={{ minHeight: 96, resize: "vertical" }}
/>
<div style={{ display: "flex", justifyContent: "flex-end" }}>