PageSourceSearch

https://infotecs.ru/local/assets-cabinet/assets/K6XKUDBg.js

js infotecs.ru collected 2026-10-02 04:24:20 UTC 196,543 bytes, 1 lines download raw bytes

1import{dW as e,bF as n,ap as t,N as a,h as r,aV as o,bW as s,bR as i,ck as l,s as c,aE as d,at as u,aA as m,aM as p,v as g,w as f,av as M,bl as b,bc as y,F as h,I as v,J as w,bK as O,aY as F,bP as S,t as j,u as I,as as R,cB as x}from"./rPzCxgu2.js";import _ from"./Djm02ss4.js";import{_ as C}from"./Vf7itc8G.js";import A from"./BvWp3utU.js";import T from"./BZFqKOVV.js";import P from"./CMqbmjDo.js";import{highlightCode as E,detectSourceLanguage as V}from"./D-GLYPUm.js";import{U as z}from"./B2ngeV_0.js";import{p as D}from"./Bw7o3a0Q.js";/* empty css        */import"./DyA0rmCF.js";import"./z1uGv1XI.js";import"./BIVETZJT.js";import"./D1DH0B25.js";import"./DH8wk0CJ.js";import"./Do6aoiPI.js";import"./Brf0lwdd.js";import"./B9HCdCam.js";import"./BP0Qtch8.js";import"./CdFh0n3O.js";import"./CUlxn__V.js";import"./Cigs5uJq.js";import"./D_8At7Qg.js";import"./XSGnpf5l.js";import"./YuzqTGE4.js";import"./CmalZX-0.js";import"./BiVzHQPq.js";import"./BvIBVZyP.js";import"./B6ZI0tfh.js";import"./BCo6x5W8.js";import"./DKpIrpRZ.js";/* empty css        *//* empty css        */import"./WnJqaMM1.js";import"./DEA_DsvZ.js";const L=e=>Object.freeze({href:`https://gitlab.infotecsglobal.net/techart.infotecs.ru/frontend-cabinet/-/blob/main/${e}`,label:e.split("/").pop(),path:e}),k=Object.freeze([Object.freeze({expected:"Проверяет, что destroy() отменяет текущие операции, ровно один раз освобождает все экземпляры и не позволяет заново инициализировать уничтоженный менеджер.",id:"MMV2-T01",source:L("src/classes/ModalManager/__tests__/ModalManagerV2.test.js"),title:"Полностью уничтожить менеджер"}),Object.freeze({expected:"Проверяет ожидание глобального менеджера: истечение времени и AbortSignal должны вернуть предсказуемые ошибки и удалить обработчики событий и таймер.",id:"MMV2-T02",source:L("src/classes/ModalManager/__tests__/waitForModalManagerV2.test.js"),title:"Прервать ожидание готовности"}),Object.freeze({expected:"Проверяет, что неизвестный ID, некорректное описание окна и экземпляр другого менеджера отклоняются без изменения текущего состояния.",id:"MMV2-T03",source:L("src/classes/ModalManager/__tests__/ModalManagerV2.test.js"),title:"Отклонить некорректные входные данные"}),Object.freeze({expected:"Проверяет изоляцию ошибок в lifecycle hook, подписчике и очистке. Зарегистрированные функции очистки должны выполниться один раз в обратном порядке.",id:"MMV2-T04",source:L("src/classes/ModalManager/__tests__/ModalManagerV2.test.js"),title:"Изолировать ошибки и выполнить очистку"}),Object.freeze({expected:"Проверяет параллельные load(), prepare() и open(): одинаковая работа не должна дублироваться, а правила хранения загруженного компонента должны давать стабильный результат.",id:"MMV2-T05",source:L("src/classes/ModalManager/__tests__/ModalManagerV2.test.js"),title:"Разрешить гонки параллельных вызовов"})]),N=e=>({code:e?.code??null,message:e?.message??String(e),name:e?.name??"Error"}),G=Object.freeze({description:"Запускает действие длительностью 1,5 секунды, блокирует его повторный запуск и умеет корректно отменять работу при закрытии окна.",expected:["Пока действие выполняется, поле busyActionId содержит ID кнопки confirm, а кнопка недоступна для повторного запуска.","Повторный вызов получает уже выполняющуюся операцию (single-flight), а не создаёт вторую.","Закрытие окна или сброс кейса отменяет AbortSignal, выполняет очистку и игнорирует запоздавший результат."],group:"Асинхронные действия",
1id:"MMV2-A01",safety:"read-only",tags:["async","busy","abort"],title:"Заблокировать повторный запуск действия",useWhen:"Для асинхронной команды внутри окна, которую нельзя запускать дважды и которую нужно остановить, если пользователь закрыл окно."}),$=Object.freeze(Object.defineProperty({__proto__:null,recipe:G,run:async({log:e,manager:n,own:t})=>{const a=t(await n.prepare("ModalPrompt",{details:"Нажмите «Запустить 1,5 с». На время выполнения кнопка станет недоступной, а busyActionId будет равен confirm."}));a.setText("title","MMV2-A01: асинхронное действие").setAction({closePolicy:"never",id:"confirm",role:"primary",text:"Запустить 1,5 с"}).setActionHandler("confirm",async({flow:n,instance:t,onCleanup:a,signal:r})=>(e("Асинхронное действие начато",t.getState()),!(await new Promise(n=>{const t=setTimeout(e=>n(!0),1500),o=e=>{clearTimeout(t),n(!1)};r.addEventListener("abort",o,{once:!0}),a(n=>{clearTimeout(t),r.removeEventListener("abort",o),e("Ресурсы асинхронного действия очищены")})}))||r.aborted?n.stay({aborted:!0}):(t.setProps({details:"Асинхронное действие завершено; окно осталось открытым."}),e("Асинхронное действие завершено",t.getState()),n.stay({completed:!0})))),await n.open(a),e("ModalPrompt готов к асинхронному действию",a.getState())}},Symbol.toStringTag,{value:"Module"})),U=Object.freeze({description:"Открывает стандартное окно ошибки ModalError и добавляет кнопку повторной попытки, которая не закрывает окно.",expected:["Строка, похожая на HTML-код, выводится обычным текстом и не исполняется браузером.","Каждое нажатие «Повторить» увеличивает счётчик, принадлежащий только этому экземпляру окна.","Результат flow.stay оставляет тот же экземпляр открытым после обработчика."],group:"Стандартные окна",id:"MMV2-C01",safety:"read-only",tags:["ModalError","retry","stay"],title:"Ошибка с повторной попыткой",useWhen:"Для исправимой ошибки интерфейса или API, после которой пользователь может безопасно повторить операцию."}),B=Object.freeze(Object.defineProperty({__proto__:null,recipe:U,run:async({log:e,manager:n,own:t})=>{const a=t(await n.prepare("ModalError",{details:`<script>${["al","ert"].join("")}("не выполнится")<\/script>`,messages:["Демонстрационная ошибка. Данные на сервере не изменяются."]}));a.setText("title","MMV2-C01: ошибка с повторной попыткой").setStore("retryCount",0).setAction({closePolicy:"never",id:"retry",role:"primary",text:"Повторить"}).setActionHandler("retry",({flow:n,instance:t})=>{const a=(t.getStore("retryCount")??0)+1;return t.setStore("retryCount",a).setProps({messages:[`Попытка ${a} выполнена локально.`]}),e("Повторная попытка завершена; окно осталось открытым",{retryCount:a}),n.stay({retryCount:a})}
1),await n.open(a),e("ModalError открыт",a.getState())}},Symbol.toStringTag,{value:"Module"})),W=Object.freeze({description:"Открывает стандартное окно ModalSuccess, сообщает об успешном завершении операции и предлагает закрыть уведомление.",expected:["Короткая форма open принимает title и message; переданный message заменяет default-текст окна.","Пустой message скрывает default-текст, а messages выводит отдельные пункты списка без default-текста.","Кнопка «Закрыть» использует встроенное закрытие и не требует отдельного обработчика.","Promise instance.closed сообщает причину закрытия (reason) и переданный результат (result)."],group:"Стандартные окна",id:"MMV2-C02",safety:"read-only",tags:["ModalSuccess","close","result"],title:"Сообщить об успешном завершении",useWhen:"После окончательно завершённой операции, когда пользователю нужно показать результат и вывести его из текущего шага."}),q=Object.freeze(Object.defineProperty({__proto__:null,recipe:W,run:async({log:e,manager:n,own:t})=>{e("ModalSuccess открыт со встроенным действием закрытия",t(await n.open("ModalSuccess",{message:"Операция завершена в демонстрационном режиме.",title:"MMV2-C02: операция завершена"})).getState())}},Symbol.toStringTag,{value:"Module"})),H=Object.freeze({description:"Открывает окно подтверждения ModalPrompt и отделяет явное согласие от любого способа отмены или закрытия.",expected:["Только кнопка подтверждения увеличивает счётчик выполненных команд businessActionCount.","Отмена, Escape, кнопка закрытия и клик по фону закрывают окно без выполнения команды.","Повторное подтверждение во время уже запущенного действия не создаёт вторую операцию."],group:"Стандартные окна",id:"MMV2-C03",safety:"read-only",tags:["ModalPrompt","confirm","dismiss"],title:"Запросить подтверждение действия",useWhen:"Перед командой, которую разрешено выполнять только после явного согласия пользователя, но не при закрытии окна."}),X=Object.freeze(Object.defineProperty({__proto__:null,recipe:H,run:async({log:e,manager:n,own:t})=>{const a=t(await n.prepare("ModalPrompt",{details:"Подтвердите локальную демонстрационную команду."}));a.setText("title","MMV2-C03: подтвердите действие").setPresentation({dismiss:{backdrop:!0}}).setStore("businessActionCount",0).setActionHandler("confirm",({flow:n,instance:t})=>{const a=(t.getStore("businessActionCount")??0)+1;return t.setStore("businessActionCount",a),e("Команда выполнена после явного подтверждения",{businessActionCount:a}),n.close({businessActionCount:a,confirmed:!0})}),await n.open(a),e("ModalPrompt ожидает решения",a.getState())}},Symbol.toStringTag,{value:"Module"})),J=Object.freeze({description:"Создаёт небольшой текстовый файл прямо в браузере и показывает стандартное окно загрузки ModalDownload.",expected:["Файл не загружается автоматически: пользователь должен явно нажать кнопку загрузки.","Окно получает временный адрес blob:, созданный браузером, поэтому запрос к API не выполняется.","При закрытии окна или сбросе кейса временный Object URL освобождается."],group:"Стандартные окна",id:"MMV2-C04",safety:"local-side-effect",tags:["ModalDownload","Blob","cleanup"],title:"Показать загрузку локального файла",useWhen:"Для проверки интерфейса загрузки без обращения к API и без изменения данных на сервере."}),K=Object.freeze(Object.defineProperty({__proto__:null,recipe:J,run:async({createLocalObjectUrl:e,globalTarget:n,log:t,manager:a,own:r})=>{const o=new n.Blob(["ModalManagerV2 local recipe\n"],{type:"text/plain;charset=utf-8"}),{revoke:s,url:i}=e(o),l=r(await a.prepare("ModalDownload",{autoClose:!1,autoStart:!1,countdown:0,filename:"modal-manager-v2-recipe.txt",url:i}));l.setText("title","MMV2-C04: локальный файл").onDispose(s),await a.open(l),t("ModalDownload открыт с локальным Blob URL",{filename:"modal-manager-v2-recipe.txt",protocol:new n.URL(i).protocol})}},Symbol.toStringTag,{value:"Module"})),Y="Catalog/ProductFilters",Z=Object.freeze([Object.freeze({label:"Фабрика формы в lazy-каталоге",source:"// dependencies: form-generator\nimport { Form, Param
1sMaker } from '@plugins/vue3-form-generator/src/';\n\n// components\nimport UIChipAccordion from '@ui/ChipAccordion/form-generator.js';\nimport UISearchField   from '@ui/SearchField/form-generator.js';\n\n\nlet sequence = 0;\n\nconst demoGroups = [\n  {\n    name:  'productLines',\n    label: 'Линейки продуктов',\n    items: [\n      { id: 'network',        text: 'Защита каналов связи' },\n      { id: 'endpoint',       text: 'Защита конечных устройств' },\n      { id: 'industry',       text: 'Защита систем автоматизации' },\n      { id: 'communications', text: 'Защищённые коммуникации' },\n      { id: 'analytics',      text: 'Аналитика безопасности' },\n    ],\n  },\n  {\n    name:  'productTypes',\n    label: 'Типы продуктов',\n    items: [\n      { id: 'ngfw',       text: 'NGFW' },\n      { id: 'management', text: 'Системы управления' },\n      { id: 'gateway',    text: 'Шлюзы безопасности' },\n      { id: 'clients',    text: 'Клиентские приложения' },\n      { id: 'screens',    text: 'Межсетевые экраны' },\n    ],\n  },\n  {\n    name:  'platforms',\n    label: 'Операционные системы',\n    items: [\n      { id: 'linux',   text: 'Linux' },\n      { id: 'windows', text: 'Windows' },\n      { id: 'android', text: 'Android' },\n      { id: 'macos',   text: 'MacOS' },\n      { id: 'ios',     text: 'iOS' },\n    ],\n  },\n];\n\nexport function createProductFiltersRuntime(input = {}) {\n  sequence += 1;\n  const groups = input.groups ?? demoGroups;\n  const form   = new Form({\n    baseId: 'form-manager',\n    key:    `product-filters-${sequence}`,\n    prefix: 'form-manager-filter-',\n  })\n    .setParams({\n      id:          input.formId ?? `form-manager-product-filters-${sequence}`,\n      buttons:     [ParamsMaker.genButton('send', 'Показать')],\n      hasFieldset: false,\n      hasGroups:   false,\n      hasSteps:    false,\n    })\n    .addField(UISearchField, {\n      name:  'query',\n      label: 'Поиск',\n      value: input.query ?? '',\n      params: {\n        options: { isLabelHidden: true },\n        texts:   { placeholder: 'Название продукта…' },\n      },\n    });\n\n  for (const group of groups) {\n    form.addField(UIChipAccordion, {\n      name:  group.name,\n      label: group.label,\n      value: [...(input.selected?.[group.name] ?? [])],\n      params: {\n        items:         group.items.map(item => ({ ...item })),\n        visibleCount:  group.visibleCount ?? 3,\n        sort:          { by: 'original', direction: 'asc' },\n        selectedFirst: false,\n        showSearch:    false,\n        showReset:     false,\n      },\n    });\n  }\n\n  form.setup().setReady();\n  const { form: dataForm, functions } = form;\n  return { dataForm, functions, title: 'Фильтры' };\n}\n\nexport default createProductFiltersRuntime;\n"}),Object.freeze({label:"Разметка триггера во frontend Blade",source:"<div data-product-filter-trigger>\n  @php\n    echo \\Infotecs\\UiRenderer\\UiRenderer::render('/common/chip/chip', [\n      'id' => 'product-filters-trigger',\n      'name' => 'Фильтры',\n    ], false);\n  @endphp\n</div>"}),Object.freeze({label:"Подключение из отдельного frontend bundle",source:"const waitForServices = (timeout = 10_000) => new Promise((resolve, reject) => {\n  let settled   = false;\n  let resolving = false;\n  const cleanup = _ => {\n    clearTimeout(timer);\n    document.removeEventListener('modal-manager-v2:ready', onReady);\n  };\n  const fail = error => {\n    if (settled) return;\n    settled = true;\n    cleanup();\n    reject(error);\n  };\n  const check = _ => {\n    const { ModalManagerV2, FormManager } = globalThis.infotecs ?? {};\n    if (!ModalManagerV2 || !FormManager || settled || resolving) return;\n    resolving = true;\n    Promise.all([ModalManagerV2.whenReady(), FormManager.whenReady()])\n      .then(_ => {\n        if (settled) return;\n        settled = true;\n        cleanup();\n        resolve({ ModalManagerV2, FormManager });\n      })\n      .catch(fail);\n  };\n  function onReady() { check(); }\n\n  const timer = setTimeout(\n    _ => fail(new Error('Сервисы frontend-cabinet не готовы')),\n    timeout,\n  );\n  document.addEventListener('modal-manager-v2:ready', onReady);\n  check();\n});\n\nconst appliedFilters = new WeakMap();\n\ndocument.addEventListener('product-filters:apply', event => {\n  const trigger = event.target.closest?.('[data-product-filter-trigger]');\n  if (!trigger) return;\n\n  const fields   = Array.isArray(event.detail?.fields) ? event.detail.fields : [];
1\n  const query    = String(fields.find(field => field.name === 'query')?.model ?? '');\n  const selected = Object.fromEntries(fields\n    .filter(field => field.name !== 'query' && Array.isArray(field.model))\n    .map(field => [field.name, [...field.model]]));\n  const count    = (query.trim() ? 1 : 0)\n    + Object.values(selected).reduce((total, values) => total + values.length, 0);\n\n  appliedFilters.set(trigger, { query, selected });\n  const chip = trigger.querySelector('.b-chip-wrapper')?.__interface;\n  chip?.setParam('count', count || null);\n  chip?.setParam('active', count > 0);\n});\n\nlet opening = false;\ndocument.addEventListener('infotecs-ui-click', async event => {\n  const trigger = event.target.closest?.('[data-product-filter-trigger]');\n  if (!trigger || opening) return;\n  opening = true;\n  try {\n    const { ModalManagerV2, FormManager } = await waitForServices();\n    const instance = await FormManager.open('Catalog/ProductFilters', {\n      ...appliedFilters.get(trigger),\n      groups: [\n        {\n          name:  'productLines',\n          label: 'Линейки продуктов',\n          items: [{ id: 'network', text: 'Защита каналов связи' }],\n        },\n        {\n          name:  'platforms',\n          label: 'Операционные системы',\n          items: [{ id: 'linux', text: 'Linux' }],\n        },\n      ],\n    }, {\n      mount:        ModalManagerV2,\n      presentation: { preset: 'sheet', side: 'right' },\n    });\n    const { result } = await instance.closed;\n    if (result) trigger.dispatchEvent(new CustomEvent('product-filters:apply', {\n      bubbles: true,\n      detail:  result,\n    }));\n  } catch (error) {\n    trigger.dispatchEvent(new CustomEvent('product-filters:error', {\n      bubbles: true,\n      detail:  error,\n    }));\n  } finally {\n    opening = false;\n  }\n});\n"})]),Q=e=>{const n=Array.isArray(e?.fields)?e.fields:[],t=String(n.find(e=>"query"===e.name)?.model??""),a=Object.fromEntries(n.filter(e=>"query"!==e.name&&Array.isArray(e.model)).map(e=>[e.name,[...e.model]])),r=(t.trim()?1:0)+Object.values(a).reduce((e,n)=>e+n.length,0);return{query:t,selected:a,count:r}},ee=Object.freeze({description:"UIChip открывает боковую шторку с FormGenerator-фильтром: одно поле общего поиска и несколько групп ChipAccordion без внутреннего поиска. Форма принадлежит lazy-каталогу frontend-cabinet, а потребитель передаёт группы и получает результат через FormManager.",expected:["Нажатие на чип «Фильтры» открывает справа шторку с поиском и тремя группами вариантов.","В каждой группе видны первые три варианта, остальные раскрываются; отдельного поиска внутри ChipAccordion нет.","«Показать» закрывает форму: цифра на чипе равна числу выбранных вариантов плюс один за непустой поисковый запрос.","Повторное открытие создаёт новый FormInstance с сохранёнными критериями; сброс кейса закрывает шторку и очищает чип."],group:"Стандартные окна",id:"MMV2-C05",safety:"read-only",tags:["UIChip","ChipAccordion","FormManager","sheet"],title:"Фильтр продуктов в шторке",trigger:"chip",useWhen:"Когда отдельный frontend-скрипт должен открыть форму-фильтр по стабильному ID после готовности сервисов, не импортируя Vue и адаптеры FormGenerator.",warnings:["Демонстрационные варианты не являются актуальным каталогом продуктов; реальные группы и обработку результата задаёт вызывающий код.","Поиск — отдельный критерий формы, а не фильтрация чипов внутри групп.","В примере для frontend атрибут data-product-filter-trigger ставится на обёртку смонтированного UIChip; событие infotecs-ui-click всплывает от кнопки.","Скрипт frontend обрабатывает события product-filters:apply и product-filters:error на обёртке чипа; пример не меняет список продуктов.","Форму можно зарегистрировать во время работы страницы через setForm(), но это меняет общий каталог форм. Здесь шаблон подготовлен в frontend-cabinet, а варианты передаются при открытии из frontend."]}),ne=Object.freeze(Object.defineProperty({__proto__:null,default:ee,readProductFilters:Q,recipe:ee,run:async({appliedFilters:n={},globalTarget:t,log:a,namespaceKey:r,onFiltersApplied:o=e=>null,own:s})=>{const i=t[r],l=i?.ModalManagerV2,c=i?.FormManager;if(!l||!c)throw new Error("ModalManagerV2 and FormManager are required");await Promise.all([l.whenReady(),c.whenReady()]);const d=s(await c.open(Y,{query:n.query??"",selected:n.selected??{}},{mount:l,onSubmit:n=>{const t=n?.sendData?.data??null,r=Q(t);return o(r),a("Критерии фильтра переданы потребителю",{filters:r,result:t}),e.close(t)},presentation:{preset:"sheet",side:"right"}}));a("Фильтр открыт по ID готовой формы",{formId:Y,instanceId:d.instanceId,modalId:d.mountResult.id})},supportingSources:Z},Symbol.toStringTag,{value:"Module"})),te=Object.freeze({description:"Показывает разницу между повторным вызовом open() для существующего экземпляра и созданием нового окна после закрытия.",expected:["Повторный open() до закрытия возвращает тот же объект и не создаёт дубликат окна.","Кнопка «Закрыть и открыть новый» окончательно завершает первый экземпляр.","Следующий prepare() создаёт новый instanceId для того же типа модального окна."],group:"Жизненный цикл",id:"MMV2-L01",safety:"read-only",tags:["repeated-open","closed","identity"],title:"Повторно открыть окно после закрытия",useWhen:"Чтобы не создавать дубликаты уже открытого окна и правильно начинать новый жизненный цикл после его закрытия."}),ae=Object.freeze(Object.defineProperty({__proto__:null,recipe:te,run:async({log:e,manager:n,own:t,signal:a})=>{const r=t(await n.prepare("ModalSuccess",{messages:["Первый экземпляр."]}));r.setText("title","MMV2-L01: первый экземпляр").setAction({closePolicy:"never",id:"reopen",role:"primary",text:"Закрыть и открыть новый"}).setActionHandler("reopen",({flow:e})=>e.close({reopen:!0}));const o=await n.open(r),s=await n.open(r);e("Повторный open() вернул существующий экземпляр",{identity:s===o,instanceId:r.instanceId}),r.closed.then(async({result:o})=>{if(!o?.reopen||a.aborted)return;const s=t(await n.prepare("ModalSuccess",{messages:["Это новый экземпляр, созданный после полного закрытия предыдущего."]}));s.setText("title","MMV2-L01: новый экземпляр").setAction({id:"close",role:"primary",text:"Закрыть"}),await n.open(s),e("После закрытия создан новый экземпляр",{changed:s.instanceId!==r.instanceId,first:r.instanceId,fresh:s.instanceId})}).catch(n=>e("Не удалось открыть новый экземпляр",{code:n?.code??null,message:n?.message??String(n)},"error"))}},Symbol.toStringTag,{value:"Module"})),re=Object.freeze({description:"Открывает окно подтверждения, затем показывает поверх него окно ошибки и сохраняет нижний экземпляр.",expected:["Нижнее окно ModalPrompt получает статус covered — оно перекрыто, но остаётся смонтированным.","После закрытия верхнего ModalError нижнее окно снова получает статус open и становится доступным.","Стек показа и activeModalId всегда указ
1ывают на фактический порядок окон."],group:"Жизненный цикл",id:"MMV2-L02",safety:"read-only",tags:["stack","covered","reveal"],title:"Открыть окно поверх другого",useWhen:"Для последовательности зависимых окон, где нижнее окно должно сохранить введённые данные, пока пользователь работает с верхним."}),oe=Object.freeze(Object.defineProperty({__proto__:null,recipe:re,run:async({log:e,manager:n,own:t})=>{const a=t(await n.prepare("ModalPrompt",{details:"Закройте верхнюю ошибку, чтобы вернуться сюда."}));a.setText("title","MMV2-L02: нижнее окно подтверждения"),await n.open(a);const r=t(await n.prepare("ModalError",{messages:["Это верхнее окно в стеке показа."]}));r.setText("title","MMV2-L02: верхняя ошибка"),await n.open(r),e("Два окна открыты слоями",{error:r.getState(),manager:n.getState(),prompt:a.getState()})}},Symbol.toStringTag,{value:"Module"})),se=Object.freeze({description:"После подтверждения заменяет текущее окно ModalPrompt новым окном ModalSuccess без промежуточного пустого состояния.",expected:["Сначала открывается целевое окно, затем исходное закрывается с причиной replaced.","После перехода в стеке показа остаётся только ModalSuccess.","Каталог регистрирует новое окно как принадлежащее кейсу и безопасно закрывает его при сбросе."],group:"Жизненный цикл",id:"MMV2-L03",safety:"read-only",tags:["replace","flow.open","ownership"],title:"Заменить одно окно другим",useWhen:"Когда следующий шаг процесса должен полностью заменить текущее окно, например подтверждение — сообщением об успехе."}),ie=Object.freeze(Object.defineProperty({__proto__:null,recipe:se,run:async({log:e,manager:n,own:t,signal:a})=>{const r=t(await n.prepare("ModalPrompt",{details:"Нажмите подтверждение: это окно будет заменено сообщением об успехе."}));r.setText("title","MMV2-L03: исходное окно подтверждения").setActionHandler("confirm",({flow:e})=>e.open("ModalSuccess",{messages:["Окно подтверждения заменено без промежуточного пустого стека."]},{transition:"replace"})),r.closed.then(r=>{if("replaced"!==r.reason||a.aborted)return;const o=n.getInstance("ModalSuccess");o&&t(o),e("Замена одного окна другим завершена",{closed:r,manager:n.getState(),targetInstanceId:o?.instanceId??null})}),await n.open(r),e("Окно подтверждения ожидает команды для перехода",r.getState())}},Symbol.toStringTag,{value:"Module"})),le=Object.freeze({description:"Открывает ModalSuccess и через 3 секунды вызывает instance.close(). Раннее закрытие или сброс кейса отменяет таймер.",expected:["Окно закрывается автоматически через 3 секунды после показа.","В журнале результат закрытия содержит result.timedOut: true; reason остаётся programmatic.","При закрытии кнопкой или крестиком раньше срока таймер отменяется.","Повторный запуск создаёт новый таймер. Сброс кейса очищает его."],group:"Жизненный цикл",id:"MMV2-L04",safety:"read-only",tags:["timeout","setTimeout","close","onDispose"],title:"Закрыть окно по таймеру",useWhen:"Для краткого сообщения, которое должно исчезнуть автоматически через заданное время.",warnings:["Таймер вызывает instance.close(), а не flow.close(): flow нужно вернуть из обработчика действия.","Закрытие проходит обычный beforeClose. Если обработчик запретит закрытие, повторной попытки по таймеру не будет."]}),ce=Object.freeze(Object.defineProperty({__proto__:null,recipe:le,run:async({log:e,manager:n,own:t,registerCleanup:a,signal:r})=>{const o=t(await n.prepare("ModalSuccess",{message:"Окно закроется через 3 секунды. Можно закрыть его раньше кнопкой или крестиком.",title:"MMV2-L04: Ð
1·Ð°ÐºÑ€Ñ‹Ñ‚ие по таймеру"}));let s;const i=e=>clearTimeout(s),l=a(i);o.onDispose(e=>{i(),l()}),o.closed.then(n=>{r.aborted||e(n.result?.timedOut?"Окно закрыто по таймеру":"Окно закрыто раньше срока; таймер отменён",n)}),await n.open(o),r.aborted||["closing","closed"].includes(o.status)||(s=setTimeout(n=>{o.close({timedOut:!0}).then(n=>{n||r.aborted||e("Закрытие по таймеру отменено")}).catch(n=>{r.aborted||e("Ошибка закрытия по таймеру",{message:n.message})})},3e3),e("Таймер запущен: 3 секунды",{instanceId:o.instanceId}))}},Symbol.toStringTag,{value:"Module"})),de=Object.freeze({description:"Подписывается на снимки состояния менеджера и на публичные события страницы, чтобы показать оба способа наблюдения.",expected:["Подписчик сразу получает текущий снимок и затем каждый снимок с новой revision.","DOM CustomEvent имеют префикс modal-manager-v2 и содержат сериализуемые данные в поле detail.","Сброс кейса отписывает функцию-наблюдатель и удаляет все DOM-обработчики."],group:"Состояние и события",id:"MMV2-O01",safety:"read-only",tags:["subscribe","events","revision"],title:"Наблюдать состояние и события",useWhen:"Для диагностики жизненного цикла окон или подключения внешнего наблюдателя, который читает изменения, но не управляет менеджером."}),ue=Object.freeze(Object.defineProperty({__proto__:null,recipe:de,run:async({eventTarget:e,log:n,manager:t,own:a,registerCleanup:r})=>{r(t.subscribe(e=>{n("Подписчик менеджера получил снимок состояния",{activeModalId:e.activeModalId,openCount:e.openCount,revision:e.revision,stack:e.stack})}));const o=["opened","covered","revealed","closed","action-start","action-completed"].map(e=>`modal-manager-v2:${e}`),s=e=>n(`DOM event ${e.type}`,e.detail);o.forEach(n=>e?.addEventListener(n,s)),r(n=>o.forEach(n=>e?.removeEventListener(n,s)));const i=a(await t.prepare("ModalSuccess",{messages:["Закройте окно и сравните записи о снимках состояния и DOM-событиях в журнале кейса."]}));i.setText("title","MMV2-O01: наблюдаемость"),await t.open(i)}},Symbol.toStringTag,{value:"Module"})),me=Object.freeze({description:"Показывает два места для временных данных: общую именованную область менеджера и хранилище отдельного экземпляра окна.",expected:["Общие значения читаются только по явно заданному имени области (manager scope), поэтому разные интеграции не смешивают данные.","Значения экземпляра доступны только через соответствующий ModalInstance и намеренно не входят в публичный снимок состояния.","Закрытие удаляет данные экземпляра автоматически, а сброс кейса явно очищает созданную им общую область."],group:"Состояние и события",id:"MMV2-O02",safety:"read-only",tags:["store","scope","cleanup"],title:"Хранить временные данные в разных областях",useWhen:"Когда одним временным данным нужно жить до закрытия конкретного окна, а другим — дольше и под ответственностью интеграции."}),pe=Object.freeze(Object.defineProperty({__proto__:null,recipe:me,run:async({log:e,manager:n,own:t,registerCleanup:a})=>{const r="modal-manager-v2-catalog:MMV2-O02";n.setStore(r,{recipeId:"MMV2-O02",source:"catalog"}),a(e=>n.clearStore(r));const o=t(await n.prepare("ModalSuccess",{messages:["Значения временных хранилищ не попадают в публичный снимок состояния менеджера."]}));o.setText("title","MMV2-O02: области временных данных").setStore({localStep:1,owner:o.instanceId}).onDispose(n=>e("Область данных экземпляра очищена при его освобождении")),await n.open(o),e("Сравнение двух областей временных данных",{instanceStore:o.getStore(["localStep","owner"]),managerState:n.getState(),managerStore:n.getStore(r,["recipeId","source"])})}},Symbol.toStringTag,{value:"Module"})),ge=Object.freeze({description:"Открывает три стандартных окна слоями, чтобы визуально сравнить варианты размещения sheet, dialog и dialog-lg.",expected:["В стеке показа одновременно находятся три экземпляра с разными вариантами внешнего вида 
1(presentation preset).","Сначала закрывается верхнее большое окно dialog-lg, затем становятся доступны dialog и sheet.","Перекрытые окна сохраняются в DOM и своё состояние; закрытие верхнего слоя не создаёт их заново."],group:"Внешний вид",id:"MMV2-P01",safety:"read-only",tags:["sheet","dialog","dialog-lg"],title:"Сравнить варианты размещения окон",useWhen:"Чтобы выбрать подходящий размер и расположение модального окна и проверить поведение нескольких слоёв."}),fe=Object.freeze(Object.defineProperty({__proto__:null,recipe:ge,run:async({log:e,manager:n,own:t})=>{const a=t(await n.prepare("ModalError",{messages:["Нижний слой использует вариант sheet."]}));a.setText("title","Вариант размещения: sheet");const r=t(await n.prepare("ModalPrompt",{details:"Средний слой использует вариант dialog."}));r.setText("title","Вариант размещения: dialog").setPresentation({preset:"dialog",size:"sm"});const o=t(await n.prepare("ModalSuccess",{messages:["Верхний слой использует вариант dialog-lg. Закрывайте окна сверху вниз."]}));o.setText("title","Вариант размещения: dialog-lg").setPresentation({preset:"dialog-lg"}),await n.open(a),await n.open(r),await n.open(o),e("Стек из трёх вариантов размещения сформирован",n.getState())}},Symbol.toStringTag,{value:"Module"})),Me=Object.freeze({description:"Открывает окно, а затем меняет его заголовок, содержимое, размер, правила закрытия и публичный CSS-класс.",expected:["Через 700 мс содержимое и заголовок меняются, но instanceId остаётся прежним.","Размер и правила закрытия обновляются без повторного создания компонента (remount).","Публичный CSS-класс добавляет видимую рамку и удаляется вместе с экземпляром окна."],group:"Внешний вид",id:"MMV2-P02",safety:"read-only",tags:["reactivity","setProps","setClasses"],title:"Обновить уже открытое окно",useWhen:"Когда открытое окно должно показать новые данные, не теряя введённые значения и другое локальное состояние компонента."}),be=Object.freeze(Object.defineProperty({__proto__:null,recipe:Me,run:async({log:e,manager:n,own:t,wait:a})=>{const r=t(await n.prepare("ModalError",{details:"Обновление произойдёт через 700 мс.",messages:["Начальное содержимое."]}));r.setText("title","MMV2-P02: начальное состояние"),await n.open(r),await a(700)&&"closed"!==r.status&&(r.setText("title","MMV2-P02: окно обновлено без пересоздания").setProps({details:"Идентификатор экземпляра instanceId сохранился.",messages:["Содержимое, размер, правила закрытия и CSS-класс обновлены без пересоздания окна."]}).setPresentation({dismiss:{backdrop:!0},size:"lg"}).setClasses({surface:"modal-manager-v2-catalog__demo-surface"}),e("Экземпляр обновлён без пересоздания компонента",r.getState()))}},Symbol.toStringTag,{value:"Module"})),ye=(e,t,a,r)=>{const o=e=>{const n=t.getState();if(["closing","closed"].includes(n.status))return;if(n.busyActionId===a)return;const o=t.getAction(a);o&&o.disabled!==r.value&&t.setAction({id:a,disabled:r.value})},s=n(r,o,{flush:"sync"}),i=e.subscribe(o);t.onDispose(e=>{s(),i()})},he=Object.freeze({description:"Флаг isBlocked хранится вне компонента окна. Флажок меняет его значение, а watch обновляет disabled кнопки через instance.setAction().",expected:["При открытии флажок установлен, а кнопка «Подтвердить» заблокирована.","Снимите флажок: кнопка станет доступна. Установите снова: она заблокируется без пересоздания окна.","Нажмите «Подтвердить» и измените флаг во время проверки. После завершения кнопка получит последнее значение флага.","Закрытие окна снимает подписки; повторный запуск начинает кейс с исходного состояния."],group:"Внешний вид",id:"MMV2-P03",safety:"read-only",tags:["setAction","disabled","boolean","watch"],title:"Блокировать кнопку по флагу",useWhen:"Когда доступность действия зависит от заполнения формы, результата проверки или другого реактивного значения.",warnings:["disabled принимает boolean. Передача ref или функции вместо значения не создаёт подписку.","Выполняющийся action менять нельзя. Подписка на состояние менеджера применит отложенное значение после его завершения."]}),ve=Object.freeze(Object.defineProperty({__proto__:null,bindActionDisabled:ye,recipe:he,run:async({log:e,manager:n,own:s})=>{const i=o(!0),l=o("Снимите флажок, чтобы разрешить подтверждение."),c=t(a({name:"RecipeDisabledAction",setup:e=>e=>r("div",[r(z,{params:{id:"mmv2-disabled-action-flag",model:i.value,texts:{label:"Блокировать кнопку подтверждения"}},onChange:e=>
1{i.value=Boolean(e.model)}}),r("p",`isBlocked: ${i.value}`),r("p",{role:"status"},l.value)])})),d=s(await n.prepare("ModalManagerV2CatalogApp/ModalRecipeContent",{contentComponent:c}));d.setText("title","MMV2-P03: кнопка зависит от флага").setPresentation({size:"sm"}).setAction({id:"confirm",text:"Подтвердить",role:"primary",closePolicy:"never"}).setActionHandler("confirm",async({flow:n,onCleanup:t,signal:a})=>(l.value="Проверка выполняется. Можно изменить флаг.",!(await new Promise(e=>{const n=setTimeout(n=>e(!0),1500),r=n=>e(!1);a.addEventListener("abort",r,{once:!0}),t(e=>{clearTimeout(n),a.removeEventListener("abort",r)})}))||a.aborted||(l.value="Проверка завершена. Кнопка снова зависит от флага.",e("Подтверждение выполнено",{isBlocked:i.value})),n.stay())),ye(n,d,"confirm",i),await n.open(d),e("Кнопка связана с флагом isBlocked",{instanceId:d.instanceId})}},Symbol.toStringTag,{value:"Module"})),we=Object.freeze({description:"Открывает немодальную панель рядом с кнопкой запуска. Страница остаётся доступной; положение пересчитывается при прокрутке и изменении размера.",expected:["Фон не затемняется, прокрутка страницы доступна.","Escape и клик снаружи закрывают панель; у края экрана она меняет сторону.","Повторный запуск создаёт новый экземпляр после закрытия предыдущего."],group:"Внешний вид",id:"MMV2-P04",safety:"read-only",tags:["anchored-dialog","popover","modal: false"],title:"Немодальная панель у кнопки",useWhen:"Для палитр и небольших вспомогательных форм на странице."}),Oe=Object.freeze(Object.defineProperty({__proto__:null,recipe:we,run:async({anchor:e,log:n,manager:t,own:a})=>{const r=a(await t.prepare("ModalPrompt",{details:"Прокрутите страницу: панель следует за кнопкой и остаётся внутри видимой области."}));r.setText("title","Немодальная панель").setPresentation({preset:"anchored-dialog",modal:!1,size:"sm"}).setAnchor(e),await t.open(r),n("Панель открыта без блокировки страницы",r.getState())}},Symbol.toStringTag,{value:"Module"})),Fe=t(a({props:{openPanel:l(Function,null,!0)},setup:e=>{const{$b:n}=s("b-modal-v2"),t=o(0);return a=>r("div",[r("p",`Счётчик родителя: ${t.value}`),r("div",{class:n`,actions`},[r(i,{buttonText:"Изменить родителя",type:"secondary",onAction:e=>{t.value++}}),r(i,{buttonText:"Открыть дочернюю панель",type:"secondary",onAction:n=>e.openPanel(n.event.currentTarget)})])])}})),Se=Object.freeze({description:"Открывает обычное окно, внутри которого можно вызвать немодальную панель. Родитель сохраняет состояние, а страница остаётся заблокированной родительским окном.",expected:["Счётчик родителя можно менять при открытой панели: модальность родителя не усиливается.","Первый Escape закрывает панель, следующий — родительское окно.","Крестик родителя закрывает оба экземпляра; дочерняя панель получает причину owner-closed."],group:"Внешний вид",id:"MMV2-P05",safety:"read-only",tags:["anchored-dialog","setOwner","nested","modal: false"],title:"Панель внутри модального окна",useWhen:"Для палитры или вспомогательной формы в уже открытом диалоге."}),je=Object.freeze(Object.defineProperty({__proto__:null,recipe:Se,run:async({log:e,manager:n,own:t})=>{let a=null;const r=t(await n.prepare("ModalManagerV2CatalogApp/ModalRecipeContent",{contentComponent:Fe,contentProps:{openPanel:async o=>{try{if(a&&"closed"!==a.status)return void(await a.close());a=t(await n.prepare("ModalPrompt",{details:"Родитель остаётся доступным. Эта панель принадлежит ему."})),a.clearActions().setText("title","Дочерняя панель").setPresentation({preset:"anchored-dialog",modal:!1,size:"sm",dismiss:{outside:!1}}).setAnchor(o).setOwner(r),await n.open(a)}catch(s){a&&"closed"!==a.status&&await a.close(),e("Панель не открылась",{message:s.message},"error")}}}}));r.setText("title","Родительское окно"),await n.open(r)}},Symbol.toStringTag,{value:"Module"})),Ie=Object.freeze({description:"Использует ту же привязку к кнопке, но открывает полноценный блокирующий диалог через presentation.modal: true.",expected:["Окно привязано к кнопке, фон затемнён и недоступен.","Фокус удерживается в диалоге; Escape и крестик закрывают окно.","После закрытия восстанавливаются прокрутка и фокус."],group:"Внешний вид",id:"MMV2-P06",safety:"read-only",tags:["anchored-dialog","modal: true"],title:"Модальное окно у кнопки",useWhen:"Когда диалог нужно расположить рядом с источником действия и временно заблокировать остальную страницу."}),Re=Object.freeze(Object.defineProperty({__proto__:null,recipe:Ie,run:async({anchor:e,log:n,manager:t,own:a})=>{const r=a(await t.prepare("ModalPrompt",{details:"Модальность задаётся отдельно от расположения окна."}));r.setText("title","Привязанный модальный диалог").setPresentation({preset:"anchored-dialog",modal:!0,size:"sm"}).setAnchor(e),await t.open(r),n("Открыт модальный диалог у кнопки",r.getState())}},Symbol.toStringTag,{value:"Module"})),xe=Object.freeze({description:"Проверяет, что каталог подключён к готовому менеджеру по ключу globalThis.infotecs.ModalManagerV2 и не создаёт его копию.",expected:["Ключ globalThis.infotecs.ModalManagerV2 указ
1ывает на тот же объект, который использует каталог.","Старый менеджер V1 остаётся доступен отдельно по ключу globalThis.infotecs.ModalManager.","getState() возвращает неизменяемый снимок: внешний код не может переписать его поля или список экземпляров."],group:"Подключение и API",id:"MMV2-R01",safety:"read-only",tags:["ready","global","snapshot"],title:"Проверить подключение к общему менеджеру",useWhen:"При добавлении новой точки входа, чтобы убедиться: она ждёт запуск приложения и использует общий менеджер, а не создаёт второй."}),_e=Object.freeze(Object.defineProperty({__proto__:null,recipe:xe,run:async({globalTarget:e,log:n,manager:t,namespaceKey:a})=>{const r=await t.whenReady(),o=e[a]??{},s=t.getState();n("Проверены готовность и глобальный ключ менеджера",{immutableInstances:Object.isFrozen(s.instances),immutableState:Object.isFrozen(s),legacyAvailable:Boolean(o.ModalManager),readyIdentity:r===t,v2GlobalIdentity:o.ModalManagerV2===t}),n("Текущий снимок состояния менеджера",s)}},Symbol.toStringTag,{value:"Module"})),Ce=Object.freeze({description:"Последовательно вызывает prepare(), настраивает подготовленный экземпляр цепочкой методов и передаёт его в open().",expected:["Каждый метод настройки возвращает тот же ModalInstance, поэтому вызовы можно объединять в цепочку.","open(instance) возвращает тот же объект и показывает соответствующее окно.","После закрытия статус экземпляра становится closed; повторно использовать завершённый экземпляр нельзя."],group:"Подключение и API",id:"MMV2-R02",safety:"read-only",tags:["prepare","fluent","identity"],title:"Подготовить, настроить и открыть окно",useWhen:"Когда содержимое, кнопки и внешний вид окна нужно полностью настроить до первого показа пользователю."}),Ae=Object.freeze(Object.defineProperty({__proto__:null,recipe:Ce,run:async({log:e,manager:n,own:t})=>{const a=t(await n.prepare("ModalSuccess",{messages:["Экземпляр подготовлен до показа."]})),r=a.setText("title","MMV2-R02: подготовленный экземпляр").setPresentation({preset:"dialog",size:"sm"}).setAction({id:"close",role:"primary",text:"Закрыть кейс"}),o=await n.open(a);e("Проверено сохранение одного экземпляра при настройке и открытии",{fluentIdentity:r===a,instanceId:a.instanceId,openIdentity:o===a,state:a.getState()})}},Symbol.toStringTag,{value:"Module"})),Te="<template>\n  <div :class=\"$b`,content`\">\n    <component\n      :is=\"contentComponent\"\n      v-bind=\"contentProps\" />\n  </div>\n</template>\n\n<script>\n// classes\nimport { defineModalConfig } from '@classes/ModalManager/index.js';\n\n\nexport const modalConfig = defineModalConfig({\n  actionPreset: 'close',\n  presentation: { preset: 'dialog' },\n});\n\nexport default { name: 'ModalRecipeContent' };\n<\/script>\n\n<script setup>\n// composables\nimport { useBEM } from '@cms/useBEM';\n\n\n//================================= composables\nconst { $b } = useBEM('b-modal-v2');\n\n\n//================================= props\ndefineProps({\n  contentComponent: {\n    type:     [Object, Function],\n    required: true,\n  },\n  contentProps: {\n    type:    Object,\n    default: _ => ({}),\n  },\n});\n<\/script>\n",Pe=Object.freeze(["code","em","li","ol","p","strong","ul"]),Ee=e=>D.sanitize(e,{ALLOWED_ATTR:[],ALLOWED_TAGS:Pe,ALLOW_ARIA_ATTR:!1,ALLOW_DATA_ATTR:!1}),Ve=t(a({name:"RecipeSafeHtmlContent",props:{html:{type:String,default:""}},setup:e=>{const n=c(n=>Ee(e.html));return e=>r("div",{"data-test":"mmv2-recipe-html-content",innerHTML:n.value})}})),ze=Object.freeze([Object.freeze({label:"Vue-оболочка произвольного содержимого",source:Te})]),De=Object.freeze({description:"Передаёт HTML-строку в Vue-компонент, который очищает её непосредственно перед HTML-sink с помощью DOMPurify и строгого списка разрешённых тегов.",expected:["Абзац, список, выделение и inline code отображаются как разметка.","img, обработчик onerror, ссылка и javascript: URL не попадают в DOM; текст ссылки остаётся читаемым.","Менеджер получает уже доверенный Vue-компонент; кейс не регистрирует runtime modal и не создаёт второй manager."],group:"Произвольное содержимое",id:"MMV2-X01",safety:"read-only",tags:["HTML","DOMPurify","XSS","allowlist"],title:"Показать очищенное HTML-содержимое",useWhen:"Только когда бизнес-требование действительно требует разметку из строки. Для обычного текста используйте интерполяцию Vue, а для структурированного интерфейса — Vue-компонент.",warnings:["v-html и innerHTML являются XSS-sink: авторизация страницы и отсутствие тега script не делают непроверенную строку безопасной.","Очищать нужно у самого sink. После DOMPurify нельзя дописывать разметку строками или передавать результат коду, который снова меняет HTML.","Allowlist этого кейса намеренно не разрешает ссылки, изображения, стили и любые атрибуты; расширение списка требует отдельного security review."]}),Le=Object.freeze(Object.defineProperty({__proto__:null,recipe:De,run:async({log:e,manager:n,own:t})=>{const a=t(await n.prepare("ModalManagerV2CatalogApp/ModalRecipeContent",{contentComponent:Ve,contentProps:{html:'\n  <p><strong>Разрешённая разметка</strong> остаётся в окне.</p>\n  <ul>\n    <li>Списки, <em>выделение</em> и <code>inline code</code>.</li>\n  </ul>\n  <img src="missing" onerror="alert(\'xss\')">\n  <a href="javascript:alert(\'xss\')">Опасная ссылка становится обычным текстом.</a>\n'}}));a.setText("title","MMV2-X01: очищенное HTML-содержимое").setPresentation({size:"sm"}),await n.open(a),e("HTML передан в компонент с очисткой у sink",{allowedTags:Pe,instanceId:a.instanceId})},sanitizeRecipeHtml:Ee,supportingSources:ze},Symbol.toStringTag,{value:"Module"})),ke=t(a({name:"RecipeJsConfiguredContent",props:{description:{type:String,default:""},phase:{type:String,default:""}},setup:e=>n=>r("div",{"data-test":"mmv2-recipe-js-content"},[r("p",[r("strong","Состояние: "),e.phase]),r("p",e.description)])})),Ne=(e,n)=>e.setText("title","MMV2-X02: тело изменено через public API").setProps({contentProps:n}),Ge=Object.freeze([Object.freeze({label:"Vue-оболочка произвольного содержимого",source:Te})]),$e=Object.freeze({description:"Передаёт ModalInstance в обычную JS-функцию. Функция меняет props вложенного компонента через setProps() и заголовок через setText(), не получая ссылку на DOM.",expected:["Окно открывается с начальным текстом и через 700 мс показывает новое состояние.","instanceId и локальный Vue-экземпляр сохраняются: manager обновляет props без remount.","Функция использует только публичный ModalInstance API; querySelector, innerHTML и ручная замена DOM не нужны."],group:"Произвольное содержимое",id:"MMV2-X02",safety:"read-only",tags:["JavaScript","setProps","
1setText","no DOM"],title:"Изменить тело JS-функцией через API",useWhen:"Когда данные приходят после открытия окна или общий configurator должен обновить представление, сохранив lifecycle, реактивность и локальное состояние Vue-компонента.",warnings:["Прямая правка DOM обходит Vue render tree, может быть затёрта следующим render и создаёт XSS-, focus- и cleanup-риски.","Функция должна получать ModalInstance или предметный callback, а не HTMLElement внутренней поверхности окна."]}),Ue=Object.freeze(Object.defineProperty({__proto__:null,recipe:$e,run:async({log:e,manager:n,own:t,wait:a})=>{const r=t(await n.prepare("ModalManagerV2CatalogApp/ModalRecipeContent",{contentComponent:ke,contentProps:{description:"JS-конфигуратор ещё не применил обновление.",phase:"начальное"}}));r.setText("title","MMV2-X02: исходное тело").setPresentation({size:"sm"}),await n.open(r),await a(700)&&"closed"!==r.status&&(Ne(r,{description:"Функция вызвала setProps({ contentProps }) у того же ModalInstance.",phase:"обновлённое"}),e("JS-функция обновила тело через ModalInstance API",r.getState()))},supportingSources:Ge,updateRecipeBody:Ne},Symbol.toStringTag,{value:"Module"})),Be="ModalManagerV2CatalogApp/ModalRecipeContent",We=t(a({name:"RecipeVueContent",props:{label:{type:String,default:""}},setup:e=>{const n=o(0);return t=>r("div",{"data-test":"mmv2-recipe-vue-content"},[r("p",e.label),r("p",`Локальный счётчик компонента: ${n.value}`),r(i,{attributes:{type:"button"},buttonText:"Увеличить счётчик",onAction:e=>{n.value+=1},type:"secondary"})])}})),qe=Object.freeze([Object.freeze({label:"Vue-оболочка произвольного содержимого",source:Te})]),He=Object.freeze({description:"Передаёт version-controlled Vue-компонент как prop каталожной modal-оболочки. Компонент содержит собственное состояние, обработчик события и UiButton из дизайн-системы.",expected:["Внутри окна отображается Vue-компонент и кнопка из ui-list.","Каждое нажатие увеличивает локальный ref без обращения к manager и без remount окна.","Закрытие окна уничтожает вложенный Vue-компонент вместе с его локальным состоянием."],group:"Произвольное содержимое",id:"MMV2-X03",safety:"read-only",tags:["Vue","dynamic component","markRaw","UiButton"],title:"Открыть окно с Vue-компонентом",useWhen:"Когда телу окна нужны формы, реактивное состояние, события или переиспользуемые UI-компоненты. Это основной путь для сложного содержимого.",warnings:["Vue-компонент является исполняемым кодом с полномочиями приложения: передавать можно только доверенный код из сборки, а не шаблон или компонент из ответа API.","Modal host не подключает router, Pinia и другие app plugins автоматически; зависимости компонента должны быть явными."]}),Xe=Object.freeze(Object.defineProperty({__proto__:null,recipe:He,run:async({log:e,manager:n,own:t})=>{const a=t(await n.prepare(Be,{contentComponent:We,contentProps:{label:"Этот блок полностью отрисован переданным Vue-компонентом."}}));a.setText("title","MMV2-X03: произвольный Vue-компонент").setPresentation({size:"sm"}),await n.open(a),e("Vue-компонент открыт через convention-based modal ID",{instanceId:a.instanceId,modalId:Be})},supportingSources:qe},Symbol.toStringTag,{value:"Module"})),Je="<template>\n  <div :class=\"$b`,content`\">\n    <FormRuntime\n      v-bind=\"$props\"\n      :close=\"close\"\n      :message-class=\"$b`,message`\" />\n  </div>\n</template>\n\n<script>\n// classes\nimport { defineModalConfig } from '@classes/ModalManager/index.js';\n\n\nexport const modalConfig = defineModalConfig({\n  actionPreset: 'none',\n  presentation: { preset: 'dialog' },\n});\n\nexport default { name: 'ModalForm' };\n<\/script>\n\n<script setup>\n// composables\nimport { useBEM } from '@cms/useBEM';\nimport { useModalInstance } from '@cmp/ModalManagerV2/composables/useModalInstance.js';
1\n\n// components\nimport FormRuntime from '@cmp/FormManager/FormRuntime.vue';\n\n\n//================================= composables\nconst { $b } = useBEM('b-modal-v2');\nconst instance = useModalInstance();\nif (!instance) throw new TypeError('ModalForm requires a ModalInstance');\nconst close = result => instance.close(result);\n\n\n//================================= props\ndefineProps({\n  dataForm:    { type: Object, required: true },\n  destroyForm: { type: Function, default: null },\n  fireContext: { type: Boolean, default: false },\n  functions:   { type: Object, default: _ => ({}) },\n  onSubmit:    { type: Function, default: null },\n  onTrigger:   { type: Function, default: null },\n  updateData:  { type: Object, default: null },\n});\n\n\n<\/script>\n",Ke="Footer/NewsletterSubscription",Ye=Object.freeze([Object.freeze({label:"Публичный FormManager facade",source:"import { FormInstance } from './FormInstance.js';\nimport {\n  FORM_MANAGER_ERROR_CODES,\n  formManagerError,\n} from './FormManagerError.js';\n\n\nconst assertFormId = value => {\n  if (typeof value !== 'string' || value.trim() === '') {\n    throw formManagerError(\n      FORM_MANAGER_ERROR_CODES.INVALID_ARGUMENT,\n      'Form id must be a non-empty string',\n    );\n  }\n  return value;\n};\n\nconst normalizeFormPath = value => {\n  if (typeof value !== 'string' || value.trim() === '') {\n    throw formManagerError(\n      FORM_MANAGER_ERROR_CODES.INVALID_ARGUMENT,\n      'Form path must be a non-empty string',\n    );\n  }\n  return value.trim().replaceAll('\\\\', '/');\n};\n\nconst assertObject = (value, label) => {\n  if (!value || typeof value !== 'object' || Array.isArray(value)) {\n    throw formManagerError(\n      FORM_MANAGER_ERROR_CODES.INVALID_ARGUMENT,\n      `${label} must be an object`,\n    );\n  }\n  return value;\n};\n\nconst assertSource = value => {\n  if (\n    typeof value !== 'function'\n    && (!value || typeof value !== 'object' || Array.isArray(value))\n  ) {\n    throw formManagerError(\n      FORM_MANAGER_ERROR_CODES.INVALID_ARGUMENT,\n      'Form scheme must be an object or a factory function',\n    );\n  }\n  return value;\n};\n\nconst invalidRuntime = (id, message) => formManagerError(\n  FORM_MANAGER_ERROR_CODES.INVALID_FORM_RUNTIME,\n  `Form \"${id}\" ${message}`,\n  { id },\n);\n\nconst assertNoDataUpdate = (value, createError) => {\n  if (Object.prototype.hasOwnProperty.call(value, 'dataUpdate')) {\n    throw createError('dataUpdate is not supported; use updateData');\n  }\n  return value;\n};\n\nconst assertRuntimeFunction = (id, value, label) => {\n  if (value !== null && value !== undefined && typeof value !== 'function') {\n    throw invalidRuntime(id, `${label} must be a function`);\n  }\n  return value ?? null;\n};\n\nconst assertRuntimeObject = (id, value, label, { nullable = false } = {}) => {\n  if (nullable && (value === null || value === undefined)) return null;\n  if (!value || typeof value !== 'object' || Array.isArray(value)) {\n    throw invalidRuntime(id, `${label} must be an object`);\n  }\n  return value;\n};\n\nconst assertRuntimeTitle = (id, value) => {\n  if (value === null || value === undefined) return '';\n  if (typeof value !== 'string') throw invalidRuntime(id, 'title must be a string');\n  return value;\n};\n\nconst normalizeCatalog = catalog => {\n  if (catalog instanceof Map) return new Map(catalog);\n  if (!catalog || typeof catalog !== 'object' || Array.isArray(catalog)) {\n    throw formManagerError(\n      FORM_MANAGER_ERROR_CODES.INVALID_ARGUMENT,\n      'Form catalog must be an object or a Map',\n    );\n  }\n  return new Map(Object.entries(catalog));\n};\n\nconst normalizePathCatalog = catalog => new Map(\n  [...normalizeCatalog(catalog)]\n    .map(([path, loader]) => [normalizeFormPath(path), loader]),\n);\n\nconst unwrapLoadedSource = source => {\n  if (!source || typeof source !== 'object' || Array.isArray(source)) return source;\n  if (Object.prototype.hasOwnProperty.call(source, 'default')) {\n    return source.default;\n  }\n  if (\n    source[Symbol.toStringTag] === 'Module'\n    && Object.prototype.hasOwnProperty.call(source, 'form')\n  ) {\n    return source.form;\n  }\n  return source;\n};\n\nconst normalizeDefinition = definition => {\n  if (\n    definition?.form\n    && definition?.functions\n    && definition.dataForm === undefined\n  ) {\n    return {\n      dataForm: definition.form,\n      functions: definition.functions,\n    };\n  }\n  if (definition?.params && definition.dataForm === undefined) {\n    return { dataForm: definition };\n  }\n  return definition;\n};\n\nconst overrideValue = (overrides, key, fallback) =>\n  Object.prototype.hasOwnProperty.call(overrides, key)\n    ? overrides[key]\n    : fallback;\n\nconst normalizeRuntime = (id, definition, overrides) => {\n  const runtime = assertNoDataUpdate(\n    assertRuntimeObject(\n      id,\n      normalizeDefinition(definition),\n      'runtime',\n    ),\n    message => invalidRuntime(id, message),\n  );\n  const dataForm = assertRuntimeObject(\n    id,\n    overrideValue(overrides, 'dataForm', runtime.dataForm),\n    'dataForm',\n  );\n  const formParams = assertRuntimeObject(id, dataForm.params, 'dataForm.params');\n  if (typeof formParams.id !== 'string' || formParams.id.trim() === '') {\n    throw invali
1dRuntime(id, 'dataForm.params.id must be a non-empty string');\n  }\n\n  const runtimeFunctions = runtime.functions ?? {};\n  const overrideFunctions = overrides.functions ?? {};\n  assertRuntimeObject(id, runtimeFunctions, 'functions');\n  assertRuntimeObject(id, overrideFunctions, 'store functions');\n\n  const runtimePresentation = runtime.presentation ?? {};\n  const overridePresentation = overrides.presentation ?? {};\n  assertRuntimeObject(id, runtimePresentation, 'presentation');\n  assertRuntimeObject(id, overridePresentation, 'store presentation');\n\n  const fireContext = overrideValue(\n    overrides,\n    'fireContext',\n    runtime.fireContext ?? false,\n  );\n  if (typeof fireContext !== 'boolean') {\n    throw invalidRuntime(id, 'fireContext must be a boolean');\n  }\n\n  return Object.freeze({\n    dataForm,\n    fireContext,\n    functions: {\n      ...runtimeFunctions,\n      ...overrideFunctions,\n    },\n    mount: overrideValue(overrides, 'mount', runtime.mount ?? null),\n    onMount: assertRuntimeFunction(\n      id,\n      overrideValue(overrides, 'onMount', runtime.onMount),\n      'onMount',\n    ),\n    onSubmit: assertRuntimeFunction(\n      id,\n      overrideValue(overrides, 'onSubmit', runtime.onSubmit),\n      'onSubmit',\n    ),\n    onTrigger: assertRuntimeFunction(\n      id,\n      overrideValue(overrides, 'onTrigger', runtime.onTrigger),\n      'onTrigger',\n    ),\n    presentation: {\n      ...runtimePresentation,\n      ...overridePresentation,\n    },\n    title: assertRuntimeTitle(\n      id,\n      overrideValue(overrides, 'title', runtime.title),\n    ),\n    updateData: assertRuntimeObject(\n      id,\n      overrideValue(overrides, 'updateData', runtime.updateData),\n      'updateData',\n      { nullable: true },\n    ),\n  });\n};\n\nexport class FormManager {\n  static Form = null;\n\n  #catalog           = null;\n  #forms             = new Map();\n  #loadRuntime       = null;\n  #loadedSources     = new Map();\n  #pathCatalog       = null;\n  #pathRegistrations = new Map();\n  #runtimeReady      = null;\n  #sequence          = 0;\n  #stores            = new Map();\n\n  constructor({\n    catalog     = {},\n    loadRuntime = async _ => ({ mount: null }),\n    pathCatalog = {},\n  } = {}) {\n    if (typeof loadRuntime !== 'function') {\n      throw formManagerError(\n        FORM_MANAGER_ERROR_CODES.INVALID_ARGUMENT,\n        'FormManager loadRuntime must be a function',\n      );\n    }\n    this.#catalog     = normalizeCatalog(catalog);\n    this.#loadRuntime = loadRuntime;\n    this.#pathCatalog = normalizePathCatalog(pathCatalog);\n\n    for (const [id, loader] of this.#catalog) {\n      assertFormId(id);\n      if (typeof loader !== 'function') {\n        throw formManagerError(\n          FORM_MANAGER_ERROR_CODES.INVALID_ARGUMENT,\n          `Form catalog loader \"${id}\" must be a function`,\n        );\n      }\n    }\n    for (const [path, loader] of this.#pathCatalog) {\n      if (typeof loader !== 'function') {\n        throw formManagerError(\n          FORM_MANAGER_ERROR_CODES.INVALID_ARGUMENT,\n          `Form path loader \"${path}\" must be a function`,\n        );\n      }\n    }\n  }\n\n  deleteForm(id) {\n    const formId = assertFormId(id);\n    this.#loadedSources.delete(formId);\n    this.#stores.delete(formId);\n    const deletedForm = this.#forms.delete(formId);\n    const deletedPath = this.#pathRegistrations.delete(formId);\n    return deletedForm || deletedPath;\n  }\n\n  getFormIds() {\n    return Object.freeze([...new Set([\n      ...this.#catalog.keys(),\n      ...this.#forms.keys(),\n      ...this.#pathRegistrations.keys(),\n    ])].sort());\n  }\n\n  getStore(id) {\n    const store = this.#stores.get(assertFormId(id));\n    return store ? Object.freeze({ ...store }) : null;\n  }\n\n  hasForm(id) {\n    const formId = assertFormId(id);\n    return this.#forms.has(formId)\n      || this.#catalog.has(formId)\n      || this.#pathRegistrations.has(formId);\n  }\n\n  loadForm(id) {\n    return this.#loadSource(assertFormId(id));\n  }\n\n  async loadFormByPath(id, path) {\n    const formId = assertFormId(id);\n    const formPath = normalizeFormPath(path);\n    const registeredPath = this.#pathRegistrations.get(formId);\n\n    if (registeredPath && registeredPath !== formPath) {\n      throw formManagerError(\n        FORM_MANAGER_ERROR_CODES.FORM_REGISTRATION_EXISTS,\n        `Form \"${formId}\" is already registered from another path`,\n        { id: formId, path: registeredPath },\n      );\n    }\n    if (\n      !registeredPath\n      && (this.#catalog.has(formId) || this.#forms.has(formId))\n    ) {\n      throw formManagerError(\n        FORM_MANAGER_ERROR_CODES.FORM_REGISTRATION_EXISTS,\n        `Form \"${formId}\" is already registered`,\n        { id: formId },\n      );\n    }\n    if (!this.#pathCatalog.has(formPath)) {\n      throw formManagerError(\n        FORM_MANAGER_ERROR_CODES.FORM_PATH_NOT_FOUND,\n        `Form path \"${formPath}\" is not available in the build catalog`,\n        { id: formId, path: formPath },\n      );\n    }\n\n    this.#pathRegistrations.set(formId, formPath);\n    return this.#loadSource(formId);\n  }\n\n  async open(id, input = {}, options = {}) {\n    const instance = await this.preload(id, input, options);\n    const target = instance.mountTarget;\n    if (target !== null && target !== undefined) await instance.mount(target);\n    return instance;\n  }\n\n  async preload(id, input = {}, options = {}) {\n    const formId = assertFormId(id);\n    assertObject(options, 'preload options');\n    assertNoDataUpdate(options, message => formManagerError(\n      FORM_MANAGER_ERROR_CODES.INVALID_ARGUMENT,\n      message,\n    ));\n\n    const [mountRuntime, source] = await Promise.all([\n      this.whenReady(),\n      this.loadForm(formId),\n    ]);\n    const definition = await (typeof source === 'function'\n      ? source(input, { id: formId, manager: this })\n      : source);\n    const store = {\n      ...this.#stores.get(formId),\n      ...options,\n    };
1\n    const runtime = normalizeRuntime(formId, definition, store);\n    this.#sequence += 1;\n\n    return new FormInstance({\n      id: formId,\n      instanceId: `form-instance-${this.#sequence}`,\n      manager: this,\n      mountRuntime,\n      runtime,\n    });\n  }\n\n  setForm(id, schemeOrFactory) {\n    const formId = assertFormId(id);\n    this.#forms.set(formId, assertSource(schemeOrFactory));\n    this.#loadedSources.delete(formId);\n    return this;\n  }\n\n  setStore(id, patch) {\n    const formId = assertFormId(id);\n    assertObject(patch, 'store patch');\n    assertNoDataUpdate(patch, message => formManagerError(\n      FORM_MANAGER_ERROR_CODES.INVALID_ARGUMENT,\n      message,\n    ));\n    this.#stores.set(formId, {\n      ...this.#stores.get(formId),\n      ...patch,\n    });\n    return this;\n  }\n\n  whenReady() {\n    if (this.#runtimeReady) return this.#runtimeReady;\n\n    this.#runtimeReady = Promise\n      .resolve()\n      .then(_ => this.#loadRuntime())\n      .then(runtime => {\n        if (!runtime || typeof runtime.mount !== 'function') {\n          throw formManagerError(\n            FORM_MANAGER_ERROR_CODES.INVALID_FORM_RUNTIME,\n            'FormManager runtime must provide mount(instance, target)',\n          );\n        }\n        return runtime;\n      })\n      .catch(error => {\n        this.#runtimeReady = null;\n        throw error;\n      });\n\n    return this.#runtimeReady;\n  }\n\n  #loadSource(id) {\n    if (this.#forms.has(id)) return Promise.resolve(this.#forms.get(id));\n    if (this.#loadedSources.has(id)) return this.#loadedSources.get(id);\n\n    const registeredPath = this.#pathRegistrations.get(id);\n    const loader = registeredPath\n      ? this.#pathCatalog.get(registeredPath)\n      : this.#catalog.get(id);\n    if (!loader) {\n      return Promise.reject(formManagerError(\n        FORM_MANAGER_ERROR_CODES.FORM_NOT_FOUND,\n        `Form \"${id}\" is not registered`,\n        { id },\n      ));\n    }\n\n    const request = Promise\n      .resolve()\n      .then(_ => loader())\n      .then(unwrapLoadedSource)\n      .then(assertSource)\n      .catch(error => {\n        this.#loadedSources.delete(id);\n        throw error;\n      });\n    this.#loadedSources.set(id, request);\n    return request;\n  }\n}\n\nexport default FormManager;\n"}),Object.freeze({label:"Project wiring и lazy catalog",source:"// classes\nimport { FormManager as BaseFormManager } from '@classes/FormManager/index.js';\n\n// plugins: form-generator\nimport { Form } from '@plugins/vue3-form-generator/src/data/index.js';\n\nimport {\n  projectFormCatalog,\n  projectFormPathCatalog,\n} from './catalog.js';\n\n\nexport const createProjectFormManager = ({\n  configureHostApp,\n  documentTarget,\n  modalManager,\n}) => {\n  const manager = new BaseFormManager({\n    catalog: projectFormCatalog,\n    loadRuntime: async _ => {\n      const { createProjectFormRuntime } = await import('./runtime.js');\n      return createProjectFormRuntime({\n        configureHostApp,\n        documentTarget,\n        modalManager,\n      });\n    },\n    pathCatalog: projectFormPathCatalog,\n  });\n\n  Object.defineProperty(manager, 'Form', {\n    enumerable: true,\n    value:      Form,\n  });\n\n  return manager;\n};\n\nexport default createProjectFormManager;\n"}),Object.freeze({label:"Экземпляр подготовленной формы",source:"import {\n  FORM_MANAGER_ERROR_CODES,\n  formManagerError,\n} from './FormManagerError.js';\n\n\nconst createDeferred = _ => {\n  let resolve;\n  const promise = new Promise(settle => { resolve = settle; });\n  return { promise, resolve };\n};\n\nconst isPromiseLike = value => value && typeof value.then === 'function';\n\nexport class FormInstance {\n  #closed        = null;\n  #closePromise  = null;\n  #handle        = null;\n  #manager       = null;\n  #mountRuntime  = null;\n  #resolveClosed = null;\n  #runtime       = null;\n  #settled       = false;\n  #status        = 'prepared';\n  #target        = null;\n\n  constructor({ id, instanceId, manager, mountRuntime, runtime }) {\n    const deferred = createDeferred();\n    this.id            = id;\n    this.instanceId    = instanceId;\n    this.#closed       = deferred.promise;\n    this.#manager      = manager;\n    this.#mountRuntime = mountRuntime;
1\n    this.#resolveClosed = deferred.resolve;\n    this.#runtime      = runtime;\n  }\n\n  get closed() {\n    return this.#closed;\n  }\n\n  get dataForm() {\n    return this.#runtime.dataForm;\n  }\n\n  get mountResult() {\n    return this.#handle?.value ?? null;\n  }\n\n  get mountTarget() {\n    return this.#runtime.mount;\n  }\n\n  get status() {\n    return this.#status;\n  }\n\n  get target() {\n    return this.#target;\n  }\n\n  close(result) {\n    if (this.#settled) return Promise.resolve(false);\n    if (this.#closePromise) return this.#closePromise;\n\n    this.#closePromise = this.#performClose(result)\n      .finally(_ => { this.#closePromise = null; });\n    return this.#closePromise;\n  }\n\n  async #performClose(result) {\n    if (this.#settled) return false;\n\n    const handle = this.#handle;\n    if (!handle) {\n      this.#finish({ reason: 'programmatic', result });\n      return true;\n    }\n\n    this.#status = 'closing';\n    try {\n      const isClosed = handle.close\n        ? await handle.close(result)\n        : (handle.unmount?.(), true);\n      if (isClosed === false) {\n        this.#status = 'open';\n        return false;\n      }\n      this.#finish({ reason: 'programmatic', result });\n      return true;\n    } catch (error) {\n      this.#status = 'open';\n      throw error;\n    }\n  }\n\n  async mount(target = this.mountTarget) {\n    if (this.#settled) {\n      throw formManagerError(\n        FORM_MANAGER_ERROR_CODES.FORM_INSTANCE_CLOSED,\n        `Form \"${this.id}\" instance is closed`,\n        { id: this.id },\n      );\n    }\n    if (this.#handle || this.#status === 'mounting') {\n      throw formManagerError(\n        FORM_MANAGER_ERROR_CODES.FORM_INSTANCE_MOUNTED,\n        `Form \"${this.id}\" instance is already mounted`,\n        { id: this.id },\n      );\n    }\n\n    this.#status = 'mounting';\n    try {\n      const resolvedTarget = typeof target === 'function'\n        ? await target({ id: this.id, instance: this, manager: this.#manager })\n        : target;\n      if (!resolvedTarget) {\n        throw formManagerError(\n          FORM_MANAGER_ERROR_CODES.INVALID_MOUNT_TARGET,\n          `Form \"${this.id}\" requires a mount target`,\n          { id: this.id },\n        );\n      }\n\n      const handle = await this.#mountRuntime.mount(this, resolvedTarget);\n      if (!handle || typeof handle !== 'object') {\n        throw formManagerError(\n          FORM_MANAGER_ERROR_CODES.INVALID_FORM_RUNTIME,\n          'FormManager mount runtime must return a handle object',\n          { id: this.id },\n        );\n      }\n      if (this.#settled) {\n        if (handle.close) await handle.close();\n        else handle.unmount?.();\n        throw formManagerError(\n          FORM_MANAGER_ERROR_CODES.FORM_INSTANCE_CLOSED,\n          `Form \"${this.id}\" instance was closed during mount`,\n          { id: this.id },\n        );\n      }\n      this.#handle = handle;\n      this.#target = resolvedTarget;\n      this.#status = 'open';\n\n      if (this.#runtime.onMount) {\n        await this.#runtime.onMount({\n          id: this.id,\n          instance: this,\n          mountResult: this.mountResult,\n          target: resolvedTarget,\n        });\n      }\n      if (isPromiseLike(handle.closed)) {\n        handle.closed.then(\n          outcome => this.#finish(outcome),\n          error => this.#finish({ error, reason: 'error' }),\n        );\n      }\n      return this;\n    } catch (error) {\n      const handle = this.#handle;\n      this.#handle = null;\n      this.#target = null;\n      this.#status = this.#settled ? 'closed' : 'prepared';\n      try {\n        if (handle?.close) await handle.close();\n        else handle?.unmount?.();\n      } catch (cleanupError) {\n        void cleanupError;\n      }\n      throw error;\n    }\n  }\n\n  getRuntime() {\n    return this.#runtime;\n  }\n\n  #finish(outcome = {}) {\n    if (this.#settled) return false;\n    this.#settled = true;\n    this.#status  = 'closed';\n    this.#resolveClosed(outcome);\n    return true;\n  }\n}\n\nexport default FormInstance;\n"}
1),Object.freeze({label:"DOM/ModalManagerV2 mount runtime",source:"// dependencies: vue\nimport { createApp } from 'vue';\n\n// classes\nimport {\n  FORM_MANAGER_ERROR_CODES,\n  formManagerError,\n} from '@classes/FormManager/FormManagerError.js';\n\n// components\nimport FormRuntime from '@cmp/FormManager/FormRuntime.vue';\n\n\nconst installedApps = new WeakMap();\n\nconst installFormGenerator = app => {\n  if (installedApps.has(app)) return installedApps.get(app);\n\n  const installation = Promise.all([\n    import('@/helpers/FormGeneratorPlugin.js'),\n    import('vue-select'),\n    import('vue-imask'),\n  ]).then(([FormGenerator, vSelect, Mask]) => {\n    app.use(FormGenerator.default);\n    app.component('v-select', vSelect.default);\n    app.component('imask-input', Mask.IMaskComponent);\n\n    const formStore = app.config.globalProperties.$store;\n    if (!formStore?.commit) {\n      throw new TypeError('FormGenerator store is not available');\n    }\n    return formStore;\n  }).catch(error => {\n    installedApps.delete(app);\n    throw error;\n  });\n\n  installedApps.set(app, installation);\n  return installation;\n};\n\nconst createDestroyForm = getStore => ({ formId }) => {\n  const formStore = getStore();\n  return formStore\n    ? formStore.commit('FormGenerator/destroyForm', { formId })\n    : false;\n};\n\nconst runtimeProps = (instance, destroyForm) => {\n  const runtime = instance.getRuntime();\n  return {\n    dataForm: runtime.dataForm,\n    destroyForm,\n    fireContext: runtime.fireContext,\n    functions: runtime.functions,\n    onSubmit: runtime.onSubmit,\n    onTrigger: runtime.onTrigger,\n    updateData: runtime.updateData,\n  };\n};\n\nconst isElement = value => value?.nodeType === 1;\n\nconst invalidTarget = (instance, message) => formManagerError(\n  FORM_MANAGER_ERROR_CODES.INVALID_MOUNT_TARGET,\n  `Form \"${instance.id}\" ${message}`,\n  { id: instance.id },\n);\n\nexport const createProjectFormRuntime = ({\n  configureHostApp,\n  documentTarget,\n  modalManager,\n}) => ({\n  async mount(instance, target) {\n    if (target === modalManager) {\n      let formStore = null;\n      formStore = await configureHostApp(installFormGenerator);\n\n      const runtime = instance.getRuntime();\n      let modalInstance = null;\n      try {\n        modalInstance = await modalManager.prepare('ModalForm', runtimeProps(\n          instance,\n          createDestroyForm(_ => formStore),\n        ));\n        modalInstance.setText('title', runtime.title);\n        if (Object.keys(runtime.presentation).length) {\n          modalInstance.setPresentation(runtime.presentation);\n        }\n        await modalManager.open(modalInstance);\n      } catch (error) {\n        if (modalInstance && modalInstance.status !== 'closed') {\n          try {\n            await modalInstance.close();\n          } catch (cleanupError) {\n            void cleanupError;\n          }\n        }\n        throw error;\n      }\n\n      return {\n        close:  result => modalInstance.close(result),\n        closed: modalInstance.closed,\n        value:  modalInstance,\n      };\n    }\n\n    let element = target;\n    if (typeof target === 'string') {\n      try {\n        element = documentTarget?.querySelector(target);\n      } catch (selectorError) {\n        void selectorError;\n        throw invalidTarget(instance, `received invalid selector \"${target}\"`);\n      }\n    }\n    if (!isElement(element)) {\n      throw invalidTarget(\n        instance,\n        'mount target must be a DOM element, selector or ModalManagerV2',\n      );\n    }\n\n    const mountElement = element.ownerDocument.createElement('div');\n    mountElement.dataset.formManagerInstance = instance.instanceId;\n    element.append(mountElement);\n\n    let app       = null;\n    let formStore = null;\n    let mounted   = null;\n    try {\n      app = createApp(FormRuntime, {\n        ...runtimeProps(\n          instance,\n          createDestroyForm(_ => formStore),\n        ),\n        close: result => instance.close(result),\n      });\n      formStore = await installFormGenerator(app);
1\n      mounted   = app.mount(mountElement);\n    } catch (error) {\n      if (mounted) app.unmount();\n      mountElement.remove();\n      throw error;\n    }\n\n    let active = true;\n    return {\n      close() {\n        if (!active) return false;\n        active = false;\n        app.unmount();\n        mountElement.remove();\n        return true;\n      },\n      value: mounted,\n    };\n  },\n});\n\nexport default createProjectFormRuntime;\n"}),Object.freeze({label:"Общий lifecycle FormGenerator",source:"<template>\n  <div class=\"b-form-manager\">\n    <p\n      v-if=\"errorMessage\"\n      :class=\"messageClass\"\n      role=\"alert\">\n      {{ errorMessage }}\n    </p>\n    <FormGenerator\n      :data-form=\"dataForm\"\n      :fire-context=\"fireContext\"\n      :functions=\"functions\"\n      :update-data=\"updateData\"\n      @trigger=\"handleTrigger\" />\n  </div>\n</template>\n\n<script>\nexport default { name: 'FormManagerRuntime' };\n<\/script>\n\n<script setup>\n// dependencies: vue\nimport {\n  onBeforeUnmount,\n  ref,\n} from 'vue';\n\n// classes\nimport { isModalFlowOutcome } from '@classes/ModalManager/modalFlow.js';\n\n\n//================================= props\nconst props = defineProps({\n  close:        { type: Function, required: true },\n  dataForm:     { type: Object, required: true },\n  destroyForm:  { type: Function, default: null },\n  fireContext:  { type: Boolean, default: false },\n  functions:    { type: Object, default: _ => ({}) },\n  messageClass: { type: [Array, Object, String], default: null },\n  onSubmit:     { type: Function, default: null },\n  onTrigger:    { type: Function, default: null },\n  updateData:   { type: Object, default: null },\n});\n\n\n//================================= data\nconst errorMessage = ref('');\nlet formContext = null;\n\n\n//================================= methods\nconst releaseBusy = payload => {\n  const { FormGenerator, formId } = payload ?? {};\n  if (!FormGenerator?.setParams || !formId) return false;\n  FormGenerator.setParams({ formId, isBusy: false });\n  return true;\n};\n\nconst destroyForm = _ => {\n  const context = formContext;\n  formContext = null;\n  const formId = context?.formId ?? props.dataForm?.params?.id;\n  if (!formId) return false;\n\n  if (props.destroyForm) {\n    Promise.resolve(props.destroyForm({ formId })).catch(_ => false);\n    return true;\n  }\n  if (!context?.api?.destroyForm) return false;\n  context.api.destroyForm({ formId });\n  return true;\n};\n\nconst handleTrigger = async payload => {\n  if (payload?.FormGenerator) {\n    formContext = {\n      api:    payload.FormGenerator,\n      formId: payload.formId ?? payload.FormGenerator.formId,\n    };\n  }\n  errorMessage.value = '';\n\n  try {\n    if (props.onTrigger) await props.onTrigger(payload);\n    if (!['send', 'trigger'].includes(payload?.button?.type)) return;\n\n    const outcome = props.onSubmit\n      ? await props.onSubmit(payload)\n      : payload?.sendData?.data ?? null;\n\n    if (isModalFlowOutcome(outcome) && outcome.type === 'stay') {\n      releaseBusy(payload);\n      return;\n    }\n    if (isModalFlowOutcome(outcome) && outcome.type !== 'close') {\n      throw new TypeError(`Unsupported form submit outcome: ${outcome.type}`);\n    }\n\n    const result = isModalFlowOutcome(outcome)\n      ? outcome.result\n      : outcome;\n    const isClosed = await props.close(result);\n    if (isClosed === false) releaseBusy(payload);\n  } catch (error) {\n    errorMessage.value = error?.message ?? String(error);\n    releaseBusy(payload);\n  }\n};\n\n\n//================================= lifecycle\nonBeforeUnmount(destroyForm);\n<\/script>\n\n<style lang=\"less\">\n@import '@less/variables/variables.less';\n\n.b-form-manager {\n  min-width: 0;\n\n  .b-modal-v2--sheet & > .form-generator {\n    padding: 0;\n  }\n\n  & .input-control_checkbox &__agreement {\n    color: @clr-ui-text;\n    font-size: 0.7rem;\n    line-height: 1.4285;\n  }\n}\n</style>\n"}),Object.freeze({label:"Универсальная оболочка ModalForm",source:Je}),Object.freeze({label:"Factory формы рассылки",source:"import {\n  Form,\n  InputCheckbox,\n  InputEmail,\n  InputText,\n  ListSearchableMulti,\n  ParamsMaker,\n} from '@plugins/vue3-form-generator/src/';
1\n\nimport {\n  agreementClasses,\n  marketingConsent,\n  personalDataConsent,\n} from './content.js';\n\n\nlet sequence = 0;\n\nconst topics = Object.freeze([\n  Object.freeze({ id: 'all', label: 'Все' }),\n  Object.freeze({ id: 'webinars', label: 'Вебинары' }),\n  Object.freeze({ id: 'company-news', label: 'Новости компании' }),\n  Object.freeze({ id: 'support', label: 'Поддержка' }),\n]);\n\nexport function createNewsletterSubscriptionRuntime({ formId = null } = {}) {\n  sequence += 1;\n  const resolvedFormId = formId ?? `form-manager-newsletter-${sequence}`;\n  const form = new Form({\n    baseId: 'form-manager',\n    key:    `newsletter-${sequence}`,\n    prefix: 'form-manager-field-',\n  })\n    .setParams({\n      id:          resolvedFormId,\n      buttons:     [ParamsMaker.genButton('send', 'Подписаться')],\n      footerText:  '* — обязательные для заполнения поля',\n      hasFieldset: false,\n      hasGroups:   false,\n      hasSteps:    false,\n    })\n    .addField(ListSearchableMulti, {\n      name: 'themes',\n      data: [\n        'Тематика',\n        [topics[0]],\n        {\n          clearable:  false,\n          multi:      true,\n          options:    topics,\n          searchable: false,\n        },\n      ],\n      validation: [['required']],\n    })\n    .addField(InputText, {\n      name:       'name',\n      label:      'Имя',\n      value:      '',\n      attributes: {\n        autocomplete: 'name',\n        placeholder:  'Иван',\n      },\n    })\n    .addField(InputEmail, {\n      name:       'email',\n      label:      'E-mail',\n      value:      '',\n      attributes: {\n        autocomplete: 'email',\n        placeholder:  '[email protected]',\n      },\n      validation: [['required'], ['email']],\n    })\n    .addField(InputText, {\n      name:       'company',\n      label:      'Компания',\n      value:      '',\n      attributes: {\n        autocomplete: 'organization',\n        placeholder:  'ООО «Телеком»',\n      },\n      validation: [['required']],\n    })\n    .addField(InputText, {\n      name:       'position',\n      label:      'Должность',\n      value:      '',\n      attributes: {\n        autocomplete: 'organization-title',\n        placeholder:  'Менеджер',\n      },\n    })\n    .addField(InputCheckbox, {\n      name:       'personal-data-consent',\n      label:      personalDataConsent('Подписаться'),\n      value:      false,\n      classes:    agreementClasses,\n      validation: [['required']],\n    })\n    .addField(InputCheckbox, {\n      name:    'marketing-consent',\n      label:   marketingConsent,\n      value:   false,\n      classes: agreementClasses,\n    })\n    .setup()\n    .setReady();\n\n  const { form: dataForm, functions } = form;\n  return {\n    dataForm,\n    functions,\n    title: 'Подписаться на рассылку',\n  };\n}\n\nexport default createNewsletterSubscriptionRuntime;\n"})]),Ze=Object.freeze({description:"Берёт предопределённую форму рассылки из lazy catalog FormManager и открывает её через универсальный ModalForm в presentation dialog-lg. Submit не обращается к backend: payload возвращается как result закрытого FormInstance.",expected:["В широком диалоге отображается FormGenerator с тематиками, контактными полями и согласиями формы рассылки.","Кнопка «Подписаться» использует client validation и busy lifecycle FormGenerator.","После submit окно закрывается, а журнал показывает JSON-совместимый form payload без сетевого запроса.","Reset закрывает только принадлежащий кейсу FormInstance и уничтожает form store."],group:"Произвольное содержимое",id:"MMV2-X04",safety:"read-only",tags:["FormGenerator","FormManager","ModalForm","dialog-lg"],title:"Открыть форму рассылки в dialog-lg",useWhen:"Когда длинной форме нужен широкий диалог, а consumer должен вызвать её по стабильному ID без ручной сборки ModalInstance и Vuex lifecycle.",warnings:["ModalForm — универсальный modal adapter для FormGenerator, а не отдельный renderer формы.","Factory создаёт новый dataForm для каждого открытия;
1 mutable runtime между сессиями не переиспользуется.","Пример намеренно не вызывает production transport формы рассылки."]}),Qe=Object.freeze(Object.defineProperty({__proto__:null,default:Ze,recipe:Ze,run:async({globalTarget:n,log:t,namespaceKey:a,own:r})=>{const o=n[a]?.FormManager,s=n[a]?.ModalManagerV2;if(!o)throw new Error("FormManager is not available");if(!s)throw new Error("ModalManagerV2 is not available");const i=r(await o.open(Ke,{},{mount:s,onSubmit:n=>{const a=n?.sendData?.data??null;return t("FormManager получил submit формы рассылки",a),e.close(a)},presentation:{preset:"dialog-lg"}}));t("Форма рассылки открыта через ModalForm",{formId:Ke,instanceId:i.instanceId,modalId:i.mountResult.id,preset:"dialog-lg"})},supportingSources:Ye},Symbol.toStringTag,{value:"Module"})),en="Footer/CallbackRequest",nn=Object.freeze([Object.freeze({label:"Factory формы обратного звонка",source:"import {\n  Form,\n  InputCheckbox,\n  InputPhone,\n  InputText,\n  ParamsMaker,\n} from '@plugins/vue3-form-generator/src/';\n\nimport {\n  agreementClasses,\n  marketingConsent,\n  personalDataConsent,\n} from './content.js';\n\n\nlet sequence = 0;\n\nexport function createCallbackRequestRuntime({ formId = null } = {}) {\n  sequence += 1;\n  const resolvedFormId = formId ?? `form-manager-callback-${sequence}`;\n  const form = new Form({\n    baseId: 'form-manager',\n    key:    `callback-${sequence}`,\n    prefix: 'form-manager-field-',\n  })\n    .setParams({\n      id:          resolvedFormId,\n      buttons:     [ParamsMaker.genButton('send', 'Отправить')],\n      hasFieldset: false,\n      hasGroups:   false,\n      hasSteps:    false,\n      undertitle:  'Наш менеджер свяжется с вами в течение рабочего дня',\n    })\n    .addField(InputText, {\n      name:       'name',\n      label:      'Имя',\n      value:      '',\n      attributes: {\n        autocomplete: 'name',\n        placeholder:  'Имя',\n      },\n      validation: [['required']],\n    })\n    .addField(InputPhone, {\n      name:       'phone',\n      label:      'Телефон',\n      value:      '',\n      attributes: {\n        autocomplete: 'tel',\n        placeholder:  'Телефон',\n      },\n      validation: [['required'], ['phone']],\n    })\n    .addField(InputCheckbox, {\n      name:       'personal-data-consent',\n      label:      personalDataConsent('Отправить'),\n      value:      false,\n      classes:    agreementClasses,\n      validation: [['required']],\n    })\n    .addField(InputCheckbox, {\n      name:    'marketing-consent',\n      label:   marketingConsent,\n      value:   false,\n      classes: agreementClasses,\n    })\n    .setup()\n    .setReady();\n\n  const { form: dataForm, functions } = form;\n  return {\n    dataForm,\n    functions,\n    title: 'Заказать обратный звонок',\n  };\n}\n\nexport default createCallbackRequestRuntime;\n"}),Object.freeze({label:"Универсальная оболочка ModalForm",source:Je})]),tn=Object.freeze({description:"Открывает предопределённую форму обратного звонка через тот же ModalForm, но меняет presentation на боковой sheet. Так форма и её lifecycle остаются прежними, а способ показа задают options текущего вызова FormManager.",expected:["Справа открывается sheet с полями имени, телефона и двумя согласиями.","Форма использует ту же universal ModalForm, что и кейс рассылки; отличается только presentation.","Submit проходит client validation, не вызывает backend и закрывает FormInstance с payload в result."],group:"Произвольное содержимое",id:"MMV2-X05",safety:"read-only",tags:["FormGenerator","FormManager","ModalForm","sheet"],title:"Открыть форму звонка в sheet",useWhen:"Когда компактную форму нужно показать сбоку и сохранить единый API open/closed независимо от presentation ModalManagerV2.",warnings:["Presentation не должен попадать внутрь FormGenerator-схемы: его применяет modal adapter FormManager.","Пример намеренно не вызывает production transport формы обратного звонка."]}),an=Object.freeze(Object.defineProperty({__proto__:null,default:tn,recipe:tn,run:async({globalTarget:n,log:t,namespaceKey:a,own:r})=>{const o=n[a]?.FormManager,s=n[a]?.ModalManagerV2;if(!o)throw new Error("FormManager is not available");if(!s)throw new Error("ModalManagerV2 is not available");const i=r(await o.open(en,{},{mount:s,onSubmit:n=>{const a=n?.sendData?.data??null;return t("FormManager получил submit формы обратного звонка",a),e.close(a)},presentation:{preset:"sheet",side:"right"}}));t("Форма обратного звонка открыта через ModalForm",{formId:en,instanceId:i.instanceId,modalId:i.mountResult.id,preset:"sheet"})},supportingSources:nn},Symbol.toStringTag,{value:"Module"})),rn=Object.freeze(["Подключение и API","Стандартные окна","Произвольное содержимое","Внешний вид","Жизненный цикл","Асинхронные действия","Состояние и события"]),on=Object.freeze({LOCAL_SIDE_EFFECT:"local-side-effect",READ_ONLY:"read-only"}),sn=Object.assign({"./mmv2-a01-async-action.js":$,"./mmv2-c01-error-retry.js":B,"./mmv2-c02-success.js":q,"./mmv2-c03-prompt.js":X,"./mmv2-c04-download.js":K,"./mmv2-c05-product-filters.js":ne,"./mmv2-l01-repeated-open.js":ae,"./mmv2-l02-stack.js":oe,"./mmv2-l03-replace.js":ie,"./mmv2-l04-timeout-close.js":ce,"./mmv2-o01-events-state.js":ue,"./mmv2-o02-store.js":pe,"./mmv2-p01-presentation.js":fe,"./mmv2-p02-dynamic.js":be,"./mmv2-p03-disabled-action.js":ve,"./mmv2-p04-anchored-panel.js":Oe,"./mmv2-p05-owned-panel.js":je,"./mmv2-p06-anchored-modal.js":Re,"./mmv2-r01-readiness.js":_e,"./mmv2-r02-prepare-open.js":Ae,"./mmv2-x01-html.js":Le,"./mmv2-x02-js-configurator.js":Ue,"./mmv2-x03-vue-component.js":Xe,"./mmv2-x04-form-generator.js":Qe,"./mmv2-x05-callback-form.js":an}),ln=Object.assign({"./mmv2-a01-async-action.js":"export const recipe = Object.freeze({\n  description: 'Запускает действие длительностью 1,5 секунды, блокирует его повторный запуск и умеет корректно отменять работу при закрытии окна.',\n  expected:    [\n    'Пока действие выполняется, поле busyActionId содержит ID кнопки confirm, а кнопка недоступна для повторного запуска.',\n    'Повторный вызов получает уже выполняющуюся операцию (single-flight), а не создаёт вторую.',\n    'Закрытие окна или сброс кейса отменяет AbortSignal, выполняет очистку и игнорирует запоздавший результат.',\n  ],\n  group:   'Асинхронные действия',\n  id:      'MMV2-A01',\n  safety:  'read-only',\n  tags:    ['async', 'busy', 'abort'],\n  title:   'Заблокировать повторный запуск действия',\n  useWhen: 'Для асинхронной команды внутри окна, которую нельзя запускать дважды и которую нужно остановить, если пользователь закрыл окно.',\n});\n\nexport const run = async ({ log, manager, own }) =>
1 {\n  const instance = own(await manager.prepare('ModalPrompt', {\n    details: 'Нажмите «Запустить 1,5 с». На время выполнения кнопка станет недоступной, а busyActionId будет равен confirm.',\n  }));\n\n  instance\n    .setText('title', 'MMV2-A01: асинхронное действие')\n    .setAction({\n      closePolicy: 'never',\n      id:          'confirm',\n      role:        'primary',\n      text:        'Запустить 1,5 с',\n    })\n    .setActionHandler('confirm', async ({\n      flow,\n      instance: current,\n      onCleanup,\n      signal,\n    }) => {\n      log('Асинхронное действие начато', current.getState());\n\n      const completed = await new Promise(resolve => {\n        const timer = setTimeout(_ => resolve(true), 1500);\n        const onAbort = _ => {\n          clearTimeout(timer);\n          resolve(false);\n        };\n        signal.addEventListener('abort', onAbort, { once: true });\n        onCleanup(_ => {\n          clearTimeout(timer);\n          signal.removeEventListener('abort', onAbort);\n          log('Ресурсы асинхронного действия очищены');\n        });\n      });\n\n      if (!completed || signal.aborted) return flow.stay({ aborted: true });\n      current.setProps({ details: 'Асинхронное действие завершено; окно осталось открытым.' });\n      log('Асинхронное действие завершено', current.getState());\n      return flow.stay({ completed: true });\n    });\n\n  await manager.open(instance);\n  log('ModalPrompt готов к асинхронному действию', instance.getState());\n};\n","./mmv2-c01-error-retry.js":"export const recipe = Object.freeze({\n  description: 'Открывает стандартное окно ошибки ModalError и добавляет кнопку повторной попытки, которая не закрывает окно.',\n  expected:    [\n    'Строка, похожая на HTML-код, выводится обычным текстом и не исполняется браузером.',\n    'Каждое нажатие «Повторить» увеличивает счётчик, принадлежащий только этому экземпляру окна.',\n    'Результат flow.stay оставляет тот же экземпляр открытым после обработчика.',\n  ],\n  group:   'Стандартные окна',\n  id:      'MMV2-C01',\n  safety:  'read-only',\n  tags:    ['ModalError', 'retry', 'stay'],\n  title:   'Ошибка с повторной попыткой',\n  useWhen: 'Для исправимой ошибки интерфейса или API, после которой пользователь может безопасно повторить операцию.',\n});\n\nexport const run = async ({ log, manager, own }) => {\n  const instance = own(await manager.prepare('ModalError', {\n    details:  `<script>${['al', 'ert'].join('')}(\"не выполнится\")<\/script>`,\n    messages: ['Демонстрационная ошибка. Данные на сервере не изменяются.'],\n  }));\n\n  instance\n    .setText('title', 'MMV2-C01: ошибка с повторной попыткой')\n    .setStore('retryCount', 0)\n    .setAction({\n      closePolicy: 'never',\n      id:          'retry',\n      role:        'primary',\n      text:        'Повторить',\n    })\n    .setActionHandler('retry', ({ flow, instance: current }) => {\n      const retryCount = (current.getStore('retryCount') ?? 0) + 1;\n      current\n        .setStore('retryCount', retryCount)\n        .setProps({ messages: [`Попытка ${retryCount} выполнена локально.`] });\n      log('Повторная попытка завершена; окно осталось открытым', { retryCount });\n      return flow.stay({ retryCount });\n    });\n\n  await manager.open(instance);\n  log('ModalError открыт', instance.getState());\n};\n","./mmv2-c02-success.js":"export const recipe = Object.freeze({\n  description: 'Открывает стандартное окно ModalSuccess, сообщает об успешном завершении операции и предлагает закрыть уведомление.',\n  expected:    [\n    'Короткая форма open принимает title и message; переданный message заменяет default-текст окна.',\n    'Пустой message скрывает default-текст, а messages выводит отдельные пункты списка без default-текста.',\n    'Кнопка «Закрыть» использует встроенное закрытие и не требует отдельного обработчика.',\n    'Promise instance.closed сообщает причину закрытия (reason) и переданный результат (result).',\n  ],\n  group:   'Стандартные окна',\n  id:      'MMV2-C02',\n  safety:  'read-only',\n  tags:    ['ModalSuccess', 'close', 'result'],\n  title:   'Сообщить об успешном завершении',\n  useWhen: 'После окончательно завершённой операции, когда пользователю нужно показать результат и вывести его из текущего шаÐ
1³Ð°.',\n});\n\nexport const run = async ({ log, manager, own }) => {\n  const instance = own(await manager.open('ModalSuccess', {\n    message: 'Операция завершена в демонстрационном режиме.',\n    title:   'MMV2-C02: операция завершена',\n  }));\n\n  log('ModalSuccess открыт со встроенным действием закрытия', instance.getState());\n};\n","./mmv2-c03-prompt.js":"export const recipe = Object.freeze({\n  description: 'Открывает окно подтверждения ModalPrompt и отделяет явное согласие от любого способа отмены или закрытия.',\n  expected:    [\n    'Только кнопка подтверждения увеличивает счётчик выполненных команд businessActionCount.',\n    'Отмена, Escape, кнопка закрытия и клик по фону закрывают окно без выполнения команды.',\n    'Повторное подтверждение во время уже запущенного действия не создаёт вторую операцию.',\n  ],\n  group:   'Стандартные окна',\n  id:      'MMV2-C03',\n  safety:  'read-only',\n  tags:    ['ModalPrompt', 'confirm', 'dismiss'],\n  title:   'Запросить подтверждение действия',\n  useWhen: 'Перед командой, которую разрешено выполнять только после явного согласия пользователя, но не при закрытии окна.',\n});\n\nexport const run = async ({ log, manager, own }) => {\n  const instance = own(await manager.prepare('ModalPrompt', {\n    details: 'Подтвердите локальную демонстрационную команду.',\n  }));\n\n  instance\n    .setText('title', 'MMV2-C03: подтвердите действие')\n    .setPresentation({ dismiss: { backdrop: true } })\n    .setStore('businessActionCount', 0)\n    .setActionHandler('confirm', ({ flow, instance: current }) => {\n      const businessActionCount = (\n        current.getStore('businessActionCount') ?? 0\n      ) + 1;\n      current.setStore('businessActionCount', businessActionCount);\n      log('Команда выполнена после явного подтверждения', { businessActionCount });\n      return flow.close({ businessActionCount, confirmed: true });\n    });\n\n  await manager.open(instance);\n  log('ModalPrompt ожидает решения', instance.getState());\n};\n","./mmv2-c04-download.js":"export const recipe = Object.freeze({\n  description: 'Создаёт небольшой текстовый файл прямо в браузере и показывает стандартное окно загрузки ModalDownload.',\n  expected:    [\n    'Файл не загружается автоматически: пользователь должен явно нажать кнопку загрузки.',\n    'Окно получает временный адрес blob:, созданный браузером, поэтому запрос к API не выполняется.',\n    'При закрытии окна или сбросе кейса временный Object URL освобождается.',\n  ],\n  group:   'Стандартные окна',\n  id:      'MMV2-C04',\n  safety:  'local-side-effect',\n  tags:    ['ModalDownload', 'Blob', 'cleanup'],\n  title:   'Показать загрузку локального файла',\n  useWhen: 'Для проверки интерфейса загрузки без обращения к API и без изменения данных на сервере.',\n});\n\nexport const run = async ({\n  createLocalObjectUrl,\n  globalTarget,\n  log,\n  manager,\n  own,\n}) => {\n  const blob = new globalTarget.Blob(\n    ['ModalManagerV2 local recipe\\n'],\n    { type: 'text/plain;charset=utf-8' },\n  );\n  const { revoke, url } = createLocalObjectUrl(blob);\n  const instance = own(await manager.prepare('ModalDownload', {\n    autoClose: false,\n    autoStart: false,\n    countdown: 0,\n    filename:  'modal-manager-v2-recipe.txt',\n    url,\n  }));\n\n  instance\n    .setText('title', 'MMV2-C04: локальный файл')\n    .onDispose(revoke);\n\n  await manager.open(instance);\n  log('ModalDownload открыт с локальным Blob URL', {\n    filename: 'modal-manager-v2-recipe.txt',\n    protocol: new globalTarget.URL(url).protocol,\n  });\n};\n","./mmv2-c05-product-filters.js":"// classes\nimport { modalFlow } from '@classes/ModalManager/index.js';\n\n// resources\nimport formSource        from '@/modules/form-manager/forms/product-filters.js?raw';\nimport integrationSource from './product-filter-frontend-integration.js?raw';\n\n\nconst FORM_ID               = 'Catalog/ProductFilters';\nconst frontendTriggerSource = `<div data-product-filter-trigger>\n  @php\n    echo \\\\Infotecs\\\\UiRenderer\\\\UiRenderer::render('/common/chip/chip', [\n      'id' => 'product-filters-trigger',\n      'name' => 'Фильтры',\n    ], false);\n  @endphp\n</div>`;
1\n\nexport const supportingSources = Object.freeze([\n  Object.freeze({ label: 'Фабрика формы в lazy-каталоге', source: formSource }),\n  Object.freeze({ label: 'Разметка триггера во frontend Blade', source: frontendTriggerSource }),\n  Object.freeze({ label: 'Подключение из отдельного frontend bundle', source: integrationSource }),\n]);\n\nexport const readProductFilters = result => {\n  const fields   = Array.isArray(result?.fields) ? result.fields : [];\n  const query    = String(fields.find(field => field.name === 'query')?.model ?? '');\n  const selected = Object.fromEntries(fields\n    .filter(field => field.name !== 'query' && Array.isArray(field.model))\n    .map(field => [field.name, [...field.model]]));\n  const count    = (query.trim() ? 1 : 0)\n    + Object.values(selected).reduce((total, values) => total + values.length, 0);\n\n  return { query, selected, count };\n};\n\nexport const recipe = Object.freeze({\n  description: 'UIChip открывает боковую шторку с FormGenerator-фильтром: одно поле общего поиска и несколько групп ChipAccordion без внутреннего поиска. Форма принадлежит lazy-каталогу frontend-cabinet, а потребитель передаёт группы и получает результат через FormManager.',\n  expected:    [\n    'Нажатие на чип «Фильтры» открывает справа шторку с поиском и тремя группами вариантов.',\n    'В каждой группе видны первые три варианта, остальные раскрываются; отдельного поиска внутри ChipAccordion нет.',\n    '«Показать» закрывает форму: цифра на чипе равна числу выбранных вариантов плюс один за непустой поисковый запрос.',\n    'Повторное открытие создаёт новый FormInstance с сохранёнными критериями; сброс кейса закрывает шторку и очищает чип.',\n  ],\n  group:    'Стандартные окна',\n  id:       'MMV2-C05',\n  safety:   'read-only',\n  tags:     ['UIChip', 'ChipAccordion', 'FormManager', 'sheet'],\n  title:    'Фильтр продуктов в шторке',\n  trigger:  'chip',\n  useWhen:  'Когда отдельный frontend-скрипт должен открыть форму-фильтр по стабильному ID после готовности сервисов, не импортируя Vue и адаптеры FormGenerator.',\n  warnings: [\n    'Демонстрационные варианты не являются актуальным каталогом продуктов; реальные группы и обработку результата задаёт вызывающий код.',\n    'Поиск — отдельный критерий формы, а не фильтрация чипов внутри групп.',\n    'В примере для frontend атрибут data-product-filter-trigger ставится на обёртку смонтированного UIChip; событие infotecs-ui-click всплывает от кнопки.',\n    'Скрипт frontend обрабатывает события product-filters:apply и product-filters:error на обёртке чипа; пример не меняет список продуктов.',\n    'Форму можно зарегистрировать во время работы страницы через setForm(), но это меняет общий каталог форм. Здесь шаблон подготовлен в frontend-cabinet, а варианты передаются при открытии из frontend.',\n  ],\n});\n\nexport const run = async ({\n  appliedFilters = {},\n  globalTarget,\n  log,\n  namespaceKey,\n  onFiltersApplied = _ => null,\n  own,\n}) => {\n  const services     = globalTarget[namespaceKey];\n  const modalManager = services?.ModalManagerV2;\n  const formManager  = services?.FormManager;\n  if (!modalManager || !formManager) {\n    throw new Error('ModalManagerV2 and FormManager are required');\n  }\n\n  await Promise.all([modalManager.whenReady(), formManager.whenReady()]);\n  const instance = own(await formManager.open(FORM_ID, {\n    query:    appliedFilters.query ?? '',\n    selected: appliedFilters.selected ?? {},\n  }, {\n    mount:    modalManager,\n    onSubmit: payload => {\n      const result  = payload?.sendData?.data ?? null;\n      const filters = readProductFilters(result);\n      onFiltersApplied(filters);\n      log('Критерии фильтра переданы потребителю', { filters, result });\n      return modalFlow.close(result);\n    },\n    presentation: { preset: 'sheet', side: 'right' },\n  }));\n  log('Фильтр открыт по ID готовой формы', {\n    formId:     FORM_ID,\n    instanceId: instance.instanceId,\n    modalId:    instance.mountResult.id,\n  });\n};\n\nexport default recipe;\n","./mmv2-l01-repeated-open.js":"export const recipe = Object.freeze({\n  description: 'Показывает разницу между повторным вызовом open() для существующего экземпляра и созданием нового окна после закрытия.',\n  expected:    [\n    'Повторный open() до закрытия возвращает тот же объект и не создаёт дубликат окна.',\n    'Кнопка «Закрыть и открыть новый» окончательно завершает первый экземпляр.',\n    'Следующий prepare() создаёт новый instanceId для того же типа модального окна.',\n  ],\n  group:   'Жизненный цикл',\n  id:      'MMV2-L01',\n  safety:  'read-only',\n  tags:    ['repeated-open', 'closed', 'identity'],\n  title:   'Повторно открыть окно после закрытия',\n  useWhen: 'Чтобы не создавать дубликаты уже открытого окна и правильно начинать новый жизненный цикл после его закрытия.',\n});\n\nexport const run = async ({ log, manager, own, signal }) =>
1 {\n  const instance = own(await manager.prepare('ModalSuccess', {\n    messages: ['Первый экземпляр.'],\n  }));\n  instance\n    .setText('title', 'MMV2-L01: первый экземпляр')\n    .setAction({\n      closePolicy: 'never',\n      id:          'reopen',\n      role:        'primary',\n      text:        'Закрыть и открыть новый',\n    })\n    .setActionHandler('reopen', ({ flow }) =>\n      flow.close({ reopen: true }));\n\n  const opened = await manager.open(instance);\n  const repeated = await manager.open(instance);\n  log('Повторный open() вернул существующий экземпляр', {\n    identity:   repeated === opened,\n    instanceId: instance.instanceId,\n  });\n\n  void instance.closed.then(async ({ result }) => {\n    if (!result?.reopen || signal.aborted) return;\n\n    const fresh = own(await manager.prepare('ModalSuccess', {\n      messages: ['Это новый экземпляр, созданный после полного закрытия предыдущего.'],\n    }));\n    fresh\n      .setText('title', 'MMV2-L01: новый экземпляр')\n      .setAction({ id: 'close', role: 'primary', text: 'Закрыть' });\n    await manager.open(fresh);\n    log('После закрытия создан новый экземпляр', {\n      changed: fresh.instanceId !== instance.instanceId,\n      first:   instance.instanceId,\n      fresh:   fresh.instanceId,\n    });\n  }).catch(error => log('Не удалось открыть новый экземпляр', {\n    code:    error?.code ?? null,\n    message: error?.message ?? String(error),\n  }, 'error'));\n};\n","./mmv2-l02-stack.js":"export const recipe = Object.freeze({\n  description: 'Открывает окно подтверждения, затем показывает поверх него окно ошибки и сохраняет нижний экземпляр.',\n  expected:    [\n    'Нижнее окно ModalPrompt получает статус covered — оно перекрыто, но остаётся смонтированным.',\n    'После закрытия верхнего ModalError нижнее окно снова получает статус open и становится доступным.',\n    'Стек показа и activeModalId всегда указывают на фактический порядок окон.',\n  ],\n  group:   'Жизненный цикл',\n  id:      'MMV2-L02',\n  safety:  'read-only',\n  tags:    ['stack', 'covered', 'reveal'],\n  title:   'Открыть окно поверх другого',\n  useWhen: 'Для последовательности зависимых окон, где нижнее окно должно сохранить введённые данные, пока пользователь работает с верхним.',\n});\n\nexport const run = async ({ log, manager, own }) => {\n  const prompt = own(await manager.prepare('ModalPrompt', {\n    details: 'Закройте верхнюю ошибку, чтобы вернуться сюда.',\n  }));\n  prompt.setText('title', 'MMV2-L02: нижнее окно подтверждения');\n  await manager.open(prompt);\n\n  const error = own(await manager.prepare('ModalError', {\n    messages: ['Это верхнее окно в стеке показа.'],\n  }));\n  error.setText('title', 'MMV2-L02: верхняя ошибка');\n  await manager.open(error);\n\n  log('Два окна открыты слоями', {\n    error:   error.getState(),\n    manager: manager.getState(),\n    prompt:  prompt.getState(),\n  });\n};\n","./mmv2-l03-replace.js":"export const recipe = Object.freeze({\n  description: 'После подтверждения заменяет текущее окно ModalPrompt новым окном ModalSuccess без промежуточного пустого состояния.',\n  expected:    [\n    'Сначала открывается целевое окно, затем исходное закрывается с причиной replaced.',\n    'После перехода в стеке показа остаётся только ModalSuccess.',\n    'Каталог регистрирует новое окно как принадлежащее кейсу и безопасно закрывает его при сбросе.',\n  ],\n  group:   'Жизненный цикл',\n  id:      'MMV2-L03',\n  safety:  'read-only',\n  tags:    ['replace', 'flow.open', 'ownership'],\n  title:   'Заменить одно окно другим',\n  useWhen: 'Когда следующий шаг процесса должен полностью заменить текущее окно, например подтверждение — сообщением об успехе.',\n});\n\nexport const run = async ({ log, manager, own, signal }) =>
1 {\n  const prompt = own(await manager.prepare('ModalPrompt', {\n    details: 'Нажмите подтверждение: это окно будет заменено сообщением об успехе.',\n  }));\n  prompt\n    .setText('title', 'MMV2-L03: исходное окно подтверждения')\n    .setActionHandler('confirm', ({ flow }) => flow.open(\n      'ModalSuccess',\n      { messages: ['Окно подтверждения заменено без промежуточного пустого стека.'] },\n      { transition: 'replace' },\n    ));\n\n  void prompt.closed.then(result => {\n    if (result.reason !== 'replaced' || signal.aborted) return;\n    const target = manager.getInstance('ModalSuccess');\n    if (target) own(target);\n    log('Замена одного окна другим завершена', {\n      closed:           result,\n      manager:          manager.getState(),\n      targetInstanceId: target?.instanceId ?? null,\n    });\n  });\n\n  await manager.open(prompt);\n  log('Окно подтверждения ожидает команды для перехода', prompt.getState());\n};\n","./mmv2-l04-timeout-close.js":"export const recipe = Object.freeze({\n  description: 'Открывает ModalSuccess и через 3 секунды вызывает instance.close(). Раннее закрытие или сброс кейса отменяет таймер.',\n  expected:    [\n    'Окно закрывается автоматически через 3 секунды после показа.',\n    'В журнале результат закрытия содержит result.timedOut: true; reason остаётся programmatic.',\n    'При закрытии кнопкой или крестиком раньше срока таймер отменяется.',\n    'Повторный запуск создаёт новый таймер. Сброс кейса очищает его.',\n  ],\n  group:    'Жизненный цикл',\n  id:       'MMV2-L04',\n  safety:   'read-only',\n  tags:     ['timeout', 'setTimeout', 'close', 'onDispose'],\n  title:    'Закрыть окно по таймеру',\n  useWhen:  'Для краткого сообщения, которое должно исчезнуть автоматически через заданное время.',\n  warnings: [\n    'Таймер вызывает instance.close(), а не flow.close(): flow нужно вернуть из обработчика действия.',\n    'Закрытие проходит обычный beforeClose. Если обработчик запретит закрытие, повторной попытки по таймеру не будет.',\n  ],\n});\n\nexport const run = async ({ log, manager, own, registerCleanup, signal }) => {\n  const instance = own(await manager.prepare('ModalSuccess', {\n    message: 'Окно закроется через 3 секунды. Можно закрыть его раньше кнопкой или крестиком.',\n    title:   'MMV2-L04: закрытие по таймеру',\n  }));\n  let timerId;\n  const cancelTimeout = _ => clearTimeout(timerId);\n  const unregisterCleanup = registerCleanup(cancelTimeout);\n  instance.onDispose(_ => {\n    cancelTimeout();\n    unregisterCleanup();\n  });\n\n  void instance.closed.then(outcome => {\n    if (signal.aborted) return;\n    log(outcome.result?.timedOut\n      ? 'Окно закрыто по таймеру'\n      : 'Окно закрыто раньше срока; таймер отменён', outcome);\n  });\n\n  await manager.open(instance);\n  if (signal.aborted || ['closing', 'closed'].includes(instance.status)) return;\n\n  timerId = setTimeout(_ => {\n    instance.close({ timedOut: true })\n      .then(closed => {\n        if (!closed && !signal.aborted) log('Закрытие по таймеру отменено');\n      })\n      .catch(error => {\n        if (!signal.aborted) log('Ошибка закрытия по таймеру', { message: error.message });\n      });\n  }, 3000);\n  log('Таймер запущен: 3 секунды', { instanceId: instance.instanceId });\n};\n","./mmv2-o01-events-state.js":"export const recipe = Object.freeze({\n  description: 'Подписывается на снимки состояния менеджера и на публичные события страницы, чтобы показать оба способа наблюдения.',\n  expected:    [\n    'Подписчик сразу получает текущий снимок и затем каждый снимок с новой revision.',\n    'DOM CustomEvent имеют префикс modal
1-manager-v2 и содержат сериализуемые данные в поле detail.',\n    'Сброс кейса отписывает функцию-наблюдатель и удаляет все DOM-обработчики.',\n  ],\n  group:   'Состояние и события',\n  id:      'MMV2-O01',\n  safety:  'read-only',\n  tags:    ['subscribe', 'events', 'revision'],\n  title:   'Наблюдать состояние и события',\n  useWhen: 'Для диагностики жизненного цикла окон или подключения внешнего наблюдателя, который читает изменения, но не управляет менеджером.',\n});\n\nexport const run = async ({\n  eventTarget,\n  log,\n  manager,\n  own,\n  registerCleanup,\n}) => {\n  const unsubscribe = manager.subscribe(state => {\n    log('Подписчик менеджера получил снимок состояния', {\n      activeModalId: state.activeModalId,\n      openCount:     state.openCount,\n      revision:      state.revision,\n      stack:         state.stack,\n    });\n  });\n  registerCleanup(unsubscribe);\n\n  const eventNames = [\n    'opened',\n    'covered',\n    'revealed',\n    'closed',\n    'action-start',\n    'action-completed',\n  ].map(name => `modal-manager-v2:${name}`);\n  const onEvent = event => log(`DOM event ${event.type}`, event.detail);\n  eventNames.forEach(name => eventTarget?.addEventListener(name, onEvent));\n  registerCleanup(_ => eventNames.forEach(name =>\n    eventTarget?.removeEventListener(name, onEvent)));\n\n  const instance = own(await manager.prepare('ModalSuccess', {\n    messages: ['Закройте окно и сравните записи о снимках состояния и DOM-событиях в журнале кейса.'],\n  }));\n  instance.setText('title', 'MMV2-O01: наблюдаемость');\n  await manager.open(instance);\n};\n","./mmv2-o02-store.js":"export const recipe = Object.freeze({\n  description: 'Показывает два места для временных данных: общую именованную область менеджера и хранилище отдельного экземпляра окна.',\n  expected:    [\n    'Общие значения читаются только по явно заданному имени области (manager scope), поэтому разные интеграции не смешивают данные.',\n    'Значения экземпляра доступны только через соответствующий ModalInstance и намеренно не входят в публичный снимок состояния.',\n    'Закрытие удаляет данные экземпляра автоматически, а сброс кейса явно очищает созданную им общую область.',\n  ],\n  group:   'Состояние и события',\n  id:      'MMV2-O02',\n  safety:  'read-only',\n  tags:    ['store', 'scope', 'cleanup'],\n  title:   'Хранить временные данные в разных областях',\n  useWhen: 'Когда одним временным данным нужно жить до закрытия конкретного окна, а другим — дольше и под ответственностью интеграции.',\n});\n\nexport const run = async ({\n  log,\n  manager,\n  own,\n  registerCleanup,\n}) => {\n  const scope = 'modal-manager-v2-catalog:MMV2-O02';\n  manager.setStore(scope, {\n    recipeId: 'MMV2-O02',\n    source:   'catalog',\n  });\n  registerCleanup(_ => manager.clearStore(scope));\n\n  const instance = own(await manager.prepare('ModalSuccess', {\n    messages: ['Значения временных хранилищ не попадают в публичный снимок состояния менеджера.'],\n  }));\n  instance\n    .setText('title', 'MMV2-O02: области временных данных')\n    .setStore({\n      localStep: 1,\n      owner:     instance.instanceId,\n    })\n    .onDispose(_ => log('Область данных экземпляра очищена при его освобождении'));\n\n  await manager.open(instance);\n  log('Сравнение двух областей временных данных', {\n    instanceStore: instance.getStore(['localStep', 'owner']),\n    managerState:  manager.getState(),\n    managerStore:  manager.getStore(scope, ['recipeId', 'source']),\n  });\n};\n","./mmv2-p01-presentation.js":"export const recipe = Object.freeze({\n  description: 'Открывает три стандартных окна слоями, чтобы визуально сравнить варианты размещения sheet, dialog и dialog-lg.',\n  expected:    [\n    'В стеке показа одновременно находятся три экземпляра с разными вариантами внешнего вида 
1(presentation preset).',\n    'Сначала закрывается верхнее большое окно dialog-lg, затем становятся доступны dialog и sheet.',\n    'Перекрытые окна сохраняются в DOM и своё состояние; закрытие верхнего слоя не создаёт их заново.',\n  ],\n  group:   'Внешний вид',\n  id:      'MMV2-P01',\n  safety:  'read-only',\n  tags:    ['sheet', 'dialog', 'dialog-lg'],\n  title:   'Сравнить варианты размещения окон',\n  useWhen: 'Чтобы выбрать подходящий размер и расположение модального окна и проверить поведение нескольких слоёв.',\n});\n\nexport const run = async ({ log, manager, own }) => {\n  const sheet = own(await manager.prepare('ModalError', {\n    messages: ['Нижний слой использует вариант sheet.'],\n  }));\n  sheet.setText('title', 'Вариант размещения: sheet');\n\n  const dialog = own(await manager.prepare('ModalPrompt', {\n    details: 'Средний слой использует вариант dialog.',\n  }));\n  dialog\n    .setText('title', 'Вариант размещения: dialog')\n    .setPresentation({ preset: 'dialog', size: 'sm' });\n\n  const dialogLarge = own(await manager.prepare('ModalSuccess', {\n    messages: ['Верхний слой использует вариант dialog-lg. Закрывайте окна сверху вниз.'],\n  }));\n  dialogLarge\n    .setText('title', 'Вариант размещения: dialog-lg')\n    .setPresentation({ preset: 'dialog-lg' });\n\n  await manager.open(sheet);\n  await manager.open(dialog);\n  await manager.open(dialogLarge);\n  log('Стек из трёх вариантов размещения сформирован', manager.getState());\n};\n","./mmv2-p02-dynamic.js":"export const recipe = Object.freeze({\n  description: 'Открывает окно, а затем меняет его заголовок, содержимое, размер, правила закрытия и публичный CSS-класс.',\n  expected:    [\n    'Через 700 мс содержимое и заголовок меняются, но instanceId остаётся прежним.',\n    'Размер и правила закрытия обновляются без повторного создания компонента (remount).',\n    'Публичный CSS-класс добавляет видимую рамку и удаляется вместе с экземпляром окна.',\n  ],\n  group:   'Внешний вид',\n  id:      'MMV2-P02',\n  safety:  'read-only',\n  tags:    ['reactivity', 'setProps', 'setClasses'],\n  title:   'Обновить уже открытое окно',\n  useWhen: 'Когда открытое окно должно показать новые данные, не теряя введённые значения и другое локальное состояние компонента.',\n});\n\nexport const run = async ({ log, manager, own, wait }) => {\n  const instance = own(await manager.prepare('ModalError', {\n    details:  'Обновление произойдёт через 700 мс.',\n    messages: ['Начальное содержимое.'],\n  }));\n  instance.setText('title', 'MMV2-P02: начальное состояние');\n  await manager.open(instance);\n\n  if (!await wait(700) || instance.status === 'closed') return;\n\n  instance\n    .setText('title', 'MMV2-P02: окно обновлено без пересоздания')\n    .setProps({\n      details:  'Идентификатор экземпляра instanceId сохранился.',\n      messages: ['Содержимое, размер, правила закрытия и CSS-класс обновлены без пересоздания окна.'],\n    })\n    .setPresentation({\n      dismiss: { backdrop: true },\n      size:    'lg',\n    })\n    .setClasses({\n      surface: 'modal-manager-v2-catalog__demo-surface',\n    });\n\n  log('Экземпляр обновлён без пересоздания компонента', instance.getState());\n};\n","./mmv2-p03-disabled-action.js":"import {\n  defineComponent,\n  h,\n  markRaw,\n  ref,\n  watch,\n} from 'vue';\n\nimport UiCheckbox from '@ui/Checkbox/index.vue';
1\n\nexport const bindActionDisabled = (manager, instance, actionId, isBlocked) => {\n  const syncButton = _ => {\n    const state = instance.getState();\n    if (['closing', 'closed'].includes(state.status)) return;\n    if (state.busyActionId === actionId) return;\n\n    const action = instance.getAction(actionId);\n    if (action && action.disabled !== isBlocked.value) {\n      instance.setAction({ id: actionId, disabled: isBlocked.value });\n    }\n  };\n\n  const stopWatch = watch(isBlocked, syncButton, { flush: 'sync' });\n  const unsubscribe = manager.subscribe(syncButton);\n  instance.onDispose(_ => {\n    stopWatch();\n    unsubscribe();\n  });\n};\n\nexport const recipe = Object.freeze({\n  description: 'Флаг isBlocked хранится вне компонента окна. Флажок меняет его значение, а watch обновляет disabled кнопки через instance.setAction().',\n  expected:    [\n    'При открытии флажок установлен, а кнопка «Подтвердить» заблокирована.',\n    'Снимите флажок: кнопка станет доступна. Установите снова: она заблокируется без пересоздания окна.',\n    'Нажмите «Подтвердить» и измените флаг во время проверки. После завершения кнопка получит последнее значение флага.',\n    'Закрытие окна снимает подписки; повторный запуск начинает кейс с исходного состояния.',\n  ],\n  group:    'Внешний вид',\n  id:       'MMV2-P03',\n  safety:   'read-only',\n  tags:     ['setAction', 'disabled', 'boolean', 'watch'],\n  title:    'Блокировать кнопку по флагу',\n  useWhen:  'Когда доступность действия зависит от заполнения формы, результата проверки или другого реактивного значения.',\n  warnings: [\n    'disabled принимает boolean. Передача ref или функции вместо значения не создаёт подписку.',\n    'Выполняющийся action менять нельзя. Подписка на состояние менеджера применит отложенное значение после его завершения.',\n  ],\n});\n\nexport const run = async ({ log, manager, own }) => {\n  const isBlocked = ref(true);\n  const message = ref('Снимите флажок, чтобы разрешить подтверждение.');\n  const content = markRaw(defineComponent({\n    name:  'RecipeDisabledAction',\n    setup: _ => _ => h('div', [\n      h(UiCheckbox, {\n        params: {\n          id:    'mmv2-disabled-action-flag',\n          model: isBlocked.value,\n          texts: { label: 'Блокировать кнопку подтверждения' },\n        },\n        onChange: event => { isBlocked.value = Boolean(event.model); },\n      }),\n      h('p', `isBlocked: ${isBlocked.value}`),\n      h('p', { role: 'status' }, message.value),\n    ]),\n  }));\n  const instance = own(await manager.prepare('ModalManagerV2CatalogApp/ModalRecipeContent', {\n    contentComponent: content,\n  }));\n\n  instance\n    .setText('title', 'MMV2-P03: кнопка зависит от флага')\n    .setPresentation({ size: 'sm' })\n    .setAction({\n      id:          'confirm',\n      text:        'Подтвердить',\n      role:        'primary',\n      closePolicy: 'never',\n    })\n    .setActionHandler('confirm', async ({ flow, onCleanup, signal }) => {\n      message.value = 'Проверка выполняется. Можно изменить флаг.';\n      const completed = await new Promise(resolve => {\n        const timer = setTimeout(_ => resolve(true), 1500);\n        const abort = _ => resolve(false);\n        signal.addEventListener('abort', abort, { once: true });\n        onCleanup(_ => {\n          clearTimeout(timer);\n          signal.removeEventListener('abort', abort);\n        });\n      });\n      if (!completed || signal.aborted) return flow.stay();\n      message.value = 'Проверка завершена. Кнопка снова зависит от флага.';\n      log('Подтверждение выполнено', { isBlocked: isBlocked.value });\n      return flow.stay();\n    });\n\n  bindActionDisabled(manager, instance, 'confirm', isBlocked);\n  await manager.open(instance);\n  log('Кнопка связана с флагом isBlocked', { instanceId: instance.instanceId });\n};\n","./mmv2-p04-anchored-panel.js":"export const recipe = Object.freeze({\n  description: 'Открывает немодальную панель рядом с кнопкой запуска. Страница остаётся доступной; положение пересчитывается при прокрутке и изменении размера.',\n  expected:    [\n    'Фон не затемняется, прокрутка страницы доступна.',\n    'Escape и клик снаружи закрывают панель; у края экрана она меняет сторону.',\n    'Повторный запуск создаёт новый экземпляр после закрытия предыдущего.',\n  ],\n  group:   'Внешний вид',\n  id:      'MMV2-P04',\n  safety:  'read-only',\n  tags:    ['anchored-dialog', 'popover', 'modal: false'],\n  title:   'Немодальная панель у кнопки',\n  useWhen: 'Для палитр и небольших вспомогательных форм на странице.',\n});
1\n\nexport const run = async ({ anchor, log, manager, own }) => {\n  const instance = own(await manager.prepare('ModalPrompt', {\n    details: 'Прокрутите страницу: панель следует за кнопкой и остаётся внутри видимой области.',\n  }));\n  instance\n    .setText('title', 'Немодальная панель')\n    .setPresentation({ preset: 'anchored-dialog', modal: false, size: 'sm' })\n    .setAnchor(anchor);\n  await manager.open(instance);\n  log('Панель открыта без блокировки страницы', instance.getState());\n};\n","./mmv2-p05-owned-panel.js":"// dependencies: vue\nimport { defineComponent, h, markRaw, ref } from 'vue';\n\n// library\nimport { propMaker } from '@lib/vue';\n\n// composables\nimport { useBEM } from '@cms/useBEM';\n\n// components\nimport UiButton from '@ui/Button/index.vue';\n\nconst Content = markRaw(defineComponent({\n  props: { openPanel: propMaker(Function, null, true) },\n  setup: props => {\n    const { $b } = useBEM('b-modal-v2');\n    const count  = ref(0);\n    return _ => h('div', [\n      h('p', `Счётчик родителя: ${count.value}`),\n      h('div', { class: $b`,actions` }, [\n        h(UiButton, {\n          buttonText: 'Изменить родителя',\n          type:       'secondary',\n          onAction:   _ => { count.value++; },\n        }),\n        h(UiButton, {\n          buttonText: 'Открыть дочернюю панель',\n          type:       'secondary',\n          onAction:   payload => props.openPanel(payload.event.currentTarget),\n        }),\n      ]),\n    ]);\n  },\n}));\n\nexport const recipe = Object.freeze({\n  description: 'Открывает обычное окно, внутри которого можно вызвать немодальную панель. Родитель сохраняет состояние, а страница остаётся заблокированной родительским окном.',\n  expected:    [\n    'Счётчик родителя можно менять при открытой панели: модальность родителя не усиливается.',\n    'Первый Escape закрывает панель, следующий — родительское окно.',\n    'Крестик родителя закрывает оба экземпляра; дочерняя панель получает причину owner-closed.',\n  ],\n  group:   'Внешний вид',\n  id:      'MMV2-P05',\n  safety:  'read-only',\n  tags:    ['anchored-dialog', 'setOwner', 'nested', 'modal: false'],\n  title:   'Панель внутри модального окна',\n  useWhen: 'Для палитры или вспомогательной формы в уже открытом диалоге.',\n});\n\nexport const run = async ({ log, manager, own }) => {\n  let child    = null;\n  const parent = own(await manager.prepare('ModalManagerV2CatalogApp/ModalRecipeContent', {\n    contentComponent: Content,\n    contentProps:     {\n      openPanel: async anchor => {\n        try {\n          if (child && child.status !== 'closed') { await child.close(); return; }\n          child = own(await manager.prepare('ModalPrompt', { details: 'Родитель остаётся доступным. Эта панель принадлежит ему.' }));\n          child\n            .clearActions()\n            .setText('title', 'Дочерняя панель')\n            .setPresentation({ preset: 'anchored-dialog', modal: false, size: 'sm', dismiss: { outside: false } })\n            .setAnchor(anchor)\n            .setOwner(parent);\n          await manager.open(child);\n        } catch (error) {\n          if (child && child.status !== 'closed') await child.close();\n          log('Панель не открылась', { message: error.message }, 'error');\n        }\n      },\n    },\n  }));\n  parent.setText('title', 'Родительское окно');\n  await manager.open(parent);\n};\n","./mmv2-p06-anchored-modal.js":"export const recipe = Object.freeze({\n  description: 'Использует ту же привязку к кнопке, но открывает полноценный блокирующий диалог через presentation.modal: true.',\n  expected:    [\n    'Окно привязано к кнопке, фон затемнён и недоступен.',\n    'Фокус удерживается в диалоге; Escape и крестик закрывают окно.',\n    'После закрытия восстанавливаются прокрутка и фокус.',\n  ],\n  group:   'Внешний вид',\n  id:      'MMV2-P06',\n  safety:  'read-only',\n  tags:    ['anchored-dialog', 'modal: true'],\n  title:   'Модальное окно у кнопки',\n  useWhen: 'Когда диалог нужно расположить рядом с источником действия и временно заблокировать остальную страницу.',\n});
1\n\nexport const run = async ({ anchor, log, manager, own }) => {\n  const instance = own(await manager.prepare('ModalPrompt', { details: 'Модальность задаётся отдельно от расположения окна.' }));\n  instance\n    .setText('title', 'Привязанный модальный диалог')\n    .setPresentation({ preset: 'anchored-dialog', modal: true, size: 'sm' })\n    .setAnchor(anchor);\n  await manager.open(instance);\n  log('Открыт модальный диалог у кнопки', instance.getState());\n};\n","./mmv2-r01-readiness.js":"export const recipe = Object.freeze({\n  description: 'Проверяет, что каталог подключён к готовому менеджеру по ключу globalThis.infotecs.ModalManagerV2 и не создаёт его копию.',\n  expected:    [\n    'Ключ globalThis.infotecs.ModalManagerV2 указывает на тот же объект, который использует каталог.',\n    'Старый менеджер V1 остаётся доступен отдельно по ключу globalThis.infotecs.ModalManager.',\n    'getState() возвращает неизменяемый снимок: внешний код не может переписать его поля или список экземпляров.',\n  ],\n  group:   'Подключение и API',\n  id:      'MMV2-R01',\n  safety:  'read-only',\n  tags:    ['ready', 'global', 'snapshot'],\n  title:   'Проверить подключение к общему менеджеру',\n  useWhen: 'При добавлении новой точки входа, чтобы убедиться: она ждёт запуск приложения и использует общий менеджер, а не создаёт второй.',\n});\n\nexport const run = async ({\n  globalTarget,\n  log,\n  manager,\n  namespaceKey,\n}) => {\n  const readyManager = await manager.whenReady();\n  const namespaceTarget = globalTarget[namespaceKey] ?? {};\n  const state = manager.getState();\n\n  log('Проверены готовность и глобальный ключ менеджера', {\n    immutableInstances: Object.isFrozen(state.instances),\n    immutableState:     Object.isFrozen(state),\n    legacyAvailable:    Boolean(namespaceTarget.ModalManager),\n    readyIdentity:      readyManager === manager,\n    v2GlobalIdentity:   namespaceTarget.ModalManagerV2 === manager,\n  });\n  log('Текущий снимок состояния менеджера', state);\n};\n","./mmv2-r02-prepare-open.js":"export const recipe = Object.freeze({\n  description: 'Последовательно вызывает prepare(), настраивает подготовленный экземпляр цепочкой методов и передаёт его в open().',\n  expected:    [\n    'Каждый метод настройки возвращает тот же ModalInstance, поэтому вызовы можно объединять в цепочку.',\n    'open(instance) возвращает тот же объект и показывает соответствующее окно.',\n    'После закрытия статус экземпляра становится closed; повторно использовать завершённый экземпляр нельзя.',\n  ],\n  group:   'Подключение и API',\n  id:      'MMV2-R02',\n  safety:  'read-only',\n  tags:    ['prepare', 'fluent', 'identity'],\n  title:   'Подготовить, настроить и открыть окно',\n  useWhen: 'Когда содержимое, кнопки и внешний вид окна нужно полностью настроить до первого показа пользователю.',\n});\n\nexport const run = async ({ log, manager, own }) => {\n  const instance = own(await manager.prepare('ModalSuccess', {\n    messages: ['Экземпляр подготовлен до показа.'],\n  }));\n  const configured = instance\n    .setText('title', 'MMV2-R02: подготовленный экземпляр')\n    .setPresentation({ preset
1: 'dialog', size: 'sm' })\n    .setAction({ id: 'close', role: 'primary', text: 'Закрыть кейс' });\n  const opened = await manager.open(instance);\n\n  log('Проверено сохранение одного экземпляра при настройке и открытии', {\n    fluentIdentity: configured === instance,\n    instanceId:     instance.instanceId,\n    openIdentity:   opened === instance,\n    state:          instance.getState(),\n  });\n};\n","./mmv2-x01-html.js":"// dependencies: dompurify, vue\nimport DOMPurify from 'dompurify';\nimport {\n  computed,\n  defineComponent,\n  h,\n  markRaw,\n} from 'vue';\n\n// resources\nimport modalRecipeContentSource from '../modals/v2/ModalRecipeContent.vue?raw';\n\n\nconst MODAL_ID = 'ModalManagerV2CatalogApp/ModalRecipeContent';\nconst ALLOWED_TAGS = Object.freeze([\n  'code',\n  'em',\n  'li',\n  'ol',\n  'p',\n  'strong',\n  'ul',\n]);\n\nconst exampleHtml = `\n  <p><strong>Разрешённая разметка</strong> остаётся в окне.</p>\n  <ul>\n    <li>Списки, <em>выделение</em> и <code>inline code</code>.</li>\n  </ul>\n  <img src=\"missing\" onerror=\"alert('xss')\">\n  <a href=\"javascript:alert('xss')\">Опасная ссылка становится обычным текстом.</a>\n`;\n\nexport const sanitizeRecipeHtml = value => DOMPurify.sanitize(value, {\n  ALLOWED_ATTR:    [],\n  ALLOWED_TAGS,\n  ALLOW_ARIA_ATTR: false,\n  ALLOW_DATA_ATTR: false,\n});\n\nconst SafeHtmlContent = markRaw(defineComponent({\n  name:  'RecipeSafeHtmlContent',\n  props: {\n    html: { type: String, default: '' },\n  },\n  setup: props => {\n    const sanitizedHtml = computed(_ => sanitizeRecipeHtml(props.html));\n\n    return _ => h('div', {\n      'data-test': 'mmv2-recipe-html-content',\n      innerHTML:   sanitizedHtml.value,\n    });\n  },\n}));\n\nexport const supportingSources = Object.freeze([\n  Object.freeze({\n    label:  'Vue-оболочка произвольного содержимого',\n    source: modalRecipeContentSource,\n  }),\n]);\n\nexport const recipe = Object.freeze({\n  description: 'Передаёт HTML-строку в Vue-компонент, который очищает её непосредственно перед HTML-sink с помощью DOMPurify и строгого списка разрешённых тегов.',\n  expected:    [\n    'Абзац, список, выделение и inline code отображаются как разметка.',\n    'img, обработчик onerror, ссылка и javascript: URL не попадают в DOM; текст ссылки остаётся читаемым.',\n    'Менеджер получает уже доверенный Vue-компонент; кейс не регистрирует runtime modal и не создаёт второй manager.',\n  ],\n  group:    'Произвольное содержимое',\n  id:       'MMV2-X01',\n  safety:   'read-only',\n  tags:     ['HTML', 'DOMPurify', 'XSS', 'allowlist'],\n  title:    'Показать очищенное HTML-содержимое',\n  useWhen:  'Только когда бизнес-требование действительно требует разметку из строки. Для обычного текста используйте интерполяцию Vue, а для структурированного интерфейса — Vue-компонент.',\n  warnings: [\n    'v-html и innerHTML являются XSS-sink: авторизация страницы и отсутствие тега script не делают непроверенную строку безопасной.',\n    'Очищать нужно у самого sink. После DOMPurify нельзя дописывать разметку строками или передавать результат коду, который снова меняет HTML.',\n    'Allowlist этого кейса намеренно не разрешает ссылки, изображения, стили и любые атрибуты; расширение списка требует отдельного security review.',\n  ],\n});\n\nexport const run = async ({ log, manager, own }) =>
1 {\n  const instance = own(await manager.prepare(MODAL_ID, {\n    contentComponent: SafeHtmlContent,\n    contentProps:     { html: exampleHtml },\n  }));\n\n  instance\n    .setText('title', 'MMV2-X01: очищенное HTML-содержимое')\n    .setPresentation({ size: 'sm' });\n\n  await manager.open(instance);\n  log('HTML передан в компонент с очисткой у sink', {\n    allowedTags: ALLOWED_TAGS,\n    instanceId:  instance.instanceId,\n  });\n};\n","./mmv2-x02-js-configurator.js":"// dependencies: vue\nimport {\n  defineComponent,\n  h,\n  markRaw,\n} from 'vue';\n\n// resources\nimport modalRecipeContentSource from '../modals/v2/ModalRecipeContent.vue?raw';\n\n\nconst MODAL_ID = 'ModalManagerV2CatalogApp/ModalRecipeContent';\n\nconst JsConfiguredContent = markRaw(defineComponent({\n  name:  'RecipeJsConfiguredContent',\n  props: {\n    description: { type: String, default: '' },\n    phase:       { type: String, default: '' },\n  },\n  setup: props => _ => h('div', {\n    'data-test': 'mmv2-recipe-js-content',\n  }, [\n    h('p', [h('strong', 'Состояние: '), props.phase]),\n    h('p', props.description),\n  ]),\n}));\n\nexport const updateRecipeBody = (instance, contentProps) => instance\n  .setText('title', 'MMV2-X02: тело изменено через public API')\n  .setProps({ contentProps });\n\nexport const supportingSources = Object.freeze([\n  Object.freeze({\n    label:  'Vue-оболочка произвольного содержимого',\n    source: modalRecipeContentSource,\n  }),\n]);\n\nexport const recipe = Object.freeze({\n  description: 'Передаёт ModalInstance в обычную JS-функцию. Функция меняет props вложенного компонента через setProps() и заголовок через setText(), не получая ссылку на DOM.',\n  expected:    [\n    'Окно открывается с начальным текстом и через 700 мс показывает новое состояние.',\n    'instanceId и локальный Vue-экземпляр сохраняются: manager обновляет props без remount.',\n    'Функция использует только публичный ModalInstance API; querySelector, innerHTML и ручная замена DOM не нужны.',\n  ],\n  group:    'Произвольное содержимое',\n  id:       'MMV2-X02',\n  safety:   'read-only',\n  tags:     ['JavaScript', 'setProps', 'setText', 'no DOM'],\n  title:    'Изменить тело JS-функцией через API',\n  useWhen:  'Когда данные приходят после открытия окна или общий configurator должен обновить представление, сохранив lifecycle, реактивность и локальное состояние Vue-компонента.',\n  warnings: [\n    'Прямая правка DOM обходит Vue render tree, может быть затёрта следующим render и создаёт XSS-, focus- и cleanup-риски.',\n    'Функция должна получать ModalInstance или предметный callback, а не HTMLElement внутренней поверхности окна.',\n  ],\n});\n\nexport const run = async ({ log, manager, own, wait }) => {\n  const instance = own(await manager.prepare(MODAL_ID, {\n    contentComponent: JsConfiguredContent,\n    contentProps:     {\n      description: 'JS-конфигуратор ещё не применил обновление.',\n      phase:       'начальное',\n    },\n  }));\n\n  instance\n    .setText('title', 'MMV2-X02: исходное тело')\n    .setPresentation({ size: 'sm' });\n\n  await manager.open(instance);\n\n  if (!await wait(700) || instance.status === 'closed') return;\n\n  updateRecipeBody(instance, {\n    description: 'Функция вызвала setProps({ contentProps }) у того же ModalInstance.',\n    phase:       'обновлённое',\n  });\n  log('JS-функция обновила тело через ModalInstance API', instance.getState());\n};\n","./mmv2-x03-vue-component.js":"// dependencies: vue\nimport {\n  defineComponent,\n  h,\n  markRaw,\n  ref,\n} from 'vue';\n\n// components\nimport UiButton from '@ui/Button/index.vue';\n\n// resources\nimport modalRecipeContentSource from '../modals/v2/ModalRecipeContent.vue?raw';
1\n\n\nconst MODAL_ID = 'ModalManagerV2CatalogApp/ModalRecipeContent';\n\nconst VueContent = markRaw(defineComponent({\n  name:  'RecipeVueContent',\n  props: {\n    label: { type: String, default: '' },\n  },\n  setup: props => {\n    const count = ref(0);\n\n    return _ => h('div', {\n      'data-test': 'mmv2-recipe-vue-content',\n    }, [\n      h('p', props.label),\n      h('p', `Локальный счётчик компонента: ${count.value}`),\n      h(UiButton, {\n        attributes: { type: 'button' },\n        buttonText: 'Увеличить счётчик',\n        onAction:   _ => { count.value += 1; },\n        type:       'secondary',\n      }),\n    ]);\n  },\n}));\n\nexport const supportingSources = Object.freeze([\n  Object.freeze({\n    label:  'Vue-оболочка произвольного содержимого',\n    source: modalRecipeContentSource,\n  }),\n]);\n\nexport const recipe = Object.freeze({\n  description: 'Передаёт version-controlled Vue-компонент как prop каталожной modal-оболочки. Компонент содержит собственное состояние, обработчик события и UiButton из дизайн-системы.',\n  expected:    [\n    'Внутри окна отображается Vue-компонент и кнопка из ui-list.',\n    'Каждое нажатие увеличивает локальный ref без обращения к manager и без remount окна.',\n    'Закрытие окна уничтожает вложенный Vue-компонент вместе с его локальным состоянием.',\n  ],\n  group:    'Произвольное содержимое',\n  id:       'MMV2-X03',\n  safety:   'read-only',\n  tags:     ['Vue', 'dynamic component', 'markRaw', 'UiButton'],\n  title:    'Открыть окно с Vue-компонентом',\n  useWhen:  'Когда телу окна нужны формы, реактивное состояние, события или переиспользуемые UI-компоненты. Это основной путь для сложного содержимого.',\n  warnings: [\n    'Vue-компонент является исполняемым кодом с полномочиями приложения: передавать можно только доверенный код из сборки, а не шаблон или компонент из ответа API.',\n    'Modal host не подключает router, Pinia и другие app plugins автоматически; зависимости компонента должны быть явными.',\n  ],\n});\n\nexport const run = async ({ log, manager, own }) => {\n  const instance = own(await manager.prepare(MODAL_ID, {\n    contentComponent: VueContent,\n    contentProps:     {\n      label: 'Этот блок полностью отрисован переданным Vue-компонентом.',\n    },\n  }));\n\n  instance\n    .setText('title', 'MMV2-X03: произвольный Vue-компонент')\n    .setPresentation({ size: 'sm' });\n\n  await manager.open(instance);\n  log('Vue-компонент открыт через convention-based modal ID', {\n    instanceId: instance.instanceId,\n    modalId:    MODAL_ID,\n  });\n};\n","./mmv2-x04-form-generator.js":"// classes\nimport { modalFlow } from '@classes/ModalManager/index.js';\n\n// resources\nimport formManagerSource from '@classes/FormManager/FormManager.js?raw';\nimport formInstanceSource from '@classes/FormManager/FormInstance.js?raw';\nimport formRuntimeSource from '@cmp/FormManager/FormRuntime.vue?raw';\nimport modalFormSource from '@cmp/CommonModals/ModalForm.vue?raw';\nimport formManagerModuleSource from '@/modules/form-manager/index.js?raw';\nimport formManagerRuntimeSource from '@/modules/form-manager/runtime.js?raw';\nimport newsletterSource from '@/modules/form-manager/forms/newsletter-subscription.js?raw';\n\n\nconst FORM_ID = 'Footer/NewsletterSubscription';\n\nexport const supportingSources = Object.freeze([\n  Object.freeze({ label: 'Публичный FormManager facade', source: formManagerSource }),\n  Object.freeze({ label: 'Project wiring и lazy catalog', source: formManagerModuleSource }),\n  Object.freeze({ label: 'Экземпляр подготовленной формы', source: formInstanceSource }),\n  Object.freeze({ label: 'DOM/ModalManagerV2 mount runtime', source: formManagerRuntimeSource }),\n  Object.freeze({ label: 'Общий lifecycle FormGenerator', source: formRuntimeSource }),\n  Object.freeze({ label: 'Универсальная оболочка ModalForm', source: modalFormSource }),\n  Object.freeze({ label: 'Factory формы рассылки', source: newsletterSource }),\n]);\n\nexport const recipe = Object.freeze({\n  description: 'Берёт предопределённую форму рассылки из lazy catalog FormManager и открывает её через универсальный ModalForm в presentation dialog-lg. Submit не обращается к backend: payload возвращается как result закрытого FormInstance.',\n  expected:    [\n    'В широком диалоге отображается FormGenerator с тематиками, контактными полями и согласиями формы рассылки.',\n    'Кнопка «Подписаться» использует client validation и busy lifecycle FormGenerator.',\n    'После submit окно закрывается, а журнал показывает JSON-совместимый form payload без сетевого запроса.',\n    'Reset закрывает только принадлежащий кейсу FormInstance и уничтожает form store.',\n  ],\n  group:    'Произвольное содержимое',\n  id:       'MMV2-X04',\n  safety:   'read-only',\n  tags:     ['FormGenerator', 'FormManager', 'ModalForm', 'dialog-lg'],\n  title:    'Открыть форму рассылки в dialog-lg',\n  useWhen:  'Когда длинной форме нужен широкий диалог, а consumer должен вызвать её по стабильному ID без ручной сборки ModalInstance и Vuex lifecycle.',\n  warnings: [\n    'ModalForm — универсальный modal adapter для FormGenerator, а не отдельный renderer формы.',\n    'Factory создаёт новый dataForm для каждого открытия;
1 mutable runtime между сессиями не переиспользуется.',\n    'Пример намеренно не вызывает production transport формы рассылки.',\n  ],\n});\n\nexport const run = async ({\n  globalTarget,\n  log,\n  namespaceKey,\n  own,\n}) => {\n  const FormManager = globalTarget[namespaceKey]?.FormManager;\n  const modalManager = globalTarget[namespaceKey]?.ModalManagerV2;\n  if (!FormManager) throw new Error('FormManager is not available');\n  if (!modalManager) throw new Error('ModalManagerV2 is not available');\n\n  const instance = own(await FormManager.open(FORM_ID, {}, {\n    mount:    modalManager,\n    onSubmit: payload => {\n      const result = payload?.sendData?.data ?? null;\n      log('FormManager получил submit формы рассылки', result);\n      return modalFlow.close(result);\n    },\n    presentation: { preset: 'dialog-lg' },\n  }));\n  log('Форма рассылки открыта через ModalForm', {\n    formId:     FORM_ID,\n    instanceId: instance.instanceId,\n    modalId:    instance.mountResult.id,\n    preset:     'dialog-lg',\n  });\n};\n\nexport default recipe;\n","./mmv2-x05-callback-form.js":"// classes\nimport { modalFlow } from '@classes/ModalManager/index.js';\n\n// resources\nimport callbackSource from '@/modules/form-manager/forms/callback-request.js?raw';\nimport modalFormSource from '@cmp/CommonModals/ModalForm.vue?raw';\n\n\nconst FORM_ID = 'Footer/CallbackRequest';\n\nexport const supportingSources = Object.freeze([\n  Object.freeze({ label: 'Factory формы обратного звонка', source: callbackSource }),\n  Object.freeze({ label: 'Универсальная оболочка ModalForm', source: modalFormSource }),\n]);\n\nexport const recipe = Object.freeze({\n  description: 'Открывает предопределённую форму обратного звонка через тот же ModalForm, но меняет presentation на боковой sheet. Так форма и её lifecycle остаются прежними, а способ показа задают options текущего вызова FormManager.',\n  expected:    [\n    'Справа открывается sheet с полями имени, телефона и двумя согласиями.',\n    'Форма использует ту же universal ModalForm, что и кейс рассылки; отличается только presentation.',\n    'Submit проходит client validation, не вызывает backend и закрывает FormInstance с payload в result.',\n  ],\n  group:    'Произвольное содержимое',\n  id:       'MMV2-X05',\n  safety:   'read-only',\n  tags:     ['FormGenerator', 'FormManager', 'ModalForm', 'sheet'],\n  title:    'Открыть форму звонка в sheet',\n  useWhen:  'Когда компактную форму нужно показать сбоку и сохранить единый API open/closed независимо от presentation ModalManagerV2.',\n  warnings: [\n    'Presentation не должен попадать внутрь FormGenerator-схемы: его применяет modal adapter FormManager.',\n    'Пример намеренно не вызывает production transport формы обратного звонка.',\n  ],\n});\n\nexport const run = async ({\n  globalTarget,\n  log,\n  namespaceKey,\n  own,\n}) => {\n  const FormManager = globalTarget[namespaceKey]?.FormManager;\n  const modalManager = globalTarget[namespaceKey]?.ModalManagerV2;\n  if (!FormManager) throw new Error('FormManager is not available');\n  if (!modalManager) throw new Error('ModalManagerV2 is not available');\n\n  const instance = own(await FormManager.open(FORM_ID, {}, {\n    mount:    modalManager,\n    onSubmit: payload => {\n      const result = payload?.sendData?.data ?? null;\n      log('FormManager получил submit формы обратного звонка', result);\n      return modalFlow.close(result);\n    },\n    presentation: { preset: 'sheet', side: 'right' },\n  }));\n  log('Форма обратного звонка открыта через ModalForm', {\n    formId:     FORM_ID,\n    instanceId: instance.instanceId,\n    modalId:    instance.mountResult.id,\n    preset:     'sheet',\n  });\n};\n\nexport default recipe;\n"}),cn=Object.freeze(Object.entries(sn).map(([e,n])=>((e,n)=>{const t=n.recipe,a=n.supportingSources??[];if(!/^MMV2-[A-Z]\d{2}$/.test(t?.id??""))throw new TypeError(`Recipe "${e}" must have a stable MMV2 ID`);if(!rn.includes(t.group))throw new TypeError(`Recipe "${t.id}" has an unknown group`);if(!Object.values(on).includes(t.safety))throw new TypeError(`Recipe "${t.id}" has an unknown safety profile`);if(!Array.isArray(t.expected)||0===t.expected.length)throw new TypeError(`Recipe "${t.id}" must describe expected results`);
1if("function"!=typeof n.run)throw new TypeError(`Recipe "${t.id}" must export run()`);if(!Array.isArray(t.warnings??[])||!Array.isArray(a)||a.some(e=>"string"!=typeof e?.label||"string"!=typeof e?.source))throw new TypeError(`Recipe "${t.id}" has invalid supporting content`);const r=ln[e],o=[{label:"Код запуска кейса",source:r},...a].map(e=>Object.freeze({...e}));return Object.freeze({...t,expected:Object.freeze([...t.expected]),run:n.run,source:r,sources:Object.freeze(o),tags:Object.freeze([...t.tags??[]]),warnings:Object.freeze([...t.warnings??[]])})})(e,n)).sort((e,n)=>rn.indexOf(e.group)-rn.indexOf(n.group)||e.id.localeCompare(n.id))),dn=["aria-live","role"],un=["href"],mn=["id"],pn=["href"],gn=["aria-label"],fn=["innerHTML"],Mn=["innerHTML"],bn=["id"],yn=["href"],hn="b-button b-button--secondary",vn={__name:"index",props:{manager:l(Object,null),managerError:l(Object,null),root:l(Object,null,!0)},setup(e){const n=e,{$b:t}=s("modal-manager-v2-catalog"),a={iconAlign:"left",loaderAlign:"right",size:"",type:"primary"},r=o(""),l=o({query:"",selected:{},count:0}),z=o(!1),D=o(!1),L=o(!1),G=o([]),$=o(n.manager?.getState()??null),U=o(""),B=o(""),W=o("");let q=null,H=null;const X=rn.filter(e=>cn.some(n=>n.group===e)),J=X.map(e=>({group:e,recipes:cn.filter(n=>n.group===e)})),K=c(e=>({id:"modal-product-filters",name:"Фильтры",count:l.value.count||null,active:l.value.count>0})),Y={content:t`,readiness-content`,root:t`,readiness`,summary:`${t`,readiness-summary`} ${hn}`},Z={tooltipClose:"Развернуть пояснение о готовности и состоянии менеджера",tooltipOpen:"Свернуть пояснение о готовности и состоянии менеджера"},Q={content:t`,source-content`,root:t`,source`,summary:`${t`,summary`} ${hn}`},ee={tooltipClose:"Развернуть исходный код кейса",tooltipOpen:"Свернуть исходный код кейса"},ne={content:t`,toc-content`,root:t`,toc`,summary:`${t`,toc-summary`} ${hn}`},te={tooltipClose:"Развернуть содержание кейсов",tooltipOpen:"Свернуть содержание кейсов"},ae=e=>e.code,re={attributes:{autocomplete:"off",placeholder:"ID, название, тег"},id:"mmv2-recipe-search",texts:{label:"Поиск по кейсам"},type:"search"},oe={options:[{code:"",id:"all",label:"Все группы"},...X.map((e,n)=>({code:e,id:`group-${n}`,label:e}))],texts:{label:"Группа",notFound:"Группа не найдена",placeholder:"Все группы"}},se={options:[{code:"",id:"all",label:"Любое влияние"},{code:"read-only",id:"read-only",label:"Без изменений данных"},{code:"local-side-effect",id:"local-side-effect",label:"Локальное действие в браузере"}],texts:{label:"Влияние кейса",notFound:"Вариант влияния не найден",placeholder:"Любое влияние"}},ie=c(e=>n.managerError?`Каталог не может запускать кейсы. Код ошибки: ${n.managerError.code??"ERROR"}. Техническое сообщение: ${n.managerError.message}`:""),le=c(e=>{const n=U.value.trim().toLocaleLowerCase("ru");return cn.filter(e=>{const t=[e.id,e.title,e.description,e.useWhen,...e.tags].join(" ").toLocaleLowerCase("ru");return(!n||t.includes(n))&&(!B.value||e.group===B.value)&&(!W.value||e.safety===W.value)})}),ce=e=>{try{return JSON.stringify(e,null,2)}catch{return String(e)}},de=c(e=>G.value.length?G.value.map(e=>{const n=null===e.data?"":`\n${ce(e.data)}`;return`[${e.time}] ${e.level.toUpperCase()} ${e.message}${n}`}).join("\n\n"):"Журнал пока пуст."),ue=(e,n="blue")=>({classes:{root:t`,badge`},texts:{text:e},type:n}),me=e=>{G.value.push({...e,time:(new Date).toLocaleTimeString("ru-RU")})},pe=async({preserveFilters:e=!1}={})=>{if(e||(l.value={query:"",selected:{},count:0}),q){D.value=!0;try{await q.reset(),me({data:null,level:"info",message:"Кейс сброшен"})}finally{q=null,z.value=!1,D.value=!1}}},ge=async(e,t)=>{const a=t?.event?.currentTarget;await pe({preserveFilters:"MMV2-C05"===e.id}),r.value=e.id,G.value=[],L.value=!0,location.hash=e.id.toLowerCase(),q=(({eventTarget:e=globalThis.document??null,globalTarget:n=globalThis,manager:t,namespaceKey:a="infotecs",onLog:r=e=>null})=>{const o=[],s=new AbortController,i=new Set;let l=null;const c=(e,n=null,t="info")=>{r({data:n,level:t,message:e})},d=e=>(o.push(e),n=>{const t=o.indexOf(e);return-1!==t&&(o.splice(t,1),!0)});return{context:{createLocalObjectUrl:e=>{const t=n.URL.createObjectURL(e);let a=!0;const r=e=>!!a&&(a=!1,n.URL.revokeObjectURL(t),!0);return d(r),{revoke:r,url:t}},eventTarget:e,globalTarget:n,log:c,manager:t,namespaceKey:a,own:e=>(i.add(e),Promise.resolve(e.closed).then(n=>{i.delete(e),c(`Экземпляр ${e.instanceId} закрыт`,n)},n=>{i.delete(e),c(`Ошибка closed у ${e.instanceId}`,N(n),"error")}),e),registerCleanup:d,signal:s.signal,wait:e=>((e,n)=>new Promise(t=>{if(n.aborted)return void t(!1);const a=e=>{clearTimeout(r),t(!1)},r=setTimeout(e=>{n.removeEventListener("abort",a),t(!0)},e);n.addEventListener("abort",a,{once:!0})}))(e,s.signal)},reset:e=>l||(l=(async()=>{s.abort();const e=[...i].reverse();for(const t of e)if("closed"!==t.status)try{await t.close({recipeReset:!0})}catch(n){c(`Не удалось закрыть ${t.instanceId}`,N(n),"error")}i.clear();for(const t of[...o].reverse())try{await t()}catch(n){c("Ошибка cleanup кейса",N(n),"error")}o.length=0})(),l)}})({manager:n.manager,namespaceKey:x,onLog:me}),z.value=!0;try{me({data:null,level:"info",message:`Запуск ${e.id}`}),await e.run({...q.context,anchor:a,appliedFilters:l.value,onFiltersApplied:e=>l.value=e}),me({data:null,level:"info",message:`${e.id} запущен`})}catch(o){me({data:{code:o?.code??null,message:o?.message??String(o)},level:"error",message:`${e.id} завершился ошибкой`})}finally{L.value=!1}};return d(async e=>{n.manager&&(H=n.manager.subscribe(e=>{$.value=e})),await u();const t=location.hash.slice(1).toUpperCase(),a=cn.find(e=>e.id===t);a&&document.getElementById(a.id.toLowerCase())?.scrollIntoView()}),m(e=>{H?.(),q?.reset()}),(n,o)=>(p(),g(h,null,[f("header",{class:M(b(t)`,header`)},[f("p",{class:M(b(t)`,eyebrow`)},"Инструменты интерфейса",2),f("h1",{class:M(b(t)`,title`)},"ModalManagerV2",2),f("p",{class:M(b(t)`,lead`)}," ModalManagerV2 — сервис фронтенда, который создаёт, настраивает, показывает и закрывает модальные окна. На этой странице «кейс» — небольшой исполняемый пример: он объясняет задачу, запускает публичный API и показывает ожидаемый результат, журнал выполнения и исходный код. ",2)],2),f("section",{"aria-live":e.managerError?"assertive":"polite",class:M([b(t)`,status`,e.managerError&&b(t)`,status,error`]),role:e.managerError?"alert":"status"},[f("h2",{class:M(b(t)`,status-title`)},y(e.managerError?"ModalManagerV2 недоступен":"ModalManagerV2 готов к запуску кейсов"),3),e.managerError?(p(),g("p",{key:0,class:M(b(t)`,status-text`)},y(ie.value),3)):(p(),g(h,{key:1},[f("p",{class:M(b(t)`,status-text`)},[...o[5]||(o[5]=[v(" Общий экземпляр доступен по ключу ",-1),f("code",null,"globalThis.infotecs.ModalManagerV2",-1),v(". Каталог дождался завершения ",-1),f("code",null,"whenReady()",-1),v(" и может запускать кейсы. ",-1)])],2),w(C,{classes:Y,"is-opened":!1,"is-use-slot":!0,texts:Z},{summary:O(()=>[...o[6]||(o[6]=[f("span",null,"Подробнее о готовности и состоянии",-1)])]),content:O(()=>[f("p",{class:M(b(t)`,status-text`)},[...o[7]||(o[7]=[f("code",null,"globalThis",-1),v(" — глобаÐ
1»ÑŒÐ½Ñ‹Ð¹ объект текущей страницы, ",-1),f("code",null,"infotecs",-1),v(" — пространство имён (namespace) фронтенда проекта, а ",-1),f("code",null,"ModalManagerV2",-1),v(" — свойство с экземпляром менеджера. Namespace собирает публичные объекты проекта под одним глобальным ключом, вместо того чтобы добавлять каждый объект отдельно в ",-1),f("code",null,"globalThis",-1),v(". ",-1)])],2),f("p",{class:M(b(t)`,status-text`)},[...o[8]||(o[8]=[v(" Успешное завершение ",-1),f("code",null,"whenReady()",-1),v(" означает, что служебный компонент для монтирования окон в DOM (в API он называется ",-1),f("code",null,"host",-1),v(") инициализирован и менеджер может принимать команды. «Готов» не означает, что какое-либо окно уже открыто. Текущий снимок показывается возле запущенного кейса, а наблюдаемые переходы остаются в его журнале. Кейсы используют общий экземпляр — отдельный менеджер для страницы не создаётся. ",-1)])],2),f("h3",{class:M(b(t)`,state-definitions-title`)},"Поля снимка состояния",2),f("dl",{class:M(b(t)`,state-definitions`)},[f("div",{class:M(b(t)`,state-definition`)},[...o[9]||(o[9]=[f("dt",null,[f("code",null,"revision")],-1),f("dd",null," Увеличивается при каждом зафиксированном изменении менеджера и показывает, появился ли новый снимок. ",-1)])],2),f("div",{class:M(b(t)`,state-definition`)},[...o[10]||(o[10]=[f("dt",null,[f("code",null,"openCount")],-1),f("dd",null," Количество экземпляров в стеке показа, включая верхнее окно и перекрытые им нижние окна. ",-1)])],2),f("div",{class:M(b(t)`,state-definition`)},[...o[11]||(o[11]=[f("dt",null,[f("code",null,"liveCount")],-1),f("dd",null," Количество созданных экземпляров, которые ещё не были полностью закрыты: подготовленных к показу и находящихся в стеке. ",-1)])],2),f("div",{class:M(b(t)`,state-definition`)},[...o[12]||(o[12]=[f("dt",null,[f("code",null,"activeModalId")],-1),f("dd",null,[v(" ID верхнего окна, с которым взаимодействует пользователь; "),f("code",null,"null"),v(" означает, что стек показа пуст. ")],-1)])],2)],2)]),_:1})],64))],10,dn),f("section",{class:M(b(t)`,guide`),"aria-labelledby":"mmv2-guide-title"},[f("h2",{id:"mmv2-guide-title",class:M(b(t)`,guide-title`)},"Как пользоваться каталогом",2),f("ol",{class:M(b(t)`,guide-list`)},[...o[13]||(o[13]=[f("li",null,"Выберите кейс и прочитайте, для какой задачи он подходит.",-1),f("li",null,"Сверьте раздел «Что должно произойти» и нажмите «Запустить кейс».",-1),f("li",null,"Выполните действие в открывшемся окне и сравните журнал с текущим состоянием менеджера.",-1),f("li",null,"Закройте окно и нажмите «Сбросить кейс», чтобы освободить созданные им локальные ресурсы.",-1)])],2),f("p",{class:M(b(t)`,guide-note`)}," Метка «без изменений данных» означает, что кейс не меняет серверные данные. «Локальное действие в браузере» может, например, создать временный файл или запустить его загрузку, но также не отправляет изменения на сервер. ",2),f("p",{class:M(b(t)`,guide-note`)}," Безопасность содержимого оценивается отдельно от влияния на данные. Предупреждение в карточке означает, что кейс имеет обязательную границу доверия: например, HTML требует очистки, а Vue-компонент — доверенного version-controlled кода. ",2),w(C,{classes:ne,"is-opened":!1,"is-use-slot":!0,texts:te},{summary:O(()=>[f("span",{class:M(b(t)`,toc-heading`)},[o[14]||(o[14]=f("span",null,"Содержание кейсов",-1)),f("span",{class:M(b(t)`,toc-count`)},"Всего: "+y(b(cn).length),3)],2)]),content:O(()=>[f("nav",{class:M(b(t)`,toc-nav`),"aria-label":"Содержание кейсов"},[f("ul",{class:M(b(t)`,toc-groups`)},[(p(!0),g(h,null,F(b(J),e=>(p(),g("li",{key:e.group,class:M(b(t)`,toc-group`)},[f("h3",{class:M(b(t)`,toc-group-title`)},y(e.group),3),f("ul",{class:M(b(t)`,toc-list`)},[(p(!0),g(h,null,F(e.recipes,e=>(p(),g("li",{key:e.id},[f("a",{class:M(b(t)`,toc-link`),href:`#${e.id.toLowerCase()}`},[f("span",{class:M(b(t)`,toc-id`)}
1,y(e.id),3),f("span",null,y(e.title),1)],10,un)]))),128))],2)],2))),128))],2)],2)]),_:1})],2),f("form",{class:M(b(t)`,filters`),role:"search",onSubmit:o[3]||(o[3]=S(()=>{},["prevent"]))},[w(T,{modelValue:U.value,"onUpdate:modelValue":o[0]||(o[0]=e=>U.value=e),params:re},null,8,["modelValue"]),w(P,{modelValue:B.value,"onUpdate:modelValue":o[1]||(o[1]=e=>B.value=e),params:oe,reduce:ae},null,8,["modelValue"]),w(P,{modelValue:W.value,"onUpdate:modelValue":o[2]||(o[2]=e=>W.value=e),params:se,reduce:ae},null,8,["modelValue"])],34),f("section",{"aria-label":"Кейсы ModalManagerV2",class:M(b(t)`,recipe-list`)},[(p(!0),g(h,null,F(le.value,n=>{return p(),g("article",{id:n.id.toLowerCase(),key:n.id,class:M([b(t)`,recipe`,r.value===n.id&&b(t)`,recipe,active`])},[f("div",{class:M(b(t)`,recipe-heading`)},[f("h2",{class:M(b(t)`,recipe-title`)},[f("a",{class:M(b(t)`,recipe-link`),href:`#${n.id.toLowerCase()}`},y(n.id)+" — "+y(n.title),11,pn)],2)],2),f("div",{class:M(b(t)`,badges`)},[w(_,{params:ue(n.group)},null,8,["params"]),w(_,{params:ue((s=n.safety,"read-only"===s?"без изменений данных":"локальное действие в браузере"),"local-side-effect"===n.safety?"red":"blue")},null,8,["params"]),(p(!0),g(h,null,F(n.tags,e=>(p(),j(_,{key:e,params:ue(e)},null,8,["params"]))),128))],2),f("p",{class:M(b(t)`,recipe-text`)},y(n.description),3),f("p",{class:M(b(t)`,recipe-usage`)},[o[15]||(o[15]=f("strong",null,"Когда использовать:",-1)),v(" "+y(n.useWhen),1)],2),n.warnings.length?(p(),g("aside",{key:0,class:M(b(t)`,warning`)},[o[16]||(o[16]=f("strong",null,"Ограничения безопасности и архитектуры:",-1)),f("ul",{class:M(b(t)`,warning-list`)},[(p(!0),g(h,null,F(n.warnings,e=>(p(),g("li",{key:e},y(e),1))),128))],2)],2)):I("",!0),f("p",{class:M(b(t)`,expected-title`)},[...o[17]||(o[17]=[f("strong",null,"Что должно произойти:",-1)])],2),f("ul",{class:M(b(t)`,expected`)},[(p(!0),g(h,null,F(n.expected,e=>(p(),g("li",{key:e},y(e),1))),128))],2),f("div",{class:M(b(t)`,actions`)},["chip"===n.trigger?(p(),j(A,{key:0,params:{...K.value,disabled:Boolean(e.managerError)||L.value},onClick:e=>ge(n)},null,8,["params","onClick"])):(p(),j(i,R({key:1,ref_for:!0},a,{"aria-busy":L.value&&r.value===n.id,"button-text":L.value&&r.value===n.id?"Кейс выполняется…":"Запустить кейс",disabled:Boolean(e.managerError)||L.value,onAction:e=>ge(n,e)}),null,16,["aria-busy","button-text","disabled","onAction"])),w(i,R({ref_for:!0},a,{"button-text":D.value&&r.value===n.id?"Кейс сбрасывается…":"Сбросить кейс",disabled:r.value!==n.id||!z.value||D.value,type:"secondary",onAction:o[4]||(o[4]=e=>pe())}),null,16,["button-text","disabled"])],2),r.value===n.id?(p(),g("section",{key:1,"aria-label":`Результат выполнения ${n.id}`,class:M(b(t)`,runtime`)},[f("h3",{class:M(b(t)`,panel-title`)},"Что произошло после запуска",2),f("div",{class:M(b(t)`,runtime-grid`)},[f("section",null,[f("h4",{class:M(b(t)`,panel-subtitle`)},"Журнал шагов кейса",2),f("pre",{class:M(b(t)`,log`),"aria-live":"polite"},y(de.value),3)]),f("section",null,[f("h4",{class:M(b(t)`,panel-subtitle`)},"Текущий снимок состояния менеджера",2),f("pre",{class:M(b(t)`,state`)},[f("code",{class:"hljs language-json",innerHTML:b(E)(ce($.value),"json")},null,8,fn)],2)])],2)],10,gn)):I("",!0),w(C,{classes:Q,"is-opened":!1,"is-use-slot":!0,texts:ee},{summary:O(()=>[...o[18]||(o[18]=[f("span",null,"Исходный код кейса",-1)])]),content:O(()=>[(p(!0),g(h,null,F(n.sources,e=>(p(),g("section",{key:e.label,class:M(b(t)`,source-entry`)},[f("h4",{class:M(b(t)`,source-title`)},y(e.label),3),f("pre",{class:M(b(t)`,code`)},[f("code",{class:M(`hljs language-${b(V)(e.source)}`),innerHTML:b(E)(e.source,b(V)(e.source))},null,10,Mn)],2)],2))),128))]),_:2},1024)],10,mn);var s}),128)),0===le.value.length?(p(),g("p",{key:0,class:M(b(t)`,empty`)}," По заданным фильтрам кейсы не найдены. ",2)):I("",!0)],2),f("section",{class:M(b(t)`,automated`),"aria-labelledby":"mmv2-automated-title"},[f("h2",{id:"mmv2-automated-title",class:M(b(t)`,automated-title`)}," Сценарии только для автоматизированной проверки ",2),f("p",{class:M(b(t)`,automated-text`)}," У этих карточек нет кнопки запуска: сценарии уничтожают менеджер, передают заведомо ошибочные данные или воспроизводят гонки асинхронных операций. Они выполняются тестовым раннером на отдельном изолированном экземпляре и никогда не используют общий менеджер этой страницы. ",2),f("div",{class:M(b(t)`,recipe-list`)},[(p(!0),g(h,null,F(b(k),e=>(p(),g("article",{id:e.id.toLowerCase(),key:e.id,class:M(b(t)`,recipe`)},[f("h3",{class:M(b(t)`,recipe-title`)},y(e.id)+" — "+y(e.title),3),f("p",{class:M(b(t)`,recipe-text`)},y(e.expected),3),f("p",{class:M(b(t)`,recipe-usage`)},[o[19]||(o[19]=f("strong",null,"Файл автоматического теста:",-1)),o[20]||(o[20]=v()),f("a",{href:e.source.href,rel:"noopener noreferrer",target:"_blank"},[f("code",null,y(e.source.label),1)],8,yn)],2)],10,bn))),128))],2)],2)
1],64))}};export{vn as default};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.