Compare commits

...
2 Commits
Author SHA1 Message Date
zemion 6e518fa6a2 feat: compose stable product surfaces
Module Package Release / publish-packages (push) Successful in 13s
2026-08-24 13:21:24 +02:00
zemion f98cf9ced8 feat(help): enforce owner-aware high-risk contexts
Module Package Release / publish-packages (push) Successful in 13s
2026-08-24 11:48:23 +02:00
32 changed files with 1306 additions and 228 deletions
+11
View File
@@ -58,6 +58,17 @@ than adding custom `F1` listeners:
headed pages. `WorkspaceLayout` owns the full-canvas workspace scope and its headed pages. `WorkspaceLayout` owns the full-canvas workspace scope and its
labelled primary/content panes; pages inside it use `PageLayout` in labelled primary/content panes; pages inside it use `PageLayout` in
`workspace` mode and retain their own route-level help identity. `workspace` mode and retain their own route-level help identity.
- `PasswordField` passes its owner context and module through reveal/generate
actions and the shared generator dialog. Credential consumers must supply an
exact owner context; the generic component does not own credential policy.
High-risk controls use one of the source-inventory risk classes (`authority`,
`credential`, `disclosure`, `encryption`, `external-effect`, `irreversible`,
`policy`, or `retention`) and require exact F1 help. The extractor infers
obvious cases conservatively; components may declare `data-help-risk`
explicitly or mark a reviewed ordinary control with
`data-help-risk-reviewed="standard"`. The strict workspace gate rejects new
unresolved high-risk debt.
Module routes, public routes, settings sections, and administration sections Module routes, public routes, settings sections, and administration sections
may also declare `helpContextId` and `helpTopicId`. Each module must keep a may also declare `helpContextId` and `helpTopicId`. Each module must keep a
+13
View File
@@ -75,6 +75,17 @@ native control nested in `FormField`. Dynamic context expressions remain
separate evidence and generic derived fallbacks remain in the richer-help separate evidence and generic derived fallbacks remain in the richer-help
candidate queue. candidate queue.
The same inventory classifies controls whose labels, identities, component
context, or explicit `data-help-risk` indicate authority, credentials,
disclosure, encryption, external effects, irreversible changes, policy, or
retention. These controls require an exact context rather than relying only on
page fallback. Reviewed false positives carry
`data-help-risk-reviewed="standard"`. Invalid risk classes and any increase
above the versioned `tools/inventory/high-risk-help-baseline.json` ceiling fail
strict declaration checks; the ceiling is lowered as the finite queue is
resolved. Password fields and their generator dialog propagate the owning
field's context so shared credential controls never invent a Core-owned topic.
The generated `help_review_candidates` list is therefore a content-depth queue, The generated `help_review_candidates` list is therefore a content-depth queue,
not a list of controls on which F1 cannot work. It should prioritize: not a list of controls on which F1 cannot work. It should prioritize:
@@ -109,6 +120,8 @@ The check must report:
- no duplicate stable IDs; - no duplicate stable IDs;
- no undeclared public WebUI surface; - no undeclared public WebUI surface;
- no stale runtime route or endpoint declaration. - no stale runtime route or endpoint declaration.
- no invalid high-risk help annotation or regression above the recorded
exact-context debt ceiling.
Browser acceptance is part of the focused workspace gate and can be run alone: Browser acceptance is part of the focused workspace gate and can be run alone:
+26
View File
@@ -1030,6 +1030,32 @@ Any future exception is extraction debt and must be temporary, documented in the
script with a reason, and removed when a capability/API/event contract replaces script with a reason, and removed when a capability/API/event contract replaces
it. it.
## Product Surface Contributions
`FrontendModule.product_surfaces` is the versioned product-composition contract
for stable identities that may have one or more technical owners. A contribution
declares contract version 1, a product identity, common label/icon/description,
stable entry path, owner route and View surfaces, supported task/reader/admin/
operator presentations, authorization requirements, capabilities, search
sources, help contexts, documentation topics, migration aliases, and standard
unavailable/degraded explanations.
Core validates every reference against the owning manifest. Contributors that
share an identity must agree on its common product metadata and entry path;
entry and alias paths cannot belong to another product identity. The WebUI
composes valid owners by product id, filters them through authorization and the
effective View, and resolves the stable entry or migration alias to the first
available owner route. It emits `govoplan:product-surface-route-resolved` before
the redirect so migration telemetry can observe alias use without making the
technical module part of the ordinary label.
Use `ProductAvailabilityState` for unavailable and degraded outcomes. The
ordinary state explains the attempted outcome, consequence, recovery path and
responsible role. Exact module, capability, provider and correlation values may
be supplied as a collapsed technical detail; they are not the primary error.
The state is presentation only and never grants authority or changes provider
health.
## Boundary Decision Register ## Boundary Decision Register
These durable decisions close older exploratory core issues. Implementation These durable decisions close older exploratory core issues. Implementation
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "govoplan-core" name = "govoplan-core"
version = "0.1.38" version = "0.1.40"
description = "Reusable GovOPlaN platform core, access, tenancy, and RBAC components." description = "Reusable GovOPlaN platform core, access, tenancy, and RBAC components."
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
+51
View File
@@ -30,6 +30,7 @@ if TYPE_CHECKING:
SUPPORTED_MANIFEST_CONTRACT_VERSION = "1" SUPPORTED_MANIFEST_CONTRACT_VERSION = "1"
SUPPORTED_FRONTEND_ASSET_MANIFEST_CONTRACT_VERSION = "1" SUPPORTED_FRONTEND_ASSET_MANIFEST_CONTRACT_VERSION = "1"
SUPPORTED_PRESENTATION_CONTRACT_VERSION = "1" SUPPORTED_PRESENTATION_CONTRACT_VERSION = "1"
SUPPORTED_PRODUCT_SURFACE_CONTRACT_VERSION = "1"
PermissionLevel = Literal["system", "tenant"] PermissionLevel = Literal["system", "tenant"]
SubjectType = Literal["account", "membership", "group", "service_account", "tenant"] SubjectType = Literal["account", "membership", "group", "service_account", "tenant"]
@@ -114,6 +115,55 @@ class ProductAreaContribution:
order: int = 100 order: int = 100
ProductSurfacePresentation = Literal["task", "reader", "admin", "operator"]
ProductAvailabilityReason = Literal[
"authorization",
"policy",
"configuration",
"disabled",
"capability",
"offline",
"provider_degraded",
]
@dataclass(frozen=True, slots=True)
class ProductAvailabilityExplanation:
"""Explain a product outcome without making package topology user-facing."""
reason: ProductAvailabilityReason
title: str
description: str
resolution: str
responsible_role: str | None = None
@dataclass(frozen=True, slots=True)
class ProductSurfaceContribution:
"""Bind an owner route to a stable, cross-module product identity."""
id: str
module_id: str
label: str
icon: str
entry_path: str
route_path: str
surface_ids: tuple[str, ...]
unavailable: ProductAvailabilityExplanation
description: str | None = None
degraded: ProductAvailabilityExplanation | None = None
presentations: tuple[ProductSurfacePresentation, ...] = ("task",)
capability_ids: tuple[str, ...] = ()
search_source_ids: tuple[str, ...] = ()
help_context_ids: tuple[str, ...] = ()
documentation_topic_ids: tuple[str, ...] = ()
required_all: tuple[str, ...] = ()
required_any: tuple[str, ...] = ()
aliases: tuple[str, ...] = ()
order: int = 100
contract_version: str = SUPPORTED_PRODUCT_SURFACE_CONTRACT_VERSION
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class QuickAccessTool: class QuickAccessTool:
"""Declare a versioned, bounded module-owned Quick Access tool.""" """Declare a versioned, bounded module-owned Quick Access tool."""
@@ -153,6 +203,7 @@ class FrontendModule:
settings_routes: tuple[FrontendRoute, ...] = () settings_routes: tuple[FrontendRoute, ...] = ()
view_surfaces: tuple[ViewSurface, ...] = () view_surfaces: tuple[ViewSurface, ...] = ()
product_areas: tuple[ProductAreaContribution, ...] = () product_areas: tuple[ProductAreaContribution, ...] = ()
product_surfaces: tuple[ProductSurfaceContribution, ...] = ()
quick_access_tools: tuple[QuickAccessTool, ...] = () quick_access_tools: tuple[QuickAccessTool, ...] = ()
@@ -22,6 +22,7 @@ PlatformInterfaceKind = Literal[
"navigation", "navigation",
"permission", "permission",
"product_area", "product_area",
"product_surface",
"provided_interface", "provided_interface",
"public_route", "public_route",
"search_provider", "search_provider",
@@ -237,6 +238,41 @@ def manifest_interface_declarations(
}, },
) )
) )
for surface in frontend.product_surfaces:
declarations.append(
PlatformInterfaceDeclaration(
id=f"{manifest.id}.{surface.id}",
module_id=manifest.id,
kind="product_surface",
label=surface.label,
path=surface.route_path,
required_all=surface.required_all,
required_any=surface.required_any,
metadata={
"contract_version": surface.contract_version,
"product_surface_id": surface.id,
"description": surface.description,
"icon": surface.icon,
"entry_path": surface.entry_path,
"surface_ids": list(surface.surface_ids),
"presentations": list(surface.presentations),
"capability_ids": list(surface.capability_ids),
"search_source_ids": list(surface.search_source_ids),
"help_context_ids": list(surface.help_context_ids),
"documentation_topic_ids": list(
surface.documentation_topic_ids
),
"aliases": list(surface.aliases),
"order": surface.order,
"unavailable_reason": surface.unavailable.reason,
"degraded_reason": (
surface.degraded.reason
if surface.degraded is not None
else None
),
},
)
)
for tool in frontend.quick_access_tools: for tool in frontend.quick_access_tools:
declarations.append( declarations.append(
PlatformInterfaceDeclaration( PlatformInterfaceDeclaration(
+198
View File
@@ -16,13 +16,16 @@ from govoplan_core.core.modules import (
ModuleManifest, ModuleManifest,
NavItem, NavItem,
PermissionDefinition, PermissionDefinition,
ProductAvailabilityExplanation,
ProductAreaContribution, ProductAreaContribution,
ProductSurfaceContribution,
PublicFrontendRoute, PublicFrontendRoute,
QuickAccessTool, QuickAccessTool,
ResourceAclProvider, ResourceAclProvider,
RoleTemplate, RoleTemplate,
SUPPORTED_FRONTEND_ASSET_MANIFEST_CONTRACT_VERSION, SUPPORTED_FRONTEND_ASSET_MANIFEST_CONTRACT_VERSION,
SUPPORTED_MANIFEST_CONTRACT_VERSION, SUPPORTED_MANIFEST_CONTRACT_VERSION,
SUPPORTED_PRODUCT_SURFACE_CONTRACT_VERSION,
TenantSummaryBatchProvider, TenantSummaryBatchProvider,
TenantSummaryProvider, TenantSummaryProvider,
user_workflow_scope_condition_issues, user_workflow_scope_condition_issues,
@@ -90,6 +93,9 @@ _WILDCARD_RE = re.compile(
) )
_INTERFACE_NAME_RE = re.compile(r"^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+$") _INTERFACE_NAME_RE = re.compile(r"^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+$")
_PRESENTATION_ID_RE = re.compile(r"^[a-z][a-z0-9_-]{1,79}$") _PRESENTATION_ID_RE = re.compile(r"^[a-z][a-z0-9_-]{1,79}$")
_PRODUCT_SURFACE_ID_RE = re.compile(
r"^[a-z][a-z0-9_-]*(?:\.[a-z][a-z0-9_-]*)+$"
)
_QUICK_ACCESS_TOOL_ID_RE = re.compile( _QUICK_ACCESS_TOOL_ID_RE = re.compile(
r"^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_-]*)+$" r"^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_-]*)+$"
) )
@@ -974,7 +980,19 @@ def _validate_manifest_shape(manifest: ModuleManifest) -> None:
def _validate_presentation_catalog(manifests: tuple[ModuleManifest, ...]) -> None: def _validate_presentation_catalog(manifests: tuple[ModuleManifest, ...]) -> None:
area_definitions: dict[str, tuple[str, str]] = {} area_definitions: dict[str, tuple[str, str]] = {}
surface_definitions: dict[str, tuple[str, str, str, str | None]] = {}
product_paths: dict[str, str] = {}
tool_owners: dict[str, str] = {} tool_owners: dict[str, str] = {}
concrete_paths = {
route.path: manifest.id
for manifest in manifests
if manifest.frontend is not None
for route in (
*manifest.frontend.routes,
*manifest.frontend.settings_routes,
*manifest.frontend.public_routes,
)
}
for manifest in manifests: for manifest in manifests:
frontend = manifest.frontend frontend = manifest.frontend
if frontend is None: if frontend is None:
@@ -987,6 +1005,33 @@ def _validate_presentation_catalog(manifests: tuple[ModuleManifest, ...]) -> Non
f"Product area {area.id!r} has conflicting labels or icons" f"Product area {area.id!r} has conflicting labels or icons"
) )
area_definitions[area.id] = definition area_definitions[area.id] = definition
for surface in frontend.product_surfaces:
definition = (
surface.label,
surface.icon,
surface.entry_path,
surface.description,
)
previous = surface_definitions.get(surface.id)
if previous is not None and previous != definition:
raise RegistryError(
f"Product surface {surface.id!r} has conflicting product identity metadata"
)
surface_definitions[surface.id] = definition
for path in (surface.entry_path, *surface.aliases):
concrete_owner = concrete_paths.get(path)
if concrete_owner is not None:
raise RegistryError(
f"Product path {path!r} collides with a concrete route "
f"owned by module {concrete_owner!r}"
)
previous_id = product_paths.get(path)
if previous_id is not None and previous_id != surface.id:
raise RegistryError(
f"Product path {path!r} is shared by product surfaces "
f"{previous_id!r} and {surface.id!r}"
)
product_paths[path] = surface.id
for tool in frontend.quick_access_tools: for tool in frontend.quick_access_tools:
previous_owner = tool_owners.get(tool.id) previous_owner = tool_owners.get(tool.id)
if previous_owner is not None: if previous_owner is not None:
@@ -1474,6 +1519,14 @@ def _validate_presentation_contributions(manifest: ModuleManifest) -> None:
f"in module {manifest.id!r}" f"in module {manifest.id!r}"
) )
seen_area_memberships.add(membership) seen_area_memberships.add(membership)
seen_product_surfaces: set[str] = set()
for surface in frontend.product_surfaces:
_validate_product_surface(manifest, surface, known_surface_ids)
if surface.id in seen_product_surfaces:
raise RegistryError(
f"Duplicate product surface {surface.id!r} in module {manifest.id!r}"
)
seen_product_surfaces.add(surface.id)
seen_tools: set[str] = set() seen_tools: set[str] = set()
for tool in frontend.quick_access_tools: for tool in frontend.quick_access_tools:
_validate_quick_access_tool(manifest.id, tool, known_surface_ids) _validate_quick_access_tool(manifest.id, tool, known_surface_ids)
@@ -1512,6 +1565,151 @@ def _validate_product_area(
) )
def _validate_product_surface(
manifest: ModuleManifest,
surface: ProductSurfaceContribution,
known_surface_ids: set[str],
) -> None:
module_id = manifest.id
frontend = manifest.frontend
assert frontend is not None
if surface.module_id != module_id:
raise RegistryError(
f"Product surface {surface.id!r} belongs to {surface.module_id!r}, "
f"not module {module_id!r}"
)
if not _PRODUCT_SURFACE_ID_RE.fullmatch(surface.id):
raise RegistryError(f"Invalid product surface id: {surface.id!r}")
if surface.contract_version != SUPPORTED_PRODUCT_SURFACE_CONTRACT_VERSION:
raise RegistryError(
f"Product surface {surface.id!r} uses unsupported contract version "
f"{surface.contract_version!r}"
)
if not surface.label.strip() or not surface.icon.strip():
raise RegistryError(
f"Product surface {surface.id!r} needs a label and icon"
)
for label, path in (
("entry", surface.entry_path),
("owner", surface.route_path),
*(("alias", alias) for alias in surface.aliases),
):
if not path.startswith("/") or "?" in path or "#" in path:
raise RegistryError(
f"Product surface {surface.id!r} has an invalid {label} path {path!r}"
)
if (
surface.entry_path == surface.route_path
or surface.entry_path in surface.aliases
or surface.route_path in surface.aliases
):
raise RegistryError(
f"Product surface {surface.id!r} must keep its stable entry distinct from owner and alias paths"
)
if len(set(surface.aliases)) != len(surface.aliases):
raise RegistryError(
f"Product surface {surface.id!r} contains duplicate aliases"
)
route_paths = {route.path for route in (*frontend.routes, *frontend.settings_routes)}
if surface.route_path not in route_paths:
raise RegistryError(
f"Product surface {surface.id!r} references unknown owner route "
f"{surface.route_path!r}"
)
if not surface.surface_ids:
raise RegistryError(
f"Product surface {surface.id!r} has no owner surfaces"
)
unknown_surfaces = set(surface.surface_ids) - known_surface_ids
if unknown_surfaces:
raise RegistryError(
f"Product surface {surface.id!r} references unknown surfaces: "
+ ", ".join(sorted(unknown_surfaces))
)
allowed_presentations = {"task", "reader", "admin", "operator"}
if (
not surface.presentations
or len(set(surface.presentations)) != len(surface.presentations)
or set(surface.presentations) - allowed_presentations
):
raise RegistryError(
f"Product surface {surface.id!r} has invalid presentations"
)
declared_capabilities = {
*manifest.required_capabilities,
*manifest.optional_capabilities,
*manifest.capability_factories,
*(provider.name for provider in manifest.provides_interfaces),
*(requirement.name for requirement in manifest.requires_interfaces),
}
unknown_capabilities = set(surface.capability_ids) - declared_capabilities
if unknown_capabilities:
raise RegistryError(
f"Product surface {surface.id!r} references undeclared capabilities: "
+ ", ".join(sorted(unknown_capabilities))
)
search_source_ids = {source.id for source in manifest.search_sources}
unknown_search_sources = set(surface.search_source_ids) - search_source_ids
if unknown_search_sources:
raise RegistryError(
f"Product surface {surface.id!r} references unknown search sources: "
+ ", ".join(sorted(unknown_search_sources))
)
topics = {topic.id: topic for topic in manifest.documentation}
unknown_topics = set(surface.documentation_topic_ids) - set(topics)
if unknown_topics:
raise RegistryError(
f"Product surface {surface.id!r} references unknown documentation topics: "
+ ", ".join(sorted(unknown_topics))
)
documented_help_contexts: set[str] = set()
for topic in manifest.documentation:
contexts = topic.metadata.get("help_contexts", ())
if isinstance(contexts, (list, tuple, set, frozenset)):
documented_help_contexts.update(
context for context in contexts if isinstance(context, str)
)
unknown_help = set(surface.help_context_ids) - documented_help_contexts
if unknown_help:
raise RegistryError(
f"Product surface {surface.id!r} references undocumented help contexts: "
+ ", ".join(sorted(unknown_help))
)
_validate_product_availability_explanation(surface.id, surface.unavailable)
if surface.degraded is not None:
_validate_product_availability_explanation(surface.id, surface.degraded)
def _validate_product_availability_explanation(
surface_id: str,
explanation: ProductAvailabilityExplanation,
) -> None:
allowed_reasons = {
"authorization",
"policy",
"configuration",
"disabled",
"capability",
"offline",
"provider_degraded",
}
if explanation.reason not in allowed_reasons:
raise RegistryError(
f"Product surface {surface_id!r} has an invalid availability reason"
)
if any(
not value.strip()
for value in (
explanation.title,
explanation.description,
explanation.resolution,
)
):
raise RegistryError(
f"Product surface {surface_id!r} has an incomplete availability explanation"
)
def _validate_quick_access_tool( def _validate_quick_access_tool(
module_id: str, module_id: str,
tool: QuickAccessTool, tool: QuickAccessTool,
+47
View File
@@ -22,7 +22,9 @@ from govoplan_core.core.modules import (
FrontendRoute, FrontendRoute,
ModuleManifest, ModuleManifest,
NavItem, NavItem,
ProductAvailabilityExplanation,
ProductAreaContribution, ProductAreaContribution,
ProductSurfaceContribution,
PublicFrontendRoute, PublicFrontendRoute,
QuickAccessTool, QuickAccessTool,
SUPPORTED_PRESENTATION_CONTRACT_VERSION, SUPPORTED_PRESENTATION_CONTRACT_VERSION,
@@ -254,6 +256,47 @@ def _product_area_payload(area: ProductAreaContribution) -> dict[str, object]:
} }
def _product_availability_payload(
explanation: ProductAvailabilityExplanation,
) -> dict[str, object]:
return {
"reason": explanation.reason,
"title": explanation.title,
"description": explanation.description,
"resolution": explanation.resolution,
"responsible_role": explanation.responsible_role,
}
def _product_surface_payload(surface: ProductSurfaceContribution) -> dict[str, object]:
return {
"contract_version": surface.contract_version,
"id": surface.id,
"module_id": surface.module_id,
"label": surface.label,
"description": surface.description,
"icon": surface.icon,
"entry_path": surface.entry_path,
"route_path": surface.route_path,
"surface_ids": list(surface.surface_ids),
"presentations": list(surface.presentations),
"capability_ids": list(surface.capability_ids),
"search_source_ids": list(surface.search_source_ids),
"help_context_ids": list(surface.help_context_ids),
"documentation_topic_ids": list(surface.documentation_topic_ids),
"required_all": list(surface.required_all),
"required_any": list(surface.required_any),
"aliases": list(surface.aliases),
"order": surface.order,
"unavailable": _product_availability_payload(surface.unavailable),
"degraded": (
_product_availability_payload(surface.degraded)
if surface.degraded is not None
else None
),
}
def _quick_access_tool_payload(tool: QuickAccessTool) -> dict[str, object]: def _quick_access_tool_payload(tool: QuickAccessTool) -> dict[str, object]:
return { return {
"id": tool.id, "id": tool.id,
@@ -372,6 +415,10 @@ def _frontend_payload(
"product_areas": [ "product_areas": [
_product_area_payload(area) for area in frontend.product_areas _product_area_payload(area) for area in frontend.product_areas
], ],
"product_surfaces": [
_product_surface_payload(surface)
for surface in frontend.product_surfaces
],
"quick_access_tools": [ "quick_access_tools": [
_quick_access_tool_payload(tool) for tool in frontend.quick_access_tools _quick_access_tool_payload(tool) for tool in frontend.quick_access_tools
], ],
+88
View File
@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import unittest import unittest
from dataclasses import replace
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
@@ -10,7 +11,9 @@ from govoplan_core.core.modules import (
FrontendModule, FrontendModule,
FrontendRoute, FrontendRoute,
ModuleManifest, ModuleManifest,
ProductAvailabilityExplanation,
ProductAreaContribution, ProductAreaContribution,
ProductSurfaceContribution,
QuickAccessTool, QuickAccessTool,
) )
from govoplan_core.core.registry import PlatformRegistry, RegistryError from govoplan_core.core.registry import PlatformRegistry, RegistryError
@@ -49,6 +52,26 @@ def presentation_manifest() -> ModuleManifest:
surface_ids=("example.route.main",), surface_ids=("example.route.main",),
), ),
), ),
product_surfaces=(
ProductSurfaceContribution(
id="work.examples",
module_id="example",
label="Examples",
description="Review and update governed examples.",
icon="list-checks",
entry_path="/work/examples",
route_path="/example",
surface_ids=("example.route.main",),
presentations=("task", "reader"),
unavailable=ProductAvailabilityExplanation(
reason="authorization",
title="Examples are unavailable",
description="Your current responsibility does not include examples.",
resolution="Ask the responsible administrator to review your assignment.",
responsible_role="Access administrator",
),
),
),
quick_access_tools=( quick_access_tools=(
QuickAccessTool( QuickAccessTool(
id="example.summary", id="example.summary",
@@ -121,6 +144,16 @@ class PresentationContractTests(unittest.TestCase):
frontend = response.json()["modules"][0]["frontend"] frontend = response.json()["modules"][0]["frontend"]
self.assertEqual("1", frontend["presentation_contract_version"]) self.assertEqual("1", frontend["presentation_contract_version"])
self.assertEqual("work", frontend["product_areas"][0]["id"]) self.assertEqual("work", frontend["product_areas"][0]["id"])
product_surface = frontend["product_surfaces"][0]
self.assertEqual("1", product_surface["contract_version"])
self.assertEqual("work.examples", product_surface["id"])
self.assertEqual("/work/examples", product_surface["entry_path"])
self.assertEqual("/example", product_surface["route_path"])
self.assertEqual(["task", "reader"], product_surface["presentations"])
self.assertEqual(
"authorization",
product_surface["unavailable"]["reason"],
)
self.assertEqual("example.summary", frontend["quick_access_tools"][0]["id"]) self.assertEqual("example.summary", frontend["quick_access_tools"][0]["id"])
self.assertEqual("1", frontend["quick_access_tools"][0]["contract_version"]) self.assertEqual("1", frontend["quick_access_tools"][0]["contract_version"])
self.assertEqual( self.assertEqual(
@@ -132,6 +165,61 @@ class PresentationContractTests(unittest.TestCase):
frontend["quick_access_tools"][0]["help_context_id"], frontend["quick_access_tools"][0]["help_context_id"],
) )
def test_registry_rejects_product_surface_without_owner_route(self) -> None:
manifest = presentation_manifest()
frontend = manifest.frontend
assert frontend is not None
surface = frontend.product_surfaces[0]
invalid = ModuleManifest(
id=manifest.id,
name=manifest.name,
version=manifest.version,
frontend=FrontendModule(
module_id=manifest.id,
routes=frontend.routes,
product_surfaces=(
ProductSurfaceContribution(
id=surface.id,
module_id=surface.module_id,
label=surface.label,
description=surface.description,
icon=surface.icon,
entry_path=surface.entry_path,
route_path="/missing",
surface_ids=surface.surface_ids,
unavailable=surface.unavailable,
),
),
),
)
registry = PlatformRegistry()
registry.register(invalid)
with self.assertRaisesRegex(RegistryError, "unknown owner route"):
registry.validate()
def test_registry_rejects_product_alias_that_shadows_a_route(self) -> None:
manifest = presentation_manifest()
frontend = manifest.frontend
assert frontend is not None
surface = frontend.product_surfaces[0]
invalid = replace(
manifest,
frontend=replace(
frontend,
routes=(
*frontend.routes,
FrontendRoute(path="/shortcut", component="ShortcutPage"),
),
product_surfaces=(replace(surface, aliases=("/shortcut",)),),
),
)
registry = PlatformRegistry()
registry.register(invalid)
with self.assertRaisesRegex(RegistryError, "collides with a concrete route"):
registry.validate()
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "@govoplan/core-webui", "name": "@govoplan/core-webui",
"version": "0.1.38", "version": "0.1.40",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@govoplan/core-webui", "name": "@govoplan/core-webui",
"version": "0.1.38", "version": "0.1.40",
"dependencies": { "dependencies": {
"@govoplan/access-webui": "file:../../govoplan-access/webui", "@govoplan/access-webui": "file:../../govoplan-access/webui",
"@govoplan/addresses-webui": "file:../../govoplan-addresses/webui", "@govoplan/addresses-webui": "file:../../govoplan-addresses/webui",
+190 -190
View File
@@ -1,30 +1,30 @@
{ {
"name": "@govoplan/core-webui", "name": "@govoplan/core-webui",
"version": "0.1.38", "version": "0.1.40",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@govoplan/core-webui", "name": "@govoplan/core-webui",
"version": "0.1.38", "version": "0.1.40",
"dependencies": { "dependencies": {
"@govoplan/access-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#v0.1.20", "@govoplan/access-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#v0.1.23",
"@govoplan/admin-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git#v0.1.19", "@govoplan/admin-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git#v0.1.22",
"@govoplan/audit-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git#v0.1.18", "@govoplan/audit-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git#v0.1.18",
"@govoplan/calendar-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git#v0.1.18", "@govoplan/calendar-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git#v0.1.22",
"@govoplan/campaign-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git#v0.1.22", "@govoplan/campaign-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git#v0.1.27",
"@govoplan/cases-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-cases.git#v0.1.20", "@govoplan/cases-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-cases.git#v0.1.20",
"@govoplan/dashboard-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-dashboard.git#v0.1.18", "@govoplan/dashboard-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-dashboard.git#v0.1.18",
"@govoplan/docs-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git#v0.1.18", "@govoplan/docs-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git#v0.1.22",
"@govoplan/files-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git#v0.1.20", "@govoplan/files-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git#v0.1.23",
"@govoplan/helpdesk-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-helpdesk.git#v0.1.20", "@govoplan/helpdesk-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-helpdesk.git#v0.1.20",
"@govoplan/idm-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git#v0.1.19", "@govoplan/idm-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git#v0.1.24",
"@govoplan/mail-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git#v0.1.18", "@govoplan/mail-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git#v0.1.25",
"@govoplan/ops-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-ops.git#v0.1.18", "@govoplan/ops-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-ops.git#v0.1.18",
"@govoplan/organizations-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git#v0.1.18", "@govoplan/organizations-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git#v0.1.20",
"@govoplan/policy-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git#v0.1.18", "@govoplan/policy-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git#v0.1.18",
"@govoplan/tickets-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-tickets.git#v0.1.20", "@govoplan/tickets-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-tickets.git#v0.1.22",
"@govoplan/wiki-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-wiki.git#v0.1.20", "@govoplan/wiki-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-wiki.git#v0.1.22",
"@tiptap/core": "^3.29.2", "@tiptap/core": "^3.29.2",
"@tiptap/extension-image": "^3.29.2", "@tiptap/extension-image": "^3.29.2",
"@tiptap/pm": "^3.29.2", "@tiptap/pm": "^3.29.2",
@@ -757,8 +757,8 @@
"optional": true "optional": true
}, },
"node_modules/@govoplan/access-webui": { "node_modules/@govoplan/access-webui": {
"version": "0.1.20", "version": "0.1.23",
"resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#206873b62a9c77ac5715f9ce7d6bc16d156efa74", "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#e55434f406e953a2fa9e881abb8fb6dccbabd812",
"peerDependencies": { "peerDependencies": {
"@govoplan/core-webui": "^0.1.18", "@govoplan/core-webui": "^0.1.18",
"lucide-react": "^1.23.0", "lucide-react": "^1.23.0",
@@ -773,8 +773,8 @@
} }
}, },
"node_modules/@govoplan/admin-webui": { "node_modules/@govoplan/admin-webui": {
"version": "0.1.19", "version": "0.1.22",
"resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git#ed424c729cd0c1c7a1cbec387a793db0819c91fe", "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git#3b450640690c5f1e123db4095663c7d5287873bb",
"peerDependencies": { "peerDependencies": {
"@govoplan/core-webui": "^0.1.35", "@govoplan/core-webui": "^0.1.35",
"lucide-react": "^1.23.0", "lucide-react": "^1.23.0",
@@ -805,8 +805,8 @@
} }
}, },
"node_modules/@govoplan/calendar-webui": { "node_modules/@govoplan/calendar-webui": {
"version": "0.1.18", "version": "0.1.22",
"resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git#089d07b60a99f76f2ef6d72de8c33fd263bc7bac", "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git#3792e9ab8a10ff6e301c59df7a3fa47d5ab14952",
"peerDependencies": { "peerDependencies": {
"@govoplan/core-webui": "^0.1.18", "@govoplan/core-webui": "^0.1.18",
"@vitejs/plugin-react": "^5.2.0", "@vitejs/plugin-react": "^5.2.0",
@@ -824,8 +824,8 @@
} }
}, },
"node_modules/@govoplan/campaign-webui": { "node_modules/@govoplan/campaign-webui": {
"version": "0.1.18", "version": "0.1.27",
"resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git#4f52f010ee6a117b7b3ad605a59b1da0a459a0c6", "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git#1b32427813b435fd47c92c95242d6029741743ec",
"dependencies": { "dependencies": {
"read-excel-file": "9.2.0" "read-excel-file": "9.2.0"
}, },
@@ -875,8 +875,8 @@
} }
}, },
"node_modules/@govoplan/docs-webui": { "node_modules/@govoplan/docs-webui": {
"version": "0.1.18", "version": "0.1.22",
"resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git#30ea95854b9c65cb5d0fdd1d281a07ad1330e290", "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git#9055f3437f0296f15f63a6fc89ff65f133a7bfaf",
"peerDependencies": { "peerDependencies": {
"@govoplan/core-webui": "^0.1.18", "@govoplan/core-webui": "^0.1.18",
"@vitejs/plugin-react": "^5.2.0", "@vitejs/plugin-react": "^5.2.0",
@@ -894,8 +894,8 @@
} }
}, },
"node_modules/@govoplan/files-webui": { "node_modules/@govoplan/files-webui": {
"version": "0.1.20", "version": "0.1.23",
"resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git#378f4d6ac5525d0f07f38eefce893bfa2924f2b9", "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git#11b9b7c4c6d919b9f6649034445edfea0c77d042",
"peerDependencies": { "peerDependencies": {
"@govoplan/core-webui": "^0.1.18", "@govoplan/core-webui": "^0.1.18",
"@vitejs/plugin-react": "^5.2.0", "@vitejs/plugin-react": "^5.2.0",
@@ -929,8 +929,8 @@
} }
}, },
"node_modules/@govoplan/idm-webui": { "node_modules/@govoplan/idm-webui": {
"version": "0.1.18", "version": "0.1.24",
"resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git#b0eda351957526bc7e59270c7a42016aa12bc1a3", "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git#5c7586f6d9a0d0b3e8d36ab47dca586e0a34a498",
"peerDependencies": { "peerDependencies": {
"@govoplan/core-webui": "^0.1.18", "@govoplan/core-webui": "^0.1.18",
"@vitejs/plugin-react": "^5.2.0", "@vitejs/plugin-react": "^5.2.0",
@@ -948,8 +948,8 @@
} }
}, },
"node_modules/@govoplan/mail-webui": { "node_modules/@govoplan/mail-webui": {
"version": "0.1.18", "version": "0.1.25",
"resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git#62721d204d946fa06160a5437d51de53428e1222", "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git#ecc1283de76f154919c786cb84354afb8f2299c1",
"peerDependencies": { "peerDependencies": {
"@govoplan/core-webui": "^0.1.18", "@govoplan/core-webui": "^0.1.18",
"lucide-react": "^1.23.0", "lucide-react": "^1.23.0",
@@ -983,8 +983,8 @@
} }
}, },
"node_modules/@govoplan/organizations-webui": { "node_modules/@govoplan/organizations-webui": {
"version": "0.1.18", "version": "0.1.20",
"resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git#30fcdbe8327ca56f6561b00a04ecca4572fcb69e", "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git#b23e6e4eeedd17d322744a765a473b5f77c08b61",
"peerDependencies": { "peerDependencies": {
"@govoplan/core-webui": "^0.1.18", "@govoplan/core-webui": "^0.1.18",
"@vitejs/plugin-react": "^5.2.0", "@vitejs/plugin-react": "^5.2.0",
@@ -1018,8 +1018,8 @@
} }
}, },
"node_modules/@govoplan/tickets-webui": { "node_modules/@govoplan/tickets-webui": {
"version": "0.1.20", "version": "0.1.22",
"resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-tickets.git#c6c643778bc73d0504e9f1ef5b710ca98436ae41", "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-tickets.git#0ad2ef96b43a83ea7e0e9188487392db735b4a85",
"peerDependencies": { "peerDependencies": {
"@govoplan/core-webui": "^0.1.30", "@govoplan/core-webui": "^0.1.30",
"lucide-react": "^1.23.0", "lucide-react": "^1.23.0",
@@ -1034,8 +1034,8 @@
} }
}, },
"node_modules/@govoplan/wiki-webui": { "node_modules/@govoplan/wiki-webui": {
"version": "0.1.20", "version": "0.1.22",
"resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-wiki.git#66c91351c9eb693ace606c9b69dd5cd804d7531b", "resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-wiki.git#705255f378edf7643cd84fe5692cf2a0378b91a2",
"peerDependencies": { "peerDependencies": {
"@govoplan/core-webui": "^0.1.31", "@govoplan/core-webui": "^0.1.31",
"lucide-react": "^1.23.0", "lucide-react": "^1.23.0",
@@ -1484,49 +1484,49 @@
] ]
}, },
"node_modules/@tiptap/core": { "node_modules/@tiptap/core": {
"version": "3.30.2", "version": "3.30.3",
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.30.2.tgz", "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.30.3.tgz",
"integrity": "sha512-QbZC/s1OOqcoUdkhIY16TjR/gCtR0qAk9e4bJwUqOJqZuv5ozqCL5hzWm22jjTPp6c6Ei2tPd6t30VwfIKW4lQ==", "integrity": "sha512-kDD8KY99lBCKntCqTBE9eNR1ul/i/wPFw2METWT+LYZvifljXq2oiX6JaGF1Sk59efe7+sq9IISxOk47YlBiWQ==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/pm": "3.30.2" "@tiptap/pm": "3.30.3"
} }
}, },
"node_modules/@tiptap/extension-blockquote": { "node_modules/@tiptap/extension-blockquote": {
"version": "3.30.2", "version": "3.30.3",
"resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.30.2.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.30.3.tgz",
"integrity": "sha512-BOkwhZenek7vzXBOgKppSrlx4YryBdAYu1p1MXKn0R9A9eNmE2HVhmm0gG49+E8BhsE/TG8wKVclwET42JJiIg==", "integrity": "sha512-Dh8yfEqBKTqEdBKZ4Ta3DkTecP0VJxvRNA5b1LCF7gBbe54Lm3A28UuVvK/Sdhh9FJDrIVCQTDS0yHGbi6G9ew==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.30.2", "@tiptap/core": "3.30.3",
"@tiptap/pm": "3.30.2" "@tiptap/pm": "3.30.3"
} }
}, },
"node_modules/@tiptap/extension-bold": { "node_modules/@tiptap/extension-bold": {
"version": "3.30.2", "version": "3.30.3",
"resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.30.2.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.30.3.tgz",
"integrity": "sha512-MsvJhPgYejY2D9MhwYJv8AmscozvLBI8qtJ7YLdYZBWkMR4bgmxHq5+xqEfBsao9bOMMwBon9p3+P+/Tq5ReWA==", "integrity": "sha512-a4BSAjWRN4mWklRTdDCIaG9R+PqyUxXNYGQ2CLtOhO51Wq6daw2qFTHR+cTqAjaiiUSuDYII86UChb2qQRq8Lg==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.30.2" "@tiptap/core": "3.30.3"
} }
}, },
"node_modules/@tiptap/extension-bubble-menu": { "node_modules/@tiptap/extension-bubble-menu": {
"version": "3.30.2", "version": "3.30.3",
"resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.30.2.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.30.3.tgz",
"integrity": "sha512-oS0WiWNXHKpiPYMkcnHm1j7iEvufTGGtLFtcJJd3olb5OS6V2acoXVDL0nNJDmRQ32K3QXut3fbRcMseMxT+lw==", "integrity": "sha512-YryCf9fq+9n1XIW6f5weSCD2MbL/LhRQ7jArRToQQ/oNxc422LqYVjijQQTNuk2cZEqE9fdO3Hw27tmwEEs9UQ==",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@@ -1537,80 +1537,80 @@
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.30.2", "@tiptap/core": "3.30.3",
"@tiptap/pm": "3.30.2" "@tiptap/pm": "3.30.3"
} }
}, },
"node_modules/@tiptap/extension-bullet-list": { "node_modules/@tiptap/extension-bullet-list": {
"version": "3.30.2", "version": "3.30.3",
"resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.30.2.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.30.3.tgz",
"integrity": "sha512-+awIL/TUz4aB3rL68igU1rWfaaoBIAkPcakkktkRq8gYf0bd9eSb48P6kHkpx/3q+JyK7g9vsnltLNHNh6twnA==", "integrity": "sha512-Z/ZqUfrd3Fd8hHpHEPv3XRzqwqVLD4CP67xFMyjy0cqEgnjgvd1iR6QzJsKD79V9zyu0Agu7ab9ZBf4NktpWcw==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/extension-list": "3.30.2" "@tiptap/extension-list": "3.30.3"
} }
}, },
"node_modules/@tiptap/extension-code": { "node_modules/@tiptap/extension-code": {
"version": "3.30.2", "version": "3.30.3",
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.30.2.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.30.3.tgz",
"integrity": "sha512-r8EZk3R9yGpF6v5xxafAU1HwrD/e+RpbfnmVi2TeB/ZHAsVO62fW96E32G0t6IdaCtOFtAd85hkDAaOfCT4yGg==", "integrity": "sha512-mOV4Fg+ji6uXmFxTMuug9WK6zk0ksNQepnuvoDR4g4d+tnz2y9iEbgOXFohVxhOXOLDLdNeKdwHxbHQdi5Ivbw==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.30.2" "@tiptap/core": "3.30.3"
} }
}, },
"node_modules/@tiptap/extension-code-block": { "node_modules/@tiptap/extension-code-block": {
"version": "3.30.2", "version": "3.30.3",
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.30.2.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.30.3.tgz",
"integrity": "sha512-9otGKaQZmePHrLXFtCtz+BYDn5z4sSumTkUqQIQHz0gVxwPoTi7g51RedwxvViTb/zu2XV5ROXYLHIxKxypMPg==", "integrity": "sha512-xT2cDil/ipy/LklPgv/JqSsXys1mjcqlIPDUetK3llB2DT7XZssYPV3qv278tqRO4pJDbrXk82I6Lh7vLwmIAg==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.30.2", "@tiptap/core": "3.30.3",
"@tiptap/pm": "3.30.2" "@tiptap/pm": "3.30.3"
} }
}, },
"node_modules/@tiptap/extension-document": { "node_modules/@tiptap/extension-document": {
"version": "3.30.2", "version": "3.30.3",
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.30.2.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.30.3.tgz",
"integrity": "sha512-+xIv67V+/2L1uvz98FAT5W7kWEfHwfNV3MD7b4UsKPU0lhcCWuVOXy0JB8yYmdNExqpI7xT9g3MWzREoBvBQSg==", "integrity": "sha512-B9gqrgM1uHjCKr/PSnnSl+bS+YQb5EN/PUW9KWp9nSgmIaqxXws2ufqowS65uTLJLqd7pwF4l5lUrsfhq0EKzw==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.30.2" "@tiptap/core": "3.30.3"
} }
}, },
"node_modules/@tiptap/extension-dropcursor": { "node_modules/@tiptap/extension-dropcursor": {
"version": "3.30.2", "version": "3.30.3",
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.30.2.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.30.3.tgz",
"integrity": "sha512-nyRKUmItATnKI9AiRChmjhcBbCEsNxRu+AaCz+cx8EvnAcNHsVRdNYL5PmBs3WlNA/Et4Eb2DG0hVQDnNF61eg==", "integrity": "sha512-xdY66lcQakBLvWbJQHeJy/Td0f10YRXLyFeOFzt9PODsX4nqwJMju9tpqcLIGp0waaztu8PGwZ81oSCdQldIHQ==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/extensions": "3.30.2" "@tiptap/extensions": "3.30.3"
} }
}, },
"node_modules/@tiptap/extension-floating-menu": { "node_modules/@tiptap/extension-floating-menu": {
"version": "3.30.2", "version": "3.30.3",
"resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.30.2.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.30.3.tgz",
"integrity": "sha512-A8PLvvh8W6PUMrqh+EpBerxm+Ucr0irGxJvwAnzYQmNGNIJ9U4OVgw4OcEU+9JH0gMmEzDcHzMPP2s/s1lIcyw==", "integrity": "sha512-4l5Qee1wBk2YBFEFbWHFhUt++6hDY7Yp81BkO3wY6KFToxs+cm9JIm3xr3KF9UJ/vnUpIhBWIbZl3uKf37SJLA==",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"funding": { "funding": {
@@ -1619,93 +1619,93 @@
}, },
"peerDependencies": { "peerDependencies": {
"@floating-ui/dom": "^1.0.0", "@floating-ui/dom": "^1.0.0",
"@tiptap/core": "3.30.2", "@tiptap/core": "3.30.3",
"@tiptap/pm": "3.30.2" "@tiptap/pm": "3.30.3"
} }
}, },
"node_modules/@tiptap/extension-gapcursor": { "node_modules/@tiptap/extension-gapcursor": {
"version": "3.30.2", "version": "3.30.3",
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.30.2.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.30.3.tgz",
"integrity": "sha512-7Xk0ut6FM+RAsvKxDN3bAtk7zvYZ6Aa8pawJ6s7dLAmLR9JwrZevlcL4FSrj4bR7rqKOj92RYCFxzgTEbWimag==", "integrity": "sha512-+mwm64+RiArd1G8xSgtaTrvx1X5Lzz6sJ93zTmaiMVNc2N29AN77BVPTJ0e+853+/F6Bec/vOnVkIjTLg/ZBRw==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/extensions": "3.30.2" "@tiptap/extensions": "3.30.3"
} }
}, },
"node_modules/@tiptap/extension-hard-break": { "node_modules/@tiptap/extension-hard-break": {
"version": "3.30.2", "version": "3.30.3",
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.30.2.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.30.3.tgz",
"integrity": "sha512-IxSNgmG3d4OZdUTeebrOI7SxdIWXXJqlcGiSNDabWqxipUitfy3mZ3gDDE6G01koKxZRbhz4KIplAZlpxnTFSg==", "integrity": "sha512-/tX+IFW2C4RJltbwcn8/4zoHwj5YycRNOugb4Ul9Fl1AOJqjNwRx8xc0NpaOjuADhlMYMczEtSaOiDh5ZEOmjg==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.30.2" "@tiptap/core": "3.30.3"
} }
}, },
"node_modules/@tiptap/extension-heading": { "node_modules/@tiptap/extension-heading": {
"version": "3.30.2", "version": "3.30.3",
"resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.30.2.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.30.3.tgz",
"integrity": "sha512-PblDvgSJ05p1t6hzyPi02xeiBjB0M2abReoGEImqSWCy79UqnAGacgsZo4EeEawtJV1NEP8chhvmX+nRtzdT1A==", "integrity": "sha512-y+0u9qdrUAOGNrP52hAEiH1mvr1ZmzmkCBICICiXRH4+QNXpKkiBK3zlbDOPVQj0g0YIg6XIIU0vIaUBDPIaMQ==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.30.2" "@tiptap/core": "3.30.3"
} }
}, },
"node_modules/@tiptap/extension-horizontal-rule": { "node_modules/@tiptap/extension-horizontal-rule": {
"version": "3.30.2", "version": "3.30.3",
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.30.2.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.30.3.tgz",
"integrity": "sha512-j8aswLTsuEdJKC62DF+kw0EgvIRL7QMUyAVp2fdjR0qgM0ZVlEwCC4qIEq3kK9tFVU4kRtQ5BSj/jn6QwrlbCA==", "integrity": "sha512-J/ioKlXu5oJ+pPtybFonByx1LusbR0PUd1nkx6XsZqY9wyW6uecZQgAyI8KUWIXqOfhnAW38UTsHMBRdm2XupA==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.30.2", "@tiptap/core": "3.30.3",
"@tiptap/pm": "3.30.2" "@tiptap/pm": "3.30.3"
} }
}, },
"node_modules/@tiptap/extension-image": { "node_modules/@tiptap/extension-image": {
"version": "3.30.2", "version": "3.30.3",
"resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-3.30.2.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-3.30.3.tgz",
"integrity": "sha512-K/BPlWauHXI6Y4s7se2A1BLcZ2pnWmRQTEDkPJYe/8kmv1GyJzPu34sZed5Mvfu5chUH8u6aNZ/utZbvSbI/0Q==", "integrity": "sha512-vbKMliRLXkABwnPenPlQDEVp3MFN7Hs+WUh1eliVmYfsBfhEo596ahahv+JVoR9JW6HSM+LVdf2TT19Iqmh0sg==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.30.2" "@tiptap/core": "3.30.3"
} }
}, },
"node_modules/@tiptap/extension-italic": { "node_modules/@tiptap/extension-italic": {
"version": "3.30.2", "version": "3.30.3",
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.30.2.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.30.3.tgz",
"integrity": "sha512-pp8uaiuXsUbLm5rYzR1jlWbwm1mAahRajdHwAKBtthFRB2rDvC7ZWhKaCSoKhZvfIDRmu9/B67+uAHoutL0dCA==", "integrity": "sha512-dVWqJ/kDXdPHRBp4nU7bwi2G+c3oNruIgQHYVRgYlWkwW/opvnlACZLBUgEhihH7ANiSaGW1zLQ7HuUDSL3vog==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.30.2" "@tiptap/core": "3.30.3"
} }
}, },
"node_modules/@tiptap/extension-link": { "node_modules/@tiptap/extension-link": {
"version": "3.30.2", "version": "3.30.3",
"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.30.2.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.30.3.tgz",
"integrity": "sha512-jwdcymKcrbFpj5hRAuGVLCq8FieVkGFnENyroYmvkad+XAt8ZLy/MTFYRN6SK3ukH6PZMY7H4iObGtciQaC5nw==", "integrity": "sha512-MAAXfLJNf6ZFiFK3w51yiLKc31HAQWJWNV+nQ1dAdRpa6WiXdjpkcrWTuxmPd/o6Qt+yNv+hoItAVxDPEox8qQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"linkifyjs": "^4.3.3" "linkifyjs": "^4.3.3"
@@ -1715,133 +1715,133 @@
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.30.2", "@tiptap/core": "3.30.3",
"@tiptap/pm": "3.30.2" "@tiptap/pm": "3.30.3"
} }
}, },
"node_modules/@tiptap/extension-list": { "node_modules/@tiptap/extension-list": {
"version": "3.30.2", "version": "3.30.3",
"resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.30.2.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.30.3.tgz",
"integrity": "sha512-MIUpo1Bd9Rf1Qg+TNYNwDZ4xsfFeQahjU9Xhy6UcaszKQzbAM7KCzn5BObNytK1NdcqNHsC8Wj5vFvMKEzrXdw==", "integrity": "sha512-cCX99WoVb4UyXkamSh0vlXiRFhlZnwk3WXWdnXpR1E9+auxsQewk2Q9NDW1Ra0vwgzer1gkFhlOl9TCVzH2VVg==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.30.2", "@tiptap/core": "3.30.3",
"@tiptap/pm": "3.30.2" "@tiptap/pm": "3.30.3"
} }
}, },
"node_modules/@tiptap/extension-list-item": { "node_modules/@tiptap/extension-list-item": {
"version": "3.30.2", "version": "3.30.3",
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.30.2.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.30.3.tgz",
"integrity": "sha512-HWgRCRlGxulE+hN1VUcnWD6P2NE08VBgGtcaxOfdXVqaI93BCK6AhRQZGpLsfKgajLk+5DXTBraaitwnBqzCxg==", "integrity": "sha512-RhSyDlAATvXDTg/1VOw5aFTaewdCm+BAU3EG3eUCw4e5eoZ5pbhPOdOPTtaO45UR4i/AvjQNAZQ9LMDWX5WZFQ==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/extension-list": "3.30.2" "@tiptap/extension-list": "3.30.3"
} }
}, },
"node_modules/@tiptap/extension-list-keymap": { "node_modules/@tiptap/extension-list-keymap": {
"version": "3.30.2", "version": "3.30.3",
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.30.2.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.30.3.tgz",
"integrity": "sha512-TTve3WOlQaYu1ahMqsQ/T0wzaxfgZvcOl3/OuPyInOi8QtxXhqGhFjmYe5jOr56G9W2QDuFWVsZecVwfDte9zg==", "integrity": "sha512-u6tOpQFnE1pOyvnzI2MvhW/Yw0omAIMlMfkykYx4N4zB1JDi+16OeOuKf3rX6G3V9VUu+qeOIGpSkifkME6H9Q==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/extension-list": "3.30.2" "@tiptap/extension-list": "3.30.3"
} }
}, },
"node_modules/@tiptap/extension-ordered-list": { "node_modules/@tiptap/extension-ordered-list": {
"version": "3.30.2", "version": "3.30.3",
"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.30.2.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.30.3.tgz",
"integrity": "sha512-Z7OO1HcF0idda1n6vodXeQ3h2ylN9JR4IfIGUYkar5Xl9JusK8PDETTBQZQn//96p49I2d+GoWsD2LXPtjHXXg==", "integrity": "sha512-lct/FV8vm9Y5kZFcF5TXIM18opi6m7AAkGoChgGP/A7LYalaniX1X6Ja2j13Y22ZdGaOoPTgd8oKRZXP1/miaA==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/extension-list": "3.30.2" "@tiptap/extension-list": "3.30.3"
} }
}, },
"node_modules/@tiptap/extension-paragraph": { "node_modules/@tiptap/extension-paragraph": {
"version": "3.30.2", "version": "3.30.3",
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.30.2.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.30.3.tgz",
"integrity": "sha512-ulEu3LNt+kPVAWEnrhoz13Fs8Q/v/8NUxQbAeteuBchQ8joxJXuWExhpy1fUfZir5+b+W5z7/NesgPjZQfv47w==", "integrity": "sha512-Z9OHCX9b2bDcMDeR3ODCFRAyHT5eNGO4AsDFeAJS/XSitffMyjFHtOkXlyyZ3+pIHxxX6xkUXAwaLMjyATRo1A==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.30.2" "@tiptap/core": "3.30.3"
} }
}, },
"node_modules/@tiptap/extension-strike": { "node_modules/@tiptap/extension-strike": {
"version": "3.30.2", "version": "3.30.3",
"resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.30.2.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.30.3.tgz",
"integrity": "sha512-fBLxMXz6hYIURzLOD+/L6aVATztsKham00ANWmGi13vN0hx2lQMYZffN+gR+QqiCDfMQxBXzrf5a7tJuDiQHLQ==", "integrity": "sha512-e9BI8Hzei23GjmYysXxL+CHq3fIgOm29BbSUMbYXxT2cJ+TkkgJp7EvHf74gSABNME0gUN+mG0pRR5wTFoOCcg==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.30.2" "@tiptap/core": "3.30.3"
} }
}, },
"node_modules/@tiptap/extension-text": { "node_modules/@tiptap/extension-text": {
"version": "3.30.2", "version": "3.30.3",
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.30.2.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.30.3.tgz",
"integrity": "sha512-n/iZnirgRmXet6f97kolAnP3j8DsgLSiTbz/KLWc8eBYiFmkjRzkuisOm5xuGdfGIxwpB4x3tlSF4ef4DLnbRg==", "integrity": "sha512-Jh8ZyI0HLOJOIPZl5a5XRJbTs4pAUakYIWCZ3Hv3lF9ouXW/C0bY67XQU7bv6Rvu8AHzDszhKklnOZ6z1NQPXQ==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.30.2" "@tiptap/core": "3.30.3"
} }
}, },
"node_modules/@tiptap/extension-underline": { "node_modules/@tiptap/extension-underline": {
"version": "3.30.2", "version": "3.30.3",
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.30.2.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.30.3.tgz",
"integrity": "sha512-SZiTMnvqXcnrtJX+X25ZbYsuDO83haGOVMBD/O+mAWYNYXhaSc5Rkph5czzItxrd+Yyp/vs4PiwD7XTNbfqmpA==", "integrity": "sha512-TiZ+b524Ee4PNlW+HNmkg3x4btR1K6btJy9pEjMuDf0Rx01UaTBvHpHmABR/DbbcC1CuZiQ3ylJwLL+90PaEoA==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.30.2" "@tiptap/core": "3.30.3"
} }
}, },
"node_modules/@tiptap/extensions": { "node_modules/@tiptap/extensions": {
"version": "3.30.2", "version": "3.30.3",
"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.30.2.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.30.3.tgz",
"integrity": "sha512-2LqAHXk26QDsryW+beECxYeBzv5Ylk4GuB3cOmfghS7/G37R2W+Te3TkUK7BT0EWoDryvBT57/5q0DEFIhfZZg==", "integrity": "sha512-vig8ZjUL/NFnniEVbYDYj2UOIdkwZVha+9myFZkanShedviE3kl7c4uCY3fMl5EnqgyFIy2TQSKbg/i99xpztA==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.30.2", "@tiptap/core": "3.30.3",
"@tiptap/pm": "3.30.2" "@tiptap/pm": "3.30.3"
} }
}, },
"node_modules/@tiptap/pm": { "node_modules/@tiptap/pm": {
"version": "3.30.2", "version": "3.30.3",
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.30.2.tgz", "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.30.3.tgz",
"integrity": "sha512-BJN8tUx4ppFN3R3cV/FJfrJbJkvo1lj4uciq+nwpjwzdRvFzqIuglWf+HLcJ6CwlYpLOHp7ArgkBg4Q5e60Gog==", "integrity": "sha512-VheaqLAFUe+PCYEgHubM96Z1OAiluFhijQ2Cy1Ghiozoxm5OzevSaKhqm8SQ1VGfzZZko59oUjRzOmu8tB8yfA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"prosemirror-changeset": "^2.4.1", "prosemirror-changeset": "^2.4.1",
@@ -1864,9 +1864,9 @@
} }
}, },
"node_modules/@tiptap/react": { "node_modules/@tiptap/react": {
"version": "3.30.2", "version": "3.30.3",
"resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.30.2.tgz", "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.30.3.tgz",
"integrity": "sha512-7hGaTstpUeTmQ008mCPkjz+GSlChWhucgy+PeX0z93v4+nh7qM5F+0lh+kJ9zo6Os5abO7v36GtgHRZdQI6+FQ==", "integrity": "sha512-gyK8UXFQlm2XgXEynNz5SzsxnTKtdBUp/PKeTeqFrk/m7L8p1eV+gcdAK+arTX+slubXRwLUm1lXrAQXRuiaTw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/use-sync-external-store": "^0.0.6", "@types/use-sync-external-store": "^0.0.6",
@@ -1878,12 +1878,12 @@
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"optionalDependencies": { "optionalDependencies": {
"@tiptap/extension-bubble-menu": "^3.30.2", "@tiptap/extension-bubble-menu": "^3.30.3",
"@tiptap/extension-floating-menu": "^3.30.2" "@tiptap/extension-floating-menu": "^3.30.3"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/core": "3.30.2", "@tiptap/core": "3.30.3",
"@tiptap/pm": "3.30.2", "@tiptap/pm": "3.30.3",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"@types/react-dom": "^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": "^17.0.0 || ^18.0.0 || ^19.0.0",
@@ -1891,35 +1891,35 @@
} }
}, },
"node_modules/@tiptap/starter-kit": { "node_modules/@tiptap/starter-kit": {
"version": "3.30.2", "version": "3.30.3",
"resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.30.2.tgz", "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.30.3.tgz",
"integrity": "sha512-fJSrhW1CyD4sjYA20evSP4Cp13B/HhbxCdM974K0xpHOVqvCtNU9w2s9hfq9mg2yGoU7MSNHKNYMkJjIi2/Xyw==", "integrity": "sha512-hj4rAhAoQm+wRk8eLNTyK+wK81+epbuo4O9PvPRv7GJGwYycej+xUN7jGM7Tney7GuTZpHoDCuwLKD2n1oG+pg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@tiptap/core": "3.30.2", "@tiptap/core": "3.30.3",
"@tiptap/extension-blockquote": "3.30.2", "@tiptap/extension-blockquote": "3.30.3",
"@tiptap/extension-bold": "3.30.2", "@tiptap/extension-bold": "3.30.3",
"@tiptap/extension-bullet-list": "3.30.2", "@tiptap/extension-bullet-list": "3.30.3",
"@tiptap/extension-code": "3.30.2", "@tiptap/extension-code": "3.30.3",
"@tiptap/extension-code-block": "3.30.2", "@tiptap/extension-code-block": "3.30.3",
"@tiptap/extension-document": "3.30.2", "@tiptap/extension-document": "3.30.3",
"@tiptap/extension-dropcursor": "3.30.2", "@tiptap/extension-dropcursor": "3.30.3",
"@tiptap/extension-gapcursor": "3.30.2", "@tiptap/extension-gapcursor": "3.30.3",
"@tiptap/extension-hard-break": "3.30.2", "@tiptap/extension-hard-break": "3.30.3",
"@tiptap/extension-heading": "3.30.2", "@tiptap/extension-heading": "3.30.3",
"@tiptap/extension-horizontal-rule": "3.30.2", "@tiptap/extension-horizontal-rule": "3.30.3",
"@tiptap/extension-italic": "3.30.2", "@tiptap/extension-italic": "3.30.3",
"@tiptap/extension-link": "3.30.2", "@tiptap/extension-link": "3.30.3",
"@tiptap/extension-list": "3.30.2", "@tiptap/extension-list": "3.30.3",
"@tiptap/extension-list-item": "3.30.2", "@tiptap/extension-list-item": "3.30.3",
"@tiptap/extension-list-keymap": "3.30.2", "@tiptap/extension-list-keymap": "3.30.3",
"@tiptap/extension-ordered-list": "3.30.2", "@tiptap/extension-ordered-list": "3.30.3",
"@tiptap/extension-paragraph": "3.30.2", "@tiptap/extension-paragraph": "3.30.3",
"@tiptap/extension-strike": "3.30.2", "@tiptap/extension-strike": "3.30.3",
"@tiptap/extension-text": "3.30.2", "@tiptap/extension-text": "3.30.3",
"@tiptap/extension-underline": "3.30.2", "@tiptap/extension-underline": "3.30.3",
"@tiptap/extensions": "3.30.2", "@tiptap/extensions": "3.30.3",
"@tiptap/pm": "3.30.2" "@tiptap/pm": "3.30.3"
}, },
"funding": { "funding": {
"type": "github", "type": "github",
@@ -1983,9 +1983,9 @@
} }
}, },
"node_modules/@types/react-dom": { "node_modules/@types/react-dom": {
"version": "19.2.4", "version": "19.2.5",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.5.tgz",
"integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", "integrity": "sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg==",
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"@types/react": "^19.2.0" "@types/react": "^19.2.0"
@@ -2027,9 +2027,9 @@
} }
}, },
"node_modules/baseline-browser-mapping": { "node_modules/baseline-browser-mapping": {
"version": "2.11.17", "version": "2.11.18",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.17.tgz", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.18.tgz",
"integrity": "sha512-KAUDn1OSS0fmPlGO+NOUMRcOQ/b/shUBH3OgkG73mPgdf+JD/BQ6fHboGxNOxnUmlwcq+lLq3dTkayRPuSfXwg==", "integrity": "sha512-1iEmLEYSiE1SeBoAfPo/Mnx3PzfzHUkDK61ASkCpuk3YXugYLH5DYK1SzqV55F8FMI6s0F+/tCP7Polz1QRjxw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"baseline-browser-mapping": "dist/cli.cjs" "baseline-browser-mapping": "dist/cli.cjs"
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@govoplan/core-webui", "name": "@govoplan/core-webui",
"version": "0.1.38", "version": "0.1.40",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
@@ -42,7 +42,7 @@
"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: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: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", "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",
"test:layout-primitives": "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/layout-primitives.test.js", "test:layout-primitives": "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 && printf 'module.exports = {};\\n' > .component-test-build/src/components/ProductAvailabilityState.css && node .component-test-build/tests/layout-primitives.test.js",
"test:module-capabilities": "rm -rf .module-test-build && mkdir -p .module-test-build && printf '{\"type\":\"commonjs\"}\n' > .module-test-build/package.json && tsc -p tsconfig.module-tests.json && node .module-test-build/tests/module-capabilities.test.js && node .module-test-build/tests/privacy-policy.test.js && node .module-test-build/tests/help-context.test.js && node .module-test-build/tests/launch-context.test.js && node .module-test-build/tests/definition-graph.test.js", "test:module-capabilities": "rm -rf .module-test-build && mkdir -p .module-test-build && printf '{\"type\":\"commonjs\"}\n' > .module-test-build/package.json && tsc -p tsconfig.module-tests.json && node .module-test-build/tests/module-capabilities.test.js && node .module-test-build/tests/privacy-policy.test.js && node .module-test-build/tests/help-context.test.js && node .module-test-build/tests/launch-context.test.js && node .module-test-build/tests/definition-graph.test.js",
"test:module-permutations": "node scripts/test-module-permutations.mjs", "test:module-permutations": "node scripts/test-module-permutations.mjs",
"test:mail-components": "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/mail-components.test.js", "test:mail-components": "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/mail-components.test.js",
+12 -12
View File
@@ -1,6 +1,6 @@
{ {
"name": "@govoplan/core-webui", "name": "@govoplan/core-webui",
"version": "0.1.38", "version": "0.1.40",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
@@ -26,23 +26,23 @@
"preview": "vite preview --host 127.0.0.1 --port 4173" "preview": "vite preview --host 127.0.0.1 --port 4173"
}, },
"dependencies": { "dependencies": {
"@govoplan/access-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#v0.1.20", "@govoplan/access-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#v0.1.23",
"@govoplan/admin-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git#v0.1.19", "@govoplan/admin-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git#v0.1.22",
"@govoplan/audit-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git#v0.1.18", "@govoplan/audit-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git#v0.1.18",
"@govoplan/calendar-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git#v0.1.18", "@govoplan/calendar-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git#v0.1.22",
"@govoplan/cases-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-cases.git#v0.1.20", "@govoplan/cases-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-cases.git#v0.1.20",
"@govoplan/dashboard-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-dashboard.git#v0.1.18", "@govoplan/dashboard-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-dashboard.git#v0.1.18",
"@govoplan/docs-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git#v0.1.18", "@govoplan/docs-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git#v0.1.22",
"@govoplan/files-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git#v0.1.20", "@govoplan/files-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git#v0.1.23",
"@govoplan/helpdesk-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-helpdesk.git#v0.1.20", "@govoplan/helpdesk-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-helpdesk.git#v0.1.20",
"@govoplan/idm-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git#v0.1.19", "@govoplan/idm-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git#v0.1.24",
"@govoplan/mail-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git#v0.1.18", "@govoplan/mail-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git#v0.1.25",
"@govoplan/campaign-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git#v0.1.22", "@govoplan/campaign-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git#v0.1.27",
"@govoplan/organizations-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git#v0.1.18", "@govoplan/organizations-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git#v0.1.20",
"@govoplan/ops-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-ops.git#v0.1.18", "@govoplan/ops-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-ops.git#v0.1.18",
"@govoplan/policy-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git#v0.1.18", "@govoplan/policy-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git#v0.1.18",
"@govoplan/tickets-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-tickets.git#v0.1.20", "@govoplan/tickets-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-tickets.git#v0.1.22",
"@govoplan/wiki-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-wiki.git#v0.1.20", "@govoplan/wiki-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-wiki.git#v0.1.22",
"@tiptap/core": "^3.29.2", "@tiptap/core": "^3.29.2",
"@tiptap/extension-image": "^3.29.2", "@tiptap/extension-image": "^3.29.2",
"@tiptap/pm": "^3.29.2", "@tiptap/pm": "^3.29.2",
+6 -4
View File
@@ -15,12 +15,14 @@ const sourceRoots = fs.readdirSync(workspaceRoot, { withFileTypes: true })
.map((entry) => path.join(workspaceRoot, entry.name, "webui", "src")) .map((entry) => path.join(workspaceRoot, entry.name, "webui", "src"))
.filter((sourceRoot) => fs.existsSync(sourceRoot)); .filter((sourceRoot) => fs.existsSync(sourceRoot));
const generatedCatalogs = sourceRoots const generatedCatalogs = sourceRoots.flatMap((sourceRoot) =>
.map((sourceRoot) => path.join(sourceRoot, "i18n", "generatedTranslations.ts")) fs.existsSync(path.join(sourceRoot, "i18n"))
.filter((file) => fs.existsSync(file)); ? rgFiles(path.join(sourceRoot, "i18n")).filter((file) => /Translations\.ts$/.test(file))
: []
);
function scanStructuralSourcePositions(roots) { function scanStructuralSourcePositions(roots) {
const files = roots.flatMap((root) => rgFiles(root).filter((file) => /\.(tsx?|jsx?)$/.test(file) && !file.endsWith("/i18n/generatedTranslations.ts"))); const files = roots.flatMap((root) => rgFiles(root).filter((file) => /\.(tsx?|jsx?)$/.test(file) && !/\/i18n\/[^/]*Translations\.ts$/.test(file)));
const findings = []; const findings = [];
for (const file of files) { for (const file of files) {
const source = ts.createSourceFile(file, fs.readFileSync(file, "utf8"), ts.ScriptTarget.Latest, true, file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS); const source = ts.createSourceFile(file, fs.readFileSync(file, "utf8"), ts.ScriptTarget.Latest, true, file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
+2 -1
View File
@@ -30,6 +30,7 @@ import { applyAppearanceOverrides } from "./components/AppearanceOverridesEditor
const DashboardPage = lazy(() => import("./features/dashboard/DashboardPage")); const DashboardPage = lazy(() => import("./features/dashboard/DashboardPage"));
const SettingsPage = lazy(() => import("./features/settings/SettingsPage")); const SettingsPage = lazy(() => import("./features/settings/SettingsPage"));
const ProductSurfaceRoute = lazy(() => import("./components/ProductSurfaceRoute"));
const DEFAULT_UI_PREFERENCES: UserUiPreferences = { const DEFAULT_UI_PREFERENCES: UserUiPreferences = {
compact_tables: false, compact_tables: false,
@@ -579,7 +580,7 @@ export default function App() {
)} )}
<Route path="/settings" element={<SettingsPage settings={settings} auth={auth} onSettingsChange={updateSettings} onAuthChange={updateAuth} />} /> <Route path="/settings" element={<SettingsPage settings={settings} auth={auth} onSettingsChange={updateSettings} onAuthChange={updateAuth} />} />
<Route path="*" element={<Navigate to={defaultRoute} replace />} /> <Route path="*" element={<ProductSurfaceRoute auth={auth} />} />
</Routes> </Routes>
</ModuleLoadBoundary> </ModuleLoadBoundary>
{reloginMessage && {reloginMessage &&
+26 -2
View File
@@ -3,8 +3,9 @@ import { Dice5, Eye, EyeOff } from "lucide-react";
import PasswordGeneratorDialog from "./PasswordGeneratorDialog"; import PasswordGeneratorDialog from "./PasswordGeneratorDialog";
import type { PasswordGeneratorOptions } from "./passwordGenerator"; import type { PasswordGeneratorOptions } from "./passwordGenerator";
import { usePlatformLanguage } from "../i18n/LanguageContext"; import { usePlatformLanguage } from "../i18n/LanguageContext";
import type { PlatformInterfaceIdentityProps } from "../types";
export type PasswordFieldProps = Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "value" | "onChange"> & { export type PasswordFieldProps = Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "value" | "onChange"> & PlatformInterfaceIdentityProps & {
value: string; value: string;
onValueChange: (value: string) => void; onValueChange: (value: string) => void;
saved?: boolean; saved?: boolean;
@@ -32,6 +33,10 @@ export default function PasswordField({
className = "", className = "",
inputClassName = "", inputClassName = "",
id, id,
interfaceId,
helpContextId,
helpModuleId,
helpTopicId,
...inputProps ...inputProps
}: PasswordFieldProps) { }: PasswordFieldProps) {
const generatedId = useId(); const generatedId = useId();
@@ -50,10 +55,20 @@ export default function PasswordField({
return ( return (
<> <>
<div className={`password-field ${canReveal || canGenerate ? "has-actions" : ""} ${canGenerate ? "has-generator" : ""} ${canReveal ? "has-reveal" : ""} ${showSavedPlaceholder ? "is-saved-empty" : ""} ${className}`.trim()}> <div
className={`password-field ${canReveal || canGenerate ? "has-actions" : ""} ${canGenerate ? "has-generator" : ""} ${canReveal ? "has-reveal" : ""} ${showSavedPlaceholder ? "is-saved-empty" : ""} ${className}`.trim()}
data-help-scope="field"
data-interface-id={interfaceId}
data-help-context-id={helpContextId}
data-help-module-id={helpModuleId}
data-help-topic-id={helpTopicId}
>
<input <input
{...inputProps} {...inputProps}
id={inputId} id={inputId}
data-help-context-id={helpContextId}
data-help-module-id={helpModuleId}
data-help-topic-id={helpTopicId}
className={inputClassName} className={inputClassName}
type={inputType} type={inputType}
value={value} value={value}
@@ -72,6 +87,9 @@ export default function PasswordField({
className="password-field-action" className="password-field-action"
aria-label={translatedGeneratorLabel} aria-label={translatedGeneratorLabel}
title={translatedGeneratorLabel} title={translatedGeneratorLabel}
data-help-context-id={helpContextId}
data-help-module-id={helpModuleId}
data-help-topic-id={helpTopicId}
onClick={() => setGeneratorOpen(true)} onClick={() => setGeneratorOpen(true)}
> >
<Dice5 size={17} aria-hidden="true" /> <Dice5 size={17} aria-hidden="true" />
@@ -83,6 +101,9 @@ export default function PasswordField({
className="password-field-action" className="password-field-action"
aria-label={visible ? translatedHideLabel : translatedRevealLabel} aria-label={visible ? translatedHideLabel : translatedRevealLabel}
title={visible ? translatedHideLabel : translatedRevealLabel} title={visible ? translatedHideLabel : translatedRevealLabel}
data-help-context-id={helpContextId}
data-help-module-id={helpModuleId}
data-help-topic-id={helpTopicId}
onClick={() => setVisible((current) => !current)} onClick={() => setVisible((current) => !current)}
> >
{visible ? <EyeOff size={17} aria-hidden="true" /> : <Eye size={17} aria-hidden="true" />} {visible ? <EyeOff size={17} aria-hidden="true" /> : <Eye size={17} aria-hidden="true" />}
@@ -95,6 +116,9 @@ export default function PasswordField({
<PasswordGeneratorDialog <PasswordGeneratorDialog
open={generatorOpen} open={generatorOpen}
initialOptions={generatorOptions} initialOptions={generatorOptions}
helpContextId={helpContextId}
helpModuleId={helpModuleId}
helpTopicId={helpTopicId}
onUse={(password) => { onUse={(password) => {
onValueChange(password); onValueChange(password);
setVisible(false); setVisible(false);
@@ -14,6 +14,7 @@ import {
type PasswordGeneratorOptions type PasswordGeneratorOptions
} from "./passwordGenerator"; } from "./passwordGenerator";
import { usePlatformLanguage } from "../i18n/LanguageContext"; import { usePlatformLanguage } from "../i18n/LanguageContext";
import type { PlatformInterfaceIdentityProps } from "../types";
const GENERATION_ERROR_LABELS: Record<PasswordGeneratorErrorCode, string> = { const GENERATION_ERROR_LABELS: Record<PasswordGeneratorErrorCode, string> = {
"invalid-length": "i18n:govoplan-core.password_length_must_be_between_12_and_128_character.4d147c07", "invalid-length": "i18n:govoplan-core.password_length_must_be_between_12_and_128_character.4d147c07",
@@ -23,7 +24,7 @@ const GENERATION_ERROR_LABELS: Record<PasswordGeneratorErrorCode, string> = {
"secure-random-unavailable": "i18n:govoplan-core.secure_browser_password_generation_is_unavailable.55275f10" "secure-random-unavailable": "i18n:govoplan-core.secure_browser_password_generation_is_unavailable.55275f10"
}; };
export type PasswordGeneratorDialogProps = { export type PasswordGeneratorDialogProps = PlatformInterfaceIdentityProps & {
open: boolean; open: boolean;
initialOptions?: Partial<PasswordGeneratorOptions>; initialOptions?: Partial<PasswordGeneratorOptions>;
onUse: (password: string) => void; onUse: (password: string) => void;
@@ -33,6 +34,9 @@ export type PasswordGeneratorDialogProps = {
export default function PasswordGeneratorDialog({ export default function PasswordGeneratorDialog({
open, open,
initialOptions, initialOptions,
helpContextId,
helpModuleId,
helpTopicId,
onUse, onUse,
onClose onClose
}: PasswordGeneratorDialogProps) { }: PasswordGeneratorDialogProps) {
@@ -95,6 +99,9 @@ export default function PasswordGeneratorDialog({
bodyClassName="password-generator-body" bodyClassName="password-generator-body"
footerClassName="button-row compact-actions" footerClassName="button-row compact-actions"
portal portal
helpContextId={helpContextId}
helpModuleId={helpModuleId}
helpTopicId={helpTopicId}
onClose={onClose} onClose={onClose}
footer={( footer={(
<> <>
@@ -103,6 +110,9 @@ export default function PasswordGeneratorDialog({
type="button" type="button"
variant="primary" variant="primary"
disabled={!candidate} disabled={!candidate}
helpContextId={helpContextId}
helpModuleId={helpModuleId}
helpTopicId={helpTopicId}
onClick={() => { onClick={() => {
if (!candidate) return; if (!candidate) return;
onUse(candidate); onUse(candidate);
@@ -116,7 +126,7 @@ export default function PasswordGeneratorDialog({
> >
{error ? <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert> : null} {error ? <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert> : null}
<div className="password-generator-options"> <div className="password-generator-options">
<FormField label="i18n:govoplan-core.length.adc95605"> <FormField label="i18n:govoplan-core.length.adc95605" helpContextId={helpContextId} helpModuleId={helpModuleId} helpTopicId={helpTopicId}>
<input <input
type="number" type="number"
min={12} min={12}
@@ -127,13 +137,13 @@ export default function PasswordGeneratorDialog({
/> />
</FormField> </FormField>
<div className="password-generator-character-sets" aria-label={translateText("i18n:govoplan-core.character_sets.db6efda2")}> <div className="password-generator-character-sets" aria-label={translateText("i18n:govoplan-core.character_sets.db6efda2")}>
<ToggleSwitch label="i18n:govoplan-core.lowercase.3b677a18" checked={options.lowercase} onChange={(checked) => setOption("lowercase", checked)} /> <ToggleSwitch label="i18n:govoplan-core.lowercase.3b677a18" checked={options.lowercase} onChange={(checked) => setOption("lowercase", checked)} helpContextId={helpContextId} helpModuleId={helpModuleId} helpTopicId={helpTopicId} />
<ToggleSwitch label="i18n:govoplan-core.uppercase.b463d690" checked={options.uppercase} onChange={(checked) => setOption("uppercase", checked)} /> <ToggleSwitch label="i18n:govoplan-core.uppercase.b463d690" checked={options.uppercase} onChange={(checked) => setOption("uppercase", checked)} helpContextId={helpContextId} helpModuleId={helpModuleId} helpTopicId={helpTopicId} />
<ToggleSwitch label="i18n:govoplan-core.digits.9cd500d3" checked={options.digits} onChange={(checked) => setOption("digits", checked)} /> <ToggleSwitch label="i18n:govoplan-core.digits.9cd500d3" checked={options.digits} onChange={(checked) => setOption("digits", checked)} helpContextId={helpContextId} helpModuleId={helpModuleId} helpTopicId={helpTopicId} />
<ToggleSwitch label="i18n:govoplan-core.symbols.9491fc41" checked={options.symbols} onChange={(checked) => setOption("symbols", checked)} /> <ToggleSwitch label="i18n:govoplan-core.symbols.9491fc41" checked={options.symbols} onChange={(checked) => setOption("symbols", checked)} helpContextId={helpContextId} helpModuleId={helpModuleId} helpTopicId={helpTopicId} />
</div> </div>
</div> </div>
<FormField label="i18n:govoplan-core.generated_password.78461854"> <FormField label="i18n:govoplan-core.generated_password.78461854" helpContextId={helpContextId} helpModuleId={helpModuleId} helpTopicId={helpTopicId}>
<div className="password-generator-result"> <div className="password-generator-result">
<input <input
type="text" type="text"
@@ -147,11 +157,17 @@ export default function PasswordGeneratorDialog({
icon={<Copy size={16} />} icon={<Copy size={16} />}
onClick={() => void copy()} onClick={() => void copy()}
disabled={!candidate || typeof navigator === "undefined" || !navigator.clipboard?.writeText} disabled={!candidate || typeof navigator === "undefined" || !navigator.clipboard?.writeText}
helpContextId={helpContextId}
helpModuleId={helpModuleId}
helpTopicId={helpTopicId}
/> />
<IconButton <IconButton
label="i18n:govoplan-core.generate_another_password.d99fc019" label="i18n:govoplan-core.generate_another_password.d99fc019"
icon={<RefreshCw size={16} />} icon={<RefreshCw size={16} />}
onClick={generate} onClick={generate}
helpContextId={helpContextId}
helpModuleId={helpModuleId}
helpTopicId={helpTopicId}
/> />
</div> </div>
</FormField> </FormField>
@@ -0,0 +1,8 @@
.product-availability-resolution,
.product-availability-owner { margin: 0; }
.product-availability-technical { width: min(100%, 560px); margin-top: 4px; color: var(--muted); text-align: start; }
.product-availability-technical summary { cursor: pointer; color: var(--text); font-weight: 700; }
.product-availability-technical dl { display: grid; gap: 6px; margin: 10px 0 0; }
.product-availability-technical dl > div { display: grid; grid-template-columns: minmax(110px, .4fr) minmax(0, 1fr); gap: 10px; }
.product-availability-technical dt { color: var(--muted); font-weight: 700; }
.product-availability-technical dd { min-width: 0; margin: 0; overflow-wrap: anywhere; color: var(--text); font-family: var(--font-mono, monospace); font-size: 12px; }
@@ -0,0 +1,114 @@
import { CircleOff, TriangleAlert } from "lucide-react";
import type { ReactNode } from "react";
import { usePlatformLanguage } from "../i18n/LanguageContext";
import type { ProductAvailabilityExplanation } from "../types";
import StatePanel, { type StatePanelProps } from "./StatePanel";
import "./ProductAvailabilityState.css";
export type ProductTechnicalProvenance = {
moduleId?: string | null;
capabilityId?: string | null;
providerId?: string | null;
correlationId?: string | null;
};
export type ProductAvailabilityStateProps = {
state: "unavailable" | "degraded";
explanation: ProductAvailabilityExplanation;
actions?: ReactNode;
technical?: ProductTechnicalProvenance | null;
size?: StatePanelProps["size"];
surface?: StatePanelProps["surface"];
className?: string;
};
export default function ProductAvailabilityState({
state,
explanation,
actions,
technical,
size = "default",
surface = "subtle",
className = ""
}: ProductAvailabilityStateProps) {
const { language, translateText } = usePlatformLanguage();
const labels = AVAILABILITY_LABELS[language.split("-", 1)[0] === "de" ? "de" : "en"];
const title = translateText(explanation.title);
const description = translateText(explanation.description);
const resolution = translateText(explanation.resolution);
const responsibleRole = explanation.responsibleRole
? translateText(explanation.responsibleRole)
: null;
const technicalEntries = technical ? Object.entries(technical).filter((entry) => Boolean(entry[1])) : [];
return (
<StatePanel
aria-live="polite"
className={["product-availability-state", `product-availability-${state}`, className].filter(Boolean).join(" ")}
icon={state === "degraded" ? <TriangleAlert size={24} /> : <CircleOff size={24} />}
title={title}
description={description}
actions={actions}
size={size}
surface={surface}
tone="warning"
>
<p className="product-availability-resolution">{resolution}</p>
{responsibleRole ? (
<p className="product-availability-owner">
<strong>{labels.responsibleRole}: </strong>
{responsibleRole}
</p>
) : null}
{technicalEntries.length ? (
<details className="product-availability-technical">
<summary>{labels.technicalDetails}</summary>
<dl>
{technicalEntries.map(([key, value]) => (
<div key={key}>
<dt>{technicalLabel(key, labels)}</dt>
<dd>{String(value)}</dd>
</div>
))}
</dl>
</details>
) : null}
</StatePanel>
);
}
type AvailabilityLabels = {
responsibleRole: string;
technicalDetails: string;
module: string;
capability: string;
provider: string;
correlationId: string;
};
const AVAILABILITY_LABELS: Record<"en" | "de", AvailabilityLabels> = {
en: {
responsibleRole: "Responsible role",
technicalDetails: "Technical details",
module: "Module",
capability: "Capability",
provider: "Provider",
correlationId: "Correlation ID"
},
de: {
responsibleRole: "Zuständige Rolle",
technicalDetails: "Technische Details",
module: "Modul",
capability: "Fähigkeit",
provider: "Anbieter",
correlationId: "Korrelations-ID"
}
};
function technicalLabel(key: string, labels: AvailabilityLabels): string {
if (key === "moduleId") return labels.module;
if (key === "capabilityId") return labels.capability;
if (key === "providerId") return labels.provider;
if (key === "correlationId") return labels.correlationId;
return key;
}
@@ -0,0 +1,63 @@
import { lazy, useEffect, useMemo } from "react";
import { Navigate, useLocation } from "react-router";
import type { AuthInfo } from "../types";
import { usePlatformModules } from "../platform/ModuleContext";
import { firstAccessibleRoute } from "../platform/modules";
import {
availableProductSurfaceContributors,
composeProductSurfaces,
dispatchProductSurfaceRouteResolved
} from "../platform/productSurfaces";
import { useEffectiveView } from "../platform/ViewContext";
const ProductAvailabilityState = lazy(() => import("./ProductAvailabilityState"));
export default function ProductSurfaceRoute({
auth
}: {
auth: AuthInfo;
}) {
const location = useLocation();
const modules = usePlatformModules();
const projection = useEffectiveView();
const surface = useMemo(
() => composeProductSurfaces(modules).find((candidate) =>
candidate.entryPath === location.pathname || candidate.aliases.includes(location.pathname)
) ?? null,
[location.pathname, modules]
);
const contributors = useMemo(
() => surface ? availableProductSurfaceContributors(surface, auth, modules, projection) : [],
[auth, modules, projection, surface]
);
const target = contributors[0] ?? null;
useEffect(() => {
if (!surface || !target) return;
dispatchProductSurfaceRouteResolved({
contractVersion: "1",
productSurfaceId: surface.id,
requestedPath: location.pathname,
targetPath: target.routePath,
contributorModuleId: target.moduleId,
usedAlias: location.pathname !== surface.entryPath
});
}, [location.pathname, surface, target]);
if (target) {
return <Navigate to={`${target.routePath}${location.search}${location.hash}`} replace />;
}
if (!surface) {
return <Navigate to={firstAccessibleRoute(auth, modules, projection)} replace />;
}
const explanation = surface.contributors[0]?.unavailable;
return explanation ? (
<ProductAvailabilityState
state="unavailable"
explanation={explanation}
size="fill"
/>
) : null;
}
+5 -5
View File
@@ -57,13 +57,13 @@ export default function LoginModal({
<FormLayout columns={1} collapseAt="standard" id={formId} className="" onSubmit={submit}> <FormLayout columns={1} collapseAt="standard" id={formId} className="" onSubmit={submit}>
{message && <DismissibleAlert tone="info" dismissible={false}>{message}</DismissibleAlert>} {message && <DismissibleAlert tone="info" dismissible={false}>{message}</DismissibleAlert>}
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>} {error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
<FormField label="i18n:govoplan-core.email.84add5b2"> <FormField label="i18n:govoplan-core.email.84add5b2" helpContextId="access.authentication.email" helpModuleId="access">
<input type="email" value={email} autoComplete="username" onChange={(e) => setEmail(e.target.value)} /> <input data-help-context-id="access.authentication.email" data-help-module-id="access" type="email" value={email} autoComplete="username" onChange={(e) => setEmail(e.target.value)} />
</FormField> </FormField>
<FormField label="i18n:govoplan-core.password.8be3c943"> <FormField label="i18n:govoplan-core.password.8be3c943" helpContextId="access.authentication.password" helpModuleId="access">
<PasswordField value={password} autoComplete="current-password" onValueChange={setPassword} /> <PasswordField helpContextId="access.authentication.password" helpModuleId="access" value={password} autoComplete="current-password" onValueChange={setPassword} />
</FormField> </FormField>
</FormLayout> </FormLayout>
</Dialog>); </Dialog>);
} }
+3 -1
View File
@@ -572,8 +572,10 @@ export default function SettingsPage({
<FormField label="i18n:govoplan-core.api_base_url.1358fba4" help="i18n:govoplan-core.leave_empty_to_use_the_same_origin_in_vite_dev_a.9a1c25d7"> <FormField label="i18n:govoplan-core.api_base_url.1358fba4" help="i18n:govoplan-core.leave_empty_to_use_the_same_origin_in_vite_dev_a.9a1c25d7">
<input value={settings.apiBaseUrl} onChange={(e) => onSettingsChange({ ...settings, apiBaseUrl: e.target.value })} placeholder="https://example.org or empty" /> <input value={settings.apiBaseUrl} onChange={(e) => onSettingsChange({ ...settings, apiBaseUrl: e.target.value })} placeholder="https://example.org or empty" />
</FormField> </FormField>
<FormField label="i18n:govoplan-core.automation_api_key.5d4e2e6e" help="i18n:govoplan-core.used_only_when_there_is_no_browser_session_token.9d399e70"> <FormField label="i18n:govoplan-core.automation_api_key.5d4e2e6e" help="i18n:govoplan-core.used_only_when_there_is_no_browser_session_token.9d399e70" helpContextId="access.settings.automation-api-key" helpModuleId="access">
<PasswordField <PasswordField
helpContextId="access.settings.automation-api-key"
helpModuleId="access"
value={settings.apiKey} value={settings.apiKey}
autoComplete="off" autoComplete="off"
onValueChange={(apiKey) => onSettingsChange({ ...settings, apiKey })} /> onValueChange={(apiKey) => onSettingsChange({ ...settings, apiKey })} />
@@ -0,0 +1,28 @@
import type { PlatformTranslations } from "../types";
export const generatedTranslations = {
en: {
"i18n:govoplan-core.product_surface.messages": "Messages",
"i18n:govoplan-core.product_surface.messages_description": "Read and act on messages without merging channel custody, policy, or delivery state.",
"i18n:govoplan-core.product_surface.messages_unavailable": "Messages are unavailable",
"i18n:govoplan-core.product_surface.messages_unavailable_description": "No message source is available for your current responsibility and permissions.",
"i18n:govoplan-core.product_surface.messages_unavailable_resolution": "Ask the responsible access administrator to review your assignment or permissions.",
"i18n:govoplan-core.product_surface.messages_degraded": "Messages are temporarily limited",
"i18n:govoplan-core.product_surface.messages_degraded_description": "Saved messages remain available, but a channel or provider may not be current.",
"i18n:govoplan-core.product_surface.messages_degraded_resolution": "Retry later or ask the integration operator to review provider health.",
"i18n:govoplan-core.access_administrator": "Access administrator",
"i18n:govoplan-core.integration_operator": "Integration operator"
},
de: {
"i18n:govoplan-core.product_surface.messages": "Nachrichten",
"i18n:govoplan-core.product_surface.messages_description": "Nachrichten lesen und bearbeiten, ohne Verwahrung, Regeln oder Zustellstatus der Kanäle zusammenzuführen.",
"i18n:govoplan-core.product_surface.messages_unavailable": "Nachrichten sind nicht verfügbar",
"i18n:govoplan-core.product_surface.messages_unavailable_description": "Für Ihre aktuelle Verantwortung und Berechtigungen ist keine Nachrichtenquelle verfügbar.",
"i18n:govoplan-core.product_surface.messages_unavailable_resolution": "Bitten Sie die zuständige Zugriffsadministration, Ihre Zuordnung oder Berechtigungen zu prüfen.",
"i18n:govoplan-core.product_surface.messages_degraded": "Nachrichten sind vorübergehend eingeschränkt",
"i18n:govoplan-core.product_surface.messages_degraded_description": "Gespeicherte Nachrichten bleiben verfügbar, ein Kanal oder Anbieter ist jedoch möglicherweise nicht aktuell.",
"i18n:govoplan-core.product_surface.messages_degraded_resolution": "Versuchen Sie es später erneut oder bitten Sie die Integrationsadministration, den Anbieterstatus zu prüfen.",
"i18n:govoplan-core.access_administrator": "Zugriffsadministration",
"i18n:govoplan-core.integration_operator": "Integrationsadministration"
}
} satisfies PlatformTranslations;
+4
View File
@@ -32,6 +32,8 @@ export * from "./platform/ModuleContext";
export * from "./platform/moduleEvents"; export * from "./platform/moduleEvents";
export * from "./platform/ViewContext"; export * from "./platform/ViewContext";
export * from "./platform/views"; export * from "./platform/views";
export * from "./platform/productSurfaces";
export { generatedTranslations as messagesProductSurfaceTranslations } from "./i18n/productSurfaceTranslations";
export * from "./platform/temporal"; export * from "./platform/temporal";
export * from "./platform/TemporalContext"; export * from "./platform/TemporalContext";
export * from "./platform/ActiveObjectContext"; export * from "./platform/ActiveObjectContext";
@@ -215,6 +217,8 @@ export { default as SelectionList, SelectionListItem, SelectionListItemContent }
export type { SelectionListItemContentProps, SelectionListItemProps, SelectionListProps } from "./components/SelectionList"; export type { SelectionListItemContentProps, SelectionListItemProps, SelectionListProps } from "./components/SelectionList";
export { default as StatePanel } from "./components/StatePanel"; export { default as StatePanel } from "./components/StatePanel";
export type { StatePanelProps, StatePanelSize, StatePanelSurface, StatePanelTone } from "./components/StatePanel"; export type { StatePanelProps, StatePanelSize, StatePanelSurface, StatePanelTone } from "./components/StatePanel";
export { default as ProductAvailabilityState } from "./components/ProductAvailabilityState";
export type { ProductAvailabilityStateProps, ProductTechnicalProvenance } from "./components/ProductAvailabilityState";
export { default as StatusBadge } from "./components/StatusBadge"; export { default as StatusBadge } from "./components/StatusBadge";
export { default as StageRail } from "./components/StageRail"; export { default as StageRail } from "./components/StageRail";
export type { export type {
+1
View File
@@ -237,6 +237,7 @@ function applyServerMetadata(module: PlatformWebModule, info: PlatformModuleInfo
publicRoutes: filterPublicRoutes(module, info.frontend?.public_routes), publicRoutes: filterPublicRoutes(module, info.frontend?.public_routes),
viewSurfaces: mergeViewSurfaces(module, info), viewSurfaces: mergeViewSurfaces(module, info),
productAreas: productAreasFromMetadata(info), productAreas: productAreasFromMetadata(info),
productSurfaceMetadata: info.frontend?.product_surfaces,
quickAccessTools: quickAccessToolsFromMetadata(info), quickAccessTools: quickAccessToolsFromMetadata(info),
helpContexts: info.help_contexts ?? module.helpContexts, helpContexts: info.help_contexts ?? module.helpContexts,
uiCapabilities: { uiCapabilities: {
+143
View File
@@ -0,0 +1,143 @@
import type {
AuthInfo,
ComposedProductSurface,
EffectiveViewProjection,
ProductSurfaceMetadata,
PlatformWebModule,
ProductSurfaceContribution
} from "../types";
import { hasAnyScope, hasScope } from "../utils/permissions";
import { isViewSurfaceVisible, viewSurfaceCatalogueForModules } from "./views";
export const PRODUCT_SURFACE_ROUTE_RESOLVED_EVENT = "govoplan:product-surface-route-resolved";
export type ProductSurfaceRouteResolvedEventDetail = {
contractVersion: "1";
productSurfaceId: string;
requestedPath: string;
targetPath: string;
contributorModuleId: string;
usedAlias: boolean;
};
export function composeProductSurfaces(
modules: readonly PlatformWebModule[]
): ComposedProductSurface[] {
const composed = new Map<string, ComposedProductSurface>();
const contributions = modules.flatMap((module) => [
...(module.productSurfaces ?? []),
...(module.productSurfaceMetadata ?? []).map(productSurfaceFromMetadata)
]);
for (const contribution of contributions) {
const existing = composed.get(contribution.id);
if (!existing) {
composed.set(contribution.id, {
contractVersion: contribution.contractVersion,
id: contribution.id,
label: contribution.label,
description: contribution.description,
iconName: contribution.iconName,
entryPath: contribution.entryPath,
presentations: [...contribution.presentations],
contributors: [contribution],
aliases: [...contribution.aliases],
order: contribution.order
});
continue;
}
assertSharedIdentity(existing, contribution);
existing.contributors.push(contribution);
existing.aliases = [...new Set([...existing.aliases, ...contribution.aliases])];
existing.order = Math.min(existing.order, contribution.order);
}
return [...composed.values()]
.map((surface) => ({
...surface,
contributors: [...surface.contributors].sort(compareContributions),
aliases: [...surface.aliases].sort()
}))
.sort((left, right) => left.order - right.order || left.label.localeCompare(right.label));
}
function productSurfaceFromMetadata(surface: ProductSurfaceMetadata): ProductSurfaceContribution {
return {
contractVersion: surface.contract_version,
id: surface.id,
moduleId: surface.module_id,
label: surface.label,
description: surface.description,
iconName: surface.icon,
entryPath: surface.entry_path,
routePath: surface.route_path,
surfaceIds: surface.surface_ids,
presentations: surface.presentations,
capabilityIds: surface.capability_ids,
searchSourceIds: surface.search_source_ids,
helpContextIds: surface.help_context_ids,
documentationTopicIds: surface.documentation_topic_ids,
allOf: surface.required_all,
anyOf: surface.required_any,
aliases: surface.aliases,
order: surface.order,
unavailable: {
...surface.unavailable,
responsibleRole: surface.unavailable.responsible_role
},
degraded: surface.degraded ? {
...surface.degraded,
responsibleRole: surface.degraded.responsible_role
} : null
};
}
export function availableProductSurfaceContributors(
surface: ComposedProductSurface,
auth: AuthInfo | null | undefined,
modules: readonly PlatformWebModule[],
projection?: EffectiveViewProjection | null
): ProductSurfaceContribution[] {
const catalogue = viewSurfaceCatalogueForModules([...modules]);
return surface.contributors.filter((contribution) => {
if (contribution.allOf.length && !contribution.allOf.every((scope) => hasScope(auth, scope))) {
return false;
}
if (contribution.anyOf.length && !hasAnyScope(auth, contribution.anyOf)) {
return false;
}
return contribution.surfaceIds.some((surfaceId) =>
isViewSurfaceVisible(projection, surfaceId, catalogue)
);
});
}
export function dispatchProductSurfaceRouteResolved(
detail: ProductSurfaceRouteResolvedEventDetail
): void {
if (typeof window === "undefined") return;
window.dispatchEvent(new CustomEvent<ProductSurfaceRouteResolvedEventDetail>(
PRODUCT_SURFACE_ROUTE_RESOLVED_EVENT,
{ detail }
));
}
function assertSharedIdentity(
existing: ComposedProductSurface,
contribution: ProductSurfaceContribution
): void {
if (
existing.contractVersion !== contribution.contractVersion
|| existing.label !== contribution.label
|| existing.iconName !== contribution.iconName
|| existing.entryPath !== contribution.entryPath
|| existing.description !== contribution.description
) {
throw new Error(`Conflicting product surface identity: ${contribution.id}`);
}
}
function compareContributions(
left: ProductSurfaceContribution,
right: ProductSurfaceContribution
): number {
return left.order - right.order || left.moduleId.localeCompare(right.moduleId);
}
+91
View File
@@ -330,6 +330,59 @@ export type ProductAreaContribution = {
order?: number; order?: number;
}; };
export type ProductSurfacePresentation = "task" | "reader" | "admin" | "operator";
export type ProductAvailabilityReason =
| "authorization"
| "policy"
| "configuration"
| "disabled"
| "capability"
| "offline"
| "provider_degraded";
export type ProductAvailabilityExplanation = {
reason: ProductAvailabilityReason;
title: string;
description: string;
resolution: string;
responsibleRole?: string | null;
};
export type ProductSurfaceContribution = {
contractVersion: "1";
id: string;
moduleId: string;
label: string;
description?: string | null;
iconName: PlatformIconName;
entryPath: string;
routePath: string;
surfaceIds: string[];
presentations: ProductSurfacePresentation[];
capabilityIds: string[];
searchSourceIds: string[];
helpContextIds: string[];
documentationTopicIds: string[];
allOf: string[];
anyOf: string[];
aliases: string[];
order: number;
unavailable: ProductAvailabilityExplanation;
degraded?: ProductAvailabilityExplanation | null;
};
export type ComposedProductSurface = Omit<
ProductSurfaceContribution,
"moduleId" | "routePath" | "surfaceIds" | "capabilityIds" |
"searchSourceIds" | "helpContextIds" | "documentationTopicIds" |
"allOf" | "anyOf" | "aliases" | "order" | "unavailable" | "degraded"
> & {
contributors: ProductSurfaceContribution[];
aliases: string[];
order: number;
};
export type QuickAccessToolMetadata = { export type QuickAccessToolMetadata = {
contractVersion: "1"; contractVersion: "1";
id: string; id: string;
@@ -507,6 +560,8 @@ export type PlatformWebModule = {
runtimeUiCapabilities?: PlatformUiCapabilities; runtimeUiCapabilities?: PlatformUiCapabilities;
viewSurfaces?: PlatformViewSurface[]; viewSurfaces?: PlatformViewSurface[];
productAreas?: ProductAreaContribution[]; productAreas?: ProductAreaContribution[];
productSurfaces?: ProductSurfaceContribution[];
productSurfaceMetadata?: ProductSurfaceMetadata[];
quickAccessTools?: QuickAccessToolMetadata[]; quickAccessTools?: QuickAccessToolMetadata[];
helpContexts?: PlatformDocumentationHelpContext[]; helpContexts?: PlatformDocumentationHelpContext[];
}; };
@@ -1255,6 +1310,40 @@ export type PlatformFrontendModuleInfo = {
surface_ids: string[]; surface_ids: string[];
order: number; order: number;
}>; }>;
product_surfaces?: Array<{
contract_version: "1";
id: string;
module_id: string;
label: string;
description?: string | null;
icon: string;
entry_path: string;
route_path: string;
surface_ids: string[];
presentations: ProductSurfacePresentation[];
capability_ids: string[];
search_source_ids: string[];
help_context_ids: string[];
documentation_topic_ids: string[];
required_all: string[];
required_any: string[];
aliases: string[];
order: number;
unavailable: {
reason: ProductAvailabilityReason;
title: string;
description: string;
resolution: string;
responsible_role?: string | null;
};
degraded?: {
reason: ProductAvailabilityReason;
title: string;
description: string;
resolution: string;
responsible_role?: string | null;
} | null;
}>;
quick_access_tools?: Array<{ quick_access_tools?: Array<{
id: string; id: string;
module_id: string; module_id: string;
@@ -1277,6 +1366,8 @@ export type PlatformFrontendModuleInfo = {
}>; }>;
}; };
export type ProductSurfaceMetadata = NonNullable<PlatformFrontendModuleInfo["product_surfaces"]>[number];
export type PlatformDocumentationHelpContext = { export type PlatformDocumentationHelpContext = {
id: string; id: string;
topic_id: string; topic_id: string;
+22
View File
@@ -19,6 +19,7 @@ import MetricGrid from "../src/components/MetricGrid";
import PageActionBar from "../src/components/PageActionBar"; import PageActionBar from "../src/components/PageActionBar";
import SelectionList, { SelectionListItem, SelectionListItemContent } from "../src/components/SelectionList"; import SelectionList, { SelectionListItem, SelectionListItemContent } from "../src/components/SelectionList";
import StatePanel from "../src/components/StatePanel"; import StatePanel from "../src/components/StatePanel";
import ProductAvailabilityState from "../src/components/ProductAvailabilityState";
import WorkspaceLayout from "../src/components/WorkspaceLayout"; import WorkspaceLayout from "../src/components/WorkspaceLayout";
import WorkspaceFrame from "../src/components/WorkspaceFrame"; import WorkspaceFrame from "../src/components/WorkspaceFrame";
import WorkspaceActionBar from "../src/components/WorkspaceActionBar"; import WorkspaceActionBar from "../src/components/WorkspaceActionBar";
@@ -183,6 +184,27 @@ assert(workspaceMarkup.includes("selection-list-navigation"), "resource navigati
assert(workspaceMarkup.includes("selection-list-item-content"), "selection-list copy owns title and description typography"); assert(workspaceMarkup.includes("selection-list-item-content"), "selection-list copy owns title and description typography");
assert(workspaceMarkup.includes("state-panel-size-fill"), "whole-surface states share sizing and action anatomy"); assert(workspaceMarkup.includes("state-panel-size-fill"), "whole-surface states share sizing and action anatomy");
const availabilityMarkup = renderToStaticMarkup(
<PlatformLanguageProvider>
<ProductAvailabilityState
state="degraded"
explanation={{
reason: "provider_degraded",
title: "Messages are delayed",
description: "Saved messages remain available, but new provider results may be delayed.",
resolution: "Retry later or contact the integration operator.",
responsibleRole: "Integration operator"
}}
technical={{ moduleId: "mail", providerId: "smtp-primary", correlationId: "event-1" }}
actions={<button type="button">Retry</button>}
/>
</PlatformLanguageProvider>
);
assert(availabilityMarkup.includes("product-availability-degraded"), "product availability uses one semantic state primitive");
assert(availabilityMarkup.includes("Retry later or contact the integration operator."), "availability states include an actionable recovery path");
assert(availabilityMarkup.includes("<details"), "technical provenance remains available on demand");
assert(availabilityMarkup.includes("smtp-primary"), "technical details preserve exact provider provenance");
const frameMarkup = renderToStaticMarkup( const frameMarkup = renderToStaticMarkup(
<PlatformLanguageProvider><WorkspaceFrame as="main" height="viewport" label="Planning workspace" surface="panel"><span>Body</span></WorkspaceFrame></PlatformLanguageProvider> <PlatformLanguageProvider><WorkspaceFrame as="main" height="viewport" label="Planning workspace" surface="panel"><span>Body</span></WorkspaceFrame></PlatformLanguageProvider>
); );
+67
View File
@@ -18,6 +18,7 @@ import {
visibleRoutesForProjection visibleRoutesForProjection
} from "../src/platform/views"; } from "../src/platform/views";
import { groupNavigationItems } from "../src/platform/productAreas"; import { groupNavigationItems } from "../src/platform/productAreas";
import { composeProductSurfaces } from "../src/platform/productSurfaces";
import { hasAnyScope, scopeGrants } from "../src/utils/permissions"; import { hasAnyScope, scopeGrants } from "../src/utils/permissions";
function assert(condition: unknown, message: string): void { function assert(condition: unknown, message: string): void {
@@ -193,6 +194,72 @@ assert(
"flat navigation should retain every authorized destination" "flat navigation should retain every authorized destination"
); );
const productSurfaceExplanation = {
reason: "authorization" as const,
title: "Messages are unavailable",
description: "No message source is available for the current responsibility.",
resolution: "Ask the responsible administrator to review the assignment."
};
const messageSurfaceModules: PlatformWebModule[] = [
{
id: "mail",
label: "Mail",
version: "test",
productSurfaces: [{
contractVersion: "1",
id: "communication.messages",
moduleId: "mail",
label: "Messages",
description: "Read messages without merging channel custody.",
iconName: "mail",
entryPath: "/messages",
routePath: "/mail",
surfaceIds: ["mail.route.mail"],
presentations: ["task", "reader"],
capabilityIds: [],
searchSourceIds: ["mail.mailbox_messages"],
helpContextIds: ["mail.quick_access.messages"],
documentationTopicIds: ["mail.quick-access-and-product-area"],
allOf: [],
anyOf: ["mail:mailbox:read"],
aliases: ["/inbox"],
order: 10,
unavailable: productSurfaceExplanation
}]
},
{
id: "postbox",
label: "Postbox",
version: "test",
productSurfaceMetadata: [{
contract_version: "1",
id: "communication.messages",
module_id: "postbox",
label: "Messages",
description: "Read messages without merging channel custody.",
icon: "mail",
entry_path: "/messages",
route_path: "/postbox",
surface_ids: ["postbox.route.postbox"],
presentations: ["task", "reader"],
capability_ids: [],
search_source_ids: ["postbox.messages"],
help_context_ids: ["postbox.quick_access.messages"],
documentation_topic_ids: ["postbox.quick-access-and-product-area"],
required_all: [],
required_any: ["postbox:message:read"],
aliases: ["/inbox"],
order: 20,
unavailable: productSurfaceExplanation
}]
}
];
const composedMessages = composeProductSurfaces(messageSurfaceModules);
assert(composedMessages.length === 1, "related owner routes should compose into one product identity");
assert(composedMessages[0]?.entryPath === "/messages", "the composed identity should keep its stable entry path");
assert(composedMessages[0]?.contributors.map((item) => item.moduleId).join(",") === "mail,postbox", "composition should retain ordered technical provenance");
assert(composedMessages[0]?.aliases.join(",") === "/inbox", "migration aliases should be de-duplicated across owners");
const viewAwareFiles: PlatformWebModule = { const viewAwareFiles: PlatformWebModule = {
...files, ...files,
navItems: [{ to: "/files", label: "Files", order: 20, anyOf: ["files:file:read"] }], navItems: [{ to: "/files", label: "Files", order: 20, anyOf: ["files:file:read"] }],
+18 -1
View File
@@ -4,6 +4,7 @@ function assert(condition: unknown, message = "assertion failed"): asserts condi
import { renderToStaticMarkup } from "react-dom/server"; import { renderToStaticMarkup } from "react-dom/server";
import PasswordField from "../src/components/PasswordField"; import PasswordField from "../src/components/PasswordField";
import PasswordGeneratorDialog from "../src/components/PasswordGeneratorDialog";
import { import {
DEFAULT_PASSWORD_GENERATOR_OPTIONS, DEFAULT_PASSWORD_GENERATOR_OPTIONS,
generateSecurePassword, generateSecurePassword,
@@ -56,11 +57,27 @@ for (const [options, expected] of [
const markup = renderToStaticMarkup( const markup = renderToStaticMarkup(
<PlatformLanguageProvider> <PlatformLanguageProvider>
<PasswordField value="" onValueChange={() => undefined} generator /> <PasswordField value="" onValueChange={() => undefined} generator helpContextId="access.authentication.password" helpModuleId="access" />
</PlatformLanguageProvider> </PlatformLanguageProvider>
); );
assert(markup.includes('aria-label="Generate password"'), "the opt-in generator action is accessible"); assert(markup.includes('aria-label="Generate password"'), "the opt-in generator action is accessible");
assert(markup.includes("lucide-dice-5"), "the familiar generator icon is used"); assert(markup.includes("lucide-dice-5"), "the familiar generator icon is used");
assert(!markup.includes("password-generator-dialog"), "the generator dialog stays closed until explicitly requested"); assert(!markup.includes("password-generator-dialog"), "the generator dialog stays closed until explicitly requested");
assert(markup.includes('data-help-context-id="access.authentication.password"'), "the owner context reaches the password field and its actions");
assert(markup.includes('data-help-module-id="access"'), "the password field retains its documentation owner");
const dialogMarkup = renderToStaticMarkup(
<PlatformLanguageProvider>
<PasswordGeneratorDialog
open
helpContextId="access.authentication.password"
helpModuleId="access"
onUse={() => undefined}
onClose={() => undefined}
/>
</PlatformLanguageProvider>
);
assert(dialogMarkup.includes('data-help-context-id="access.authentication.password"'), "the generator dialog inherits the calling credential context");
assert(dialogMarkup.includes('data-help-module-id="access"'), "generated-password controls retain the credential owner's module");
console.log("Password generator contract passed."); console.log("Password generator contract passed.");
+1
View File
@@ -22,6 +22,7 @@
"tests/definition-graph.test.ts", "tests/definition-graph.test.ts",
"src/platform/moduleLogic.ts", "src/platform/moduleLogic.ts",
"src/platform/productAreas.ts", "src/platform/productAreas.ts",
"src/platform/productSurfaces.ts",
"src/platform/launchContext.ts", "src/platform/launchContext.ts",
"src/utils/helpContext.ts", "src/utils/helpContext.ts",
"src/features/privacy/policyLogic.ts", "src/features/privacy/policyLogic.ts",
+4
View File
@@ -214,6 +214,10 @@ export default defineConfig({
rollupOptions: { rollupOptions: {
output: { output: {
manualChunks: deferredVendorChunk, manualChunks: deferredVendorChunk,
// Dynamic chunk names are implementation details. Keeping only the
// content hash avoids shipping every source/module name in Vite's
// preload table, which is part of the initial application payload.
chunkFileNames: "assets/c-[hash].js",
// Keep dependencies of deferred BPMN packages in their lazy graph. The // Keep dependencies of deferred BPMN packages in their lazy graph. The
// legacy Rollup behavior merged those dependencies into manual chunks // legacy Rollup behavior merged those dependencies into manual chunks
// and hoisted the properties-panel runtime into the application entry. // and hoisted the properties-panel runtime into the application entry.