@exegia/corpora-ui 0.16.0 → 0.18.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.
Files changed (115) hide show
  1. package/dist-lib/components/blocks/nav/sidebar-block.d.ts +1 -1
  2. package/dist-lib/components/blocks/scaffold/__tests__/scaffold.test.d.ts +1 -0
  3. package/dist-lib/components/blocks/scaffold/constants.d.ts +20 -0
  4. package/dist-lib/components/blocks/scaffold/index.d.ts +25 -0
  5. package/dist-lib/components/blocks/scaffold/panel-close-button.d.ts +4 -0
  6. package/dist-lib/components/blocks/scaffold/panel-menu-button.d.ts +7 -0
  7. package/dist-lib/components/blocks/scaffold/scaffold-actions.d.ts +9 -0
  8. package/dist-lib/components/blocks/scaffold/scaffold-canvas.d.ts +7 -0
  9. package/dist-lib/components/blocks/scaffold/scaffold-context.d.ts +5 -0
  10. package/dist-lib/components/blocks/scaffold/scaffold-inspector.d.ts +9 -0
  11. package/dist-lib/components/blocks/scaffold/scaffold-main.d.ts +8 -0
  12. package/dist-lib/components/blocks/scaffold/scaffold-panel.d.ts +10 -0
  13. package/dist-lib/components/blocks/scaffold/scaffold-root.d.ts +8 -0
  14. package/dist-lib/components/blocks/scaffold/scaffold-sidebar.d.ts +7 -0
  15. package/dist-lib/components/blocks/scaffold/scaffold-sub-panel.d.ts +3 -0
  16. package/dist-lib/components/blocks/scaffold/type.d.ts +133 -0
  17. package/dist-lib/components/blocks/scaffold/use-scaffold.d.ts +8 -0
  18. package/dist-lib/components/blocks/scaffold/utils.d.ts +15 -0
  19. package/dist-lib/components/blocks/shell/animated-panel-inset.d.ts +2 -0
  20. package/dist-lib/components/blocks/shell/animated-panel-provider.d.ts +2 -0
  21. package/dist-lib/components/blocks/shell/animated-panel-trigger.d.ts +2 -0
  22. package/dist-lib/components/blocks/shell/animated-panel.d.ts +2 -0
  23. package/dist-lib/components/blocks/shell/animated-sidebar-content.d.ts +2 -0
  24. package/dist-lib/components/blocks/shell/animated-sidebar-footer.d.ts +2 -0
  25. package/dist-lib/components/blocks/shell/animated-sidebar-group-content.d.ts +2 -0
  26. package/dist-lib/components/blocks/shell/animated-sidebar-group-label.d.ts +2 -0
  27. package/dist-lib/components/blocks/shell/animated-sidebar-group.d.ts +2 -0
  28. package/dist-lib/components/blocks/shell/animated-sidebar-header.d.ts +2 -0
  29. package/dist-lib/components/blocks/shell/animated-sidebar-menu-button.d.ts +3 -0
  30. package/dist-lib/components/blocks/shell/animated-sidebar-menu-item.d.ts +2 -0
  31. package/dist-lib/components/blocks/shell/animated-sidebar-menu-sub-button.d.ts +3 -0
  32. package/dist-lib/components/blocks/shell/animated-sidebar-menu-sub-item.d.ts +2 -0
  33. package/dist-lib/components/blocks/shell/animated-sidebar-menu-sub.d.ts +2 -0
  34. package/dist-lib/components/blocks/shell/animated-sidebar-menu.d.ts +2 -0
  35. package/dist-lib/components/blocks/shell/index.d.ts +16 -0
  36. package/dist-lib/components/blocks/shell/shell-layout.d.ts +1 -1
  37. package/dist-lib/components/blocks/shell/type.d.ts +91 -4
  38. package/dist-lib/components/blocks/shell/utils.d.ts +37 -0
  39. package/dist-lib/components/motion/index.d.ts +1 -0
  40. package/dist-lib/components/motion/overflow-actions.d.ts +35 -0
  41. package/dist-lib/components/ui/button-group.d.ts +11 -0
  42. package/dist-lib/components/ui/glasscn/glass-button-group.d.ts +5 -0
  43. package/dist-lib/components/ui/glasscn/glass-container.d.ts +7 -0
  44. package/dist-lib/components/ui/glasscn/glass-separator.d.ts +5 -0
  45. package/dist-lib/components/ui/group.d.ts +17 -0
  46. package/dist-lib/components/ui/separator.d.ts +2 -3
  47. package/dist-lib/components/ui/sheet.d.ts +14 -0
  48. package/dist-lib/components/ui/tooltip.d.ts +6 -0
  49. package/dist-lib/index.d.ts +2 -1
  50. package/dist-lib/index.js +1432 -932
  51. package/dist-lib/index.js.map +1 -1
  52. package/dist-lib/lib/ease.d.ts +3 -0
  53. package/dist-lib/lib/hooks/use-hover-capable.d.ts +6 -0
  54. package/package.json +2 -1
  55. package/src/components/beste/piece/browser-frame.tsx +2 -2
  56. package/src/components/blocks/nav/sidebar-block.tsx +5 -5
  57. package/src/components/blocks/nav/sidebar-nav-row.tsx +4 -5
  58. package/src/components/blocks/scaffold/__tests__/scaffold.test.tsx +203 -0
  59. package/src/components/blocks/scaffold/constants.ts +28 -0
  60. package/src/components/blocks/scaffold/index.ts +38 -0
  61. package/src/components/blocks/scaffold/panel-close-button.tsx +36 -0
  62. package/src/components/blocks/scaffold/panel-menu-button.tsx +240 -0
  63. package/src/components/blocks/scaffold/scaffold-actions.tsx +97 -0
  64. package/src/components/blocks/scaffold/scaffold-canvas.tsx +29 -0
  65. package/src/components/blocks/scaffold/scaffold-context.ts +18 -0
  66. package/src/components/blocks/scaffold/scaffold-inspector.tsx +57 -0
  67. package/src/components/blocks/scaffold/scaffold-main.tsx +28 -0
  68. package/src/components/blocks/scaffold/scaffold-panel.tsx +98 -0
  69. package/src/components/blocks/scaffold/scaffold-root.tsx +68 -0
  70. package/src/components/blocks/scaffold/scaffold-sidebar.tsx +31 -0
  71. package/src/components/blocks/scaffold/scaffold-sub-panel.tsx +51 -0
  72. package/src/components/blocks/scaffold/type.ts +171 -0
  73. package/src/components/blocks/scaffold/use-scaffold.ts +48 -0
  74. package/src/components/blocks/scaffold/utils.ts +30 -0
  75. package/src/components/blocks/shell/__tests__/shell-layout.test.tsx +3 -3
  76. package/src/components/blocks/shell/animated-panel-inset.tsx +23 -0
  77. package/src/components/blocks/shell/animated-panel-provider.tsx +155 -0
  78. package/src/components/blocks/shell/animated-panel-trigger.tsx +42 -0
  79. package/src/components/blocks/shell/animated-panel.tsx +135 -0
  80. package/src/components/blocks/shell/animated-sidebar-content.tsx +19 -0
  81. package/src/components/blocks/shell/animated-sidebar-footer.tsx +19 -0
  82. package/src/components/blocks/shell/animated-sidebar-group-content.tsx +16 -0
  83. package/src/components/blocks/shell/animated-sidebar-group-label.tsx +29 -0
  84. package/src/components/blocks/shell/animated-sidebar-group.tsx +16 -0
  85. package/src/components/blocks/shell/animated-sidebar-header.tsx +16 -0
  86. package/src/components/blocks/shell/animated-sidebar-menu-button.tsx +151 -0
  87. package/src/components/blocks/shell/animated-sidebar-menu-item.tsx +20 -0
  88. package/src/components/blocks/shell/animated-sidebar-menu-sub-button.tsx +85 -0
  89. package/src/components/blocks/shell/animated-sidebar-menu-sub-item.tsx +19 -0
  90. package/src/components/blocks/shell/animated-sidebar-menu-sub.tsx +44 -0
  91. package/src/components/blocks/shell/animated-sidebar-menu.tsx +26 -0
  92. package/src/components/blocks/shell/index.ts +18 -0
  93. package/src/components/blocks/shell/shell-layout.tsx +44 -64
  94. package/src/components/blocks/shell/type.ts +113 -6
  95. package/src/components/blocks/shell/use-shell-panels.ts +1 -1
  96. package/src/components/blocks/shell/utils.ts +142 -1
  97. package/src/components/docs/demo-controls.tsx +1 -1
  98. package/src/components/motion/index.ts +0 -0
  99. package/src/components/motion/overflow-actions.tsx +376 -0
  100. package/src/components/ui/button-group.tsx +88 -0
  101. package/src/components/ui/button.tsx +3 -3
  102. package/src/components/ui/glasscn/glass-button-group.tsx +48 -0
  103. package/src/components/ui/glasscn/glass-container.tsx +125 -0
  104. package/src/components/ui/glasscn/glass-separator.tsx +39 -0
  105. package/src/components/ui/glasscn/liquid-glass.tsx +250 -210
  106. package/src/components/ui/group.tsx +92 -0
  107. package/src/components/ui/separator.tsx +11 -11
  108. package/src/components/ui/sheet.tsx +135 -0
  109. package/src/components/ui/tooltip.tsx +66 -0
  110. package/src/index.css +1 -1
  111. package/src/index.ts +13 -1
  112. package/src/lib/ease.ts +26 -0
  113. package/src/lib/hooks/use-hover-capable.ts +23 -0
  114. package/dist-lib/components/motion/animated-sidebar.d.ts +0 -109
  115. package/src/components/motion/animated-sidebar.tsx +0 -1196
@@ -2,10 +2,10 @@
2
2
 
3
3
  import { useCallback, useMemo, useState } from "react"
4
4
 
5
- import type { SidebarSide } from "@/components/motion/animated-sidebar"
6
5
  import type {
7
6
  ShellPanelControlProps,
8
7
  ShellPanelControls,
8
+ SidebarSide,
9
9
  UseShellPanelsOptions,
10
10
  } from "./type"
11
11
 
@@ -1,2 +1,143 @@
1
-
2
1
  export const TITLE_BAR_HEIGHT = 48
2
+ // The desktop rail settles at a hard zero-width boundary. Keep the spring
3
+ // critically damped so it cannot overshoot, pause against that boundary, and
4
+ // then snap back during the final frame.
5
+ import { EASE_OUT, EASE_DRAWER } from "@/lib/ease.ts"
6
+ import type { Variants } from "motion/react"
7
+ import { createContext, useContext, useSyncExternalStore } from "react"
8
+ import type {
9
+ AnimatedSidebarContextValue,
10
+ AnimatedSidebarPanelContextValue,
11
+ } from "./type"
12
+
13
+ export const SIDEBAR_MORPH_TRANSITION = {
14
+ type: "spring",
15
+ stiffness: 380,
16
+ damping: 35,
17
+ mass: 0.75,
18
+ } as const
19
+
20
+ export const LABEL_ENTER_TRANSITION = {
21
+ duration: 0.2,
22
+ delay: 0.08,
23
+ ease: EASE_OUT,
24
+ } as const
25
+
26
+ export const LABEL_EXIT_TRANSITION = {
27
+ duration: 0.12,
28
+ ease: EASE_OUT,
29
+ } as const
30
+
31
+ export const PANEL_TRANSITION = {
32
+ duration: 0.36,
33
+ ease: EASE_DRAWER,
34
+ } as const
35
+
36
+ export const REDUCED_TRANSITION = {
37
+ duration: 0.16,
38
+ ease: EASE_OUT,
39
+ } as const
40
+
41
+ const SUBMENU_TRANSITION = {
42
+ duration: 0.18,
43
+ ease: EASE_OUT,
44
+ } as const
45
+
46
+ export const SUBMENU_VARIANTS: Variants = {
47
+ closed: {
48
+ opacity: 0,
49
+ clipPath: "inset(0 0 100% 0 round 8px)",
50
+ transition: {
51
+ duration: 0.14,
52
+ ease: EASE_OUT,
53
+ staggerChildren: 0.025,
54
+ staggerDirection: -1,
55
+ },
56
+ },
57
+ open: {
58
+ opacity: 1,
59
+ clipPath: "inset(0 0 0% 0 round 8px)",
60
+ transition: {
61
+ duration: 0.2,
62
+ delayChildren: 0.035,
63
+ ease: EASE_OUT,
64
+ staggerChildren: 0.045,
65
+ },
66
+ },
67
+ }
68
+
69
+ export const SUBMENU_ITEM_VARIANTS: Variants = {
70
+ closed: {
71
+ opacity: 0,
72
+ y: -6,
73
+ filter: "blur(3px)",
74
+ },
75
+ open: {
76
+ opacity: 1,
77
+ y: 0,
78
+ filter: "blur(0px)",
79
+ transition: SUBMENU_TRANSITION,
80
+ },
81
+ }
82
+
83
+ export const FOCUSABLE_SELECTOR = [
84
+ "a[href]",
85
+ "button:not([disabled])",
86
+ "input:not([disabled])",
87
+ "select:not([disabled])",
88
+ "textarea:not([disabled])",
89
+ "[tabindex]:not([tabindex='-1'])",
90
+ ].join(",")
91
+
92
+ export const MOBILE_QUERY = "(max-width: 767px)"
93
+ export const SIDEBAR_KEYBOARD_SHORTCUT = "b"
94
+
95
+
96
+ export const AnimatedSidebarContext =
97
+ createContext<AnimatedSidebarContextValue | null>(null)
98
+
99
+ export const AnimatedSidebarPanelContext =
100
+ createContext<AnimatedSidebarPanelContextValue | null>(null)
101
+
102
+ export function useAnimatedSidebar() {
103
+ const context = useContext(AnimatedSidebarContext)
104
+ if (!context) {
105
+ throw new Error(
106
+ "useAnimatedSidebar must be used inside AnimatedPanelProvider."
107
+ )
108
+ }
109
+ return context
110
+ }
111
+
112
+ function subscribeToMobileQuery(callback: () => void) {
113
+ const query = window.matchMedia(MOBILE_QUERY)
114
+ query.addEventListener("change", callback)
115
+ return () => query.removeEventListener("change", callback)
116
+ }
117
+
118
+ export function getMobileSnapshot() {
119
+ return window.matchMedia(MOBILE_QUERY).matches
120
+ }
121
+
122
+ export function getServerMobileSnapshot() {
123
+ return false
124
+ }
125
+
126
+ export function useIsMobile() {
127
+ return useSyncExternalStore(
128
+ subscribeToMobileQuery,
129
+ getMobileSnapshot,
130
+ getServerMobileSnapshot
131
+ )
132
+ }
133
+
134
+
135
+ export function useAnimatedSidebarPanel() {
136
+ const context = useContext(AnimatedSidebarPanelContext)
137
+ if (!context) {
138
+ throw new Error(
139
+ "Animated Sidebar parts must be used inside AnimatedPanel."
140
+ )
141
+ }
142
+ return context
143
+ }
@@ -124,7 +124,7 @@ export function DemoStage({
124
124
  <div
125
125
  className={
126
126
  canvasClassName ??
127
- "flex min-h-32 w-full items-center justify-center rounded-lg"
127
+ "flex min-h-24 max-h-32 w-full items-center justify-center rounded-lg"
128
128
  }
129
129
  >
130
130
  {children}
File without changes
@@ -0,0 +1,376 @@
1
+ "use client";
2
+ // beui.dev/components/blocks/overflow-actions
3
+
4
+ import { MoreHorizontal, X } from "lucide-react";
5
+ import {
6
+ AnimatePresence,
7
+ motion,
8
+ useReducedMotion,
9
+ type Transition,
10
+ type Variants,
11
+ } from "motion/react";
12
+ import {
13
+ useCallback,
14
+ useId,
15
+ useLayoutEffect,
16
+ useRef,
17
+ useState,
18
+ type ReactNode,
19
+ } from "react";
20
+ import { EASE_OUT } from "@/lib/ease";
21
+ import { useHoverCapable } from "@/lib/hooks/use-hover-capable";
22
+ import { cn } from "@/lib/utils";
23
+
24
+ export type OverflowActionsSize = "sm" | "md";
25
+
26
+ export type OverflowActionItem = {
27
+ id: string;
28
+ label: ReactNode;
29
+ icon?: ReactNode;
30
+ onClick?: () => void;
31
+ disabled?: boolean;
32
+ ariaLabel?: string;
33
+ };
34
+
35
+ export type OverflowActionsClassNames = {
36
+ root?: string;
37
+ track?: string;
38
+ action?: string;
39
+ primaryAction?: string;
40
+ overflowAction?: string;
41
+ toggle?: string;
42
+ icon?: string;
43
+ label?: string;
44
+ };
45
+
46
+ export interface OverflowActionsProps {
47
+ primaryActions: OverflowActionItem[];
48
+ overflowActions: OverflowActionItem[];
49
+ expanded?: boolean;
50
+ defaultExpanded?: boolean;
51
+ onExpandedChange?: (expanded: boolean) => void;
52
+ onAction?: (item: OverflowActionItem) => void;
53
+ collapseOnAction?: boolean;
54
+ size?: OverflowActionsSize;
55
+ openLabel?: string;
56
+ closeLabel?: string;
57
+ className?: string;
58
+ classNames?: OverflowActionsClassNames;
59
+ }
60
+
61
+ // This needs a softer layout spring than the app defaults, so the overflow group
62
+ // stays visually attached to the toggle while entering and leaving.
63
+ const SHELL_TRANSITION: Transition = {
64
+ type: "spring",
65
+ stiffness: 220,
66
+ damping: 17,
67
+ mass: 0.85,
68
+ };
69
+
70
+ const ICON_VARIANTS: Variants = {
71
+ hidden: { opacity: 0, filter: "blur(3px)" },
72
+ visible: {
73
+ opacity: 1,
74
+ filter: "blur(0px)",
75
+ transition: { duration: 0.18, ease: EASE_OUT },
76
+ },
77
+ exit: {
78
+ opacity: 0,
79
+ filter: "blur(3px)",
80
+ transition: { duration: 0.18, ease: EASE_OUT },
81
+ },
82
+ };
83
+
84
+ const OVERFLOW_ACTION_VARIANTS: Variants = {
85
+ hidden: { opacity: 0, filter: "blur(4px)" },
86
+ visible: { opacity: 1, filter: "blur(0px)" },
87
+ exit: { opacity: 0, filter: "blur(4px)" },
88
+ };
89
+
90
+ const TRACK_SIZE_CLASS: Record<OverflowActionsSize, string> = {
91
+ sm: "gap-1 p-1 text-xs",
92
+ md: "gap-1.5 p-1.5 text-sm",
93
+ };
94
+
95
+ const GROUP_GAP_CLASS: Record<OverflowActionsSize, string> = {
96
+ sm: "gap-1",
97
+ md: "gap-1.5",
98
+ };
99
+
100
+ const ACTION_SIZE_CLASS: Record<OverflowActionsSize, string> = {
101
+ sm: "h-8 min-w-8 gap-1.5 px-3",
102
+ md: "h-9 min-w-9 gap-2 px-3.5",
103
+ };
104
+
105
+ const TOGGLE_SIZE_CLASS: Record<OverflowActionsSize, string> = {
106
+ sm: "h-8 w-8",
107
+ md: "h-9 w-9",
108
+ };
109
+
110
+ const ICON_SIZE_CLASS: Record<OverflowActionsSize, string> = {
111
+ sm: "h-3.5 w-3.5",
112
+ md: "h-4 w-4",
113
+ };
114
+
115
+ function useControllableExpanded({
116
+ expanded,
117
+ defaultExpanded,
118
+ onExpandedChange,
119
+ }: {
120
+ expanded?: boolean;
121
+ defaultExpanded?: boolean;
122
+ onExpandedChange?: (expanded: boolean) => void;
123
+ }) {
124
+ const [internalExpanded, setInternalExpanded] = useState(
125
+ defaultExpanded ?? false,
126
+ );
127
+ const isControlled = expanded !== undefined;
128
+ const value = expanded ?? internalExpanded;
129
+
130
+ const setValue = useCallback(
131
+ (next: boolean) => {
132
+ if (!isControlled) setInternalExpanded(next);
133
+ onExpandedChange?.(next);
134
+ },
135
+ [isControlled, onExpandedChange],
136
+ );
137
+
138
+ return [value, setValue] as const;
139
+ }
140
+
141
+ export function OverflowActions({
142
+ primaryActions,
143
+ overflowActions,
144
+ expanded,
145
+ defaultExpanded = false,
146
+ onExpandedChange,
147
+ onAction,
148
+ collapseOnAction = false,
149
+ size = "md",
150
+ openLabel = "Show extra actions",
151
+ closeLabel = "Hide extra actions",
152
+ className,
153
+ classNames,
154
+ }: OverflowActionsProps) {
155
+ const reduce = useReducedMotion();
156
+ const canHover = useHoverCapable();
157
+ const overflowId = useId();
158
+ const overflowWrapperRef = useRef<HTMLDivElement>(null);
159
+ const overflowWrapperLeftRef = useRef(0);
160
+ const [isExpanded, setIsExpanded] = useControllableExpanded({
161
+ expanded,
162
+ defaultExpanded,
163
+ onExpandedChange,
164
+ });
165
+
166
+ const transition = reduce ? { duration: 0 } : SHELL_TRANSITION;
167
+
168
+ useLayoutEffect(() => {
169
+ const overflowNode = overflowWrapperRef.current;
170
+ if (!overflowNode) return;
171
+
172
+ if (!isExpanded) {
173
+ overflowNode.style.left = `${
174
+ overflowWrapperLeftRef.current -
175
+ overflowNode.getBoundingClientRect().left
176
+ }px`;
177
+ return;
178
+ }
179
+
180
+ overflowNode.style.left = "";
181
+ overflowWrapperLeftRef.current = overflowNode.getBoundingClientRect().left;
182
+ }, [isExpanded]);
183
+
184
+ const handleAction = (item: OverflowActionItem) => {
185
+ item.onClick?.();
186
+ onAction?.(item);
187
+ if (collapseOnAction) setIsExpanded(false);
188
+ };
189
+
190
+ return (
191
+ <motion.div
192
+ layout
193
+ transition={transition}
194
+ className={cn("inline-flex", classNames?.root, className)}
195
+ >
196
+ <motion.div
197
+ layout
198
+ transition={transition}
199
+ className={cn(
200
+ "relative inline-flex items-center overflow-hidden rounded-full border border-border bg-card",
201
+ TRACK_SIZE_CLASS[size],
202
+ classNames?.track,
203
+ )}
204
+ >
205
+ <motion.div
206
+ layout
207
+ transition={transition}
208
+ className={cn("inline-flex items-center", GROUP_GAP_CLASS[size])}
209
+ >
210
+ {primaryActions.map((item) => (
211
+ <ActionButton
212
+ key={item.id}
213
+ item={item}
214
+ size={size}
215
+ reduce={reduce}
216
+ canHover={canHover}
217
+ onAction={handleAction}
218
+ layoutTransition={transition}
219
+ className={cn(classNames?.action, classNames?.primaryAction)}
220
+ iconClassName={classNames?.icon}
221
+ labelClassName={classNames?.label}
222
+ />
223
+ ))}
224
+ </motion.div>
225
+
226
+ <AnimatePresence mode="popLayout" initial={false}>
227
+ {isExpanded ? (
228
+ <motion.div
229
+ key="overflow-actions"
230
+ ref={overflowWrapperRef}
231
+ id={overflowId}
232
+ layout
233
+ aria-hidden={!isExpanded}
234
+ transition={transition}
235
+ className={cn(
236
+ "relative inline-flex w-max items-center",
237
+ GROUP_GAP_CLASS[size],
238
+ )}
239
+ >
240
+ {overflowActions.map((item) => (
241
+ <ActionButton
242
+ key={item.id}
243
+ item={item}
244
+ size={size}
245
+ reduce={reduce}
246
+ canHover={canHover}
247
+ overflow
248
+ visible={isExpanded}
249
+ variants={OVERFLOW_ACTION_VARIANTS}
250
+ onAction={handleAction}
251
+ layoutTransition={transition}
252
+ className={cn(classNames?.action, classNames?.overflowAction)}
253
+ iconClassName={classNames?.icon}
254
+ labelClassName={classNames?.label}
255
+ />
256
+ ))}
257
+ </motion.div>
258
+ ) : null}
259
+ </AnimatePresence>
260
+
261
+ <motion.button
262
+ type="button"
263
+ layout
264
+ aria-expanded={isExpanded}
265
+ aria-controls={isExpanded ? overflowId : undefined}
266
+ aria-label={isExpanded ? closeLabel : openLabel}
267
+ title={isExpanded ? closeLabel : openLabel}
268
+ onClick={() => setIsExpanded(!isExpanded)}
269
+ whileTap={reduce ? undefined : { scale: 0.96 }}
270
+ whileHover={reduce || !canHover ? undefined : { scale: 1.03 }}
271
+ transition={transition}
272
+ className={cn(
273
+ "relative inline-grid shrink-0 place-items-center rounded-full bg-primary text-primary-foreground outline-none disabled:pointer-events-none disabled:opacity-50",
274
+ "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
275
+ TOGGLE_SIZE_CLASS[size],
276
+ classNames?.toggle,
277
+ )}
278
+ >
279
+ <AnimatePresence mode="popLayout" initial={false}>
280
+ <motion.span
281
+ key={isExpanded ? "close" : "open"}
282
+ variants={ICON_VARIANTS}
283
+ initial={reduce ? { opacity: 0 } : "hidden"}
284
+ animate={reduce ? { opacity: 1 } : "visible"}
285
+ exit={reduce ? { opacity: 0 } : "exit"}
286
+ className="inline-grid place-items-center"
287
+ >
288
+ {isExpanded ? (
289
+ <X className={ICON_SIZE_CLASS[size]} />
290
+ ) : (
291
+ <MoreHorizontal className={ICON_SIZE_CLASS[size]} />
292
+ )}
293
+ </motion.span>
294
+ </AnimatePresence>
295
+ </motion.button>
296
+ </motion.div>
297
+ </motion.div>
298
+ );
299
+ }
300
+
301
+ function ActionButton({
302
+ item,
303
+ size,
304
+ reduce,
305
+ canHover,
306
+ overflow,
307
+ visible = true,
308
+ variants,
309
+ onAction,
310
+ layoutTransition,
311
+ className,
312
+ iconClassName,
313
+ labelClassName,
314
+ }: {
315
+ item: OverflowActionItem;
316
+ size: OverflowActionsSize;
317
+ reduce: boolean | null;
318
+ canHover: boolean;
319
+ overflow?: boolean;
320
+ visible?: boolean;
321
+ variants?: Variants;
322
+ onAction: (item: OverflowActionItem) => void;
323
+ layoutTransition: Transition;
324
+ className?: string;
325
+ iconClassName?: string;
326
+ labelClassName?: string;
327
+ }) {
328
+ const label = typeof item.label === "string" ? item.label : undefined;
329
+
330
+ return (
331
+ <motion.span
332
+ layout="position"
333
+ variants={variants}
334
+ initial={variants ? (reduce ? { opacity: 0 } : "hidden") : undefined}
335
+ animate={variants ? (reduce ? { opacity: 1 } : "visible") : undefined}
336
+ exit={variants ? (reduce ? { opacity: 0 } : "exit") : undefined}
337
+ whileTap={reduce || item.disabled ? undefined : { scale: 0.97 }}
338
+ whileHover={
339
+ reduce || !canHover || item.disabled ? undefined : { scale: 1.008 }
340
+ }
341
+ transition={layoutTransition}
342
+ className="inline-flex shrink-0"
343
+ >
344
+ <button
345
+ type="button"
346
+ disabled={item.disabled}
347
+ aria-label={item.ariaLabel}
348
+ tabIndex={overflow && !visible ? -1 : undefined}
349
+ title={label}
350
+ onClick={() => onAction(item)}
351
+ className={cn(
352
+ "inline-flex shrink-0 items-center justify-center rounded-full bg-background font-medium text-foreground outline-none",
353
+ "disabled:pointer-events-none disabled:opacity-45",
354
+ "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
355
+ ACTION_SIZE_CLASS[size],
356
+ className,
357
+ )}
358
+ >
359
+ {item.icon ? (
360
+ <span
361
+ className={cn(
362
+ "inline-flex shrink-0 items-center justify-center",
363
+ ICON_SIZE_CLASS[size],
364
+ iconClassName,
365
+ )}
366
+ >
367
+ {item.icon}
368
+ </span>
369
+ ) : null}
370
+ <span className={cn("whitespace-nowrap", labelClassName)}>
371
+ {item.label}
372
+ </span>
373
+ </button>
374
+ </motion.span>
375
+ );
376
+ }
@@ -0,0 +1,88 @@
1
+ import { mergeProps } from "@base-ui/react/merge-props"
2
+ import { useRender } from "@base-ui/react/use-render"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+ import { motion, type HTMLMotionProps } from "framer-motion"
5
+
6
+ import { cn } from "@/lib/utils"
7
+ import { Separator } from "@/components/ui/separator"
8
+
9
+ const buttonGroupVariants = cva(
10
+ "flex w-fit items-stretch *:focus-visible:relative *:focus-visible:z-10 has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-lg [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
11
+ {
12
+ variants: {
13
+ orientation: {
14
+ horizontal:
15
+ "*:data-slot:rounded-r-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-lg! [&>[data-slot]~[data-slot]]:rounded-l-none [&>[data-slot]~[data-slot]]:border-l-0",
16
+ vertical:
17
+ "flex-col *:data-slot:rounded-b-none [&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-lg! [&>[data-slot]~[data-slot]]:rounded-t-none [&>[data-slot]~[data-slot]]:border-t-0",
18
+ },
19
+ },
20
+ defaultVariants: {
21
+ orientation: "horizontal",
22
+ },
23
+ }
24
+ )
25
+
26
+ function ButtonGroup({
27
+ className,
28
+ orientation,
29
+ ...props
30
+ }: HTMLMotionProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
31
+ return (
32
+ <motion.div
33
+ role="group"
34
+ data-slot="button-group"
35
+ data-orientation={orientation}
36
+ className={cn(buttonGroupVariants({ orientation }), className)}
37
+ {...props}
38
+ />
39
+ )
40
+ }
41
+
42
+ function ButtonGroupText({
43
+ className,
44
+ render,
45
+ ...props
46
+ }: useRender.ComponentProps<"div">) {
47
+ return useRender({
48
+ defaultTagName: "div",
49
+ props: mergeProps<"div">(
50
+ {
51
+ className: cn(
52
+ "flex items-center gap-2 rounded-lg border bg-muted px-2.5 text-sm font-medium [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
53
+ className
54
+ ),
55
+ },
56
+ props
57
+ ),
58
+ render,
59
+ state: {
60
+ slot: "button-group-text",
61
+ },
62
+ })
63
+ }
64
+
65
+ function ButtonGroupSeparator({
66
+ className,
67
+ orientation = "vertical",
68
+ ...props
69
+ }: React.ComponentProps<typeof Separator>) {
70
+ return (
71
+ <Separator
72
+ data-slot="button-group-separator"
73
+ orientation={orientation}
74
+ className={cn(
75
+ "relative self-stretch bg-input data-horizontal:mx-px data-horizontal:w-auto data-vertical:my-px data-vertical:h-auto",
76
+ className
77
+ )}
78
+ {...props}
79
+ />
80
+ )
81
+ }
82
+
83
+ export {
84
+ ButtonGroup,
85
+ ButtonGroupSeparator,
86
+ ButtonGroupText,
87
+ buttonGroupVariants,
88
+ }
@@ -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",
@@ -0,0 +1,48 @@
1
+ "use client"
2
+
3
+ import {
4
+ type FrostGlassVariantProp,
5
+ glassVariantStyles,
6
+ } from "@/lib/glass-variants"
7
+ import { cn } from "@/lib/utils"
8
+
9
+ import { ButtonGroup } from "../button-group"
10
+ import { LiquidGlass } from "./liquid-glass"
11
+
12
+ type GlassButtonGroupProps = React.ComponentProps<typeof ButtonGroup> &
13
+ FrostGlassVariantProp
14
+
15
+ function GlassButtonGroup({
16
+ className,
17
+ glassVariant = "liquid-refract",
18
+ children,
19
+ ...props
20
+ }: GlassButtonGroupProps) {
21
+ if (glassVariant === "liquid-refract") {
22
+ return (
23
+ <LiquidGlass className={cn("", className)}>
24
+ <ButtonGroup
25
+ data-slot="glass-button-group"
26
+ data-glass-variant={glassVariant}
27
+ className={cn("bg-transparent", className)}
28
+ {...props}
29
+ >
30
+ {children}
31
+ </ButtonGroup>
32
+ </LiquidGlass>
33
+ )
34
+ }
35
+
36
+ return (
37
+ <ButtonGroup
38
+ data-slot="glass-button-group"
39
+ data-glass-variant={glassVariant}
40
+ className={cn("rounded-lg", glassVariantStyles[glassVariant], className)}
41
+ {...props}
42
+ >
43
+ {children}
44
+ </ButtonGroup>
45
+ )
46
+ }
47
+
48
+ export { GlassButtonGroup }