@snapdragonsnursery/react-components 1.6.0 → 1.8.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.
@@ -0,0 +1,100 @@
1
+ // Stat Card UI Component (shared library)
2
+ // Reusable statistic card with optional icon and caption.
3
+ // Example:
4
+ // <StatCard title="Active Users" value="97K" caption="+24.3% this month" icon={ArrowTrendingUpIcon} tone="success" />
5
+
6
+ import React from 'react'
7
+ import { cn } from '../../lib/utils'
8
+
9
+ // Lightweight Card primitives aligned with this package styling
10
+ const Card = React.forwardRef(function Card({ className, ...props }, ref) {
11
+ return (
12
+ <div
13
+ ref={ref}
14
+ className={cn('rounded-lg border bg-card text-card-foreground shadow-sm', className)}
15
+ {...props}
16
+ />
17
+ )
18
+ })
19
+
20
+ const CardHeader = React.forwardRef(function CardHeader({ className, ...props }, ref) {
21
+ return <div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
22
+ })
23
+
24
+ const CardTitle = React.forwardRef(function CardTitle({ className, ...props }, ref) {
25
+ return (
26
+ <h3
27
+ ref={ref}
28
+ className={cn('text-2xl font-semibold leading-none tracking-tight', className)}
29
+ {...props}
30
+ />
31
+ )
32
+ })
33
+
34
+ const CardContent = React.forwardRef(function CardContent({ className, ...props }, ref) {
35
+ return <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
36
+ })
37
+
38
+ function Skeleton({ className, ...props }) {
39
+ return <div className={cn('animate-pulse rounded-md bg-muted', className)} {...props} />
40
+ }
41
+
42
+ const toneClasses = {
43
+ default: {
44
+ badge: 'bg-gray-100 text-gray-700',
45
+ value: 'text-foreground',
46
+ ring: 'ring-gray-200',
47
+ },
48
+ success: {
49
+ badge: 'bg-emerald-100 text-emerald-700',
50
+ value: 'text-emerald-700',
51
+ ring: 'ring-emerald-200',
52
+ },
53
+ danger: {
54
+ badge: 'bg-rose-100 text-rose-700',
55
+ value: 'text-rose-700',
56
+ ring: 'ring-rose-200',
57
+ },
58
+ warning: {
59
+ badge: 'bg-amber-100 text-amber-700',
60
+ value: 'text-amber-700',
61
+ ring: 'ring-amber-200',
62
+ },
63
+ info: {
64
+ badge: 'bg-sky-100 text-sky-700',
65
+ value: 'text-sky-700',
66
+ ring: 'ring-sky-200',
67
+ },
68
+ }
69
+
70
+ export function StatCard({ title, value, caption, icon: Icon, tone = 'default', className, loading = false }) {
71
+ const styles = toneClasses[tone] || toneClasses.default
72
+ return (
73
+ <Card className={cn('shadow-sm hover:shadow transition-shadow', className)}>
74
+ <CardHeader className="pb-3">
75
+ <div className="flex items-center justify-between">
76
+ <CardTitle className="text-sm font-medium text-muted-foreground">
77
+ {loading ? <Skeleton className="h-4 w-24" /> : title}
78
+ </CardTitle>
79
+ {Icon ? (
80
+ <div className={cn('inline-flex h-10 w-10 items-center justify-center rounded-full ring-1', styles.badge, styles.ring)}>
81
+ {loading ? <Skeleton className="h-5 w-5 rounded-full" /> : <Icon className="h-5 w-5" />}
82
+ </div>
83
+ ) : null}
84
+ </div>
85
+ </CardHeader>
86
+ <CardContent>
87
+ <div className={cn('text-3xl font-extrabold tracking-tight', styles.value)}>
88
+ {loading ? <Skeleton className="h-8 w-20" /> : value}
89
+ </div>
90
+ {caption ? (
91
+ <div className="mt-1 text-sm text-muted-foreground">
92
+ {loading ? <Skeleton className="h-4 w-28" /> : caption}
93
+ </div>
94
+ ) : null}
95
+ </CardContent>
96
+ </Card>
97
+ )
98
+ }
99
+
100
+ export default StatCard
@@ -0,0 +1,24 @@
1
+ // Basic render tests for StatCard
2
+ // Example usage demonstrated in the component file header.
3
+
4
+ import React from 'react'
5
+ import { render, screen } from '@testing-library/react'
6
+ import StatCard from './stat-card'
7
+
8
+ describe('StatCard (library)', () => {
9
+ it('renders title and value', () => {
10
+ render(<StatCard title="Total" value={42} caption="units" />)
11
+ expect(screen.getByText('Total')).toBeInTheDocument()
12
+ expect(screen.getByText('42')).toBeInTheDocument()
13
+ expect(screen.getByText('units')).toBeInTheDocument()
14
+ })
15
+
16
+ it('renders skeletons when loading', () => {
17
+ const { container } = render(<StatCard title="Total" value={42} caption="units" loading />)
18
+ expect(screen.queryByText('Total')).toBeNull()
19
+ expect(screen.queryByText('42')).toBeNull()
20
+ expect(screen.queryByText('units')).toBeNull()
21
+ // Skeletons exist
22
+ expect(container.querySelectorAll('.animate-pulse').length).toBeGreaterThan(0)
23
+ })
24
+ })
@@ -0,0 +1,53 @@
1
+ import * as React from "react"
2
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip"
3
+
4
+ import { cn } from "../../lib/utils"
5
+
6
+ function TooltipProvider({
7
+ delayDuration = 0,
8
+ ...props
9
+ }) {
10
+ return (<TooltipPrimitive.Provider data-slot="tooltip-provider" delayDuration={delayDuration} {...props} />);
11
+ }
12
+
13
+ function Tooltip({
14
+ ...props
15
+ }) {
16
+ return (
17
+ <TooltipProvider>
18
+ <TooltipPrimitive.Root data-slot="tooltip" {...props} />
19
+ </TooltipProvider>
20
+ );
21
+ }
22
+
23
+ function TooltipTrigger({
24
+ ...props
25
+ }) {
26
+ return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
27
+ }
28
+
29
+ function TooltipContent({
30
+ className,
31
+ sideOffset = 0,
32
+ children,
33
+ ...props
34
+ }) {
35
+ return (
36
+ <TooltipPrimitive.Portal>
37
+ <TooltipPrimitive.Content
38
+ data-slot="tooltip-content"
39
+ sideOffset={sideOffset}
40
+ className={cn(
41
+ "bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
42
+ className
43
+ )}
44
+ {...props}>
45
+ {children}
46
+ <TooltipPrimitive.Arrow
47
+ className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
48
+ </TooltipPrimitive.Content>
49
+ </TooltipPrimitive.Portal>
50
+ );
51
+ }
52
+
53
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
@@ -0,0 +1,61 @@
1
+ import * as React from "react"
2
+
3
+ export const SITE_STORAGE_KEY = "current_site_id"
4
+ export const SITE_EVENT_NAME = "siteChanged"
5
+
6
+ export function readCurrentSiteId(storageKey = SITE_STORAGE_KEY) {
7
+ if (typeof window === "undefined") return null
8
+ try {
9
+ const v = window.localStorage.getItem(storageKey)
10
+ if (v == null) return null
11
+ const n = Number(v)
12
+ return Number.isNaN(n) ? v : n
13
+ } catch {
14
+ return null
15
+ }
16
+ }
17
+
18
+ export function writeCurrentSiteId(siteId, storageKey = SITE_STORAGE_KEY) {
19
+ if (typeof window === "undefined") return
20
+ try {
21
+ if (siteId == null) {
22
+ window.localStorage.removeItem(storageKey)
23
+ } else {
24
+ window.localStorage.setItem(storageKey, String(siteId))
25
+ }
26
+ } catch {}
27
+
28
+ try {
29
+ const numeric = typeof siteId === "number" ? siteId : Number(siteId)
30
+ const detail = { siteId: Number.isFinite(numeric) ? numeric : siteId }
31
+ window.dispatchEvent(new CustomEvent(SITE_EVENT_NAME, { detail }))
32
+ } catch {}
33
+ }
34
+
35
+ export function useCurrentSiteId({ storageKey = SITE_STORAGE_KEY, eventName = SITE_EVENT_NAME } = {}) {
36
+ const [currentSiteId, setCurrentSiteId] = React.useState(null)
37
+
38
+ React.useEffect(() => {
39
+ // Initialize from storage on mount
40
+ const initial = readCurrentSiteId(storageKey)
41
+ setCurrentSiteId(initial)
42
+
43
+ // Listen for global site changes
44
+ const handler = (e) => {
45
+ try {
46
+ const id = e?.detail?.siteId
47
+ if (typeof id !== "undefined") setCurrentSiteId(id)
48
+ } catch {}
49
+ }
50
+ window.addEventListener(eventName, handler)
51
+ return () => window.removeEventListener(eventName, handler)
52
+ }, [storageKey, eventName])
53
+
54
+ const update = React.useCallback((id) => {
55
+ setCurrentSiteId(id)
56
+ writeCurrentSiteId(id, storageKey)
57
+ }, [storageKey])
58
+
59
+ return [currentSiteId, update]
60
+ }
61
+
@@ -0,0 +1,19 @@
1
+ import * as React from "react"
2
+
3
+ const MOBILE_BREAKPOINT = 768
4
+
5
+ export function useIsMobile() {
6
+ const [isMobile, setIsMobile] = React.useState(undefined)
7
+
8
+ React.useEffect(() => {
9
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10
+ const onChange = () => {
11
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12
+ }
13
+ mql.addEventListener("change", onChange)
14
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15
+ return () => mql.removeEventListener("change", onChange);
16
+ }, [])
17
+
18
+ return !!isMobile
19
+ }
package/src/index.css CHANGED
@@ -1,4 +1,6 @@
1
1
  @tailwind base;
2
+
3
+ @custom-variant dark (&:is(.dark *));
2
4
  @tailwind components;
3
5
  @tailwind utilities;
4
6
 
@@ -56,4 +58,46 @@
56
58
  body {
57
59
  @apply bg-background text-foreground;
58
60
  }
61
+ }
62
+
63
+ :root {
64
+ --sidebar: hsl(0 0% 98%);
65
+ --sidebar-foreground: hsl(240 5.3% 26.1%);
66
+ --sidebar-primary: hsl(240 5.9% 10%);
67
+ --sidebar-primary-foreground: hsl(0 0% 98%);
68
+ --sidebar-accent: hsl(240 4.8% 95.9%);
69
+ --sidebar-accent-foreground: hsl(240 5.9% 10%);
70
+ --sidebar-border: hsl(220 13% 91%);
71
+ --sidebar-ring: hsl(217.2 91.2% 59.8%);
72
+ }
73
+
74
+ .dark {
75
+ --sidebar: hsl(240 5.9% 10%);
76
+ --sidebar-foreground: hsl(240 4.8% 95.9%);
77
+ --sidebar-primary: hsl(224.3 76.3% 48%);
78
+ --sidebar-primary-foreground: hsl(0 0% 100%);
79
+ --sidebar-accent: hsl(240 3.7% 15.9%);
80
+ --sidebar-accent-foreground: hsl(240 4.8% 95.9%);
81
+ --sidebar-border: hsl(240 3.7% 15.9%);
82
+ --sidebar-ring: hsl(217.2 91.2% 59.8%);
83
+ }
84
+
85
+ @theme inline {
86
+ --color-sidebar: var(--sidebar);
87
+ --color-sidebar-foreground: var(--sidebar-foreground);
88
+ --color-sidebar-primary: var(--sidebar-primary);
89
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
90
+ --color-sidebar-accent: var(--sidebar-accent);
91
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
92
+ --color-sidebar-border: var(--sidebar-border);
93
+ --color-sidebar-ring: var(--sidebar-ring);
94
+ }
95
+
96
+ @layer base {
97
+ * {
98
+ @apply border-border outline-ring/50;
99
+ }
100
+ body {
101
+ @apply bg-background text-foreground;
102
+ }
59
103
  }
package/src/index.d.ts CHANGED
@@ -36,13 +36,99 @@ export const EmployeeSearchDemo: React.ComponentType<any>
36
36
  export const EmployeeSearchFilters: React.ComponentType<any>
37
37
 
38
38
  export const DateRangePicker: React.ComponentType<any>
39
- export const DatePicker: React.ComponentType<any>
39
+ export interface DatePickerProps {
40
+ selectedDate?: Date | null
41
+ onSelect: (date: Date | null) => void
42
+ className?: string
43
+ placeholder?: string
44
+ disabled?: boolean
45
+ disableFuture?: boolean
46
+ }
47
+ export const DatePicker: React.ComponentType<DatePickerProps>
40
48
  export const Calendar: React.ComponentType<any>
41
49
  export const SimpleCalendar: React.ComponentType<any>
42
50
  export const Popover: React.ComponentType<any>
43
51
  export const PopoverContent: React.ComponentType<any>
44
52
  export const PopoverTrigger: React.ComponentType<any>
45
53
 
54
+ export interface StatCardProps {
55
+ title: string
56
+ value: React.ReactNode
57
+ caption?: React.ReactNode
58
+ icon?: React.ComponentType<any>
59
+ tone?: 'default' | 'success' | 'danger' | 'warning' | 'info'
60
+ className?: string
61
+ loading?: boolean
62
+ }
63
+ export const StatCard: React.ComponentType<StatCardProps>
64
+
46
65
  export function configureTelemetry(...args: any[]): any
47
66
 
48
67
 
68
+ // Sidebar + UI exports
69
+ export const AppSidebar: React.ComponentType<any>
70
+ export const Sidebar: React.ComponentType<any>
71
+ export const SidebarContent: React.ComponentType<any>
72
+ export const SidebarFooter: React.ComponentType<any>
73
+ export const SidebarGroup: React.ComponentType<any>
74
+ export const SidebarGroupAction: React.ComponentType<any>
75
+ export const SidebarGroupContent: React.ComponentType<any>
76
+ export const SidebarGroupLabel: React.ComponentType<any>
77
+ export const SidebarHeader: React.ComponentType<any>
78
+ export const SidebarInput: React.ComponentType<any>
79
+ export const SidebarInset: React.ComponentType<any>
80
+ export const SidebarMenu: React.ComponentType<any>
81
+ export const SidebarMenuAction: React.ComponentType<any>
82
+ export const SidebarMenuBadge: React.ComponentType<any>
83
+ export const SidebarMenuButton: React.ComponentType<any>
84
+ export const SidebarMenuItem: React.ComponentType<any>
85
+ export const SidebarMenuSkeleton: React.ComponentType<any>
86
+ export const SidebarMenuSub: React.ComponentType<any>
87
+ export const SidebarMenuSubButton: React.ComponentType<any>
88
+ export const SidebarMenuSubItem: React.ComponentType<any>
89
+ export const SidebarProvider: React.ComponentType<any>
90
+ export const SidebarRail: React.ComponentType<any>
91
+ export const SidebarSeparator: React.ComponentType<any>
92
+ export const SidebarTrigger: React.ComponentType<any>
93
+ export function useSidebar(): any
94
+
95
+ export const Separator: React.ComponentType<any>
96
+ export const Breadcrumb: React.ComponentType<any>
97
+ export const BreadcrumbItem: React.ComponentType<any>
98
+ export const BreadcrumbLink: React.ComponentType<any>
99
+ export const BreadcrumbList: React.ComponentType<any>
100
+ export const BreadcrumbPage: React.ComponentType<any>
101
+ export const BreadcrumbSeparator: React.ComponentType<any>
102
+ export const BreadcrumbEllipsis: React.ComponentType<any>
103
+
104
+ // Switchers
105
+ export interface SwitcherItem {
106
+ id?: string | number
107
+ name: string
108
+ icon?: React.ComponentType<any>
109
+ logo?: React.ComponentType<any>
110
+ }
111
+ export interface SiteSwitcherProps {
112
+ items?: SwitcherItem[]
113
+ activeId?: string | number
114
+ onChange?: (item: SwitcherItem) => void
115
+ label?: string
116
+ isLoading?: boolean
117
+ }
118
+ export interface RoomSwitcherProps {
119
+ items?: SwitcherItem[]
120
+ activeId?: string | number
121
+ onChange?: (item: SwitcherItem) => void
122
+ label?: string
123
+ isLoading?: boolean
124
+ baseColor?: string
125
+ }
126
+ export const SiteSwitcher: React.ComponentType<SiteSwitcherProps>
127
+ export const RoomSwitcher: React.ComponentType<RoomSwitcherProps>
128
+
129
+ // Site selection helpers
130
+ export const SITE_STORAGE_KEY: string
131
+ export const SITE_EVENT_NAME: string
132
+ export function readCurrentSiteId(storageKey?: string): string | number | null
133
+ export function writeCurrentSiteId(siteId: string | number | null, storageKey?: string): void
134
+ export function useCurrentSiteId(options?: { storageKey?: string; eventName?: string }): [string | number | null, (id: string | number | null) => void]
package/src/index.js CHANGED
@@ -1,27 +1,76 @@
1
- export { default as AuthButtons } from "./AuthButtons";
2
- export { default as ThemeToggle } from "./ThemeToggle";
3
- export { default as ChildSearchModal } from "./ChildSearchModal";
4
- export { default as ChildSearchPage } from "./ChildSearchPage";
5
- export { default as ChildSearchPageDemo } from "./ChildSearchPageDemo";
6
- export { default as ThemeToggleTest } from "./ThemeToggleTest";
7
- export { default as LandingPage } from "./LandingPage";
8
- export { default as ChildSearchFilters } from "./components/ChildSearchFilters";
9
- export { default as DateRangePickerDemo } from "./DateRangePickerDemo";
10
- export { default as CalendarDemo } from "./CalendarDemo";
11
- export { default as DateRangePickerTest } from "./DateRangePickerTest";
12
- export { default as ApplyButtonDemo } from "./ApplyButtonDemo";
1
+ export { default as AuthButtons } from "./AuthButtons.jsx";
2
+ export { default as ThemeToggle } from "./ThemeToggle.jsx";
3
+ export { default as ChildSearchModal } from "./ChildSearchModal.jsx";
4
+ export { default as ChildSearchPage } from "./ChildSearchPage.jsx";
5
+ export { default as ChildSearchPageDemo } from "./ChildSearchPageDemo.jsx";
6
+ export { default as ThemeToggleTest } from "./ThemeToggleTest.jsx";
7
+ export { default as LandingPage } from "./LandingPage.jsx";
8
+ export { default as ChildSearchFilters } from "./components/ChildSearchFilters.jsx";
9
+ export { default as DateRangePickerDemo } from "./DateRangePickerDemo.jsx";
10
+ export { default as CalendarDemo } from "./CalendarDemo.jsx";
11
+ export { default as DateRangePickerTest } from "./DateRangePickerTest.jsx";
12
+ export { default as ApplyButtonDemo } from "./ApplyButtonDemo.jsx";
13
13
 
14
14
  // Employee Search Components
15
- export { default as EmployeeSearchPage } from "./EmployeeSearchPage";
16
- export { default as EmployeeSearchModal } from "./EmployeeSearchModal";
17
- export { default as EmployeeSearchDemo } from "./EmployeeSearchDemo";
18
- export { default as EmployeeSearchFilters } from "./components/EmployeeSearchFilters";
15
+ export { default as EmployeeSearchPage } from "./EmployeeSearchPage.jsx";
16
+ export { default as EmployeeSearchModal } from "./EmployeeSearchModal.jsx";
17
+ export { default as EmployeeSearchDemo } from "./EmployeeSearchDemo.jsx";
18
+ export { default as EmployeeSearchFilters } from "./components/EmployeeSearchFilters.jsx";
19
19
 
20
- export { configureTelemetry } from "./telemetry";
20
+ export { configureTelemetry } from "./telemetry.js";
21
21
 
22
22
  // UI Components
23
- export { DateRangePicker, DatePicker } from "./components/ui/date-range-picker";
24
- export { Calendar } from "./components/ui/calendar";
25
- export { SimpleCalendar } from "./components/ui/simple-calendar";
26
- export { Popover, PopoverContent, PopoverTrigger } from "./components/ui/popover";
27
- export { default as SoftWarningAlert } from "./components/ui/soft-warning-alert";
23
+ export { DateRangePicker, DatePicker } from "./components/ui/date-range-picker.jsx";
24
+ export { Calendar } from "./components/ui/calendar.jsx";
25
+ export { SimpleCalendar } from "./components/ui/simple-calendar.jsx";
26
+ export { Popover, PopoverContent, PopoverTrigger } from "./components/ui/popover.jsx";
27
+ export { default as SoftWarningAlert } from "./components/ui/soft-warning-alert.jsx";
28
+ export { default as StatCard } from "./components/ui/stat-card.jsx";
29
+
30
+ // Shadcn Sidebar + related UI
31
+ export { AppSidebar } from "./components/app-sidebar.jsx";
32
+ export { SiteSwitcher } from "./components/site-switcher.jsx";
33
+ export { RoomSwitcher } from "./components/room-switcher.jsx";
34
+ export {
35
+ useCurrentSiteId,
36
+ readCurrentSiteId,
37
+ writeCurrentSiteId,
38
+ SITE_STORAGE_KEY,
39
+ SITE_EVENT_NAME,
40
+ } from "./hooks/use-current-site.js";
41
+ export {
42
+ Sidebar,
43
+ SidebarContent,
44
+ SidebarFooter,
45
+ SidebarGroup,
46
+ SidebarGroupAction,
47
+ SidebarGroupContent,
48
+ SidebarGroupLabel,
49
+ SidebarHeader,
50
+ SidebarInput,
51
+ SidebarInset,
52
+ SidebarMenu,
53
+ SidebarMenuAction,
54
+ SidebarMenuBadge,
55
+ SidebarMenuButton,
56
+ SidebarMenuItem,
57
+ SidebarMenuSkeleton,
58
+ SidebarMenuSub,
59
+ SidebarMenuSubButton,
60
+ SidebarMenuSubItem,
61
+ SidebarProvider,
62
+ SidebarRail,
63
+ SidebarSeparator,
64
+ SidebarTrigger,
65
+ useSidebar,
66
+ } from "./components/ui/sidebar.jsx";
67
+ export { Separator } from "./components/ui/separator.jsx";
68
+ export {
69
+ Breadcrumb,
70
+ BreadcrumbItem,
71
+ BreadcrumbLink,
72
+ BreadcrumbList,
73
+ BreadcrumbPage,
74
+ BreadcrumbSeparator,
75
+ BreadcrumbEllipsis,
76
+ } from "./components/ui/breadcrumb.jsx";