Skip to content
27 changes: 13 additions & 14 deletions src/app/(dashboard)/dashboard/combos/ComboStepTestButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useMemo, useSyncExternalStore } from "react";
import { useTranslations } from "next-intl";
import { Button, Icon } from "@/shared/components";
import { getI18nOrFallback } from "./comboPageHelpers";
import {
describeComboStepTestResult,
Expand All @@ -15,8 +16,8 @@ import {
const STATE_STYLES = {
idle: { icon: "science", tone: "text-text-muted border-transparent" },
running: { icon: "progress_activity", tone: "text-text-muted border-transparent" },
ok: { icon: "check_circle", tone: "text-green-500 border-green-500/40" },
error: { icon: "cancel", tone: "text-red-500 border-red-500/40" },
ok: { icon: "check_circle", tone: "text-success border-success/40" },
error: { icon: "cancel", tone: "text-error border-error/40" },
} as const;

/** Per-step Test button for the combo modal model list (one probe per click). */
Expand All @@ -41,23 +42,21 @@ export default function ComboStepTestButton({ entry }: { entry: unknown }) {
: getI18nOrFallback(t, "testStep", "Test this model");

return (
<button
<Button
type="button"
variant="ghost"
size="icon-sm"
onClick={() => target && void runComboStepTest(target)}
disabled={!target || state === "running"}
className={`p-0.5 rounded border transition-colors ${style.tone} ${
target
? "hover:text-primary hover:bg-black/5 dark:hover:bg-white/5"
: "opacity-30 cursor-not-allowed"
}`}
className={`border ${style.tone} ${target ? "" : "opacity-30 cursor-not-allowed"}`}
title={title}
aria-label={title}
>
<span
className={`material-symbols-outlined text-[12px] ${state === "running" ? "animate-spin" : ""}`}
>
{style.icon}
</span>
</button>
<Icon
name={style.icon}
size={12}
className={state === "running" ? "animate-spin" : undefined}
/>
</Button>
);
}
2 changes: 1 addition & 1 deletion src/app/(dashboard)/dashboard/costs/teams-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ function TeamsAlert({ error }: { error: string }) {
const t = useTranslations("teamCosts");
if (!error) return null;
return (
<p role="alert" className="text-sm text-red-600 dark:text-red-400">
<p role="alert" className="text-sm text-error">
{t(error)}
</p>
);
Expand Down
10 changes: 5 additions & 5 deletions src/app/(dashboard)/dashboard/costs/teams/TeamBudgetCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ function isIncomplete(status: BudgetStatus) {
}

function meterColor(status: BudgetStatus) {
if (status.exceeded) return "bg-red-500";
return isIncomplete(status) ? "bg-amber-500" : "bg-primary";
if (status.exceeded) return "bg-error";
return isIncomplete(status) ? "bg-warning" : "bg-primary";
}

function BudgetMeter({ status, used, t }: { status: BudgetStatus; used: number; t: Translate }) {
Expand Down Expand Up @@ -54,17 +54,17 @@ function BudgetNotices({
return (
<>
{status.hasUnpricedUsage && (!status.exceeded || overBudget) && (
<p role="status" className="text-amber-700 dark:text-amber-400">
<p role="status" className="text-warning">
{t("unpricedBudget")}
</p>
)}
{status.hasPartialRetainedUsage && (
<p role="status" className="text-amber-700 dark:text-amber-400">
<p role="status" className="text-warning">
{t("partialBudget")}
</p>
)}
{status.exceeded && (
<p role="alert" className="text-red-600 dark:text-red-400">
<p role="alert" className="text-error">
{overBudget
? t("overBudget", { amount: money(used - status.maxBudgetUsd, locale) })
: t("unpricedBudget")}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export default function TeamConfirmModal({
<>
{confirmation && <span>{confirmationMessage(t, confirmation)}</span>}
{mutationError && (
<span role="alert" className="block mt-3 text-red-600 dark:text-red-400">
<span role="alert" className="block mt-3 text-error">
{t(mutationError)}
</span>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export default function TeamDetailPane({
return (
<main className="min-w-0 space-y-5">
{detailError && (
<p role="alert" className="text-red-600 dark:text-red-400">
<p role="alert" className="text-error">
{t(detailError)}
</p>
)}
Expand Down
49 changes: 25 additions & 24 deletions src/app/(dashboard)/dashboard/costs/teams/TeamForm.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
"use client";
import { useState } from "react";
import { useId, useState } from "react";
import { useTranslations } from "next-intl";
import { Field } from "@omniroute/ui-kit/field";
import Button from "@/shared/components/Button";
import Checkbox from "@/shared/components/Checkbox";
import Input from "@/shared/components/Input";
import Modal from "@/shared/components/Modal";
import Select from "@/shared/components/Select";
import Textarea from "@/shared/components/Textarea";
import type { Team } from "./helpers";
export interface TeamPayload {
name: string;
Expand Down Expand Up @@ -47,10 +51,11 @@ function BudgetFields({
const t = useTranslations("teamCosts");
return (
<>
<label className="flex items-center gap-2 text-sm">
<input type="checkbox" checked={enabled} onChange={(e) => onEnabled(e.target.checked)} />
{t("enableBudget")}
</label>
<Checkbox
label={t("enableBudget")}
checked={enabled}
onChange={(e) => onEnabled(e.target.checked)}
/>
<div
className={`grid motion-safe:transition-[grid-template-rows] motion-safe:duration-200 ${enabled ? "grid-rows-[1fr]" : "grid-rows-[0fr]"}`}
aria-hidden={!enabled}
Expand All @@ -64,19 +69,15 @@ function BudgetFields({
value={amount}
onChange={(e) => onAmount(e.target.value)}
/>
<label className="block text-sm">
{t("duration")}
<select
aria-label={t("duration")}
className="ml-3 rounded-control border border-border bg-bg p-2"
value={duration}
onChange={(e) => onDuration(e.target.value as Duration)}
>
<option value="1d">{t("day1")}</option>
<option value="7d">{t("day7")}</option>
<option value="30d">{t("day30")}</option>
</select>
</label>
<Select
label={t("duration")}
value={duration}
onChange={(e) => onDuration(e.target.value as Duration)}
>
<option value="1d">{t("day1")}</option>
<option value="7d">{t("day7")}</option>
<option value="30d">{t("day30")}</option>
</Select>
</fieldset>
</div>
</div>
Expand All @@ -96,6 +97,7 @@ function ProfileFields({
onDescription: (value: string) => void;
}) {
const t = useTranslations("teamCosts");
const descriptionId = useId();
return (
<>
<Input
Expand All @@ -105,15 +107,14 @@ function ProfileFields({
required
maxLength={200}
/>
<label className="block text-sm">
{t("description")}
<textarea
className="mt-2 w-full rounded-control border border-border bg-bg p-3 text-text-main"
<Field label={t("description")} htmlFor={descriptionId}>
<Textarea
id={descriptionId}
value={description}
maxLength={2000}
onChange={(e) => onDescription(e.target.value)}
/>
</label>
</Field>
</>
);
}
Expand All @@ -131,7 +132,7 @@ function FormFooter({
return (
<>
{problem && (
<p role="alert" className="text-sm text-red-600 dark:text-red-400">
<p role="alert" className="text-sm text-error">
{t(problem)}
</p>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useLocale, useTranslations } from "next-intl";
import Card from "@/shared/components/Card";
import Button from "@/shared/components/Button";
import Input from "@/shared/components/Input";
import Select from "@/shared/components/Select";
import { matchesSearch } from "@/shared/utils/turkishText";
import type { Detail, TeamKeyOption } from "./helpers";

Expand Down Expand Up @@ -36,9 +37,9 @@ function KeyAssigner({
onChange={(e) => setQuery(e.target.value)}
/>
<div className="flex flex-col sm:flex-row gap-2">
<select
<Select
aria-label={t("chooseKey")}
className="min-w-0 flex-1 rounded-control border border-border bg-bg p-2 text-sm"
className="min-w-0 flex-1"
value={selected?.id ?? ""}
onChange={(e) => setChosen(e.target.value)}
>
Expand All @@ -48,7 +49,7 @@ function KeyAssigner({
{key.name || t("unnamedKey")} · {key.teamName || t("unassigned")}
</option>
))}
</select>
</Select>
<Button
disabled={pending || !selected}
onClick={() => {
Expand Down
48 changes: 38 additions & 10 deletions src/app/(dashboard)/dashboard/costs/teams/TeamPicker.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
"use client";
import { useId } from "react";
import { useTranslations } from "next-intl";
import { Field } from "@omniroute/ui-kit/field";
import { SearchableSelect } from "@omniroute/ui-kit/searchable-select";
import Card from "@/shared/components/Card";
import SearchableSelect from "@/shared/components/SearchableSelect";
import Select from "@/shared/components/Select";
import { normalizeForSearch } from "@/shared/utils/turkishText";
import type { Team } from "./helpers";

/**
* The kit SearchableSelect commits the active option on Enter even while an IME composition is
* still open (e.g. confirming a Chinese candidate). React portals bubble through this wrapper, so
* stopping that one keydown in the capture phase keeps typing from committing a new team.
*/
function ignoreComposingEnter(event: React.KeyboardEvent) {
if (event.key === "Enter" && event.nativeEvent.isComposing) event.stopPropagation();
}

export default function TeamPicker({
teams,
selectedId,
Expand All @@ -21,22 +33,38 @@ export default function TeamPicker({
onFilter: (filter: string) => void;
}) {
const t = useTranslations("teamCosts");
const pickerId = useId();
return (
<Card padding="sm">
<fieldset
disabled={blocked}
className="grid grid-cols-1 items-end gap-3 sm:grid-cols-[minmax(0,1fr)_12rem]"
>
<SearchableSelect
key={filter}
<Field
htmlFor={pickerId}
label={t("selectTeam")}
searchLabel={t("searchTeams")}
emptyLabel={t("noTeams")}
options={teams.map((team) => ({ value: team.id, label: team.name }))}
value={selectedId}
disabled={blocked}
onChange={onSelect}
/>
className="min-w-0 gap-1.5"
onKeyDownCapture={ignoreComposingEnter}
>
<SearchableSelect
key={filter}
id={pickerId}
variant="field"
label={t("selectTeam")}
placeholder={t("noTeams")}
searchPlaceholder={t("searchTeams")}
emptyMessage={t("noTeams")}
// The folded name keeps the accent-insensitive, Turkish-safe matching of the old picker.
options={teams.map((team) => ({
value: team.id,
label: team.name,
keywords: normalizeForSearch(team.name),
}))}
value={selectedId}
disabled={blocked}
onValueChange={onSelect}
/>
</Field>
<Select
label={t("statusFilter")}
aria-label={t("statusFilter")}
Expand Down
Loading
Loading