feat: first commit, working
commit
b4abe21060
@ -0,0 +1 @@
|
|||||||
|
node_modules/
|
||||||
@ -0,0 +1,178 @@
|
|||||||
|
# Minimal Calendar Component
|
||||||
|
|
||||||
|
A flexible and customizable calendar component built with React, TypeScript, and styled-components.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 📅 Year, month, and week views
|
||||||
|
- 🎨 Multiple header styles (expanded, compacted, tiny, none)
|
||||||
|
- 🎯 Customizable month cutoff behavior
|
||||||
|
- 🎭 Weekend highlighting
|
||||||
|
- 📱 Fully responsive design
|
||||||
|
- 🔧 TypeScript support
|
||||||
|
- 💅 Styled with styled-components
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install date-fns styled-components
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Year } from './components/calendar/Year';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<Year
|
||||||
|
year={2024}
|
||||||
|
dayHeaderStyle="tiny"
|
||||||
|
weekendDays={[6, 0]} // Saturday and Sunday
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Usage
|
||||||
|
|
||||||
|
### With Date Selection
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Year } from './components/calendar/Year';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [selectedDate, setSelectedDate] = useState<Date | undefined>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Year
|
||||||
|
year={2024}
|
||||||
|
dayHeaderStyle="expanded"
|
||||||
|
monthCutoff="truncate"
|
||||||
|
weekendDays={[6, 0]}
|
||||||
|
selectedDate={selectedDate}
|
||||||
|
onDateSelect={setSelectedDate}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### With Controls
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Year } from './components/calendar/Year';
|
||||||
|
import { Controls } from './components/calendar/Controls';
|
||||||
|
import { HeaderStyle, MonthCutoffType } from './types/calendar';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [headerStyle, setHeaderStyle] = useState<HeaderStyle>('tiny');
|
||||||
|
const [monthCutoff, setMonthCutoff] = useState<MonthCutoffType>('truncate');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Controls
|
||||||
|
headerStyle={headerStyle}
|
||||||
|
monthCutoff={monthCutoff}
|
||||||
|
onHeaderStyleChange={setHeaderStyle}
|
||||||
|
onMonthCutoffChange={setMonthCutoff}
|
||||||
|
/>
|
||||||
|
<Year
|
||||||
|
year={2024}
|
||||||
|
dayHeaderStyle={headerStyle}
|
||||||
|
monthCutoff={monthCutoff}
|
||||||
|
weekendDays={[6, 0]}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Component Props
|
||||||
|
|
||||||
|
### Year Component
|
||||||
|
|
||||||
|
| Prop | Type | Description |
|
||||||
|
|------|------|-------------|
|
||||||
|
| year | number | The year to display |
|
||||||
|
| dayHeaderStyle | 'expanded' \| 'compacted' \| 'tiny' \| 'none' | Style of day headers |
|
||||||
|
| monthDayOfWeekHeaderStyle | HeaderStyle (optional) | Style of month day headers |
|
||||||
|
| monthCutoff | 'dimmed' \| 'truncate' \| undefined | How to handle days from other months |
|
||||||
|
| weekendDays | number[] | Array of day indices to mark as weekends (0-6) |
|
||||||
|
| selectedDate | Date (optional) | Currently selected date |
|
||||||
|
| rangeStart | Date (optional) | Start date for range selection |
|
||||||
|
| rangeEnd | Date (optional) | End date for range selection |
|
||||||
|
| onDateSelect | (date: Date) => void (optional) | Date selection callback |
|
||||||
|
| compact | boolean (optional) | Use compact layout |
|
||||||
|
|
||||||
|
### Controls Component
|
||||||
|
|
||||||
|
| Prop | Type | Description |
|
||||||
|
|------|------|-------------|
|
||||||
|
| headerStyle | HeaderStyle | Current header style |
|
||||||
|
| monthCutoff | MonthCutoffType | Current month cutoff type |
|
||||||
|
| onHeaderStyleChange | (type: HeaderStyle) => void | Header style change handler |
|
||||||
|
| onMonthCutoffChange | (type: MonthCutoffType) => void | Month cutoff change handler |
|
||||||
|
|
||||||
|
## Styling
|
||||||
|
|
||||||
|
The calendar uses styled-components for styling. You can customize the appearance by:
|
||||||
|
|
||||||
|
1. Using the built-in props
|
||||||
|
2. Extending the styled components
|
||||||
|
3. Wrapping components with custom styled containers
|
||||||
|
|
||||||
|
Example of custom styling:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { Year } from './components/calendar/Year';
|
||||||
|
|
||||||
|
const CustomCalendarContainer = styled.div`
|
||||||
|
padding: 2rem;
|
||||||
|
background: #fafafa;
|
||||||
|
|
||||||
|
// Custom styles for the calendar
|
||||||
|
.month-title {
|
||||||
|
color: #1a73e8;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<CustomCalendarContainer>
|
||||||
|
<Year
|
||||||
|
year={2024}
|
||||||
|
dayHeaderStyle="tiny"
|
||||||
|
/>
|
||||||
|
</CustomCalendarContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Header Styles
|
||||||
|
|
||||||
|
- **expanded**: Full day names (e.g., "MONDAY")
|
||||||
|
- **compacted**: Three-letter day names (e.g., "MON")
|
||||||
|
- **tiny**: Single letter day names (e.g., "M")
|
||||||
|
- **none**: No day headers, only numbers
|
||||||
|
|
||||||
|
## Month Cutoff Options
|
||||||
|
|
||||||
|
- **dimmed**: Show days from other months with reduced opacity
|
||||||
|
- **truncate**: Hide days from other months
|
||||||
|
- **undefined**: Show all days normally
|
||||||
|
|
||||||
|
## Browser Support
|
||||||
|
|
||||||
|
The calendar component supports all modern browsers:
|
||||||
|
|
||||||
|
- Chrome (latest)
|
||||||
|
- Firefox (latest)
|
||||||
|
- Safari (latest)
|
||||||
|
- Edge (latest)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Calendar</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "react-calendar",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"date-fns": "^2.30.0",
|
||||||
|
"classnames": "^2.3.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.15",
|
||||||
|
"@types/react-dom": "^18.2.7",
|
||||||
|
"@vitejs/plugin-react": "^4.0.3",
|
||||||
|
"typescript": "^5.0.2",
|
||||||
|
"vite": "^4.4.5",
|
||||||
|
"sass": "^1.69.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
|
||||||
|
$border-color;
|
||||||
|
$border-color; // replaced shared.$border-color
|
||||||
|
$content-normal-bg; // replaced shared.$content-background-color
|
||||||
|
$content-normal-bg; // replaced shared.$content-background-color-selecting
|
||||||
|
$content-normal-text; // replaced shared.$content-color
|
||||||
|
$day-content-defaultmode-defaultstate-background-color;
|
||||||
|
$day-content-defaultmode-defaultstate-color;
|
||||||
|
$day-content-defaultmode-defaultstate-color; // replaced shared.$day-content-defaultmode-defaultstate-color
|
||||||
|
$day-content-defaultmode-selectedstate-midrange-background-color;
|
||||||
|
$day-content-defaultmode-selectedstate-midrange-color;
|
||||||
|
$day-content-defaultmode-selectedstate-rangeend-background-color;
|
||||||
|
$day-content-defaultmode-selectedstate-rangeend-color;
|
||||||
|
$day-content-defaultmode-selectedstate-rangeend-hover-background-color;
|
||||||
|
$day-content-defaultmode-selectedstate-rangestart-background-color;
|
||||||
|
$day-content-defaultmode-selectedstate-rangestart-color;
|
||||||
|
$day-content-defaultmode-selectedstate-rangestart-hover-background-color;
|
||||||
|
$day-content-defaultmode-selectingstate-midrange-background-color;
|
||||||
|
$day-content-defaultmode-selectingstate-midrange-color;
|
||||||
|
$day-content-defaultmode-selectingstate-midrange-hover-background-color;
|
||||||
|
$day-content-defaultmode-selectingstate-rangeend-background-color;
|
||||||
|
$day-content-defaultmode-selectingstate-rangeend-color;
|
||||||
|
$day-content-defaultmode-selectingstate-rangeend-hover-background-color;
|
||||||
|
$day-content-defaultmode-selectingstate-rangestart-background-color;
|
||||||
|
$day-content-defaultmode-selectingstate-rangestart-color;
|
||||||
|
$day-content-defaultmode-selectingstate-rangestart-hover-background-color;
|
||||||
|
$day-content-greyedmode-defaultstate-background-color;
|
||||||
|
$day-content-greyedmode-defaultstate-color;
|
||||||
|
$day-content-greyedmode-selectedstate-midrange-background-color;
|
||||||
|
$day-content-greyedmode-selectedstate-midrange-color;
|
||||||
|
$day-content-greyedmode-selectedstate-rangeend-background-color;
|
||||||
|
$day-content-greyedmode-selectedstate-rangeend-color;
|
||||||
|
$day-content-greyedmode-selectedstate-rangeend-hover-background-color;
|
||||||
|
$day-content-greyedmode-selectedstate-rangestart-background-color;
|
||||||
|
$day-content-greyedmode-selectedstate-rangestart-color;
|
||||||
|
$day-content-greyedmode-selectedstate-rangestart-hover-background-color;
|
||||||
|
$day-content-greyedmode-selectingstate-midrange-background-color;
|
||||||
|
$day-content-greyedmode-selectingstate-midrange-color;
|
||||||
|
$day-content-greyedmode-selectingstate-rangeend-background-color;
|
||||||
|
$day-content-greyedmode-selectingstate-rangeend-color;
|
||||||
|
$day-content-greyedmode-selectingstate-rangeend-hover-background-color;
|
||||||
|
$day-content-greyedmode-selectingstate-rangestart-background-color;
|
||||||
|
$day-content-greyedmode-selectingstate-rangestart-color;
|
||||||
|
$day-content-greyedmode-selectingstate-rangestart-hover-background-color;
|
||||||
|
$day-header-defaultmode-defaultstate-background-color;
|
||||||
|
$day-header-defaultmode-defaultstate-color;
|
||||||
|
$day-header-defaultmode-selectedstate-midrange-background-color;
|
||||||
|
$day-header-defaultmode-selectedstate-midrange-color;
|
||||||
|
$day-header-defaultmode-selectedstate-rangeend-background-color;
|
||||||
|
$day-header-defaultmode-selectedstate-rangeend-color;
|
||||||
|
$day-header-defaultmode-selectedstate-rangeend-hover-background-color;
|
||||||
|
$day-header-defaultmode-selectedstate-rangestart-background-color;
|
||||||
|
$day-header-defaultmode-selectedstate-rangestart-color;
|
||||||
|
$day-header-defaultmode-selectedstate-rangestart-hover-background-color;
|
||||||
|
$day-header-defaultmode-selectingstate-midrange-background-color;
|
||||||
|
$day-header-defaultmode-selectingstate-midrange-color;
|
||||||
|
$day-header-defaultmode-selectingstate-rangeend-background-color;
|
||||||
|
$day-header-defaultmode-selectingstate-rangeend-color;
|
||||||
|
$day-header-defaultmode-selectingstate-rangeend-hover-background-color;
|
||||||
|
$day-header-defaultmode-selectingstate-rangestart-background-color;
|
||||||
|
$day-header-defaultmode-selectingstate-rangestart-color;
|
||||||
|
$day-header-defaultmode-selectingstate-rangestart-hover-background-color;
|
||||||
|
$day-header-greyedmode-defaultstate-background-color;
|
||||||
|
$day-header-greyedmode-defaultstate-background-color; // replaced shared.$header-background-color-greyed
|
||||||
|
$day-header-greyedmode-defaultstate-color;
|
||||||
|
$day-header-greyedmode-defaultstate-color; // replaced shared.$header-color-greyed
|
||||||
|
$day-header-greyedmode-selectedstate-midrange-background-color;
|
||||||
|
$day-header-greyedmode-selectedstate-midrange-color;
|
||||||
|
$day-header-greyedmode-selectedstate-rangeend-background-color;
|
||||||
|
$day-header-greyedmode-selectedstate-rangeend-color;
|
||||||
|
$day-header-greyedmode-selectedstate-rangeend-hover-background-color;
|
||||||
|
$day-header-greyedmode-selectedstate-rangestart-background-color;
|
||||||
|
$day-header-greyedmode-selectedstate-rangestart-color;
|
||||||
|
$day-header-greyedmode-selectedstate-rangestart-hover-background-color;
|
||||||
|
$day-header-greyedmode-selectingstate-midrange-background-color;
|
||||||
|
$day-header-greyedmode-selectingstate-midrange-color;
|
||||||
|
$day-header-greyedmode-selectingstate-rangeend-background-color;
|
||||||
|
$day-header-greyedmode-selectingstate-rangeend-color;
|
||||||
|
$day-header-greyedmode-selectingstate-rangeend-hover-background-color;
|
||||||
|
$day-header-greyedmode-selectingstate-rangestart-background-color;
|
||||||
|
$day-header-greyedmode-selectingstate-rangestart-color;
|
||||||
|
$day-header-greyedmode-selectingstate-rangestart-hover-background-color;
|
||||||
|
$focus-blue;
|
||||||
|
$header-normal-bg; // replaced shared.$header-background-color
|
||||||
|
$header-normal-text; // replaced shared.$header-color
|
||||||
|
scss)
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
.container {
|
||||||
|
padding: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #fafafa;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
padding: 2rem;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
color: #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionDescription {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #666;
|
||||||
|
margin: 0 0 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demoContainer {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rangeInfo {
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { DateRangePicker } from './examples/DateRangePicker';
|
||||||
|
import { SingleMonth } from './examples/SingleMonth';
|
||||||
|
import { WeekView } from './examples/WeekView';
|
||||||
|
import { CompactYear } from './examples/CompactYear';
|
||||||
|
import styles from './App.module.scss';
|
||||||
|
|
||||||
|
const App: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<DateRangePicker />
|
||||||
|
<SingleMonth />
|
||||||
|
<WeekView />
|
||||||
|
<CompactYear />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default App;
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
.controlsContainer {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
flex-direction: column;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,135 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Button } from '../ui/Button';
|
||||||
|
import { ButtonGroup } from '../ui/ButtonGroup';
|
||||||
|
import { RangeInput } from '../ui/RangeInput';
|
||||||
|
import { HeaderStyle, MonthCutoffType, DaySize } from '../../types/calendar';
|
||||||
|
import styles from './Controls.module.scss';
|
||||||
|
|
||||||
|
interface ControlsProps {
|
||||||
|
headerStyle: HeaderStyle;
|
||||||
|
monthCutoff: MonthCutoffType;
|
||||||
|
size: DaySize;
|
||||||
|
fontProportion: number;
|
||||||
|
magnify: boolean;
|
||||||
|
onHeaderStyleChange: (type: HeaderStyle) => void;
|
||||||
|
onMonthCutoffChange: (type: MonthCutoffType) => void;
|
||||||
|
onSizeChange: (size: DaySize) => void;
|
||||||
|
onFontProportionChange: (proportion: number) => void;
|
||||||
|
onMagnifyChange: (magnify: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Controls: React.FC<ControlsProps> = ({
|
||||||
|
headerStyle,
|
||||||
|
monthCutoff,
|
||||||
|
size,
|
||||||
|
fontProportion,
|
||||||
|
magnify,
|
||||||
|
onHeaderStyleChange,
|
||||||
|
onMonthCutoffChange,
|
||||||
|
onSizeChange,
|
||||||
|
onFontProportionChange,
|
||||||
|
onMagnifyChange,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.controlsContainer}>
|
||||||
|
<ButtonGroup>
|
||||||
|
<Button
|
||||||
|
active={headerStyle === 'expanded'}
|
||||||
|
onClick={() => onHeaderStyleChange('expanded')}
|
||||||
|
>
|
||||||
|
Expanded
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
active={headerStyle === 'compacted'}
|
||||||
|
onClick={() => onHeaderStyleChange('compacted')}
|
||||||
|
>
|
||||||
|
Compacted
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
active={headerStyle === 'tiny'}
|
||||||
|
onClick={() => onHeaderStyleChange('tiny')}
|
||||||
|
>
|
||||||
|
Tiny
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
active={headerStyle === 'none'}
|
||||||
|
onClick={() => onHeaderStyleChange('none')}
|
||||||
|
>
|
||||||
|
Numbers
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
|
||||||
|
<ButtonGroup>
|
||||||
|
<Button
|
||||||
|
active={monthCutoff === 'dimmed'}
|
||||||
|
onClick={() => onMonthCutoffChange('dimmed')}
|
||||||
|
>
|
||||||
|
Dimmed
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
active={monthCutoff === 'truncate'}
|
||||||
|
onClick={() => onMonthCutoffChange('truncate')}
|
||||||
|
>
|
||||||
|
Truncate
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
active={monthCutoff === undefined}
|
||||||
|
onClick={() => onMonthCutoffChange(undefined)}
|
||||||
|
>
|
||||||
|
Show All
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
|
||||||
|
<ButtonGroup>
|
||||||
|
<Button
|
||||||
|
active={size === 'xl'}
|
||||||
|
onClick={() => onSizeChange('xl')}
|
||||||
|
>
|
||||||
|
XL
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
active={size === 'l'}
|
||||||
|
onClick={() => onSizeChange('l')}
|
||||||
|
>
|
||||||
|
L
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
active={size === 'm'}
|
||||||
|
onClick={() => onSizeChange('m')}
|
||||||
|
>
|
||||||
|
M
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
active={size === 's'}
|
||||||
|
onClick={() => onSizeChange('s')}
|
||||||
|
>
|
||||||
|
S
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
active={size === 'xs'}
|
||||||
|
onClick={() => onSizeChange('xs')}
|
||||||
|
>
|
||||||
|
XS
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
|
||||||
|
<ButtonGroup>
|
||||||
|
<Button
|
||||||
|
active={magnify}
|
||||||
|
onClick={() => onMagnifyChange(!magnify)}
|
||||||
|
>
|
||||||
|
{magnify ? 'Magnify On -> Turn off' : 'Magnify Off - Turn on'}
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
|
||||||
|
<RangeInput
|
||||||
|
value={fontProportion}
|
||||||
|
min={10}
|
||||||
|
max={100}
|
||||||
|
step={5}
|
||||||
|
onChange={onFontProportionChange}
|
||||||
|
label="Text Size"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,463 @@
|
|||||||
|
@use 'variables' as v;
|
||||||
|
@use '../shared/variables' as shared;
|
||||||
|
@use '../shared/colors' as colors;
|
||||||
|
|
||||||
|
.Day {
|
||||||
|
|
||||||
|
&__Header {
|
||||||
|
color: colors.$day-header-defaultmode-defaultstate-color;
|
||||||
|
background: colors.$day-header-defaultmode-defaultstate-background-color;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__HeaderText {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__Content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: v.$content-padding-base;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__Container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
border: shared.$border-size solid colors.$border-color;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
background: colors.$day-content-defaultmode-defaultstate-background-color;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
border-radius: shared.$border-radius-sm;
|
||||||
|
|
||||||
|
@media (min-width: shared.$breakpoint-tablet) {
|
||||||
|
border-radius: shared.$border-radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--interactive {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: shared.$shadow-hover;
|
||||||
|
background: colors.$day-content-defaultmode-selectingstate-midrange-hover-background-color;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: shared.$shadow-focus;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--otherMonth {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// *****************
|
||||||
|
// Border Management
|
||||||
|
// *****************
|
||||||
|
&--selecting, &--selected {
|
||||||
|
&:not(.Day__Container--rowStart):not(.Day__Container--rowEnd):not(.Day__Container--rangeStart):not(.Day__Container--rangeEnd) {
|
||||||
|
border-left: 0;
|
||||||
|
border-right: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
&.Day__Container--rangeStart:not(.Day__Container--rangeEnd):not(.Day__Container--rowEnd) {
|
||||||
|
border-right: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
&.Day__Container--rangeEnd:not(.Day__Container--rangeStart):not(.Day__Container--rowStart) {
|
||||||
|
border-left: 0;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
||||||
|
&.Day__Container--rowStart:not(.Day__Container--rangeEnd) {
|
||||||
|
border-right: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.Day__Container--rowEnd:not(.Day__Container--rangeStart) {
|
||||||
|
border-left: 0;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// implicit (defaultstate(not selected not selecting)) {
|
||||||
|
// implicit (defaultmode(not greyed)) {
|
||||||
|
|
||||||
|
.Day__Header {
|
||||||
|
background: colors.$day-header-defaultmode-defaultstate-background-color;
|
||||||
|
color: colors.$day-header-defaultmode-defaultstate-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Day__Number {
|
||||||
|
color: colors.$day-content-defaultmode-defaultstate-color;
|
||||||
|
}
|
||||||
|
// end implicit (defaultmode(not greyed)) {
|
||||||
|
|
||||||
|
&.Day__Container--greyed {
|
||||||
|
background: colors.$day-content-greyedmode-defaultstate-background-color;
|
||||||
|
|
||||||
|
.Day__Header {
|
||||||
|
background: colors.$day-header-greyedmode-defaultstate-background-color;
|
||||||
|
color: colors.$day-header-greyedmode-defaultstate-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Day__Number {
|
||||||
|
color: colors.$day-content-greyedmode-defaultstate-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// end implicit (defaultstate(not selected not selecting))
|
||||||
|
|
||||||
|
// implicit (midrange) {
|
||||||
|
&.Day__Container--selecting {
|
||||||
|
// implicit (defaultmode(not greyed)) {
|
||||||
|
background: colors.$day-content-defaultmode-selectingstate-midrange-background-color;
|
||||||
|
|
||||||
|
.Day__Header {
|
||||||
|
background: colors.$day-header-defaultmode-selectingstate-midrange-background-color;
|
||||||
|
color: colors.$day-header-defaultmode-selectingstate-midrange-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Day__Number {
|
||||||
|
color: colors.$day-content-defaultmode-selectingstate-midrange-color;
|
||||||
|
}
|
||||||
|
// end implicit (defaultmode(not greyed)) {
|
||||||
|
|
||||||
|
&.Day__Container--greyed {
|
||||||
|
background: colors.$day-content-greyedmode-selectingstate-midrange-background-color;
|
||||||
|
|
||||||
|
.Day__Header {
|
||||||
|
background: colors.$day-header-greyedmode-selectingstate-midrange-background-color;
|
||||||
|
color: colors.$day-header-greyedmode-selectingstate-midrange-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Day__Number {
|
||||||
|
color: colors.$day-content-greyedmode-selectingstate-midrange-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.Day__Container--selected {
|
||||||
|
// implicit (defaultmode(not greyed)) {
|
||||||
|
background: colors.$day-content-defaultmode-selectedstate-midrange-background-color;
|
||||||
|
|
||||||
|
.Day__Header {
|
||||||
|
background: colors.$day-header-defaultmode-selectedstate-midrange-background-color;
|
||||||
|
color: colors.$day-header-defaultmode-selectedstate-midrange-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Day__Number {
|
||||||
|
color: colors.$day-content-defaultmode-selectedstate-midrange-color;
|
||||||
|
}
|
||||||
|
// end implicit (defaultmode(not greyed)) {
|
||||||
|
|
||||||
|
&.Day__Container--greyed {
|
||||||
|
background: colors.$day-content-greyedmode-selectedstate-midrange-background-color;
|
||||||
|
|
||||||
|
.Day__Header {
|
||||||
|
background: colors.$day-header-greyedmode-selectedstate-midrange-background-color;
|
||||||
|
color: colors.$day-header-greyedmode-selectedstate-midrange-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Day__Number {
|
||||||
|
color: colors.$day-content-greyedmode-selectedstate-midrange-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// end implicit (midrange) {
|
||||||
|
|
||||||
|
// rangestart (selected|selecting)(greyed|defaultMode)
|
||||||
|
&.Day__Container--rangeStart:not(.Day__Container--rangeEnd) {
|
||||||
|
// implicit .Day__Container--selected {
|
||||||
|
// implicit (defaultmode(not greyed)) {
|
||||||
|
background: colors.$day-content-defaultmode-selectedstate-rangestart-background-color;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: colors.$day-content-defaultmode-selectedstate-rangestart-hover-background-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Day__Header {
|
||||||
|
background: colors.$day-header-defaultmode-selectedstate-rangestart-background-color;
|
||||||
|
color: colors.$day-header-defaultmode-selectedstate-rangestart-color;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: colors.$day-header-defaultmode-selectedstate-rangestart-hover-background-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Day__Number {
|
||||||
|
color: colors.$day-content-defaultmode-selectedstate-rangestart-color;
|
||||||
|
}
|
||||||
|
// end implicit (defaultmode(not greyed)) {
|
||||||
|
|
||||||
|
&.Day__Container--greyed {
|
||||||
|
background: colors.$day-content-greyedmode-selectedstate-rangestart-background-color;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: colors.$day-content-greyedmode-selectedstate-rangestart-hover-background-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Day__Header {
|
||||||
|
background: colors.$day-header-greyedmode-selectedstate-rangestart-background-color;
|
||||||
|
color: colors.$day-header-greyedmode-selectedstate-rangestart-color;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: colors.$day-header-greyedmode-selectedstate-rangestart-hover-background-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Day__Number {
|
||||||
|
color: colors.$day-content-greyedmode-selectedstate-rangestart-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// end implicit selected
|
||||||
|
|
||||||
|
// selecting
|
||||||
|
&.Day__Container--selecting {
|
||||||
|
// implicit (defaultmode(not greyed)) {
|
||||||
|
background: colors.$day-content-defaultmode-selectingstate-rangestart-background-color;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: colors.$day-content-defaultmode-selectingstate-rangestart-hover-background-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Day__Header {
|
||||||
|
background: colors.$day-header-defaultmode-selectingstate-rangestart-background-color;
|
||||||
|
color: colors.$day-header-defaultmode-selectingstate-rangestart-color;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: colors.$day-header-defaultmode-selectingstate-rangestart-hover-background-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Day__Number {
|
||||||
|
color: colors.$day-content-defaultmode-selectingstate-rangestart-color;
|
||||||
|
}
|
||||||
|
// end implicit (defaultmode(not greyed)) {
|
||||||
|
|
||||||
|
&.Day__Container--greyed {
|
||||||
|
background: colors.$day-content-greyedmode-selectingstate-rangestart-background-color;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: colors.$day-content-greyedmode-selectingstate-rangestart-hover-background-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Day__Header {
|
||||||
|
background: colors.$day-header-greyedmode-selectingstate-rangestart-background-color;
|
||||||
|
color: colors.$day-header-greyedmode-selectingstate-rangestart-color;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: colors.$day-header-greyedmode-selectingstate-rangestart-hover-background-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Day__Number {
|
||||||
|
color: colors.$day-content-greyedmode-selectingstate-rangestart-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RangeEnd (selected|selecting)(greyed|defaultMode)
|
||||||
|
&.Day__Container--rangeEnd:not(.Day__Container--rangeStart) {
|
||||||
|
// implicit .Day__Container--selected {
|
||||||
|
// implicit (defaultmode(not greyed)) {
|
||||||
|
background: colors.$day-content-defaultmode-selectedstate-rangeend-background-color;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: colors.$day-content-defaultmode-selectedstate-rangeend-hover-background-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Day__Header {
|
||||||
|
background: colors.$day-header-defaultmode-selectedstate-rangeend-background-color;
|
||||||
|
color: colors.$day-header-defaultmode-selectedstate-rangeend-color;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: colors.$day-header-defaultmode-selectedstate-rangeend-hover-background-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Day__Number {
|
||||||
|
color: colors.$day-content-defaultmode-selectedstate-rangeend-color;
|
||||||
|
}
|
||||||
|
// end implicit (defaultmode(not greyed)) {
|
||||||
|
|
||||||
|
&.Day__Container--greyed {
|
||||||
|
background: colors.$day-content-greyedmode-selectedstate-rangeend-background-color;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: colors.$day-content-greyedmode-selectedstate-rangeend-hover-background-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Day__Header {
|
||||||
|
background: colors.$day-header-greyedmode-selectedstate-rangeend-background-color;
|
||||||
|
color: colors.$day-header-greyedmode-selectedstate-rangeend-color;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: colors.$day-header-greyedmode-selectedstate-rangeend-hover-background-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Day__Number {
|
||||||
|
color: colors.$day-content-greyedmode-selectedstate-rangeend-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// end implicit selected
|
||||||
|
|
||||||
|
// selecting
|
||||||
|
&.Day__Container--selecting {
|
||||||
|
// implicit (defaultmode(not greyed)) {
|
||||||
|
background: colors.$day-content-defaultmode-selectingstate-rangeend-background-color;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: colors.$day-content-defaultmode-selectingstate-rangeend-hover-background-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Day__Header {
|
||||||
|
background: colors.$day-header-defaultmode-selectingstate-rangeend-background-color;
|
||||||
|
color: colors.$day-header-defaultmode-selectingstate-rangeend-color;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: colors.$day-header-defaultmode-selectingstate-rangeend-hover-background-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Day__Number {
|
||||||
|
color: colors.$day-content-defaultmode-selectingstate-rangeend-color;
|
||||||
|
}
|
||||||
|
// end implicit (defaultmode(not greyed)) {
|
||||||
|
|
||||||
|
&.Day__Container--greyed {
|
||||||
|
background: colors.$day-content-greyedmode-selectingstate-rangeend-background-color;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: colors.$day-content-greyedmode-selectingstate-rangeend-hover-background-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Day__Header {
|
||||||
|
background: colors.$day-header-greyedmode-selectingstate-rangeend-background-color;
|
||||||
|
color: colors.$day-header-greyedmode-selectingstate-rangeend-color;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: colors.$day-header-greyedmode-selectingstate-rangeend-hover-background-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Day__Number {
|
||||||
|
color: colors.$day-content-greyedmode-selectingstate-rangeend-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base case (no magnify)
|
||||||
|
.Day__Container:not(.magnify) {
|
||||||
|
&.Day__Container--rangeStart:not(.Day__Container--rangeEnd) .Day__Content,
|
||||||
|
&.Day__Container--selected:not(.Day__Container--rowEnd):not(.Day__Container--rangeEnd) .Day__Content,
|
||||||
|
&.Day__Container--selecting:not(.Day__Container--rowEnd):not(.Day__Container--rangeEnd) .Day__Content {
|
||||||
|
padding-right: v.$content-padding-base + v.$content-padding-border-compensation + shared.$week-wrapper-padding;
|
||||||
|
|
||||||
|
@media (min-width: shared.$breakpoint-tablet) {
|
||||||
|
padding-right: v.$content-padding-base + v.$content-padding-border-compensation + shared.$week-wrapper-padding-desktop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.Day__Container--rangeEnd:not(.Day__Container--rangeStart) .Day__Content,
|
||||||
|
&.Day__Container--selected:not(.Day__Container--rowStart):not(.Day__Container--rangeStart) .Day__Content,
|
||||||
|
&.Day__Container--selecting:not(.Day__Container--rowStart):not(.Day__Container--rangeStart) .Day__Content {
|
||||||
|
padding-left: v.$content-padding-base + v.$content-padding-border-compensation + shared.$week-wrapper-padding;
|
||||||
|
|
||||||
|
@media (min-width: shared.$breakpoint-tablet) {
|
||||||
|
padding-left: v.$content-padding-base + v.$content-padding-border-compensation + shared.$week-wrapper-padding-desktop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Magnify case
|
||||||
|
.Day__Container.magnify {
|
||||||
|
&.Day__Container--rangeStart:not(.Day__Container--rangeEnd) .Day__Content,
|
||||||
|
&.Day__Container--selected:not(.Day__Container--rowEnd):not(.Day__Container--rangeEnd) .Day__Content,
|
||||||
|
&.Day__Container--selecting:not(.Day__Container--rowEnd):not(.Day__Container--rangeEnd) .Day__Content {
|
||||||
|
padding-right: v.$content-padding-base + v.$content-padding-border-compensation;
|
||||||
|
}
|
||||||
|
&.Day__Container--rangeStart:not(.Day__Container--rangeEnd) .Day__Content,
|
||||||
|
&.Day__Container--selected.Day__Container--rowStart .Day__Content {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.Day__Container--rangeEnd:not(.Day__Container--rangeStart) .Day__Content,
|
||||||
|
&.Day__Container--selected:not(.Day__Container--rowStart):not(.Day__Container--rangeStart) .Day__Content,
|
||||||
|
&.Day__Container--selecting:not(.Day__Container--rowStart):not(.Day__Container--rangeStart) .Day__Content {
|
||||||
|
padding-left: v.$content-padding-base + v.$content-padding-border-compensation;
|
||||||
|
}
|
||||||
|
&.Day__Container--rangeEnd:not(.Day__Container--rangeStart) .Day__Content,
|
||||||
|
&.Day__Container--selected.Day__Container--rowEnd .Day__Content {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size variants
|
||||||
|
@each $size in ('xl', 'l', 'm', 's', 'xs') {
|
||||||
|
.Day--#{$size} {
|
||||||
|
.Day__Header {
|
||||||
|
height: v.get-size-value($size, 'header-height');
|
||||||
|
font-size: v.get-size-value($size, 'header-font');
|
||||||
|
|
||||||
|
@media (min-width: shared.$breakpoint-tablet) {
|
||||||
|
height: v.get-size-value($size, 'header-height-desktop');
|
||||||
|
font-size: v.get-size-value($size, 'header-font-desktop');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Day__Content {
|
||||||
|
height: v.get-size-value($size, 'header-height') * 2;
|
||||||
|
|
||||||
|
@media (min-width: shared.$breakpoint-tablet) {
|
||||||
|
height: v.get-size-value($size, 'header-height-desktop') * 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header style variants
|
||||||
|
.Day--expanded .Day__Header {
|
||||||
|
padding: 0 shared.$spacing-unit-quadruple;
|
||||||
|
.Day__HeaderText {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Day--compacted .Day__Header {
|
||||||
|
padding: 0 shared.$spacing-unit-double;
|
||||||
|
.Day__HeaderText {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Day--tiny .Day__Header {
|
||||||
|
padding: 0 shared.$spacing-unit;
|
||||||
|
.Day__HeaderText {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
@use '../shared/variables' as shared;
|
||||||
|
@use "sass:map";
|
||||||
|
|
||||||
|
// Content padding
|
||||||
|
$content-padding-base: shared.$spacing-unit-double;
|
||||||
|
$content-padding-border-compensation: shared.$border-size;
|
||||||
|
$content-padding-border-compensation-magnify: shared.$border-size * 0.5;
|
||||||
|
$content-padding-wrapper-compensation: shared.$week-wrapper-padding;
|
||||||
|
|
||||||
|
// Sizes
|
||||||
|
$size-xl: (
|
||||||
|
header-height: 48px,
|
||||||
|
header-height-desktop: 56px,
|
||||||
|
header-font: 1rem,
|
||||||
|
header-font-desktop: 1.1rem
|
||||||
|
);
|
||||||
|
|
||||||
|
$size-l: (
|
||||||
|
header-height: 36px,
|
||||||
|
header-height-desktop: 42px,
|
||||||
|
header-font: 0.9rem,
|
||||||
|
header-font-desktop: 1rem
|
||||||
|
);
|
||||||
|
|
||||||
|
$size-m: (
|
||||||
|
header-height: 32px,
|
||||||
|
header-height-desktop: 36px,
|
||||||
|
header-font: 0.8rem,
|
||||||
|
header-font-desktop: 0.9rem
|
||||||
|
);
|
||||||
|
|
||||||
|
$size-s: (
|
||||||
|
header-height: 28px,
|
||||||
|
header-height-desktop: 32px,
|
||||||
|
header-font: 0.75rem,
|
||||||
|
header-font-desktop: 0.8rem
|
||||||
|
);
|
||||||
|
|
||||||
|
$size-xs: (
|
||||||
|
header-height: 24px,
|
||||||
|
header-height-desktop: 28px,
|
||||||
|
header-font: 0.7rem,
|
||||||
|
header-font-desktop: 0.75rem
|
||||||
|
);
|
||||||
|
|
||||||
|
// Functions
|
||||||
|
@function get-size-value($size, $property) {
|
||||||
|
$size-map: null;
|
||||||
|
|
||||||
|
@if $size == 'xl' {
|
||||||
|
$size-map: $size-xl;
|
||||||
|
} @else if $size == 'l' {
|
||||||
|
$size-map: $size-l;
|
||||||
|
} @else if $size == 'm' {
|
||||||
|
$size-map: $size-m;
|
||||||
|
} @else if $size == 's' {
|
||||||
|
$size-map: $size-s;
|
||||||
|
} @else if $size == 'xs' {
|
||||||
|
$size-map: $size-xs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@return map.get($size-map, $property);
|
||||||
|
}
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
import React, { useCallback, KeyboardEvent } from 'react';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { getDayLabel } from '../utils';
|
||||||
|
import styles from './Day.module.scss';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { DayProps } from '../../../types/calendar';
|
||||||
|
import { DayNumber } from '../DayNumber';
|
||||||
|
|
||||||
|
export const Day: React.FC<DayProps> = ({
|
||||||
|
date,
|
||||||
|
headerStyle,
|
||||||
|
isOtherMonth,
|
||||||
|
variations = [],
|
||||||
|
onSelect,
|
||||||
|
onHover,
|
||||||
|
size = 'l',
|
||||||
|
fontProportion = 100,
|
||||||
|
magnify = false
|
||||||
|
}) => {
|
||||||
|
const isInteractive = Boolean(onSelect || onHover);
|
||||||
|
|
||||||
|
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||||
|
if (!onSelect) return;
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
onSelect();
|
||||||
|
}
|
||||||
|
}, [onSelect]);
|
||||||
|
|
||||||
|
const containerClasses = classNames(
|
||||||
|
'Day__Container',
|
||||||
|
styles.Day__Container,
|
||||||
|
{
|
||||||
|
[styles['Day__Container--interactive']]: isInteractive,
|
||||||
|
[styles['Day__Container--otherMonth']]: isOtherMonth,
|
||||||
|
[styles['Day__Container--selected']]: variations.includes('selected'),
|
||||||
|
[styles['Day__Container--rangeStart']]: variations.includes('rangeStart'),
|
||||||
|
[styles['Day__Container--rangeEnd']]: variations.includes('rangeEnd'),
|
||||||
|
[styles['Day__Container--selecting']]: variations.includes('selecting'),
|
||||||
|
[styles['Day__Container--greyed']]: variations.includes('greyed'),
|
||||||
|
[styles['Day__Container--rowStart']]: variations.includes('rowStart'),
|
||||||
|
[styles['Day__Container--rowEnd']]: variations.includes('rowEnd'),
|
||||||
|
[styles['magnify']]: magnify && (variations.includes('selected') || variations.includes('selecting'))
|
||||||
|
},
|
||||||
|
styles[`Day--${headerStyle}`],
|
||||||
|
styles[`Day--${size}`]
|
||||||
|
);
|
||||||
|
|
||||||
|
const headerClasses = classNames(
|
||||||
|
styles.Day__Header,
|
||||||
|
{
|
||||||
|
[styles['Day__Header--greyed']]: variations.includes('greyed')
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const interactiveProps = isInteractive ? {
|
||||||
|
onClick: onSelect,
|
||||||
|
onMouseEnter: onHover,
|
||||||
|
onKeyDown: handleKeyDown,
|
||||||
|
role: "button",
|
||||||
|
tabIndex: 0,
|
||||||
|
"aria-label": `Select ${format(date, 'PPP')}`
|
||||||
|
} : {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={containerClasses}
|
||||||
|
{...interactiveProps}
|
||||||
|
>
|
||||||
|
{headerStyle !== 'none' && (
|
||||||
|
<div className={headerClasses}>
|
||||||
|
<div className={styles.Day__HeaderText}>
|
||||||
|
{getDayLabel(date, headerStyle)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.Day__Content}>
|
||||||
|
<DayNumber
|
||||||
|
number={parseInt(format(date, 'd'), 10)}
|
||||||
|
proportion={fontProportion}
|
||||||
|
isGreyed={variations.includes('greyed')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
import { HeaderStyle, DayVariation } from '../../../types/calendar';
|
||||||
|
|
||||||
|
export interface DayContainerProps {
|
||||||
|
headerStyle: HeaderStyle;
|
||||||
|
isOtherMonth?: boolean;
|
||||||
|
variations?: DayVariation[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DayHeaderProps {
|
||||||
|
headerStyle: HeaderStyle;
|
||||||
|
variations?: DayVariation[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DayNumberProps {
|
||||||
|
headerStyle: HeaderStyle;
|
||||||
|
variations?: DayVariation[];
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
@use '../shared/colors' as shared;
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&.greyed {
|
||||||
|
.text {
|
||||||
|
fill: shared.$day-content-defaultmode-defaultstate-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
fill: shared.$day-content-defaultmode-defaultstate-color;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
font-feature-settings: 'tnum' on, 'lnum' on;
|
||||||
|
}
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
import React, { useRef, useEffect, useState } from 'react';
|
||||||
|
import styles from './DayNumber.module.scss';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
interface DayNumberProps {
|
||||||
|
number: number;
|
||||||
|
proportion: number;
|
||||||
|
isGreyed?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DayNumber: React.FC<DayNumberProps> = ({
|
||||||
|
number,
|
||||||
|
proportion,
|
||||||
|
isGreyed
|
||||||
|
}) => {
|
||||||
|
const textRef = useRef<SVGTextElement>(null);
|
||||||
|
const [scale, setScale] = useState({ x: 1, y: 1 });
|
||||||
|
const isSingleDigit = number < 10;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (textRef.current) {
|
||||||
|
// Get the natural dimensions of the text
|
||||||
|
const bbox = textRef.current.getBBox();
|
||||||
|
const naturalWidth = bbox.width;
|
||||||
|
const naturalHeight = bbox.height;
|
||||||
|
|
||||||
|
// Calculate desired width based on proportion and single/double digit
|
||||||
|
const containerWidth = 100; // viewBox width
|
||||||
|
const maxWidth = containerWidth * 0.8; // 80% of container width
|
||||||
|
const baseWidth = maxWidth * (isSingleDigit ? 0.5 : 1);
|
||||||
|
// Apply the proportion (0-100) to scale the base width
|
||||||
|
const desiredWidth = baseWidth * (proportion / 100);
|
||||||
|
|
||||||
|
// Calculate scale factors
|
||||||
|
const scaleX = desiredWidth / naturalWidth;
|
||||||
|
// Use the same scale for height to maintain proportion
|
||||||
|
const scaleY = scaleX;
|
||||||
|
|
||||||
|
setScale({ x: scaleX, y: scaleY });
|
||||||
|
|
||||||
|
// Debug log
|
||||||
|
console.log(`Number: ${number}, Proportion: ${proportion}, Scale: ${scaleX}`);
|
||||||
|
}
|
||||||
|
}, [number, proportion, isSingleDigit]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames(
|
||||||
|
styles.container,
|
||||||
|
{ [styles.greyed]: isGreyed }
|
||||||
|
)}>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
className={styles.svg}
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
|
>
|
||||||
|
<text
|
||||||
|
ref={textRef}
|
||||||
|
x="50"
|
||||||
|
y="50"
|
||||||
|
textAnchor="middle"
|
||||||
|
dominantBaseline="central"
|
||||||
|
className={styles.text}
|
||||||
|
transform={`matrix(${scale.x}, 0, 0, ${scale.y}, ${50 * (1 - scale.x)}, ${50 * (1 - scale.y)})`}
|
||||||
|
>
|
||||||
|
{number}
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,103 @@
|
|||||||
|
@use '../shared/variables' as shared;
|
||||||
|
@use '../shared/colors' as colors;
|
||||||
|
|
||||||
|
.Month {
|
||||||
|
width: 100%;
|
||||||
|
background: colors.$content-normal-bg; // replaced shared.$content-background-color
|
||||||
|
border-radius: shared.$border-radius-sm;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: shared.$shadow-month;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
@media (min-width: shared.$breakpoint-tablet) {
|
||||||
|
border-radius: shared.$border-radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__Container {
|
||||||
|
display: flex;
|
||||||
|
gap: shared.$spacing-unit;
|
||||||
|
background: colors.$content-normal-bg; // replaced shared.$content-background-color-selecting
|
||||||
|
padding: 0.75rem;
|
||||||
|
min-width: 0;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
@media (min-width: shared.$breakpoint-tablet) {
|
||||||
|
gap: shared.$spacing-unit-double;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--row {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--column {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__Title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
padding: shared.$spacing-unit-double;
|
||||||
|
margin: 0;
|
||||||
|
color: colors.$day-content-defaultmode-defaultstate-color; // replaced shared.$day-content-defaultmode-defaultstate-color
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
border-bottom: shared.$border-size solid colors.$border-color; // replaced shared.$border-color
|
||||||
|
background: colors.$content-normal-bg; // replaced shared.$content-background-color
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
@media (min-width: shared.$breakpoint-tablet) {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__WeeksContainer {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: shared.$spacing-unit;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
@media (min-width: shared.$breakpoint-tablet) {
|
||||||
|
gap: shared.$spacing-unit-double;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__DayHeadersRow {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||||
|
gap: shared.$spacing-unit;
|
||||||
|
margin-bottom: shared.$spacing-unit;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
@media (min-width: shared.$breakpoint-tablet) {
|
||||||
|
gap: shared.$spacing-unit-double;
|
||||||
|
margin-bottom: shared.$spacing-unit-double;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__DayHeader {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.4rem 0.1rem;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
background: transparent;
|
||||||
|
color: colors.$content-normal-text; // replaced shared.$header-color
|
||||||
|
border-bottom: shared.$spacing-unit solid colors.$header-normal-bg; // replaced shared.$header-background-color
|
||||||
|
|
||||||
|
&--weekend {
|
||||||
|
color: colors.$day-header-greyedmode-defaultstate-color; // replaced shared.$header-color-greyed
|
||||||
|
border-bottom-color: colors.$day-header-greyedmode-defaultstate-background-color; // replaced shared.$header-background-color-greyed
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: shared.$breakpoint-tablet) {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 0.5rem 0.1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { startOfMonth, startOfWeek, addDays, addWeeks, endOfMonth, format } from 'date-fns';
|
||||||
|
import { MonthProps, HeaderStyle } from '../../../types/calendar';
|
||||||
|
import { Week } from '../Week';
|
||||||
|
import styles from './Month.module.scss';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
export const Month: React.FC<MonthProps> = ({
|
||||||
|
date,
|
||||||
|
dayHeaderStyle,
|
||||||
|
monthDayOfWeekHeaderStyle,
|
||||||
|
direction,
|
||||||
|
monthCutoff,
|
||||||
|
weekendDays = [6, 0],
|
||||||
|
dateRange,
|
||||||
|
onDateSelect,
|
||||||
|
onDateHover,
|
||||||
|
size = 'l',
|
||||||
|
fontProportion = 100,
|
||||||
|
magnify = false
|
||||||
|
}) => {
|
||||||
|
const monthStart = startOfMonth(date);
|
||||||
|
const start = startOfWeek(monthStart, { weekStartsOn: 1 });
|
||||||
|
const end = endOfMonth(date);
|
||||||
|
const referenceMonth = date.getMonth();
|
||||||
|
const weeks: JSX.Element[] = [];
|
||||||
|
|
||||||
|
const effectiveMonthDayOfWeekHeaderStyle: HeaderStyle = monthDayOfWeekHeaderStyle ??
|
||||||
|
(dayHeaderStyle === 'none' ? 'compacted' : 'none');
|
||||||
|
|
||||||
|
let current = start;
|
||||||
|
|
||||||
|
while (current <= end) {
|
||||||
|
weeks.push(
|
||||||
|
<Week
|
||||||
|
key={current.toISOString()}
|
||||||
|
startDate={current}
|
||||||
|
headerStyle={dayHeaderStyle}
|
||||||
|
monthCutoff={monthCutoff}
|
||||||
|
referenceMonth={referenceMonth}
|
||||||
|
weekendDays={weekendDays}
|
||||||
|
dateRange={dateRange}
|
||||||
|
onDateSelect={onDateSelect}
|
||||||
|
onDateHover={onDateHover}
|
||||||
|
size={size}
|
||||||
|
fontProportion={fontProportion}
|
||||||
|
magnify={magnify}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
current = addWeeks(current, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.Month}>
|
||||||
|
<h2 className={styles.Month__Title}>{format(date, 'MMMM yyyy')}</h2>
|
||||||
|
<div className={classNames(styles.Month__Container, {
|
||||||
|
[styles['Month__Container--row']]: direction === 'row',
|
||||||
|
[styles['Month__Container--column']]: direction === 'column'
|
||||||
|
})}>
|
||||||
|
{effectiveMonthDayOfWeekHeaderStyle !== 'none' && (
|
||||||
|
<div className={styles.Month__DayHeadersRow}>
|
||||||
|
{Array.from({ length: 7 }, (_, i) => {
|
||||||
|
const dayDate = addDays(start, i);
|
||||||
|
const dayOfWeek = dayDate.getDay();
|
||||||
|
const isWeekend = weekendDays.includes(dayOfWeek);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={classNames(styles.Month__DayHeader, {
|
||||||
|
[styles['Month__DayHeader--weekend']]: isWeekend
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{format(dayDate, 'EEEEE')}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.Month__WeeksContainer}>
|
||||||
|
{weeks}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
@use '../shared/variables' as shared;
|
||||||
|
@use '../shared/colors' as colors;
|
||||||
|
|
||||||
|
.Week {
|
||||||
|
&__Container {
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
&__DayWrapper {
|
||||||
|
padding: shared.$week-wrapper-padding;
|
||||||
|
|
||||||
|
@media (min-width: shared.$breakpoint-tablet) {
|
||||||
|
padding: shared.$week-wrapper-padding-desktop;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--rangeStart:not(.Week__DayWrapper--rangeEnd),
|
||||||
|
&--selected.Week__DayWrapper--rowStart:not(.Week__DayWrapper--rowEnd),
|
||||||
|
&--selecting.Week__DayWrapper--rowStart:not(.Week__DayWrapper--rowEnd) {
|
||||||
|
padding: shared.$week-wrapper-padding 0 shared.$week-wrapper-padding shared.$week-wrapper-padding;
|
||||||
|
|
||||||
|
@media (min-width: shared.$breakpoint-tablet) {
|
||||||
|
padding: shared.$week-wrapper-padding-desktop 0 shared.$week-wrapper-padding-desktop shared.$week-wrapper-padding-desktop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--rangeEnd:not(.Week__DayWrapper--rangeStart),
|
||||||
|
&--selected.Week__DayWrapper--rowEnd:not(.Week__DayWrapper--rowStart),
|
||||||
|
&--selecting.Week__DayWrapper--rowEnd:not(.Week__DayWrapper--rowStart) {
|
||||||
|
padding: shared.$week-wrapper-padding shared.$week-wrapper-padding shared.$week-wrapper-padding 0;
|
||||||
|
|
||||||
|
@media (min-width: shared.$breakpoint-tablet) {
|
||||||
|
padding: shared.$week-wrapper-padding-desktop shared.$week-wrapper-padding-desktop shared.$week-wrapper-padding-desktop 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--selected:not(&--rangeStart):not(&--rangeEnd):not(&--rowStart):not(&--rowEnd),
|
||||||
|
&--selecting:not(&--rangeStart):not(&--rangeEnd):not(&--rowStart):not(&--rowEnd) {
|
||||||
|
padding: shared.$week-wrapper-padding 0;
|
||||||
|
|
||||||
|
@media (min-width: shared.$breakpoint-tablet) {
|
||||||
|
padding: shared.$week-wrapper-padding-desktop 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__EmptyCell {
|
||||||
|
padding: shared.$week-wrapper-padding;
|
||||||
|
background: colors.$content-normal-bg; // replaced shared.$content-background-color
|
||||||
|
|
||||||
|
@media (min-width: shared.$breakpoint-tablet) {
|
||||||
|
padding: shared.$week-wrapper-padding-desktop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,99 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { addDays } from 'date-fns';
|
||||||
|
import { WeekProps } from '../../../types/calendar';
|
||||||
|
import { Day } from '../Day';
|
||||||
|
import { getDateVariations } from '../../../utils/dateUtils';
|
||||||
|
import styles from './Week.module.scss';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
export const Week: React.FC<WeekProps> = ({
|
||||||
|
startDate,
|
||||||
|
headerStyle,
|
||||||
|
monthCutoff,
|
||||||
|
referenceMonth,
|
||||||
|
weekendDays = [6, 0],
|
||||||
|
dateRange,
|
||||||
|
onDateSelect,
|
||||||
|
onDateHover,
|
||||||
|
size = 'l',
|
||||||
|
fontProportion = 100,
|
||||||
|
magnify = false
|
||||||
|
}) => {
|
||||||
|
const allDays = Array.from({ length: 7 }, (_, i) => {
|
||||||
|
const date = addDays(startDate, i);
|
||||||
|
const isOtherMonth = date.getMonth() !== referenceMonth;
|
||||||
|
const variations = getDateVariations(date, dateRange, weekendDays, i);
|
||||||
|
|
||||||
|
const wrapperClasses = classNames(
|
||||||
|
styles.Week__DayWrapper,
|
||||||
|
{
|
||||||
|
[styles['Week__DayWrapper--rangeStart']]: variations.includes('rangeStart'),
|
||||||
|
[styles['Week__DayWrapper--rangeEnd']]: variations.includes('rangeEnd'),
|
||||||
|
[styles['Week__DayWrapper--selected']]: variations.includes('selected'),
|
||||||
|
[styles['Week__DayWrapper--selecting']]: variations.includes('selecting'),
|
||||||
|
[styles['Week__DayWrapper--rowStart']]: variations.includes('rowStart'),
|
||||||
|
[styles['Week__DayWrapper--rowEnd']]: variations.includes('rowEnd')
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (monthCutoff && isOtherMonth) {
|
||||||
|
if (monthCutoff === 'truncate') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={wrapperClasses} key={i}>
|
||||||
|
<Day
|
||||||
|
date={date}
|
||||||
|
headerStyle={headerStyle}
|
||||||
|
isOtherMonth={true}
|
||||||
|
variations={variations}
|
||||||
|
onSelect={onDateSelect ? () => onDateSelect(date) : undefined}
|
||||||
|
onHover={onDateHover ? () => onDateHover(date) : undefined}
|
||||||
|
size={size}
|
||||||
|
fontProportion={fontProportion}
|
||||||
|
magnify={magnify}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={wrapperClasses} key={i}>
|
||||||
|
<Day
|
||||||
|
date={date}
|
||||||
|
headerStyle={headerStyle}
|
||||||
|
variations={variations}
|
||||||
|
onSelect={onDateSelect ? () => onDateSelect(date) : undefined}
|
||||||
|
onHover={onDateHover ? () => onDateHover(date) : undefined}
|
||||||
|
size={size}
|
||||||
|
fontProportion={fontProportion}
|
||||||
|
magnify={magnify}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const firstValidIndex = allDays.findIndex(Boolean);
|
||||||
|
const lastValidIndex = allDays.length - [...allDays].reverse().findIndex(Boolean) - 1;
|
||||||
|
|
||||||
|
if (monthCutoff === 'truncate') {
|
||||||
|
const truncatedDays = Array.from({ length: 7 }, (_, i) => {
|
||||||
|
if (i < firstValidIndex || i > lastValidIndex) {
|
||||||
|
return <div key={i} className={styles.Week__EmptyCell} />;
|
||||||
|
}
|
||||||
|
return allDays[i];
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.Week__Container}>
|
||||||
|
{truncatedDays}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.Week__Container}>
|
||||||
|
{allDays}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
import { DayViewType, DayVariation } from '../../../types/calendar';
|
||||||
|
|
||||||
|
export interface WeekContainerProps {
|
||||||
|
viewType: DayViewType;
|
||||||
|
isOtherMonth?: boolean;
|
||||||
|
variations?: DayVariation[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DayHeaderProps {
|
||||||
|
viewType: DayViewType;
|
||||||
|
variations?: DayVariation[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DayNumberProps {
|
||||||
|
viewType: DayViewType;
|
||||||
|
variations?: DayVariation[];
|
||||||
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
@use '../shared/variables' as shared;
|
||||||
|
@use '../shared/colors' as colors;
|
||||||
|
|
||||||
|
.Year {
|
||||||
|
&__Wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: shared.$spacing-unit-quadruple;
|
||||||
|
padding: shared.$spacing-unit-double;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
@media (min-width: shared.$breakpoint-tablet) {
|
||||||
|
padding: shared.$spacing-unit-quadruple;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__Title {
|
||||||
|
font-size: 2rem;
|
||||||
|
color: colors.$content-normal-text; // replaced shared.$content-color
|
||||||
|
margin: 0 0 shared.$spacing-unit-quadruple;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__MonthsGrid {
|
||||||
|
display: grid;
|
||||||
|
gap: shared.$spacing-unit-quadruple;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
@media (min-width: shared.$breakpoint-tablet) {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { YearProps } from '../../../types/calendar';
|
||||||
|
import { Month } from '../Month';
|
||||||
|
import styles from './Year.module.scss';
|
||||||
|
|
||||||
|
export const Year: React.FC<YearProps> = ({
|
||||||
|
year,
|
||||||
|
dayHeaderStyle,
|
||||||
|
monthDayOfWeekHeaderStyle,
|
||||||
|
monthCutoff,
|
||||||
|
weekendDays,
|
||||||
|
dateRange,
|
||||||
|
onDateSelect,
|
||||||
|
onDateHover,
|
||||||
|
size = 'l',
|
||||||
|
fontProportion = 100,
|
||||||
|
magnify = false
|
||||||
|
}) => {
|
||||||
|
const months = Array.from({ length: 12 }, (_, index) => {
|
||||||
|
const monthDate = new Date(year, index, 1);
|
||||||
|
return (
|
||||||
|
<Month
|
||||||
|
key={index}
|
||||||
|
date={monthDate}
|
||||||
|
dayHeaderStyle={dayHeaderStyle}
|
||||||
|
monthDayOfWeekHeaderStyle={monthDayOfWeekHeaderStyle}
|
||||||
|
direction="column"
|
||||||
|
monthCutoff={monthCutoff}
|
||||||
|
weekendDays={weekendDays}
|
||||||
|
dateRange={dateRange}
|
||||||
|
onDateSelect={onDateSelect}
|
||||||
|
onDateHover={onDateHover}
|
||||||
|
size={size}
|
||||||
|
fontProportion={fontProportion}
|
||||||
|
magnify={magnify}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.Year__Wrapper}>
|
||||||
|
<h1 className={styles.Year__Title}>{year}</h1>
|
||||||
|
<div className={styles.Year__MonthsGrid}>
|
||||||
|
{months}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,189 @@
|
|||||||
|
@use "sass:color";
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// BORDER COLORS
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
$border-color: #e5e5e5;
|
||||||
|
$border-color-selecting: #d1d1d1;
|
||||||
|
$border-color-in-range: $border-color;
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// FOCUS COLOR
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
$focus-blue: #2196f3;
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// HEADER BASE COLORS
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
$header-normal-bg: #2c2c2c; // Anthracite
|
||||||
|
$header-normal-text: #ffffff;
|
||||||
|
|
||||||
|
$header-selected-bg: #f321c9; // Blue
|
||||||
|
$header-selected-text: #ffffff;
|
||||||
|
|
||||||
|
$header-selecting-bg: #404040; // Lighter anthracite
|
||||||
|
$header-selecting-text: #ffffff;
|
||||||
|
|
||||||
|
// Hover variants for header
|
||||||
|
$header-normal-hover-bg: #383838;
|
||||||
|
$header-selected-hover-bg: #1976d2;
|
||||||
|
$header-selecting-hover-bg: #4a4a4a;
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// CONTENT BASE COLORS
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
$content-normal-bg: #ffffff;
|
||||||
|
$content-normal-text: #333333;
|
||||||
|
|
||||||
|
$content-selected-bg: #2196f3;
|
||||||
|
$content-selected-text: #ffffff;
|
||||||
|
|
||||||
|
$content-selecting-bg: #f5f5f5;
|
||||||
|
$content-selecting-text: #333333;
|
||||||
|
|
||||||
|
// Hover variants for content
|
||||||
|
$content-normal-hover-bg: #f8f8f8;
|
||||||
|
$content-selected-hover-bg: #1976d2;
|
||||||
|
$content-selecting-hover-bg: #eeeeee;
|
||||||
|
|
||||||
|
$content-selecting-bg: color.mix($content-normal-bg, $content-selected-bg, 50%);
|
||||||
|
$content-selecting-text: color.mix($content-normal-text, $content-selected-text, 50%);
|
||||||
|
|
||||||
|
// Hover variants for content.
|
||||||
|
$content-normal-hover-bg: color.adjust($content-normal-bg, $lightness: -5%);
|
||||||
|
$content-selected-hover-bg: color.adjust($content-selected-bg, $lightness: 5%);
|
||||||
|
$content-selecting-hover-bg: color.adjust($content-selecting-bg, $lightness: -5%);
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
|
||||||
|
// Colors
|
||||||
|
$day-color-primary: #2196f3; // day default mode selected sate container
|
||||||
|
$day-color-primary-dark: #1976d2; // day default mode selected sate header
|
||||||
|
$day-color-background: white; // day default mode default state container background
|
||||||
|
$day-color-border: #f0f0f0; // any day container border
|
||||||
|
$day-color-text: #2c2c2c; // day default mode default state container text color
|
||||||
|
$day-color-text-light: #757575; // day greyed mode default state text color
|
||||||
|
$day-color-hover: #f5f5f5; // day default mode default state hover container
|
||||||
|
$day-color-greyed: #bcbcbc; // day greyed mode default state container
|
||||||
|
|
||||||
|
// Range colors
|
||||||
|
$day-color-selected: #e3f2fd; // day default mode selected state container
|
||||||
|
$day-color-selected-hover: #bbdefb; // day default mode selected state hover container
|
||||||
|
$day-color-selecting: #f5f5f5; // day default mode selecting state container
|
||||||
|
$day-color-selecting-border: #e0e0e0; // day default|greyed mode selecting container border
|
||||||
|
$day-color-selecting-hover: #eeeeee;
|
||||||
|
|
||||||
|
// Header colors
|
||||||
|
$day-color-header: #2c2c2c; // day default mode default state header
|
||||||
|
$day-color-header-text: white; // day default mode default state header text
|
||||||
|
$day-color-header-greyed: #acacac; // day greyed mode default state header
|
||||||
|
$day-color-header-greyed-text: #101010; // day default mode default state header text
|
||||||
|
|
||||||
|
// Base Colors
|
||||||
|
$day-border-color: $day-color-border;
|
||||||
|
$day-header-defaultmode-defaultstate-background-color: $day-color-header;
|
||||||
|
$day-header-defaultmode-defaultstate-color: $day-color-header-text;
|
||||||
|
|
||||||
|
$day-content-defaultmode-defaultstate-background-color: $day-color-background;
|
||||||
|
$day-content-defaultmode-defaultstate-color: $day-color-text;
|
||||||
|
|
||||||
|
$day-content-greyedmode-defaultstate-background-color: color.adjust($day-color-greyed, $lightness: 25%); // Much lighter grey
|
||||||
|
$day-content-greyedmode-defaultstate-color: $day-color-text-light;
|
||||||
|
|
||||||
|
$day-header-greyedmode-defaultstate-background-color: $day-color-header-greyed;
|
||||||
|
$day-header-greyedmode-defaultstate-color: $day-color-header-greyed-text;
|
||||||
|
|
||||||
|
// Selecting State Colors
|
||||||
|
$day-content-defaultmode-selectingstate-midrange-background-color: $day-color-selecting;
|
||||||
|
$day-content-defaultmode-selectingstate-midrange-color: $day-color-text;
|
||||||
|
$day-content-defaultmode-selectingstate-midrange-hover-background-color: $day-color-selecting-hover;
|
||||||
|
|
||||||
|
$day-content-greyedmode-selectingstate-midrange-background-color: color.mix($day-color-selecting, $day-color-greyed, 80%);
|
||||||
|
$day-content-greyedmode-selectingstate-midrange-color: $day-color-text-light;
|
||||||
|
|
||||||
|
$day-header-defaultmode-selectingstate-midrange-background-color: $day-color-header;
|
||||||
|
$day-header-defaultmode-selectingstate-midrange-color: $day-color-header-text;
|
||||||
|
|
||||||
|
$day-header-greyedmode-selectingstate-midrange-background-color: color.mix($day-color-header-greyed, $day-color-selecting, 80%);
|
||||||
|
$day-header-greyedmode-selectingstate-midrange-color: $day-color-header-greyed-text;
|
||||||
|
|
||||||
|
// Selected State Colors
|
||||||
|
$day-content-defaultmode-selectedstate-midrange-background-color: $day-color-selected;
|
||||||
|
$day-content-defaultmode-selectedstate-midrange-color: $day-color-text;
|
||||||
|
|
||||||
|
$day-content-greyedmode-selectedstate-midrange-background-color: color.mix($day-color-selected, $day-color-greyed, 80%);
|
||||||
|
$day-content-greyedmode-selectedstate-midrange-color: $day-color-text-light;
|
||||||
|
|
||||||
|
$day-header-defaultmode-selectedstate-midrange-background-color: $day-color-primary-dark;
|
||||||
|
$day-header-defaultmode-selectedstate-midrange-color: $day-color-header-text;
|
||||||
|
|
||||||
|
$day-header-greyedmode-selectedstate-midrange-background-color: color.mix($day-color-primary-dark, $day-color-greyed, 80%);
|
||||||
|
$day-header-greyedmode-selectedstate-midrange-color: $day-color-header-text; // Changed to white for visibility
|
||||||
|
|
||||||
|
// Range Start Colors
|
||||||
|
$day-content-defaultmode-selectedstate-rangestart-background-color: $day-color-primary;
|
||||||
|
$day-content-defaultmode-selectedstate-rangestart-color: $day-color-header-text; // White text
|
||||||
|
$day-content-defaultmode-selectedstate-rangestart-hover-background-color: $day-color-selected-hover;
|
||||||
|
|
||||||
|
$day-content-greyedmode-selectedstate-rangestart-background-color: color.mix($day-color-primary, $day-color-greyed, 80%);
|
||||||
|
$day-content-greyedmode-selectedstate-rangestart-color: $day-color-header-text; // White text
|
||||||
|
$day-content-greyedmode-selectedstate-rangestart-hover-background-color: color.mix($day-color-selected-hover, $day-color-greyed, 80%);
|
||||||
|
|
||||||
|
$day-header-defaultmode-selectedstate-rangestart-background-color: $day-color-primary-dark;
|
||||||
|
$day-header-defaultmode-selectedstate-rangestart-color: $day-color-header-text;
|
||||||
|
$day-header-defaultmode-selectedstate-rangestart-hover-background-color: color.adjust($day-color-primary-dark, $lightness: 10%);
|
||||||
|
|
||||||
|
$day-header-greyedmode-selectedstate-rangestart-background-color: color.mix($day-color-primary-dark, $day-color-greyed, 80%);
|
||||||
|
$day-header-greyedmode-selectedstate-rangestart-color: $day-color-header-text; // White text
|
||||||
|
$day-header-greyedmode-selectedstate-rangestart-hover-background-color: color.adjust(color.mix($day-color-primary-dark, $day-color-greyed, 80%), $lightness: 10%);
|
||||||
|
|
||||||
|
// Range End Colors
|
||||||
|
$day-content-defaultmode-selectedstate-rangeend-background-color: $day-color-primary;
|
||||||
|
$day-content-defaultmode-selectedstate-rangeend-color: $day-color-header-text; // White text
|
||||||
|
$day-content-defaultmode-selectedstate-rangeend-hover-background-color: $day-color-selected-hover;
|
||||||
|
|
||||||
|
$day-content-greyedmode-selectedstate-rangeend-background-color: color.mix($day-color-primary, $day-color-greyed, 80%);
|
||||||
|
$day-content-greyedmode-selectedstate-rangeend-color: $day-color-header-text; // White text
|
||||||
|
$day-content-greyedmode-selectedstate-rangeend-hover-background-color: color.mix($day-color-selected-hover, $day-color-greyed, 80%);
|
||||||
|
|
||||||
|
$day-header-defaultmode-selectedstate-rangeend-background-color: $day-color-primary-dark;
|
||||||
|
$day-header-defaultmode-selectedstate-rangeend-color: $day-color-header-text;
|
||||||
|
$day-header-defaultmode-selectedstate-rangeend-hover-background-color: color.adjust($day-color-primary-dark, $lightness: 10%);
|
||||||
|
|
||||||
|
$day-header-greyedmode-selectedstate-rangeend-background-color: color.mix($day-color-primary-dark, $day-color-greyed, 80%);
|
||||||
|
$day-header-greyedmode-selectedstate-rangeend-color: $day-color-header-text; // White text
|
||||||
|
$day-header-greyedmode-selectedstate-rangeend-hover-background-color: color.adjust(color.mix($day-color-primary-dark, $day-color-greyed, 80%), $lightness: 10%);
|
||||||
|
|
||||||
|
// Selecting State for Range
|
||||||
|
$day-content-defaultmode-selectingstate-rangestart-background-color: $day-color-selecting;
|
||||||
|
$day-content-defaultmode-selectingstate-rangestart-color: $day-color-text;
|
||||||
|
$day-content-defaultmode-selectingstate-rangestart-hover-background-color: $day-color-selecting-hover;
|
||||||
|
|
||||||
|
$day-content-greyedmode-selectingstate-rangestart-background-color: color.mix($day-color-selecting, $day-color-greyed, 80%);
|
||||||
|
$day-content-greyedmode-selectingstate-rangestart-color: $day-color-text-light;
|
||||||
|
$day-content-greyedmode-selectingstate-rangestart-hover-background-color: color.mix($day-color-selecting-hover, $day-color-greyed, 80%);
|
||||||
|
|
||||||
|
$day-header-defaultmode-selectingstate-rangestart-background-color: $day-color-header;
|
||||||
|
$day-header-defaultmode-selectingstate-rangestart-color: $day-color-header-text;
|
||||||
|
$day-header-defaultmode-selectingstate-rangestart-hover-background-color: color.adjust($day-color-header, $lightness: 10%);
|
||||||
|
|
||||||
|
$day-header-greyedmode-selectingstate-rangestart-background-color: color.mix($day-color-header-greyed, $day-color-selecting, 80%);
|
||||||
|
$day-header-greyedmode-selectingstate-rangestart-color: $day-color-header-greyed-text;
|
||||||
|
$day-header-greyedmode-selectingstate-rangestart-hover-background-color: color.adjust(color.mix($day-color-header-greyed, $day-color-selecting, 80%), $lightness: 10%);
|
||||||
|
|
||||||
|
// Selecting Range End
|
||||||
|
$day-content-defaultmode-selectingstate-rangeend-background-color: $day-color-selecting;
|
||||||
|
$day-content-defaultmode-selectingstate-rangeend-color: $day-color-text;
|
||||||
|
$day-content-defaultmode-selectingstate-rangeend-hover-background-color: $day-color-selecting-hover;
|
||||||
|
|
||||||
|
$day-content-greyedmode-selectingstate-rangeend-background-color: color.mix($day-color-selecting, $day-color-greyed, 80%);
|
||||||
|
$day-content-greyedmode-selectingstate-rangeend-color: $day-color-text-light;
|
||||||
|
$day-content-greyedmode-selectingstate-rangeend-hover-background-color: color.mix($day-color-selecting-hover, $day-color-greyed, 80%);
|
||||||
|
|
||||||
|
$day-header-defaultmode-selectingstate-rangeend-background-color: $day-color-header;
|
||||||
|
$day-header-defaultmode-selectingstate-rangeend-color: $day-color-header-text;
|
||||||
|
$day-header-defaultmode-selectingstate-rangeend-hover-background-color: color.adjust($day-color-header, $lightness: 10%);
|
||||||
|
|
||||||
|
$day-header-greyedmode-selectingstate-rangeend-background-color: color.mix($day-color-header-greyed, $day-color-selecting, 80%);
|
||||||
|
$day-header-greyedmode-selectingstate-rangeend-color: $day-color-header-greyed-text;
|
||||||
|
$day-header-greyedmode-selectingstate-rangeend-hover-background-color: color.adjust(color.mix($day-color-header-greyed, $day-color-selecting, 80%), $lightness: 10%);
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
// Base units
|
||||||
|
$base-unit: 2px;
|
||||||
|
$border-size: 1px;
|
||||||
|
|
||||||
|
// Spacing derived from base unit
|
||||||
|
$spacing-unit: $base-unit;
|
||||||
|
$spacing-unit-double: $spacing-unit * 2;
|
||||||
|
$spacing-unit-quadruple: $spacing-unit * 4;
|
||||||
|
|
||||||
|
// Week wrapper padding
|
||||||
|
$week-wrapper-padding: $spacing-unit;
|
||||||
|
$week-wrapper-padding-desktop: $spacing-unit-double;
|
||||||
|
|
||||||
|
// Border radiuses
|
||||||
|
$border-radius-sm: 6px;
|
||||||
|
$border-radius: 8px;
|
||||||
|
|
||||||
|
// Shadows (importing the new focus color from colors.scss)
|
||||||
|
@use "sass:color";
|
||||||
|
@use "colors" as colors;
|
||||||
|
$shadow-hover: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
$shadow-month: 0 2px 4px rgba(0, 0, 0, 0.15);
|
||||||
|
$shadow-focus: 0 0 0 2px colors.$focus-blue;
|
||||||
|
|
||||||
|
// Breakpoints
|
||||||
|
$breakpoint-tablet: 768px;
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
import { HeaderStyle } from '../../types/calendar';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
|
||||||
|
export const getHeightByHeaderStyle = (headerStyle: HeaderStyle) => {
|
||||||
|
const baseHeights: Record<HeaderStyle, [string, string]> = {
|
||||||
|
expanded: ['100px', '140px'],
|
||||||
|
compacted: ['80px', '100px'],
|
||||||
|
tiny: ['60px', '80px'],
|
||||||
|
none: ['50px', '60px']
|
||||||
|
};
|
||||||
|
|
||||||
|
const [mobile, desktop] = baseHeights[headerStyle];
|
||||||
|
return `
|
||||||
|
height: ${mobile};
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
height: ${desktop};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMinWidthByHeaderStyle = (headerStyle: HeaderStyle) => {
|
||||||
|
const baseWidths: Record<HeaderStyle, string> = {
|
||||||
|
expanded: '120px',
|
||||||
|
compacted: '90px',
|
||||||
|
tiny: '60px',
|
||||||
|
none: '60px'
|
||||||
|
};
|
||||||
|
|
||||||
|
return `min-width: ${baseWidths[headerStyle]};`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDayLabel = (date: Date, headerStyle: HeaderStyle): string => {
|
||||||
|
const day = format(date, 'EEEE');
|
||||||
|
switch (headerStyle) {
|
||||||
|
case 'expanded':
|
||||||
|
return day.toUpperCase();
|
||||||
|
case 'compacted':
|
||||||
|
return day.slice(0, 3).toUpperCase();
|
||||||
|
case 'tiny':
|
||||||
|
if (day === 'Thursday') return 'T';
|
||||||
|
if (day === 'Tuesday') return 't';
|
||||||
|
if (day === 'Saturday') return 's';
|
||||||
|
if (day === 'Sunday') return 'S';
|
||||||
|
if (day === 'Monday') return 'M';
|
||||||
|
if (day === 'Wednesday') return 'W';
|
||||||
|
if (day === 'Friday') return 'F';
|
||||||
|
return day[0];
|
||||||
|
default:
|
||||||
|
return day;
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
.button {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #2c2c2c;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
// transition: all 0.2s ease;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: #2c2c2c;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #2c2c2c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
padding: 0.75rem 1.25rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styles from './Button.module.scss';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
|
active?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Button: React.FC<ButtonProps> = ({
|
||||||
|
children,
|
||||||
|
active,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<button
|
||||||
|
className={classNames(styles.button, { [styles.active]: active }, className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
.buttonGroup {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
background: white;
|
||||||
|
padding: 0.25rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styles from './ButtonGroup.module.scss';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
interface ButtonGroupProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ButtonGroup: React.FC<ButtonGroupProps> = ({ children, className }) => (
|
||||||
|
<div className={classNames(styles.buttonGroup, className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
@ -0,0 +1,106 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
width: auto;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.labelContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #2c2c2c;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #666;
|
||||||
|
font-weight: 500;
|
||||||
|
font-feature-settings: 'tnum' on, 'lnum' on;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.range {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 20px; // Increased height to center the thumb
|
||||||
|
margin: 0;
|
||||||
|
background: transparent;
|
||||||
|
outline: none;
|
||||||
|
appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
// Track styles
|
||||||
|
&::-webkit-slider-runnable-track {
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background: #e0e0e0;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-moz-range-track {
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background: #e0e0e0;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thumb styles
|
||||||
|
&::-webkit-slider-thumb {
|
||||||
|
appearance: none;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #2c2c2c;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: -7px; // Centers the thumb on the track: (thumb height - track height) / -2
|
||||||
|
transition: background 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #404040;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-moz-range-thumb {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #2c2c2c;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #404040;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus styles
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
&::-webkit-slider-thumb {
|
||||||
|
box-shadow: 0 0 0 2px rgba(44, 44, 44, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-moz-range-thumb {
|
||||||
|
box-shadow: 0 0 0 2px rgba(44, 44, 44, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styles from './RangeInput.module.scss';
|
||||||
|
|
||||||
|
interface RangeInputProps {
|
||||||
|
value: number;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
step: number;
|
||||||
|
onChange: (value: number) => void;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RangeInput: React.FC<RangeInputProps> = ({
|
||||||
|
value,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
step,
|
||||||
|
onChange,
|
||||||
|
label
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.labelContainer}>
|
||||||
|
<label className={styles.label}>{label}</label>
|
||||||
|
<span className={styles.value}>{value}%</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
step={step}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(Number(e.target.value))}
|
||||||
|
className={styles.range}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Year } from '../components/calendar/Year';
|
||||||
|
import { Controls } from '../components/calendar/Controls';
|
||||||
|
import { HeaderStyle, MonthCutoffType } from '../types/calendar';
|
||||||
|
import styles from './Examples.module.scss';
|
||||||
|
|
||||||
|
export const CompactYear: React.FC = () => {
|
||||||
|
const [dayHeaderStyle, setDayHeaderStyle] = useState<HeaderStyle>('tiny');
|
||||||
|
const [monthCutoff, setMonthCutoff] = useState<MonthCutoffType>('truncate');
|
||||||
|
const [fontProportion, setFontProportion] = useState<number>(100);
|
||||||
|
const [magnify, setMagnify] = useState<boolean>(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={styles.section}>
|
||||||
|
<h2 className={styles.sectionTitle}>Compact Year View</h2>
|
||||||
|
<p className={styles.sectionDescription}>
|
||||||
|
Full year view optimized for compact display.
|
||||||
|
</p>
|
||||||
|
<Controls
|
||||||
|
headerStyle={dayHeaderStyle}
|
||||||
|
monthCutoff={monthCutoff}
|
||||||
|
magnify={magnify}
|
||||||
|
size="xs"
|
||||||
|
fontProportion={fontProportion}
|
||||||
|
onHeaderStyleChange={setDayHeaderStyle}
|
||||||
|
onMonthCutoffChange={setMonthCutoff}
|
||||||
|
onMagnifyChange={setMagnify}
|
||||||
|
onSizeChange={() => {}}
|
||||||
|
onFontProportionChange={setFontProportion}
|
||||||
|
/>
|
||||||
|
<div className={styles.demoContainer}>
|
||||||
|
<Year
|
||||||
|
year={2025}
|
||||||
|
dayHeaderStyle={dayHeaderStyle}
|
||||||
|
monthCutoff={monthCutoff}
|
||||||
|
weekendDays={[6, 0]}
|
||||||
|
size="xs"
|
||||||
|
fontProportion={fontProportion}
|
||||||
|
magnify={magnify}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,131 @@
|
|||||||
|
import React, { useState, useCallback } from 'react';
|
||||||
|
import { format, isBefore, isEqual } from 'date-fns';
|
||||||
|
import { Year } from '../components/calendar/Year';
|
||||||
|
import { Controls } from '../components/calendar/Controls';
|
||||||
|
import { HeaderStyle, MonthCutoffType, DaySize, DateRange } from '../types/calendar';
|
||||||
|
import styles from './Examples.module.scss';
|
||||||
|
|
||||||
|
export const DateRangePicker: React.FC = () => {
|
||||||
|
const [dateRange, setDateRange] = useState<DateRange>({
|
||||||
|
startDate: null,
|
||||||
|
endDate: null,
|
||||||
|
selecting: false,
|
||||||
|
hoverDate: null,
|
||||||
|
anchorDate: null
|
||||||
|
});
|
||||||
|
|
||||||
|
const [dayHeaderStyle, setDayHeaderStyle] = useState<HeaderStyle>('tiny');
|
||||||
|
const [monthCutoff, setMonthCutoff] = useState<MonthCutoffType>('truncate');
|
||||||
|
const [size, setSize] = useState<DaySize>('l');
|
||||||
|
const [fontProportion, setFontProportion] = useState<number>(100);
|
||||||
|
const [magnify, setMagnify] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const handleDateSelect = useCallback((date: Date) => {
|
||||||
|
setDateRange(prev => {
|
||||||
|
if (!prev.selecting || !prev.anchorDate) {
|
||||||
|
return {
|
||||||
|
startDate: date,
|
||||||
|
endDate: date,
|
||||||
|
selecting: true,
|
||||||
|
hoverDate: date,
|
||||||
|
anchorDate: date
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEqual(date, prev.anchorDate)) {
|
||||||
|
return {
|
||||||
|
startDate: date,
|
||||||
|
endDate: date,
|
||||||
|
selecting: false,
|
||||||
|
hoverDate: null,
|
||||||
|
anchorDate: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const [start, end] = isBefore(date, prev.anchorDate)
|
||||||
|
? [date, prev.anchorDate]
|
||||||
|
: [prev.anchorDate, date];
|
||||||
|
|
||||||
|
return {
|
||||||
|
startDate: start,
|
||||||
|
endDate: end,
|
||||||
|
selecting: false,
|
||||||
|
hoverDate: null,
|
||||||
|
anchorDate: null
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleDateHover = useCallback((date: Date) => {
|
||||||
|
setDateRange(prev => {
|
||||||
|
if (prev.selecting && prev.anchorDate) {
|
||||||
|
if (isEqual(date, prev.anchorDate)) {
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
startDate: date,
|
||||||
|
endDate: date,
|
||||||
|
hoverDate: date
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const [start, end] = isBefore(date, prev.anchorDate)
|
||||||
|
? [date, prev.anchorDate]
|
||||||
|
: [prev.anchorDate, date];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
startDate: start,
|
||||||
|
endDate: end,
|
||||||
|
hoverDate: date
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const formatDateOrNull = (date: Date | null) => {
|
||||||
|
return date ? format(date, 'PPP') : 'Not selected';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={styles.section}>
|
||||||
|
<h2 className={styles.sectionTitle}>Date Range Picker</h2>
|
||||||
|
<p className={styles.sectionDescription}>
|
||||||
|
Click to select start date, then hover and click to select end date.
|
||||||
|
</p>
|
||||||
|
<Controls
|
||||||
|
headerStyle={dayHeaderStyle}
|
||||||
|
monthCutoff={monthCutoff}
|
||||||
|
size={size}
|
||||||
|
fontProportion={fontProportion}
|
||||||
|
magnify={magnify}
|
||||||
|
onHeaderStyleChange={setDayHeaderStyle}
|
||||||
|
onMonthCutoffChange={setMonthCutoff}
|
||||||
|
onSizeChange={setSize}
|
||||||
|
onFontProportionChange={setFontProportion}
|
||||||
|
onMagnifyChange={setMagnify}
|
||||||
|
/>
|
||||||
|
<div className={styles.demoContainer}>
|
||||||
|
<Year
|
||||||
|
year={2025}
|
||||||
|
dayHeaderStyle={dayHeaderStyle}
|
||||||
|
monthCutoff={monthCutoff}
|
||||||
|
weekendDays={[6, 0]}
|
||||||
|
dateRange={dateRange}
|
||||||
|
onDateSelect={handleDateSelect}
|
||||||
|
onDateHover={handleDateHover}
|
||||||
|
size={size}
|
||||||
|
fontProportion={fontProportion}
|
||||||
|
magnify={magnify}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.rangeInfo}>
|
||||||
|
<div>Start Date: {formatDateOrNull(dateRange.startDate)}</div>
|
||||||
|
<div>End Date: {formatDateOrNull(dateRange.endDate)}</div>
|
||||||
|
<div>Selecting: {dateRange.selecting ? 'Yes' : 'No'}</div>
|
||||||
|
<div>Hover Date: {formatDateOrNull(dateRange.hoverDate)}</div>
|
||||||
|
<div>Anchor Date: {formatDateOrNull(dateRange.anchorDate)}</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
.section {
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
padding: 2rem;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
color: #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionDescription {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #666;
|
||||||
|
margin: 0 0 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demoContainer {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rangeInfo {
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Month } from '../components/calendar/Month';
|
||||||
|
import { Controls } from '../components/calendar/Controls';
|
||||||
|
import { HeaderStyle, MonthCutoffType, DaySize } from '../types/calendar';
|
||||||
|
import styles from './Examples.module.scss';
|
||||||
|
|
||||||
|
export const SingleMonth: React.FC = () => {
|
||||||
|
const [dayHeaderStyle, setDayHeaderStyle] = useState<HeaderStyle>('tiny');
|
||||||
|
const [monthCutoff, setMonthCutoff] = useState<MonthCutoffType>('truncate');
|
||||||
|
const [size, setSize] = useState<DaySize>('l');
|
||||||
|
const [fontProportion, setFontProportion] = useState<number>(100);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={styles.section}>
|
||||||
|
<h2 className={styles.sectionTitle}>Single Month View</h2>
|
||||||
|
<p className={styles.sectionDescription}>
|
||||||
|
Showcase of a single month component with various styling options.
|
||||||
|
</p>
|
||||||
|
<Controls
|
||||||
|
headerStyle={dayHeaderStyle}
|
||||||
|
monthCutoff={monthCutoff}
|
||||||
|
size={size}
|
||||||
|
fontProportion={fontProportion}
|
||||||
|
onHeaderStyleChange={setDayHeaderStyle}
|
||||||
|
onMonthCutoffChange={setMonthCutoff}
|
||||||
|
onSizeChange={setSize}
|
||||||
|
onFontProportionChange={setFontProportion}
|
||||||
|
/>
|
||||||
|
<div className={styles.demoContainer}>
|
||||||
|
<Month
|
||||||
|
date={new Date(2025, 0, 1)}
|
||||||
|
dayHeaderStyle={dayHeaderStyle}
|
||||||
|
direction="column"
|
||||||
|
monthCutoff={monthCutoff}
|
||||||
|
weekendDays={[6, 0]}
|
||||||
|
size={size}
|
||||||
|
fontProportion={fontProportion}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Week } from '../components/calendar/Week';
|
||||||
|
import { Controls } from '../components/calendar/Controls';
|
||||||
|
import { HeaderStyle, MonthCutoffType, DaySize } from '../types/calendar';
|
||||||
|
import styles from './Examples.module.scss';
|
||||||
|
|
||||||
|
export const WeekView: React.FC = () => {
|
||||||
|
const [dayHeaderStyle, setDayHeaderStyle] = useState<HeaderStyle>('tiny');
|
||||||
|
const [monthCutoff, setMonthCutoff] = useState<MonthCutoffType>('truncate');
|
||||||
|
const [size, setSize] = useState<DaySize>('l');
|
||||||
|
const [fontProportion, setFontProportion] = useState<number>(100);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={styles.section}>
|
||||||
|
<h2 className={styles.sectionTitle}>Week View</h2>
|
||||||
|
<p className={styles.sectionDescription}>
|
||||||
|
Display of a single week with various styling options.
|
||||||
|
</p>
|
||||||
|
<Controls
|
||||||
|
headerStyle={dayHeaderStyle}
|
||||||
|
monthCutoff={monthCutoff}
|
||||||
|
size={size}
|
||||||
|
fontProportion={fontProportion}
|
||||||
|
onHeaderStyleChange={setDayHeaderStyle}
|
||||||
|
onMonthCutoffChange={setMonthCutoff}
|
||||||
|
onSizeChange={setSize}
|
||||||
|
onFontProportionChange={setFontProportion}
|
||||||
|
/>
|
||||||
|
<div className={styles.demoContainer}>
|
||||||
|
<Week
|
||||||
|
startDate={new Date(2025, 0, 1)}
|
||||||
|
headerStyle={dayHeaderStyle}
|
||||||
|
referenceMonth={0}
|
||||||
|
weekendDays={[6, 0]}
|
||||||
|
size={size}
|
||||||
|
fontProportion={fontProportion}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
// src/global.d.ts
|
||||||
|
declare module '*.module.scss' {
|
||||||
|
const classes: { [key: string]: string };
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import App from './App'
|
||||||
|
import './style.css'
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
)
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
:root {
|
||||||
|
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
html {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
html {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,75 @@
|
|||||||
|
export type DateRange = {
|
||||||
|
startDate: Date | null;
|
||||||
|
endDate: Date | null;
|
||||||
|
selecting: boolean;
|
||||||
|
hoverDate: Date | null;
|
||||||
|
anchorDate: Date | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HeaderStyle = 'expanded' | 'compacted' | 'tiny' | 'none';
|
||||||
|
export type MonthCutoffType = 'dimmed' | 'truncate' | undefined;
|
||||||
|
export type DirectionType = 'row' | 'column';
|
||||||
|
export type DayVariation =
|
||||||
|
| 'greyed'
|
||||||
|
| 'selected'
|
||||||
|
| 'rangeStart'
|
||||||
|
| 'rangeEnd'
|
||||||
|
| 'selecting'
|
||||||
|
| 'rowStart'
|
||||||
|
| 'rowEnd';
|
||||||
|
export type DaySize = 'xl' | 'l' | 'm' | 's' | 'xs';
|
||||||
|
|
||||||
|
export interface YearProps {
|
||||||
|
year: number;
|
||||||
|
dayHeaderStyle: HeaderStyle;
|
||||||
|
monthDayOfWeekHeaderStyle?: HeaderStyle;
|
||||||
|
monthCutoff?: MonthCutoffType;
|
||||||
|
weekendDays?: number[];
|
||||||
|
dateRange?: DateRange;
|
||||||
|
onDateSelect?: (date: Date) => void;
|
||||||
|
onDateHover?: (date: Date) => void;
|
||||||
|
size?: DaySize;
|
||||||
|
fontProportion?: number;
|
||||||
|
magnify?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MonthProps {
|
||||||
|
date: Date;
|
||||||
|
dayHeaderStyle: HeaderStyle;
|
||||||
|
monthDayOfWeekHeaderStyle?: HeaderStyle;
|
||||||
|
direction: DirectionType;
|
||||||
|
monthCutoff?: MonthCutoffType;
|
||||||
|
weekendDays?: number[];
|
||||||
|
dateRange?: DateRange;
|
||||||
|
onDateSelect?: (date: Date) => void;
|
||||||
|
onDateHover?: (date: Date) => void;
|
||||||
|
size?: DaySize;
|
||||||
|
fontProportion?: number;
|
||||||
|
magnify?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WeekProps {
|
||||||
|
startDate: Date;
|
||||||
|
headerStyle: HeaderStyle;
|
||||||
|
monthCutoff?: MonthCutoffType;
|
||||||
|
referenceMonth: number;
|
||||||
|
weekendDays?: number[];
|
||||||
|
dateRange?: DateRange;
|
||||||
|
onDateSelect?: (date: Date) => void;
|
||||||
|
onDateHover?: (date: Date) => void;
|
||||||
|
size?: DaySize;
|
||||||
|
fontProportion?: number;
|
||||||
|
magnify?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DayProps {
|
||||||
|
date: Date;
|
||||||
|
headerStyle: HeaderStyle;
|
||||||
|
isOtherMonth?: boolean;
|
||||||
|
variations?: DayVariation[];
|
||||||
|
onSelect?: () => void;
|
||||||
|
onHover?: () => void;
|
||||||
|
size?: DaySize;
|
||||||
|
fontProportion?: number;
|
||||||
|
magnify?: boolean;
|
||||||
|
}
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
import { isSameDay, isWithinInterval, startOfDay } from 'date-fns';
|
||||||
|
import { DateRange, DayVariation } from '../types/calendar';
|
||||||
|
|
||||||
|
export const getDateVariations = (
|
||||||
|
date: Date,
|
||||||
|
dateRange: DateRange | undefined,
|
||||||
|
weekendDays: number[] = [],
|
||||||
|
dayIndex: number = 0
|
||||||
|
): DayVariation[] => {
|
||||||
|
const greyedClasses: DayVariation[] = weekendDays.includes(date.getDay())
|
||||||
|
? ['greyed']
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const greyedAndRowRelativeClasses = ['rowStart', 'rowEnd'].reduce((p, c) => {
|
||||||
|
switch (c) {
|
||||||
|
case 'rowStart':
|
||||||
|
return dayIndex === 0 ? [...p, c] : p;
|
||||||
|
case 'rowEnd':
|
||||||
|
return dayIndex === 6 ? [...p, c] : p;
|
||||||
|
default:
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
}, greyedClasses);
|
||||||
|
|
||||||
|
if (!dateRange?.startDate || !dateRange?.endDate) {
|
||||||
|
return greyedAndRowRelativeClasses;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentDate = startOfDay(date);
|
||||||
|
const startDate = startOfDay(dateRange.startDate);
|
||||||
|
const endDate = startOfDay(dateRange.endDate);
|
||||||
|
|
||||||
|
if (!isWithinInterval(currentDate, { start: startDate, end: endDate })) {
|
||||||
|
return greyedAndRowRelativeClasses;
|
||||||
|
}
|
||||||
|
|
||||||
|
const withSelectingOrSelected: DayVariation[] = [
|
||||||
|
...greyedAndRowRelativeClasses,
|
||||||
|
dateRange.selecting
|
||||||
|
? 'selecting'
|
||||||
|
: 'selected',
|
||||||
|
];
|
||||||
|
|
||||||
|
return ['rangeStart', 'rangeEnd'].reduce((p, c) => {
|
||||||
|
switch (c) {
|
||||||
|
case 'rangeStart':
|
||||||
|
return isSameDay(currentDate, startDate) ? [...p, c] : p;
|
||||||
|
case 'rangeEnd':
|
||||||
|
return isSameDay(currentDate, endDate) ? [...p, c] : p;
|
||||||
|
default:
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
}, withSelectingOrSelected);
|
||||||
|
};
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare module '*.module.scss' {
|
||||||
|
const classes: { [key: string]: string };
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
css: {
|
||||||
|
modules: {
|
||||||
|
localsConvention: 'camelCase'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue