@exegia/corpora-ui 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exegia/corpora-ui",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "shadcn-ready React UI library for the corpora manuscript-research apps.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -73,12 +73,31 @@ describe("ShellLayout", () => {
73
73
  expect(leftRail().getAttribute("data-state")).toBe("collapsed")
74
74
  })
75
75
 
76
- test("a panel's own open flag seeds its side's initial state", () => {
76
+ test("a panel's trigger node replaces the default icon, keeping the toggle", async () => {
77
+ const user = userEvent.setup()
78
+ render(
79
+ <ShellLayout
80
+ panels={{
81
+ ...PANELS,
82
+ right: { ...PANELS.right!, trigger: <span>Custom trigger</span> },
83
+ }}
84
+ variant="web"
85
+ />
86
+ )
87
+
88
+ const rightTrigger = screen.getByRole("button", { name: "Toggle panel" })
89
+ expect(rightTrigger.textContent).toContain("Custom trigger")
90
+
91
+ await user.click(rightTrigger)
92
+ expect(rightDrawer().getAttribute("data-state")).toBe("expanded")
93
+ })
94
+
95
+ test("a panel's own defaultOpen flag seeds its side's initial state", () => {
77
96
  render(
78
97
  <ShellLayout
79
98
  panels={{
80
99
  ...PANELS,
81
- right: { ...PANELS.right!, open: true },
100
+ right: { ...PANELS.right!, defaultOpen: true },
82
101
  }}
83
102
  variant="web"
84
103
  />
@@ -93,7 +112,7 @@ describe("ShellLayout", () => {
93
112
  defaultOpen={{ left: false, right: false }}
94
113
  panels={{
95
114
  ...PANELS,
96
- right: { ...PANELS.right!, open: true },
115
+ right: { ...PANELS.right!, defaultOpen: true },
97
116
  }}
98
117
  variant="web"
99
118
  />
@@ -19,6 +19,7 @@ export function ShellLayout({
19
19
  variant = "desktop",
20
20
  panels,
21
21
  className,
22
+ header,
22
23
  defaultOpen,
23
24
  ...panelControlProps
24
25
  }: ShellLayoutProps): React.ReactElement {
@@ -29,22 +30,27 @@ export function ShellLayout({
29
30
  // an explicit `defaultOpen` record — usually from useShellPanels — wins
30
31
  // per side.
31
32
  const initialOpen: ShellPanelControlProps["defaultOpen"] = {
32
- ...(panels?.left && {
33
- left: panels.left.defaultOpen ?? panels.left.open,
34
- }),
35
- ...(panels?.right && {
36
- right: panels.right.defaultOpen ?? panels.right.open,
37
- }),
38
- ...(defaultOpen)
39
- }
33
+ ...(panels?.left && {
34
+ left: panels.left.defaultOpen ?? true,
35
+ }),
36
+ ...(panels?.right && {
37
+ right: panels.right.defaultOpen ?? false,
38
+ }),
39
+ ...defaultOpen,
40
+ }
41
+
40
42
 
41
43
  return (
42
44
  <AnimatedSidebarProvider
43
45
  {...panelControlProps}
44
46
  defaultOpen={initialOpen}
45
- className={cn("block-full", className, background, "relative pr-2")}
47
+ className={cn(
48
+ "relative block-full pr-2",
49
+ className,
50
+ background
51
+ )}
46
52
  style={{
47
- paddingTop: variant === "desktop" ? TITLE_BAR_HEIGHT : 0
53
+ paddingTop: variant === "desktop" ? TITLE_BAR_HEIGHT : 0,
48
54
  }}
49
55
  >
50
56
  <AnimatedSidebar
@@ -56,21 +62,21 @@ export function ShellLayout({
56
62
  {panels?.left?.component}
57
63
  </AnimatedSidebar>
58
64
 
59
- <AnimatedSidebarInset
60
- className={cn(
61
- "min-w-24 overflow-hidden border-t border-neutral-200 dark:border-neutral-700 inset-ring-1 inset-ring-neutral-50 dark:inset-ring-black bg-white dark:bg-card rounded-lg!",
62
- `mx-0!`
63
- )}
64
- >
65
- <header className="flex h-16 items-center justify-between gap-3 px-4 sm:px-6">
66
- <div className="flex min-w-0 items-center gap-3">
65
+ <AnimatedSidebarInset className="min-w-24">
66
+ <header className="flex h-12 items-center justify-between gap-2 px-2">
67
+ <div className="flex min-w-0 items-center gap-2">
67
68
  <AnimatedSidebarTrigger>
68
- <MotionIcon name="PanelLeft" size={24} animation="press" />
69
+ {panels?.left?.trigger ?? (
70
+ <MotionIcon name="PanelLeft" size={24} animation="press" />
71
+ )}
69
72
  </AnimatedSidebarTrigger>
70
73
  </div>
71
- <div className="flex shrink-0 items-center gap-3">
74
+ {header && <div className="flex flex-1 items-center">{header}</div>}
75
+ <div className="flex shrink-0 items-center gap-2">
72
76
  <AnimatedSidebarTrigger aria-label="Toggle panel" side="right">
73
- <MotionIcon name="PanelRight" size={24} animation="press" />
77
+ {panels?.right?.trigger ?? (
78
+ <MotionIcon className="opacity-70" name="PanelRight" size={24} />
79
+ )}
74
80
  </AnimatedSidebarTrigger>
75
81
  </div>
76
82
  </header>
@@ -83,16 +89,10 @@ export function ShellLayout({
83
89
  ariaLabel={panels?.right?.name ?? "Secondary panel"}
84
90
  // Below md the panel is portal led over the page, so it carries the
85
91
  // surface itself; the desktop rail keeps it on the inner panel.
86
- className={cn("max-md:border-l max-md:border-neutral-200 max-md:bg-white dark:max-md:border-neutral-700 dark:max-md:bg-card")}
87
- collapsible="offcanvas"
88
- panelClassName={cn(
89
- // The off canvas panel keeps a fixed width so it can slide out of the
90
- // zero-width rail, so the inset gutter has to come out of that width
91
- // rather than from a left margin.
92
- "top-2 ml-0 h-[calc(100%-1rem)] min-w-[calc(var(--sidebar-width))]",
93
- "rounded-lg border-t border-neutral-200 bg-white shadow-md inset-ring-1 inset-ring-neutral-50",
94
- "dark:border-neutral-700 dark:bg-card dark:inset-ring-black"
92
+ className={cn(
93
+ "max-md:border-l max-md:border-neutral-200 max-md:bg-white dark:max-md:border-neutral-700 dark:max-md:bg-card"
95
94
  )}
95
+ collapsible="offcanvas"
96
96
  role="complementary"
97
97
  side="right"
98
98
  variant="inset"
@@ -105,16 +105,8 @@ export function ShellLayout({
105
105
 
106
106
  function ShellLayoutPlaceholder() {
107
107
  return (
108
- <div className="grid gap-5">
109
- <div className="grid gap-5 lg:grid-cols-3">
110
- {Array.from({ length: 3 }).map((_, index) => (
111
- <div
112
- className="h-32 border border-border bg-background/60"
113
- key={index}
114
- />
115
- ))}
116
- </div>
117
- <div className="h-72 border border-border bg-background/60" />
108
+ <div className="flex flex-col flex-1 p-4">
109
+ <div className="h-96 border border-border bg-muted-foreground/10 rounded-lg" />
118
110
  </div>
119
111
  )
120
112
  }
@@ -23,6 +23,10 @@ export interface IPanel {
23
23
  id: string
24
24
  name: string
25
25
  component: ReactNode
26
+ /** Replaces the default icon inside the panel's header trigger. The shell
27
+ * keeps the trigger button itself (toggle wiring, aria-label) — this only
28
+ * swaps what renders inside it. */
29
+ trigger?: ReactNode
26
30
  open: boolean
27
31
  defaultOpen?: boolean
28
32
  side: "left" | "bottom" | "right" | "sidebar"
@@ -87,6 +91,7 @@ export interface ShellLayoutProps extends ShellPanelControlProps {
87
91
  /** Content for the shell's panels, keyed by the side. The shell renders
88
92
  * the `left` rail and the `right` drawer; other sides are reserved. */
89
93
  panels?: TPanelMap
94
+ header?: ReactNode
90
95
  className?: string
91
96
  variant?: "web" | "desktop"
92
97
  }
@@ -29,9 +29,10 @@ import { createPortal } from "react-dom"
29
29
  import { SharedLayoutBg } from "@/components/motion/shared-layout-bg"
30
30
  import { EASE_DRAWER, EASE_OUT, SPRING_LAYOUT, SPRING_PRESS } from "@/lib/ease"
31
31
  import { cn } from "@/lib/utils"
32
+ import { Button } from "@/components/ui/button";
32
33
 
33
34
  export type SidebarSide = "left" | "right"
34
- /** Open flags keyed by side. Sides left out stay uncontrolled / at their
35
+ /** Open flags keyed by the side. Sides left out stay uncontrolled / at their
35
36
  * default — there is no per-side prop, the record IS the API. */
36
37
  export type SidebarOpenState = Partial<Record<SidebarSide, boolean>>
37
38
  type SidebarVariant = "sidebar" | "floating" | "inset"
@@ -198,7 +199,7 @@ type SidebarProviderStyle = CSSProperties & {
198
199
  }
199
200
 
200
201
  export interface AnimatedSidebarProviderProps extends HTMLAttributes<HTMLDivElement> {
201
- /** Controlled desktop open state, keyed by side. A side left undefined
202
+ /** Controlled desktop open state, keyed by the side. A side left undefined
202
203
  * stays uncontrolled. */
203
204
  open?: SidebarOpenState
204
205
  /** Initial desktop open state, merged over `{ left: true, right: false }`. */
@@ -572,7 +573,7 @@ export const AnimatedSidebar = forwardRef<HTMLElement, AnimatedSidebarProps>(
572
573
  className={cn(
573
574
  "group/sidebar relative hidden h-auto shrink-0 will-change-[width] md:block",
574
575
  "peer flex!",
575
- // The offcanvas panel keeps its full width and slides out of the
576
+ // The off canvas panel keeps its full width and slides out of the
576
577
  // zero-width rail, so clip it rather than letting it bleed past the
577
578
  // shell edge. The clip margin lets the panel's own shadow escape
578
579
  // without letting the slid-out panel escape with it
@@ -583,7 +584,9 @@ export const AnimatedSidebar = forwardRef<HTMLElement, AnimatedSidebarProps>(
583
584
  )}
584
585
  >
585
586
  {side === "right" && !collapsed && (
586
- <motion.div className="h-full p-0.5 flex flex-1 bg-radial/decreasing from-amber-400 to-0% hover:to-50% ease-smooth-out to-amber-500/0 cursor-col-resize transition-colors duration-1000 animate-in" />
587
+ <motion.div className="flex h-full cursor-col-resize justify-center p-1 group relative">
588
+ <motion.div className="h-full w-full scale-y-0 animate-in bg-radial/decreasing from-amber-300/70 to-amber-500/0 to-40% transition-[colors,scale] duration-200 ease-smooth-out group-hover:scale-y-100 absolute" />
589
+ </motion.div>
587
590
  )}
588
591
  <motion.div
589
592
  initial={false}
@@ -594,10 +597,19 @@ export const AnimatedSidebar = forwardRef<HTMLElement, AnimatedSidebarProps>(
594
597
  transition={context.reduce ? REDUCED_TRANSITION : PANEL_TRANSITION}
595
598
  className={cn(
596
599
  "sticky top-0 flex h-svh w-full flex-col overflow-hidden",
597
- collapsible === "offcanvas" && "w-[var(--sidebar-width)]",
600
+ "bg-neutral-50 dark:bg-neutral-900",
601
+ "outline-offset-0.5 border-t-3 border-white outline-neutral-100 dark:border-neutral-800 dark:inset-ring-black",
602
+ "rounded-lg shadow-md shadow-neutral-200 dark:shadow-neutral-950",
603
+ collapsible === "offcanvas" && "w-(--sidebar-width)",
598
604
 
599
605
  variant === "floating" && "m-2 h-[calc(100svh-1rem)]",
600
- variant === "inset" && "m-2 h-[calc(100svh-1rem)]",
606
+ variant === "inset" && "my-2 h-[calc(100svh-1rem)]",
607
+ variant === "inset" &&
608
+ side === "right" &&
609
+ "w-[calc(var(--sidebar-width)-0.5rem)]",
610
+ variant === "inset" &&
611
+ side === "left" &&
612
+ "border-transparent! bg-transparent! shadow-none!",
601
613
  panelClassName
602
614
  )}
603
615
  >
@@ -620,7 +632,7 @@ export const AnimatedSidebarTrigger = forwardRef<
620
632
  HTMLButtonElement,
621
633
  AnimatedSidebarTriggerProps
622
634
  >(function AnimatedSidebarTrigger(
623
- { className, onClick, side = "left", type = "button", ...props },
635
+ { onClick, side = "left", type = "button", ...props },
624
636
  forwardedRef
625
637
  ) {
626
638
  const context = useAnimatedSidebar()
@@ -630,7 +642,7 @@ export const AnimatedSidebarTrigger = forwardRef<
630
642
  const triggerRef = context.triggerRefs[side]
631
643
 
632
644
  return (
633
- <button
645
+ <Button
634
646
  {...props}
635
647
  ref={(node) => {
636
648
  triggerRef.current = node
@@ -638,6 +650,8 @@ export const AnimatedSidebarTrigger = forwardRef<
638
650
  else if (forwardedRef) forwardedRef.current = node
639
651
  }}
640
652
  type={type}
653
+ variant="ghost"
654
+ size="icon-xl"
641
655
  aria-label={props["aria-label"] ?? "Toggle sidebar"}
642
656
  aria-expanded={expanded}
643
657
  data-slot="sidebar-trigger"
@@ -648,11 +662,6 @@ export const AnimatedSidebarTrigger = forwardRef<
648
662
  if (event.defaultPrevented) return
649
663
  context.toggleSidebar(side)
650
664
  }}
651
- className={cn(
652
- "inline-flex size-10 shrink-0 items-center justify-center rounded-xl outline-none",
653
- "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
654
- className
655
- )}
656
665
  />
657
666
  )
658
667
  })
@@ -692,41 +701,6 @@ export const AnimatedSidebarClose = forwardRef<
692
701
  )
693
702
  })
694
703
 
695
- export interface AnimatedSidebarRailProps extends ButtonHTMLAttributes<HTMLButtonElement> {}
696
-
697
- export const AnimatedSidebarRail = forwardRef<
698
- HTMLButtonElement,
699
- AnimatedSidebarRailProps
700
- >(function AnimatedSidebarRail(
701
- { className, onClick, type = "button", ...props },
702
- forwardedRef
703
- ) {
704
- const context = useAnimatedSidebar()
705
- const panel = useAnimatedSidebarPanel()
706
-
707
- return (
708
- <button
709
- {...props}
710
- ref={forwardedRef}
711
- type={type}
712
- data-side={panel.side}
713
- aria-label={props["aria-label"] ?? "Toggle sidebar"}
714
- title="Toggle sidebar"
715
- tabIndex={-1}
716
- onClick={(event) => {
717
- onClick?.(event)
718
- if (event.defaultPrevented) return
719
- context.toggleSidebar(panel.side)
720
- }}
721
- className={cn(
722
- "absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 outline-none md:block",
723
- "after:absolute after:inset-y-0 after:left-1/2 after:w-px after:bg-transparent after:transition-colors hover:after:bg-border",
724
- "data-[side=left]:left-full data-[side=right]:right-0 data-[side=right]:translate-x-1/2",
725
- className
726
- )}
727
- />
728
- )
729
- })
730
704
 
731
705
  export interface AnimatedSidebarInsetProps extends HTMLMotionProps<"main"> {}
732
706
 
@@ -740,8 +714,9 @@ export const AnimatedSidebarInset = forwardRef<
740
714
  ref={forwardedRef}
741
715
  data-slot="sidebar-inset"
742
716
  className={cn(
743
- "relative flex min-w-0 flex-1 flex-col bg-background",
744
- "md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:mr-1.5 md:peer-data-[variant=inset]:rounded-md md:peer-data-[variant=inset]:shadow-md",
717
+ "relative flex min-w-0 flex-1 flex-col bg-neutral-50 dark:bg-neutral-900",
718
+ "outline-offset-0.5 overflow-hidden border-t-3 border-white outline-neutral-100 dark:border-neutral-800 dark:inset-ring-black",
719
+ "m-2 mx-0 rounded-lg shadow-md shadow-neutral-200 dark:shadow-neutral-950",
745
720
  className
746
721
  )}
747
722
  />
@@ -46,10 +46,10 @@ export const buttonVariants = cva(
46
46
  size: {
47
47
  default: "h-9 px-[calc(--spacing(3)-1px)] sm:h-8",
48
48
  icon: "size-9 sm:size-8",
49
- "icon-lg": "size-10 sm:size-9",
50
- "icon-sm": "size-8 sm:size-7",
49
+ "icon-lg": "size-8 sm:size-8",
50
+ "icon-sm": "size-7 sm:size-6",
51
51
  "icon-xl":
52
- "size-11 sm:size-10 [&_svg:not([class*='size-'])]:size-5 sm:[&_svg:not([class*='size-'])]:size-4.5",
52
+ "size-10 sm:size-9 [&_svg:not([class*='size-'])]:size-5 sm:[&_svg:not([class*='size-'])]:size-4.5",
53
53
  "icon-xs":
54
54
  "size-7 rounded-md before:rounded-[calc(var(--radius-md)-1px)] sm:size-6 not-in-data-[slot=input-group]:[&_svg:not([class*='size-'])]:size-4 sm:not-in-data-[slot=input-group]:[&_svg:not([class*='size-'])]:size-3.5",
55
55
  lg: "h-10 px-[calc(--spacing(3.5)-1px)] sm:h-9",