diff --git a/apps/web/src/components/JalaliDateInput.vue b/apps/web/src/components/JalaliDateInput.vue
index 7d51480..dab256f 100644
--- a/apps/web/src/components/JalaliDateInput.vue
+++ b/apps/web/src/components/JalaliDateInput.vue
@@ -59,6 +59,11 @@ const draftMinute = ref(0);
const clockFace = ref<'hour' | 'minute'>('hour');
const isMobile = ref(false);
const dragging = ref(false);
+/** نمای تقویم: روزها ← کلیک عنوان → ماهها ← کلیک عنوان → سالها */
+const calView = ref<'days' | 'months' | 'years'>('days');
+const YEAR_PAGE = 12;
+const yearPageStart = ref(todayJ.jy - 5);
+const MONTHS = Array.from({ length: 12 }, (_, i) => i + 1);
type Cell = {
key: string;
@@ -107,6 +112,26 @@ const monthTitle = computed(
() => `${jalaliMonthName(cursor.value.jm)} ${toFaDigits(cursor.value.jy)}`,
);
+const navTitle = computed(() => {
+ if (calView.value === 'months') return toFaDigits(cursor.value.jy);
+ if (calView.value === 'years') {
+ return `${toFaDigits(yearPageStart.value)} – ${toFaDigits(yearPageStart.value + YEAR_PAGE - 1)}`;
+ }
+ return monthTitle.value;
+});
+
+const navLabels = computed(() => {
+ if (calView.value === 'months') return { forward: 'سال بعد', back: 'سال قبل' };
+ if (calView.value === 'years') return { forward: 'سالهای بعد', back: 'سالهای قبل' };
+ return { forward: 'ماه بعد', back: 'ماه قبل' };
+});
+
+const yearOptions = computed(() =>
+ Array.from({ length: YEAR_PAGE }, (_, i) => yearPageStart.value + i),
+);
+
+const selectedJ = computed(() => (parsed.value ? dateToJalali(parsed.value) : null));
+
const hours = Array.from({ length: 24 }, (_, i) => i);
const minuteMarks = Array.from({ length: 12 }, (_, i) => i * 5);
const columnMinutes = Array.from({ length: 60 }, (_, i) => i);
@@ -316,6 +341,7 @@ function onWindowChange() {
async function show() {
if (props.disabled) return;
syncDraftFrom(parsed.value);
+ calView.value = 'days';
clockFace.value = 'hour';
syncViewport();
open.value = true;
@@ -341,6 +367,38 @@ function goMonth(delta: number) {
void nextTick(place);
}
+function navStep(delta: number) {
+ if (calView.value === 'months') {
+ cursor.value = { jy: cursor.value.jy + delta, jm: cursor.value.jm };
+ } else if (calView.value === 'years') {
+ yearPageStart.value += delta * YEAR_PAGE;
+ } else {
+ goMonth(delta);
+ }
+}
+
+function onTitleClick() {
+ if (calView.value === 'days') {
+ calView.value = 'months';
+ } else if (calView.value === 'months') {
+ yearPageStart.value = cursor.value.jy - 5;
+ calView.value = 'years';
+ }
+ void nextTick(place);
+}
+
+function pickMonth(jm: number) {
+ cursor.value = { jy: cursor.value.jy, jm };
+ calView.value = 'days';
+ void nextTick(place);
+}
+
+function pickYear(jy: number) {
+ cursor.value = { jy, jm: cursor.value.jm };
+ calView.value = 'months';
+ void nextTick(place);
+}
+
function pickDay(cell: Cell) {
if (!cell.inMonth) {
cursor.value = { jy: cell.jy, jm: cell.jm };
@@ -483,16 +541,53 @@ onBeforeUnmount(() => {
:style="{ top: `${top}px`, left: `${left}px` }"
>
-
+
- {{ monthTitle }}
-
+
+ {{ navTitle }}
+
+
-
+
+
+ {{ jalaliMonthName(m) }}
+
+
+
+
+
+ {{ toFaDigits(y) }}
+
+
+
+
{{ day }}
@@ -781,8 +876,42 @@ onBeforeUnmount(() => {
margin-bottom: 0.55rem;
}
-.nav strong {
+.nav-title {
font-size: 0.92rem;
+ font-weight: 700;
+ color: #0f172a;
+}
+
+.nav-title:not(.static):hover {
+ color: #0f766e;
+}
+
+.nav-title.static {
+ cursor: default;
+}
+
+.pick-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 0.35rem;
+ padding: 0.25rem 0;
+}
+
+.pick-cell {
+ height: 44px;
+ border-radius: var(--radius);
+ color: #0f172a;
+ font-size: 0.85rem;
+}
+
+.pick-cell.today {
+ box-shadow: inset 0 0 0 1px #99f6e4;
+}
+
+.pick-cell.on,
+.pick-cell:hover {
+ background: #0d9488;
+ color: #fff;
}
.nav-btn {