diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 7704696..1c037bd 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -24,6 +24,10 @@ jobs: skip-build: true - command: build skip-build: true + - command: test + skip-build: true + # The cross-repo contract suite requires a sibling server checkout. + args: --exclude "tests/e2e/**" --exclude tests/api/cross-repo-contract.test.js fail-fast: false @@ -42,7 +46,37 @@ jobs: with: skip-yarn-build: ${{ matrix.mode.skip-build }} - - run: yarn ${{ matrix.mode.command }} + - run: yarn ${{ matrix.mode.command }} ${{ matrix.mode.args }} + + playwright: + name: playwright + runs-on: ubuntu-24.04 + timeout-minutes: 25 + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + lfs: true + + - uses: ./.github/actions/setup + with: + skip-yarn-build: true + + - name: Install Chromium + run: yarn playwright install --with-deps chromium + + - name: Run proposal journeys + run: yarn test:e2e + + - name: Upload browser artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: proposal-wizard-playwright + path: test-results/playwright + if-no-files-found: ignore check-cache: name: Cache integrity check diff --git a/.gitignore b/.gitignore index c898689..7b48a12 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,8 @@ node_modules # production dist storybook-static +playwright-report +test-results # ts *.tsbuildinfo diff --git a/package.json b/package.json index 196f4e0..9b3fedc 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "build": "rsbuild build", "preview": "rsbuild preview", "test": "RSTEST=1 rstest", + "test:e2e": "playwright test", "typecheck": "tsc -p tsconfig.json --noEmit", "prettier:check": "prettier --list-different .", "prettier:fix": "prettier --write ." @@ -18,6 +19,12 @@ "@polkadot/util": "^14.0.1", "@polkadot/util-crypto": "^14.0.1", "@radix-ui/react-slot": "^1.2.4", + "@tiptap/core": "^3.28.0", + "@tiptap/extension-link": "^3.28.0", + "@tiptap/markdown": "^3.28.0", + "@tiptap/pm": "^3.28.0", + "@tiptap/react": "^3.28.0", + "@tiptap/starter-kit": "^3.28.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.554.0", @@ -29,6 +36,7 @@ "zod": "^4.2.1" }, "devDependencies": { + "@playwright/test": "^1.61.1", "@rsbuild/core": "^1.6.6", "@rsbuild/plugin-react": "^1.4.2", "@rstest/core": "^0.7.9", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..d0f3d4d --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from "@playwright/test"; + +export default defineConfig({ + testDir: "./tests/e2e", + outputDir: "./test-results/playwright", + fullyParallel: false, + retries: 0, + reporter: "line", + use: { + baseURL: "http://127.0.0.1:4189", + colorScheme: "dark", + reducedMotion: "reduce", + screenshot: "only-on-failure", + trace: "retain-on-failure", + }, + webServer: { + command: "yarn dev --host 127.0.0.1 --port 4189", + url: "http://127.0.0.1:4189", + reuseExistingServer: true, + timeout: 120_000, + }, +}); diff --git a/rstest.config.ts b/rstest.config.ts index 9d89faa..9e78036 100644 --- a/rstest.config.ts +++ b/rstest.config.ts @@ -19,6 +19,7 @@ const rstestServerPlugin = (): RsbuildPlugin => ({ }); export default defineConfig({ + exclude: ["tests/e2e/**"], testMatch: ["tests/**/*.test.js"], environment: "node", browser: { enabled: false }, diff --git a/src/app/AppSidebar.css b/src/app/AppSidebar.css index d440bbf..47418e6 100644 --- a/src/app/AppSidebar.css +++ b/src/app/AppSidebar.css @@ -442,7 +442,7 @@ padding: 0.95rem 1rem; border-right: 0; border-bottom: 1px solid var(--sidebar-border); - overflow: visible; + overflow: clip; } .sidebar__brand { diff --git a/src/app/MainAtmosphere.css b/src/app/MainAtmosphere.css index ea9e433..22beed7 100644 --- a/src/app/MainAtmosphere.css +++ b/src/app/MainAtmosphere.css @@ -3,7 +3,7 @@ inset: 0 0 0 var(--app-sidebar-width, 260px); z-index: 0; pointer-events: none; - overflow: hidden; + overflow: clip; contain: paint; } diff --git a/src/components/AttachmentList.tsx b/src/components/AttachmentList.tsx index 45c4767..361de06 100644 --- a/src/components/AttachmentList.tsx +++ b/src/components/AttachmentList.tsx @@ -1,5 +1,6 @@ import type { ReactNode } from "react"; +import { safeExternalHref } from "@/lib/safeExternalHref"; import { cn } from "@/lib/utils"; import { Surface } from "@/components/Surface"; @@ -29,34 +30,37 @@ export function AttachmentList({ >
{title}
+ {part.slice(1, -1)}
+
+ );
+ }
+ const link = /^\[([^\]]+)\]\(([^\s)]+)\)$/.exec(part);
+ if (link) {
+ const href = safeNarrativeHref(link[2]);
+ return href ? (
+
+ {link[1]}
+
+ ) : (
+ {link[1]}
+ );
+ }
+ return part;
+ })}
+ >
+ );
+}
+
+export function ProposalNarrative({
+ className,
+ value,
+}: {
+ className?: string;
+ value: ProposalNarrativeValue;
+}) {
+ const blocks = parseNarrative(value);
+ if (blocks.length === 0) return null;
+
+ return (
+ ++ ); + } + if (block.type === "ordered-list" || block.type === "unordered-list") { + const Tag = block.type === "ordered-list" ? "ol" : "ul"; + return ( ++
+
+ Use the formatting controls or standard editor shortcuts to structure + the proposal. The saved proposal remains portable Markdown. +
+{title}
++ Genesis members +
+Where
+When
++ {milestone.title} +
++ {milestone.timeframe ?? "Timeline not specified"} + {milestone.budgetHmnd + ? ` · ${milestone.budgetHmnd} HMND` + : ""} +
+Budget
+{budgetScope}
+{overview}
-{budgetScope}
-{budgetScope}
+{ms.title}
-{ms.desc}
-{ms.title}
+{ms.desc}
++ {wizardAnnouncement} +
+ Last saved {new Date(savedAt).toLocaleTimeString()} +
+ ) : null}{emptyMessage}
+ ) : null; + } + + return ( +- Changes autosave locally. Eligible human nodes can save drafts to the - simulation backend (see Drafts). -
-+ Build the proposal in order. Completed steps remain available while + the next requirement stays visible. +
+{description}
++ Continue saved local work or leave it untouched and keep this new + proposal. +
+{description}
+ ) : null} +Total
{budgetTotal.toLocaleString()} HMND
diff --git a/src/pages/proposals/proposalCreation/steps/EssentialsStep.tsx b/src/pages/proposals/proposalCreation/steps/EssentialsStep.tsx
deleted file mode 100644
index 694940b..0000000
--- a/src/pages/proposals/proposalCreation/steps/EssentialsStep.tsx
+++ /dev/null
@@ -1,768 +0,0 @@
-import { useMemo, useState } from "react";
-import type React from "react";
-import { Label } from "@/components/primitives/label";
-import { Badge } from "@/components/primitives/badge";
-import { Select } from "@/components/primitives/select";
-import { Input } from "@/components/primitives/input";
-import { TierLabel } from "@/components/TierLabel";
-import type { ProposalDraftForm } from "../types";
-import {
- getSystemActionMeta,
- type SystemActionId,
-} from "../templates/systemActions";
-import {
- filterPresetsForEligibility,
- getPresetCategory,
- type ProposalPreset,
-} from "../presets/registry";
-import {
- isTierEligible,
- requiredTierForProposalType,
-} from "@/lib/proposalTypes";
-
-const PROPOSAL_TYPE_OPTIONS: Array<{
- value: ProposalDraftForm["proposalType"];
- label: string;
- helper: string;
-}> = [
- {
- value: "basic",
- label: "Basic",
- helper: "Routine proposals that do not change core system parameters.",
- },
- {
- value: "fee",
- label: "Fee distribution",
- helper: "Adjust fee/treasury allocation rules.",
- },
- {
- value: "monetary",
- label: "Monetary system",
- helper: "Token issuance, emission, or monetary policy changes.",
- },
- {
- value: "core",
- label: "Core infrastructure",
- helper: "Protocol and infrastructure-level changes.",
- },
- {
- value: "administrative",
- label: "Administrative",
- helper: "Governance operations (e.g., chamber lifecycle).",
- },
- {
- value: "dao-core",
- label: "DAO core",
- helper: "Changes to the governance protocol itself.",
- },
-];
-
-export function EssentialsStep(props: {
- attemptedNext: boolean;
- chamberOptions: { value: string; label: string }[];
- draft: ProposalDraftForm;
- initiativeOptions: { value: string; label: string }[];
- setDraft: React.Dispatch
- System changes affect simulation variables directly (e.g., chamber
- creation). Project proposals describe work outside the system.
-
- {hasChosenType
- ? PROPOSAL_TYPE_OPTIONS.find(
- (option) => option.value === draft.proposalType,
- )?.helper
- : "Choose proposal type to continue."}
-
- Required tier:
- {selectedPresetCategory ? (
-
-
- {hasFormationVariants
- ? "Choose Formation (project with milestones) or Policy."
- : "This type has a fixed mode and cannot be switched."}
- Title is required.
- System proposals must target General chamber.
-
- Optional provenance tag. It does not change quorum, voting power, CM,
- MM, chamber membership, or proposal lifecycle rules.
- System change
- {systemActionMeta?.description ??
- "Select a system preset for this type to set an executable action."}
-
- Target chamber id is required.
-
- Target governor address is required.
-
- Title is required for chamber creation.
-
+ {isSystem && kindChosen
+ ? "Change a simulation entity or governance rule through General chamber."
+ : "Propose policy or Formation work in a chamber."}
+
+ {typeChosen
+ ? PROPOSAL_TYPE_OPTIONS.find(
+ (option) => option.value === draft.proposalType,
+ )?.helper
+ : "Choose the governing right used for this proposal."}
+
Title is required.
+ ) : null} ++ Initiative association records provenance only. It does not alter + voting, quorum, CM, MM, chamber membership, or lifecycle. +
+What is required.
+ ) : null} +Why is required.
+ ) : null} +{emptyLabel}
; + } + + return ( +- Proposer (auto-filled) -
-+ Genesis members +
+ {(draft.metaGovernance?.genesisMembers ?? []).length > 0 ? ( +No genesis members set.
+ )} +{draft.summary}
: null} +{draft.summary}
: null} +No milestones added.
+ ) : ( +No open roles defined.
+ ) : ( +Preview
-{draft.title}
- {draft.summary.trim().length > 0 ? ( -{draft.summary}
- ) : null} - {selectedChamber ? ( -- Chamber: {selectedChamber.name} -
- ) : null} -- Proposal type: {proposalTypeLabel[draft.proposalType]} -
- {selectedInitiative ? ( -- Initiative: {selectedInitiative.title} -
- ) : null} -Action
-- {systemActionMeta?.label ?? "No preset selected"} -
-- Target chamber id -
-- {draft.metaGovernance?.chamberId ?? "—"} -
-- Target governor -
-- {draft.metaGovernance?.targetAddress ?? "—"} -
-- Chamber name -
-- {draft.metaGovernance?.title ?? "—"} -
-Multiplier
-- {draft.metaGovernance?.multiplier ?? "—"} -
-- Genesis members -
-- {(draft.metaGovernance?.genesisMembers ?? []).length > 0 - ? draft.metaGovernance?.genesisMembers?.join(", ") - : "—"} -
-- Implementation notes -
-{draft.how}
-What
-{draft.what}
-Why
-{draft.why}
-How
-{draft.how}
-When
-Budget
-- Total: {budgetTotal.toLocaleString()} HMND -
-- Team slots: 1 / {1 + draft.openSlotNeeds.length} -
-- Open positions -
- {draft.openSlotNeeds.length === 0 ? ( -- No open positions defined. -
- ) : ( -- Attachments (optional, recommended) -
+Attachments are optional.
- Add links to PDFs, docs, spreadsheets, or any supporting material. -
- ) : ( -Rules
-- You can navigate steps freely. Submit unlocks once required fields - are filled and both checkboxes are checked. + Submit unlocks after the required fields and both confirmations are + complete.
) : null} {SIM_AUTH_ENABLED && !canAct ? ( @@ -386,7 +473,7 @@ export function ReviewStep(props: { Submitting is available only to eligible human nodes. ) : null} -+ Target chamber id is required. +
+ ) : null} ++ Target governor address is required. +
+ ) : null} +Title is required.
+ ) : null} +Title is required.
+ ) : null} +]*>Development will be managed through:<\/p>/); + assert.match( + html, + /
]*>Weekly public progress updates and testable release evidence\.<\/p>/, + ); + assert.match(html, /
]*>vortex check<\/code>/);
+ assert.match(html, /]*>Publish the result\.<\/blockquote>/);
+ assert.match(html, /href="https:\/\/humanode\.io\/?"/);
+ assert.doesNotMatch(html, /href="javascript:/);
+ assert.match(html, /<b>not markup<\/b>/);
+});
diff --git a/tests/unit/proposal-presets.test.ts b/tests/unit/proposal-presets.test.ts
index 5d10172..5d1461f 100644
--- a/tests/unit/proposal-presets.test.ts
+++ b/tests/unit/proposal-presets.test.ts
@@ -10,7 +10,10 @@ import {
PROPOSAL_PRESETS,
} from "../../src/pages/proposals/proposalCreation/presets/registry";
import { DEFAULT_DRAFT } from "../../src/pages/proposals/proposalCreation/types";
-import { projectTemplate } from "../../src/pages/proposals/proposalCreation/templates/project";
+import {
+ pathIdForDraft,
+ validateWizardStep,
+} from "../../src/pages/proposals/proposalCreation/wizardModel";
test("applyPresetToDraft seeds formation flags and meta governance", () => {
const policyPreset = getProposalPreset("project.policy");
@@ -107,7 +110,7 @@ test("inferPresetIdFromDraft aligns with system and policy-only drafts", () => {
).toBe("system.governor.censure");
});
-test("project template treats policy-only proposals as budget-valid", () => {
+test("policy-only proposals use the path without Formation funding", () => {
const draft = {
...DEFAULT_DRAFT,
formationEligible: false,
@@ -117,10 +120,21 @@ test("project template treats policy-only proposals as budget-valid", () => {
how: "Execution notes",
budgetItems: [],
};
- const computed = projectTemplate.compute(draft, { budgetTotal: 0 });
- expect(computed.budgetValid).toBe(true);
- expect(computed.planValid).toBe(true);
- expect(computed.essentialsValid).toBe(true);
+ expect(pathIdForDraft(draft, "project")).toBe("project-policy");
+ expect(
+ validateWizardStep("essentials", {
+ draft,
+ presetId: "project.policy",
+ tierBlocked: false,
+ }).valid,
+ ).toBe(true);
+ expect(
+ validateWizardStep("plan", {
+ draft,
+ presetId: "project.policy",
+ tierBlocked: false,
+ }).valid,
+ ).toBe(true);
});
test("getPresetCategory groups presets by taxonomy", () => {
diff --git a/tests/unit/proposal-ui.test.ts b/tests/unit/proposal-ui.test.ts
index 169e338..4ba45b3 100644
--- a/tests/unit/proposal-ui.test.ts
+++ b/tests/unit/proposal-ui.test.ts
@@ -11,6 +11,19 @@ import type { ChamberProposalPageDto } from "../../src/types/api";
const genericAddress = "5C62Ck4UrFPiBtoCmeSrgF7x9yv9mn38446dhCpsi2mLHiFT";
const canonicalAddress = "hmnVXRhJsFLh5CbdxZNrn5Lu6FR2nDacxgSLrsVoyoW9ERXAP";
+const authoringFixture: ChamberProposalPageDto["authoring"] = {
+ kind: "project",
+ presetId: null,
+ proposalType: "basic",
+ what: "",
+ why: "",
+ how: "",
+ aboutMe: "",
+ outputs: [],
+ timeline: [],
+ budgetItems: [],
+ systemAction: null,
+};
function chamberProposalFixture(
overrides: Partial = {},
@@ -69,6 +82,7 @@ function chamberProposalFixture(
chambers: [],
},
...overrides,
+ authoring: overrides.authoring ?? authoringFixture,
};
}
diff --git a/tests/unit/proposal-wizard-model.test.ts b/tests/unit/proposal-wizard-model.test.ts
new file mode 100644
index 0000000..8ef6573
--- /dev/null
+++ b/tests/unit/proposal-wizard-model.test.ts
@@ -0,0 +1,162 @@
+import { expect, test } from "@rstest/core";
+
+import {
+ DEFAULT_DRAFT,
+ type ProposalDraftForm,
+} from "../../src/pages/proposals/proposalCreation/types";
+import {
+ createWizardState,
+ firstIncompleteWizardStep,
+ normalizeWizardStepId,
+ pathIdForDraft,
+ proposalBudgetTotal,
+ reachableWizardSteps,
+ resolveRequestedWizardStep,
+ transitionWizard,
+ validateWizardStep,
+ type WizardContext,
+} from "../../src/pages/proposals/proposalCreation/wizardModel";
+
+function completePolicyDraft(): ProposalDraftForm {
+ return {
+ ...structuredClone(DEFAULT_DRAFT),
+ title: "Policy",
+ what: "Change the policy",
+ why: "Improve governance",
+ how: "Publish the change",
+ formationEligible: false,
+ agreeRules: true,
+ confirmBudget: true,
+ };
+}
+
+function context(
+ draft: ProposalDraftForm,
+ presetId = "project.policy",
+): WizardContext {
+ return { draft, presetId, tierBlocked: false };
+}
+
+test("new and invalid navigation resolve to the first incomplete step", () => {
+ const emptyContext = context(structuredClone(DEFAULT_DRAFT), "");
+ expect(
+ resolveRequestedWizardStep("project-formation", "review", emptyContext),
+ ).toBe("intent");
+ expect(firstIncompleteWizardStep("project-formation", emptyContext)).toBe(
+ "intent",
+ );
+});
+
+test("paths are derived from template and Formation mode", () => {
+ expect(pathIdForDraft(completePolicyDraft(), "project")).toBe(
+ "project-policy",
+ );
+ expect(
+ pathIdForDraft(
+ { ...completePolicyDraft(), formationEligible: true },
+ "project",
+ ),
+ ).toBe("project-formation");
+ expect(pathIdForDraft(completePolicyDraft(), "system")).toBe("system-change");
+});
+
+test("legacy query steps normalize into the current path vocabulary", () => {
+ expect(normalizeWizardStepId("budget", "project")).toBe("funding");
+ expect(normalizeWizardStepId("essentials", "system")).toBe("system-change");
+ expect(normalizeWizardStepId("plan", "system")).toBe("rationale");
+});
+
+test("reachable steps stop after the first invalid requirement", () => {
+ const draft = completePolicyDraft();
+ expect(reachableWizardSteps("project-policy", context(draft))).toEqual([
+ "intent",
+ "essentials",
+ "plan",
+ "review",
+ ]);
+ expect(
+ reachableWizardSteps("project-policy", context({ ...draft, how: "" })),
+ ).toEqual(["intent", "essentials", "plan"]);
+});
+
+test("Formation funding validates every milestone budget", () => {
+ const draft = {
+ ...completePolicyDraft(),
+ formationEligible: true,
+ timeline: [
+ { id: "m1", title: "M1", timeframe: "1 week", budgetHmnd: "10" },
+ { id: "m2", title: "M2", timeframe: "2 weeks", budgetHmnd: "" },
+ ],
+ };
+ expect(validateWizardStep("funding", context(draft))).toEqual({
+ valid: false,
+ firstInvalidFieldId: "timeline-budget-1",
+ });
+ draft.timeline[1].budgetHmnd = "20";
+ expect(validateWizardStep("funding", context(draft))).toEqual({
+ valid: true,
+ });
+ expect(proposalBudgetTotal(draft)).toBe(30);
+});
+
+test("system changes enforce General chamber and action targets", () => {
+ const draft: ProposalDraftForm = {
+ ...completePolicyDraft(),
+ chamberId: "general",
+ metaGovernance: {
+ action: "chamber.create",
+ chamberId: "research",
+ title: "Research",
+ },
+ };
+ expect(validateWizardStep("system-change", context(draft))).toEqual({
+ valid: true,
+ });
+ expect(
+ validateWizardStep(
+ "system-change",
+ context({ ...draft, chamberId: "design" }),
+ ),
+ ).toEqual({ valid: false, firstInvalidFieldId: "chamber" });
+});
+
+test("transition model guards future steps and focuses blocking fields", () => {
+ const emptyDraft = structuredClone(DEFAULT_DRAFT);
+ let state = createWizardState("project-formation");
+ const blocked = transitionWizard(
+ state,
+ { type: "CONTINUE_REQUESTED" },
+ context(emptyDraft, ""),
+ );
+ expect(blocked.state.stepId).toBe("intent");
+ expect(blocked.effects).toEqual([
+ { type: "focus-field", fieldId: "proposal-kind" },
+ ]);
+ state = blocked.state;
+ const skipped = transitionWizard(
+ state,
+ { type: "STEP_REQUESTED", stepId: "review" },
+ context(emptyDraft, ""),
+ );
+ expect(skipped.state.stepId).toBe("intent");
+});
+
+test("save and submit status transitions are idempotent", () => {
+ const ctx = context(completePolicyDraft());
+ let state = createWizardState("project-policy", "review");
+ state = transitionWizard(state, { type: "LOCAL_SAVE_COMPLETED" }, ctx).state;
+ expect(state.saveStatus).toBe("saved-local");
+ state = transitionWizard(state, { type: "SERVER_SAVE_REQUESTED" }, ctx).state;
+ expect(state.saveStatus).toBe("syncing");
+ state = transitionWizard(state, { type: "LOCAL_SAVE_COMPLETED" }, ctx).state;
+ expect(state.saveStatus).toBe("syncing");
+ state = transitionWizard(state, { type: "SERVER_SAVE_SUCCEEDED" }, ctx).state;
+ expect(state.saveStatus).toBe("synced");
+ state = transitionWizard(state, { type: "SUBMIT_REQUESTED" }, ctx).state;
+ const repeated = transitionWizard(
+ state,
+ { type: "SUBMIT_REQUESTED" },
+ ctx,
+ ).state;
+ expect(repeated).toEqual(state);
+});
diff --git a/tests/unit/proposal-wizard-payload-parity.test.ts b/tests/unit/proposal-wizard-payload-parity.test.ts
new file mode 100644
index 0000000..8f1cf7b
--- /dev/null
+++ b/tests/unit/proposal-wizard-payload-parity.test.ts
@@ -0,0 +1,142 @@
+import { expect, test } from "@rstest/core";
+
+import { draftToApiForm } from "../../src/pages/proposals/proposalCreation/toApiForm";
+import {
+ SYSTEM_ACTIONS,
+ type SystemActionId,
+} from "../../src/pages/proposals/proposalCreation/templates/systemActions";
+import {
+ DEFAULT_DRAFT,
+ type ProposalDraftForm,
+} from "../../src/pages/proposals/proposalCreation/types";
+
+test("policy payload preserves Initiative and reconsideration provenance", () => {
+ const payload = draftToApiForm(
+ {
+ ...structuredClone(DEFAULT_DRAFT),
+ title: "Policy",
+ chamberId: "general",
+ summary: "Policy summary",
+ what: "Policy change",
+ why: "Policy reason",
+ how: "Policy publication",
+ formationEligible: false,
+ presetId: "project.policy",
+ initiativeId: "initiative-1",
+ resubmitsProposalId: "proposal-root",
+ agreeRules: true,
+ confirmBudget: true,
+ },
+ { templateId: "project" },
+ );
+ expect(payload).toMatchObject({
+ templateId: "project",
+ presetId: "project.policy",
+ initiativeId: "initiative-1",
+ resubmitsProposalId: "proposal-root",
+ formationEligible: false,
+ title: "Policy",
+ chamberId: "general",
+ });
+ expect(payload.timeline).toEqual([]);
+ expect(payload.openSlotNeeds).toEqual([]);
+ expect(payload.budgetItems).toEqual([]);
+});
+
+test("Formation payload aligns positive milestone budgets", () => {
+ const payload = draftToApiForm(
+ {
+ ...structuredClone(DEFAULT_DRAFT),
+ title: "Formation",
+ formationEligible: true,
+ timeline: [
+ {
+ id: "m1",
+ title: "Research",
+ timeframe: "2 weeks",
+ budgetHmnd: "150",
+ },
+ {
+ id: "m2",
+ title: "",
+ timeframe: "1 month",
+ budgetHmnd: "250",
+ },
+ ],
+ },
+ { templateId: "project" },
+ );
+ expect(payload.budgetItems).toEqual([
+ { id: "m1", description: "Research", amount: "150" },
+ { id: "m2", description: "Milestone 2", amount: "250" },
+ ]);
+});
+
+test("system payload preserves executable action metadata", () => {
+ const payload = draftToApiForm(
+ {
+ ...structuredClone(DEFAULT_DRAFT),
+ title: "Create research chamber",
+ chamberId: "general",
+ how: "Adopt the chamber definition",
+ proposalType: "administrative",
+ presetId: "system.chamber.create",
+ formationEligible: false,
+ metaGovernance: {
+ action: "chamber.create",
+ chamberId: "research",
+ title: "Research Chamber",
+ multiplier: 1.4,
+ genesisMembers: ["hm123"],
+ },
+ },
+ { templateId: "system" },
+ );
+ expect(payload).toMatchObject({
+ templateId: "system",
+ presetId: "system.chamber.create",
+ chamberId: "general",
+ metaGovernance: {
+ action: "chamber.create",
+ chamberId: "research",
+ title: "Research Chamber",
+ multiplier: 1.4,
+ genesisMembers: ["hm123"],
+ },
+ });
+});
+
+test("every registered system action keeps its declared target shape", () => {
+ for (const action of Object.keys(SYSTEM_ACTIONS) as SystemActionId[]) {
+ const meta = SYSTEM_ACTIONS[action];
+ const payload = draftToApiForm(
+ {
+ ...structuredClone(DEFAULT_DRAFT),
+ title: `${meta.label} proposal`,
+ chamberId: "general",
+ how: "Apply and verify the approved system change.",
+ proposalType: "administrative",
+ presetId: `system.${action}` as ProposalDraftForm["presetId"],
+ formationEligible: false,
+ metaGovernance: {
+ action,
+ ...(meta.requiresChamberId ? { chamberId: "research" } : {}),
+ ...(meta.requiresTargetAddress ? { targetAddress: "hm123" } : {}),
+ ...(meta.requiresTitle ? { title: "Research Chamber" } : {}),
+ ...(meta.showMultiplier ? { multiplier: 1.4 } : {}),
+ ...(meta.showGenesisMembers ? { genesisMembers: ["hm123"] } : {}),
+ },
+ },
+ { templateId: "system" },
+ );
+
+ expect(payload.metaGovernance).toEqual({
+ action,
+ ...(meta.requiresChamberId ? { chamberId: "research" } : {}),
+ ...(meta.requiresTargetAddress ? { targetAddress: "hm123" } : {}),
+ ...(meta.requiresTitle ? { title: "Research Chamber" } : {}),
+ ...(meta.showMultiplier ? { multiplier: 1.4 } : {}),
+ ...(meta.showGenesisMembers ? { genesisMembers: ["hm123"] } : {}),
+ });
+ }
+});
diff --git a/tests/unit/proposal-wizard-session.test.ts b/tests/unit/proposal-wizard-session.test.ts
new file mode 100644
index 0000000..c04617b
--- /dev/null
+++ b/tests/unit/proposal-wizard-session.test.ts
@@ -0,0 +1,241 @@
+import { expect, test } from "@rstest/core";
+
+import { DEFAULT_DRAFT } from "../../src/pages/proposals/proposalCreation/types";
+import {
+ createProposalWizardSessionRepository,
+ mergeProposalWizardServerSave,
+ normalizeSessionDraft,
+} from "../../src/pages/proposals/proposalCreation/sessionStorage";
+
+class MemoryStorage {
+ private readonly values = new Map();
+
+ getItem(key: string) {
+ return this.values.get(key) ?? null;
+ }
+
+ setItem(key: string, value: string) {
+ this.values.set(key, value);
+ }
+
+ removeItem(key: string) {
+ this.values.delete(key);
+ }
+}
+
+function repository(storage = new MemoryStorage()) {
+ let id = 0;
+ let tick = 0;
+ return {
+ repository: createProposalWizardSessionRepository(storage, {
+ createId: () => `session-${++id}`,
+ now: () => `2026-07-02T00:00:0${tick++}.000Z`,
+ }),
+ storage,
+ };
+}
+
+test("sessions isolate form, step, template, preset, and server identity", () => {
+ const { repository: repo } = repository();
+ const first = repo.create();
+ const second = repo.create();
+ repo.save({
+ ...first,
+ draftId: "draft-one",
+ presetId: "project.policy",
+ lastVisitedStep: "plan",
+ form: { ...first.form, title: "First", formationEligible: false },
+ });
+ expect(repo.get(first.sessionId)?.form.title).toBe("First");
+ expect(repo.get(second.sessionId)?.form.title).toBe("");
+ expect(repo.get(second.sessionId)?.draftId).toBeUndefined();
+});
+
+test("legacy migration preserves content but ignores the legacy step", () => {
+ const { repository: repo, storage } = repository();
+ storage.setItem(
+ "vortex:proposalCreation:draft",
+ JSON.stringify({
+ ...DEFAULT_DRAFT,
+ title: "Recovered",
+ formationEligible: false,
+ }),
+ );
+ storage.setItem("vortex:proposalCreation:step", "review");
+ storage.setItem("vortex:proposalCreation:preset", "project.policy");
+ storage.setItem("vortex:proposalCreation:template", "project");
+ const migrated = repo.migrateLegacy();
+ expect(migrated?.form.title).toBe("Recovered");
+ expect(migrated?.lastVisitedStep).toBe("essentials");
+ expect(storage.getItem("vortex:proposalCreation:draft")).not.toBeNull();
+ expect(repo.migrateLegacy()).toBeNull();
+});
+
+test("malformed stores fail closed without affecting new in-memory work", () => {
+ const { repository: repo, storage } = repository();
+ storage.setItem("vortex:proposalWizard:sessions:v2", "{bad json");
+ const created = repo.create({ form: { ...DEFAULT_DRAFT, title: "Safe" } });
+ expect(repo.get(created.sessionId)?.form.title).toBe("Safe");
+});
+
+test("parseable recovery content normalizes scalar and collection values before rendering", () => {
+ const normalized = normalizeSessionDraft({
+ title: 42,
+ chamberId: ["general"],
+ proposalType: "unknown",
+ timeline: [
+ null,
+ { id: 7, title: "Milestone", timeframe: 3, budgetHmnd: 50 },
+ ],
+ outputs: ["invalid", { id: 3, label: "Evidence", url: 9 }],
+ openSlotNeeds: [{ id: 4, title: "Reviewer", desc: null }],
+ budgetItems: [{ id: 5, description: "Research", amount: 100 }],
+ attachments: [{ id: 6, label: "Brief", url: false }],
+ metaGovernance: { action: "not-a-system-action" },
+ agreeRules: "yes",
+ confirmBudget: 1,
+ });
+
+ expect(normalized.title).toBe("");
+ expect(normalized.chamberId).toBe("");
+ expect(normalized.proposalType).toBe("basic");
+ expect(normalized.timeline).toEqual([
+ {
+ id: "milestone-2",
+ title: "Milestone",
+ timeframe: "",
+ budgetHmnd: "",
+ },
+ ]);
+ expect(normalized.outputs).toEqual([
+ { id: "output-2", label: "Evidence", url: "" },
+ ]);
+ expect(normalized.openSlotNeeds).toEqual([
+ { id: "slot-1", title: "Reviewer", desc: "" },
+ ]);
+ expect(normalized.budgetItems).toEqual([
+ { id: "budget-1", description: "Research", amount: "" },
+ ]);
+ expect(normalized.attachments).toEqual([
+ { id: "attachment-1", label: "Brief", url: "" },
+ ]);
+ expect(normalized.metaGovernance).toBeUndefined();
+ expect(normalized.agreeRules).toBe(false);
+ expect(normalized.confirmBudget).toBe(false);
+});
+
+test("parseable session metadata cannot crash recovery sorting", () => {
+ const { repository: repo, storage } = repository();
+ storage.setItem(
+ "vortex:proposalWizard:sessions:v2",
+ JSON.stringify({
+ version: 2,
+ sessions: {
+ corrupted: {
+ version: 2,
+ sessionId: "corrupted",
+ templateId: "project",
+ presetId: 3,
+ pathId: "not-a-real-path",
+ lastVisitedStep: "not-a-real-step",
+ localRevision: -12,
+ updatedAt: 42,
+ form: { ...DEFAULT_DRAFT, title: "Recovered safely" },
+ },
+ },
+ }),
+ );
+
+ expect(repo.listRecoverable()).toEqual([
+ expect.objectContaining({
+ sessionId: "corrupted",
+ presetId: "",
+ lastVisitedStep: "intent",
+ localRevision: 0,
+ updatedAt: "",
+ }),
+ ]);
+});
+
+test("system-action recovery never regains Formation defaults", () => {
+ const normalized = normalizeSessionDraft({
+ ...DEFAULT_DRAFT,
+ formationEligible: true,
+ metaGovernance: {
+ action: "chamber.censure",
+ chamberId: "general",
+ },
+ });
+
+ expect(normalized.formationEligible).toBe(false);
+ expect(normalized.metaGovernance).toEqual({
+ action: "chamber.censure",
+ chamberId: "general",
+ });
+});
+
+test("recoverable sessions omit empty current sessions and sort newest first", () => {
+ const { repository: repo } = repository();
+ const current = repo.create();
+ const older = repo.create({ form: { ...DEFAULT_DRAFT, title: "Older" } });
+ const newer = repo.create({ form: { ...DEFAULT_DRAFT, title: "Newer" } });
+ expect(
+ repo.listRecoverable(current.sessionId).map((session) => session.sessionId),
+ ).toEqual([newer.sessionId, older.sessionId]);
+});
+
+test("removing one session does not clear another", () => {
+ const { repository: repo } = repository();
+ const first = repo.create({ form: { ...DEFAULT_DRAFT, title: "First" } });
+ const second = repo.create({ form: { ...DEFAULT_DRAFT, title: "Second" } });
+ repo.remove(first.sessionId);
+ expect(repo.get(first.sessionId)).toBeNull();
+ expect(repo.get(second.sessionId)?.form.title).toBe("Second");
+});
+
+test("legacy data is removed only through explicit repository cleanup", () => {
+ const { repository: repo, storage } = repository();
+ storage.setItem(
+ "vortex:proposalCreation:draft",
+ JSON.stringify({ ...DEFAULT_DRAFT, title: "Legacy" }),
+ );
+ const migrated = repo.migrateLegacy();
+ expect(migrated?.legacyRecovery).toBe(true);
+ expect(storage.getItem("vortex:proposalCreation:draft")).not.toBeNull();
+ repo.clearLegacy();
+ expect(storage.getItem("vortex:proposalCreation:draft")).toBeNull();
+});
+
+test("server synchronization binds identity without replacing newer local work", () => {
+ const { repository: repo } = repository();
+ const requested = repo.create({
+ form: { ...DEFAULT_DRAFT, title: "Requested" },
+ });
+ const latest = repo.save({
+ ...requested,
+ form: { ...requested.form, title: "Edited during synchronization" },
+ });
+ const merged = mergeProposalWizardServerSave({
+ draftId: "draft-server",
+ latest,
+ requested,
+ serverSavedAt: "2026-07-02T12:00:00.000Z",
+ });
+ expect(merged.changedDuringSync).toBe(true);
+ expect(merged.session.draftId).toBe("draft-server");
+ expect(merged.session.form.title).toBe("Edited during synchronization");
+});
+
+test("server synchronization cannot cross proposal sessions", () => {
+ const { repository: repo } = repository();
+ const requested = repo.create();
+ const latest = repo.create();
+ expect(() =>
+ mergeProposalWizardServerSave({
+ draftId: "draft-server",
+ latest,
+ requested,
+ serverSavedAt: "2026-07-02T12:00:00.000Z",
+ }),
+ ).toThrow("another wizard session");
+});
diff --git a/tests/unit/proposal-wizard-submit-routing.test.ts b/tests/unit/proposal-wizard-submit-routing.test.ts
new file mode 100644
index 0000000..cec2011
--- /dev/null
+++ b/tests/unit/proposal-wizard-submit-routing.test.ts
@@ -0,0 +1,61 @@
+import { expect, test } from "@rstest/core";
+
+import { DEFAULT_DRAFT } from "../../src/pages/proposals/proposalCreation/types";
+import { proposalSubmitErrorStep } from "../../src/pages/proposals/proposalCreation/submitErrorRouting";
+
+const context = {
+ draft: {
+ ...structuredClone(DEFAULT_DRAFT),
+ formationEligible: false,
+ title: "Policy",
+ what: "Change",
+ why: "Reason",
+ how: "",
+ },
+ presetId: "project.policy",
+ tierBlocked: false,
+};
+
+function apiError(code: string) {
+ return { data: { error: { code } } };
+}
+
+test("proposal-right errors return to Intent", () => {
+ expect(
+ proposalSubmitErrorStep(
+ apiError("proposal_type_ineligible"),
+ "project-policy",
+ context,
+ ),
+ ).toBe("intent");
+});
+
+test("system target errors return to System change", () => {
+ expect(
+ proposalSubmitErrorStep(
+ apiError("invalid_meta_chamber"),
+ "system-change",
+ context,
+ ),
+ ).toBe("system-change");
+});
+
+test("scope errors return to the path-owned scope step", () => {
+ expect(
+ proposalSubmitErrorStep(
+ apiError("invalid_initiative_association"),
+ "project-policy",
+ context,
+ ),
+ ).toBe("essentials");
+});
+
+test("incomplete server drafts resolve to the first incomplete local step", () => {
+ expect(
+ proposalSubmitErrorStep(
+ apiError("draft_not_submittable"),
+ "project-policy",
+ context,
+ ),
+ ).toBe("plan");
+});
diff --git a/tests/unit/proposal-wizard-ui-contract.test.ts b/tests/unit/proposal-wizard-ui-contract.test.ts
new file mode 100644
index 0000000..faf95c6
--- /dev/null
+++ b/tests/unit/proposal-wizard-ui-contract.test.ts
@@ -0,0 +1,77 @@
+import { readFileSync } from "node:fs";
+import { join } from "node:path";
+
+import { expect, test } from "@rstest/core";
+
+const root = process.cwd();
+
+test("Proposal Wizard 2.0 owns submission and uses the shared glass shell", () => {
+ const creation = readFileSync(
+ join(root, "src/pages/proposals/ProposalCreation.tsx"),
+ "utf8",
+ );
+ const shell = readFileSync(
+ join(root, "src/pages/proposals/proposalCreation/ProposalWizardShell.tsx"),
+ "utf8",
+ );
+ expect(creation).toContain("apiProposalSubmitToPool");
+ expect(creation).toContain(" {
+ const detail = readFileSync(
+ join(root, "src/pages/proposals/ProposalDraft.tsx"),
+ "utf8",
+ );
+ expect(detail).toContain("Continue editing");
+ expect(detail).toContain("apiProposalDraftDelete");
+ expect(detail).toContain("Delete draft");
+ expect(detail).not.toContain("apiProposalSubmitToPool");
+ expect(detail).not.toContain("Submit to pool");
+});
+
+test("global proposal creation runtime is removed", () => {
+ const creation = readFileSync(
+ join(root, "src/pages/proposals/ProposalCreation.tsx"),
+ "utf8",
+ );
+ const sessions = readFileSync(
+ join(root, "src/pages/proposals/proposalCreation/sessionStorage.ts"),
+ "utf8",
+ );
+ expect(creation).not.toContain("loadStep");
+ expect(creation).not.toContain("STORAGE_STEP_KEY");
+ expect(sessions).toContain("vortex:proposalWizard:sessions:v2");
+ expect(sessions).toContain("migrateLegacy");
+});
+
+test("Review preserves the wizard's path-specific authoring sections", () => {
+ const review = readFileSync(
+ join(root, "src/pages/proposals/proposalCreation/steps/ReviewStep.tsx"),
+ "utf8",
+ );
+
+ for (const section of [
+ "Proposal path",
+ "Identity",
+ "Case",
+ "Plan",
+ "Funding and team",
+ "System action",
+ "Proposal identity",
+ "Rationale",
+ "Proposer",
+ "Supporting material",
+ "Confirm",
+ ]) {
+ expect(review).toContain(`title=\"${section}\"`);
+ }
+ expect(review).toContain(" ");
+ expect(review).toContain(" ");
+ expect(review).toContain(" ");
+});
diff --git a/yarn.lock b/yarn.lock
index 135ef4e..70dd8da 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -40,6 +40,41 @@ __metadata:
languageName: node
linkType: hard
+"@floating-ui/core@npm:^1.8.0":
+ version: 1.8.0
+ resolution: "@floating-ui/core@npm:1.8.0"
+ dependencies:
+ "@floating-ui/utils": "npm:^0.2.12"
+ checksum: 10c0/cece958f6fab0f8cd7740cd57988c4f8bc5356dca39dd4c093433d44c91952c3a02c7a54d93d7ed6c078b6544d531a29f66621d2839c8bfbb4467dca6aed7df0
+ languageName: node
+ linkType: hard
+
+"@floating-ui/dom@npm:^1.0.0":
+ version: 1.8.0
+ resolution: "@floating-ui/dom@npm:1.8.0"
+ dependencies:
+ "@floating-ui/core": "npm:^1.8.0"
+ "@floating-ui/utils": "npm:^0.2.12"
+ checksum: 10c0/c32b2de7a596b69cfffa909c7028825e14f6319be1b45da529f8943c89fe16b75a49c214e800c87ded3f172353d3b99163d50072f11410488e756961b7b629cc
+ languageName: node
+ linkType: hard
+
+"@floating-ui/utils@npm:^0.2.12":
+ version: 0.2.12
+ resolution: "@floating-ui/utils@npm:0.2.12"
+ checksum: 10c0/bb910b90d56991a62011afaf5f8e0c4b7fb6dab69403f4dd17fbd6eb25560b28d533dff9791f270b4f459852e9006a1077b5d73cbedb5e34d9424afc6a828314
+ languageName: node
+ linkType: hard
+
+"@isaacs/fs-minipass@npm:^4.0.0":
+ version: 4.0.1
+ resolution: "@isaacs/fs-minipass@npm:4.0.1"
+ dependencies:
+ minipass: "npm:^7.0.4"
+ checksum: 10c0/c25b6dc1598790d5b55c0947a9b7d111cfa92594db5296c3b907e2f533c033666f692a3939eadac17b1c7c40d362d0b0635dc874cbfe3e70db7c2b07cc97a5d2
+ languageName: node
+ linkType: hard
+
"@jridgewell/gen-mapping@npm:^0.3.5":
version: 0.3.13
resolution: "@jridgewell/gen-mapping@npm:0.3.13"
@@ -221,6 +256,17 @@ __metadata:
languageName: node
linkType: hard
+"@playwright/test@npm:^1.61.1":
+ version: 1.61.1
+ resolution: "@playwright/test@npm:1.61.1"
+ dependencies:
+ playwright: "npm:1.61.1"
+ bin:
+ playwright: cli.js
+ checksum: 10c0/45004139eaa7c7ec8129e4ed77a9d734aa09ed40b69162b871e4123f1d70217b7b73651431a9343ed5090d5fed11c6df1bff2a56ff4b87dc7b0371b5da87cf9c
+ languageName: node
+ linkType: hard
+
"@polkadot/keyring@npm:^14.0.1":
version: 14.0.1
resolution: "@polkadot/keyring@npm:14.0.1"
@@ -988,6 +1034,334 @@ __metadata:
languageName: node
linkType: hard
+"@tiptap/core@npm:^3.28.0":
+ version: 3.28.0
+ resolution: "@tiptap/core@npm:3.28.0"
+ peerDependencies:
+ "@tiptap/pm": 3.28.0
+ checksum: 10c0/a7a6f09ad01491bab03d442fb10b6b30a04eaa42be3bad5a1d12d2b2d2bfefe4dba172b9b62324eb102dd50d997ee0d104e563eb4a7c6b4753b95bf70ffc6b68
+ languageName: node
+ linkType: hard
+
+"@tiptap/extension-blockquote@npm:^3.28.0":
+ version: 3.28.0
+ resolution: "@tiptap/extension-blockquote@npm:3.28.0"
+ peerDependencies:
+ "@tiptap/core": 3.28.0
+ "@tiptap/pm": 3.28.0
+ checksum: 10c0/fa11a2ddf9cb5a9dcc5839bfa38e9e8a68a58857b67337f45eef5bb2d958e65152e41940d786830e26fdb2244bd9b5abf740461a7921777f64090fd3c11b625b
+ languageName: node
+ linkType: hard
+
+"@tiptap/extension-bold@npm:^3.28.0":
+ version: 3.28.0
+ resolution: "@tiptap/extension-bold@npm:3.28.0"
+ peerDependencies:
+ "@tiptap/core": 3.28.0
+ checksum: 10c0/48ec3828099ee9a36a6daef276809772bfc005e088fd2f793f5754d9009b533f3bf769a9c4c52bc312c554f3add8e0f282b873f14a9f86dd826f09e5e6978236
+ languageName: node
+ linkType: hard
+
+"@tiptap/extension-bubble-menu@npm:^3.28.0":
+ version: 3.28.0
+ resolution: "@tiptap/extension-bubble-menu@npm:3.28.0"
+ dependencies:
+ "@floating-ui/dom": "npm:^1.0.0"
+ peerDependencies:
+ "@tiptap/core": 3.28.0
+ "@tiptap/pm": 3.28.0
+ checksum: 10c0/ffc5152e0026785ddee586b738e75acb90883a13019e685c8f7c88a58683967a06005ef8e803caf7b83f904a4b8ad21a23b85e5a8e1f8b331ef89cb8d111060a
+ languageName: node
+ linkType: hard
+
+"@tiptap/extension-bullet-list@npm:^3.28.0":
+ version: 3.28.0
+ resolution: "@tiptap/extension-bullet-list@npm:3.28.0"
+ peerDependencies:
+ "@tiptap/extension-list": 3.28.0
+ checksum: 10c0/c79d2a163fdb901974830f919a568893a9f5c6cd7d91952e141d929d072928e88e177721dce197a4a7251f8362f9cda27f046172f2f1b9e8b4f84edc214e6a06
+ languageName: node
+ linkType: hard
+
+"@tiptap/extension-code-block@npm:^3.28.0":
+ version: 3.28.0
+ resolution: "@tiptap/extension-code-block@npm:3.28.0"
+ peerDependencies:
+ "@tiptap/core": 3.28.0
+ "@tiptap/pm": 3.28.0
+ checksum: 10c0/43da443674b91fb3768afbfb18a27cc5d93c0c0eb70ae61a8a98564b4d27faa32dd680f6607ea4a6d73f10e89824b649a319dd3017f186b7d7a771f69b8b7532
+ languageName: node
+ linkType: hard
+
+"@tiptap/extension-code@npm:^3.28.0":
+ version: 3.28.0
+ resolution: "@tiptap/extension-code@npm:3.28.0"
+ peerDependencies:
+ "@tiptap/core": 3.28.0
+ checksum: 10c0/deda757b77e060f3bc8039b14103f7767c920789ae90b3ce94d27fd9f6939b95577ed805ffbbc8dcd18184382c15c361bcc577eabe363f9870118f54f6d88f9e
+ languageName: node
+ linkType: hard
+
+"@tiptap/extension-document@npm:^3.28.0":
+ version: 3.28.0
+ resolution: "@tiptap/extension-document@npm:3.28.0"
+ peerDependencies:
+ "@tiptap/core": 3.28.0
+ checksum: 10c0/e7b712711de27984773884d59431c888e36803c8a981870cc2aaf846a60382a21385f205da730e605b1752743cae6bae2057af3460ff40415e8789a8c3d4e873
+ languageName: node
+ linkType: hard
+
+"@tiptap/extension-dropcursor@npm:^3.28.0":
+ version: 3.28.0
+ resolution: "@tiptap/extension-dropcursor@npm:3.28.0"
+ peerDependencies:
+ "@tiptap/extensions": 3.28.0
+ checksum: 10c0/547af0ee9371c99b1540e32ca1526296a363de0b6bbd9163ca2b851112e8f277bf200fd583d3eecb9116fc3ed4e7bd72dc31c7a87a93b7456484e9f8677a3a73
+ languageName: node
+ linkType: hard
+
+"@tiptap/extension-floating-menu@npm:^3.28.0":
+ version: 3.28.0
+ resolution: "@tiptap/extension-floating-menu@npm:3.28.0"
+ peerDependencies:
+ "@floating-ui/dom": ^1.0.0
+ "@tiptap/core": 3.28.0
+ "@tiptap/pm": 3.28.0
+ checksum: 10c0/a5a895761841d55c668ba89ef19d7fafd0cb1e4137d4c78e3ffb9cb560f4ef385069688f4ef8d17610201b4e3cee9baf1180a389c60ad25a707a1562bfcd82bc
+ languageName: node
+ linkType: hard
+
+"@tiptap/extension-gapcursor@npm:^3.28.0":
+ version: 3.28.0
+ resolution: "@tiptap/extension-gapcursor@npm:3.28.0"
+ peerDependencies:
+ "@tiptap/extensions": 3.28.0
+ checksum: 10c0/6c53091aed12579abc5b48f8ad60a66199d3231ca5c4c99d02e14d8fe7e6fa4fc1294ae9cca1d2968d028479a9edf4fa72063e7e67f7b4dc32f20626a98e308e
+ languageName: node
+ linkType: hard
+
+"@tiptap/extension-hard-break@npm:^3.28.0":
+ version: 3.28.0
+ resolution: "@tiptap/extension-hard-break@npm:3.28.0"
+ peerDependencies:
+ "@tiptap/core": 3.28.0
+ checksum: 10c0/081de40034e3b6058bd3ce118360d862220a202be0c5af2c72ac2b98109228ba9ccebae9c599cfb42b0f2c857b56fe956158450d02b67e9322e334dd978d5ad8
+ languageName: node
+ linkType: hard
+
+"@tiptap/extension-heading@npm:^3.28.0":
+ version: 3.28.0
+ resolution: "@tiptap/extension-heading@npm:3.28.0"
+ peerDependencies:
+ "@tiptap/core": 3.28.0
+ checksum: 10c0/ca8c20cdec4bfc7b25070eb4fcf1e1c3143c714c2f0f3b9d20df6bf57de3d6e1109529f93d77954394e6615ed898d109a918e4baa56ec0e16699c29aa799a8c2
+ languageName: node
+ linkType: hard
+
+"@tiptap/extension-horizontal-rule@npm:^3.28.0":
+ version: 3.28.0
+ resolution: "@tiptap/extension-horizontal-rule@npm:3.28.0"
+ peerDependencies:
+ "@tiptap/core": 3.28.0
+ "@tiptap/pm": 3.28.0
+ checksum: 10c0/eae05eadce7910f988dff419c2cdda6178ad27428e3a218fe242e15917c87a5171cad019a088e57cf0278a6453a30b01f8836550b76c789757fc63e67a0253e4
+ languageName: node
+ linkType: hard
+
+"@tiptap/extension-italic@npm:^3.28.0":
+ version: 3.28.0
+ resolution: "@tiptap/extension-italic@npm:3.28.0"
+ peerDependencies:
+ "@tiptap/core": 3.28.0
+ checksum: 10c0/0cd63e8443e415da3e7e7d4327569c96c561b1618a6455f3f7e29fd70eeec49542d16a6e8da4faa4a2acf51f1d2a5141910f0849ba3a4f1dbf5934458d8aad5e
+ languageName: node
+ linkType: hard
+
+"@tiptap/extension-link@npm:^3.28.0":
+ version: 3.28.0
+ resolution: "@tiptap/extension-link@npm:3.28.0"
+ dependencies:
+ linkifyjs: "npm:^4.3.3"
+ peerDependencies:
+ "@tiptap/core": 3.28.0
+ "@tiptap/pm": 3.28.0
+ checksum: 10c0/9679f42194ddd48a5ef3e1b9a5b472216ffd6e7de506d294ad92993fad96364e0a49b594f3728abbcef5958a4c17acd9d0384d8007cba866be24d6c1a410e7d3
+ languageName: node
+ linkType: hard
+
+"@tiptap/extension-list-item@npm:^3.28.0":
+ version: 3.28.0
+ resolution: "@tiptap/extension-list-item@npm:3.28.0"
+ peerDependencies:
+ "@tiptap/extension-list": 3.28.0
+ checksum: 10c0/316069ba85275afede42d0fc8c4e78d9660853e94cfd0e2e53009d3cc61d94101583ebfe2a7d902c544a1fb36cabea4ff3b9e9f094da5695f3ea22dd949e2cce
+ languageName: node
+ linkType: hard
+
+"@tiptap/extension-list-keymap@npm:^3.28.0":
+ version: 3.28.0
+ resolution: "@tiptap/extension-list-keymap@npm:3.28.0"
+ peerDependencies:
+ "@tiptap/extension-list": 3.28.0
+ checksum: 10c0/86a20fec54e1a9e460fb4f4bbf8325bda388ea69d3eac4febfcdc5db09aa780391b08cfeb3fec3b589dfdbe88cc3cb2fcb1560cd3cd2edbd7d997b9b61158631
+ languageName: node
+ linkType: hard
+
+"@tiptap/extension-list@npm:^3.28.0":
+ version: 3.28.0
+ resolution: "@tiptap/extension-list@npm:3.28.0"
+ peerDependencies:
+ "@tiptap/core": 3.28.0
+ "@tiptap/pm": 3.28.0
+ checksum: 10c0/45aa6157e0d6dbe84178c09cf877902aa57afa78aba9636a101d4fe47a4da3007b6dbd9af8018e6b80d2322828e95924380427fdf54707a2287cf284738028de
+ languageName: node
+ linkType: hard
+
+"@tiptap/extension-ordered-list@npm:^3.28.0":
+ version: 3.28.0
+ resolution: "@tiptap/extension-ordered-list@npm:3.28.0"
+ peerDependencies:
+ "@tiptap/extension-list": 3.28.0
+ checksum: 10c0/29b8b4000beca8dd88ef9ff6ac7291fa80c591f9964c732bcf1de0d59f116b5a6d495bb8a88418131d936ed2bcb1c2796d22841e45cac1237ba395b8db7fada4
+ languageName: node
+ linkType: hard
+
+"@tiptap/extension-paragraph@npm:^3.28.0":
+ version: 3.28.0
+ resolution: "@tiptap/extension-paragraph@npm:3.28.0"
+ peerDependencies:
+ "@tiptap/core": 3.28.0
+ checksum: 10c0/a56f3dad58f2b2510dc951ccfbb6dfb9355470e866d849c3fc18a18bb3735a828876ba79ff525e68dd1b1a7c637217494db2586008bbaa658b4fd29f42c36c7b
+ languageName: node
+ linkType: hard
+
+"@tiptap/extension-strike@npm:^3.28.0":
+ version: 3.28.0
+ resolution: "@tiptap/extension-strike@npm:3.28.0"
+ peerDependencies:
+ "@tiptap/core": 3.28.0
+ checksum: 10c0/91c13710b3f09143aeab7e8e1c19632fd1802ed6c1152f198831e66a49c3445c19e797c0c0ccd5e14a7e49b1e19ab563fe15fefd32b8701324b49beab114663d
+ languageName: node
+ linkType: hard
+
+"@tiptap/extension-text@npm:^3.28.0":
+ version: 3.28.0
+ resolution: "@tiptap/extension-text@npm:3.28.0"
+ peerDependencies:
+ "@tiptap/core": 3.28.0
+ checksum: 10c0/28997a7d8ceca886eda1dad6b6bdf1e71e7f5b8ee121103c5a13b573a56df792669e326991dd19fb6e756a23368afd102b5c8aa6d8d1708527d4fa40478cb7bd
+ languageName: node
+ linkType: hard
+
+"@tiptap/extension-underline@npm:^3.28.0":
+ version: 3.28.0
+ resolution: "@tiptap/extension-underline@npm:3.28.0"
+ peerDependencies:
+ "@tiptap/core": 3.28.0
+ checksum: 10c0/c7bdd7d8063f25c2f31e2747d2d7a6bf4b5c259495b7ce8dcda669bc2b976b17c503f3d7ff467722005f2e9467c7dd5d234b6e93d2beea63f8957e88b03e1962
+ languageName: node
+ linkType: hard
+
+"@tiptap/extensions@npm:^3.28.0":
+ version: 3.28.0
+ resolution: "@tiptap/extensions@npm:3.28.0"
+ peerDependencies:
+ "@tiptap/core": 3.28.0
+ "@tiptap/pm": 3.28.0
+ checksum: 10c0/24127dd95dcf4e035d4a6b0cde9d68184bce245f0ee7a43cceb484b45ec891d5a96ed89f46ec8e6df33c5fdb1e081a9598a71b4a63f2fab910635e18d32231ca
+ languageName: node
+ linkType: hard
+
+"@tiptap/markdown@npm:^3.28.0":
+ version: 3.28.0
+ resolution: "@tiptap/markdown@npm:3.28.0"
+ dependencies:
+ marked: "npm:^17.0.1"
+ peerDependencies:
+ "@tiptap/core": 3.28.0
+ "@tiptap/pm": 3.28.0
+ checksum: 10c0/e27cdc22240e1d254440d6b1eb8eec51762fe2399072679c300e4c6d6e1261cc064e204b07a447aeb202325a97ef14fa6f5b27d4c8010d988fc0c6b1b265f2e6
+ languageName: node
+ linkType: hard
+
+"@tiptap/pm@npm:^3.28.0":
+ version: 3.28.0
+ resolution: "@tiptap/pm@npm:3.28.0"
+ dependencies:
+ prosemirror-changeset: "npm:^2.4.1"
+ prosemirror-commands: "npm:^1.7.1"
+ prosemirror-dropcursor: "npm:^1.8.2"
+ prosemirror-gapcursor: "npm:^1.4.1"
+ prosemirror-history: "npm:^1.5.0"
+ prosemirror-inputrules: "npm:^1.5.1"
+ prosemirror-keymap: "npm:^1.2.3"
+ prosemirror-model: "npm:^1.25.9"
+ prosemirror-schema-list: "npm:^1.5.1"
+ prosemirror-state: "npm:^1.4.4"
+ prosemirror-tables: "npm:^1.8.5"
+ prosemirror-transform: "npm:^1.12.0"
+ prosemirror-view: "npm:^1.41.9"
+ checksum: 10c0/36458bb98ea54a602d4fc851c8e5bc1db1f2e01c2c61affeea300c11a0bff68636ed5826ab0b34ca7e94bfddb333475be741b0e366ee3b6280e0d6b88197145b
+ languageName: node
+ linkType: hard
+
+"@tiptap/react@npm:^3.28.0":
+ version: 3.28.0
+ resolution: "@tiptap/react@npm:3.28.0"
+ dependencies:
+ "@tiptap/extension-bubble-menu": "npm:^3.28.0"
+ "@tiptap/extension-floating-menu": "npm:^3.28.0"
+ "@types/use-sync-external-store": "npm:^0.0.6"
+ fast-equals: "npm:^5.3.3"
+ use-sync-external-store: "npm:^1.4.0"
+ peerDependencies:
+ "@tiptap/core": 3.28.0
+ "@tiptap/pm": 3.28.0
+ "@types/react": ^17.0.0 || ^18.0.0 || ^19.0.0
+ "@types/react-dom": ^17.0.0 || ^18.0.0 || ^19.0.0
+ react: ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
+ dependenciesMeta:
+ "@tiptap/extension-bubble-menu":
+ optional: true
+ "@tiptap/extension-floating-menu":
+ optional: true
+ checksum: 10c0/6089ec60dfd743c66b4de6f1e0ab9b4df5d880c8959bc3653fe05f85acdbbc43a3ff7e4798eaf95814306eb25be82c6fc0e1d1c1125ef29c7c4834f885f9a1fb
+ languageName: node
+ linkType: hard
+
+"@tiptap/starter-kit@npm:^3.28.0":
+ version: 3.28.0
+ resolution: "@tiptap/starter-kit@npm:3.28.0"
+ dependencies:
+ "@tiptap/core": "npm:^3.28.0"
+ "@tiptap/extension-blockquote": "npm:^3.28.0"
+ "@tiptap/extension-bold": "npm:^3.28.0"
+ "@tiptap/extension-bullet-list": "npm:^3.28.0"
+ "@tiptap/extension-code": "npm:^3.28.0"
+ "@tiptap/extension-code-block": "npm:^3.28.0"
+ "@tiptap/extension-document": "npm:^3.28.0"
+ "@tiptap/extension-dropcursor": "npm:^3.28.0"
+ "@tiptap/extension-gapcursor": "npm:^3.28.0"
+ "@tiptap/extension-hard-break": "npm:^3.28.0"
+ "@tiptap/extension-heading": "npm:^3.28.0"
+ "@tiptap/extension-horizontal-rule": "npm:^3.28.0"
+ "@tiptap/extension-italic": "npm:^3.28.0"
+ "@tiptap/extension-link": "npm:^3.28.0"
+ "@tiptap/extension-list": "npm:^3.28.0"
+ "@tiptap/extension-list-item": "npm:^3.28.0"
+ "@tiptap/extension-list-keymap": "npm:^3.28.0"
+ "@tiptap/extension-ordered-list": "npm:^3.28.0"
+ "@tiptap/extension-paragraph": "npm:^3.28.0"
+ "@tiptap/extension-strike": "npm:^3.28.0"
+ "@tiptap/extension-text": "npm:^3.28.0"
+ "@tiptap/extension-underline": "npm:^3.28.0"
+ "@tiptap/extensions": "npm:^3.28.0"
+ "@tiptap/pm": "npm:^3.28.0"
+ checksum: 10c0/498ad6e5fc52b289c1517342025755550dd09870a86271ca85255c152e17f49b77f309f2dbb6c318ebba6caabbbccfd26aad1f400295971968411e5cfaabb953
+ languageName: node
+ linkType: hard
+
"@tybys/wasm-util@npm:^0.10.1":
version: 0.10.1
resolution: "@tybys/wasm-util@npm:0.10.1"
@@ -1050,6 +1424,20 @@ __metadata:
languageName: node
linkType: hard
+"@types/use-sync-external-store@npm:^0.0.6":
+ version: 0.0.6
+ resolution: "@types/use-sync-external-store@npm:0.0.6"
+ checksum: 10c0/77c045a98f57488201f678b181cccd042279aff3da34540ad242f893acc52b358bd0a8207a321b8ac09adbcef36e3236944390e2df4fcedb556ce7bb2a88f2a8
+ languageName: node
+ linkType: hard
+
+"abbrev@npm:^5.0.0":
+ version: 5.0.0
+ resolution: "abbrev@npm:5.0.0"
+ checksum: 10c0/8e88f5c798ea4562d28c5a3e9ad69e3879890bc5d695d8f2dffb8609be4c890aacc8f80ef4553fdd2c6a62d70c2ce8bc57b38074e383beb7487bdafa9ed42ea5
+ languageName: node
+ linkType: hard
+
"assertion-error@npm:^2.0.1":
version: 2.0.1
resolution: "assertion-error@npm:2.0.1"
@@ -1064,6 +1452,13 @@ __metadata:
languageName: node
linkType: hard
+"chownr@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "chownr@npm:3.0.0"
+ checksum: 10c0/43925b87700f7e3893296c8e9c56cc58f926411cce3a6e5898136daaf08f08b9a8eb76d37d3267e707d0dcc17aed2e2ebdf5848c0c3ce95cf910a919935c1b10
+ languageName: node
+ linkType: hard
+
"class-variance-authority@npm:^0.7.1":
version: 0.7.1
resolution: "class-variance-authority@npm:0.7.1"
@@ -1125,6 +1520,13 @@ __metadata:
languageName: node
linkType: hard
+"env-paths@npm:^2.2.0":
+ version: 2.2.1
+ resolution: "env-paths@npm:2.2.1"
+ checksum: 10c0/285325677bf00e30845e330eec32894f5105529db97496ee3f598478e50f008c5352a41a30e5e72ec9de8a542b5a570b85699cd63bd2bc646dbcb9f311d83bc4
+ languageName: node
+ linkType: hard
+
"error-stack-parser@npm:^2.1.4":
version: 2.1.4
resolution: "error-stack-parser@npm:2.1.4"
@@ -1134,7 +1536,52 @@ __metadata:
languageName: node
linkType: hard
-"graceful-fs@npm:^4.2.4":
+"exponential-backoff@npm:^3.1.1":
+ version: 3.1.3
+ resolution: "exponential-backoff@npm:3.1.3"
+ checksum: 10c0/77e3ae682b7b1f4972f563c6dbcd2b0d54ac679e62d5d32f3e5085feba20483cf28bd505543f520e287a56d4d55a28d7874299941faf637e779a1aa5994d1267
+ languageName: node
+ linkType: hard
+
+"fast-equals@npm:^5.3.3":
+ version: 5.4.1
+ resolution: "fast-equals@npm:5.4.1"
+ checksum: 10c0/18faf5894c39591d61e802b2f574cbc57d2ff2530c71b8cd4e2b4f3c250174002a9aa0b5f4d6f61073a9942a96d71bc7f36514a9b0dc9a2a5cc3395b2a910dbf
+ languageName: node
+ linkType: hard
+
+"fdir@npm:^6.5.0":
+ version: 6.5.0
+ resolution: "fdir@npm:6.5.0"
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+ checksum: 10c0/e345083c4306b3aed6cb8ec551e26c36bab5c511e99ea4576a16750ddc8d3240e63826cc624f5ae17ad4dc82e68a253213b60d556c11bfad064b7607847ed07f
+ languageName: node
+ linkType: hard
+
+"fsevents@npm:2.3.2":
+ version: 2.3.2
+ resolution: "fsevents@npm:2.3.2"
+ dependencies:
+ node-gyp: "npm:latest"
+ checksum: 10c0/be78a3efa3e181cda3cf7a4637cb527bcebb0bd0ea0440105a3bb45b86f9245b307dc10a2507e8f4498a7d4ec349d1910f4d73e4d4495b16103106e07eee735b
+ conditions: os=darwin
+ languageName: node
+ linkType: hard
+
+"fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin":
+ version: 2.3.2
+ resolution: "fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin::version=2.3.2&hash=df0bf1"
+ dependencies:
+ node-gyp: "npm:latest"
+ conditions: os=darwin
+ languageName: node
+ linkType: hard
+
+"graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6":
version: 4.2.11
resolution: "graceful-fs@npm:4.2.11"
checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2
@@ -1148,6 +1595,13 @@ __metadata:
languageName: node
linkType: hard
+"isexe@npm:^4.0.0":
+ version: 4.0.0
+ resolution: "isexe@npm:4.0.0"
+ checksum: 10c0/5884815115bceac452877659a9c7726382531592f43dc29e5d48b7c4100661aed54018cb90bd36cb2eaeba521092570769167acbb95c18d39afdccbcca06c5ce
+ languageName: node
+ linkType: hard
+
"jiti@npm:^2.6.1":
version: 2.6.1
resolution: "jiti@npm:2.6.1"
@@ -1277,6 +1731,13 @@ __metadata:
languageName: node
linkType: hard
+"linkifyjs@npm:^4.3.3":
+ version: 4.3.3
+ resolution: "linkifyjs@npm:4.3.3"
+ checksum: 10c0/0eac293927f1465d13625c8c67c83c50d7fda9137c255a4a2ff5970ea4e9ba57de4846b170326e54b9e4e82be24f3705572bc50773737be9b13af5f1e4577798
+ languageName: node
+ linkType: hard
+
"lucide-react@npm:^0.554.0":
version: 0.554.0
resolution: "lucide-react@npm:0.554.0"
@@ -1295,6 +1756,31 @@ __metadata:
languageName: node
linkType: hard
+"marked@npm:^17.0.1":
+ version: 17.0.6
+ resolution: "marked@npm:17.0.6"
+ bin:
+ marked: bin/marked.js
+ checksum: 10c0/77961fbb360511d6638491e097cb15b1ef5e643f27c25b9cd52aa3cd92e216fc3cda7dbfc6a10adb7922d1b38df132510df65de4afe6c53c7280772c9d4221bd
+ languageName: node
+ linkType: hard
+
+"minipass@npm:^7.0.4, minipass@npm:^7.1.2":
+ version: 7.1.3
+ resolution: "minipass@npm:7.1.3"
+ checksum: 10c0/539da88daca16533211ea5a9ee98dc62ff5742f531f54640dd34429e621955e91cc280a91a776026264b7f9f6735947629f920944e9c1558369e8bf22eb33fbb
+ languageName: node
+ linkType: hard
+
+"minizlib@npm:^3.1.0":
+ version: 3.1.0
+ resolution: "minizlib@npm:3.1.0"
+ dependencies:
+ minipass: "npm:^7.1.2"
+ checksum: 10c0/5aad75ab0090b8266069c9aabe582c021ae53eb33c6c691054a13a45db3b4f91a7fb1bd79151e6b4e9e9a86727b522527c0a06ec7d45206b745d54cd3097bcec
+ languageName: node
+ linkType: hard
+
"nanoid@npm:^3.3.11":
version: 3.3.11
resolution: "nanoid@npm:3.3.11"
@@ -1304,6 +1790,44 @@ __metadata:
languageName: node
linkType: hard
+"node-gyp@npm:latest":
+ version: 13.0.0
+ resolution: "node-gyp@npm:13.0.0"
+ dependencies:
+ env-paths: "npm:^2.2.0"
+ exponential-backoff: "npm:^3.1.1"
+ graceful-fs: "npm:^4.2.6"
+ nopt: "npm:^10.0.0"
+ proc-log: "npm:^7.0.0"
+ semver: "npm:^7.3.5"
+ tar: "npm:^7.5.4"
+ tinyglobby: "npm:^0.2.12"
+ undici: "npm:^6.25.0"
+ which: "npm:^7.0.0"
+ bin:
+ node-gyp: bin/node-gyp.js
+ checksum: 10c0/e7525c427db2d16aa368b8947187de83083d2a8dda23e3e096a71c22ae637ac5bb8ed7cf6c871f1b9118cd2729dbfee4ff3a4245e2b79226900227b15831b492
+ languageName: node
+ linkType: hard
+
+"nopt@npm:^10.0.0":
+ version: 10.0.1
+ resolution: "nopt@npm:10.0.1"
+ dependencies:
+ abbrev: "npm:^5.0.0"
+ bin:
+ nopt: bin/nopt.js
+ checksum: 10c0/980d89257f9587f3e1f77877ddbf905d6aa3b738ec33e49a4fa1a059a0dd82eb28063982b150654a7ae9de386f2ead60e56172db7d37cf56de545f7392a2a26a
+ languageName: node
+ linkType: hard
+
+"orderedmap@npm:^2.0.0":
+ version: 2.1.1
+ resolution: "orderedmap@npm:2.1.1"
+ checksum: 10c0/8d7d266659d1828937046e8b2a7b5f75914e0391db985da0ca75cd2246cccbf6d6f3a0886aa2034da15ee4923e8c45f95f8b588f575f535f0adecdefccc54634
+ languageName: node
+ linkType: hard
+
"picocolors@npm:^1.1.1":
version: 1.1.1
resolution: "picocolors@npm:1.1.1"
@@ -1311,6 +1835,37 @@ __metadata:
languageName: node
linkType: hard
+"picomatch@npm:^4.0.4":
+ version: 4.0.4
+ resolution: "picomatch@npm:4.0.4"
+ checksum: 10c0/e2c6023372cc7b5764719a5ffb9da0f8e781212fa7ca4bd0562db929df8e117460f00dff3cb7509dacfc06b86de924b247f504d0ce1806a37fac4633081466b0
+ languageName: node
+ linkType: hard
+
+"playwright-core@npm:1.61.1":
+ version: 1.61.1
+ resolution: "playwright-core@npm:1.61.1"
+ bin:
+ playwright-core: cli.js
+ checksum: 10c0/c28896ba82a602182e240ed4f9c467fb0dd9cb57d510f8aba9f25183fe1cde3a0105725a29baffa4157fe885bbb11e228032a2aad0424111584fde45303f07bd
+ languageName: node
+ linkType: hard
+
+"playwright@npm:1.61.1":
+ version: 1.61.1
+ resolution: "playwright@npm:1.61.1"
+ dependencies:
+ fsevents: "npm:2.3.2"
+ playwright-core: "npm:1.61.1"
+ dependenciesMeta:
+ fsevents:
+ optional: true
+ bin:
+ playwright: cli.js
+ checksum: 10c0/cea1bc4d2a64ec3ef683891606774029da7b8a8036ed98332565cec2f8e89549ca1fab9a89fbfba4e08e27e0d7e7d148031e965af66d5ff97bb3c34058cfcad0
+ languageName: node
+ linkType: hard
+
"postcss@npm:^8.4.41":
version: 8.5.6
resolution: "postcss@npm:8.5.6"
@@ -1389,6 +1944,152 @@ __metadata:
languageName: node
linkType: hard
+"proc-log@npm:^7.0.0":
+ version: 7.0.0
+ resolution: "proc-log@npm:7.0.0"
+ checksum: 10c0/b89c2d862604f35fec795477b0c7e376feab3ba0d4f4d291c4e959567442697cf451ac557d0623c1cc38af45a78128b983410f397a10c5d3a67f76c33de4754b
+ languageName: node
+ linkType: hard
+
+"prosemirror-changeset@npm:^2.4.1":
+ version: 2.4.1
+ resolution: "prosemirror-changeset@npm:2.4.1"
+ dependencies:
+ prosemirror-transform: "npm:^1.0.0"
+ checksum: 10c0/ca6b6bab73809dbb8554cddc0a3af666db8bcfed57938314ce6b04b28e41ea355441eaadabcddd9c785727219ad3779534dfa6e33521f0d333e90788b8dc9e3c
+ languageName: node
+ linkType: hard
+
+"prosemirror-commands@npm:^1.7.1":
+ version: 1.7.1
+ resolution: "prosemirror-commands@npm:1.7.1"
+ dependencies:
+ prosemirror-model: "npm:^1.0.0"
+ prosemirror-state: "npm:^1.0.0"
+ prosemirror-transform: "npm:^1.10.2"
+ checksum: 10c0/4884ea7a66b79b51e72bb2ef358284d70e9a071deb4cbfab3dd8ee3449e9a0e34cb391d92f487c013d3716b823fc5568ad5e409a9444b3630ae0b87617c2fca1
+ languageName: node
+ linkType: hard
+
+"prosemirror-dropcursor@npm:^1.8.2":
+ version: 1.8.3
+ resolution: "prosemirror-dropcursor@npm:1.8.3"
+ dependencies:
+ prosemirror-state: "npm:^1.0.0"
+ prosemirror-transform: "npm:^1.1.0"
+ prosemirror-view: "npm:^1.1.0"
+ checksum: 10c0/123381f83a1843ee97d98143bde070cef727dbd20228f63bf7ebf30446d4352baf6982702de9907b27b7958a33d46c8c3d1a274331862c8d7a5912c838a6abce
+ languageName: node
+ linkType: hard
+
+"prosemirror-gapcursor@npm:^1.4.1":
+ version: 1.4.1
+ resolution: "prosemirror-gapcursor@npm:1.4.1"
+ dependencies:
+ prosemirror-keymap: "npm:^1.0.0"
+ prosemirror-model: "npm:^1.0.0"
+ prosemirror-state: "npm:^1.0.0"
+ prosemirror-view: "npm:^1.0.0"
+ checksum: 10c0/1719ef91274d0f76f6c5e667d839aae0576e38b1fc08e178425fa076821ef3da88ebd0fe40049d7dd157aa224672270e569f032316de9de588a1baabcfe802f1
+ languageName: node
+ linkType: hard
+
+"prosemirror-history@npm:^1.5.0":
+ version: 1.5.0
+ resolution: "prosemirror-history@npm:1.5.0"
+ dependencies:
+ prosemirror-state: "npm:^1.2.2"
+ prosemirror-transform: "npm:^1.0.0"
+ prosemirror-view: "npm:^1.31.0"
+ rope-sequence: "npm:^1.3.0"
+ checksum: 10c0/9f24c99316c30a52ff40ddd59fc83b5180f1326ee6f466bfa82b847a503b0cdff234c35d5e3decb39ae1382a189ceec7462333ed7d6c34e2c95921892bb98b78
+ languageName: node
+ linkType: hard
+
+"prosemirror-inputrules@npm:^1.5.1":
+ version: 1.5.1
+ resolution: "prosemirror-inputrules@npm:1.5.1"
+ dependencies:
+ prosemirror-state: "npm:^1.0.0"
+ prosemirror-transform: "npm:^1.0.0"
+ checksum: 10c0/cff1ff9f7e726bf324e6cddd2c48984e6b2970cc98cd5dc59e6dbe2e9df0e01e4a2d100c77c721067804170b9607769c0fee7c98f2cfb4f3cff9af919fce31b9
+ languageName: node
+ linkType: hard
+
+"prosemirror-keymap@npm:^1.0.0, prosemirror-keymap@npm:^1.2.3":
+ version: 1.2.3
+ resolution: "prosemirror-keymap@npm:1.2.3"
+ dependencies:
+ prosemirror-state: "npm:^1.0.0"
+ w3c-keyname: "npm:^2.2.0"
+ checksum: 10c0/0ec2f8bd9b608d0e6a0cdab1d66f9a6b41edcff0239b32ccca1018a0733e52448e4758218a2d472fb8c33c1609426dc6bad4944b28c1c3d509a83201a23035e9
+ languageName: node
+ linkType: hard
+
+"prosemirror-model@npm:^1.0.0, prosemirror-model@npm:^1.21.0, prosemirror-model@npm:^1.25.4, prosemirror-model@npm:^1.25.8, prosemirror-model@npm:^1.25.9":
+ version: 1.25.11
+ resolution: "prosemirror-model@npm:1.25.11"
+ dependencies:
+ orderedmap: "npm:^2.0.0"
+ checksum: 10c0/e41faa477414160a602289b778189163b39a4563799defea1862183087f9ec77f02c292e3e79dd32fcc1a8878aedbdcea50f0f752ee03fe64c24ce6780b82205
+ languageName: node
+ linkType: hard
+
+"prosemirror-schema-list@npm:^1.5.1":
+ version: 1.5.1
+ resolution: "prosemirror-schema-list@npm:1.5.1"
+ dependencies:
+ prosemirror-model: "npm:^1.0.0"
+ prosemirror-state: "npm:^1.0.0"
+ prosemirror-transform: "npm:^1.7.3"
+ checksum: 10c0/e6fd27446bc90556a9797f6ca0cb54e7db53cc7c20fbf633b7d0f4709c45accfa2f3a0f6575fe47aa83cb75781a9b773198d236a44db9d8eef2802a1501e4301
+ languageName: node
+ linkType: hard
+
+"prosemirror-state@npm:^1.0.0, prosemirror-state@npm:^1.2.2, prosemirror-state@npm:^1.4.4":
+ version: 1.4.4
+ resolution: "prosemirror-state@npm:1.4.4"
+ dependencies:
+ prosemirror-model: "npm:^1.0.0"
+ prosemirror-transform: "npm:^1.0.0"
+ prosemirror-view: "npm:^1.27.0"
+ checksum: 10c0/1428636a37c127afe0d11a4f4eb44d75a7f16717940405082bd16fa4c28cfeef9375d49f48e411e5f0fa26f3e5798af7f270edc9bc9b0e647df8bb79983bcd59
+ languageName: node
+ linkType: hard
+
+"prosemirror-tables@npm:^1.8.5":
+ version: 1.8.5
+ resolution: "prosemirror-tables@npm:1.8.5"
+ dependencies:
+ prosemirror-keymap: "npm:^1.2.3"
+ prosemirror-model: "npm:^1.25.4"
+ prosemirror-state: "npm:^1.4.4"
+ prosemirror-transform: "npm:^1.10.5"
+ prosemirror-view: "npm:^1.41.4"
+ checksum: 10c0/d6e8cd9d333987ce6492202d5f815b268bb9a722d81456628eff999444d44fa97e8a801a5a20a44e678892f37053f314885d5a03e1df834653e3ea455f0f9290
+ languageName: node
+ linkType: hard
+
+"prosemirror-transform@npm:^1.0.0, prosemirror-transform@npm:^1.1.0, prosemirror-transform@npm:^1.10.2, prosemirror-transform@npm:^1.10.5, prosemirror-transform@npm:^1.12.0, prosemirror-transform@npm:^1.7.3":
+ version: 1.12.0
+ resolution: "prosemirror-transform@npm:1.12.0"
+ dependencies:
+ prosemirror-model: "npm:^1.21.0"
+ checksum: 10c0/e9e94fbb36fa53badc73d27833bac834835246f28a6176fb33c65a85c9025da0c08ac503f860898591a14fcacef58ba18a29e8df5a916cf376084bc17f324753
+ languageName: node
+ linkType: hard
+
+"prosemirror-view@npm:^1.0.0, prosemirror-view@npm:^1.1.0, prosemirror-view@npm:^1.27.0, prosemirror-view@npm:^1.31.0, prosemirror-view@npm:^1.41.4, prosemirror-view@npm:^1.41.9":
+ version: 1.42.1
+ resolution: "prosemirror-view@npm:1.42.1"
+ dependencies:
+ prosemirror-model: "npm:^1.25.8"
+ prosemirror-state: "npm:^1.0.0"
+ prosemirror-transform: "npm:^1.1.0"
+ checksum: 10c0/159131ef97b274fa9604dae5e7089dc9046bf536ef4aa561698582ea305413fe758a717fc22bad3dfb0c1781a3e986ef910c0f84fb018d92c61d549a7abd9d97
+ languageName: node
+ linkType: hard
+
"react-dom@npm:^19.2.0":
version: 19.2.0
resolution: "react-dom@npm:19.2.0"
@@ -1430,6 +2131,13 @@ __metadata:
languageName: node
linkType: hard
+"rope-sequence@npm:^1.3.0":
+ version: 1.3.4
+ resolution: "rope-sequence@npm:1.3.4"
+ checksum: 10c0/caa90be3d7a7cad155fb354a4679a1280dc9819c81bd319542a0d893a64e152284abb9cc1631d4351b328016a8d6c35a48c912234edfaf5173daef44b2e3609b
+ languageName: node
+ linkType: hard
+
"scheduler@npm:^0.27.0":
version: 0.27.0
resolution: "scheduler@npm:0.27.0"
@@ -1437,6 +2145,15 @@ __metadata:
languageName: node
linkType: hard
+"semver@npm:^7.3.5":
+ version: 7.8.5
+ resolution: "semver@npm:7.8.5"
+ bin:
+ semver: bin/semver.js
+ checksum: 10c0/b1f3127a5be8125a94f37188b361c212466c292c6910adce3ec106cff5dc211ccaedc4739c11bb70fda59d6fc1f040a9bca289f4e093451521a2372e5231fe0c
+ languageName: node
+ linkType: hard
+
"set-cookie-parser@npm:^2.6.0":
version: 2.7.2
resolution: "set-cookie-parser@npm:2.7.2"
@@ -1479,6 +2196,29 @@ __metadata:
languageName: node
linkType: hard
+"tar@npm:^7.5.4":
+ version: 7.5.19
+ resolution: "tar@npm:7.5.19"
+ dependencies:
+ "@isaacs/fs-minipass": "npm:^4.0.0"
+ chownr: "npm:^3.0.0"
+ minipass: "npm:^7.1.2"
+ minizlib: "npm:^3.1.0"
+ yallist: "npm:^5.0.0"
+ checksum: 10c0/7022e8cb04a8ceccc0689f2c731743fa2aab2e3c3f559f7dbc37b65ef7d5913049b427284eded2ec0765c5db5ff72dd7939fe2ae15785ff422cef2116c95d798
+ languageName: node
+ linkType: hard
+
+"tinyglobby@npm:^0.2.12":
+ version: 0.2.17
+ resolution: "tinyglobby@npm:0.2.17"
+ dependencies:
+ fdir: "npm:^6.5.0"
+ picomatch: "npm:^4.0.4"
+ checksum: 10c0/7f7bb0f197c88bc4b20c231e0deca4240ca3bf313a88f5a7fee93a872b84966a4d50220947c0455ad07a60b3b360961c5b7fd979222aeb716a9f99b412002e4c
+ languageName: node
+ linkType: hard
+
"tinypool@npm:^1.1.1":
version: 1.1.1
resolution: "tinypool@npm:1.1.1"
@@ -1527,10 +2267,27 @@ __metadata:
languageName: node
linkType: hard
+"undici@npm:^6.25.0":
+ version: 6.27.0
+ resolution: "undici@npm:6.27.0"
+ checksum: 10c0/f88c3dae3957dbf9d93cb481440aced317bd3c4941b5914fea5efba516d51138988cdb5c76006f0bb1337e41d56c3443351055d492e73af2428521c37ba2a76f
+ languageName: node
+ linkType: hard
+
+"use-sync-external-store@npm:^1.4.0":
+ version: 1.6.0
+ resolution: "use-sync-external-store@npm:1.6.0"
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ checksum: 10c0/35e1179f872a53227bdf8a827f7911da4c37c0f4091c29b76b1e32473d1670ebe7bcd880b808b7549ba9a5605c233350f800ffab963ee4a4ee346ee983b6019b
+ languageName: node
+ linkType: hard
+
"vortex-experimental-mockups@workspace:.":
version: 0.0.0-use.local
resolution: "vortex-experimental-mockups@workspace:."
dependencies:
+ "@playwright/test": "npm:^1.61.1"
"@polkadot/keyring": "npm:^14.0.1"
"@polkadot/util": "npm:^14.0.1"
"@polkadot/util-crypto": "npm:^14.0.1"
@@ -1539,6 +2296,12 @@ __metadata:
"@rsbuild/plugin-react": "npm:^1.4.2"
"@rstest/core": "npm:^0.7.9"
"@tailwindcss/postcss": "npm:^4.1.17"
+ "@tiptap/core": "npm:^3.28.0"
+ "@tiptap/extension-link": "npm:^3.28.0"
+ "@tiptap/markdown": "npm:^3.28.0"
+ "@tiptap/pm": "npm:^3.28.0"
+ "@tiptap/react": "npm:^3.28.0"
+ "@tiptap/starter-kit": "npm:^3.28.0"
"@types/react": "npm:^19.2.5"
"@types/react-dom": "npm:^19.2.3"
class-variance-authority: "npm:^0.7.1"
@@ -1557,6 +2320,31 @@ __metadata:
languageName: unknown
linkType: soft
+"w3c-keyname@npm:^2.2.0":
+ version: 2.2.8
+ resolution: "w3c-keyname@npm:2.2.8"
+ checksum: 10c0/37cf335c90efff31672ebb345577d681e2177f7ff9006a9ad47c68c5a9d265ba4a7b39d6c2599ceea639ca9315584ce4bd9c9fbf7a7217bfb7a599e71943c4c4
+ languageName: node
+ linkType: hard
+
+"which@npm:^7.0.0":
+ version: 7.0.0
+ resolution: "which@npm:7.0.0"
+ dependencies:
+ isexe: "npm:^4.0.0"
+ bin:
+ node-which: bin/which.js
+ checksum: 10c0/ca0b54f198f78bbc4b7c02e34bda8d335cb352e0adb4cbca1c37b1a957af3a879a82c4c27ca6525bc942f548d8b64f816ef6528360af9f3de55ffb9b979b620d
+ languageName: node
+ linkType: hard
+
+"yallist@npm:^5.0.0":
+ version: 5.0.0
+ resolution: "yallist@npm:5.0.0"
+ checksum: 10c0/a499c81ce6d4a1d260d4ea0f6d49ab4da09681e32c3f0472dee16667ed69d01dae63a3b81745a24bd78476ec4fcf856114cb4896ace738e01da34b2c42235416
+ languageName: node
+ linkType: hard
+
"zod@npm:^4.2.1":
version: 4.2.1
resolution: "zod@npm:4.2.1"