-
Notifications
You must be signed in to change notification settings - Fork 0
002 shared UI primitives #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b8107bf
314c6a6
d33eae6
ece9b42
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,3 @@ | ||
| { | ||
| "feature_directory": "specs/001-auth-setup" | ||
| "feature_directory": "specs/002-shared-ui-primitives" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| <!-- SPECKIT START --> | ||
| For additional context about technologies to be used, project structure, | ||
| shell commands, and other important information, read the current plan at | ||
| `specs/001-auth-setup/plan.md`. | ||
| `specs/002-shared-ui-primitives/plan.md`. | ||
| <!-- SPECKIT END --> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| # Specification Quality Checklist: Shared UI Primitives | ||
|
|
||
| **Purpose**: Validate specification completeness and quality before proceeding to planning | ||
| **Created**: 2026-04-30 | ||
| **Feature**: [spec.md](../spec.md) | ||
|
|
||
| ## Content Quality | ||
|
|
||
| - [x] No implementation details (languages, frameworks, APIs) | ||
| _Note: Angular signals/OnPush/ControlValueAccessor references are unavoidable — they ARE | ||
| the deliverable for a component library targeting Angular projects._ | ||
| - [x] Focused on user value and business needs | ||
| _Framed around developer workflows: "developer can use without additional config"._ | ||
| - [x] Written for non-technical stakeholders | ||
| _Caveated: developer is the primary stakeholder; component API references are appropriate._ | ||
| - [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) | ||
| _Note: SC-004 references `ng build` — justified exception for a component library spec._ | ||
| - [x] All acceptance scenarios are defined | ||
| - [x] Edge cases are identified (dual disabled+loading, empty errorMessage, self-contained spinner ARIA) | ||
| - [x] Scope is clearly bounded (3 primitives, single-line only, no icons, dark-theme only) | ||
| - [x] Dependencies and assumptions identified | ||
|
|
||
| ## Feature Readiness | ||
|
|
||
| - [x] All functional requirements have clear acceptance criteria | ||
| - [x] User scenarios cover primary flows (Button P1, TextInput P2, Spinner P3) | ||
| - [x] Feature meets measurable outcomes defined in Success Criteria | ||
| - [x] No implementation details leak into specification | ||
| _Caveated: see Content Quality note above._ | ||
|
|
||
| ## Notes | ||
|
|
||
| - All items pass. No spec updates required before `/speckit-clarify` or `/speckit-plan`. | ||
| - The intentional use of Angular/component terminology is consistent with the feature's nature as | ||
| a developer-facing component library. Justified deviation from the "technology-agnostic" guideline. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| # Contract: ButtonComponent | ||
|
|
||
| **Selector**: `app-button` | ||
| **File**: `src/app/shared/ui/button/button.component.ts` | ||
| **Barrel**: `src/app/shared/ui/index.ts` | ||
|
|
||
| --- | ||
|
|
||
| ## Usage | ||
|
|
||
| ```html | ||
| <!-- Primary action (default) --> | ||
| <app-button (click)="submit()">Sign In</app-button> | ||
|
|
||
| <!-- Form submit button (triggers form's ngSubmit on Enter) --> | ||
| <app-button type="submit" [loading]="isSubmitting()">Sign In</app-button> | ||
|
|
||
| <!-- Secondary action --> | ||
| <app-button variant="secondary" (click)="cancel()">Cancel</app-button> | ||
|
|
||
| <!-- Disabled state --> | ||
| <app-button [disabled]="!form.valid">Sign In</app-button> | ||
|
|
||
| <!-- Loading state (during async operation) --> | ||
| <app-button [loading]="isLoading()">Sign In</app-button> | ||
| ``` | ||
|
|
||
| ## Input API | ||
|
|
||
| | Input | Type | Default | Description | | ||
| |-------|------|---------|-------------| | ||
| | `variant` | `'primary' \| 'secondary'` | `'primary'` | Visual style. Primary uses `$color-accent` background; secondary uses transparent background with `$color-border` border. | | ||
| | `type` | `'button' \| 'submit'` | `'button'` | HTML `type` attribute. Use `'submit'` to trigger `(ngSubmit)` on the parent form when the button is clicked or Enter is pressed. | | ||
| | `loading` | `boolean` | `false` | When `true`: renders an internal Spinner at `size="sm"`, sets native `disabled` on the `<button>`, and sets `aria-busy="true"`. Click events are suppressed. | | ||
| | `disabled` | `boolean` | `false` | When `true`: sets native `disabled` on the `<button>`. Click events are suppressed. No spinner is shown. | | ||
|
|
||
| ## Output API | ||
|
|
||
| None. Consumers bind `(click)` on the host element directly. | ||
|
|
||
| ## Slots (Content Projection) | ||
|
|
||
| | Slot | Description | | ||
| |------|-------------| | ||
| | Default `<ng-content>` | Button label text. May be plain text or inline elements. MUST contain visible text for accessibility. | | ||
|
|
||
| ## CSS Classes Applied to Internal `<button>` Element | ||
|
|
||
| | Class | Condition | | ||
| |-------|-----------| | ||
| | `btn` | Always | | ||
| | `btn--primary` | When `variant() === 'primary'` | | ||
| | `btn--secondary` | When `variant() === 'secondary'` | | ||
| | `btn--loading` | When `loading() === true` | | ||
| | `btn--disabled` | When `isDisabled() === true` | | ||
|
|
||
| ## Accessibility Contract | ||
|
|
||
| | Attribute | Value | Condition | | ||
| |-----------|-------|-----------| | ||
| | `type` | `"button"` or `"submit"` | Always (from `type` input) | | ||
| | `disabled` | present | When `loading() || disabled()` | | ||
| | `aria-busy` | `"true"` | When `loading() === true` | | ||
| | Focus ring | visible, ≥ 3:1 contrast | When focused via keyboard | | ||
| | Internal Spinner | `aria-hidden="true"` | When `loading() === true` — the button's `aria-busy` already announces loading state | | ||
|
|
||
| ## Token Usage | ||
|
|
||
| | Token | Applied To | | ||
| |-------|-----------| | ||
| | `$color-accent` | Primary button background | | ||
| | `$color-bg-surface` | Secondary button background | | ||
| | `$color-text-primary` | Button text color | | ||
| | `$color-border` | Secondary button border | | ||
| | `$border-radius-md` | Button border radius | | ||
| | `$spacing-3` (`12px`) | Vertical padding | | ||
| | `$spacing-4` (`16px`) | Horizontal padding | | ||
| | `$font-size-md` | Button label font size | | ||
| | `$font-weight-medium` | Button label font weight | | ||
|
|
||
| ## Behavior Contracts | ||
|
|
||
| ### Loading state | ||
| 1. When `[loading]="true"` is set, the `<button>` receives `disabled` attribute and `aria-busy="true"`. | ||
| 2. A `<app-spinner size="sm" aria-hidden="true">` appears inside the button, before the projected content. | ||
| 3. Clicking the button while loading has no effect (native `disabled` blocks events). | ||
| 4. When `[loading]` returns to `false`, `disabled` and `aria-busy` are removed; the Spinner disappears. | ||
|
|
||
| ### Disabled vs Loading precedence | ||
| - If both `[disabled]="true"` AND `[loading]="true"`, the button is `disabled`; the Spinner is shown (loading visual takes precedence). | ||
|
|
||
| ### Type="submit" behavior | ||
| - When `type="submit"`, clicking the button (or pressing Enter while any form control inside the `<form>` is focused) submits the form and triggers the form's `(ngSubmit)` handler. | ||
| - `type="submit"` combined with `[loading]="true"` is safe: the `disabled` attribute prevents form re-submission while the operation is in flight. | ||
|
|
||
| ## Import | ||
|
|
||
| ```typescript | ||
| import { ButtonComponent } from '@app/shared/ui'; | ||
| // or | ||
| import { ButtonComponent } from '../../../shared/ui'; | ||
| ``` | ||
|
|
||
| ## File Structure | ||
|
|
||
| ``` | ||
| src/app/shared/ui/button/ | ||
| ├── button.component.ts # Component class + template (inline) | ||
| └── button.component.scss # Component styles | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,98 @@ | ||||||||||||||||||||||||||||||||||||||
| # Contract: SpinnerComponent | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| **Selector**: `app-spinner` | ||||||||||||||||||||||||||||||||||||||
| **File**: `src/app/shared/ui/spinner/spinner.component.ts` | ||||||||||||||||||||||||||||||||||||||
| **Barrel**: `src/app/shared/ui/index.ts` | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| --- | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ## Usage | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ```html | ||||||||||||||||||||||||||||||||||||||
| <!-- Default (standalone page-level loading) --> | ||||||||||||||||||||||||||||||||||||||
| <app-spinner /> | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| <!-- Custom accessible label --> | ||||||||||||||||||||||||||||||||||||||
| <app-spinner label="Loading chat history…" /> | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| <!-- Small (used inside Button — consumers rarely set this directly) --> | ||||||||||||||||||||||||||||||||||||||
| <app-spinner size="sm" /> | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| <!-- Conditionally shown while data loads --> | ||||||||||||||||||||||||||||||||||||||
| @if (isLoading()) { | ||||||||||||||||||||||||||||||||||||||
| <app-spinner label="Loading messages…" /> | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ## Input API | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| | Input | Type | Default | Description | | ||||||||||||||||||||||||||||||||||||||
| |-------|------|---------|-------------| | ||||||||||||||||||||||||||||||||||||||
| | `label` | `string` | `'Loading…'` | Accessible label text set on `aria-label` of the `role="status"` element. Screen readers announce this text when the Spinner is inserted into the DOM (via `aria-live="polite"` implied by `role="status"`). | | ||||||||||||||||||||||||||||||||||||||
| | `size` | `'sm' \| 'md'` | `'md'` | Visual size. `'sm'` = 16×16 px (used internally by Button). `'md'` = 24×24 px (standalone). | | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ## Output API | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| None. Spinner is stateless; it emits no events. | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ## Visual Specification | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| | Size | Width | Height | Ring border-width | Animation | | ||||||||||||||||||||||||||||||||||||||
| |------|-------|--------|------------------|-----------| | ||||||||||||||||||||||||||||||||||||||
| | `sm` | 16px | 16px | 2px | 0.75s linear spin | | ||||||||||||||||||||||||||||||||||||||
| | `md` | 24px | 24px | 2px | 0.75s linear spin | | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Ring colors: | ||||||||||||||||||||||||||||||||||||||
| - Track (rest of ring): `$color-bg-elevated` | ||||||||||||||||||||||||||||||||||||||
| - Active arc (top segment): `$color-accent` | ||||||||||||||||||||||||||||||||||||||
| - Animation: `transform: rotate(0 → 360deg)`, GPU-composited, no layout/paint | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ## Accessibility Contract | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| | Element | Role / Attribute | Value | | ||||||||||||||||||||||||||||||||||||||
| |---------|-----------------|-------| | ||||||||||||||||||||||||||||||||||||||
| | Outer `<span>` | `role="status"` | Implies `aria-live="polite"` — announces when inserted | | ||||||||||||||||||||||||||||||||||||||
| | Outer `<span>` | `aria-label` | Value of `label()` input | | ||||||||||||||||||||||||||||||||||||||
| | Inner `<span>` (ring) | `aria-hidden` | `"true"` — purely decorative | | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| **When conditionally rendered** (`@if`): As soon as the `<app-spinner>` is inserted into the | ||||||||||||||||||||||||||||||||||||||
| DOM, its `role="status"` causes screen readers to queue an announcement of the `aria-label` | ||||||||||||||||||||||||||||||||||||||
| value after the current utterance completes. | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| **When used inside Button**: Button sets `aria-hidden="true"` on the `<app-spinner>` host | ||||||||||||||||||||||||||||||||||||||
| element so the Spinner does not announce. The `<button aria-busy="true">` handles the | ||||||||||||||||||||||||||||||||||||||
| loading announcement instead. | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ## Token Usage | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| | Token | Applied To | | ||||||||||||||||||||||||||||||||||||||
| |-------|-----------| | ||||||||||||||||||||||||||||||||||||||
| | `$color-bg-elevated` | Spinner ring track (inactive arc) | | ||||||||||||||||||||||||||||||||||||||
| | `$color-accent` | Spinner ring active arc (top) | | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Spinner uses no spacing, typography, or border-radius tokens (the ring shape is defined by | ||||||||||||||||||||||||||||||||||||||
| the circular `border-radius: 50%` and fixed pixel dimensions). | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ## File Structure | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||
| src/app/shared/ui/spinner/ | ||||||||||||||||||||||||||||||||||||||
| ├── spinner.component.ts # Component class (inline template) | ||||||||||||||||||||||||||||||||||||||
| └── spinner.component.scss # Component styles | ||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Spinner uses an inline template because the template is minimal (2 elements). | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ## Composition Note | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| `ButtonComponent` imports `SpinnerComponent` and renders it internally: | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| ```html | ||||||||||||||||||||||||||||||||||||||
| <!-- Inside button.component.html --> | ||||||||||||||||||||||||||||||||||||||
| @if (loading()) { | ||||||||||||||||||||||||||||||||||||||
| <app-spinner size="sm" label="Loading…" aria-hidden="true" /> | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+78
to
+95
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tag the fenced examples with a language. Both fenced blocks are missing a language identifier, which will keep markdownlint failing here. Use Suggested fix-```
+```text
src/app/shared/ui/spinner/
├── spinner.component.ts # Component class (inline template)
└── spinner.component.scss # Component styles
-```
+```
-```
+```text
<!-- Inside button.component.html -->
`@if` (loading()) {
<app-spinner size="sm" label="Loading…" aria-hidden="true" />
}
-```
+```📝 Committable suggestion
Suggested change
🧰 Tools🪛 markdownlint-cli2 (0.22.1)[warning] 78-78: Fenced code blocks should have a language specified (MD040, fenced-code-language) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| This is the only internal composition relationship among the three primitives. | ||||||||||||||||||||||||||||||||||||||
| `SpinnerComponent` MUST be implemented before `ButtonComponent`. | ||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| # Contract: TextInputComponent | ||
|
|
||
| **Selector**: `app-text-input` | ||
| **File**: `src/app/shared/ui/text-input/text-input.component.ts` | ||
| **Barrel**: `src/app/shared/ui/index.ts` | ||
| **Implements**: `ControlValueAccessor` | ||
|
|
||
| --- | ||
|
|
||
| ## Usage | ||
|
|
||
| ```typescript | ||
| // In the consuming component: | ||
| readonly emailCtrl = new FormControl('', [Validators.required, Validators.email]); | ||
| readonly passwordCtrl = new FormControl('', [Validators.required, Validators.minLength(8)]); | ||
| ``` | ||
|
|
||
| ```html | ||
| <!-- Email field --> | ||
| <app-text-input | ||
| label="Email" | ||
| type="email" | ||
| placeholder="you@example.com" | ||
| [formControl]="emailCtrl" | ||
| [errorMessage]="emailCtrl.errors?.['required'] ? 'Email is required' : ''" | ||
| /> | ||
|
|
||
| <!-- Password field with show/hide toggle --> | ||
| <app-text-input | ||
| label="Password" | ||
| type="password" | ||
| [formControl]="passwordCtrl" | ||
| [errorMessage]="passwordCtrl.touched && passwordCtrl.invalid ? 'Password must be 8+ characters' : ''" | ||
| /> | ||
|
|
||
| <!-- Plain text without form control (uncontrolled) --> | ||
| <app-text-input label="Search" placeholder="Type to search…" /> | ||
| ``` | ||
|
|
||
| ## Input API | ||
|
|
||
| | Input | Type | Default | Required | Description | | ||
| |-------|------|---------|----------|-------------| | ||
| | `label` | `string` | — | **Yes** | Visible label text above the input. Also used as the programmatic accessible name via the associated `<label>` element. | | ||
| | `type` | `string` | `'text'` | No | HTML input type. Supports all native values (`'text'`, `'email'`, `'password'`, `'search'`, etc.). When `'password'`, a show/hide toggle button appears. | | ||
| | `errorMessage` | `string` | `''` | No | Error message displayed below the input. When non-empty: error text renders, `aria-invalid="true"` is set on the `<input>`, and `aria-describedby` links to the error element. When empty or `undefined`: no error container is rendered (prevents CLS). | | ||
| | `placeholder` | `string` | `''` | No | Native `placeholder` attribute forwarded to the `<input>` element. | | ||
|
|
||
| ## Reactive Forms Integration | ||
|
|
||
| `TextInputComponent` implements Angular's `ControlValueAccessor`. It is usable with | ||
| any directive that provides `NG_VALUE_ACCESSOR`: | ||
|
|
||
| - `[formControl]="ctrl"` — bind to a standalone `FormControl` | ||
| - `[formControlName]="'fieldName'"` — bind inside a `[formGroup]` | ||
| - `[(ngModel)]="value"` — bind via template-driven forms (not recommended per constitution) | ||
|
|
||
| **No manual wiring required.** The component registers itself via `NG_VALUE_ACCESSOR` in its | ||
| `providers` array. | ||
|
|
||
| ## Output API | ||
|
|
||
| None. The component pushes value changes to the bound `FormControl` via CVA callbacks. | ||
|
|
||
| ## Accessibility Contract | ||
|
|
||
| | Attribute / Element | Value | Condition | | ||
| |--------------------|-------|-----------| | ||
| | `<label for="inputId">` | `label()` | Always | | ||
| | `<input id="inputId">` | — | Always — stable unique id per instance | | ||
| | `aria-invalid` on `<input>` | `"true"` | When `errorMessage()` is non-empty | | ||
| | `aria-describedby` on `<input>` | `"${inputId}-error"` | When `errorMessage()` is non-empty | | ||
| | `<span id="${inputId}-error" role="alert">` | `errorMessage()` | When `errorMessage()` is non-empty | | ||
| | Toggle button `aria-label` | `"Show password"` / `"Hide password"` | When `type === 'password'` | | ||
| | Toggle button `type` | `"button"` | Always (prevents form submission) | | ||
| | Focus visible | ring, ≥ 3:1 contrast | When `<input>` is focused via keyboard | | ||
|
|
||
| ## Password Toggle Behavior | ||
|
|
||
| When `type="password"`: | ||
| 1. A toggle button appears at the right edge of the input wrapper. | ||
| 2. The toggle button has `type="button"` (never submits the form). | ||
| 3. `aria-label` alternates between `"Show password"` and `"Hide password"` based on | ||
| current toggle state. | ||
| 4. Activating the toggle switches the `<input type>` between `"password"` and `"text"`. | ||
| 5. The input value is preserved across toggles (no value reset). | ||
|
|
||
| ## Token Usage | ||
|
|
||
| | Token | Applied To | | ||
| |-------|-----------| | ||
| | `$color-text-primary` | Label text, input text | | ||
| | `$color-text-secondary` | Placeholder text | | ||
| | `$color-text-disabled` | Disabled input text | | ||
| | `$color-bg-surface` | Input background | | ||
| | `$color-border` | Input border (default state) | | ||
| | `$color-accent` | Input border (focus state) | | ||
| | `$border-radius-md` | Input border radius | | ||
| | `$spacing-2` (`8px`) | Input vertical padding | | ||
| | `$spacing-3` (`12px`) | Input horizontal padding | | ||
| | `$spacing-1` (`4px`) | Gap between label and input, error and input | | ||
| | `$font-size-sm` | Error message font size | | ||
| | `$font-size-md` | Input and label font size | | ||
|
|
||
| ## Error rendering rule | ||
|
|
||
| ``` | ||
| errorMessage() === '' or undefined → @if block not rendered (zero height, zero CLS) | ||
| errorMessage() !== '' → <span role="alert"> renders with error text | ||
| ``` | ||
|
|
||
| The error span has `role="alert"` so that when it is inserted into the DOM (which happens | ||
| when `errorMessage` changes from empty to non-empty), screen readers announce it immediately | ||
| (`aria-live="assertive"` implied by `role="alert"`). | ||
|
|
||
| ## File Structure | ||
|
|
||
| ``` | ||
| src/app/shared/ui/text-input/ | ||
| ├── text-input.component.ts # Component class + template (external) | ||
| ├── text-input.component.html # External template | ||
| └── text-input.component.scss # Component styles | ||
| ``` | ||
|
Comment on lines
+107
to
+123
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tag the fenced examples with a language. These blocks will keep tripping the markdownlint fenced-code-language rule. Add a language identifier such as Suggested fix-```
+```text
errorMessage() === '' or undefined → `@if` block not rendered (zero height, zero CLS)
errorMessage() !== '' → <span role="alert"> renders with error text
-```
+```
-```
+```text
src/app/shared/ui/text-input/
├── text-input.component.ts # Component class + template (external)
├── text-input.component.html # External template
└── text-input.component.scss # Component styles
-```
+```🧰 Tools🪛 markdownlint-cli2 (0.22.1)[warning] 107-107: Fenced code blocks should have a language specified (MD040, fenced-code-language) [warning] 118-118: Fenced code blocks should have a language specified (MD040, fenced-code-language) 🤖 Prompt for AI Agents |
||
|
|
||
| Note: TextInput uses an external template (not inline) because the template is non-trivial. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rewrite the
disabledcontract row so the Markdown table stays valid.The
||inside the code span is being parsed as extra table separators, which is why markdownlint reports the row as having too many columns. Rephrase it to avoid pipe characters in the cell.📝 Proposed wording fix
📝 Committable suggestion
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 62-62: Table column count
Expected: 3; Actual: 5; Too many cells, extra data will be missing
(MD056, table-column-count)
🤖 Prompt for AI Agents