Closes the migrate-button-input-mobilenav-to-glass-primitive convoy (seeded by PR #127). All 3 residual handrolled var(--glass-surface-*) inline-style usages migrated to either purpose-built utility classes or the <GlassSurface> primitive. CI allowlist reduced from 6 entries to 3 (chrome only). Architect decisions (D1-D3, ratified): D1 — Button.secondary → new .btn-glass-secondary utility class. NOT <GlassSurface>: the primitive sets `background` inline via composedStyle, which CSS :hover rules can't override without !important. The new class composes the same high-tint gradient-border that .glass-panel-strong uses, plus a pure-CSS :hover swap (high → mid fill on the padding-box layer). Identical visual contract; the hover behavior is now driven by CSS, not Tailwind's `hover:bg-[var(...)]` arbitrary class. D2 — Input → new .glass-input utility class. NOT <GlassSurface as="input"> and NOT <GlassSurface as="div"> wrap. Reason: <GlassSurface>'s gradient-border trick requires `border: 1px solid transparent` to expose the border-box layers, which conflicts with <Input>'s conditional error-state `1px solid #dc2626` red border. The new class adopts only the tint + blur layer; the visible 1px border + focus ring stay in JSX (class-controlled, not inline). Same visual contract as before for both normal AND error states. D3 — MobileNavigation → <GlassSurface as="div" tint="mid" blur="mid" rim="subtle" elevation="flat" cornerLights="chrome">. NOT .page-header-glass (the seed's first recommendation): .page-header-glass uses var(--glass-surface-high) (wrong tint — MobileNav uses mid) and sets a bottom-border separator (wrong for a fixed-bottom-nav where the bottom edge is the viewport edge). <GlassSurface> is the better fit AND brings the chrome-tier corner-light bleed that the parent convoy is unifying across all chrome surfaces. Implementation choice — single PR (not 3 parallel briefs): The seed recommended 3 small parallel-safe briefs (one per file). D1 and D2 both need styles/globals.css to gain new utility classes, so those 2 changes can't run truly in parallel without merge conflicts. Single PR is faster, simpler to review end-to-end, and the natural shape for a 2-3 hour convoy with tightly-coupled artifacts. Files changed (4): styles/globals.css (+50 / -1): - Adds .btn-glass-secondary (with :hover variant) — D1. - Adds .glass-input — D2. - Both classes documented inline with architect-decision references. components/ui/Button.js (+2 / -10): - Replaces inline variantStyle + Tailwind hover arbitrary class for `variant === 'secondary'` with `variantClass = 'btn-glass-secondary font-medium'`. variantStyle now `{}`. - Other variants (primary, danger, ghost) UNCHANGED. components/ui/Input.js (+1 / -7): - Adds `glass-input` to the className list. - Removes inline `background` + `backdropFilter` + `WebkitBackdropFilter` from the input's style block. - Conditional `border: inputBorder` stays in JSX (error swap). - All other props/behavior preserved. components/MobileNavigation.js (+11 / -8): - Adds `import { GlassSurface } from './ui'`. - Replaces the inline-styled backdrop <div> with <GlassSurface as="div" ...>. Same className ("absolute inset-0"), same visible behavior, plus the chrome-tier corner-light bleed. - Comment block updated to reference the convoy + decision. .github/workflows/ci.yml (+8 / -22): - forbidden-patterns Check 7/7 GLASS_ALLOWLIST reduced from 6 entries to 3 (chrome only). The TODO comments referencing this convoy are deleted (work is done). .convoys/migrate-button-input-mobilenav-to-glass-primitive.md (+74 / -3): - status: queued → closed, closed: 2026-06-05, prs: [131]. - Architect ratifications D1-D3 written into front-matter docs. - Closeout checklist with all acceptance criteria checked. - Note that parent convoy unify-glass-panel-surfaces is now fully closed — no residual handrolled glass-surface usage outside the 3 chrome blocks. Verification: - POSITIVE TEST: post-migration grep with the reduced 3-entry allowlist returns 0 violations. ✅ - grep on raw files: only Layout.js + TopSearchBar.js still match the literal regex (GlassSurface.js uses template literal which doesn't match — intentional, allowlist is forward-compat). - YAML parses (python3 yaml.safe_load). - npm run lint passes (1 pre-existing unrelated warning). - npm run test:run: 118/118 tests pass. Visual diff to be verified by reviewer in light + dark mode for: - <Button variant="secondary"> default + hover state. - <Input> default + error state. - Mobile bottom-nav backdrop. Co-authored-by: Cursor <cursoragent@cursor.com>
109 lines
2.8 KiB
JavaScript
109 lines
2.8 KiB
JavaScript
import { forwardRef, useId } from 'react';
|
|
|
|
const Input = forwardRef(function Input(
|
|
{
|
|
label,
|
|
helperText,
|
|
error,
|
|
leadingIcon,
|
|
trailingIcon,
|
|
trailingAction,
|
|
id: idProp,
|
|
type = 'text',
|
|
className = '',
|
|
style,
|
|
...rest
|
|
},
|
|
ref
|
|
) {
|
|
const reactId = useId();
|
|
const id = idProp ?? reactId;
|
|
const describedBy = [];
|
|
if (helperText && !error) describedBy.push(`${id}-helper`);
|
|
if (error) describedBy.push(`${id}-error`);
|
|
|
|
const inputBorder = error
|
|
? '1px solid #dc2626'
|
|
: '1px solid var(--border)';
|
|
|
|
return (
|
|
<div className={className}>
|
|
{label && (
|
|
<label
|
|
htmlFor={id}
|
|
className="block text-sm font-medium mb-2"
|
|
style={{ color: 'var(--text-primary)' }}
|
|
>
|
|
{label}
|
|
</label>
|
|
)}
|
|
<div className="relative">
|
|
{leadingIcon && (
|
|
<span
|
|
aria-hidden="true"
|
|
className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 inline-flex"
|
|
style={{ color: 'var(--text-secondary)' }}
|
|
>
|
|
{leadingIcon}
|
|
</span>
|
|
)}
|
|
<input
|
|
ref={ref}
|
|
id={id}
|
|
type={type}
|
|
aria-invalid={error ? 'true' : undefined}
|
|
aria-describedby={describedBy.length > 0 ? describedBy.join(' ') : undefined}
|
|
className={[
|
|
'glass-input w-full rounded-xl px-4 py-3 transition-all duration-200',
|
|
'focus:outline-none focus-visible:ring-2',
|
|
leadingIcon ? 'pl-10' : '',
|
|
trailingIcon || trailingAction ? 'pr-10' : '',
|
|
]
|
|
.filter(Boolean)
|
|
.join(' ')}
|
|
style={{
|
|
border: inputBorder,
|
|
color: 'var(--text-primary)',
|
|
'--tw-ring-color': error ? '#dc2626' : 'var(--accent-ember)',
|
|
...style,
|
|
}}
|
|
{...rest}
|
|
/>
|
|
{trailingIcon && (
|
|
<span
|
|
aria-hidden="true"
|
|
className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 inline-flex"
|
|
style={{ color: 'var(--text-secondary)' }}
|
|
>
|
|
{trailingIcon}
|
|
</span>
|
|
)}
|
|
{trailingAction && (
|
|
<span className="absolute right-2 top-1/2 -translate-y-1/2 inline-flex">
|
|
{trailingAction}
|
|
</span>
|
|
)}
|
|
</div>
|
|
{error && (
|
|
<p
|
|
id={`${id}-error`}
|
|
className="mt-1 text-xs"
|
|
style={{ color: '#dc2626' }}
|
|
>
|
|
{error}
|
|
</p>
|
|
)}
|
|
{!error && helperText && (
|
|
<p
|
|
id={`${id}-helper`}
|
|
className="mt-1 text-xs"
|
|
style={{ color: 'var(--text-secondary)' }}
|
|
>
|
|
{helperText}
|
|
</p>
|
|
)}
|
|
</div>
|
|
);
|
|
});
|
|
|
|
export default Input;
|