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.
- package/LICENSE +21 -0
- package/README.md +426 -0
- package/dist/DatePicker.cjs +2 -0
- package/dist/DatePicker.cjs.map +1 -0
- package/dist/DatePicker.d.ts +45 -0
- package/dist/DatePicker.js +2 -0
- package/dist/DatePicker.js.map +1 -0
- package/dist/Label.cjs +2 -0
- package/dist/Label.cjs.map +1 -0
- package/dist/Label.d.ts +16 -0
- package/dist/Label.js +2 -0
- package/dist/Label.js.map +1 -0
- package/dist/SearchSelect.cjs +2 -0
- package/dist/SearchSelect.cjs.map +1 -0
- package/dist/SearchSelect.d.ts +28 -0
- package/dist/SearchSelect.js +2 -0
- package/dist/SearchSelect.js.map +1 -0
- package/dist/chunk-46AJ2Q5H.js +2 -0
- package/dist/chunk-46AJ2Q5H.js.map +1 -0
- package/dist/chunk-4ISYCPRB.cjs +2 -0
- package/dist/chunk-4ISYCPRB.cjs.map +1 -0
- package/dist/chunk-CZV7QBHA.cjs +2 -0
- package/dist/chunk-CZV7QBHA.cjs.map +1 -0
- package/dist/chunk-DIUGH74M.js +2 -0
- package/dist/chunk-DIUGH74M.js.map +1 -0
- package/dist/chunk-F3KMQZXU.js +2 -0
- package/dist/chunk-F3KMQZXU.js.map +1 -0
- package/dist/chunk-HBS6VOGV.cjs +2 -0
- package/dist/chunk-HBS6VOGV.cjs.map +1 -0
- package/dist/chunk-NOGRIWSU.js +2 -0
- package/dist/chunk-NOGRIWSU.js.map +1 -0
- package/dist/chunk-QQNPKA7A.cjs +2 -0
- package/dist/chunk-QQNPKA7A.cjs.map +1 -0
- package/dist/css/css/datepicker-bootstrap.css +299 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +125 -0
- package/src/components/DatePicker.d.ts +45 -0
- package/src/components/DatePicker.jsx +690 -0
- package/src/components/Label.d.ts +16 -0
- package/src/components/Label.jsx +21 -0
- package/src/components/SearchSelect.d.ts +28 -0
- package/src/components/SearchSelect.jsx +152 -0
- package/src/css/datepicker-bootstrap.css +299 -0
- package/src/index.js +15 -0
- 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
|
+
}
|