mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Remove OS titlebar and add custom window chrome (#29)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,5 +3,13 @@
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"permissions": ["core:default", "opener:default", "websocket:default"]
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-show",
|
||||
"core:window:allow-close",
|
||||
"opener:default",
|
||||
"websocket:default",
|
||||
"window-state:default"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -609,7 +609,7 @@ pub fn run() {
|
||||
.plugin(
|
||||
tauri_plugin_window_state::Builder::default()
|
||||
.with_state_flags(
|
||||
StateFlags::SIZE | StateFlags::POSITION | StateFlags::MAXIMIZED,
|
||||
StateFlags::all() & !StateFlags::VISIBLE,
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
|
||||
@@ -14,7 +14,11 @@
|
||||
{
|
||||
"title": "",
|
||||
"width": 800,
|
||||
"height": 600
|
||||
"height": 600,
|
||||
"visible": false,
|
||||
"titleBarStyle": "Overlay",
|
||||
"hiddenTitle": true,
|
||||
"trafficLightPosition": { "x": 12, "y": 22 }
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { AppShell } from "@/app/AppShell";
|
||||
|
||||
export function App() {
|
||||
useEffect(() => {
|
||||
getCurrentWindow().show();
|
||||
}, []);
|
||||
|
||||
return <AppShell />;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import * as React from "react";
|
||||
import { Settings2 } from "lucide-react";
|
||||
|
||||
@@ -26,7 +27,11 @@ import { getEventById } from "@/shared/api/tauri";
|
||||
import { useIdentityQuery } from "@/shared/api/hooks";
|
||||
import type { RelayEvent, SearchHit } from "@/shared/api/types";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { SidebarInset, SidebarProvider } from "@/shared/ui/sidebar";
|
||||
import {
|
||||
SidebarInset,
|
||||
SidebarProvider,
|
||||
SidebarTrigger,
|
||||
} from "@/shared/ui/sidebar";
|
||||
|
||||
type AppView = "home" | "channel";
|
||||
|
||||
@@ -168,6 +173,7 @@ export function AppShell() {
|
||||
|
||||
return (
|
||||
<SidebarProvider className="h-dvh overflow-hidden overscroll-none">
|
||||
<SidebarTrigger className="fixed left-[80px] top-[9px] z-50 h-6 w-6" />
|
||||
<AppSidebar
|
||||
channels={channels}
|
||||
errorMessage={
|
||||
@@ -210,9 +216,20 @@ export function AppShell() {
|
||||
/>
|
||||
|
||||
<SidebarInset
|
||||
className="min-h-0 min-w-0 overflow-hidden"
|
||||
className="relative min-h-0 min-w-0 overflow-hidden pt-7"
|
||||
key={contentPaneKey}
|
||||
>
|
||||
{/* Drag strip covering the traffic-light inset area */}
|
||||
<div
|
||||
className="absolute inset-x-0 top-0 flex h-7 items-center px-2"
|
||||
onPointerDown={(e) => {
|
||||
if (e.button !== 0) return;
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.closest('button, a, input, [role="button"]')) return;
|
||||
e.preventDefault();
|
||||
getCurrentWindow().startDragging();
|
||||
}}
|
||||
/>
|
||||
{selectedView === "home" ? (
|
||||
<ChatHeader
|
||||
description="Personalized feed for mentions, reminders, channel activity, and agent work."
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { CircleDot, FileText, Hash, Home } from "lucide-react";
|
||||
import type * as React from "react";
|
||||
|
||||
import type { ChannelType } from "@/shared/api/types";
|
||||
import { SidebarTrigger } from "@/shared/ui/sidebar";
|
||||
|
||||
type ChatHeaderProps = {
|
||||
actions?: React.ReactNode;
|
||||
@@ -34,6 +34,14 @@ function ChannelIcon({
|
||||
return <Hash className="h-5 w-5 text-primary" />;
|
||||
}
|
||||
|
||||
function handlePointerDown(e: React.PointerEvent) {
|
||||
if (e.button !== 0) return;
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.closest('button, a, input, [role="button"]')) return;
|
||||
e.preventDefault();
|
||||
getCurrentWindow().startDragging();
|
||||
}
|
||||
|
||||
export function ChatHeader({
|
||||
actions,
|
||||
title,
|
||||
@@ -45,9 +53,8 @@ export function ChatHeader({
|
||||
<header
|
||||
className="flex min-w-0 items-center gap-3 border-b border-border/80 bg-background px-4 py-3 sm:px-6"
|
||||
data-testid="chat-header"
|
||||
onPointerDown={handlePointerDown}
|
||||
>
|
||||
<SidebarTrigger />
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<ChannelIcon channelType={channelType} mode={mode} />
|
||||
|
||||
@@ -144,11 +144,11 @@ function FeedSection({
|
||||
onOpenChannel,
|
||||
}: FeedSectionProps) {
|
||||
return (
|
||||
<section className="rounded-[1.75rem] border border-border/80 bg-card/80 p-5 shadow-sm">
|
||||
<section className="rounded-xl border border-border/80 bg-card/80 p-5 shadow-sm">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-2xl bg-primary/10 text-primary">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
||||
<Icon className="h-4 w-4" />
|
||||
</div>
|
||||
<div>
|
||||
@@ -166,7 +166,7 @@ function FeedSection({
|
||||
|
||||
<div className="mt-5 space-y-3">
|
||||
{items.length === 0 ? (
|
||||
<div className="rounded-3xl border border-dashed border-border/80 bg-background/60 px-5 py-7 text-center">
|
||||
<div className="rounded-lg border border-dashed border-border/80 bg-background/60 px-5 py-7 text-center">
|
||||
<p className="text-sm font-semibold tracking-tight">{emptyTitle}</p>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
{emptyDescription}
|
||||
@@ -181,11 +181,11 @@ function FeedSection({
|
||||
|
||||
return (
|
||||
<article
|
||||
className="rounded-3xl border border-border/70 bg-background/70 p-4 shadow-sm"
|
||||
className="rounded-lg border border-border/70 bg-background/70 p-4 shadow-sm"
|
||||
key={item.id}
|
||||
>
|
||||
<div className="flex gap-3">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl bg-secondary/70 text-secondary-foreground">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-secondary/70 text-secondary-foreground">
|
||||
<Icon className="h-4 w-4" />
|
||||
</div>
|
||||
|
||||
@@ -271,7 +271,7 @@ function SummaryCard({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-3xl border p-4 shadow-sm",
|
||||
"rounded-lg border p-4 shadow-sm",
|
||||
tone === "urgent"
|
||||
? "border-primary/20 bg-primary/10"
|
||||
: "border-border/80 bg-background/70",
|
||||
@@ -280,7 +280,7 @@ function SummaryCard({
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-10 w-10 items-center justify-center rounded-2xl",
|
||||
"flex h-10 w-10 items-center justify-center rounded-lg",
|
||||
tone === "urgent"
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-secondary text-secondary-foreground",
|
||||
@@ -303,13 +303,13 @@ function HomeLoadingState() {
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto overflow-x-hidden overscroll-contain px-4 py-4 sm:px-6">
|
||||
<div className="mx-auto flex w-full max-w-6xl flex-col gap-6">
|
||||
<div className="rounded-[1.75rem] border border-border/80 bg-card/80 p-5 shadow-sm">
|
||||
<div className="rounded-xl border border-border/80 bg-card/80 p-5 shadow-sm">
|
||||
<Skeleton className="h-6 w-44" />
|
||||
<Skeleton className="mt-3 h-4 w-full max-w-xl" />
|
||||
|
||||
<div className="mt-5 grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
||||
{["first", "second", "third", "fourth"].map((item) => (
|
||||
<Skeleton className="h-24 rounded-3xl" key={item} />
|
||||
<Skeleton className="h-24 rounded-lg" key={item} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -317,14 +317,14 @@ function HomeLoadingState() {
|
||||
<div className="grid gap-4 xl:grid-cols-2">
|
||||
{["mentions", "actions", "activity", "agents"].map((section) => (
|
||||
<div
|
||||
className="rounded-[1.75rem] border border-border/80 bg-card/80 p-5 shadow-sm"
|
||||
className="rounded-xl border border-border/80 bg-card/80 p-5 shadow-sm"
|
||||
key={section}
|
||||
>
|
||||
<Skeleton className="h-6 w-32" />
|
||||
<Skeleton className="mt-3 h-4 w-full max-w-xs" />
|
||||
<div className="mt-5 space-y-3">
|
||||
{["a", "b", "c"].map((row) => (
|
||||
<Skeleton className="h-28 rounded-3xl" key={row} />
|
||||
<Skeleton className="h-28 rounded-lg" key={row} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -364,7 +364,7 @@ export function HomeView({
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto overflow-x-hidden overscroll-contain px-4 py-4 sm:px-6">
|
||||
<div className="mx-auto flex w-full max-w-3xl flex-col gap-4">
|
||||
<div className="rounded-[1.75rem] border border-destructive/30 bg-destructive/5 px-6 py-8 shadow-sm">
|
||||
<div className="rounded-xl border border-destructive/30 bg-destructive/5 px-6 py-8 shadow-sm">
|
||||
<p className="text-base font-semibold tracking-tight">
|
||||
Home feed unavailable
|
||||
</p>
|
||||
@@ -386,11 +386,11 @@ export function HomeView({
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto overflow-x-hidden overscroll-contain px-4 py-4 sm:px-6">
|
||||
<div className="mx-auto flex w-full max-w-6xl flex-col gap-6">
|
||||
<section className="rounded-[1.75rem] border border-border/80 bg-card/80 p-5 shadow-sm">
|
||||
<section className="rounded-xl border border-border/80 bg-card/80 p-5 shadow-sm">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-[1.25rem] bg-primary text-primary-foreground shadow-sm">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-primary text-primary-foreground shadow-sm">
|
||||
<Sparkles className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import {
|
||||
CircleDot,
|
||||
FileText,
|
||||
@@ -294,13 +295,24 @@ export function AppSidebar({
|
||||
}
|
||||
}
|
||||
|
||||
function handleDragPointerDown(e: React.PointerEvent) {
|
||||
if (e.button !== 0) return;
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.closest('button, a, input, [role="button"]')) return;
|
||||
e.preventDefault();
|
||||
getCurrentWindow().startDragging();
|
||||
}
|
||||
|
||||
return (
|
||||
<Sidebar
|
||||
collapsible="offcanvas"
|
||||
data-testid="app-sidebar"
|
||||
variant="sidebar"
|
||||
>
|
||||
<SidebarHeader className="gap-3">
|
||||
<SidebarHeader
|
||||
className="gap-3 pt-7"
|
||||
onPointerDown={handleDragPointerDown}
|
||||
>
|
||||
<div className="flex items-center gap-3 rounded-xl bg-sidebar-accent/80 px-3 py-3">
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-xl text-lg">
|
||||
<span aria-hidden="true">🌱</span>
|
||||
@@ -324,9 +336,7 @@ export function AppSidebar({
|
||||
<Search className="h-4 w-4" />
|
||||
Search messages
|
||||
</span>
|
||||
<span className="rounded-md border border-sidebar-border/80 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-sidebar-foreground/60">
|
||||
Cmd K
|
||||
</span>
|
||||
<span className="text-xs text-sidebar-foreground/50">⌘K</span>
|
||||
</Button>
|
||||
</SidebarHeader>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { PanelLeft } from "lucide-react";
|
||||
import { PanelLeftClose, PanelLeftOpen } from "lucide-react";
|
||||
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import { useIsMobile } from "@/shared/hooks/use-mobile";
|
||||
@@ -271,7 +271,7 @@ const SidebarTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof Button>,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, onClick, ...props }, ref) => {
|
||||
const { toggleSidebar } = useSidebar();
|
||||
const { toggleSidebar, open } = useSidebar();
|
||||
|
||||
return (
|
||||
<Button
|
||||
@@ -286,7 +286,7 @@ const SidebarTrigger = React.forwardRef<
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<PanelLeft />
|
||||
{open ? <PanelLeftClose /> : <PanelLeftOpen />}
|
||||
<span className="sr-only">Toggle Sidebar</span>
|
||||
</Button>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user