Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions .doctor/dupe-ignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ MAGIC:ghost
MAGIC:secondary
MAGIC:accent
MAGIC:default
MAGIC:link

# Config key — only 2 files, not worth extracting
MAGIC:sttModel
Expand Down
50 changes: 50 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,30 @@
--color-warning: #f59e0b;
--color-recording: #ef4444;
--color-border: #e5e5e0;

/* macOS window-control colors (was hardcoded in the sidebar). */
--color-mac-close: #ff5f57;
--color-mac-min: #febc2e;
--color-mac-max: #28c840;

/* Elevation scale — each pairs a hairline ring with a soft shadow so cards
and overlays blend on any background instead of showing a harsh border.
Maps to `shadow-card` / `shadow-popover` / `shadow-overlay` utilities. */
--shadow-card: 0 1px 2px rgba(0, 0, 0, 0.05), 0 0 0 0.5px rgba(0, 0, 0, 0.03);
--shadow-popover: 0 8px 28px -6px rgba(0, 0, 0, 0.16), 0 0 0 0.5px rgba(0, 0, 0, 0.04);
--shadow-overlay: 0 16px 48px -12px rgba(0, 0, 0, 0.22), 0 0 0 0.5px rgba(0, 0, 0, 0.05);
}

@layer base {
/* Fixed z-index scale — reference with `z-[var(--z-modal)]` etc. so stacking
order is intentional instead of a scatter of arbitrary values. */
:root {
--z-dropdown: 100;
--z-modal: 200;
--z-tooltip: 300;
--z-toast: 400;
}

html,
body {
height: 100%;
Expand Down Expand Up @@ -67,6 +88,35 @@
transform-origin: top;
}

/* Backdrop fade + centered dialog entrance. */
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

@keyframes modal-in {
from {
opacity: 0;
transform: scale(0.97);
}
to {
opacity: 1;
transform: scale(1);
}
}

.animate-fade-in {
animation: fade-in 0.15s ease-out;
}

.animate-modal-in {
animation: modal-in 0.18s cubic-bezier(0.16, 1, 0.3, 1);
}

/* Respect the user's motion preference everywhere by default. */
@media (prefers-reduced-motion: reduce) {
*,
Expand Down
26 changes: 18 additions & 8 deletions src/layout/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NavLink } from "react-router-dom";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { open } from "@tauri-apps/plugin-shell";
import cn from "../shared/lib/utils/cn";
import Button from "../shared/components/button/button";
import appVersion from "../shared/constants/app-version";
import useUpdateCheck from "../shared/hooks/use-update-check";
import sidebarNavItems from "./sidebar-nav-items";
Expand All @@ -17,12 +18,16 @@ export default function Sidebar() {
>
<div className="flex items-center gap-1.5">
<button
type="button"
aria-label="Close window"
onClick={() => getCurrentWindow().close()}
className="w-3 h-3 rounded-full bg-[#ff5f57] hover:brightness-90 transition-all"
className="w-3 h-3 rounded-full bg-mac-close transition-[filter] duration-150 hover:brightness-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
/>
<button
type="button"
aria-label="Minimize window"
onClick={() => getCurrentWindow().minimize()}
className="w-3 h-3 rounded-full bg-[#febc2e] hover:brightness-90 transition-all"
className="w-3 h-3 rounded-full bg-mac-min transition-[filter] duration-150 hover:brightness-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
/>
</div>
<span
Expand All @@ -41,9 +46,13 @@ export default function Sidebar() {
end={item.to === "/"}
className={({ isActive }) =>
cn(
"flex items-center gap-3 px-3 py-2.5 rounded-lg text-[13px] transition-colors duration-150",
// font-medium is constant across states — only color/bg change,
// so the active item never shifts the layout.
"flex items-center gap-3 px-3 py-2.5 rounded-lg text-[13px] font-medium",
"transition-colors duration-150 ease-out motion-reduce:transition-none",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40",
isActive
? "bg-accent-subtle text-accent font-medium"
? "bg-accent-subtle text-accent"
: "text-text-secondary hover:bg-bg-tertiary hover:text-text-primary"
)
}
Expand All @@ -55,14 +64,15 @@ export default function Sidebar() {
</nav>

<div className="px-4 py-3 border-t border-border">
<div className="text-[11px] text-text-tertiary">v{appVersion}</div>
<div className="text-[11px] text-text-tertiary tabular-nums">v{appVersion}</div>
{updateAvailable && (
<button
<Button
variant="link"
onClick={() => open("https://loqui.impulselab.ai")}
className="text-[11px] text-accent hover:underline"
className="text-[11px]"
>
Update available
</button>
</Button>
)}
</div>
</aside>
Expand Down
14 changes: 8 additions & 6 deletions src/onboarding/onboarding-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export default function OnboardingLayout({ onComplete }: OnboardingLayoutProps)
if (!steps) {
return (
<div className="flex items-center justify-center h-screen bg-bg-primary text-sm text-text-tertiary">
Loading...
Loading
</div>
);
}
Expand Down Expand Up @@ -84,8 +84,10 @@ export default function OnboardingLayout({ onComplete }: OnboardingLayoutProps)
</div>
<span
className={cn(
"text-sm",
i === currentStep ? "text-text-primary font-medium" : "text-text-tertiary"
// Constant weight — only the color changes per step, so the
// stepper labels never reflow as you advance.
"text-sm font-medium",
i === currentStep ? "text-text-primary" : "text-text-tertiary"
)}
>
{step.label}
Expand Down Expand Up @@ -115,7 +117,7 @@ export default function OnboardingLayout({ onComplete }: OnboardingLayoutProps)
<button
onClick={handleBack}
disabled={currentStep === 0}
className="px-4 py-2 text-sm text-text-secondary hover:text-text-primary disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
className="px-4 py-2 rounded-lg text-sm text-text-secondary hover:text-text-primary disabled:opacity-30 disabled:cursor-not-allowed transition-colors duration-150 ease-out motion-reduce:transition-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
>
Back
</button>
Expand All @@ -131,11 +133,11 @@ export default function OnboardingLayout({ onComplete }: OnboardingLayoutProps)
<button
onClick={footer.onClick}
disabled={footer.disabled || footer.loading}
className="relative overflow-hidden px-6 py-2 text-sm font-medium bg-accent text-white rounded-lg hover:bg-accent-hover disabled:opacity-50 disabled:cursor-not-allowed transition-colors min-w-[120px]"
className="relative overflow-hidden px-6 py-2 text-sm font-medium bg-accent text-white rounded-lg hover:bg-accent-hover disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-150 ease-out motion-reduce:transition-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 focus-visible:ring-offset-2 focus-visible:ring-offset-bg-primary min-w-[120px]"
>
{footer.loading && footer.progress != null && (
<div
className="absolute inset-0 bg-white/15 transition-all duration-300"
className="absolute inset-0 bg-white/15 transition-[width] duration-300 motion-reduce:transition-none"
style={{ width: `${footer.progress}%` }}
/>
)}
Expand Down
2 changes: 1 addition & 1 deletion src/onboarding/steps/step-microphone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ export default function StepMicrophone({ goNext, setFooter }: StepComponentProps

<div className="flex-1 h-3 bg-bg-tertiary rounded-full overflow-hidden">
<div
className="h-full bg-accent rounded-full transition-all duration-75"
className="h-full bg-accent rounded-full transition-[width] duration-75 motion-reduce:transition-none"
style={{ width: `${Math.min(level * 100, 100)}%` }}
/>
</div>
Expand Down
4 changes: 3 additions & 1 deletion src/profiles/list/profile-list-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ export default function ProfileListItem({
type="button"
onClick={onSelect}
className={cn(
"w-full text-left px-3 py-3 rounded-lg transition-colors cursor-pointer flex items-center justify-between",
"w-full text-left px-3 py-3 rounded-lg cursor-pointer flex items-center justify-between gap-2",
"transition-colors duration-150 ease-out motion-reduce:transition-none",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40",
selected
? "bg-accent-subtle"
: "hover:bg-bg-secondary"
Expand Down
4 changes: 3 additions & 1 deletion src/profiles/llm/llm-model-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,9 @@ export default function LlmModelSelector({
type="button"
onClick={() => onProviderChange(tab.id)}
className={cn(
"flex-1 px-2 py-1.5 text-xs font-medium rounded-md transition-colors cursor-pointer",
"flex-1 px-2 py-1.5 text-xs font-medium rounded-md cursor-pointer",
"transition-colors duration-150 ease-out motion-reduce:transition-none",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40",
llmProvider === tab.id
? "bg-bg-primary text-text-primary shadow-sm"
: "text-text-tertiary hover:text-text-secondary",
Expand Down
15 changes: 9 additions & 6 deletions src/settings/about/about-section.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { open } from "@tauri-apps/plugin-shell";
import { ExternalLink } from "lucide-react";
import Card from "../../shared/components/card";
import Button from "../../shared/components/button/button";
import appVersion from "../../shared/constants/app-version";

export default function AboutSection() {
Expand All @@ -15,24 +16,26 @@ export default function AboutSection() {

<div className="flex items-center justify-between px-5 py-4">
<span className="text-sm">Source code</span>
<button
<Button
variant="link"
onClick={() => open("https://github.com/impulse-studio/loqui")}
className="inline-flex items-center gap-1.5 text-xs text-accent font-medium hover:underline"
className="text-xs"
>
GitHub
<ExternalLink size={12} />
</button>
</Button>
</div>

<div className="flex items-center justify-between px-5 py-4">
<span className="text-sm">Report an issue</span>
<button
<Button
variant="link"
onClick={() => open("https://github.com/impulse-studio/loqui/issues")}
className="inline-flex items-center gap-1.5 text-xs text-accent font-medium hover:underline"
className="text-xs"
>
Feedback
<ExternalLink size={12} />
</button>
</Button>
</div>
</Card>
</section>
Expand Down
8 changes: 5 additions & 3 deletions src/settings/general/general-section.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useState } from "react";
import Card from "../../shared/components/card";
import Toggle from "../../shared/components/toggle";
import Button from "../../shared/components/button/button";
import {
setConfig,
enableAutostart,
Expand Down Expand Up @@ -53,12 +54,13 @@ export default function GeneralSection() {
</div>
<div className="flex items-center gap-3">
<HotkeyDisplay hotkey={hotkey} />
<button
<Button
variant="link"
onClick={() => setShowHotkeyModal(true)}
className="text-xs text-accent font-medium hover:underline"
className="text-xs"
>
Change
</button>
</Button>
</div>
</div>
<div className="flex items-center justify-between px-5 py-4">
Expand Down
2 changes: 1 addition & 1 deletion src/settings/llm/llm-model-download-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export default function LlmModelDownloadRow({
<div className="mt-2 space-y-1">
<div className="h-1.5 rounded-full bg-bg-tertiary overflow-hidden">
<div
className="h-full bg-accent rounded-full transition-all duration-300"
className="h-full bg-accent rounded-full transition-[width] duration-300 motion-reduce:transition-none"
style={{ width: `${percent}%` }}
/>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/settings/speech/speech-model-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export default function SpeechModelModal({
>
{downloading && (
<span
className="absolute inset-0 bg-accent/20 transition-all duration-300"
className="absolute inset-0 bg-accent/20 transition-[width] duration-300 motion-reduce:transition-none"
style={{ width: `${pct}%` }}
/>
)}
Expand Down
2 changes: 1 addition & 1 deletion src/shared/components/badge/badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default function Badge({ children, variant = "default" }: BadgeProps) {
return (
<span
className={cn(
"inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium",
"inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium tabular-nums",
badgeVariantStyles[variant]
)}
>
Expand Down
2 changes: 2 additions & 0 deletions src/shared/components/button/button-size-styles.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const buttonSizeStyles: Record<string, string> = {
// No box chrome — for variant="link" sitting inside flowing text.
inline: "text-sm",
sm: "h-8 px-3 text-[13px] rounded-lg",
md: "h-9 px-4 text-sm rounded-lg",
lg: "h-11 px-6 text-sm rounded-lg",
Expand Down
3 changes: 3 additions & 0 deletions src/shared/components/button/button-variant-styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ const buttonVariantStyles: Record<string, string> = {
"bg-bg-secondary text-text-primary border border-border hover:bg-bg-tertiary",
destructive: "bg-error text-white hover:bg-error/90 active:bg-error/80",
ghost: "text-text-secondary hover:bg-bg-secondary active:bg-bg-tertiary",
// Inline text action (e.g. "Change", "Update available"). No box — pairs
// with `size="inline"` so it sits in flowing text without button chrome.
link: "text-accent hover:text-accent-hover hover:underline underline-offset-2 rounded-sm",
};

export default buttonVariantStyles;
16 changes: 12 additions & 4 deletions src/shared/components/button/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import buttonVariantStyles from "./button-variant-styles";
import buttonSizeStyles from "./button-size-styles";

interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary" | "destructive" | "ghost";
size?: "sm" | "md" | "lg";
variant?: "primary" | "secondary" | "destructive" | "ghost" | "link";
size?: "inline" | "sm" | "md" | "lg";
}

export default function Button({
Expand All @@ -16,12 +16,20 @@ export default function Button({
children,
...props
}: ButtonProps) {
const isLink = variant === "link";
return (
<button
className={cn(
"inline-flex items-center justify-center gap-2 font-medium transition-colors duration-150 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed",
"inline-flex items-center justify-center gap-2 font-medium cursor-pointer",
// Tailwind's `transition` animates a curated property set (colors,
// transform, box-shadow) — intentional, not `transition: all`.
"transition duration-150 ease-out motion-reduce:transition-none",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 focus-visible:ring-offset-2 focus-visible:ring-offset-bg-primary",
"disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none",
// Tactile press feedback for real buttons; links stay flat.
!isLink && "active:scale-[0.98]",
buttonVariantStyles[variant],
buttonSizeStyles[size],
buttonSizeStyles[isLink ? "inline" : size],
className
)}
disabled={disabled}
Expand Down
7 changes: 6 additions & 1 deletion src/shared/components/card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,24 @@ import cn from "../lib/utils/cn";
interface CardProps extends HTMLAttributes<HTMLDivElement> {
children: ReactNode;
featured?: boolean;
/** Adds a subtle hover lift for clickable cards. */
interactive?: boolean;
}

export default function Card({
children,
featured = false,
interactive = false,
className = "",
...props
}: CardProps) {
return (
<div
className={cn(
"bg-bg-card rounded-xl border shadow-[0_1px_3px_rgba(0,0,0,0.04)]",
"bg-bg-card rounded-xl border shadow-[var(--shadow-card)]",
featured ? "border-accent" : "border-border",
interactive &&
"transition-shadow duration-200 ease-out motion-reduce:transition-none hover:shadow-[var(--shadow-popover)]",
className
)}
{...props}
Expand Down
5 changes: 5 additions & 0 deletions src/shared/components/modal-focusable-selector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Tabbable elements inside the modal — used by the focus trap.
const modalFocusableSelector =
'a[href],button:not([disabled]),textarea,input,select,[tabindex]:not([tabindex="-1"])';
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

export default modalFocusableSelector;
Loading
Loading