@snapdragonsnursery/react-components 1.10.0 → 1.12.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": "@snapdragonsnursery/react-components",
3
- "version": "1.10.0",
3
+ "version": "1.12.0",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -173,6 +173,7 @@ export function AppSidebar({
173
173
  navItems,
174
174
  projects,
175
175
  user,
176
+ version,
176
177
  ...props
177
178
  }) {
178
179
  return (
@@ -198,6 +199,11 @@ export function AppSidebar({
198
199
  <SidebarTrigger aria-label="Toggle sidebar" />
199
200
  </div>
200
201
  <NavUser user={user ?? data.user} />
202
+ {version ? (
203
+ <div className="mt-1 text-xs text-muted-foreground">
204
+ <a href="/changelog" title="View changelog" className="hover:underline">v{version}</a>
205
+ </div>
206
+ ) : null}
201
207
  </SidebarFooter>
202
208
  <SidebarRail />
203
209
  </Sidebar>
@@ -51,7 +51,7 @@ export function ThemeModeToggle() {
51
51
  return (
52
52
  <DropdownMenu>
53
53
  <DropdownMenuTrigger asChild>
54
- <Button variant="ghost" size="icon" className="h-7 w-7" aria-label={`Theme: ${mode}`}>
54
+ <Button variant="ghost" size="icon" className="h-7 w-7 p-0" aria-label={`Theme: ${mode}`}>
55
55
  {currentIcon}
56
56
  <span className="sr-only">Toggle theme</span>
57
57
  </Button>
@@ -0,0 +1,126 @@
1
+ // FloatingActionButton.jsx
2
+ // ---------------------------------
3
+ // A shadcn-styled Floating Action Button (FAB).
4
+ // - Single fluid button that contains both icon and (optional) label
5
+ // - Label reveal is controlled by showLabel: 'hover' | 'always' | 'never'
6
+ // - Subtle icon and padding animation on hover when showLabel='hover'
7
+ // - Supports fixed positioning to screen corners
8
+ //
9
+ // Example usage:
10
+ //
11
+ // import { FloatingActionButton } from "@snapdragonsnursery/react-components";
12
+ // import { Plus } from "lucide-react";
13
+ //
14
+ // export default function Example() {
15
+ // return (
16
+ // <FloatingActionButton
17
+ // icon={Plus}
18
+ // label="Create"
19
+ // onClick={() => console.log("Create clicked")}
20
+ // showLabel="hover"
21
+ // position="bottom-right"
22
+ // />
23
+ // );
24
+ // }
25
+ //
26
+ // Notes:
27
+ // - Provide ariaLabel if label is omitted.
28
+ // - Uses Tailwind CSS + shadcn design tokens (bg-primary, text-primary-foreground, etc.).
29
+
30
+ import * as React from "react";
31
+ import { cn } from "../../lib/utils";
32
+ import { Button } from "./button.jsx";
33
+
34
+ const POSITION_CLASSES = {
35
+ "bottom-right": "fixed bottom-5 right-5",
36
+ "bottom-left": "fixed bottom-5 left-5",
37
+ "top-right": "fixed top-5 right-5",
38
+ "top-left": "fixed top-5 left-5",
39
+ none: "",
40
+ };
41
+
42
+ function FloatingActionButton({
43
+ icon: Icon,
44
+ label = "",
45
+ onClick,
46
+ showLabel,
47
+ // Deprecated: expandOnHover kept for backwards compatibility. If provided and showLabel is undefined,
48
+ // we map: true -> 'hover', false -> 'never'.
49
+ expandOnHover,
50
+ position = "bottom-right",
51
+ className,
52
+ disabled = false,
53
+ ariaLabel,
54
+ positionClassName,
55
+ colorClassName,
56
+ ...props
57
+ }) {
58
+ const resolvedShowLabel = React.useMemo(() => {
59
+ if (showLabel) return showLabel;
60
+ if (typeof expandOnHover === "boolean") {
61
+ return expandOnHover ? "hover" : "never";
62
+ }
63
+ return label ? "hover" : "never";
64
+ }, [showLabel, expandOnHover, label]);
65
+
66
+ const containerClasses = cn(
67
+ positionClassName || (POSITION_CLASSES[position ?? "none"] ?? POSITION_CLASSES.none),
68
+ className
69
+ );
70
+
71
+ const baseButton = cn(
72
+ "rounded-full shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 leading-none",
73
+ colorClassName || "bg-primary hover:bg-primary/90 text-primary-foreground"
74
+ );
75
+
76
+ const shapeClasses =
77
+ resolvedShowLabel === "never"
78
+ ? "h-14 w-14 inline-flex items-center justify-center gap-0 p-0"
79
+ : resolvedShowLabel === "always"
80
+ ? "h-14 inline-flex items-center justify-center gap-0 px-4"
81
+ : // hover mode
82
+ "group h-14 inline-flex items-center gap-0 justify-center min-w-[3.5rem] px-0 transition-all duration-300 ease-out hover:px-4";
83
+
84
+ const buttonClasses = cn(baseButton, shapeClasses);
85
+
86
+ const iconClasses = cn(
87
+ "h-6 w-6 block shrink-0 pointer-events-none transition-transform duration-200 ease-out",
88
+ resolvedShowLabel === "hover" ? "group-hover:scale-110 group-hover:rotate-3" : ""
89
+ );
90
+
91
+ const labelClasses = cn(
92
+ "leading-none whitespace-nowrap",
93
+ resolvedShowLabel === "always"
94
+ ? "ml-2"
95
+ : resolvedShowLabel === "hover"
96
+ ? "max-w-0 overflow-hidden opacity-0 scale-95 group-hover:max-w-[14rem] group-hover:opacity-100 group-hover:scale-100 group-hover:ml-2 transition-all duration-300 ease-out"
97
+ : "sr-only"
98
+ );
99
+
100
+ return (
101
+ <div className={containerClasses} data-testid="fab-container">
102
+ <Button
103
+ type="button"
104
+ data-slot="floating-action-button"
105
+ className={buttonClasses}
106
+ size={resolvedShowLabel === "never" ? "icon" : undefined}
107
+ aria-label={ariaLabel || label || "Action"}
108
+ title={ariaLabel || label || "Action"}
109
+ onClick={onClick}
110
+ disabled={disabled}
111
+ {...props}
112
+ >
113
+ {Icon ? <Icon className={iconClasses} aria-hidden="true" /> : null}
114
+ {label ? (
115
+ <span className={labelClasses} data-testid={resolvedShowLabel !== "never" ? "fab-label" : undefined}>
116
+ {label}
117
+ </span>
118
+ ) : null}
119
+ </Button>
120
+ </div>
121
+ );
122
+ }
123
+
124
+ export default FloatingActionButton;
125
+
126
+
@@ -0,0 +1,46 @@
1
+ // Basic tests for FloatingActionButton
2
+ // - Renders icon and label behaviour depending on expandOnHover
3
+
4
+ import React from 'react'
5
+ import { render, screen, fireEvent } from '@testing-library/react'
6
+ import FloatingActionButton from './floating-action-button'
7
+
8
+ function DummyIcon(props) {
9
+ return <svg aria-hidden="true" data-testid="dummy-icon" {...props} />
10
+ }
11
+
12
+ describe('FloatingActionButton (library)', () => {
13
+ it('renders with icon', () => {
14
+ render(<FloatingActionButton icon={DummyIcon} ariaLabel="Action" />)
15
+ expect(screen.getByTestId('dummy-icon')).toBeInTheDocument()
16
+ })
17
+
18
+ it('expands label on hover when showLabel="hover"', () => {
19
+ render(<FloatingActionButton icon={DummyIcon} label="Create" showLabel="hover" />)
20
+ // Label exists in DOM (hidden initially via CSS)
21
+ const label = screen.getByTestId('fab-label')
22
+ expect(label).toBeInTheDocument()
23
+ // Simulate hover on container
24
+ const container = screen.getByTestId('fab-container')
25
+ fireEvent.mouseOver(container)
26
+ // Ensure label text is present
27
+ expect(label).toHaveTextContent('Create')
28
+ })
29
+
30
+ it('hides label visually when showLabel="never"', () => {
31
+ render(<FloatingActionButton icon={DummyIcon} label="Search" showLabel="never" ariaLabel="Search" />)
32
+ // No test id for label since not rendered
33
+ const button = screen.getByRole('button', { name: /search/i })
34
+ expect(button).toBeInTheDocument()
35
+ })
36
+
37
+ it('calls onClick when clicked', () => {
38
+ const onClick = jest.fn()
39
+ render(<FloatingActionButton icon={DummyIcon} ariaLabel="Action" onClick={onClick} />)
40
+ const button = screen.getByRole('button', { name: /action/i })
41
+ fireEvent.click(button)
42
+ expect(onClick).toHaveBeenCalledTimes(1)
43
+ })
44
+ })
45
+
46
+
@@ -233,7 +233,7 @@ function SidebarTrigger({
233
233
  data-slot="sidebar-trigger"
234
234
  variant="ghost"
235
235
  size="icon"
236
- className={cn("size-7", className)}
236
+ className={cn("h-7 w-7 p-0", className)}
237
237
  onClick={(event) => {
238
238
  onClick?.(event)
239
239
  toggleSidebar()
package/src/index.d.ts CHANGED
@@ -66,7 +66,22 @@ export function configureTelemetry(...args: any[]): any
66
66
 
67
67
 
68
68
  // Sidebar + UI exports
69
- export const AppSidebar: React.ComponentType<any>
69
+ export interface AppSidebarProps {
70
+ sites?: any
71
+ activeSiteId?: any
72
+ onSiteChange?: (item: any) => void
73
+ sitesLoading?: boolean
74
+ rooms?: any
75
+ activeRoomId?: any
76
+ onRoomChange?: (item: any) => void
77
+ roomsLoading?: boolean
78
+ roomBaseColor?: string
79
+ navItems?: any
80
+ projects?: any
81
+ user?: any
82
+ version?: string
83
+ }
84
+ export const AppSidebar: React.ComponentType<AppSidebarProps>
70
85
  export const Sidebar: React.ComponentType<any>
71
86
  export const SidebarContent: React.ComponentType<any>
72
87
  export const SidebarFooter: React.ComponentType<any>
@@ -101,6 +116,23 @@ export const BreadcrumbPage: React.ComponentType<any>
101
116
  export const BreadcrumbSeparator: React.ComponentType<any>
102
117
  export const BreadcrumbEllipsis: React.ComponentType<any>
103
118
 
119
+ // Floating Action Button
120
+ export interface FloatingActionButtonProps {
121
+ icon?: React.ComponentType<any>
122
+ label?: string
123
+ onClick?: () => void
124
+ showLabel?: 'hover' | 'always' | 'never'
125
+ // @deprecated use showLabel instead. If provided: true => 'hover', false => 'never'
126
+ expandOnHover?: boolean
127
+ position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' | 'none'
128
+ className?: string
129
+ positionClassName?: string
130
+ colorClassName?: string
131
+ disabled?: boolean
132
+ ariaLabel?: string
133
+ }
134
+ export const FloatingActionButton: React.ComponentType<FloatingActionButtonProps>
135
+
104
136
  // Switchers
105
137
  export interface SwitcherItem {
106
138
  id?: string | number
package/src/index.js CHANGED
@@ -74,3 +74,6 @@ export {
74
74
  BreadcrumbSeparator,
75
75
  BreadcrumbEllipsis,
76
76
  } from "./components/ui/breadcrumb.jsx";
77
+
78
+ // Floating Action Button
79
+ export { default as FloatingActionButton } from "./components/ui/floating-action-button.jsx";