diff --git a/.gitignore b/.gitignore index 3493a7c756..836bddbb49 100644 --- a/.gitignore +++ b/.gitignore @@ -237,6 +237,10 @@ scripts/stress-test/reports/ .playwright-mcp/ .serena/ +# vitest browser mode attachments (failure screenshots, traces, etc.) +.vitest-attachments/ +**/__screenshots__/ + # settings *.local.json *.local.md diff --git a/api/controllers/service_api/dataset/document.py b/api/controllers/service_api/dataset/document.py index 6db047567f..bc28ecb6b7 100644 --- a/api/controllers/service_api/dataset/document.py +++ b/api/controllers/service_api/dataset/document.py @@ -1,4 +1,12 @@ +"""Service API endpoints for dataset document management. + +The canonical Service API paths use hyphenated route segments. Legacy underscore +aliases remain registered for backward compatibility, but they must stay marked +deprecated in generated API docs so clients migrate toward the canonical paths. +""" + import json +from collections.abc import Mapping from contextlib import ExitStack from typing import Self from uuid import UUID @@ -117,12 +125,137 @@ register_schema_models( ) -@service_api_ns.route( - "/datasets//document/create_by_text", - "/datasets//document/create-by-text", -) +def _create_document_by_text(tenant_id: str, dataset_id: UUID) -> tuple[Mapping[str, object], int]: + """Create a document from text for both canonical and legacy routes.""" + payload = DocumentTextCreatePayload.model_validate(service_api_ns.payload or {}) + args = payload.model_dump(exclude_none=True) + + dataset_id_str = str(dataset_id) + tenant_id_str = str(tenant_id) + dataset = db.session.scalar( + select(Dataset).where(Dataset.tenant_id == tenant_id_str, Dataset.id == dataset_id_str).limit(1) + ) + + if not dataset: + raise ValueError("Dataset does not exist.") + + if not dataset.indexing_technique and not args["indexing_technique"]: + raise ValueError("indexing_technique is required.") + + embedding_model_provider = payload.embedding_model_provider + embedding_model = payload.embedding_model + if embedding_model_provider and embedding_model: + DatasetService.check_embedding_model_setting(tenant_id_str, embedding_model_provider, embedding_model) + + retrieval_model = payload.retrieval_model + if ( + retrieval_model + and retrieval_model.reranking_model + and retrieval_model.reranking_model.reranking_provider_name + and retrieval_model.reranking_model.reranking_model_name + ): + DatasetService.check_reranking_model_setting( + tenant_id_str, + retrieval_model.reranking_model.reranking_provider_name, + retrieval_model.reranking_model.reranking_model_name, + ) + + if not current_user: + raise ValueError("current_user is required") + + upload_file = FileService(db.engine).upload_text( + text=payload.text, text_name=payload.name, user_id=current_user.id, tenant_id=tenant_id_str + ) + data_source = { + "type": "upload_file", + "info_list": {"data_source_type": "upload_file", "file_info_list": {"file_ids": [upload_file.id]}}, + } + args["data_source"] = data_source + knowledge_config = KnowledgeConfig.model_validate(args) + DocumentService.document_create_args_validate(knowledge_config) + + if not current_user: + raise ValueError("current_user is required") + + try: + documents, batch = DocumentService.save_document_with_dataset_id( + dataset=dataset, + knowledge_config=knowledge_config, + account=current_user, + dataset_process_rule=dataset.latest_process_rule if "process_rule" not in args else None, + created_from="api", + ) + except ProviderTokenNotInitError as ex: + raise ProviderNotInitializeError(ex.description) + document = documents[0] + + documents_and_batch_fields = {"document": marshal(document, document_fields), "batch": batch} + return documents_and_batch_fields, 200 + + +def _update_document_by_text(tenant_id: str, dataset_id: UUID, document_id: UUID) -> tuple[Mapping[str, object], int]: + """Update a document from text for both canonical and legacy routes.""" + payload = DocumentTextUpdate.model_validate(service_api_ns.payload or {}) + dataset = db.session.scalar( + select(Dataset).where(Dataset.tenant_id == tenant_id, Dataset.id == str(dataset_id)).limit(1) + ) + args = payload.model_dump(exclude_none=True) + if not dataset: + raise ValueError("Dataset does not exist.") + + retrieval_model = payload.retrieval_model + if ( + retrieval_model + and retrieval_model.reranking_model + and retrieval_model.reranking_model.reranking_provider_name + and retrieval_model.reranking_model.reranking_model_name + ): + DatasetService.check_reranking_model_setting( + tenant_id, + retrieval_model.reranking_model.reranking_provider_name, + retrieval_model.reranking_model.reranking_model_name, + ) + + # indexing_technique is already set in dataset since this is an update + args["indexing_technique"] = dataset.indexing_technique + + if args.get("text"): + text = args.get("text") + name = args.get("name") + if not current_user: + raise ValueError("current_user is required") + upload_file = FileService(db.engine).upload_text( + text=str(text), text_name=str(name), user_id=current_user.id, tenant_id=tenant_id + ) + data_source = { + "type": "upload_file", + "info_list": {"data_source_type": "upload_file", "file_info_list": {"file_ids": [upload_file.id]}}, + } + args["data_source"] = data_source + + args["original_document_id"] = str(document_id) + knowledge_config = KnowledgeConfig.model_validate(args) + DocumentService.document_create_args_validate(knowledge_config) + + try: + documents, batch = DocumentService.save_document_with_dataset_id( + dataset=dataset, + knowledge_config=knowledge_config, + account=current_user, + dataset_process_rule=dataset.latest_process_rule if "process_rule" not in args else None, + created_from="api", + ) + except ProviderTokenNotInitError as ex: + raise ProviderNotInitializeError(ex.description) + document = documents[0] + + documents_and_batch_fields = {"document": marshal(document, document_fields), "batch": batch} + return documents_and_batch_fields, 200 + + +@service_api_ns.route("/datasets//document/create-by-text") class DocumentAddByTextApi(DatasetApiResource): - """Resource for documents.""" + """Resource for the canonical text document creation route.""" @service_api_ns.expect(service_api_ns.models[DocumentTextCreatePayload.__name__]) @service_api_ns.doc("create_document_by_text") @@ -138,81 +271,43 @@ class DocumentAddByTextApi(DatasetApiResource): @cloud_edition_billing_resource_check("vector_space", "dataset") @cloud_edition_billing_resource_check("documents", "dataset") @cloud_edition_billing_rate_limit_check("knowledge", "dataset") - def post(self, tenant_id, dataset_id): + def post(self, tenant_id: str, dataset_id: UUID): """Create document by text.""" - payload = DocumentTextCreatePayload.model_validate(service_api_ns.payload or {}) - args = payload.model_dump(exclude_none=True) + return _create_document_by_text(tenant_id=tenant_id, dataset_id=dataset_id) - dataset_id = str(dataset_id) - tenant_id = str(tenant_id) - dataset = db.session.scalar( - select(Dataset).where(Dataset.tenant_id == tenant_id, Dataset.id == dataset_id).limit(1) + +@service_api_ns.route("/datasets//document/create_by_text") +class DeprecatedDocumentAddByTextApi(DatasetApiResource): + """Deprecated resource alias for text document creation.""" + + @service_api_ns.expect(service_api_ns.models[DocumentTextCreatePayload.__name__]) + @service_api_ns.doc("create_document_by_text_deprecated") + @service_api_ns.doc(deprecated=True) + @service_api_ns.doc( + description=( + "Deprecated legacy alias for creating a new document by providing text content. " + "Use /datasets/{dataset_id}/document/create-by-text instead." ) - - if not dataset: - raise ValueError("Dataset does not exist.") - - if not dataset.indexing_technique and not args["indexing_technique"]: - raise ValueError("indexing_technique is required.") - - embedding_model_provider = payload.embedding_model_provider - embedding_model = payload.embedding_model - if embedding_model_provider and embedding_model: - DatasetService.check_embedding_model_setting(tenant_id, embedding_model_provider, embedding_model) - - retrieval_model = payload.retrieval_model - if ( - retrieval_model - and retrieval_model.reranking_model - and retrieval_model.reranking_model.reranking_provider_name - and retrieval_model.reranking_model.reranking_model_name - ): - DatasetService.check_reranking_model_setting( - tenant_id, - retrieval_model.reranking_model.reranking_provider_name, - retrieval_model.reranking_model.reranking_model_name, - ) - - if not current_user: - raise ValueError("current_user is required") - - upload_file = FileService(db.engine).upload_text( - text=payload.text, text_name=payload.name, user_id=current_user.id, tenant_id=tenant_id - ) - data_source = { - "type": "upload_file", - "info_list": {"data_source_type": "upload_file", "file_info_list": {"file_ids": [upload_file.id]}}, + ) + @service_api_ns.doc(params={"dataset_id": "Dataset ID"}) + @service_api_ns.doc( + responses={ + 200: "Document created successfully", + 401: "Unauthorized - invalid API token", + 400: "Bad request - invalid parameters", } - args["data_source"] = data_source - knowledge_config = KnowledgeConfig.model_validate(args) - # validate args - DocumentService.document_create_args_validate(knowledge_config) - - if not current_user: - raise ValueError("current_user is required") - - try: - documents, batch = DocumentService.save_document_with_dataset_id( - dataset=dataset, - knowledge_config=knowledge_config, - account=current_user, - dataset_process_rule=dataset.latest_process_rule if "process_rule" not in args else None, - created_from="api", - ) - except ProviderTokenNotInitError as ex: - raise ProviderNotInitializeError(ex.description) - document = documents[0] - - documents_and_batch_fields = {"document": marshal(document, document_fields), "batch": batch} - return documents_and_batch_fields, 200 + ) + @cloud_edition_billing_resource_check("vector_space", "dataset") + @cloud_edition_billing_resource_check("documents", "dataset") + @cloud_edition_billing_rate_limit_check("knowledge", "dataset") + def post(self, tenant_id: str, dataset_id: UUID): + """Create document by text through the deprecated underscore alias.""" + return _create_document_by_text(tenant_id=tenant_id, dataset_id=dataset_id) -@service_api_ns.route( - "/datasets//documents//update_by_text", - "/datasets//documents//update-by-text", -) +@service_api_ns.route("/datasets//documents//update-by-text") class DocumentUpdateByTextApi(DatasetApiResource): - """Resource for update documents.""" + """Resource for the canonical text document update route.""" @service_api_ns.expect(service_api_ns.models[DocumentTextUpdate.__name__]) @service_api_ns.doc("update_document_by_text") @@ -229,62 +324,35 @@ class DocumentUpdateByTextApi(DatasetApiResource): @cloud_edition_billing_rate_limit_check("knowledge", "dataset") def post(self, tenant_id: str, dataset_id: UUID, document_id: UUID): """Update document by text.""" - payload = DocumentTextUpdate.model_validate(service_api_ns.payload or {}) - dataset = db.session.scalar( - select(Dataset).where(Dataset.tenant_id == tenant_id, Dataset.id == str(dataset_id)).limit(1) + return _update_document_by_text(tenant_id=tenant_id, dataset_id=dataset_id, document_id=document_id) + + +@service_api_ns.route("/datasets//documents//update_by_text") +class DeprecatedDocumentUpdateByTextApi(DatasetApiResource): + """Deprecated resource alias for text document updates.""" + + @service_api_ns.expect(service_api_ns.models[DocumentTextUpdate.__name__]) + @service_api_ns.doc("update_document_by_text_deprecated") + @service_api_ns.doc(deprecated=True) + @service_api_ns.doc( + description=( + "Deprecated legacy alias for updating an existing document by providing text content. " + "Use /datasets/{dataset_id}/documents/{document_id}/update-by-text instead." ) - args = payload.model_dump(exclude_none=True) - if not dataset: - raise ValueError("Dataset does not exist.") - - retrieval_model = payload.retrieval_model - if ( - retrieval_model - and retrieval_model.reranking_model - and retrieval_model.reranking_model.reranking_provider_name - and retrieval_model.reranking_model.reranking_model_name - ): - DatasetService.check_reranking_model_setting( - tenant_id, - retrieval_model.reranking_model.reranking_provider_name, - retrieval_model.reranking_model.reranking_model_name, - ) - - # indexing_technique is already set in dataset since this is an update - args["indexing_technique"] = dataset.indexing_technique - - if args.get("text"): - text = args.get("text") - name = args.get("name") - if not current_user: - raise ValueError("current_user is required") - upload_file = FileService(db.engine).upload_text( - text=str(text), text_name=str(name), user_id=current_user.id, tenant_id=tenant_id - ) - data_source = { - "type": "upload_file", - "info_list": {"data_source_type": "upload_file", "file_info_list": {"file_ids": [upload_file.id]}}, - } - args["data_source"] = data_source - # validate args - args["original_document_id"] = str(document_id) - knowledge_config = KnowledgeConfig.model_validate(args) - DocumentService.document_create_args_validate(knowledge_config) - - try: - documents, batch = DocumentService.save_document_with_dataset_id( - dataset=dataset, - knowledge_config=knowledge_config, - account=current_user, - dataset_process_rule=dataset.latest_process_rule if "process_rule" not in args else None, - created_from="api", - ) - except ProviderTokenNotInitError as ex: - raise ProviderNotInitializeError(ex.description) - document = documents[0] - - documents_and_batch_fields = {"document": marshal(document, document_fields), "batch": batch} - return documents_and_batch_fields, 200 + ) + @service_api_ns.doc(params={"dataset_id": "Dataset ID", "document_id": "Document ID"}) + @service_api_ns.doc( + responses={ + 200: "Document updated successfully", + 401: "Unauthorized - invalid API token", + 404: "Document not found", + } + ) + @cloud_edition_billing_resource_check("vector_space", "dataset") + @cloud_edition_billing_rate_limit_check("knowledge", "dataset") + def post(self, tenant_id: str, dataset_id: UUID, document_id: UUID): + """Update document by text through the deprecated underscore alias.""" + return _update_document_by_text(tenant_id=tenant_id, dataset_id=dataset_id, document_id=document_id) @service_api_ns.route( diff --git a/api/core/app/file_access/scope.py b/api/core/app/file_access/scope.py index 80d504ef1c..a583301f9b 100644 --- a/api/core/app/file_access/scope.py +++ b/api/core/app/file_access/scope.py @@ -1,6 +1,6 @@ from __future__ import annotations -from collections.abc import Iterator +from collections.abc import Generator # Changed from Iterator from contextlib import contextmanager from contextvars import ContextVar from dataclasses import dataclass @@ -32,7 +32,7 @@ def get_current_file_access_scope() -> FileAccessScope | None: @contextmanager -def bind_file_access_scope(scope: FileAccessScope) -> Iterator[None]: +def bind_file_access_scope(scope: FileAccessScope) -> Generator[None, None, None]: # Changed from Iterator[None] token = _current_file_access_scope.set(scope) try: yield diff --git a/api/core/provider_manager.py b/api/core/provider_manager.py index c3bbe8fc09..8969825be4 100644 --- a/api/core/provider_manager.py +++ b/api/core/provider_manager.py @@ -70,12 +70,32 @@ class ProviderManager: Request-bound managers may carry caller identity in that runtime, and the resulting ``ProviderConfiguration`` objects must reuse it for downstream model-type and schema lookups. + + Configuration assembly is cached per manager instance so call chains that + share one request-scoped manager can reuse the same provider graph instead + of rebuilding it for every lookup. Call ``clear_configurations_cache()`` + when a long-lived manager needs to observe writes performed within the same + instance scope. """ + decoding_rsa_key: Any | None + decoding_cipher_rsa: Any | None + _model_runtime: ModelRuntime + _configurations_cache: dict[str, ProviderConfigurations] + def __init__(self, model_runtime: ModelRuntime): self.decoding_rsa_key = None self.decoding_cipher_rsa = None self._model_runtime = model_runtime + self._configurations_cache = {} + + def clear_configurations_cache(self, tenant_id: str | None = None) -> None: + """Drop assembled provider configurations cached on this manager instance.""" + if tenant_id is None: + self._configurations_cache.clear() + return + + self._configurations_cache.pop(tenant_id, None) def get_configurations(self, tenant_id: str) -> ProviderConfigurations: """ @@ -114,6 +134,10 @@ class ProviderManager: :param tenant_id: :return: """ + cached_configurations = self._configurations_cache.get(tenant_id) + if cached_configurations is not None: + return cached_configurations + # Get all provider records of the workspace provider_name_to_provider_records_dict = self._get_all_providers(tenant_id) @@ -273,6 +297,8 @@ class ProviderManager: provider_configurations[str(provider_id_entity)] = provider_configuration + self._configurations_cache[tenant_id] = provider_configurations + # Return the encapsulated object return provider_configurations diff --git a/api/core/rag/datasource/keyword/jieba/jieba.py b/api/core/rag/datasource/keyword/jieba/jieba.py index ed264878d3..242da520c1 100644 --- a/api/core/rag/datasource/keyword/jieba/jieba.py +++ b/api/core/rag/datasource/keyword/jieba/jieba.py @@ -139,8 +139,10 @@ class Jieba(BaseKeyword): "__data__": {"index_id": self.dataset.id, "summary": None, "table": keyword_table}, } dataset_keyword_table = self.dataset.dataset_keyword_table - keyword_data_source_type = dataset_keyword_table.data_source_type + keyword_data_source_type = dataset_keyword_table.data_source_type if dataset_keyword_table else "file" if keyword_data_source_type == "database": + if dataset_keyword_table is None: + return dataset_keyword_table.keyword_table = dumps_with_sets(keyword_table_dict) db.session.commit() else: diff --git a/api/core/rag/datasource/keyword/jieba/jieba_keyword_table_handler.py b/api/core/rag/datasource/keyword/jieba/jieba_keyword_table_handler.py index 84f35c25f8..1ca6303af6 100644 --- a/api/core/rag/datasource/keyword/jieba/jieba_keyword_table_handler.py +++ b/api/core/rag/datasource/keyword/jieba/jieba_keyword_table_handler.py @@ -1,4 +1,5 @@ import re +from collections.abc import Callable from operator import itemgetter from typing import cast @@ -80,12 +81,14 @@ class JiebaKeywordTableHandler: def extract_tags(self, sentence: str, top_k: int | None = 20, **kwargs): # Basic frequency-based keyword extraction as a fallback when TF-IDF is unavailable. - top_k = kwargs.pop("topK", top_k) + top_k = cast(int | None, kwargs.pop("topK", top_k)) + if top_k is None: + top_k = 20 cut = getattr(jieba, "cut", None) if self._lcut: tokens = self._lcut(sentence) elif callable(cut): - tokens = list(cut(sentence)) + tokens = list(cast(Callable[[str], list[str]], cut)(sentence)) else: tokens = re.findall(r"\w+", sentence) @@ -108,7 +111,7 @@ class JiebaKeywordTableHandler: sentence=text, topK=max_keywords_per_chunk, ) - # jieba.analyse.extract_tags returns list[Any] when withFlag is False by default. + # jieba.analyse.extract_tags returns an untyped list when withFlag is False by default. keywords = cast(list[str], keywords) return set(self._expand_tokens_with_subtokens(set(keywords))) diff --git a/api/core/rag/datasource/retrieval_service.py b/api/core/rag/datasource/retrieval_service.py index 7e71d67ec0..2997710daf 100644 --- a/api/core/rag/datasource/retrieval_service.py +++ b/api/core/rag/datasource/retrieval_service.py @@ -158,7 +158,7 @@ class RetrievalService: ) if futures: - for future in concurrent.futures.as_completed(futures, timeout=3600): + for _ in concurrent.futures.as_completed(futures, timeout=3600): if exceptions: for f in futures: f.cancel() diff --git a/api/core/rag/extractor/extract_processor.py b/api/core/rag/extractor/extract_processor.py index fbd2a6db93..b679edab36 100644 --- a/api/core/rag/extractor/extract_processor.py +++ b/api/core/rag/extractor/extract_processor.py @@ -94,6 +94,7 @@ class ExtractProcessor: cls, extract_setting: ExtractSetting, is_automatic: bool = False, file_path: str | None = None ) -> list[Document]: if extract_setting.datasource_type == DatasourceType.FILE: + upload_file = extract_setting.upload_file with tempfile.TemporaryDirectory() as temp_dir: upload_file = extract_setting.upload_file if not file_path: @@ -104,6 +105,7 @@ class ExtractProcessor: storage.download(upload_file.key, file_path) input_file = Path(file_path) file_extension = input_file.suffix.lower() + assert upload_file is not None, "upload_file is required" etl_type = dify_config.ETL_TYPE extractor: BaseExtractor | None = None if etl_type == "Unstructured": diff --git a/api/core/rag/retrieval/router/multi_dataset_function_call_router.py b/api/core/rag/retrieval/router/multi_dataset_function_call_router.py index e617a9660e..426d1b67dc 100644 --- a/api/core/rag/retrieval/router/multi_dataset_function_call_router.py +++ b/api/core/rag/retrieval/router/multi_dataset_function_call_router.py @@ -28,7 +28,7 @@ class FunctionCallMultiDatasetRouter: SystemPromptMessage(content="You are a helpful AI assistant."), UserPromptMessage(content=query), ] - result: LLMResult = model_instance.invoke_llm( + result: LLMResult = model_instance.invoke_llm( # pyright: ignore[reportCallIssue, reportArgumentType] prompt_messages=prompt_messages, tools=dataset_tools, stream=False, diff --git a/api/core/rag/splitter/fixed_text_splitter.py b/api/core/rag/splitter/fixed_text_splitter.py index 66b375dad1..52c9a02f97 100644 --- a/api/core/rag/splitter/fixed_text_splitter.py +++ b/api/core/rag/splitter/fixed_text_splitter.py @@ -4,7 +4,7 @@ from __future__ import annotations import codecs import re -from collections.abc import Collection +from collections.abc import Set as AbstractSet from typing import Any, Literal from core.model_manager import ModelInstance @@ -21,8 +21,8 @@ class EnhanceRecursiveCharacterTextSplitter(RecursiveCharacterTextSplitter): def from_encoder[T: EnhanceRecursiveCharacterTextSplitter]( cls: type[T], embedding_model_instance: ModelInstance | None, - allowed_special: Literal["all"] | set[str] = set(), - disallowed_special: Literal["all"] | Collection[str] = "all", + allowed_special: Literal["all"] | AbstractSet[str] = frozenset(), + disallowed_special: Literal["all"] | AbstractSet[str] = "all", **kwargs: Any, ) -> T: def _token_encoder(texts: list[str]) -> list[int]: @@ -40,6 +40,7 @@ class EnhanceRecursiveCharacterTextSplitter(RecursiveCharacterTextSplitter): return [len(text) for text in texts] + _ = _token_encoder # kept for future token-length wiring return cls(length_function=_character_encoder, **kwargs) diff --git a/api/core/rag/splitter/text_splitter.py b/api/core/rag/splitter/text_splitter.py index 7f2117e2dd..a8d9013fbc 100644 --- a/api/core/rag/splitter/text_splitter.py +++ b/api/core/rag/splitter/text_splitter.py @@ -4,7 +4,8 @@ import copy import logging import re from abc import ABC, abstractmethod -from collections.abc import Callable, Collection, Iterable, Sequence, Set +from collections.abc import Callable, Iterable, Sequence +from collections.abc import Set as AbstractSet from dataclasses import dataclass from typing import Any, Literal @@ -187,8 +188,8 @@ class TokenTextSplitter(TextSplitter): self, encoding_name: str = "gpt2", model_name: str | None = None, - allowed_special: Literal["all"] | Set[str] = set(), - disallowed_special: Literal["all"] | Collection[str] = "all", + allowed_special: Literal["all"] | AbstractSet[str] = frozenset(), + disallowed_special: Literal["all"] | AbstractSet[str] = "all", **kwargs: Any, ): """Create a new TextSplitter.""" @@ -207,8 +208,8 @@ class TokenTextSplitter(TextSplitter): else: enc = tiktoken.get_encoding(encoding_name) self._tokenizer = enc - self._allowed_special = allowed_special - self._disallowed_special = disallowed_special + self._allowed_special: Literal["all"] | AbstractSet[str] = allowed_special + self._disallowed_special: Literal["all"] | AbstractSet[str] = disallowed_special def split_text(self, text: str) -> list[str]: def _encode(_text: str) -> list[int]: diff --git a/api/core/tools/utils/web_reader_tool.py b/api/core/tools/utils/web_reader_tool.py index ed3ed3e0de..94a2c0427b 100644 --- a/api/core/tools/utils/web_reader_tool.py +++ b/api/core/tools/utils/web_reader_tool.py @@ -105,7 +105,7 @@ class Article: def extract_using_readabilipy(html: str): - json_article: dict[str, Any] = simple_json_from_html_string(html, use_readability=True) + json_article: dict[str, Any] = simple_json_from_html_string(html, use_readability=False) article = Article( title=json_article.get("title") or "", author=json_article.get("byline") or "", diff --git a/api/libs/flask_utils.py b/api/libs/flask_utils.py index 52fc787c79..838af2bf32 100644 --- a/api/libs/flask_utils.py +++ b/api/libs/flask_utils.py @@ -1,5 +1,5 @@ import contextvars -from collections.abc import Iterator +from collections.abc import Generator # Changed from Iterator from contextlib import contextmanager from typing import TYPE_CHECKING @@ -13,7 +13,7 @@ if TYPE_CHECKING: def preserve_flask_contexts( flask_app: Flask, context_vars: contextvars.Context, -) -> Iterator[None]: +) -> Generator[None, None, None]: # Changed from Iterator[None] """ A context manager that handles: 1. flask-login's UserProxy copy diff --git a/api/providers/vdb/vdb-analyticdb/src/dify_vdb_analyticdb/analyticdb_vector_sql.py b/api/providers/vdb/vdb-analyticdb/src/dify_vdb_analyticdb/analyticdb_vector_sql.py index b2908ebdae..11398efb58 100644 --- a/api/providers/vdb/vdb-analyticdb/src/dify_vdb_analyticdb/analyticdb_vector_sql.py +++ b/api/providers/vdb/vdb-analyticdb/src/dify_vdb_analyticdb/analyticdb_vector_sql.py @@ -1,6 +1,6 @@ import json import uuid -from collections.abc import Iterator +from collections.abc import Generator # Added Generator from contextlib import contextmanager from typing import Any @@ -75,7 +75,7 @@ class AnalyticdbVectorBySql: ) @contextmanager - def _get_cursor(self) -> Iterator[Any]: + def _get_cursor(self) -> Generator[Any, None, None]: # Changed from Iterator[Any] assert self.pool is not None, "Connection pool is not initialized" conn = self.pool.getconn() cur = conn.cursor() diff --git a/api/pyproject.toml b/api/pyproject.toml index b13c744f0a..fbd5d394ad 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -114,10 +114,10 @@ override-dependencies = [ dev = [ "coverage>=7.13.4", "dotenv-linter>=0.7.0", - "faker>=20.1.0", + "faker>=40.15.0", "lxml-stubs>=0.5.1", - "basedpyright>=1.39.0", - "ruff>=0.15.10", + "basedpyright>=1.39.3", + "ruff>=0.15.11", "pytest>=9.0.3", "pytest-benchmark>=5.2.3", "pytest-cov>=7.1.0", @@ -157,14 +157,14 @@ dev = [ "types-tensorflow>=2.18.0.20260408", "types-tqdm>=4.67.3.20260408", "types-ujson>=5.10.0", - "boto3-stubs>=1.42.88", + "boto3-stubs>=1.42.92", "types-jmespath>=1.1.0.20260408", - "hypothesis>=6.151.12", + "hypothesis>=6.152.1", "types_pyOpenSSL>=24.1.0", "types_cffi>=2.0.0.20260408", "types_setuptools>=82.0.0.20260408", "pandas-stubs>=3.0.0", - "scipy-stubs>=1.15.3.0", + "scipy-stubs>=1.17.1.4", "types-python-http-client>=3.3.7.20260408", "import-linter>=2.3", "types-redis>=4.6.0.20241004", diff --git a/api/services/file_service.py b/api/services/file_service.py index 52da2a7951..f60afe2f19 100644 --- a/api/services/file_service.py +++ b/api/services/file_service.py @@ -2,7 +2,7 @@ import base64 import hashlib import os import uuid -from collections.abc import Iterator, Sequence +from collections.abc import Generator, Sequence # Changed Iterator to Generator from contextlib import contextmanager, suppress from tempfile import NamedTemporaryFile from typing import Literal @@ -324,7 +324,7 @@ class FileService: def build_upload_files_zip_tempfile( *, upload_files: Sequence[UploadFile], - ) -> Iterator[str]: + ) -> Generator[str, None, None]: # Changed from Iterator[str] """ Build a ZIP from `UploadFile`s and yield a tempfile path. diff --git a/api/services/hit_testing_service.py b/api/services/hit_testing_service.py index ca84b2a3d8..2e5987dd28 100644 --- a/api/services/hit_testing_service.py +++ b/api/services/hit_testing_service.py @@ -1,10 +1,10 @@ import json import logging import time -from typing import Any, TypedDict +from typing import Any, TypedDict, cast from core.app.app_config.entities import ModelConfig -from core.rag.datasource.retrieval_service import RetrievalService +from core.rag.datasource.retrieval_service import DefaultRetrievalModelDict, RetrievalService from core.rag.index_processor.constant.query_type import QueryType from core.rag.models.document import Document from core.rag.retrieval.dataset_retrieval import DatasetRetrieval @@ -36,6 +36,10 @@ default_retrieval_model = { } +class HitTestingRetrievalModelDict(DefaultRetrievalModelDict, total=False): + metadata_filtering_conditions: dict[str, Any] + + class HitTestingService: @classmethod def retrieve( @@ -51,17 +55,18 @@ class HitTestingService: start = time.perf_counter() # get retrieval model , if the model is not setting , using default - if not retrieval_model: - retrieval_model = dataset.retrieval_model or default_retrieval_model - assert isinstance(retrieval_model, dict) + resolved_retrieval_model = cast( + HitTestingRetrievalModelDict, + retrieval_model or dataset.retrieval_model or default_retrieval_model, + ) document_ids_filter = None - metadata_filtering_conditions = retrieval_model.get("metadata_filtering_conditions", {}) - if metadata_filtering_conditions and query: + metadata_filtering_conditions_raw = resolved_retrieval_model.get("metadata_filtering_conditions", {}) + if metadata_filtering_conditions_raw and query: dataset_retrieval = DatasetRetrieval() from core.rag.entities import MetadataFilteringCondition - metadata_filtering_conditions = MetadataFilteringCondition.model_validate(metadata_filtering_conditions) + metadata_filtering_conditions = MetadataFilteringCondition.model_validate(metadata_filtering_conditions_raw) metadata_filter_document_ids, metadata_condition = dataset_retrieval.get_metadata_filter_condition( dataset_ids=[dataset.id], @@ -78,19 +83,21 @@ class HitTestingService: if metadata_condition and not document_ids_filter: return cls.compact_retrieve_response(query, []) all_documents = RetrievalService.retrieve( - retrieval_method=RetrievalMethod(retrieval_model.get("search_method", RetrievalMethod.SEMANTIC_SEARCH)), + retrieval_method=RetrievalMethod( + resolved_retrieval_model.get("search_method", RetrievalMethod.SEMANTIC_SEARCH) + ), dataset_id=dataset.id, query=query, attachment_ids=attachment_ids, - top_k=retrieval_model.get("top_k", 4), - score_threshold=retrieval_model.get("score_threshold", 0.0) - if retrieval_model["score_threshold_enabled"] + top_k=resolved_retrieval_model.get("top_k", 4), + score_threshold=resolved_retrieval_model.get("score_threshold", 0.0) + if resolved_retrieval_model["score_threshold_enabled"] else 0.0, - reranking_model=retrieval_model.get("reranking_model", None) - if retrieval_model["reranking_enable"] + reranking_model=resolved_retrieval_model.get("reranking_model", None) + if resolved_retrieval_model["reranking_enable"] else None, - reranking_mode=retrieval_model.get("reranking_mode") or "reranking_model", - weights=retrieval_model.get("weights", None), + reranking_mode=resolved_retrieval_model.get("reranking_mode") or "reranking_model", + weights=resolved_retrieval_model.get("weights", None), document_ids_filter=document_ids_filter, ) diff --git a/api/services/plugin/plugin_auto_upgrade_service.py b/api/services/plugin/plugin_auto_upgrade_service.py index 9bb0ab6ae2..b96b8140ac 100644 --- a/api/services/plugin/plugin_auto_upgrade_service.py +++ b/api/services/plugin/plugin_auto_upgrade_service.py @@ -23,7 +23,7 @@ class PluginAutoUpgradeService: exclude_plugins: list[str], include_plugins: list[str], ) -> bool: - with session_factory.create_session() as session: + with session_factory.create_session() as session, session.begin(): exist_strategy = session.scalar( select(TenantPluginAutoUpgradeStrategy) .where(TenantPluginAutoUpgradeStrategy.tenant_id == tenant_id) @@ -50,7 +50,7 @@ class PluginAutoUpgradeService: @staticmethod def exclude_plugin(tenant_id: str, plugin_id: str) -> bool: - with session_factory.create_session() as session: + with session_factory.create_session() as session, session.begin(): exist_strategy = session.scalar( select(TenantPluginAutoUpgradeStrategy) .where(TenantPluginAutoUpgradeStrategy.tenant_id == tenant_id) diff --git a/api/tests/unit_tests/controllers/service_api/dataset/test_document.py b/api/tests/unit_tests/controllers/service_api/dataset/test_document.py index 12d5e7345d..288659b192 100644 --- a/api/tests/unit_tests/controllers/service_api/dataset/test_document.py +++ b/api/tests/unit_tests/controllers/service_api/dataset/test_document.py @@ -22,6 +22,8 @@ import pytest from werkzeug.exceptions import Forbidden, NotFound from controllers.service_api.dataset.document import ( + DeprecatedDocumentAddByTextApi, + DeprecatedDocumentUpdateByTextApi, DocumentAddByFileApi, DocumentAddByTextApi, DocumentApi, @@ -1005,7 +1007,7 @@ class TestDocumentAddByTextApi: # Act with app.test_request_context( - f"/datasets/{mock_dataset.id}/document/create_by_text", + f"/datasets/{mock_dataset.id}/document/create-by-text", method="POST", json={ "name": "Test Document", @@ -1037,7 +1039,7 @@ class TestDocumentAddByTextApi: # Act & Assert with app.test_request_context( - f"/datasets/{mock_dataset.id}/document/create_by_text", + f"/datasets/{mock_dataset.id}/document/create-by-text", method="POST", json={"name": "Test Document", "text": "Content"}, headers={"Authorization": "Bearer test_token"}, @@ -1066,7 +1068,7 @@ class TestDocumentAddByTextApi: # Act & Assert with app.test_request_context( - f"/datasets/{mock_dataset.id}/document/create_by_text", + f"/datasets/{mock_dataset.id}/document/create-by-text", method="POST", json={"name": "Test Document", "text": "Content"}, headers={"Authorization": "Bearer test_token"}, @@ -1093,6 +1095,20 @@ class TestArchivedDocumentImmutableError: assert error.code == 403 +class TestDocumentTextRouteDeprecation: + """Test that legacy underscore text routes stay marked deprecated.""" + + def test_create_by_text_legacy_alias_is_deprecated(self): + """Ensure only the legacy create-by-text alias is marked deprecated.""" + assert DeprecatedDocumentAddByTextApi.post.__apidoc__["deprecated"] is True + assert DocumentAddByTextApi.post.__apidoc__.get("deprecated") is not True + + def test_update_by_text_legacy_alias_is_deprecated(self): + """Ensure only the legacy update-by-text alias is marked deprecated.""" + assert DeprecatedDocumentUpdateByTextApi.post.__apidoc__["deprecated"] is True + assert DocumentUpdateByTextApi.post.__apidoc__.get("deprecated") is not True + + # ============================================================================= # Endpoint tests for DocumentUpdateByTextApi, DocumentAddByFileApi, # DocumentUpdateByFileApi. @@ -1162,7 +1178,7 @@ class TestDocumentUpdateByTextApiPost: doc_id = str(uuid.uuid4()) with app.test_request_context( - f"/datasets/{mock_dataset.id}/documents/{doc_id}/update_by_text", + f"/datasets/{mock_dataset.id}/documents/{doc_id}/update-by-text", method="POST", json={"name": "Updated Doc", "text": "New content"}, headers={"Authorization": "Bearer test_token"}, @@ -1195,7 +1211,7 @@ class TestDocumentUpdateByTextApiPost: doc_id = str(uuid.uuid4()) with app.test_request_context( - f"/datasets/{mock_dataset.id}/documents/{doc_id}/update_by_text", + f"/datasets/{mock_dataset.id}/documents/{doc_id}/update-by-text", method="POST", json={"name": "Doc", "text": "Content"}, headers={"Authorization": "Bearer test_token"}, diff --git a/api/tests/unit_tests/core/test_provider_manager.py b/api/tests/unit_tests/core/test_provider_manager.py index f45b43082c..a5a542c94f 100644 --- a/api/tests/unit_tests/core/test_provider_manager.py +++ b/api/tests/unit_tests/core/test_provider_manager.py @@ -372,6 +372,78 @@ def test_get_configurations_binds_manager_runtime_to_provider_configuration( provider_configuration.bind_model_runtime.assert_called_once_with(manager._model_runtime) +def test_get_configurations_reuses_cached_result_for_same_tenant(mocker: MockerFixture, mock_provider_entity): + manager = _build_provider_manager(mocker) + provider_configuration = Mock() + provider_factory = Mock() + provider_factory.get_providers.return_value = [mock_provider_entity] + custom_configuration = SimpleNamespace(provider=None, models=[]) + system_configuration = SimpleNamespace(enabled=False, quota_configurations=[], current_quota_type=None) + + with ( + patch.object(manager, "_get_all_providers", return_value={"openai": []}) as mock_get_all_providers, + patch.object(manager, "_init_trial_provider_records", return_value={"openai": []}), + patch.object(manager, "_get_all_provider_models", return_value={"openai": []}), + patch.object(manager, "_get_all_preferred_model_providers", return_value={}), + patch.object(manager, "_get_all_provider_model_settings", return_value={}), + patch.object(manager, "_get_all_provider_load_balancing_configs", return_value={}), + patch.object(manager, "_get_all_provider_model_credentials", return_value={}), + patch.object(manager, "_to_custom_configuration", return_value=custom_configuration), + patch.object(manager, "_to_system_configuration", return_value=system_configuration), + patch.object(manager, "_to_model_settings", return_value=[]), + patch("core.provider_manager.ModelProviderFactory", return_value=provider_factory) as mock_factory_cls, + patch( + "core.provider_manager.ProviderConfiguration", + return_value=provider_configuration, + ) as mock_provider_configuration, + ): + first = manager.get_configurations("tenant-id") + second = manager.get_configurations("tenant-id") + + assert first is second + mock_get_all_providers.assert_called_once_with("tenant-id") + mock_factory_cls.assert_called_once_with(model_runtime=manager._model_runtime) + mock_provider_configuration.assert_called_once() + provider_configuration.bind_model_runtime.assert_called_once_with(manager._model_runtime) + + +def test_clear_configurations_cache_rebuilds_requested_tenant(mocker: MockerFixture, mock_provider_entity): + manager = _build_provider_manager(mocker) + provider_factory = Mock() + provider_factory.get_providers.return_value = [mock_provider_entity] + custom_configuration = SimpleNamespace(provider=None, models=[]) + system_configuration = SimpleNamespace(enabled=False, quota_configurations=[], current_quota_type=None) + provider_configuration_first = Mock() + provider_configuration_second = Mock() + + with ( + patch.object(manager, "_get_all_providers", return_value={"openai": []}) as mock_get_all_providers, + patch.object(manager, "_init_trial_provider_records", return_value={"openai": []}), + patch.object(manager, "_get_all_provider_models", return_value={"openai": []}), + patch.object(manager, "_get_all_preferred_model_providers", return_value={}), + patch.object(manager, "_get_all_provider_model_settings", return_value={}), + patch.object(manager, "_get_all_provider_load_balancing_configs", return_value={}), + patch.object(manager, "_get_all_provider_model_credentials", return_value={}), + patch.object(manager, "_to_custom_configuration", return_value=custom_configuration), + patch.object(manager, "_to_system_configuration", return_value=system_configuration), + patch.object(manager, "_to_model_settings", return_value=[]), + patch("core.provider_manager.ModelProviderFactory", return_value=provider_factory), + patch( + "core.provider_manager.ProviderConfiguration", + side_effect=[provider_configuration_first, provider_configuration_second], + ) as mock_provider_configuration, + ): + first = manager.get_configurations("tenant-id") + manager.clear_configurations_cache("tenant-id") + second = manager.get_configurations("tenant-id") + + assert first is not second + assert mock_get_all_providers.call_count == 2 + assert mock_provider_configuration.call_count == 2 + provider_configuration_first.bind_model_runtime.assert_called_once_with(manager._model_runtime) + provider_configuration_second.bind_model_runtime.assert_called_once_with(manager._model_runtime) + + def test_get_provider_model_bundle_returns_selected_model_type_instance(mocker: MockerFixture): manager = _build_provider_manager(mocker) provider_configuration = Mock() diff --git a/api/uv.lock b/api/uv.lock index 40d196a160..bab24a87d2 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -469,14 +469,14 @@ wheels = [ [[package]] name = "basedpyright" -version = "1.39.0" +version = "1.39.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodejs-wheel-binaries" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ac/f4/4a77cc1ffb3dab7391642cde30163961d8ee973e9e6b6740c7d15aa3d3ba/basedpyright-1.39.0.tar.gz", hash = "sha256:6666f51c378c7ac45877c4c1c7041ee0b5b83d755ebc82f898f47b6fafe0cc4f", size = 25357403, upload-time = "2026-04-01T12:27:41.92Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/19/5a5b9b9197973da732638957be3a65cf514d2f5a4964eeedbf33b6c65bbd/basedpyright-1.39.3.tar.gz", hash = "sha256:2f794e6b5f4260fb89f614ca6cd23c6f305373bb6b50c4ed7794ff2ae647fb14", size = 25503187, upload-time = "2026-04-20T22:14:47.424Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/47/08145d1bcc3083ed20059bdecbde404bd767f91b91e2764ec01cffec9f4b/basedpyright-1.39.0-py3-none-any.whl", hash = "sha256:91b8ad50bc85ee4a985b928f9368c35c99eee5a56c44e99b2442fa12ecc3d670", size = 12353868, upload-time = "2026-04-01T12:27:38.495Z" }, + { url = "https://files.pythonhosted.org/packages/54/5c/f950c1239ad26f3bb453e665428a2cf1893995de725a5eb0b64a2520b366/basedpyright-1.39.3-py3-none-any.whl", hash = "sha256:aba760dc83307727554f936d6b4381caa14482f30dbc2173167710e217c1f7ab", size = 12419181, upload-time = "2026-04-20T22:14:51.975Z" }, ] [[package]] @@ -618,15 +618,15 @@ wheels = [ [[package]] name = "boto3-stubs" -version = "1.42.88" +version = "1.42.92" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore-stubs" }, { name = "types-s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/c7/d4dfbb4757cd72fd350ba666902ec3ac19e04d6be639e96cdad4543d4726/boto3_stubs-1.42.88.tar.gz", hash = "sha256:85215fb4938a94d1cf83cd8632f46ae7728b5ec88187d83468f393bbe64236d6", size = 102495, upload-time = "2026-04-10T19:55:57.526Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/b4/7f472d64a89f6aa6b8e8eeadc876667b7e4edfb526c6118efe2b2c98ba17/boto3_stubs-1.42.92.tar.gz", hash = "sha256:4bc934069c5e8c7b3cdd2442569dae14e8272fe207d445bd38aa578b8463638f", size = 102696, upload-time = "2026-04-20T19:55:19.858Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/6f/3befd72080aedbb4ad26b353a6e364645668664930ce49668fd0bab8f2b5/boto3_stubs-1.42.88-py3-none-any.whl", hash = "sha256:9e74350715ca8ccd63fc250f8eca9fa3161b3d1704339554344d72e4e21c5ed1", size = 70603, upload-time = "2026-04-10T19:55:49.921Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ce/2fe2c6456f8dc0b8bb8d80e05e154c7975ec058991bedf54f3aeed634b79/boto3_stubs-1.42.92-py3-none-any.whl", hash = "sha256:b3994e60f0133b2dd3d9a88ceaeef48fa6367d9a9429426e919575768a1ad9c6", size = 70666, upload-time = "2026-04-20T19:55:16.398Z" }, ] [package.optional-dependencies] @@ -1616,13 +1616,13 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ - { name = "basedpyright", specifier = ">=1.39.0" }, - { name = "boto3-stubs", specifier = ">=1.42.88" }, + { name = "basedpyright", specifier = ">=1.39.3" }, + { name = "boto3-stubs", specifier = ">=1.42.92" }, { name = "celery-types", specifier = ">=0.23.0" }, { name = "coverage", specifier = ">=7.13.4" }, { name = "dotenv-linter", specifier = ">=0.7.0" }, - { name = "faker", specifier = ">=20.1.0" }, - { name = "hypothesis", specifier = ">=6.151.12" }, + { name = "faker", specifier = ">=40.15.0" }, + { name = "hypothesis", specifier = ">=6.152.1" }, { name = "import-linter", specifier = ">=2.3" }, { name = "lxml-stubs", specifier = ">=0.5.1" }, { name = "mypy", specifier = ">=1.20.1" }, @@ -1635,8 +1635,8 @@ dev = [ { name = "pytest-mock", specifier = ">=3.15.1" }, { name = "pytest-timeout", specifier = ">=2.4.0" }, { name = "pytest-xdist", specifier = ">=3.8.0" }, - { name = "ruff", specifier = ">=0.15.10" }, - { name = "scipy-stubs", specifier = ">=1.15.3.0" }, + { name = "ruff", specifier = ">=0.15.11" }, + { name = "scipy-stubs", specifier = ">=1.17.1.4" }, { name = "testcontainers", specifier = ">=4.14.2" }, { name = "types-aiofiles", specifier = ">=25.1.0" }, { name = "types-beautifulsoup4", specifier = ">=4.12.0" }, @@ -2351,14 +2351,14 @@ wheels = [ [[package]] name = "faker" -version = "40.13.0" +version = "40.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/89/95/4822ffe94723553789aef783104f4f18fc20d7c4c68e1bbd633e11d09758/faker-40.13.0.tar.gz", hash = "sha256:a0751c84c3abac17327d7bb4c98e8afe70ebf7821e01dd7d0b15cd8856415525", size = 1962043, upload-time = "2026-04-06T16:44:55.68Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/13/6741787bd91c4109c7bed047d68273965cd52ce8a5f773c471b949334b6d/faker-40.15.0.tar.gz", hash = "sha256:20f3a6ec8c266b74d4c554e34118b21c3c2056c0b4a519d15c8decb3a4e6e795", size = 1967447, upload-time = "2026-04-17T20:05:27.555Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/8a/708103325edff16a0b0e004de0d37db8ba216a32713948c64d71f6d4a4c2/faker-40.13.0-py3-none-any.whl", hash = "sha256:c1298fd0d819b3688fb5fd358c4ba8f56c7c8c740b411fd3dbd8e30bf2c05019", size = 1994597, upload-time = "2026-04-06T16:44:53.698Z" }, + { url = "https://files.pythonhosted.org/packages/a7/a7/a600f8f30d4505e89166de51dd121bd540ab8e560e8cf0901de00a81de8c/faker-40.15.0-py3-none-any.whl", hash = "sha256:71ab3c3370da9d2205ab74ffb0fd51273063ad562b3a3bb69d0026a20923e318", size = 2004447, upload-time = "2026-04-17T20:05:25.437Z" }, ] [[package]] @@ -3317,14 +3317,14 @@ wheels = [ [[package]] name = "hypothesis" -version = "6.151.12" +version = "6.152.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sortedcontainers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/ab/67ca321d1ab96fd3828b12142f1c258e2d4a668a025d06cd50ab3409787f/hypothesis-6.151.12.tar.gz", hash = "sha256:be485f503979af4c3dfa19e3fc2b967d0458e7f8c4e28128d7e215e0a55102e0", size = 463900, upload-time = "2026-04-08T19:40:06.205Z" } +sdist = { url = "https://files.pythonhosted.org/packages/64/b1/c32bcddb9aab9e3abc700f1f56faf14e7655c64a16ca47701a57362276ea/hypothesis-6.152.1.tar.gz", hash = "sha256:4f4ed934eee295dd84ee97592477d23e8dc03e9f12ae0ee30a4e7c9ef3fca3b0", size = 465029, upload-time = "2026-04-14T22:29:24.062Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/5a/6cecf134b631050a1f8605096adbe812483b60790d951470989d39b56860/hypothesis-6.151.12-py3-none-any.whl", hash = "sha256:37d4f3a768365c30571b11dfd7a6857a12173d933010b2c4ab65619f1b5952c5", size = 529656, upload-time = "2026-04-08T19:40:03.126Z" }, + { url = "https://files.pythonhosted.org/packages/5d/83/860fb3075e00b0fc19a22a2301bc3c96f00437558c3911bdd0a3573a4a53/hypothesis-6.152.1-py3-none-any.whl", hash = "sha256:40a3619d9e0cb97b018857c7986f75cf5de2e5ec0fa8a0b172d00747758f749e", size = 530752, upload-time = "2026-04-14T22:29:20.893Z" }, ] [[package]] @@ -5887,27 +5887,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.10" +version = "0.15.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/d9/aa3f7d59a10ef6b14fe3431706f854dbf03c5976be614a9796d36326810c/ruff-0.15.10.tar.gz", hash = "sha256:d1f86e67ebfdef88e00faefa1552b5e510e1d35f3be7d423dc7e84e63788c94e", size = 4631728, upload-time = "2026-04-09T14:06:09.884Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/192f3d7103816158dfd5ea50d098ef2aec19194e6cbccd4b3485bdb2eb2d/ruff-0.15.11.tar.gz", hash = "sha256:f092b21708bf0e7437ce9ada249dfe688ff9a0954fc94abab05dcea7dcd29c33", size = 4637264, upload-time = "2026-04-16T18:46:26.58Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/00/a1c2fdc9939b2c03691edbda290afcd297f1f389196172826b03d6b6a595/ruff-0.15.10-py3-none-linux_armv6l.whl", hash = "sha256:0744e31482f8f7d0d10a11fcbf897af272fefdfcb10f5af907b18c2813ff4d5f", size = 10563362, upload-time = "2026-04-09T14:06:21.189Z" }, - { url = "https://files.pythonhosted.org/packages/5c/15/006990029aea0bebe9d33c73c3e28c80c391ebdba408d1b08496f00d422d/ruff-0.15.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b1e7c16ea0ff5a53b7c2df52d947e685973049be1cdfe2b59a9c43601897b22e", size = 10951122, upload-time = "2026-04-09T14:06:02.236Z" }, - { url = "https://files.pythonhosted.org/packages/f2/c0/4ac978fe874d0618c7da647862afe697b281c2806f13ce904ad652fa87e4/ruff-0.15.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93cc06a19e5155b4441dd72808fdf84290d84ad8a39ca3b0f994363ade4cebb1", size = 10314005, upload-time = "2026-04-09T14:06:00.026Z" }, - { url = "https://files.pythonhosted.org/packages/da/73/c209138a5c98c0d321266372fc4e33ad43d506d7e5dd817dd89b60a8548f/ruff-0.15.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e1dd04312997c99ea6965df66a14fb4f03ba978564574ffc68b0d61fd3989e", size = 10643450, upload-time = "2026-04-09T14:05:42.137Z" }, - { url = "https://files.pythonhosted.org/packages/ec/76/0deec355d8ec10709653635b1f90856735302cb8e149acfdf6f82a5feb70/ruff-0.15.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8154d43684e4333360fedd11aaa40b1b08a4e37d8ffa9d95fee6fa5b37b6fab1", size = 10379597, upload-time = "2026-04-09T14:05:49.984Z" }, - { url = "https://files.pythonhosted.org/packages/dc/be/86bba8fc8798c081e28a4b3bb6d143ccad3fd5f6f024f02002b8f08a9fa3/ruff-0.15.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ab88715f3a6deb6bde6c227f3a123410bec7b855c3ae331b4c006189e895cef", size = 11146645, upload-time = "2026-04-09T14:06:12.246Z" }, - { url = "https://files.pythonhosted.org/packages/a8/89/140025e65911b281c57be1d385ba1d932c2366ca88ae6663685aed8d4881/ruff-0.15.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a768ff5969b4f44c349d48edf4ab4f91eddb27fd9d77799598e130fb628aa158", size = 12030289, upload-time = "2026-04-09T14:06:04.776Z" }, - { url = "https://files.pythonhosted.org/packages/88/de/ddacca9545a5e01332567db01d44bd8cf725f2db3b3d61a80550b48308ea/ruff-0.15.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ee3ef42dab7078bda5ff6a1bcba8539e9857deb447132ad5566a038674540d0", size = 11496266, upload-time = "2026-04-09T14:05:55.485Z" }, - { url = "https://files.pythonhosted.org/packages/bc/bb/7ddb00a83760ff4a83c4e2fc231fd63937cc7317c10c82f583302e0f6586/ruff-0.15.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51cb8cc943e891ba99989dd92d61e29b1d231e14811db9be6440ecf25d5c1609", size = 11256418, upload-time = "2026-04-09T14:05:57.69Z" }, - { url = "https://files.pythonhosted.org/packages/dc/8d/55de0d35aacf6cd50b6ee91ee0f291672080021896543776f4170fc5c454/ruff-0.15.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e59c9bdc056a320fb9ea1700a8d591718b8faf78af065484e801258d3a76bc3f", size = 11288416, upload-time = "2026-04-09T14:05:44.695Z" }, - { url = "https://files.pythonhosted.org/packages/68/cf/9438b1a27426ec46a80e0a718093c7f958ef72f43eb3111862949ead3cc1/ruff-0.15.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:136c00ca2f47b0018b073f28cb5c1506642a830ea941a60354b0e8bc8076b151", size = 10621053, upload-time = "2026-04-09T14:05:52.782Z" }, - { url = "https://files.pythonhosted.org/packages/4c/50/e29be6e2c135e9cd4cb15fbade49d6a2717e009dff3766dd080fcb82e251/ruff-0.15.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b80a2f3c9c8a950d6237f2ca12b206bccff626139be9fa005f14feb881a1ae8", size = 10378302, upload-time = "2026-04-09T14:06:14.361Z" }, - { url = "https://files.pythonhosted.org/packages/18/2f/e0b36a6f99c51bb89f3a30239bc7bf97e87a37ae80aa2d6542d6e5150364/ruff-0.15.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e3e53c588164dc025b671c9df2462429d60357ea91af7e92e9d56c565a9f1b07", size = 10850074, upload-time = "2026-04-09T14:06:16.581Z" }, - { url = "https://files.pythonhosted.org/packages/11/08/874da392558ce087a0f9b709dc6ec0d60cbc694c1c772dab8d5f31efe8cb/ruff-0.15.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b0c52744cf9f143a393e284125d2576140b68264a93c6716464e129a3e9adb48", size = 11358051, upload-time = "2026-04-09T14:06:18.948Z" }, - { url = "https://files.pythonhosted.org/packages/e4/46/602938f030adfa043e67112b73821024dc79f3ab4df5474c25fa4c1d2d14/ruff-0.15.10-py3-none-win32.whl", hash = "sha256:d4272e87e801e9a27a2e8df7b21011c909d9ddd82f4f3281d269b6ba19789ca5", size = 10588964, upload-time = "2026-04-09T14:06:07.14Z" }, - { url = "https://files.pythonhosted.org/packages/25/b6/261225b875d7a13b33a6d02508c39c28450b2041bb01d0f7f1a83d569512/ruff-0.15.10-py3-none-win_amd64.whl", hash = "sha256:28cb32d53203242d403d819fd6983152489b12e4a3ae44993543d6fe62ab42ed", size = 11745044, upload-time = "2026-04-09T14:05:39.473Z" }, - { url = "https://files.pythonhosted.org/packages/58/ed/dea90a65b7d9e69888890fb14c90d7f51bf0c1e82ad800aeb0160e4bacfd/ruff-0.15.10-py3-none-win_arm64.whl", hash = "sha256:601d1610a9e1f1c2165a4f561eeaa2e2ea1e97f3287c5aa258d3dab8b57c6188", size = 11035607, upload-time = "2026-04-09T14:05:47.593Z" }, + { url = "https://files.pythonhosted.org/packages/02/1e/6aca3427f751295ab011828e15e9bf452200ac74484f1db4be0197b8170b/ruff-0.15.11-py3-none-linux_armv6l.whl", hash = "sha256:e927cfff503135c558eb581a0c9792264aae9507904eb27809cdcff2f2c847b7", size = 10607943, upload-time = "2026-04-16T18:46:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/e7/26/1341c262e74f36d4e84f3d6f4df0ac68cd53331a66bfc5080daa17c84c0b/ruff-0.15.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7a1b5b2938d8f890b76084d4fa843604d787a912541eae85fd7e233398bbb73e", size = 10988592, upload-time = "2026-04-16T18:46:00.742Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/850b1d6ffa9564fbb6740429bad53df1094082fe515c8c1e74b6d8d05f18/ruff-0.15.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d4176f3d194afbdaee6e41b9ccb1a2c287dba8700047df474abfbe773825d1cb", size = 10338501, upload-time = "2026-04-16T18:46:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/f2/11/cc1284d3e298c45a817a6aadb6c3e1d70b45c9b36d8d9cce3387b495a03a/ruff-0.15.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b17c886fb88203ced3afe7f14e8d5ae96e9d2f4ccc0ee66aa19f2c2675a27e4", size = 10670693, upload-time = "2026-04-16T18:46:41.941Z" }, + { url = "https://files.pythonhosted.org/packages/ce/9e/f8288b034ab72b371513c13f9a41d9ba3effac54e24bfb467b007daee2ca/ruff-0.15.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49fafa220220afe7758a487b048de4c8f9f767f37dfefad46b9dd06759d003eb", size = 10416177, upload-time = "2026-04-16T18:46:21.717Z" }, + { url = "https://files.pythonhosted.org/packages/85/71/504d79abfd3d92532ba6bbe3d1c19fada03e494332a59e37c7c2dabae427/ruff-0.15.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2ab8427e74a00d93b8bda1307b1e60970d40f304af38bccb218e056c220120d", size = 11221886, upload-time = "2026-04-16T18:46:15.086Z" }, + { url = "https://files.pythonhosted.org/packages/43/5a/947e6ab7a5ad603d65b474be15a4cbc6d29832db5d762cd142e4e3a74164/ruff-0.15.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:195072c0c8e1fc8f940652073df082e37a5d9cb43b4ab1e4d0566ab8977a13b7", size = 12075183, upload-time = "2026-04-16T18:46:07.944Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a1/0b7bb6268775fdd3a0818aee8efd8f5b4e231d24dd4d528ced2534023182/ruff-0.15.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a0996d486af3920dec930a2e7daed4847dfc12649b537a9335585ada163e9e", size = 11516575, upload-time = "2026-04-16T18:46:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/30/c3/bb5168fc4d233cc06e95f482770d0f3c87945a0cd9f614b90ea8dc2f2833/ruff-0.15.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bef2cb556d509259f1fe440bb9cd33c756222cf0a7afe90d15edf0866702431", size = 11306537, upload-time = "2026-04-16T18:46:36.988Z" }, + { url = "https://files.pythonhosted.org/packages/e4/92/4cfae6441f3967317946f3b788136eecf093729b94d6561f963ed810c82e/ruff-0.15.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:030d921a836d7d4a12cf6e8d984a88b66094ccb0e0f17ddd55067c331191bf19", size = 11296813, upload-time = "2026-04-16T18:46:24.182Z" }, + { url = "https://files.pythonhosted.org/packages/43/26/972784c5dde8313acde8ac71ba8ac65475b85db4a2352a76c9934361f9bc/ruff-0.15.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0e783b599b4577788dbbb66b9addcef87e9a8832f4ce0c19e34bf55543a2f890", size = 10633136, upload-time = "2026-04-16T18:46:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/5b/53/3985a4f185020c2f367f2e08a103032e12564829742a1b417980ce1514a0/ruff-0.15.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ae90592246625ba4a34349d68ec28d4400d75182b71baa196ddb9f82db025ef5", size = 10424701, upload-time = "2026-04-16T18:46:10.381Z" }, + { url = "https://files.pythonhosted.org/packages/d3/57/bf0dfb32241b56c83bb663a826133da4bf17f682ba8c096973065f6e6a68/ruff-0.15.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1f111d62e3c983ed20e0ca2e800f8d77433a5b1161947df99a5c2a3fb60514f0", size = 10873887, upload-time = "2026-04-16T18:46:29.157Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/e48076b2a57dc33ee8c7a957296f97c744ca891a8ffb4ffb1aaa3b3f517d/ruff-0.15.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:06f483d6646f59eaffba9ae30956370d3a886625f511a3108994000480621d1c", size = 11404316, upload-time = "2026-04-16T18:46:19.462Z" }, + { url = "https://files.pythonhosted.org/packages/88/27/0195d15fe7a897cbcba0904792c4b7c9fdd958456c3a17d2ea6093716a9a/ruff-0.15.11-py3-none-win32.whl", hash = "sha256:476a2aa56b7da0b73a3ee80b6b2f0e19cce544245479adde7baa65466664d5f3", size = 10655535, upload-time = "2026-04-16T18:46:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5e/c927b325bd4c1d3620211a4b96f47864633199feed60fa936025ab27e090/ruff-0.15.11-py3-none-win_amd64.whl", hash = "sha256:8b6756d88d7e234fb0c98c91511aae3cd519d5e3ed271cae31b20f39cb2a12a3", size = 11779692, upload-time = "2026-04-16T18:46:17.268Z" }, + { url = "https://files.pythonhosted.org/packages/63/b6/aeadee5443e49baa2facd51131159fd6301cc4ccfc1541e4df7b021c37dd/ruff-0.15.11-py3-none-win_arm64.whl", hash = "sha256:063fed18cc1bbe0ee7393957284a6fe8b588c6a406a285af3ee3f46da2391ee4", size = 11032614, upload-time = "2026-04-16T18:46:34.487Z" }, ] [[package]] @@ -5946,14 +5946,14 @@ wheels = [ [[package]] name = "scipy-stubs" -version = "1.17.1.3" +version = "1.17.1.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "optype", extra = ["numpy"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a7/59/59c6cc3f9970154b9ed6b1aff42a0185cdd60cef54adc0404b9e77972221/scipy_stubs-1.17.1.3.tar.gz", hash = "sha256:5eb87a8d23d726706259b012ebe76a4a96a9ae9e141fc59bf55fc8eac2ed9e0f", size = 392185, upload-time = "2026-03-22T22:11:58.34Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/75/d944a11fca64aa84fbb4bfcf613b758319c6103cb30a304a0e9727009d62/scipy_stubs-1.17.1.4.tar.gz", hash = "sha256:cae00c5207aa62ceb4bcadea202d9fbbf002e958f9e4de981720436b8d5c1802", size = 396980, upload-time = "2026-04-13T11:46:54.528Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/d4/94304532c0a75a55526119043dd44a9bd1541a21e14483cbb54261c527d2/scipy_stubs-1.17.1.3-py3-none-any.whl", hash = "sha256:7b91d3f05aa47da06fbca14eb6c5bb4c28994e9245fd250cc847e375bab31297", size = 597933, upload-time = "2026-03-22T22:11:56.525Z" }, + { url = "https://files.pythonhosted.org/packages/92/f8/334aa5a7a482ea89cb14d92f6a4d9ffa1e193e733144d4d14c7ffcb33583/scipy_stubs-1.17.1.4-py3-none-any.whl", hash = "sha256:e6e5c390fb864745bc3d5f591de81f5cb4f84403857d4f660acb5b6339956f5b", size = 604752, upload-time = "2026-04-13T11:46:53.135Z" }, ] [[package]] diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 96af36d27a..0e0970f90d 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -488,11 +488,6 @@ "count": 1 } }, - "web/app/components/app/configuration/dataset-config/context-var/var-picker.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/app/configuration/dataset-config/index.tsx": { "ts/no-explicit-any": { "count": 1 @@ -1931,11 +1926,6 @@ "count": 2 } }, - "web/app/components/base/prompt-editor/plugins/hitl-input-block/variable-block.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/base/prompt-editor/plugins/last-run-block/index.tsx": { "no-barrel-files/no-barrel-files": { "count": 3 @@ -2223,11 +2213,6 @@ "count": 3 } }, - "web/app/components/billing/usage-info/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/datasets/common/image-previewer/index.tsx": { "no-irregular-whitespace": { "count": 1 @@ -3070,14 +3055,6 @@ "count": 3 } }, - "web/app/components/header/account-setting/members-page/transfer-ownership-modal/member-selector.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "ts/no-explicit-any": { - "count": 2 - } - }, "web/app/components/header/account-setting/model-provider-page/declarations.ts": { "erasable-syntax-only/enums": { "count": 11 @@ -3564,11 +3541,6 @@ "count": 2 } }, - "web/app/components/plugins/plugin-detail-panel/subscription-list/selector-entry.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/plugins/plugin-detail-panel/subscription-list/selector-view.tsx": { "no-restricted-imports": { "count": 1 @@ -3682,11 +3654,6 @@ "count": 1 } }, - "web/app/components/plugins/reference-setting-modal/auto-update-setting/tool-picker.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/plugins/reference-setting-modal/auto-update-setting/types.ts": { "erasable-syntax-only/enums": { "count": 2 @@ -3943,11 +3910,6 @@ "count": 1 } }, - "web/app/components/tools/labels/selector.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/tools/mcp/create-card.tsx": { "ts/no-explicit-any": { "count": 1 @@ -4087,15 +4049,7 @@ "count": 1 } }, - "web/app/components/workflow/block-selector/blocks.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/block-selector/featured-tools.tsx": { - "no-restricted-imports": { - "count": 1 - }, "react/set-state-in-effect": { "count": 2 }, @@ -4104,9 +4058,6 @@ } }, "web/app/components/workflow/block-selector/featured-triggers.tsx": { - "no-restricted-imports": { - "count": 1 - }, "react/set-state-in-effect": { "count": 2 }, @@ -4139,26 +4090,11 @@ "count": 1 } }, - "web/app/components/workflow/block-selector/start-blocks.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/block-selector/tabs.tsx": { "no-restricted-imports": { "count": 1 } }, - "web/app/components/workflow/block-selector/tool-picker.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "web/app/components/workflow/block-selector/tool/action-item.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/block-selector/tool/tool-list-flat-view/list.tsx": { "ts/no-explicit-any": { "count": 1 @@ -4170,9 +4106,6 @@ } }, "web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 1 } @@ -4347,14 +4280,6 @@ "count": 2 } }, - "web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx": { - "no-restricted-imports": { - "count": 3 - }, - "ts/no-explicit-any": { - "count": 4 - } - }, "web/app/components/workflow/nodes/_base/components/agent-strategy.tsx": { "ts/no-empty-object-type": { "count": 1 @@ -4581,22 +4506,6 @@ "count": 1 } }, - "web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "react/set-state-in-effect": { - "count": 1 - }, - "ts/no-explicit-any": { - "count": 3 - } - }, - "web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/_base/components/variable/var-type-picker.tsx": { "no-restricted-imports": { "count": 1 @@ -4776,11 +4685,6 @@ "count": 1 } }, - "web/app/components/workflow/nodes/code/dependency-picker.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/code/types.ts": { "erasable-syntax-only/enums": { "count": 1 @@ -4931,16 +4835,6 @@ "count": 1 } }, - "web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/email-input.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/member-selector.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/human-input/components/delivery-method/test-email-sender.tsx": { "no-restricted-imports": { "count": 1 @@ -4991,11 +4885,6 @@ "count": 2 } }, - "web/app/components/workflow/nodes/if-else/components/condition-add.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/if-else/components/condition-list/condition-input.tsx": { "ts/no-explicit-any": { "count": 1 @@ -5011,11 +4900,6 @@ "count": 1 } }, - "web/app/components/workflow/nodes/if-else/components/condition-list/condition-var-selector.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/if-else/components/condition-number-input.tsx": { "no-restricted-imports": { "count": 1 @@ -5119,16 +5003,6 @@ "count": 1 } }, - "web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/add-condition.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-common-variable-selector.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-item.tsx": { "ts/no-explicit-any": { "count": 1 @@ -5144,11 +5018,6 @@ "count": 1 } }, - "web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-variable-selector.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/metadata-filter/index.tsx": { "no-restricted-imports": { "count": 1 @@ -5328,11 +5197,6 @@ "count": 1 } }, - "web/app/components/workflow/nodes/loop/components/condition-add.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/loop/components/condition-list/condition-input.tsx": { "ts/no-explicit-any": { "count": 1 @@ -5348,11 +5212,6 @@ "count": 1 } }, - "web/app/components/workflow/nodes/loop/components/condition-list/condition-var-selector.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/workflow/nodes/loop/components/condition-number-input.tsx": { "no-restricted-imports": { "count": 1 @@ -6129,14 +5988,6 @@ "count": 5 } }, - "web/app/education-apply/search-input.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "ts/no-explicit-any": { - "count": 1 - } - }, "web/app/education-apply/verify-state-modal.tsx": { "react/set-state-in-effect": { "count": 1 diff --git a/packages/dify-ui/AGENTS.md b/packages/dify-ui/AGENTS.md index 651b117070..4a7fe2f22a 100644 --- a/packages/dify-ui/AGENTS.md +++ b/packages/dify-ui/AGENTS.md @@ -11,6 +11,27 @@ Shared design tokens, the `cn()` utility, a Tailwind CSS preset, and headless pr - Props pattern: `Omit & VariantProps & { /* custom */ }`. - When a component accepts a prop typed from a shared internal module, `export type` it from that component so consumers import it from the component subpath. +## Overlay Primitive Selection: Tooltip vs PreviewCard vs Popover + +Pick by the **trigger's purpose** and **a11y reach**, not visual richness. + +| Primitive | Opens on | Trigger's purpose | Content | Reachable on touch / SR? | +| ------------- | --------------------- | -------------------------- | ------------------------- | ------------------------ | +| `Tooltip` | hover / focus | has its own action | short plain-text label | ❌ (label only) | +| `PreviewCard` | hover / focus | has a primary click target | supplementary preview | ❌ (via click target) | +| `Popover` | click / tap (+ hover) | **to open the popup** | anything, incl. long text | ✅ | + +Base UI decision rule ([docs]): + +> _"If the trigger's purpose is to open the popup itself, it's a popover. +> If the trigger's purpose is unrelated to opening the popup, it's a tooltip."_ + +Apply this first, then narrow: + +- `Tooltip` — ephemeral visual label. Trigger must already carry its own `aria-label` / visible text; tooltip mirrors it for sighted mouse/keyboard users. No interactive UI, no multi-line prose. Not dwell-able. +- `PreviewCard` — hover-revealed rich supplementary preview anchored to a trigger whose click goes somewhere (link, selectable row, jumpable chip). **Hard contract:** the popup MUST NOT contain information or actions unreachable from the trigger's click destination — touch and SR users can't open it. If the info is unique to the popup, switch to `Popover` (click or `openOnHover`) or move it to the click destination. Do not hand-roll "hover to open" on top of `Popover` to evade this split. +- `Popover` — any popup with its own interactions, or any "infotip" (`?` / `(i)` glyph whose sole purpose is to reveal help text). Pass `openOnHover` on `PopoverTrigger` for the infotip case — unlike `Tooltip` / `PreviewCard`, this stays accessible to touch and SR users because the popover still opens on tap and focus. + ## Border Radius: Figma Token → Tailwind Class Mapping The Figma design system uses `--radius/*` tokens whose scale is **offset by one step** from Tailwind CSS v4 defaults. When translating Figma specs to code, always use this mapping — never use `radius-*` as a CSS class, and never extend `borderRadius` in the preset. @@ -34,3 +55,5 @@ The Figma design system uses `--radius/*` tokens whose scale is **offset by one - **Do not** use `radius-*` as CSS class names. The old `@utility radius-*` definitions have been removed. - When the Figma MCP returns `rounded-[var(--radius/sm, 6px)]`, convert it to the standard Tailwind class from the table above (e.g. `rounded-md`). - For values without a standard Tailwind equivalent (10px, 20px, 28px), use arbitrary values like `rounded-[10px]`. + +[docs]: https://base-ui.com/react/components/tooltip#infotips diff --git a/packages/dify-ui/package.json b/packages/dify-ui/package.json index e1b7a3c1ef..408ba2c432 100644 --- a/packages/dify-ui/package.json +++ b/packages/dify-ui/package.json @@ -49,6 +49,10 @@ "types": "./src/popover/index.tsx", "import": "./src/popover/index.tsx" }, + "./preview-card": { + "types": "./src/preview-card/index.tsx", + "import": "./src/preview-card/index.tsx" + }, "./scroll-area": { "types": "./src/scroll-area/index.tsx", "import": "./src/scroll-area/index.tsx" diff --git a/packages/dify-ui/src/popover/index.stories.tsx b/packages/dify-ui/src/popover/index.stories.tsx index dcea5018ab..802337634b 100644 --- a/packages/dify-ui/src/popover/index.stories.tsx +++ b/packages/dify-ui/src/popover/index.stories.tsx @@ -20,7 +20,7 @@ const meta = { layout: 'centered', docs: { description: { - component: 'Compound popover built on Base UI Popover. Use it for contextual affordances, overflow menus, filters, and forms that anchor to a trigger. Control placement via the `placement` prop on `PopoverContent` and compose arbitrary children inside the popup.', + component: 'Compound popover built on Base UI Popover. Use it for contextual affordances, overflow menus, filters, and forms that anchor to a trigger. Control placement via the `placement` prop on `PopoverContent` and compose arbitrary children inside the popup.\n\nPass `openOnHover` on `PopoverTrigger` when the popup should also reveal on hover (see the **Infotip** story). Unlike `Tooltip` and `PreviewCard`, hover on `Popover` still falls back to tap/focus, so touch and screen-reader users can reach the content.', }, }, }, @@ -101,6 +101,48 @@ export const WithActions: Story = { ), } +export const Infotip: Story = { + parameters: { + docs: { + description: { + story: [ + 'The **infotip** pattern from [Base UI](https://base-ui.com/react/components/tooltip#infotips): an info glyph (`?`, `(i)`) whose sole purpose is to reveal explanatory text. Use `Popover` with `openOnHover` on the trigger — never `Tooltip`.', + '', + 'Why not `Tooltip`? Tooltips are disabled on touch devices and not announced to screen readers; descriptive help text hidden in them is unreachable for those users. Why not `PreviewCard`? PreviewCard\'s a11y contract requires the trigger to already own a primary click destination, but an info glyph has no other purpose.', + '', + 'Base UI rule of thumb: *"If the trigger\'s purpose is to open the popup itself, it\'s a popover. If the trigger\'s purpose is unrelated to opening the popup, it\'s a tooltip."*', + '', + 'Hover, tap, or focus the `?` icon to open. In the Dify app, reach for `@/app/components/base/infotip` (`{helpText}`) which wraps this pattern with consistent delays (300/200), typography, and `aria-label` plumbing.', + ].join('\n'), + }, + }, + }, + render: () => ( +
+ Usage priority + + + + + )} + /> + + Set which resource to use first when running models. The Trial quota will be used after the paid quota is exhausted. + + +
+ ), +} + const PLACEMENTS: Placement[] = [ 'top-start', 'top', diff --git a/packages/dify-ui/src/preview-card/__tests__/index.spec.tsx b/packages/dify-ui/src/preview-card/__tests__/index.spec.tsx new file mode 100644 index 0000000000..5d1e325051 --- /dev/null +++ b/packages/dify-ui/src/preview-card/__tests__/index.spec.tsx @@ -0,0 +1,127 @@ +import { render } from 'vitest-browser-react' +import { + PreviewCard, + PreviewCardContent, + PreviewCardTrigger, +} from '..' + +const renderWithSafeViewport = (ui: import('react').ReactNode) => render( +
+ {ui} +
, +) + +describe('PreviewCardContent', () => { + describe('Placement', () => { + it('should use bottom placement and default offsets when placement props are not provided', async () => { + const screen = await renderWithSafeViewport( + + Open} + /> + + Default content + + , + ) + + await expect.element(screen.getByRole('group', { name: 'default positioner' })).toHaveAttribute('data-side', 'bottom') + await expect.element(screen.getByRole('group', { name: 'default positioner' })).toHaveAttribute('data-align', 'center') + await expect.element(screen.getByRole('dialog', { name: 'default popup' })).toHaveTextContent('Default content') + }) + + it('should apply parsed custom placement and custom offsets when placement props are provided', async () => { + const screen = await renderWithSafeViewport( + + Open} + /> + + Custom placement content + + , + ) + + await expect.element(screen.getByRole('group', { name: 'custom positioner' })).toHaveAttribute('data-side', 'top') + await expect.element(screen.getByRole('group', { name: 'custom positioner' })).toHaveAttribute('data-align', 'end') + await expect.element(screen.getByRole('dialog', { name: 'custom popup' })).toHaveTextContent('Custom placement content') + }) + }) + + describe('Passthrough props', () => { + it('should forward positionerProps and popupProps when passthrough props are provided', async () => { + const onPopupClick = vi.fn() + + const screen = await render( + + Open} + /> + + Preview body + + , + ) + + const popup = screen.getByRole('dialog', { name: 'preview content' }) + await popup.click() + + await expect.element(screen.getByRole('group', { name: 'preview positioner' })).toHaveAttribute('id', 'preview-positioner-id') + await expect.element(popup).toHaveAttribute('id', 'preview-popup-id') + expect(onPopupClick).toHaveBeenCalledTimes(1) + }) + }) + + describe('Trigger click behavior', () => { + it('should forward the trigger click to the consumer handler so the primary action runs', async () => { + const onPrimaryClick = vi.fn() + + const screen = await renderWithSafeViewport( + + + Open + + )} + /> + + Preview body + + , + ) + + const trigger = screen.getByRole('button', { name: 'preview trigger' }) + await trigger.click() + + expect(onPrimaryClick).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/packages/dify-ui/src/preview-card/index.stories.tsx b/packages/dify-ui/src/preview-card/index.stories.tsx new file mode 100644 index 0000000000..540ac08c1a --- /dev/null +++ b/packages/dify-ui/src/preview-card/index.stories.tsx @@ -0,0 +1,213 @@ +import type { Meta, StoryObj } from '@storybook/react-vite' +import type { Placement } from '.' +import { useState } from 'react' +import { + createPreviewCardHandle, + PreviewCard, + PreviewCardContent, + PreviewCardTrigger, +} from '.' + +const rowButtonClassName + = 'flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm text-text-secondary hover:bg-state-base-hover' + +const triggerButtonClassName + = 'rounded-lg border border-divider-subtle bg-components-button-secondary-bg px-3 py-1.5 text-sm text-text-secondary shadow-xs hover:bg-state-base-hover' + +const inlineLinkClassName + = 'text-text-accent underline decoration-text-accent/60 decoration-1 underline-offset-2 outline-hidden hover:decoration-text-accent focus-visible:rounded-xs focus-visible:no-underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-text-accent data-[popup-open]:decoration-text-accent' + +const meta = { + title: 'Base/UI/PreviewCard', + component: PreviewCard, + parameters: { + layout: 'centered', + docs: { + description: { + component: + 'Hover- and focus-activated rich preview for triggers whose primary click has its own destination (following a link, selecting a row, jumping to a definition). Built on Base UI PreviewCard.\n\n**A11y contract:** touch and screen-reader users cannot open the preview. Never place information or actions in the popup that are not also reachable from the trigger\'s primary click destination. If that is unavoidable, add a separate click affordance (Popover) or move the unique content onto the destination.', + }, + }, + }, + tags: ['autodocs'], +} satisfies Meta + +export default meta +type Story = StoryObj + +// --- Canonical: inline link preview --------------------------------------- +// Mirrors Base UI's own PreviewCard docs demo: an inline `` in a +// paragraph, hovering reveals a rich preview (image + summary) of the link's +// destination. The Wikipedia URL and Unsplash image are the exact assets used +// in base-ui.com's public docs so the story renders a real preview. +// https://base-ui.com/react/components/preview-card +const typographyPreview = createPreviewCardHandle() + +export const LinkPreview: Story = { + name: 'Link preview (canonical)', + parameters: { + docs: { + description: { + story: + 'The prototypical PreviewCard use case: an inline hyperlink with a rich hover preview of the destination. Uses a detached trigger + `createPreviewCardHandle()` so the trigger can sit inline in prose while the popup content is defined elsewhere. The trigger renders a real `` — click still follows the link; the preview is strictly supplementary.', + }, + }, + }, + render: () => ( +
+

+ The principles of good + {' '} + + typography + + {' '} + remain in the digital age. +

+ + + +
+ Station Hofplein signage in Rotterdam, Netherlands +

+ Typography + {' '} + is the art and science of arranging type to make written language legible, readable, and visually appealing. +

+
+
+
+
+ ), +} + +export const Supplementary: Story = { + name: 'Supplementary preview on a button trigger', + parameters: { + docs: { + description: { + story: + 'Application-level adaptation of the same semantic: the trigger is a `
+ )} + /> + +
+
gpt-4o
+
+ Multimodal flagship model. Vision, audio and 128k context. +
+
+
+ + ), +} + +const PLACEMENTS: Placement[] = [ + 'top-start', + 'top', + 'top-end', + 'right-start', + 'right', + 'right-end', + 'bottom-start', + 'bottom', + 'bottom-end', + 'left-start', + 'left', + 'left-end', +] + +const PlacementsDemo = () => { + const [placement, setPlacement] = useState('bottom') + + return ( +
+
+ {PLACEMENTS.map(value => ( + + ))} +
+ + Hover me} + /> + +
+
+ placement=" + {placement} + " +
+
+ Preview positions itself relative to the trigger. +
+
+
+
+
+ ) +} + +export const Placements: Story = { + parameters: { + layout: 'fullscreen', + }, + render: () => , +} + +const CustomDelayDemo = () => ( + + Snappy trigger} + /> + +
+
Fast hover
+
+ Base UI defaults (600ms / 300ms) are tuned for link previews. Override per trigger for denser UIs. +
+
+
+
+) + +export const CustomDelays: Story = { + render: () => , +} diff --git a/packages/dify-ui/src/preview-card/index.tsx b/packages/dify-ui/src/preview-card/index.tsx new file mode 100644 index 0000000000..771b15cf13 --- /dev/null +++ b/packages/dify-ui/src/preview-card/index.tsx @@ -0,0 +1,81 @@ +'use client' + +import type { ReactNode } from 'react' +import type { Placement } from '../placement' +import { PreviewCard as BasePreviewCard } from '@base-ui/react/preview-card' +import { cn } from '../cn' +import { parsePlacement } from '../placement' + +export type { Placement } + +/** + * PreviewCard is a hover/focus-triggered rich preview intended to supplement a + * trigger whose primary action is its own click destination (e.g. a link, a + * selectable row, a chip that jumps to a definition). + * + * A11y contract — match Base UI's guidance: + * - The popup MUST NOT contain information or actions that are not also + * reachable from the trigger's primary click destination. Touch and screen + * reader users cannot open the card and must be able to get the same + * information/actions without it. + * - If content is unique to the popup, either (a) add a separate click-triggered + * affordance (Popover) next to the trigger, or (b) move the unique content + * onto the click destination. + */ +export const PreviewCard = BasePreviewCard.Root +export const PreviewCardTrigger = BasePreviewCard.Trigger +export const createPreviewCardHandle = BasePreviewCard.createHandle + +type PreviewCardContentProps = { + children: ReactNode + placement?: Placement + sideOffset?: number + alignOffset?: number + className?: string + popupClassName?: string + positionerProps?: Omit< + BasePreviewCard.Positioner.Props, + 'children' | 'className' | 'side' | 'align' | 'sideOffset' | 'alignOffset' + > + popupProps?: Omit< + BasePreviewCard.Popup.Props, + 'children' | 'className' + > +} + +export function PreviewCardContent({ + children, + placement = 'bottom', + sideOffset = 8, + alignOffset = 0, + className, + popupClassName, + positionerProps, + popupProps, +}: PreviewCardContentProps) { + const { side, align } = parsePlacement(placement) + + return ( + + + + {children} + + + + ) +} diff --git a/packages/dify-ui/src/tooltip/__tests__/index.spec.tsx b/packages/dify-ui/src/tooltip/__tests__/index.spec.tsx index 3660c2c8e5..043835f697 100644 --- a/packages/dify-ui/src/tooltip/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/tooltip/__tests__/index.spec.tsx @@ -46,20 +46,7 @@ describe('TooltipContent', () => { }) }) - describe('Variant and popup props', () => { - it('should render popup content when variant is plain', async () => { - const screen = await render( - - Trigger - - Plain tooltip body - - , - ) - - await expect.element(screen.getByRole('tooltip', { name: 'plain tooltip' })).toHaveTextContent('Plain tooltip body') - }) - + describe('Popup props', () => { it('should forward popup props and handlers when popup props are provided', async () => { const onMouseEnter = vi.fn() @@ -83,7 +70,11 @@ describe('TooltipContent', () => { await expect.element(popup).toHaveAttribute('id', 'tooltip-popup-id') await expect.element(popup).toHaveAttribute('data-track-id', 'tooltip-track') - expect(onMouseEnter).toHaveBeenCalledTimes(1) + // Intent of the assertion is "handler is wired up". The exact call count + // depends on vitest-browser's pointer simulation and Base UI's internal + // pointer tracking (both of which may fire more than one enter event for + // a single `.hover()` action), so assert presence, not count. + expect(onMouseEnter).toHaveBeenCalled() }) it('should apply className to the popup and positionerClassName to the positioner', async () => { diff --git a/packages/dify-ui/src/tooltip/index.stories.tsx b/packages/dify-ui/src/tooltip/index.stories.tsx index dca3be32f3..902449d4a4 100644 --- a/packages/dify-ui/src/tooltip/index.stories.tsx +++ b/packages/dify-ui/src/tooltip/index.stories.tsx @@ -8,8 +8,8 @@ import { TooltipTrigger, } from '.' -const triggerButtonClassName = 'rounded-lg border border-divider-subtle bg-components-button-secondary-bg px-3 py-1.5 text-sm text-text-secondary shadow-xs hover:bg-state-base-hover' const iconButtonClassName = 'inline-flex h-8 w-8 items-center justify-center rounded-lg border border-divider-subtle bg-components-button-secondary-bg text-text-secondary shadow-xs hover:bg-state-base-hover' +const triggerButtonClassName = 'rounded-lg border border-divider-subtle bg-components-button-secondary-bg px-3 py-1.5 text-sm text-text-secondary shadow-xs hover:bg-state-base-hover' const meta = { title: 'Base/UI/Tooltip', @@ -25,7 +25,7 @@ const meta = { layout: 'centered', docs: { description: { - component: 'Compound tooltip built on Base UI Tooltip. Wrap the app in `TooltipProvider` (done automatically in these stories) so multiple tooltips share open/close delays. Each tooltip pairs a `TooltipTrigger` with a `TooltipContent` and supports placement, offsets, and two style variants.', + component: 'Compound tooltip built on Base UI Tooltip. Wrap the app in `TooltipProvider` (done automatically in these stories) so multiple tooltips share open/close delays. Each tooltip pairs a `TooltipTrigger` with a `TooltipContent` and supports placement and offsets.\n\n**Usage contract** (mirrors the [Base UI tooltip guidelines](https://base-ui.com/react/components/tooltip#alternatives-to-tooltips)):\n\n- Tooltips are **supplementary visual labels** for sighted mouse and keyboard users. They are disabled on touch devices and are not announced to screen readers.\n- The trigger **must carry its own `aria-label` or visible text** that matches the tooltip — the tooltip does not replace labeling.\n- Keep content short and non-interactive (an icon-button label, a keyboard shortcut, one-word clarification).\n- **Do not** place descriptions, prose, links, or interactive controls inside a tooltip — touch and screen-reader users cannot reach them.\n- For hover-triggered rich previews that users move their cursor onto, use `PreviewCard` (dwell-able, structured content).\n- For an info icon that explains a concept (an "infotip"), or for any hover popup that needs interactive content or to reach touch/assistive-tech users, use `Popover` with `openOnHover` on the trigger.', }, }, }, @@ -35,47 +35,58 @@ const meta = { export default meta type Story = StoryObj -export const Default: Story = { - render: () => ( - - } - > - Hover me - - - Tooltips describe interactive elements without a click. - - - ), -} +const ICON_ACTIONS = [ + { icon: 'i-ri-pencil-line', label: 'Edit' }, + { icon: 'i-ri-file-copy-line', label: 'Duplicate' }, + { icon: 'i-ri-archive-line', label: 'Archive' }, + { icon: 'i-ri-delete-bin-line', label: 'Delete' }, +] as const -export const Plain: Story = { +export const IconButton: Story = { + name: 'Icon button (canonical)', parameters: { docs: { description: { - story: 'Use `variant="plain"` to render the popup without default chrome (background, padding, typography). Apply your own styling via `className` on `TooltipContent`.', + story: 'The canonical tooltip use case: an icon-only button surfaces its accessible label as a tooltip for sighted mouse and keyboard users. The trigger already carries `aria-label` — the tooltip mirrors that label visually; it does **not** replace it.', + }, + }, + }, + render: () => ( +
+ {ICON_ACTIONS.map(({ icon, label }) => ( + + + + + )} + /> + {label} + + ))} +
+ ), +} + +export const KeyboardShortcut: Story = { + parameters: { + docs: { + description: { + story: 'A short, supplementary hint that surfaces a keyboard shortcut next to a visible button label. The trigger is fully self-describing ("Save"); the tooltip only adds non-essential extra clarity for mouse/keyboard users.', }, }, }, render: () => ( } - > - Preview details - - -
- Dataset preview - - 32 documents • Last indexed 2 minutes ago - -
-
+ render={( + + )} + /> + ⌘S
), } @@ -116,14 +127,10 @@ const PlacementsDemo = () => { } - > - Anchor - + render={} + /> - placement=" - {placement} - " + {`placement="${placement}"`} @@ -133,113 +140,45 @@ const PlacementsDemo = () => { export const Placements: Story = { parameters: { layout: 'fullscreen', + docs: { + description: { + story: 'Placement reference. `placement` accepts the 12 standard side/align combinations; Base UI flips automatically if the tooltip would overflow the viewport.', + }, + }, }, render: () => , } -export const OnIconButtons: Story = { - parameters: { - docs: { - description: { - story: 'Tooltips are essential for icon-only buttons. The trigger is the button; the tooltip provides the accessible label and hover hint.', - }, - }, - }, - render: () => ( -
- - - - - )} - /> - Edit - - - - - - )} - /> - Duplicate - - - - - - )} - /> - Archive - - - - - - )} - /> - Delete - -
- ), -} - -export const LongContent: Story = { - render: () => ( - - } - > - What are tokens? - - - Tokens are the basic units a model reads. English text averages ~4 characters per token; non-Latin scripts often use more tokens per character. Both input and output count toward your quota. - - - ), -} - const DELAY_PRESETS: Array<{ label: string, delay: number }> = [ - { label: 'Instant (0ms)', delay: 0 }, - { label: 'Fast (150ms)', delay: 150 }, - { label: 'Default (600ms)', delay: 600 }, + { label: 'Instant', delay: 0 }, + { label: 'Fast', delay: 150 }, + { label: 'Default', delay: 600 }, ] -const DelayDemo = () => { - return ( -
- {DELAY_PRESETS.map(({ label, delay }) => ( - - - } - > - {label} - - - Appeared after - {delay} - ms hover delay. - - - - ))} -
- ) -} +const DelayDemo = () => ( +
+ {DELAY_PRESETS.map(({ label, delay }) => ( + + + + + + )} + /> + {`${label} (${delay}ms)`} + + + ))} +
+) export const WithDelay: Story = { parameters: { docs: { description: { - story: '`TooltipProvider` controls hover `delay` (and `closeDelay`) for the tooltips nested inside it. Adjacent tooltips under the same provider open instantly after the first has been shown.', + story: '`TooltipProvider` controls hover `delay` (and `closeDelay`) for the tooltips nested inside it. Adjacent tooltips under the same provider open instantly after the first has been shown. The Dify app root sets `delay={300} closeDelay={200}` — override locally only when the surrounding UX demands it.', }, }, }, diff --git a/packages/dify-ui/src/tooltip/index.tsx b/packages/dify-ui/src/tooltip/index.tsx index e0fcd7c5c3..1f9772ce2d 100644 --- a/packages/dify-ui/src/tooltip/index.tsx +++ b/packages/dify-ui/src/tooltip/index.tsx @@ -8,7 +8,28 @@ import { parsePlacement } from '../placement' export type { Placement } -type TooltipContentVariant = 'default' | 'plain' +/** + * Tooltip is an **ephemeral hint** tied to a trigger (typically an icon button, + * badge, or short label). It follows Base UI's Tooltip semantics: + * + * - Opens on pointer hover or keyboard focus on the trigger. + * - Closes as soon as the pointer leaves the trigger — the popup itself is + * **not dwell-able**; users cannot move their cursor onto the tooltip. + * - Must contain only short, non-interactive text. No links, buttons, form + * controls, or structured panels. + * + * If you need any of the following, use `PreviewCard` instead (hover-triggered + * rich preview that users can move their cursor onto): + * + * - Multi-line or structured content (icon + title + metadata) + * - Content the user needs to "stop and read" for more than ~1 second + * - Content wider than ~300px + * + * If you need interactive affordances (buttons, links, forms) use `Popover`. + */ +export const TooltipProvider = BaseTooltip.Provider +export const Tooltip = BaseTooltip.Root +export const TooltipTrigger = BaseTooltip.Trigger type TooltipContentProps = { children: ReactNode @@ -17,7 +38,6 @@ type TooltipContentProps = { alignOffset?: number positionerClassName?: string className?: string - variant?: TooltipContentVariant } & Omit export function TooltipContent({ @@ -27,7 +47,6 @@ export function TooltipContent({ alignOffset = 0, positionerClassName, className, - variant = 'default', ...props }: TooltipContentProps) { const { side, align } = parsePlacement(placement) @@ -43,7 +62,7 @@ export function TooltipContent({ > ) } - -export const TooltipProvider = BaseTooltip.Provider -export const Tooltip = BaseTooltip.Root -export const TooltipTrigger = BaseTooltip.Trigger diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c276fae9e..d573664e3a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,8 +16,8 @@ catalogs: specifier: 8.2.0 version: 8.2.0 '@base-ui/react': - specifier: 1.4.0 - version: 1.4.0 + specifier: 1.4.1 + version: 1.4.1 '@chromatic-com/storybook': specifier: 5.1.2 version: 5.1.2 @@ -532,8 +532,8 @@ catalogs: specifier: 12.0.0-beta.1 version: 12.0.0-beta.1 vite-plus: - specifier: 0.1.18 - version: 0.1.18 + specifier: 0.1.19 + version: 0.1.19 vitest-browser-react: specifier: 2.2.0 version: 2.2.0 @@ -574,8 +574,8 @@ overrides: svgo@>=3.0.0 <3.3.3: 3.3.3 tar@<=7.5.10: 7.5.11 undici@>=7.0.0 <7.24.0: 7.24.0 - vite: npm:@voidzero-dev/vite-plus-core@0.1.18 - vitest: npm:@voidzero-dev/vite-plus-test@0.1.18 + vite: npm:@voidzero-dev/vite-plus-core@0.1.19 + vitest: npm:@voidzero-dev/vite-plus-test@0.1.19 yaml@>=2.0.0 <2.8.3: 2.8.3 yauzl@<3.2.1: 3.2.1 @@ -585,7 +585,7 @@ importers: devDependencies: '@antfu/eslint-config': specifier: 'catalog:' - version: 8.2.0(@eslint-react/eslint-plugin@3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@next/eslint-plugin-next@16.2.3)(@types/node@25.6.0)(@typescript-eslint/typescript-estree@8.58.2(typescript@6.0.2))(@typescript-eslint/utils@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(eslint-plugin-react-refresh@0.5.2(eslint@10.2.0(jiti@2.6.1)))(eslint@10.2.0(jiti@2.6.1))(happy-dom@20.9.0)(jiti@2.6.1)(oxlint@1.60.0(oxlint-tsgolint@0.20.0))(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + version: 8.2.0(@eslint-react/eslint-plugin@3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@next/eslint-plugin-next@16.2.3)(@types/node@25.6.0)(@typescript-eslint/typescript-estree@8.58.2(typescript@6.0.2))(@typescript-eslint/utils@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(eslint-plugin-react-refresh@0.5.2(eslint@10.2.0(jiti@2.6.1)))(eslint@10.2.0(jiti@2.6.1))(happy-dom@20.9.0)(jiti@2.6.1)(oxlint@1.60.0(oxlint-tsgolint@0.21.1))(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) eslint: specifier: 'catalog:' version: 10.2.0(jiti@2.6.1) @@ -599,11 +599,11 @@ importers: specifier: 'catalog:' version: 1.3.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) vite: - specifier: npm:@voidzero-dev/vite-plus-core@0.1.18 - version: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + specifier: npm:@voidzero-dev/vite-plus-core@0.1.19 + version: '@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' vite-plus: specifier: 'catalog:' - version: 0.1.18(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + version: 0.1.19(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) e2e: devDependencies: @@ -626,11 +626,11 @@ importers: specifier: 'catalog:' version: 6.0.2 vite: - specifier: npm:@voidzero-dev/vite-plus-core@0.1.18 - version: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + specifier: npm:@voidzero-dev/vite-plus-core@0.1.19 + version: '@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' vite-plus: specifier: 'catalog:' - version: 0.1.18(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + version: 0.1.19(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) packages/dify-ui: dependencies: @@ -643,7 +643,7 @@ importers: devDependencies: '@base-ui/react': specifier: 'catalog:' - version: 1.4.0(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + version: 1.4.1(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@chromatic-com/storybook': specifier: 'catalog:' version: 5.1.2(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) @@ -658,7 +658,7 @@ importers: version: 1.2.10 '@storybook/addon-docs': specifier: 'catalog:' - version: 10.3.5(@types/react@19.2.14)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) + version: 10.3.5(@types/react@19.2.14)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) '@storybook/addon-links': specifier: 'catalog:' version: 10.3.5(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) @@ -667,10 +667,10 @@ importers: version: 10.3.5(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) '@storybook/react-vite': specifier: 'catalog:' - version: 10.3.5(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2) + version: 10.3.5(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2) '@tailwindcss/vite': specifier: 'catalog:' - version: 4.2.2(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) + version: 4.2.2(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) '@types/react': specifier: 'catalog:' version: 19.2.14 @@ -679,10 +679,10 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': specifier: 'catalog:' - version: 6.0.1(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) + version: 6.0.1(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + version: 4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) class-variance-authority: specifier: 'catalog:' version: 0.7.1 @@ -705,14 +705,14 @@ importers: specifier: 'catalog:' version: 6.0.2 vite: - specifier: npm:@voidzero-dev/vite-plus-core@0.1.18 - version: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + specifier: npm:@voidzero-dev/vite-plus-core@0.1.19 + version: '@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' vite-plus: specifier: 'catalog:' - version: 0.1.18(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + version: 0.1.19(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) vitest-browser-react: specifier: 'catalog:' - version: 2.2.0(@types/node@25.6.0)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + version: 2.2.0(@types/node@25.6.0)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) packages/iconify-collections: devDependencies: @@ -736,11 +736,11 @@ importers: specifier: 'catalog:' version: 25.6.0 vite: - specifier: npm:@voidzero-dev/vite-plus-core@0.1.18 - version: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + specifier: npm:@voidzero-dev/vite-plus-core@0.1.19 + version: '@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' vite-plus: specifier: 'catalog:' - version: 0.1.18(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + version: 0.1.19(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) packages/tsconfig: {} @@ -763,7 +763,7 @@ importers: version: 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + version: 4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) eslint: specifier: 'catalog:' version: 10.2.0(jiti@2.6.1) @@ -771,14 +771,14 @@ importers: specifier: 'catalog:' version: 6.0.2 vite: - specifier: npm:@voidzero-dev/vite-plus-core@0.1.18 - version: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + specifier: npm:@voidzero-dev/vite-plus-core@0.1.19 + version: '@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' vite-plus: specifier: 'catalog:' - version: 0.1.18(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + version: 0.1.19(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) vitest: - specifier: npm:@voidzero-dev/vite-plus-test@0.1.18 - version: '@voidzero-dev/vite-plus-test@0.1.18(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + specifier: npm:@voidzero-dev/vite-plus-test@0.1.19 + version: '@voidzero-dev/vite-plus-test@0.1.19(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' web: dependencies: @@ -790,7 +790,7 @@ importers: version: 1.27.7(@amplitude/rrweb@2.0.0-alpha.37) '@base-ui/react': specifier: 'catalog:' - version: 1.4.0(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + version: 1.4.1(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@emoji-mart/data': specifier: 'catalog:' version: 1.2.1 @@ -1091,7 +1091,7 @@ importers: devDependencies: '@antfu/eslint-config': specifier: 'catalog:' - version: 8.2.0(@eslint-react/eslint-plugin@3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@next/eslint-plugin-next@16.2.3)(@types/node@25.6.0)(@typescript-eslint/typescript-estree@8.58.2(typescript@6.0.2))(@typescript-eslint/utils@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(eslint-plugin-react-refresh@0.5.2(eslint@10.2.0(jiti@2.6.1)))(eslint@10.2.0(jiti@2.6.1))(happy-dom@20.9.0)(jiti@2.6.1)(oxlint@1.60.0(oxlint-tsgolint@0.20.0))(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + version: 8.2.0(@eslint-react/eslint-plugin@3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@next/eslint-plugin-next@16.2.3)(@types/node@25.6.0)(@typescript-eslint/typescript-estree@8.58.2(typescript@6.0.2))(@typescript-eslint/utils@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(eslint-plugin-react-refresh@0.5.2(eslint@10.2.0(jiti@2.6.1)))(eslint@10.2.0(jiti@2.6.1))(happy-dom@20.9.0)(jiti@2.6.1)(oxlint@1.60.0(oxlint-tsgolint@0.21.1))(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) '@chromatic-com/storybook': specifier: 'catalog:' version: 5.1.2(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) @@ -1139,7 +1139,7 @@ importers: version: 4.2.0 '@storybook/addon-docs': specifier: 'catalog:' - version: 10.3.5(@types/react@19.2.14)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) + version: 10.3.5(@types/react@19.2.14)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) '@storybook/addon-links': specifier: 'catalog:' version: 10.3.5(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) @@ -1151,7 +1151,7 @@ importers: version: 10.3.5(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) '@storybook/nextjs-vite': specifier: 'catalog:' - version: 10.3.5(@babel/core@7.29.0)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2) + version: 10.3.5(@babel/core@7.29.0)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2) '@storybook/react': specifier: 'catalog:' version: 10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2) @@ -1160,7 +1160,7 @@ importers: version: 4.2.2 '@tailwindcss/vite': specifier: 'catalog:' - version: 4.2.2(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) + version: 4.2.2(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) '@tanstack/eslint-plugin-query': specifier: 'catalog:' version: 5.99.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) @@ -1226,13 +1226,13 @@ importers: version: 7.0.0-dev.20260413.1 '@vitejs/plugin-react': specifier: 'catalog:' - version: 6.0.1(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) + version: 6.0.1(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.24(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5) + version: 0.5.24(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5) '@vitest/coverage-v8': specifier: 'catalog:' - version: 4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + version: 4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) agentation: specifier: 'catalog:' version: 3.0.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -1247,7 +1247,7 @@ importers: version: 0.6.1(eslint@10.2.0(jiti@2.6.1)) eslint-plugin-better-tailwindcss: specifier: 'catalog:' - version: 4.4.1(eslint@10.2.0(jiti@2.6.1))(oxlint@1.60.0(oxlint-tsgolint@0.20.0))(tailwindcss@4.2.2)(typescript@6.0.2) + version: 4.4.1(eslint@10.2.0(jiti@2.6.1))(oxlint@1.60.0(oxlint-tsgolint@0.21.1))(tailwindcss@4.2.2)(typescript@6.0.2) eslint-plugin-hyoban: specifier: 'catalog:' version: 0.14.1(eslint@10.2.0(jiti@2.6.1)) @@ -1298,22 +1298,22 @@ importers: version: 3.19.3 vinext: specifier: 'catalog:' - version: 0.0.41(@mdx-js/rollup@3.1.1)(@vitejs/plugin-react@6.0.1(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)))(@vitejs/plugin-rsc@0.5.24(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5))(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + version: 0.0.41(@mdx-js/rollup@3.1.1)(@vitejs/plugin-react@6.0.1(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)))(@vitejs/plugin-rsc@0.5.24(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5))(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)(typescript@6.0.2) vite: - specifier: npm:@voidzero-dev/vite-plus-core@0.1.18 - version: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + specifier: npm:@voidzero-dev/vite-plus-core@0.1.19 + version: '@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' vite-plugin-inspect: specifier: 'catalog:' - version: 12.0.0-beta.1(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2)(ws@8.20.0) + version: 12.0.0-beta.1(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2)(ws@8.20.0) vite-plus: specifier: 'catalog:' - version: 0.1.18(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + version: 0.1.19(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) vitest: - specifier: npm:@voidzero-dev/vite-plus-test@0.1.18 - version: '@voidzero-dev/vite-plus-test@0.1.18(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + specifier: npm:@voidzero-dev/vite-plus-test@0.1.19 + version: '@voidzero-dev/vite-plus-test@0.1.19(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' vitest-canvas-mock: specifier: 'catalog:' - version: 1.1.4(@voidzero-dev/vite-plus-test@0.1.18(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) + version: 1.1.4(@voidzero-dev/vite-plus-test@0.1.19(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) packages: @@ -1539,8 +1539,8 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} - '@base-ui/react@1.4.0': - resolution: {integrity: sha512-QcqdVbr/+ba2/RAKJIV1PV6S02Q5+r6a4Eym8ndBw+ZbBILkkmQAyRxXCg/pArrHnkrGeU8goe26aw0h6eE8pg==} + '@base-ui/react@1.4.1': + resolution: {integrity: sha512-Ab5/LIhcmL8BQcsBUYiOfkSDRdLpvgUBzMK30cu684JPcLclYlztharvCZyNNgzJtbAiREzI9q0pI5erHCMgCw==} engines: {node: '>=14.0.0'} peerDependencies: '@date-fns/tz': ^1.2.0 @@ -1549,11 +1549,15 @@ packages: react: ^17 || ^18 || ^19 react-dom: ^17 || ^18 || ^19 peerDependenciesMeta: + '@date-fns/tz': + optional: true '@types/react': optional: true + date-fns: + optional: true - '@base-ui/utils@0.2.7': - resolution: {integrity: sha512-nXYKhiL/0JafyJE8PfcflipGftOftlIwKd72rU15iZ1M5yqgg5J9P8NHU71GReDuXco5MJA/eVQqUT5WRqX9sA==} + '@base-ui/utils@0.2.8': + resolution: {integrity: sha512-jvOi+c+ftGlGotNcKnzPVg2IhCaDTB6/6R3JeqdjdXktuAJi3wKH9T7+svuaKh1mmfVU11UWzUZVH74JDfi/wQ==} peerDependencies: '@types/react': ^17 || ^18 || ^19 react: ^17 || ^18 || ^19 @@ -2661,15 +2665,15 @@ packages: cpu: [x64] os: [win32] - '@oxc-project/runtime@0.124.0': - resolution: {integrity: sha512-sSg6n37J3w3mM4odFvRqzQENf6+qxKnvStr/gU0FgRRg1VE/4MqryLd9PJmE0a7K5xlDfbrctBtSagaFH6ij9Q==} + '@oxc-project/runtime@0.126.0': + resolution: {integrity: sha512-oksjxfqDNmIYMGlIgLzYgnz5YjZax27RtQezsPpKEGo9AC5LOaIGHsivCCeaAWdCtPnRyjZXM/7svreCC8kZVQ==} engines: {node: ^20.19.0 || >=22.12.0} '@oxc-project/types@0.121.0': resolution: {integrity: sha512-CGtOARQb9tyv7ECgdAlFxi0Fv7lmzvmlm2rpD/RdijOO9rfk/JvB1CjT8EnoD+tjna/IYgKKw3IV7objRb+aYw==} - '@oxc-project/types@0.124.0': - resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==} + '@oxc-project/types@0.126.0': + resolution: {integrity: sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==} '@oxc-resolver/binding-android-arm-eabi@11.19.1': resolution: {integrity: sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg==} @@ -2901,33 +2905,33 @@ packages: cpu: [x64] os: [win32] - '@oxlint-tsgolint/darwin-arm64@0.20.0': - resolution: {integrity: sha512-KKQcIHZHMxqpHUA1VXIbOG6chNCFkUWbQy6M+AFVtPKkA/3xAeJkJ3njoV66bfzwPHRcWQO+kcj5XqtbkjakoA==} + '@oxlint-tsgolint/darwin-arm64@0.21.1': + resolution: {integrity: sha512-7TLjyWe4wG9saJc992VWmaHq2hwKfOEEVTjheReXJXaDhavMZI4X9a6nKhbEng4IVkYtzjD2jw16vw2WFXLYLw==} cpu: [arm64] os: [darwin] - '@oxlint-tsgolint/darwin-x64@0.20.0': - resolution: {integrity: sha512-7HeVMuclGfG+NLZi2ybY0T4fMI7/XxO/208rJk+zEIloKkVnlh11Wd241JMGwgNFXn+MLJbOqOfojDb2Dt4L1g==} + '@oxlint-tsgolint/darwin-x64@0.21.1': + resolution: {integrity: sha512-7wf9Wf75nTzA7zpL9myhFe2RKvfuqGUOADNvUooCjEWvh7hmPz3lSEqTMh5Z/VQhzsG04mM9ACyghxhRzq7zFw==} cpu: [x64] os: [darwin] - '@oxlint-tsgolint/linux-arm64@0.20.0': - resolution: {integrity: sha512-zxhUwz+WSxE6oWlZLK2z2ps9yC6ebmgoYmjAl0Oa48+GqkZ56NVgo+wb8DURNv6xrggzHStQxqQxe3mK51HZag==} + '@oxlint-tsgolint/linux-arm64@0.21.1': + resolution: {integrity: sha512-IPuQN/Vd0Rjklg/cCGBbQyUuRBp2f6LQXpZYwk5ivOR6V/+CgiYsv8pn/PVY7gjeyoNvPQrXB7xMjHUO2YZbdw==} cpu: [arm64] os: [linux] - '@oxlint-tsgolint/linux-x64@0.20.0': - resolution: {integrity: sha512-/1l6FnahC9im8PK+Ekkx/V3yetO/PzZnJegE2FXcv/iXEhbeVxP/ouiTYcUQu9shT1FWJCSNti1VJHH+21Y1dg==} + '@oxlint-tsgolint/linux-x64@0.21.1': + resolution: {integrity: sha512-d1niGuTbh2qiv7dR7tqkbOcM5cIR63of0lMBFdEQavL1KrJV8zuRdwdi68K7MNGdgoR+J5A9ajpGGvsHwp1bPg==} cpu: [x64] os: [linux] - '@oxlint-tsgolint/win32-arm64@0.20.0': - resolution: {integrity: sha512-oPZ5Yz8sVdo7P/5q+i3IKeix31eFZ55JAPa1+RGPoe9PoaYVsdMvR6Jvib6YtrqoJnFPlg3fjEjlEPL8VBKYJA==} + '@oxlint-tsgolint/win32-arm64@0.21.1': + resolution: {integrity: sha512-ICu9y2JLnFPvFqstnWPPNqBM8LK8BWw2OTeaR0UgEMm4hOSbrZAKv1/hwZYyiLqnCNjBL87AGSQIgTHCYlsipw==} cpu: [arm64] os: [win32] - '@oxlint-tsgolint/win32-x64@0.20.0': - resolution: {integrity: sha512-4stx8RHj3SP9vQyRF/yZbz5igtPvYMEUR8CUoha4BVNZihi39DpCR8qkU7lpjB5Ga1DRMo2pHaA4bdTOMaY4mw==} + '@oxlint-tsgolint/win32-x64@0.21.1': + resolution: {integrity: sha512-cTEFCFjCj6iXfrSHcvajSPNqhEA4TxSzU3gFxbdGSAUTNXGToU99IbdhWAPSbhcucoym0XE4Zl7E41NiSkNTug==} cpu: [x64] os: [win32] @@ -4332,13 +4336,13 @@ packages: '@vitest/utils@4.1.4': resolution: {integrity: sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==} - '@voidzero-dev/vite-plus-core@0.1.18': - resolution: {integrity: sha512-3PmXOL26yHzlw8ET9SwXCmglGzUYq2fOTYf2t0mxvVIs7ua3bnf6tOnmR+6YX5k1Ez26B0ooYzx+znc8k+CAMw==} + '@voidzero-dev/vite-plus-core@0.1.19': + resolution: {integrity: sha512-BTmz50juSDolIN4Vtu5iVaPONV1XSrMB5V+9IoBhhxdogfvp7PBhaHuAcPjTN2RTVowhLZXoo8mn+aHjq//bkw==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: '@arethetypeswrong/core': ^0.18.1 - '@tsdown/css': 0.21.8 - '@tsdown/exe': 0.21.8 + '@tsdown/css': 0.21.9 + '@tsdown/exe': 0.21.9 '@types/node': ^20.19.0 || >=22.12.0 '@vitejs/devtools': ^0.1.0 esbuild: 0.27.2 @@ -4392,48 +4396,48 @@ packages: yaml: optional: true - '@voidzero-dev/vite-plus-darwin-arm64@0.1.18': - resolution: {integrity: sha512-bw2pWWE8RZRELWjXcdxdmRaOaYjmGmsxEm23TxvGxQXFb7k9l51W8tpjxariPGLxrEl+Cw5u601IL5LASaPJ5w==} + '@voidzero-dev/vite-plus-darwin-arm64@0.1.19': + resolution: {integrity: sha512-6MY/RiaRXKJ6wD/ftZnf+ohEqU68zHp3bVWetIw9dakcPL7TXoiIkDoechmZXCh+5eqxehvap4eh2eNEvWSM1Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@voidzero-dev/vite-plus-darwin-x64@0.1.18': - resolution: {integrity: sha512-8TFj6yJNsumoH+yFc+6zf3g2UuzvrPHq2FAAVORffaVZ29PWnDSsXjegaIBmoAtGO5Xb4lcilQx7NoF9hONrZg==} + '@voidzero-dev/vite-plus-darwin-x64@0.1.19': + resolution: {integrity: sha512-jV6ygWCarMFW5DRqRyFkB2jpRDiAlLYzyQu0HZfYNoxfdNyO7isfuR5X6gV+ji7J3Kp0RZOiGrQUCjxTPqZg5w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@voidzero-dev/vite-plus-linux-arm64-gnu@0.1.18': - resolution: {integrity: sha512-xHRqncKanOZ0zNnZSufL4Yx/gWrIFkCjU6jFzCukBOOCrcemq3SrALPHrNf+Nw1RLwNptGUZn2Vx/IjRLzUQDw==} + '@voidzero-dev/vite-plus-linux-arm64-gnu@0.1.19': + resolution: {integrity: sha512-jIWMgAok77aDuTK2kCQXn4Zp7pnUM56BvKhHCvnAmsF4yrs1KLQfH6YOdQMnVbNjQDneQgqdwHVDnkOfJRokYw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@voidzero-dev/vite-plus-linux-arm64-musl@0.1.18': - resolution: {integrity: sha512-CA6XxZbkT8lYwWzS2yAj6exr7nHl3R8Sz+ZdOhYCU4yR2qvzGatdVgFr7oPnrkHLF426cHJ172rmNNj8NKie/w==} + '@voidzero-dev/vite-plus-linux-arm64-musl@0.1.19': + resolution: {integrity: sha512-fUuXUqCl3zMbS5QpMJzewVjrpbtzlwuzYQSh5q59CMq65uCXT07amJzmuAFReDEMrwEAmjGgbamJ1ctLAYCxrA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@voidzero-dev/vite-plus-linux-x64-gnu@0.1.18': - resolution: {integrity: sha512-xBO3MtLGVASPjH/GDRxexfLCT0othVpiFMdEQ83Y+woVNbrrzcdQTGFUuFG4cAiMhtmjytyFwPBtZ76BWsDO3w==} + '@voidzero-dev/vite-plus-linux-x64-gnu@0.1.19': + resolution: {integrity: sha512-xFVGMo1Yo5p9gABpOSSGgu5LhhMQs6qVXU7xL+NAGnaVViAYujNuOhCpBk2yK4Cy98KiNOjwnR5jG0TnRd22xg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@voidzero-dev/vite-plus-linux-x64-musl@0.1.18': - resolution: {integrity: sha512-ADNis6SMarY7i8+b2ynUJ1PiqCHqnVwY7EQ+fSGug5zZ+W/cZq14+VWPxOvGR9LJk+iol8XuqsHy4BaV2+gjzw==} + '@voidzero-dev/vite-plus-linux-x64-musl@0.1.19': + resolution: {integrity: sha512-iEDxL85v/C01yF2EJKknkjDhKbgY10NL9/sZ4HxezWykePK6QpYY5ClWGL7gIi+YFp8rtAdRPKlrf0mTlYMvxw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@voidzero-dev/vite-plus-test@0.1.18': - resolution: {integrity: sha512-dovC2kJgiwMI8ay0i+3NvQGCDWPj8HQB2ONP/HbdJ5/XQVPq13+BihnCq8/ztz6uGhiDD8Nu4OZ3RgB14uvTfA==} + '@voidzero-dev/vite-plus-test@0.1.19': + resolution: {integrity: sha512-KK0lfqyiEOEykp3hrcHT49f1j3M3t15ZKCuO+e9KbDRambU7tdz70xoHCKkRXcFgnds9gqi09PSLVy1k8XN+Hg==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} peerDependencies: '@edge-runtime/vm': '*' @@ -4463,14 +4467,14 @@ packages: jsdom: optional: true - '@voidzero-dev/vite-plus-win32-arm64-msvc@0.1.18': - resolution: {integrity: sha512-EcDETMHG8xgjIlMizIu/wf0UtRZLGz+lHFvYFZVCkz4vLLz93a06vZ+3Oi9xY2Kc8aOHsCf8Gj5/dox/03cscw==} + '@voidzero-dev/vite-plus-win32-arm64-msvc@0.1.19': + resolution: {integrity: sha512-2GGeGr2mtXLjV9O8CXEEZkV6O8q8rMBhq8fj5fyaSuBe5FQ1OxGYYMDqNBxvbg+hSUw0ThKK6qmirj5fF2e/iw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@voidzero-dev/vite-plus-win32-x64-msvc@0.1.18': - resolution: {integrity: sha512-jBgL4ZjSJJu3FDcrqj4muzbr0WKlU6Ym1ilHQnq8R+2TRvE0AtvAMMuphICDslZGi6EK3fwJ+r2Lv7GU1AipQA==} + '@voidzero-dev/vite-plus-win32-x64-msvc@0.1.19': + resolution: {integrity: sha512-//xUNHQnd+p4Xd4rlObAvum3DW1ugbWZ+kfaqD7biHQ9HQwHF28WSpJ3+d31vLUHj4o3DXYSA67g1Bq2d4tVgg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -6793,8 +6797,8 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - oxlint-tsgolint@0.20.0: - resolution: {integrity: sha512-/Uc9TQyN1l8w9QNvXtVHYtz+SzDJHKpb5X0UnHodl0BVzijUPk0LPlDOHAvogd1UI+iy9ZSF6gQxEqfzUxCULQ==} + oxlint-tsgolint@0.21.1: + resolution: {integrity: sha512-O2hxiT14C2HJkwzBU6CQBFPoagSd/IcV+Tt3e3UUaXFwbW4BO5DSDPSSboc3UM5MIDY+MLyepvtQwBQafNxWdw==} hasBin: true oxlint@1.60.0: @@ -8035,8 +8039,8 @@ packages: storybook: ^0.0.0-0 || ^9.0.0 || ^10.0.0 || ^10.0.0-0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0 vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - vite-plus@0.1.18: - resolution: {integrity: sha512-RiWUoOmQiJMtd4Dfm6WD0v0Selqh/nQzmaGVIrkfnr+2s5UxGVZy7n2TCO5ZnR7w9noMIgtUAQN8GtKhwHEiOQ==} + vite-plus@0.1.19: + resolution: {integrity: sha512-QWuTqkO/a8Q7I3hHnYdvwlJa7mcc6hgh99/8CHoRb27pgo+z1ux+NGYYCZPJHKVtatAtVRaQQvy4cEQBHyB87A==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -8433,17 +8437,17 @@ snapshots: idb: 8.0.0 tslib: 2.8.1 - '@antfu/eslint-config@8.2.0(@eslint-react/eslint-plugin@3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@next/eslint-plugin-next@16.2.3)(@types/node@25.6.0)(@typescript-eslint/typescript-estree@8.58.2(typescript@6.0.2))(@typescript-eslint/utils@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(eslint-plugin-react-refresh@0.5.2(eslint@10.2.0(jiti@2.6.1)))(eslint@10.2.0(jiti@2.6.1))(happy-dom@20.9.0)(jiti@2.6.1)(oxlint@1.60.0(oxlint-tsgolint@0.20.0))(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)': + '@antfu/eslint-config@8.2.0(@eslint-react/eslint-plugin@3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@next/eslint-plugin-next@16.2.3)(@types/node@25.6.0)(@typescript-eslint/typescript-estree@8.58.2(typescript@6.0.2))(@typescript-eslint/utils@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(eslint-plugin-react-refresh@0.5.2(eslint@10.2.0(jiti@2.6.1)))(eslint@10.2.0(jiti@2.6.1))(happy-dom@20.9.0)(jiti@2.6.1)(oxlint@1.60.0(oxlint-tsgolint@0.21.1))(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)': dependencies: '@antfu/install-pkg': 1.1.0 '@clack/prompts': 1.2.0 - '@e18e/eslint-plugin': 0.3.0(eslint@10.2.0(jiti@2.6.1))(oxlint@1.60.0(oxlint-tsgolint@0.20.0)) + '@e18e/eslint-plugin': 0.3.0(eslint@10.2.0(jiti@2.6.1))(oxlint@1.60.0(oxlint-tsgolint@0.21.1)) '@eslint-community/eslint-plugin-eslint-comments': 4.7.1(eslint@10.2.0(jiti@2.6.1)) '@eslint/markdown': 8.0.1 '@stylistic/eslint-plugin': 5.10.0(eslint@10.2.0(jiti@2.6.1)) '@typescript-eslint/eslint-plugin': 8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) '@typescript-eslint/parser': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) - '@vitest/eslint-plugin': 1.6.15(@types/node@25.6.0)(@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(eslint@10.2.0(jiti@2.6.1))(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + '@vitest/eslint-plugin': 1.6.15(@types/node@25.6.0)(@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(eslint@10.2.0(jiti@2.6.1))(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) ansis: 4.2.0 cac: 7.0.0 eslint: 10.2.0(jiti@2.6.1) @@ -8622,10 +8626,10 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@base-ui/react@1.4.0(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@base-ui/react@1.4.1(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@babel/runtime': 7.29.2 - '@base-ui/utils': 0.2.7(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@base-ui/utils': 0.2.8(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@floating-ui/react-dom': 2.1.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@floating-ui/utils': 0.2.11 react: 19.2.5 @@ -8634,7 +8638,7 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 - '@base-ui/utils@0.2.7(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + '@base-ui/utils@0.2.8(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@babel/runtime': 7.29.2 '@floating-ui/utils': 0.2.11 @@ -8853,12 +8857,12 @@ snapshots: '@cucumber/tag-expressions@9.1.0': {} - '@e18e/eslint-plugin@0.3.0(eslint@10.2.0(jiti@2.6.1))(oxlint@1.60.0(oxlint-tsgolint@0.20.0))': + '@e18e/eslint-plugin@0.3.0(eslint@10.2.0(jiti@2.6.1))(oxlint@1.60.0(oxlint-tsgolint@0.21.1))': dependencies: eslint-plugin-depend: 1.5.0(eslint@10.2.0(jiti@2.6.1)) optionalDependencies: eslint: 10.2.0(jiti@2.6.1) - oxlint: 1.60.0(oxlint-tsgolint@0.20.0) + oxlint: 1.60.0(oxlint-tsgolint@0.21.1) '@egoist/tailwindcss-icons@1.9.2(tailwindcss@4.2.2)': dependencies: @@ -9386,11 +9390,11 @@ snapshots: dependencies: minipass: 7.1.3 - '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2)': + '@joshwooding/vite-plugin-react-docgen-typescript@0.7.0(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2)': dependencies: glob: 13.0.6 react-docgen-typescript: 2.4.0(typescript@6.0.2) - vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' optionalDependencies: typescript: 6.0.2 @@ -9827,11 +9831,11 @@ snapshots: '@oxc-parser/binding-win32-x64-msvc@0.121.0': optional: true - '@oxc-project/runtime@0.124.0': {} + '@oxc-project/runtime@0.126.0': {} '@oxc-project/types@0.121.0': {} - '@oxc-project/types@0.124.0': {} + '@oxc-project/types@0.126.0': {} '@oxc-resolver/binding-android-arm-eabi@11.19.1': optional: true @@ -9955,22 +9959,22 @@ snapshots: '@oxfmt/binding-win32-x64-msvc@0.45.0': optional: true - '@oxlint-tsgolint/darwin-arm64@0.20.0': + '@oxlint-tsgolint/darwin-arm64@0.21.1': optional: true - '@oxlint-tsgolint/darwin-x64@0.20.0': + '@oxlint-tsgolint/darwin-x64@0.21.1': optional: true - '@oxlint-tsgolint/linux-arm64@0.20.0': + '@oxlint-tsgolint/linux-arm64@0.21.1': optional: true - '@oxlint-tsgolint/linux-x64@0.20.0': + '@oxlint-tsgolint/linux-x64@0.21.1': optional: true - '@oxlint-tsgolint/win32-arm64@0.20.0': + '@oxlint-tsgolint/win32-arm64@0.21.1': optional: true - '@oxlint-tsgolint/win32-x64@0.20.0': + '@oxlint-tsgolint/win32-x64@0.21.1': optional: true '@oxlint/binding-android-arm-eabi@1.60.0': @@ -10470,10 +10474,10 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@storybook/addon-docs@10.3.5(@types/react@19.2.14)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))': + '@storybook/addon-docs@10.3.5(@types/react@19.2.14)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))': dependencies: '@mdx-js/react': 3.1.1(@types/react@19.2.14)(react@19.2.5) - '@storybook/csf-plugin': 10.3.5(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) + '@storybook/csf-plugin': 10.3.5(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) '@storybook/icons': 2.0.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@storybook/react-dom-shim': 10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) react: 19.2.5 @@ -10503,24 +10507,24 @@ snapshots: storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) ts-dedent: 2.2.0 - '@storybook/builder-vite@10.3.5(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))': + '@storybook/builder-vite@10.3.5(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))': dependencies: - '@storybook/csf-plugin': 10.3.5(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) + '@storybook/csf-plugin': 10.3.5(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) ts-dedent: 2.2.0 - vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' transitivePeerDependencies: - esbuild - rollup - webpack - '@storybook/csf-plugin@10.3.5(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))': + '@storybook/csf-plugin@10.3.5(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))': dependencies: storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) unplugin: 2.3.11 optionalDependencies: esbuild: 0.27.2 - vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' '@storybook/global@5.0.0': {} @@ -10529,18 +10533,18 @@ snapshots: react: 19.2.5 react-dom: 19.2.5(react@19.2.5) - '@storybook/nextjs-vite@10.3.5(@babel/core@7.29.0)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2)': + '@storybook/nextjs-vite@10.3.5(@babel/core@7.29.0)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2)': dependencies: - '@storybook/builder-vite': 10.3.5(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) + '@storybook/builder-vite': 10.3.5(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) '@storybook/react': 10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2) - '@storybook/react-vite': 10.3.5(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2) + '@storybook/react-vite': 10.3.5(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2) next: 16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) react: 19.2.5 react-dom: 19.2.5(react@19.2.5) storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.5) - vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' - vite-plugin-storybook-nextjs: 3.2.4(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2) + vite: '@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vite-plugin-storybook-nextjs: 3.2.4(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2) optionalDependencies: typescript: 6.0.2 transitivePeerDependencies: @@ -10557,11 +10561,11 @@ snapshots: react-dom: 19.2.5(react@19.2.5) storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@storybook/react-vite@10.3.5(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2)': + '@storybook/react-vite@10.3.5(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2)': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2) '@rollup/pluginutils': 5.3.0 - '@storybook/builder-vite': 10.3.5(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) + '@storybook/builder-vite': 10.3.5(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) '@storybook/react': 10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2) empathic: 2.0.0 magic-string: 0.30.21 @@ -10571,7 +10575,7 @@ snapshots: resolve: 1.22.11 storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) tsconfig-paths: 4.2.0 - vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' transitivePeerDependencies: - esbuild - rollup @@ -10710,12 +10714,12 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 4.2.2 - '@tailwindcss/vite@4.2.2(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))': + '@tailwindcss/vite@4.2.2(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))': dependencies: '@tailwindcss/node': 4.2.2 '@tailwindcss/oxide': 4.2.2 tailwindcss: 4.2.2 - vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' '@tanstack/devtools-client@0.0.6': dependencies: @@ -11333,12 +11337,12 @@ snapshots: '@resvg/resvg-wasm': 2.4.0 satori: 0.16.0 - '@vitejs/devtools-kit@0.1.11(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2)(ws@8.20.0)': + '@vitejs/devtools-kit@0.1.11(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2)(ws@8.20.0)': dependencies: '@vitejs/devtools-rpc': 0.1.11(typescript@6.0.2)(ws@8.20.0) birpc: 4.0.0 ohash: 2.0.11 - vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' transitivePeerDependencies: - typescript - ws @@ -11355,12 +11359,12 @@ snapshots: transitivePeerDependencies: - typescript - '@vitejs/plugin-react@6.0.1(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))': + '@vitejs/plugin-react@6.0.1(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 - vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' - '@vitejs/plugin-rsc@0.5.24(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)': + '@vitejs/plugin-rsc@0.5.24(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)': dependencies: '@rolldown/pluginutils': 1.0.0-rc.15 es-module-lexer: 2.0.0 @@ -11371,12 +11375,12 @@ snapshots: srvx: 0.11.15 strip-literal: 3.1.0 turbo-stream: 3.2.0 - vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' - vitefu: 1.1.3(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) + vite: '@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vitefu: 1.1.3(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) optionalDependencies: react-server-dom-webpack: 19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)': + '@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)': dependencies: '@bcoe/v8-coverage': 1.0.2 '@vitest/utils': 4.1.4 @@ -11388,7 +11392,7 @@ snapshots: obug: 2.1.1 std-env: 4.0.0 tinyrainbow: 3.1.0 - vitest: '@voidzero-dev/vite-plus-test@0.1.18(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vitest: '@voidzero-dev/vite-plus-test@0.1.19(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' transitivePeerDependencies: - '@arethetypeswrong/core' - '@edge-runtime/vm' @@ -11418,12 +11422,12 @@ snapshots: - vite - yaml - '@vitest/eslint-plugin@1.6.15(@types/node@25.6.0)(@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(eslint@10.2.0(jiti@2.6.1))(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)': + '@vitest/eslint-plugin@1.6.15(@types/node@25.6.0)(@typescript-eslint/eslint-plugin@8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(eslint@10.2.0(jiti@2.6.1))(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)': dependencies: '@typescript-eslint/scope-manager': 8.58.2 '@typescript-eslint/utils': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) eslint: 10.2.0(jiti@2.6.1) - vitest: '@voidzero-dev/vite-plus-test@0.1.18(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vitest: '@voidzero-dev/vite-plus-test@0.1.19(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' optionalDependencies: '@typescript-eslint/eslint-plugin': 8.58.2(@typescript-eslint/parser@8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) typescript: 6.0.2 @@ -11489,10 +11493,10 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 - '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)': + '@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)': dependencies: - '@oxc-project/runtime': 0.124.0 - '@oxc-project/types': 0.124.0 + '@oxc-project/runtime': 0.126.0 + '@oxc-project/types': 0.126.0 lightningcss: 1.32.0 postcss: 8.5.9 optionalDependencies: @@ -11504,29 +11508,29 @@ snapshots: typescript: 6.0.2 yaml: 2.8.3 - '@voidzero-dev/vite-plus-darwin-arm64@0.1.18': + '@voidzero-dev/vite-plus-darwin-arm64@0.1.19': optional: true - '@voidzero-dev/vite-plus-darwin-x64@0.1.18': + '@voidzero-dev/vite-plus-darwin-x64@0.1.19': optional: true - '@voidzero-dev/vite-plus-linux-arm64-gnu@0.1.18': + '@voidzero-dev/vite-plus-linux-arm64-gnu@0.1.19': optional: true - '@voidzero-dev/vite-plus-linux-arm64-musl@0.1.18': + '@voidzero-dev/vite-plus-linux-arm64-musl@0.1.19': optional: true - '@voidzero-dev/vite-plus-linux-x64-gnu@0.1.18': + '@voidzero-dev/vite-plus-linux-x64-gnu@0.1.19': optional: true - '@voidzero-dev/vite-plus-linux-x64-musl@0.1.18': + '@voidzero-dev/vite-plus-linux-x64-musl@0.1.19': optional: true - '@voidzero-dev/vite-plus-test@0.1.18(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)': + '@voidzero-dev/vite-plus-test@0.1.19(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@voidzero-dev/vite-plus-core': 0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + '@voidzero-dev/vite-plus-core': 0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) es-module-lexer: 1.7.0 obug: 2.1.1 pixelmatch: 7.1.0 @@ -11536,11 +11540,11 @@ snapshots: tinybench: 2.9.0 tinyexec: 1.0.4 tinyglobby: 0.2.16 - vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' ws: 8.20.0 optionalDependencies: '@types/node': 25.6.0 - '@vitest/coverage-v8': 4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + '@vitest/coverage-v8': 4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) happy-dom: 20.9.0 transitivePeerDependencies: - '@arethetypeswrong/core' @@ -11563,10 +11567,10 @@ snapshots: - utf-8-validate - yaml - '@voidzero-dev/vite-plus-win32-arm64-msvc@0.1.18': + '@voidzero-dev/vite-plus-win32-arm64-msvc@0.1.19': optional: true - '@voidzero-dev/vite-plus-win32-x64-msvc@0.1.18': + '@voidzero-dev/vite-plus-win32-x64-msvc@0.1.19': optional: true '@volar/language-core@2.4.28': @@ -12463,7 +12467,7 @@ snapshots: dependencies: eslint: 10.2.0(jiti@2.6.1) - eslint-plugin-better-tailwindcss@4.4.1(eslint@10.2.0(jiti@2.6.1))(oxlint@1.60.0(oxlint-tsgolint@0.20.0))(tailwindcss@4.2.2)(typescript@6.0.2): + eslint-plugin-better-tailwindcss@4.4.1(eslint@10.2.0(jiti@2.6.1))(oxlint@1.60.0(oxlint-tsgolint@0.21.1))(tailwindcss@4.2.2)(typescript@6.0.2): dependencies: '@eslint/css-tree': 4.0.1 '@valibot/to-json-schema': 1.6.0(valibot@1.3.1(typescript@6.0.2)) @@ -12476,7 +12480,7 @@ snapshots: valibot: 1.3.1(typescript@6.0.2) optionalDependencies: eslint: 10.2.0(jiti@2.6.1) - oxlint: 1.60.0(oxlint-tsgolint@0.20.0) + oxlint: 1.60.0(oxlint-tsgolint@0.21.1) transitivePeerDependencies: - '@eslint/css' - typescript @@ -14488,16 +14492,16 @@ snapshots: '@oxfmt/binding-win32-ia32-msvc': 0.45.0 '@oxfmt/binding-win32-x64-msvc': 0.45.0 - oxlint-tsgolint@0.20.0: + oxlint-tsgolint@0.21.1: optionalDependencies: - '@oxlint-tsgolint/darwin-arm64': 0.20.0 - '@oxlint-tsgolint/darwin-x64': 0.20.0 - '@oxlint-tsgolint/linux-arm64': 0.20.0 - '@oxlint-tsgolint/linux-x64': 0.20.0 - '@oxlint-tsgolint/win32-arm64': 0.20.0 - '@oxlint-tsgolint/win32-x64': 0.20.0 + '@oxlint-tsgolint/darwin-arm64': 0.21.1 + '@oxlint-tsgolint/darwin-x64': 0.21.1 + '@oxlint-tsgolint/linux-arm64': 0.21.1 + '@oxlint-tsgolint/linux-x64': 0.21.1 + '@oxlint-tsgolint/win32-arm64': 0.21.1 + '@oxlint-tsgolint/win32-x64': 0.21.1 - oxlint@1.60.0(oxlint-tsgolint@0.20.0): + oxlint@1.60.0(oxlint-tsgolint@0.21.1): optionalDependencies: '@oxlint/binding-android-arm-eabi': 1.60.0 '@oxlint/binding-android-arm64': 1.60.0 @@ -14518,7 +14522,7 @@ snapshots: '@oxlint/binding-win32-arm64-msvc': 1.60.0 '@oxlint/binding-win32-ia32-msvc': 1.60.0 '@oxlint/binding-win32-x64-msvc': 1.60.0 - oxlint-tsgolint: 0.20.0 + oxlint-tsgolint: 0.21.1 p-limit@3.1.0: dependencies: @@ -15822,20 +15826,20 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vinext@0.0.41(@mdx-js/rollup@3.1.1)(@vitejs/plugin-react@6.0.1(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)))(@vitejs/plugin-rsc@0.5.24(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5))(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)(typescript@6.0.2): + vinext@0.0.41(@mdx-js/rollup@3.1.1)(@vitejs/plugin-react@6.0.1(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)))(@vitejs/plugin-rsc@0.5.24(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5))(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)(typescript@6.0.2): dependencies: '@unpic/react': 1.0.2(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@vercel/og': 0.8.6 - '@vitejs/plugin-react': 6.0.1(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) + '@vitejs/plugin-react': 6.0.1(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) magic-string: 0.30.21 react: 19.2.5 react-dom: 19.2.5(react@19.2.5) - vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' vite-plugin-commonjs: 0.10.4 - vite-tsconfig-paths: 6.1.1(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2) + vite-tsconfig-paths: 6.1.1(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2) optionalDependencies: '@mdx-js/rollup': 3.1.1 - '@vitejs/plugin-rsc': 0.5.24(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5) + '@vitejs/plugin-rsc': 0.5.24(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5) react-server-dom-webpack: 19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5) transitivePeerDependencies: - next @@ -15855,9 +15859,9 @@ snapshots: fast-glob: 3.3.3 magic-string: 0.30.21 - vite-plugin-inspect@12.0.0-beta.1(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2)(ws@8.20.0): + vite-plugin-inspect@12.0.0-beta.1(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2)(ws@8.20.0): dependencies: - '@vitejs/devtools-kit': 0.1.11(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2)(ws@8.20.0) + '@vitejs/devtools-kit': 0.1.11(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2)(ws@8.20.0) ansis: 4.2.0 error-stack-parser-es: 1.0.5 obug: 2.1.1 @@ -15866,12 +15870,12 @@ snapshots: perfect-debounce: 2.1.0 sirv: 3.0.2 unplugin-utils: 0.3.1 - vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' transitivePeerDependencies: - typescript - ws - vite-plugin-storybook-nextjs@3.2.4(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2): + vite-plugin-storybook-nextjs@3.2.4(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2): dependencies: '@next/env': 16.0.0 image-size: 2.0.2 @@ -15880,29 +15884,29 @@ snapshots: next: 16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) ts-dedent: 2.2.0 - vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' - vite-tsconfig-paths: 5.1.4(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2) + vite: '@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vite-tsconfig-paths: 5.1.4(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2) transitivePeerDependencies: - supports-color - typescript - vite-plus@0.1.18(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3): + vite-plus@0.1.19(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3): dependencies: - '@oxc-project/types': 0.124.0 - '@voidzero-dev/vite-plus-core': 0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) - '@voidzero-dev/vite-plus-test': 0.1.18(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + '@oxc-project/types': 0.126.0 + '@voidzero-dev/vite-plus-core': 0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + '@voidzero-dev/vite-plus-test': 0.1.19(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) oxfmt: 0.45.0 - oxlint: 1.60.0(oxlint-tsgolint@0.20.0) - oxlint-tsgolint: 0.20.0 + oxlint: 1.60.0(oxlint-tsgolint@0.21.1) + oxlint-tsgolint: 0.21.1 optionalDependencies: - '@voidzero-dev/vite-plus-darwin-arm64': 0.1.18 - '@voidzero-dev/vite-plus-darwin-x64': 0.1.18 - '@voidzero-dev/vite-plus-linux-arm64-gnu': 0.1.18 - '@voidzero-dev/vite-plus-linux-arm64-musl': 0.1.18 - '@voidzero-dev/vite-plus-linux-x64-gnu': 0.1.18 - '@voidzero-dev/vite-plus-linux-x64-musl': 0.1.18 - '@voidzero-dev/vite-plus-win32-arm64-msvc': 0.1.18 - '@voidzero-dev/vite-plus-win32-x64-msvc': 0.1.18 + '@voidzero-dev/vite-plus-darwin-arm64': 0.1.19 + '@voidzero-dev/vite-plus-darwin-x64': 0.1.19 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 0.1.19 + '@voidzero-dev/vite-plus-linux-arm64-musl': 0.1.19 + '@voidzero-dev/vite-plus-linux-x64-gnu': 0.1.19 + '@voidzero-dev/vite-plus-linux-x64-musl': 0.1.19 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 0.1.19 + '@voidzero-dev/vite-plus-win32-x64-msvc': 0.1.19 transitivePeerDependencies: - '@arethetypeswrong/core' - '@edge-runtime/vm' @@ -15933,36 +15937,36 @@ snapshots: - vite - yaml - vite-tsconfig-paths@5.1.4(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2): + vite-tsconfig-paths@5.1.4(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2): dependencies: debug: 4.4.3(supports-color@8.1.1) globrex: 0.1.2 tsconfck: 3.1.6(typescript@6.0.2) optionalDependencies: - vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' transitivePeerDependencies: - supports-color - typescript - vite-tsconfig-paths@6.1.1(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2): + vite-tsconfig-paths@6.1.1(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2): dependencies: debug: 4.4.3(supports-color@8.1.1) globrex: 0.1.2 tsconfck: 3.1.6(typescript@6.0.2) - vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' transitivePeerDependencies: - supports-color - typescript - vitefu@1.1.3(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)): + vitefu@1.1.3(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)): optionalDependencies: - vite: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' - vitest-browser-react@2.2.0(@types/node@25.6.0)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3): + vitest-browser-react@2.2.0(@types/node@25.6.0)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3): dependencies: react: 19.2.5 react-dom: 19.2.5(react@19.2.5) - vitest: '@voidzero-dev/vite-plus-test@0.1.18(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vitest: '@voidzero-dev/vite-plus-test@0.1.19(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) @@ -15996,11 +16000,11 @@ snapshots: - vite - yaml - vitest-canvas-mock@1.1.4(@voidzero-dev/vite-plus-test@0.1.18(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)): + vitest-canvas-mock@1.1.4(@voidzero-dev/vite-plus-test@0.1.19(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)): dependencies: cssfontparser: 1.2.1 moo-color: 1.0.3 - vitest: '@voidzero-dev/vite-plus-test@0.1.18(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vitest: '@voidzero-dev/vite-plus-test@0.1.19(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' void-elements@3.1.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f02d05b233..7a81789267 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -42,15 +42,15 @@ overrides: svgo@>=3.0.0 <3.3.3: 3.3.3 tar@<=7.5.10: 7.5.11 undici@>=7.0.0 <7.24.0: 7.24.0 - vite: npm:@voidzero-dev/vite-plus-core@0.1.18 - vitest: npm:@voidzero-dev/vite-plus-test@0.1.18 + vite: npm:@voidzero-dev/vite-plus-core@0.1.19 + vitest: npm:@voidzero-dev/vite-plus-test@0.1.19 yaml@>=2.0.0 <2.8.3: 2.8.3 yauzl@<3.2.1: 3.2.1 catalog: '@amplitude/analytics-browser': 2.39.0 '@amplitude/plugin-session-replay-browser': 1.27.7 '@antfu/eslint-config': 8.2.0 - '@base-ui/react': 1.4.0 + '@base-ui/react': 1.4.1 '@chromatic-com/storybook': 5.1.2 '@cucumber/cucumber': 12.8.0 '@egoist/tailwindcss-icons': 1.9.2 @@ -222,10 +222,10 @@ catalog: use-context-selector: 2.0.0 uuid: 13.0.0 vinext: 0.0.41 - vite: npm:@voidzero-dev/vite-plus-core@0.1.18 + vite: npm:@voidzero-dev/vite-plus-core@0.1.19 vite-plugin-inspect: 12.0.0-beta.1 - vite-plus: 0.1.18 - vitest: npm:@voidzero-dev/vite-plus-test@0.1.18 + vite-plus: 0.1.19 + vitest: npm:@voidzero-dev/vite-plus-test@0.1.19 vitest-browser-react: 2.2.0 vitest-canvas-mock: 1.1.4 zod: 4.3.6 diff --git a/sdks/nodejs-client/src/http/client.test.ts b/sdks/nodejs-client/src/http/client.test.ts index af859801c6..4dc2087782 100644 --- a/sdks/nodejs-client/src/http/client.test.ts +++ b/sdks/nodejs-client/src/http/client.test.ts @@ -39,7 +39,7 @@ const jsonResponse = ( ...init, headers: { "content-type": "application/json", - ...(init.headers ?? {}), + ...init.headers, }, }); @@ -47,7 +47,7 @@ const textResponse = (body: string, init: ResponseInit = {}): Response => new Response(body, { ...init, headers: { - ...(init.headers ?? {}), + ...init.headers, }, }); diff --git a/sdks/nodejs-client/src/index.test.ts b/sdks/nodejs-client/src/index.test.ts index 8d56b994c4..b323371891 100644 --- a/sdks/nodejs-client/src/index.test.ts +++ b/sdks/nodejs-client/src/index.test.ts @@ -14,7 +14,7 @@ const jsonResponse = (body: unknown, init: ResponseInit = {}): Response => ...init, headers: { "content-type": "application/json", - ...(init.headers ?? {}), + ...init.headers, }, }); diff --git a/sdks/nodejs-client/vite.config.ts b/sdks/nodejs-client/vite.config.ts index 8d89508682..68ced551a3 100644 --- a/sdks/nodejs-client/vite.config.ts +++ b/sdks/nodejs-client/vite.config.ts @@ -4,6 +4,7 @@ export default defineConfig({ pack: { entry: ["src/index.ts"], format: ["esm"], + platform: "node", dts: true, clean: true, sourcemap: true, diff --git a/web/app/components/app/configuration/dataset-config/context-var/__tests__/index.spec.tsx b/web/app/components/app/configuration/dataset-config/context-var/__tests__/index.spec.tsx index 6726ba0583..91fe47d83d 100644 --- a/web/app/components/app/configuration/dataset-config/context-var/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/dataset-config/context-var/__tests__/index.spec.tsx @@ -10,6 +10,72 @@ vi.mock('@/next/navigation', () => ({ usePathname: () => '/test', })) +vi.mock('@langgenius/dify-ui/popover', async () => { + const React = await import('react') + const PopoverContext = React.createContext({ + open: false, + setOpen: (_open: boolean) => {}, + }) + + const Popover = ({ + children, + open: controlledOpen, + onOpenChange, + }: { + children: React.ReactNode + open?: boolean + onOpenChange?: (open: boolean) => void + }) => { + const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false) + const isControlled = controlledOpen !== undefined + const open = isControlled ? !!controlledOpen : uncontrolledOpen + const setOpen = (nextOpen: boolean) => { + if (!isControlled) + setUncontrolledOpen(nextOpen) + onOpenChange?.(nextOpen) + } + + return ( + + {children} + + ) + } + + const PopoverTrigger = ({ render }: { render: React.ReactNode }) => { + const { open, setOpen } = React.useContext(PopoverContext) + return ( +
setOpen(!open)} + > + {render} +
+ ) + } + + const PopoverContent = ({ + children, + ...props + }: React.HTMLAttributes & { children?: React.ReactNode }) => { + const { open } = React.useContext(PopoverContext) + if (!open) + return null + + return ( +
+ {children} +
+ ) + } + + return { + Popover, + PopoverTrigger, + PopoverContent, + } +}) + type PortalToFollowElemProps = { children: React.ReactNode open?: boolean @@ -209,20 +275,17 @@ describe('ContextVar', () => { // Act render() - const triggers = screen.getAllByTestId('portal-trigger') - const varPickerTrigger = triggers[triggers.length - 1] + const varPickerTrigger = screen.getByTestId('popover-trigger') await user.click(varPickerTrigger!) - expect(screen.getByTestId('portal-content'))!.toBeInTheDocument() + expect(screen.getByTestId('popover-content'))!.toBeInTheDocument() // Select a different option - const options = screen.getAllByText('var2') - expect(options.length).toBeGreaterThan(0) - await user.click(options[0]!) + await user.click(screen.getByText('var2')) // Assert expect(onChange).toHaveBeenCalledWith('var2') - expect(screen.queryByTestId('portal-content')).not.toBeInTheDocument() + expect(screen.queryByTestId('popover-content')).not.toBeInTheDocument() }) it('should toggle dropdown when clicking the trigger button', async () => { @@ -233,16 +296,15 @@ describe('ContextVar', () => { // Act render() - const triggers = screen.getAllByTestId('portal-trigger') - const varPickerTrigger = triggers[triggers.length - 1] + const varPickerTrigger = screen.getByTestId('popover-trigger') // Open dropdown await user.click(varPickerTrigger!) - expect(screen.getByTestId('portal-content'))!.toBeInTheDocument() + expect(screen.getByTestId('popover-content'))!.toBeInTheDocument() // Close dropdown await user.click(varPickerTrigger!) - expect(screen.queryByTestId('portal-content')).not.toBeInTheDocument() + expect(screen.queryByTestId('popover-content')).not.toBeInTheDocument() }) }) diff --git a/web/app/components/app/configuration/dataset-config/context-var/__tests__/var-picker.spec.tsx b/web/app/components/app/configuration/dataset-config/context-var/__tests__/var-picker.spec.tsx index 1d81a31091..7890343720 100644 --- a/web/app/components/app/configuration/dataset-config/context-var/__tests__/var-picker.spec.tsx +++ b/web/app/components/app/configuration/dataset-config/context-var/__tests__/var-picker.spec.tsx @@ -18,18 +18,21 @@ type PortalToFollowElemProps = { type PortalToFollowElemTriggerProps = React.HTMLAttributes & { children?: React.ReactNode, asChild?: boolean } type PortalToFollowElemContentProps = React.HTMLAttributes & { children?: React.ReactNode } -vi.mock('@/app/components/base/portal-to-follow-elem', () => { - const PortalContext = React.createContext({ open: false }) +vi.mock('@langgenius/dify-ui/popover', () => { + const PortalContext = React.createContext({ + open: false, + onOpenChange: undefined as ((open: boolean) => void) | undefined, + }) - const PortalToFollowElem = ({ children, open }: PortalToFollowElemProps) => { + const Popover = ({ children, open, onOpenChange }: PortalToFollowElemProps) => { return ( - +
{children}
) } - const PortalToFollowElemContent = ({ children, ...props }: PortalToFollowElemContentProps) => { + const PopoverContent = ({ children, ...props }: PortalToFollowElemContentProps) => { const { open } = React.useContext(PortalContext) if (!open) return null @@ -40,24 +43,41 @@ vi.mock('@/app/components/base/portal-to-follow-elem', () => { ) } - const PortalToFollowElemTrigger = ({ children, asChild, ...props }: PortalToFollowElemTriggerProps) => { + const PopoverTrigger = ({ children, asChild, render, ...props }: PortalToFollowElemTriggerProps & { render?: React.ReactNode }) => { + const { open, onOpenChange } = React.useContext(PortalContext) + const content = render ?? children + const handleClick = (e: React.MouseEvent) => { + props.onClick?.(e) + if (!props.onClick) + onOpenChange?.(!open) + } + + if (React.isValidElement(content)) { + return React.cloneElement(content, { + ...props, + 'onClick': handleClick, + 'data-testid': 'portal-trigger', + } as React.HTMLAttributes) + } + if (asChild && React.isValidElement(children)) { return React.cloneElement(children, { ...props, + 'onClick': handleClick, 'data-testid': 'portal-trigger', } as React.HTMLAttributes) } return ( -
- {children} +
+ {content}
) } return { - PortalToFollowElem, - PortalToFollowElemContent, - PortalToFollowElemTrigger, + Popover, + PopoverContent, + PopoverTrigger, } }) diff --git a/web/app/components/app/configuration/dataset-config/context-var/var-picker.tsx b/web/app/components/app/configuration/dataset-config/context-var/var-picker.tsx index d29b2e34df..9bac1c7a41 100644 --- a/web/app/components/app/configuration/dataset-config/context-var/var-picker.tsx +++ b/web/app/components/app/configuration/dataset-config/context-var/var-picker.tsx @@ -3,15 +3,15 @@ import type { FC } from 'react' import type { IInputTypeIconProps } from '@/app/components/app/configuration/config-var/input-type-icon' import { ChevronDownIcon } from '@heroicons/react/24/outline' import { cn } from '@langgenius/dify-ui/cn' +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@langgenius/dify-ui/popover' import * as React from 'react' import { useState } from 'react' import { useTranslation } from 'react-i18next' import IconTypeIcon from '@/app/components/app/configuration/config-var/input-type-icon' -import { - PortalToFollowElem, - PortalToFollowElemContent, - PortalToFollowElemTrigger, -} from '@/app/components/base/portal-to-follow-elem' type Option = { name: string, value: string, type: string } export type Props = { @@ -33,6 +33,7 @@ const VarItem: FC<{ item: Option }> = ({ item }) => (
) + const VarPicker: FC = ({ triggerClassName, className, @@ -45,47 +46,51 @@ const VarPicker: FC = ({ const [open, setOpen] = useState(false) const currItem = options.find(item => item.value === value) const notSetVar = !currItem + return ( - - setOpen(v => !v)}> -
-
- {value - ? ( - - ) - : ( -
- {notSelectedVarTip || t('feature.dataSet.queryVariable.choosePlaceholder', { ns: 'appDebug' })} -
- )} + + +
+
+ {currItem + ? ( + + ) + : ( +
+ {notSelectedVarTip || t('feature.dataSet.queryVariable.choosePlaceholder', { ns: 'appDebug' })} +
+ )} +
+ +
- -
-
- + )} + /> + {options.length > 0 ? (
- {options.map(({ name, value, type }, index) => ( + {options.map(({ name, value, type }) => (
{ onChange(value) @@ -103,9 +108,9 @@ const VarPicker: FC = ({
{t('feature.dataSet.queryVariable.noVarTip', { ns: 'appDebug' })}
)} - - - + + ) } + export default React.memo(VarPicker) diff --git a/web/app/components/base/infotip/index.tsx b/web/app/components/base/infotip/index.tsx new file mode 100644 index 0000000000..b97b499af3 --- /dev/null +++ b/web/app/components/base/infotip/index.tsx @@ -0,0 +1,82 @@ +'use client' + +import type { Placement } from '@langgenius/dify-ui/popover' +import type { ReactNode } from 'react' +import { cn } from '@langgenius/dify-ui/cn' +import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' + +/** + * Infotip — a `?` icon that reveals a long-form explanation on hover / focus / tap. + * + * Implements the pattern Base UI calls an "infotip": + * https://base-ui.com/react/components/tooltip#infotips + * + * > "Popups that open when hovering an info icon should use Popover with the + * > `openOnHover` prop on the trigger instead of a tooltip. This way, touch + * > users and screen reader users can access the content." + * + * Use whenever the trigger is an info glyph whose sole purpose is to open a + * popup (help text, documentation-style explanation). Do NOT use `Tooltip` for + * this — Tooltip is reserved for ephemeral, non-interactive visual labels that + * are unreachable on touch devices and by screen readers. + * + * Base UI rule of thumb: + * + * > "If the trigger's purpose is to open the popup itself, it's a popover. + * > If the trigger's purpose is unrelated to opening the popup, it's a tooltip." + * + * For hover-revealed supplementary previews of a link / row trigger that has + * its own primary click destination, use `PreviewCard` instead. + */ + +type InfotipProps = { + /** Popup content. Rich nodes are allowed. */ + 'children': ReactNode + /** Accessible name for the trigger. Required; should match the popup text. */ + 'aria-label': string + /** Placement of the popup relative to the trigger. Defaults to `top`. */ + 'placement'?: Placement + /** Extra classes on the outer trigger wrapper (layout / margin). */ + 'className'?: string + /** Extra classes on the `?` icon itself (size / color overrides). */ + 'iconClassName'?: string + /** Extra classes on the popup body (width / padding / whitespace overrides). */ + 'popupClassName'?: string + /** Hover open delay in ms. Defaults to 300 to match the app-wide Tooltip delay. */ + 'delay'?: number + /** Hover close delay in ms. Defaults to 200 to match the app-wide Tooltip delay. */ + 'closeDelay'?: number +} + +export function Infotip({ + children, + 'aria-label': ariaLabel, + placement = 'top', + className, + iconClassName, + popupClassName, + delay = 300, + closeDelay = 200, +}: InfotipProps) { + return ( + + + + + )} + /> + + {children} + + + ) +} diff --git a/web/app/components/base/prompt-editor/__tests__/hooks.spec.tsx b/web/app/components/base/prompt-editor/__tests__/hooks.spec.tsx index 9917918628..13c0c05803 100644 --- a/web/app/components/base/prompt-editor/__tests__/hooks.spec.tsx +++ b/web/app/components/base/prompt-editor/__tests__/hooks.spec.tsx @@ -423,11 +423,11 @@ describe('prompt-editor/hooks', () => { maxLength: 5, })) - const match = result.current('prefix @..', {} as LexicalEditor) + const match = result.current('prefix @ab', {} as LexicalEditor) expect(match).toEqual({ leadOffset: 7, - matchingString: '..', - replaceableString: '@..', + matchingString: 'ab', + replaceableString: '@ab', }) }) @@ -437,7 +437,7 @@ describe('prompt-editor/hooks', () => { maxLength: 5, })) - expect(result.current('prefix @.', {} as LexicalEditor)).toBeNull() + expect(result.current('prefix @a', {} as LexicalEditor)).toBeNull() }) it('should return null when matching text exceeds maxLength', () => { @@ -445,7 +445,7 @@ describe('prompt-editor/hooks', () => { minLength: 1, maxLength: 2, })) - expect(result.current('prefix @...', {} as LexicalEditor)).toBeNull() + expect(result.current('prefix @abc', {} as LexicalEditor)).toBeNull() }) it('should return null when text has no trigger character', () => { diff --git a/web/app/components/base/prompt-editor/hooks.ts b/web/app/components/base/prompt-editor/hooks.ts index dd6c6295a6..70869d6a17 100644 --- a/web/app/components/base/prompt-editor/hooks.ts +++ b/web/app/components/base/prompt-editor/hooks.ts @@ -154,17 +154,18 @@ type TriggerFn = ( text: string, editor: LexicalEditor, ) => MenuTextMatch | null -const PUNCTUATION = '\\.,\\+\\*\\?\\$\\@\\|#{}\\(\\)\\^\\-\\[\\]\\\\/!%\'"~=<>_:;' +const escapeForCharacterClass = (value: string) => value.replace(/[[\]\\^-]/g, '\\$&') export function useBasicTypeaheadTriggerMatch( trigger: string, { minLength = 1, maxLength = 75 }: { minLength?: number, maxLength?: number }, ): TriggerFn { return useCallback( (text: string) => { - const validChars = `[${PUNCTUATION}\\s]` + const escapedTrigger = escapeForCharacterClass(trigger) + const validChars = `[^${escapedTrigger}\\n\\r]` const TypeaheadTriggerRegex = new RegExp( '(.*)(' - + `[${trigger}]` + + `[${escapedTrigger}]` + `((?:${validChars}){0,${maxLength}})` + ')$', ) diff --git a/web/app/components/base/prompt-editor/plugins/component-picker-block/__tests__/index.spec.tsx b/web/app/components/base/prompt-editor/plugins/component-picker-block/__tests__/index.spec.tsx index 6cc38ad78f..3c734700a7 100644 --- a/web/app/components/base/prompt-editor/plugins/component-picker-block/__tests__/index.spec.tsx +++ b/web/app/components/base/prompt-editor/plugins/component-picker-block/__tests__/index.spec.tsx @@ -521,6 +521,84 @@ describe('ComponentPicker (component-picker-block/index.tsx)', () => { await waitFor(() => expect(readEditorText(editor)).not.toContain('{')) }) + it('filters workflow variables from slash input and matches child paths', async () => { + const captures: Captures = { editor: null, eventEmitter: null } + const user = userEvent.setup() + + const workflowVariableBlock = makeWorkflowVariableBlock({}, [ + makeWorkflowVarNode('node-1', 'Node 1', [ + makeWorkflowNodeVar('payload', VarType.object, [makeWorkflowNodeVar('child_name', VarType.string)]), + makeWorkflowNodeVar('other_value', VarType.string), + ]), + ]) + + render(( + + )) + + const editor = await waitForEditor(captures) + const dispatchSpy = vi.spyOn(editor, 'dispatchCommand') + + await setEditorText(editor, '/child', true) + await flushNextTick() + + expect(screen.queryByPlaceholderText('workflow.common.searchVar')).not.toBeInTheDocument() + expect(await screen.findByText('payload')).toBeInTheDocument() + expect(screen.queryByText('other_value')).not.toBeInTheDocument() + + const label = document.querySelector('[title="payload"]') + expect(label).not.toBeNull() + const row = (label as HTMLElement).parentElement?.parentElement + expect(row).not.toBeNull() + + await user.hover(row as HTMLElement) + const childField = await screen.findByText('child_name') + fireEvent.mouseDown(childField) + await user.unhover(row as HTMLElement) + + expect(dispatchSpy).toHaveBeenCalledWith(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, ['node-1', 'payload', 'child_name']) + await waitFor(() => expect(readEditorText(editor)).not.toContain('/child')) + }) + + it('filters workflow variables on the first character after slash and does not highlight context by default', async () => { + const captures: Captures = { editor: null, eventEmitter: null } + + const workflowVariableBlock = makeWorkflowVariableBlock({}, [ + makeWorkflowVarNode('node-1', 'Node 1', [ + makeWorkflowNodeVar('child_value', VarType.string), + makeWorkflowNodeVar('other_value', VarType.string), + ]), + ]) + + render(( + + )) + + const editor = await waitForEditor(captures) + await setEditorText(editor, '/c', true) + await flushNextTick() + + expect(await screen.findByText('child_value')).toBeInTheDocument() + expect(screen.queryByText('other_value')).not.toBeInTheDocument() + + const contextTitle = screen.getByText('common.promptEditor.context.item.title') + expect(contextTitle.closest('[tabindex="-1"]')).not.toHaveClass('bg-state-base-hover!') + + await waitFor(() => { + expect(readEditorText(editor)).toContain('/c') + }) + }) + it('skips removing the trigger when selection is null (needRemove is null) and still dispatches', async () => { const captures: Captures = { editor: null, eventEmitter: null } diff --git a/web/app/components/base/prompt-editor/plugins/component-picker-block/index.tsx b/web/app/components/base/prompt-editor/plugins/component-picker-block/index.tsx index 5903060911..5e983ed09a 100644 --- a/web/app/components/base/prompt-editor/plugins/component-picker-block/index.tsx +++ b/web/app/components/base/prompt-editor/plugins/component-picker-block/index.tsx @@ -1,5 +1,5 @@ import type { MenuRenderFn } from '@lexical/react/LexicalTypeaheadMenuPlugin' -import type { TextNode } from 'lexical' +import type { LexicalEditor, TextNode } from 'lexical' import type { ContextBlockType, CurrentBlockType, @@ -89,10 +89,16 @@ const ComponentPicker = ({ ], }) const [editor] = useLexicalComposerContext() - const checkForTriggerMatch = useBasicTypeaheadTriggerMatch(triggerString, { + const triggerMatchRef = useRef(null) + const baseCheckForTriggerMatch = useBasicTypeaheadTriggerMatch(triggerString, { minLength: 0, - maxLength: 0, + maxLength: 75, }) + const checkForTriggerMatch = useCallback((text: string, editor: LexicalEditor) => { + const match = baseCheckForTriggerMatch(text, editor) + triggerMatchRef.current = match?.matchingString ?? null + return match + }, [baseCheckForTriggerMatch]) const [queryString, setQueryString] = useState(null) const [blurHidden, setBlurHidden] = useState(false) @@ -155,6 +161,7 @@ const ComponentPicker = ({ currentBlock, errorMessageBlock, lastRunBlock, + queryString || undefined, ) const onSelectOption = useCallback( @@ -207,6 +214,8 @@ const ComponentPicker = ({ anchorElementRef, { options, selectedIndex, selectOptionAndCleanUp, setHighlightedIndex }, ) => { + const effectiveQueryString = triggerMatchRef.current ?? queryString + if (blurHidden) return null if (!(anchorElementRef.current && (allFlattenOptions.length || workflowVariableBlock?.show))) @@ -237,6 +246,8 @@ const ComponentPicker = ({ workflowVariableBlock?.show && (
{ @@ -270,8 +281,8 @@ const ComponentPicker = ({ ) } {option.renderMenuOption({ - queryString, - isSelected: selectedIndex === index, + queryString: effectiveQueryString, + isSelected: workflowVariableBlock?.show ? false : selectedIndex === index, onSelect: () => { selectOptionAndCleanUp(option) }, diff --git a/web/app/components/base/prompt-editor/plugins/hitl-input-block/__tests__/variable-block.spec.tsx b/web/app/components/base/prompt-editor/plugins/hitl-input-block/__tests__/variable-block.spec.tsx index db3e474b60..b0338cb823 100644 --- a/web/app/components/base/prompt-editor/plugins/hitl-input-block/__tests__/variable-block.spec.tsx +++ b/web/app/components/base/prompt-editor/plugins/hitl-input-block/__tests__/variable-block.spec.tsx @@ -302,8 +302,8 @@ describe('HITLInputVariableBlockComponent', () => { }) }) - describe('Tooltip payload', () => { - it('should call getVarType with rag selector and use rag node id mapping', () => { + describe('Full-path preview payload', () => { + it('should resolve the rag node via isRagVar offset and skip the full-path preview', () => { const getVarType = vi.fn(() => Type.number) const { container } = renderVariableBlock({ variables: ['rag', 'node-rag', 'chunk'], @@ -314,10 +314,9 @@ describe('HITLInputVariableBlockComponent', () => { expect(screen.getByText('chunk')).toBeInTheDocument() expect(hasErrorIcon(container)).toBe(false) - expect(getVarType).toHaveBeenCalledWith({ - nodeId: 'rag', - valueSelector: ['rag', 'node-rag', 'chunk'], - }) + // Rag selectors always have `isShowAPart === false`, so the full-path + // preview is not rendered and `getVarType` is not invoked. + expect(getVarType).not.toHaveBeenCalled() }) it('should use shortened display name for deep non-rag selectors', () => { diff --git a/web/app/components/base/prompt-editor/plugins/hitl-input-block/variable-block.tsx b/web/app/components/base/prompt-editor/plugins/hitl-input-block/variable-block.tsx index 44303c8faa..913d5039c1 100644 --- a/web/app/components/base/prompt-editor/plugins/hitl-input-block/variable-block.tsx +++ b/web/app/components/base/prompt-editor/plugins/hitl-input-block/variable-block.tsx @@ -1,6 +1,7 @@ import type { UpdateWorkflowNodesMapPayload } from '../workflow-variable-block' import type { WorkflowNodesMap } from '../workflow-variable-block/node' import type { ValueSelector, Var } from '@/app/components/workflow/types' +import { PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { mergeRegister } from '@lexical/utils' import { @@ -13,7 +14,6 @@ import { useState, } from 'react' import { useTranslation } from 'react-i18next' -import Tooltip from '@/app/components/base/tooltip' import { isConversationVar, isENV, @@ -119,13 +119,13 @@ const HITLInputVariableBlockComponent = ({ /> ) - if (!node) + if (!node || !isShowAPart) return Item return ( - + {Item}
} /> + - )} - disabled={!isShowAPart} - > -
{Item}
- +
+ ) } diff --git a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx index ecd5183a14..cb9178f36b 100644 --- a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx +++ b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx @@ -3,7 +3,7 @@ import type { } from './index' import type { WorkflowNodesMap } from './node' import type { NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types' -import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' +import { PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { mergeRegister } from '@lexical/utils' import { @@ -151,13 +151,13 @@ const WorkflowVariableBlockComponent = ({ /> ) - if (!node) + if (!node || !isShowAPart) return Item return ( - - {Item}
} /> - + + {Item}} /> + - - + + ) } diff --git a/web/app/components/billing/usage-info/__tests__/index.spec.tsx b/web/app/components/billing/usage-info/__tests__/index.spec.tsx index 073c2e7fe2..3322d35809 100644 --- a/web/app/components/billing/usage-info/__tests__/index.spec.tsx +++ b/web/app/components/billing/usage-info/__tests__/index.spec.tsx @@ -309,7 +309,7 @@ describe('UsageInfo', () => { />, ) - expect(container.querySelector('[data-state]')).toBeInTheDocument() + expect(container.querySelectorAll('.cursor-default').length).toBeGreaterThan(0) }) }) }) diff --git a/web/app/components/billing/usage-info/__tests__/vector-space-info.spec.tsx b/web/app/components/billing/usage-info/__tests__/vector-space-info.spec.tsx index 43e132c00f..3422d09c2f 100644 --- a/web/app/components/billing/usage-info/__tests__/vector-space-info.spec.tsx +++ b/web/app/components/billing/usage-info/__tests__/vector-space-info.spec.tsx @@ -3,7 +3,8 @@ import { defaultPlan } from '../../config' import { Plan } from '../../type' import VectorSpaceInfo from '../vector-space-info' -const queryPlaceholder = () => document.body.querySelector('[aria-hidden="true"]') +const queryPlaceholder = () => + document.body.querySelector('[aria-hidden="true"].bg-components-progress-bar-bg') // Mock provider context with configurable plan let mockPlanType = Plan.sandbox diff --git a/web/app/components/billing/usage-info/index.tsx b/web/app/components/billing/usage-info/index.tsx index 68e889f320..ee1929863f 100644 --- a/web/app/components/billing/usage-info/index.tsx +++ b/web/app/components/billing/usage-info/index.tsx @@ -3,9 +3,10 @@ import type { MeterTone } from '@langgenius/dify-ui/meter' import type { ComponentType, FC, ReactNode } from 'react' import { cn } from '@langgenius/dify-ui/cn' import { MeterIndicator, MeterRoot, MeterTrack } from '@langgenius/dify-ui/meter' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import * as React from 'react' import { useTranslation } from 'react-i18next' -import Tooltip from '@/app/components/base/tooltip' +import { Infotip } from '@/app/components/base/infotip' import { NUM_INFINITE } from '../config' type Props = { @@ -159,11 +160,11 @@ const UsageInfo: FC = ({ const wrapWithStorageTooltip = (children: ReactNode) => { if (storageMode && storageTooltip) { return ( - {storageTooltip}} - asChild={false} - > -
{children}
+ + {children}} /> + + {storageTooltip} + ) } @@ -178,13 +179,9 @@ const UsageInfo: FC = ({
{name}
{tooltip && ( - - {tooltip} -
- )} - /> + + {tooltip} + )}
diff --git a/web/app/components/header/account-setting/members-page/transfer-ownership-modal/member-selector.tsx b/web/app/components/header/account-setting/members-page/transfer-ownership-modal/member-selector.tsx index 5875b4fb6a..4dc41a307b 100644 --- a/web/app/components/header/account-setting/members-page/transfer-ownership-modal/member-selector.tsx +++ b/web/app/components/header/account-setting/members-page/transfer-ownership-modal/member-selector.tsx @@ -2,16 +2,20 @@ import type { FC } from 'react' import { Avatar } from '@langgenius/dify-ui/avatar' import { cn } from '@langgenius/dify-ui/cn' +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@langgenius/dify-ui/popover' import * as React from 'react' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import Input from '@/app/components/base/input' -import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem' import { useMembers } from '@/service/use-common' type Props = { - value?: any - onSelect: (value: any) => void + value?: string + onSelect: (value: string) => void exclude?: string[] } @@ -27,12 +31,9 @@ const MemberSelector: FC = ({ const { data } = useMembers() const currentValue = useMemo(() => { - if (!data?.accounts) + if (!data?.accounts || !value) return null - const accounts = data.accounts || [] - if (!value) - return null - return accounts.find(account => account.id === value) + return data.accounts.find(account => account.id === value) ?? null }, [data, value]) const filteredList = useMemo(() => { @@ -47,37 +48,36 @@ const MemberSelector: FC = ({ return name.toLowerCase().includes(searchValue.toLowerCase()) || email.toLowerCase().includes(searchValue.toLowerCase()) }).filter(account => !exclude.includes(account.id)) - }, [data, searchValue, exclude]) + }, [data, exclude, searchValue]) return ( - - setOpen(v => !v)} + + + {!currentValue && ( +
{t('members.transferModal.transferPlaceholder', { ns: 'common' })}
+ )} + {currentValue && ( + <> + +
{currentValue.name}
+
{currentValue.email}
+ + )} +
+
+ )} + /> + -
- {!currentValue && ( -
{t('members.transferModal.transferPlaceholder', { ns: 'common' })}
- )} - {currentValue && ( - <> - -
{currentValue.name}
-
{currentValue.email}
- - )} -
-
- -
= ({ ))}
-
- + + ) } + export default MemberSelector diff --git a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/parameter-item.tsx b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/parameter-item.tsx index f7e1962fd7..733b299664 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/parameter-item.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/parameter-item.tsx @@ -7,9 +7,9 @@ import { cn } from '@langgenius/dify-ui/cn' import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger, SelectValue } from '@langgenius/dify-ui/select' import { Slider } from '@langgenius/dify-ui/slider' import { Switch } from '@langgenius/dify-ui/switch' -import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { Infotip } from '@/app/components/base/infotip' import PromptEditor from '@/app/components/base/prompt-editor' import Radio from '@/app/components/base/radio' import TagInput from '@/app/components/base/tag-input' @@ -349,18 +349,13 @@ function ParameterItem({
{ parameterRule.help && ( - - - - - )} - /> - -
{parameterRule.help[language] || parameterRule.help.en_US}
-
-
+ + {parameterRule.help[language] || parameterRule.help.en_US} + ) }
diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx index ed16cd7904..72c52a9429 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/popup-item.tsx @@ -10,7 +10,11 @@ import { PopoverContent, PopoverTrigger, } from '@langgenius/dify-ui/popover' -import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' +import { + PreviewCard, + PreviewCardContent, + PreviewCardTrigger, +} from '@langgenius/dify-ui/preview-card' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { CreditsCoin } from '@/app/components/base/icons/src/vender/line/financeAndECommerce' @@ -160,8 +164,13 @@ const PopupItem: FC = ({ {!collapsed && model.models.map(modelItem => ( - - + = ({ )} /> -
@@ -245,8 +253,8 @@ const PopupItem: FC = ({
)}
-
-
+ + ))} ) diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/popup.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/popup.tsx index ec07b6c114..47ddb55b6c 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-selector/popup.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-selector/popup.tsx @@ -225,7 +225,7 @@ const Popup: FC = ({ {showCreditsExhaustedAlert && ( )} -
+
{ filteredModelList.map(model => ( @@ -31,19 +32,9 @@ export default function UsagePrioritySection({ value, disabled, onSelect }: Usag {t('modelProvider.card.usagePriority', { ns: 'common' })} - - - - - )} - /> - - {t('modelProvider.card.usagePriorityTip', { ns: 'common' })} - - + + {usagePriorityTip} +
{options.map(option => ( diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.tsx index bbc2cfaa98..8de8167dc2 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/quota-panel.tsx @@ -9,6 +9,7 @@ import { useBoolean } from 'ahooks' import * as React from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { Infotip } from '@/app/components/base/infotip' import Loading from '@/app/components/base/loading' import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace' import useTimestamp from '@/hooks/use-timestamp' @@ -100,19 +101,9 @@ const QuotaPanel: FC = ({
{t('modelProvider.quota', { ns: 'common' })} - - - - - )} - /> - - {tipText} - - + + {tipText} +
diff --git a/web/app/components/header/account-setting/model-provider-page/system-model-selector/index.tsx b/web/app/components/header/account-setting/model-provider-page/system-model-selector/index.tsx index 9f993260ca..8ceaaaa32e 100644 --- a/web/app/components/header/account-setting/model-provider-page/system-model-selector/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/system-model-selector/index.tsx @@ -11,13 +11,9 @@ import { DialogTitle, } from '@langgenius/dify-ui/dialog' import { toast } from '@langgenius/dify-ui/toast' -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from '@langgenius/dify-ui/tooltip' import { useState } from 'react' import { useTranslation } from 'react-i18next' +import { Infotip } from '@/app/components/base/infotip' import { useAppContext } from '@/context/app-context' import { useProviderContext } from '@/context/provider-context' import { updateDefaultModel } from '@/service/common' @@ -138,21 +134,13 @@ const SystemModel: FC = ({ return (
{t(labelKey, { ns: 'common' })} - - - - - )} - /> - -
- {tipText} -
-
-
+ + {tipText} +
) } diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/selector-entry.spec.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/selector-entry.spec.tsx index 37d828591f..1eb02fb15a 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/selector-entry.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/selector-entry.spec.tsx @@ -4,6 +4,59 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types' import { SubscriptionSelectorEntry } from '../selector-entry' +vi.mock('@langgenius/dify-ui/popover', async () => { + const React = await import('react') + const PopoverContext = React.createContext({ + open: false, + setOpen: (_open: boolean) => {}, + }) + + const Popover = ({ + children, + open: controlledOpen, + onOpenChange, + }: { + children: React.ReactNode + open?: boolean + onOpenChange?: (open: boolean) => void + }) => { + const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false) + const isControlled = controlledOpen !== undefined + const open = isControlled ? !!controlledOpen : uncontrolledOpen + const setOpen = (nextOpen: boolean) => { + if (!isControlled) + setUncontrolledOpen(nextOpen) + onOpenChange?.(nextOpen) + } + + return ( + + {children} + + ) + } + + const PopoverTrigger = ({ render }: { render: React.ReactNode }) => { + const { open, setOpen } = React.useContext(PopoverContext) + return ( +
setOpen(!open)}> + {render} +
+ ) + } + + const PopoverContent = ({ children }: { children: React.ReactNode }) => { + const { open } = React.useContext(PopoverContext) + return open ?
{children}
: null + } + + return { + Popover, + PopoverTrigger, + PopoverContent, + } +}) + let mockSubscriptions: TriggerSubscription[] = [] const mockRefetch = vi.fn() @@ -92,6 +145,6 @@ describe('SubscriptionSelectorEntry', () => { fireEvent.click(screen.getByRole('button', { name: 'Subscription One' })) expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({ id: 'sub-1', name: 'Subscription One' }), expect.any(Function)) - expect(screen.queryByText('Subscription One')).not.toBeInTheDocument() + expect(screen.queryByTestId('popover-content')).not.toBeInTheDocument() }) }) diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/selector-entry.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/selector-entry.tsx index 5f755ff634..21f1d4898b 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/selector-entry.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/selector-entry.tsx @@ -1,28 +1,26 @@ 'use client' import type { SimpleSubscription } from './types' import { cn } from '@langgenius/dify-ui/cn' +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@langgenius/dify-ui/popover' import { RiArrowDownSLine, RiWebhookLine } from '@remixicon/react' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { - PortalToFollowElem, - PortalToFollowElemContent, - PortalToFollowElemTrigger, -} from '@/app/components/base/portal-to-follow-elem' import { SubscriptionList } from '@/app/components/plugins/plugin-detail-panel/subscription-list' import { SubscriptionListMode } from './types' import { useSubscriptionList } from './use-subscription-list' type SubscriptionTriggerButtonProps = { selectedId?: string - onClick?: () => void isOpen?: boolean className?: string } const SubscriptionTriggerButton: React.FC = ({ selectedId, - onClick, isOpen = false, className, }) => { @@ -44,7 +42,7 @@ const SubscriptionTriggerButton: React.FC = ({ } if (subscriptions && subscriptions.length > 0) { - const selectedSubscription = subscriptions?.find(sub => sub.id === selectedId) + const selectedSubscription = subscriptions.find(sub => sub.id === selectedId) if (!selectedSubscription) { return { @@ -67,13 +65,13 @@ const SubscriptionTriggerButton: React.FC = ({ return (
+ )} + /> +
- - +
+ ) } diff --git a/web/app/components/plugins/plugin-page/plugin-tasks/__tests__/index.spec.tsx b/web/app/components/plugins/plugin-page/plugin-tasks/__tests__/index.spec.tsx index c87673b750..12fc796531 100644 --- a/web/app/components/plugins/plugin-page/plugin-tasks/__tests__/index.spec.tsx +++ b/web/app/components/plugins/plugin-page/plugin-tasks/__tests__/index.spec.tsx @@ -61,6 +61,9 @@ const setupMocks = (plugins: PluginStatus[] = []) => { return { mockMutateAsync, mockHandleRefetch } } +const getTaskMenuTrigger = () => + document.getElementById('plugin-task-trigger')!.closest('[role="button"]') as HTMLElement + describe('usePluginTaskStatus Hook', () => { beforeEach(() => { vi.clearAllMocks() @@ -637,7 +640,7 @@ describe('PluginTasks Component', () => { render() // Click to open - fireEvent.click(document.getElementById('plugin-task-trigger')!) + fireEvent.click(getTaskMenuTrigger()) // The popover content should be visible (PluginTaskList) // The popover content should be visible (PluginTaskList) @@ -666,7 +669,7 @@ describe('PluginTasks Component', () => { render() // Open popover - fireEvent.click(document.getElementById('plugin-task-trigger')!) + fireEvent.click(getTaskMenuTrigger()) // Wait for popover content to render await waitFor(() => { @@ -692,7 +695,7 @@ describe('PluginTasks Component', () => { render() - fireEvent.click(document.getElementById('plugin-task-trigger')!) + fireEvent.click(getTaskMenuTrigger()) await waitFor(() => { expect(document.querySelector('.w-\\[360px\\]')).toBeInTheDocument() @@ -713,16 +716,14 @@ describe('PluginTasks Component', () => { render() // Open popover - fireEvent.click(document.getElementById('plugin-task-trigger')!) + fireEvent.click(getTaskMenuTrigger()) await waitFor(() => { expect(document.querySelector('.w-\\[360px\\]'))!.toBeInTheDocument() }) // Find and click the clear all button in error section - const clearButtons = screen.getAllByRole('button') - if (clearButtons.length > 0) - fireEvent.click(clearButtons[0]!) + fireEvent.click(screen.getByRole('button', { name: /task\.clearAll/i })) await waitFor(() => { expect(mockMutateAsync).toHaveBeenCalled() @@ -741,7 +742,7 @@ describe('PluginTasks Component', () => { render() // Open popover - fireEvent.click(document.getElementById('plugin-task-trigger')!) + fireEvent.click(getTaskMenuTrigger()) await waitFor(() => { expect(document.querySelector('.w-\\[360px\\]'))!.toBeInTheDocument() @@ -813,7 +814,7 @@ describe('PluginTasks Component', () => { render() // Open popover - fireEvent.click(document.getElementById('plugin-task-trigger')!) + fireEvent.click(getTaskMenuTrigger()) expect(document.querySelector('.w-\\[360px\\]'))!.toBeInTheDocument() }) @@ -825,7 +826,7 @@ describe('PluginTasks Component', () => { ]) render() - fireEvent.click(document.getElementById('plugin-task-trigger')!) + fireEvent.click(getTaskMenuTrigger()) expect(document.querySelector('.w-\\[360px\\]')).toBeInTheDocument() }) @@ -837,7 +838,7 @@ describe('PluginTasks Component', () => { ]) render() - fireEvent.click(document.getElementById('plugin-task-trigger')!) + fireEvent.click(getTaskMenuTrigger()) expect(document.querySelector('.w-\\[360px\\]')).toBeInTheDocument() }) @@ -892,7 +893,7 @@ describe('PluginTasks Integration', () => { render() // Open popover - fireEvent.click(document.getElementById('plugin-task-trigger')!) + fireEvent.click(getTaskMenuTrigger()) // All sections should be visible const sections = document.querySelectorAll('.max-h-\\[300px\\]') diff --git a/web/app/components/plugins/plugin-page/plugin-tasks/index.tsx b/web/app/components/plugins/plugin-page/plugin-tasks/index.tsx index 7603cae33d..f3102c4909 100644 --- a/web/app/components/plugins/plugin-page/plugin-tasks/index.tsx +++ b/web/app/components/plugins/plugin-page/plugin-tasks/index.tsx @@ -97,6 +97,7 @@ const PluginTasks = () => { onOpenChange={setOpen} > } disabled={!canOpenMenu} > diff --git a/web/app/components/plugins/reference-setting-modal/auto-update-setting/__tests__/index.spec.tsx b/web/app/components/plugins/reference-setting-modal/auto-update-setting/__tests__/index.spec.tsx index 19ce12b328..ed3c457aaf 100644 --- a/web/app/components/plugins/reference-setting-modal/auto-update-setting/__tests__/index.spec.tsx +++ b/web/app/components/plugins/reference-setting-modal/auto-update-setting/__tests__/index.spec.tsx @@ -61,35 +61,78 @@ vi.mock('@/service/use-plugins', () => ({ }), })) -// Mock portal component for ToolPicker and StrategyPicker +// Mock popover component for ToolPicker and StrategyPicker let mockPortalOpen = false let forcePortalContentVisible = false // Allow tests to force content visibility -vi.mock('@/app/components/base/portal-to-follow-elem', () => ({ - PortalToFollowElem: ({ children, open, onOpenChange: _onOpenChange }: { +let mockPortalOnOpenChange: ((open: boolean) => void) | undefined +vi.mock('@langgenius/dify-ui/popover', () => ({ + Popover: ({ children, open = false, onOpenChange }: { children: React.ReactNode - open: boolean - onOpenChange: (open: boolean) => void + open?: boolean + onOpenChange?: (open: boolean) => void }) => { mockPortalOpen = open + mockPortalOnOpenChange = onOpenChange + return ( +
{children}
+ ) + }, + PopoverTrigger: ({ children, render, onClick, className }: { + children?: React.ReactNode + render?: React.ReactNode + onClick?: (e: React.MouseEvent) => void + className?: string + }) => ( +
{ + onClick?.(e) + if (!onClick) + mockPortalOnOpenChange?.(!mockPortalOpen) + }} + className={className} + > + {render ?? children} +
+ ), + PopoverContent: ({ children, className, popupClassName }: { + children: React.ReactNode + className?: string + popupClassName?: string + }) => { + if (!mockPortalOpen && !forcePortalContentVisible) + return null + return
{children}
+ }, +})) + +vi.mock('@/app/components/base/portal-to-follow-elem', () => ({ + PortalToFollowElem: ({ children, open = false, onOpenChange }: { + children: React.ReactNode + open?: boolean + onOpenChange?: (open: boolean) => void + }) => { + mockPortalOpen = open + mockPortalOnOpenChange = onOpenChange return
{children}
}, PortalToFollowElemTrigger: ({ children, onClick, className }: { - children: React.ReactNode - onClick: (e: React.MouseEvent) => void + children?: React.ReactNode + onClick?: (e: React.MouseEvent) => void className?: string }) => (
{children}
), - PortalToFollowElemContent: ({ children, className }: { + PortalToFollowElemContent: ({ children, className, popupClassName }: { children: React.ReactNode className?: string + popupClassName?: string }) => { - // Allow forcing content visibility for testing option selection if (!mockPortalOpen && !forcePortalContentVisible) return null - return
{children}
+ return
{children}
}, })) @@ -319,6 +362,7 @@ describe('auto-update-setting', () => { beforeEach(() => { vi.clearAllMocks() mockPortalOpen = false + mockPortalOnOpenChange = undefined forcePortalContentVisible = false mockPluginsData.plugins = [] }) diff --git a/web/app/components/plugins/reference-setting-modal/auto-update-setting/__tests__/tool-picker.spec.tsx b/web/app/components/plugins/reference-setting-modal/auto-update-setting/__tests__/tool-picker.spec.tsx index e89b1b3161..176598158c 100644 --- a/web/app/components/plugins/reference-setting-modal/auto-update-setting/__tests__/tool-picker.spec.tsx +++ b/web/app/components/plugins/reference-setting-modal/auto-update-setting/__tests__/tool-picker.spec.tsx @@ -4,8 +4,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { PluginSource } from '@/app/components/plugins/types' import ToolPicker from '../tool-picker' -let portalOpen = false - const mockInstalledPluginList = vi.hoisted(() => ({ data: { plugins: [] as PluginDetail[], @@ -21,33 +19,51 @@ vi.mock('@/app/components/base/loading', () => ({ default: () =>
loading
, })) -vi.mock('@/app/components/base/portal-to-follow-elem', async () => { - const _React = await import('react') +vi.mock('@langgenius/dify-ui/popover', async () => { + const React = await import('react') + const PopoverContext = React.createContext({ + open: false, + setOpen: (_open: boolean) => {}, + }) + + const Popover = ({ + children, + open, + onOpenChange, + }: { + children: React.ReactNode + open?: boolean + onOpenChange?: (open: boolean) => void + }) => ( + onOpenChange?.(nextOpen) }}> + {children} + + ) + + const PopoverTrigger = ({ render }: { render: React.ReactNode }) => { + const { open, setOpen } = React.useContext(PopoverContext) + return ( +
setOpen(!open)}> + {render} +
+ ) + } + + const PopoverContent = ({ + children, + className, + }: { + children: React.ReactNode + className?: string + }) => { + const { open } = React.useContext(PopoverContext) + return open ?
{children}
: null + } + return { - PortalToFollowElem: ({ - open, - children, - }: { - open: boolean - children: React.ReactNode - }) => { - portalOpen = open - return
{children}
- }, - PortalToFollowElemTrigger: ({ - children, - onClick, - }: { - children: React.ReactNode - onClick: () => void - }) => , - PortalToFollowElemContent: ({ - children, - className, - }: { - children: React.ReactNode - className?: string - }) => portalOpen ?
{children}
: null, + Popover, + PopoverTrigger, + PopoverContent, } }) @@ -118,7 +134,6 @@ const createPlugin = ( describe('ToolPicker', () => { beforeEach(() => { vi.clearAllMocks() - portalOpen = false mockInstalledPluginList.data = { plugins: [], } @@ -137,7 +152,7 @@ describe('ToolPicker', () => { />, ) - fireEvent.click(screen.getByTestId('trigger')) + fireEvent.click(screen.getByText('trigger')) expect(onShowChange).toHaveBeenCalledWith(true) }) diff --git a/web/app/components/plugins/reference-setting-modal/auto-update-setting/tool-picker.tsx b/web/app/components/plugins/reference-setting-modal/auto-update-setting/tool-picker.tsx index 975dd2217e..1c499104c2 100644 --- a/web/app/components/plugins/reference-setting-modal/auto-update-setting/tool-picker.tsx +++ b/web/app/components/plugins/reference-setting-modal/auto-update-setting/tool-picker.tsx @@ -2,15 +2,15 @@ import type { FC } from 'react' import type { ActivePluginType } from '../../marketplace/constants' import { cn } from '@langgenius/dify-ui/cn' +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@langgenius/dify-ui/popover' import * as React from 'react' -import { useCallback, useMemo, useState } from 'react' +import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import Loading from '@/app/components/base/loading' -import { - PortalToFollowElem, - PortalToFollowElemContent, - PortalToFollowElemTrigger, -} from '@/app/components/base/portal-to-follow-elem' import SearchBox from '@/app/components/plugins/marketplace/search-box' import { useInstalledPluginList } from '@/service/use-plugins' import { PLUGIN_TYPE_SEARCH_MAP } from '../../marketplace/constants' @@ -24,7 +24,6 @@ type Props = { onChange: (value: string[]) => void isShow: boolean onShowChange: (isShow: boolean) => void - } const ToolPicker: FC = ({ @@ -35,43 +34,16 @@ const ToolPicker: FC = ({ onShowChange, }) => { const { t } = useTranslation() - const toggleShowPopup = useCallback(() => { - onShowChange(!isShow) - }, [onShowChange, isShow]) const tabs = [ - { - key: PLUGIN_TYPE_SEARCH_MAP.all, - name: t('category.all', { ns: 'plugin' }), - }, - { - key: PLUGIN_TYPE_SEARCH_MAP.model, - name: t('category.models', { ns: 'plugin' }), - }, - { - key: PLUGIN_TYPE_SEARCH_MAP.tool, - name: t('category.tools', { ns: 'plugin' }), - }, - { - key: PLUGIN_TYPE_SEARCH_MAP.agent, - name: t('category.agents', { ns: 'plugin' }), - }, - { - key: PLUGIN_TYPE_SEARCH_MAP.extension, - name: t('category.extensions', { ns: 'plugin' }), - }, - { - key: PLUGIN_TYPE_SEARCH_MAP.datasource, - name: t('category.datasources', { ns: 'plugin' }), - }, - { - key: PLUGIN_TYPE_SEARCH_MAP.trigger, - name: t('category.triggers', { ns: 'plugin' }), - }, - { - key: PLUGIN_TYPE_SEARCH_MAP.bundle, - name: t('category.bundles', { ns: 'plugin' }), - }, + { key: PLUGIN_TYPE_SEARCH_MAP.all, name: t('category.all', { ns: 'plugin' }) }, + { key: PLUGIN_TYPE_SEARCH_MAP.model, name: t('category.models', { ns: 'plugin' }) }, + { key: PLUGIN_TYPE_SEARCH_MAP.tool, name: t('category.tools', { ns: 'plugin' }) }, + { key: PLUGIN_TYPE_SEARCH_MAP.agent, name: t('category.agents', { ns: 'plugin' }) }, + { key: PLUGIN_TYPE_SEARCH_MAP.extension, name: t('category.extensions', { ns: 'plugin' }) }, + { key: PLUGIN_TYPE_SEARCH_MAP.datasource, name: t('category.datasources', { ns: 'plugin' }) }, + { key: PLUGIN_TYPE_SEARCH_MAP.trigger, name: t('category.triggers', { ns: 'plugin' }) }, + { key: PLUGIN_TYPE_SEARCH_MAP.bundle, name: t('category.bundles', { ns: 'plugin' }) }, ] const [pluginType, setPluginType] = useState(PLUGIN_TYPE_SEARCH_MAP.all) @@ -89,14 +61,13 @@ const ToolPicker: FC = ({ ) }) }, [data, pluginType, query, tags]) - const handleCheckChange = useCallback((pluginId: string) => { - return () => { - const newValue = value.includes(pluginId) - ? value.filter(id => id !== pluginId) - : [...value, pluginId] - onChange(newValue) - } - }, [onChange, value]) + + const handleCheckChange = (pluginId: string) => { + const newValue = value.includes(pluginId) + ? value.filter(id => id !== pluginId) + : [...value, pluginId] + onChange(newValue) + } const listContent = (
@@ -105,7 +76,7 @@ const ToolPicker: FC = ({ key={item.plugin_id} payload={item} isChecked={value.includes(item.plugin_id)} - onCheckChange={handleCheckChange(item.plugin_id)} + onCheckChange={() => handleCheckChange(item.plugin_id)} /> ))}
@@ -121,21 +92,18 @@ const ToolPicker: FC = ({ ) + const resolvedTrigger = React.isValidElement(trigger) ? trigger :
{trigger}
+ return ( - - + + - {trigger} - - -
+
= ({
- { - tabs.map(tab => ( -
setPluginType(tab.key)} - > - {tab.name} -
- )) - } + {tabs.map(tab => ( +
setPluginType(tab.key)} + > + {tab.name} +
+ ))}
{!isLoading && filteredList.length > 0 && listContent} {!isLoading && filteredList.length === 0 && noData} {isLoading && loadingContent}
- - + + ) } diff --git a/web/app/components/tools/labels/__tests__/selector.spec.tsx b/web/app/components/tools/labels/__tests__/selector.spec.tsx index b495d2d227..4ef4534759 100644 --- a/web/app/components/tools/labels/__tests__/selector.spec.tsx +++ b/web/app/components/tools/labels/__tests__/selector.spec.tsx @@ -2,6 +2,65 @@ import { act, fireEvent, render, screen } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import LabelSelector from '../selector' +vi.mock('@langgenius/dify-ui/popover', async () => { + const React = await import('react') + const PopoverContext = React.createContext({ + open: false, + setOpen: (_open: boolean) => {}, + }) + + const Popover = ({ + children, + open: controlledOpen, + onOpenChange, + }: { + children: React.ReactNode + open?: boolean + onOpenChange?: (open: boolean) => void + }) => { + const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false) + const isControlled = controlledOpen !== undefined + const open = isControlled ? !!controlledOpen : uncontrolledOpen + const setOpen = (nextOpen: boolean) => { + if (!isControlled) + setUncontrolledOpen(nextOpen) + onOpenChange?.(nextOpen) + } + + return ( + + {children} + + ) + } + + const PopoverTrigger = ({ render }: { render: React.ReactNode }) => { + const { open, setOpen } = React.useContext(PopoverContext) + return ( +
setOpen(!open)}> + {render} +
+ ) + } + + const PopoverContent = ({ + children, + ...props + }: React.HTMLAttributes & { children?: React.ReactNode }) => { + const { open } = React.useContext(PopoverContext) + if (!open) + return null + + return
{children}
+ } + + return { + Popover, + PopoverTrigger, + PopoverContent, + } +}) + // Mock useTags hook with controlled test data const mockTags = [ { name: 'agent', label: 'Agent' }, diff --git a/web/app/components/tools/labels/selector.tsx b/web/app/components/tools/labels/selector.tsx index 830b4d7aaf..67539715a7 100644 --- a/web/app/components/tools/labels/selector.tsx +++ b/web/app/components/tools/labels/selector.tsx @@ -1,6 +1,11 @@ import type { FC } from 'react' import type { Label } from '@/app/components/tools/labels/constant' import { cn } from '@langgenius/dify-ui/cn' +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@langgenius/dify-ui/popover' import { RiArrowDownSLine } from '@remixicon/react' import { useDebounceFn } from 'ahooks' import { noop } from 'es-toolkit/function' @@ -9,17 +14,13 @@ import { useTranslation } from 'react-i18next' import Checkbox from '@/app/components/base/checkbox' import { Tag03 } from '@/app/components/base/icons/src/vender/line/financeAndECommerce' import Input from '@/app/components/base/input' -import { - PortalToFollowElem, - PortalToFollowElemContent, - PortalToFollowElemTrigger, -} from '@/app/components/base/portal-to-follow-elem' import { useTags } from '@/app/components/plugins/hooks' type LabelSelectorProps = { value: string[] onChange: (v: string[]) => void } + const LabelSelector: FC = ({ value, onChange, @@ -34,6 +35,7 @@ const LabelSelector: FC = ({ const { run: handleSearch } = useDebounceFn(() => { setSearchKeywords(keywords) }, { wait: 500 }) + const handleKeywordsChange = (value: string) => { setKeywords(value) handleSearch() @@ -55,32 +57,31 @@ const LabelSelector: FC = ({ } return ( - +
- setOpen(v => !v)} - className="block" - > -
+
0 ? selectedLabels : ''} className={cn('grow truncate text-[13px] leading-[18px] text-text-secondary', !value.length && 'text-text-quaternary!')}> + {!value.length && t('createTool.toolInput.labelPlaceholder', { ns: 'tools' })} + {!!value.length && selectedLabels} +
+
+ +
+
)} - > -
0 ? selectedLabels : ''} className={cn('grow truncate text-[13px] leading-[18px] text-text-secondary', !value.length && 'text-text-quaternary!')}> - {!value.length && t('createTool.toolInput.labelPlaceholder', { ns: 'tools' })} - {!!value.length && selectedLabels} -
-
- -
-
- - + /> +
= ({ )}
-
+
-
+ ) } diff --git a/web/app/components/workflow/__tests__/custom-edge.spec.tsx b/web/app/components/workflow/__tests__/custom-edge.spec.tsx index f8ff9a1a0e..5c98402d9e 100644 --- a/web/app/components/workflow/__tests__/custom-edge.spec.tsx +++ b/web/app/components/workflow/__tests__/custom-edge.spec.tsx @@ -231,5 +231,9 @@ describe('CustomEdge', () => { expect(screen.getByTestId('base-edge')).toHaveAttribute('data-stroke', 'var(--color-workflow-link-line-normal)') expect(screen.getByTestId('block-selector')).toHaveAttribute('data-trigger-class', 'hover:scale-150 transition-all') + expect(screen.getByTestId('block-selector').parentElement).toHaveStyle({ + opacity: '0', + pointerEvents: 'none', + }) }) }) diff --git a/web/app/components/workflow/block-selector/__tests__/tool-picker.spec.tsx b/web/app/components/workflow/block-selector/__tests__/tool-picker.spec.tsx index 9c55d174fd..1ec4ed241f 100644 --- a/web/app/components/workflow/block-selector/__tests__/tool-picker.spec.tsx +++ b/web/app/components/workflow/block-selector/__tests__/tool-picker.spec.tsx @@ -455,12 +455,12 @@ describe('ToolPicker', () => { it('should create a custom collection from the add button and refresh custom tools', async () => { const user = userEvent.setup() - const { container } = renderToolPicker({ + renderToolPicker({ isShow: true, supportAddCustomTool: true, }) - const addCustomToolButton = Array.from(container.querySelectorAll('button')).find((button) => { + const addCustomToolButton = Array.from(document.querySelectorAll('button')).find((button) => { return button.className.includes('bg-components-button-primary-bg') }) diff --git a/web/app/components/workflow/block-selector/blocks.tsx b/web/app/components/workflow/block-selector/blocks.tsx index 70497ec69d..ecd6142975 100644 --- a/web/app/components/workflow/block-selector/blocks.tsx +++ b/web/app/components/workflow/block-selector/blocks.tsx @@ -1,5 +1,10 @@ import type { NodeDefault } from '../types' import type { BlockClassificationEnum } from './types' +import { + PreviewCard, + PreviewCardContent, + PreviewCardTrigger, +} from '@langgenius/dify-ui/preview-card' import { groupBy } from 'es-toolkit/compat' import { memo, @@ -10,7 +15,6 @@ import { useTranslation } from 'react-i18next' import { useStoreApi } from 'reactflow' import { useStore as useAppStore } from '@/app/components/app/store' import Badge from '@/app/components/base/badge' -import Tooltip from '@/app/components/base/tooltip' import { filterEvaluationWorkflowRestrictedBlockTypes, isEvaluationWorkflow } from '@/app/components/workflow/utils/evaluation-workflow' import BlockIcon from '../block-icon' import { BlockEnum } from '../types' @@ -101,13 +105,40 @@ const Blocks = ({ ) } { + // Preview is supplementary: icon/title/description are all reachable + // from the node that gets added on click (inspector + canvas), so + // hover/focus-only activation is a11y-safe. See + // packages/dify-ui/AGENTS.md → Overlay Primitive Selection. filteredList.map(block => ( - + onSelect(block.metaData.type)} + > + +
{block.metaData.title}
+ { + block.metaData.type === BlockEnum.LoopEnd && ( + + ) + } +
+ )} + /> +
{block.metaData.title}
{block.metaData.description}
- )} - > -
onSelect(block.metaData.type)} - > - -
{block.metaData.title}
- { - block.metaData.type === BlockEnum.LoopEnd && ( - - ) - } -
- + + )) }
diff --git a/web/app/components/workflow/block-selector/featured-tools.tsx b/web/app/components/workflow/block-selector/featured-tools.tsx index 68c6d63a52..0cdeebcb79 100644 --- a/web/app/components/workflow/block-selector/featured-tools.tsx +++ b/web/app/components/workflow/block-selector/featured-tools.tsx @@ -3,12 +3,12 @@ import type { ToolWithProvider } from '../types' import type { ToolDefaultValue, ToolValue } from './types' import type { Plugin } from '@/app/components/plugins/types' import type { Locale } from '@/i18n-config' +import { PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card' import { RiMoreLine } from '@remixicon/react' import { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { ArrowDownDoubleLine, ArrowDownRoundFill, ArrowUpDoubleLine } from '@/app/components/base/icons/src/vender/solid/arrows' import Loading from '@/app/components/base/loading' -import Tooltip from '@/app/components/base/tooltip' import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace' import Action from '@/app/components/workflow/block-selector/market-place-plugin/action' import { useGetLanguage } from '@/context/i18n' @@ -235,7 +235,6 @@ function FeaturedToolUninstalledItem({ const description = typeof plugin.brief === 'object' ? plugin.brief[language] : plugin.brief const installCountLabel = t('install', { ns: 'plugin', num: formatNumber(plugin.install_count || 0) }) const [actionOpen, setActionOpen] = useState(false) - const [isActionHovered, setIsActionHovered] = useState(false) const [isInstallModalOpen, setIsInstallModalOpen] = useState(false) useEffect(() => { @@ -244,7 +243,6 @@ function FeaturedToolUninstalledItem({ const handleScroll = () => { setActionOpen(false) - setIsActionHovered(false) } window.addEventListener('scroll', handleScroll, true) @@ -254,77 +252,72 @@ function FeaturedToolUninstalledItem({ } }, [actionOpen]) + const row = ( +
+
+ +
+
{label}
+
+
+
+ {installCountLabel} +
+ + +
+
+
+ ) + return ( <> - - -
{label}
-
{description}
-
- )} - disabled={!description || isActionHovered || actionOpen || isInstallModalOpen} - > -
-
- -
-
{label}
-
-
-
- {installCountLabel} -
setIsActionHovered(true)} - onMouseLeave={() => { - if (!actionOpen) - setIsActionHovered(false) - }} - > - - { - setActionOpen(value) - setIsActionHovered(value) - }} - author={plugin.org} - name={plugin.name} - version={plugin.latest_version} - /> -
-
-
-
+ {description + ? ( + // Preview is supplementary: icon / label / brief are all reachable from + // the InstallFromMarketplace modal that opens on click, so hover/focus-only + // activation is a11y-safe. See packages/dify-ui/AGENTS.md → Overlay Primitive Selection. + + + +
+ +
{label}
+
{description}
+
+
+
+ ) + : row} {isInstallModalOpen && ( { setIsInstallModalOpen(false) - setIsActionHovered(false) await onInstallSuccess?.() }} onClose={() => { setIsInstallModalOpen(false) - setIsActionHovered(false) }} /> )} diff --git a/web/app/components/workflow/block-selector/featured-triggers.tsx b/web/app/components/workflow/block-selector/featured-triggers.tsx index c7c83410a6..3d3cdee2b7 100644 --- a/web/app/components/workflow/block-selector/featured-triggers.tsx +++ b/web/app/components/workflow/block-selector/featured-triggers.tsx @@ -2,12 +2,12 @@ import type { TriggerDefaultValue, TriggerWithProvider } from './types' import type { Plugin } from '@/app/components/plugins/types' import type { Locale } from '@/i18n-config' +import { PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card' import { RiMoreLine } from '@remixicon/react' import { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { ArrowDownDoubleLine, ArrowDownRoundFill, ArrowUpDoubleLine } from '@/app/components/base/icons/src/vender/solid/arrows' import Loading from '@/app/components/base/loading' -import Tooltip from '@/app/components/base/tooltip' import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace' import Action from '@/app/components/workflow/block-selector/market-place-plugin/action' import { useGetLanguage } from '@/context/i18n' @@ -230,7 +230,6 @@ function FeaturedTriggerUninstalledItem({ const description = typeof plugin.brief === 'object' ? plugin.brief[language] : plugin.brief const installCountLabel = t('install', { ns: 'plugin', num: formatNumber(plugin.install_count || 0) }) const [actionOpen, setActionOpen] = useState(false) - const [isActionHovered, setIsActionHovered] = useState(false) const [isInstallModalOpen, setIsInstallModalOpen] = useState(false) useEffect(() => { @@ -239,7 +238,6 @@ function FeaturedTriggerUninstalledItem({ const handleScroll = () => { setActionOpen(false) - setIsActionHovered(false) } window.addEventListener('scroll', handleScroll, true) @@ -249,77 +247,72 @@ function FeaturedTriggerUninstalledItem({ } }, [actionOpen]) + const row = ( +
+
+ +
+
{label}
+
+
+
+ {installCountLabel} +
+ + +
+
+
+ ) + return ( <> - - -
{label}
-
{description}
- - )} - disabled={!description || isActionHovered || actionOpen || isInstallModalOpen} - > -
-
- -
-
{label}
-
-
-
- {installCountLabel} -
setIsActionHovered(true)} - onMouseLeave={() => { - if (!actionOpen) - setIsActionHovered(false) - }} - > - - { - setActionOpen(value) - setIsActionHovered(value) - }} - author={plugin.org} - name={plugin.name} - version={plugin.latest_version} - /> -
-
-
-
+ {description + ? ( + // Preview is supplementary: icon / label / brief are all reachable from + // the InstallFromMarketplace modal that opens on click, so hover/focus-only + // activation is a11y-safe. See packages/dify-ui/AGENTS.md → Overlay Primitive Selection. + + + +
+ +
{label}
+
{description}
+
+
+
+ ) + : row} {isInstallModalOpen && ( { setIsInstallModalOpen(false) - setIsActionHovered(false) await onInstallSuccess?.() }} onClose={() => { setIsInstallModalOpen(false) - setIsActionHovered(false) }} /> )} diff --git a/web/app/components/workflow/block-selector/start-blocks.tsx b/web/app/components/workflow/block-selector/start-blocks.tsx index ef332844d5..efc1e5c1b9 100644 --- a/web/app/components/workflow/block-selector/start-blocks.tsx +++ b/web/app/components/workflow/block-selector/start-blocks.tsx @@ -1,5 +1,10 @@ import type { BlockEnum, CommonNodeType } from '../types' import type { TriggerDefaultValue } from './types' +import { + PreviewCard, + PreviewCardContent, + PreviewCardTrigger, +} from '@langgenius/dify-ui/preview-card' import { memo, useCallback, @@ -7,9 +12,7 @@ import { useMemo, } from 'react' import { useTranslation } from 'react-i18next' -import Tooltip from '@/app/components/base/tooltip' import useNodes from '@/app/components/workflow/store/workflow/use-nodes' -import { useAvailableNodesMetaData } from '../../workflow-app/hooks' import BlockIcon from '../block-icon' import { BlockEnum as BlockEnumValues } from '../types' // import { useNodeMetaData } from '../hooks' @@ -33,7 +36,6 @@ const StartBlocks = ({ const { t } = useTranslation() const nodes = useNodes() // const nodeMetaData = useNodeMetaData() - const availableNodesMetaData = useAvailableNodesMetaData() const filteredBlocks = useMemo(() => { // Check if Start node already exists in workflow @@ -67,13 +69,34 @@ const StartBlocks = ({ onContentStateChange?.(!isEmpty) }, [isEmpty, onContentStateChange]) + // Preview is supplementary: the block icon, title and description all become + // reachable from the inspector + canvas once the row is clicked to insert + // the start node, so hover/focus-only activation is a11y-safe. See + // packages/dify-ui/AGENTS.md → Overlay Primitive Selection. const renderBlock = useCallback((block: typeof START_BLOCKS[number]) => ( - + onSelect(block.type)} + > + +
+ {t(`blocks.${block.type}`, { ns: 'workflow' })} + {block.type === BlockEnumValues.Start && ( + {t('blocks.originalStartNode', { ns: 'workflow' })} + )} +
+ + )} + /> +
)}
- )} - > -
onSelect(block.type)} - > - -
- {t(`blocks.${block.type}`, { ns: 'workflow' })} - {block.type === BlockEnumValues.Start && ( - {t('blocks.originalStartNode', { ns: 'workflow' })} - )} -
-
-
- ), [availableNodesMetaData, onSelect, t]) + + + ), [onSelect, t]) if (isEmpty) return null diff --git a/web/app/components/workflow/block-selector/tool-picker.tsx b/web/app/components/workflow/block-selector/tool-picker.tsx index e7e948a54d..0ee25a9ce6 100644 --- a/web/app/components/workflow/block-selector/tool-picker.tsx +++ b/web/app/components/workflow/block-selector/tool-picker.tsx @@ -8,17 +8,17 @@ import type { ToolDefaultValue, ToolValue } from './types' import type { CustomCollectionBackend } from '@/app/components/tools/types' import type { BlockEnum, OnSelectBlock } from '@/app/components/workflow/types' import { cn } from '@langgenius/dify-ui/cn' +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@langgenius/dify-ui/popover' import { toast } from '@langgenius/dify-ui/toast' import { useSuspenseQuery } from '@tanstack/react-query' import { useBoolean } from 'ahooks' import * as React from 'react' -import { useMemo, useState } from 'react' +import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { - PortalToFollowElem, - PortalToFollowElemContent, - PortalToFollowElemTrigger, -} from '@/app/components/base/portal-to-follow-elem' import SearchBox from '@/app/components/plugins/marketplace/search-box' import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal' import AllTools from '@/app/components/workflow/block-selector/all-tools' @@ -43,7 +43,7 @@ type Props = { disabled: boolean trigger: React.ReactNode placement?: Placement - offset?: OffsetOptions + offset?: OffsetOptions | number isShow: boolean onShowChange: (isShow: boolean) => void onSelect: (tool: ToolDefaultValue) => void @@ -120,12 +120,6 @@ const ToolPicker: FC = ({ const handleAddedCustomTool = invalidateCustomTools - const handleTriggerClick = () => { - if (disabled) - return - onShowChange(true) - } - const handleSelect = (_type: BlockEnum, tool?: ToolDefaultValue) => { onSelect(tool!) } @@ -139,6 +133,11 @@ const ToolPicker: FC = ({ setTrue: showEditCustomCollectionModal, }] = useBoolean(false) + const handleShowAddCustomCollectionModal = useCallback(() => { + onShowChange(false) + showEditCustomCollectionModal() + }, [onShowChange, showEditCustomCollectionModal]) + const doCreateCustomToolCollection = async (data: CustomCollectionBackend) => { await createCustomCollection(data) toast.success(t('api.actionSuccess', { ns: 'common' })) @@ -157,20 +156,35 @@ const ToolPicker: FC = ({ ) } - return ( - - - {trigger} - + const resolvedTrigger = React.isValidElement(trigger) ? trigger :
{trigger}
+ const resolvedOffset = typeof offset === 'object' && offset !== null + ? offset as { mainAxis?: number, crossAxis?: number, alignmentAxis?: number | null } + : undefined + const sideOffset = typeof offset === 'number' ? offset : resolvedOffset?.mainAxis ?? 0 + const alignOffset = typeof offset === 'number' ? 0 : resolvedOffset?.crossAxis ?? resolvedOffset?.alignmentAxis ?? 0 - + return ( + { + if (disabled && nextOpen) + return + onShowChange(nextOpen) + }} + > + { + if (disabled) + e.preventDefault() + }} + /> +
= ({ placeholder={t('searchTools', { ns: 'plugin' })!} supportAddCustomTool={supportAddCustomTool} onAddedCustomTool={handleAddedCustomTool} - onShowAddCustomCollectionModal={showEditCustomCollectionModal} + onShowAddCustomCollectionModal={handleShowAddCustomCollectionModal} inputClassName="grow" />
@@ -209,8 +223,8 @@ const ToolPicker: FC = ({ }} />
-
-
+
+ ) } diff --git a/web/app/components/workflow/block-selector/tool/action-item.tsx b/web/app/components/workflow/block-selector/tool/action-item.tsx index 4afe1b1da3..343f8482df 100644 --- a/web/app/components/workflow/block-selector/tool/action-item.tsx +++ b/web/app/components/workflow/block-selector/tool/action-item.tsx @@ -4,11 +4,11 @@ import type { ToolWithProvider } from '../../types' import type { ToolDefaultValue } from '../types' import type { Tool } from '@/app/components/tools/types' import { cn } from '@langgenius/dify-ui/cn' +import { PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card' import * as React from 'react' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { trackEvent } from '@/app/components/base/amplitude' -import Tooltip from '@/app/components/base/tooltip' import { useGetLanguage } from '@/context/i18n' import useTheme from '@/hooks/use-theme' import { Theme } from '@/types/app' @@ -57,13 +57,59 @@ const ToolItem: FC = ({ return normalizedIcon }, [theme, normalizedIcon, normalizedIconDark]) - return ( - { + if (disabled) + return + const params: Record = {} + if (payload.parameters) { + payload.parameters.forEach((item) => { + params[item.name] = '' + }) + } + onSelect(BlockEnum.Tool, { + provider_id: provider.id, + provider_type: provider.type, + provider_name: provider.name, + plugin_id: provider.plugin_id, + plugin_unique_identifier: provider.plugin_unique_identifier, + provider_icon: normalizedIcon, + provider_icon_dark: normalizedIconDark, + tool_name: payload.name, + tool_label: payload.label[language]!, + tool_description: payload.description[language], + title: payload.label[language]!, + is_team_authorization: provider.is_team_authorization, + paramSchemas: payload.parameters, + params, + meta: provider.meta, + }) + trackEvent('tool_selected', { + tool_name: payload.name, + plugin_id: provider.plugin_id, + }) + }} + > +
+ {payload.label[language]} +
+ {isAdded && ( +
{t('addToolModal.added', { ns: 'tools' })}
+ )} + + ) + + return ( + // Preview is supplementary: provider icon, tool label and description are all + // reachable from the node inspector after the row is clicked to add the tool, + // so hover/focus-only activation is a11y-safe. See + // packages/dify-ui/AGENTS.md → Overlay Primitive Selection. + + +
= ({
{payload.label[language]}
{payload.description[language]}
- )} - > -
{ - if (disabled) - return - const params: Record = {} - if (payload.parameters) { - payload.parameters.forEach((item) => { - params[item.name] = '' - }) - } - onSelect(BlockEnum.Tool, { - provider_id: provider.id, - provider_type: provider.type, - provider_name: provider.name, - plugin_id: provider.plugin_id, - plugin_unique_identifier: provider.plugin_unique_identifier, - provider_icon: normalizedIcon, - provider_icon_dark: normalizedIconDark, - tool_name: payload.name, - tool_label: payload.label[language]!, - tool_description: payload.description[language], - title: payload.label[language]!, - is_team_authorization: provider.is_team_authorization, - paramSchemas: payload.parameters, - params, - meta: provider.meta, - }) - trackEvent('tool_selected', { - tool_name: payload.name, - plugin_id: provider.plugin_id, - }) - }} - > -
- {payload.label[language]} -
- {isAdded && ( -
{t('addToolModal.added', { ns: 'tools' })}
- )} -
-
+ + ) } export default React.memo(ToolItem) diff --git a/web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx b/web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx index 4ffb84e1b8..38c4c2b0f5 100644 --- a/web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx +++ b/web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx @@ -3,9 +3,9 @@ import type { FC } from 'react' import type { TriggerDefaultValue, TriggerWithProvider } from '../types' import type { Event } from '@/app/components/tools/types' import { cn } from '@langgenius/dify-ui/cn' +import { PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card' import * as React from 'react' import { useTranslation } from 'react-i18next' -import Tooltip from '@/app/components/base/tooltip' import { useGetLanguage } from '@/context/i18n' import BlockIcon from '../../block-icon' import { BlockEnum } from '../../types' @@ -28,13 +28,54 @@ const TriggerPluginActionItem: FC = ({ const { t } = useTranslation() const language = useGetLanguage() - return ( - { + if (disabled) + return + const params: Record = {} + if (payload.parameters) { + payload.parameters.forEach((item: any) => { + params[item.name] = '' + }) + } + onSelect(BlockEnum.TriggerPlugin, { + plugin_id: provider.plugin_id, + provider_id: provider.name, + provider_type: provider.type as string, + provider_name: provider.name, + event_name: payload.name, + event_label: payload.label[language]!, + event_description: payload.description[language]!, + plugin_unique_identifier: provider.plugin_unique_identifier, + title: payload.label[language]!, + is_team_authorization: provider.is_team_authorization, + output_schema: payload.output_schema || {}, + paramSchemas: payload.parameters, + params, + meta: provider.meta, + }) + }} + > +
+ {payload.label[language]} +
+ {isAdded && ( +
{t('addToolModal.added', { ns: 'tools' })}
+ )} + + ) + + return ( + // Preview is supplementary: provider icon, event label and description are all + // reachable from the node inspector after the row is clicked to add the trigger, + // so hover/focus-only activation is a11y-safe. See + // packages/dify-ui/AGENTS.md → Overlay Primitive Selection. + + +
= ({
{payload.label[language]}
{payload.description[language]}
- )} - > -
{ - if (disabled) - return - const params: Record = {} - if (payload.parameters) { - payload.parameters.forEach((item: any) => { - params[item.name] = '' - }) - } - onSelect(BlockEnum.TriggerPlugin, { - plugin_id: provider.plugin_id, - provider_id: provider.name, - provider_type: provider.type as string, - provider_name: provider.name, - event_name: payload.name, - event_label: payload.label[language]!, - event_description: payload.description[language]!, - plugin_unique_identifier: provider.plugin_unique_identifier, - title: payload.label[language]!, - is_team_authorization: provider.is_team_authorization, - output_schema: payload.output_schema || {}, - paramSchemas: payload.parameters, - params, - meta: provider.meta, - }) - }} - > -
- {payload.label[language]} -
- {isAdded && ( -
{t('addToolModal.added', { ns: 'tools' })}
- )} -
-
+ + ) } export default React.memo(TriggerPluginActionItem) diff --git a/web/app/components/workflow/custom-edge.tsx b/web/app/components/workflow/custom-edge.tsx index af0fb19e6b..d8c3b59a4a 100644 --- a/web/app/components/workflow/custom-edge.tsx +++ b/web/app/components/workflow/custom-edge.tsx @@ -63,6 +63,7 @@ const CustomEdge = ({ _sourceRunningStatus, _targetRunningStatus, } = data + const isTriggerVisible = !!(data?._hovering || isTriggerHovered || open) const linearGradientId = useMemo(() => { if ( @@ -144,16 +145,15 @@ const CustomEdge = ({
setIsTriggerHovered(true)} onMouseLeave={() => setIsTriggerHovered(false)} diff --git a/web/app/components/workflow/nodes/_base/components/__tests__/agent-strategy-selector.spec.tsx b/web/app/components/workflow/nodes/_base/components/__tests__/agent-strategy-selector.spec.tsx new file mode 100644 index 0000000000..995d3c8a70 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/__tests__/agent-strategy-selector.spec.tsx @@ -0,0 +1,446 @@ +import type { ReactNode } from 'react' +import type { StrategyPluginDetail } from '@/app/components/plugins/types' +import { render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { PluginCategoryEnum } from '@/app/components/plugins/types' +import { AgentStrategySelector } from '../agent-strategy-selector' + +const mocks = vi.hoisted(() => ({ + useSuspenseQuery: vi.fn(), + useStrategyProviders: vi.fn(), + useMarketplacePlugins: vi.fn(), + useStrategyInfo: vi.fn(), + refetchStrategyInfo: vi.fn(), + queryPluginsWithDebounced: vi.fn(), +})) + +vi.mock('@tanstack/react-query', () => ({ + useSuspenseQuery: mocks.useSuspenseQuery, +})) + +vi.mock('@/service/system-features', () => ({ + systemFeaturesQueryOptions: () => ({}), +})) + +vi.mock('@/service/use-strategy', () => ({ + useStrategyProviders: mocks.useStrategyProviders, +})) + +vi.mock('@/app/components/plugins/marketplace/hooks', () => ({ + useMarketplacePlugins: mocks.useMarketplacePlugins, +})) + +vi.mock('@/app/components/workflow/nodes/agent/use-config', () => ({ + useStrategyInfo: mocks.useStrategyInfo, +})) + +vi.mock('@/app/components/plugins/install-plugin/base/use-get-icon', () => ({ + default: () => ({ + getIconUrl: (icon: string) => `https://example.com/${icon}`, + }), +})) + +vi.mock('@/app/components/base/search-input', () => ({ + default: ({ + value, + onChange, + placeholder, + }: { + value: string + onChange: (value: string) => void + placeholder?: string + className?: string + }) => ( + onChange(e.target.value)} + /> + ), +})) + +vi.mock('@/app/components/workflow/block-selector/view-type-select', () => ({ + default: ({ + onChange, + }: { + viewType: string + onChange: (value: string) => void + }) => ( + + ), + ViewType: { + flat: 'flat', + grid: 'grid', + }, +})) + +vi.mock('@/app/components/workflow/block-selector/tools', () => ({ + default: ({ + tools, + onSelect, + }: { + tools: Array<{ + id: string + name: string + meta?: unknown + tools: Array<{ + name: string + label: string | { en_US?: string } + output_schema?: Record + }> + }> + onSelect: (value: unknown, tool: { + tool_name: string + provider_name: string + tool_label: string + output_schema?: Record + provider_id: string + meta?: unknown + }) => void + }) => ( +
+ {tools.map(tool => ( +
+ {tool.name} + +
+ ))} +
+ ), +})) + +vi.mock('@/app/components/workflow/block-selector/market-place-plugin/list', () => ({ + default: ({ + list, + searchText, + }: { + list: Array<{ plugin_id: string }> + searchText: string + }) => ( +
+ {`${searchText}:${list.map(item => item.plugin_id).join(',')}`} +
+ ), +})) + +vi.mock('@/app/components/workflow/nodes/_base/components/install-plugin-button', () => ({ + InstallPluginButton: ({ + onClick, + }: { + onClick?: (event: { stopPropagation: () => void }) => void + uniqueIdentifier: string + size: string + }) => ( + + ), +})) + +vi.mock('@/app/components/workflow/nodes/_base/components/switch-plugin-version', () => ({ + SwitchPluginVersion: ({ + onChange, + }: { + onChange: () => void + uniqueIdentifier: string + tooltip: ReactNode + }) => ( + + ), +})) + +vi.mock('@/next/link', () => ({ + default: ({ + href, + children, + className, + }: { + href: string + children: ReactNode + className?: string + }) =>
{children}, +})) + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})) + +vi.mock('@langgenius/dify-ui/popover', async () => { + const React = await import('react') + const PopoverContext = React.createContext({ + open: false, + setOpen: (_open: boolean) => {}, + }) + + const Popover = ({ + children, + open: controlledOpen, + onOpenChange, + }: { + children: React.ReactNode + open?: boolean + onOpenChange?: (open: boolean) => void + }) => { + const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false) + const isControlled = controlledOpen !== undefined + const open = isControlled ? !!controlledOpen : uncontrolledOpen + const setOpen = (nextOpen: boolean) => { + if (!isControlled) + setUncontrolledOpen(nextOpen) + onOpenChange?.(nextOpen) + } + + return ( + + {children} + + ) + } + + const PopoverTrigger = ({ render }: { render: React.ReactNode }) => { + const { open, setOpen } = React.useContext(PopoverContext) + return ( +
setOpen(!open)}> + {render} +
+ ) + } + + const PopoverContent = ({ children }: { children: React.ReactNode }) => { + const { open } = React.useContext(PopoverContext) + return open ?
{children}
: null + } + + return { + Popover, + PopoverTrigger, + PopoverContent, + } +}) + +vi.mock('@langgenius/dify-ui/tooltip', () => ({ + Tooltip: ({ children }: { children: ReactNode }) =>
{children}
, + TooltipTrigger: ({ render }: { render: ReactNode }) =>
{render}
, + TooltipContent: ({ children }: { children: ReactNode }) =>
{children}
, +})) + +const createStrategyDetail = ( + name: string, + strategyName: string, + strategyLabel: string, +): StrategyPluginDetail => ({ + plugin_unique_identifier: `provider/${name}`, + plugin_id: `plugin-${name}`, + declaration: { + identity: { + author: 'Dify', + name, + description: { en_US: `${name} description` }, + icon: `${name}.png`, + label: { en_US: `${name} label` }, + tags: [], + }, + strategies: [{ + identity: { + name: strategyName, + author: 'Dify', + label: { en_US: strategyLabel }, + }, + description: { en_US: `${strategyLabel} description` }, + parameters: [], + output_schema: { result: { type: 'string' } }, + }], + }, + meta: { version: '1.0.0' }, +} as unknown as StrategyPluginDetail) + +describe('AgentStrategySelector', () => { + const alphaDetail = createStrategyDetail('alpha', 'alpha-strategy', 'Alpha Strategy') + const betaDetail = createStrategyDetail('beta', 'beta-strategy', 'Beta Strategy') + + beforeEach(() => { + vi.clearAllMocks() + + mocks.useSuspenseQuery.mockReturnValue({ data: true }) + mocks.useStrategyProviders.mockReturnValue({ data: [alphaDetail, betaDetail] }) + mocks.useMarketplacePlugins.mockReturnValue({ + queryPluginsWithDebounced: mocks.queryPluginsWithDebounced, + plugins: [{ plugin_id: 'market-agent' }], + }) + mocks.useStrategyInfo.mockReturnValue({ + strategyStatus: undefined, + refetch: mocks.refetchStrategyInfo, + }) + }) + + it('filters strategies and queries marketplace when searching', async () => { + const user = userEvent.setup() + + render( + , + ) + + await user.click(screen.getByTestId('agent-strategy-trigger')) + + expect(screen.getByText('alpha')).toBeInTheDocument() + expect(screen.getByText('beta')).toBeInTheDocument() + expect(screen.getByTestId('plugin-list')).toHaveTextContent(':market-agent') + + await user.type( + screen.getByRole('textbox', { name: 'nodes.agent.strategy.searchPlaceholder' }), + 'alp', + ) + + await waitFor(() => { + expect(mocks.queryPluginsWithDebounced).toHaveBeenLastCalledWith({ + query: 'alp', + category: PluginCategoryEnum.agent, + }) + }) + + expect(screen.getByText('alpha')).toBeInTheDocument() + expect(screen.queryByText('beta')).not.toBeInTheDocument() + }) + + it('maps the selected tool and closes the popover', async () => { + const user = userEvent.setup() + const onChange = vi.fn() + + render( + , + ) + + await user.click(screen.getByTestId('agent-strategy-trigger')) + await user.click(screen.getByRole('button', { name: 'select-alpha' })) + + expect(onChange).toHaveBeenCalledWith({ + agent_strategy_name: 'alpha-strategy', + agent_strategy_provider_name: 'provider/alpha', + agent_strategy_label: 'Alpha Strategy', + agent_output_schema: { result: { type: 'string' } }, + plugin_unique_identifier: 'provider/alpha', + meta: { version: '1.0.0' }, + }) + expect(screen.queryByTestId('agent-strategy-popover')).not.toBeInTheDocument() + }) + + it('renders the plugin-not-installed warning for external strategies', () => { + mocks.useStrategyInfo.mockReturnValue({ + strategyStatus: { + plugin: { + source: 'external', + installed: false, + }, + isExistInPlugin: true, + }, + refetch: mocks.refetchStrategyInfo, + }) + + render( + , + ) + + expect(screen.getByText('nodes.agent.pluginNotInstalled')).toBeInTheDocument() + expect(screen.getByText('nodes.agent.pluginNotInstalledDesc')).toBeInTheDocument() + }) + + it('renders install and switch-version actions for marketplace strategies', async () => { + const user = userEvent.setup() + + mocks.useStrategyInfo.mockReturnValueOnce({ + strategyStatus: { + plugin: { + source: 'marketplace', + installed: false, + }, + isExistInPlugin: false, + }, + refetch: mocks.refetchStrategyInfo, + }) + + const { rerender } = render( + , + ) + + expect(screen.getByTestId('install-plugin-button')).toBeInTheDocument() + + mocks.useStrategyInfo.mockReturnValue({ + strategyStatus: { + plugin: { + source: 'marketplace', + installed: true, + }, + isExistInPlugin: false, + }, + refetch: mocks.refetchStrategyInfo, + }) + + rerender( + , + ) + + await user.click(screen.getByTestId('switch-plugin-version')) + + expect(mocks.refetchStrategyInfo).toHaveBeenCalled() + }) +}) diff --git a/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx b/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx index 8e7c1609d1..765c8ea76a 100644 --- a/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx +++ b/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx @@ -4,14 +4,21 @@ import type { Strategy } from './agent-strategy' import type { StrategyPluginDetail } from '@/app/components/plugins/types' import type { ListProps, ListRef } from '@/app/components/workflow/block-selector/market-place-plugin/list' import { cn } from '@langgenius/dify-ui/cn' +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@langgenius/dify-ui/popover' +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@langgenius/dify-ui/tooltip' import { RiArrowDownSLine, RiErrorWarningFill } from '@remixicon/react' import { useSuspenseQuery } from '@tanstack/react-query' import { memo, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem' import SearchInput from '@/app/components/base/search-input' -import Tooltip from '@/app/components/base/tooltip' -import { ToolTipContent } from '@/app/components/base/tooltip/content' import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon' import { useMarketplacePlugins } from '@/app/components/plugins/marketplace/hooks' import { PluginCategoryEnum } from '@/app/components/plugins/types' @@ -36,8 +43,11 @@ const NotFoundWarn = (props: { const { t } = useTranslation() return ( - +
} + /> +

{title} @@ -51,11 +61,7 @@ const NotFoundWarn = (props: {

- )} - > -
- -
+
) } @@ -66,18 +72,18 @@ function formatStrategy(input: StrategyPluginDetail[], getIcon: (i: string) => s id: item.plugin_unique_identifier, author: item.declaration.identity.author, name: item.declaration.identity.name, - description: item.declaration.identity.description as any, + description: item.declaration.identity.description as ToolWithProvider['description'], plugin_id: item.plugin_id, icon: getIcon(item.declaration.identity.icon), - label: item.declaration.identity.label as any, + label: item.declaration.identity.label as ToolWithProvider['label'], type: CollectionType.all, meta: item.meta, tools: item.declaration.strategies.map(strategy => ({ name: strategy.identity.name, author: strategy.identity.author, - label: strategy.identity.label as any, + label: strategy.identity.label as ToolWithProvider['tools'][number]['label'], description: strategy.description, - parameters: strategy.parameters as any, + parameters: strategy.parameters as unknown as ToolWithProvider['tools'][number]['parameters'], output_schema: strategy.output_schema, labels: [], })), @@ -151,76 +157,82 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => category: PluginCategoryEnum.agent, }) } - }, [query]) + }, [enable_marketplace, fetchPlugins, query]) const pluginRef = useRef(null) return ( - - -
setOpen(o => !o)} - > - { } - {icon && ( -
- icon -
- )} -

- {value?.agent_strategy_label || t('nodes.agent.strategy.selectTip', { ns: 'workflow' })} -

-
- {showInstallButton && value && ( - e.stopPropagation()} - size="small" - uniqueIdentifier={value.plugin_unique_identifier} - /> + + + {icon && ( +
+ icon +
)} - {showPluginNotInstalledWarn - ? ( - - ) - : showUnsupportedStrategy +

+ {value?.agent_strategy_label || t('nodes.agent.strategy.selectTip', { ns: 'workflow' })} +

+
+ {showInstallButton && value && ( + e.stopPropagation()} + size="small" + uniqueIdentifier={value.plugin_unique_identifier} + /> + )} + {showPluginNotInstalledWarn ? ( ) - : } - {showSwitchVersion && ( - - {t('nodes.agent.strategyNotFoundDescAndSwitchVersion', { ns: 'workflow' })} - - )} - onChange={() => { - refetchStrategyInfo() - }} - /> - )} + : showUnsupportedStrategy + ? ( + + ) + : } + {showSwitchVersion && value && ( + +

+ {t('nodes.agent.unsupportedStrategy', { ns: 'workflow' })} +

+

+ {t('nodes.agent.strategyNotFoundDescAndSwitchVersion', { ns: 'workflow' })} +

+
+ )} + onChange={() => { + refetchStrategyInfo() + }} + /> + )} +
- -
- + )} + /> +
@@ -260,8 +272,8 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => )}
-
-
+ + ) }) diff --git a/web/app/components/workflow/nodes/_base/components/variable/__tests__/var-reference-picker.trigger.spec.tsx b/web/app/components/workflow/nodes/_base/components/variable/__tests__/var-reference-picker.trigger.spec.tsx index f7264bcc99..a01d2a0387 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/__tests__/var-reference-picker.trigger.spec.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/__tests__/var-reference-picker.trigger.spec.tsx @@ -1,4 +1,8 @@ import type { ComponentProps } from 'react' +import { + Popover, + PopoverContent, +} from '@langgenius/dify-ui/popover' import { fireEvent, render, screen } from '@testing-library/react' import { BlockEnum, VarType } from '@/app/components/workflow/types' import { VarType as VarKindType } from '../../../../tool/types' @@ -28,7 +32,7 @@ const createProps = ( readonly: false, setControlFocus: vi.fn(), setOpen: vi.fn(), - tooltipPopup: null, + hoverPopup: null, triggerRef: { current: null }, value: [], varKindType: VarKindType.constant, @@ -38,46 +42,53 @@ const createProps = ( ], varName: '', variableCategory: 'system', - WrapElem: 'div', - VarPickerWrap: 'div', ...overrides, }) +const renderWithPopover = ( + overrides: Partial> = {}, +) => { + const onOpenChange = vi.fn() + + render( + + + +
picker-content
+
+
, + ) + + return { onOpenChange } +} + describe('VarReferencePickerTrigger', () => { it('should show the placeholder state and open the picker for variable mode', () => { - const setOpen = vi.fn() - render( - , - ) + const { onOpenChange } = renderWithPopover({ + placeholder: 'Pick variable', + }) expect(screen.getByText('Pick variable'))!.toBeInTheDocument() fireEvent.click(screen.getByTestId('var-reference-picker-trigger')) - expect(setOpen).toHaveBeenCalledWith(true) + expect(onOpenChange).toHaveBeenCalledWith(true, expect.anything()) }) it('should render the selected variable state and clear it', () => { const handleClearVar = vi.fn() const handleVariableJump = vi.fn() - render( - , - ) + renderWithPopover({ + handleClearVar, + handleVariableJump, + hasValue: true, + outputVarNode: { title: 'Source Node', desc: '', type: BlockEnum.Code }, + outputVarNodeId: 'node-a', + type: VarType.string, + value: ['node-a', 'answer'], + varName: 'answer', + }) expect(screen.getByText('Source Node'))!.toBeInTheDocument() expect(screen.getByText('answer'))!.toBeInTheDocument() @@ -93,20 +104,16 @@ describe('VarReferencePickerTrigger', () => { const setControlFocus = vi.fn() const setOpen = vi.fn() - render( - , - ) + renderWithPopover({ + isConstant: true, + isSupportConstantValue: true, + schemaWithDynamicSelect: { + type: 'text-input', + } as never, + setOpen, + setControlFocus, + value: 'constant-value', + }) fireEvent.click(screen.getByTestId('var-reference-picker-trigger')) expect(setControlFocus).toHaveBeenCalledTimes(1) @@ -116,38 +123,27 @@ describe('VarReferencePickerTrigger', () => { }) it('should render add button trigger in table mode', () => { - render( - , - ) + renderWithPopover({ + hasValue: true, + isAddBtnTrigger: true, + isInTable: true, + value: ['node-a', 'answer'], + varName: 'answer', + }) - expect(document.querySelector('button'))!.toBeInTheDocument() + expect(screen.getByTestId('add-button'))!.toBeInTheDocument() }) it('should stay inert in readonly mode and show value type placeholder badge', () => { - const setOpen = vi.fn() - - render( - , - ) + const { onOpenChange } = renderWithPopover({ + placeholder: 'Readonly placeholder', + readonly: true, + typePlaceHolder: 'string', + valueTypePlaceHolder: 'text', + }) fireEvent.click(screen.getByTestId('var-reference-picker-trigger')) - expect(setOpen).not.toHaveBeenCalled() + expect(onOpenChange).not.toHaveBeenCalled() expect(screen.getByText('string'))!.toBeInTheDocument() expect(screen.getByText('text'))!.toBeInTheDocument() }) @@ -155,17 +151,13 @@ describe('VarReferencePickerTrigger', () => { it('should show loading placeholder and remove rows in table mode', () => { const onRemove = vi.fn() - render( - , - ) + renderWithPopover({ + hasValue: false, + isInTable: true, + isLoading: true, + onRemove, + placeholder: 'Loading variable', + }) expect(screen.getByText('Loading variable'))!.toBeInTheDocument() diff --git a/web/app/components/workflow/nodes/_base/components/variable/__tests__/var-reference-vars.helpers.spec.ts b/web/app/components/workflow/nodes/_base/components/variable/__tests__/var-reference-vars.helpers.spec.ts index 773ecacc39..13b7879bad 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/__tests__/var-reference-vars.helpers.spec.ts +++ b/web/app/components/workflow/nodes/_base/components/variable/__tests__/var-reference-vars.helpers.spec.ts @@ -81,4 +81,27 @@ describe('var-reference-vars helpers', () => { expect(vars[0]!.title).toBe('Node B') expect(vars[0]!.vars).toEqual([expect.objectContaining({ variable: 'another_value' })]) }) + + it('should keep parent vars when search text matches a child variable', () => { + const vars = filterReferenceVars([ + { + title: 'Node A', + nodeId: 'node-a', + vars: [{ + variable: 'payload', + type: VarType.object, + children: [{ variable: 'child_name', type: VarType.string }], + }], + }, + { + title: 'Node B', + nodeId: 'node-b', + vars: [{ variable: 'other_value', type: VarType.string }], + }, + ] as NodeOutPutVar[], 'child') + + expect(vars).toHaveLength(1) + expect(vars[0]!.title).toBe('Node A') + expect(vars[0]!.vars).toEqual([expect.objectContaining({ variable: 'payload' })]) + }) }) diff --git a/web/app/components/workflow/nodes/_base/components/variable/__tests__/var-reference-vars.spec.tsx b/web/app/components/workflow/nodes/_base/components/variable/__tests__/var-reference-vars.spec.tsx index 93c857223a..372fcb3508 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/__tests__/var-reference-vars.spec.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/__tests__/var-reference-vars.spec.tsx @@ -199,6 +199,34 @@ describe('VarReferenceVars', () => { })) }) + it('should filter by externally controlled search text and match child variables', () => { + render( + , + ) + + expect(screen.queryByPlaceholderText('workflow.common.searchVar')).not.toBeInTheDocument() + expect(screen.getByText('payload')).toBeInTheDocument() + expect(screen.queryByText('other_value')).not.toBeInTheDocument() + }) + it('should ignore file vars when file support is disabled and forward blur-sm events', () => { const onChange = vi.fn() const onBlur = vi.fn() diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.trigger.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.trigger.tsx index 8c46118615..7f77f37987 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.trigger.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.trigger.tsx @@ -1,12 +1,14 @@ 'use client' -import type { FC, ReactNode } from 'react' +import type { FC, ReactElement } from 'react' import type { VarType as VarKindType } from '../../../tool/types' import type { CredentialFormSchema, CredentialFormSchemaSelect } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { Tool } from '@/app/components/tools/types' import type { TriggerWithProvider } from '@/app/components/workflow/block-selector/types' import type { Node, ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/types' import { cn } from '@langgenius/dify-ui/cn' +import { PopoverTrigger } from '@langgenius/dify-ui/popover' +import { PreviewCard, PreviewCardContent, PreviewCardTrigger } from '@langgenius/dify-ui/preview-card' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiArrowDownSLine, RiCloseLine, RiErrorWarningFill, RiLoader4Line, RiMoreLine } from '@remixicon/react' import Badge from '@/app/components/base/badge' @@ -18,6 +20,10 @@ import { VariableIconWithColor } from '@/app/components/workflow/nodes/_base/com import RemoveButton from '../remove-button' import ConstantField from './constant-field' +export type HoverPopup + = | { kind: 'full-path', panel: ReactElement } + | { kind: 'invalid-variable', message: string } + type Props = { className?: string controlFocus: number @@ -53,7 +59,7 @@ type Props = { setControlFocus: (value: number) => void setOpen: (value: boolean) => void showErrorIcon?: boolean - tooltipPopup: ReactNode + hoverPopup: HoverPopup | null triggerRef: React.RefObject type?: string typePlaceHolder?: string @@ -63,8 +69,6 @@ type Props = { varKindTypes: Array<{ label: string, value: VarKindType }> varName: string variableCategory: string - WrapElem: React.ElementType - VarPickerWrap: React.ElementType } const VarReferencePickerTrigger: FC = ({ @@ -99,7 +103,7 @@ const VarReferencePickerTrigger: FC = ({ setControlFocus, setOpen, showErrorIcon = false, - tooltipPopup, + hoverPopup, triggerRef, type, typePlaceHolder, @@ -109,21 +113,139 @@ const VarReferencePickerTrigger: FC = ({ varKindTypes, varName, variableCategory, - VarPickerWrap, - WrapElem, }) => { - return ( - { - if (readonly) - return - if (!isConstant) - setOpen(!open) - else - setControlFocus(Date.now()) - }} + const handleTriggerReadonlyClick = (e: React.MouseEvent) => { + if (!readonly) + return + e.preventDefault() + e.stopPropagation() + } + + const pill = ( +
+ {hasValue + ? ( + <> + {isShowNodeName && ( +
{ + if (e.metaKey || e.ctrlKey) + handleVariableJump(outputVarNodeId || '') + }} + > +
+ {'type' in (outputVarNode || {}) && outputVarNode?.type && ( + + )} +
+
+ {outputVarNode?.title as string | undefined} +
+ +
+ )} + {isShowAPart && ( +
+ + +
+ )} +
+ {isLoading && } + +
+ {varName} +
+
+
+ {type} +
+ {showErrorIcon && } + + ) + : ( +
+ {isLoading + ? ( +
+ + {placeholder} +
+ ) + : placeholder} +
+ )} +
+ ) + + const hoveredPill = hoverPopup?.kind === 'full-path' + ? ( + + + + {hoverPopup.panel} + + + ) + : hoverPopup?.kind === 'invalid-variable' + ? ( + + + {hoverPopup.message} + + ) + : pill + + const variablePicker = ( +
+
+ {hoveredPill} +
+
+ ) + + const resolvedVariablePicker = isSupportConstantValue + ? ( + readonly + ? variablePicker + : ( + + ) + ) + : variablePicker + + const triggerContent = ( +
{ + if (!isConstant || readonly) + return + setControlFocus(Date.now()) + }} > <> {isAddBtnTrigger @@ -178,109 +300,7 @@ const VarReferencePickerTrigger: FC = ({ isLoading={isLoading} /> ) - : ( - { - if (readonly) - return - if (!isConstant) - setOpen(!open) - else - setControlFocus(Date.now()) - }} - className="h-full grow" - > -
- - - {hasValue - ? ( - <> - {isShowNodeName && ( -
{ - if (e.metaKey || e.ctrlKey) - handleVariableJump(outputVarNodeId || '') - }} - > -
- {'type' in (outputVarNode || {}) && outputVarNode?.type && ( - - )} -
-
- {outputVarNode?.title as string | undefined} -
- -
- )} - {isShowAPart && ( -
- - -
- )} -
- {isLoading && } - -
- {varName} -
-
-
- {type} -
- {showErrorIcon && } - - ) - : ( -
- {isLoading - ? ( -
- - {placeholder} -
- ) - : placeholder} -
- )} -
- )} - /> - {tooltipPopup !== null && tooltipPopup !== undefined && ( - - {tooltipPopup} - - )} - -
- - - )} + : resolvedVariablePicker} {(hasValue && !readonly && !isInTable && !isJustShowValue) && (
= ({ )} - +
) + + if (!isSupportConstantValue) { + if (readonly) + return triggerContent + + return ( + + ) + } + + return triggerContent } export default VarReferencePickerTrigger diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx index 1cfccf1622..7e99988ae8 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx @@ -1,10 +1,16 @@ 'use client' import type { FC } from 'react' +import type { HoverPopup } from './var-reference-picker.trigger' import type { CredentialFormSchema, CredentialFormSchemaSelect, FormOption } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { Tool } from '@/app/components/tools/types' import type { TriggerWithProvider } from '@/app/components/workflow/block-selector/types' import type { CommonNodeType, Node, NodeOutPutVar, ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/types' import { cn } from '@langgenius/dify-ui/cn' +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@langgenius/dify-ui/popover' import { noop } from 'es-toolkit/function' import { produce } from 'immer' import * as React from 'react' @@ -15,11 +21,6 @@ import { useReactFlow, useStoreApi, } from 'reactflow' -import { - PortalToFollowElem, - PortalToFollowElemContent, - PortalToFollowElemTrigger, -} from '@/app/components/base/portal-to-follow-elem' import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { useIsChatMode, @@ -140,10 +141,10 @@ const VarReferencePicker: FC = ({ }) const node = nodes.find(n => n.id === nodeId) - const isInIteration = !!(node?.data as any)?.isInIteration + const isInIteration = !!node?.data.isInIteration const iterationNode = isInIteration ? (nodes.find(n => n.id === node?.parentId) ?? null) : null - const isInLoop = !!(node?.data as any)?.isInLoop + const isInLoop = !!node?.data.isInLoop const loopNode = isInLoop ? (nodes.find(n => n.id === node?.parentId) ?? null) : null const triggerRef = useRef(null) @@ -209,13 +210,11 @@ const VarReferencePicker: FC = ({ }, [onChange]) const inputRef = useRef(null) - const [isFocus, setIsFocus] = useState(false) const [controlFocus, setControlFocus] = useState(0) + const isFocus = controlFocus > 0 useEffect(() => { - if (controlFocus && inputRef.current) { + if (controlFocus && inputRef.current) inputRef.current.focus() - setIsFocus(true) - } }, [controlFocus]) const handleVarReferenceChange = useCallback((value: ValueSelector, varInfo: Var) => { @@ -263,7 +262,7 @@ const VarReferencePicker: FC = ({ }, [availableNodes, reactflow, store]) const type = getCurrentVariableType({ - parentNode: (isInIteration ? iterationNode : loopNode) as any, + parentNode: isInIteration ? iterationNode : loopNode, valueSelector: value as ValueSelector, availableNodes, isChatMode, @@ -288,23 +287,23 @@ const VarReferencePicker: FC = ({ maxVarNameWidth, } = getWidthAllocations(triggerWidth, outputVarNode?.title || '', varName || '', type || '') - const WrapElem = isSupportConstantValue ? 'div' : PortalToFollowElemTrigger - const VarPickerWrap = !isSupportConstantValue ? 'div' : PortalToFollowElemTrigger - - const tooltipPopup = useMemo(() => { + const hoverPopup = useMemo(() => { const tooltipType = getTooltipContent(hasValue, isShowAPart, isValidVar) if (tooltipType === 'full-path') { - return ( - - ) + return { + kind: 'full-path', + panel: ( + + ), + } } if (tooltipType === 'invalid-variable') - return t('errorMsg.invalidVariable', { ns: 'workflow' }) + return { kind: 'invalid-variable', message: t('errorMsg.invalidVariable', { ns: 'workflow' }) } return null }, [isValidVar, isShowAPart, hasValue, t, outputVarNode?.title, outputVarNode?.type, value, type]) @@ -345,15 +344,23 @@ const VarReferencePicker: FC = ({ ) const triggerPlaceholder = placeholder ?? t('common.setVarValuePlaceholder', { ns: 'workflow' }) + const resolvedTrigger = React.isValidElement(trigger) ? trigger :
{trigger}
return (
- - {!!trigger && setOpen(!open)}>{trigger}} + {!!trigger && ( + { + if (readonly) + e.preventDefault() + }} + /> + )} {!trigger && ( = ({ setControlFocus={setControlFocus} setOpen={setOpen} showErrorIcon={showErrorIcon} - tooltipPopup={tooltipPopup} + hoverPopup={hoverPopup} triggerRef={triggerRef} type={type} typePlaceHolder={typePlaceHolder} @@ -399,15 +406,18 @@ const VarReferencePicker: FC = ({ varKindTypes={varKindTypes} varName={varName} variableCategory={variableCategory} - VarPickerWrap={VarPickerWrap} - WrapElem={WrapElem} /> )} - {!isConstant && ( = ({ preferSchemaType={preferSchemaType} /> )} - - + +
) } diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.helpers.ts b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.helpers.ts index d36dc807a8..a9941bf72c 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.helpers.ts +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.helpers.ts @@ -1,3 +1,4 @@ +import type { Field, StructuredOutput } from '@/app/components/workflow/nodes/llm/types' import type { NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types' import { VAR_SHOW_NAME_MAP } from '@/app/components/workflow/constants' import { checkKeys } from '@/utils/var' @@ -76,6 +77,51 @@ const getVisibleChildren = (vars: Var[]) => { return vars.filter(variable => checkKeys([variable.variable], false).isValid || isSpecialVar(variable.variable.split('.')[0]!)) } +const includesSearchText = (value: string | undefined, searchTextLower: string) => { + if (!value) + return false + + return value.toLowerCase().includes(searchTextLower) +} + +const isStructuredOutputChildren = (children: Var['children']): children is StructuredOutput => { + return !!children && !Array.isArray(children) && 'schema' in children +} + +const matchesStructuredField = (fieldName: string, field: Field, searchTextLower: string): boolean => { + if (includesSearchText(fieldName, searchTextLower)) + return true + + if (field.properties) + return Object.entries(field.properties).some(([childName, childField]) => matchesStructuredField(childName, childField, searchTextLower)) + + if (field.items) + return matchesStructuredField(field.items.type, field.items, searchTextLower) + + return false +} + +const matchesVariableSearch = (variable: Var, searchTextLower: string): boolean => { + if ( + includesSearchText(variable.variable, searchTextLower) + || includesSearchText(variable.des, searchTextLower) + || includesSearchText(variable.schemaType, searchTextLower) + ) { + return true + } + + if (!variable.children) + return false + + if (Array.isArray(variable.children)) + return getVisibleChildren(variable.children).some(child => matchesVariableSearch(child, searchTextLower)) + + if (isStructuredOutputChildren(variable.children)) + return Object.entries(variable.children.schema.properties).some(([fieldName, field]) => matchesStructuredField(fieldName, field, searchTextLower)) + + return false +} + export const filterReferenceVars = (vars: NodeOutPutVar[], searchText: string) => { const searchTextLower = searchText.toLowerCase() @@ -85,7 +131,7 @@ export const filterReferenceVars = (vars: NodeOutPutVar[], searchText: string) = .filter((node) => { if (!searchText) return true - return node.vars.some(variable => variable.variable.toLowerCase().includes(searchTextLower)) + return node.vars.some(variable => matchesVariableSearch(variable, searchTextLower)) || node.title.toLowerCase().includes(searchTextLower) }) .map((node) => { @@ -94,7 +140,7 @@ export const filterReferenceVars = (vars: NodeOutPutVar[], searchText: string) = return { ...node, - vars: node.vars.filter(variable => variable.variable.toLowerCase().includes(searchTextLower)), + vars: node.vars.filter(variable => matchesVariableSearch(variable, searchTextLower)), } }) } diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx index 5c67723d78..28ad104ed7 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx @@ -4,6 +4,11 @@ import type { StructuredOutput } from '../../../llm/types' import type { Field } from '@/app/components/workflow/nodes/llm/types' import type { NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types' import { cn } from '@langgenius/dify-ui/cn' +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@langgenius/dify-ui/popover' import { useHover } from 'ahooks' import { noop } from 'es-toolkit/function' import * as React from 'react' @@ -13,11 +18,6 @@ import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows import { CodeAssistant, MagicEdit } from '@/app/components/base/icons/src/vender/line/general' import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' import Input from '@/app/components/base/input' -import { - PortalToFollowElem, - PortalToFollowElemContent, - PortalToFollowElemTrigger, -} from '@/app/components/base/portal-to-follow-elem' import PickerStructurePanel from '@/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/picker' import { VariableIconWithColor } from '@/app/components/workflow/nodes/_base/components/variable/variable-label' import { VarType } from '@/app/components/workflow/types' @@ -143,7 +143,7 @@ const Item: FC = ({ const open = (isObj || isStructureOutput) && isHovering useEffect(() => { onHovering?.(isHovering) - }, [isHovering]) + }, [isHovering, onHovering]) const handleChosen = (e: React.MouseEvent) => { e.stopPropagation() e.nativeEvent.stopImmediatePropagation() @@ -167,62 +167,70 @@ const Item: FC = ({ () => getVariableCategory({ isEnv, isChatVar, isLoopVar, isRagVariable }), [isEnv, isChatVar, isLoopVar, isRagVariable], ) + + const itemTrigger = ( +
{ + e.preventDefault() + e.stopPropagation() + e.nativeEvent.stopImmediatePropagation() + }} + > +
+ {!isFlat && ( + + )} + {isFlat && flatVarIcon} + + {!isEnv && !isChatVar && !isRagVariable && ( +
{varName}
+ )} + {isEnv && ( +
{itemData.variable.replace('env.', '')}
+ )} + {isChatVar && ( +
{itemData.variable.replace('conversation.', '')}
+ )} + {isRagVariable && ( +
{itemData.variable.split('.').slice(-1)[0]}
+ )} +
+
{(preferSchemaType && itemData.schemaType) ? itemData.schemaType : itemData.type}
+ { + (isObj || isStructureOutput) && ( + + ) + } +
+ ) + return ( - - -
{ - e.preventDefault() - e.stopPropagation() - e.nativeEvent.stopImmediatePropagation() - }} - > -
- {!isFlat && ( - - )} - {isFlat && flatVarIcon} - - {!isEnv && !isChatVar && !isRagVariable && ( -
{varName}
- )} - {isEnv && ( -
{itemData.variable.replace('env.', '')}
- )} - {isChatVar && ( -
{itemData.variable.replace('conversation.', '')}
- )} - {isRagVariable && ( -
{itemData.variable.split('.').slice(-1)[0]}
- )} -
-
{(preferSchemaType && itemData.schemaType) ? itemData.schemaType : itemData.type}
- { - (isObj || isStructureOutput) && ( - - ) - } -
-
- + {(isStructureOutput || isObj) && ( = ({ }} /> )} - -
+ + ) } type Props = { hideSearch?: boolean + searchText?: string searchBoxClassName?: string vars: NodeOutPutVar[] isSupportFileVar?: boolean @@ -258,6 +267,7 @@ type Props = { } const VarReferenceVars: FC = ({ hideSearch, + searchText, searchBoxClassName, vars, isSupportFileVar, @@ -274,7 +284,8 @@ const VarReferenceVars: FC = ({ preferSchemaType, }) => { const { t } = useTranslation() - const [searchText, setSearchText] = useState('') + const [internalSearchValue, setInternalSearchValue] = useState('') + const searchValue = searchText ?? internalSearchValue const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Escape') { @@ -283,7 +294,7 @@ const VarReferenceVars: FC = ({ } } - const filteredVars = useMemo(() => filterReferenceVars(vars, searchText), [vars, searchText]) + const filteredVars = useMemo(() => filterReferenceVars(vars, searchValue), [vars, searchValue]) return ( <> @@ -295,11 +306,11 @@ const VarReferenceVars: FC = ({ className="var-search-input" showLeftIcon showClearIcon - value={searchText} + value={searchValue} placeholder={t('common.searchVar', { ns: 'workflow' }) || ''} - onChange={e => setSearchText(e.target.value)} + onChange={e => setInternalSearchValue(e.target.value)} onKeyDown={handleKeyDown} - onClear={() => setSearchText('')} + onClear={() => setInternalSearchValue('')} onBlur={onBlur} autoFocus={autoFocus} /> diff --git a/web/app/components/workflow/nodes/code/dependency-picker.tsx b/web/app/components/workflow/nodes/code/dependency-picker.tsx index feed5234a7..dd9e97af6f 100644 --- a/web/app/components/workflow/nodes/code/dependency-picker.tsx +++ b/web/app/components/workflow/nodes/code/dependency-picker.tsx @@ -1,5 +1,10 @@ import type { FC } from 'react' import type { CodeDependency } from './types' +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@langgenius/dify-ui/popover' import { RiArrowDownSLine, } from '@remixicon/react' @@ -8,7 +13,6 @@ import * as React from 'react' import { useCallback, useState } from 'react' import { Check } from '@/app/components/base/icons/src/vender/line/general' import Input from '@/app/components/base/input' -import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem' type Props = { value: CodeDependency @@ -32,21 +36,22 @@ const DependencyPicker: FC = ({ }, [onChange]) return ( - - setOpen(!open)} className="grow cursor-pointer"> -
-
{value.name}
- -
-
- + +
+
{value.name}
+ +
+ + )} + /> +
= ({ ))}
-
-
+ + ) } diff --git a/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/__tests__/member-selector.spec.tsx b/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/__tests__/member-selector.spec.tsx index 823aa68047..e1d959bd86 100644 --- a/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/__tests__/member-selector.spec.tsx +++ b/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/__tests__/member-selector.spec.tsx @@ -2,6 +2,59 @@ import type { Member } from '@/models/common' import { fireEvent, render, screen } from '@testing-library/react' import MemberSelector from '../member-selector' +vi.mock('@langgenius/dify-ui/popover', async () => { + const React = await import('react') + const PopoverContext = React.createContext({ + open: false, + setOpen: (_open: boolean) => {}, + }) + + const Popover = ({ + children, + open: controlledOpen, + onOpenChange, + }: { + children: import('react').ReactNode + open?: boolean + onOpenChange?: (open: boolean) => void + }) => { + const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false) + const isControlled = controlledOpen !== undefined + const open = isControlled ? !!controlledOpen : uncontrolledOpen + const setOpen = (nextOpen: boolean) => { + if (!isControlled) + setUncontrolledOpen(nextOpen) + onOpenChange?.(nextOpen) + } + + return ( + + {children} + + ) + } + + const PopoverTrigger = ({ render }: { render: import('react').ReactNode }) => { + const { open, setOpen } = React.useContext(PopoverContext) + return ( +
setOpen(!open)}> + {render} +
+ ) + } + + const PopoverContent = ({ children }: { children: import('react').ReactNode }) => { + const { open } = React.useContext(PopoverContext) + return open ?
{children}
: null + } + + return { + Popover, + PopoverTrigger, + PopoverContent, + } +}) + const mockMemberList = vi.hoisted(() => vi.fn()) vi.mock('../member-list', () => ({ diff --git a/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/email-input.tsx b/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/email-input.tsx index 1f18ef076d..cb8de634a2 100644 --- a/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/email-input.tsx +++ b/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/email-input.tsx @@ -1,14 +1,13 @@ import type { Recipient as RecipientItem } from '../../../types' import type { Member } from '@/models/common' import { cn } from '@langgenius/dify-ui/cn' +import { + Popover, + PopoverContent, +} from '@langgenius/dify-ui/popover' import * as React from 'react' import { useCallback, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { - PortalToFollowElem, - PortalToFollowElemContent, - PortalToFollowElemTrigger, -} from '@/app/components/base/portal-to-follow-elem' import EmailItem from './email-item' import MemberList from './member-list' @@ -58,8 +57,7 @@ const EmailInput = ({ if (disabled) return setIsFocus(true) - const input = inputRef.current?.children[0] as HTMLInputElement - input?.focus() + inputRef.current?.focus() } const handleValueChange = (e: React.ChangeEvent) => { @@ -141,28 +139,29 @@ const EmailInput = ({ /> ))} {!disabled && ( - - - setIsFocus(true)} - onBlur={handleInputBlur} - value={searchKey} - onChange={handleValueChange} - onKeyDown={handleKeyDown} - /> - - + + setIsFocus(true)} + onBlur={handleInputBlur} + value={searchKey} + onChange={handleValueChange} + onKeyDown={handleKeyDown} + /> + - - + + )} diff --git a/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/member-selector.tsx b/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/member-selector.tsx index c2df6afb15..f091ec5a30 100644 --- a/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/member-selector.tsx +++ b/web/app/components/workflow/nodes/human-input/components/delivery-method/recipient/member-selector.tsx @@ -4,12 +4,16 @@ import type { Recipient } from '@/app/components/workflow/nodes/human-input/type import type { Member } from '@/models/common' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@langgenius/dify-ui/popover' import { RiContactsBookLine, } from '@remixicon/react' -import { useState } from 'react' +import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' -import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem' import MemberList from './member-list' const i18nPrefix = 'nodes.humanInput' @@ -31,39 +35,42 @@ const MemberSelector: FC = ({ const [open, setOpen] = useState(false) const [searchValue, setSearchValue] = useState('') + const handleSelect = useCallback((memberId: string) => { + onSelect(memberId) + setOpen(false) + }, [onSelect]) + return ( - - setOpen(v => !v)} + + + +
{t(`${i18nPrefix}.deliveryMethod.emailConfigure.memberSelector.trigger`, { ns: 'workflow' })}
+ + )} + /> + - -
- - -
+ + ) } + export default MemberSelector diff --git a/web/app/components/workflow/nodes/if-else/components/condition-add.tsx b/web/app/components/workflow/nodes/if-else/components/condition-add.tsx index 85a45ac5c7..b8f5dab85b 100644 --- a/web/app/components/workflow/nodes/if-else/components/condition-add.tsx +++ b/web/app/components/workflow/nodes/if-else/components/condition-add.tsx @@ -5,17 +5,17 @@ import type { Var, } from '@/app/components/workflow/types' import { Button } from '@langgenius/dify-ui/button' +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@langgenius/dify-ui/popover' import { RiAddLine } from '@remixicon/react' import { useCallback, useState, } from 'react' import { useTranslation } from 'react-i18next' -import { - PortalToFollowElem, - PortalToFollowElemContent, - PortalToFollowElemTrigger, -} from '@/app/components/base/portal-to-follow-elem' import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars' type ConditionAddProps = { @@ -25,6 +25,7 @@ type ConditionAddProps = { onSelectVariable: HandleAddCondition disabled?: boolean } + const ConditionAdd = ({ className, caseId, @@ -38,29 +39,32 @@ const ConditionAdd = ({ const handleSelectVariable = useCallback((valueSelector: ValueSelector, varItem: Var) => { onSelectVariable(caseId, valueSelector, varItem) setOpen(false) - }, [caseId, onSelectVariable, setOpen]) + }, [caseId, onSelectVariable]) return ( - - setOpen(!open)}> - - - + + + + {t('nodes.ifElse.addCondition', { ns: 'workflow' })} + + )} + onClick={(e) => { + if (disabled) + e.preventDefault() + }} + /> +
-
-
+ + ) } diff --git a/web/app/components/workflow/nodes/if-else/components/condition-list/condition-var-selector.tsx b/web/app/components/workflow/nodes/if-else/components/condition-list/condition-var-selector.tsx index 2740471610..e94eca7b38 100644 --- a/web/app/components/workflow/nodes/if-else/components/condition-list/condition-var-selector.tsx +++ b/web/app/components/workflow/nodes/if-else/components/condition-list/condition-var-selector.tsx @@ -1,5 +1,9 @@ import type { Node, NodeOutPutVar, ValueSelector, Var, VarType } from '@/app/components/workflow/types' -import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem' +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@langgenius/dify-ui/popover' import VariableTag from '@/app/components/workflow/nodes/_base/components/variable-tag' import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars' @@ -23,26 +27,25 @@ const ConditionVarSelector = ({ onChange, }: ConditionVarSelectorProps) => { return ( - - onOpenChange(!open)}> -
- -
-
- + + + + + )} + /> +
-
-
+ + ) } diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/add-condition.tsx b/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/add-condition.tsx index ea7870431a..2787aafafd 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/add-condition.tsx +++ b/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/add-condition.tsx @@ -2,8 +2,11 @@ import type { MetadataShape } from '@/app/components/workflow/nodes/knowledge-re import type { MetadataInDoc } from '@/models/datasets' import { Button } from '@langgenius/dify-ui/button' import { - RiAddLine, -} from '@remixicon/react' + Popover, + PopoverContent, + PopoverTrigger, +} from '@langgenius/dify-ui/popover' +import { RiAddLine } from '@remixicon/react' import { useCallback, useMemo, @@ -11,11 +14,6 @@ import { } from 'react' import { useTranslation } from 'react-i18next' import Input from '@/app/components/base/input' -import { - PortalToFollowElem, - PortalToFollowElemContent, - PortalToFollowElemTrigger, -} from '@/app/components/base/portal-to-follow-elem' import MetadataIcon from './metadata-icon' const AddCondition = ({ @@ -36,25 +34,24 @@ const AddCondition = ({ }, [handleAddCondition]) return ( - - setOpen(!open)}> - - - + + + + {t('nodes.knowledgeRetrieval.metadata.panel.add', { ns: 'workflow' })} + + )} + /> +
- { - filteredMetadataList?.map(metadata => ( -
-
- -
-
handleAddConditionWrapped(metadata)} - > - {metadata.name} -
-
{metadata.type}
+ {filteredMetadataList?.map(metadata => ( +
+
+
- )) - } +
handleAddConditionWrapped(metadata)} + > + {metadata.name} +
+
{metadata.type}
+
+ ))}
- - + + ) } diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-common-variable-selector.tsx b/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-common-variable-selector.tsx index dd0858942d..d4aedbd8b9 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-common-variable-selector.tsx +++ b/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-common-variable-selector.tsx @@ -1,12 +1,12 @@ import type { VarType } from '@/app/components/workflow/types' +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@langgenius/dify-ui/popover' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' -import { - PortalToFollowElem, - PortalToFollowElemContent, - PortalToFollowElemTrigger, -} from '@/app/components/base/portal-to-follow-elem' type ConditionCommonVariableSelectorProps = { variables?: { name: string, type: string, value: string }[] @@ -31,34 +31,17 @@ const ConditionCommonVariableSelector = ({ }, [onChange]) return ( - - { - if (!variables.length) - return - setOpen(!open) - }} - > -
- { - selected && ( + + + {selected && (
{selected.value}
- ) - } - { - !selected && ( + )} + {!selected && ( <>
@@ -68,27 +51,34 @@ const ConditionCommonVariableSelector = ({ {varType}
- ) - } -
-
- + )} +
+ )} + onClick={(e) => { + if (!variables.length) + e.preventDefault() + }} + /> +
- { - variables.map(v => ( -
handleChange(v.value)} - > - - {v.value} -
- )) - } + {variables.map(v => ( +
handleChange(v.value)} + > + + {v.value} +
+ ))}
-
-
+ + ) } diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-variable-selector.tsx b/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-variable-selector.tsx index 15f5ec9377..7ef4ed7388 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-variable-selector.tsx +++ b/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-variable-selector.tsx @@ -4,14 +4,14 @@ import type { ValueSelector, Var, } from '@/app/components/workflow/types' +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@langgenius/dify-ui/popover' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' -import { - PortalToFollowElem, - PortalToFollowElemContent, - PortalToFollowElemTrigger, -} from '@/app/components/base/portal-to-follow-elem' import VariableTag from '@/app/components/workflow/nodes/_base/components/variable-tag' import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars' import { VarType } from '@/app/components/workflow/types' @@ -34,35 +34,25 @@ const ConditionVariableSelector = ({ const { t } = useTranslation() const [open, setOpen] = useState(false) - const handleChange = useCallback((valueSelector: ValueSelector, varItem: Var) => { - onChange(valueSelector, varItem) + const handleChange = useCallback((nextValueSelector: ValueSelector, varItem: Var) => { + onChange(nextValueSelector, varItem) setOpen(false) }, [onChange]) return ( - - setOpen(!open)}> -
- { - !!valueSelector.length && ( + + + {!!valueSelector.length && ( - ) - } - { - !valueSelector.length && ( + )} + {!valueSelector.length && ( <>
@@ -72,11 +62,16 @@ const ConditionVariableSelector = ({ {varType}
- ) - } -
-
- + )} + + )} + /> +
-
-
+ + ) } diff --git a/web/app/components/workflow/nodes/loop/components/condition-add.tsx b/web/app/components/workflow/nodes/loop/components/condition-add.tsx index 36a09dd434..a28b066098 100644 --- a/web/app/components/workflow/nodes/loop/components/condition-add.tsx +++ b/web/app/components/workflow/nodes/loop/components/condition-add.tsx @@ -5,17 +5,17 @@ import type { Var, } from '@/app/components/workflow/types' import { Button } from '@langgenius/dify-ui/button' +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@langgenius/dify-ui/popover' import { RiAddLine } from '@remixicon/react' import { useCallback, useState, } from 'react' import { useTranslation } from 'react-i18next' -import { - PortalToFollowElem, - PortalToFollowElemContent, - PortalToFollowElemTrigger, -} from '@/app/components/base/portal-to-follow-elem' import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars' type ConditionAddProps = { @@ -24,6 +24,7 @@ type ConditionAddProps = { onSelectVariable: HandleAddCondition disabled?: boolean } + const ConditionAdd = ({ className, variables, @@ -36,29 +37,32 @@ const ConditionAdd = ({ const handleSelectVariable = useCallback((valueSelector: ValueSelector, varItem: Var) => { onSelectVariable(valueSelector, varItem) setOpen(false) - }, [onSelectVariable, setOpen]) + }, [onSelectVariable]) return ( - - setOpen(!open)}> - - - + + + + {t('nodes.ifElse.addCondition', { ns: 'workflow' })} + + )} + onClick={(e) => { + if (disabled) + e.preventDefault() + }} + /> +
-
-
+ + ) } diff --git a/web/app/components/workflow/nodes/loop/components/condition-list/condition-var-selector.tsx b/web/app/components/workflow/nodes/loop/components/condition-list/condition-var-selector.tsx index 7a7957ad71..496ed4087d 100644 --- a/web/app/components/workflow/nodes/loop/components/condition-list/condition-var-selector.tsx +++ b/web/app/components/workflow/nodes/loop/components/condition-list/condition-var-selector.tsx @@ -1,5 +1,9 @@ import type { Node, NodeOutPutVar, ValueSelector, Var, VarType } from '@/app/components/workflow/types' -import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem' +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@langgenius/dify-ui/popover' import VariableTag from '@/app/components/workflow/nodes/_base/components/variable-tag' import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars' @@ -23,26 +27,25 @@ const ConditionVarSelector = ({ onChange, }: ConditionVarSelectorProps) => { return ( - - onOpenChange(!open)}> -
- -
-
- + + + + + )} + /> +
-
-
+ + ) } diff --git a/web/app/education-apply/__tests__/search-input.spec.tsx b/web/app/education-apply/__tests__/search-input.spec.tsx new file mode 100644 index 0000000000..bb3cd8cc84 --- /dev/null +++ b/web/app/education-apply/__tests__/search-input.spec.tsx @@ -0,0 +1,159 @@ +import type { ReactNode } from 'react' +import { fireEvent, render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { useState } from 'react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import SearchInput from '../search-input' + +const educationMocks = vi.hoisted(() => ({ + schools: ['Alpha University', 'Beta College'], + setSchools: vi.fn(), + querySchoolsWithDebounced: vi.fn(), + handleUpdateSchools: vi.fn(), + hasNext: false, +})) + +vi.mock('../hooks', () => ({ + useEducation: () => educationMocks, +})) + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})) + +vi.mock('@/app/components/base/input', () => ({ + default: ({ + value, + onChange, + placeholder, + className, + }: { + value?: string + onChange: (event: { target: { value: string } }) => void + placeholder?: string + className?: string + }) => ( + onChange({ target: { value: e.target.value } })} + /> + ), +})) + +vi.mock('@langgenius/dify-ui/popover', async () => { + const React = await import('react') + const PopoverContext = React.createContext({ + open: false, + setOpen: (_open: boolean) => {}, + }) + + const Popover = ({ + children, + open: controlledOpen, + onOpenChange, + }: { + children: ReactNode + open?: boolean + onOpenChange?: (open: boolean) => void + }) => { + const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false) + const isControlled = controlledOpen !== undefined + const open = isControlled ? !!controlledOpen : uncontrolledOpen + const setOpen = (nextOpen: boolean) => { + if (!isControlled) + setUncontrolledOpen(nextOpen) + onOpenChange?.(nextOpen) + } + + return ( + + {children} + + ) + } + + const PopoverTrigger = ({ render }: { render: ReactNode }) => <>{render} + + const PopoverContent = ({ children }: { children: ReactNode }) => { + const { open } = React.useContext(PopoverContext) + return open ?
{children}
: null + } + + return { + Popover, + PopoverTrigger, + PopoverContent, + } +}) + +const ControlledSearchInput = () => { + const [value, setValue] = useState('') + return +} + +describe('education-apply/search-input', () => { + beforeEach(() => { + vi.clearAllMocks() + educationMocks.schools = ['Alpha University', 'Beta College'] + educationMocks.hasNext = false + }) + + it('opens the popover, queries schools, and closes after selection', async () => { + const user = userEvent.setup() + + render() + + const input = screen.getByPlaceholderText('form.schoolName.placeholder') + await user.type(input, 'A') + + expect(educationMocks.setSchools).toHaveBeenCalledWith([]) + expect(educationMocks.querySchoolsWithDebounced).toHaveBeenLastCalledWith({ + keywords: 'A', + page: 0, + }) + + expect(screen.getByTestId('education-search-popover')).toBeInTheDocument() + expect(screen.getByText('Alpha University')).toBeInTheDocument() + + await user.click(screen.getByText('Beta College')) + + expect(screen.getByDisplayValue('Beta College')).toBeInTheDocument() + expect(screen.queryByTestId('education-search-popover')).not.toBeInTheDocument() + }) + + it('loads the next page when the dropdown is scrolled to the bottom', async () => { + const user = userEvent.setup() + educationMocks.hasNext = true + + render() + + await user.type(screen.getByPlaceholderText('form.schoolName.placeholder'), 'A') + + const scrollContainer = screen.getByText('Alpha University').parentElement as HTMLDivElement + Object.defineProperties(scrollContainer, { + scrollTop: { + value: 60, + configurable: true, + }, + scrollHeight: { + value: 100, + configurable: true, + }, + clientHeight: { + value: 40, + configurable: true, + }, + }) + + fireEvent.scroll(scrollContainer) + + expect(educationMocks.handleUpdateSchools).toHaveBeenCalledWith({ + keywords: 'A', + page: 1, + }) + }) +}) diff --git a/web/app/education-apply/search-input.tsx b/web/app/education-apply/search-input.tsx index 47eca921ee..4f930eb3eb 100644 --- a/web/app/education-apply/search-input.tsx +++ b/web/app/education-apply/search-input.tsx @@ -1,4 +1,9 @@ -import type { ChangeEventHandler } from 'react' +import type { ChangeEventHandler, UIEventHandler } from 'react' +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@langgenius/dify-ui/popover' import { useCallback, useRef, @@ -6,17 +11,13 @@ import { } from 'react' import { useTranslation } from 'react-i18next' import Input from '@/app/components/base/input' -import { - PortalToFollowElem, - PortalToFollowElemContent, - PortalToFollowElemTrigger, -} from '@/app/components/base/portal-to-follow-elem' import { useEducation } from './hooks' type SearchInputProps = { value?: string onChange: (value: string) => void } + const SearchInput = ({ value, onChange, @@ -48,7 +49,7 @@ const SearchInput = ({ keywords, page, }) - }, [querySchoolsWithDebounced, handleUpdateSchools]) + }, [handleUpdateSchools, querySchoolsWithDebounced]) const handleValueChange: ChangeEventHandler = useCallback((e) => { setOpen(true) @@ -58,10 +59,10 @@ const SearchInput = ({ valueRef.current = inputValue onChange(inputValue) handleSearch(true) - }, [onChange, handleSearch, setSchools]) + }, [handleSearch, onChange, setSchools]) - const handleScroll = useCallback((e: Event) => { - const target = e.target as HTMLDivElement + const handleScroll: UIEventHandler = useCallback((e) => { + const target = e.currentTarget const { scrollTop, scrollHeight, @@ -74,48 +75,45 @@ const SearchInput = ({ }, [handleSearch, hasNext]) return ( - - - - - - { - !!schools.length && value && ( -
- { - schools.map((school, index) => ( -
{ - onChange(school) - setOpen(false) - }} - > - {school} -
- )) - } -
- ) - } -
-
+ + + )} + /> + {!!schools.length && !!value && ( + +
+ {schools.map(school => ( +
{ + onChange(school) + setOpen(false) + }} + > + {school} +
+ ))} +
+
+ )} +
) } diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs index 62cc63536f..2a043ac73d 100644 --- a/web/eslint.config.mjs +++ b/web/eslint.config.mjs @@ -13,9 +13,9 @@ import storybook from 'eslint-plugin-storybook' import { HYOBAN_PREFER_TAILWIND_ICONS_OPTIONS, NEXT_PLATFORM_RESTRICTED_IMPORT_PATHS, - NEXT_PLATFORM_RESTRICTED_IMPORT_PATTERNS, OVERLAY_MIGRATION_LEGACY_BASE_FILES, OVERLAY_RESTRICTED_IMPORT_PATTERNS, + WEB_RESTRICTED_IMPORT_PATTERNS, } from './eslint.constants.mjs' import dify from './plugins/eslint/index.js' @@ -161,13 +161,13 @@ export default antfu( }, }, { - name: 'dify/no-direct-next-imports', + name: 'dify/restricted-imports', files: [GLOB_TS, GLOB_TSX], ignores: ['next/**'], rules: { 'no-restricted-imports': ['error', { paths: NEXT_PLATFORM_RESTRICTED_IMPORT_PATHS, - patterns: NEXT_PLATFORM_RESTRICTED_IMPORT_PATTERNS, + patterns: WEB_RESTRICTED_IMPORT_PATTERNS, }], }, }, @@ -183,7 +183,7 @@ export default antfu( 'no-restricted-imports': ['error', { paths: NEXT_PLATFORM_RESTRICTED_IMPORT_PATHS, patterns: [ - ...NEXT_PLATFORM_RESTRICTED_IMPORT_PATTERNS, + ...WEB_RESTRICTED_IMPORT_PATTERNS, ...OVERLAY_RESTRICTED_IMPORT_PATTERNS, ], }], diff --git a/web/eslint.constants.mjs b/web/eslint.constants.mjs index 1c09cbcb23..1039a300ec 100644 --- a/web/eslint.constants.mjs +++ b/web/eslint.constants.mjs @@ -5,7 +5,7 @@ export const NEXT_PLATFORM_RESTRICTED_IMPORT_PATHS = [ }, ] -export const NEXT_PLATFORM_RESTRICTED_IMPORT_PATTERNS = [ +const NEXT_PLATFORM_RESTRICTED_IMPORT_PATTERNS = [ { group: ['next/image'], message: 'Do not import next/image. Use native img tags instead.', @@ -20,6 +20,21 @@ export const NEXT_PLATFORM_RESTRICTED_IMPORT_PATTERNS = [ }, ] +const BASE_UI_RESTRICTED_IMPORT_PATTERNS = [ + { + group: [ + '@base-ui/react', + '@base-ui/react/*', + ], + message: 'Do not import Base UI directly in web. Use @langgenius/dify-ui/* primitives instead.', + }, +] + +export const WEB_RESTRICTED_IMPORT_PATTERNS = [ + ...NEXT_PLATFORM_RESTRICTED_IMPORT_PATTERNS, + ...BASE_UI_RESTRICTED_IMPORT_PATTERNS, +] + export const OVERLAY_RESTRICTED_IMPORT_PATTERNS = [ { group: [