refactor(ui): remove the agent chat primitives and sidebar

Attachment, bubble, empty, marker, message, message scroller, questionnaire, resizable and sidebar had no users after the assistant moved into the editor, and neither did react-resizable-panels or @shadcn/react.
This commit is contained in:
Amruth Pillai
2026-09-29 00:55:50 +02:00
parent f2eb230f69
commit 3a69dfc4d5
13 changed files with 0 additions and 1587 deletions
-2
View File
@@ -24,12 +24,10 @@
"@fontsource-variable/newsreader": "^5.3.0",
"@phosphor-icons/web": "^2.1.2",
"@reactive-resume/utils": "workspace:*",
"@shadcn/react": "^0.3.1",
"class-variance-authority": "^0.7.1",
"cmdk": "^1.1.1",
"react": "^19.3.0",
"react-dom": "^19.3.0",
"react-resizable-panels": "^4.14.1",
"shadcn": "^4.21.0",
"tw-animate-css": "^1.4.0"
},
-116
View File
@@ -1,116 +0,0 @@
import type { VariantProps } from "class-variance-authority";
import type * as React from "react";
import { cva } from "class-variance-authority";
import { cn } from "@reactive-resume/utils/style";
const attachmentVariants = cva(
"group/attachment relative flex w-fit min-w-0 max-w-full shrink-0 flex-wrap rounded-xl border bg-card text-card-foreground transition-colors focus-within:ring-1 focus-within:ring-ring/50 has-[>a,>button]:hover:bg-muted/50 data-[state=error]:border-destructive/30 data-[state=idle]:border-dashed",
{
variants: {
size: {
default:
"gap-2 text-sm has-data-[slot=attachment-media]:p-2 has-data-[slot=attachment-content]:px-2.5 has-data-[slot=attachment-content]:py-2",
sm: "gap-2.5 text-xs has-data-[slot=attachment-media]:p-1.5 has-data-[slot=attachment-content]:px-2 has-data-[slot=attachment-content]:py-1.5",
xs: "gap-1.5 rounded-lg text-xs has-data-[slot=attachment-media]:p-1 has-data-[slot=attachment-content]:px-1.5 has-data-[slot=attachment-content]:py-1",
},
orientation: {
horizontal: "min-w-40 items-center",
vertical: "w-24 flex-col has-data-[slot=attachment-content]:w-30",
},
},
},
);
function Attachment({
className,
state = "done",
size = "default",
orientation = "horizontal",
...props
}: React.ComponentProps<"div"> &
VariantProps<typeof attachmentVariants> & {
state?: "idle" | "uploading" | "processing" | "error" | "done";
}) {
return (
<div
data-slot="attachment"
data-state={state}
data-size={size}
data-orientation={orientation}
className={cn(attachmentVariants({ size, orientation }), className)}
{...props}
/>
);
}
const attachmentMediaVariants = cva(
"relative flex aspect-square w-10 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-muted text-foreground group-data-[orientation=vertical]/attachment:w-full group-data-[size=sm]/attachment:w-8 group-data-[size=xs]/attachment:w-7 group-data-[size=xs]/attachment:rounded-md group-data-[state=error]/attachment:bg-destructive/10 group-data-[state=error]/attachment:text-destructive group-data-[orientation=vertical]/attachment:*:data-[slot=spinner]:size-6! [&_svg:not([class*='size-'])]:size-4 group-data-[orientation=vertical]/attachment:[&_svg:not([class*='size-'])]:size-6 group-data-[size=xs]/attachment:[&_svg:not([class*='size-'])]:size-3.5 [&_svg]:pointer-events-none",
{
variants: {
variant: {
icon: "",
image:
"opacity-60 group-data-[state=done]/attachment:opacity-100 group-data-[state=idle]/attachment:opacity-100 *:[img]:aspect-square *:[img]:w-full *:[img]:object-cover",
},
},
defaultVariants: {
variant: "icon",
},
},
);
function AttachmentMedia({
className,
variant = "icon",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof attachmentMediaVariants>) {
return (
<div
data-slot="attachment-media"
data-variant={variant}
className={cn(attachmentMediaVariants({ variant }), className)}
{...props}
/>
);
}
function AttachmentContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="attachment-content"
className={cn(
"min-w-0 max-w-full flex-1 leading-tight group-data-[orientation=vertical]/attachment:px-1",
className,
)}
{...props}
/>
);
}
function AttachmentTitle({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="attachment-title"
className={cn(
"group-data-[state=processing]/attachment:shimmer group-data-[state=uploading]/attachment:shimmer block min-w-0 max-w-full truncate font-medium",
className,
)}
{...props}
/>
);
}
function AttachmentGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="attachment-group"
className={cn(
"scroll-fade-x scrollbar-none flex min-w-0 snap-x snap-mandatory scroll-px-1 gap-3 overflow-x-auto overscroll-x-contain py-1 *:data-[slot=attachment]:flex-none *:data-[slot=attachment]:snap-start",
className,
)}
{...props}
/>
);
}
export { Attachment, AttachmentContent, AttachmentGroup, AttachmentMedia, AttachmentTitle };
-71
View File
@@ -1,71 +0,0 @@
import type { VariantProps } from "class-variance-authority";
import type * as React from "react";
import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import { cva } from "class-variance-authority";
import { cn } from "@reactive-resume/utils/style";
const bubbleVariants = cva(
"group/bubble relative flex w-fit min-w-0 max-w-[80%] flex-col gap-1 data-[variant=ghost]:max-w-full data-[align=end]:self-end group-data-[align=end]/message:self-end",
{
variants: {
variant: {
default:
"*:data-[slot=bubble-content]:bg-primary *:data-[slot=bubble-content]:text-primary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-primary/80",
secondary:
"*:data-[slot=bubble-content]:bg-secondary *:data-[slot=bubble-content]:text-secondary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-secondary/80",
muted: "*:data-[slot=bubble-content]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted/80",
tinted:
"*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.93_calc(c*0.4)_h)] *:data-[slot=bubble-content]:text-foreground dark:*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.3_calc(c*0.4)_h)]",
outline:
"*:data-[slot=bubble-content]:border-border *:data-[slot=bubble-content]:bg-background [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground",
ghost:
"border-none *:data-[slot=bubble-content]:rounded-none *:data-[slot=bubble-content]:bg-transparent *:data-[slot=bubble-content]:p-0 [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground",
destructive:
"*:data-[slot=bubble-content]:bg-destructive/10 *:data-[slot=bubble-content]:text-destructive [&>[data-slot=bubble-content]:is(button,a):hover]:bg-destructive/20",
},
},
defaultVariants: {
variant: "default",
},
},
);
function Bubble({
variant = "default",
align = "start",
className,
...props
}: React.ComponentProps<"div"> &
VariantProps<typeof bubbleVariants> & {
align?: "start" | "end";
}) {
return (
<div
data-slot="bubble"
data-variant={variant}
data-align={align}
className={cn(bubbleVariants({ variant }), className)}
{...props}
/>
);
}
function BubbleContent({ className, render, ...props }: useRender.ComponentProps<"div">) {
return useRender({
defaultTagName: "div",
props: mergeProps<"div">(
{
className: cn(
"w-fit min-w-0 max-w-full overflow-hidden break-words rounded-xl border border-transparent px-3 py-2 text-sm leading-relaxed group-data-[align=end]/bubble:self-end [button,a]:outline-none [button,a]:transition-colors [button,a]:focus-visible:border-ring [button,a]:focus-visible:ring-3 [button,a]:focus-visible:ring-ring/50 [button]:text-left",
className,
),
},
props,
),
render,
state: { slot: "bubble-content" },
});
}
export { Bubble, BubbleContent };
-93
View File
@@ -1,93 +0,0 @@
import type { VariantProps } from "class-variance-authority";
import type * as React from "react";
import { cva } from "class-variance-authority";
import { cn } from "@reactive-resume/utils/style";
function Empty({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty"
className={cn(
"flex w-full min-w-0 flex-1 flex-col items-center justify-center gap-5 text-balance rounded-xl p-6 text-center",
className,
)}
{...props}
/>
);
}
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div data-slot="empty-header" className={cn("flex max-w-sm flex-col items-center gap-2", className)} {...props} />
);
}
const emptyMediaVariants = cva(
"mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-transparent",
icon: "flex size-10 shrink-0 items-center justify-center rounded-full bg-sunken text-ink-2 [&_svg:not([class*='size-'])]:size-5",
success: "flex size-10 shrink-0 items-center justify-center rounded-full bg-accent-soft text-accent-text",
error: "flex size-10 shrink-0 items-center justify-center rounded-full bg-danger-soft text-danger-text",
},
},
defaultVariants: {
variant: "default",
},
},
);
function EmptyMedia({
className,
variant = "default",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
return (
<div
data-slot="empty-icon"
data-variant={variant}
className={cn(emptyMediaVariants({ variant, className }))}
{...props}
/>
);
}
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-title"
className={cn("font-display font-medium text-[22px] text-ink leading-7", className)}
{...props}
/>
);
}
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<div
data-slot="empty-description"
className={cn(
"max-w-[300px] text-ink-2 text-sm leading-5 [&>a]:text-accent-text [&>a]:underline [&>a]:underline-offset-4",
className,
)}
{...props}
/>
);
}
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-content"
className={cn(
"flex w-full min-w-0 max-w-sm flex-wrap items-center justify-center gap-2 text-balance text-sm",
className,
)}
{...props}
/>
);
}
export { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle };
-64
View File
@@ -1,64 +0,0 @@
import type { VariantProps } from "class-variance-authority";
import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import { cva } from "class-variance-authority";
import { cn } from "@reactive-resume/utils/style";
const markerVariants = cva(
"group/marker relative flex min-h-4 w-full items-center gap-2 text-left text-muted-foreground text-sm [&_svg:not([class*='size-'])]:size-4 [a]:underline [a]:underline-offset-3 [a]:hover:text-foreground",
{
variants: {
variant: {
default: "",
separator:
"before:me-1 before:h-px before:min-w-0 before:flex-1 before:bg-border after:ms-1 after:h-px after:min-w-0 after:flex-1 after:bg-border",
border: "border-border border-b pb-2",
},
},
},
);
function Marker({
className,
variant = "default",
render,
...props
}: useRender.ComponentProps<"div"> & VariantProps<typeof markerVariants>) {
return useRender({
defaultTagName: "div",
props: mergeProps<"div">(
{
className: cn(markerVariants({ variant }), className),
},
props,
),
render,
state: { slot: "marker", variant },
});
}
function MarkerIcon({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="marker-icon"
aria-hidden="true"
className={cn("size-4 shrink-0 [&_svg:not([class*='size-'])]:size-4", className)}
{...props}
/>
);
}
function MarkerContent({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="marker-content"
className={cn(
"min-w-0 break-words group-data-[variant=separator]/marker:flex-none group-data-[variant=separator]/marker:text-center *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
className,
)}
{...props}
/>
);
}
export { Marker, MarkerContent, MarkerIcon, markerVariants };
@@ -1,116 +0,0 @@
"use client";
import type * as React from "react";
import {
MessageScroller as MessageScrollerPrimitive,
useMessageScroller,
useMessageScrollerScrollable,
useMessageScrollerVisibility,
} from "@shadcn/react/message-scroller";
import { Icon } from "@reactive-resume/ui/components/icon";
import { cn } from "@reactive-resume/utils/style";
import { Button } from "./button";
function MessageScrollerProvider(props: React.ComponentProps<typeof MessageScrollerPrimitive.Provider>) {
return <MessageScrollerPrimitive.Provider {...props} />;
}
function MessageScroller({ className, ...props }: React.ComponentProps<typeof MessageScrollerPrimitive.Root>) {
return (
<MessageScrollerPrimitive.Root
data-slot="message-scroller"
className={cn("group/message-scroller relative flex size-full min-h-0 flex-col overflow-hidden", className)}
{...props}
/>
);
}
function MessageScrollerViewport({
className,
...props
}: React.ComponentProps<typeof MessageScrollerPrimitive.Viewport>) {
return (
<MessageScrollerPrimitive.Viewport
data-slot="message-scroller-viewport"
className={cn(
"scroll-fade-b scrollbar-thin scrollbar-gutter-stable data-autoscrolling:scrollbar-none size-full min-h-0 min-w-0 overflow-y-auto overscroll-contain contain-content",
className,
)}
{...props}
/>
);
}
function MessageScrollerContent({
className,
...props
}: React.ComponentProps<typeof MessageScrollerPrimitive.Content>) {
return (
<MessageScrollerPrimitive.Content
data-slot="message-scroller-content"
className={cn("flex h-max min-h-full flex-col gap-8", className)}
{...props}
/>
);
}
function MessageScrollerItem({
className,
scrollAnchor = false,
...props
}: React.ComponentProps<typeof MessageScrollerPrimitive.Item>) {
return (
<MessageScrollerPrimitive.Item
data-slot="message-scroller-item"
scrollAnchor={scrollAnchor}
className={cn("min-w-0 shrink-0 [contain-intrinsic-size:auto_10rem] [content-visibility:auto]", className)}
{...props}
/>
);
}
function MessageScrollerButton({
direction = "end",
className,
children,
render,
variant = "secondary",
size = "icon-sm",
...props
}: React.ComponentProps<typeof MessageScrollerPrimitive.Button> &
Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
return (
<MessageScrollerPrimitive.Button
data-slot="message-scroller-button"
data-direction={direction}
data-variant={variant}
data-size={size}
direction={direction}
className={cn(
"absolute inset-s-1/2 -translate-x-1/2 border-line-2 bg-raised text-ink shadow-e2 transition-[translate,scale,opacity] duration-standard hover:bg-sunken data-[direction=end]:data-[active=false]:translate-y-full data-[direction=start]:data-[active=false]:-translate-y-full data-[active=false]:pointer-events-none data-[direction=start]:top-4 data-[direction=end]:bottom-4 data-[active=true]:translate-y-0 data-[active=false]:scale-95 data-[active=true]:scale-100 data-[active=false]:opacity-0 data-[active=true]:opacity-100 data-[active=false]:duration-150 data-[active=false]:ease-out-strong data-[active=true]:ease-out-strong rtl:translate-x-1/2 data-[direction=start]:[&_[data-slot=icon]]:rotate-180",
className,
)}
render={render ?? <Button variant={variant} size={size} />}
{...props}
>
{children ?? (
<>
<Icon name="arrow_downward" />
<span className="sr-only">{direction === "end" ? "Scroll to end" : "Scroll to start"}</span>
</>
)}
</MessageScrollerPrimitive.Button>
);
}
export {
MessageScroller,
MessageScrollerButton,
MessageScrollerContent,
MessageScrollerItem,
MessageScrollerProvider,
MessageScrollerViewport,
useMessageScroller,
useMessageScrollerScrollable,
useMessageScrollerVisibility,
};
-31
View File
@@ -1,31 +0,0 @@
import type * as React from "react";
import { cn } from "@reactive-resume/utils/style";
function Message({ className, align = "start", ...props }: React.ComponentProps<"div"> & { align?: "start" | "end" }) {
return (
<div
data-slot="message"
data-align={align}
className={cn(
"group/message relative flex w-full min-w-0 gap-2 text-sm data-[align=end]:flex-row-reverse",
className,
)}
{...props}
/>
);
}
function MessageContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="message-content"
className={cn(
"flex w-full min-w-0 flex-col gap-2.5 break-words group-data-[align=end]/message:*:data-slot:self-end",
className,
)}
{...props}
/>
);
}
export { Message, MessageContent };
@@ -1,174 +0,0 @@
import type * as React from "react";
import type { Button } from "./button";
import { Questionnaire as QuestionnairePrimitive } from "@shadcn/react/questionnaire";
import { Icon } from "@reactive-resume/ui/components/icon";
import { cn } from "@reactive-resume/utils/style";
import { buttonVariants } from "./button";
function Questionnaire({ className, ...props }: React.ComponentProps<typeof QuestionnairePrimitive.Root>) {
return (
<QuestionnairePrimitive.Root
data-slot="questionnaire"
className={cn("flex w-full min-w-0 flex-col gap-4", className)}
{...props}
/>
);
}
function QuestionnaireItem({ className, ...props }: React.ComponentProps<typeof QuestionnairePrimitive.Item>) {
return (
<QuestionnairePrimitive.Item
data-slot="questionnaire-item"
className={cn("flex min-w-0 flex-col gap-4 border-0 p-0 outline-none", className)}
{...props}
/>
);
}
function QuestionnaireTitle({ className, ...props }: React.ComponentProps<typeof QuestionnairePrimitive.Title>) {
return (
<QuestionnairePrimitive.Title
data-slot="questionnaire-title"
className={cn(
"cn-font-heading text-pretty font-medium text-base leading-snug [&:not(:has(~[data-slot=questionnaire-description]))]:mb-4",
className,
)}
{...props}
/>
);
}
function QuestionnaireChoices({ className, ...props }: React.ComponentProps<typeof QuestionnairePrimitive.Choices>) {
return (
<QuestionnairePrimitive.Choices
data-slot="questionnaire-choices"
className={cn("group/questionnaire-choices grid min-w-0 gap-2", className)}
{...props}
/>
);
}
function QuestionnaireChoice({
children,
className,
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Choice>) {
return (
<QuestionnairePrimitive.Choice
data-slot="questionnaire-choice"
className={cn(
"group/questionnaire-choice relative flex min-h-11 cursor-pointer select-none items-start gap-2.5 rounded-lg border border-input bg-transparent px-3 py-2.5 text-start text-sm outline-none transition-colors hover:bg-muted/50 has-[>input:focus-visible]:border-ring has-[>input:focus-visible]:ring-3 has-[>input:focus-visible]:ring-ring/50 data-checked:border-primary/40 data-invalid:border-destructive data-checked:bg-muted dark:bg-input/20 dark:data-checked:bg-muted",
"data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:opacity-50",
className,
)}
{...props}
>
<QuestionnairePrimitive.ChoiceInput
data-slot="questionnaire-choice-input"
className="absolute inset-0 z-10 size-full cursor-pointer opacity-0"
/>
<span
aria-hidden="true"
data-slot="questionnaire-choice-indicator"
className="pointer-events-none relative flex size-4 shrink-0 translate-y-[--spacing(0.45)] items-center justify-center rounded-[5px] border-[1.5px] border-line-2 group-has-data-[slot=questionnaire-choice-description]/questionnaire-choice:translate-y-0.5 group-data-[type=radio]/questionnaire-choice:rounded-full group-data-checked/questionnaire-choice:border-accent group-data-checked/questionnaire-choice:bg-accent group-data-checked/questionnaire-choice:text-on-accent"
>
<span
data-slot="questionnaire-choice-indicator-dot"
className="hidden size-2 rounded-full bg-on-accent group-data-checked/questionnaire-choice:block group-data-[type=checkbox]/questionnaire-choice:hidden"
/>
<Icon
name="check"
size={16}
data-slot="questionnaire-choice-indicator-check"
className="hidden group-data-checked/questionnaire-choice:inline-block group-data-[type=radio]/questionnaire-choice:hidden"
/>
</span>
<QuestionnairePrimitive.ChoiceLabel
data-slot="questionnaire-choice-label"
className="flex min-w-0 flex-1 flex-col gap-0.5 leading-snug"
>
{children}
</QuestionnairePrimitive.ChoiceLabel>
<QuestionnairePrimitive.ChoiceShortcut
data-slot="questionnaire-choice-shortcut"
className="pointer-events-none ms-auto hidden size-5 shrink-0 translate-y-[--spacing(0.45)] items-center justify-center rounded-md border border-input bg-background font-medium font-mono text-[0.625rem] text-muted-foreground leading-none group-has-data-[slot=questionnaire-choice-description]/questionnaire-choice:translate-y-0.5 group-data-[shortcut]/questionnaire-choice:inline-flex"
/>
</QuestionnairePrimitive.Choice>
);
}
function QuestionnaireInput({ className, ...props }: React.ComponentProps<typeof QuestionnairePrimitive.Input>) {
return (
<div data-slot="questionnaire-input-wrapper" className="group/questionnaire-input relative w-full min-w-0">
<QuestionnairePrimitive.Input
data-slot="questionnaire-input"
className={cn(
"h-9 min-h-11 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base outline-none transition-[color,box-shadow,background-color] focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 sm:min-h-0 md:text-sm dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 dark:disabled:bg-input/80",
"selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground",
className,
)}
{...props}
/>
</div>
);
}
function QuestionnaireError({ className, ...props }: React.ComponentProps<typeof QuestionnairePrimitive.Error>) {
return (
<QuestionnairePrimitive.Error
data-slot="questionnaire-error"
className={cn("mt-2 text-destructive text-sm", className)}
{...props}
/>
);
}
function QuestionnaireActions({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="questionnaire-actions"
className={cn(
"grid min-h-11 w-full grid-cols-[minmax(0,1fr)_auto_auto] items-center gap-2 sm:min-h-8",
className,
)}
{...props}
/>
);
}
function QuestionnaireSubmit({
children,
className,
size = "default",
variant = "primary",
...props
}: React.ComponentProps<typeof QuestionnairePrimitive.Submit> &
Pick<React.ComponentProps<typeof Button>, "size" | "variant">) {
return (
<QuestionnairePrimitive.Submit
data-slot="questionnaire-submit"
data-size={size}
data-variant={variant}
className={cn(
buttonVariants({ size, variant }),
"col-start-3 row-start-1 min-h-11 justify-self-end sm:min-h-0",
className,
)}
{...props}
>
{children ?? "Submit"}
</QuestionnairePrimitive.Submit>
);
}
export {
Questionnaire,
QuestionnaireActions,
QuestionnaireChoice,
QuestionnaireChoices,
QuestionnaireError,
QuestionnaireInput,
QuestionnaireItem,
QuestionnaireSubmit,
QuestionnaireTitle,
};
@@ -1,64 +0,0 @@
import { render } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { ResizableGroup, ResizablePanel, ResizableSeparator } from "./resizable";
describe("ResizableGroup", () => {
it("renders with data-slot='resizable-panel-group'", () => {
const { container } = render(
<ResizableGroup orientation="horizontal">
<ResizablePanel>A</ResizablePanel>
<ResizableSeparator />
<ResizablePanel>B</ResizablePanel>
</ResizableGroup>,
);
expect(container.querySelector("[data-slot=resizable-panel-group]")).toBeInTheDocument();
});
it("merges custom className", () => {
const { container } = render(
<ResizableGroup orientation="horizontal" className="my-group">
<ResizablePanel>x</ResizablePanel>
</ResizableGroup>,
);
expect(container.querySelector("[data-slot=resizable-panel-group]")).toHaveClass("my-group");
});
});
describe("ResizablePanel", () => {
it("renders with data-slot='resizable-panel'", () => {
const { container } = render(
<ResizableGroup orientation="horizontal">
<ResizablePanel>x</ResizablePanel>
</ResizableGroup>,
);
expect(container.querySelector("[data-slot=resizable-panel]")).toBeInTheDocument();
});
});
describe("ResizableSeparator", () => {
it("renders with data-slot='resizable-handle' and no inner handle by default", () => {
const { container } = render(
<ResizableGroup orientation="horizontal">
<ResizablePanel>A</ResizablePanel>
<ResizableSeparator />
<ResizablePanel>B</ResizablePanel>
</ResizableGroup>,
);
const handle = container.querySelector("[data-slot=resizable-handle]");
expect(handle).toBeInTheDocument();
// No inner handle div by default
expect(handle?.children).toHaveLength(0);
});
it("renders inner handle when withHandle=true", () => {
const { container } = render(
<ResizableGroup orientation="horizontal">
<ResizablePanel>A</ResizablePanel>
<ResizableSeparator withHandle />
<ResizablePanel>B</ResizablePanel>
</ResizableGroup>,
);
const handle = container.querySelector("[data-slot=resizable-handle]");
expect(handle?.children.length).toBeGreaterThan(0);
});
});
-39
View File
@@ -1,39 +0,0 @@
import * as ResizablePrimitive from "react-resizable-panels";
import { cn } from "@reactive-resume/utils/style";
function ResizableGroup({ className, ...props }: ResizablePrimitive.GroupProps) {
return (
<ResizablePrimitive.Group
data-slot="resizable-panel-group"
className={cn("flex h-full w-full aria-[orientation=vertical]:flex-col", className)}
{...props}
/>
);
}
function ResizablePanel({ ...props }: ResizablePrimitive.PanelProps) {
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />;
}
function ResizableSeparator({
withHandle,
className,
...props
}: ResizablePrimitive.SeparatorProps & {
withHandle?: boolean;
}) {
return (
<ResizablePrimitive.Separator
data-slot="resizable-handle"
className={cn(
"relative flex w-px items-center justify-center bg-border ring-offset-background after:absolute after:inset-s-1/2 after:inset-y-0 after:w-1 after:-translate-x-1/2 focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full aria-[orientation=horizontal]:after:inset-s-0 aria-[orientation=horizontal]:after:h-1 aria-[orientation=horizontal]:after:w-full aria-[orientation=horizontal]:after:translate-x-0 aria-[orientation=horizontal]:after:-translate-y-1/2 rtl:after:translate-x-1/2 rtl:aria-[orientation=horizontal]:after:translate-x-0 [&[aria-orientation=horizontal]>div]:rotate-90",
className,
)}
{...props}
>
{withHandle && <div className="z-10 flex h-6 w-1 shrink-0 rounded-lg bg-border" />}
</ResizablePrimitive.Separator>
);
}
export { ResizableGroup, ResizablePanel, ResizableSeparator };
-286
View File
@@ -1,286 +0,0 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
} from "./sidebar";
const renderInProvider = (children: React.ReactNode, props: Partial<Parameters<typeof SidebarProvider>[0]> = {}) =>
render(
<SidebarProvider {...props}>
<Sidebar>{children}</Sidebar>
</SidebarProvider>,
);
describe("SidebarProvider", () => {
it("renders children inside a sidebar-wrapper div", () => {
const { container } = render(
<SidebarProvider>
<div data-testid="child">child</div>
</SidebarProvider>,
);
expect(container.querySelector("[data-slot=sidebar-wrapper]")).toBeInTheDocument();
expect(screen.getByTestId("child")).toBeInTheDocument();
});
it("merges custom className on wrapper", () => {
const { container } = render(
<SidebarProvider className="my-wrapper">
<div>x</div>
</SidebarProvider>,
);
expect(container.querySelector("[data-slot=sidebar-wrapper]")).toHaveClass("my-wrapper");
});
it("calls onOpenChange when toggled in controlled mode", async () => {
const onOpenChange = vi.fn();
render(
<SidebarProvider open={true} onOpenChange={onOpenChange}>
<Sidebar>x</Sidebar>
<SidebarTrigger />
</SidebarProvider>,
);
await userEvent.click(screen.getByRole("button", { name: /toggle sidebar/i }));
expect(onOpenChange).toHaveBeenCalledWith(false);
});
it("toggles via Cmd/Ctrl+B keyboard shortcut", async () => {
const onOpenChange = vi.fn();
render(
<SidebarProvider open={true} onOpenChange={onOpenChange}>
<Sidebar>x</Sidebar>
</SidebarProvider>,
);
await userEvent.keyboard("{Control>}b{/Control}");
expect(onOpenChange).toHaveBeenCalledWith(false);
});
});
describe("Sidebar", () => {
it("renders with data-slot='sidebar' when collapsible='none'", () => {
const { container } = render(
<SidebarProvider>
<Sidebar collapsible="none">x</Sidebar>
</SidebarProvider>,
);
expect(container.querySelector("[data-slot=sidebar]")).toBeInTheDocument();
});
it("renders with data-state='expanded' by default", () => {
const { container } = render(
<SidebarProvider defaultOpen>
<Sidebar>x</Sidebar>
</SidebarProvider>,
);
// data-state is on the desktop wrapper
const sidebar = container.querySelector("[data-slot=sidebar]");
expect(sidebar).toHaveAttribute("data-state", "expanded");
});
it("renders with data-state='collapsed' when defaultOpen=false", () => {
const { container } = render(
<SidebarProvider defaultOpen={false}>
<Sidebar>x</Sidebar>
</SidebarProvider>,
);
const sidebar = container.querySelector("[data-slot=sidebar]");
expect(sidebar).toHaveAttribute("data-state", "collapsed");
});
it.each(["sidebar", "floating", "inset"] as const)("supports variant=%s", (variant) => {
const { container } = render(
<SidebarProvider>
<Sidebar variant={variant}>x</Sidebar>
</SidebarProvider>,
);
const sidebar = container.querySelector("[data-slot=sidebar]");
expect(sidebar).toHaveAttribute("data-variant", variant);
});
it.each(["left", "right"] as const)("supports side=%s", (side) => {
const { container } = render(
<SidebarProvider>
<Sidebar side={side}>x</Sidebar>
</SidebarProvider>,
);
const sidebar = container.querySelector("[data-slot=sidebar]");
expect(sidebar).toHaveAttribute("data-side", side);
});
});
describe("SidebarTrigger", () => {
it("renders with sr-only label 'Toggle Sidebar'", () => {
render(
<SidebarProvider>
<SidebarTrigger />
</SidebarProvider>,
);
expect(screen.getByText("Toggle Sidebar")).toBeInTheDocument();
});
it("has data-slot='sidebar-trigger' and data-sidebar='trigger'", () => {
render(
<SidebarProvider>
<SidebarTrigger data-testid="trigger" />
</SidebarProvider>,
);
const trigger = screen.getByTestId("trigger");
expect(trigger).toHaveAttribute("data-slot", "sidebar-trigger");
expect(trigger).toHaveAttribute("data-sidebar", "trigger");
});
it("invokes user onClick before toggling", async () => {
const onClick = vi.fn();
const onOpenChange = vi.fn();
render(
<SidebarProvider open={true} onOpenChange={onOpenChange}>
<SidebarTrigger onClick={onClick} />
</SidebarProvider>,
);
await userEvent.click(screen.getByRole("button", { name: /toggle sidebar/i }));
expect(onClick).toHaveBeenCalled();
expect(onOpenChange).toHaveBeenCalledWith(false);
});
});
describe("SidebarRail", () => {
it("renders with data-slot='sidebar-rail' and aria-label='Toggle Sidebar'", () => {
render(
<SidebarProvider>
<SidebarRail />
</SidebarProvider>,
);
const rail = screen.getByLabelText("Toggle Sidebar");
expect(rail).toHaveAttribute("data-slot", "sidebar-rail");
});
it("toggles sidebar on click", async () => {
const onOpenChange = vi.fn();
render(
<SidebarProvider open={true} onOpenChange={onOpenChange}>
<SidebarRail />
</SidebarProvider>,
);
await userEvent.click(screen.getByLabelText("Toggle Sidebar"));
expect(onOpenChange).toHaveBeenCalledWith(false);
});
});
describe("SidebarHeader / SidebarFooter / SidebarSeparator", () => {
it("SidebarHeader uses data-slot='sidebar-header'", () => {
render(<SidebarHeader data-testid="h">x</SidebarHeader>);
expect(screen.getByTestId("h")).toHaveAttribute("data-slot", "sidebar-header");
});
it("SidebarFooter uses data-slot='sidebar-footer'", () => {
render(<SidebarFooter data-testid="f">x</SidebarFooter>);
expect(screen.getByTestId("f")).toHaveAttribute("data-slot", "sidebar-footer");
});
it("SidebarSeparator uses data-slot='sidebar-separator'", () => {
render(<SidebarSeparator data-testid="s" />);
expect(screen.getByTestId("s")).toHaveAttribute("data-slot", "sidebar-separator");
});
});
describe("SidebarContent / SidebarGroup", () => {
it("SidebarContent uses data-slot='sidebar-content'", () => {
render(<SidebarContent data-testid="c">x</SidebarContent>);
expect(screen.getByTestId("c")).toHaveAttribute("data-slot", "sidebar-content");
});
it("SidebarGroup uses data-slot='sidebar-group'", () => {
render(<SidebarGroup data-testid="g">x</SidebarGroup>);
expect(screen.getByTestId("g")).toHaveAttribute("data-slot", "sidebar-group");
});
it("SidebarGroupContent uses data-slot='sidebar-group-content'", () => {
render(<SidebarGroupContent data-testid="g">x</SidebarGroupContent>);
expect(screen.getByTestId("g")).toHaveAttribute("data-slot", "sidebar-group-content");
});
it("SidebarGroupLabel uses data-slot='sidebar-group-label'", () => {
render(<SidebarGroupLabel data-testid="l">Group</SidebarGroupLabel>);
expect(screen.getByTestId("l")).toHaveAttribute("data-slot", "sidebar-group-label");
});
});
describe("SidebarMenu and items", () => {
it("SidebarMenu uses data-slot='sidebar-menu' and renders as <ul>", () => {
render(<SidebarMenu data-testid="m" />);
const menu = screen.getByTestId("m");
expect(menu.tagName).toBe("UL");
expect(menu).toHaveAttribute("data-slot", "sidebar-menu");
});
it("SidebarMenuItem uses data-slot='sidebar-menu-item' and renders as <li>", () => {
render(<SidebarMenuItem data-testid="i">x</SidebarMenuItem>);
const item = screen.getByTestId("i");
expect(item.tagName).toBe("LI");
expect(item).toHaveAttribute("data-slot", "sidebar-menu-item");
});
it("SidebarMenuButton uses data-slot='sidebar-menu-button'", () => {
render(
<SidebarProvider>
<SidebarMenuButton data-testid="b">Click</SidebarMenuButton>
</SidebarProvider>,
);
expect(screen.getByTestId("b")).toHaveAttribute("data-slot", "sidebar-menu-button");
});
it("SidebarMenuButton supports isActive", () => {
render(
<SidebarProvider>
<SidebarMenuButton data-testid="b" isActive>
x
</SidebarMenuButton>
</SidebarProvider>,
);
// data-active is set as a presence attribute when isActive is truthy
expect(screen.getByTestId("b")).toHaveAttribute("data-active");
});
});
describe("Sidebar composition", () => {
it("composes a complete sidebar without errors", () => {
expect(() =>
renderInProvider(
<>
<SidebarHeader>Header</SidebarHeader>
<SidebarSeparator />
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Group 1</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton>Item</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>Footer</SidebarFooter>
</>,
),
).not.toThrow();
});
});
-498
View File
@@ -1,498 +0,0 @@
import type { VariantProps } from "class-variance-authority";
import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import { cva } from "class-variance-authority";
import * as React from "react";
import { Button } from "@reactive-resume/ui/components/button";
import { Icon } from "@reactive-resume/ui/components/icon";
import { Separator } from "@reactive-resume/ui/components/separator";
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@reactive-resume/ui/components/sheet";
import { Tooltip, TooltipContent, TooltipTrigger } from "@reactive-resume/ui/components/tooltip";
import { useIsMobile } from "@reactive-resume/ui/hooks/use-mobile";
import { cn } from "@reactive-resume/utils/style";
const SIDEBAR_COOKIE_NAME = "sidebar_state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = "18rem";
const SIDEBAR_WIDTH_MOBILE = "20rem";
const SIDEBAR_WIDTH_ICON = "3rem";
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
type SidebarContextProps = {
state: "expanded" | "collapsed";
open: boolean;
setOpen: (open: boolean) => void;
openMobile: boolean;
setOpenMobile: (open: boolean) => void;
isMobile: boolean;
toggleSidebar: () => void;
};
const SidebarContext = React.createContext<SidebarContextProps | null>(null);
function useSidebarState() {
const context = React.use(SidebarContext);
if (!context) {
throw new Error("useSidebarState must be used within a SidebarProvider.");
}
return context;
}
function SidebarProvider({
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
className,
style,
children,
...props
}: React.ComponentProps<"div"> & {
defaultOpen?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}) {
const isMobile = useIsMobile();
const [openMobile, setOpenMobile] = React.useState(false);
// Keyboard toggles snap; only pointer toggles animate the collapse.
const [instant, setInstant] = 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.useReducer((_state: boolean, nextOpen: boolean) => nextOpen, 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 using the Cookie Store API.
if ("cookieStore" in window) {
window.cookieStore.set({
name: SIDEBAR_COOKIE_NAME,
value: String(openState),
expires: Date.now() + SIDEBAR_COOKIE_MAX_AGE * 1000,
path: "/",
});
} else {
// biome-ignore lint/suspicious/noDocumentCookie: only use if cookieStore is not available
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
}
},
[setOpenProp, open],
);
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
setInstant(false);
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);
}, [isMobile, setOpen]);
// 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();
setInstant(true);
}
};
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<SidebarContextProps>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, toggleSidebar],
);
return (
<SidebarContext.Provider value={contextValue}>
<div
data-slot="sidebar-wrapper"
data-instant={instant || undefined}
style={
{
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
}
className={cn("group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar", className)}
{...props}
>
{children}
</div>
</SidebarContext.Provider>
);
}
function Sidebar({
side = "left",
variant = "sidebar",
collapsible = "offcanvas",
className,
children,
dir,
...props
}: React.ComponentProps<"div"> & {
side?: "left" | "right";
variant?: "sidebar" | "floating" | "inset";
collapsible?: "offcanvas" | "icon" | "none";
}) {
const { isMobile, state, openMobile, setOpenMobile } = useSidebarState();
if (collapsible === "none") {
return (
<div
data-slot="sidebar"
className={cn("flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground", className)}
{...props}
>
{children}
</div>
);
}
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent
dir={dir}
data-sidebar="sidebar"
data-slot="sidebar"
data-mobile="true"
className="w-(--sidebar-width) bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
} as React.CSSProperties
}
side={side}
>
<SheetHeader className="sr-only">
<SheetTitle>Sidebar</SheetTitle>
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
</SheetHeader>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>
);
}
return (
<div
className="group peer hidden text-sidebar-foreground md:block"
data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""}
data-variant={variant}
data-side={side}
data-slot="sidebar"
>
{/* This is what handles the sidebar gap on desktop */}
<div
data-slot="sidebar-gap"
className={cn(
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-out-strong group-data-instant/sidebar-wrapper:transition-none",
"group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)",
)}
/>
<div
data-slot="sidebar-container"
data-side={side}
className={cn(
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-out-strong data-[side=right]:right-0 data-[side=left]:left-0 group-data-instant/sidebar-wrapper:transition-none data-[side=right]:group-data-[collapsible=offcanvas]:-right-(--sidebar-width) data-[side=left]:group-data-[collapsible=offcanvas]:-left-(--sidebar-width) md:flex",
// Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=right]:border-s group-data-[side=left]:border-e",
className,
)}
{...props}
>
<div
data-sidebar="sidebar"
data-slot="sidebar-inner"
className="flex size-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:shadow-sm group-data-[variant=floating]:ring-1 group-data-[variant=floating]:ring-sidebar-border"
>
{children}
</div>
</div>
</div>
);
}
function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps<typeof Button>) {
const { toggleSidebar } = useSidebarState();
return (
<Button
data-sidebar="trigger"
data-slot="sidebar-trigger"
variant="ghost"
size="icon-sm"
className={cn(className)}
onClick={(event) => {
onClick?.(event);
toggleSidebar();
}}
{...props}
>
<Icon name="left_panel_close" className="rtl:-scale-x-100" />
<span className="sr-only">Toggle Sidebar</span>
</Button>
);
}
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
const { toggleSidebar } = useSidebarState();
return (
<button
type="button"
data-sidebar="rail"
data-slot="sidebar-rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
"absolute inset-y-0 z-20 hidden w-4 transition-[translate] duration-200 ease-out-strong after:absolute after:inset-s-1/2 after:inset-y-0 after:w-[2px] after:transition-colors hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2",
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize rtl:in-data-[side=left]:cursor-e-resize rtl:in-data-[side=right]:cursor-w-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize rtl:[[data-side=left][data-state=collapsed]_&]:cursor-w-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize rtl:[[data-side=right][data-state=collapsed]_&]:cursor-e-resize",
"group-data-[collapsible=offcanvas]:translate-x-0 hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:after:inset-s-full rtl:group-data-[collapsible=offcanvas]:translate-x-0",
"[[data-side=left][data-collapsible=offcanvas]_&]:-inset-e-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-inset-s-2",
className,
)}
{...props}
/>
);
}
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-header"
data-sidebar="header"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
);
}
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-footer"
data-sidebar="footer"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
);
}
function SidebarSeparator({ className, ...props }: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="sidebar-separator"
data-sidebar="separator"
className={cn("w-auto bg-sidebar-border", className)}
{...props}
/>
);
}
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-content"
data-sidebar="content"
className={cn(
"no-scrollbar flex min-h-0 flex-1 flex-col gap-0 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className,
)}
{...props}
/>
);
}
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-group"
data-sidebar="group"
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props}
/>
);
}
function SidebarGroupLabel({
className,
render,
...props
}: useRender.ComponentProps<"div"> & React.ComponentProps<"div">) {
return useRender({
defaultTagName: "div",
props: mergeProps<"div">(
{
className: cn(
"flex h-9 shrink-0 items-center rounded-md px-2 font-medium text-sidebar-foreground/70 text-xs outline-hidden ring-sidebar-ring transition-[margin,opacity] duration-200 ease-out-strong focus-visible:ring-2 group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 group-data-instant/sidebar-wrapper:transition-none [&>svg]:size-4 [&>svg]:shrink-0",
className,
),
},
props,
),
render,
state: {
slot: "sidebar-group-label",
sidebar: "group-label",
},
});
}
function SidebarGroupContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-group-content"
data-sidebar="group-content"
className={cn("w-full text-sm", className)}
{...props}
/>
);
}
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
return (
<ul
data-slot="sidebar-menu"
data-sidebar="menu"
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
{...props}
/>
);
}
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="sidebar-menu-item"
data-sidebar="menu-item"
className={cn("group/menu-item relative", className)}
{...props}
/>
);
}
const sidebarMenuButtonVariants = cva(
"peer/menu-button group/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-start text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] duration-200 ease-out-strong hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pe-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-active:bg-sidebar-accent data-active:font-medium data-active:text-sidebar-accent-foreground data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! group-data-instant/sidebar-wrapper:transition-none [&>span:last-child]:truncate [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"bg-background shadow-[0_0_0_1px_var(--sidebar-border)] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_var(--sidebar-accent)]",
},
size: {
default: "h-9 text-sm",
sm: "h-8 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function SidebarMenuButton({
render,
isActive = false,
variant = "default",
size = "default",
tooltip,
className,
...props
}: useRender.ComponentProps<"button"> &
React.ComponentProps<"button"> & {
isActive?: boolean;
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
} & VariantProps<typeof sidebarMenuButtonVariants>) {
const { isMobile, state } = useSidebarState();
const comp = useRender({
defaultTagName: "button",
props: mergeProps<"button">(
{
className: cn(sidebarMenuButtonVariants({ variant, size }), className),
},
props,
),
render: !tooltip ? render : <TooltipTrigger render={render} />,
state: {
slot: "sidebar-menu-button",
sidebar: "menu-button",
size,
active: isActive,
},
});
if (!tooltip) {
return comp;
}
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
};
}
return (
<Tooltip>
{comp}
<TooltipContent side="right" align="center" hidden={state !== "collapsed" || isMobile} {...tooltip} />
</Tooltip>
);
}
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebarState,
};
-33
View File
@@ -1405,9 +1405,6 @@ importers:
'@reactive-resume/utils':
specifier: workspace:*
version: link:../utils
'@shadcn/react':
specifier: ^0.3.1
version: 0.3.1(@types/react@19.3.0)(react@19.3.0)
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
@@ -1420,9 +1417,6 @@ importers:
react-dom:
specifier: ^19.3.0
version: 19.3.0(react@19.3.0)
react-resizable-panels:
specifier: ^4.14.1
version: 4.14.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
shadcn:
specifier: ^4.21.0
version: 4.21.0(babel-plugin-macros@3.1.0)(supports-color@7.2.0)(typescript@7.0.2)
@@ -4153,17 +4147,6 @@ packages:
'@selderee/plugin-htmlparser2@0.11.0':
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
'@shadcn/react@0.3.1':
resolution: {integrity: sha512-2gOR0HDMtWeRsCZfNDaU0YDFdgH3zsDQ6lz67Fv/y/qjY9y+R8kJqAn6q56phqv7/zHi0wqURjntrNO9zL7vnQ==}
peerDependencies:
'@types/react': '>=19'
react: '>=19'
peerDependenciesMeta:
'@types/react':
optional: true
react:
optional: true
'@simple-libs/child-process-utils@2.0.0':
resolution: {integrity: sha512-dvNoRKLijXnD0XoJAz94pbNuB5GQgDr55UhpSPhffDkTT0Cmcqh9jSCOtwfT2d4H6MI9E7c4SgtMuJXZ6F3c6A==}
engines: {node: '>=22'}
@@ -8085,12 +8068,6 @@ packages:
'@types/react':
optional: true
react-resizable-panels@4.14.1:
resolution: {integrity: sha512-OB1bXDNTLcGgTTbaX6Dn5efZhlMboSnkfr1w4xpsJTHPEbRJpACBhXpgRCXozgPO1ztgFeZRUe5J3mFNCVKm5g==}
peerDependencies:
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
react-style-singleton@2.2.3:
resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
engines: {node: '>=10'}
@@ -11718,11 +11695,6 @@ snapshots:
domhandler: 5.0.3
selderee: 0.11.0
'@shadcn/react@0.3.1(@types/react@19.3.0)(react@19.3.0)':
optionalDependencies:
'@types/react': 19.3.0
react: 19.3.0
'@simple-libs/child-process-utils@2.0.0':
dependencies:
'@simple-libs/stream-utils': 2.0.0
@@ -15676,11 +15648,6 @@ snapshots:
optionalDependencies:
'@types/react': 19.3.0
react-resizable-panels@4.14.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0):
dependencies:
react: 19.3.0
react-dom: 19.3.0(react@19.3.0)
react-style-singleton@2.2.3(@types/react@19.3.0)(react@19.3.0):
dependencies:
get-nonce: 1.0.1