Skip to content
Merged
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
8 changes: 8 additions & 0 deletions apps/web-ui/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,14 @@ textarea {
border: 0;
}

/* Disable hover-only elevation/motion from app and imported UI primitives. */
:where(*:hover) {
--tw-translate-y: 0 !important;
box-shadow: none !important;
transform: none !important;
translate: none !important;
}

@keyframes thinking-shimmer {
0% {
background-position: 200% 50%;
Expand Down
6 changes: 3 additions & 3 deletions apps/web-ui/src/app/settings/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default function SettingsLayout({ children }: { children?: React.ReactNod
return (
<AppShell pathname={pathname}>
<div className="flex h-full w-full flex-col gap-0 p-0">
<div className="flex flex-col gap-1 border-b border-border-subtle bg-surface-1 px-6 pt-5 pb-3 max-app:px-4 max-app:pt-4 max-app:pb-3 max-sm:p-4">
<div className="flex flex-col gap-1 px-6 pt-5 pb-3 max-app:px-4 max-app:pt-4 max-app:pb-3 max-sm:p-4">
<span className="text-xs font-semibold uppercase tracking-[0.08em] text-text-tertiary">Workspace</span>
<h2 className="m-0 font-heading text-2xl font-bold tracking-[-0.01em] text-text-heading">Settings</h2>
<p className="m-0 leading-[1.5] text-text-muted">
Expand All @@ -34,7 +34,7 @@ export default function SettingsLayout({ children }: { children?: React.ReactNod

<div className="flex min-h-0 flex-1 flex-col overflow-hidden p-0">
<Tabs value={activePanel} onValueChange={handleTabChange} className="flex min-h-0 flex-1 flex-col gap-0">
<TabsList className="inline-flex gap-1 border-b border-border-subtle bg-surface-1 px-6 pt-3 pb-0 max-app:px-4 max-app:pt-2 max-app:pb-0" aria-label="Settings sections">
<TabsList className="inline-flex gap-1 bg-transparent px-6 pt-3 pb-0 max-app:px-4 max-app:pt-2 max-app:pb-0" aria-label="Settings sections">
{settingsPanels.map((panel) => (
<TabsTrigger
key={panel.id}
Expand All @@ -46,7 +46,7 @@ export default function SettingsLayout({ children }: { children?: React.ReactNod
))}
</TabsList>

<TabsContent value={activePanel} className="flex min-h-0 flex-1 flex-col gap-(--app-section-gap) overflow-auto px-6 py-5 data-[state=inactive]:hidden max-app:px-4 max-app:py-4 max-sm:p-4">
<TabsContent value={activePanel} className="flex min-h-0 flex-1 flex-col gap-(--app-section-gap) overflow-auto p-0 data-[state=inactive]:hidden">
{children ?? <Outlet />}
</TabsContent>
</Tabs>
Expand Down
4 changes: 2 additions & 2 deletions apps/web-ui/src/components/app-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export function AppShell({
} = useSessions();
const recentSessions = sessions.filter((session) => session.archivedAt === null).slice(0, 20);
const { isDesktop } = useControllerState();
const [desktopPlatform, setDesktopPlatform] = useState<string | undefined>();
const [desktopPlatform, setDesktopPlatform] = useState<string | undefined>(() => window.monetDesktop?.platform);
const isSettingsOpen = pathname.startsWith("/settings");
const openSettingsHref = "/settings/general";

Expand Down Expand Up @@ -133,7 +133,7 @@ export function AppShell({
data-desktop-platform={desktopPlatform}
>
<Sidebar className={`sticky top-0 flex h-screen min-h-0 flex-col gap-5 overflow-hidden border-r border-border-subtle bg-app-sidebar px-4 pb-5 max-app:static max-app:h-auto max-app:overflow-visible max-app:border-r-0 max-app:border-b ${isDesktop ? "pt-3" : "pt-5"}`}>
{isDesktop ? <div className="mb-2 block min-h-[28px] [-webkit-app-region:drag]" aria-hidden="true" /> : null}
<div className={isDesktop ? "mb-2 block min-h-[28px] [-webkit-app-region:drag]" : "hidden"} aria-hidden="true" />

<SidebarHeader className="flex flex-col gap-4">
<div className="flex items-center gap-2.5 p-1">
Expand Down
78 changes: 63 additions & 15 deletions apps/web-ui/src/components/composer.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"use client";

import { useEffect, useRef, useState } from "react";
import type { ChangeEvent, FormEvent, KeyboardEvent } from "react";
import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@nexu-design/ui-web";
import { Button } from "@nexu-design/ui-web";

import type { ProviderReadinessTarget } from "../lib/provider-readiness";

Expand Down Expand Up @@ -49,6 +50,8 @@ export function Composer({
onStop,
onChangeTarget
}: ComposerProps) {
const [isModelMenuOpen, setIsModelMenuOpen] = useState(false);
const modelMenuRef = useRef<HTMLDivElement | null>(null);
const isBusy = status === "submitted" || status === "streaming";
const isDisabled = disabled || isBusy;
const sendLabel = status === "submitted" ? "Sending…" : status === "streaming" ? "Stop" : "Send";
Expand Down Expand Up @@ -100,8 +103,25 @@ export function Composer({

const target = readyProviders.find((candidate) => getTargetValue(candidate) === value) ?? null;
onChangeTarget?.(target);
setIsModelMenuOpen(false);
}

useEffect(() => {
if (!isModelMenuOpen) {
return;
}

function handlePointerDown(event: PointerEvent) {
if (!modelMenuRef.current?.contains(event.target as Node)) {
setIsModelMenuOpen(false);
}
}

document.addEventListener("pointerdown", handlePointerDown);

return () => document.removeEventListener("pointerdown", handlePointerDown);
}, [isModelMenuOpen]);

return (
<form
className="flex flex-col gap-2.5 rounded-xl border border-border-subtle bg-surface-1 p-4 shadow-xs transition-[box-shadow,border-color] duration-[var(--duration-normal)] ease-[var(--ease-standard)] focus-within:border-border-strong focus-within:shadow-sm"
Expand Down Expand Up @@ -131,23 +151,51 @@ export function Composer({
<label className="sr-only" htmlFor="chat-composer-model">
Model
</label>
<Select value={getTargetValue(activeTarget)} onValueChange={handleModelChange} disabled={isDisabled}>
<SelectTrigger
<div ref={modelMenuRef} className="relative">
<button
id="chat-composer-model"
className="h-8 max-w-64 rounded-md border-border-subtle bg-surface-0 px-2 py-1 text-xs font-medium text-text-secondary shadow-xs hover:border-border-hover focus:border-border-hover focus:ring-0 focus:shadow-focus"
type="button"
className="flex h-8 max-w-64 items-center justify-between gap-2 rounded-md border border-border-subtle bg-surface-0 px-2 py-1 text-xs font-medium text-text-secondary shadow-xs outline-none transition-colors hover:border-border-hover focus:border-border-hover focus:shadow-focus disabled:cursor-not-allowed disabled:opacity-50"
title={isTargetOverridden ? "Custom model selected for this chat" : "Chat model"}
aria-haspopup="listbox"
aria-expanded={isModelMenuOpen}
disabled={isDisabled}
onClick={() => setIsModelMenuOpen((open) => !open)}
>
<SelectValue placeholder="Chat model" />
</SelectTrigger>
<SelectContent className="border-border-subtle shadow-dropdown">
{readyProviders.map((target) => (
<SelectItem key={getTargetValue(target)} value={getTargetValue(target)}>
{target.modelName} ({target.providerDisplayName})
</SelectItem>
))}
</SelectContent>
</Select>
{isTargetOverridden ? <span className="text-xs text-accent">custom</span> : null}
<span className="truncate">
{activeTarget ? `${activeTarget.modelName} (${activeTarget.providerDisplayName})` : "Chat model"}
</span>
<span aria-hidden="true" className="text-text-muted">⌄</span>
</button>
{isModelMenuOpen ? (
<div
className="absolute bottom-[calc(100%+0.25rem)] left-0 z-50 max-h-80 min-w-full w-max max-w-[min(32rem,calc(100vw-2rem))] overflow-y-auto rounded-xl border border-border-subtle bg-surface-0 p-1 text-text-primary shadow-dropdown"
role="listbox"
aria-labelledby="chat-composer-model"
>
{readyProviders.map((target) => {
const value = getTargetValue(target);
const isSelected = value === getTargetValue(activeTarget);

return (
<button
key={value}
type="button"
role="option"
aria-selected={isSelected}
className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm transition-colors hover:bg-surface-1 aria-selected:bg-surface-1"
onClick={() => handleModelChange(value)}
>
<span className="min-w-0 flex-1 truncate">
{target.modelName} ({target.providerDisplayName})
</span>
{isSelected ? <span className="shrink-0 text-text-primary">✓</span> : null}
</button>
);
})}
</div>
) : null}
</div>
</>
) : null}
{statusHint ? (
Expand Down
Loading
Loading