diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2f9d3f16f4..ce88b38731 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -78,6 +78,17 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged.
`COMPUTER_MEMORY_HIGH_PERCENT=0` and `COMPUTER_TAB_IDLE_MS=0` (Helm: `computers.memoryHighPercent`
and `computers.tabIdleMs`) keep the old behaviour.
+### The default look: an inset page, slate greys and quieter headings
+
+Each page now sits in a panel inset from the window, on a darker frame shared with the sidebar,
+with white cards on a light grey page; admin and settings scroll inside it like the rest of the
+app. The greys are Tailwind's slate rather than neutral, and page and section headings are lighter.
+Rows show an app's logo or initial instead of the same plug; Home shows Bots as compact cards; a
+Bot's page leads with its own one-liner; Connections tiles no longer cut their text off; and the
+Composio directory shows its rows loading. Empty states carry a faint dot pattern whose file,
+colour and strength are theme tokens (`--empty-pattern*` in `theme.css`), so a deployment can
+replace it or turn it off. Skins are unchanged.
+
### A computer closes browsers nobody is using when its memory runs high
A computer runs one browser for each Bot that is browsing, and the only limits were a count and half
diff --git a/app/src/assets/patterns/dot-matrix.svg b/app/src/assets/patterns/dot-matrix.svg
new file mode 100644
index 0000000000..be9834f09a
--- /dev/null
+++ b/app/src/assets/patterns/dot-matrix.svg
@@ -0,0 +1,136 @@
+
+
diff --git a/app/src/components/admin/admin-sidebar.tsx b/app/src/components/admin/admin-sidebar.tsx
index 2b4a7d5d6c..62286f8498 100644
--- a/app/src/components/admin/admin-sidebar.tsx
+++ b/app/src/components/admin/admin-sidebar.tsx
@@ -1,15 +1,16 @@
import {
IconArrowLeft,
IconBuildingBank,
+ IconBulb,
IconChecks,
IconCode,
IconDeviceDesktop,
- IconFileText,
IconKey,
IconLayoutGrid,
IconListDetails,
IconPuzzle,
IconShieldCheck,
+ IconSlash,
IconUsers,
} from "@tabler/icons-react";
import { Link, type LinkOptions } from "@tanstack/react-router";
@@ -79,12 +80,12 @@ const GROUPS: {
},
{
title: "Skills",
- icon: IconFileText,
+ icon: IconSlash,
linkOptions: { to: "/admin/skills" },
},
{
title: "Automatic learning",
- icon: IconFileText,
+ icon: IconBulb,
linkOptions: { to: "/admin/learning" },
},
{
@@ -130,7 +131,7 @@ export function AdminSidebar({
...props
}: React.ComponentProps) {
return (
-
+
{/*
* Pinned to the same height as the app sidebar's header. Left to itself this one is 60px
* against the app's 45px — `p-2` around a `size="lg"` button rather than a fixed height — so
diff --git a/app/src/components/agents/agent-card.tsx b/app/src/components/agents/agent-card.tsx
index 67cc270a06..2aa5abeb83 100644
--- a/app/src/components/agents/agent-card.tsx
+++ b/app/src/components/agents/agent-card.tsx
@@ -9,23 +9,23 @@ export function AgentCard({
appearance = "compact",
}: {
agent: AgentProfile;
- appearance?: "compact" | "tile";
+ appearance?: "compact" | "row";
}) {
- if (appearance === "tile") {
+ if (appearance === "row") {
return (
-
+ /*
+ * The avatar beside the name rather than above it: a tall card left most of its height empty
+ * between the two, and a row reads the name and what the Bot is for in one glance.
+ */
+
-
+
- {/* Bottom-aligned, where the text sat on the earlier artwork card; the avatar holds the top. */}
-
-
+
+
{agent.name}
-
+
{agent.roleDescription}
diff --git a/app/src/components/agents/handoff-panel.tsx b/app/src/components/agents/handoff-panel.tsx
index 6a693c4910..90ad484fdd 100644
--- a/app/src/components/agents/handoff-panel.tsx
+++ b/app/src/components/agents/handoff-panel.tsx
@@ -1,4 +1,4 @@
-import { IconArrowsExchange, IconLock, IconRobot } from "@tabler/icons-react";
+import { IconArrowsExchange, IconBolt, IconLock } from "@tabler/icons-react";
import { useMutation, useQuery } from "@tanstack/react-query";
import { Fragment } from "react";
import { AbstractAvatar } from "@/components/agents/abstract-avatar";
@@ -102,7 +102,7 @@ export function HandoffPanel({ agentId }: { agentId: string }) {
) : null}
{grantable && total === 0 ? (
-
+
When there are more, you can let this Bot ask them here.
) : null}
diff --git a/app/src/components/app-sidebar/app-sidebar.tsx b/app/src/components/app-sidebar/app-sidebar.tsx
index 27fe657b18..e7e8e99b59 100644
--- a/app/src/components/app-sidebar/app-sidebar.tsx
+++ b/app/src/components/app-sidebar/app-sidebar.tsx
@@ -220,7 +220,7 @@ export function AppSidebar({ ...props }: React.ComponentProps) {
const navigation = navigationFor(currentUser?.role);
return (
-
+
diff --git a/app/src/components/app-sidebar/channel-item-content.tsx b/app/src/components/app-sidebar/channel-item-content.tsx
index ece35115c2..a8128c95bc 100644
--- a/app/src/components/app-sidebar/channel-item-content.tsx
+++ b/app/src/components/app-sidebar/channel-item-content.tsx
@@ -26,7 +26,7 @@ export function ChannelItemContent({
pinned?: boolean;
revealing?: boolean;
}) {
- const primaryText = `text-[0.9rem] leading-5 tracking-[-1%] ${unread ? "font-semibold" : "font-medium"}`;
+ const primaryText = `text-sm leading-5 tracking-[-1%] ${unread ? "font-semibold" : "font-medium"}`;
const secondaryText = "text-[12px] leading-4 text-muted-foreground";
return (
diff --git a/app/src/components/bot-profile/activity.tsx b/app/src/components/bot-profile/activity.tsx
index 91c0fad637..41c3d17599 100644
--- a/app/src/components/bot-profile/activity.tsx
+++ b/app/src/components/bot-profile/activity.tsx
@@ -197,22 +197,31 @@ function ActivitySection({
/** How many running or scheduled items the Bot's page shows before "All activity". */
const GLANCE = 3;
+/** Running work first, then what is lined up, as many as the Bot's page shows. */
+function upcomingOf(activity: BotActivity | undefined) {
+ return activity
+ ? [
+ ...activity.inProgress.map((item) => ({ item, live: true })),
+ ...activity.scheduled.map((item) => ({ item, live: false })),
+ ].slice(0, GLANCE)
+ : [];
+}
+
/**
* The Bot page's view of its activity: what is under way and what is lined up, so either is seen
* without opening anything, then a row to the full lists. Running work comes first, with its dot.
*
+ * Only when there is something running or lined up. With nothing to show, a whole section for one
+ * row to the full lists is a heading over nothing; that row joins "For you" instead, through
+ * `BotActivityRow`.
+ *
* The full lists used to sit here and ran on for screens; finished work is the long part and the
* least urgent, so it lives only on the Activity page.
*/
export function BotActivityGlance({ agentId }: { agentId: string }) {
const activity = useQuery(botActivityQueryOptions(agentId));
- if (activity.isPending) return null;
- const upcoming = activity.data
- ? [
- ...activity.data.inProgress.map((item) => ({ item, live: true })),
- ...activity.data.scheduled.map((item) => ({ item, live: false })),
- ].slice(0, GLANCE)
- : [];
+ const upcoming = upcomingOf(activity.data);
+ if (activity.isPending || upcoming.length === 0) return null;
return (
@@ -222,32 +231,69 @@ export function BotActivityGlance({ agentId }: { agentId: string }) {
))}
- }
- size="sm"
- >
- {upcoming.length ? null : (
-
-
-
- )}
-
- All activity
-
- {activity.data
- ? activitySummary(activity.data)
- : "Couldn't load this Bot's activity."}
-
-
-
-
-
-
+
);
}
+/**
+ * The row to a Bot's activity, inside "For you", for when nothing is running or lined up and
+ * `BotActivityGlance` draws no section of its own. Starts with a separator, because it follows the
+ * card's other rows.
+ */
+export function BotActivityRow({ agentId }: { agentId: string }) {
+ const activity = useQuery(botActivityQueryOptions(agentId));
+ if (activity.isPending || upcomingOf(activity.data).length > 0) return null;
+ return (
+ <>
+
+
+ >
+ );
+}
+
+function AllActivityItem({
+ activity,
+ agentId,
+ title = "All activity",
+ withIcon = false,
+}: {
+ activity: BotActivity | undefined;
+ agentId: string;
+ title?: string;
+ withIcon?: boolean;
+}) {
+ return (
+ }
+ size="sm"
+ >
+ {withIcon ? (
+
+
+
+ ) : null}
+
+ {title}
+
+ {activity
+ ? activitySummary(activity)
+ : "Couldn't load this Bot's activity."}
+
+
+
+
+
+
+ );
+}
+
/**
* The Bot page's one line about its activity: what is running and what is lined up, the two that
* might want a look. Finished work is only counted when there is nothing else to say.
diff --git a/app/src/components/bot-profile/profile.tsx b/app/src/components/bot-profile/profile.tsx
index fef082c983..f452d292e2 100644
--- a/app/src/components/bot-profile/profile.tsx
+++ b/app/src/components/bot-profile/profile.tsx
@@ -41,7 +41,7 @@ import {
botLifecycleQueryOptions,
} from "@/lib/bot-lifecycle/queries";
import { queryClient } from "@/query-client";
-import { BotActivityGlance } from "./activity";
+import { BotActivityGlance, BotActivityRow } from "./activity";
import { BotPausedBanner } from "./pause-banner";
const NOTIFY_LABEL: Record = {
@@ -77,14 +77,12 @@ export function BotProfile({
return (
<>
+ {/* The one-liner is the page's description, under the title; here, the face and its title. */}
-
- {agent.title &&
{agent.title}
}
-
- {agent.roleDescription}
-
-
+ {agent.title ? (
+
{agent.title}
+ ) : null}
@@ -218,6 +216,7 @@ export function BotProfile({
+
)}
diff --git a/app/src/components/bots/bot-roster.tsx b/app/src/components/bots/bot-roster.tsx
index c801d12f9e..209741e0d2 100644
--- a/app/src/components/bots/bot-roster.tsx
+++ b/app/src/components/bots/bot-roster.tsx
@@ -1,7 +1,7 @@
import {
+ IconBolt,
IconChevronRight,
IconPlus,
- IconRobot,
IconShare,
} from "@tabler/icons-react";
import { useQuery } from "@tanstack/react-query";
@@ -96,7 +96,7 @@ export function BotRoster() {
)
}
- icon={IconRobot}
+ icon={IconBolt}
title={
yoursPinned
? "All pinned"
diff --git a/app/src/components/component-preview.tsx b/app/src/components/component-preview.tsx
index 57d991c949..fad1047c31 100644
--- a/app/src/components/component-preview.tsx
+++ b/app/src/components/component-preview.tsx
@@ -36,7 +36,7 @@ function PreviewNotice({ children }: { children: ReactNode }) {
return (
{/* The artwork stays pale in both themes, so this text must stay dark. */}
-
{children}
+
{children}
);
}
diff --git a/app/src/components/layout/page-parts.tsx b/app/src/components/layout/page-parts.tsx
index 7fa01f8dba..33e4ff17b7 100644
--- a/app/src/components/layout/page-parts.tsx
+++ b/app/src/components/layout/page-parts.tsx
@@ -40,20 +40,13 @@ export function PageColumn({
return (
<>
{/*
- * The scroller, and it has to live here rather than in either shell, because the two shells
- * this frame is used under disagree about who scrolls.
- *
- * Under `_authed` — admin, settings — the document scrolls, so a page taller than the window
- * has always just worked. Under `_authed/_app` it does not: that shell is `h-svh
- * overflow-hidden` on purpose ("one viewport, never scrolls: panes scroll inside it") and its
- * `main` is `overflow-hidden` too, so a PageShell taller than the window was silently CLIPPED
- * — 246px of Skills sat below the fold with no way to reach it, by keyboard or otherwise.
+ * The scroller. Every shell — app, admin, settings — is exactly the window's height and holds
+ * the page in an inset panel that never grows past it, so a page taller than the window
+ * scrolls here, inside the panel, and the panel's rounded corners stay on screen.
*
* `min-h-0` is the load-bearing half of `min-h-0 flex-1`: a flex item's default `min-height:
- * auto` refuses to shrink below its content, so `flex-1` alone would grow this past the pane
- * and clip exactly as before. Under `_authed` both are inert — the parent is not a flex
- * container — and `overflow-y-auto` on an auto-height element shows no scrollbar, so those
- * eighteen screens are unaffected.
+ * auto` refuses to shrink below its content, so `flex-1` alone would grow this past the panel
+ * and the overflow would be clipped instead of scrolled.
*
* Separate from the centred column below so the scrollbar rides the pane's edge instead of
* appearing inside the measure with content either side of it.
@@ -98,7 +91,10 @@ export function PageHeader({
return (
+ {/*
+ * Close to body size and lighter than the page title, so the sections read as parts of
+ * the page rather than as a second row of titles competing with it.
+ */}
+
{title}
{action}
) : null}
@@ -243,7 +243,10 @@ export function PageEmpty({
title?: string;
}) {
return (
-
+ // `isolate` gives the pattern its own stacking context, so it sits behind the text, not on it.
+
+ {/* The pattern is decoration: see `.empty-pattern` in styles.css and its tokens in theme.css. */}
+
{Icon ? (
diff --git a/app/src/components/layout/page-shell.tsx b/app/src/components/layout/page-shell.tsx
index 073ed9e42e..73c4c01727 100644
--- a/app/src/components/layout/page-shell.tsx
+++ b/app/src/components/layout/page-shell.tsx
@@ -83,10 +83,10 @@ export function PageShell({
if (width === "full") {
return (
/*
- * One viewport, never scrolls. `min-w-0` lets a table wider than the pane scroll inside its own
- * container instead of pushing the page sideways.
+ * Exactly the panel's height, never scrolls. `min-w-0` lets a table wider than the pane scroll
+ * inside its own container instead of pushing the page sideways.
*/
-
+
{barRow}
{header}
diff --git a/app/src/components/onboarding/laptop-illustration.tsx b/app/src/components/onboarding/laptop-illustration.tsx
index cd648a6e94..5ce12fac53 100644
--- a/app/src/components/onboarding/laptop-illustration.tsx
+++ b/app/src/components/onboarding/laptop-illustration.tsx
@@ -18,12 +18,12 @@ export function LaptopIllustration() {
-
+
-
-
+
+
);
diff --git a/app/src/components/plugins/broker-directory.tsx b/app/src/components/plugins/broker-directory.tsx
index 7137bb66e3..4f9b97b8a7 100644
--- a/app/src/components/plugins/broker-directory.tsx
+++ b/app/src/components/plugins/broker-directory.tsx
@@ -7,6 +7,7 @@ import {
PageSection,
PageShell,
} from "@/components/layout/page-shell";
+import { AppInitial } from "@/components/plugins/plugin-logo";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
@@ -18,6 +19,7 @@ import {
ItemTitle,
} from "@/components/ui/item";
import { Separator } from "@/components/ui/separator";
+import { Skeleton } from "@/components/ui/skeleton";
import {
consentBreadth,
coverageWarning,
@@ -56,6 +58,9 @@ import { queryClient } from "@/query-client";
* here branches on which one it is — a `provider === "composio"` test inside a screen is the same
* mistake wearing a conditional.
*/
+/** Placeholder rows while the directory loads: enough to fill the view, not to suggest a count. */
+const LOADING_ROWS = ["a", "b", "c", "d", "e", "f"];
+
export function BrokerDirectory({ provider }: { provider: PluginsProvider }) {
const { name, directoryLabel } = providerVocabulary(provider);
const [search, setSearch] = React.useState("");
@@ -146,8 +151,31 @@ export function BrokerDirectory({ provider }: { provider: PluginsProvider }) {
value={search}
/>
- {/* Pending, error, empty, rows — pending first, so no sentence asserts anything mid-fetch. */}
- {apps.isPending ? null : apps.error ? (
+ {/*
+ * Pending, error, empty, rows. Pending draws placeholder rows: the full directory takes a
+ * couple of seconds to arrive, and a bare search box over blank space read as a page with
+ * nothing on it. They assert nothing, so no sentence claims anything mid-fetch.
+ */}
+ {apps.isPending ? (
+
{/*
* The server's own sentence, as the enable failure above already does. A keyless
@@ -167,64 +195,72 @@ export function BrokerDirectory({ provider }: { provider: PluginsProvider }) {
: `${name} returned no apps.`}
) : (
-
- {listed.map((entry, index) => {
- // One mutation serves every row, so which rows are mid-request is tracked beside it
- // rather than read off it. See `adding` above.
- const isAdding = adding.has(entry.slug);
+ <>
+ {/* How many there are, before the list: with no search, this is the whole directory. */}
+
+ {query
+ ? `${listed.length.toLocaleString()} ${listed.length === 1 ? "match" : "matches"}`
+ : `${listed.length.toLocaleString()} apps. Search by name to narrow them.`}
+
+
+ {listed.map((entry, index) => {
+ // One mutation serves every row, so which rows are mid-request is tracked beside it
+ // rather than read off it. See `adding` above.
+ const isAdding = adding.has(entry.slug);
- return (
-
- {/* The test id names the screen, not the vendor: this module draws whichever
+ return (
+
+ {/* The test id names the screen, not the vendor: this module draws whichever
broker is active, and the id is not user-visible. */}
-
- {/* The vendor's own mark where the broker supplies one, and a plug where it does
- not, so a list of third parties still has a fixed left edge. */}
-
- {entry.logo ? (
-
- ) : (
-
- )}
-
-
- {entry.name}
- {entry.description}
- {entry.enabled ? null : }
- {entry.enabled ? null : }
-
-
- {/* The size of the decision, stated before it is made: an app is not one
+
+ {/* The vendor's own mark where the broker supplies one, and its initial where it
+ does not, so a list of third parties still has a fixed left edge. */}
+
+ {entry.logo ? (
+
+ ) : (
+
+ )}
+
+
+ {entry.name}
+ {entry.description}
+ {entry.enabled ? null : }
+ {entry.enabled ? null : }
+
+
+ {/* The size of the decision, stated before it is made: an app is not one
tool, and "167 actions" is what says so. */}
-
- {entry.actionCount} actions
-
- {/*
- * An app this deployment already has says so rather than offering Add again.
- * A second Add would ask the server to record a connector it already holds,
- * and there is nothing on the row to suggest that would be harmless.
- */}
- {entry.enabled ? (
- Added
+ {entry.actionCount} actions
- ) : (
-
- )}
-
-
- {index !== listed.length - 1 && }
-
- );
- })}
-
+ {/*
+ * An app this deployment already has says so rather than offering Add again.
+ * A second Add would ask the server to record a connector it already holds,
+ * and there is nothing on the row to suggest that would be harmless.
+ */}
+ {entry.enabled ? (
+
+ Added
+
+ ) : (
+
+ )}
+
+
+ {index !== listed.length - 1 && }
+
+ );
+ })}
+
+ >
)}
diff --git a/app/src/components/plugins/plugin-logo.tsx b/app/src/components/plugins/plugin-logo.tsx
index c457a11b0c..d2c279a25f 100644
--- a/app/src/components/plugins/plugin-logo.tsx
+++ b/app/src/components/plugins/plugin-logo.tsx
@@ -1,9 +1,24 @@
import { IconPlug } from "@tabler/icons-react";
import { useState } from "react";
-export function PluginLogo({ logo }: { logo?: string | null }) {
+/**
+ * An app's logo, or its initial where it has none.
+ *
+ * The initial rather than a generic plug because a list of apps that all show the same plug is a
+ * column of identical tiles, which tells nobody anything; "T" and "G" at least tell Todoist from
+ * GitHub at a glance. The plug is left only for a caller with no name to take an initial from.
+ */
+export function PluginLogo({
+ logo,
+ name,
+}: {
+ logo?: string | null;
+ name?: string;
+}) {
const [failedUrl, setFailedUrl] = useState(null);
- if (!logo || failedUrl === logo) return ;
+ if (!logo || failedUrl === logo) {
+ return name ? : ;
+ }
return (
);
}
+
+/** The first letter of an app's name, drawn where its logo would go. Hidden from screen readers: the row's title already says the name. */
+export function AppInitial({ name }: { name: string }) {
+ const initial = name.trim().charAt(0).toUpperCase();
+ return (
+
+ {initial || "?"}
+
+ );
+}
diff --git a/app/src/components/settings/settings-sidebar.tsx b/app/src/components/settings/settings-sidebar.tsx
index 53e5d9bec3..d0694cca64 100644
--- a/app/src/components/settings/settings-sidebar.tsx
+++ b/app/src/components/settings/settings-sidebar.tsx
@@ -84,7 +84,7 @@ export function SettingsSidebar({
...props
}: React.ComponentProps) {
return (
-
+
{/* Matched to the app sidebar's header, as Admin's is. See admin-sidebar.tsx. */}
diff --git a/app/src/components/ui/input-group.tsx b/app/src/components/ui/input-group.tsx
index c5af85eeaa..429c2230a1 100644
--- a/app/src/components/ui/input-group.tsx
+++ b/app/src/components/ui/input-group.tsx
@@ -11,7 +11,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
data-slot="input-group"
role="group"
className={cn(
- "group/input-group relative flex h-9 w-full min-w-0 items-center rounded-lg border border-input transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
+ "group/input-group relative flex h-9 w-full min-w-0 items-center rounded-lg border border-input bg-card transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
className,
)}
{...props}
diff --git a/app/src/components/ui/input.tsx b/app/src/components/ui/input.tsx
index 11dff01045..2a5363d8f0 100644
--- a/app/src/components/ui/input.tsx
+++ b/app/src/components/ui/input.tsx
@@ -9,7 +9,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
type={type}
data-slot="input"
className={cn(
- "h-9 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
+ "h-9 w-full min-w-0 rounded-lg border border-input bg-card px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className,
)}
{...props}
diff --git a/app/src/components/ui/item.tsx b/app/src/components/ui/item.tsx
index 0f68d1c1bf..51b336baca 100644
--- a/app/src/components/ui/item.tsx
+++ b/app/src/components/ui/item.tsx
@@ -90,7 +90,7 @@ const itemMediaVariants = cva(
variant: {
default: "bg-transparent",
// A 32px tile behind a 16px glyph, so a column of rows reads as a column of marks.
- icon: "size-8 rounded-md bg-muted text-muted-foreground group-data-[size=xs]/item:size-6 [&_svg:not([class*='size-'])]:size-4",
+ icon: "size-8 rounded-md bg-background text-muted-foreground ring-1 ring-border group-data-[size=xs]/item:size-6 [&_svg:not([class*='size-'])]:size-4",
image:
"size-10 overflow-hidden rounded-sm group-data-[size=sm]/item:size-8 group-data-[size=xs]/item:size-6 [&_img]:size-full [&_img]:object-cover",
},
diff --git a/app/src/components/ui/select.tsx b/app/src/components/ui/select.tsx
index f854d1b542..e101c0fb6f 100644
--- a/app/src/components/ui/select.tsx
+++ b/app/src/components/ui/select.tsx
@@ -43,7 +43,7 @@ function SelectTrigger({
data-slot="select-trigger"
data-size={size}
className={cn(
- "flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+ "flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-card py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
diff --git a/app/src/components/ui/sidebar.tsx b/app/src/components/ui/sidebar.tsx
index b5148fffe0..89672ca877 100644
--- a/app/src/components/ui/sidebar.tsx
+++ b/app/src/components/ui/sidebar.tsx
@@ -325,7 +325,7 @@ function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
) {