531 lines
17 KiB
Vue
531 lines
17 KiB
Vue
<script setup lang="ts">
|
||||
|
|
import { computed, inject, onMounted, reactive, ref } from 'vue';
|
|||
|
|
import { useRoute, useRouter } from 'vue-router';
|
|||
|
|
import type { ActivityPriority, RoutineDto, RoutineEntityLink, SaveRoutineRequest } from '@mento/shared';
|
|||
|
|
import { ROUTINE_ENTITY_LINK_LABELS, ROUTINE_WEEKDAY_LABELS } from '@mento/shared';
|
|||
|
|
import JalaliDateInput from '@/components/JalaliDateInput.vue';
|
|||
|
|
import RecurrencePicker, { type RecurrenceValue } from '@/components/routines/RecurrencePicker.vue';
|
|||
|
|
import {
|
|||
|
|
UiButton,
|
|||
|
|
UiCard,
|
|||
|
|
UiField,
|
|||
|
|
UiInput,
|
|||
|
|
UiPageHeader,
|
|||
|
|
UiSegmented,
|
|||
|
|
UiSelect,
|
|||
|
|
UiTextarea,
|
|||
|
|
} from '@/components/ui';
|
|||
|
|
import { api } from '@/lib/api';
|
|||
|
|
import { toFaDigits } from '@/lib/jalali';
|
|||
|
|
import { PRIORITY_LABELS, ROUTINES_CTX, apiError, formatDayNumeric, todayKey } from '@/lib/routines';
|
|||
|
|
|
|||
|
|
const shell = inject(ROUTINES_CTX)!;
|
|||
|
|
const route = useRoute();
|
|||
|
|
const router = useRouter();
|
|||
|
|
|
|||
|
|
const id = computed(() => (typeof route.params.id === 'string' ? route.params.id : null));
|
|||
|
|
const isEdit = computed(() => !!id.value);
|
|||
|
|
|
|||
|
|
const form = reactive({
|
|||
|
|
title: '',
|
|||
|
|
description: '',
|
|||
|
|
target: 'user' as 'user' | 'team',
|
|||
|
|
assigneeId: '',
|
|||
|
|
teamId: '',
|
|||
|
|
categoryId: '',
|
|||
|
|
priority: 'medium' as ActivityPriority,
|
|||
|
|
timeOfDay: '09:00',
|
|||
|
|
durationMinutes: '' as string | number,
|
|||
|
|
startDate: todayKey(),
|
|||
|
|
endDate: '',
|
|||
|
|
isMandatory: true,
|
|||
|
|
allowSkip: false,
|
|||
|
|
skipReasonRequired: true,
|
|||
|
|
reportRequired: false,
|
|||
|
|
allowAfterDeadline: true,
|
|||
|
|
carryOverOverdue: false,
|
|||
|
|
entityLink: 'none' as RoutineEntityLink,
|
|||
|
|
reminderMinutesBefore: '' as string,
|
|||
|
|
isActive: true,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const recurrence = ref<RecurrenceValue>({
|
|||
|
|
recurrence: 'daily',
|
|||
|
|
weekdays: [],
|
|||
|
|
intervalDays: null,
|
|||
|
|
monthDay: null,
|
|||
|
|
specificDates: [],
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const loading = ref(false);
|
|||
|
|
const saving = ref(false);
|
|||
|
|
const error = ref('');
|
|||
|
|
|
|||
|
|
const targetOptions = [
|
|||
|
|
{ value: 'user' as const, label: 'یک کاربر' },
|
|||
|
|
{ value: 'team' as const, label: 'همه اعضای یک تیم' },
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const priorities = Object.entries(PRIORITY_LABELS) as Array<[ActivityPriority, string]>;
|
|||
|
|
const entityLinks = Object.entries(ROUTINE_ENTITY_LINK_LABELS) as Array<[RoutineEntityLink, string]>;
|
|||
|
|
const reminderOptions = [
|
|||
|
|
{ value: '', label: 'طبق تنظیمات هر کاربر' },
|
|||
|
|
{ value: '0', label: 'سر ساعت' },
|
|||
|
|
{ value: '10', label: '۱۰ دقیقه قبل' },
|
|||
|
|
{ value: '15', label: '۱۵ دقیقه قبل' },
|
|||
|
|
{ value: '30', label: '۳۰ دقیقه قبل' },
|
|||
|
|
{ value: '60', label: '۱ ساعت قبل' },
|
|||
|
|
{ value: '120', label: '۲ ساعت قبل' },
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
function fill(r: RoutineDto) {
|
|||
|
|
Object.assign(form, {
|
|||
|
|
title: r.title,
|
|||
|
|
description: r.description ?? '',
|
|||
|
|
target: r.team ? 'team' : 'user',
|
|||
|
|
assigneeId: r.assignee?.id ?? '',
|
|||
|
|
teamId: r.team?.id ?? '',
|
|||
|
|
categoryId: r.category?.id ?? '',
|
|||
|
|
priority: r.priority,
|
|||
|
|
timeOfDay: r.timeOfDay,
|
|||
|
|
durationMinutes: r.durationMinutes ?? '',
|
|||
|
|
startDate: r.startDate,
|
|||
|
|
endDate: r.endDate ?? '',
|
|||
|
|
isMandatory: r.isMandatory,
|
|||
|
|
allowSkip: r.allowSkip,
|
|||
|
|
skipReasonRequired: r.skipReasonRequired,
|
|||
|
|
reportRequired: r.reportRequired,
|
|||
|
|
allowAfterDeadline: r.allowAfterDeadline,
|
|||
|
|
carryOverOverdue: r.carryOverOverdue,
|
|||
|
|
entityLink: r.entityLink,
|
|||
|
|
reminderMinutesBefore: r.reminderMinutesBefore == null ? '' : String(r.reminderMinutesBefore),
|
|||
|
|
isActive: r.isActive,
|
|||
|
|
});
|
|||
|
|
recurrence.value = {
|
|||
|
|
recurrence: r.recurrence,
|
|||
|
|
weekdays: [...r.weekdays],
|
|||
|
|
intervalDays: r.intervalDays,
|
|||
|
|
monthDay: r.monthDay,
|
|||
|
|
specificDates: [...r.specificDates],
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
onMounted(async () => {
|
|||
|
|
if (!id.value) return;
|
|||
|
|
loading.value = true;
|
|||
|
|
try {
|
|||
|
|
const { data } = await api.get<RoutineDto>(`/routines/${id.value}`);
|
|||
|
|
fill(data);
|
|||
|
|
} catch (e) {
|
|||
|
|
error.value = apiError(e, 'روتین پیدا نشد.');
|
|||
|
|
} finally {
|
|||
|
|
loading.value = false;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const users = computed(() => shell.context.value?.users ?? []);
|
|||
|
|
const teams = computed(() => shell.context.value?.teams ?? []);
|
|||
|
|
const categories = computed(() => shell.context.value?.categories ?? []);
|
|||
|
|
|
|||
|
|
function joinFa(items: string[]) {
|
|||
|
|
if (items.length <= 1) return items.join('');
|
|||
|
|
return `${items.slice(0, -1).join('، ')} و ${items[items.length - 1]}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const summary = computed(() => {
|
|||
|
|
const r = recurrence.value;
|
|||
|
|
const days = r.weekdays.map((d) => ROUTINE_WEEKDAY_LABELS[d]!);
|
|||
|
|
let rec = '';
|
|||
|
|
switch (r.recurrence) {
|
|||
|
|
case 'daily':
|
|||
|
|
rec = 'هر روز';
|
|||
|
|
break;
|
|||
|
|
case 'weekdays':
|
|||
|
|
case 'weekly':
|
|||
|
|
rec = days.length ? `هر ${joinFa(days)}` : 'روز هفته انتخاب نشده';
|
|||
|
|
break;
|
|||
|
|
case 'biweekly':
|
|||
|
|
rec = days.length ? `یک هفته در میان، ${joinFa(days)}` : 'روز هفته انتخاب نشده';
|
|||
|
|
break;
|
|||
|
|
case 'monthly':
|
|||
|
|
rec = r.monthDay === 1 ? 'اول هر ماه' : `روز ${toFaDigits(r.monthDay ?? 1)} هر ماه`;
|
|||
|
|
break;
|
|||
|
|
case 'every_n_days':
|
|||
|
|
rec = `هر ${toFaDigits(r.intervalDays ?? 1)} روز یکبار`;
|
|||
|
|
break;
|
|||
|
|
case 'specific_dates':
|
|||
|
|
rec = `${toFaDigits(r.specificDates.length)} تاریخ مشخص`;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
const who =
|
|||
|
|
form.target === 'team'
|
|||
|
|
? teams.value.find((t) => t.id === form.teamId)?.name
|
|||
|
|
? `برای همه اعضای تیم «${teams.value.find((t) => t.id === form.teamId)?.name}»`
|
|||
|
|
: ''
|
|||
|
|
: users.value.find((u) => u.id === form.assigneeId)?.fullName
|
|||
|
|
? `برای ${users.value.find((u) => u.id === form.assigneeId)?.fullName}`
|
|||
|
|
: '';
|
|||
|
|
return `${rec}، ساعت ${toFaDigits(form.timeOfDay || '—')}${who ? ` ${who}` : ''} · از ${formatDayNumeric(form.startDate)}${form.endDate ? ` تا ${formatDayNumeric(form.endDate)}` : ''}`;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
function validate(): string | null {
|
|||
|
|
if (form.title.trim().length < 2) return 'عنوان روتین را وارد کنید.';
|
|||
|
|
if (form.target === 'user' && !form.assigneeId) return 'مسئول روتین را انتخاب کنید.';
|
|||
|
|
if (form.target === 'team' && !form.teamId) return 'تیم را انتخاب کنید.';
|
|||
|
|
if (!/^\d{2}:\d{2}$/.test(form.timeOfDay)) return 'ساعت انجام را وارد کنید.';
|
|||
|
|
if (!form.startDate) return 'تاریخ شروع را انتخاب کنید.';
|
|||
|
|
if (form.endDate && form.endDate < form.startDate) return 'تاریخ پایان نباید قبل از تاریخ شروع باشد.';
|
|||
|
|
const r = recurrence.value;
|
|||
|
|
if (['weekdays', 'weekly', 'biweekly'].includes(r.recurrence) && !r.weekdays.length) {
|
|||
|
|
return 'حداقل یک روز هفته را انتخاب کنید.';
|
|||
|
|
}
|
|||
|
|
if (r.recurrence === 'every_n_days' && !r.intervalDays) return 'فاصله تکرار را وارد کنید.';
|
|||
|
|
if (r.recurrence === 'specific_dates' && !r.specificDates.length) return 'حداقل یک تاریخ اضافه کنید.';
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function save() {
|
|||
|
|
error.value = validate() ?? '';
|
|||
|
|
if (error.value) return;
|
|||
|
|
const r = recurrence.value;
|
|||
|
|
const duration = Number(form.durationMinutes);
|
|||
|
|
const body: SaveRoutineRequest = {
|
|||
|
|
title: form.title.trim(),
|
|||
|
|
description: form.description.trim() || null,
|
|||
|
|
assigneeId: form.target === 'user' ? form.assigneeId : null,
|
|||
|
|
teamId: form.target === 'team' ? form.teamId : null,
|
|||
|
|
categoryId: form.categoryId || null,
|
|||
|
|
priority: form.priority,
|
|||
|
|
timeOfDay: form.timeOfDay,
|
|||
|
|
durationMinutes: duration > 0 ? Math.floor(duration) : null,
|
|||
|
|
startDate: form.startDate,
|
|||
|
|
endDate: form.endDate || null,
|
|||
|
|
recurrence: r.recurrence,
|
|||
|
|
weekdays: ['weekdays', 'weekly', 'biweekly'].includes(r.recurrence) ? r.weekdays : [],
|
|||
|
|
intervalDays: r.recurrence === 'every_n_days' ? r.intervalDays : null,
|
|||
|
|
monthDay: r.recurrence === 'monthly' ? r.monthDay : null,
|
|||
|
|
specificDates: r.recurrence === 'specific_dates' ? r.specificDates : [],
|
|||
|
|
isMandatory: form.isMandatory,
|
|||
|
|
allowSkip: form.allowSkip,
|
|||
|
|
skipReasonRequired: form.allowSkip && form.skipReasonRequired,
|
|||
|
|
reportRequired: form.reportRequired,
|
|||
|
|
allowAfterDeadline: form.allowAfterDeadline,
|
|||
|
|
carryOverOverdue: form.carryOverOverdue,
|
|||
|
|
entityLink: form.entityLink,
|
|||
|
|
reminderMinutesBefore: form.reminderMinutesBefore === '' ? null : Number(form.reminderMinutesBefore),
|
|||
|
|
isActive: form.isActive,
|
|||
|
|
};
|
|||
|
|
saving.value = true;
|
|||
|
|
try {
|
|||
|
|
if (isEdit.value) await api.patch(`/routines/${id.value}`, body);
|
|||
|
|
else await api.post('/routines', body);
|
|||
|
|
shell.notifyChanged();
|
|||
|
|
void router.push('/routines/manage');
|
|||
|
|
} catch (e) {
|
|||
|
|
error.value = apiError(e, 'ذخیره روتین ممکن نشد.');
|
|||
|
|
} finally {
|
|||
|
|
saving.value = false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<template>
|
|||
|
|
<div class="rform">
|
|||
|
|
<UiPageHeader
|
|||
|
|
:title="isEdit ? 'ویرایش روتین' : 'روتین جدید'"
|
|||
|
|
lead="کاری که باید بهصورت منظم انجام شود؛ هر روزِ تکرار، یک کار برای مسئول ساخته میشود."
|
|||
|
|
back-to="/routines/manage"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<div v-if="loading" class="state">در حال بارگذاری…</div>
|
|||
|
|
|
|||
|
|
<form v-else class="grid" @submit.prevent="save">
|
|||
|
|
<div class="col">
|
|||
|
|
<UiCard>
|
|||
|
|
<h3 class="sec-title">اطلاعات پایه</h3>
|
|||
|
|
<div class="fields">
|
|||
|
|
<UiField label="عنوان" required>
|
|||
|
|
<UiInput v-model="form.title" placeholder="مثلاً: تماس با لیدهای جدید" :maxlength="200" />
|
|||
|
|
</UiField>
|
|||
|
|
<UiField label="توضیحات">
|
|||
|
|
<UiTextarea v-model="form.description" :rows="3" placeholder="جزئیات یا دستورالعمل انجام کار" />
|
|||
|
|
</UiField>
|
|||
|
|
<div class="grid-2">
|
|||
|
|
<UiField label="دستهبندی">
|
|||
|
|
<UiSelect v-model="form.categoryId">
|
|||
|
|
<option value="">بدون دسته</option>
|
|||
|
|
<option v-for="c in categories" :key="c.id" :value="c.id">{{ c.name }}</option>
|
|||
|
|
</UiSelect>
|
|||
|
|
</UiField>
|
|||
|
|
<UiField label="اولویت">
|
|||
|
|
<UiSelect v-model="form.priority">
|
|||
|
|
<option v-for="[k, label] in priorities" :key="k" :value="k">{{ label }}</option>
|
|||
|
|
</UiSelect>
|
|||
|
|
</UiField>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</UiCard>
|
|||
|
|
|
|||
|
|
<UiCard>
|
|||
|
|
<h3 class="sec-title">مسئول</h3>
|
|||
|
|
<div class="fields">
|
|||
|
|
<UiSegmented v-model="form.target" :options="targetOptions" />
|
|||
|
|
<UiField v-if="form.target === 'user'" label="کاربر" required>
|
|||
|
|
<UiSelect v-model="form.assigneeId">
|
|||
|
|
<option value="" disabled>انتخاب کنید</option>
|
|||
|
|
<option v-for="u in users" :key="u.id" :value="u.id">{{ u.fullName }}</option>
|
|||
|
|
</UiSelect>
|
|||
|
|
</UiField>
|
|||
|
|
<UiField v-else label="تیم" required hint="برای هر عضو فعال تیم یک کار جداگانه ساخته میشود.">
|
|||
|
|
<UiSelect v-model="form.teamId">
|
|||
|
|
<option value="" disabled>انتخاب کنید</option>
|
|||
|
|
<option v-for="t in teams" :key="t.id" :value="t.id">{{ t.name }}</option>
|
|||
|
|
</UiSelect>
|
|||
|
|
</UiField>
|
|||
|
|
</div>
|
|||
|
|
</UiCard>
|
|||
|
|
|
|||
|
|
<UiCard>
|
|||
|
|
<h3 class="sec-title">زمانبندی و تکرار</h3>
|
|||
|
|
<div class="fields">
|
|||
|
|
<div class="grid-2">
|
|||
|
|
<UiField label="ساعت انجام" required>
|
|||
|
|
<JalaliDateInput v-model="form.timeOfDay" mode="time" :clearable="false" />
|
|||
|
|
</UiField>
|
|||
|
|
<UiField label="مدت تقریبی (دقیقه)">
|
|||
|
|
<UiInput v-model="form.durationMinutes" type="number" placeholder="اختیاری" />
|
|||
|
|
</UiField>
|
|||
|
|
<UiField label="تاریخ شروع" required>
|
|||
|
|
<JalaliDateInput v-model="form.startDate" mode="date" :clearable="false" />
|
|||
|
|
</UiField>
|
|||
|
|
<UiField label="تاریخ پایان" hint="خالی = بدون پایان">
|
|||
|
|
<JalaliDateInput v-model="form.endDate" mode="date" />
|
|||
|
|
</UiField>
|
|||
|
|
</div>
|
|||
|
|
<UiField label="الگوی تکرار" required>
|
|||
|
|
<RecurrencePicker v-model="recurrence" />
|
|||
|
|
</UiField>
|
|||
|
|
</div>
|
|||
|
|
</UiCard>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="col side">
|
|||
|
|
<UiCard>
|
|||
|
|
<h3 class="sec-title">قوانین انجام</h3>
|
|||
|
|
<div class="toggles">
|
|||
|
|
<label class="tg">
|
|||
|
|
<input v-model="form.isMandatory" type="checkbox" />
|
|||
|
|
<span>
|
|||
|
|
<strong>اجباری</strong>
|
|||
|
|
<small>در امتیاز روزانه و روزهای پیاپی کامل حساب میشود.</small>
|
|||
|
|
</span>
|
|||
|
|
</label>
|
|||
|
|
<label class="tg">
|
|||
|
|
<input v-model="form.allowSkip" type="checkbox" />
|
|||
|
|
<span>
|
|||
|
|
<strong>امکان رد کردن</strong>
|
|||
|
|
<small>کاربر میتواند این کار را برای یک روز رد کند.</small>
|
|||
|
|
</span>
|
|||
|
|
</label>
|
|||
|
|
<label v-if="form.allowSkip" class="tg nested">
|
|||
|
|
<input v-model="form.skipReasonRequired" type="checkbox" />
|
|||
|
|
<span><strong>ثبت دلیل رد کردن الزامی باشد</strong></span>
|
|||
|
|
</label>
|
|||
|
|
<label class="tg">
|
|||
|
|
<input v-model="form.reportRequired" type="checkbox" />
|
|||
|
|
<span>
|
|||
|
|
<strong>گزارش نتیجه الزامی</strong>
|
|||
|
|
<small>قبل از انجامشدن، کاربر باید نتیجه را بنویسد.</small>
|
|||
|
|
</span>
|
|||
|
|
</label>
|
|||
|
|
<label class="tg">
|
|||
|
|
<input v-model="form.allowAfterDeadline" type="checkbox" />
|
|||
|
|
<span>
|
|||
|
|
<strong>انجام بعد از مهلت مجاز باشد</strong>
|
|||
|
|
<small>اگر خاموش باشد، کار عقبافتاده فقط توسط مدیر بسته میشود.</small>
|
|||
|
|
</span>
|
|||
|
|
</label>
|
|||
|
|
<label class="tg">
|
|||
|
|
<input v-model="form.carryOverOverdue" type="checkbox" />
|
|||
|
|
<span>
|
|||
|
|
<strong>انتقال به روز بعد</strong>
|
|||
|
|
<small>کار انجامنشده در فهرست «کارهای امروز» روز بعد هم نمایش داده میشود.</small>
|
|||
|
|
</span>
|
|||
|
|
</label>
|
|||
|
|
</div>
|
|||
|
|
<UiField label="اتصال به پرونده" class="mt">
|
|||
|
|
<UiSelect v-model="form.entityLink">
|
|||
|
|
<option v-for="[k, label] in entityLinks" :key="k" :value="k">{{ label }}</option>
|
|||
|
|
</UiSelect>
|
|||
|
|
</UiField>
|
|||
|
|
</UiCard>
|
|||
|
|
|
|||
|
|
<UiCard>
|
|||
|
|
<h3 class="sec-title">یادآوری و وضعیت</h3>
|
|||
|
|
<div class="fields">
|
|||
|
|
<UiField label="یادآوری">
|
|||
|
|
<UiSelect v-model="form.reminderMinutesBefore">
|
|||
|
|
<option v-for="o in reminderOptions" :key="o.value" :value="o.value">{{ o.label }}</option>
|
|||
|
|
</UiSelect>
|
|||
|
|
</UiField>
|
|||
|
|
<label class="tg">
|
|||
|
|
<input v-model="form.isActive" type="checkbox" />
|
|||
|
|
<span><strong>روتین فعال است</strong></span>
|
|||
|
|
</label>
|
|||
|
|
</div>
|
|||
|
|
</UiCard>
|
|||
|
|
|
|||
|
|
<div class="summary">
|
|||
|
|
<span>خلاصه</span>
|
|||
|
|
<p>{{ summary }}</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<p v-if="error" class="error">{{ error }}</p>
|
|||
|
|
<div class="actions">
|
|||
|
|
<UiButton type="submit" :loading="saving" block>{{ isEdit ? 'ذخیره تغییرات' : 'ایجاد روتین' }}</UiButton>
|
|||
|
|
<UiButton variant="ghost" block @click="router.push('/routines/manage')">انصراف</UiButton>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</form>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.rform {
|
|||
|
|
display: grid;
|
|||
|
|
gap: var(--sp-4);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.state {
|
|||
|
|
padding: var(--sp-8);
|
|||
|
|
text-align: center;
|
|||
|
|
color: var(--muted);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.grid {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: minmax(0, 1fr) 360px;
|
|||
|
|
gap: var(--sp-4);
|
|||
|
|
align-items: start;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.col {
|
|||
|
|
display: grid;
|
|||
|
|
gap: var(--sp-4);
|
|||
|
|
min-width: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.side {
|
|||
|
|
position: sticky;
|
|||
|
|
top: var(--sp-4);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.sec-title {
|
|||
|
|
margin: 0 0 var(--sp-4);
|
|||
|
|
font-size: var(--fs-md);
|
|||
|
|
font-weight: var(--fw-semibold);
|
|||
|
|
color: var(--ink);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.fields {
|
|||
|
|
display: grid;
|
|||
|
|
gap: var(--sp-4);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.grid-2 {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|||
|
|
gap: var(--sp-3);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.toggles {
|
|||
|
|
display: grid;
|
|||
|
|
gap: var(--sp-3);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tg {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: flex-start;
|
|||
|
|
gap: var(--sp-2);
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tg.nested {
|
|||
|
|
margin-inline-start: 26px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tg input {
|
|||
|
|
width: 18px;
|
|||
|
|
height: 18px;
|
|||
|
|
margin-top: 2px;
|
|||
|
|
accent-color: var(--accent);
|
|||
|
|
flex-shrink: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tg span {
|
|||
|
|
display: grid;
|
|||
|
|
gap: 2px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tg strong {
|
|||
|
|
font-size: var(--fs-sm);
|
|||
|
|
font-weight: var(--fw-medium);
|
|||
|
|
color: var(--ink);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tg small {
|
|||
|
|
font-size: var(--fs-xs);
|
|||
|
|
color: var(--muted);
|
|||
|
|
line-height: 1.6;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.mt {
|
|||
|
|
margin-top: var(--sp-4);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.summary {
|
|||
|
|
padding: var(--sp-3) var(--sp-4);
|
|||
|
|
border-radius: var(--radius);
|
|||
|
|
background: var(--accent-subtle);
|
|||
|
|
border: 1px solid color-mix(in srgb, var(--accent) 25%, transparent);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.summary span {
|
|||
|
|
font-size: var(--fs-xs);
|
|||
|
|
color: var(--accent-deep);
|
|||
|
|
font-weight: var(--fw-semibold);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.summary p {
|
|||
|
|
margin: 4px 0 0;
|
|||
|
|
font-size: var(--fs-sm);
|
|||
|
|
color: var(--ink);
|
|||
|
|
line-height: 1.8;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.actions {
|
|||
|
|
display: grid;
|
|||
|
|
gap: var(--sp-2);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.error {
|
|||
|
|
margin: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@media (max-width: 1100px) {
|
|||
|
|
.grid {
|
|||
|
|
grid-template-columns: 1fr;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.side {
|
|||
|
|
position: static;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@media (max-width: 600px) {
|
|||
|
|
.grid-2 {
|
|||
|
|
grid-template-columns: 1fr;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|