• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

stacklok / codegate-ui / 12807178728

16 Jan 2025 10:42AM UTC coverage: 68.636% (-0.3%) from 68.977%
12807178728

Pull #85

github

web-flow
Merge 384e79ca6 into 48e7103bb
Pull Request #85: fix(alerts-table): trigger token box and copy to clipboard icon

206 of 379 branches covered (54.35%)

Branch coverage included in aggregate %.

398 of 501 relevant lines covered (79.44%)

31.21 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

56.59
/src/components/ui/sidebar.tsx
1
import * as React from "react";
2
import { Slot } from "@radix-ui/react-slot";
3
import { VariantProps, cva } from "class-variance-authority";
4
import { PanelLeft } from "lucide-react";
5
import { useIsMobile } from "@/hooks/use-mobile";
6
import { cn } from "@/lib/utils";
7
import { Button } from "@stacklok/ui-kit";
8
import { Separator } from "@/components/ui/separator";
9
import { Sheet, SheetContent } from "@/components/ui/sheet";
10
import { Skeleton } from "@stacklok/ui-kit";
11
import { Tooltip, TooltipTrigger } from "@stacklok/ui-kit";
12

13
const SIDEBAR_COOKIE_NAME = "sidebar:state";
7✔
14
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
7✔
15
const SIDEBAR_WIDTH = "16rem";
7✔
16
const SIDEBAR_WIDTH_MOBILE = "18rem";
7✔
17
const SIDEBAR_WIDTH_ICON = "3rem";
7✔
18
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
7✔
19

20
type SidebarContext = {
21
  state: "expanded" | "collapsed";
22
  open: boolean;
23
  setOpen: (open: boolean) => void;
24
  openMobile: boolean;
25
  setOpenMobile: (open: boolean) => void;
26
  isMobile: boolean;
27
  toggleSidebar: () => void;
28
};
29

30
const SidebarContext = React.createContext<SidebarContext | null>(null);
7✔
31

32
function useSidebar() {
33
  const context = React.useContext(SidebarContext);
12✔
34
  if (!context) {
12!
35
    throw new Error("useSidebar must be used within a SidebarProvider.");
×
36
  }
37

38
  return context;
12✔
39
}
40

41
const SidebarProvider = React.forwardRef<
7✔
42
  HTMLDivElement,
43
  React.ComponentProps<"div"> & {
44
    defaultOpen?: boolean;
45
    open?: boolean;
46
    onOpenChange?: (open: boolean) => void;
47
  }
48
>(
49
  (
50
    {
51
      defaultOpen = true,
37✔
52
      open: openProp,
53
      onOpenChange: setOpenProp,
54
      className,
55
      style,
56
      children,
57
      ...props
58
    },
59
    ref
60
  ) => {
61
    const isMobile = useIsMobile();
37✔
62
    const [openMobile, setOpenMobile] = React.useState(false);
37✔
63

64
    // This is the internal state of the sidebar.
65
    // We use openProp and setOpenProp for control from outside the component.
66
    const [_open, _setOpen] = React.useState(defaultOpen);
37✔
67
    const open = openProp ?? _open;
37✔
68
    const setOpen = React.useCallback(
37✔
69
      (value: boolean | ((value: boolean) => boolean)) => {
70
        const openState = typeof value === "function" ? value(open) : value;
×
71
        if (setOpenProp) {
×
72
          setOpenProp(openState);
×
73
        } else {
74
          _setOpen(openState);
×
75
        }
76

77
        // This sets the cookie to keep the sidebar state.
78
        document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
×
79
      },
80
      [setOpenProp, open]
81
    );
82

83
    // Helper to toggle the sidebar.
84
    const toggleSidebar = React.useCallback(() => {
37✔
85
      return isMobile
×
86
        ? setOpenMobile((open) => !open)
×
87
        : setOpen((open) => !open);
×
88
    }, [isMobile, setOpen, setOpenMobile]);
89

90
    // Adds a keyboard shortcut to toggle the sidebar.
91
    React.useEffect(() => {
37✔
92
      const handleKeyDown = (event: KeyboardEvent) => {
18✔
93
        if (
×
94
          event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
×
95
          (event.metaKey || event.ctrlKey)
96
        ) {
97
          event.preventDefault();
×
98
          toggleSidebar();
×
99
        }
100
      };
101

102
      window.addEventListener("keydown", handleKeyDown);
18✔
103
      return () => window.removeEventListener("keydown", handleKeyDown);
18✔
104
    }, [toggleSidebar]);
105

106
    // We add a state so that we can do data-state="expanded" or "collapsed".
107
    // This makes it easier to style the sidebar with Tailwind classes.
108
    const state = open ? "expanded" : "collapsed";
37!
109

110
    const contextValue = React.useMemo<SidebarContext>(
37✔
111
      () => ({
19✔
112
        state,
113
        open,
114
        setOpen,
115
        isMobile,
116
        openMobile,
117
        setOpenMobile,
118
        toggleSidebar,
119
      }),
120
      [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
121
    );
122

123
    return (
124
      <SidebarContext.Provider value={contextValue}>
125
        <div
126
          style={
127
            {
128
              "--sidebar-width": SIDEBAR_WIDTH,
129
              "--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
130
              ...style,
131
            } as React.CSSProperties
132
          }
133
          className={cn(
134
            "group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-gray-25",
135
            className
136
          )}
137
          ref={ref}
138
          {...props}
139
        >
140
          {children}
141
        </div>
142
      </SidebarContext.Provider>
143
    );
144
  }
145
);
146
SidebarProvider.displayName = "SidebarProvider";
7✔
147

148
const Sidebar = React.forwardRef<
7✔
149
  HTMLDivElement,
150
  React.ComponentProps<"div"> & {
151
    side?: "left" | "right";
152
    variant?: "sidebar" | "floating" | "inset";
153
    collapsible?: "offcanvas" | "icon" | "none";
154
  }
155
>(
156
  (
157
    {
158
      side = "left",
×
159
      variant = "sidebar",
6✔
160
      collapsible = "offcanvas",
6✔
161
      className,
162
      children,
163
      ...props
164
    },
165
    ref
166
  ) => {
167
    const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
6✔
168

169
    if (collapsible === "none") {
6!
170
      return (
171
        <div
172
          className={cn(
173
            "flex h-full w-[--sidebar-width] flex-col bg-gray-25 text-sidebar-foreground",
174
            className
175
          )}
176
          ref={ref}
177
          {...props}
178
        >
179
          {children}
180
        </div>
181
      );
182
    }
183

184
    if (isMobile) {
6!
185
      return (
186
        <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
187
          <SheetContent
188
            data-sidebar="sidebar"
189
            data-mobile="true"
190
            className="w-[--sidebar-width] bg-gray-25 p-0 text-secondary [&>button]:hidden"
191
            style={
192
              {
193
                "--sidebar-width": SIDEBAR_WIDTH_MOBILE,
194
              } as React.CSSProperties
195
            }
196
            side={side}
197
          >
198
            <div className="flex size-full flex-col">{children}</div>
199
          </SheetContent>
200
        </Sheet>
201
      );
202
    }
203

204
    return (
205
      <div
206
        ref={ref}
207
        className="group peer hidden md:block text-secondary"
208
        data-state={state}
209
        data-collapsible={state === "collapsed" ? collapsible : ""}
6!
210
        data-variant={variant}
211
        data-side={side}
212
      >
213
        {/* This is what handles the sidebar gap on desktop */}
214
        <div
215
          className={cn(
216
            "duration-200 relative h-svh w-[--sidebar-width] bg-transparent transition-[width] ease-linear",
217
            "group-data-[collapsible=offcanvas]:w-0",
218
            "group-data-[side=right]:rotate-180",
219
            variant === "floating" || variant === "inset"
18!
220
              ? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
221
              : "group-data-[collapsible=icon]:w-[--sidebar-width-icon]"
222
          )}
223
        />
224
        <div
225
          className={cn(
226
            "duration-200 fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] ease-linear md:flex",
227
            side === "left"
6!
228
              ? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
229
              : "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
230
            // Adjust the padding for floating and inset variants.
231
            variant === "floating" || variant === "inset"
18!
232
              ? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
233
              : "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
234
            className
235
          )}
236
          {...props}
237
        >
238
          <div
239
            data-sidebar="sidebar"
240
            className="flex size-full flex-col bg-gray-25 group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-gray-200 group-data-[variant=floating]:shadow"
241
          >
242
            {children}
243
          </div>
244
        </div>
245
      </div>
246
    );
247
  }
248
);
249
Sidebar.displayName = "Sidebar";
7✔
250

251
const SidebarTrigger = React.forwardRef<
7✔
252
  React.ElementRef<typeof Button>,
253
  React.ComponentProps<typeof Button>
254
>(({  onPress, ...props }, ref) => {
255
  const { toggleSidebar } = useSidebar();
6✔
256

257
  return (
258
    <Button
259
      ref={ref}
260
      data-sidebar="trigger"
261
      variant="tertiary"
262
      isIcon
263
      onPress={(event) => {
264
        onPress?.(event);
×
265
        toggleSidebar();
×
266
      }}
267
      {...props}
268
    >
269
      <PanelLeft className="!w-6 !h-6 text-secondary" />
270
      <span className="sr-only">Toggle Sidebar</span>
271
    </Button>
272
  );
273
});
274
SidebarTrigger.displayName = "SidebarTrigger";
7✔
275

276
const SidebarHeader = React.forwardRef<
7✔
277
  HTMLDivElement,
278
  React.ComponentProps<"div">
279
>(({ className, ...props }, ref) => {
280
  return (
281
    <div
282
      ref={ref}
283
      data-sidebar="header"
284
      className={cn("flex flex-col gap-2 p-2", className)}
285
      {...props}
286
    />
287
  );
288
});
289
SidebarHeader.displayName = "SidebarHeader";
7✔
290

291
const SidebarFooter = React.forwardRef<
7✔
292
  HTMLDivElement,
293
  React.ComponentProps<"div">
294
>(({ className, ...props }, ref) => {
295
  return (
296
    <div
297
      ref={ref}
298
      data-sidebar="footer"
299
      className={cn("flex flex-col gap-2 p-2", className)}
300
      {...props}
301
    />
302
  );
303
});
304
SidebarFooter.displayName = "SidebarFooter";
7✔
305

306
const SidebarSeparator = React.forwardRef<
7✔
307
  React.ElementRef<typeof Separator>,
308
  React.ComponentProps<typeof Separator>
309
>(({ className, ...props }, ref) => {
310
  return (
311
    <Separator
312
      ref={ref}
313
      data-sidebar="separator"
314
      className={cn("mx-2 w-auto bg-gray-25-border", className)}
315
      {...props}
316
    />
317
  );
318
});
319
SidebarSeparator.displayName = "SidebarSeparator";
7✔
320

321
const SidebarContent = React.forwardRef<
7✔
322
  HTMLDivElement,
323
  React.ComponentProps<"div">
324
>(({ className, ...props }, ref) => {
325
  return (
326
    <div
327
      ref={ref}
328
      data-sidebar="content"
329
      className={cn(
330
        "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
331
        className
332
      )}
333
      {...props}
334
    />
335
  );
336
});
337
SidebarContent.displayName = "SidebarContent";
7✔
338

339
const SidebarGroup = React.forwardRef<
7✔
340
  HTMLDivElement,
341
  React.ComponentProps<"div">
342
>(({ className, ...props }, ref) => {
343
  return (
344
    <div
345
      ref={ref}
346
      data-sidebar="group"
347
      className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
348
      {...props}
349
    />
350
  );
351
});
352
SidebarGroup.displayName = "SidebarGroup";
7✔
353

354
const SidebarMenu = React.forwardRef<
355
  HTMLUListElement,
356
  React.ComponentProps<"ul">
357
>(({ className, ...props }, ref) => (
358
  <ul
359
    ref={ref}
360
    data-sidebar="menu"
361
    className={cn("flex w-full min-w-0 flex-col gap-1", className)}
362
    {...props}
363
  />
364
));
365
SidebarMenu.displayName = "SidebarMenu";
7✔
366

367
const SidebarMenuItem = React.forwardRef<
368
  HTMLLIElement,
369
  React.ComponentProps<"li">
370
>(({ className, ...props }, ref) => (
371
  <li
372
    ref={ref}
373
    data-sidebar="menu-item"
374
    className={cn("group/menu-item relative", className)}
375
    {...props}
376
  />
377
));
378
SidebarMenuItem.displayName = "SidebarMenuItem";
7✔
379

380
const sidebarMenuButtonVariants = cva(
7✔
381
  "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-gray-25-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-gray-25-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-gray-25-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-gray-25-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
382
  {
383
    variants: {
384
      variant: {
385
        default: "hover:bg-gray-25-accent hover:text-sidebar-accent-foreground",
386
        outline:
387
          "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-gray-25-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
388
      },
389
      size: {
390
        default: "h-8 text-sm",
391
        sm: "h-7 text-xs",
392
        lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
393
      },
394
    },
395
    defaultVariants: {
396
      variant: "default",
397
      size: "default",
398
    },
399
  }
400
);
401

402
const SidebarMenuButton = React.forwardRef<
7✔
403
  HTMLButtonElement,
404
  React.ComponentProps<"button"> & {
405
    asChild?: boolean;
406
    isActive?: boolean;
407
    tooltip?: string;
408
  } & VariantProps<typeof sidebarMenuButtonVariants>
409
>(
410
  (
411
    {
412
      asChild = false,
×
413
      isActive = false,
×
414
      variant = "default",
×
415
      size = "default",
×
416
      tooltip,
417
      className,
418
      ...props
419
    },
420
    ref
421
  ) => {
422
    const Comp = asChild ? Slot : "button";
×
423
    const { 
424
      isMobile,
425
       state } = useSidebar();
×
426

427
    const button = (
428
      <Comp
429
        ref={ref}
430
        data-sidebar="menu-button"
431
        data-size={size}
432
        data-active={isActive}
433
        className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
434
        {...props}
435
      />
436
    );
437

438
    if (!tooltip) {
×
439
      return button;
×
440
    }
441

442

443

444
    return (
445
      <TooltipTrigger delay={0} isDisabled={state === "expanded" || isMobile}>
×
446
        {button}
447
        <Tooltip placement="right">{tooltip}</Tooltip>
448
      </TooltipTrigger>
449
    );
450
  }
451
);
452
SidebarMenuButton.displayName = "SidebarMenuButton";
7✔
453

454
const SidebarMenuAction = React.forwardRef<
7✔
455
  HTMLButtonElement,
456
  React.ComponentProps<"button"> & {
457
    asChild?: boolean;
458
    showOnHover?: boolean;
459
  }
460
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
×
461
  const Comp = asChild ? Slot : "button";
×
462

463
  return (
464
    <Comp
465
      ref={ref}
466
      data-sidebar="menu-action"
467
      className={cn(
468
        "absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-gray-25-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0",
469
        // Increases the hit area of the button on mobile.
470
        "after:absolute after:-inset-2 after:md:hidden",
471
        "peer-data-[size=sm]/menu-button:top-1",
472
        "peer-data-[size=default]/menu-button:top-1.5",
473
        "peer-data-[size=lg]/menu-button:top-2.5",
474
        "group-data-[collapsible=icon]:hidden",
475
        showOnHover &&
×
476
          "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
477
        className
478
      )}
479
      {...props}
480
    />
481
  );
482
});
483
SidebarMenuAction.displayName = "SidebarMenuAction";
7✔
484

485
const SidebarMenuBadge = React.forwardRef<
486
  HTMLDivElement,
487
  React.ComponentProps<"div">
488
>(({ className, ...props }, ref) => (
489
  <div
490
    ref={ref}
491
    data-sidebar="menu-badge"
492
    className={cn(
493
      "absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground select-none pointer-events-none",
494
      "peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
495
      "peer-data-[size=sm]/menu-button:top-1",
496
      "peer-data-[size=default]/menu-button:top-1.5",
497
      "peer-data-[size=lg]/menu-button:top-2.5",
498
      "group-data-[collapsible=icon]:hidden",
499
      className
500
    )}
501
    {...props}
502
  />
503
));
504
SidebarMenuBadge.displayName = "SidebarMenuBadge";
7✔
505

506
const SidebarMenuSkeleton = React.forwardRef<
7✔
507
  HTMLDivElement,
508
  React.ComponentProps<"div"> & {
509
    showIcon?: boolean;
510
  }
511
>(({ className, showIcon = false, ...props }, ref) => {
×
512
  // Random width between 50 to 90%.
513
  const width = React.useMemo(() => {
40✔
514
    return `${Math.floor(Math.random() * 40) + 50}%`;
40✔
515
  }, []);
516

517
  return (
518
    <div
519
      ref={ref}
520
      data-sidebar="menu-skeleton"
521
      className={cn("rounded-md h-8 flex gap-2 px-2 items-center", className)}
522
      {...props}
523
    >
524
      {showIcon && (
40✔
525
        <Skeleton
526
          className="size-4 rounded-md"
527
          data-sidebar="menu-skeleton-icon"
528
        />
529
      )}
530
      <Skeleton
531
        className="h-4 flex-1 max-w-[--skeleton-width]"
532
        data-sidebar="menu-skeleton-text"
533
        style={
534
          {
535
            "--skeleton-width": width,
536
          } as React.CSSProperties
537
        }
538
      />
539
    </div>
540
  );
541
});
542
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton";
7✔
543

544
export {
545
  Sidebar,
546
  SidebarContent,
547
  SidebarFooter,
548
  SidebarGroup,
549
  SidebarHeader,
550
  SidebarMenu,
551
  SidebarMenuAction,
552
  SidebarMenuBadge,
553
  SidebarMenuButton,
554
  SidebarMenuItem,
555
  SidebarMenuSkeleton,
556
  SidebarProvider,
557
  SidebarSeparator,
558
  SidebarTrigger,
559
  useSidebar,
560
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc