Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions desktop/src/renderer/components/AccountSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useEscClose } from '../hooks/use-esc-close';
import { useAccount } from '../state/account-context';
import type { MarketplaceUser } from '../../main/marketplace-auth-store';
import type { BlockRow } from '../state/marketplace-api-client';
import { Button, Dialog, InputGroup, SettingRow, Callout } from './ui';
import { Button, Dialog, FieldError, InputGroup, SettingRow, Callout } from './ui';
import { ConnectedAccountsBody } from './ConnectedAccounts';

// Settings → Account section. One self-contained row-button + popup, mounted in
Expand Down Expand Up @@ -272,7 +272,7 @@ function SignedOutBody({
<p className="text-3xs text-fg-muted leading-relaxed">
Uses your GitHub profile to sign in — GitHub only shares your public info.
</p>
{signInError && <p className="text-3xs text-destructive-fg">{signInError}</p>}
{signInError && <FieldError as="p">{signInError}</FieldError>}
</div>
);
}
Expand Down Expand Up @@ -435,7 +435,7 @@ function SignedInBody({
{unblockingId === b.id ? 'Unblocking…' : 'Unblock'}
</Button>
</div>
{unblockErrors[b.id] && <p className="text-3xs text-destructive-fg">{unblockErrors[b.id]}</p>}
{unblockErrors[b.id] && <FieldError as="p">{unblockErrors[b.id]}</FieldError>}
</div>
))}
</section>
Expand Down Expand Up @@ -484,7 +484,7 @@ function SignedInBody({
Downloads a file containing everything YouCoded's server stores about your account.
</p>
{exportSavedPath && <p className="text-3xs text-fg-muted">Saved to {exportSavedPath}</p>}
{exportError && <p className="text-3xs text-destructive-fg">{exportError}</p>}
{exportError && <FieldError as="p">{exportError}</FieldError>}
</section>
</>
) : (
Expand Down Expand Up @@ -634,7 +634,7 @@ function EditAccountBody({
{nameSaving ? 'Saving…' : 'Save'}
</Button>
</InputGroup>
{nameError && <p className="text-3xs text-destructive-fg">{nameError}</p>}
{nameError && <FieldError as="p">{nameError}</FieldError>}
{nameSaved && !nameError && <p className="text-3xs text-fg-muted">Saved</p>}
</section>

Expand Down Expand Up @@ -708,7 +708,7 @@ function EditAccountBody({
)}

{/* Plain words for status, never glyphs. */}
{handleError && <p className="text-3xs text-destructive-fg">{handleError}</p>}
{handleError && <FieldError as="p">{handleError}</FieldError>}
{handleSaved && !handleError && <p className="text-3xs text-fg-muted">Saved</p>}
</section>

Expand Down Expand Up @@ -782,7 +782,7 @@ function EditAccountBody({
{deleting ? 'Deleting…' : 'Delete my account'}
</Button>
</div>
{deleteError && <p className="text-3xs text-destructive-fg">{deleteError}</p>}
{deleteError && <FieldError as="p">{deleteError}</FieldError>}
</div>
)}
</section>
Expand Down
4 changes: 2 additions & 2 deletions desktop/src/renderer/components/ConnectedAccounts.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useCallback, useState } from 'react';
import { Button } from './ui';
import { Button, FieldError } from './ui';
import ConnectGithubModal from './ConnectGithubModal';

// Connected-accounts sub-page INSIDE the Account popup (Destin feedback,
Expand Down Expand Up @@ -128,7 +128,7 @@ export function ConnectedAccountsBody({ status, refresh }: {
</div>
)}

{error && <p className="text-2xs text-destructive-fg">{error}</p>}
{error && <FieldError as="p" size="2xs">{error}</FieldError>}
</div>

{showConnect && (
Expand Down
4 changes: 2 additions & 2 deletions desktop/src/renderer/components/EngineCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// so the card reads as part of the section. As of change 25 the surface IS that
// row surface (bg-inset/50, borderless), not a lookalike.
import { useEffect, useState } from 'react';
import { Button, TextInput } from './ui';
import { Button, FieldError, TextInput } from './ui';

interface EngineStatusView {
installed: boolean;
Expand Down Expand Up @@ -149,7 +149,7 @@ export default function EngineCard({ showDetails = false }: { showDetails?: bool
{busy && (progress?.kind === 'verify' || progress?.kind === 'unpack') && (
<p className="mt-2 text-3xs text-fg-dim">{progress.kind === 'verify' ? 'Verifying download…' : 'Unpacking…'}</p>
)}
{error && <p className="mt-2 text-3xs text-destructive-fg">{error}</p>}
{error && <FieldError as="p" className="mt-2">{error}</FieldError>}
{/* Say WHY the button is there. "A newer engine is available" alone tells a
non-developer nothing about whether they need it. */}
{updateAvailable && !busy && (
Expand Down
4 changes: 2 additions & 2 deletions desktop/src/renderer/components/HandlePrompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import React, { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { useEscClose } from '../hooks/use-esc-close';
import { useAccount } from '../state/account-context';
import { Button, Dialog, InputGroup } from './ui';
import { Button, Dialog, FieldError, InputGroup } from './ui';

// Persisted "don't nag me again" flag. Set on skip (and on ESC, which is the
// same as skip), never set when the user actually claims a handle.
Expand Down Expand Up @@ -128,7 +128,7 @@ function HandlePromptPopup({
</Button>
</InputGroup>
{/* Plain words for status, never glyphs. */}
{error && <p className="text-3xs text-destructive-fg">{error}</p>}
{error && <FieldError as="p">{error}</FieldError>}

<Button variant="secondary" onClick={skip} className="w-full py-2">
Skip for now
Expand Down
12 changes: 6 additions & 6 deletions desktop/src/renderer/components/LocalModelsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
// consequence-gated destructive actions.
import React, { useCallback, useEffect, useRef, useState } from 'react';
import EngineCard from './EngineCard';
import { Button, InputGroup, ProgressBar, Callout, AnchorTip } from './ui';
import { Button, FieldError, InputGroup, ProgressBar, Callout, AnchorTip } from './ui';
import type {
CuratedModel, QuantOption, FitEstimate, DownloadProgress,
InstalledLocalModel, DetectedEndpoint, HFSearchHit,
Expand Down Expand Up @@ -292,7 +292,7 @@ function ModelBrowser({
<div className="space-y-2">
<p className="text-3xs font-medium text-fg-muted tracking-wider uppercase">More on Hugging Face</p>
{hfState === 'loading' && <p className="text-2xs text-fg-muted px-1">Searching Hugging Face…</p>}
{hfState === 'error' && <p className="text-2xs text-destructive-fg px-1">Couldn't reach Hugging Face.</p>}
{hfState === 'error' && <FieldError as="p" size="2xs" className="px-1">Couldn't reach Hugging Face.</FieldError>}
{hfState === 'idle' && hfFiltered.length === 0 && (
<p className="text-2xs text-fg-muted px-1">No other models found.</p>
)}
Expand Down Expand Up @@ -417,7 +417,7 @@ function RepoCard({
)}
</div>
{dl && <DownloadProgressRow dl={dl} />}
{dlError && <p className="text-3xs text-destructive-fg mt-1">{dlError}</p>}
{dlError && <FieldError as="p" className="mt-1">{dlError}</FieldError>}

{/* Expanded: the full quant list. */}
{expanded && (
Expand Down Expand Up @@ -795,7 +795,7 @@ export function LocalModelRow({
</div>
</div>
)}
{error && <p className="text-3xs text-destructive-fg mt-1">{error}</p>}
{error && <FieldError as="p" className="mt-1">{error}</FieldError>}
</div>
</div>
);
Expand Down Expand Up @@ -835,7 +835,7 @@ function QuantDownloadRow({ repo, q, downloads }: { repo: string; q: QuantWithFi
)}
</div>
{dl && <DownloadProgressRow dl={dl} />}
{dlError && <p className="text-3xs text-destructive-fg mt-1">{dlError}</p>}
{dlError && <FieldError as="p" className="mt-1">{dlError}</FieldError>}
</div>
);
}
Expand Down Expand Up @@ -913,7 +913,7 @@ function OtherLocalApps() {
<p className="text-3xs text-fg-muted mt-0.5">Added — manage it in Providers above.</p>
)}
{addError[hit.baseUrl] && (
<p className="text-3xs text-destructive-fg mt-0.5">{addError[hit.baseUrl]}</p>
<FieldError as="p" className="mt-0.5">{addError[hit.baseUrl]}</FieldError>
)}
</div>
{!isAdded && (
Expand Down
5 changes: 3 additions & 2 deletions desktop/src/renderer/components/PermissionsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
Button,
EmptyState,
ErrorState,
FieldError,
FOCUS_RING,
LoadingState,
SettingRow,
Expand Down Expand Up @@ -697,7 +698,7 @@ function FolderCard({
</div>
</div>
)}
{note && <p className="text-3xs text-destructive-fg mt-1">{note}</p>}
{note && <FieldError as="p" className="mt-1">{note}</FieldError>}
</div>
)}
</div>
Expand Down Expand Up @@ -876,7 +877,7 @@ function RuleRow({
</div>
)}

{note && <p className="text-3xs text-destructive-fg mt-1 px-3">{note}</p>}
{note && <FieldError as="p" className="mt-1 px-3">{note}</FieldError>}
</div>
);
}
8 changes: 4 additions & 4 deletions desktop/src/renderer/components/ProvidersSection.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState, useEffect, useCallback } from 'react';
import { Button, InputGroup, Select, TextInput, Toggle } from './ui';
import { Button, FieldError, InputGroup, Select, TextInput, Toggle } from './ui';
import type { ProviderStatus, ProviderConfig, ProviderType } from '../../shared/provider-types';

// Settings → Providers section (Phase 1 Plan A, Task 13). Lets the user add,
Expand Down Expand Up @@ -165,11 +165,11 @@ export default function ProvidersSection({ embedded = false }: { embedded?: bool
still renders, and Retry re-runs list(). */}
{listError && (
<div className="flex items-center gap-2 px-1">
<p className="text-2xs text-destructive-fg flex-1">
<FieldError as="p" size="2xs" className="flex-1">
{visibleRows.length === 0
? `Couldn't load providers — ${listError}`
: `Couldn't refresh — ${listError}`}
</p>
</FieldError>
<Button variant="secondary" size="sm" onClick={() => void refresh()} className="shrink-0">
Retry
</Button>
Expand Down Expand Up @@ -478,7 +478,7 @@ function AddProviderForm({ onDone, onCancel }: { onDone: () => Promise<void>; on
/>
</div>

{error && <p className="text-3xs text-destructive-fg">{error}</p>}
{error && <FieldError as="p">{error}</FieldError>}

<div className="flex gap-2 pt-1">
<Button variant="secondary" onClick={onCancel} className="flex-1 py-2">
Expand Down
6 changes: 3 additions & 3 deletions desktop/src/renderer/components/SessionDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { fileTypeGroup } from '../../shared/artifacts/categorization';
import type { FileTypeGroup } from '../../shared/artifacts/categorization';
import { getPlatform } from '../platform';
import { formatRelativeTime } from '../utils/format-time';
import { Button, CloseButton, EmptyState, SearchFilterPill } from './ui';
import { Button, CloseButton, EmptyState, FieldError, SearchFilterPill } from './ui';
import { FileFilterPopover } from './project-view/FileFilterPopover';
import { useResolvedConversations } from '../hooks/useResolvedConversations';
import { useTagRegistry } from '../hooks/useTagRegistry';
Expand Down Expand Up @@ -858,9 +858,9 @@ export function SessionDrawer({ sessionId, projectRoot, projectId, projectName,
</span>
{/* Inline failure note — keeps the field open so the user can correct it. */}
{renameError && (
<span className="absolute left-1 top-full mt-1 text-2xs text-destructive-fg whitespace-nowrap z-10">
<FieldError size="2xs" className="absolute left-1 top-full mt-1 whitespace-nowrap z-10">
{renameError}
</span>
</FieldError>
)}
</div>
) : (
Expand Down
4 changes: 2 additions & 2 deletions desktop/src/renderer/components/SettingsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { DonateConfirm } from './DonateConfirm';
import { formatVersionLine } from '../../shared/version-line';
// UiToggle is aliased because this file still exports its own `Toggle` (the
// compat wrapper below) that AboutPopup imports by that name.
import { Button, CloseButton, Toggle as UiToggle, TextInput, InputGroup, LoadingState, RadioGroup, SegmentedTabs, Dialog, SettingRow, Callout, StatusStrip, ErrorState } from './ui';
import { Button, CloseButton, Toggle as UiToggle, TextInput, InputGroup, LoadingState, RadioGroup, SegmentedTabs, Dialog, SettingRow, Callout, StatusStrip, ErrorState, FieldError } from './ui';

// Both are Vite `define` substitutions, so they're constants at module scope.
// The typeof guard covers paths where the define isn't applied (unit tests).
Expand Down Expand Up @@ -1216,7 +1216,7 @@ function RemoteButton({
(port already bound, permission denied). Show the real
reason here — the toggle has already snapped back off. */}
{enableError && (
<p className="text-2xs text-destructive-fg pb-2">{enableError}</p>
<FieldError as="p" size="2xs" className="pb-2">{enableError}</FieldError>
)}

<div className="py-2">
Expand Down
4 changes: 2 additions & 2 deletions desktop/src/renderer/components/SyncPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*/

import React, { useState, useEffect, useCallback, useRef } from 'react';
import { Button, Dialog, TextInput, Toggle, LoadingState, SettingRow } from './ui';
import { Button, Dialog, FieldError, TextInput, Toggle, LoadingState, SettingRow } from './ui';
import type { SyncWarning } from '../../main/sync-state';
import { deriveSettingsRowState, type SyncDisplayState } from '../state/sync-display-state';
import { createPortal } from 'react-dom';
Expand Down Expand Up @@ -1818,7 +1818,7 @@ function DevicesTab({ devices, onRename, onRemove, syncInProgress, lastSyncByDev
{/* Why the remove didn't take. Never invents a cause: the handler's own
reason when it gave one, otherwise non-committal. */}
{removeNote?.id === d.id && (
<p className="text-3xs text-destructive-fg mt-1">{removeNote.text}</p>
<FieldError as="p" className="mt-1">{removeNote.text}</FieldError>
)}
</li>
);
Expand Down
6 changes: 3 additions & 3 deletions desktop/src/renderer/components/SyncSetupWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*/

import { useState, useEffect, useCallback } from 'react';
import { Button, CloseButton, TextInput, Toggle, Radio, RadioGroup, Callout } from './ui';
import { Button, CloseButton, FieldError, TextInput, Toggle, Radio, RadioGroup, Callout } from './ui';
import { isAndroid as checkIsAndroid } from '../platform';
import { useEscClose } from '../hooks/use-esc-close';
import { useScrollFade } from '../hooks/useScrollFade';
Expand Down Expand Up @@ -846,9 +846,9 @@ function GhInstallHelp({ onRecheck }: { onRecheck: () => void }) {
)}

{installError && (
<div className="text-3xs text-destructive-fg">
<FieldError as="div">
Couldn't install it automatically: {installError}
</div>
</FieldError>
)}

{/* Manual instructions: only after an automated attempt failed (or on a
Expand Down
23 changes: 20 additions & 3 deletions desktop/src/renderer/components/ui/states.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,13 +164,30 @@ export function ErrorState(props: ErrorStateProps) {
export type FieldErrorProps = {
children: React.ReactNode;
className?: string;
/** Type step. The app has always used both: 19 of the 25 hand-rolled copies
* this primitive replaced were `text-3xs`, 6 were `text-2xs`. It is a PROP
* rather than something a caller passes through `className` because this
* component CONCATENATES className onto the base — and Tailwind resolves two
* competing utilities by CSS SOURCE ORDER, not by the order they appear in
* the attribute, so `className="text-2xs"` would silently keep rendering at
* 3xs (the same trap that made Button's pills render as rectangles). */
size?: '3xs' | '2xs';
/** Element to render. Default `span` (inline) matches how the primitive
* shipped. Pass `p`/`div` where the line is a BLOCK under a field: vertical
* margin and padding (`mt-1`, `pb-2`) do not lay out on an inline element,
* so a `<p className="mt-1 …">` swapped to a bare span would silently lose
* its gap wherever the parent is not a flex/grid container. */
as?: 'span' | 'p' | 'div';
};

/** Field-level errors stay short lines under the input — not cards. */
export function FieldError({ children, className = '' }: FieldErrorProps) {
export function FieldError({ children, className = '', size = '3xs', as: Tag = 'span' }: FieldErrorProps) {
// Literal class strings, not `text-${size}` — Tailwind scans source text for
// whole class names and never sees an interpolated one.
const sizeClass = size === '2xs' ? 'text-2xs' : 'text-3xs';
return (
<span className={`text-3xs text-destructive-fg ${className}`.trim()} role="alert">
<Tag className={`${sizeClass} text-destructive-fg ${className}`.trim()} role="alert">
{children}
</span>
</Tag>
);
}
Loading