[350-1][@ant] feat: вынести реестры ledger2 в cooptypes, slim-YAML §6 и деплой standards-site на /standards — один TS-источник правды для имён счетов/кошельков (standards-site + desktop/shared/lib/ledger2 станет re-export после мёржа в reports), spec манифеста приведена к тому что реально рендерит фронт (без ролей-enum, без scenario, без Doxygen-id), FocusBar показывает Проводки/Переводы отдельными карточками с tooltip-подсказками из @coopenomics/cooptypes, клик по × и ● даёт описание из YAML, автопан при навигации ←/→, мобильная заглушка <900px, scrollBehavior при переходе на related; публикация через mono publish-docs с base=/standards/ и ссылкой в mkdocs nav, чтобы председатели сразу попадали на визуальный паспорт процесса с docs.coopenomics.world

This commit is contained in:
coopops
2026-04-23 10:45:02 +00:00
parent 948388d7e8
commit f80a6fdf47
25 changed files with 795 additions and 818 deletions
+14
View File
@@ -84,6 +84,20 @@ jobs:
mkdocs build
working-directory: ./components/docs
# ── standards-site → /standards/ на docs.coopenomics.world ───────
# Отдельный Vue-сайт с BPMN-графом кооперативных стандартов,
# публикуется как поддиректория на том же домене. Vite собирает
# с base='/standards/' (см. vite.config.ts), относительные пути
# внутри dist/ и hash-router работают корректно.
- name: Build standards-site
run: pnpm run build
working-directory: ./components/contracts/standards-site
- name: Copy standards-site into docs site/standards
run: |
mkdir -p ./components/docs/site/standards
cp -r ./components/contracts/standards-site/dist/* ./components/docs/site/standards/
- name: Remove specific large file before publishing
run: |
# Удаляем конкретный большой файл sdk/typedoc.json
@@ -79,6 +79,7 @@ actions:
# финальный `active` проставляется в таблице `accounts` в действии confirmreg.
# Для BPMN-графа мы показываем единый жизненный цикл процесса.
entity: "registrator::candidate → registrator::account"
entity_human: "Кандидат → пайщик"
entity_source: cpp/registrator/src/user/
states:
@@ -237,62 +238,41 @@ documents:
stored_in: "(authorization — в параметре действия, не хранится в candidates после удаления записи)"
note: "TODO: сверить идентификатор шаблона с fabric/registry.md"
# ── Секция 6. Операции ledger2 ──────────────────────────────────────────────
# ── Секция 6. Операции ──────────────────────────────────────────────────────
# Процесс мульти-операционный: две записи ACTION_REGISTRY с process_type = reg.regist.
# Обе срабатывают на закрывающем действии confirmreg.
# Источник: ledger2/actions.hpp — записи #1 и #2.
# Slim-формат: коды счетов и id кошельков → человеческие имена
# подставляет фронтенд из `standards-site/src/data/registries.ts`
# (зеркало реестра из mono-ai-1/components/desktop/src/shared/lib/ledger2/).
operations:
- ledger_code: reg.entrfee
human_name: Вступительный взнос пайщика
wallet_op: ISSUE
wallet_from: null
wallet_to:
id: 3001
name: ENTRANCE_FEES
human: Вступительные взносы
debit:
account: 51
name: BANK_ACCOUNT
human: Расчётный счёт
type: ACTIVE
credit:
account: 86
name: TARGET_RECEIPTS
human: Целевое финансирование
type: PASSIVE
wallet_to: 3001 # Вступительные взносы
debit: 51 # Расчётный счёт
credit: 86 # Целевое финансирование
amount_ref: candidate.initial
triggered_by: registrator::confirmreg
description: >
Первичный вход вступительного взноса на кошелёк ENTRANCE_FEES.
Двойная запись: Дт 51 «Расчётный счёт» / Кт 86 «Целевое финансирование» —
средства поступили на расчётный счёт и квалифицируются как целевые
поступления в кооперативный фонд.
Первичный вход вступительного взноса на кошелёк «Вступительные взносы».
Двойная запись Дт 51 / Кт 86 — средства поступили на расчётный счёт и
квалифицируются как целевые поступления в кооперативный фонд.
- ledger_code: reg.minshare
human_name: Минимальный паевой взнос при регистрации
wallet_op: ISSUE
wallet_from: null
wallet_to:
id: 2002
name: MIN_SHARE_FUND
human: Минимальный паевой взнос
debit:
account: 51
name: BANK_ACCOUNT
human: Расчётный счёт
type: ACTIVE
credit:
account: 80
name: SHARE_FUND
human: Паевой фонд (складочный капитал)
type: PASSIVE
wallet_to: 2002 # Минимальный паевой взнос
debit: 51 # Расчётный счёт
credit: 80 # Паевой фонд (складочный капитал)
amount_ref: candidate.minimum
triggered_by: registrator::confirmreg
description: >
Первичный вход минимального паевого взноса на кошелёк MIN_SHARE_FUND.
Двойная запись: Дт 51 «Расчётный счёт» / Кт 80 «Паевой фонд» —
средства поступили на расчётный счёт и формируют складочный капитал
кооператива.
Первичный вход минимального паевого взноса на кошелёк «Минимальный
паевой взнос». Двойная запись Дт 51 / Кт 80 — средства поступили
на расчётный счёт и формируют складочный капитал кооператива.
# ── Секция 7. Связи ─────────────────────────────────────────────────────────
related:
@@ -65,6 +65,7 @@ actions:
# после completedpst. Узел в графе нужен, чтобы обозначить успешное завершение
# и привязать к нему ledger2-операцию.
entity: wallet::deposit
entity_human: Заявка на взнос
entity_source: cpp/wallet/src/deposit/
states:
@@ -164,36 +165,26 @@ scenario:
# самим платежом. Секция оставлена пустой намеренно.
documents: []
# ── Секция 6. Операции ledger2 ──────────────────────────────────────────────
# ── Секция 6. Операции ──────────────────────────────────────────────────────
# Одноактовый процесс: в ACTION_REGISTRY одна запись с process_type=wall.deposit.
# Источник: ledger2/actions.hpp — запись #3 (wall.depcpl).
# Slim-формат: коды/id → имена подставляет фронтенд из registries.ts.
operations:
- ledger_code: wall.depcpl
human_name: Внесение пайщиком паевого взноса
wallet_op: ISSUE
wallet_from: null
wallet_to:
id: 2001
name: SHARE_FUND_PAY
human: ЦПП «Цифровой Кошелёк» — паевые взносы деньгами
debit:
account: 51
name: BANK_ACCOUNT
human: Расчётный счёт
type: ACTIVE
credit:
account: 80
name: SHARE_FUND
human: Паевой фонд (складочный капитал)
type: PASSIVE
wallet_to: 2001 # ЦПП «Цифровой Кошелёк» — паевые взносы деньгами
debit: 51 # Расчётный счёт
credit: 80 # Паевой фонд (складочный капитал)
amount_ref: deposit.quantity
triggered_by: wallet::completedpst
description: >
Первичный вход средств на кошелёк SHARE_FUND_PAY пайщика.
Двойная запись: Дт 51 «Расчётный счёт» / Кт 80 «Паевой фонд» —
деньги пайщика поступили на расчётный счёт и увеличили складочный
капитал кооператива. Средства доступны пайщику для дальнейших
операций (инвестиции в программы, конвертации, возврат).
Первичный вход средств на кошелёк пайщика «ЦПП Цифровой Кошелёк».
Двойная запись Дт 51 / Кт 80 — деньги пайщика поступили на расчётный
счёт и увеличили складочный капитал кооператива. Средства доступны
пайщику для дальнейших операций (инвестиции в программы, конвертации,
возврат).
# ── Секция 7. Связи ─────────────────────────────────────────────────────────
related:
@@ -15,6 +15,7 @@
"@vue-flow/controls": "^1.1.3",
"@vue-flow/core": "^1.48.2",
"@vue-flow/minimap": "^1.5.4",
"cooptypes": "workspace:*",
"lucide-vue-next": "^0.460.0",
"vue": "^3.5.31",
"vue-router": "^4.6.4",
@@ -1,9 +1,38 @@
<script setup lang="ts">
import { onMounted, onBeforeUnmount, ref } from 'vue';
import Sidebar from '@/components/Sidebar.vue';
const MOBILE_BREAKPOINT = 900;
const isMobile = ref(false);
function updateIsMobile(): void {
if (typeof window === 'undefined') return;
isMobile.value = window.innerWidth < MOBILE_BREAKPOINT;
}
onMounted(() => {
updateIsMobile();
window.addEventListener('resize', updateIsMobile);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', updateIsMobile);
});
</script>
<template>
<div class="app-shell">
<div v-if="isMobile" class="mobile-stub">
<div class="mobile-stub__box">
<h1>Только для десктопа</h1>
<p>
Реестр кооперативных стандартов рассчитан на широкие экраны
BPMN-граф процесса не помещается на мобильных устройствах.
Откройте сайт с компьютера или планшета.
</p>
</div>
</div>
<div v-else class="app-shell">
<aside class="app-sidebar">
<Sidebar />
</aside>
@@ -5,8 +5,8 @@
* Показывает одно из: статус / действие / документ / операцию.
*/
import { computed } from 'vue';
import { useRouter } from 'vue-router';
import type { Standard, Transition, Ledger2Operation, ContractAction, ProcessDocument } from '@/types/standard';
import { getAccount, getWallet } from '@/data/registries';
const props = defineProps<{
standard: Standard;
@@ -16,10 +16,10 @@ const props = defineProps<{
focusOperation: string | null;
}>();
const router = useRouter();
const INITIAL_MARKER = '∅';
const START_ID = '__start__';
const END_ID = '__end__';
const REJECTED_PREFIX = '__rejected__';
const ROLE_HUMAN: Record<string, string> = {
contributor: 'Участник',
@@ -95,19 +95,55 @@ const focusedState = computed(() => {
});
const focusMode = computed<
'action' | 'document' | 'operation' | 'state' | 'process-start' | 'process-end' | 'none'
| 'action'
| 'document'
| 'operation'
| 'state'
| 'process-start'
| 'process-end'
| 'process-rejected'
| 'none'
>(() => {
if (focusedTransition.value) return 'action';
if (focusedDocument.value) return 'document';
if (focusedOperation.value) return 'operation';
if (props.focusStatus === START_ID) return 'process-start';
if (props.focusStatus === END_ID) return 'process-end';
if (rejectedInfo.value) return 'process-rejected';
if (focusedState.value) return 'state';
return 'none';
});
const relatedCount = computed(() => (props.standard.related ?? []).length);
// ── Данные для «завершения отказом» ─────────────────────────────────────
interface RejectedInfo {
transition: Transition;
action: ContractAction | undefined;
virtualState: Standard['states'][number] | undefined;
}
function parseRejectedId(id: string): { from: string; actionShort: string } | null {
if (!id.startsWith(REJECTED_PREFIX)) return null;
const rest = id.slice(REJECTED_PREFIX.length);
const idx = rest.indexOf('__');
if (idx < 0) return null;
return { from: rest.slice(0, idx), actionShort: rest.slice(idx + 2) };
}
const rejectedInfo = computed<RejectedInfo | null>(() => {
const id = props.focusStatus;
if (!id) return null;
const parsed = parseRejectedId(id);
if (!parsed) return null;
const transition = props.standard.transitions.find((t) => {
const from = t.from === INITIAL_MARKER ? 'start' : t.from;
return from === parsed.from && actionShort(t.action) === parsed.actionShort;
});
if (!transition) return null;
const action = props.standard.actions.find((a) => a.name === transition.action);
const virtualState = props.standard.states.find((s) => s.name === transition.to);
return { transition, action, virtualState };
});
// ── Данные для «конца процесса» ─────────────────────────────────────────
const terminalStates = computed(() => {
const vset = virtualStates.value;
@@ -121,18 +157,29 @@ const terminalStates = computed(() => {
});
});
function goToState(name: string): void {
if (virtualStates.value.has(name)) return;
router.push({ query: { s: name } });
function accountTitle(code: number | null | undefined): string {
if (code == null) return 'Счёт не задан';
const meta = getAccount(code);
if (!meta) return `${code} (нет в реестре)`;
const kind = meta.kind === 'active' ? 'актив' : meta.kind === 'passive' ? 'пассив' : 'актив/пассив';
return `${code} · ${meta.name} · ${kind}`;
}
function accLabel(acc: Ledger2Operation['debit'] | Ledger2Operation['credit']): string {
return acc ? `${acc.account} ${acc.name}` : '—';
function walletTitle(id: number | null | undefined, op: Ledger2Operation['wallet_op']): string {
if (id == null) {
// null допустим только для ISSUE (из ниоткуда) или CONSUME (в никуда)
if (op === 'ISSUE') return 'Выпуск средств извне системы';
return 'Кошелёк не задан';
}
const meta = getWallet(id);
if (!meta) return `${id} (нет в реестре)`;
return `${id} · ${meta.name}`;
}
function walletLabel(w: Ledger2Operation['wallet_to']): string {
return w ? `${w.name} (${w.id})` : '';
function walletDisplayId(id: number | null): string {
return id == null ? '' : String(id);
}
</script>
<template>
@@ -145,6 +192,7 @@ function walletLabel(w: Ledger2Operation['wallet_to']): string {
'focus-bar--op': focusMode === 'operation',
'focus-bar--process':
focusMode === 'process-start' || focusMode === 'process-end',
'focus-bar--rejected': focusMode === 'process-rejected',
}"
>
<!-- Начало процесса -->
@@ -157,6 +205,32 @@ function walletLabel(w: Ledger2Operation['wallet_to']): string {
</div>
</template>
<!-- Завершение отказом -->
<template v-else-if="focusMode === 'process-rejected' && rejectedInfo">
<div class="focus-bar__col focus-bar__col--main">
<div class="focus-bar__kicker">Завершение отказом</div>
<div class="focus-bar__title-row">
<span class="focus-bar__human">
{{ rejectedInfo.action?.human ?? actionShort(rejectedInfo.transition.action) }}
</span>
<code class="focus-bar__code">{{ rejectedInfo.transition.action }}</code>
<span class="focus-bar__actor">· {{ roleHuman(rejectedInfo.transition.actor) }}</span>
</div>
<p v-if="rejectedInfo.action?.purpose" class="focus-bar__desc">
{{ rejectedInfo.action.purpose }}
</p>
<p v-if="rejectedInfo.virtualState" class="focus-bar__desc">
<span class="focus-bar__kicker-mini">Результат:</span>
виртуальный статус
<code class="focus-bar__inline-code">{{ rejectedInfo.virtualState.name }}</code>
<template v-if="rejectedInfo.virtualState.human">
<strong>{{ rejectedInfo.virtualState.human }}</strong>
</template>.
{{ rejectedInfo.virtualState.description }}
</p>
</div>
</template>
<!-- Конец процесса -->
<template v-else-if="focusMode === 'process-end'">
<div class="focus-bar__col focus-bar__col--main">
@@ -185,26 +259,6 @@ function walletLabel(w: Ledger2Operation['wallet_to']): string {
<span class="focus-bar__actor">· {{ roleHuman(focusedTransition.actor) }}</span>
</div>
<p v-if="focusedAction?.purpose" class="focus-bar__desc">{{ focusedAction.purpose }}</p>
</div>
<div class="focus-bar__col focus-bar__col--meta">
<div class="focus-bar__peers">
<button
class="focus-bar__peer"
:disabled="focusedTransition.from === INITIAL_MARKER"
@click="goToState(focusedTransition.from === INITIAL_MARKER ? START_ID : focusedTransition.from)"
>
<code>{{ focusedTransition.from === INITIAL_MARKER ? 'Старт' : focusedTransition.from }}</code>
</button>
<span class="focus-bar__arrow"></span>
<button
class="focus-bar__peer"
:disabled="virtualStates.has(focusedTransition.to)"
@click="goToState(focusedTransition.to)"
>
<code>{{ virtualStates.has(focusedTransition.to) ? 'Отклонено' : focusedTransition.to }}</code>
</button>
</div>
<div v-if="focusedTransition.guards && focusedTransition.guards.length" class="focus-bar__guards">
<span class="focus-bar__kicker-mini">Условия:</span>
<span class="focus-bar__guard-list">
@@ -214,13 +268,43 @@ function walletLabel(w: Ledger2Operation['wallet_to']): string {
</div>
<div v-if="opsForFocusedAction.length" class="focus-bar__col focus-bar__col--ops">
<div class="focus-bar__kicker-mini">Операции ledger2</div>
<div class="focus-bar__kicker-mini">Операция</div>
<div class="focus-bar__ops">
<span v-for="op in opsForFocusedAction" :key="op.ledger_code" class="focus-bar__op" :title="op.human_name">
<code>{{ op.ledger_code }}</code>
<span class="focus-bar__op-dr">Дт {{ op.debit ? op.debit.account : '—' }}</span>
<span class="focus-bar__op-cr">Кт {{ op.credit ? op.credit.account : '—' }}</span>
</span>
<div v-for="op in opsForFocusedAction" :key="op.ledger_code" class="focus-bar__op-block">
<div class="focus-bar__op-name">{{ op.human_name }}</div>
<div
v-if="op.debit != null || op.credit != null"
class="focus-bar__op-sub"
>
<div class="focus-bar__op-sub-label">Проводки</div>
<div class="focus-bar__op-sub-body">
<span class="tooltip" :data-tip="accountTitle(op.debit)">
Дт <code>{{ op.debit ?? '—' }}</code>
</span>
<span class="focus-bar__op-sep">/</span>
<span class="tooltip" :data-tip="accountTitle(op.credit)">
Кт <code>{{ op.credit ?? '—' }}</code>
</span>
</div>
</div>
<div
v-if="op.wallet_op !== 'WALLET_ONLY' && (op.wallet_from != null || op.wallet_to != null)"
class="focus-bar__op-sub"
>
<div class="focus-bar__op-sub-label">Переводы</div>
<div class="focus-bar__op-sub-body">
<span class="tooltip" :data-tip="walletTitle(op.wallet_from, op.wallet_op)">
<code>{{ walletDisplayId(op.wallet_from) }}</code>
</span>
<span class="focus-bar__arrow"></span>
<span class="tooltip" :data-tip="walletTitle(op.wallet_to, op.wallet_op)">
<code>{{ walletDisplayId(op.wallet_to) }}</code>
</span>
</div>
</div>
</div>
</div>
</div>
</template>
@@ -244,29 +328,61 @@ function walletLabel(w: Ledger2Operation['wallet_to']): string {
<!-- Операция -->
<template v-else-if="focusMode === 'operation' && focusedOperation">
<div class="focus-bar__col focus-bar__col--main">
<div class="focus-bar__kicker">Операция ledger2</div>
<div class="focus-bar__kicker">Операция</div>
<div class="focus-bar__title-row">
<span class="focus-bar__human">{{ focusedOperation.human_name }}</span>
<code class="focus-bar__code">{{ focusedOperation.ledger_code }}</code>
<span class="focus-bar__kind-chip">{{ focusedOperation.wallet_op }}</span>
</div>
<p v-if="focusedOperation.description" class="focus-bar__desc">{{ focusedOperation.description }}</p>
</div>
<div class="focus-bar__col focus-bar__col--meta">
<dl class="focus-bar__opdata">
<div><dt>Кошелёк</dt><dd>{{ walletLabel(focusedOperation.wallet_from) }} {{ walletLabel(focusedOperation.wallet_to) }}</dd></div>
<div><dt>Дт</dt><dd>{{ accLabel(focusedOperation.debit) }}</dd></div>
<div><dt>Кт</dt><dd>{{ accLabel(focusedOperation.credit) }}</dd></div>
<div><dt>Сумма</dt><dd><code>{{ focusedOperation.amount_ref }}</code></dd></div>
</dl>
<div
v-if="focusedOperation.debit != null || focusedOperation.credit != null"
class="focus-bar__op-sub"
>
<div class="focus-bar__op-sub-label">Проводки</div>
<div class="focus-bar__op-sub-body">
<span class="tooltip" :data-tip="accountTitle(focusedOperation.debit)">
Дт <code>{{ focusedOperation.debit ?? '—' }}</code>
</span>
<span class="focus-bar__op-sep">/</span>
<span class="tooltip" :data-tip="accountTitle(focusedOperation.credit)">
Кт <code>{{ focusedOperation.credit ?? '—' }}</code>
</span>
</div>
</div>
<div
v-if="focusedOperation.wallet_op !== 'WALLET_ONLY'
&& (focusedOperation.wallet_from != null || focusedOperation.wallet_to != null)"
class="focus-bar__op-sub"
>
<div class="focus-bar__op-sub-label">Переводы</div>
<div class="focus-bar__op-sub-body">
<span class="tooltip" :data-tip="walletTitle(focusedOperation.wallet_from, focusedOperation.wallet_op)">
<code>{{ walletDisplayId(focusedOperation.wallet_from) }}</code>
</span>
<span class="focus-bar__arrow">→</span>
<span class="tooltip" :data-tip="walletTitle(focusedOperation.wallet_to, focusedOperation.wallet_op)">
<code>{{ walletDisplayId(focusedOperation.wallet_to) }}</code>
</span>
</div>
</div>
<div class="focus-bar__op-sub">
<div class="focus-bar__op-sub-label">Сумма</div>
<div class="focus-bar__op-sub-body"><code>{{ focusedOperation.amount_ref }}</code></div>
</div>
</div>
</template>
<!-- Статус -->
<template v-else-if="focusMode === 'state' && focusedState">
<div class="focus-bar__col focus-bar__col--main">
<div class="focus-bar__kicker">Статус</div>
<div class="focus-bar__kicker">
Статус<template v-if="standard.entity_human"> · {{ standard.entity_human }}</template>
</div>
<div class="focus-bar__title-row">
<code class="focus-bar__code focus-bar__code--state">{{ focusedState.label }}</code>
<span v-if="focusedState.human" class="focus-bar__human">{{ focusedState.human }}</span>
@@ -296,6 +412,9 @@ function walletLabel(w: Ledger2Operation['wallet_to']): string {
.focus-bar--process { background: var(--accent-soft); border-color: var(--accent-border); }
.focus-bar--process .focus-bar__kicker { color: var(--accent); }
.focus-bar--process .focus-bar__code { color: var(--accent); border-color: var(--accent-border); }
.focus-bar--rejected { background: var(--reject-soft); border-color: var(--reject); }
.focus-bar--rejected .focus-bar__kicker { color: var(--reject); }
.focus-bar--rejected .focus-bar__code { color: var(--reject); border-color: var(--reject); }
.focus-bar__desc--hint {
color: var(--text-subtle);
font-style: italic;
@@ -373,35 +492,81 @@ function walletLabel(w: Ledger2Operation['wallet_to']): string {
display: inline-block; margin-left: 6px; color: var(--text-subtle); font-style: italic;
}
.focus-bar__peers { display: inline-flex; align-items: center; gap: 8px; margin-top: 2px; }
.focus-bar__peer {
background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
padding: 3px 8px; font-family: var(--font-mono); font-size: 12px;
color: var(--text); cursor: pointer;
}
.focus-bar__peer:not(:disabled):hover {
background: var(--accent-soft); border-color: var(--accent-border);
}
.focus-bar__peer:disabled { cursor: default; color: var(--text-muted); }
.focus-bar__arrow { color: var(--text-subtle); }
.focus-bar__guards { margin-top: 4px; display: flex; flex-direction: column; gap: 3px; font-size: 12px; color: var(--text); }
.focus-bar__guard-list { display: flex; flex-direction: column; gap: 2px; }
.focus-bar__guard::before { content: '· '; color: var(--text-subtle); }
.focus-bar__ops { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 2px; }
.focus-bar__op {
display: inline-flex; align-items: center; gap: 8px;
padding: 3px 8px; background: var(--bg);
border: 1px solid var(--edge-focus-border); border-radius: 4px;
font-size: 11px; color: var(--text);
.focus-bar__ops { display: flex; flex-direction: column; gap: 10px; margin-top: 2px; }
.focus-bar__op-block {
display: flex; flex-direction: column; gap: 6px;
}
.focus-bar__op code {
font-family: var(--font-mono); color: var(--edge-focus);
font-weight: 600; background: transparent; border: none; padding: 0;
.focus-bar__op-block + .focus-bar__op-block {
padding-top: 8px;
border-top: 1px dashed var(--edge-focus-border);
}
.focus-bar__op-dr, .focus-bar__op-cr {
font-family: var(--font-mono); color: var(--text-muted); font-size: 10.5px;
.focus-bar__op-name {
font-size: 13px; font-weight: 600; color: var(--text);
line-height: 1.3;
}
.focus-bar__op-sub {
display: flex;
flex-direction: column;
gap: 2px;
padding: 6px 10px;
background: var(--bg);
border: 1px solid var(--edge-focus-border);
border-radius: 6px;
}
.focus-bar__op-sub-label {
font-size: 10px; font-weight: 600; letter-spacing: 0.06em;
text-transform: uppercase; color: var(--text-subtle);
}
.focus-bar__op-sub-body {
display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap;
font-size: 12px; color: var(--text);
}
.focus-bar__op-sub-body code {
font-family: var(--font-mono); font-size: 11px;
padding: 1px 6px; border-radius: 3px;
background: var(--edge-focus-soft); border: 1px solid var(--edge-focus-border);
color: var(--edge-focus); font-weight: 600;
}
.focus-bar__op-sep { color: var(--text-subtle); }
/* ── CSS-tooltip, надёжный (title часто блокируется) ─────────────────────── */
.tooltip { position: relative; display: inline-flex; align-items: center; gap: 4px; cursor: help; }
.tooltip[data-tip]:hover::after,
.tooltip[data-tip]:focus-visible::after {
content: attr(data-tip);
position: absolute;
bottom: calc(100% + 6px);
left: 50%;
transform: translateX(-50%);
padding: 5px 9px;
background: var(--text);
color: var(--bg);
font-size: 11px;
font-weight: 500;
line-height: 1.35;
white-space: nowrap;
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
z-index: 1000;
pointer-events: none;
}
.tooltip[data-tip]:hover::before,
.tooltip[data-tip]:focus-visible::before {
content: '';
position: absolute;
bottom: calc(100% + 1px);
left: 50%;
transform: translateX(-50%);
border: 4px solid transparent;
border-top-color: var(--text);
z-index: 1000;
pointer-events: none;
}
.focus-bar__opdata {
@@ -110,6 +110,54 @@ function onPaneReady(instance: VueFlowStore) {
watch(() => props.standard.process_type, doFit);
// ── Авто-пан: держим фокусный узел в видимой области ──────────────────
function focusedNodeId(): string | null {
if (props.focusAction && vfInstance.value) {
const nodes = vfInstance.value.getNodes ?? [];
const found = (nodes as Array<{ id: string; type?: string; data?: unknown }>).find(
(n) =>
n.type === NODE_TYPES.ACTION &&
(n.data as { actionName?: string } | undefined)?.actionName === props.focusAction,
);
if (found) return found.id;
}
if (props.focusStatus) return props.focusStatus;
return null;
}
function ensureInView(nodeId: string | null): void {
if (!nodeId || !vfInstance.value || !containerRef.value) return;
const vf = vfInstance.value;
const node = vf.findNode(nodeId);
if (!node) return;
const vp = vf.getViewport();
const cw = containerRef.value.clientWidth;
const ch = containerRef.value.clientHeight;
const w = node.dimensions?.width ?? 120;
const h = node.dimensions?.height ?? 80;
const left = vp.x + node.position.x * vp.zoom;
const top = vp.y + node.position.y * vp.zoom;
const right = left + w * vp.zoom;
const bottom = top + h * vp.zoom;
const pad = 48;
const outX = left < pad || right > cw - pad;
const outY = top < pad || bottom > ch - pad;
if (!outX && !outY) return;
const cx = node.position.x + w / 2;
const cy = node.position.y + h / 2;
vf.setCenter(cx, cy, { zoom: vp.zoom, duration: 280 });
}
watch(
() => [props.focusStatus, props.focusAction, props.focusDocument, props.focusOperation],
() => {
// Ждём обновления слоя VueFlow с новым isFocus, затем корректируем viewport.
requestAnimationFrame(() => {
requestAnimationFrame(() => ensureInView(focusedNodeId()));
});
},
);
let resizeObserver: ResizeObserver | null = null;
watch(containerRef, (el) => {
resizeObserver?.disconnect();
@@ -128,6 +176,7 @@ function onNodeClick({ node }: NodeMouseEvent) {
case NODE_TYPES.START:
case NODE_TYPES.STATE:
case NODE_TYPES.END:
case NODE_TYPES.REJECTED:
router.push({ query: { s: node.id } });
break;
case NODE_TYPES.ACTION: {
@@ -136,7 +185,6 @@ function onNodeClick({ node }: NodeMouseEvent) {
break;
}
default:
// rejected — не навигируем
break;
}
}
@@ -1,575 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useRouter } from 'vue-router';
import type { Standard, Transition, Ledger2Operation } from '@/types/standard';
const props = defineProps<{
standard: Standard;
focusStatus: string | null;
focusEdge: string | null;
}>();
const router = useRouter();
const INITIAL_MARKER = '∅';
const START_ID = '__start__';
function actionShort(name: string): string {
return name.split('::').pop() ?? name;
}
// Ищем human-имя действия в manifest.actions
function actionHumanFor(name: string): string | undefined {
return props.standard.actions.find((a) => a.name === name)?.human;
}
function roleHuman(role: string): string {
const map: Record<string, string> = {
contributor: 'Участник',
chairman: 'Председатель',
soviet: 'Совет',
soviet_members: 'Члены совета',
meet: 'Общее собрание',
gateway_operator: 'Оператор платежей',
administrator: 'Администратор',
chairman_or_soviet: 'Председатель / Совет',
};
return map[role] ?? role;
}
const focus = computed(() => props.focusStatus);
// Описание статуса (или метка «Старт», если фокус — стартовая точка)
const stateInfo = computed(() => {
if (!focus.value || focus.value === '__start__') {
return { label: 'Старт', description: 'Процесс ещё не начат — нет исходной записи сущности.' };
}
const s = props.standard.states.find((x) => x.name === focus.value);
return s
? { label: s.name, description: s.description, kind: s.kind ?? (s.virtual ? 'virtual' : 'normal') }
: { label: focus.value, description: 'Статус не описан в манифесте.' };
});
const virtualStates = computed(() =>
new Set(props.standard.states.filter((s) => s.virtual || s.kind === 'virtual').map((s) => s.name)),
);
const incoming = computed<Transition[]>(() => {
if (!focus.value || focus.value === '__start__') return [];
return props.standard.transitions.filter((t) => t.to === focus.value);
});
const outgoing = computed<Transition[]>(() => {
const f = focus.value ?? '__start__';
const src = f === '__start__' ? INITIAL_MARKER : f;
return props.standard.transitions.filter((t) => t.from === src);
});
// Поиск действия по имени (для описания)
function getActionPurpose(actionName: string): string | undefined {
return props.standard.actions.find((a) => a.name === actionName)?.purpose;
}
// Поиск операций ledger2, привязанных к action (через triggered_by + явный transitions[].operations)
function getOperationsForTransition(t: Transition): Ledger2Operation[] {
const byExplicit = t.operations ?? [];
const byImplicit = props.standard.operations
.filter((op) => op.triggered_by === t.action)
.map((op) => op.ledger_code);
const codes = new Set([...byExplicit, ...byImplicit]);
return props.standard.operations.filter((op) => codes.has(op.ledger_code));
}
function goToState(name: string): void {
if (virtualStates.value.has(name)) return;
router.push({ query: { s: name } });
}
function goToEdge(edgeId: string): void {
router.push({ query: { e: edgeId } });
}
// Поиск transition, соответствующего фокусному edgeId.
// edge.id имеет вид `${source}->${target}::${actionShort}`.
const focusedTransition = computed<{ t: Transition; edgeId: string } | null>(() => {
if (!props.focusEdge) return null;
for (const t of props.standard.transitions) {
const source = t.from === INITIAL_MARKER ? START_ID : t.from;
const short = t.action.split('::').pop() ?? t.action;
const id = `${source}->${t.to}::${short}`;
if (id === props.focusEdge) return { t, edgeId: id };
}
return null;
});
const focusedAction = computed(() => {
const tr = focusedTransition.value;
if (!tr) return null;
return props.standard.actions.find((a) => a.name === tr.t.action) ?? null;
});
function accountLabel(
acc: Ledger2Operation['debit'] | Ledger2Operation['credit'] | null,
): string {
if (!acc) return '—';
return `${acc.account} ${acc.name}`;
}
function walletLabel(w: Ledger2Operation['wallet_from'] | Ledger2Operation['wallet_to']): string {
if (!w) return '—';
return `${w.name} (${w.id})`;
}
</script>
<template>
<section v-if="focusedTransition" class="details details--edge">
<header class="details__head">
<div class="details__kicker details__kicker--edge">Фокусное действие</div>
<h3 class="details__title">
<span v-if="focusedAction?.human" class="details__human">{{ focusedAction.human }}</span>
<code class="details__title-code">{{ focusedTransition.t.action }}</code>
</h3>
<p class="details__desc">{{ focusedAction?.purpose ?? 'Действие контракта.' }}</p>
<div class="details__meta">
<span class="details__meta-item">
<span class="details__label-mini">От</span>
<button
class="transitions__peer"
:disabled="focusedTransition.t.from === INITIAL_MARKER"
@click="goToState(focusedTransition.t.from === INITIAL_MARKER ? START_ID : focusedTransition.t.from)"
>
<code>{{ focusedTransition.t.from === INITIAL_MARKER ? 'Старт' : focusedTransition.t.from }}</code>
</button>
</span>
<span class="transitions__arrow"></span>
<span class="details__meta-item">
<span class="details__label-mini">К</span>
<button
class="transitions__peer"
:disabled="virtualStates.has(focusedTransition.t.to)"
@click="goToState(focusedTransition.t.to)"
>
<code>{{ virtualStates.has(focusedTransition.t.to) ? 'Отклонено' : focusedTransition.t.to }}</code>
</button>
</span>
<span class="details__meta-item details__meta-item--actor">
<span class="details__label-mini">Вызывает</span>
<strong>{{ roleHuman(focusedTransition.t.actor) }}</strong>
</span>
</div>
</header>
<div v-if="focusedTransition.t.guards && focusedTransition.t.guards.length" class="details__block">
<h4>Условия выполнения</h4>
<ul class="bullets">
<li v-for="(g, i) in focusedTransition.t.guards" :key="i">{{ g }}</li>
</ul>
</div>
<div v-if="getOperationsForTransition(focusedTransition.t).length" class="details__block">
<h4>Операции ledger2</h4>
<div
v-for="op in getOperationsForTransition(focusedTransition.t)"
:key="op.ledger_code"
class="transitions__op"
>
<div class="op__head">
<code>{{ op.ledger_code }}</code>
<span class="op__kind">{{ op.wallet_op }}</span>
</div>
<div class="op__body">{{ op.human_name }}</div>
<dl class="op__grid">
<div><dt>Кошелёк</dt><dd>{{ walletLabel(op.wallet_from) }} {{ walletLabel(op.wallet_to) }}</dd></div>
<div><dt>Дт</dt><dd>{{ accountLabel(op.debit) }}</dd></div>
<div><dt>Кт</dt><dd>{{ accountLabel(op.credit) }}</dd></div>
<div><dt>Сумма</dt><dd><code>{{ op.amount_ref }}</code></dd></div>
</dl>
</div>
</div>
</section>
<section v-else class="details">
<header class="details__head">
<div class="details__kicker">Фокусный статус</div>
<h3 class="details__title">
<code>{{ stateInfo.label }}</code>
<span v-if="stateInfo.kind === 'final'" class="details__badge">финал</span>
<span v-else-if="stateInfo.kind === 'initial'" class="details__badge">старт</span>
</h3>
<p class="details__desc">{{ stateInfo.description }}</p>
</header>
<!-- Входы -->
<div v-if="incoming.length" class="details__block">
<h4>Как сюда попадают</h4>
<ul class="transitions">
<li v-for="t in incoming" :key="`in:${t.from}:${t.action}`">
<button
class="transitions__peer"
:disabled="virtualStates.has(t.from) || t.from === INITIAL_MARKER"
@click="goToState(t.from === INITIAL_MARKER ? '__start__' : t.from)"
>
<code>{{ t.from === INITIAL_MARKER ? 'Старт' : t.from }}</code>
</button>
<span class="transitions__arrow"></span>
<button
type="button"
class="transitions__action transitions__action--clickable"
:title="`Открыть описание действия ${actionShort(t.action)}`"
@click="goToEdge(`${t.from === INITIAL_MARKER ? '__start__' : t.from}->${focus}::${actionShort(t.action)}`)"
>
<span v-if="actionHumanFor(t.action)" class="transitions__action-human">
{{ actionHumanFor(t.action) }}
</span>
<code class="transitions__action-id">{{ actionShort(t.action) }}</code>
<span class="transitions__actor">{{ roleHuman(t.actor) }}</span>
</button>
</li>
</ul>
</div>
<!-- Выходы -->
<div v-if="outgoing.length" class="details__block">
<h4>Что может произойти дальше</h4>
<ol class="transitions transitions--out">
<li v-for="t in outgoing" :key="`out:${t.to}:${t.action}`" :class="{ 'transitions__row--reject': virtualStates.has(t.to) }">
<div class="transitions__row">
<button
type="button"
class="transitions__action transitions__action--clickable"
:title="`Открыть описание действия ${actionShort(t.action)}`"
@click="goToEdge(`${focus === '__start__' ? '__start__' : focus}->${t.to}::${actionShort(t.action)}`)"
>
<span v-if="actionHumanFor(t.action)" class="transitions__action-human">
{{ actionHumanFor(t.action) }}
</span>
<code class="transitions__action-id">{{ actionShort(t.action) }}</code>
<span class="transitions__actor">{{ roleHuman(t.actor) }}</span>
</button>
<span class="transitions__arrow"></span>
<button
class="transitions__peer"
:disabled="virtualStates.has(t.to)"
@click="goToState(t.to)"
>
<code>{{ virtualStates.has(t.to) ? 'Отклонено' : t.to }}</code>
</button>
</div>
<div v-if="getActionPurpose(t.action)" class="transitions__purpose">
{{ getActionPurpose(t.action) }}
</div>
<div v-if="t.guards && t.guards.length" class="transitions__guards">
<span class="transitions__label">Условия:</span>
<ul>
<li v-for="(g, i) in t.guards" :key="i">{{ g }}</li>
</ul>
</div>
<div v-for="op in getOperationsForTransition(t)" :key="op.ledger_code" class="transitions__op">
<div class="op__head">
<code>{{ op.ledger_code }}</code>
<span class="op__kind">{{ op.wallet_op }}</span>
</div>
<div class="op__body">
<span>{{ op.human_name }}</span>
</div>
<dl class="op__grid">
<div><dt>Кошелёк</dt><dd>{{ walletLabel(op.wallet_from) }} {{ walletLabel(op.wallet_to) }}</dd></div>
<div><dt>Дт</dt><dd>{{ accountLabel(op.debit) }}</dd></div>
<div><dt>Кт</dt><dd>{{ accountLabel(op.credit) }}</dd></div>
<div><dt>Сумма</dt><dd><code>{{ op.amount_ref }}</code></dd></div>
</dl>
</div>
</li>
</ol>
</div>
<div v-if="!incoming.length && !outgoing.length" class="details__empty">
У этого статуса нет входов и выходов в манифесте.
</div>
</section>
</template>
<style scoped>
.details {
margin-top: 24px;
border: 1px solid var(--border);
border-radius: var(--radius-lg);
background: var(--bg);
padding: 20px 24px;
}
.details--edge {
border-color: var(--edge-focus-border);
background: var(--edge-focus-soft);
}
.details__kicker--edge {
color: var(--edge-focus);
}
.details__title-code {
font-family: var(--font-mono);
font-size: 12px !important;
background: var(--bg) !important;
border-color: var(--edge-focus-border) !important;
color: var(--edge-focus) !important;
}
.details__human {
font-size: 18px;
font-weight: 600;
color: var(--text);
}
.details__meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
margin-top: 12px;
}
.details__meta-item {
display: inline-flex;
align-items: center;
gap: 6px;
}
.details__meta-item--actor {
margin-left: auto;
font-size: 13px;
color: var(--text);
}
.details__meta-item strong {
color: var(--text);
font-weight: 600;
}
.details__label-mini {
font-size: 10px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-subtle);
}
.bullets {
margin: 4px 0 0;
padding-left: 18px;
font-size: 13px;
color: var(--text);
line-height: 1.55;
}
.details__kicker {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--text-subtle);
}
.details__title {
margin: 4px 0 6px;
font-size: 20px;
font-weight: 600;
display: flex;
align-items: center;
gap: 10px;
}
.details__title code {
font-family: var(--font-mono);
font-size: 17px;
background: var(--surface);
padding: 2px 8px;
border-radius: 4px;
border: 1px solid var(--border);
}
.details__badge {
font-size: 10px;
padding: 2px 6px;
background: var(--accent-soft);
color: var(--accent);
border: 1px solid var(--accent-border);
border-radius: 10px;
letter-spacing: 0.04em;
text-transform: uppercase;
font-weight: 600;
}
.details__desc {
color: var(--text-muted);
margin: 0;
}
.details__block {
margin-top: 20px;
}
.details__block h4 {
font-size: 12px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-subtle);
margin: 0 0 10px;
}
.details__empty {
margin-top: 16px;
color: var(--text-muted);
font-style: italic;
}
.transitions {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 10px;
}
.transitions--out { gap: 18px; }
.transitions__row {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.transitions__row--reject { opacity: 0.75; }
.transitions__arrow {
color: var(--text-subtle);
}
.transitions__peer {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 6px;
padding: 4px 10px;
font-family: var(--font-mono);
font-size: 13px;
color: var(--text);
transition: background 80ms ease, border-color 80ms ease;
}
.transitions__peer:not(:disabled):hover {
background: var(--accent-soft);
border-color: var(--accent-border);
}
.transitions__peer:disabled {
cursor: default;
color: var(--text-muted);
}
.transitions__action {
display: inline-flex;
gap: 8px;
align-items: baseline;
flex-wrap: wrap;
background: transparent;
border: 1px solid transparent;
padding: 3px 8px;
border-radius: 6px;
font: inherit;
color: inherit;
text-align: left;
}
.transitions__action--clickable {
cursor: pointer;
transition: background 80ms ease, border-color 80ms ease;
}
.transitions__action--clickable:hover {
background: var(--edge-focus-soft);
border-color: var(--edge-focus-border);
}
.transitions__action-human {
font-size: 13px;
color: var(--text);
font-weight: 500;
}
.transitions__action-id {
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-muted);
background: var(--surface);
border: 1px solid var(--border);
padding: 1px 6px;
border-radius: 4px;
}
.transitions__actor {
font-size: 12px;
color: var(--text-muted);
}
.transitions__purpose {
font-size: 13px;
color: var(--text-muted);
margin: 6px 0 0 2px;
}
.transitions__guards {
margin-top: 6px;
padding: 8px 12px;
border-left: 2px solid var(--border-strong);
background: var(--surface);
border-radius: 0 6px 6px 0;
}
.transitions__label {
font-size: 11px;
font-weight: 600;
color: var(--text-muted);
letter-spacing: 0.04em;
text-transform: uppercase;
}
.transitions__guards ul {
margin: 4px 0 0;
padding-left: 16px;
font-size: 13px;
color: var(--text);
}
.transitions__op {
margin-top: 10px;
padding: 10px 12px;
border: 1px solid var(--accent-border);
background: var(--accent-soft);
border-radius: 6px;
}
.op__head {
display: flex;
gap: 10px;
align-items: baseline;
}
.op__head code {
font-family: var(--font-mono);
font-size: 13px;
color: var(--accent);
font-weight: 600;
background: transparent;
border: none;
padding: 0;
}
.op__kind {
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 0.06em;
padding: 1px 6px;
border: 1px solid var(--accent-border);
border-radius: 4px;
color: var(--accent);
}
.op__body {
font-size: 13px;
color: var(--text);
margin-top: 2px;
}
.op__grid {
margin: 8px 0 0;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 4px 24px;
}
.op__grid > div {
display: flex;
gap: 8px;
font-size: 12px;
min-width: 0;
}
.op__grid dt {
color: var(--text-muted);
min-width: 60px;
}
.op__grid dd {
margin: 0;
color: var(--text);
font-family: var(--font-mono);
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
@@ -26,9 +26,12 @@ defineProps<{
<style scoped>
.end-wrap {
width: 128px;
height: 80px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
}
.node-end {
@@ -2,32 +2,76 @@
import { Handle, Position } from '@vue-flow/core';
defineProps<{
data: { label: string; action?: string; fromState?: string; isFocus: boolean };
data: {
label: string;
actionShort?: string;
actionName?: string;
virtualState?: string;
isFocus: boolean;
};
}>();
</script>
<template>
<div class="node-rejected" :title="data.action ? `Отклонено: ${data.action}` : 'Отклонено'">
<span>{{ data.label }}</span>
<Handle type="target" :position="Position.Left" :connectable="false" />
<Handle type="source" :position="Position.Right" :connectable="false" />
<div class="rejected-wrap">
<div
class="node-rejected"
:class="{ 'node-rejected--focus': data.isFocus }"
:title="data.actionShort ? `Отклонено: ${data.actionShort}` : 'Отклонено'"
>
<span>{{ data.label }}</span>
<Handle type="target" :position="Position.Left" :connectable="false" />
<Handle type="source" :position="Position.Right" :connectable="false" />
</div>
<div class="rejected-wrap__caption">завершение отказом</div>
</div>
</template>
<style scoped>
.rejected-wrap {
width: 120px;
height: 70px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
}
.node-rejected {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--reject-soft);
border: 1.5px dashed var(--reject);
opacity: 0.55;
opacity: 0.75;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
color: var(--reject);
font-weight: 500;
cursor: pointer;
transition: opacity 100ms ease, background 100ms ease, box-shadow 100ms ease;
}
.node-rejected:hover {
opacity: 1;
background: var(--reject-soft);
box-shadow: 0 0 0 3px var(--reject-soft);
}
.node-rejected--focus {
opacity: 1;
background: var(--reject-soft);
box-shadow: 0 0 0 3px var(--reject-soft);
border-style: solid;
}
.rejected-wrap__caption {
font-size: 10px;
color: var(--reject);
opacity: 0.8;
text-transform: uppercase;
letter-spacing: 0.06em;
white-space: nowrap;
pointer-events: none;
}
:deep(.vue-flow__handle) {
opacity: 0;
@@ -25,9 +25,12 @@ defineProps<{
<style scoped>
.start-wrap {
width: 128px;
height: 80px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
}
.node-start {
@@ -3,18 +3,24 @@ import { computed } from 'vue';
import { Handle, Position } from '@vue-flow/core';
const props = defineProps<{
data: { label: string; human?: string; description?: string; isFocus: boolean };
data: {
label: string;
human?: string;
description?: string;
entity?: string;
isFocus: boolean;
};
}>();
// В квадратике показываем только короткое имя: приоритет data.human,
// fallback — description, обрезанный до одной строки.
const humanLabel = computed(() => props.data.human ?? '');
const entityLabel = computed(() => props.data.entity ?? '');
</script>
<template>
<div class="node-state" :class="{ 'node-state--focus': data.isFocus }">
<div class="node-state__label">{{ data.label }}</div>
<div v-if="entityLabel" class="node-state__entity">{{ entityLabel }}</div>
<div v-if="humanLabel" class="node-state__human">{{ humanLabel }}</div>
<div class="node-state__label">{{ data.label }}</div>
<Handle type="target" :position="Position.Left" :connectable="false" />
<Handle type="source" :position="Position.Right" :connectable="false" />
</div>
@@ -24,14 +30,14 @@ const humanLabel = computed(() => props.data.human ?? '');
.node-state {
position: relative;
width: 180px;
height: 68px;
padding: 10px 12px;
height: 80px;
padding: 8px 12px;
border-radius: 3px;
background: var(--bg);
border: 1.5px solid var(--border-strong);
display: flex;
flex-direction: column;
gap: 2px;
gap: 1px;
cursor: pointer;
transition: border-color 100ms ease, box-shadow 100ms ease, background 100ms ease;
overflow: hidden;
@@ -66,20 +72,35 @@ const humanLabel = computed(() => props.data.human ?? '');
background: var(--accent);
opacity: 1;
}
.node-state__label {
font-family: var(--font-mono);
font-size: 13px;
font-weight: 500;
color: var(--text);
line-height: 1.2;
}
.node-state__human {
font-size: 12px;
line-height: 1.3;
color: var(--text-muted);
.node-state__entity {
font-size: 9.5px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-subtle);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.1;
margin-bottom: 2px;
}
.node-state__human {
font-size: 14px;
font-weight: 600;
color: var(--text);
line-height: 1.25;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.node-state__label {
font-family: var(--font-mono);
font-size: 10.5px;
font-weight: 400;
color: var(--text-muted);
line-height: 1.1;
margin-top: 2px;
opacity: 0.75;
}
:deep(.vue-flow__handle) {
opacity: 0;
@@ -0,0 +1,21 @@
/**
* Реестры ledger2 для standards-site — re-export из общего пакета
* `cooptypes` (модуль `Ledger2`).
*
* Source of truth — в `components/cooptypes/src/ledger2/`; здесь только
* тонкие обёртки под прежний API, чтобы не переписывать потребителей.
*/
import { Ledger2 } from 'cooptypes'
export const LEDGER2_ACCOUNT_REGISTRY = Ledger2.LEDGER2_ACCOUNT_REGISTRY
export const LEDGER2_WALLET_REGISTRY = Ledger2.LEDGER2_WALLET_REGISTRY
export function getAccount(code: number | null | undefined) {
return Ledger2.getAccountMeta(code)
}
export function getWallet(id: number | null | undefined) {
if (id == null) return undefined
return Ledger2.LEDGER2_WALLET_REGISTRY.find((w) => w.id === id)
}
@@ -42,11 +42,17 @@ export const START_ID = '__start__';
export const END_ID = '__end__';
export const INITIAL_MARKER = '∅';
/* В dagre-боксе для start/end оставляем широкий «воздушный» footprint,
чтобы кружок не прилипал к первой/последней карточке. Визуальный
кружок (48px) центрируется внутри wrapper-а в соответствующем узле. */
/* В dagre-боксе для start/end оставляем широкий «воздушный» footprint,
чтобы кружок не прилипал к первой/последней карточке. Визуальный
кружок (48px) центрируется внутри wrapper-а в соответствующем узле. */
const SIZE = {
start: { width: 48, height: 48 },
state: { width: 180, height: 68 },
end: { width: 48, height: 48 },
rejected: { width: 40, height: 40 },
start: { width: 128, height: 80 },
state: { width: 180, height: 80 },
end: { width: 128, height: 80 },
rejected: { width: 120, height: 70 },
action: { width: 170, height: 54 },
};
@@ -260,6 +266,7 @@ function buildLayout(standard: Standard): LayoutResult {
// Statuses — все рендерятся как прямоугольник `state`.
// Финальные статусы теперь подключаются к отдельному ●-финиш-узлу.
const entityHuman = standard.entity_human ?? '';
for (const s of states) {
if (isVirtual(s)) continue;
const pos = g.node(s.name);
@@ -272,6 +279,7 @@ function buildLayout(standard: Standard): LayoutResult {
label: s.name,
human: s.human,
description: s.description,
entity: entityHuman,
isFocus: false,
},
draggable: false,
@@ -331,7 +339,7 @@ function buildLayout(standard: Standard): LayoutResult {
const hasDocs = (docsByAction.get(t.action) ?? []).length > 0;
const hasWalletMove = ops.length > 0;
const hasPosting = ops.some(
(op) => op.wallet_op !== 'WALLET_ONLY' && (op.debit || op.credit),
(op) => op.wallet_op !== 'WALLET_ONLY' && (op.debit != null || op.credit != null),
);
nodes.push({
@@ -395,9 +403,14 @@ function buildLayout(standard: Standard): LayoutResult {
id: info.targetId,
type: NODE_TYPES.REJECTED,
position: { x: pos.x - pos.width / 2, y: pos.y - pos.height / 2 },
data: { label: '×', action: actionShort(info.t.action), isFocus: false },
data: {
label: '×',
actionShort: actionShort(info.t.action),
actionName: info.t.action,
virtualState: info.t.to,
isFocus: false,
},
draggable: false,
selectable: false,
targetPosition: TL,
sourcePosition: TR,
});
@@ -24,6 +24,16 @@ function relationHuman(r: string): string {
return RELATION_HUMAN[r] ?? r;
}
const LIFECYCLE_HUMAN: Record<string, string> = {
proposed: 'Предложен',
approved: 'Утверждён',
active: 'Действующий',
deprecated: 'Устаревший',
};
function lifecycleHuman(s: string): string {
return LIFECYCLE_HUMAN[s] ?? s;
}
/**
* Для каждой записи `related[]` смотрим, есть ли такой process_type у нас
* в индексе — если да, делаем кликабельный RouterLink, иначе просто метку.
@@ -92,18 +102,42 @@ const focusStatus = computed<string | null>(() => {
<div v-else class="process-page">
<header class="process-head">
<div class="process-head__top">
<div class="process-head__title-box">
<span class="process-head__kicker">{{ standard.contract }}</span>
<h1>{{ standard.title }}</h1>
<code class="process-head__code">{{ standard.process_type }}</code>
</div>
<div class="process-head__meta">
<span v-if="standard.area">зона <code>{{ standard.area }}</code></span>
<span>статус: <strong>{{ standard.status }}</strong></span>
</div>
<div class="process-head__title-box">
<h1>{{ standard.title }}</h1>
<p class="process-head__summary">{{ standard.summary }}</p>
</div>
<p class="process-head__summary">{{ standard.summary }}</p>
<dl class="process-head__meta">
<div class="meta-item">
<dt>Контракт</dt>
<dd><code>{{ standard.contract }}</code></dd>
</div>
<div class="meta-item">
<dt>Процесс</dt>
<dd><code>{{ standard.process_type }}</code></dd>
</div>
<div v-if="standard.entity_human || standard.entity" class="meta-item meta-item--stacked">
<dt>Сущность</dt>
<dd>
<span v-if="standard.entity_human" class="meta-item__primary">
«{{ standard.entity_human }}»
</span>
<code v-if="standard.entity" class="meta-item__sub-code">{{ standard.entity }}</code>
</dd>
</div>
<div v-if="standard.area" class="meta-item">
<dt>Зона</dt>
<dd><code>{{ standard.area }}</code></dd>
</div>
<div class="meta-item">
<dt>Статус стандарта</dt>
<dd>
<span class="status-badge" :class="`status-badge--${standard.status}`">
{{ lifecycleHuman(standard.status) }}
</span>
</dd>
</div>
</dl>
</header>
<ProcessGraph
@@ -136,58 +170,108 @@ const focusStatus = computed<string | null>(() => {
<style scoped>
.process-head {
margin-bottom: 12px;
}
.process-head__top {
display: flex;
align-items: flex-end;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
gap: 24px;
flex-wrap: wrap;
margin-bottom: 14px;
padding-bottom: 14px;
border-bottom: 1px solid var(--border);
}
.process-head__title-box {
display: flex;
align-items: baseline;
gap: 10px;
flex-wrap: wrap;
flex: 1 1 420px;
min-width: 0;
}
.process-head__title-box h1 {
margin: 0;
font-size: 20px;
margin: 0 0 4px;
font-size: 22px;
line-height: 1.2;
}
.process-head__kicker {
font-size: 10px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--text-subtle);
font-weight: 600;
}
.process-head__code {
font-family: var(--font-mono);
font-size: 12px;
padding: 2px 8px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text-muted);
}
.process-head__summary {
font-size: 13px;
color: var(--text-muted);
line-height: 1.5;
line-height: 1.55;
max-width: 780px;
margin: 6px 0 0;
margin: 0;
}
.process-head__meta {
margin: 0;
display: grid;
grid-template-columns: repeat(2, minmax(0, auto));
gap: 10px 22px;
align-content: start;
}
.meta-item {
display: flex;
flex-wrap: wrap;
gap: 16px;
flex-direction: column;
gap: 3px;
min-width: 0;
}
.meta-item dt {
font-size: 10px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-subtle);
}
.meta-item dd {
margin: 0;
font-size: 12.5px;
color: var(--text);
}
.meta-item dd code {
font-family: var(--font-mono);
font-size: 12px;
padding: 2px 7px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text);
}
.meta-item--stacked dd {
display: flex;
flex-direction: column;
gap: 2px;
align-items: flex-start;
}
.meta-item__primary {
font-size: 12.5px;
color: var(--text);
}
.meta-item__sub-code {
font-size: 10.5px !important;
padding: 1px 5px !important;
color: var(--text-muted) !important;
background: transparent !important;
border: none !important;
}
.status-badge {
display: inline-block;
padding: 2px 10px;
border-radius: 10px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.03em;
border: 1px solid var(--border);
background: var(--surface);
color: var(--text-muted);
}
.process-head__meta code {
font-family: var(--font-mono);
.status-badge--proposed {
background: var(--edge-focus-soft);
border-color: var(--edge-focus-border);
color: var(--edge-focus);
}
.status-badge--approved,
.status-badge--active {
background: var(--accent-soft);
border-color: var(--accent-border);
color: var(--accent);
}
.status-badge--deprecated {
background: var(--reject-soft);
border-color: var(--reject);
color: var(--reject);
}
.related {
margin-top: 28px;
@@ -22,4 +22,20 @@ const routes: RouteRecordRaw[] = [
export const router = createRouter({
history: createWebHashHistory(),
routes,
scrollBehavior(to, from) {
// Скроллим к началу только при смене процесса (params.processType).
// При смене query (фокуса) не двигаем — user смотрит на ту же область.
const changedProcess =
to.name !== from.name || to.params.processType !== from.params.processType;
if (!changedProcess) return false;
if (typeof document !== 'undefined') {
const main = document.querySelector('.app-main');
if (main) {
main.scrollTo({ top: 0, behavior: 'smooth' });
} else {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
}
return { top: 0 };
},
});
@@ -92,6 +92,32 @@ button { font: inherit; cursor: pointer; }
flex-direction: column;
}
/* ── Мобильная заглушка ──────────────────────────────────────────────────── */
.mobile-stub {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
background: var(--bg);
z-index: 100;
text-align: center;
}
.mobile-stub__box {
max-width: 360px;
}
.mobile-stub h1 {
font-size: 18px;
margin: 0 0 10px;
}
.mobile-stub p {
font-size: 14px;
color: var(--text-muted);
line-height: 1.5;
margin: 0;
}
.app-main {
overflow-y: auto;
padding: 20px 28px 24px;
@@ -33,19 +33,11 @@ export type Role =
// open для расширения без правки типа
| (string & {});
// ── Элементарные структуры ──────────────────────────────────────────────────
export interface WalletRef {
id: number;
name: string;
human: string;
}
export interface AccountRef {
account: number; // номер счёта по плану счетов (51, 58, 80, …)
name: string; // константа ledger2_accounts (BANK_ACCOUNT …)
human: string;
type?: 'ACTIVE' | 'PASSIVE';
}
// ── Элементарные ссылки в ledger2 ───────────────────────────────────────────
// В YAML — только числовые коды. Имена подтягиваются из
// `data/registries.ts` (зеркало source-of-truth из mono-ai-1).
export type AccountCode = number; // код счёта (51, 80, 86, …)
export type WalletId = number; // id кошелька (2001, 3001, …)
// ── Секции стандарта ────────────────────────────────────────────────────────
@@ -119,15 +111,16 @@ export interface ProcessDocument {
note?: string;
}
// §6 Операции ledger2
// §6 Операции
// В YAML указываются только коды/id. Человекочитаемые имена — из registries.
export interface Ledger2Operation {
ledger_code: string; // cap.lnissue
human_name: string;
wallet_op: WalletOp;
wallet_from: WalletRef | null;
wallet_to: WalletRef | null;
debit: AccountRef | null;
credit: AccountRef | null;
wallet_from: WalletId | null; // id кошелька-источника (null — ISSUE/extern)
wallet_to: WalletId | null; // id кошелька-приёмника (null — CONSUME)
debit: AccountCode | null; // код счёта (51, 80, …)
credit: AccountCode | null;
amount_ref: string; // debt.amount
triggered_by: string; // capital::debtpaycnfrm
description?: string;
@@ -159,7 +152,8 @@ export interface Standard {
actions: ContractAction[];
// §3
entity?: string;
entity?: string; // технический идентификатор (`wallet::deposit`)
entity_human?: string; // человекочитаемое имя, им. падеж ед. ч. («Заявка на взнос»)
entity_source?: string;
states: EntityState[];
transitions: Transition[];
@@ -5,7 +5,10 @@ import { fileURLToPath } from 'node:url';
const rootDir = path.dirname(fileURLToPath(import.meta.url));
export default defineConfig({
export default defineConfig(({ command }) => ({
// При dev-сервере — корень; при build-е сайт публикуется как
// поддиректория /standards/ на docs.coopenomics.world.
base: command === 'build' ? '/standards/' : '/',
plugins: [vue()],
resolve: {
alias: {
@@ -28,4 +31,4 @@ export default defineConfig({
strictPort: false,
allowedHosts: true,
},
});
}));
+1
View File
@@ -14,3 +14,4 @@ export * as _Common from './common'
export * from './contracts'
export * as Cooperative from './cooperative'
export * as Interfaces from './interfaces'
export * as Ledger2 from './ledger2'
@@ -0,0 +1,44 @@
/**
* Реестр плана счетов ledger2 — source of truth в контракте:
* `components/contracts/cpp/lib/core/ledger2/accounts.hpp`
* (`LEDGER2_ACCOUNT_MAP`). В контракте id хранится как `code * 1000`
* (51 → 51000), здесь — человеческий код счёта (51).
*
* При добавлении нового счёта синхронизировать contract-side.
*/
export type AccountKind = 'active' | 'passive' | 'active_passive'
export interface AccountMeta {
code: number
name: string
kind: AccountKind
}
export const LEDGER2_ACCOUNT_REGISTRY: readonly AccountMeta[] = [
{ code: 4, name: 'Нематериальные активы', kind: 'active' },
{ code: 8, name: 'Вложения во внеоборотные активы', kind: 'active' },
{ code: 51, name: 'Расчётный счёт', kind: 'active' },
{ code: 58, name: 'Финансовые вложения', kind: 'active' },
{ code: 80, name: 'Паевой фонд (складочный капитал)', kind: 'passive' },
{ code: 86, name: 'Целевое финансирование', kind: 'passive' },
] as const
const accountByCode = new Map<number, AccountMeta>(
LEDGER2_ACCOUNT_REGISTRY.map((a) => [a.code, a]),
)
export function getAccountName(code: number | null | undefined): string | undefined {
if (code == null) return undefined
return accountByCode.get(code)?.name
}
export function getAccountMeta(code: number | null | undefined): AccountMeta | undefined {
if (code == null) return undefined
return accountByCode.get(code)
}
/** Преобразует stored id (51000) в человеческий code (51). */
export function storedIdToCode(storedId: number | null | undefined): number | null {
if (storedId == null) return null
return Math.round(storedId / 1000)
}
@@ -0,0 +1,9 @@
/**
* ledger2 — реестры счетов и кошельков, зеркало source-of-truth из
* смарт-контрактов (`cpp/lib/core/ledger2/`). Используется потребителями
* (standards-site, desktop/shared/lib/ledger2) как единый справочник имён
* для UI-подсказок.
*/
export * from './accounts'
export * from './wallets'
@@ -0,0 +1,44 @@
/**
* Реестр кошельков ledger2 — source of truth в контракте:
* `components/contracts/cpp/lib/core/ledger2/wallets.hpp`
* (`LEDGER2_WALLET_REGISTRY`). При добавлении/переименовании кошелька
* синхронизировать обе стороны.
*
* Используется для tooltip-подсказок в UI (standards-site, desktop).
*/
export interface WalletMeta {
id: number
name: string
}
export const LEDGER2_WALLET_REGISTRY: readonly WalletMeta[] = [
// id=0 — frontend-only: в контракте нет, но walletop issue/consume использует
// 0 как «кошелёк-источник/сток извне системы» (эмиссия / сжигание).
{ id: 0, name: 'Выпуск' },
{ id: 2001, name: 'ЦПП «Цифровой Кошелёк» — паевые взносы деньгами' },
{ id: 2002, name: 'Минимальный паевой взнос' },
{ id: 2003, name: 'Паевой фонд — принятые РИД' },
{ id: 3001, name: 'Вступительные взносы' },
{ id: 3002, name: 'Членские взносы (платформенные)' },
{ id: 3003, name: 'Делегатские членские взносы' },
{ id: 4001, name: 'Возвраты паевых взносов пайщикам' },
{ id: 4002, name: 'Выплаты поставщикам' },
{ id: 4051, name: 'Выданные пайщикам беспроцентные займы' },
{ id: 5001, name: 'Ручные корректировки' },
{ id: 9001, name: 'ЦПП «Благорост» — инвестиции деньгами' },
{ id: 9002, name: 'ЦПП «Благорост» — принятые РИД' },
{ id: 9003, name: 'ЦПП «Благорост» — имущественные паевые взносы' },
{ id: 9004, name: 'ЦПП «Благорост» — членские взносы' },
{ id: 10001, name: 'ЦПП «Генератор» — принятый коммит (имущество)' },
{ id: 10002, name: 'ЦПП «Генератор» — членские взносы' },
{ id: 11001, name: 'ЦПП «Стол Заказов» — общий кошелёк' },
] as const
const walletNameById = new Map<number, string>(
LEDGER2_WALLET_REGISTRY.map((w) => [w.id, w.name]),
)
export function getWalletName(id: number | null | undefined): string | undefined {
if (id == null) return undefined
return walletNameById.get(id)
}
+2 -1
View File
@@ -96,8 +96,9 @@ markdown_extensions:
permalink: true
- pymdownx.tabbed:
alternate_style: true
nav:
nav:
- Введение: index.md
- Реестр стандартов: https://docs.coopenomics.world/standards/
- Лицензия: new/installation/index.md # Главная страница раздела
- Облачная поставка: new/connection/index.md
- Установка: new/installation/chairman.md
+18 -21
View File
@@ -222,6 +222,9 @@ importers:
'@vue-flow/minimap':
specifier: ^1.5.4
version: 1.5.4(@vue-flow/core@1.48.2(vue@3.5.31(typescript@5.9.3)))(vue@3.5.31(typescript@5.9.3))
cooptypes:
specifier: workspace:*
version: link:../../cooptypes
lucide-vue-next:
specifier: ^0.460.0
version: 0.460.0(vue@3.5.31(typescript@5.9.3))
@@ -18847,7 +18850,7 @@ snapshots:
'@stylistic/eslint-plugin': 2.13.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/parser': 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
'@vitest/eslint-plugin': 1.6.13(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@7.18.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@1.6.1(@types/node@20.19.37)(lightningcss@1.32.0)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.46.1))
'@vitest/eslint-plugin': 1.6.13(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@1.6.1(@types/node@20.19.37)(lightningcss@1.32.0)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.46.1))
eslint: 9.39.4(jiti@2.6.1)
eslint-config-flat-gitignore: 0.1.8
eslint-flat-config-utils: 0.3.1
@@ -18864,7 +18867,7 @@ snapshots:
eslint-plugin-regexp: 2.10.0(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-toml: 0.11.1(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-unicorn: 55.0.0(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-unused-imports: 4.4.1(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@7.18.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-unused-imports: 4.4.1(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-vue: 9.33.0(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-yml: 1.19.1(eslint@9.39.4(jiti@2.6.1))
eslint-processor-vue-blocks: 0.1.2(@vue/compiler-sfc@3.5.31)(eslint@9.39.4(jiti@2.6.1))
@@ -18912,7 +18915,7 @@ snapshots:
eslint-plugin-regexp: 2.10.0(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-toml: 0.12.0(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-unicorn: 56.0.1(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-unused-imports: 4.4.1(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@7.18.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-unused-imports: 4.4.1(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-vue: 9.33.0(eslint@9.39.4(jiti@2.6.1))
eslint-plugin-yml: 1.19.1(eslint@9.39.4(jiti@2.6.1))
eslint-processor-vue-blocks: 1.0.0(@vue/compiler-sfc@3.5.31)(eslint@9.39.4(jiti@2.6.1))
@@ -26276,18 +26279,6 @@ snapshots:
vite: 8.0.3(@types/node@22.19.15)(esbuild@0.27.4)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
vue: 3.5.31(typescript@5.9.3)
'@vitest/eslint-plugin@1.6.13(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@7.18.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@1.6.1(@types/node@20.19.37)(lightningcss@1.32.0)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.46.1))':
dependencies:
'@typescript-eslint/scope-manager': 8.57.2
'@typescript-eslint/utils': 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
eslint: 9.39.4(jiti@2.6.1)
optionalDependencies:
'@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
typescript: 5.9.3
vitest: 1.6.1(@types/node@20.19.37)(lightningcss@1.32.0)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.46.1)
transitivePeerDependencies:
- supports-color
'@vitest/eslint-plugin@1.6.13(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@7.32.0)(typescript@5.9.3))(eslint@7.32.0)(typescript@5.9.3))(eslint@7.32.0)(typescript@5.9.3)(vitest@4.1.1(@opentelemetry/api@1.9.1)(@types/node@20.19.37)(vite@8.0.3(@types/node@20.19.37)(esbuild@0.27.4)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))':
dependencies:
'@typescript-eslint/scope-manager': 8.57.2
@@ -26312,6 +26303,18 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@vitest/eslint-plugin@1.6.13(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@1.6.1(@types/node@20.19.37)(lightningcss@1.32.0)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.46.1))':
dependencies:
'@typescript-eslint/scope-manager': 8.57.2
'@typescript-eslint/utils': 8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
eslint: 9.39.4(jiti@2.6.1)
optionalDependencies:
'@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
typescript: 5.9.3
vitest: 1.6.1(@types/node@20.19.37)(lightningcss@1.32.0)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.46.1)
transitivePeerDependencies:
- supports-color
'@vitest/eslint-plugin@1.6.13(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@2.1.9(@types/node@22.19.15)(lightningcss@1.32.0)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.46.1))':
dependencies:
'@typescript-eslint/scope-manager': 8.57.2
@@ -30587,12 +30590,6 @@ snapshots:
semver: 7.7.4
strip-indent: 3.0.0
eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@7.18.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1)):
dependencies:
eslint: 9.39.4(jiti@2.6.1)
optionalDependencies:
'@typescript-eslint/eslint-plugin': 8.57.2(@typescript-eslint/parser@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
eslint-plugin-unused-imports@4.4.1(@typescript-eslint/eslint-plugin@8.57.2(@typescript-eslint/parser@8.57.2(eslint@7.32.0)(typescript@5.9.3))(eslint@7.32.0)(typescript@5.9.3))(eslint@7.32.0):
dependencies:
eslint: 7.32.0