From b419af32de837f834cad7f41a2cdae863391052c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Fri, 7 Aug 2026 11:26:33 +0000 Subject: [PATCH] sweep the dead code in section 8 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each item re-verified before deleting; three of the ten entries were stale and are corrected in place rather than silently fixed. - WorkspaceLayout's isMobile/mobilePanelId/onMobileBack: none of its ten callers set them, so the mobile collapse they fed was permanently off in that renderer. WorkspaceView passes the same props to WorkspaceRenderer itself, where they are live. - fixedHeight on AppRegistryEntry, and getFixedHeight with it: no app has ever declared one, so it only contributed undefined. The flex-column branch it shared with fitContent stays, keyed on fitContent alone. - getDefaults: getAllDashboardState already folds the defaults row into the one payload the client fetches, which is why it never got a caller. - upsertScreen's terminals/hostTerminals: never read is right, never written was not — it inserted them, which is why all 15 rows hold {}. The columns are left in place; dropping them needs a db:push, and this tree holds another agent's uncommitted schema file. - SELECTED_DASHBOARD_KEY: H2 (01365cb) replaced it with ?selected= four months ago and it has had no reader since. - ui/sidebar.tsx and the stray ui/hooks/ beside it. use-mobile was not orphaned as claimed — the sidebar imported it — and the use-toast in there was a near-identical copy of the live one. - findChildById's unreachable duplicate condition, and the doc comment that described the wrong behaviour rather than the code being wrong. Left deliberately: DragOverlay/LayoutEditor (gated on 5.3, an owner decision) and the two chat-owned channels, whose docs are fixed here even though the publishers are not mine to delete. --- CLAUDE.md | 5 +- docs/navigation-audit.md | 13 +- docs/workspace-panel-todo.md | 57 +- src/databases/officer_db/src/index.ts | 1 - .../officer_db/src/queries/dashboards.ts | 20 +- .../components/ui/hooks/use-mobile.tsx | 19 - .../components/ui/hooks/use-toast.ts | 189 ------ src/workspaces/components/ui/sidebar.tsx | 638 ------------------ .../src/apps/Dashboards/constants.ts | 4 +- .../officerdev/src/apps/Dashboards/index.ts | 2 +- .../components/Workspace/WorkspaceLayout.tsx | 13 +- .../Workspace/WorkspaceRenderer.tsx | 32 +- .../src/components/Workspace/types.ts | 1 - src/workspaces/officerdev/src/index.ts | 1 - 14 files changed, 88 insertions(+), 907 deletions(-) delete mode 100644 src/workspaces/components/ui/hooks/use-mobile.tsx delete mode 100644 src/workspaces/components/ui/hooks/use-toast.ts delete mode 100644 src/workspaces/components/ui/sidebar.tsx diff --git a/CLAUDE.md b/CLAUDE.md index b857e9be..e241aab5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -253,8 +253,9 @@ in `usePanelChannel`/`useGlobal`, and has no anchor semantics (no cmd-click, no link-focusable). Half the app still does this; none of the new code should. - **Addressable state goes in the URL** (`useParams` / `useSearchParams`), never in a channel. - `usePanelChannel` is for genuine signals and refresh buses (`preview:refresh`, - `files:refresh-signal`, `SLSKD_REFRESH_CHANNEL`, `MUSIC_RESYNC_CHANNEL`). + `usePanelChannel` is for genuine signals and refresh buses (`files:refresh-signal`, + `SLSKD_REFRESH_CHANNEL`, `MUSIC_RESYNC_CHANNEL`) — and declare one with `defineChannel` in + `officerdev/src/channels.ts` rather than spelling the name and type at each site. "Which thing is open" is a URL. Panels each read the URL rather than passing it between themselves. - **Rows and nav items are real links.** `` for rows (exemplar: the `/chat` session list, `f35c145`); **react-router's ``** for nav chrome, so active state comes from the router. diff --git a/docs/navigation-audit.md b/docs/navigation-audit.md index b43b969b..78d97856 100644 --- a/docs/navigation-audit.md +++ b/docs/navigation-audit.md @@ -123,10 +123,15 @@ This is the primary surface to convert to URL-driven selection. | settings `useGlobal(*_SELECTED)` | settings section | `/settings/:page/:section` | `SettingsPanel.tsx:56` (M6) | | local `useState` selections | search / room / conversation / peer / open file / preview slug | respective nested routes | `SearchView`, `SoulseekRooms`, `SoulseekChat`, `SoulseekUsers`, `CodeEditor/useEditorState`, `PreviewProvider` | -**Excluded — event-bus / refresh signals, NOT selection:** `preview:refresh`, `files:refresh-signal`, -`SLSKD_REFRESH_CHANNEL`, `MUSIC_RESYNC_CHANNEL`, `chat:active-session` (embedded-chat signal). +**Excluded — event-bus / refresh signals, NOT selection:** `files:refresh-signal`, +`SLSKD_REFRESH_CHANNEL`, `MUSIC_RESYNC_CHANNEL`. (`FILE_VIEWER_CHANNEL` was listed here too; it had no publisher and has been deleted — the file viewer -reads `?view=` from the URL.) +reads `?view=` from the URL. `preview:refresh` and `chat:active-session` were also listed, and +`preview:refresh` was cited above as the exemplar of a *legitimate* channel — but both have a publisher +in `ChatPanelWrapper` and **no subscriber at all**, and `preview:refresh`'s reader, `PreviewProvider`, is +no longer in the repo. They are declared in `officerdev/src/channels.ts` with that stated; deleting the +publishers means changing the chat panel, which is another agent's, so it is written up in +`COMMS/chat-agent-handoff-2026-08-07.md` instead. Use `files:refresh-signal` as the exemplar.) --- @@ -177,7 +182,7 @@ reads `?view=` from the URL.) ### Phase 1 — Quick wins (routes already exist; mechanical, high value) - [x] **H1** Jobs rows → `` (`JobsPage.tsx`). Done — `46482f3`. (active-row highlight already keyed off `useParams().id`.) -- [x] **H2** Dashboards rows → ``; `SELECTED_DASHBOARD_KEY` global replaced by `?selected=` URL param across `DashboardListApp`/`DashboardsScreen`/`DashboardPreview`. Done — `01365cb`. **Needs runtime test.** +- [x] **H2** Dashboards rows → ``; `SELECTED_DASHBOARD_KEY` global replaced by `?selected=` URL param across `DashboardListApp`/`DashboardsScreen`/`DashboardPreview`. Done — `01365cb`. **Needs runtime test.** (The constant itself outlived its last reader by four months and has now been deleted; its siblings in `Dashboards/constants.ts` are dialog form state, not selection, and stay.) - [x] **H3** Projects rows → ``; `SELECTED_PROJECT` global replaced by `?selected=` URL param across `ProjectListApp`/`ProjectListScreen`/`ProjectPreview`. Done — `2aaacc8`. **Needs runtime test.** - [ ] **H4** Chat detail: read `sessionId` from `useParams`, retire `chat:selected-session` as source of truth (`ChatDetailPanel.tsx:136`) — **finishes the /chat fix**. *Deferred: overlaps the in-flight `sidecars-*` chat-comms work; do after that lands.* - [ ] **H5** Email rows → `` driven by `useParams().emailId`; delete the state↔URL sync effects (`EmailList.tsx:336`, `EmailScreen.tsx:27-33`). diff --git a/docs/workspace-panel-todo.md b/docs/workspace-panel-todo.md index 0f47b5b6..f4a289d3 100644 --- a/docs/workspace-panel-todo.md +++ b/docs/workspace-panel-todo.md @@ -836,23 +836,54 @@ _(move items here with the commit and a one-line resolution)_ Low priority, but each line here is a line someone will read and believe. +Swept 2026-08-07 (`__SWEEP__`). Every claim was re-checked against the code first; three were stale, and +they are marked below, because a dead-code list that is itself wrong is the worst kind. + - [ ] `DragOverlay.tsx` (99) + `LayoutEditor.tsx` (51) — imported by nothing. Gated on 5.3. -- [ ] `WorkspaceLayout`'s `isMobile`/`mobilePanelId`/`onMobileBack` props — no caller passes them. -- [ ] `fixedHeight` on `AppRegistryEntry` — declared, read at `WorkspaceRenderer.tsx:76`, set by **zero** - metas. -- [ ] `registerApp` (`useAppRegistry`) — zero callers. -- [ ] `getDefaults` (`queries/dashboards.ts:136`) — zero callers. -- [ ] `screens.terminals` / `screens.hostTerminals` columns — never written, never read. -- [ ] `SELECTED_DASHBOARD` constant — zero consumers. + **Deliberately left**: 5.3 is an owner decision ("decide, then act"), and deleting these makes it + for him. +- [x] `WorkspaceLayout`'s `isMobile`/`mobilePanelId`/`onMobileBack` props — no caller passes them. + Confirmed across all ten call sites; removed. `WorkspaceView` still passes the same props to + `WorkspaceRenderer`, where they are live — it was only the `WorkspaceLayout` pass-through that was + dead. +- [x] `fixedHeight` on `AppRegistryEntry` — declared, read at `WorkspaceRenderer.tsx:76`, set by **zero** + metas. Removed, along with `getFixedHeight` and the pixel-height branch it fed; the flex-column + layout it shared with `fitContent` stays, now keyed on `fitContent` alone. +- [x] `registerApp` (`useAppRegistry`) — zero callers. Already deleted in `9fcc9c2` (§5.6), with + `registerWidget`. +- [x] `getDefaults` (~~`queries/dashboards.ts:136`~~ `:195` — **line was stale**) — zero callers. + Removed. `getAllDashboardState` already folds the defaults row into the one state payload, which + is why the single-row getter never acquired one. +- [x] `screens.terminals` / `screens.hostTerminals` columns — never read (confirmed), **but "never + written" was stale**: `upsertScreen` accepted and inserted them, so all 15 rows hold the `{}` it + wrote. The dead parameters and inserts are gone. **The columns themselves are not dropped** — that + needs `bun db:push`, which diffs the *whole* schema, and this tree currently holds another agent's + uncommitted `schema/agent-panels.ts`. Drop them in a push of their own. +- [x] ~~`SELECTED_DASHBOARD`~~ **`SELECTED_DASHBOARD_KEY`** constant — zero consumers. The parenthetical + claiming `SELECTED_DASHBOARD_KEY` was the live one was **backwards**: `'SELECTED_DASHBOARD'` is the + string value, `SELECTED_DASHBOARD_KEY` the constant, and H2 (`01365cb`) took its last reader four + months ago. Deleted, with its two barrel re-exports. Its file-mates survive — they are dialog form + state, not selection. - [ ] `preview:refresh` and `chat:active-session` channels — written by `ChatPanelWrapper.tsx:58-59`, - **read by nobody**. Note `preview:refresh` is the exemplar `docs/navigation-audit.md:126` cites as - the canonical _good_ channel; fix the doc too. -- [ ] `components/ui/hooks/use-mobile.tsx` + `ui/sidebar.tsx` (~720 lines) — a second `useIsMobile` - implementation, and a sidebar imported by nothing. -- [ ] `WorkspaceRenderer.tsx:84` — unreachable duplicate condition inside `findChildById`. While there: + **read by nobody**. **Deliberately left**: deleting the publishers changes the chat panel, which is + another agent's. Declared with the fact stated in `officerdev/src/channels.ts` and written up in + `COMMS/chat-agent-handoff-2026-08-07.md` §6. The doc half **is** done — + `docs/navigation-audit.md:126` and `CLAUDE.md` no longer offer `preview:refresh` as the exemplar + channel; `files:refresh-signal` is. +- [x] `components/ui/hooks/use-mobile.tsx` + `ui/sidebar.tsx` (~720 lines) — a second `useIsMobile` + implementation, and a sidebar imported by nothing. Both deleted. **Nuance the entry missed**: + `use-mobile.tsx` was not orphaned, it was imported by `sidebar.tsx` — dead only once the sidebar + went. The whole `ui/hooks/` directory turned out to be a stray shadcn copy, its `use-toast.ts` a + near-identical duplicate of the live `ui/use-toast.ts`, referenced by nothing; it went too. + _Noticed, not fixed:_ `ui/toaster.tsx` + `ui/use-toast.ts` are their own dead chain (the app uses + `sonner`), and `globals.css` carries eight `--sidebar-*` tokens now used by nothing. +- [x] `WorkspaceRenderer.tsx:84` — unreachable duplicate condition inside `findChildById`. While there: the doc comment says "find a panel node by id anywhere in the tree", but it returns _the direct child whose subtree contains the id_, which is what the mobile collapse needs. **The behaviour is - right and the description is wrong** — fix the comment, not the code. + right and the description is wrong** — fix the comment, not the code. Done exactly that. (Worth + recording: a reviewer sent at this function without the entry in hand reported the `return child` + as the bug and the dead line as incidental. The entry is right and it is the reason the code + survived review.) --- diff --git a/src/databases/officer_db/src/index.ts b/src/databases/officer_db/src/index.ts index cd12995d..953bc9b5 100644 --- a/src/databases/officer_db/src/index.ts +++ b/src/databases/officer_db/src/index.ts @@ -61,7 +61,6 @@ export { setDashboardPanelState, upsertScreen, deleteScreen, - getDefaults, upsertDefaults, setDefaultsPanelState, } from './queries/dashboards'; diff --git a/src/databases/officer_db/src/queries/dashboards.ts b/src/databases/officer_db/src/queries/dashboards.ts index 0cec5fb4..a79cc036 100644 --- a/src/databases/officer_db/src/queries/dashboards.ts +++ b/src/databases/officer_db/src/queries/dashboards.ts @@ -152,18 +152,20 @@ export async function deleteDashboard(userId: number, id: string): Promise // ── Screen CRUD ── +// No `terminals`/`hostTerminals` here, unlike `UpsertDefaultsData` below. A screen's terminal state +// resolves to the `-default` key — the `dashboard_defaults` row — so the two same-named columns on +// `screens` have never been read by anything, and the only caller (`api/dashboards`) has never passed +// them: every one of the 15 rows holds the `{}` the column default wrote. The columns still exist; they +// are not dropped here because `db:push` diffs the whole schema and there is another agent's uncommitted +// schema file in this tree. type UpsertScreenData = { layout?: unknown; - terminals?: unknown; - hostTerminals?: unknown; }; export async function upsertScreen(userId: number, name: string, data: UpsertScreenData): Promise { const now = new Date(); const set: Record = { updatedAt: now }; if (data.layout !== undefined) set.layout = data.layout; - if (data.terminals !== undefined) set.terminals = data.terminals; - if (data.hostTerminals !== undefined) set.hostTerminals = data.hostTerminals; await db .insert(screens) @@ -171,8 +173,6 @@ export async function upsertScreen(userId: number, name: string, data: UpsertScr userId, name, layout: (data.layout ?? null) as never, - terminals: (data.terminals ?? {}) as never, - hostTerminals: (data.hostTerminals ?? {}) as never, updatedAt: now, }) .onConflictDoUpdate({ @@ -192,11 +192,9 @@ type UpsertDefaultsData = { hostTerminals?: unknown; }; -export async function getDefaults(userId: number): Promise<{ terminals: unknown; hostTerminals: unknown }> { - const [row] = await db.select().from(dashboardDefaults).where(eq(dashboardDefaults.userId, userId)); - return { terminals: row?.terminals ?? {}, hostTerminals: row?.hostTerminals ?? {} }; -} - +// No `getDefaults`. Nothing read the defaults row on its own: `getAllDashboardState` above already folds +// it into the one state payload the client fetches, which is why the single-row getter never acquired a +// caller. `upsertDefaults` below is live (`api/dashboards`). export async function upsertDefaults(userId: number, data: UpsertDefaultsData): Promise { const now = new Date(); const set: Record = { updatedAt: now }; diff --git a/src/workspaces/components/ui/hooks/use-mobile.tsx b/src/workspaces/components/ui/hooks/use-mobile.tsx deleted file mode 100644 index ba553c6f..00000000 --- a/src/workspaces/components/ui/hooks/use-mobile.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import * as React from 'react'; - -const MOBILE_BREAKPOINT = 768; - -export function useIsMobile() { - const [isMobile, setIsMobile] = React.useState(undefined); - - React.useEffect(() => { - const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); - const onChange = () => { - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); - }; - mql.addEventListener('change', onChange); - setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); - return () => mql.removeEventListener('change', onChange); - }, []); - - return !!isMobile; -} diff --git a/src/workspaces/components/ui/hooks/use-toast.ts b/src/workspaces/components/ui/hooks/use-toast.ts deleted file mode 100644 index 5eb0ac36..00000000 --- a/src/workspaces/components/ui/hooks/use-toast.ts +++ /dev/null @@ -1,189 +0,0 @@ -'use client'; - -// Inspired by react-hot-toast library -import * as React from 'react'; - -import type { ToastActionElement, ToastProps } from '@/components/ui/toast'; - -const TOAST_LIMIT = 1; -const TOAST_REMOVE_DELAY = 1000000; - -type ToasterToast = ToastProps & { - id: string; - title?: React.ReactNode; - description?: React.ReactNode; - action?: ToastActionElement; -}; - -const actionTypes = { - ADD_TOAST: 'ADD_TOAST', - UPDATE_TOAST: 'UPDATE_TOAST', - DISMISS_TOAST: 'DISMISS_TOAST', - REMOVE_TOAST: 'REMOVE_TOAST', -} as const; - -let count = 0; - -function genId() { - count = (count + 1) % Number.MAX_SAFE_INTEGER; - return count.toString(); -} - -type ActionType = typeof actionTypes; - -type Action = - | { - type: ActionType['ADD_TOAST']; - toast: ToasterToast; - } - | { - type: ActionType['UPDATE_TOAST']; - toast: Partial; - } - | { - type: ActionType['DISMISS_TOAST']; - toastId?: ToasterToast['id']; - } - | { - type: ActionType['REMOVE_TOAST']; - toastId?: ToasterToast['id']; - }; - -interface State { - toasts: ToasterToast[]; -} - -const toastTimeouts = new Map>(); - -const addToRemoveQueue = (toastId: string) => { - if (toastTimeouts.has(toastId)) { - return; - } - - const timeout = setTimeout(() => { - toastTimeouts.delete(toastId); - dispatch({ - type: 'REMOVE_TOAST', - toastId: toastId, - }); - }, TOAST_REMOVE_DELAY); - - toastTimeouts.set(toastId, timeout); -}; - -export const reducer = (state: State, action: Action): State => { - switch (action.type) { - case 'ADD_TOAST': - return { - ...state, - toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), - }; - - case 'UPDATE_TOAST': - return { - ...state, - toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)), - }; - - case 'DISMISS_TOAST': { - const { toastId } = action; - - // ! Side effects ! - This could be extracted into a dismissToast() action, - // but I'll keep it here for simplicity - if (toastId) { - addToRemoveQueue(toastId); - } else { - state.toasts.forEach((toast) => { - addToRemoveQueue(toast.id); - }); - } - - return { - ...state, - toasts: state.toasts.map((t) => - t.id === toastId || toastId === undefined - ? { - ...t, - open: false, - } - : t, - ), - }; - } - case 'REMOVE_TOAST': - if (action.toastId === undefined) { - return { - ...state, - toasts: [], - }; - } - return { - ...state, - toasts: state.toasts.filter((t) => t.id !== action.toastId), - }; - } -}; - -const listeners: Array<(state: State) => void> = []; - -let memoryState: State = { toasts: [] }; - -function dispatch(action: Action) { - memoryState = reducer(memoryState, action); - listeners.forEach((listener) => { - listener(memoryState); - }); -} - -type Toast = Omit; - -function toast({ ...props }: Toast) { - const id = genId(); - - const update = (props: ToasterToast) => - dispatch({ - type: 'UPDATE_TOAST', - toast: { ...props, id }, - }); - const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id }); - - dispatch({ - type: 'ADD_TOAST', - toast: { - ...props, - id, - open: true, - onOpenChange: (open) => { - if (!open) dismiss(); - }, - }, - }); - - return { - id: id, - dismiss, - update, - }; -} - -function useToast() { - const [state, setState] = React.useState(memoryState); - - React.useEffect(() => { - listeners.push(setState); - return () => { - const index = listeners.indexOf(setState); - if (index > -1) { - listeners.splice(index, 1); - } - }; - }, [state]); - - return { - ...state, - toast, - dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }), - }; -} - -export { useToast, toast }; diff --git a/src/workspaces/components/ui/sidebar.tsx b/src/workspaces/components/ui/sidebar.tsx deleted file mode 100644 index 33bee0a6..00000000 --- a/src/workspaces/components/ui/sidebar.tsx +++ /dev/null @@ -1,638 +0,0 @@ -import * as React from 'react'; -import { Slot } from '@radix-ui/react-slot'; -import { type VariantProps, cva } from 'class-variance-authority'; -import { PanelLeft } from 'lucide-react'; - -import { useIsMobile } from './hooks/use-mobile'; -import { cn } from '@/lib/utils'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Separator } from '@/components/ui/separator'; -import { Sheet, SheetContent } from '@/components/ui/sheet'; -import { Skeleton } from '@/components/ui/skeleton'; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; - -const SIDEBAR_COOKIE_NAME = 'sidebar:state'; -const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; -const SIDEBAR_WIDTH = '16rem'; -const SIDEBAR_WIDTH_MOBILE = '18rem'; -const SIDEBAR_WIDTH_ICON = '3rem'; -const SIDEBAR_KEYBOARD_SHORTCUT = 'b'; - -type SidebarContext = { - state: 'expanded' | 'collapsed'; - open: boolean; - setOpen: (open: boolean) => void; - openMobile: boolean; - setOpenMobile: (open: boolean) => void; - isMobile: boolean; - toggleSidebar: () => void; -}; - -const SidebarContext = React.createContext(null); - -function useSidebar() { - const context = React.useContext(SidebarContext); - if (!context) { - throw new Error('useSidebar must be used within a SidebarProvider.'); - } - - return context; -} - -const SidebarProvider = React.forwardRef< - HTMLDivElement, - React.ComponentProps<'div'> & { - defaultOpen?: boolean; - open?: boolean; - onOpenChange?: (open: boolean) => void; - } ->(({ defaultOpen = true, open: openProp, onOpenChange: setOpenProp, className, style, children, ...props }, ref) => { - const isMobile = useIsMobile(); - const [openMobile, setOpenMobile] = React.useState(false); - - // This is the internal state of the sidebar. - // We use openProp and setOpenProp for control from outside the component. - const [_open, _setOpen] = React.useState(defaultOpen); - const open = openProp ?? _open; - const setOpen = React.useCallback( - (value: boolean | ((value: boolean) => boolean)) => { - const openState = typeof value === 'function' ? value(open) : value; - if (setOpenProp) { - setOpenProp(openState); - } 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], - ); - - // Helper to toggle the sidebar. - const toggleSidebar = React.useCallback(() => { - return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open); - }, [isMobile, setOpen, setOpenMobile]); - - // Adds a keyboard shortcut to toggle the sidebar. - React.useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) { - event.preventDefault(); - toggleSidebar(); - } - }; - - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, [toggleSidebar]); - - // We add a state so that we can do data-state="expanded" or "collapsed". - // This makes it easier to style the sidebar with Tailwind classes. - const state = open ? 'expanded' : 'collapsed'; - - const contextValue = React.useMemo( - () => ({ - state, - open, - setOpen, - isMobile, - openMobile, - setOpenMobile, - toggleSidebar, - }), - [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar], - ); - - return ( - - -
- {children} -
-
-
- ); -}); -SidebarProvider.displayName = 'SidebarProvider'; - -const Sidebar = React.forwardRef< - HTMLDivElement, - React.ComponentProps<'div'> & { - side?: 'left' | 'right'; - variant?: 'sidebar' | 'floating' | 'inset'; - collapsible?: 'offcanvas' | 'icon' | 'none'; - } ->(({ side = 'left', variant = 'sidebar', collapsible = 'offcanvas', className, children, ...props }, ref) => { - const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); - - if (collapsible === 'none') { - return ( -
- {children} -
- ); - } - - if (isMobile) { - return ( - - -
{children}
-
-
- ); - } - - return ( -
- {/* This is what handles the sidebar gap on desktop */} -
- -
- ); -}); -Sidebar.displayName = 'Sidebar'; - -const SidebarTrigger = React.forwardRef, React.ComponentProps>( - ({ className, onClick, ...props }, ref) => { - const { toggleSidebar } = useSidebar(); - - return ( - - ); - }, -); -SidebarTrigger.displayName = 'SidebarTrigger'; - -const SidebarRail = React.forwardRef>( - ({ className, ...props }, ref) => { - const { toggleSidebar } = useSidebar(); - - return ( -