diff --git a/BlogWriter.Web.Tests/BlogWorkspaceServiceTests.cs b/BlogWriter.Web.Tests/BlogWorkspaceServiceTests.cs index 6f3c26e..64973ba 100644 --- a/BlogWriter.Web.Tests/BlogWorkspaceServiceTests.cs +++ b/BlogWriter.Web.Tests/BlogWorkspaceServiceTests.cs @@ -2,8 +2,30 @@ namespace BlogWriter.Web.Tests; -public sealed class BlogWorkspaceServiceTests +public sealed class BlogWorkspaceServiceTests : IDisposable { + private readonly SynchronizationContext? _previousSynchronizationContext = SynchronizationContext.Current; + + public BlogWorkspaceServiceTests() => + // BlogWorkspaceService reports progress via IProgress, which posts to + // SynchronizationContext.Current. Without an ambient context (the default + // under xUnit), Progress falls back to ThreadPool.QueueUserWorkItem, + // which races with the synchronous continuation after each awaited + // operation and can append reviewer feedback out of order. Installing an + // immediate, single-threaded context here makes that ordering + // deterministic for every test in this class, matching how a Blazor + // Server circuit's single-threaded dispatcher behaves in production. + SynchronizationContext.SetSynchronizationContext(new ImmediateSynchronizationContext()); + + public void Dispose() => SynchronizationContext.SetSynchronizationContext(_previousSynchronizationContext); + + private sealed class ImmediateSynchronizationContext : SynchronizationContext + { + public override void Post(SendOrPostCallback d, object? state) => d(state); + + public override void Send(SendOrPostCallback d, object? state) => d(state); + } + [Fact] public async Task NewState_DisablesRevisionInputUntilDraftExists() { diff --git a/BlogWriter.Web.Tests/HomePageTests.cs b/BlogWriter.Web.Tests/HomePageTests.cs index e58ddc0..8fae5b9 100644 --- a/BlogWriter.Web.Tests/HomePageTests.cs +++ b/BlogWriter.Web.Tests/HomePageTests.cs @@ -11,7 +11,7 @@ public sealed class HomePageTests : BunitContext public HomePageTests() => JSInterop.Mode = JSRuntimeMode.Loose; [Fact] - public void Home_RendersWritingWorkspaceAndFourCommands() + public void Home_RendersWritingWorkspaceAndFiveCommands() { BlogWorkspaceService workspace = RegisterWorkspace(); @@ -21,7 +21,7 @@ public void Home_RendersWritingWorkspaceAndFourCommands() Assert.NotNull(cut.Find("#revision-prompt")); Assert.NotNull(cut.Find("[aria-labelledby='draft-heading']")); Assert.NotNull(cut.Find("[aria-labelledby='review-heading']")); - Assert.Equal(["New", "List", "Quit", "?"], + Assert.Equal(["New", "List", "Go", "Quit", "?"], cut.FindAll(".command-bar button").Select(button => button.TextContent.Trim()).ToArray()); Assert.True(cut.Find("#revision-prompt").HasAttribute("disabled")); Assert.False(workspace.State.IsSelectionVisible); @@ -98,7 +98,7 @@ public void Home_UsesAccessibleRegionsLabelsAndLiveStatus() RegisterWorkspace(); IRenderedComponent cut = Render(); - Assert.Equal("New writing prompt", cut.Find("label[for='initial-prompt']").TextContent.Trim()); + Assert.Equal("New query", cut.Find("label[for='initial-prompt']").TextContent.Trim()); Assert.Equal("Revision request", cut.Find("label[for='revision-prompt']").TextContent.Trim()); Assert.NotNull(cut.Find("nav[aria-label='Workspace commands']")); Assert.NotNull(cut.Find("[aria-live='polite']")); diff --git a/BlogWriter.Web/wwwroot/app.css b/BlogWriter.Web/wwwroot/app.css index 86a554f..f27f281 100644 --- a/BlogWriter.Web/wwwroot/app.css +++ b/BlogWriter.Web/wwwroot/app.css @@ -128,7 +128,9 @@ button, a { touch-action: manipulation; } cursor: pointer; } .range-go:hover:not(:disabled) { background: var(--forest-dark); } +.range-go:active:not(:disabled) { background: var(--forest-dark); } .range-go:disabled { border-color: var(--line); background: #ecece7; color: var(--muted); cursor: not-allowed; } +.range-go:active:disabled { background: #ecece7; } .work-grid { min-height: 32vh; display: grid; grid-template-columns: minmax(0, 1.55fr) minmax(280px, .75fr); border: 1px solid var(--ink); } .work-pane { min-width: 0; display: grid; grid-template-rows: auto minmax(0, 1fr); background: var(--surface); } diff --git a/specs/010-go-button-press-colors/checklists/requirements.md b/specs/010-go-button-press-colors/checklists/requirements.md new file mode 100644 index 0000000..21dfd36 --- /dev/null +++ b/specs/010-go-button-press-colors/checklists/requirements.md @@ -0,0 +1,34 @@ +# Specification Quality Checklist: Go Button Press Color Feedback + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-09-24 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- All checklist items pass on first pass; no spec updates required before `/speckit-clarify` or `/speckit-plan`. diff --git a/specs/010-go-button-press-colors/contracts/workspace-ui.md b/specs/010-go-button-press-colors/contracts/workspace-ui.md new file mode 100644 index 0000000..5b36133 --- /dev/null +++ b/specs/010-go-button-press-colors/contracts/workspace-ui.md @@ -0,0 +1,25 @@ +# Workspace UI Contract: Go Button Press Color Feedback + +## Controls + +| Control | Identifier/relationship | Behavior | +|---|---|---| +| Go | Existing command-bar button, `.range-go` (`CommandBar.razor`) | Idle/enabled: light green background (`--forest`). Hover (enabled, not pressed): dark green background (`--forest-dark`), unchanged from existing behavior. Pressed (enabled, mouse-down/touch-active/keyboard-activation in progress): dark green background (`--forest-dark`) via `:active`. Disabled: existing muted/gray disabled styling takes precedence over pressed styling. | + +## Visual State Transitions + +- Idle → Pressed: on press start (`:active`), while enabled. +- Pressed → Idle: on press end, cancellation, or pointer/focus leaving the button before release; no stuck-pressed state is permitted. +- Any state → Disabled: the `disabled` attribute styling always wins; a disabled button never shows the pressed (dark green) color even if a press gesture is attempted. + +## Accessibility and Keyboard Behavior + +- No change to the Go button's accessible name, role, tab order, or `disabled` semantics. +- The pressed color is a supplementary visual cue only; it does not replace or alter any existing text, label, or ARIA attributes conveying the button's action or state. +- Keyboard activation (Space/Enter) on a focused, enabled Go button must trigger the same pressed-color feedback as a mouse or touch press. + +## Non-Goals + +- No new HTTP, public API, storage, or hosted-agent contract. +- No change to the Go button's click/submission behavior, word-range parsing, session ownership, cancellation, or workflow-output contracts (see `009-explicit-go-submission`). +- No change to any other command-bar control's styling or behavior. diff --git a/specs/010-go-button-press-colors/data-model.md b/specs/010-go-button-press-colors/data-model.md new file mode 100644 index 0000000..3b3fa7f --- /dev/null +++ b/specs/010-go-button-press-colors/data-model.md @@ -0,0 +1,18 @@ +# Phase 1 Data Model: Go Button Press Color Feedback + +This feature introduces no persisted data, domain entities, or state stored beyond the +browser's native rendering of CSS pseudo-classes. There is no `IBlogSessionStore` / +`ResearchState` change and nothing to add to the workflow's domain model. + +## Conceptual state (not persisted) + +| State | Trigger | Visual Result | Notes | +|-------|---------|----------------|-------| +| Idle | Default; button enabled, not pressed | Light green background (`--forest`) | Existing behavior, unchanged | +| Hover | Pointer over enabled, non-pressed button | Dark green background (`--forest-dark`) | Existing `:hover:not(:disabled)` rule, unchanged | +| Pressed | Pointer/touch/keyboard press held on enabled button | Dark green background (`--forest-dark`) | New `:active:not(:disabled)` rule (this feature) | +| Disabled | `disabled` attribute present | Muted gray background, no pointer cursor | Existing `:disabled` rule; MUST take precedence over Pressed (FR-005), enforced by an `:active:disabled` override | + +These are transient CSS/browser render states, not application entities — they have no +identifiers, relationships, or persistence lifecycle, and require no repository, store, or +model class changes. diff --git a/specs/010-go-button-press-colors/plan.md b/specs/010-go-button-press-colors/plan.md new file mode 100644 index 0000000..c3f4f76 --- /dev/null +++ b/specs/010-go-button-press-colors/plan.md @@ -0,0 +1,80 @@ +# Implementation Plan: Go Button Press Color Feedback + +**Branch**: `010-go-button-press-colors` | **Date**: 2026-09-24 | **Spec**: [spec.md](./spec.md) + +**Input**: Feature specification from `/specs/010-go-button-press-colors/spec.md` + +**Note**: This template is filled in by the `/speckit-plan` command; its definition describes the execution workflow. + +## Summary + +Add visible press-state feedback to the existing `Go` command-bar button: it already renders with a light-green background (`--forest`) and darkens (`--forest-dark`) on hover, but has no distinct feedback while actually pressed/activated. The fix is a CSS-only addition of an `:active` (and disabled-safe) style rule on `.range-go` in `BlogWriter.Web/wwwroot/app.css`, using the existing `--forest` / `--forest-dark` custom properties, with no Razor markup or C# behavior changes required. + +## Technical Context + +**Language/Version**: C# / .NET 10 (Blazor Web App, `BlogWriter.Web`); plain CSS for styling + +**Primary Dependencies**: ASP.NET Core Blazor (existing `CommandBar.razor` component); no new packages + +**Storage**: N/A (purely presentational; no persisted state) + +**Testing**: bUnit component tests in `BlogWriter.Web.Tests` (e.g. `HomePageTests.cs`) for markup/class assertions; manual/visual verification for `:active` pseudo-class behavior, which bUnit cannot exercise directly since it does not run real CSS pseudo-class matching + +**Target Platform**: Web browser (desktop and touch), served by the Blazor Web App + +**Project Type**: Web application — single existing Blazor project (`BlogWriter.Web`) plus its test project; no frontend/backend split to introduce + +**Performance Goals**: N/A beyond standard browser rendering (color change must be perceptible within a single frame of press/release, per SC-001) + +**Constraints**: Must not alter the button's label, size, position, disabled semantics, or keyboard/pointer accessibility; must preserve sufficient contrast between idle and pressed colors (FR-007) + +**Scale/Scope**: Single UI element (`.range-go` in the command bar); no other buttons, pages, or components affected + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +- **I. Hosted-Agent Boundaries** — N/A. No hosted-agent, workflow, or console-orchestration code is touched; this is a Blazor UI styling change confined to `BlogWriter.Web`. +- **II. MAF-Native Workflow Composition** — N/A. No agents, executors, or workflow edges/states are involved. +- **III. Identity, Secrets, and Budget Control** — N/A. No Azure/Foundry calls, credentials, or token budget are affected. +- **IV. Testable and Observable Behavior** — Satisfied. The change is small enough to verify via existing bUnit component tests (asserting the button still renders with its expected class/attributes and disabled behavior is unchanged) plus a manual/visual check of the new `:active` color, since bUnit does not evaluate CSS pseudo-classes. +- **V. Simple, Compatible Evolution** — Satisfied. This is the smallest possible change (a CSS rule addition reusing existing `--forest`/`--forest-dark` tokens) that preserves the existing markup, component API, and all other button behavior. + +No violations identified. Complexity Tracking section is not needed. + +## Project Structure + +### Documentation (this feature) + +```text +specs/[###-feature]/ +├── plan.md # This file (/speckit-plan command output) +├── research.md # Phase 0 output (/speckit-plan command) +├── data-model.md # Phase 1 output (/speckit-plan command) +├── quickstart.md # Phase 1 output (/speckit-plan command) +├── contracts/ # Phase 1 output (/speckit-plan command) +└── tasks.md # Phase 2 output (/speckit-tasks command - NOT created by /speckit-plan) +``` + +### Source Code (repository root) + +```text +BlogWriter.Web/ +├── Components/ +│ └── CommandBar.razor # Renders the .range-go "Go" button (markup unchanged) +└── wwwroot/ + └── app.css # .range-go rule set: add :active / :active:disabled styling here + +BlogWriter.Web.Tests/ +└── HomePageTests.cs # Existing bUnit coverage of the command bar / Go button markup +``` + +**Structure Decision**: This feature stays entirely inside the existing single Blazor web +project `BlogWriter.Web` and its companion test project `BlogWriter.Web.Tests`. No new +projects, folders, or cross-project contracts are introduced; the only source change is a +CSS rule addition in `BlogWriter.Web/wwwroot/app.css` for the `.range-go` selector already +used by `CommandBar.razor`. + +## Complexity Tracking + +*No Constitution Check violations were identified; this section is not applicable.* diff --git a/specs/010-go-button-press-colors/quickstart.md b/specs/010-go-button-press-colors/quickstart.md new file mode 100644 index 0000000..4fc67ce --- /dev/null +++ b/specs/010-go-button-press-colors/quickstart.md @@ -0,0 +1,59 @@ +# Quickstart: Validate Go Button Press Color Feedback + +## Prerequisites + +- .NET 10 SDK installed +- Repository checked out on branch `010-go-button-press-colors` +- No additional packages or services required (pure front-end CSS change) + +## Setup + +```powershell +cd E:\ai\.net\blog\BlogWriter +dotnet build BlogWriter.Web\BlogWriter.Web.csproj +``` + +## Automated check (regression only) + +Run the existing Blazor component tests to confirm the Go button's markup, class name, +and disabled behavior are unchanged (bUnit does not evaluate `:active` CSS, so this does +not verify the color itself — see manual check below): + +```powershell +dotnet test BlogWriter.Web.Tests\BlogWriter.Web.Tests.csproj +``` + +Expected: all existing tests referencing the command bar / Go button continue to pass +with no changes required to the test assertions (see `contracts/workspace-ui.md` for the +behavior these tests should continue to cover). + +## Manual validation (visual) + +1. Run the web app: + ```powershell + dotnet run --project BlogWriter.Web\BlogWriter.Web.csproj + ``` +2. Open the app in a browser and locate the **Go** button in the command bar (after the + word-range Min/Max fields). +3. **Idle**: Confirm the Go button shows a light green background (`--forest`) when not + hovered or pressed. +4. **Pressed — mouse**: Click and hold the mouse button down on Go. Confirm the background + switches to dark green (`--forest-dark`) while held down, and reverts to light green on + release. +5. **Pressed — drag off**: Press down on Go, then drag the pointer off the button before + releasing. Confirm the button returns to light green and does not stay stuck in the + dark green pressed state. +6. **Pressed — keyboard**: Tab to focus the Go button, then press and hold Space or press + Enter. Confirm the same dark green pressed feedback appears. +7. **Pressed — touch** (if a touch device/emulator is available): Touch and hold the Go + button. Confirm the same dark green pressed feedback appears, and reverts on release. +8. **Disabled**: Trigger a state where Go is disabled (e.g. per existing validation rules + in the word-range row) and attempt to press it. Confirm it never shows the dark green + pressed color and instead keeps its existing disabled (muted) appearance. + +## Expected Outcome + +- All steps above match the acceptance scenarios and edge cases in + `spec.md` (User Story 1, Edge Cases). +- No other command-bar button's appearance or behavior changes. +- The Go button's label, size, position, and click/submission behavior are unchanged. diff --git a/specs/010-go-button-press-colors/research.md b/specs/010-go-button-press-colors/research.md new file mode 100644 index 0000000..d51265f --- /dev/null +++ b/specs/010-go-button-press-colors/research.md @@ -0,0 +1,56 @@ +# Phase 0 Research: Go Button Press Color Feedback + +No `NEEDS CLARIFICATION` markers remain in the Technical Context — the feature is scoped +entirely within existing, well-understood project conventions. This document records the +small set of decisions made while confirming the approach. + +## Decision: Reuse existing color tokens instead of introducing new ones + +- **Decision**: Implement "light green" and "dark green" using the existing CSS custom + properties `--forest` (`#1e5a46`) and `--forest-dark` (`#123d30`), already defined in + `BlogWriter.Web/wwwroot/app.css` and already used as the Go button's idle background and + hover background respectively. +- **Rationale**: These tokens already read as "lighter green" vs. "darker green," are used + consistently elsewhere in the stylesheet (e.g. `.sign-out`), and satisfy FR-007's + contrast/brightness requirement without introducing new design tokens or inconsistency + with the rest of the app's palette. +- **Alternatives considered**: Defining new `--go-pressed` / `--go-idle` variables was + considered but rejected as unnecessary — it would duplicate values that are functionally + identical to `--forest`/`--forest-dark` and add indirection for no benefit, conflicting + with Constitution Principle V (smallest change that preserves existing conventions). + +## Decision: Implement press feedback via CSS `:active` pseudo-class, not component state + +- **Decision**: Add an `:active` (and `:active:disabled` guard) rule to the existing + `.range-go` selector rather than introducing C#/Razor state (e.g. a `isPressed` field + toggled by `@onmousedown`/`@onmouseup`). +- **Rationale**: The browser's native `:active` pseudo-class already fires for mouse, + touch, and keyboard (Space/Enter) activation on a `