@snapdragonsnursery/react-components 1.5.0 → 1.6.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/README.md
CHANGED
|
@@ -6,13 +6,14 @@ A collection of reusable React components for Snapdragons Nursery applications.
|
|
|
6
6
|
|
|
7
7
|
- **ChildSearchModal**: Advanced child search and selection component with filtering, pagination, and multi-select capabilities
|
|
8
8
|
- **ChildSearchFilters**: Advanced filtering component with date range picker, status, site, and age filters (includes Apply button for better UX)
|
|
9
|
-
- **DateRangePicker**: Shadcn-style date range picker component
|
|
9
|
+
- **DateRangePicker**: Shadcn-style date range picker component (supports optional presets)
|
|
10
10
|
- **DatePicker**: Shadcn-style single date picker component
|
|
11
11
|
- **Calendar**: Official shadcn calendar component
|
|
12
12
|
- **Popover**: Official shadcn popover component
|
|
13
13
|
- **AuthButtons**: Authentication buttons for MSAL integration
|
|
14
14
|
- **ThemeToggle**: Dark/light theme toggle component
|
|
15
15
|
- **LandingPage**: Landing page component with authentication
|
|
16
|
+
- **SoftWarningAlert**: Soft-styled alert for non-blocking warnings with optional action
|
|
16
17
|
|
|
17
18
|
## Installation
|
|
18
19
|
|
|
@@ -39,6 +40,25 @@ function MyComponent() {
|
|
|
39
40
|
}
|
|
40
41
|
```
|
|
41
42
|
|
|
43
|
+
### SoftWarningAlert Example
|
|
44
|
+
|
|
45
|
+
```jsx
|
|
46
|
+
import { SoftWarningAlert } from '@snapdragonsnursery/react-components';
|
|
47
|
+
import { AlertTriangle } from 'lucide-react';
|
|
48
|
+
|
|
49
|
+
function Notice() {
|
|
50
|
+
return (
|
|
51
|
+
<SoftWarningAlert
|
|
52
|
+
icon={AlertTriangle}
|
|
53
|
+
title="Unsubmitted claims"
|
|
54
|
+
description="You have 3 unsubmitted mileage claims. Create a report to submit."
|
|
55
|
+
actionLabel="Create report"
|
|
56
|
+
onAction={() => console.log('clicked')}
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
42
62
|
## Shadcn Components
|
|
43
63
|
|
|
44
64
|
This package includes official shadcn components with proper styling. The components use shadcn CSS variables, so make sure your consuming project has the shadcn CSS variables defined in your CSS file.
|
|
@@ -118,11 +138,28 @@ function MyComponent() {
|
|
|
118
138
|
};
|
|
119
139
|
|
|
120
140
|
return (
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
141
|
+
<>
|
|
142
|
+
<DateRangePicker
|
|
143
|
+
selectedRange={selectedRange}
|
|
144
|
+
onSelect={handleDateRangeChange}
|
|
145
|
+
placeholder="Select a date range"
|
|
146
|
+
numberOfMonths={2}
|
|
147
|
+
/>
|
|
148
|
+
{/* With presets */}
|
|
149
|
+
<DateRangePicker
|
|
150
|
+
selectedRange={selectedRange}
|
|
151
|
+
onSelect={handleDateRangeChange}
|
|
152
|
+
presetsEnabled
|
|
153
|
+
presets={[
|
|
154
|
+
{ key: 'thisWeek', label: 'This week', getRange: () => ({ from: startOfWeek(new Date(), { weekStartsOn: 1 }), to: new Date() }) },
|
|
155
|
+
{ key: 'lastWeek', label: 'Last week', getRange: () => { const ref = subWeeks(new Date(), 1); return { from: startOfWeek(ref, { weekStartsOn: 1 }), to: endOfWeek(ref, { weekStartsOn: 1 }) } } },
|
|
156
|
+
{ key: 'thisMonth', label: 'This month', getRange: () => ({ from: startOfMonth(new Date()), to: new Date() }) },
|
|
157
|
+
{ key: 'lastMonth', label: 'Last month', getRange: () => { const ref = subMonths(new Date(), 1); return { from: startOfMonth(ref), to: endOfMonth(ref) } } },
|
|
158
|
+
{ key: 'thisYear', label: 'This year', getRange: () => ({ from: startOfYear(new Date()), to: new Date() }) },
|
|
159
|
+
{ key: 'lastYear', label: 'Last year', getRange: () => { const ref = subYears(new Date(), 1); return { from: startOfYear(ref), to: endOfYear(ref) } } },
|
|
160
|
+
]}
|
|
161
|
+
/>
|
|
162
|
+
</>
|
|
126
163
|
);
|
|
127
164
|
}
|
|
128
165
|
```
|
|
@@ -162,6 +199,7 @@ VITE_COMMON_API_BASE_URL=https://snaps-common-api.azurewebsites.net
|
|
|
162
199
|
- [ChildSearchModal Documentation](./CHILD_SEARCH_MODAL_DOCUMENTATION.md)
|
|
163
200
|
- [ChildSearchModal README](./CHILD_SEARCH_README.md)
|
|
164
201
|
- [Release Guide](./RELEASE.md)
|
|
202
|
+
- [SoftWarningAlert](./SOFT_WARNING_ALERT.md)
|
|
165
203
|
|
|
166
204
|
---
|
|
167
205
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@snapdragonsnursery/react-components",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"react": "^18.0.0 || ^19.0.0"
|
|
60
60
|
},
|
|
61
61
|
"module": "src/index.js",
|
|
62
|
+
"types": "src/index.d.ts",
|
|
62
63
|
"files": [
|
|
63
64
|
"src",
|
|
64
65
|
"src/index.css"
|
|
@@ -4,19 +4,28 @@
|
|
|
4
4
|
// Usage: <DateRangePicker selectedRange={range} onSelect={setRange} />
|
|
5
5
|
|
|
6
6
|
import React, { useState, useEffect, useRef } from 'react'
|
|
7
|
-
import { format } from 'date-fns'
|
|
7
|
+
import { format, addDays, addMonths, endOfMonth, endOfYear, startOfMonth, startOfYear, subDays, subMonths, subYears } from 'date-fns'
|
|
8
8
|
import { CalendarIcon } from '@heroicons/react/24/outline'
|
|
9
9
|
import { Popover, PopoverContent, PopoverTrigger } from './popover'
|
|
10
10
|
import { Calendar } from './calendar'
|
|
11
11
|
import { Button } from './button'
|
|
12
12
|
import { cn } from '../../lib/utils'
|
|
13
13
|
|
|
14
|
+
// Optional preset type: { key: string, label: string, getRange: () => ({ from: Date, to: Date }) }
|
|
14
15
|
export function DateRangePicker({
|
|
15
16
|
selectedRange,
|
|
16
17
|
onSelect,
|
|
17
18
|
className,
|
|
18
19
|
placeholder = "Select a date range",
|
|
19
20
|
disabled,
|
|
21
|
+
// New: optional presets support (backwards compatible)
|
|
22
|
+
presetsEnabled = false,
|
|
23
|
+
presets,
|
|
24
|
+
// New: allow consumers to control month count
|
|
25
|
+
numberOfMonths = 2,
|
|
26
|
+
// New: allow styling popover content and calendar for layout control
|
|
27
|
+
contentClassName,
|
|
28
|
+
calendarClassName,
|
|
20
29
|
...props
|
|
21
30
|
}) {
|
|
22
31
|
const [isOpen, setIsOpen] = useState(false)
|
|
@@ -185,8 +194,8 @@ export function DateRangePicker({
|
|
|
185
194
|
)}
|
|
186
195
|
</Button>
|
|
187
196
|
</PopoverTrigger>
|
|
188
|
-
<PopoverContent className=
|
|
189
|
-
<div className="relative">
|
|
197
|
+
<PopoverContent className={cn('w-[90vw] p-3 sm:w-auto', contentClassName)} align="start">
|
|
198
|
+
<div className="relative w-full sm:w-[520px]">
|
|
190
199
|
<Calendar
|
|
191
200
|
mode="range"
|
|
192
201
|
defaultMonth={internalRange?.from}
|
|
@@ -197,8 +206,47 @@ export function DateRangePicker({
|
|
|
197
206
|
handleSelect(range)
|
|
198
207
|
}
|
|
199
208
|
}}
|
|
200
|
-
numberOfMonths={
|
|
209
|
+
numberOfMonths={numberOfMonths}
|
|
210
|
+
className={cn('w-full', calendarClassName)}
|
|
201
211
|
/>
|
|
212
|
+
{presetsEnabled && (
|
|
213
|
+
<div className="mt-2 flex flex-wrap gap-2 p-2">
|
|
214
|
+
{(presets && presets.length > 0 ? presets : defaultPresets()).map((p) => {
|
|
215
|
+
const r = p.getRange()
|
|
216
|
+
return (
|
|
217
|
+
<Button
|
|
218
|
+
key={p.key}
|
|
219
|
+
variant="outline"
|
|
220
|
+
size="sm"
|
|
221
|
+
onClick={() => {
|
|
222
|
+
setInternalRange(r)
|
|
223
|
+
isSelectingRange.current = false
|
|
224
|
+
onSelect(r)
|
|
225
|
+
}}
|
|
226
|
+
>
|
|
227
|
+
{p.label}
|
|
228
|
+
</Button>
|
|
229
|
+
)
|
|
230
|
+
})}
|
|
231
|
+
</div>
|
|
232
|
+
)}
|
|
233
|
+
{/* Always-present footer actions for clarity on mobile/desktop */}
|
|
234
|
+
<div className="mt-3 flex items-center justify-between px-2">
|
|
235
|
+
<Button
|
|
236
|
+
variant="ghost"
|
|
237
|
+
size="sm"
|
|
238
|
+
onClick={() => {
|
|
239
|
+
setInternalRange(null)
|
|
240
|
+
isSelectingRange.current = false
|
|
241
|
+
onSelect(null)
|
|
242
|
+
}}
|
|
243
|
+
>
|
|
244
|
+
Clear
|
|
245
|
+
</Button>
|
|
246
|
+
<Button size="sm" onClick={() => setIsOpen(false)}>
|
|
247
|
+
Done
|
|
248
|
+
</Button>
|
|
249
|
+
</div>
|
|
202
250
|
{internalRange?.from && (
|
|
203
251
|
<div className="absolute top-2 right-2 flex gap-1">
|
|
204
252
|
<button
|
|
@@ -227,6 +275,23 @@ export function DateRangePicker({
|
|
|
227
275
|
)
|
|
228
276
|
}
|
|
229
277
|
|
|
278
|
+
// Provide a default preset set similar to app usage, but minimal
|
|
279
|
+
function defaultPresets() {
|
|
280
|
+
const today = new Date()
|
|
281
|
+
const last7Days = { from: subDays(today, 6), to: today }
|
|
282
|
+
const monthToDate = { from: startOfMonth(today), to: today }
|
|
283
|
+
const yearToDate = { from: startOfYear(today), to: today }
|
|
284
|
+
const lastMonth = { from: startOfMonth(subMonths(today, 1)), to: endOfMonth(subMonths(today, 1)) }
|
|
285
|
+
const lastYear = { from: startOfYear(subYears(today, 1)), to: endOfYear(subYears(today, 1)) }
|
|
286
|
+
return [
|
|
287
|
+
{ key: 'last7', label: 'Last 7 days', getRange: () => last7Days },
|
|
288
|
+
{ key: 'mtd', label: 'Month to date', getRange: () => monthToDate },
|
|
289
|
+
{ key: 'lastMonth', label: 'Last month', getRange: () => lastMonth },
|
|
290
|
+
{ key: 'ytd', label: 'Year to date', getRange: () => yearToDate },
|
|
291
|
+
{ key: 'lastYear', label: 'Last year', getRange: () => lastYear },
|
|
292
|
+
]
|
|
293
|
+
}
|
|
294
|
+
|
|
230
295
|
// Single date picker variant
|
|
231
296
|
export function DatePicker({
|
|
232
297
|
selectedDate,
|
|
@@ -38,7 +38,7 @@ describe('DateRangePicker', () => {
|
|
|
38
38
|
fireEvent.click(button)
|
|
39
39
|
|
|
40
40
|
await waitFor(() => {
|
|
41
|
-
expect(screen.getAllByRole('grid')).toHaveLength(2) // Two months
|
|
41
|
+
expect(screen.getAllByRole('grid')).toHaveLength(2) // Two months default
|
|
42
42
|
})
|
|
43
43
|
})
|
|
44
44
|
|
|
@@ -64,6 +64,24 @@ describe('DateRangePicker', () => {
|
|
|
64
64
|
})
|
|
65
65
|
})
|
|
66
66
|
|
|
67
|
+
describe('DateRangePicker presets', () => {
|
|
68
|
+
it('renders presets when enabled and selects a range on click', async () => {
|
|
69
|
+
const onSelect = jest.fn()
|
|
70
|
+
render(<DateRangePicker selectedRange={null} onSelect={onSelect} presetsEnabled numberOfMonths={1} />)
|
|
71
|
+
// Open popover
|
|
72
|
+
fireEvent.click(screen.getByRole('button'))
|
|
73
|
+
// Default preset button e.g. Last 7 days should appear
|
|
74
|
+
await waitFor(() => {
|
|
75
|
+
expect(screen.getByText(/last 7 days/i)).toBeInTheDocument()
|
|
76
|
+
})
|
|
77
|
+
fireEvent.click(screen.getByText(/last 7 days/i))
|
|
78
|
+
expect(onSelect).toHaveBeenCalled()
|
|
79
|
+
const arg = onSelect.mock.calls[0][0]
|
|
80
|
+
expect(arg?.from).toBeInstanceOf(Date)
|
|
81
|
+
expect(arg?.to).toBeInstanceOf(Date)
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
67
85
|
describe('DatePicker', () => {
|
|
68
86
|
it('renders with placeholder text', () => {
|
|
69
87
|
const mockOnSelect = jest.fn()
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
//
|
|
2
|
+
// soft-warning-alert.jsx
|
|
3
|
+
// -----------------------
|
|
4
|
+
// A soft-styled warning alert built on shadcn-like primitives used in this package.
|
|
5
|
+
// Use this for non-blocking warnings with an optional action button.
|
|
6
|
+
//
|
|
7
|
+
// Example:
|
|
8
|
+
// import { SoftWarningAlert } from '@snapdragonsnursery/react-components'
|
|
9
|
+
// import { AlertTriangle } from 'lucide-react'
|
|
10
|
+
//
|
|
11
|
+
// export default function Example() {
|
|
12
|
+
// return (
|
|
13
|
+
// <SoftWarningAlert
|
|
14
|
+
// icon={AlertTriangle}
|
|
15
|
+
// title="Unsubmitted claims"
|
|
16
|
+
// description="You have 3 unsubmitted mileage claims. Create a report to submit."
|
|
17
|
+
// actionLabel="Create report"
|
|
18
|
+
// onAction={() => console.log('clicked')}
|
|
19
|
+
// />
|
|
20
|
+
// )
|
|
21
|
+
// }
|
|
22
|
+
|
|
23
|
+
import React from 'react'
|
|
24
|
+
import { cn } from '../../lib/utils'
|
|
25
|
+
|
|
26
|
+
// Lightweight Alert primitives compatible with package styling
|
|
27
|
+
function Alert({ className, children }) {
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
className={cn(
|
|
31
|
+
'w-full rounded-lg border p-4 text-sm',
|
|
32
|
+
'bg-amber-600/10 text-amber-700 border-amber-200',
|
|
33
|
+
'dark:bg-amber-400/10 dark:text-amber-300 dark:border-amber-300/20',
|
|
34
|
+
className
|
|
35
|
+
)}
|
|
36
|
+
role="alert"
|
|
37
|
+
>
|
|
38
|
+
{children}
|
|
39
|
+
</div>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Prop types omitted to avoid runtime dependency
|
|
44
|
+
|
|
45
|
+
function AlertTitle({ className, children }) {
|
|
46
|
+
return <div className={cn('font-medium', className)}>{children}</div>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Prop types omitted to avoid runtime dependency
|
|
50
|
+
|
|
51
|
+
function AlertDescription({ className, children }) {
|
|
52
|
+
return (
|
|
53
|
+
<div className={cn('mt-1 text-amber-700/80 dark:text-amber-300/80', className)}>
|
|
54
|
+
{children}
|
|
55
|
+
</div>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Prop types omitted to avoid runtime dependency
|
|
60
|
+
|
|
61
|
+
function Button({ className, children, ...props }) {
|
|
62
|
+
return (
|
|
63
|
+
<button
|
|
64
|
+
type="button"
|
|
65
|
+
className={cn(
|
|
66
|
+
'inline-flex items-center rounded-md px-3 py-1.5 text-xs font-medium',
|
|
67
|
+
'bg-amber-600 text-white hover:bg-amber-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-500',
|
|
68
|
+
'dark:bg-amber-500 dark:text-black dark:hover:bg-amber-400',
|
|
69
|
+
className
|
|
70
|
+
)}
|
|
71
|
+
{...props}
|
|
72
|
+
>
|
|
73
|
+
{children}
|
|
74
|
+
</button>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Prop types omitted to avoid runtime dependency
|
|
79
|
+
|
|
80
|
+
export function SoftWarningAlert({
|
|
81
|
+
title,
|
|
82
|
+
description,
|
|
83
|
+
icon: Icon,
|
|
84
|
+
className,
|
|
85
|
+
actionLabel,
|
|
86
|
+
onAction,
|
|
87
|
+
}) {
|
|
88
|
+
return (
|
|
89
|
+
<Alert className={className}>
|
|
90
|
+
<div className="flex items-start justify-between gap-3">
|
|
91
|
+
<div className="flex items-start gap-3">
|
|
92
|
+
{Icon ? <Icon className="h-5 w-5 mt-0.5 shrink-0" /> : null}
|
|
93
|
+
<div>
|
|
94
|
+
<AlertTitle>{title}</AlertTitle>
|
|
95
|
+
{description ? (
|
|
96
|
+
<AlertDescription>{description}</AlertDescription>
|
|
97
|
+
) : null}
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
{typeof onAction === 'function' && actionLabel ? (
|
|
101
|
+
<Button onClick={onAction}>{actionLabel}</Button>
|
|
102
|
+
) : null}
|
|
103
|
+
</div>
|
|
104
|
+
</Alert>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Prop types omitted to avoid runtime dependency
|
|
109
|
+
|
|
110
|
+
export default SoftWarningAlert
|
|
111
|
+
|
|
112
|
+
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//
|
|
2
|
+
// index.d.ts
|
|
3
|
+
// -------------
|
|
4
|
+
// TypeScript declarations for @snapdragonsnursery/react-components
|
|
5
|
+
// This provides minimal typings for components used in consuming TypeScript apps.
|
|
6
|
+
|
|
7
|
+
import * as React from 'react'
|
|
8
|
+
|
|
9
|
+
export interface SoftWarningAlertProps {
|
|
10
|
+
title: React.ReactNode
|
|
11
|
+
description?: React.ReactNode
|
|
12
|
+
icon?: React.ComponentType<any>
|
|
13
|
+
className?: string
|
|
14
|
+
actionLabel?: string
|
|
15
|
+
onAction?: () => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const SoftWarningAlert: React.FC<SoftWarningAlertProps>
|
|
19
|
+
|
|
20
|
+
// Existing components (typed as any for now)
|
|
21
|
+
export const AuthButtons: React.ComponentType<any>
|
|
22
|
+
export const ThemeToggle: React.ComponentType<any>
|
|
23
|
+
export const ChildSearchModal: React.ComponentType<any>
|
|
24
|
+
export const ChildSearchPage: React.ComponentType<any>
|
|
25
|
+
export const ChildSearchPageDemo: React.ComponentType<any>
|
|
26
|
+
export const ThemeToggleTest: React.ComponentType<any>
|
|
27
|
+
export const LandingPage: React.ComponentType<any>
|
|
28
|
+
export const ChildSearchFilters: React.ComponentType<any>
|
|
29
|
+
export const DateRangePickerDemo: React.ComponentType<any>
|
|
30
|
+
export const CalendarDemo: React.ComponentType<any>
|
|
31
|
+
export const DateRangePickerTest: React.ComponentType<any>
|
|
32
|
+
export const ApplyButtonDemo: React.ComponentType<any>
|
|
33
|
+
export const EmployeeSearchPage: React.ComponentType<any>
|
|
34
|
+
export const EmployeeSearchModal: React.ComponentType<any>
|
|
35
|
+
export const EmployeeSearchDemo: React.ComponentType<any>
|
|
36
|
+
export const EmployeeSearchFilters: React.ComponentType<any>
|
|
37
|
+
|
|
38
|
+
export const DateRangePicker: React.ComponentType<any>
|
|
39
|
+
export const DatePicker: React.ComponentType<any>
|
|
40
|
+
export const Calendar: React.ComponentType<any>
|
|
41
|
+
export const SimpleCalendar: React.ComponentType<any>
|
|
42
|
+
export const Popover: React.ComponentType<any>
|
|
43
|
+
export const PopoverContent: React.ComponentType<any>
|
|
44
|
+
export const PopoverTrigger: React.ComponentType<any>
|
|
45
|
+
|
|
46
|
+
export function configureTelemetry(...args: any[]): any
|
|
47
|
+
|
|
48
|
+
|
package/src/index.js
CHANGED
|
@@ -24,3 +24,4 @@ export { DateRangePicker, DatePicker } from "./components/ui/date-range-picker";
|
|
|
24
24
|
export { Calendar } from "./components/ui/calendar";
|
|
25
25
|
export { SimpleCalendar } from "./components/ui/simple-calendar";
|
|
26
26
|
export { Popover, PopoverContent, PopoverTrigger } from "./components/ui/popover";
|
|
27
|
+
export { default as SoftWarningAlert } from "./components/ui/soft-warning-alert";
|