first commit
This commit is contained in:
83
src/components/dates/Calendar.tsx
Normal file
83
src/components/dates/Calendar.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import React from "react";
|
||||
import { Button } from "../button/Button";
|
||||
|
||||
|
||||
const DOW = ["Пн","Вт","Ср","Чт","Пт","Сб","Вс"];
|
||||
|
||||
function startOfMonth(d: Date) { return new Date(d.getFullYear(), d.getMonth(), 1); }
|
||||
function endOfMonth(d: Date) { return new Date(d.getFullYear(), d.getMonth() + 1, 0); }
|
||||
function addMonths(d: Date, n: number) { return new Date(d.getFullYear(), d.getMonth() + n, 1); }
|
||||
function sameDay(a?: Date, b?: Date) {
|
||||
if (!a || !b) return false;
|
||||
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
||||
}
|
||||
function toKey(d: Date) { return `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`; }
|
||||
|
||||
export function Calendar(props: {
|
||||
value?: Date;
|
||||
onChange: (d: Date) => void;
|
||||
month?: Date;
|
||||
onMonthChange?: (d: Date) => void;
|
||||
}) {
|
||||
const controlledMonth = props.month;
|
||||
const [innerMonth, setInnerMonth] = React.useState<Date>(() => startOfMonth(props.value ?? new Date()));
|
||||
const month = controlledMonth ?? innerMonth;
|
||||
|
||||
const setMonth = (d: Date) => {
|
||||
props.onMonthChange?.(d);
|
||||
if (!controlledMonth) setInnerMonth(d);
|
||||
};
|
||||
|
||||
const start = startOfMonth(month);
|
||||
const end = endOfMonth(month);
|
||||
|
||||
const jsDow = start.getDay();
|
||||
const shift = (jsDow + 6) % 7;
|
||||
|
||||
const days: { date: Date; muted: boolean }[] = [];
|
||||
const prevMonthEnd = new Date(start.getFullYear(), start.getMonth(), 0);
|
||||
for (let i = 0; i < shift; i++) {
|
||||
const d = new Date(prevMonthEnd.getFullYear(), prevMonthEnd.getMonth(), prevMonthEnd.getDate() - (shift - 1 - i));
|
||||
days.push({ date: d, muted: true });
|
||||
}
|
||||
for (let day = 1; day <= end.getDate(); day++) {
|
||||
days.push({ date: new Date(start.getFullYear(), start.getMonth(), day), muted: false });
|
||||
}
|
||||
while (days.length % 7 !== 0) {
|
||||
const last = days[days.length - 1].date;
|
||||
const d = new Date(last.getFullYear(), last.getMonth(), last.getDate() + 1);
|
||||
days.push({ date: d, muted: true });
|
||||
}
|
||||
|
||||
const title = month.toLocaleString("ru-RU", { month: "long", year: "numeric" });
|
||||
|
||||
return (
|
||||
<div className="ui-cal">
|
||||
<div className="ui-cal__head">
|
||||
<Button variant="ghost" tone="neutral" size="sm" onClick={() => setMonth(addMonths(month, -1))}>
|
||||
←
|
||||
</Button>
|
||||
<div className="ui-cal__title">{title}</div>
|
||||
<Button variant="ghost" tone="neutral" size="sm" onClick={() => setMonth(addMonths(month, 1))}>
|
||||
→
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="ui-cal__grid">
|
||||
{DOW.map((d) => <div key={d} className="ui-cal__dow">{d}</div>)}
|
||||
{days.map(({ date, muted }) => (
|
||||
<button
|
||||
key={toKey(date)}
|
||||
type="button"
|
||||
className="ui-cal__day"
|
||||
data-muted={muted ? "true" : "false"}
|
||||
data-selected={sameDay(date, props.value) ? "true" : "false"}
|
||||
onClick={() => props.onChange(date)}
|
||||
>
|
||||
{date.getDate()}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
98
src/components/dates/CalendarRange.tsx
Normal file
98
src/components/dates/CalendarRange.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import React from "react";
|
||||
import { Button } from "../button/Button";
|
||||
// import "./calendar.css";
|
||||
|
||||
|
||||
const DOW = ["Пн","Вт","Ср","Чт","Пт","Сб","Вс"];
|
||||
|
||||
function startOfMonth(d: Date) { return new Date(d.getFullYear(), d.getMonth(), 1); }
|
||||
function endOfMonth(d: Date) { return new Date(d.getFullYear(), d.getMonth() + 1, 0); }
|
||||
function addMonths(d: Date, n: number) { return new Date(d.getFullYear(), d.getMonth() + n, 1); }
|
||||
function toKey(d: Date) { return `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`; }
|
||||
function inRange(d: Date, a?: Date, b?: Date) {
|
||||
if (!a || !b) return false;
|
||||
const t = d.setHours(0,0,0,0);
|
||||
const ta = a.getTime();
|
||||
const tb = b.getTime();
|
||||
return t >= Math.min(ta, tb) && t <= Math.max(ta, tb);
|
||||
}
|
||||
function sameDay(a?: Date, b?: Date) {
|
||||
if (!a || !b) return false;
|
||||
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
||||
}
|
||||
|
||||
export function CalendarRange(props: {
|
||||
value?: { start?: Date; end?: Date };
|
||||
onChange: (v: { start?: Date; end?: Date }) => void;
|
||||
}) {
|
||||
const [month, setMonth] = React.useState<Date>(() => startOfMonth(props.value?.start ?? new Date()));
|
||||
const start = props.value?.start;
|
||||
const end = props.value?.end;
|
||||
|
||||
const startM = startOfMonth(month);
|
||||
const endM = endOfMonth(month);
|
||||
|
||||
const jsDow = startM.getDay();
|
||||
const shift = (jsDow + 6) % 7;
|
||||
|
||||
const days: { date: Date; muted: boolean }[] = [];
|
||||
const prevMonthEnd = new Date(startM.getFullYear(), startM.getMonth(), 0);
|
||||
for (let i = 0; i < shift; i++) {
|
||||
const d = new Date(prevMonthEnd.getFullYear(), prevMonthEnd.getMonth(), prevMonthEnd.getDate() - (shift - 1 - i));
|
||||
days.push({ date: d, muted: true });
|
||||
}
|
||||
for (let day = 1; day <= endM.getDate(); day++) {
|
||||
days.push({ date: new Date(startM.getFullYear(), startM.getMonth(), day), muted: false });
|
||||
}
|
||||
while (days.length % 7 !== 0) {
|
||||
const last = days[days.length - 1].date;
|
||||
const d = new Date(last.getFullYear(), last.getMonth(), last.getDate() + 1);
|
||||
days.push({ date: d, muted: true });
|
||||
}
|
||||
|
||||
const title = month.toLocaleString("ru-RU", { month: "long", year: "numeric" });
|
||||
|
||||
const clickDay = (d: Date) => {
|
||||
if (!start || (start && end)) {
|
||||
props.onChange({ start: d, end: undefined });
|
||||
return;
|
||||
}
|
||||
props.onChange({ start, end: d });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="ui-cal">
|
||||
<div className="ui-cal__head">
|
||||
<Button variant="ghost" tone="neutral" size="sm" onClick={() => setMonth(addMonths(month, -1))}>←</Button>
|
||||
<div className="ui-cal__title">{title}</div>
|
||||
<Button variant="ghost" tone="neutral" size="sm" onClick={() => setMonth(addMonths(month, 1))}>→</Button>
|
||||
</div>
|
||||
|
||||
<div style={{ display: "flex", gap: 10, color: "var(--ui-muted)", fontSize: 12, marginBottom: 8 }}>
|
||||
<div>От: {start ? start.toLocaleDateString("ru-RU") : "—"}</div>
|
||||
<div>До: {end ? end.toLocaleDateString("ru-RU") : "—"}</div>
|
||||
</div>
|
||||
|
||||
<div className="ui-cal__grid">
|
||||
{DOW.map((d) => <div key={d} className="ui-cal__dow">{d}</div>)}
|
||||
{days.map(({ date, muted }) => {
|
||||
const selected = sameDay(date, start) || sameDay(date, end);
|
||||
const range = inRange(new Date(date.getFullYear(), date.getMonth(), date.getDate()), start, end);
|
||||
return (
|
||||
<button
|
||||
key={toKey(date)}
|
||||
type="button"
|
||||
className="ui-cal__day"
|
||||
data-muted={muted ? "true" : "false"}
|
||||
data-selected={selected ? "true" : "false"}
|
||||
data-range={range && !selected ? "true" : "false"}
|
||||
onClick={() => clickDay(date)}
|
||||
>
|
||||
{date.getDate()}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
24
src/components/dates/DateInput.tsx
Normal file
24
src/components/dates/DateInput.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
// import "../_shared/control.css";
|
||||
|
||||
export function DateInput(props: {
|
||||
value?: string; // yyyy-mm-dd
|
||||
onChange: (value: string) => void;
|
||||
label?: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
const { value, onChange, label, className } = props;
|
||||
|
||||
return (
|
||||
<div style={{ display: "grid", gap: 8 }}>
|
||||
{label ? <div style={{ fontWeight: 700, fontSize: 13 }}>{label}</div> : null}
|
||||
<input
|
||||
type="date"
|
||||
className={clsx("ui-control", className)}
|
||||
value={value ?? ""}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
32
src/components/dates/DateRangeInput.tsx
Normal file
32
src/components/dates/DateRangeInput.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
// import "../_shared/control.css";
|
||||
|
||||
export function DateRangeInput(props: {
|
||||
start?: string;
|
||||
end?: string;
|
||||
onChange: (v: { start?: string; end?: string }) => void;
|
||||
label?: React.ReactNode;
|
||||
}) {
|
||||
const { start, end, onChange, label } = props;
|
||||
|
||||
return (
|
||||
<div style={{ display: "grid", gap: 8 }}>
|
||||
{label ? <div style={{ fontWeight: 700, fontSize: 13 }}>{label}</div> : null}
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
|
||||
<input
|
||||
type="date"
|
||||
className={clsx("ui-control")}
|
||||
value={start ?? ""}
|
||||
onChange={(e) => onChange({ start: e.target.value, end })}
|
||||
/>
|
||||
<input
|
||||
type="date"
|
||||
className={clsx("ui-control")}
|
||||
value={end ?? ""}
|
||||
onChange={(e) => onChange({ start, end: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
63
src/components/dates/calendar.css
Normal file
63
src/components/dates/calendar.css
Normal file
@@ -0,0 +1,63 @@
|
||||
.ui-cal{
|
||||
border: 1px solid var(--ui-border);
|
||||
background: var(--ui-panel);
|
||||
width: 100%;
|
||||
padding: var(--cal-p, 12px);
|
||||
border-radius: var(--ui-radius);
|
||||
font-family: var(--ui-font), serif;
|
||||
}
|
||||
|
||||
.ui-cal__head{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.ui-cal__title{
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.ui-cal__grid{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: var(--cal-gap, 6px);;
|
||||
}
|
||||
|
||||
.ui-cal__dow{
|
||||
text-align: center;
|
||||
color: var(--ui-muted);
|
||||
font-size: 11px;
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
.ui-cal__day{
|
||||
height: var(--cal-day, 36px);
|
||||
border-radius: var(--cal-day-radius, 10px);;
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
color: var(--ui-fg);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ui-cal__day:hover{
|
||||
background: rgba(255,255,255,0.06);
|
||||
}
|
||||
:root[data-theme="light"] .ui-cal__day:hover{
|
||||
background: rgba(17,19,24,0.06);
|
||||
}
|
||||
|
||||
.ui-cal__day[data-muted="true"]{
|
||||
color: color-mix(in oklab, var(--ui-muted) 85%, transparent);
|
||||
}
|
||||
|
||||
.ui-cal__day[data-selected="true"]{
|
||||
background: var(--ui-primary);
|
||||
color: var(--ui-primary-fg);
|
||||
}
|
||||
|
||||
.ui-cal__day[data-range="true"]{
|
||||
background: color-mix(in oklab, var(--ui-primary) 25%, transparent);
|
||||
border-color: color-mix(in oklab, var(--ui-primary) 40%, transparent);
|
||||
}
|
||||
Reference in New Issue
Block a user