<script setup>
import {ref,computed,watch,onMounted,nextTick} from 'vue';
import {DialogRoot,DialogTrigger,DialogPortal,DialogOverlay,DialogContent,DialogTitle,DialogDescription,DialogClose,
ComboboxRoot,ComboboxAnchor,ComboboxInput,ComboboxTrigger,ComboboxContent,ComboboxItem,ComboboxItemIndicator,ComboboxEmpty,
DropdownMenuRoot,DropdownMenuTrigger,DropdownMenuPortal,DropdownMenuContent,DropdownMenuItem,DropdownMenuLabel,DropdownMenuSeparator,
TabsRoot,TabsList,TabsTrigger,TabsContent,AccordionRoot,AccordionItem,AccordionHeader,AccordionTrigger,AccordionContent,
SwitchRoot,SwitchThumb,PopoverRoot,PopoverTrigger,PopoverPortal,PopoverContent,PopoverClose,
CalendarRoot,CalendarHeader,CalendarPrev,CalendarHeading,CalendarNext,CalendarGrid,CalendarGridHead,CalendarGridRow,CalendarHeadCell,CalendarGridBody,CalendarCell,CalendarCellTrigger} from 'reka-ui';
import {CalendarDate,getDayOfWeek} from '@internationalized/date';
import {scenarios,clients,initialRequests,statusNames,filterRequests,validateProfile,defaults,sanitizeSettings,sameSettings,dateLabel} from './model.js';
const scenario=ref('dialog'),current=computed(()=>scenarios.find(s=>s.id===scenario.value)),index=computed(()=>scenarios.findIndex(s=>s.id===scenario.value));
const notice=ref(''),dialogOpen=ref(false),profile=ref('Анна Смирнова'),draftName=ref(''),nameError=ref(''),nameField=ref(null);
watch(dialogOpen,open=>{if(open){draftName.value=profile.value;nameError.value='';}});
async function saveProfile(){nameError.value=validateProfile(draftName.value);if(nameError.value){await nextTick();nameField.value?.focus();return;}profile.value=draftName.value.trim();dialogOpen.value=false;notice.value='Карточка обновлена в памяти этой страницы.';}
const client=ref('c1'),clientInfo=computed(()=>clients.find(x=>x.id===client.value));
const requests=ref(initialRequests.map(x=>({...x}))),activeRequest=ref('R-101'),request=computed(()=>requests.value.find(x=>x.id===activeRequest.value)),actions=ref([]);
function log(s){actions.value.unshift(s);actions.value=actions.value.slice(0,8);notice.value=s;}
function changeStatus(s){request.value.status=s;log(`${request.value.id}: ${statusNames[s]}`);}
function duplicate(){const id='R-'+(101+requests.value.length);const item={...request.value,id,title:request.value.title+' · копия',status:'new'};requests.value.push(item);activeRequest.value=id;log(`Создана учебная копия ${id}`);}
const tab=ref('overview'),tasks=ref([{id:1,title:'Согласовать поля карточки',done:false},{id:2,title:'Проверить мобильную версию',done:false},{id:3,title:'Подготовить инструкцию',done:true}]);
function completeTask(t){t.done=!t.done;log(`${t.done?'Завершена':'Возобновлена'} задача: ${t.title}`);}
const faqs=[{id:'one',q:'Где сохраняются изменения?',a:'В большинстве примеров — только в памяти вкладки. Общая панель отдельно сохраняет учебные настройки в localStorage этого браузера. Сервер и личный кабинет не подключены.'},{id:'two',q:'Можно ли пользоваться без мыши?',a:'Да. Перейдите Tab к элементу, используйте Enter или Space. В меню и списках работают стрелки, Escape закрывает всплывающее содержимое. Видимая рамка показывает текущий фокус.'},{id:'three',q:'Как перенести это в рабочий сервис?',a:'Скачайте исходники, сохраните структуру Reka-компонентов, подключите свой API и добавьте проверку прав и данных на сервере. После изменения разметки повторите проверку клавиатурой и на мобильном.'}];
const questions=ref(['one']),notifications=ref({email:true,push:false,digest:true});
const notificationText=computed(()=>[notifications.value.email?'письма':null,notifications.value.push?'push':null,notifications.value.digest?'еженедельная сводка':null].filter(Boolean).join(', ')||'все каналы выключены');
const filterOpen=ref(false),filter=ref({status:'all',minimum:0}),draftFilter=ref({...filter.value}),filterError=ref('');
watch(filterOpen,o=>{if(o){draftFilter.value={...filter.value};filterError.value='';}});
const filtered=computed(()=>filterRequests(requests.value,filter.value));
function applyFilter(){if(!Number.isFinite(Number(draftFilter.value.minimum))||Number(draftFilter.value.minimum)<0){filterError.value='Сумма должна быть числом от 0.';return;}filter.value={...draftFilter.value,minimum:Number(draftFilter.value.minimum)};filterOpen.value=false;notice.value=`Фильтр применён: ${filtered.value.length} заявок.`;}
const visit=ref(new CalendarDate(2026,11,16));const isUnavailable=d=>[0,6].includes(getDayOfWeek(d,'en-US'));
const STORE='aen-reka-ui-settings-v1',settings=ref(defaults()),saved=ref(defaults()),settingsError=ref(''),settingsMessage=ref('');
const dirty=computed(()=>!sameSettings(settings.value,saved.value));
function loadSettings(){try{const raw=localStorage.getItem(STORE);saved.value=sanitizeSettings(raw?JSON.parse(raw):defaults());settings.value={...saved.value};settingsMessage.value=raw?'Восстановлены учебные настройки этого браузера.':'Учебные значения ещё не сохранены.';}catch{settingsMessage.value='Хранилище недоступно или данные повреждены. Можно работать в памяти страницы.';}}
function saveSettings(){settingsError.value='';if(settings.value.workspace.trim().length<2||settings.value.workspace.trim().length>60){settingsError.value='Название должно содержать от 2 до 60 символов.';return;}settings.value=sanitizeSettings(settings.value);try{localStorage.setItem(STORE,JSON.stringify(settings.value));saved.value={...settings.value};settingsMessage.value='Сохранено в этом браузере. Это не серверный аккаунт.';}catch{settingsMessage.value='Браузер не разрешил сохранение. Изменения остались в памяти страницы.';}}
function resetSettings(){settings.value=defaults();saved.value=defaults();try{localStorage.removeItem(STORE);settingsMessage.value='Учебные настройки сброшены; сохранённая копия удалена из этого браузера.';}catch{settingsMessage.value='Сброшены значения в памяти. Хранилище недоступно.';}settingsError.value='';}
const source=ref('App.vue'),sources=ref({}),sourceMessage=ref('');
const sourceText=computed(()=>sources.value[source.value]||'Загружаем исходник…');
function download(name,body,type='text/plain;charset=utf-8'){const url=URL.createObjectURL(new Blob([body],{type}));const a=document.createElement('a');a.href=url;a.download=name;a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);}
async function copySource(){try{await navigator.clipboard.writeText(sourceText.value);sourceMessage.value='Исходник скопирован.';}catch{sourceMessage.value='Копирование недоступно. Используйте кнопку скачивания файла.';}}
function exportState(){download('reka-ui-demo-state.json',JSON.stringify({scenario:scenario.value,profile:profile.value,client:clientInfo.value,requests:requests.value,notifications:notifications.value,filter:filter.value,visit:visit.value?.toString(),settings:settings.value},null,2),'application/json');}
onMounted(async()=>{loadSettings();try{const response=await fetch('./sources.json');if(!response.ok)throw Error('HTTP '+response.status);sources.value=await response.json();}catch{sources.value={'App.vue':'Не удалось получить исходники. Скачайте полный ZIP по ссылке ниже.'};}});
watch(scenario,()=>{notice.value='';dialogOpen.value=false;filterOpen.value=false;});
const money=v=>new Intl.NumberFormat('ru-RU',{style:'currency',currency:'RUB',maximumFractionDigits:0}).format(v);
</script>

<template>
<main class="workshop">
 <header><p class="eyebrow">REKA UI + VUE · 9 ИНТЕРФЕЙСОВ</p><h1>Действия, которые понятны человеку</h1><p class="lead">Откройте диалог, найдите клиента, настройте список и сохраните учебную панель. Здесь работают настоящие компоненты Reka UI.</p><p class="mechanism">Все данные вымышленные. Компоненты выполняются в браузере. Первые восемь примеров меняют память вкладки; в девятом можно явно сохранить настройки в этом браузере. Запросов к CRM и отправки уведомлений нет.</p></header>
 <section class="panel controls"><label for="scenario">Практический экран</label><select class="form-select" id="scenario" v-model="scenario"><option v-for="s in scenarios" :value="s.id">{{s.title}}</option></select><div class="button-row"><button class="btn btn-outline-dark" :disabled="index===0" @click="scenario=scenarios[index-1].id">← Предыдущий</button><button class="btn btn-outline-dark" :disabled="index===8" @click="scenario=scenarios[index+1].id">Следующий →</button></div></section>
 <section class="panel demo" aria-labelledby="demo-title"><p class="eyebrow">ГДЕ ПРИМЕНИТЬ</p><h2 id="demo-title">{{current.title}}</h2><p>{{current.use}}</p><p class="try"><strong>Попробуйте:</strong> {{current.try}}</p>
  <div v-if="scenario==='dialog'" class="card-view"><p class="small-label">Карточка клиента «Маяк»</p><h3>{{profile}}</h3><p>Контактное лицо · Казань · учебная запись</p>
   <DialogRoot v-model:open="dialogOpen"><DialogTrigger class="btn btn-primary">Редактировать контакт</DialogTrigger><DialogPortal><DialogOverlay class="dialog-overlay"/><DialogContent class="dialog-content"><DialogTitle class="dialog-title">Контактное лицо</DialogTitle><DialogDescription>Измените учебное имя. Сохранение обновит карточку только на этой странице.</DialogDescription><form @submit.prevent="saveProfile" novalidate><label for="contact-name">Имя</label><input id="contact-name" ref="nameField" v-model="draftName" class="form-control" maxlength="80" :aria-invalid="Boolean(nameError)" :aria-describedby="nameError?'name-error':undefined" autocomplete="off"><p v-if="nameError" id="name-error" class="error" role="alert">{{nameError}}</p><div class="button-row"><button class="btn btn-primary" type="submit">Сохранить контакт</button><DialogClose class="btn btn-outline-dark" type="button">Отмена</DialogClose></div></form></DialogContent></DialogPortal></DialogRoot>
  </div>
  <div v-else-if="scenario==='combobox'"><label for="client-search">Найти клиента по названию или городу</label><ComboboxRoot v-model="client"><ComboboxAnchor class="combo-anchor"><ComboboxInput id="client-search" class="form-control" :display-value="v=>clients.find(c=>c.id===v)?.name||''" placeholder="Например, Маяк"/><ComboboxTrigger class="icon-btn" aria-label="Показать клиентов">⌄</ComboboxTrigger></ComboboxAnchor><ComboboxContent class="combo-content" position="inline"><ComboboxEmpty class="empty">Ничего не найдено. Измените название или город.</ComboboxEmpty><ComboboxItem v-for="c in clients" :key="c.id" :value="c.id" :text-value="c.name+' '+c.city" class="combo-item"><span>{{c.name}} <small>{{c.city}}</small></span><ComboboxItemIndicator>✓</ComboboxItemIndicator></ComboboxItem></ComboboxContent></ComboboxRoot><div class="result" v-if="clientInfo"><strong>{{clientInfo.name}}</strong><p>{{clientInfo.city}} · {{clientInfo.contact}}</p><code>clientId: {{clientInfo.id}}</code></div></div>
  <div v-else-if="scenario==='menu'"><label for="active-request">Заявка</label><select id="active-request" class="form-select" v-model="activeRequest"><option v-for="r in requests" :value="r.id">{{r.id}} · {{r.title}}</option></select><div class="card-view"><div class="section-head"><h3>{{request.title}}</h3><span class="pill">{{statusNames[request.status]}}</span></div><p>{{request.id}} · {{money(request.amount)}}</p><DropdownMenuRoot><DropdownMenuTrigger class="btn btn-primary">Действия с заявкой</DropdownMenuTrigger><DropdownMenuPortal><DropdownMenuContent class="floating-menu" :side-offset="6" :collision-padding="12" align="start"><DropdownMenuLabel class="menu-label">{{request.id}}</DropdownMenuLabel><DropdownMenuItem class="menu-item" @select="changeStatus('progress')" :disabled="request.status==='progress'">Взять в работу</DropdownMenuItem><DropdownMenuItem class="menu-item" @select="changeStatus('done')" :disabled="request.status==='done'">Отметить готовой</DropdownMenuItem><DropdownMenuSeparator class="separator"/><DropdownMenuItem class="menu-item" @select="duplicate">Создать учебную копию</DropdownMenuItem></DropdownMenuContent></DropdownMenuPortal></DropdownMenuRoot></div></div>
  <div v-else-if="scenario==='tabs'"><TabsRoot v-model="tab"><TabsList class="tab-list" aria-label="Разделы карточки клиента"><TabsTrigger class="tab-trigger" value="overview">Обзор</TabsTrigger><TabsTrigger class="tab-trigger" value="tasks">Задачи {{tasks.filter(t=>!t.done).length}}</TabsTrigger><TabsTrigger class="tab-trigger" value="history">История</TabsTrigger></TabsList><TabsContent value="overview" class="tab-content"><h3>Запуск кабинета клиента</h3><p>Клиент: «Маяк». Координатор: {{profile}}.</p><strong>{{tasks.filter(t=>t.done).length}} из {{tasks.length}} задач завершено</strong></TabsContent><TabsContent value="tasks" class="tab-content"><ul class="task-list"><li v-for="t in tasks"><span :class="{completed:t.done}">{{t.title}}</span><button class="btn btn-outline-dark" @click="completeTask(t)">{{t.done?'Возобновить':'Завершить'}}</button></li></ul></TabsContent><TabsContent value="history" class="tab-content"><p v-if="!actions.length">Пока нет действий. Завершите задачу на соседней вкладке.</p><ol v-else><li v-for="a in actions">{{a}}</li></ol></TabsContent></TabsRoot></div>
  <div v-else-if="scenario==='accordion'"><AccordionRoot type="multiple" v-model="questions"><AccordionItem v-for="f in faqs" :value="f.id" class="faq-item"><AccordionHeader class="faq-header"><AccordionTrigger class="faq-trigger">{{f.q}}<span aria-hidden="true">+</span></AccordionTrigger></AccordionHeader><AccordionContent class="faq-content"><p>{{f.a}}</p></AccordionContent></AccordionItem></AccordionRoot></div>
  <div v-else-if="scenario==='switches'"><div v-for="(title,key) in {email:'Письма о заявках',push:'Push-уведомления',digest:'Еженедельная сводка'}" class="switch-row"><label :for="'notify-'+key">{{title}}<small>{{notifications[key]?'Включено':'Выключено'}}</small></label><SwitchRoot :id="'notify-'+key" class="switch" v-model="notifications[key]"><SwitchThumb class="switch-thumb"/></SwitchRoot></div><p class="result" aria-live="polite">Активные каналы: {{notificationText}}. Это настройки примера; реальные сообщения не отправляются.</p></div>
  <div v-else-if="scenario==='popover'"><div class="section-head"><strong>{{filtered.length}} из {{requests.length}} заявок</strong><PopoverRoot v-model:open="filterOpen"><PopoverTrigger class="btn btn-primary">Настроить фильтр</PopoverTrigger><PopoverPortal><PopoverContent class="filter-popover" :side-offset="8" :collision-padding="12" align="end"><h3>Фильтр заявок</h3><label for="filter-status">Статус</label><select id="filter-status" v-model="draftFilter.status" class="form-select"><option value="all">Все статусы</option><option v-for="(v,k) in statusNames" :value="k">{{v}}</option></select><label for="filter-minimum">Сумма от, ₽</label><input id="filter-minimum" v-model="draftFilter.minimum" type="number" min="0" step="1000" class="form-control"><p v-if="filterError" role="alert" class="error">{{filterError}}</p><div class="button-row"><button class="btn btn-primary" @click="applyFilter">Применить фильтр</button><PopoverClose class="btn btn-outline-dark">Отмена</PopoverClose></div></PopoverContent></PopoverPortal></PopoverRoot></div><p class="small-note">Сейчас: {{filter.status==='all'?'все статусы':statusNames[filter.status]}}, от {{money(filter.minimum)}}.</p><div class="request-grid"><article v-for="r in filtered" class="request-card"><small>{{r.id}} · {{statusNames[r.status]}}</small><h3>{{r.title}}</h3><strong>{{money(r.amount)}}</strong></article></div><p v-if="!filtered.length" class="empty">Подходящих заявок нет. Уменьшите сумму или выберите другой статус.</p><button class="btn btn-outline-dark" @click="filter={status:'all',minimum:0}">Сбросить фильтр</button></div>
  <div v-else-if="scenario==='calendar'"><CalendarRoot v-model="visit" v-slot="{grid,weekDays}" locale="ru-RU" :week-starts-on="1" :is-date-unavailable="isUnavailable" class="calendar"><CalendarHeader class="calendar-header"><CalendarPrev class="icon-btn" aria-label="Предыдущий месяц">←</CalendarPrev><CalendarHeading/><CalendarNext class="icon-btn" aria-label="Следующий месяц">→</CalendarNext></CalendarHeader><CalendarGrid v-for="month in grid" :key="month.value.toString()"><CalendarGridHead><CalendarGridRow><CalendarHeadCell v-for="day in weekDays" :key="day">{{day}}</CalendarHeadCell></CalendarGridRow></CalendarGridHead><CalendarGridBody><CalendarGridRow v-for="(week,i) in month.rows" :key="i"><CalendarCell v-for="day in week" :key="day.toString()" :date="day"><CalendarCellTrigger :day="day" :month="month.value" class="calendar-day"/></CalendarCell></CalendarGridRow></CalendarGridBody></CalendarGrid></CalendarRoot><p class="result" aria-live="polite">{{visit?dateLabel(visit.toString()):'Выберите день'}}<br><code>{{visit?.toString()||'Дата не выбрана'}}</code></p><p class="small-note">Выходные недоступны. Дата — календарный день, без времени и смещения зоны. Реального бронирования нет. Calendar в актуальной документации помечен Alpha.</p></div>
  <form v-else-if="scenario==='settings'" @submit.prevent="saveSettings" novalidate><label for="workspace-name">Название рабочего пространства</label><input id="workspace-name" class="form-control" maxlength="60" v-model="settings.workspace" :aria-invalid="Boolean(settingsError)" :aria-describedby="settingsError?'settings-error':undefined"><p v-if="settingsError" id="settings-error" class="error" role="alert">{{settingsError}}</p><label for="density">Плотность списка</label><select id="density" class="form-select" v-model="settings.density"><option value="comfortable">Свободная</option><option value="compact">Компактная</option></select><div v-for="(title,key) in {email:'Письма о заявках',push:'Push-уведомления',digest:'Еженедельная сводка'}" class="switch-row"><label :for="'settings-'+key">{{title}}</label><SwitchRoot :id="'settings-'+key" v-model="settings[key]" class="switch"><SwitchThumb class="switch-thumb"/></SwitchRoot></div><div class="preview" :class="settings.density"><strong>{{settings.workspace||'Название не заполнено'}}</strong><div v-for="r in requests.slice(0,3)"><span>{{r.title}}</span><small>{{statusNames[r.status]}}</small></div></div><p class="small-note">{{dirty?'Есть несохранённые изменения.':'Изменений относительно сохранённой версии нет.'}}</p><div class="button-row"><button class="btn btn-primary" type="submit">Сохранить настройки</button><button class="btn btn-outline-dark" type="button" @click="settings={...saved};settingsError='';settingsMessage='Изменения отменены.'">Отменить изменения</button><button class="btn btn-outline-dark" type="button" @click="resetSettings">Вернуть учебные значения</button></div><p role="status" class="result">{{settingsMessage}}</p></form>
  <p v-if="notice" class="notice" role="status">{{notice}}</p>
 </section>
 <section class="panel"><h2>Возьмите работающий исходник</h2><p>Меняйте оформление вокруг компонентов, сохраняя их связи, подписи и поведение. Экспорт содержит только текущие учебные значения.</p><div class="button-row"><button class="btn btn-outline-dark" @click="exportState">Скачать состояние JSON</button><a class="btn btn-primary" href="../reka-ui-project.zip" download>Скачать весь проект</a></div><label for="source" class="mt-3">Файл проекта</label><select id="source" class="form-select" v-model="source"><option v-for="(_,name) in sources" :value="name">{{name}}</option></select><div class="button-row"><button class="btn btn-outline-dark" @click="copySource">Копировать исходник</button><button class="btn btn-outline-dark" @click="download(source,sourceText)">Скачать файл</button></div><p role="status" class="small-note">{{sourceMessage}}</p><details><summary>Показать исходный код</summary><pre><code>{{sourceText}}</code></pre></details><p class="small-note">Reka UI 2.11.0 · Vue 3.5.45 · Bootstrap 5.3.9. <a href="https://reka-ui.com/docs/overview/introduction" target="_blank" rel="noopener">Официальная документация</a> · <a href="README.md">Windows / Linux / VPS</a> · <a href="THIRD-PARTY-LICENSES.txt">Лицензии</a></p></section>
</main>
</template>
