feat: first commit, working

master
Guillermo Pages 10 months ago
commit b4abe21060

BIN
.DS_Store vendored

Binary file not shown.

1
.gitignore vendored

@ -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>

1739
package-lock.json generated

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>
);
};

5
src/global.d.ts vendored

@ -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);
};

6
src/vite-env.d.ts vendored

@ -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…
Cancel
Save