From 28770c7f1e4e4c9b05ded130090a3689b3238861 Mon Sep 17 00:00:00 2001 From: klopez4212 Date: Fri, 12 Jun 2026 15:05:32 +0100 Subject: [PATCH 1/7] Add shared skeleton loader primitives --- desktop/src/shared/styles/globals.css | 125 ++++++++++++++++++++++++++ desktop/src/shared/ui/skeleton.tsx | 94 +++++++++++++++++-- desktop/src/shared/ui/spinner.tsx | 32 +++++-- 3 files changed, 237 insertions(+), 14 deletions(-) diff --git a/desktop/src/shared/styles/globals.css b/desktop/src/shared/styles/globals.css index bb3fdb249..3cb2da523 100644 --- a/desktop/src/shared/styles/globals.css +++ b/desktop/src/shared/styles/globals.css @@ -2,6 +2,16 @@ @import "tw-animate-css"; @config "../../../tailwind.config.js"; +.sprout-arc-spinner { + animation: sprout-arc-spinner-spin 500ms linear infinite; +} + +@keyframes sprout-arc-spinner-spin { + to { + transform: rotate(360deg); + } +} + .buzz-emoji-mart { align-items: stretch; display: flex; @@ -837,6 +847,121 @@ } } +:root { + --skel-pulse-dur: 1000ms; + --skel-pulse-count: infinite; + --skel-pulse-min: 0.5; + --skel-reveal-dur: 400ms; + --skel-reveal-blur: 2px; + --skel-reveal-ease: ease-in-out; +} + +.t-skel { + position: relative; +} + +.t-skel[data-layout="flow"] { + display: grid; +} + +.t-skel-skeleton, +.t-skel-content { + inset: 0; + position: absolute; +} + +.t-skel[data-layout="flow"] > .t-skel-skeleton, +.t-skel[data-layout="flow"] > .t-skel-content { + grid-area: 1 / 1; + width: 100%; +} + +.t-skel[data-layout="flow"] > .t-skel-skeleton { + inset: auto; + position: relative; +} + +.t-skel[data-layout="flow"] > .t-skel-content, +.t-skel[data-layout="flow"].is-revealed > .t-skel-skeleton { + inset: 0; + position: absolute; +} + +.t-skel[data-layout="flow"].is-revealed > .t-skel-content { + inset: auto; + position: relative; +} + +.t-skel-skeleton { + filter: blur(0); + opacity: 1; + transition: + opacity var(--skel-reveal-dur) var(--skel-reveal-ease), + filter var(--skel-reveal-dur) var(--skel-reveal-ease); + z-index: 1; +} + +.t-skel-content { + filter: blur(var(--skel-reveal-blur)); + opacity: 0; + pointer-events: none; + transition: + opacity var(--skel-reveal-dur) var(--skel-reveal-ease), + filter var(--skel-reveal-dur) var(--skel-reveal-ease); + z-index: 2; +} + +.t-skel.is-revealed .t-skel-skeleton { + filter: blur(var(--skel-reveal-blur)); + opacity: 0; + pointer-events: none; +} + +.t-skel.is-revealed .t-skel-content { + filter: blur(0); + opacity: 1; + pointer-events: auto; +} + +.t-skel.is-resetting .t-skel-skeleton, +.t-skel.is-resetting .t-skel-content { + transition: none; +} + +.t-skel-bar.is-pulsing, +.t-skel-skeleton.is-pulsing > :not(:has(.t-skel-bar)) { + animation: t-skel-pulse var(--skel-pulse-dur) ease-in-out + var(--skel-pulse-count); +} + +.t-skel.is-revealed .t-skel-bar.is-pulsing, +.t-skel.is-revealed .t-skel-skeleton.is-pulsing > :not(:has(.t-skel-bar)) { + animation: none; +} + +@keyframes t-skel-pulse { + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: var(--skel-pulse-min); + } +} + +@media (prefers-reduced-motion: reduce) { + .t-skel-skeleton, + .t-skel-content { + transition: none; + } + + .t-skel-bar.is-pulsing, + .t-skel-skeleton.is-pulsing > :not(:has(.t-skel-bar)) { + animation: none; + } +} + @layer components { .buzz-startup-shell { min-height: 100dvh; diff --git a/desktop/src/shared/ui/skeleton.tsx b/desktop/src/shared/ui/skeleton.tsx index 563cc4c9e..b324eb5da 100644 --- a/desktop/src/shared/ui/skeleton.tsx +++ b/desktop/src/shared/ui/skeleton.tsx @@ -1,15 +1,99 @@ +import * as React from "react"; + import { cn } from "@/shared/lib/cn"; -function Skeleton({ +type SkeletonProps = React.HTMLAttributes & { + pulsing?: boolean; +}; + +function Skeleton({ className, pulsing = true, ...props }: SkeletonProps) { + return ( + @@ -109,7 +109,7 @@ export function WorkflowWebhookHeadersEditor({ type="button" variant="ghost" > - + ))} diff --git a/desktop/src/features/workflows/ui/workflowFormPrimitives.tsx b/desktop/src/features/workflows/ui/workflowFormPrimitives.tsx index e6173ee23..ca0f8a872 100644 --- a/desktop/src/features/workflows/ui/workflowFormPrimitives.tsx +++ b/desktop/src/features/workflows/ui/workflowFormPrimitives.tsx @@ -25,7 +25,7 @@ export function FormSelect({ > {children} - + ); } diff --git a/desktop/src/features/workspaces/ui/WorkspaceSwitcher.tsx b/desktop/src/features/workspaces/ui/WorkspaceSwitcher.tsx index 2e7e14d04..89b17a93c 100644 --- a/desktop/src/features/workspaces/ui/WorkspaceSwitcher.tsx +++ b/desktop/src/features/workspaces/ui/WorkspaceSwitcher.tsx @@ -121,7 +121,7 @@ export function WorkspaceSwitcher({ data-testid="relay-connection-warning" role="img" > - + @@ -154,8 +154,8 @@ export function WorkspaceSwitcher({ )} @@ -209,7 +209,7 @@ export function WorkspaceSwitcher({ > {activeWorkspace?.id === workspace.id ? ( - + ) : null} @@ -226,7 +226,7 @@ export function WorkspaceSwitcher({ }} type="button" > - + ))} @@ -296,7 +296,7 @@ export function WorkspaceSwitcher({ > {activeWorkspace?.id === workspace.id ? ( - + ) : null} {workspace.name} @@ -311,7 +311,7 @@ export function WorkspaceSwitcher({ }} type="button" > - + ))} diff --git a/desktop/src/shared/ui/VideoPlayer.tsx b/desktop/src/shared/ui/VideoPlayer.tsx index b967b00e9..5dd42bff9 100644 --- a/desktop/src/shared/ui/VideoPlayer.tsx +++ b/desktop/src/shared/ui/VideoPlayer.tsx @@ -1627,7 +1627,7 @@ function VideoReviewDialog({ type="button" onClick={() => setIsEmojiPickerOpen((open) => !open)} > - + More reactions @@ -1900,7 +1900,7 @@ function VideoReviewCommentBody({

{reactions.some((reaction) => reaction.reactedByCurrentUser) ? ( ) : null} diff --git a/desktop/src/shared/ui/import-status-icon.tsx b/desktop/src/shared/ui/import-status-icon.tsx index 9617e44eb..aadb5b9bc 100644 --- a/desktop/src/shared/ui/import-status-icon.tsx +++ b/desktop/src/shared/ui/import-status-icon.tsx @@ -16,7 +16,9 @@ export function ImportStatusIcon({ }) { switch (status) { case "importing": - return ; + return ( + + ); case "done": return ; case "error": diff --git a/desktop/src/shared/ui/markdown.tsx b/desktop/src/shared/ui/markdown.tsx index b21b45e73..2f4a84663 100644 --- a/desktop/src/shared/ui/markdown.tsx +++ b/desktop/src/shared/ui/markdown.tsx @@ -520,7 +520,7 @@ function MarkdownCodeBlock({ type="button" variant="ghost" > - + Copy code block @@ -579,7 +579,7 @@ function FileCard({ className="my-1 inline-flex max-w-sm items-center gap-3 rounded-xl border border-border/70 bg-muted/40 px-3 py-2 text-left no-underline transition-colors hover:bg-muted/70" > - + diff --git a/web/src/features/repos/ui/RepoBlobViewer.tsx b/web/src/features/repos/ui/RepoBlobViewer.tsx index 0a53696d3..5adfdd2f0 100644 --- a/web/src/features/repos/ui/RepoBlobViewer.tsx +++ b/web/src/features/repos/ui/RepoBlobViewer.tsx @@ -258,7 +258,7 @@ export function RepoBlobPage() {
- +

{filepath}

{view && diff --git a/web/src/features/repos/ui/RepoRefsSection.tsx b/web/src/features/repos/ui/RepoRefsSection.tsx index 11b38acd7..07fdafa82 100644 --- a/web/src/features/repos/ui/RepoRefsSection.tsx +++ b/web/src/features/repos/ui/RepoRefsSection.tsx @@ -22,12 +22,12 @@ export function RepoRefsSection({ <>
- + {refs.head.ref} {refs.head.sha && ( - + {refs.head.sha.slice(0, 7)} )} @@ -36,13 +36,13 @@ export function RepoRefsSection({ )} - + {refs.branches.length}{" "} {refs.branches.length === 1 ? "branch" : "branches"} · - + {refs.tags.length} {refs.tags.length === 1 ? "tag" : "tags"}
diff --git a/web/src/features/repos/ui/ReposPage.tsx b/web/src/features/repos/ui/ReposPage.tsx index 72a972a4a..c738f769a 100644 --- a/web/src/features/repos/ui/ReposPage.tsx +++ b/web/src/features/repos/ui/ReposPage.tsx @@ -104,7 +104,7 @@ export function ReposPage() {

- Repositories + Repositories

{["a", "b", "c", "d", "e"].map((key) => ( @@ -131,7 +131,7 @@ export function ReposPage() {

- Repositories + Repositories

{/* Search + Sort bar */} From 9f121b0e2bff3adce5bfe84e585fbd5727622fa7 Mon Sep 17 00:00:00 2001 From: klopez4212 Date: Fri, 12 Jun 2026 15:26:04 +0100 Subject: [PATCH 3/7] Update narrow header menu spacing expectation --- desktop/tests/e2e/messaging.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/desktop/tests/e2e/messaging.spec.ts b/desktop/tests/e2e/messaging.spec.ts index 610f174e0..825b53b0b 100644 --- a/desktop/tests/e2e/messaging.spec.ts +++ b/desktop/tests/e2e/messaging.spec.ts @@ -751,8 +751,8 @@ test("narrow thread view collapses channel header actions into a menu", async ({ throw new Error("Expected header action menu and thread panel bounds"); } const menuGapPx = threadPanelBox.x - (menuBox.x + menuBox.width); - expect(menuGapPx).toBeGreaterThanOrEqual(10); - expect(menuGapPx).toBeLessThanOrEqual(14); + expect(menuGapPx).toBeGreaterThanOrEqual(22); + expect(menuGapPx).toBeLessThanOrEqual(26); await menuTrigger.click(); From 64686ef926504bcc702f36b324574cb080df2c9d Mon Sep 17 00:00:00 2001 From: klopez4212 Date: Fri, 12 Jun 2026 15:08:19 +0100 Subject: [PATCH 4/7] Polish channel modal forms --- .../channels/ui/ChannelBrowserDialog.tsx | 535 ++++++++++-------- .../messages/lib/useRichTextEditor.ts | 3 + .../src/features/search/ui/ChannelFindBar.tsx | 3 + .../sidebar/ui/CreateChannelDialog.tsx | 318 +++++++---- .../sidebar/ui/NewDirectMessageDialog.tsx | 4 +- desktop/src/shared/ui/alert-dialog.tsx | 6 +- desktop/src/shared/ui/checkbox.tsx | 2 +- .../src/shared/ui/chooser-dialog-content.tsx | 23 +- desktop/src/shared/ui/dialog.tsx | 26 +- desktop/src/shared/ui/input.tsx | 3 + desktop/src/shared/ui/textarea.tsx | 3 + 11 files changed, 579 insertions(+), 347 deletions(-) diff --git a/desktop/src/features/channels/ui/ChannelBrowserDialog.tsx b/desktop/src/features/channels/ui/ChannelBrowserDialog.tsx index 2fc794bb2..22e17c007 100644 --- a/desktop/src/features/channels/ui/ChannelBrowserDialog.tsx +++ b/desktop/src/features/channels/ui/ChannelBrowserDialog.tsx @@ -1,52 +1,20 @@ import * as React from "react"; -import { - Compass, - FileText, - Hash, - LogIn, - Search, - Users, - type LucideIcon, -} from "lucide-react"; +import { Compass, Search, X, type LucideIcon } from "lucide-react"; import type { Channel } from "@/shared/api/types"; import { Dialog, + DialogClose, DialogContent, - DialogDescription, DialogHeader, DialogTitle, } from "@/shared/ui/dialog"; import { Badge } from "@/shared/ui/badge"; -import { Input } from "@/shared/ui/input"; import { Button } from "@/shared/ui/button"; +import { Tabs, TabsList, TabsTrigger } from "@/shared/ui/tabs"; const BROWSE_CHANNELS_SHORTCUT_HINT = "\u21E7\u2318O"; - -function formatRelativeTime(isoString: string | null) { - if (!isoString) { - return "No activity"; - } - - const diff = Math.floor((Date.now() - new Date(isoString).getTime()) / 1_000); - - if (diff < 60) { - return "just now"; - } - - if (diff < 60 * 60) { - return `${Math.floor(diff / 60)}m ago`; - } - - if (diff < 60 * 60 * 24) { - return `${Math.floor(diff / (60 * 60))}h ago`; - } - - return new Intl.DateTimeFormat("en-US", { - month: "short", - day: "numeric", - }).format(new Date(isoString)); -} +type BrowserTab = "all" | "joined" | "archived"; function BrowseState({ icon: Icon, @@ -60,7 +28,7 @@ function BrowseState({ return (
- +

{title}

@@ -88,24 +56,35 @@ export function ChannelBrowserDialog({ onSelectChannel, }: ChannelBrowserDialogProps) { const [query, setQuery] = React.useState(""); - const [selectedIndex, setSelectedIndex] = React.useState(0); + const [activeTab, setActiveTab] = React.useState("all"); + const [selectedIndex, setSelectedIndex] = React.useState(null); const [joiningChannelId, setJoiningChannelId] = React.useState( null, ); + const contentRef = React.useRef(null); const inputRef = React.useRef(null); + const tabListRef = React.useRef(null); + const tabTriggerRefs = React.useRef< + Record + >({ + all: null, + joined: null, + archived: null, + }); + const [tabIndicator, setTabIndicator] = React.useState({ + left: 0, + width: 0, + }); const deferredQuery = React.useDeferredValue(query.trim().toLowerCase()); const isForumMode = channelTypeFilter === "forum"; const browseTitle = isForumMode ? "Browse Forums" : "Browse Channels"; - const browseDescription = isForumMode - ? "Discover and join open forums." - : "Discover and join open channels."; const searchPlaceholder = isForumMode ? "Search forums by name or description" : "Search channels by name or description"; const entityLabel = isForumMode ? "forum" : "channel"; - const browsableChannels = React.useMemo(() => { + const matchingChannels = React.useMemo(() => { const filtered = channels.filter( (channel) => channel.channelType !== "dm" && @@ -126,53 +105,114 @@ export function ChannelBrowserDialog({ ); }, [channels, channelTypeFilter, deferredQuery]); - const notJoined = React.useMemo( - () => browsableChannels.filter((channel) => !channel.isMember), - [browsableChannels], + const currentChannels = React.useMemo( + () => matchingChannels.filter((channel) => channel.archivedAt === null), + [matchingChannels], ); - const joined = React.useMemo( - () => browsableChannels.filter((channel) => channel.isMember), - [browsableChannels], + const joinedChannels = React.useMemo( + () => currentChannels.filter((channel) => channel.isMember), + [currentChannels], ); - const hasArchivedJoinedChannels = React.useMemo( - () => joined.some((channel) => channel.archivedAt !== null), - [joined], + + const archivedChannels = React.useMemo( + () => matchingChannels.filter((channel) => channel.archivedAt !== null), + [matchingChannels], ); - // Flat list for keyboard navigation: not-joined first, then joined - const allItems = React.useMemo( - () => [...notJoined, ...joined], - [notJoined, joined], + const visibleChannels = + activeTab === "archived" + ? archivedChannels + : activeTab === "joined" + ? joinedChannels + : currentChannels; + + const orderedVisibleChannels = React.useMemo( + () => [ + ...visibleChannels.filter((channel) => !channel.isMember), + ...visibleChannels.filter((channel) => channel.isMember), + ], + [visibleChannels], ); - React.useEffect(() => { + const allTabLabel = isForumMode ? "All forums" : "All channels"; + + const updateTabIndicator = React.useCallback(() => { + const list = tabListRef.current; + const trigger = tabTriggerRefs.current[activeTab]; + + if (!open || !list || !trigger) { + return; + } + + const nextIndicator = { + left: trigger.offsetLeft, + width: trigger.offsetWidth, + }; + + setTabIndicator((current) => + Math.abs(current.left - nextIndicator.left) < 0.5 && + Math.abs(current.width - nextIndicator.width) < 0.5 + ? current + : nextIndicator, + ); + }, [activeTab, open]); + + React.useLayoutEffect(() => { + updateTabIndicator(); + if (!open) { - setQuery(""); - setSelectedIndex(0); - setJoiningChannelId(null); return; } - const timeout = window.setTimeout(() => { - inputRef.current?.focus(); - inputRef.current?.select(); - }, 0); + let isCancelled = false; + const updateIfActive = () => { + if (!isCancelled) { + updateTabIndicator(); + } + }; + const frameId = window.requestAnimationFrame(updateIfActive); + const observer = new ResizeObserver(updateTabIndicator); + const list = tabListRef.current; + + void document.fonts.ready.then(updateIfActive); + + if (list) { + observer.observe(list); + } + + for (const trigger of Object.values(tabTriggerRefs.current)) { + if (trigger) { + observer.observe(trigger); + } + } return () => { - window.clearTimeout(timeout); + isCancelled = true; + window.cancelAnimationFrame(frameId); + observer.disconnect(); }; + }, [open, updateTabIndicator]); + + React.useEffect(() => { + if (!open) { + setQuery(""); + setActiveTab("all"); + setSelectedIndex(null); + setJoiningChannelId(null); + return; + } }, [open]); React.useEffect(() => { setSelectedIndex((current) => { - if (allItems.length === 0) { - return 0; + if (current === null || orderedVisibleChannels.length === 0) { + return null; } - return Math.min(current, allItems.length - 1); + return Math.min(current, orderedVisibleChannels.length - 1); }); - }, [allItems]); + }, [orderedVisibleChannels]); async function handleJoin(channelId: string) { setJoiningChannelId(channelId); @@ -191,45 +231,90 @@ export function ChannelBrowserDialog({ onSelectChannel(channel.id); } - const selectedItem = allItems[selectedIndex]; + const selectedItem = + selectedIndex !== null ? orderedVisibleChannels[selectedIndex] : undefined; + const emptyTitle = + deferredQuery.length > 0 + ? `No ${entityLabel}s match your search` + : activeTab === "archived" + ? `No archived ${entityLabel}s` + : activeTab === "joined" + ? `No joined ${entityLabel}s` + : `No ${entityLabel}s to browse`; + const emptyDescription = + deferredQuery.length > 0 + ? "Try a different name or keyword." + : activeTab === "archived" + ? `Archived ${entityLabel}s you have joined will appear here.` + : activeTab === "joined" + ? `${entityLabel[0].toUpperCase()}${entityLabel.slice(1)}s you join will appear here.` + : `All open ${entityLabel}s are available in the sidebar. Create a new ${entityLabel} to get started.`; return (

{ + event.preventDefault(); + contentRef.current?.focus(); + }} + ref={contentRef} + showCloseButton={false} > - - - - - - {browseTitle} - - {browseDescription} -
- - +
+ {browseTitle} + + + Close + +
+
+
-
- {browsableChannels.length === 0 ? ( - deferredQuery.length > 0 ? ( - - ) : ( - - ) - ) : ( -
- {notJoined.length > 0 ? ( - <> -
- - {notJoined.length} {entityLabel} - {notJoined.length !== 1 ? "s" : ""} to join - - Enter to view -
-
- {notJoined.map((channel) => { - const flatIndex = allItems.indexOf(channel); - return ( - { - void handleJoin(channel.id); - }} - onMouseEnter={() => setSelectedIndex(flatIndex)} - onSelect={() => handleSelect(channel)} - /> - ); - })} -
- - ) : null} - - {joined.length > 0 ? ( - <> -
- Joined -
-
- {joined.map((channel) => { - const flatIndex = allItems.indexOf(channel); - return ( - setSelectedIndex(flatIndex)} - onSelect={() => handleSelect(channel)} - /> - ); - })} -
- - ) : null} +
+
+ { + setActiveTab(value as BrowserTab); + setSelectedIndex(null); + }} + value={activeTab} + > + + + + +
+ {orderedVisibleChannels.length === 0 ? ( + 0 ? Search : Compass} + title={emptyTitle} + /> + ) : ( +
+ {orderedVisibleChannels.map((channel, index) => ( + { + void handleJoin(channel.id); + } + : undefined + } + onSelect={() => handleSelect(channel)} + /> + ))} +
+ )}
- )} -
- -
- {hasArchivedJoinedChannels - ? `Showing open ${entityLabel}s and your archived ${entityLabel}s. Private ${entityLabel}s require an invite.` - : `Showing open ${entityLabel}s. Private ${entityLabel}s require an invite.`} +
@@ -340,80 +436,77 @@ function ChannelCard({ isJoining, isSelected, onJoin, - onMouseEnter, onSelect, }: { channel: Channel; isJoining: boolean; isSelected: boolean; onJoin?: () => void; - onMouseEnter: () => void; onSelect: () => void; }) { + const memberLabel = `${channel.memberCount} ${ + channel.memberCount === 1 ? "member" : "members" + }`; + return ( - - ) : null} -
- + + + {!channel.isMember && onJoin ? ( + + ) : null} +
); } diff --git a/desktop/src/features/messages/lib/useRichTextEditor.ts b/desktop/src/features/messages/lib/useRichTextEditor.ts index e293aa9ab..0a4885cd5 100644 --- a/desktop/src/features/messages/lib/useRichTextEditor.ts +++ b/desktop/src/features/messages/lib/useRichTextEditor.ts @@ -321,9 +321,12 @@ export function useRichTextEditor({ ], editorProps: { attributes: { + autocapitalize: "none", + autocorrect: "off", class: "min-h-0 resize-none overflow-y-hidden border-0 bg-transparent px-0 py-0 text-sm leading-6 md:leading-6 shadow-none focus-visible:ring-0 caret-foreground outline-hidden prose-sm max-w-none", "data-testid": "message-input", + spellcheck: "false", }, // ArrowUp in an empty composer → edit your last message (Slack // parity). Handled here in ProseMirror's own DOM `keydown` hook — diff --git a/desktop/src/features/search/ui/ChannelFindBar.tsx b/desktop/src/features/search/ui/ChannelFindBar.tsx index 0e306a13e..3babd14cb 100644 --- a/desktop/src/features/search/ui/ChannelFindBar.tsx +++ b/desktop/src/features/search/ui/ChannelFindBar.tsx @@ -62,6 +62,8 @@ export function ChannelFindBar({
onQueryChange(event.target.value)} onKeyDown={handleKeyDown} placeholder="Find in channel" + spellCheck={false} type="text" value={query} /> diff --git a/desktop/src/features/sidebar/ui/CreateChannelDialog.tsx b/desktop/src/features/sidebar/ui/CreateChannelDialog.tsx index 9dcf5f411..907d35cfd 100644 --- a/desktop/src/features/sidebar/ui/CreateChannelDialog.tsx +++ b/desktop/src/features/sidebar/ui/CreateChannelDialog.tsx @@ -1,17 +1,22 @@ -import { Lock, Zap } from "lucide-react"; +import { ClockFading, Hash } from "lucide-react"; import * as React from "react"; import { useChannelTemplatesQuery } from "@/features/channel-templates/hooks"; import type { ChannelTemplate, ChannelVisibility } from "@/shared/api/types"; +import { cn } from "@/shared/lib/cn"; import { Button } from "@/shared/ui/button"; import { ChooserDialogContent } from "@/shared/ui/chooser-dialog-content"; import { Dialog } from "@/shared/ui/dialog"; import { Input } from "@/shared/ui/input"; -import { Switch } from "@/shared/ui/switch"; +import { Tabs, TabsList, TabsTrigger } from "@/shared/ui/tabs"; import { Textarea } from "@/shared/ui/textarea"; /** Default TTL for ephemeral channels: 1 day of inactivity. */ const EPHEMERAL_TTL_SECONDS = 86400; +const CREATE_FIELD_SHELL_CLASS = + "rounded-xl border border-input bg-background shadow-xs transition-colors duration-150 ease-out hover:border-muted-foreground/40 hover:bg-muted/70 focus-within:border-muted-foreground/50 focus-within:bg-muted/70"; +const CREATE_FIELD_CONTROL_CLASS = + "border-0 bg-transparent shadow-none outline-none ring-0 placeholder:text-muted-foreground/45 focus:bg-transparent focus:outline-hidden focus-visible:ring-0"; type ChannelKind = "stream" | "forum"; @@ -136,7 +141,10 @@ export function CreateChannelDialog({ >
{ void handleSubmit(event); @@ -179,24 +187,34 @@ export function CreateChannelDialog({ > Name - { - setName(event.target.value); - setErrorMessage(null); - }} - placeholder={ - channelKind === "forum" ? "design-discussions" : "release-notes" - } - ref={nameInputRef} - spellCheck={false} - value={name} - /> +
+ { + setName(event.target.value); + setErrorMessage(null); + }} + placeholder={ + channelKind === "forum" + ? "design-discussions" + : "release-notes" + } + ref={nameInputRef} + spellCheck={false} + value={name} + /> +
{/* Description */} @@ -210,91 +228,136 @@ export function CreateChannelDialog({ (optional) -