react-bootstrap-plugins 1.0.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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +426 -0
  3. package/dist/DatePicker.cjs +2 -0
  4. package/dist/DatePicker.cjs.map +1 -0
  5. package/dist/DatePicker.d.ts +45 -0
  6. package/dist/DatePicker.js +2 -0
  7. package/dist/DatePicker.js.map +1 -0
  8. package/dist/Label.cjs +2 -0
  9. package/dist/Label.cjs.map +1 -0
  10. package/dist/Label.d.ts +16 -0
  11. package/dist/Label.js +2 -0
  12. package/dist/Label.js.map +1 -0
  13. package/dist/SearchSelect.cjs +2 -0
  14. package/dist/SearchSelect.cjs.map +1 -0
  15. package/dist/SearchSelect.d.ts +28 -0
  16. package/dist/SearchSelect.js +2 -0
  17. package/dist/SearchSelect.js.map +1 -0
  18. package/dist/chunk-46AJ2Q5H.js +2 -0
  19. package/dist/chunk-46AJ2Q5H.js.map +1 -0
  20. package/dist/chunk-4ISYCPRB.cjs +2 -0
  21. package/dist/chunk-4ISYCPRB.cjs.map +1 -0
  22. package/dist/chunk-CZV7QBHA.cjs +2 -0
  23. package/dist/chunk-CZV7QBHA.cjs.map +1 -0
  24. package/dist/chunk-DIUGH74M.js +2 -0
  25. package/dist/chunk-DIUGH74M.js.map +1 -0
  26. package/dist/chunk-F3KMQZXU.js +2 -0
  27. package/dist/chunk-F3KMQZXU.js.map +1 -0
  28. package/dist/chunk-HBS6VOGV.cjs +2 -0
  29. package/dist/chunk-HBS6VOGV.cjs.map +1 -0
  30. package/dist/chunk-NOGRIWSU.js +2 -0
  31. package/dist/chunk-NOGRIWSU.js.map +1 -0
  32. package/dist/chunk-QQNPKA7A.cjs +2 -0
  33. package/dist/chunk-QQNPKA7A.cjs.map +1 -0
  34. package/dist/css/css/datepicker-bootstrap.css +299 -0
  35. package/dist/index.cjs +2 -0
  36. package/dist/index.cjs.map +1 -0
  37. package/dist/index.d.ts +10 -0
  38. package/dist/index.js +2 -0
  39. package/dist/index.js.map +1 -0
  40. package/package.json +125 -0
  41. package/src/components/DatePicker.d.ts +45 -0
  42. package/src/components/DatePicker.jsx +690 -0
  43. package/src/components/Label.d.ts +16 -0
  44. package/src/components/Label.jsx +21 -0
  45. package/src/components/SearchSelect.d.ts +28 -0
  46. package/src/components/SearchSelect.jsx +152 -0
  47. package/src/css/datepicker-bootstrap.css +299 -0
  48. package/src/index.js +15 -0
  49. package/src/lib/cn.js +35 -0
@@ -0,0 +1,16 @@
1
+ import * as React from 'react'
2
+
3
+ export interface LabelProps {
4
+ /** Additional CSS classes */
5
+ className?: string
6
+ /** htmlFor attribute — associates label with input ID (default 'default') */
7
+ hf?: string
8
+ /** Whether to show the required asterisk (default true) */
9
+ required?: boolean
10
+ /** Label text content (default 'Title') */
11
+ label?: string
12
+ }
13
+
14
+ export const Label: React.ForwardRefExoticComponent<
15
+ LabelProps & React.RefAttributes<HTMLLabelElement>
16
+ >
@@ -0,0 +1,21 @@
1
+ import * as React from 'react'
2
+
3
+ /**
4
+ * A simple Bootstrap-styled form label with an optional required indicator.
5
+ *
6
+ * @param {string} [className] - Additional CSS classes
7
+ * @param {string} [hf='default'] - htmlFor attribute (associates label with input ID)
8
+ * @param {boolean} [required=true] - Whether to show the required asterisk
9
+ * @param {string} [label='Title'] - Label text content
10
+ */
11
+ const Label = React.forwardRef(({ className, hf = 'default', required = true, label = 'Title' }, ref) => {
12
+ return (
13
+ <label htmlFor={hf} className={className} ref={ref}>
14
+ {label}{required ? <b className="text-danger">*</b> : ''}
15
+ </label>
16
+ )
17
+ })
18
+
19
+ Label.displayName = 'Label'
20
+
21
+ export { Label }
@@ -0,0 +1,28 @@
1
+ import * as React from 'react'
2
+
3
+ export interface SearchSelectProps<T = any> {
4
+ /** Additional CSS classes on the wrapper */
5
+ className?: string
6
+ /** Selectable options — primitives (string/number/boolean) or objects */
7
+ options?: T[] | Array<string | number | boolean>
8
+ /** Key for display text when options are objects (default 'label') */
9
+ labelKey?: string
10
+ /** Key for the value when options are objects (default 'value') */
11
+ valueKey?: string
12
+ /** Placeholder text (default 'Select...') */
13
+ placeholder?: string
14
+ /** Currently selected value */
15
+ value?: any
16
+ /** Called with the selected value when user picks */
17
+ onChange?: (value: any) => void
18
+ /** Input element ID */
19
+ id?: string
20
+ /** Disable the input */
21
+ disabled?: boolean
22
+ }
23
+
24
+ declare const SearchSelect: React.ForwardRefExoticComponent<
25
+ SearchSelectProps & React.RefAttributes<HTMLDivElement>
26
+ >
27
+
28
+ export default SearchSelect
@@ -0,0 +1,152 @@
1
+ import React, { useState, useRef, useEffect } from 'react'
2
+ import { cn } from '../lib/cn.js'
3
+
4
+ /**
5
+ * A filterable, searchable select dropdown styled with Bootstrap 5.
6
+ * Supports both primitive arrays and object arrays with configurable label/value keys.
7
+ *
8
+ * @param {string} [className] - Additional CSS classes on the wrapper
9
+ * @param {Array<string|number|boolean|object>} [options=[]] - Selectable options (primitives or objects)
10
+ * @param {string} [labelKey='label'] - Key for display text when options are objects
11
+ * @param {string} [valueKey='value'] - Key for the value when options are objects
12
+ * @param {string} [placeholder='Select...'] - Placeholder text
13
+ * @param {any} [value=null] - Currently selected value
14
+ * @param {(value: any) => void} [onChange] - Called with the selected value when user picks
15
+ * @param {string} [id='filterable-select'] - Input element ID
16
+ * @param {boolean} [disabled=false] - Disable the input
17
+ */
18
+ const SearchSelect = React.forwardRef(({
19
+ className,
20
+ options = [],
21
+ labelKey = 'label',
22
+ valueKey = 'value',
23
+ placeholder = 'Select...',
24
+ value = null,
25
+ onChange = null,
26
+ id = 'filterable-select',
27
+ disabled = false,
28
+ ...props
29
+ }, ref) => {
30
+ const [isOpen, setIsOpen] = useState(false)
31
+ const [searchTerm, setSearchTerm] = useState('')
32
+ const wrapperRef = useRef(null)
33
+
34
+ // AUTO-DETECT: primitives vs objects
35
+ const isPrimitiveArray = options.length > 0 &&
36
+ (typeof options[0] === 'string' || typeof options[0] === 'number' || typeof options[0] === 'boolean')
37
+
38
+ const getLabel = (option) => {
39
+ if (isPrimitiveArray) return String(option)
40
+ return option?.[labelKey] || ''
41
+ }
42
+
43
+ const getValue = (option) => {
44
+ if (isPrimitiveArray) return option
45
+ return option?.[valueKey] ?? null
46
+ }
47
+
48
+ const selectedOption = options.find(opt => getValue(opt) === value)
49
+
50
+ // Sync input when value changes externally
51
+ useEffect(() => {
52
+ if (selectedOption !== undefined) {
53
+ setSearchTerm(getLabel(selectedOption))
54
+ } else {
55
+ setSearchTerm('')
56
+ }
57
+ }, [value, options, labelKey, valueKey])
58
+
59
+ // Filter options
60
+ const filteredOptions = options.filter(option =>
61
+ getLabel(option).toLowerCase().includes(searchTerm.toLowerCase())
62
+ )
63
+
64
+ // Click outside to close
65
+ useEffect(() => {
66
+ const handleClickOutside = (event) => {
67
+ if (wrapperRef.current && !wrapperRef.current.contains(event.target)) {
68
+ setIsOpen(false)
69
+ }
70
+ }
71
+ document.addEventListener('mousedown', handleClickOutside)
72
+ return () => document.removeEventListener('mousedown', handleClickOutside)
73
+ }, [])
74
+
75
+ const handleSelect = (option) => {
76
+ const selectedValue = getValue(option)
77
+ setSearchTerm(getLabel(option))
78
+ setIsOpen(false)
79
+ if (onChange) onChange(selectedValue)
80
+ }
81
+
82
+ return (
83
+ <div
84
+ ref={(node) => {
85
+ wrapperRef.current = node
86
+ if (typeof ref === 'function') ref(node)
87
+ else if (ref) ref.current = node
88
+ }}
89
+ // flex-fill lets the wrapper participate in Bootstrap input-group flex layout
90
+ // rounded gives the wrapper a border-radius so the inner input can inherit it
91
+ // correctly when the input-group adjusts border-radius on adjacent items
92
+ className={cn('position-relative ', className)}
93
+ style={{ minWidth: 0 }}
94
+ {...props}
95
+ >
96
+ <input
97
+ id={id}
98
+ type="text"
99
+ className="form-select"
100
+ placeholder={placeholder}
101
+ value={searchTerm}
102
+ disabled={disabled}
103
+ onChange={(e) => {
104
+ setSearchTerm(e.target.value)
105
+ setIsOpen(true)
106
+ }}
107
+ onFocus={() => setIsOpen(true)}
108
+ style={{ cursor: 'text', borderRadius: 'inherit' }}
109
+ />
110
+
111
+ {isOpen && !disabled && (
112
+ <ul
113
+ className="dropdown-menu show w-100 overflow-x-hidden"
114
+ style={{
115
+ maxHeight: '200px',
116
+ marginTop: '2px',
117
+ zIndex: 1050,
118
+ borderRadius: '0.375rem',
119
+ boxShadow: '0 0.5rem 1rem rgba(0,0,0,0.15)',
120
+ }}
121
+ >
122
+ {filteredOptions.length > 0 ? (
123
+ filteredOptions.map((option, index) => {
124
+ const key = getValue(option) ?? `option-${index}`
125
+ return (
126
+ <li key={key}>
127
+ <button
128
+ type="button"
129
+ className="dropdown-item text-truncate"
130
+ onClick={() => handleSelect(option)}
131
+ >
132
+ {getLabel(option)}
133
+ </button>
134
+ </li>
135
+ )
136
+ })
137
+ ) : (
138
+ <li>
139
+ <span className="dropdown-item disabled text-muted" style={{ cursor: 'default' }}>
140
+ No options found
141
+ </span>
142
+ </li>
143
+ )}
144
+ </ul>
145
+ )}
146
+ </div>
147
+ )
148
+ })
149
+
150
+ SearchSelect.displayName = 'SearchSelect'
151
+
152
+ export default SearchSelect
@@ -0,0 +1,299 @@
1
+ /* ==========================================================================
2
+ Custom DatePicker — Bootstrap 5 design tokens
3
+ Zero dependencies. Calendar grid + time picker + year picker + datetime combo.
4
+ Popover is portaled to document.body — positioning is set inline by JS.
5
+ ========================================================================== */
6
+
7
+ /* --- Popover container --- */
8
+ .datepicker-popover {
9
+ border-radius: var(--bs-border-radius, 0.375rem);
10
+ box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
11
+ user-select: none;
12
+ -webkit-user-select: none;
13
+ }
14
+
15
+ .datepicker-popover--wide {
16
+ min-width: 420px;
17
+ }
18
+
19
+ /* --- Calendar panel --- */
20
+ .datepicker-calendar {
21
+ min-width: 260px;
22
+ }
23
+
24
+ /* 7-column day-of-week header + day grid */
25
+ .datepicker-days-header,
26
+ .datepicker-days-grid {
27
+ display: grid;
28
+ grid-template-columns: repeat(7, 1fr);
29
+ gap: 2px;
30
+ }
31
+
32
+ /* Day-of-week label */
33
+ .datepicker-day-label {
34
+ font-size: 0.7rem;
35
+ font-weight: 600;
36
+ color: var(--bs-secondary-color, #6c757d);
37
+ text-transform: uppercase;
38
+ text-align: center;
39
+ padding: 2px 0;
40
+ width: 34px;
41
+ }
42
+
43
+ /* Individual day cell */
44
+ .datepicker-day {
45
+ width: 34px;
46
+ height: 34px;
47
+ display: flex;
48
+ align-items: center;
49
+ justify-content: center;
50
+ font-size: 0.8rem;
51
+ font-weight: 400;
52
+ color: var(--bs-body-color, #212529);
53
+ background: transparent;
54
+ border: none;
55
+ border-radius: var(--bs-border-radius-sm, 0.25rem);
56
+ cursor: pointer;
57
+ transition: background-color 0.15s ease-in-out, color 0.15s ease-in-out;
58
+ padding: 0;
59
+ margin: 0 auto;
60
+ }
61
+
62
+ .datepicker-day:hover:not(:disabled):not(.datepicker-day--selected) {
63
+ background-color: var(--bs-primary-bg-subtle, #cfe2ff);
64
+ }
65
+
66
+ /* Today */
67
+ .datepicker-day--today {
68
+ font-weight: 700;
69
+ border: 1px solid var(--bs-border-color, #dee2e6);
70
+ }
71
+
72
+ /* Selected (Bootstrap primary) */
73
+ .datepicker-day--selected {
74
+ background-color: var(--bs-primary, #0d6efd) !important;
75
+ color: #fff !important;
76
+ font-weight: 600;
77
+ }
78
+
79
+ /* Outside current month */
80
+ .datepicker-day--outside {
81
+ color: var(--bs-tertiary-color, #adb5bd);
82
+ opacity: 0.45;
83
+ pointer-events: none;
84
+ }
85
+
86
+ /* Disabled (outside min/max range) */
87
+ .datepicker-day--disabled {
88
+ color: var(--bs-tertiary-color, #adb5bd);
89
+ opacity: 0.35;
90
+ cursor: not-allowed;
91
+ pointer-events: none;
92
+ }
93
+
94
+ /* --- Year picker --- */
95
+ .datepicker-year-picker {
96
+ min-width: 260px;
97
+ }
98
+
99
+ /* 4-column year grid */
100
+ .datepicker-year-grid {
101
+ display: grid;
102
+ grid-template-columns: repeat(4, 1fr);
103
+ gap: 6px;
104
+ }
105
+
106
+ .datepicker-year-cell {
107
+ font-size: 0.85rem;
108
+ padding: 6px 4px;
109
+ text-align: center;
110
+ }
111
+
112
+ /* --- Time panel --- */
113
+ .datepicker-time {
114
+ min-width: 120px;
115
+ }
116
+
117
+ /* Three-column scrollable time picker */
118
+ .datepicker-time-col {
119
+ display: flex;
120
+ flex-direction: column;
121
+ align-items: center;
122
+ flex: 0 0 auto;
123
+ width: 52px;
124
+ }
125
+
126
+ .datepicker-time-col-label {
127
+ font-size: 0.6rem;
128
+ font-weight: 600;
129
+ color: var(--bs-secondary-color, #6c757d);
130
+ text-transform: uppercase;
131
+ letter-spacing: 0.04em;
132
+ margin-bottom: 2px;
133
+ user-select: none;
134
+ -webkit-user-select: none;
135
+ }
136
+
137
+ .datepicker-time-col-scroll {
138
+ display: flex;
139
+ flex-direction: column;
140
+ align-items: center;
141
+ gap: 1px;
142
+ overflow-y: scroll;
143
+ scrollbar-width: none; /* Firefox */
144
+ -ms-overflow-style: none; /* IE/Edge */
145
+ height: 220px;
146
+ scroll-behavior: smooth;
147
+ }
148
+
149
+ .datepicker-time-col-scroll::-webkit-scrollbar {
150
+ display: none; /* Chrome/Safari/Opera */
151
+ }
152
+
153
+ .datepicker-time-item {
154
+ width: 44px;
155
+ height: 30px;
156
+ display: flex;
157
+ align-items: center;
158
+ justify-content: center;
159
+ font-size: 0.82rem;
160
+ font-weight: 500;
161
+ color: var(--bs-body-color, #212529);
162
+ background: transparent;
163
+ border: none;
164
+ border-radius: var(--bs-border-radius-sm, 0.25rem);
165
+ cursor: pointer;
166
+ transition: background-color 0.1s ease, color 0.1s ease;
167
+ padding: 0;
168
+ flex-shrink: 0;
169
+ }
170
+
171
+ .datepicker-time-item:hover {
172
+ background-color: var(--bs-primary-bg-subtle, #cfe2ff);
173
+ }
174
+
175
+ .datepicker-time-item.active {
176
+ background-color: var(--bs-primary, #0d6efd);
177
+ color: #fff;
178
+ font-weight: 600;
179
+ }
180
+
181
+ /* AM/PM items — slightly taller since only 2 */
182
+ .datepicker-time-item--ampm {
183
+ height: 36px;
184
+ width: 46px;
185
+ font-size: 0.78rem;
186
+ }
187
+
188
+ .datepicker-time-separator {
189
+ font-size: 1.1rem;
190
+ font-weight: 600;
191
+ color: var(--bs-body-color, #212529);
192
+ align-self: center;
193
+ margin-top: 14px;
194
+ flex: 0 0 auto;
195
+ }
196
+
197
+ /* Footer bar (Today/Now + Clear) */
198
+ .datepicker-footer {
199
+ border-bottom-left-radius: var(--bs-border-radius, 0.375rem);
200
+ border-bottom-right-radius: var(--bs-border-radius, 0.375rem);
201
+ }
202
+
203
+ /* --- Dark mode --- */
204
+ [data-bs-theme="dark"] .datepicker-popover {
205
+ background-color: var(--bs-body-bg, #212529);
206
+ border-color: var(--bs-border-color, #495057);
207
+ }
208
+
209
+ [data-bs-theme="dark"] .datepicker-day:hover:not(:disabled):not(.datepicker-day--selected) {
210
+ background-color: var(--bs-primary-bg-subtle, #2c3e50);
211
+ }
212
+
213
+ [data-bs-theme="dark"] .datepicker-time-item:hover {
214
+ background-color: var(--bs-primary-bg-subtle, #2c3e50);
215
+ }
216
+
217
+ [data-bs-theme="dark"] .datepicker-time-item.active {
218
+ background-color: var(--bs-primary, #0d6efd);
219
+ color: #fff;
220
+ }
221
+
222
+ [data-bs-theme="dark"] .datepicker-footer {
223
+ border-top-color: var(--bs-border-color, #495057);
224
+ }
225
+
226
+ /* ==========================================================================
227
+ Mobile responsive — adapts layout and sizing for small screens
228
+ ========================================================================== */
229
+
230
+ @media (max-width: 575.98px) {
231
+ /* Popover: never wider than the viewport */
232
+ .datepicker-popover {
233
+ max-width: calc(100vw - 8px) !important;
234
+ overflow-x: hidden;
235
+ }
236
+
237
+ /* Stack calendar + time vertically in datetime mode */
238
+ .datepicker-popover--wide .d-flex.flex-row {
239
+ flex-direction: column !important;
240
+ }
241
+
242
+ /* Hide vertical divider between calendar and time */
243
+ .datepicker-popover--wide .vr {
244
+ display: none;
245
+ }
246
+
247
+ /* Slightly smaller day cells to fit narrow screens */
248
+ .datepicker-day {
249
+ width: 28px;
250
+ height: 28px;
251
+ font-size: 0.72rem;
252
+ }
253
+
254
+ .datepicker-day-label {
255
+ width: 28px;
256
+ font-size: 0.62rem;
257
+ }
258
+
259
+ /* Reduce calendar min-width */
260
+ .datepicker-calendar,
261
+ .datepicker-year-picker {
262
+ min-width: 210px;
263
+ }
264
+
265
+ /* Time columns — stack more compactly */
266
+ .datepicker-time-col-scroll {
267
+ height: 200px;
268
+ }
269
+
270
+ .datepicker-time-item {
271
+ width: 38px;
272
+ height: 26px;
273
+ font-size: 0.75rem;
274
+ }
275
+
276
+ .datepicker-time-item--ampm {
277
+ width: 40px;
278
+ height: 32px;
279
+ }
280
+
281
+ .datepicker-time-separator {
282
+ font-size: 1rem;
283
+ margin-top: 12px;
284
+ }
285
+
286
+ /* Increase touch target for footer buttons */
287
+ .datepicker-footer .btn {
288
+ font-size: 0.8rem;
289
+ padding: 6px 8px;
290
+ min-height: 34px;
291
+ }
292
+
293
+ /* Year grid cells — slightly roomier for tap */
294
+ .datepicker-year-cell {
295
+ font-size: 0.8rem;
296
+ padding: 8px 4px;
297
+ min-height: 34px;
298
+ }
299
+ }
package/src/index.js ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * react-bootstrap-plugins — Production-grade Bootstrap 5 UI components for React
3
+ *
4
+ * @example
5
+ * // Tree-shakeable: import only what you need
6
+ * import { DatePicker, Label } from 'react-bootstrap-plugins'
7
+ *
8
+ * // Or individual entry points for maximum tree-shaking
9
+ * import DatePicker from 'react-bootstrap-plugins/DatePicker'
10
+ * import SearchSelect from 'react-bootstrap-plugins/SearchSelect'
11
+ */
12
+
13
+ export { DatePicker } from './components/DatePicker.jsx'
14
+ export { default as SearchSelect } from './components/SearchSelect.jsx'
15
+ export { Label } from './components/Label.jsx'
package/src/lib/cn.js ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Utility for conditionally joining CSS class names together.
3
+ * Filters out falsy values and joins with space.
4
+ *
5
+ * @param {...any} args - Class name strings, objects, or arrays
6
+ * @returns {string} Joined class string
7
+ *
8
+ * @example
9
+ * cn('btn', isActive && 'btn-primary', 'px-3')
10
+ * // => 'btn btn-primary px-3'
11
+ *
12
+ * @example
13
+ * cn('base-class', { 'active': isActive, 'disabled': isDisabled })
14
+ * // => 'base-class active' (when isActive=true, isDisabled=false)
15
+ */
16
+ export function cn(...args) {
17
+ const classes = []
18
+
19
+ for (const arg of args) {
20
+ if (!arg) continue
21
+
22
+ if (typeof arg === 'string') {
23
+ classes.push(arg)
24
+ } else if (Array.isArray(arg)) {
25
+ const nested = cn(...arg)
26
+ if (nested) classes.push(nested)
27
+ } else if (typeof arg === 'object') {
28
+ for (const [key, value] of Object.entries(arg)) {
29
+ if (value) classes.push(key)
30
+ }
31
+ }
32
+ }
33
+
34
+ return classes.join(' ')
35
+ }