chore(desktop): zero eslint/typecheck debt + prettier pass (#39100)

- eslint --fix across src/ and electron/ (unused imports, import/prop sort, padding)
- flatten empty catch blocks in electron CJS; drop unused applyUpdatesPosixInApp arg
- add setMutableRef helper for imperative ref writes (react-compiler clean)
- move sidebar cookie persistence into an effect; extract scrollElementToBottom helper
This commit is contained in:
brooklyn!
2026-06-04 14:10:38 +00:00
committed by GitHub
parent 3858cf4307
commit e003c53b06
58 changed files with 685 additions and 427 deletions
@@ -1,7 +1,18 @@
import { ThreadPrimitive, useAuiEvent, useAuiState } from '@assistant-ui/react'
import { useVirtualizer, type Virtualizer } from '@tanstack/react-virtual'
import { type ComponentProps, type FC, memo, type ReactNode, useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react'
import {
type ComponentProps,
type FC,
memo,
type ReactNode,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef
} from 'react'
import { setMutableRef } from '@/lib/mutable-ref'
import { cn } from '@/lib/utils'
import { setThreadScrolledUp } from '@/store/thread-scroll'
@@ -66,6 +77,7 @@ const VirtualizedThreadInner: FC<VirtualizedThreadProps> = ({
const messageSignature = useAuiState(s =>
s.thread.messages.map((message, index) => `${index}:${message.id}:${message.role}`).join('\n')
)
const isRunning = useAuiState(s => s.thread.isRunning)
const groups = useMemo(() => buildGroups(messageSignature), [messageSignature])
@@ -93,6 +105,7 @@ const VirtualizedThreadInner: FC<VirtualizedThreadProps> = ({
// then jumps back up).
scrollToFn: (offset, _options, instance) => {
const el = instance.scrollElement
if (!el) {
return
}
@@ -202,6 +215,10 @@ const VirtualizedThreadInner: FC<VirtualizedThreadProps> = ({
export const VirtualizedThread = memo(VirtualizedThreadInner)
function scrollElementToBottom(el: HTMLDivElement) {
el.scrollTop = el.scrollHeight
}
interface ScrollAnchorOptions {
enabled: boolean
groupCount: number
@@ -250,14 +267,14 @@ function useThreadScrollAnchor({
// Hold the disarm gate across the scroll event the next line will fire.
programmaticScrollPendingRef.current += 1
el.scrollTop = el.scrollHeight
scrollElementToBottom(el)
lastTopRef.current = el.scrollTop
lastHeightRef.current = el.scrollHeight
lastClientHeightRef.current = el.clientHeight
}, [scrollerRef])
const jumpToBottom = useCallback(() => {
stickyBottomRef.current = true
setMutableRef(stickyBottomRef, true)
if (groupCount > 0) {
virtualizer.scrollToIndex(groupCount - 1, { align: 'end', behavior: 'auto' })
@@ -282,7 +299,7 @@ function useThreadScrollAnchor({
}
const disarm = () => {
stickyBottomRef.current = false
setMutableRef(stickyBottomRef, false)
programmaticScrollPendingRef.current = 0
}
@@ -302,7 +319,7 @@ function useThreadScrollAnchor({
lastHeightRef.current = el.scrollHeight
lastClientHeightRef.current = el.clientHeight
// Always re-arm — sticky-bottom should hold through clamp races.
stickyBottomRef.current = true
setMutableRef(stickyBottomRef, true)
const atBottom = el.scrollHeight - (top + el.clientHeight) <= AT_BOTTOM_THRESHOLD
setThreadScrolledUp(!atBottom)
@@ -317,8 +334,9 @@ function useThreadScrollAnchor({
// their own listeners below, so real user intent remains covered.
const heightGrew = el.scrollHeight > lastHeightRef.current
const clientHeightChanged = Math.abs(el.clientHeight - lastClientHeightRef.current) > 1
if (!heightGrew && !clientHeightChanged && top + 1 < lastTopRef.current) {
stickyBottomRef.current = false
setMutableRef(stickyBottomRef, false)
}
lastTopRef.current = top
@@ -328,7 +346,7 @@ function useThreadScrollAnchor({
const atBottom = el.scrollHeight - (top + el.clientHeight) <= AT_BOTTOM_THRESHOLD
if (atBottom) {
stickyBottomRef.current = true
setMutableRef(stickyBottomRef, true)
}
setThreadScrolledUp(!atBottom)
@@ -369,13 +387,16 @@ function useThreadScrollAnchor({
}
let pinRafScheduled = false
const schedulePin = () => {
if (pinRafScheduled || !stickyBottomRef.current) {
return
}
pinRafScheduled = true
requestAnimationFrame(() => {
pinRafScheduled = false
if (stickyBottomRef.current) {
pinToBottom()
}
@@ -429,6 +450,7 @@ function useThreadScrollAnchor({
if (!enabled) {
return
}
if (groupCount > prevGroupCountForLayoutRef.current && stickyBottomRef.current) {
// Defer to rAF so that browser scroll/wheel events from the current
// frame are processed first. Without this deferral, a trackpad
@@ -442,6 +464,7 @@ function useThreadScrollAnchor({
}
})
}
prevGroupCountForLayoutRef.current = groupCount
}, [enabled, groupCount, pinToBottom, stickyBottomRef])
@@ -12,12 +12,7 @@ import {
DialogHeader,
DialogTitle
} from '@/components/ui/dialog'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
import { triggerHaptic } from '@/lib/haptics'
import { ChevronDown, Loader2 } from '@/lib/icons'
import { $gateway } from '@/store/gateway'
@@ -33,9 +33,7 @@ export function DisclosureRow({
// max-w-fit so the click target hugs the title text width — no
// background fill, just the cursor + the affordance caret.
'flex min-w-0 max-w-fit items-start gap-1.5 text-left transition-colors',
onToggle
? 'hover:text-foreground focus-visible:text-foreground focus-visible:outline-none'
: 'cursor-default'
onToggle ? 'hover:text-foreground focus-visible:text-foreground focus-visible:outline-none' : 'cursor-default'
)}
disabled={!onToggle}
onClick={onToggle}
@@ -1,8 +1,6 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import { Button } from '@/components/ui/button'
import { AlertTriangle, Check, ChevronDown, ChevronRight, Loader2 } from '@/lib/icons'
import { cn } from '@/lib/utils'
import type {
DesktopBootstrapEvent,
DesktopBootstrapStageDescriptor,
@@ -10,6 +8,8 @@ import type {
DesktopBootstrapStageState,
DesktopBootstrapState
} from '@/global'
import { AlertTriangle, Check, ChevronDown, ChevronRight, Loader2 } from '@/lib/icons'
import { cn } from '@/lib/utils'
/**
* DesktopInstallOverlay
@@ -59,7 +59,10 @@ const STATE_LABEL: Record<DesktopBootstrapStageState, string> = {
function formatStageName(name: string): string {
// 'system-packages' -> 'System packages'; 'uv' stays 'uv'
if (name.length <= 3) return name
if (name.length <= 3) {
return name
}
return name
.split('-')
.map((word, i) => (i === 0 ? word.charAt(0).toUpperCase() + word.slice(1) : word))
@@ -67,38 +70,61 @@ function formatStageName(name: string): string {
}
function formatDuration(ms: number | null | undefined): string {
if (typeof ms !== 'number' || !Number.isFinite(ms)) return ''
if (ms < 1000) return `${ms} ms`
if (typeof ms !== 'number' || !Number.isFinite(ms)) {
return ''
}
if (ms < 1000) {
return `${ms} ms`
}
const s = ms / 1000
if (s < 60) return `${s.toFixed(1)}s`
if (s < 60) {
return `${s.toFixed(1)}s`
}
const m = Math.floor(s / 60)
const rs = Math.round(s - m * 60)
return `${m}m ${rs}s`
}
// Live elapsed for a running stage, as m:ss (or s for sub-minute).
function formatElapsed(ms: number): string {
const s = Math.max(0, Math.floor(ms / 1000))
if (s < 60) return `${s}s`
if (s < 60) {
return `${s}s`
}
const m = Math.floor(s / 60)
return `${m}:${String(s - m * 60).padStart(2, '0')}`
}
function StageRow({ descriptor, result, isCurrent, now }: StageRowProps) {
const state: DesktopBootstrapStageState = result?.state || 'pending'
const elapsed =
state === 'running' && typeof result?.startedAt === 'number' ? formatElapsed(now - result.startedAt) : ''
const icon = useMemo(() => {
switch (state) {
case 'running':
return <Loader2 className="h-4 w-4 animate-spin text-primary" />
case 'succeeded':
return <Check className="h-4 w-4 text-emerald-600" />
case 'skipped':
return <Check className="h-4 w-4 text-muted-foreground" />
case 'failed':
return <AlertTriangle className="h-4 w-4 text-destructive" />
case 'pending':
default:
return <div className="h-2 w-2 rounded-full border border-muted-foreground/40" />
}
@@ -146,9 +172,11 @@ const EMPTY_STATE: DesktopBootstrapState = {
function applyEvent(state: DesktopBootstrapState, ev: DesktopBootstrapEvent): DesktopBootstrapState {
if (ev.type === 'manifest') {
const stages: Record<string, DesktopBootstrapStageResult> = {}
for (const stage of ev.stages) {
stages[stage.name] = { state: 'pending', durationMs: null, startedAt: null, json: null, error: null }
}
return {
...state,
active: true,
@@ -158,8 +186,10 @@ function applyEvent(state: DesktopBootstrapState, ev: DesktopBootstrapEvent): De
startedAt: state.startedAt || Date.now()
}
}
if (ev.type === 'stage') {
const prev = state.stages[ev.name]
return {
...state,
stages: {
@@ -176,17 +206,25 @@ function applyEvent(state: DesktopBootstrapState, ev: DesktopBootstrapEvent): De
}
}
}
if (ev.type === 'log') {
const next = state.log.concat({ ts: Date.now(), stage: ev.stage ?? null, line: ev.line, stream: ev.stream })
while (next.length > 500) next.shift()
while (next.length > 500) {
next.shift()
}
return { ...state, log: next }
}
if (ev.type === 'complete') {
return { ...state, active: false, completedAt: Date.now(), error: null }
}
if (ev.type === 'failed') {
return { ...state, active: false, error: ev.error || 'unknown error' }
}
if (ev.type === 'unsupported-platform') {
return {
...state,
@@ -199,6 +237,7 @@ function applyEvent(state: DesktopBootstrapState, ev: DesktopBootstrapEvent): De
}
}
}
return state
}
@@ -213,23 +252,35 @@ export function DesktopInstallOverlay({ enabled = true }: DesktopInstallOverlayP
// Tick once a second while a bootstrap is in flight so running steps show a
// live elapsed timer. Stops when nothing is active to avoid idle renders.
useEffect(() => {
if (!state.active) return
if (!state.active) {
return
}
const id = window.setInterval(() => setNow(Date.now()), 1000)
return () => window.clearInterval(id)
}, [state.active])
// Subscribe to bootstrap events + load initial snapshot
useEffect(() => {
if (!enabled) return
if (!enabled) {
return
}
const desktop = window.hermesDesktop
if (!desktop || typeof desktop.onBootstrapEvent !== 'function') return
if (!desktop || typeof desktop.onBootstrapEvent !== 'function') {
return
}
let cancelled = false
desktop
.getBootstrapState()
.then(snapshot => {
if (!cancelled && snapshot) setState(snapshot)
if (!cancelled && snapshot) {
setState(snapshot)
}
})
.catch(() => {
// Older Electron build without the IPC handler -- bootstrap UI just
@@ -237,6 +288,7 @@ export function DesktopInstallOverlay({ enabled = true }: DesktopInstallOverlayP
})
const off = desktop.onBootstrapEvent(ev => setState(prev => applyEvent(prev, ev)))
return () => {
cancelled = true
off?.()
@@ -255,21 +307,37 @@ export function DesktopInstallOverlay({ enabled = true }: DesktopInstallOverlayP
// the top-level error message and the user has to click "Show installer
// output" to see WHY the stage failed.
useEffect(() => {
if (state.error) setLogOpen(true)
if (state.error) {
setLogOpen(true)
}
}, [state.error])
// Mount logic: show whenever a bootstrap is in flight, completed-with-error,
// or actively running with a manifest. Hide entirely after a successful
// completion so the rest of the UI can take over.
const shouldShow = useMemo(() => {
if (!enabled) return false
if (state.active) return true
if (state.error) return true
if (state.unsupportedPlatform) return true
if (!enabled) {
return false
}
if (state.active) {
return true
}
if (state.error) {
return true
}
if (state.unsupportedPlatform) {
return true
}
return false
}, [enabled, state.active, state.error, state.unsupportedPlatform])
if (!shouldShow) return null
if (!shouldShow) {
return null
}
// Unsupported-platform branch: macOS/Linux packaged builds hit this when
// there's no Hermes Agent installed yet and we can't drive install.sh
@@ -278,6 +346,7 @@ export function DesktopInstallOverlay({ enabled = true }: DesktopInstallOverlayP
if (state.unsupportedPlatform) {
const ups = state.unsupportedPlatform
const platformLabel = ups.platform === 'darwin' ? 'macOS' : ups.platform === 'linux' ? 'Linux' : ups.platform
return (
<div className="fixed inset-0 z-[1400] flex items-center justify-center bg-background/90 backdrop-blur-md">
<div className="w-full max-w-xl rounded-xl border bg-card p-8 shadow-xl">
@@ -294,20 +363,20 @@ export function DesktopInstallOverlay({ enabled = true }: DesktopInstallOverlayP
</pre>
<div className="mt-2 flex items-center gap-2">
<Button
variant="secondary"
size="sm"
onClick={() => {
void navigator.clipboard?.writeText(ups.installCommand).catch(() => {})
}}
size="sm"
variant="secondary"
>
Copy command
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => {
window.hermesDesktop?.openExternal?.(ups.docsUrl)
}}
size="sm"
variant="ghost"
>
View install docs
</Button>
@@ -318,7 +387,7 @@ export function DesktopInstallOverlay({ enabled = true }: DesktopInstallOverlayP
<span className="text-xs text-muted-foreground">
Will install to <code className="rounded bg-muted/50 px-1 py-0.5 font-mono">{ups.activeRoot}</code>
</span>
<Button variant="default" size="sm" onClick={() => window.location.reload()}>
<Button onClick={() => window.location.reload()} size="sm" variant="default">
I{'\u2019'}ve run it -- retry
</Button>
</div>
@@ -329,9 +398,11 @@ export function DesktopInstallOverlay({ enabled = true }: DesktopInstallOverlayP
const stages = state.manifest?.stages || []
const currentStage = stages.find(s => state.stages[s.name]?.state === 'running')?.name
const completedCount = stages.filter(
s => state.stages[s.name]?.state === 'succeeded' || state.stages[s.name]?.state === 'skipped'
).length
const totalCount = stages.length
const failed = Boolean(state.error)
const progressPct = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0
@@ -396,11 +467,11 @@ export function DesktopInstallOverlay({ enabled = true }: DesktopInstallOverlayP
<ol className="mb-4 space-y-1">
{stages.map(stage => (
<StageRow
key={stage.name}
descriptor={stage}
result={state.stages[stage.name]}
isCurrent={stage.name === currentStage}
key={stage.name}
now={now}
result={state.stages[stage.name]}
/>
))}
</ol>
@@ -408,9 +479,9 @@ export function DesktopInstallOverlay({ enabled = true }: DesktopInstallOverlayP
<div className="border-t pt-3">
<button
type="button"
onClick={() => setLogOpen(v => !v)}
className="flex items-center gap-1.5 text-xs text-muted-foreground transition-colors hover:text-foreground"
onClick={() => setLogOpen(v => !v)}
type="button"
>
{logOpen ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
<span>{logOpen ? 'Hide installer output' : 'Show installer output'}</span>
@@ -432,8 +503,11 @@ export function DesktopInstallOverlay({ enabled = true }: DesktopInstallOverlayP
<>
{state.log.map((entry, i) => (
<div
className={cn(
'whitespace-pre-wrap break-words',
entry.stream === 'stderr' && 'text-muted-foreground'
)}
key={i}
className={cn('whitespace-pre-wrap break-words', entry.stream === 'stderr' && 'text-muted-foreground')}
>
{entry.stage ? <span className="text-muted-foreground/70">[{entry.stage}] </span> : null}
<span>{entry.line}</span>
@@ -482,13 +556,13 @@ export function DesktopInstallOverlay({ enabled = true }: DesktopInstallOverlayP
</span>
<div className="flex gap-2">
<Button
variant="secondary"
size="sm"
onClick={async () => {
const text = state.log
.map(entry => (entry.stage ? `[${entry.stage}] ${entry.line}` : entry.line))
.join('\n')
const fullText = state.error ? `Error: ${state.error}\n\n${text}` : text
try {
await navigator.clipboard.writeText(fullText)
setCopied(true)
@@ -497,12 +571,12 @@ export function DesktopInstallOverlay({ enabled = true }: DesktopInstallOverlayP
// ignore -- some environments forbid clipboard writes
}
}}
size="sm"
variant="secondary"
>
{copied ? 'Copied!' : 'Copy output'}
</Button>
<Button
variant="default"
size="sm"
onClick={async () => {
// Tell main.cjs to clear its latched failure BEFORE we
// reload. Otherwise the renderer reload calls getConnection
@@ -513,8 +587,11 @@ export function DesktopInstallOverlay({ enabled = true }: DesktopInstallOverlayP
} catch {
// best-effort -- continue with reload regardless
}
window.location.reload()
}}
size="sm"
variant="default"
>
Reload and retry
</Button>
@@ -1,9 +1,8 @@
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it } from 'vitest'
import type { OAuthProvider } from '@/types/hermes'
import { $desktopOnboarding, type DesktopOnboardingState, type OnboardingContext } from '@/store/onboarding'
import type { OAuthProvider } from '@/types/hermes'
import { Picker } from './desktop-onboarding-overlay'
@@ -584,7 +584,9 @@ export function ApiKeyForm({
className="font-mono"
onChange={e => setValue(e.target.value)}
onKeyDown={e => e.key === 'Enter' && void submit()}
placeholder={currentRedacted ?? (alreadySet ? 'Replace current value' : option.placeholder || 'Paste API key')}
placeholder={
currentRedacted ?? (alreadySet ? 'Replace current value' : option.placeholder || 'Paste API key')
}
type={isLocal ? 'text' : 'password'}
value={value}
/>
@@ -676,8 +678,8 @@ function FlowPanel({ ctx, flow }: { ctx: OnboardingContext; flow: OnboardingFlow
return (
<Step title={`Sign in with ${title}`}>
<p className="text-sm text-muted-foreground">
We opened {title} in your browser. Authorize Hermes there and you'll be connected
automatically — nothing to copy or paste.
We opened {title} in your browser. Authorize Hermes there and you'll be connected automatically — nothing to
copy or paste.
</p>
<FlowFooter left={<DocsLink href={flow.start.auth_url}>Re-open sign-in page</DocsLink>}>
<span className="flex items-center gap-2 text-xs text-muted-foreground">
@@ -65,10 +65,7 @@ function RootErrorFallback({ error, reset }: ErrorBoundaryFallbackProps) {
<Button onClick={() => window.location.reload()} variant="text">
Reload window
</Button>
<Button
onClick={() => void window.hermesDesktop?.revealLogs()?.catch(() => undefined)}
variant="text"
>
<Button onClick={() => void window.hermesDesktop?.revealLogs()?.catch(() => undefined)} variant="text">
Open logs
</Button>
</ErrorState>
@@ -185,6 +185,7 @@ function ModelResults({
}
const q = search.trim().toLowerCase()
const matches = (provider: ModelOptionProvider, model: string) =>
!q ||
model.toLowerCase().includes(q) ||
@@ -25,7 +25,13 @@ interface ModelVisibilityDialogProps {
sessionId?: string | null
}
export function ModelVisibilityDialog({ gw, onOpenChange, onOpenProviders, open, sessionId }: ModelVisibilityDialogProps) {
export function ModelVisibilityDialog({
gw,
onOpenChange,
onOpenProviders,
open,
sessionId
}: ModelVisibilityDialogProps) {
const [search, setSearch] = useState('')
const stored = useStore($visibleModels)
@@ -87,17 +93,11 @@ export function ModelVisibilityDialog({ gw, onOpenChange, onOpenProviders, open,
<div className="max-h-[55vh] overflow-y-auto pb-1">
{providers.length === 0 ? (
<div className="px-3 py-5 text-center text-xs text-muted-foreground">
{modelOptions.isPending ? (
<BrailleSpinner className="mx-auto text-sm" />
) : (
'No authenticated providers.'
)}
{modelOptions.isPending ? <BrailleSpinner className="mx-auto text-sm" /> : 'No authenticated providers.'}
</div>
) : (
providers.map(provider => {
const models = collapseModelFamilies(provider.models ?? []).filter(family =>
matches(provider, family.id)
)
const models = collapseModelFamilies(provider.models ?? []).filter(family => matches(provider, family.id))
if (models.length === 0) {
return null
@@ -121,10 +121,7 @@ export function ModelVisibilityDialog({ gw, onOpenChange, onOpenProviders, open,
{name}
{tag ? <span className="text-(--ui-text-tertiary)"> {tag}</span> : null}
</span>
<Switch
checked={visible.has(key)}
onCheckedChange={() => toggle(provider, family.id)}
/>
<Switch checked={visible.has(key)} onCheckedChange={() => toggle(provider, family.id)} />
</label>
)
})}
@@ -132,9 +132,7 @@ function NotificationItem({ notification }: { notification: AppNotification }) {
function NotificationDetail({ detail }: { detail: string }) {
return (
<details className="mt-2 text-xs text-muted-foreground">
<summary className="select-none font-medium text-muted-foreground hover:text-foreground">
Details
</summary>
<summary className="select-none font-medium text-muted-foreground hover:text-foreground">Details</summary>
<div className="mt-1 rounded-md border border-border/70 bg-background/65 p-2">
<pre className="max-h-32 whitespace-pre-wrap wrap-break-word font-mono text-[0.6875rem] leading-relaxed">
{detail}
@@ -4,7 +4,14 @@ import { useStore } from '@nanostores/react'
import { type FormEvent, useCallback, useEffect, useState } from 'react'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { triggerHaptic } from '@/lib/haptics'
import { KeyRound, Loader2, Lock } from '@/lib/icons'
+2 -1
View File
@@ -36,7 +36,8 @@ const buttonVariants = cva(
'icon-xs': "size-6 rounded-[4px] [&_svg:not([class*='size-'])]:size-3",
'icon-sm': 'size-8 rounded-[4px]',
'icon-lg': 'size-10 rounded-[4px]',
'icon-titlebar': 'h-(--titlebar-control-height) w-(--titlebar-control-size) rounded-[4px] [&_.codicon]:text-[0.875rem]'
'icon-titlebar':
'h-(--titlebar-control-height) w-(--titlebar-control-size) rounded-[4px] [&_.codicon]:text-[0.875rem]'
}
},
defaultVariants: {
+1 -6
View File
@@ -4,12 +4,7 @@ import { cn } from '@/lib/utils'
import { type ControlVariantProps, controlVariants } from './control'
function Input({
className,
type,
size,
...props
}: Omit<React.ComponentProps<'input'>, 'size'> & ControlVariantProps) {
function Input({ className, type, size, ...props }: Omit<React.ComponentProps<'input'>, 'size'> & ControlVariantProps) {
return (
<input
className={cn(
+4 -3
View File
@@ -73,13 +73,14 @@ function SidebarProvider({
} else {
_setOpen(openState)
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
},
[setOpenProp, open]
)
React.useEffect(() => {
document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
}, [open])
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile ? setOpenMobile(open => !open) : setOpen(open => !open)
+1 -7
View File
@@ -5,13 +5,7 @@ import { cn } from '@/lib/utils'
import { type ControlVariantProps, controlVariants } from './control'
function Textarea({ className, size, ...props }: React.ComponentProps<'textarea'> & ControlVariantProps) {
return (
<textarea
className={cn(controlVariants({ size }), 'min-h-16', className)}
data-slot="textarea"
{...props}
/>
)
return <textarea className={cn(controlVariants({ size }), 'min-h-16', className)} data-slot="textarea" {...props} />
}
export { Textarea }