@snapdragonsnursery/react-components 1.4.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 +44 -6
- package/package.json +4 -1
- package/src/EmployeeSearchPage.test.jsx +7 -10
- package/src/__mocks__/EmployeeSearchPage.jsx +87 -0
- package/src/__mocks__/importMetaEnv.js +5 -0
- package/src/components/EmployeeSearchFilters.jsx +53 -0
- package/src/components/EmployeeSearchFilters.test.jsx +5 -4
- package/src/components/ui/date-range-picker.jsx +70 -5
- package/src/components/ui/date-range-picker.test.jsx +19 -1
- package/src/components/ui/soft-warning-alert.jsx +112 -0
- package/src/index.d.ts +48 -0
- package/src/index.js +1 -0
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,11 +59,14 @@
|
|
|
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"
|
|
65
66
|
],
|
|
66
67
|
"devDependencies": {
|
|
68
|
+
"@babel/plugin-syntax-import-meta": "^7.10.4",
|
|
69
|
+
"@babel/plugin-transform-runtime": "^7.28.0",
|
|
67
70
|
"@babel/preset-env": "^7.28.0",
|
|
68
71
|
"@babel/preset-react": "^7.27.1",
|
|
69
72
|
"@testing-library/jest-dom": "^6.6.4",
|
|
@@ -31,22 +31,19 @@ jest.mock('./lib/utils', () => ({
|
|
|
31
31
|
// Mock fetch
|
|
32
32
|
global.fetch = jest.fn();
|
|
33
33
|
|
|
34
|
-
// Mock process.env
|
|
34
|
+
// Mock process.env
|
|
35
35
|
process.env.VITE_COMMON_API_FUNCTION_KEY = 'test-key';
|
|
36
36
|
process.env.VITE_COMMON_API_BASE_URL = 'https://test-api.example.com';
|
|
37
37
|
|
|
38
38
|
// Mock import.meta.env for Vite environment variables
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
VITE_COMMON_API_BASE_URL: 'https://test-api.example.com',
|
|
45
|
-
},
|
|
39
|
+
global.import = {
|
|
40
|
+
meta: {
|
|
41
|
+
env: {
|
|
42
|
+
VITE_COMMON_API_FUNCTION_KEY: 'test-key',
|
|
43
|
+
VITE_COMMON_API_BASE_URL: 'https://test-api.example.com',
|
|
46
44
|
},
|
|
47
45
|
},
|
|
48
|
-
|
|
49
|
-
});
|
|
46
|
+
};
|
|
50
47
|
|
|
51
48
|
// Mock the UI components
|
|
52
49
|
jest.mock('./components/ui/input', () => ({
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Mock EmployeeSearchPage component for testing
|
|
2
|
+
// This avoids the import.meta.env issue by providing a comprehensive mock
|
|
3
|
+
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
const EmployeeSearchPage = (props) => {
|
|
7
|
+
const {
|
|
8
|
+
title = "Employee Search",
|
|
9
|
+
searchPlaceholder = "Search by name, employee ID, or email...",
|
|
10
|
+
onSelect,
|
|
11
|
+
multiSelect = false,
|
|
12
|
+
selectedEmployees = [],
|
|
13
|
+
sites = [],
|
|
14
|
+
roles = [],
|
|
15
|
+
managers = [],
|
|
16
|
+
activeOnly = true,
|
|
17
|
+
status = null,
|
|
18
|
+
startDateFrom = null,
|
|
19
|
+
startDateTo = null,
|
|
20
|
+
endDateFrom = null,
|
|
21
|
+
endDateTo = null,
|
|
22
|
+
sortBy = "full_name",
|
|
23
|
+
sortOrder = "asc",
|
|
24
|
+
applicationContext = "employee-search",
|
|
25
|
+
bypassPermissions = false,
|
|
26
|
+
maxSelections = null,
|
|
27
|
+
showRoleFilter = true,
|
|
28
|
+
showManagerFilter = true,
|
|
29
|
+
showTermTimeFilter = true,
|
|
30
|
+
showMaternityFilter = true,
|
|
31
|
+
showStartDateFilter = true,
|
|
32
|
+
showEndDateFilter = false,
|
|
33
|
+
showWorkingHoursFilter = true,
|
|
34
|
+
loadAllResults = false,
|
|
35
|
+
} = props;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div data-testid="employee-search-page">
|
|
39
|
+
<h1>{title}</h1>
|
|
40
|
+
|
|
41
|
+
{/* Search Input */}
|
|
42
|
+
<input
|
|
43
|
+
type="text"
|
|
44
|
+
placeholder={searchPlaceholder}
|
|
45
|
+
data-testid="search-input"
|
|
46
|
+
aria-label="Search employees"
|
|
47
|
+
/>
|
|
48
|
+
|
|
49
|
+
{/* Employee Search Filters */}
|
|
50
|
+
<div data-testid="employee-search-filters">
|
|
51
|
+
<button>Change Status</button>
|
|
52
|
+
<button>Apply Filters</button>
|
|
53
|
+
<button>Clear Filters</button>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
{/* Loading State */}
|
|
57
|
+
<div role="status" aria-live="polite">
|
|
58
|
+
Loading employees...
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
{/* Mock Table */}
|
|
62
|
+
<table>
|
|
63
|
+
<thead>
|
|
64
|
+
<tr>
|
|
65
|
+
<th>Name</th>
|
|
66
|
+
<th>Email</th>
|
|
67
|
+
<th>Status</th>
|
|
68
|
+
</tr>
|
|
69
|
+
</thead>
|
|
70
|
+
<tbody>
|
|
71
|
+
<tr>
|
|
72
|
+
<td>John Doe</td>
|
|
73
|
+
<td>john@example.com</td>
|
|
74
|
+
<td>Active</td>
|
|
75
|
+
</tr>
|
|
76
|
+
</tbody>
|
|
77
|
+
</table>
|
|
78
|
+
|
|
79
|
+
{/* Props Display for debugging */}
|
|
80
|
+
<div style={{ display: 'none' }}>
|
|
81
|
+
Props: {JSON.stringify(props)}
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export default EmployeeSearchPage;
|
|
@@ -33,6 +33,7 @@ const EmployeeSearchFilters = ({
|
|
|
33
33
|
showStartDateFilter = true,
|
|
34
34
|
showEndDateFilter = true,
|
|
35
35
|
showWorkingHoursFilter = true,
|
|
36
|
+
showDbsFilter = true,
|
|
36
37
|
}) => {
|
|
37
38
|
// Local state for filters that haven't been applied yet
|
|
38
39
|
const [localFilters, setLocalFilters] = React.useState(filters);
|
|
@@ -310,6 +311,22 @@ const EmployeeSearchFilters = ({
|
|
|
310
311
|
</div>
|
|
311
312
|
)}
|
|
312
313
|
|
|
314
|
+
{/* DBS Number Filter */}
|
|
315
|
+
{showDbsFilter && (
|
|
316
|
+
<div>
|
|
317
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
318
|
+
DBS Number
|
|
319
|
+
</label>
|
|
320
|
+
<Input
|
|
321
|
+
type="text"
|
|
322
|
+
value={localFilters.dbsNumber}
|
|
323
|
+
onChange={(e) => handleFilterChange("dbsNumber", e.target.value)}
|
|
324
|
+
placeholder="Enter DBS number"
|
|
325
|
+
className="bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white"
|
|
326
|
+
/>
|
|
327
|
+
</div>
|
|
328
|
+
)}
|
|
329
|
+
|
|
313
330
|
{/* Working Hours Filter */}
|
|
314
331
|
{showWorkingHoursFilter && (
|
|
315
332
|
<div>
|
|
@@ -330,6 +347,42 @@ const EmployeeSearchFilters = ({
|
|
|
330
347
|
|
|
331
348
|
</div>
|
|
332
349
|
|
|
350
|
+
{/* Sorting Options */}
|
|
351
|
+
<div className="mt-6 pt-4 border-t border-gray-200 dark:border-gray-700">
|
|
352
|
+
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Sorting Options</h3>
|
|
353
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
354
|
+
<div>
|
|
355
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
356
|
+
Sort By
|
|
357
|
+
</label>
|
|
358
|
+
<Select
|
|
359
|
+
value={localFilters.sortBy}
|
|
360
|
+
onChange={(e) => handleFilterChange("sortBy", e.target.value)}
|
|
361
|
+
className="bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white"
|
|
362
|
+
>
|
|
363
|
+
<SelectOption value="surname">Surname</SelectOption>
|
|
364
|
+
<SelectOption value="first_name">First Name</SelectOption>
|
|
365
|
+
<SelectOption value="full_name">Full Name</SelectOption>
|
|
366
|
+
<SelectOption value="date_of_birth">Date of Birth</SelectOption>
|
|
367
|
+
<SelectOption value="site_name">Site Name</SelectOption>
|
|
368
|
+
</Select>
|
|
369
|
+
</div>
|
|
370
|
+
<div>
|
|
371
|
+
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
372
|
+
Sort Order
|
|
373
|
+
</label>
|
|
374
|
+
<Select
|
|
375
|
+
value={localFilters.sortOrder}
|
|
376
|
+
onChange={(e) => handleFilterChange("sortOrder", e.target.value)}
|
|
377
|
+
className="bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white"
|
|
378
|
+
>
|
|
379
|
+
<SelectOption value="asc">Ascending</SelectOption>
|
|
380
|
+
<SelectOption value="desc">Descending</SelectOption>
|
|
381
|
+
</Select>
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
384
|
+
</div>
|
|
385
|
+
|
|
333
386
|
{/* Apply/Cancel Buttons */}
|
|
334
387
|
{hasUnsavedChanges && (
|
|
335
388
|
<div className="mt-6 pt-4 border-t border-gray-200 dark:border-gray-700">
|
|
@@ -233,7 +233,7 @@ describe('EmployeeSearchFilters', () => {
|
|
|
233
233
|
it('updates DBS number filter when changed', () => {
|
|
234
234
|
render(<EmployeeSearchFilters {...defaultProps} isAdvancedFiltersOpen={true} />);
|
|
235
235
|
|
|
236
|
-
const dbsInput = screen.
|
|
236
|
+
const dbsInput = screen.getByPlaceholderText('Enter DBS number');
|
|
237
237
|
fireEvent.change(dbsInput, { target: { value: 'DBS123456' } });
|
|
238
238
|
|
|
239
239
|
expect(defaultProps.onFiltersChange).toHaveBeenCalledWith({
|
|
@@ -245,7 +245,7 @@ describe('EmployeeSearchFilters', () => {
|
|
|
245
245
|
it('updates working hours filter when changed', () => {
|
|
246
246
|
render(<EmployeeSearchFilters {...defaultProps} isAdvancedFiltersOpen={true} />);
|
|
247
247
|
|
|
248
|
-
const hoursInput = screen.
|
|
248
|
+
const hoursInput = screen.getByPlaceholderText('0');
|
|
249
249
|
fireEvent.change(hoursInput, { target: { value: '30' } });
|
|
250
250
|
|
|
251
251
|
expect(defaultProps.onFiltersChange).toHaveBeenCalledWith({
|
|
@@ -459,7 +459,7 @@ describe('EmployeeSearchFilters', () => {
|
|
|
459
459
|
render(<EmployeeSearchFilters {...defaultProps} isAdvancedFiltersOpen={true} />);
|
|
460
460
|
|
|
461
461
|
const sortBySelect = screen.getAllByTestId('select').find(select =>
|
|
462
|
-
select.querySelector('option[value="
|
|
462
|
+
select.querySelector('option[value="surname"]')
|
|
463
463
|
);
|
|
464
464
|
fireEvent.change(sortBySelect, { target: { value: 'first_name' } });
|
|
465
465
|
|
|
@@ -471,7 +471,7 @@ describe('EmployeeSearchFilters', () => {
|
|
|
471
471
|
render(<EmployeeSearchFilters {...defaultProps} isAdvancedFiltersOpen={true} />);
|
|
472
472
|
|
|
473
473
|
const sortOrderSelect = screen.getAllByTestId('select').find(select =>
|
|
474
|
-
select.querySelector('option[value="
|
|
474
|
+
select.querySelector('option[value="asc"]')
|
|
475
475
|
);
|
|
476
476
|
fireEvent.change(sortOrderSelect, { target: { value: 'desc' } });
|
|
477
477
|
|
|
@@ -541,6 +541,7 @@ describe('EmployeeSearchFilters', () => {
|
|
|
541
541
|
const statusLabels = screen.getAllByText('Status');
|
|
542
542
|
expect(statusLabels.length).toBeGreaterThan(0);
|
|
543
543
|
expect(screen.getByText('Sort By')).toBeInTheDocument();
|
|
544
|
+
expect(screen.getByText('Sort Order')).toBeInTheDocument();
|
|
544
545
|
});
|
|
545
546
|
});
|
|
546
547
|
});
|
|
@@ -3,20 +3,29 @@
|
|
|
3
3
|
// Features cycling behavior: start date -> end date -> start date -> end date...
|
|
4
4
|
// Usage: <DateRangePicker selectedRange={range} onSelect={setRange} />
|
|
5
5
|
|
|
6
|
-
import { useState, useEffect, useRef } from 'react'
|
|
7
|
-
import { format } from 'date-fns'
|
|
6
|
+
import React, { useState, useEffect, useRef } from 'react'
|
|
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";
|