diff --git a/docs/ACTION_EFFECT_AUTOMATION_LAYER.md b/docs/ACTION_EFFECT_AUTOMATION_LAYER.md index e28166f..aae185d 100644 --- a/docs/ACTION_EFFECT_AUTOMATION_LAYER.md +++ b/docs/ACTION_EFFECT_AUTOMATION_LAYER.md @@ -29,6 +29,10 @@ of module capabilities. ## Action Definition An `ActionDefinition` describes something a human or system actor can request. +The versioned runtime DTOs and provider protocol live in +`govoplan_core.core.automation`; domain modules implement the protocol and +Workflow resolves providers through module capabilities rather than importing +their implementations. Recommended fields: @@ -110,6 +114,11 @@ Automation should use explicit failure states: These states should be visible in workflow, task, and admin diagnostics. +The contract names these states explicitly as `ActionExecutionState`, alongside +`pending`, `running`, and `completed`. A provider returns observed effects even +for partial failures; the runner, not the provider, owns durable attempts, +recovery decisions, and workflow advancement. + ## Boundary Core may own stable DTOs, registry contracts, and generic audit/event hooks. diff --git a/docs/DEPLOYMENT_OPERATOR_GUIDE.md b/docs/DEPLOYMENT_OPERATOR_GUIDE.md index 7c00222..674ee19 100644 --- a/docs/DEPLOYMENT_OPERATOR_GUIDE.md +++ b/docs/DEPLOYMENT_OPERATOR_GUIDE.md @@ -189,6 +189,11 @@ python -m celery -A govoplan_core.celery_app:celery beat --loglevel INFO | `FILE_STORAGE_S3_ACCESS_KEY_ID` | empty | Secret; inject through deployment environment. | | `FILE_STORAGE_S3_SECRET_ACCESS_KEY` | empty | Secret; inject through deployment environment. | | `FILE_STORAGE_S3_BUCKET` | `files` | Managed-file object bucket. | +| `FILE_STORAGE_S3_DEPLOYMENT_MANAGED` | `false` | Reserved for the installer-owned `http://garage:3900` service. It does not authorize arbitrary internal or external S3 endpoints. | +| `FILE_ARCHIVE_MAX_ENTRIES` | `10000` | Maximum declared entries accepted during archive preview and confirmation. | +| `FILE_ARCHIVE_MAX_EXPANDED_BYTES` | `2147483648` | Maximum actual expanded bytes across selected archive content. | +| `FILE_ARCHIVE_MAX_EXPANSION_RATIO` | `100` | Maximum expanded-to-compressed archive ratio. | +| `FILE_ARCHIVE_PREVIEW_TTL_SECONDS` | `1800` | Lifetime of the sealed actor/archive/destination-bound preview token. | Legacy `S3_*` settings remain for older storage paths but new deployments should prefer `FILE_STORAGE_*`. diff --git a/docs/UI_UX_DECISION_LEDGER.md b/docs/UI_UX_DECISION_LEDGER.md index 8cf44c9..a8dda61 100644 --- a/docs/UI_UX_DECISION_LEDGER.md +++ b/docs/UI_UX_DECISION_LEDGER.md @@ -245,6 +245,25 @@ instead of reproducing their behavior. - Feedback and confirmation use `Dialog`, `ConfirmDialog`, or `DismissibleAlert`. They never fall back to `window.alert`. +### DUE-012: Rich HTML Editing Contract + +Decision: modules that edit persisted HTML use the central +`WysiwygEditor` exported from `@govoplan/core-webui/wysiwyg`. + +- The dedicated subpath is intentional: the editor and its engine remain a + shared Core contract without adding their code to module combinations that + never consume rich-text editing. +- Consumers provide controlled HTML and domain-specific token labels. The + editor owns visual/source switching, formatting, links, images, safe URL + handling, and atomic inline token rendering; it does not own template + semantics or persistence. +- Existing HTML outside the supported visual subset opens in source mode. + Rendering the value must not rewrite it, and users receive an explicit + warning before choosing the visual surface. +- Domain placeholders remain their original serialized text. Atomic token + presentation is an editing aid only, so backend renderers and existing + templates do not need a new storage format. + #### FieldLabel Omission Register Every Core field surface that intentionally does not render `FieldLabel` is diff --git a/src/govoplan_core/core/automation.py b/src/govoplan_core/core/automation.py index 21a14a1..02de25b 100644 --- a/src/govoplan_core/core/automation.py +++ b/src/govoplan_core/core/automation.py @@ -21,6 +21,172 @@ AutomationInvocationKind = Literal[ ] AutomationSubjectKind = Literal["delegated_user", "service_account"] AUTOMATION_PRINCIPAL_CONTRACT_VERSION = "1" +ACTION_EFFECT_CONTRACT_VERSION = "1" + +ActionRiskLevel = Literal["low", "moderate", "high", "critical"] +ActionReversibility = Literal[ + "reversible", + "compensatable", + "corrective_only", + "irreversible", +] +ActionExecutionState = Literal[ + "pending", + "running", + "completed", + "blocked", + "retryable", + "quarantined", + "manual_required", + "compensation_required", +] +EffectOperation = Literal[ + "created", + "changed", + "deleted", + "sent", + "notified", + "locked", + "retained", + "external", +] + + +@dataclass(frozen=True, slots=True) +class EffectDefinition: + effect_key: str + owner_module: str + operation: EffectOperation + description: str + resource_types: tuple[str, ...] = () + visibility_classification: str = "internal" + audit_event_types: tuple[str, ...] = () + compensation_hint: str | None = None + contract_version: str = ACTION_EFFECT_CONTRACT_VERSION + + def __post_init__(self) -> None: + _validate_contract_version(self.contract_version) + _require_text(self.effect_key, "Effect key") + _require_text(self.owner_module, "Effect owner module") + _require_text(self.description, "Effect description") + + +@dataclass(frozen=True, slots=True) +class ActionDefinition: + action_key: str + owner_module: str + description: str + input_schema_ref: str + required_scopes: tuple[str, ...] = () + required_capabilities: tuple[str, ...] = () + policy_checks: tuple[str, ...] = () + risk_level: ActionRiskLevel = "moderate" + reversibility: ActionReversibility = "corrective_only" + expected_effect_keys: tuple[str, ...] = () + idempotency_strategy: str = "caller_supplied" + audit_event_types: tuple[str, ...] = () + preview_required: bool = True + contract_version: str = ACTION_EFFECT_CONTRACT_VERSION + + def __post_init__(self) -> None: + _validate_contract_version(self.contract_version) + _require_text(self.action_key, "Action key") + _require_text(self.owner_module, "Action owner module") + _require_text(self.description, "Action description") + _require_text(self.input_schema_ref, "Action input schema reference") + _require_text(self.idempotency_strategy, "Action idempotency strategy") + if any(not value.strip() for value in self.required_scopes): + raise ValueError("Action scopes must not be empty") + if any(not value.strip() for value in self.required_capabilities): + raise ValueError("Action capabilities must not be empty") + if any(not value.strip() for value in self.expected_effect_keys): + raise ValueError("Expected effect keys must not be empty") + + +@dataclass(frozen=True, slots=True) +class ActionExecutionRequest: + tenant_id: str + action_key: str + input: Mapping[str, object] + idempotency_key: str + invocation: AutomationInvocation + actor_ref: str | None = None + preview_ref: str | None = None + metadata: Mapping[str, object] = field(default_factory=dict) + contract_version: str = ACTION_EFFECT_CONTRACT_VERSION + + def __post_init__(self) -> None: + _validate_contract_version(self.contract_version) + _require_text(self.tenant_id, "Action tenant id") + _require_text(self.action_key, "Action key") + _require_text(self.idempotency_key, "Action idempotency key") + + +@dataclass(frozen=True, slots=True) +class EffectPreview: + effect_key: str + summary: str + resource_refs: tuple[str, ...] = () + external_system_refs: tuple[str, ...] = () + + +@dataclass(frozen=True, slots=True) +class ActionPreview: + action_key: str + allowed: bool + summary: str + risk_level: ActionRiskLevel + reversibility: ActionReversibility + effects: tuple[EffectPreview, ...] = () + blockers: tuple[str, ...] = () + policy_provenance: tuple[Mapping[str, object], ...] = () + preview_ref: str | None = None + + +@dataclass(frozen=True, slots=True) +class ObservedEffect: + effect_key: str + operation: EffectOperation + resource_ref: str | None = None + external_system_ref: str | None = None + audit_event_ref: str | None = None + summary: str | None = None + metadata: Mapping[str, object] = field(default_factory=dict) + + +@dataclass(frozen=True, slots=True) +class ActionExecutionResult: + state: ActionExecutionState + output: Mapping[str, object] = field(default_factory=dict) + observed_effects: tuple[ObservedEffect, ...] = () + error: str | None = None + retry_after: datetime | None = None + manual_instructions: str | None = None + compensation_action_key: str | None = None + audit_event_refs: tuple[str, ...] = () + + +@runtime_checkable +class ActionEffectProvider(Protocol): + def action_definitions(self) -> tuple[ActionDefinition, ...]: ... + + def effect_definitions(self) -> tuple[EffectDefinition, ...]: ... + + def preview_action( + self, + session: object, + principal: object, + *, + request: ActionExecutionRequest, + ) -> ActionPreview: ... + + def execute_action( + self, + session: object, + principal: object, + *, + request: ActionExecutionRequest, + ) -> ActionExecutionResult: ... @dataclass(frozen=True, slots=True) @@ -170,14 +336,58 @@ def automation_principal_provider( ) +def action_effect_provider( + registry: object | None, + capability_name: str, +) -> ActionEffectProvider | None: + name = capability_name.strip() + if ( + not name + or registry is None + or not hasattr(registry, "has_capability") + or not registry.has_capability(name) + ): + return None + capability = registry.capability(name) + return ( + capability + if isinstance(capability, ActionEffectProvider) + else None + ) + + +def _validate_contract_version(value: str) -> None: + if value != ACTION_EFFECT_CONTRACT_VERSION: + raise ValueError("Unsupported action/effect contract version") + + +def _require_text(value: str, label: str) -> None: + if not value.strip(): + raise ValueError(f"{label} is required") + + __all__ = [ + "ACTION_EFFECT_CONTRACT_VERSION", "CAPABILITY_AUTH_AUTOMATION_PRINCIPAL_PROVIDER", "AUTOMATION_PRINCIPAL_CONTRACT_VERSION", + "ActionDefinition", + "ActionEffectProvider", + "ActionExecutionRequest", + "ActionExecutionResult", + "ActionExecutionState", "AutomationInvocation", "AutomationInvocationKind", "AutomationPrincipalProvider", "AutomationPrincipalRequest", "AutomationPrincipalResolution", "AutomationSubjectKind", + "ActionPreview", + "ActionReversibility", + "ActionRiskLevel", + "EffectDefinition", + "EffectOperation", + "EffectPreview", + "ObservedEffect", + "action_effect_provider", "automation_principal_provider", ] diff --git a/src/govoplan_core/core/configuration_safety.py b/src/govoplan_core/core/configuration_safety.py index 58bcf02..78d8dd8 100644 --- a/src/govoplan_core/core/configuration_safety.py +++ b/src/govoplan_core/core/configuration_safety.py @@ -301,6 +301,18 @@ _CONFIGURATION_FIELD_SAFETY: tuple[ConfigurationFieldSafety, ...] = ( maintenance_required=True, notes="This deployment-wide egress boundary remains out of band and applies to every connector worker.", ), + ConfigurationFieldSafety( + key="FILE_STORAGE_S3_DEPLOYMENT_MANAGED", + label="Installer-managed Garage trust", + owner_module="files", + scope="system", + storage="environment", + ui_managed=False, + risk="high", + secret_handling="env_only", # noqa: S106 # nosec B106 - policy vocabulary. + maintenance_required=True, + notes="Reserved for the exact installer-owned http://garage:3900 service; it must never authorize another S3 endpoint.", + ), ConfigurationFieldSafety( key="GOVOPLAN_CONNECTOR_MAX_STRUCTURED_RESPONSE_BYTES", label="Structured connector response limit", diff --git a/src/govoplan_core/core/install_config.py b/src/govoplan_core/core/install_config.py index 7577c10..bac9ea8 100644 --- a/src/govoplan_core/core/install_config.py +++ b/src/govoplan_core/core/install_config.py @@ -239,10 +239,30 @@ def _validate_cors_settings(env: Mapping[str, str], runtime: _RuntimeProfile, co def _validate_file_storage_settings(env: Mapping[str, str], runtime: _RuntimeProfile, collector: _ConfigIssueCollector) -> None: storage_backend = _clean(env.get("FILE_STORAGE_BACKEND")) or "local" + deployment_managed_raw = _clean(env.get("FILE_STORAGE_S3_DEPLOYMENT_MANAGED")).lower() + if deployment_managed_raw and deployment_managed_raw not in {"true", "false", "1", "0", "yes", "no", "on", "off"}: + collector.add( + "error", + "FILE_STORAGE_S3_DEPLOYMENT_MANAGED", + "Managed S3 trust must be an explicit boolean.", + "Set FILE_STORAGE_S3_DEPLOYMENT_MANAGED=false, or let the supported installer manage Garage.", + ) + deployment_managed = _truthy(deployment_managed_raw) if storage_backend == "local": + if deployment_managed: + collector.add( + "error", + "FILE_STORAGE_S3_DEPLOYMENT_MANAGED", + "Managed S3 trust cannot be enabled for local file storage.", + "Set FILE_STORAGE_S3_DEPLOYMENT_MANAGED=false.", + ) _validate_local_file_storage(env, runtime, collector) elif storage_backend == "s3": - _validate_s3_file_storage(env, collector) + _validate_s3_file_storage( + env, + collector, + deployment_managed=deployment_managed, + ) else: collector.add("error", "FILE_STORAGE_BACKEND", f"Unsupported FILE_STORAGE_BACKEND={storage_backend!r}.", "Use `local` or `s3`.") @@ -254,10 +274,25 @@ def _validate_local_file_storage(env: Mapping[str, str], runtime: _RuntimeProfil collector.add("warning", "FILE_STORAGE_BACKEND", "Production is configured for local file storage.", "Confirm the path is durable and backed up, or use object storage once the deployment needs independent file scaling.") -def _validate_s3_file_storage(env: Mapping[str, str], collector: _ConfigIssueCollector) -> None: +def _validate_s3_file_storage( + env: Mapping[str, str], + collector: _ConfigIssueCollector, + *, + deployment_managed: bool, +) -> None: for key in ("FILE_STORAGE_S3_ENDPOINT_URL", "FILE_STORAGE_S3_REGION", "FILE_STORAGE_S3_ACCESS_KEY_ID", "FILE_STORAGE_S3_SECRET_ACCESS_KEY", "FILE_STORAGE_S3_BUCKET"): if not _clean(env.get(key)): collector.add("error", key, f"{key} is required when FILE_STORAGE_BACKEND=s3.", "Configure all FILE_STORAGE_S3_* settings through deployment secrets.") + if ( + deployment_managed + and _clean(env.get("FILE_STORAGE_S3_ENDPOINT_URL")) != "http://garage:3900" + ): + collector.add( + "error", + "FILE_STORAGE_S3_ENDPOINT_URL", + "Installer-managed S3 trust is restricted to http://garage:3900.", + "Use the exact managed Garage endpoint or disable FILE_STORAGE_S3_DEPLOYMENT_MANAGED.", + ) def _validate_outbound_connector_policy( @@ -373,6 +408,11 @@ AUTH_COOKIE_DOMAIN= FILE_STORAGE_BACKEND=local FILE_STORAGE_LOCAL_ROOT=/var/lib/govoplan/files FILE_STORAGE_LOCAL_FALLBACK_ROOTS= +FILE_STORAGE_S3_DEPLOYMENT_MANAGED=false +FILE_ARCHIVE_MAX_ENTRIES=10000 +FILE_ARCHIVE_MAX_EXPANDED_BYTES=2147483648 +FILE_ARCHIVE_MAX_EXPANSION_RATIO=100 +FILE_ARCHIVE_PREVIEW_TTL_SECONDS=1800 DEV_AUTO_MIGRATE_ENABLED=false DEV_BOOTSTRAP_ENABLED=false @@ -433,6 +473,11 @@ FORWARDED_ALLOW_IPS=127.0.0.1 AUTH_COOKIE_SECURE=false FILE_STORAGE_BACKEND=local FILE_STORAGE_LOCAL_ROOT=runtime/production-like/files +FILE_STORAGE_S3_DEPLOYMENT_MANAGED=false +FILE_ARCHIVE_MAX_ENTRIES=10000 +FILE_ARCHIVE_MAX_EXPANDED_BYTES=2147483648 +FILE_ARCHIVE_MAX_EXPANSION_RATIO=100 +FILE_ARCHIVE_PREVIEW_TTL_SECONDS=1800 DEV_AUTO_MIGRATE_ENABLED=false DEV_BOOTSTRAP_ENABLED=true """ diff --git a/src/govoplan_core/core/views.py b/src/govoplan_core/core/views.py index 1bcf25a..fbb0cdb 100644 --- a/src/govoplan_core/core/views.py +++ b/src/govoplan_core/core/views.py @@ -56,9 +56,22 @@ class ViewResolver(Protocol): account_id: str, group_ids: Iterable[str] = (), workflow_view_id: str | None = None, + workflow_revision_id: str | None = None, + workflow_surface_ids: Iterable[str] = (), ) -> EffectiveView: ... +def views_resolver(registry: object | None) -> ViewResolver | None: + if ( + registry is None + or not hasattr(registry, "has_capability") + or not registry.has_capability(CAPABILITY_VIEWS_RESOLVER) + ): + return None + capability = registry.capability(CAPABILITY_VIEWS_RESOLVER) + return capability if isinstance(capability, ViewResolver) else None + + def module_view_surface_id(module_id: str) -> str: return f"{module_id}.module" @@ -92,4 +105,5 @@ __all__ = [ "navigation_view_surface_id", "route_view_surface_id", "validate_view_surface_id", + "views_resolver", ] diff --git a/src/govoplan_core/security/secrets.py b/src/govoplan_core/security/secrets.py index 912b5da..e3f6d7a 100644 --- a/src/govoplan_core/security/secrets.py +++ b/src/govoplan_core/security/secrets.py @@ -2,8 +2,9 @@ from __future__ import annotations import base64 import hashlib +import json from functools import lru_cache -from typing import Protocol, runtime_checkable +from typing import Any, Mapping, Protocol, runtime_checkable from cryptography.fernet import Fernet, InvalidToken @@ -18,6 +19,10 @@ class SecretDecryptionError(RuntimeError): pass +class TransientPayloadError(RuntimeError): + pass + + CAPABILITY_SECURITY_SECRET_PROVIDER = "security.secretProvider" # noqa: S105 # nosec B105 - capability identifier. @@ -72,3 +77,29 @@ def decrypt_secret(value: str | None) -> str | None: return _fernet().decrypt(value.encode("utf-8")).decode("utf-8") except InvalidToken as exc: raise SecretDecryptionError("Stored secret cannot be decrypted with the configured master key") from exc + + +def seal_transient_payload(payload: Mapping[str, Any]) -> str: + """Seal a short-lived JSON object without persisting server-side state.""" + + encoded = json.dumps( + dict(payload), + separators=(",", ":"), + sort_keys=True, + ).encode("utf-8") + return _fernet().encrypt(encoded).decode("utf-8") + + +def open_transient_payload(token: str, *, ttl_seconds: int) -> dict[str, Any]: + """Open a sealed JSON object and enforce its maximum age.""" + + if ttl_seconds <= 0: + raise ValueError("Transient payload TTL must be positive") + try: + encoded = _fernet().decrypt(token.encode("utf-8"), ttl=ttl_seconds) + payload = json.loads(encoded.decode("utf-8")) + except (InvalidToken, UnicodeDecodeError, json.JSONDecodeError, TypeError, ValueError) as exc: + raise TransientPayloadError("Transient payload is invalid or expired") from exc + if not isinstance(payload, dict): + raise TransientPayloadError("Transient payload must contain a JSON object") + return payload diff --git a/src/govoplan_core/settings.py b/src/govoplan_core/settings.py index 3b473ca..da02f57 100644 --- a/src/govoplan_core/settings.py +++ b/src/govoplan_core/settings.py @@ -74,8 +74,28 @@ class Settings(BaseSettings): file_storage_s3_access_key_id: str | None = Field(default=None, alias="FILE_STORAGE_S3_ACCESS_KEY_ID") file_storage_s3_secret_access_key: str | None = Field(default=None, alias="FILE_STORAGE_S3_SECRET_ACCESS_KEY") file_storage_s3_bucket: str | None = Field(default="files", alias="FILE_STORAGE_S3_BUCKET") + file_storage_s3_deployment_managed: bool = Field( + default=False, + alias="FILE_STORAGE_S3_DEPLOYMENT_MANAGED", + ) file_upload_max_bytes: int = Field(default=50 * 1024 * 1024, alias="FILE_UPLOAD_MAX_BYTES") file_upload_zip_max_bytes: int = Field(default=250 * 1024 * 1024, alias="FILE_UPLOAD_ZIP_MAX_BYTES") + file_archive_max_entries: int = Field(default=10_000, ge=1, alias="FILE_ARCHIVE_MAX_ENTRIES") + file_archive_max_expanded_bytes: int = Field( + default=2 * 1024 * 1024 * 1024, + ge=1, + alias="FILE_ARCHIVE_MAX_EXPANDED_BYTES", + ) + file_archive_max_expansion_ratio: int = Field( + default=100, + ge=1, + alias="FILE_ARCHIVE_MAX_EXPANSION_RATIO", + ) + file_archive_preview_ttl_seconds: int = Field( + default=30 * 60, + ge=60, + alias="FILE_ARCHIVE_PREVIEW_TTL_SECONDS", + ) auth_session_cookie_name: str = Field(default="govoplan_session", alias="AUTH_SESSION_COOKIE_NAME") auth_csrf_cookie_name: str = Field(default="govoplan_csrf", alias="AUTH_CSRF_COOKIE_NAME") diff --git a/tests/test_automation_contract.py b/tests/test_automation_contract.py index af7eff2..a60db62 100644 --- a/tests/test_automation_contract.py +++ b/tests/test_automation_contract.py @@ -3,11 +3,20 @@ from __future__ import annotations import unittest from govoplan_core.core.automation import ( + ActionDefinition, + ActionEffectProvider, + ActionExecutionRequest, + ActionExecutionResult, + ActionPreview, CAPABILITY_AUTH_AUTOMATION_PRINCIPAL_PROVIDER, AutomationInvocation, AutomationPrincipalProvider, AutomationPrincipalRequest, AutomationPrincipalResolution, + EffectDefinition, + EffectPreview, + ObservedEffect, + action_effect_provider, automation_principal_provider, ) from govoplan_core.core.modules import ModuleContext, ModuleManifest @@ -24,6 +33,66 @@ class _Provider: ) +class _ActionProvider: + action = ActionDefinition( + action_key="postbox.message.deliver", + owner_module="postbox", + description="Deliver one governed Postbox message.", + input_schema_ref="schema:postbox.message.deliver@1", + required_scopes=("postbox:message:write",), + required_capabilities=("postbox.delivery",), + risk_level="high", + reversibility="compensatable", + expected_effect_keys=("postbox.message.created",), + audit_event_types=("postbox.message.delivered",), + ) + effect = EffectDefinition( + effect_key="postbox.message.created", + owner_module="postbox", + operation="created", + description="A durable Postbox message was created.", + resource_types=("postbox_message",), + ) + + def action_definitions(self): + return (self.action,) + + def effect_definitions(self): + return (self.effect,) + + def preview_action(self, session, principal, *, request): + del session, principal + return ActionPreview( + action_key=request.action_key, + allowed=True, + summary="One Postbox message will be created.", + risk_level=self.action.risk_level, + reversibility=self.action.reversibility, + effects=( + EffectPreview( + effect_key=self.effect.effect_key, + summary="Create message.", + ), + ), + preview_ref="preview:1", + ) + + def execute_action(self, session, principal, *, request): + del session, principal + return ActionExecutionResult( + state="completed", + output={"message_ref": "postbox-message:1"}, + observed_effects=( + ObservedEffect( + effect_key=self.effect.effect_key, + operation="created", + resource_ref="postbox-message:1", + ), + ), + audit_event_refs=("audit-event:1",), + ) + + class AutomationContractTests(unittest.TestCase): def test_principal_request_has_explicit_subject_contracts(self) -> None: service = AutomationPrincipalRequest.service_account( @@ -91,6 +160,81 @@ class AutomationContractTests(unittest.TestCase): self.assertTrue(result.allowed) self.assertEqual(("dataflow:pipeline:run",), result.granted_scopes) + def test_action_effect_provider_is_previewable_and_idempotent_by_contract( + self, + ) -> None: + provider = _ActionProvider() + self.assertIsInstance(provider, ActionEffectProvider) + request = ActionExecutionRequest( + tenant_id="tenant-1", + action_key=provider.action.action_key, + input={"postbox_ref": "postbox:1"}, + idempotency_key="workflow:instance-1:step-2:attempt-1", + invocation=AutomationInvocation( + kind="workflow", + trigger_ref="workflow-instance:1", + ), + ) + + preview = provider.preview_action(object(), object(), request=request) + result = provider.execute_action(object(), object(), request=request) + + self.assertTrue(preview.allowed) + self.assertEqual("compensatable", preview.reversibility) + self.assertEqual("completed", result.state) + self.assertEqual( + "postbox-message:1", + result.observed_effects[0].resource_ref, + ) + + def test_action_effect_provider_is_resolved_by_capability_name(self) -> None: + provider = _ActionProvider() + registry = PlatformRegistry() + registry.register( + ModuleManifest( + id="action_contract_test", + name="Action contract test", + version="test", + capability_factories={ + "postbox.actions": lambda context: provider, + "invalid.actions": lambda context: object(), + }, + ) + ) + registry.configure_capability_context( + ModuleContext(registry=registry, settings=object()) + ) + + self.assertIs( + provider, + action_effect_provider(registry, "postbox.actions"), + ) + self.assertIsNone( + action_effect_provider(registry, "invalid.actions") + ) + self.assertIsNone( + action_effect_provider(registry, "missing.actions") + ) + + def test_action_contract_rejects_unversioned_or_incomplete_definitions( + self, + ) -> None: + with self.assertRaisesRegex(ValueError, "input schema"): + ActionDefinition( + action_key="invalid", + owner_module="test", + description="Invalid action", + input_schema_ref="", + ) + with self.assertRaisesRegex(ValueError, "contract version"): + EffectDefinition( + effect_key="test.effect", + owner_module="test", + operation="changed", + description="Test effect", + contract_version="2", + ) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_install_config.py b/tests/test_install_config.py index 1be03ec..33e0b38 100644 --- a/tests/test_install_config.py +++ b/tests/test_install_config.py @@ -10,6 +10,25 @@ from govoplan_core.settings import Settings class InstallConfigTests(unittest.TestCase): + def test_archive_preview_safety_defaults_are_validated(self) -> None: + defaults = Settings() + self.assertEqual(defaults.file_archive_max_entries, 10_000) + self.assertEqual( + defaults.file_archive_max_expanded_bytes, + 2 * 1024 * 1024 * 1024, + ) + self.assertEqual(defaults.file_archive_max_expansion_ratio, 100) + self.assertEqual(defaults.file_archive_preview_ttl_seconds, 30 * 60) + + for values in ( + {"FILE_ARCHIVE_MAX_ENTRIES": "0"}, + {"FILE_ARCHIVE_MAX_EXPANDED_BYTES": "0"}, + {"FILE_ARCHIVE_MAX_EXPANSION_RATIO": "0"}, + {"FILE_ARCHIVE_PREVIEW_TTL_SECONDS": "59"}, + ): + with self.subTest(values=values), self.assertRaises(ValidationError): + Settings(**values) + def test_calendar_outbox_retention_setting_is_validated(self) -> None: self.assertEqual(Settings().calendar_outbox_terminal_retention_days, 90) self.assertEqual( @@ -128,6 +147,45 @@ class InstallConfigTests(unittest.TestCase): self.assertIn("GOVOPLAN_TRUSTED_HOSTS", error_keys) self.assertIn("FORWARDED_ALLOW_IPS", error_keys) + def test_managed_s3_trust_is_limited_to_installer_garage(self) -> None: + base = { + "APP_ENV": "development", + "FILE_STORAGE_BACKEND": "s3", + "FILE_STORAGE_S3_REGION": "garage", + "FILE_STORAGE_S3_ACCESS_KEY_ID": "access", + "FILE_STORAGE_S3_SECRET_ACCESS_KEY": "secret", + "FILE_STORAGE_S3_BUCKET": "files", + "FILE_STORAGE_S3_DEPLOYMENT_MANAGED": "true", + } + accepted = validate_runtime_configuration( + { + **base, + "FILE_STORAGE_S3_ENDPOINT_URL": "http://garage:3900", + }, + profile="development", + ) + rejected = validate_runtime_configuration( + { + **base, + "FILE_STORAGE_S3_ENDPOINT_URL": "http://other-s3:3900", + }, + profile="development", + ) + + self.assertNotIn( + "FILE_STORAGE_S3_ENDPOINT_URL", + {issue.key for issue in accepted.errors}, + ) + self.assertIn( + "FILE_STORAGE_S3_ENDPOINT_URL", + {issue.key for issue in rejected.errors}, + ) + self.assertTrue( + Settings( + FILE_STORAGE_S3_DEPLOYMENT_MANAGED="true" + ).file_storage_s3_deployment_managed + ) + def test_connector_deployment_allowlists_require_exact_names_and_absolute_paths(self) -> None: result = validate_runtime_configuration( { @@ -156,6 +214,7 @@ class InstallConfigTests(unittest.TestCase): self.assertIn("GOVOPLAN_TRUSTED_HOSTS=govoplan.example.org", self_hosted) self.assertIn("GOVOPLAN_HTTP_HSTS_SECONDS=31536000", self_hosted) self.assertIn("AUTH_LOGIN_THROTTLE_IDENTITY_LIMIT=10", self_hosted) + self.assertIn("FILE_STORAGE_S3_DEPLOYMENT_MANAGED=false", self_hosted) self.assertIn("GOVOPLAN_INSTALL_PROFILE=production-like", production_like) self.assertNotIn("MASTER_KEY_B64= None: + token = seal_transient_payload( + { + "purpose": "test", + "tenant_id": "tenant-1", + "selected": ["one", "two"], + } + ) + + self.assertEqual( + { + "purpose": "test", + "tenant_id": "tenant-1", + "selected": ["one", "two"], + }, + open_transient_payload(token, ttl_seconds=60), + ) + + def test_tampered_payload_is_rejected(self) -> None: + token = seal_transient_payload({"purpose": "test"}) + replacement = "A" if token[-1] != "A" else "B" + + with self.assertRaisesRegex( + TransientPayloadError, "invalid or expired" + ): + open_transient_payload(f"{token[:-1]}{replacement}", ttl_seconds=60) + + def test_non_positive_ttl_is_rejected(self) -> None: + token = seal_transient_payload({"purpose": "test"}) + + with self.assertRaisesRegex(ValueError, "TTL must be positive"): + open_transient_payload(token, ttl_seconds=0) + + +if __name__ == "__main__": + unittest.main() diff --git a/webui/package-lock.json b/webui/package-lock.json index 993dd17..e0aa541 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -30,26 +30,31 @@ "@govoplan/scheduling-webui": "file:../../govoplan-scheduling/webui", "@govoplan/search-webui": "file:../../govoplan-search/webui", "@govoplan/views-webui": "file:../../govoplan-views/webui", - "@govoplan/workflow-webui": "file:../../govoplan-workflow/webui" + "@govoplan/workflow-webui": "file:../../govoplan-workflow/webui", + "@tiptap/core": "^3.29.2", + "@tiptap/extension-image": "^3.29.2", + "@tiptap/pm": "^3.29.2", + "@tiptap/react": "^3.29.2", + "@tiptap/starter-kit": "^3.29.2" }, "devDependencies": { - "@types/react": "^19.0.2", - "@types/react-dom": "^19.0.2", - "@vitejs/plugin-react": "^4.3.4", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.2.0", "@xyflow/react": "^12.11.2", "lucide-react": "^1.23.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-router-dom": "7.18.2", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-router": "8.3.0", "read-excel-file": "^9.2.0", "typescript": "^5.7.2", - "vite": "^6.0.6" + "vite": "^7.3.6" }, "peerDependencies": { "lucide-react": "^1.23.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-router-dom": ">=7.18.2 <8" + "react": ">=19.2.7 <20", + "react-dom": ">=19.2.7 <20", + "react-router": ">=8.3.0 <9" } }, "../../govoplan-access/webui": { @@ -61,9 +66,9 @@ "peerDependencies": { "@govoplan/core-webui": "^0.1.11", "lucide-react": "^1.23.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-router-dom": ">=7.18.2 <8" + "react": ">=19.2.7 <20", + "react-dom": ">=19.2.7 <20", + "react-router": ">=8.3.0 <9" }, "peerDependenciesMeta": { "@govoplan/core-webui": { @@ -77,9 +82,9 @@ "peerDependencies": { "@govoplan/core-webui": "^0.1.11", "lucide-react": "^1.23.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-router-dom": ">=7.18.2 <8" + "react": ">=19.2.7 <20", + "react-dom": ">=19.2.7 <20", + "react-router": ">=8.3.0 <9" }, "peerDependenciesMeta": { "@govoplan/core-webui": { @@ -96,9 +101,9 @@ "peerDependencies": { "@govoplan/core-webui": "^0.1.9", "lucide-react": "^1.23.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-router-dom": ">=7.18.2 <8" + "react": ">=19.2.7 <20", + "react-dom": ">=19.2.7 <20", + "react-router": ">=8.3.0 <9" }, "peerDependenciesMeta": { "@govoplan/core-webui": { @@ -112,9 +117,9 @@ "peerDependencies": { "@govoplan/core-webui": "^0.1.9", "lucide-react": "^1.23.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-router-dom": ">=7.18.2 <8" + "react": ">=19.2.7 <20", + "react-dom": ">=19.2.7 <20", + "react-router": ">=8.3.0 <9" }, "peerDependenciesMeta": { "@govoplan/core-webui": { @@ -127,13 +132,13 @@ "version": "0.1.8", "peerDependencies": { "@govoplan/core-webui": "^0.1.9", - "@vitejs/plugin-react": "^4.3.4", + "@vitejs/plugin-react": "^5.2.0", "lucide-react": "^1.23.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-router-dom": ">=7.18.2 <8", + "react": ">=19.2.7 <20", + "react-dom": ">=19.2.7 <20", + "react-router": ">=8.3.0 <9", "typescript": "^5.7.2", - "vite": "^6.0.6" + "vite": "^7.3.6" }, "peerDependenciesMeta": { "@govoplan/core-webui": { @@ -151,11 +156,11 @@ "typescript": "^5.7.2" }, "peerDependencies": { - "@govoplan/core-webui": "^0.1.12", + "@govoplan/core-webui": "^0.1.14", "lucide-react": "^1.23.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-router-dom": ">=7.18.2 <8" + "react": ">=19.2.7 <20", + "react-dom": ">=19.2.7 <20", + "react-router": ">=8.3.0 <9" }, "peerDependenciesMeta": { "@govoplan/core-webui": { @@ -169,9 +174,9 @@ "peerDependencies": { "@govoplan/core-webui": "^0.1.8", "lucide-react": "^1.23.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-router-dom": ">=7.18.2 <8" + "react": ">=19.2.7 <20", + "react-dom": ">=19.2.7 <20", + "react-router": ">=8.3.0 <9" }, "peerDependenciesMeta": { "@govoplan/core-webui": { @@ -186,9 +191,9 @@ "@govoplan/core-webui": "^0.1.14", "@xyflow/react": "^12.11.2", "lucide-react": "^1.23.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-router-dom": ">=7.18.2 <8", + "react": ">=19.2.7 <20", + "react-dom": ">=19.2.7 <20", + "react-router": ">=8.3.0 <9", "typescript": "^5.7.2" }, "peerDependenciesMeta": { @@ -203,9 +208,9 @@ "peerDependencies": { "@govoplan/core-webui": "^0.1.14", "lucide-react": "^1.23.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-router-dom": ">=7.18.2 <8", + "react": ">=19.2.7 <20", + "react-dom": ">=19.2.7 <20", + "react-router": ">=8.3.0 <9", "typescript": "^5.7.2" }, "peerDependenciesMeta": { @@ -219,13 +224,13 @@ "version": "0.1.10", "peerDependencies": { "@govoplan/core-webui": "^0.1.10", - "@vitejs/plugin-react": "^4.3.4", + "@vitejs/plugin-react": "^5.2.0", "lucide-react": "^1.23.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-router-dom": ">=7.18.2 <8", + "react": ">=19.2.7 <20", + "react-dom": ">=19.2.7 <20", + "react-router": ">=8.3.0 <9", "typescript": "^5.7.2", - "vite": "^6.0.6" + "vite": "^7.3.6" }, "peerDependenciesMeta": { "@govoplan/core-webui": { @@ -238,13 +243,13 @@ "version": "0.1.9", "peerDependencies": { "@govoplan/core-webui": "^0.1.9", - "@vitejs/plugin-react": "^4.3.4", + "@vitejs/plugin-react": "^5.2.0", "lucide-react": "^1.23.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-router-dom": ">=7.18.2 <8", + "react": ">=19.2.7 <20", + "react-dom": ">=19.2.7 <20", + "react-router": ">=8.3.0 <9", "typescript": "^5.7.2", - "vite": "^6.0.6" + "vite": "^7.3.6" }, "peerDependenciesMeta": { "@govoplan/core-webui": { @@ -257,13 +262,13 @@ "version": "0.1.8", "peerDependencies": { "@govoplan/core-webui": "^0.1.9", - "@vitejs/plugin-react": "^4.3.4", + "@vitejs/plugin-react": "^5.2.0", "lucide-react": "^1.23.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-router-dom": ">=7.18.2 <8", + "react": ">=19.2.7 <20", + "react-dom": ">=19.2.7 <20", + "react-router": ">=8.3.0 <9", "typescript": "^5.7.2", - "vite": "^6.0.6" + "vite": "^7.3.6" }, "peerDependenciesMeta": { "@govoplan/core-webui": { @@ -280,9 +285,9 @@ "peerDependencies": { "@govoplan/core-webui": "^0.1.10", "lucide-react": "^1.23.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-router-dom": ">=7.18.2 <8" + "react": ">=19.2.7 <20", + "react-dom": ">=19.2.7 <20", + "react-router": ">=8.3.0 <9" }, "peerDependenciesMeta": { "@govoplan/core-webui": { @@ -295,13 +300,13 @@ "version": "0.1.8", "peerDependencies": { "@govoplan/core-webui": "^0.1.9", - "@vitejs/plugin-react": "^4.3.4", + "@vitejs/plugin-react": "^5.2.0", "lucide-react": "^1.23.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-router-dom": ">=7.18.2 <8", + "react": ">=19.2.7 <20", + "react-dom": ">=19.2.7 <20", + "react-router": ">=8.3.0 <9", "typescript": "^5.7.2", - "vite": "^6.0.6" + "vite": "^7.3.6" }, "peerDependenciesMeta": { "@govoplan/core-webui": { @@ -314,13 +319,13 @@ "version": "0.1.8", "peerDependencies": { "@govoplan/core-webui": "^0.1.8", - "@vitejs/plugin-react": "^4.3.4", + "@vitejs/plugin-react": "^5.2.0", "lucide-react": "^1.23.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-router-dom": ">=7.18.2 <8", + "react": ">=19.2.7 <20", + "react-dom": ">=19.2.7 <20", + "react-router": ">=8.3.0 <9", "typescript": "^5.7.2", - "vite": "^6.0.6" + "vite": "^7.3.6" }, "peerDependenciesMeta": { "@govoplan/core-webui": { @@ -333,13 +338,13 @@ "version": "0.1.8", "peerDependencies": { "@govoplan/core-webui": "^0.1.9", - "@vitejs/plugin-react": "^4.3.4", + "@vitejs/plugin-react": "^5.2.0", "lucide-react": "^1.23.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-router-dom": ">=7.18.2 <8", + "react": ">=19.2.7 <20", + "react-dom": ">=19.2.7 <20", + "react-router": ">=8.3.0 <9", "typescript": "^5.7.2", - "vite": "^6.0.6" + "vite": "^7.3.6" }, "peerDependenciesMeta": { "@govoplan/core-webui": { @@ -353,9 +358,9 @@ "peerDependencies": { "@govoplan/core-webui": "^0.1.9", "lucide-react": "^1.23.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-router-dom": ">=7.18.2 <8" + "react": ">=19.2.7 <20", + "react-dom": ">=19.2.7 <20", + "react-router": ">=8.3.0 <9" }, "peerDependenciesMeta": { "@govoplan/core-webui": { @@ -365,13 +370,13 @@ }, "../../govoplan-postbox/webui": { "name": "@govoplan/postbox-webui", - "version": "0.1.1", + "version": "0.1.2", "peerDependencies": { "@govoplan/core-webui": "^0.1.14", "lucide-react": "^1.23.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-router-dom": ">=7.18.2 <8" + "react": ">=19.2.7 <20", + "react-dom": ">=19.2.7 <20", + "react-router": ">=8.3.0 <9" }, "peerDependenciesMeta": { "@govoplan/core-webui": { @@ -385,9 +390,9 @@ "peerDependencies": { "@govoplan/core-webui": "^0.1.14", "lucide-react": "^1.23.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-router-dom": ">=7.18.2 <8" + "react": ">=19.2.7 <20", + "react-dom": ">=19.2.7 <20", + "react-router": ">=8.3.0 <9" }, "peerDependenciesMeta": { "@govoplan/core-webui": { @@ -400,13 +405,13 @@ "version": "0.1.11", "peerDependencies": { "@govoplan/core-webui": "^0.1.11", - "@vitejs/plugin-react": "^4.3.4", + "@vitejs/plugin-react": "^5.2.0", "lucide-react": "^1.23.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-router-dom": ">=7.18.2 <8", + "react": ">=19.2.7 <20", + "react-dom": ">=19.2.7 <20", + "react-router": ">=8.3.0 <9", "typescript": "^5.7.2", - "vite": "^6.0.6" + "vite": "^7.3.6" }, "peerDependenciesMeta": { "@govoplan/core-webui": { @@ -420,9 +425,9 @@ "peerDependencies": { "@govoplan/core-webui": "^0.1.14", "lucide-react": "^1.23.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-router-dom": ">=7.18.2 <8" + "react": ">=19.2.7 <20", + "react-dom": ">=19.2.7 <20", + "react-router": ">=8.3.0 <9" }, "peerDependenciesMeta": { "@govoplan/core-webui": { @@ -436,9 +441,9 @@ "peerDependencies": { "@govoplan/core-webui": "^0.1.14", "lucide-react": "^1.23.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-router-dom": ">=7.18.2 <8" + "react": ">=19.2.7 <20", + "react-dom": ">=19.2.7 <20", + "react-router": ">=8.3.0 <9" }, "peerDependenciesMeta": { "@govoplan/core-webui": { @@ -453,9 +458,9 @@ "@govoplan/core-webui": "^0.1.14", "@xyflow/react": "^12.11.2", "lucide-react": "^1.23.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-router-dom": ">=7.18.2 <8", + "react": ">=19.2.7 <20", + "react-dom": ">=19.2.7 <20", + "react-router": ">=8.3.0 <9", "typescript": "^5.7.2" }, "peerDependenciesMeta": { @@ -747,9 +752,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], @@ -764,9 +769,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], @@ -781,9 +786,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -798,9 +803,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], @@ -815,9 +820,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -832,9 +837,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], @@ -849,9 +854,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], @@ -866,9 +871,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], @@ -883,9 +888,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], @@ -900,9 +905,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], @@ -917,9 +922,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], @@ -934,9 +939,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], @@ -951,9 +956,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], @@ -968,9 +973,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], @@ -985,9 +990,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], @@ -1002,9 +1007,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], @@ -1019,9 +1024,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], @@ -1036,9 +1041,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], @@ -1053,9 +1058,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], @@ -1070,9 +1075,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], @@ -1087,9 +1092,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], @@ -1104,9 +1109,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], @@ -1121,9 +1126,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], @@ -1138,9 +1143,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], @@ -1155,9 +1160,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], @@ -1172,9 +1177,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], @@ -1188,6 +1193,34 @@ "node": ">=18" } }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT", + "optional": true + }, "node_modules/@govoplan/access-webui": { "resolved": "../../govoplan-access/webui", "link": true @@ -1331,9 +1364,9 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", "dev": true, "license": "MIT" }, @@ -1726,6 +1759,449 @@ "win32" ] }, + "node_modules/@tiptap/core": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.29.2.tgz", + "integrity": "sha512-oKUkiPUB7noilVYxI9lNzUD4rX17sHub+PYjMfHMWHG9A3nvIy+FdePIVIIhThKWF7ijhr3eIqHY51Bn+GAFtw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/pm": "3.29.2" + } + }, + "node_modules/@tiptap/extension-blockquote": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.29.2.tgz", + "integrity": "sha512-ca4OzKDh0yaxg2+Z56bC2QnWsNsFp2YMRfVig1PDXyMVFMNJpLcnhxgq/9btn+xYAlYrj8RymOeCTYREOR6Zjg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2", + "@tiptap/pm": "3.29.2" + } + }, + "node_modules/@tiptap/extension-bold": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.29.2.tgz", + "integrity": "sha512-elYbGxJsYnBb4leqrcjdIJuiG380BcOgN+UUzvOv+qEjfGVzHodFOMBl3qnmD6urYHNu5/qQK2S0qSSRXKCLNQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2" + } + }, + "node_modules/@tiptap/extension-bubble-menu": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.29.2.tgz", + "integrity": "sha512-kzcWarcpr031rZu+R3Hzut5uxb3Qj/xxMDEYZIQ3uiq1yb5vEMXj8/SIyWautk6Nu8Rr1leV3rGdR4NzhzyFAA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2", + "@tiptap/pm": "3.29.2" + } + }, + "node_modules/@tiptap/extension-bullet-list": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.29.2.tgz", + "integrity": "sha512-3bWcCUPbCHv0XttlMdnAtXLNYWx2pblByMgxmGsaP9FU0QnslGXty6A6gHCqI33ygRg1vrA6U5Wtpwbi5aKu5g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.29.2" + } + }, + "node_modules/@tiptap/extension-code": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.29.2.tgz", + "integrity": "sha512-c6W5UGuB7WNLpYocsgRzpO2OOTI4QjaI9jjHRMuty9z+s9DtaYM/HrRLNwVh6MopkHb+i/89Wkv8gCS34fftig==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2" + } + }, + "node_modules/@tiptap/extension-code-block": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.29.2.tgz", + "integrity": "sha512-w153ct8g6dLiPTdXQ6SOIMxX4SEo5Q50AmjdEEEcJ7ZcYUcde/ScSskLHfOYmyt5ZFAiyEwr121+pux+p3/oAQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2", + "@tiptap/pm": "3.29.2" + } + }, + "node_modules/@tiptap/extension-document": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.29.2.tgz", + "integrity": "sha512-YUamvefLnsqu6124GavVTI7nqcFlQJ12ROB0oSwG69eSBZYNjg1tIs05LFrBxkwf4Xgqd6YzfJ9+FeG428RvzQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2" + } + }, + "node_modules/@tiptap/extension-dropcursor": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.29.2.tgz", + "integrity": "sha512-KKno7cU9r1HdR48CRrsDu69/1UjZdoslq/UcE+Kx+tdhAv/aljXMkRSNzGMrBNOBDmHRgS1+58zm21WQWdQzwA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "3.29.2" + } + }, + "node_modules/@tiptap/extension-floating-menu": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.29.2.tgz", + "integrity": "sha512-CBTq4Xs5aGWr65uFCIkKBms796OhmirWf/ax//iJ4fl9IfwqjwFuc2vQs9PmuwpKn9ctDHo2sMTtvyeCvIt5LQ==", + "license": "MIT", + "optional": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@floating-ui/dom": "^1.0.0", + "@tiptap/core": "3.29.2", + "@tiptap/pm": "3.29.2" + } + }, + "node_modules/@tiptap/extension-gapcursor": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.29.2.tgz", + "integrity": "sha512-8Q39UR4/Tit759IeW9xZIe3NMwN11GsuA3FLheDyyGn7RrW02HD3HhUDlazE54Ki4HoosjFmChPlN4Ik2ubdRQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "3.29.2" + } + }, + "node_modules/@tiptap/extension-hard-break": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.29.2.tgz", + "integrity": "sha512-eUW3LN3fq8rXnjEUeI3D2QONYdLsU3yYQm4jxlErs2h4cfwrFjgf19VSUFVmm6LrFbbQ0OnDVPeVLL6iOwDw2w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2" + } + }, + "node_modules/@tiptap/extension-heading": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.29.2.tgz", + "integrity": "sha512-6W4aIy70Mh7BNlbG9zZ5FBLhJhU2UUEzgZJ/jwYSCcB30o8McLxJSEjhtoHiX8R78Ah2/JzBGvIe5olZlbeE4A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2" + } + }, + "node_modules/@tiptap/extension-horizontal-rule": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.29.2.tgz", + "integrity": "sha512-8/ZPzbB9X85Mc9/7xVLZupQKBr2UVcQTGr512xtqMW+XkCQRHCph46tRo828YE13IMWI5fWn/FaNCqXG9cULSw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2", + "@tiptap/pm": "3.29.2" + } + }, + "node_modules/@tiptap/extension-image": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-3.29.2.tgz", + "integrity": "sha512-F+S4iru247mNVZdkNwNDZHeb281DkjsBJinaOGsOyJTvXY+KU5Uq7vY1LQTn493dTfU48Z0yMBUXwJffCT92Mg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2" + } + }, + "node_modules/@tiptap/extension-italic": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.29.2.tgz", + "integrity": "sha512-iH63V/5wsaMnY4Jz0+meaAGhaec4AiOzOduzl6ZZr5IyGhZ1kthyW84ELt0dyLI3hNceAUhaNWc+I7+vX0aoXA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2" + } + }, + "node_modules/@tiptap/extension-link": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.29.2.tgz", + "integrity": "sha512-DcVer5SqrexKCEP6Ip1UPxJUMvcRCCItSv0wxoGytanrimBh2smvcg6X0DWnjlsi5H0updhyl+atYCmmQXUIXA==", + "license": "MIT", + "dependencies": { + "linkifyjs": "^4.3.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2", + "@tiptap/pm": "3.29.2" + } + }, + "node_modules/@tiptap/extension-list": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.29.2.tgz", + "integrity": "sha512-WPZ9BHAPT6QeIm1vdVkuoOWvy9a8/EZeJwV2VhU8LXyTAttvzyj4rsbbHyJWvYWlUSTt/QF2AZ2zhKo7u1w3/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2", + "@tiptap/pm": "3.29.2" + } + }, + "node_modules/@tiptap/extension-list-item": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.29.2.tgz", + "integrity": "sha512-s8vBVHHFT0Qpu7CzAZ7S1kYmSiVaDvUNvMNcZUWnxj6VPfiwmx0eXd9FsjePrRCMoM5tFmPnhFTHDxY3D/eZeQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.29.2" + } + }, + "node_modules/@tiptap/extension-list-keymap": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.29.2.tgz", + "integrity": "sha512-R+3k8OLnxdCH7Xy9ieOwUt5m2Je74u8mikothGmsYVO2Zyq48fIbmZ+X6RBPCu7DBOI2FIUhHEFbKQeDWvDNmA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.29.2" + } + }, + "node_modules/@tiptap/extension-ordered-list": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.29.2.tgz", + "integrity": "sha512-ndCunC+UsYOpkOtL7vGnDz21UNa45WUlcO9wMT1fbuYow2QnRhsuMlCWENXI52YPPARWuQ0RDgN7q6TaxPERBg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "3.29.2" + } + }, + "node_modules/@tiptap/extension-paragraph": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.29.2.tgz", + "integrity": "sha512-7qJj5YTr11vvjNgjDN1ypOfwTovc0QOCYcit/rskeuVgnmQZOZQzC/BbyKLLG7UGnpRLemU/mEGbW9pAqjAXkQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2" + } + }, + "node_modules/@tiptap/extension-strike": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.29.2.tgz", + "integrity": "sha512-aEvLAbddUQZ+FukCreV3q4G2HfNI+odE7E9U+wbq6XsSWKyo8/pDu1muz+TFKNre4blSMOQ3JQmw5UeHDKy+fg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2" + } + }, + "node_modules/@tiptap/extension-text": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.29.2.tgz", + "integrity": "sha512-Ubko45JWWHe8glBt2PiGNF8hcbys/JNalFhiR7Y1X4iOOtAxAKJJxh3+eq+//NTlGuBPdWGp7zw8EEUp7anjKA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2" + } + }, + "node_modules/@tiptap/extension-underline": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.29.2.tgz", + "integrity": "sha512-K7XwH/xS/5AIREWQ00VTEf/W5U0olp7j6wwit7cdd/8nHv6h6AGr1+iEApHKoLXWQZLfGQKzJlT9W61LAl+fHA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2" + } + }, + "node_modules/@tiptap/extensions": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.29.2.tgz", + "integrity": "sha512-BCz+FCAChSYtUe4BFj97HEO+nSK+J7GxbJgZG4Hg7DT/gI+hRyeNndU8efiQAx3WGdzsFi3UxRpcF1tTQM7iMQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2", + "@tiptap/pm": "3.29.2" + } + }, + "node_modules/@tiptap/pm": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.29.2.tgz", + "integrity": "sha512-GCOme7xHaS+DSoaA4CDcAD3l6JyBlvZhvCyfsy2Vp6j8tEoBkZWio7soYVosmlyn7zq8/64VeFZP5s47yfG7fQ==", + "license": "MIT", + "dependencies": { + "prosemirror-changeset": "^2.4.1", + "prosemirror-commands": "^1.7.1", + "prosemirror-dropcursor": "^1.8.2", + "prosemirror-gapcursor": "^1.4.1", + "prosemirror-history": "^1.5.0", + "prosemirror-inputrules": "^1.5.1", + "prosemirror-keymap": "^1.2.3", + "prosemirror-model": "^1.25.11", + "prosemirror-schema-list": "^1.5.1", + "prosemirror-state": "^1.4.4", + "prosemirror-tables": "^1.8.5", + "prosemirror-transform": "^1.12.0", + "prosemirror-view": "^1.41.9" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@tiptap/react": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.29.2.tgz", + "integrity": "sha512-ZMKgteYmZXnq7C22U9fbCTC6jAF8XlQM5n2K2FLWCdYAlP4FNV3XeQ9hY2a13KSQ0Q3yltWXZlcWBpt5ClxScg==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "fast-equals": "^5.3.3", + "use-sync-external-store": "^1.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "optionalDependencies": { + "@tiptap/extension-bubble-menu": "^3.29.2", + "@tiptap/extension-floating-menu": "^3.29.2" + }, + "peerDependencies": { + "@tiptap/core": "3.29.2", + "@tiptap/pm": "3.29.2", + "@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" + } + }, + "node_modules/@tiptap/starter-kit": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.29.2.tgz", + "integrity": "sha512-oTu0tysiqk4zgjEtxRHjAQgxUKaAevZwueOWwSWubHdokqp7SpcbE5n9USJv89HKuTUDm3GjnQH6q8HNn/2DsA==", + "license": "MIT", + "dependencies": { + "@tiptap/core": "^3.29.2", + "@tiptap/extension-blockquote": "^3.29.2", + "@tiptap/extension-bold": "^3.29.2", + "@tiptap/extension-bullet-list": "^3.29.2", + "@tiptap/extension-code": "^3.29.2", + "@tiptap/extension-code-block": "^3.29.2", + "@tiptap/extension-document": "^3.29.2", + "@tiptap/extension-dropcursor": "^3.29.2", + "@tiptap/extension-gapcursor": "^3.29.2", + "@tiptap/extension-hard-break": "^3.29.2", + "@tiptap/extension-heading": "^3.29.2", + "@tiptap/extension-horizontal-rule": "^3.29.2", + "@tiptap/extension-italic": "^3.29.2", + "@tiptap/extension-link": "^3.29.2", + "@tiptap/extension-list": "^3.29.2", + "@tiptap/extension-list-item": "^3.29.2", + "@tiptap/extension-list-keymap": "^3.29.2", + "@tiptap/extension-ordered-list": "^3.29.2", + "@tiptap/extension-paragraph": "^3.29.2", + "@tiptap/extension-strike": "^3.29.2", + "@tiptap/extension-text": "^3.29.2", + "@tiptap/extension-underline": "^3.29.2", + "@tiptap/extensions": "^3.29.2", + "@tiptap/pm": "^3.29.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1837,7 +2313,6 @@ "version": "19.2.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", - "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -1847,31 +2322,36 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.28.0", + "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", + "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" + "react-refresh": "^0.18.0" }, "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/@xmldom/xmldom": { @@ -2010,25 +2490,17 @@ "dev": true, "license": "MIT" }, - "node_modules/cookie": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", - "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "node_modules/cookie-es": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-3.1.1.tgz", + "integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } + "license": "MIT" }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, "license": "MIT" }, "node_modules/d3-color": { @@ -2171,9 +2643,9 @@ "license": "ISC" }, "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2184,32 +2656,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" } }, "node_modules/escalade": { @@ -2222,6 +2694,15 @@ "node": ">=6" } }, + "node_modules/fast-equals": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.1.tgz", + "integrity": "sha512-DjlFSM5Pk9cGcL0q5QXl66eGzx0N6szNgaswwc5ZphlBohjTVJSnGgI+rJVOgOi65qUoQnDZN4nDqi33udtydQ==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2312,6 +2793,12 @@ "node": ">=6" } }, + "node_modules/linkifyjs": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.3.tgz", + "integrity": "sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==", + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2375,6 +2862,12 @@ "node": ">=18" } }, + "node_modules/orderedmap": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", + "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2424,33 +2917,170 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prosemirror-changeset": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.1.tgz", + "integrity": "sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==", + "license": "MIT", + "dependencies": { + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-commands": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz", + "integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.10.2" + } + }, + "node_modules/prosemirror-dropcursor": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.3.tgz", + "integrity": "sha512-FoYbsJR8gK+DGlqhNoE29Loa38eIZPzQRIb1VMaDNBoo4OLP6vVof/jR8qFY/6XvUd6Dhug8MDCHl2a/h8RTfQ==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0", + "prosemirror-view": "^1.1.0" + } + }, + "node_modules/prosemirror-gapcursor": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz", + "integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.0.0", + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" + } + }, + "node_modules/prosemirror-history": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz", + "integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.2.2", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.31.0", + "rope-sequence": "^1.3.0" + } + }, + "node_modules/prosemirror-inputrules": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz", + "integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-keymap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz", + "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "w3c-keyname": "^2.2.0" + } + }, + "node_modules/prosemirror-model": { + "version": "1.25.11", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.11.tgz", + "integrity": "sha512-QWg9RhnpLlogAmp3p96uEFrE5txQpFynd4vhBAELkwgOCWQs/X0yCzB3/hrHqiPwf91RG5KyWq6553zs9JqIOQ==", + "license": "MIT", + "dependencies": { + "orderedmap": "^2.0.0" + } + }, + "node_modules/prosemirror-schema-list": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz", + "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.7.3" + } + }, + "node_modules/prosemirror-state": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", + "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.27.0" + } + }, + "node_modules/prosemirror-tables": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz", + "integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.2.3", + "prosemirror-model": "^1.25.4", + "prosemirror-state": "^1.4.4", + "prosemirror-transform": "^1.10.5", + "prosemirror-view": "^1.41.4" + } + }, + "node_modules/prosemirror-transform": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.0.tgz", + "integrity": "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.21.0" + } + }, + "node_modules/prosemirror-view": { + "version": "1.42.2", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.42.2.tgz", + "integrity": "sha512-Pdg0l5kXm8aLDquFAnQFTCITg0q44sLqBlHlpsVLD9segdOao8TOfQdAhCrCXyVgPSRr6UDDROOIWA3bIrN9YQ==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.25.8", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + }, "node_modules/react": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", - "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", - "dev": true, + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", - "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", - "dev": true, + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.7" + "react": "^19.2.8" } }, "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", "dev": true, "license": "MIT", "engines": { @@ -2458,21 +3088,20 @@ } }, "node_modules/react-router": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz", - "integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-8.3.0.tgz", + "integrity": "sha512-qyPMvW83jGIct3yiieisxdk9M745anqhpIMKN5m1t6yBMfgVPpt77aHOqs5fUlEJRMCGffg9BaQLH9oPVOL7xQ==", "dev": true, "license": "MIT", "dependencies": { - "cookie": "^1.0.1", - "set-cookie-parser": "^2.6.0" + "cookie-es": "^3.1.1" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.22.0" }, "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" + "react": ">=19.2.7", + "react-dom": ">=19.2.7" }, "peerDependenciesMeta": { "react-dom": { @@ -2480,23 +3109,6 @@ } } }, - "node_modules/react-router-dom": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.2.tgz", - "integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==", - "dev": true, - "license": "MIT", - "dependencies": { - "react-router": "7.18.2" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - } - }, "node_modules/read-excel-file": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/read-excel-file/-/read-excel-file-9.2.0.tgz", @@ -2557,11 +3169,16 @@ "fsevents": "~2.3.2" } }, + "node_modules/rope-sequence": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", + "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "dev": true, "license": "MIT" }, "node_modules/semver": { @@ -2574,13 +3191,6 @@ "semver": "bin/semver.js" } }, - "node_modules/set-cookie-parser": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", - "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", - "dev": true, - "license": "MIT" - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2671,31 +3281,30 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", - "dev": true, "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/vite": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", - "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -2704,14 +3313,14 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", - "less": "*", + "less": "^4.0.0", "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" @@ -2752,6 +3361,12 @@ } } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/webui/package.json b/webui/package.json index 6eea570..1b7cbe5 100644 --- a/webui/package.json +++ b/webui/package.json @@ -18,6 +18,10 @@ "./definition-graph": { "types": "./src/definitionGraph.ts", "import": "./src/definitionGraph.ts" + }, + "./wysiwyg": { + "types": "./src/wysiwyg.ts", + "import": "./src/wysiwyg.ts" } }, "scripts": { @@ -29,7 +33,7 @@ "audit:i18n-structural": "node scripts/audit-i18n-structural.mjs", "test:i18n-catalog": "node --test tests/i18n-catalog-validation.test.mjs", "test:file-drop-zone": "rm -rf .file-drop-test-build && mkdir -p .file-drop-test-build && printf '{\"type\":\"commonjs\"}\\n' > .file-drop-test-build/package.json && tsc -p tsconfig.file-drop-tests.json && node .file-drop-test-build/tests/file-drop-resolver.test.js && node scripts/test-file-drop-zone-structure.mjs", - "test:data-grid-actions": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/data-grid-actions.test.js", + "test:data-grid-actions": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/data-grid-actions.test.js && node .component-test-build/tests/data-grid-sizing.test.js", "test:dialog-focus": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/dialog-focus.test.js && node scripts/test-dialog-focus-structure.mjs", "test:explorer-tree": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/explorer-tree.test.js", "test:icon-button": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/icon-button.test.js", @@ -39,53 +43,59 @@ "test:metric-card": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/metric-card.test.js", "test:people-picker": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/people-picker.test.js", "test:resource-access": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/resource-access-explanation.test.js", - "test:selection-list": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/selection-list.test.js" + "test:selection-list": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/selection-list.test.js", + "test:wysiwyg-editor": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/wysiwyg-editor-utils.test.js" }, "dependencies": { "@govoplan/access-webui": "file:../../govoplan-access/webui", - "@govoplan/admin-webui": "file:../../govoplan-admin/webui", "@govoplan/addresses-webui": "file:../../govoplan-addresses/webui", + "@govoplan/admin-webui": "file:../../govoplan-admin/webui", "@govoplan/audit-webui": "file:../../govoplan-audit/webui", "@govoplan/calendar-webui": "file:../../govoplan-calendar/webui", "@govoplan/campaign-webui": "file:../../govoplan-campaign/webui", + "@govoplan/dashboard-webui": "file:../../govoplan-dashboard/webui", "@govoplan/dataflow-webui": "file:../../govoplan-dataflow/webui", "@govoplan/datasources-webui": "file:../../govoplan-datasources/webui", - "@govoplan/dashboard-webui": "file:../../govoplan-dashboard/webui", "@govoplan/docs-webui": "file:../../govoplan-docs/webui", "@govoplan/files-webui": "file:../../govoplan-files/webui", "@govoplan/idm-webui": "file:../../govoplan-idm/webui", "@govoplan/mail-webui": "file:../../govoplan-mail/webui", "@govoplan/notifications-webui": "file:../../govoplan-notifications/webui", - "@govoplan/organizations-webui": "file:../../govoplan-organizations/webui", "@govoplan/ops-webui": "file:../../govoplan-ops/webui", + "@govoplan/organizations-webui": "file:../../govoplan-organizations/webui", "@govoplan/policy-webui": "file:../../govoplan-policy/webui", "@govoplan/postbox-webui": "file:../../govoplan-postbox/webui", "@govoplan/risk-compliance-webui": "file:../../govoplan-risk-compliance/webui", "@govoplan/scheduling-webui": "file:../../govoplan-scheduling/webui", "@govoplan/search-webui": "file:../../govoplan-search/webui", "@govoplan/views-webui": "file:../../govoplan-views/webui", - "@govoplan/workflow-webui": "file:../../govoplan-workflow/webui" + "@govoplan/workflow-webui": "file:../../govoplan-workflow/webui", + "@tiptap/core": "^3.29.2", + "@tiptap/extension-image": "^3.29.2", + "@tiptap/pm": "^3.29.2", + "@tiptap/react": "^3.29.2", + "@tiptap/starter-kit": "^3.29.2" }, "devDependencies": { - "@types/react": "^19.0.2", - "@types/react-dom": "^19.0.2", - "@vitejs/plugin-react": "^4.3.4", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.2.0", "@xyflow/react": "^12.11.2", "lucide-react": "^1.23.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-router-dom": "7.18.2", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-router": "8.3.0", "read-excel-file": "^9.2.0", "typescript": "^5.7.2", - "vite": "^6.0.6" + "vite": "^7.3.6" }, "peerDependencies": { "lucide-react": "^1.23.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-router-dom": ">=7.18.2 <8" + "react": ">=19.2.7 <20", + "react-dom": ">=19.2.7 <20", + "react-router": ">=8.3.0 <9" }, "allowScripts": { - "esbuild@0.25.12": true + "esbuild@0.28.1": true } } diff --git a/webui/package.release.json b/webui/package.release.json index d57eb95..4ac3280 100644 --- a/webui/package.release.json +++ b/webui/package.release.json @@ -14,6 +14,10 @@ "./app": { "types": "./src/app.ts", "import": "./src/app.ts" + }, + "./wysiwyg": { + "types": "./src/wysiwyg.ts", + "import": "./src/wysiwyg.ts" } }, "scripts": { @@ -34,26 +38,31 @@ "@govoplan/campaign-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git#v0.1.11", "@govoplan/organizations-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git#v0.1.8", "@govoplan/ops-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-ops.git#v0.1.8", - "@govoplan/policy-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git#v0.1.8" + "@govoplan/policy-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git#v0.1.8", + "@tiptap/core": "^3.29.2", + "@tiptap/extension-image": "^3.29.2", + "@tiptap/pm": "^3.29.2", + "@tiptap/react": "^3.29.2", + "@tiptap/starter-kit": "^3.29.2" }, "devDependencies": { "lucide-react": "^1.23.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-router-dom": "^7.1.1", - "@types/react": "^19.0.2", - "@types/react-dom": "^19.0.2", - "@vitejs/plugin-react": "^4.3.4", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-router": "8.3.0", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.2.0", "typescript": "^5.7.2", - "vite": "^6.0.6" + "vite": "^7.3.6" }, "peerDependencies": { "lucide-react": "^1.23.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-router-dom": "^7.1.1" + "react": ">=19.2.7 <20", + "react-dom": ">=19.2.7 <20", + "react-router": ">=8.3.0 <9" }, "allowScripts": { - "esbuild@0.25.12": true + "esbuild@0.28.1": true } } diff --git a/webui/src/App.tsx b/webui/src/App.tsx index 1b5b368..5777328 100644 --- a/webui/src/App.tsx +++ b/webui/src/App.tsx @@ -1,4 +1,4 @@ -import { Navigate, Route, Routes, useLocation } from "react-router-dom"; +import { Navigate, Route, Routes, useLocation } from "react-router"; import { lazy, useEffect, useMemo, useState } from "react"; import { fetchSession, fetchShellAuth, updateProfile } from "./api/auth"; import { fetchPlatformModules, fetchPlatformPublicModules, fetchPlatformStatus } from "./api/platform"; @@ -11,7 +11,11 @@ import { PermissionBoundary } from "./components/AccessBoundary"; import { firstAccessibleRoute, loadInstalledPublicWebModules, loadInstalledWebModules, loadRemotePublicWebModules, loadRemoteWebModules, moduleInstalled, navItemsForModules, publicRouteContributionsForModules, routeContributionsForModules, uiCapability } from "./platform/modules"; import { PlatformModulesProvider } from "./platform/ModuleContext"; import { PlatformViewProvider } from "./platform/ViewContext"; -import { PLATFORM_VIEW_CHANGED_EVENT } from "./platform/views"; +import { + PLATFORM_VIEW_CHANGED_EVENT, + PLATFORM_WORKFLOW_VIEW_CHANGED_EVENT, + type WorkflowViewChangedEventDetail +} from "./platform/views"; import { PLATFORM_MODULES_CHANGED_EVENT } from "./platform/moduleEvents"; import { UnsavedChangesProvider } from "./components/UnsavedChangesGuard"; import { PlatformLanguageProvider, type PlatformLanguage } from "./i18n/LanguageContext"; @@ -45,7 +49,9 @@ export default function App() { const [backendReachable, setBackendReachable] = useState(true); const [systemLanguages, setSystemLanguages] = useState<{available: PlatformLanguage[];enabled: string[];defaultLanguage: string;} | null>(null); const [reloginMessage, setReloginMessage] = useState(""); - const [viewProjection, setViewProjection] = useState(null); + const [baseViewProjection, setBaseViewProjection] = useState(null); + const [workflowViewProjection, setWorkflowViewProjection] = useState(null); + const viewProjection = workflowViewProjection ?? baseViewProjection; const webModules = useMemo(() => mergeWebModules(localWebModules, remoteWebModules), [localWebModules, remoteWebModules]); const publicWebModules = useMemo(() => mergeWebModules(localPublicWebModules, remotePublicWebModules), [localPublicWebModules, remotePublicWebModules]); @@ -65,7 +71,8 @@ export default function App() { useEffect(() => { if (!auth || !viewsRuntime) { - setViewProjection(null); + setBaseViewProjection(null); + setWorkflowViewProjection(null); return; } const currentAuth = auth; @@ -74,17 +81,22 @@ export default function App() { async function loadEffectiveView() { try { const projection = await currentRuntime.loadEffectiveView(settings, currentAuth); - if (!cancelled) setViewProjection(projection); + if (!cancelled) setBaseViewProjection(projection); } catch (error) { - if (!cancelled) setViewProjection(null); + if (!cancelled) setBaseViewProjection(null); console.error("Failed to load the effective View", error); } } + function reloadNormalView() { + setWorkflowViewProjection(null); + clearStoredWorkflowView(currentAuth); + void loadEffectiveView(); + } void loadEffectiveView(); - window.addEventListener(PLATFORM_VIEW_CHANGED_EVENT, loadEffectiveView); + window.addEventListener(PLATFORM_VIEW_CHANGED_EVENT, reloadNormalView); return () => { cancelled = true; - window.removeEventListener(PLATFORM_VIEW_CHANGED_EVENT, loadEffectiveView); + window.removeEventListener(PLATFORM_VIEW_CHANGED_EVENT, reloadNormalView); }; }, [ auth?.user?.id, @@ -96,6 +108,36 @@ export default function App() { viewsRuntime ]); + useEffect(() => { + if (!auth || !viewsRuntime) { + setWorkflowViewProjection(null); + return; + } + const currentAuth = auth; + setWorkflowViewProjection(loadStoredWorkflowView(currentAuth)); + function applyWorkflowView(event: Event) { + const detail = (event as CustomEvent).detail; + const projection = detail?.projection ?? null; + setWorkflowViewProjection(projection); + storeWorkflowView(currentAuth, projection, detail?.contextId); + } + window.addEventListener( + PLATFORM_WORKFLOW_VIEW_CHANGED_EVENT, + applyWorkflowView + ); + return () => { + window.removeEventListener( + PLATFORM_WORKFLOW_VIEW_CHANGED_EVENT, + applyWorkflowView + ); + }; + }, [ + auth?.user?.id, + auth?.active_tenant?.id, + auth?.tenant.id, + viewsRuntime + ]); + function updateSettings(next: ApiSettings) { setSettings(next); saveApiSettings(next); @@ -662,3 +704,65 @@ function mergeWebModules(localModules: PlatformWebModule[], remoteModules: Platf })]; } + +const WORKFLOW_VIEW_SESSION_TTL_MS = 12 * 60 * 60 * 1000; + +type StoredWorkflowView = { + projection: EffectiveViewProjection; + contextId?: string | null; + expiresAt: number; +}; + +function workflowViewStorageKey(auth: AuthInfo): string { + const tenant = auth.active_tenant ?? auth.tenant; + return `govoplan:workflow-view:${tenant.id}:${auth.user.account_id}`; +} + +function loadStoredWorkflowView( + auth: AuthInfo +): EffectiveViewProjection | null { + try { + const key = workflowViewStorageKey(auth); + const raw = window.sessionStorage.getItem(key); + if (!raw) return null; + const stored = JSON.parse(raw) as Partial; + if ( + typeof stored.expiresAt !== "number" + || stored.expiresAt <= Date.now() + || !stored.projection + || !Array.isArray(stored.projection.visibleSurfaceIds) + ) { + window.sessionStorage.removeItem(key); + return null; + } + return stored.projection; + } catch { + return null; + } +} + +function storeWorkflowView( + auth: AuthInfo, + projection: EffectiveViewProjection | null, + contextId?: string | null +): void { + try { + const key = workflowViewStorageKey(auth); + if (!projection) { + window.sessionStorage.removeItem(key); + return; + } + const stored: StoredWorkflowView = { + projection, + contextId, + expiresAt: Date.now() + WORKFLOW_VIEW_SESSION_TTL_MS + }; + window.sessionStorage.setItem(key, JSON.stringify(stored)); + } catch { + // Session persistence is optional; the in-memory projection still applies. + } +} + +function clearStoredWorkflowView(auth: AuthInfo): void { + storeWorkflowView(auth, null); +} diff --git a/webui/src/components/AccessBoundary.tsx b/webui/src/components/AccessBoundary.tsx index 56aebb3..7a75aab 100644 --- a/webui/src/components/AccessBoundary.tsx +++ b/webui/src/components/AccessBoundary.tsx @@ -1,5 +1,5 @@ import { useEffect, useState, type ReactNode } from "react"; -import { Navigate } from "react-router-dom"; +import { Navigate } from "react-router"; import type { AuthInfo } from "../types"; import { isApiError } from "../api/client"; import DismissibleAlert from "./DismissibleAlert"; @@ -57,4 +57,4 @@ export function ResourceAccessBoundary({ probe, resetKey, fallback, children }: if (state === "error") return
{message || "i18n:govoplan-core.the_resource_could_not_be_loaded.0d1b6cbf"}
; if (state === "checking") return

i18n:govoplan-core.checking_access.c5108c02

; return <>{children}; -} \ No newline at end of file +} diff --git a/webui/src/components/DashboardWidgetContent.tsx b/webui/src/components/DashboardWidgetContent.tsx index 137cf55..1a9d640 100644 --- a/webui/src/components/DashboardWidgetContent.tsx +++ b/webui/src/components/DashboardWidgetContent.tsx @@ -3,7 +3,7 @@ import { useState, type ReactNode } from "react"; -import { Link } from "react-router-dom"; +import { Link } from "react-router"; import { adminErrorMessage } from "./admin/adminUtils"; export type DashboardWidgetDataState = { diff --git a/webui/src/components/StageRail.tsx b/webui/src/components/StageRail.tsx new file mode 100644 index 0000000..eb32904 --- /dev/null +++ b/webui/src/components/StageRail.tsx @@ -0,0 +1,118 @@ +import type { CSSProperties, ReactNode } from "react"; +import { LockKeyhole } from "lucide-react"; + +export type StageRailTone = + | "neutral" + | "success" + | "warning" + | "danger" + | "active" + | "partial"; + +export type StageRailItem = { + id: string; + label: string; + detail?: string; + statusLabel?: string; + title?: string; + icon?: ReactNode; + tone?: StageRailTone; + connectorTone?: StageRailTone; + current?: boolean; + locked?: boolean; + lockedLabel?: string; +}; + +export type StageRailProps = { + items: StageRailItem[]; + ariaLabel: string; + className?: string; + onSelect?: (id: string) => void; +}; + +const toneColors: Record = { + neutral: "var(--line-dark)", + success: "var(--green)", + warning: "var(--amber)", + danger: "var(--red)", + active: "var(--blue)", + partial: "var(--stage-rail-partial, #8069aa)" +}; + +export default function StageRail({ + items, + ariaLabel, + className = "", + onSelect +}: StageRailProps) { + return ( + + ); +} diff --git a/webui/src/components/UnsavedChangesGuard.tsx b/webui/src/components/UnsavedChangesGuard.tsx index fccd133..982a782 100644 --- a/webui/src/components/UnsavedChangesGuard.tsx +++ b/webui/src/components/UnsavedChangesGuard.tsx @@ -1,5 +1,5 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; -import { useNavigate, type NavigateFunction, type NavigateOptions, type To } from "react-router-dom"; +import { useNavigate, type NavigateFunction, type NavigateOptions, type To } from "react-router"; import Button from "./Button"; import Dialog from "./Dialog"; import DismissibleAlert from "./DismissibleAlert"; diff --git a/webui/src/components/WysiwygEditor.tsx b/webui/src/components/WysiwygEditor.tsx new file mode 100644 index 0000000..dcd1fc2 --- /dev/null +++ b/webui/src/components/WysiwygEditor.tsx @@ -0,0 +1,767 @@ +import { + mergeAttributes, + Node, + nodeInputRule, + nodePasteRule, + type Editor +} from "@tiptap/core"; +import Image from "@tiptap/extension-image"; +import { EditorContent, useEditor, useEditorState } from "@tiptap/react"; +import StarterKit from "@tiptap/starter-kit"; +import { + Bold, + Code2, + ImagePlus, + Italic, + Link2, + Link2Off, + List, + ListOrdered, + Quote, + Redo2, + RemoveFormatting, + Strikethrough, + Underline, + Undo2 +} from "lucide-react"; +import { + forwardRef, + useEffect, + useImperativeHandle, + useMemo, + useRef, + useState, + type CSSProperties, + type ChangeEvent, + type MouseEvent +} from "react"; +import { usePlatformLanguage } from "../i18n/LanguageContext"; +import Button from "./Button"; +import Dialog from "./Dialog"; +import FormField from "./FormField"; +import IconButton from "./IconButton"; +import SegmentedControl from "./SegmentedControl"; +import { normalizeWysiwygImageUrl, normalizeWysiwygLinkUrl } from "./wysiwygEditorUrls"; + +export type WysiwygEditorMode = "visual" | "source"; + +export type WysiwygEditorToken = { + value: string; + label?: string; +}; + +export type WysiwygEditorHandle = { + focus: () => void; + insertText: (value: string) => boolean; + insertToken: (token: WysiwygEditorToken | string) => boolean; +}; + +export type WysiwygEditorLabels = { + editor: string; + visual: string; + source: string; + blockStyle: string; + paragraph: string; + heading1: string; + heading2: string; + heading3: string; + undo: string; + redo: string; + bold: string; + italic: string; + underline: string; + strike: string; + inlineCodeLabel: string; + bulletList: string; + numberedList: string; + quote: string; + insertLink: string; + removeLink: string; + insertImage: string; + clearFormatting: string; + linkText: string; + linkUrlLabel: string; + imageUrlLabel: string; + alternativeText: string; + apply: string; + cancel: string; + invalidLink: string; + invalidImage: string; + sourceWarning: string; +}; + +export type WysiwygEditorProps = { + value: string; + onChange: (value: string) => void; + disabled?: boolean; + defaultMode?: WysiwygEditorMode; + allowSourceMode?: boolean; + ariaLabel?: string; + className?: string; + minHeight?: number | string; + sourceRows?: number; + labels?: Partial; + onFocus?: () => void; +}; + +type BlockStyle = "paragraph" | "heading-1" | "heading-2" | "heading-3"; + +const INLINE_TOKEN_NAME = "govoplanInlineToken"; +const TOKEN_INPUT_PATTERN = /((?:\{\{[^{}\r\n]+\}\}|\$\{[^{}\r\n]+\}))$/; +const TOKEN_PASTE_PATTERN = /(?:\{\{[^{}\r\n]+\}\}|\$\{[^{}\r\n]+\})/g; +const TOKEN_SCAN_PATTERN = /(?:\{\{[^{}\r\n]+\}\}|\$\{[^{}\r\n]+\})/g; + +const DEFAULT_LABELS: WysiwygEditorLabels = { + editor: "i18n:govoplan-core.rich_text_editor.b56157a6", + visual: "i18n:govoplan-core.visual.770d690e", + source: "i18n:govoplan-core.html_source.1d4100b2", + blockStyle: "i18n:govoplan-core.block_style.c050e2eb", + paragraph: "i18n:govoplan-core.paragraph.05058e0b", + heading1: "i18n:govoplan-core.heading_1.fdc7d47a", + heading2: "i18n:govoplan-core.heading_2.6542d386", + heading3: "i18n:govoplan-core.heading_3.9694d18f", + undo: "i18n:govoplan-core.undo.39fc7212", + redo: "i18n:govoplan-core.redo.471b94d4", + bold: "i18n:govoplan-core.bold.19e07430", + italic: "i18n:govoplan-core.italic.1616e2e5", + underline: "i18n:govoplan-core.underline.39773aa3", + strike: "i18n:govoplan-core.strikethrough.a93b9a68", + inlineCodeLabel: "i18n:govoplan-core.inline_code.6f3a4263", + bulletList: "i18n:govoplan-core.bullet_list.660d9fbe", + numberedList: "i18n:govoplan-core.numbered_list.8294ea4f", + quote: "i18n:govoplan-core.block_quote.b3399cd3", + insertLink: "i18n:govoplan-core.insert_link.4d4ac9a1", + removeLink: "i18n:govoplan-core.remove_link.8d48d1f9", + insertImage: "i18n:govoplan-core.insert_image.c8141eb5", + clearFormatting: "i18n:govoplan-core.clear_formatting.6c8a26c4", + linkText: "i18n:govoplan-core.link_text.502f872d", + linkUrlLabel: "i18n:govoplan-core.link_url.08738f6e", + imageUrlLabel: "i18n:govoplan-core.image_url.35c80f96", + alternativeText: "i18n:govoplan-core.alternative_text.8d87ae1b", + apply: "i18n:govoplan-core.apply.cfea419c", + cancel: "i18n:govoplan-core.cancel.77dfd213", + invalidLink: "i18n:govoplan-core.enter_a_valid_http_https_mail_or_phone_link.45447137", + invalidImage: "i18n:govoplan-core.enter_a_valid_http_https_cid_or_raster_data_image.2cec4b3c", + sourceWarning: "i18n:govoplan-core.this_html_uses_markup_outside_the_visual_editor.5adb2a3c" +}; + +const InlineToken = Node.create({ + name: INLINE_TOKEN_NAME, + group: "inline", + inline: true, + atom: true, + selectable: true, + + addAttributes() { + return { + value: { default: "" }, + label: { default: "" } + }; + }, + + parseHTML() { + return [{ + tag: "span[data-govoplan-token]", + getAttrs: (element) => { + if (!(element instanceof HTMLElement)) return false; + const value = element.dataset.govoplanToken || ""; + if (!value) return false; + return { + value, + label: element.dataset.govoplanTokenLabel || tokenLabel(value) + }; + } + }]; + }, + + renderHTML({ node, HTMLAttributes }) { + const value = String(node.attrs.value || ""); + const label = String(node.attrs.label || tokenLabel(value)); + return [ + "span", + mergeAttributes(HTMLAttributes, { + class: "wysiwyg-inline-token", + "data-govoplan-token": value, + "data-govoplan-token-label": label, + contenteditable: "false" + }), + label + ]; + }, + + addInputRules() { + return [nodeInputRule({ + find: TOKEN_INPUT_PATTERN, + type: this.type, + getAttributes: (match) => tokenAttributes(match[1] || match[0]) + })]; + }, + + addPasteRules() { + return [nodePasteRule({ + find: TOKEN_PASTE_PATTERN, + type: this.type, + getAttributes: (match) => tokenAttributes(match[0]) + })]; + } +}); + +const WysiwygEditor = forwardRef(function WysiwygEditor({ + value, + onChange, + disabled = false, + defaultMode = "visual", + allowSourceMode = true, + ariaLabel, + className = "", + minHeight = 300, + sourceRows = 16, + labels, + onFocus +}, forwardedRef) { + const { translateText } = usePlatformLanguage(); + const translatedLabels = useMemo( + () => translateLabels({ ...DEFAULT_LABELS, ...labels }, translateText), + [labels, translateText] + ); + const translatedAriaLabel = translateText(ariaLabel || translatedLabels.editor); + const [mode, setMode] = useState(() => { + if (!allowSourceMode) return "visual"; + if (defaultMode === "source" || hasUnsupportedVisualMarkup(value)) return "source"; + return "visual"; + }); + const [linkDialogOpen, setLinkDialogOpen] = useState(false); + const [linkUrl, setLinkUrl] = useState(""); + const [linkText, setLinkText] = useState(""); + const [linkError, setLinkError] = useState(""); + const [imageDialogOpen, setImageDialogOpen] = useState(false); + const [imageUrl, setImageUrl] = useState(""); + const [imageAlt, setImageAlt] = useState(""); + const [imageError, setImageError] = useState(""); + const [editingImage, setEditingImage] = useState(false); + const sourceRef = useRef(null); + const onChangeRef = useRef(onChange); + const onFocusRef = useRef(onFocus); + const valueRef = useRef(value); + const appliedValueRef = useRef(value); + const modeWasChosenRef = useRef(false); + const linkSelectionRef = useRef({ from: 0, to: 0 }); + onChangeRef.current = onChange; + onFocusRef.current = onFocus; + valueRef.current = value; + + const editor = useEditor({ + extensions: [ + StarterKit.configure({ + heading: { levels: [1, 2, 3, 4, 5, 6] }, + link: { + openOnClick: false, + autolink: true, + defaultProtocol: "https", + HTMLAttributes: { + rel: "noopener noreferrer", + target: "_blank" + } + } + }), + Image.configure({ allowBase64: true }), + InlineToken + ], + content: decorateTokensForEditor(value), + editable: !disabled, + immediatelyRender: true, + shouldRerenderOnTransaction: false, + editorProps: { + attributes: { + "aria-label": translatedAriaLabel, + "aria-multiline": "true", + class: "wysiwyg-prosemirror", + role: "textbox" + } + }, + onFocus: () => onFocusRef.current?.(), + onUpdate: ({ editor: currentEditor }) => { + const nextValue = editorHtmlValue(currentEditor); + appliedValueRef.current = nextValue; + onChangeRef.current(nextValue); + } + }); + + const toolbarState = useEditorState({ + editor, + selector: ({ editor: currentEditor }) => ({ + blockStyle: activeBlockStyle(currentEditor), + bold: currentEditor.isActive("bold"), + italic: currentEditor.isActive("italic"), + underline: currentEditor.isActive("underline"), + strike: currentEditor.isActive("strike"), + code: currentEditor.isActive("code"), + bulletList: currentEditor.isActive("bulletList"), + numberedList: currentEditor.isActive("orderedList"), + quote: currentEditor.isActive("blockquote"), + link: currentEditor.isActive("link"), + canUndo: currentEditor.can().chain().undo().run(), + canRedo: currentEditor.can().chain().redo().run() + }) + }); + + const unsupportedSource = useMemo(() => hasUnsupportedVisualMarkup(value), [value]); + const editorStyle = { + "--wysiwyg-editor-min-height": typeof minHeight === "number" ? `${minHeight}px` : minHeight + } as CSSProperties; + + useEffect(() => { + editor.setEditable(!disabled); + }, [disabled, editor]); + + useEffect(() => { + editor.setOptions({ + editorProps: { + attributes: { + "aria-label": translatedAriaLabel, + "aria-multiline": "true", + class: "wysiwyg-prosemirror", + role: "textbox" + } + } + }); + }, [editor, translatedAriaLabel]); + + useEffect(() => { + if (value === appliedValueRef.current) return; + appliedValueRef.current = value; + if (mode === "visual") { + editor.commands.setContent(decorateTokensForEditor(value), { emitUpdate: false }); + } + }, [editor, mode, value]); + + useEffect(() => { + if ( + !allowSourceMode + || mode !== "visual" + || modeWasChosenRef.current + || !hasUnsupportedVisualMarkup(value) + ) return; + setMode("source"); + }, [allowSourceMode, mode, value]); + + useImperativeHandle(forwardedRef, () => ({ + focus() { + if (mode === "source") sourceRef.current?.focus(); + else editor.commands.focus(); + }, + insertText(text) { + if (disabled || !text) return false; + if (mode === "source") return insertIntoSource(text, valueRef.current, sourceRef.current, onChangeRef.current); + return editor.chain().focus().insertContent(text).run(); + }, + insertToken(token) { + if (disabled) return false; + const normalized = typeof token === "string" ? tokenAttributes(token) : tokenAttributes(token.value, token.label); + if (!normalized.value) return false; + if (mode === "source") { + return insertIntoSource(normalized.value, valueRef.current, sourceRef.current, onChangeRef.current); + } + return editor.chain().focus().insertContent({ + type: INLINE_TOKEN_NAME, + attrs: normalized + }).run(); + } + }), [disabled, editor, mode]); + + function selectMode(nextMode: WysiwygEditorMode) { + if (nextMode === mode || (!allowSourceMode && nextMode === "source")) return; + modeWasChosenRef.current = true; + if (nextMode === "visual") { + appliedValueRef.current = valueRef.current; + editor.commands.setContent(decorateTokensForEditor(valueRef.current), { emitUpdate: false }); + setMode("visual"); + window.requestAnimationFrame(() => editor.commands.focus("end")); + return; + } + const nextValue = editorHtmlValue(editor); + appliedValueRef.current = nextValue; + if (nextValue !== valueRef.current) onChangeRef.current(nextValue); + setMode("source"); + window.requestAnimationFrame(() => sourceRef.current?.focus()); + } + + function changeSource(event: ChangeEvent) { + const nextValue = event.target.value; + appliedValueRef.current = nextValue; + onChangeRef.current(nextValue); + } + + function changeBlockStyle(nextStyle: BlockStyle) { + if (nextStyle === "paragraph") { + editor.chain().focus().setParagraph().run(); + return; + } + const level = Number(nextStyle.slice(-1)) as 1 | 2 | 3; + editor.chain().focus().setHeading({ level }).run(); + } + + function openLinkDialog() { + if (toolbarState.link) editor.chain().focus().extendMarkRange("link").run(); + const { from, to } = editor.state.selection; + const attributes = editor.getAttributes("link"); + linkSelectionRef.current = { from, to }; + setLinkUrl(String(attributes.href || "")); + setLinkText(from === to ? "" : editor.state.doc.textBetween(from, to, " ")); + setLinkError(""); + setLinkDialogOpen(true); + } + + function closeLinkDialog() { + setLinkDialogOpen(false); + setLinkError(""); + } + + function applyLink() { + const href = normalizeWysiwygLinkUrl(linkUrl); + if (!href) { + setLinkError(translatedLabels.invalidLink); + return; + } + const { from, to } = linkSelectionRef.current; + if (from === to) { + editor.chain().focus().setTextSelection(from).insertContent({ + type: "text", + text: linkText.trim() || href, + marks: [{ type: "link", attrs: { href } }] + }).run(); + } else { + editor.chain().focus().setTextSelection({ from, to }).setLink({ href }).run(); + } + closeLinkDialog(); + } + + function openImageDialog() { + const isEditing = editor.isActive("image"); + const attributes = isEditing ? editor.getAttributes("image") : {}; + setEditingImage(isEditing); + setImageUrl(String(attributes.src || "")); + setImageAlt(String(attributes.alt || "")); + setImageError(""); + setImageDialogOpen(true); + } + + function closeImageDialog() { + setImageDialogOpen(false); + setImageError(""); + } + + function applyImage() { + const src = normalizeWysiwygImageUrl(imageUrl); + if (!src) { + setImageError(translatedLabels.invalidImage); + return; + } + const attributes = { src, alt: imageAlt.trim() || undefined }; + if (editingImage) editor.chain().focus().updateAttributes("image", attributes).run(); + else editor.chain().focus().setImage(attributes).run(); + closeImageDialog(); + } + + function preserveSelection(event: MouseEvent) { + event.preventDefault(); + } + + const rootClassName = [ + "wysiwyg-editor", + disabled ? "is-disabled" : "", + mode === "source" ? "is-source-mode" : "", + className + ].filter(Boolean).join(" "); + + return ( +
+
+ {mode === "visual" && ( +
+
+ } + disabled={disabled || !toolbarState.canUndo} + onMouseDown={preserveSelection} + onClick={() => editor.chain().focus().undo().run()} + /> + } + disabled={disabled || !toolbarState.canRedo} + onMouseDown={preserveSelection} + onClick={() => editor.chain().focus().redo().run()} + /> +
+ +
+ } onMouseDown={preserveSelection} onClick={() => editor.chain().focus().toggleBold().run()} /> + } onMouseDown={preserveSelection} onClick={() => editor.chain().focus().toggleItalic().run()} /> + } onMouseDown={preserveSelection} onClick={() => editor.chain().focus().toggleUnderline().run()} /> + } onMouseDown={preserveSelection} onClick={() => editor.chain().focus().toggleStrike().run()} /> + } onMouseDown={preserveSelection} onClick={() => editor.chain().focus().toggleCode().run()} /> +
+
+ } onMouseDown={preserveSelection} onClick={() => editor.chain().focus().toggleBulletList().run()} /> + } onMouseDown={preserveSelection} onClick={() => editor.chain().focus().toggleOrderedList().run()} /> + } onMouseDown={preserveSelection} onClick={() => editor.chain().focus().toggleBlockquote().run()} /> +
+
+ } disabled={disabled} onMouseDown={preserveSelection} onClick={openLinkDialog} /> + } disabled={disabled || !toolbarState.link} onMouseDown={preserveSelection} onClick={() => editor.chain().focus().unsetLink().run()} /> + } disabled={disabled} onMouseDown={preserveSelection} onClick={openImageDialog} /> + } disabled={disabled} onMouseDown={preserveSelection} onClick={() => editor.chain().focus().unsetAllMarks().clearNodes().run()} /> +
+
+ )} + {allowSourceMode && ( + + )} +
+ + {mode === "visual" ? ( + + ) : ( + <> + {unsupportedSource &&

{translatedLabels.sourceWarning}

} +