fix(team): aria-disabled buttons look disabled; guarded plan handlers; Send/Create disabled when empty; Leave back in the SSO card with a divider; warning tone on the blocking bar; cap hint under the row; switcher icons

This commit is contained in:
Neil
2026-11-09 07:46:59 -08:00
parent 17ef4821da
commit c252c0fa72
7 changed files with 35 additions and 20 deletions
@@ -76,7 +76,11 @@ export function UsageSummary({
</div>
<Progress
value={includedUsagePercent(entitlement)}
tone={includedUsagePercent(entitlement) >= 100 ? 'warning' : 'default'}
tone={
includedUsagePercent(entitlement) >= 100 && !entitlement.overage.optedIn
? 'warning'
: 'default'
}
aria-label={t('auto.components.billing.sandboxHoursUsedLabel', 'Sandbox hours used')}
/>
{entitlement.overageSeconds === 0 &&
@@ -167,6 +171,14 @@ export function OverageControls({
value2: formatCents(entitlement.offers?.team.defaultSpendCapCents ?? 2500, locale)
}
)
const descriptionOn = t(
'auto.components.billing.overageDescriptionOn',
'Optional overage at {{value0}}/hour ({{value1}}/hour on Large), up to a monthly spend cap you set.',
{
value0: formatCents(overage.centsPerHour, locale),
value1: formatCents(Math.round(overage.centsPerHour * largeWeight), locale)
}
)
const capPercent =
overage.spendCapCents > 0
? Math.min(100, Math.round((overage.spentCents / overage.spendCapCents) * 100))
@@ -175,6 +187,7 @@ export function OverageControls({
<div className="space-y-1.5">
<Progress
value={capPercent}
tone={capPercent >= 100 ? 'warning' : 'default'}
aria-label={t('auto.components.billing.capUsedLabel', 'Overage spend cap used')}
/>
<p className="text-xs text-muted-foreground tabular-nums">
@@ -222,7 +235,7 @@ export function OverageControls({
{t('auto.components.billing.overageTitle', 'Overage')}
</p>
<p id={descriptionId} className="text-xs text-muted-foreground">
{description}
{optedIn ? descriptionOn : description}
</p>
</div>
<SettingsDisabledControlTooltip
@@ -287,14 +300,6 @@ export function OverageControls({
}}
/>
</div>
{capValid && entitlement.reason === 'overage_cap_reached' ? (
<p className="text-xs text-muted-foreground">
{t(
'auto.components.billing.capReachedHint',
'Raise the cap to keep starting sandboxes. Your files are safe.'
)}
</p>
) : null}
{capValid ? null : (
<p id={capErrorId} className="text-xs text-destructive">
{t('auto.components.billing.capInvalid', 'Enter a whole dollar amount.')}
@@ -318,6 +323,14 @@ export function OverageControls({
{t('auto.components.billing.saveCap', 'Save')}
</Button>
) : null}
{capValid && entitlement.reason === 'overage_cap_reached' ? (
<p className="basis-full text-xs text-muted-foreground">
{t(
'auto.components.billing.capReachedHint',
'Raise the cap to keep starting sandboxes. Your files are safe.'
)}
</p>
) : null}
</form>
)}
</div>
@@ -151,7 +151,7 @@ export function TeamPlanCard({
type="button"
className="w-full"
aria-disabled={busy || undefined}
onClick={onCheckout}
onClick={busy ? undefined : onCheckout}
data-plans-primary=""
>
{busy ? <LoadingSpinner /> : null}
@@ -188,7 +188,7 @@ export function TeamPlanCard({
variant="outline"
className="w-full"
aria-disabled={busy || undefined}
onClick={onManage}
onClick={busy ? undefined : onManage}
data-plans-primary=""
>
{t('auto.components.billing.manageBilling', 'Manage billing')}
@@ -53,7 +53,7 @@ export function TeamCreateView(props: TeamCreateViewProps): React.JSX.Element {
className="mt-4 space-y-2"
onSubmit={(event) => {
event.preventDefault()
if (trimmed && !props.creating) {
if (trimmed && !props.creating && !props.unavailable) {
props.onCreate(trimmed)
}
}}
@@ -79,7 +79,8 @@ export function TeamCreateView(props: TeamCreateViewProps): React.JSX.Element {
/>
<Button
type="submit"
aria-disabled={!trimmed || props.creating || props.unavailable || undefined}
disabled={!props.creating && (!trimmed || props.unavailable)}
aria-disabled={props.creating || undefined}
>
{props.creating ? <LoadingSpinner className="size-3.5" /> : null}
{t('auto.components.billing.team.createCta', 'Create team')}
@@ -315,7 +315,8 @@ export function TeamInviteComposer(props: TeamInviteComposerProps): React.JSX.El
type="submit"
size="sm"
// aria-disabled keeps focus here while sending; submit ignores it meanwhile.
aria-disabled={sending || valid.length === 0 || undefined}
disabled={!sending && valid.length === 0}
aria-disabled={sending || undefined}
>
{sending ? <LoadingSpinner className="size-3.5" /> : <Send />}
{valid.length === 0
@@ -293,8 +293,8 @@ export function TeamPageView(props: TeamPageViewProps): React.JSX.Element {
) : null}
</section>
{roster?.canManageMembers && enterprise ? (
<div className="rounded-xl border border-border">
{(roster?.canManageMembers && enterprise) || selfRow ? (
<div className="divide-y divide-border/60 rounded-xl border border-border">
{roster?.canManageMembers && enterprise ? (
<section aria-labelledby={ssoTitleId} className="flex flex-wrap items-center gap-3 p-4">
<div className="min-w-[14rem] flex-1 space-y-0.5">
@@ -1,5 +1,5 @@
import { useRef, useState } from 'react'
import { Users } from 'lucide-react'
import { ChevronsUpDown, User } from 'lucide-react'
import { toast } from 'sonner'
import {
DropdownMenuItem,
@@ -87,7 +87,7 @@ export function SidebarTeamSubmenu({
)
}
>
<Users className="size-3.5" />
{activeName ? <ChevronsUpDown className="size-3.5" /> : <User className="size-3.5" />}
<span className="min-w-0 flex-1 truncate">
{activeName ?? t('auto.components.sidebar.teamSwitcher.personal', 'Personal account')}
</span>
+1 -1
View File
@@ -14,7 +14,7 @@ const pressClasses = 'motion-safe:active:scale-[0.98]'
const disabledSolidClasses = 'disabled:bg-muted disabled:text-muted-foreground'
const buttonVariants = cva(
"inline-flex shrink-0 items-center justify-center gap-2 rounded-md cursor-pointer text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-2 focus-visible:outline-focus-ring disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"inline-flex shrink-0 items-center justify-center gap-2 rounded-md cursor-pointer text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-2 focus-visible:outline-focus-ring disabled:pointer-events-none disabled:opacity-50 aria-disabled:cursor-not-allowed aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {