@snapdragonsnursery/react-components 1.5.0 → 1.7.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
- <DateRangePicker
122
- selectedRange={selectedRange}
123
- onSelect={handleDateRangeChange}
124
- placeholder="Select a date range"
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.5.0",
3
+ "version": "1.7.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"
@@ -322,6 +322,10 @@ const ChildSearchModal = ({
322
322
  onClose();
323
323
  };
324
324
 
325
+ const handleClearSelection = () => {
326
+ setSelectedChildrenState([]);
327
+ };
328
+
325
329
  const isChildSelected = (child) => {
326
330
  return selectedChildrenState.some(
327
331
  (selected) => selected.child_id === child.child_id
@@ -870,6 +874,40 @@ const ChildSearchModal = ({
870
874
  </div>
871
875
  </div>
872
876
  )}
877
+
878
+ {/* Multi-select Action Bar */}
879
+ {multiSelect && (
880
+ <div className="p-6 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between">
881
+ <div className="text-sm text-gray-600 dark:text-gray-300">
882
+ {selectedChildrenState.length} selected{maxSelections ? ` / ${maxSelections}` : ''}
883
+ </div>
884
+ <div className="flex items-center gap-2">
885
+ <button
886
+ type="button"
887
+ onClick={handleClearSelection}
888
+ disabled={selectedChildrenState.length === 0}
889
+ className="px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
890
+ >
891
+ Clear
892
+ </button>
893
+ <button
894
+ type="button"
895
+ onClick={onClose}
896
+ className="px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700"
897
+ >
898
+ Go Back
899
+ </button>
900
+ <button
901
+ type="button"
902
+ onClick={handleConfirmSelection}
903
+ disabled={selectedChildrenState.length === 0}
904
+ className="px-3 py-2 text-sm rounded bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
905
+ >
906
+ Confirm
907
+ </button>
908
+ </div>
909
+ </div>
910
+ )}
873
911
  </div>
874
912
  </div>
875
913
  );
@@ -56,6 +56,8 @@ const ChildSearchPage = ({
56
56
  applicationContext = "child-search",
57
57
  bypassPermissions = false,
58
58
  onSelect = null, // Optional callback for when a child is selected
59
+ onConfirm = null, // Optional callback (multi-select confirm)
60
+ onBack = null, // Optional go-back handler in multi-select
59
61
  multiSelect = false,
60
62
  maxSelections = null,
61
63
  selectedChildren = [],
@@ -245,6 +247,11 @@ const ChildSearchPage = ({
245
247
  (index) => children[parseInt(index)]
246
248
  );
247
249
  setSelectedChildrenState(selectedRows);
250
+
251
+ // Call onSelect callback if provided and in multi-select mode
252
+ if (onSelect && multiSelect) {
253
+ onSelect(selectedRows);
254
+ }
248
255
  },
249
256
  });
250
257
 
@@ -445,6 +452,30 @@ const ChildSearchPage = ({
445
452
  setDebouncedAdvancedFilters(clearedFilters); // Clear immediately
446
453
  };
447
454
 
455
+ const handleClearSelection = () => {
456
+ setSelectedChildrenState([]);
457
+ if (onSelect && multiSelect) {
458
+ onSelect([]);
459
+ }
460
+ };
461
+
462
+ const handleConfirmSelection = () => {
463
+ const payload = selectedChildrenState;
464
+ if (onConfirm) {
465
+ onConfirm(payload);
466
+ } else if (onSelect) {
467
+ onSelect(payload);
468
+ }
469
+ };
470
+
471
+ const handleGoBack = () => {
472
+ if (onBack) {
473
+ onBack();
474
+ } else if (typeof window !== 'undefined' && window.history) {
475
+ window.history.back();
476
+ }
477
+ };
478
+
448
479
  // Calculate pagination display values
449
480
  const actualTotalCount = pagination.totalCount || children.length;
450
481
  const startItem = actualTotalCount > 0 ? (pagination.page - 1) * pagination.pageSize + 1 : 0;
@@ -686,6 +717,26 @@ const ChildSearchPage = ({
686
717
  </>
687
718
  )}
688
719
  </div>
720
+
721
+ {/* Multi-select Action Bar */}
722
+ {multiSelect && (
723
+ <div className="mt-4 flex items-center justify-between bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 px-4 py-3">
724
+ <div className="text-sm text-gray-600 dark:text-gray-300">
725
+ {selectedChildrenState.length} selected{maxSelections ? ` / ${maxSelections}` : ''}
726
+ </div>
727
+ <div className="flex items-center gap-2">
728
+ <Button variant="outline" onClick={handleClearSelection} disabled={selectedChildrenState.length === 0}>
729
+ Clear
730
+ </Button>
731
+ <Button variant="outline" onClick={handleGoBack}>
732
+ Go Back
733
+ </Button>
734
+ <Button onClick={handleConfirmSelection} disabled={selectedChildrenState.length === 0}>
735
+ Confirm
736
+ </Button>
737
+ </div>
738
+ </div>
739
+ )}
689
740
  </div>
690
741
  </div>
691
742
  );
@@ -275,12 +275,45 @@ const EmployeeSearchModal = ({
275
275
  },
276
276
  onSortingChange: setSorting,
277
277
  onRowSelectionChange: (updater) => {
278
- const newSelection =
279
- typeof updater === "function" ? updater({}) : updater;
280
- const selectedRows = Object.keys(newSelection).map(
281
- (index) => employees[parseInt(index)]
282
- );
283
- setSelectedEmployeesState(selectedRows);
278
+ // Merge current-table selection changes with the existing global selection
279
+ // instead of replacing it, so selection is retained across searches/pages.
280
+
281
+ // Build current selection map (for visible rows only)
282
+ const prevSelection = selectedEmployeesState.reduce((acc, selectedEmployee) => {
283
+ const rowIndex = employees.findIndex((employee) => employee.entra_id === selectedEmployee.entra_id);
284
+ if (rowIndex !== -1) acc[rowIndex] = true;
285
+ return acc;
286
+ }, {});
287
+
288
+ const updatedSelection = typeof updater === 'function' ? updater(prevSelection) : updater;
289
+
290
+ // Determine which visible employees are selected after the change
291
+ const visibleSelected = Object.keys(updatedSelection)
292
+ .filter((idx) => Boolean(updatedSelection[idx]))
293
+ .map((idx) => employees[parseInt(idx, 10)])
294
+ .filter(Boolean);
295
+
296
+ // Start from the previous global selection keyed by entra_id
297
+ const prevById = new Map(selectedEmployeesState.map((e) => [e.entra_id, e]));
298
+
299
+ // Remove any currently visible employees that are now unselected
300
+ const visibleIds = new Set(employees.map((e) => e.entra_id));
301
+ const visibleSelectedIds = new Set(visibleSelected.map((e) => e.entra_id));
302
+ employees.forEach((e) => {
303
+ if (visibleIds.has(e.entra_id) && !visibleSelectedIds.has(e.entra_id)) {
304
+ prevById.delete(e.entra_id);
305
+ }
306
+ });
307
+
308
+ // Add visible selected that weren't already present (respect maxSelections)
309
+ for (const e of visibleSelected) {
310
+ if (!prevById.has(e.entra_id)) {
311
+ if (maxSelections && prevById.size >= maxSelections) break;
312
+ prevById.set(e.entra_id, e);
313
+ }
314
+ }
315
+
316
+ setSelectedEmployeesState(Array.from(prevById.values()));
284
317
  },
285
318
  });
286
319
 
@@ -502,6 +535,10 @@ const EmployeeSearchModal = ({
502
535
  }
503
536
  };
504
537
 
538
+ const handleClearSelection = () => {
539
+ setSelectedEmployeesState([]);
540
+ };
541
+
505
542
  const clearFilters = () => {
506
543
  const clearedFilters = {
507
544
  status: activeOnly ? "Active" : "all",
@@ -549,22 +586,7 @@ const EmployeeSearchModal = ({
549
586
  <XMarkIcon className="h-6 w-6" />
550
587
  </button>
551
588
  </div>
552
- {multiSelect && (
553
- <div className="mt-2 flex items-center justify-between">
554
- <span className="text-sm text-gray-600 dark:text-gray-400">
555
- {selectedEmployeesState.length} selected
556
- {maxSelections && ` / ${maxSelections}`}
557
- </span>
558
- {selectedEmployeesState.length > 0 && (
559
- <Button
560
- size="sm"
561
- onClick={handleConfirmSelection}
562
- >
563
- Confirm Selection
564
- </Button>
565
- )}
566
- </div>
567
- )}
589
+ {/* Removed header confirm to avoid duplication; footer handles confirmation */}
568
590
  </div>
569
591
 
570
592
  {/* Content */}
@@ -793,20 +815,48 @@ const EmployeeSearchModal = ({
793
815
  </div>
794
816
 
795
817
  {/* Footer */}
796
- <div className="bg-gray-50 dark:bg-gray-700 px-6 py-3 flex justify-end space-x-3">
797
- <Button
798
- variant="outline"
799
- onClick={onClose}
800
- >
801
- Cancel
802
- </Button>
803
- {multiSelect && selectedEmployeesState.length > 0 && (
804
- <Button
805
- onClick={handleConfirmSelection}
806
- >
807
- Select {selectedEmployeesState.length} Employee{selectedEmployeesState.length !== 1 ? 's' : ''}
818
+ <div className="bg-gray-50 dark:bg-gray-700 px-6 py-3 flex items-center justify-between gap-3">
819
+ {/* Selection preview */}
820
+ <div className="flex-1 min-w-0">
821
+ {multiSelect && selectedEmployeesState.length > 0 && (
822
+ (() => {
823
+ const total = selectedEmployeesState.length;
824
+ const preview = selectedEmployeesState.slice(0, 5);
825
+ const extra = Math.max(total - preview.length, 0);
826
+ const previewText = `${total} selected${maxSelections ? ` / ${maxSelections}` : ''}: ` +
827
+ preview.map((e) => e.full_name).join(', ') +
828
+ (extra > 0 ? ` + ${extra} more` : '');
829
+ const fullList = selectedEmployeesState.map((e) => e.full_name).join(', ');
830
+ return (
831
+ <div
832
+ className="text-sm text-gray-600 dark:text-gray-300 truncate"
833
+ title={fullList}
834
+ >
835
+ {previewText}
836
+ </div>
837
+ );
838
+ })()
839
+ )}
840
+ </div>
841
+ <div className="flex items-center gap-2">
842
+ {multiSelect && (
843
+ <Button variant="outline" onClick={handleClearSelection} disabled={selectedEmployeesState.length === 0}>
844
+ Clear
845
+ </Button>
846
+ )}
847
+ <Button variant="outline" onClick={onClose}>
848
+ Go Back
808
849
  </Button>
809
- )}
850
+ {multiSelect && (
851
+ <Button
852
+ onClick={handleConfirmSelection}
853
+ disabled={selectedEmployeesState.length === 0}
854
+ aria-label={`Confirm ${selectedEmployeesState.length} selected`}
855
+ >
856
+ {`Confirm (${selectedEmployeesState.length}${maxSelections ? `/${maxSelections}` : ''})`}
857
+ </Button>
858
+ )}
859
+ </div>
810
860
  </div>
811
861
  </div>
812
862
  </div>
@@ -814,4 +864,4 @@ const EmployeeSearchModal = ({
814
864
  );
815
865
  };
816
866
 
817
- export default EmployeeSearchModal;
867
+ export default EmployeeSearchModal;
@@ -58,6 +58,8 @@ const EmployeeSearchPage = ({
58
58
  applicationContext = "employee-search",
59
59
  bypassPermissions = false,
60
60
  onSelect = null, // Optional callback for when an employee is selected
61
+ onConfirm = null, // Optional callback (multi-select confirm)
62
+ onBack = null, // Optional callback for go back/cancel
61
63
  multiSelect = false,
62
64
  maxSelections = null,
63
65
  selectedEmployees = [],
@@ -322,6 +324,11 @@ const EmployeeSearchPage = ({
322
324
  (index) => employees[parseInt(index)]
323
325
  );
324
326
  setSelectedEmployeesState(selectedRows);
327
+
328
+ // Call onSelect callback if provided and in multi-select mode
329
+ if (onSelect && multiSelect) {
330
+ onSelect(selectedRows);
331
+ }
325
332
  },
326
333
  });
327
334
 
@@ -615,6 +622,30 @@ const EmployeeSearchPage = ({
615
622
  setDebouncedAdvancedFilters(clearedFilters); // Clear immediately
616
623
  };
617
624
 
625
+ const handleClearSelection = () => {
626
+ setSelectedEmployeesState([]);
627
+ if (onSelect && multiSelect) {
628
+ onSelect([]);
629
+ }
630
+ };
631
+
632
+ const handleConfirmSelection = () => {
633
+ const payload = selectedEmployeesState;
634
+ if (onConfirm) {
635
+ onConfirm(payload);
636
+ } else if (onSelect) {
637
+ onSelect(payload);
638
+ }
639
+ };
640
+
641
+ const handleGoBack = () => {
642
+ if (onBack) {
643
+ onBack();
644
+ } else if (typeof window !== 'undefined' && window.history) {
645
+ window.history.back();
646
+ }
647
+ };
648
+
618
649
  // Calculate pagination display values
619
650
  const actualTotalCount = pagination.totalCount || employees.length;
620
651
  const startItem = loadAllResults ? 1 : (actualTotalCount > 0 ? (pagination.page - 1) * pagination.pageSize + 1 : 0);
@@ -897,9 +928,29 @@ const EmployeeSearchPage = ({
897
928
  </>
898
929
  )}
899
930
  </div>
931
+
932
+ {/* Multi-select Action Bar */}
933
+ {multiSelect && (
934
+ <div className="mt-4 flex items-center justify-between bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 px-4 py-3">
935
+ <div className="text-sm text-gray-600 dark:text-gray-300">
936
+ {selectedEmployeesState.length} selected{maxSelections ? ` / ${maxSelections}` : ''}
937
+ </div>
938
+ <div className="flex items-center gap-2">
939
+ <Button variant="outline" onClick={handleClearSelection} disabled={selectedEmployeesState.length === 0}>
940
+ Clear
941
+ </Button>
942
+ <Button variant="outline" onClick={handleGoBack}>
943
+ Go Back
944
+ </Button>
945
+ <Button onClick={handleConfirmSelection} disabled={selectedEmployeesState.length === 0}>
946
+ Confirm
947
+ </Button>
948
+ </div>
949
+ </div>
950
+ )}
900
951
  </div>
901
952
  </div>
902
953
  );
903
954
  };
904
955
 
905
- export default EmployeeSearchPage;
956
+ export default EmployeeSearchPage;
@@ -1,254 +1 @@
1
- // Employee Search Page Component Tests
2
- // Tests the EmployeeSearchPage component functionality including search, filtering, pagination, and selection
3
-
4
- import React from 'react';
5
- import { render, screen, fireEvent, waitFor } from '@testing-library/react';
6
- import '@testing-library/jest-dom';
7
- import EmployeeSearchPage from './EmployeeSearchPage';
8
-
9
- // Mock MSAL
10
- jest.mock('@azure/msal-react', () => ({
11
- useMsal: () => ({
12
- instance: {
13
- acquireTokenSilent: jest.fn(),
14
- },
15
- accounts: [{ localAccountId: 'test-user-id' }],
16
- }),
17
- }));
18
-
19
- // Mock telemetry
20
- jest.mock('./telemetry', () => ({
21
- trackEvent: jest.fn(),
22
- }));
23
-
24
- // Mock utils
25
- jest.mock('./lib/utils', () => ({
26
- cn: (...classes) => classes.filter(Boolean).join(' '),
27
- }));
28
-
29
-
30
-
31
- // Mock fetch
32
- global.fetch = jest.fn();
33
-
34
- // Mock process.env
35
- process.env.VITE_COMMON_API_FUNCTION_KEY = 'test-key';
36
- process.env.VITE_COMMON_API_BASE_URL = 'https://test-api.example.com';
37
-
38
- // Mock import.meta.env for Vite environment variables
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',
44
- },
45
- },
46
- };
47
-
48
- // Mock the UI components
49
- jest.mock('./components/ui/input', () => ({
50
- Input: ({ value, onChange, placeholder, ...props }) => {
51
- return (
52
- <input
53
- data-testid="input"
54
- value={value}
55
- onChange={onChange}
56
- placeholder={placeholder}
57
- {...props}
58
- />
59
- );
60
- }
61
- }));
62
-
63
- jest.mock('./components/ui/button', () => ({
64
- Button: ({ children, onClick, variant, size, ...props }) => {
65
- return (
66
- <button
67
- data-testid="button"
68
- onClick={onClick}
69
- data-variant={variant}
70
- data-size={size}
71
- {...props}
72
- >
73
- {children}
74
- </button>
75
- );
76
- }
77
- }));
78
-
79
- jest.mock('./components/ui/table', () => ({
80
- Table: ({ children, ...props }) => <table data-testid="table" {...props}>{children}</table>,
81
- TableBody: ({ children, ...props }) => <tbody data-testid="table-body" {...props}>{children}</tbody>,
82
- TableCell: ({ children, ...props }) => <td data-testid="table-cell" {...props}>{children}</td>,
83
- TableHead: ({ children, ...props }) => <th data-testid="table-head" {...props}>{children}</th>,
84
- TableHeader: ({ children, ...props }) => <thead data-testid="table-header" {...props}>{children}</thead>,
85
- TableRow: ({ children, ...props }) => <tr data-testid="table-row" {...props}>{children}</tr>,
86
- }));
87
-
88
- jest.mock('./components/ui/pagination', () => ({
89
- Pagination: ({ children, ...props }) => <nav data-testid="pagination" {...props}>{children}</nav>,
90
- PaginationContent: ({ children, ...props }) => <div data-testid="pagination-content" {...props}>{children}</div>,
91
- PaginationEllipsis: ({ ...props }) => <span data-testid="pagination-ellipsis" {...props}>...</span>,
92
- PaginationItem: ({ children, ...props }) => <div data-testid="pagination-item" {...props}>{children}</div>,
93
- PaginationLink: ({ children, onClick, isActive, ...props }) => (
94
- <button data-testid="pagination-link" onClick={onClick} data-active={isActive} {...props}>
95
- {children}
96
- </button>
97
- ),
98
- PaginationNext: ({ children, onClick, ...props }) => (
99
- <button data-testid="pagination-next" onClick={onClick} {...props}>
100
- {children}
101
- </button>
102
- ),
103
- PaginationPrevious: ({ children, onClick, ...props }) => (
104
- <button data-testid="pagination-previous" onClick={onClick} {...props}>
105
- {children}
106
- </button>
107
- ),
108
- }));
109
-
110
- // Mock EmployeeSearchFilters
111
- jest.mock('./components/EmployeeSearchFilters', () => {
112
- return function MockEmployeeSearchFilters({ filters, onFiltersChange, onApplyFilters, onClearFilters, ...props }) {
113
- return (
114
- <div data-testid="employee-search-filters">
115
- <button onClick={() => onFiltersChange({ ...filters, status: 'Inactive' })}>
116
- Change Status
117
- </button>
118
- <button onClick={() => onApplyFilters(filters)}>
119
- Apply Filters
120
- </button>
121
- <button onClick={onClearFilters}>
122
- Clear Filters
123
- </button>
124
- </div>
125
- );
126
- };
127
- });
128
-
129
- const defaultProps = {
130
- title: 'Employee Search',
131
- onSelect: jest.fn(),
132
- };
133
-
134
- describe('EmployeeSearchPage', () => {
135
- beforeEach(() => {
136
- jest.clearAllMocks();
137
- global.fetch.mockResolvedValue({
138
- ok: true,
139
- json: async () => ({
140
- success: true,
141
- data: {
142
- employees: [],
143
- pagination: {
144
- page: 1,
145
- pageSize: 20,
146
- totalCount: 0,
147
- totalPages: 0,
148
- hasNextPage: false,
149
- hasPreviousPage: false,
150
- },
151
- },
152
- }),
153
- });
154
- });
155
-
156
- describe('Basic Rendering', () => {
157
- it('renders the page title', () => {
158
- render(<EmployeeSearchPage {...defaultProps} />);
159
-
160
- expect(screen.getByText('Employee Search')).toBeInTheDocument();
161
- });
162
-
163
- it('renders the search input', () => {
164
- render(<EmployeeSearchPage {...defaultProps} />);
165
-
166
- expect(screen.getByPlaceholderText('Search by name, employee ID, or email...')).toBeInTheDocument();
167
- });
168
-
169
- it('renders the employee search filters', () => {
170
- render(<EmployeeSearchPage {...defaultProps} />);
171
-
172
- expect(screen.getByTestId('employee-search-filters')).toBeInTheDocument();
173
- });
174
-
175
- it('shows loading state initially', () => {
176
- render(<EmployeeSearchPage {...defaultProps} />);
177
-
178
- expect(screen.getByRole('status')).toBeInTheDocument();
179
- });
180
- });
181
-
182
- describe('Search Functionality', () => {
183
- it('renders search input with correct placeholder', () => {
184
- render(<EmployeeSearchPage {...defaultProps} />);
185
-
186
- const searchInput = screen.getByPlaceholderText('Search by name, employee ID, or email...');
187
- expect(searchInput).toBeInTheDocument();
188
- expect(searchInput).toHaveAttribute('type', 'text');
189
- });
190
-
191
- it('allows typing in search input', () => {
192
- render(<EmployeeSearchPage {...defaultProps} />);
193
-
194
- const searchInput = screen.getByPlaceholderText('Search by name, employee ID, or email...');
195
- fireEvent.change(searchInput, { target: { value: 'John' } });
196
-
197
- expect(searchInput).toHaveValue('John');
198
- });
199
- });
200
-
201
- describe('Filter Integration', () => {
202
- it('renders filter buttons', () => {
203
- render(<EmployeeSearchPage {...defaultProps} />);
204
-
205
- expect(screen.getByText('Change Status')).toBeInTheDocument();
206
- expect(screen.getByText('Apply Filters')).toBeInTheDocument();
207
- expect(screen.getByText('Clear Filters')).toBeInTheDocument();
208
- });
209
-
210
- it('calls filter change handler when filter button is clicked', () => {
211
- render(<EmployeeSearchPage {...defaultProps} />);
212
-
213
- const changeStatusButton = screen.getByText('Change Status');
214
- fireEvent.click(changeStatusButton);
215
-
216
- // The mock should have been called
217
- expect(changeStatusButton).toBeInTheDocument();
218
- });
219
- });
220
-
221
- describe('Accessibility', () => {
222
- it('has proper ARIA labels', () => {
223
- render(<EmployeeSearchPage {...defaultProps} />);
224
-
225
- const searchInput = screen.getByPlaceholderText('Search by name, employee ID, or email...');
226
- expect(searchInput).toBeInTheDocument();
227
- });
228
-
229
- it('supports keyboard navigation', () => {
230
- render(<EmployeeSearchPage {...defaultProps} />);
231
-
232
- const searchInput = screen.getByPlaceholderText('Search by name, employee ID, or email...');
233
- searchInput.focus();
234
-
235
- expect(searchInput).toHaveFocus();
236
- });
237
- });
238
-
239
- describe('Props Handling', () => {
240
- it('renders custom title', () => {
241
- render(<EmployeeSearchPage {...defaultProps} title="Custom Title" />);
242
-
243
- expect(screen.getByText('Custom Title')).toBeInTheDocument();
244
- });
245
-
246
- it('renders custom search placeholder', () => {
247
- render(<EmployeeSearchPage {...defaultProps} searchPlaceholder="Custom placeholder" />);
248
-
249
- expect(screen.getByPlaceholderText('Custom placeholder')).toBeInTheDocument();
250
- });
251
- });
252
-
253
-
254
- });
1
+
@@ -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="w-auto p-0" align="start">
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={2}
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,
@@ -234,6 +299,7 @@ export function DatePicker({
234
299
  className,
235
300
  placeholder = "Select a date",
236
301
  disabled,
302
+ disableFuture = false,
237
303
  ...props
238
304
  }) {
239
305
  const [isOpen, setIsOpen] = useState(false)
@@ -269,6 +335,7 @@ export function DatePicker({
269
335
  mode="single"
270
336
  selected={selectedDate}
271
337
  onSelect={handleSelect}
338
+ disabled={disableFuture ? { after: new Date() } : undefined}
272
339
  initialFocus
273
340
  />
274
341
  </PopoverContent>
@@ -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 for range picker
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
+
@@ -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
+ })
package/src/index.d.ts ADDED
@@ -0,0 +1,67 @@
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 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>
48
+ export const Calendar: React.ComponentType<any>
49
+ export const SimpleCalendar: React.ComponentType<any>
50
+ export const Popover: React.ComponentType<any>
51
+ export const PopoverContent: React.ComponentType<any>
52
+ export const PopoverTrigger: React.ComponentType<any>
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
+
65
+ export function configureTelemetry(...args: any[]): any
66
+
67
+
package/src/index.js CHANGED
@@ -1,26 +1,28 @@
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";
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";