feat: Implement Calendar component, UiProvider, and a cn utility, while enhancing picker components with new icons and styles.

This commit is contained in:
Firman Ramdhani
2026-02-04 16:53:37 +07:00
parent 1ceb0fad53
commit 660a9ace20
10 changed files with 1890 additions and 470 deletions
@@ -59,6 +59,11 @@ export function generateComponentPicker<DateType extends object>(
generateConfig={generateConfig}
locale={locale || enUS}
className={getStatusClassName(prefixCls, className, status, props.disabled)}
prevIcon={<span className={`${prefixCls}-prev-icon`} />}
nextIcon={<span className={`${prefixCls}-next-icon`} />}
superPrevIcon={<span className={`${prefixCls}-super-prev-icon`} />}
superNextIcon={<span className={`${prefixCls}-super-next-icon`} />}
transitionName={`rc-slide-up`}
{...restProps}
/>
);
@@ -150,7 +155,13 @@ export function generateComponentPicker<DateType extends object>(
MonthRangePicker.displayName = 'MonthRangePicker';
const QuarterRangePicker = forwardRef<any, SpecificRangePickerProps<DateType>>((props, ref) => (
<RangePicker ref={ref} mode={undefined} placeholder={['Start quarter', 'End quarter']} {...props} picker="quarter" />
<RangePicker
ref={ref}
mode={undefined}
placeholder={['Start quarter', 'End quarter']}
{...props}
picker="quarter"
/>
));
QuarterRangePicker.displayName = 'QuarterRangePicker';
@@ -0,0 +1,647 @@
@layer components {
/* =========================================
PICKER BASE (INPUT WRAPPER)
========================================= */
.rc-picker {
box-sizing: border-box;
display: inline-flex;
align-items: center;
width: 100%;
position: relative;
/* Using variable dimensions from globals.css */
height: var(--input-height);
padding: 0 var(--input-padding-x);
font-size: var(--input-font-size);
line-height: 1.5;
color: var(--input-text);
background-color: var(--input-bg);
border: 1px solid transparent;
border-color: var(--input-border);
border-radius: var(--input-radius);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
outline: none;
}
/* Input inside Picker */
.rc-picker-input {
position: relative;
display: flex;
align-items: center;
width: 100%;
height: 100%;
flex: 1;
}
.rc-picker-input > input {
width: 100%;
height: 100%;
min-width: 0;
padding: 0;
background-color: transparent;
border: 0;
outline: none;
color: inherit;
font-size: inherit;
line-height: inherit;
transition: all 0.2s;
}
.rc-picker-input > input::placeholder {
color: var(--input-placeholder);
opacity: 1;
}
/* Suffix Icon (Calendar Icon) */
.rc-picker-suffix {
margin-left: 0.5rem;
color: var(--color-gray-400);
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
font-size: var(--text-lg);
transition: all 0.2s;
}
/* Clear Icon */
.rc-picker-clear {
position: absolute;
top: 50%;
right: var(--input-padding-x);
transform: translateY(-50%);
cursor: pointer;
opacity: 0;
transition:
opacity 0.2s,
color 0.2s;
background: var(--input-bg);
color: var(--color-gray-400);
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
width: 1.2em;
height: 1.2em;
border-radius: 50%;
}
.rc-picker:hover:not(.rc-picker-disabled) .rc-picker-clear {
opacity: 1;
}
.rc-picker-clear:hover {
color: var(--color-gray-500);
background-color: var(--color-gray-100);
}
.rc-picker-clear-btn::after {
content: '×';
font-size: 1.2em;
line-height: 1;
}
/* =========================================
STATES (HOVER, FOCUS, DISABLED, ERROR)
========================================= */
/* Hover State */
.rc-picker:hover:not(.rc-picker-disabled) {
border-color: var(--input-border-hover);
}
/* Focused State */
.rc-picker-focused {
border-color: var(--focus-border-color);
box-shadow: 0 0 0 3px var(--focus-ring-color);
z-index: 1;
}
/* Disabled State */
.rc-picker-disabled {
background-color: var(--disabled-bg);
color: var(--disabled-text);
border-color: var(--disabled-border);
cursor: not-allowed;
}
.rc-picker-disabled input {
cursor: not-allowed;
}
/* Range Picker Specific */
.rc-picker-range.rc-picker-focused .rc-picker-active-bar {
opacity: 1;
}
.rc-picker-range-separator {
align-items: center;
padding: 0 8px;
line-height: 1;
color: var(--color-gray-400);
display: inline-flex;
}
.rc-picker-active-bar {
bottom: -1px;
height: 2px;
margin-left: var(--input-padding-x); /* Align with input start */
background: var(--color-brand-500);
opacity: 0;
transition: all 0.3s ease-out;
pointer-events: none;
z-index: 2;
position: absolute;
left: 0;
width: auto;
}
/* =========================================
PICKER DROPDOWN PANEL (THE MEAT)
========================================= */
.rc-picker-dropdown {
position: absolute;
z-index: 1050;
box-sizing: border-box;
font-size: var(--text-sm);
font-family: inherit;
}
.rc-picker-dropdown-hidden {
display: none;
}
/* Arrow styling removed for modern clean look (AntD v5/v6 typically removes large arrows) */
.rc-picker-dropdown .rc-picker-range-arrow {
display: none;
}
/* Panel Container */
.rc-picker-panel-container {
overflow: hidden;
vertical-align: top;
background: #fff;
border-radius: var(--radius-lg);
/* Elevation 3/4 equivalent */
box-shadow:
0 6px 16px 0 rgba(0, 0, 0, 0.08),
0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 9px 28px 8px rgba(0, 0, 0, 0.05);
transition: margin 0.3s;
border: 1px solid var(--color-gray-100);
}
.rc-picker-panel-layout {
display: flex;
flex-wrap: nowrap;
align-items: stretch;
}
.rc-picker-panel {
vertical-align: top;
background: transparent;
display: inline-flex;
flex-direction: column;
}
/* Double Panel (Range Picker) */
.rc-picker-panel + .rc-picker-panel {
border-left: 1px solid var(--color-gray-100);
}
/* =========================================
HEADER SECTION
========================================= */
.rc-picker-header {
display: flex;
padding: 0 8px;
border-bottom: 1px solid var(--color-gray-100);
min-height: 40px;
align-items: center;
justify-content: space-between;
}
.rc-picker-header > * {
flex: none;
}
.rc-picker-header-view {
flex: auto;
text-align: center;
font-weight: var(--font-weight-semibold);
display: flex;
justify-content: center;
gap: 4px;
}
.rc-picker-header-view button {
color: var(--color-gray-800);
font-weight: inheriting;
padding: 0 4px;
background: transparent;
border: 0;
cursor: pointer;
font-size: var(--text-sm);
line-height: 40px;
transition: color 0.2s;
}
.rc-picker-header-view button:hover {
color: var(--color-brand-500);
}
/* Navigation Buttons (< << >> >) */
.rc-picker-header-super-prev-btn,
.rc-picker-header-prev-btn,
.rc-picker-header-next-btn,
.rc-picker-header-super-next-btn {
min-width: 1.6em;
font-size: 14px;
color: var(--color-gray-400);
cursor: pointer;
background: transparent;
border: 0;
line-height: 40px;
padding: 0 5px;
display: inline-flex;
justify-content: center;
align-items: center;
transition: color 0.2s;
position: relative;
}
.rc-picker-header-super-prev-btn:hover,
.rc-picker-header-prev-btn:hover,
.rc-picker-header-next-btn:hover,
.rc-picker-header-super-next-btn:hover {
color: var(--color-gray-900);
}
/* Arrow Symbols using pseudo elements if needed, or rely on RC Picker default text symbols but styled */
/* RC Picker default uses unicode arrows usually. */
/* =========================================
BODY SECTION (CALENDAR GRID)
========================================= */
.rc-picker-body {
padding: 8px 12px;
}
.rc-picker-content {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
}
.rc-picker-content th,
.rc-picker-content td {
position: relative;
min-width: 24px;
font-weight: 400;
}
.rc-picker-content th {
height: 32px;
color: var(--color-gray-500);
line-height: 32px;
text-align: center;
}
.rc-picker-cell {
padding: 4px 0;
color: var(--color-gray-400);
cursor: pointer;
}
.rc-picker-cell-in-view {
color: var(--color-gray-800);
}
.rc-picker-cell-disabled {
cursor: not-allowed;
color: var(--disabled-text);
}
.rc-picker-cell-disabled .rc-picker-cell-inner {
background: var(--disabled-bg);
}
/* Inner Cell (The Circle/Square) */
.rc-picker-cell-inner {
position: relative;
z-index: 2;
display: inline-block;
min-width: 24px;
height: 24px;
line-height: 24px;
border-radius: var(
--radius-md
); /* Ant Design v5 uses slightly rounded squares, not full circles usually, but we can do md radius */
transition:
background 0.2s,
border 0.2s,
color 0.2s;
text-align: center;
margin: 0 auto;
padding: 0 4px; /* For longer text like months */
}
/* Month/Year Panel cells are wider */
.rc-picker-month-panel .rc-picker-cell-inner,
.rc-picker-year-panel .rc-picker-cell-inner,
.rc-picker-quarter-panel .rc-picker-cell-inner,
.rc-picker-decade-panel .rc-picker-cell-inner {
width: auto;
padding: 0 12px;
min-width: 48px;
}
/* Hover State */
.rc-picker-cell:hover:not(.rc-picker-cell-selected):not(.rc-picker-cell-range-start):not(
.rc-picker-cell-range-end
):not(.rc-picker-cell-disabled)
.rc-picker-cell-inner {
background: var(--color-gray-100);
}
/* Selected Date */
.rc-picker-cell-selected .rc-picker-cell-inner,
.rc-picker-cell-range-start .rc-picker-cell-inner,
.rc-picker-cell-range-end .rc-picker-cell-inner {
color: #fff;
background: var(--color-brand-500);
box-shadow: 0 2px 0 rgba(0, 0, 0, 0.045);
}
.rc-picker-cell-selected:hover .rc-picker-cell-inner {
background: var(--color-brand-600);
}
/* Today */
.rc-picker-cell-today .rc-picker-cell-inner::before {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
border: 1px solid var(--color-brand-500);
border-radius: var(--radius-md);
content: '';
transition: all 0.2s;
}
.rc-picker-cell-selected.rc-picker-cell-today .rc-picker-cell-inner::before {
border-color: #fff; /* White border inside selected today */
}
/* =========================================
RANGE SELECTION STYLING
========================================= */
/* The strip between start and end */
.rc-picker-cell-in-range > .rc-picker-cell-inner {
background: var(--color-brand-50);
border-radius: 0;
min-width: 100%; /* Fill the whole cell width */
color: var(--color-gray-800);
}
.rc-picker-cell-in-range::before {
background: var(--color-brand-50);
}
/* Range Edge Adjustments */
.rc-picker-cell-range-start > .rc-picker-cell-inner {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-top-left-radius: var(--radius-md);
border-bottom-left-radius: var(--radius-md);
}
.rc-picker-cell-range-end > .rc-picker-cell-inner {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-top-right-radius: var(--radius-md);
border-bottom-right-radius: var(--radius-md);
}
/* Ghost/Pseudo elements for continuous range background */
/* RC Picker handles this logic via class combinations.
We need to ensure the background flows nicely. */
.rc-picker-cell-range-start.rc-picker-cell-range-end .rc-picker-cell-inner {
border-radius: var(--radius-md);
}
/* Range Hover (when selecting the second date) */
.rc-picker-cell-range-hover-start::after,
.rc-picker-cell-range-hover-end::after,
.rc-picker-cell-range-hover::after {
content: ''; /* Should show dotted line or lighter highlight */
position: absolute;
top: 50%;
left: 0;
right: 0;
bottom: 0;
border-top: 2px dashed var(--color-brand-300);
transform: translateY(-50%);
z-index: 0;
display: none; /* AntD v6 doesn't really emphasize this as much, can keep simple */
}
/* =========================================
TIME PANEL
========================================= */
.rc-picker-time-panel {
border-left: 1px solid var(--color-gray-100);
width: auto;
}
.rc-picker-time-panel .rc-picker-content {
display: flex;
max-height: 224px;
height: 224px;
}
.rc-picker-time-panel-column {
flex: 1 0 auto;
width: 60px; /* Wider for easier clicking */
margin: 0;
padding: 0;
overflow-y: hidden;
text-align: left;
list-style: none;
transition: background 0.3s;
overflow-x: hidden;
border-left: 1px solid var(--color-gray-100); /* Separator between hrs/min/sec */
}
.rc-picker-time-panel-column:first-child {
border-left: 0;
}
.rc-picker-time-panel-column:hover {
overflow-y: auto;
}
.rc-picker-time-panel-column > li {
margin: 0;
padding: 0;
}
.rc-picker-time-panel-column > li .rc-picker-time-panel-cell-inner {
display: block;
width: 100%;
height: 28px;
margin: 0;
padding: 0 0 0 12px;
color: var(--color-gray-800);
line-height: 28px;
text-align: left;
cursor: pointer;
transition: background 0.2s;
}
.rc-picker-time-panel-column > li:hover .rc-picker-time-panel-cell-inner {
background: var(--color-gray-100);
}
.rc-picker-time-panel-column > li.rc-picker-time-panel-cell-selected .rc-picker-time-panel-cell-inner {
background: var(--color-brand-50);
color: var(--color-brand-600);
font-weight: 600;
}
.rc-picker-time-panel-column > li.rc-picker-time-panel-cell-disabled .rc-picker-time-panel-cell-inner {
color: var(--disabled-text);
cursor: not-allowed;
background: transparent;
}
/* Date + Time Layout */
.rc-picker-datetime-panel {
display: flex;
}
.rc-picker-datetime-panel .rc-picker-date-panel,
.rc-picker-datetime-panel .rc-picker-time-panel {
transition: opacity 0.3s;
}
/* =========================================
FOOTER
========================================= */
.rc-picker-footer {
width: auto;
min-width: 100%;
line-height: 38px;
text-align: center;
border-top: 1px solid var(--color-gray-100);
padding: 0 12px;
}
.rc-picker-footer-extra {
text-align: left;
}
/* Ranges (Presets) */
.rc-picker-ranges {
margin: 0;
padding: 0;
list-style: none;
overflow: hidden;
display: flex;
justify-content: space-between;
align-items: center;
}
.rc-picker-ranges > li {
display: inline-block;
}
.rc-picker-ranges .rc-picker-preset > .rc-picker-preset-tag {
display: inline-block;
margin-right: 8px;
color: var(--color-brand-500);
cursor: pointer;
transition: color 0.2s;
}
.rc-picker-ranges .rc-picker-preset > .rc-picker-preset-tag:hover {
color: var(--color-brand-600);
}
.rc-picker-ranges .rc-picker-ok {
float: right;
}
/* OK Button */
.rc-picker-ok button {
background: var(--color-brand-500);
color: #fff;
border: none;
border-radius: var(--radius-sm);
padding: 2px 12px;
height: 24px;
cursor: pointer;
font-size: var(--text-sm);
transition: background 0.2s;
}
.rc-picker-ok button:hover {
background: var(--color-brand-600);
}
.rc-picker-ok button:disabled {
background: var(--disabled-bg);
color: var(--disabled-text);
cursor: not-allowed;
}
.rc-picker-now-btn {
color: var(--color-brand-500);
cursor: pointer;
transition: color 0.2s;
}
.rc-picker-now-btn:hover {
color: var(--color-brand-600);
}
/* =========================================
STATUS MODIFIERS (ERROR, WARNING, ETC)
========================================= */
.picker-status-error.rc-picker {
border-color: var(--color-error-500);
}
.picker-status-error.rc-picker:hover:not(.rc-picker-disabled) {
border-color: var(--color-error-600);
}
.picker-status-error.rc-picker.rc-picker-focused {
border-color: var(--color-error-600);
box-shadow: 0 0 0 3px var(--color-error-100);
}
.picker-status-warning.rc-picker {
border-color: var(--color-warning-500);
}
.picker-status-warning.rc-picker:hover:not(.rc-picker-disabled) {
border-color: var(--color-warning-600);
}
.picker-status-warning.rc-picker.rc-picker-focused {
border-color: var(--color-warning-600);
box-shadow: 0 0 0 3px var(--color-warning-100);
}
.picker-status-success.rc-picker {
border-color: var(--color-success-500);
}
.picker-status-success.rc-picker:hover:not(.rc-picker-disabled) {
border-color: var(--color-success-600);
}
.picker-status-success.rc-picker.rc-picker-focused {
border-color: var(--color-success-600);
box-shadow: 0 0 0 3px var(--color-success-100);
}
}
File diff suppressed because it is too large Load Diff
+5 -2
View File
@@ -1,2 +1,5 @@
// components
export * from "./components";
// UI Components
export * from './components';
// UI utils
export * from './utils/cn';
+10
View File
@@ -0,0 +1,10 @@
import React from 'react';
import { ConfigProvider, App } from 'antd';
export const UIProvider = ({ children }: { children: React.ReactNode }) => {
return (
<ConfigProvider>
<App>{children}</App>
</ConfigProvider>
);
};
+11
View File
@@ -0,0 +1,11 @@
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
/**
* Intelligently merges classnames.
* 1. clsx: handles conditional logic (true/false)
* 2. twMerge: removes conflicting Tailwind classes
*/
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}