mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-06-14 04:01:30 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 46e2d58795 |
@@ -20,15 +20,54 @@
|
|||||||
* Each row therefore represents the cumulative session total up to that point.
|
* Each row therefore represents the cumulative session total up to that point.
|
||||||
* To get per-session cost, take the last row per session_id. To get per-day
|
* To get per-session cost, take the last row per session_id. To get per-day
|
||||||
* spend, aggregate.
|
* spend, aggregate.
|
||||||
|
*
|
||||||
|
* Harness-cost contract (optional, opt-in by the statusline):
|
||||||
|
* If the user's statusline (which receives `cost.total_cost_usd` directly
|
||||||
|
* from Claude Code) writes `{ts, cost_usd}` to
|
||||||
|
* `<os.tmpdir()>/harness-cost-<session_id>.json` on each render, this hook
|
||||||
|
* prefers that authoritative value over the transcript-sum estimate when
|
||||||
|
* the cache is fresh (≤ 300s). The transcript-sum is kept as a safe
|
||||||
|
* fallback because:
|
||||||
|
* - the hard-coded rate table cannot represent Opus 4.7's >200K-token
|
||||||
|
* 2x tier or the 1h-cache 2x tier (under-counts on long sessions);
|
||||||
|
* - summing the full transcript double-counts work done across
|
||||||
|
* `--resume` boundaries while `cost.total_cost_usd` is per-process.
|
||||||
|
* Absent a writer, behavior is unchanged.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
const os = require('os');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { ensureDir, appendFile, getClaudeDir } = require('../lib/utils');
|
const { ensureDir, appendFile, getClaudeDir } = require('../lib/utils');
|
||||||
const { sanitizeSessionId } = require('../lib/session-bridge');
|
const { sanitizeSessionId } = require('../lib/session-bridge');
|
||||||
|
|
||||||
|
const HARNESS_COST_MAX_AGE_SECONDS = 300;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read authoritative harness cost from the per-session cache file.
|
||||||
|
* @param {string} sessionId
|
||||||
|
* @param {number} maxAgeSeconds
|
||||||
|
* @returns {number|null} cost in USD, or null on miss / stale / parse error
|
||||||
|
*/
|
||||||
|
function readHarnessCost(sessionId, maxAgeSeconds) {
|
||||||
|
if (!sessionId) return null;
|
||||||
|
try {
|
||||||
|
const fp = path.join(os.tmpdir(), `harness-cost-${sessionId}.json`);
|
||||||
|
if (!fs.existsSync(fp)) return null;
|
||||||
|
const obj = JSON.parse(fs.readFileSync(fp, 'utf8'));
|
||||||
|
const ts = Number(obj && obj.ts);
|
||||||
|
const cost = Number(obj && obj.cost_usd);
|
||||||
|
if (!Number.isFinite(ts) || !Number.isFinite(cost) || cost < 0) return null;
|
||||||
|
const age = Math.floor(Date.now() / 1000) - ts;
|
||||||
|
if (age < 0 || age > maxAgeSeconds) return null;
|
||||||
|
return cost;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Approximate per-1M-token billing rates (USD).
|
// Approximate per-1M-token billing rates (USD).
|
||||||
// Cache creation: 1.25x input rate. Cache read: 0.1x input rate.
|
// Cache creation: 1.25x input rate. Cache read: 0.1x input rate.
|
||||||
const RATE_TABLE = {
|
const RATE_TABLE = {
|
||||||
@@ -125,13 +164,23 @@ process.stdin.on('end', () => {
|
|||||||
} = usageTotals || {};
|
} = usageTotals || {};
|
||||||
|
|
||||||
const rates = getRates(model);
|
const rates = getRates(model);
|
||||||
const estimatedCostUsd = Math.round((
|
const transcriptCostUsd = Math.round((
|
||||||
(inputTokens / 1e6) * rates.in +
|
(inputTokens / 1e6) * rates.in +
|
||||||
(outputTokens / 1e6) * rates.out +
|
(outputTokens / 1e6) * rates.out +
|
||||||
(cacheWriteTokens / 1e6) * rates.cacheWrite +
|
(cacheWriteTokens / 1e6) * rates.cacheWrite +
|
||||||
(cacheReadTokens / 1e6) * rates.cacheRead
|
(cacheReadTokens / 1e6) * rates.cacheRead
|
||||||
) * 1e6) / 1e6;
|
) * 1e6) / 1e6;
|
||||||
|
|
||||||
|
// Prefer the harness's authoritative `cost.total_cost_usd` when the
|
||||||
|
// statusline has written it to the per-session cache (see contract in
|
||||||
|
// the file header). The harness number reflects API-billed truth
|
||||||
|
// (correct rates, 1h-cache 2x, >200K tier 2x) and is per-process so it
|
||||||
|
// does not drift across `--resume`. Cache miss → transcript-sum.
|
||||||
|
const harnessCost = readHarnessCost(sessionId, HARNESS_COST_MAX_AGE_SECONDS);
|
||||||
|
const estimatedCostUsd = harnessCost !== null
|
||||||
|
? Math.round(harnessCost * 1e6) / 1e6
|
||||||
|
: transcriptCostUsd;
|
||||||
|
|
||||||
const metricsDir = path.join(getClaudeDir(), 'metrics');
|
const metricsDir = path.join(getClaudeDir(), 'metrics');
|
||||||
ensureDir(metricsDir);
|
ensureDir(metricsDir);
|
||||||
|
|
||||||
|
|||||||
@@ -1,446 +0,0 @@
|
|||||||
---
|
|
||||||
name: frontend-a11y
|
|
||||||
description: >
|
|
||||||
Accessibility patterns for React and Next.js — semantic HTML, ARIA attributes,
|
|
||||||
form labeling, keyboard navigation, focus management, and screen reader support.
|
|
||||||
Use when building any interactive UI component or form.
|
|
||||||
origin: community
|
|
||||||
---
|
|
||||||
|
|
||||||
# Frontend Accessibility Patterns
|
|
||||||
|
|
||||||
Practical accessibility patterns for React and Next.js. Covers the issues most commonly flagged in code review: missing form labels, incorrect ARIA usage, non-semantic interactive elements, and broken keyboard navigation.
|
|
||||||
|
|
||||||
## When to Activate
|
|
||||||
|
|
||||||
- Building or reviewing form components (`<input>`, `<select>`, `<textarea>`)
|
|
||||||
- Creating interactive elements (modals, dropdowns, tooltips, tabs)
|
|
||||||
- Using `<div>` or `<span>` with `onClick`
|
|
||||||
- Adding `aria-*` attributes to any element
|
|
||||||
- Implementing keyboard navigation or focus management
|
|
||||||
- Receiving accessibility feedback from code review tools (CodeRabbit, ESLint a11y)
|
|
||||||
- Building components that must support screen readers
|
|
||||||
|
|
||||||
## Form Accessibility
|
|
||||||
|
|
||||||
Missing `htmlFor` / `id` pairing and disconnected error messages are the most common issues flagged in code review.
|
|
||||||
|
|
||||||
### Label Connection
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// BAD: label has no connection to input — screen readers cannot associate them
|
|
||||||
<label>Email</label>
|
|
||||||
<input type="email" />
|
|
||||||
|
|
||||||
// GOOD: htmlFor matches input id
|
|
||||||
<label htmlFor="email">Email</label>
|
|
||||||
<input id="email" type="email" />
|
|
||||||
```
|
|
||||||
|
|
||||||
### Required Fields
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// BAD: visual-only asterisk conveys nothing to screen readers
|
|
||||||
<label htmlFor="email">Email *</label>
|
|
||||||
<input id="email" type="email" />
|
|
||||||
|
|
||||||
// GOOD: required enables native browser validation; aria-required signals it to screen readers
|
|
||||||
<label htmlFor="email">
|
|
||||||
Email <span aria-hidden="true">*</span>
|
|
||||||
</label>
|
|
||||||
<input id="email" type="email" required aria-required="true" />
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error Messages
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// BAD: error text exists visually but is not linked to the input
|
|
||||||
<input id="email" type="email" />
|
|
||||||
<span className="error">Invalid email address</span>
|
|
||||||
|
|
||||||
// GOOD: aria-describedby connects input to its error message
|
|
||||||
// aria-invalid signals the invalid state to screen readers
|
|
||||||
<input
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
aria-describedby="email-error"
|
|
||||||
aria-invalid={!!error}
|
|
||||||
/>
|
|
||||||
{error && (
|
|
||||||
<span id="email-error" role="alert">
|
|
||||||
{error}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Complete Accessible Form
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
interface LoginFormProps {
|
|
||||||
onSubmit: (email: string, password: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function LoginForm({ onSubmit }: LoginFormProps) {
|
|
||||||
const [email, setEmail] = useState('');
|
|
||||||
const [password, setPassword] = useState('');
|
|
||||||
const [errors, setErrors] = useState<{ email?: string; password?: string }>({});
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const newErrors: typeof errors = {};
|
|
||||||
if (!email) newErrors.email = 'Email is required';
|
|
||||||
if (!password) newErrors.password = 'Password is required';
|
|
||||||
if (Object.keys(newErrors).length) {
|
|
||||||
setErrors(newErrors);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onSubmit(email, password);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit} noValidate>
|
|
||||||
<div>
|
|
||||||
<label htmlFor="email">
|
|
||||||
Email <span aria-hidden="true">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
value={email}
|
|
||||||
onChange={e => setEmail(e.target.value)}
|
|
||||||
aria-required="true"
|
|
||||||
aria-describedby={errors.email ? 'email-error' : undefined}
|
|
||||||
aria-invalid={!!errors.email}
|
|
||||||
autoComplete="email"
|
|
||||||
/>
|
|
||||||
{errors.email && (
|
|
||||||
<span id="email-error" role="alert">
|
|
||||||
{errors.email}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="password">
|
|
||||||
Password <span aria-hidden="true">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="password"
|
|
||||||
type="password"
|
|
||||||
value={password}
|
|
||||||
onChange={e => setPassword(e.target.value)}
|
|
||||||
aria-required="true"
|
|
||||||
aria-describedby={errors.password ? 'password-error' : undefined}
|
|
||||||
aria-invalid={!!errors.password}
|
|
||||||
autoComplete="current-password"
|
|
||||||
/>
|
|
||||||
{errors.password && (
|
|
||||||
<span id="password-error" role="alert">
|
|
||||||
{errors.password}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit">Log in</button>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Semantic HTML
|
|
||||||
|
|
||||||
Use the element that matches the intent. Screen readers and keyboard users depend on native semantics.
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// BAD: div has no role, no keyboard support, no accessible name
|
|
||||||
<div onClick={handleClick}>Submit</div>
|
|
||||||
|
|
||||||
// GOOD: button is focusable, activates on Enter/Space, announces as "button"
|
|
||||||
<button type="button" onClick={handleClick}>Submit</button>
|
|
||||||
```
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// BAD: non-semantic navigation
|
|
||||||
<div onClick={() => navigate('/home')}>Home</div>
|
|
||||||
|
|
||||||
// GOOD: anchor supports right-click, middle-click, and keyboard navigation
|
|
||||||
<a href="/home">Home</a>
|
|
||||||
```
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// BAD: heading hierarchy skipped (h1 to h4)
|
|
||||||
<h1>Dashboard</h1>
|
|
||||||
<h4>Recent Activity</h4>
|
|
||||||
|
|
||||||
// GOOD: sequential heading levels
|
|
||||||
<h1>Dashboard</h1>
|
|
||||||
<h2>Recent Activity</h2>
|
|
||||||
```
|
|
||||||
|
|
||||||
## ARIA Attributes
|
|
||||||
|
|
||||||
Use ARIA only when native HTML semantics are insufficient. Wrong ARIA is worse than no ARIA.
|
|
||||||
|
|
||||||
### aria-label vs aria-labelledby
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// aria-label: inline string label — use when no visible label text exists
|
|
||||||
<button aria-label="Close modal">
|
|
||||||
<XIcon />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
// aria-labelledby: references another element's text — use when a visible label exists
|
|
||||||
<section aria-labelledby="section-title">
|
|
||||||
<h2 id="section-title">Recent Orders</h2>
|
|
||||||
{/* content */}
|
|
||||||
</section>
|
|
||||||
```
|
|
||||||
|
|
||||||
### aria-describedby
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// Provides supplementary description beyond the label
|
|
||||||
<button
|
|
||||||
aria-describedby="delete-warning"
|
|
||||||
onClick={handleDelete}
|
|
||||||
>
|
|
||||||
Delete account
|
|
||||||
</button>
|
|
||||||
<p id="delete-warning">This action cannot be undone.</p>
|
|
||||||
```
|
|
||||||
|
|
||||||
### aria-live for Dynamic Content
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// Use aria-live to announce content that updates without a page reload
|
|
||||||
// polite: waits for user to finish current action before announcing
|
|
||||||
// assertive: interrupts immediately — use only for urgent errors
|
|
||||||
|
|
||||||
export function StatusMessage({ message, isError }: { message: string; isError?: boolean }) {
|
|
||||||
return (
|
|
||||||
<div role="status" aria-live={isError ? 'assertive' : 'polite'} aria-atomic="true">
|
|
||||||
{message}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### aria-expanded and aria-controls
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
export function Accordion({ title, children }: { title: string; children: React.ReactNode }) {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const contentId = useId();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<button aria-expanded={isOpen} aria-controls={contentId} onClick={() => setIsOpen(prev => !prev)}>
|
|
||||||
{title}
|
|
||||||
</button>
|
|
||||||
<div id={contentId} hidden={!isOpen}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Keyboard Navigation
|
|
||||||
|
|
||||||
Every interactive element must be reachable and operable by keyboard alone.
|
|
||||||
|
|
||||||
### Custom Dropdown
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
export function Dropdown({ options, onSelect }: { options: string[]; onSelect: (value: string) => void }) {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const [activeIndex, setActiveIndex] = useState(0);
|
|
||||||
const listId = useId();
|
|
||||||
|
|
||||||
if (!options.length) return null;
|
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
||||||
switch (e.key) {
|
|
||||||
case 'ArrowDown':
|
|
||||||
e.preventDefault();
|
|
||||||
setActiveIndex(i => Math.min(i + 1, options.length - 1));
|
|
||||||
break;
|
|
||||||
case 'ArrowUp':
|
|
||||||
e.preventDefault();
|
|
||||||
setActiveIndex(i => Math.max(i - 1, 0));
|
|
||||||
break;
|
|
||||||
case 'Enter':
|
|
||||||
case ' ':
|
|
||||||
e.preventDefault();
|
|
||||||
if (isOpen) onSelect(options[activeIndex]);
|
|
||||||
setIsOpen(prev => !prev);
|
|
||||||
break;
|
|
||||||
case 'Escape':
|
|
||||||
setIsOpen(false);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
role="combobox"
|
|
||||||
aria-expanded={isOpen}
|
|
||||||
aria-haspopup="listbox"
|
|
||||||
aria-controls={listId}
|
|
||||||
tabIndex={0}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
onClick={() => setIsOpen(prev => !prev)}
|
|
||||||
>
|
|
||||||
<span>{options[activeIndex]}</span>
|
|
||||||
{isOpen && (
|
|
||||||
<ul id={listId} role="listbox">
|
|
||||||
{options.map((option, index) => (
|
|
||||||
<li
|
|
||||||
key={option}
|
|
||||||
role="option"
|
|
||||||
aria-selected={index === activeIndex}
|
|
||||||
onClick={() => {
|
|
||||||
onSelect(option);
|
|
||||||
setIsOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{option}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Focus Management
|
|
||||||
|
|
||||||
Focus must move logically when UI state changes — especially for modals and route transitions.
|
|
||||||
|
|
||||||
### Modal Focus Restoration
|
|
||||||
|
|
||||||
> This example covers initial focus and restoration. For a full focus trap (Tab/Shift+Tab cycling within the modal), use a library like [`focus-trap-react`](https://github.com/focus-trap/focus-trap-react) which handles edge cases like dynamic content and nested portals.
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
export function Modal({ isOpen, onClose, title, children }: { isOpen: boolean; onClose: () => void; title: string; children: React.ReactNode }) {
|
|
||||||
const modalRef = useRef<HTMLDivElement>(null);
|
|
||||||
const previousFocusRef = useRef<HTMLElement | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isOpen) {
|
|
||||||
// Save currently focused element and move focus into modal
|
|
||||||
previousFocusRef.current = document.activeElement as HTMLElement;
|
|
||||||
modalRef.current?.focus();
|
|
||||||
} else {
|
|
||||||
// Restore focus to the element that opened the modal
|
|
||||||
previousFocusRef.current?.focus();
|
|
||||||
}
|
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
if (!isOpen) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={modalRef} role="dialog" aria-modal="true" aria-labelledby="modal-title" tabIndex={-1} onKeyDown={e => e.key === 'Escape' && onClose()}>
|
|
||||||
<h2 id="modal-title">{title}</h2>
|
|
||||||
{children}
|
|
||||||
<button onClick={onClose}>Close</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Images and Icons
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// BAD: decorative icon announced as unlabeled image
|
|
||||||
<img src="/icon.svg" />
|
|
||||||
|
|
||||||
// GOOD: decorative image hidden from screen readers
|
|
||||||
<img src="/decoration.png" alt="" aria-hidden="true" />
|
|
||||||
|
|
||||||
// GOOD: meaningful image with descriptive alt text
|
|
||||||
<img src="/chart.png" alt="Monthly revenue increased 23% from January to March" />
|
|
||||||
|
|
||||||
// GOOD: icon button with accessible label
|
|
||||||
<button aria-label="Delete item">
|
|
||||||
<TrashIcon aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Reduced Motion
|
|
||||||
|
|
||||||
Respect users who have requested reduced motion in their OS settings.
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
export function useReducedMotion(): boolean {
|
|
||||||
const [prefersReduced, setPrefersReduced] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
||||||
setPrefersReduced(mq.matches);
|
|
||||||
const handler = (e: MediaQueryListEvent) => setPrefersReduced(e.matches);
|
|
||||||
mq.addEventListener('change', handler);
|
|
||||||
return () => mq.removeEventListener('change', handler);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return prefersReduced;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Usage
|
|
||||||
export function AnimatedCard({ children }: { children: React.ReactNode }) {
|
|
||||||
const reduceMotion = useReducedMotion();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
transition: reduceMotion ? 'none' : 'transform 300ms ease'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Anti-Patterns
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// BAD: onClick on non-interactive element with no keyboard support
|
|
||||||
<div onClick={handleClick}>Click me</div>
|
|
||||||
|
|
||||||
// BAD: aria-label on a div that has no role
|
|
||||||
<div aria-label="Navigation">...</div>
|
|
||||||
|
|
||||||
// BAD: placeholder used as a substitute for label
|
|
||||||
<input placeholder="Enter your email" />
|
|
||||||
|
|
||||||
// BAD: positive tabIndex creates unpredictable tab order
|
|
||||||
<button tabIndex={3}>Submit</button>
|
|
||||||
|
|
||||||
// BAD: aria-hidden on a focusable element — keyboard users get trapped
|
|
||||||
<button aria-hidden="true">Open</button>
|
|
||||||
|
|
||||||
// BAD: role="button" on div without keyboard handler
|
|
||||||
<div role="button" onClick={handleClick}>Submit</div>
|
|
||||||
// Missing: tabIndex={0}, onKeyDown for Enter/Space
|
|
||||||
```
|
|
||||||
|
|
||||||
## Checklist
|
|
||||||
|
|
||||||
Before submitting any interactive component for review:
|
|
||||||
|
|
||||||
- [ ] Every `<input>`, `<select>`, and `<textarea>` has a connected `<label>` via `htmlFor`/`id`
|
|
||||||
- [ ] Error messages are linked with `aria-describedby` and marked `role="alert"`
|
|
||||||
- [ ] No `onClick` on `<div>` or `<span>` without `role`, `tabIndex`, and `onKeyDown`
|
|
||||||
- [ ] Icon-only buttons have `aria-label`
|
|
||||||
- [ ] Decorative images use `alt=""` and `aria-hidden="true"`
|
|
||||||
- [ ] Modals restore focus on close (for full focus trapping with Tab/Shift+Tab cycling, use a library like `focus-trap-react`)
|
|
||||||
- [ ] Dynamic content updates use `aria-live`
|
|
||||||
- [ ] `prefers-reduced-motion` is respected for animations
|
|
||||||
|
|
||||||
## Related Skills
|
|
||||||
|
|
||||||
- `frontend-patterns` — general React component and state patterns
|
|
||||||
- `design-system` — design token and component consistency
|
|
||||||
- `motion-ui` — animation patterns with accessibility considerations
|
|
||||||
@@ -215,6 +215,93 @@ function runTests() {
|
|||||||
fs.rmSync(tmpHome, { recursive: true, force: true });
|
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||||
}) ? passed++ : failed++);
|
}) ? passed++ : failed++);
|
||||||
|
|
||||||
|
// 8. Prefers harness-cost cache value over transcript-sum when fresh
|
||||||
|
(test('prefers fresh harness-cost cache over transcript estimate', () => {
|
||||||
|
const tmpHome = makeTempDir();
|
||||||
|
const sessionId = 'harness-fresh-' + Date.now();
|
||||||
|
const transcriptPath = path.join(tmpHome, 'session.jsonl');
|
||||||
|
writeTranscript(transcriptPath, [
|
||||||
|
{
|
||||||
|
type: 'assistant',
|
||||||
|
message: {
|
||||||
|
model: 'claude-opus-4-20250514',
|
||||||
|
usage: {
|
||||||
|
input_tokens: 10000,
|
||||||
|
output_tokens: 5000,
|
||||||
|
cache_creation_input_tokens: 200000,
|
||||||
|
cache_read_input_tokens: 1000000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const harnessCachePath = path.join(os.tmpdir(), `harness-cost-${sessionId}.json`);
|
||||||
|
const nowEpoch = Math.floor(Date.now() / 1000);
|
||||||
|
fs.writeFileSync(
|
||||||
|
harnessCachePath,
|
||||||
|
JSON.stringify({ ts: nowEpoch, cost_usd: 1.23 }),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = runScript(
|
||||||
|
{ session_id: sessionId, transcript_path: transcriptPath },
|
||||||
|
withTempHome(tmpHome)
|
||||||
|
);
|
||||||
|
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
|
||||||
|
|
||||||
|
const metricsFile = path.join(tmpHome, '.claude', 'metrics', 'costs.jsonl');
|
||||||
|
const row = JSON.parse(fs.readFileSync(metricsFile, 'utf8').trim());
|
||||||
|
assert.strictEqual(row.estimated_cost_usd, 1.23, 'Expected harness cost to win');
|
||||||
|
// Token totals still reflect the transcript scan
|
||||||
|
assert.strictEqual(row.input_tokens, 10000, 'Token totals should still come from transcript');
|
||||||
|
assert.strictEqual(row.output_tokens, 5000, 'Token totals should still come from transcript');
|
||||||
|
} finally {
|
||||||
|
try { fs.unlinkSync(harnessCachePath); } catch { /* best-effort */ }
|
||||||
|
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}) ? passed++ : failed++);
|
||||||
|
|
||||||
|
// 9. Ignores stale harness-cost cache and falls back to transcript estimate
|
||||||
|
(test('ignores stale harness-cost cache (>300s) and uses transcript estimate', () => {
|
||||||
|
const tmpHome = makeTempDir();
|
||||||
|
const sessionId = 'harness-stale-' + Date.now();
|
||||||
|
const transcriptPath = path.join(tmpHome, 'session.jsonl');
|
||||||
|
writeTranscript(transcriptPath, [
|
||||||
|
{
|
||||||
|
type: 'assistant',
|
||||||
|
message: {
|
||||||
|
model: 'claude-sonnet-4-20250514',
|
||||||
|
usage: { input_tokens: 1000, output_tokens: 500 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const harnessCachePath = path.join(os.tmpdir(), `harness-cost-${sessionId}.json`);
|
||||||
|
const staleEpoch = Math.floor(Date.now() / 1000) - 3600;
|
||||||
|
fs.writeFileSync(
|
||||||
|
harnessCachePath,
|
||||||
|
JSON.stringify({ ts: staleEpoch, cost_usd: 999.99 }),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = runScript(
|
||||||
|
{ session_id: sessionId, transcript_path: transcriptPath },
|
||||||
|
withTempHome(tmpHome)
|
||||||
|
);
|
||||||
|
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
|
||||||
|
|
||||||
|
const metricsFile = path.join(tmpHome, '.claude', 'metrics', 'costs.jsonl');
|
||||||
|
const row = JSON.parse(fs.readFileSync(metricsFile, 'utf8').trim());
|
||||||
|
assert.notStrictEqual(row.estimated_cost_usd, 999.99, 'Stale cache must not win');
|
||||||
|
assert.ok(row.estimated_cost_usd > 0, 'Expected fallback transcript estimate to be positive');
|
||||||
|
// Sonnet rates: 1000/1e6*3 + 500/1e6*15 ≈ $0.011 — well below the 999.99 stale value
|
||||||
|
assert.ok(row.estimated_cost_usd < 1, 'Expected small transcript estimate, not the stale 999.99');
|
||||||
|
} finally {
|
||||||
|
try { fs.unlinkSync(harnessCachePath); } catch { /* best-effort */ }
|
||||||
|
fs.rmSync(tmpHome, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}) ? passed++ : failed++);
|
||||||
|
|
||||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||||
process.exit(failed > 0 ? 1 : 0);
|
process.exit(failed > 0 ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user