@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 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.4.0",
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 and import.meta.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
- Object.defineProperty(globalThis, 'import', {
40
- value: {
41
- meta: {
42
- env: {
43
- VITE_COMMON_API_FUNCTION_KEY: 'test-key',
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
- writable: true,
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;
@@ -0,0 +1,5 @@
1
+ // Mock for import.meta.env to handle Vite environment variables in Jest tests
2
+ export default {
3
+ VITE_COMMON_API_FUNCTION_KEY: 'test-key',
4
+ VITE_COMMON_API_BASE_URL: 'https://test-api.example.com',
5
+ };
@@ -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.getAllByTestId('input')[0];
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.getAllByTestId('input')[1];
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="first_name"]')
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="desc"]')
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="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,
@@ -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
+
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";