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>} {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>} {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)" }}> <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="Current password" aria-label="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="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" }}> <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"} {isLoading ? "Updating…" : "Update password"}
</button> </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)" }}> <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> <strong>Send feedback</strong>
<div style={{ display: "grid", gridTemplateColumns: "140px 1fr", gap: "var(--nl-space-3)" }}> <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="bug">Bug</option>
<option value="feature">Feature request</option> <option value="feature">Feature request</option>
<option value="praise">Praise</option> <option value="praise">Praise</option>
<option value="other">Other</option> <option value="other">Other</option>
</select> </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> </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 <button
onClick={handleSubmitFeedback} onClick={handleSubmitFeedback}
disabled={submittingFeedback || !feedbackTitle.trim()} disabled={submittingFeedback || !feedbackTitle.trim()}

View File

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

View File

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

View File

@ -51,12 +51,12 @@ export function CreateWorkspaceModal({ onCreated, onClose }: Props) {
<label style={{ display: "grid", gap: 6 }}> <label style={{ display: "grid", gap: 6 }}>
<span style={{ fontWeight: 600, fontSize: "var(--nl-fs-sm)" }}>Name</span> <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>
<label style={{ display: "grid", gap: 6 }}> <label style={{ display: "grid", gap: 6 }}>
<span style={{ fontWeight: 600, fontSize: "var(--nl-fs-sm)" }}>Description</span> <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> </label>
<div style={{ display: "flex", justifyContent: "flex-end", gap: "var(--nl-space-3)" }}> <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} value={query}
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
placeholder="Search notes..." placeholder="Search notes..."
aria-label="Search notes to link"
style={{ flex: 1 }} style={{ flex: 1 }}
autoFocus 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", border: selectedId === note.id ? "2px solid var(--nl-accent-primary)" : "2px solid transparent",
}} }}
onClick={() => setSelectedId(note.id)} onClick={() => setSelectedId(note.id)}
aria-label={`Select note: ${note.title}`}
> >
<div style={{ fontWeight: 600 }}>{note.title}</div> <div style={{ fontWeight: 600 }}>{note.title}</div>
<div style={{ color: "var(--nl-text-secondary)", fontSize: "0.875rem" }}>{note.excerpt}</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 && ( {selectedId && (
<label style={{ display: "grid", gap: "var(--nl-space-1)" }}> <label style={{ display: "grid", gap: "var(--nl-space-1)" }}>
<span style={{ fontWeight: 600, fontSize: "0.875rem" }}>Relationship type</span> <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) => ( {RELATIONSHIP_TYPES.map((rt) => (
<option key={rt} value={rt}> <option key={rt} value={rt}>
{rt.replace("_", " ")} {rt.replace("_", " ")}

View File

@ -25,9 +25,9 @@ const TOOLBAR_BTN_ACTIVE: React.CSSProperties = {
color: "var(--nl-text-primary)", 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 ( 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} {label}
</button> </button>
); );
@ -194,18 +194,18 @@ export function NoteEditor({
{editor && ( {editor && (
<div style={{ display: "flex", gap: 4, flexWrap: "wrap", padding: "4px 0" }}> <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("bold")} onClick={() => editor.chain().focus().toggleBold().run()} label="B" ariaLabel="Bold" />
<ToolbarButton active={editor.isActive("italic")} onClick={() => editor.chain().focus().toggleItalic().run()} label="I" /> <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" /> <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" }} /> <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: 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" /> <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" /> <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" }} /> <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("bulletList")} onClick={() => editor.chain().focus().toggleBulletList().run()} label="•" ariaLabel="Bullet list" />
<ToolbarButton active={editor.isActive("orderedList")} onClick={() => editor.chain().focus().toggleOrderedList().run()} label="1." /> <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="<>" /> <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="❝" /> <ToolbarButton active={editor.isActive("blockquote")} onClick={() => editor.chain().focus().toggleBlockquote().run()} label="❝" ariaLabel="Block quote" />
</div> </div>
)} )}
@ -244,7 +244,7 @@ export function NoteEditor({
{explainResult && ( {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={{ 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> <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> </div>
)} )}

View File

@ -84,7 +84,7 @@ export function SurveyBanner() {
<span><strong>{survey.title}</strong> Quick survey ({survey.questions.length} questions)</span> <span><strong>{survey.title}</strong> Quick survey ({survey.questions.length} questions)</span>
<div style={{ display: "flex", gap: "var(--nl-space-3)" }}> <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={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>
</div> </div>
); );
@ -125,6 +125,7 @@ export function SurveyBanner() {
placeholder="Your answer…" placeholder="Your answer…"
value={getTextValue()} value={getTextValue()}
onChange={(e) => handleAnswer(question, e.target.value)} onChange={(e) => handleAnswer(question, e.target.value)}
aria-label="Survey answer"
/> />
)} )}
@ -134,6 +135,7 @@ export function SurveyBanner() {
<button <button
key={n} key={n}
onClick={() => handleAnswer(question, String(n))} onClick={() => handleAnswer(question, String(n))}
aria-label={`Rate ${n}`}
style={{ style={{
width: 36, height: 36, width: 36, height: 36,
borderRadius: "var(--nl-radius-sm)", borderRadius: "var(--nl-radius-sm)",
@ -155,6 +157,7 @@ export function SurveyBanner() {
<button <button
key={opt.id} key={opt.id}
onClick={() => handleAnswer(question, opt.id)} onClick={() => handleAnswer(question, opt.id)}
aria-label={`Choose ${opt.text}`}
style={{ style={{
padding: "4px 12px", padding: "4px 12px",
borderRadius: "var(--nl-radius-sm)", borderRadius: "var(--nl-radius-sm)",

View File

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