Compare commits

..

2 Commits

Author SHA1 Message Date
3788ba6102 [autofix.ci] apply automated fixes 2026-05-25 02:24:18 +00:00
ea5aab7dd4 chore(deps): bump the vdb group in /api with 12 updates
Bumps the vdb group in /api with 12 updates:

| Package | From | To |
| --- | --- | --- |
| [mysql-connector-python](https://github.com/mysql/mysql-connector-python) | `9.6.0` | `9.7.0` |
| [alibabacloud-gpdb20160503](https://github.com/aliyun/alibabacloud-python-sdk) | `5.2.0` | `5.3.0` |
| [clickhouse-connect](https://github.com/ClickHouse/clickhouse-connect) | `0.15.1` | `1.0.1` |
| [pymochow](http://bce.baidu.com) | `2.4.0` | `2.4.1` |
| clickzetta-connector-python | `0.8.104` | `0.8.109` |
| [couchbase](https://github.com/couchbase/couchbase-python-client) | `4.6.0` | `4.6.1` |
| [opensearch-py](https://github.com/opensearch-project/opensearch-py) | `3.1.0` | `3.2.0` |
| [pymilvus](https://github.com/milvus-io/pymilvus) | `2.6.12` | `3.0.0` |
| [pyobvector](https://github.com/oceanbase/pyobvector) | `0.2.25` | `0.2.26` |
| [oracledb](https://github.com/oracle/python-oracledb) | `3.4.2` | `4.0.1` |
| [tablestore](https://github.com/aliyun/aliyun-tablestore-python-sdk) | `6.4.4` | `6.4.6` |
| [weaviate-client](https://github.com/weaviate/weaviate-python-client) | `4.20.5` | `4.21.0` |


Updates `mysql-connector-python` from 9.6.0 to 9.7.0
- [Changelog](https://github.com/mysql/mysql-connector-python/blob/trunk/CHANGES.txt)
- [Commits](https://github.com/mysql/mysql-connector-python/compare/9.6.0...9.7.0)

Updates `alibabacloud-gpdb20160503` from 5.2.0 to 5.3.0
- [Changelog](https://github.com/aliyun/alibabacloud-python-sdk/blob/master/docmind-api-20220711/ChangeLog.md)
- [Commits](https://github.com/aliyun/alibabacloud-python-sdk/commits)

Updates `clickhouse-connect` from 0.15.1 to 1.0.1
- [Release notes](https://github.com/ClickHouse/clickhouse-connect/releases)
- [Changelog](https://github.com/ClickHouse/clickhouse-connect/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ClickHouse/clickhouse-connect/compare/v0.15.1...v1.0.1)

Updates `pymochow` from 2.4.0 to 2.4.1

Updates `clickzetta-connector-python` from 0.8.104 to 0.8.109

Updates `couchbase` from 4.6.0 to 4.6.1
- [Commits](https://github.com/couchbase/couchbase-python-client/compare/4.6.0...4.6.1)

Updates `opensearch-py` from 3.1.0 to 3.2.0
- [Release notes](https://github.com/opensearch-project/opensearch-py/releases)
- [Changelog](https://github.com/opensearch-project/opensearch-py/blob/main/CHANGELOG.md)
- [Commits](https://github.com/opensearch-project/opensearch-py/compare/v3.1.0...v3.2.0)

Updates `pymilvus` from 2.6.12 to 3.0.0
- [Release notes](https://github.com/milvus-io/pymilvus/releases)
- [Commits](https://github.com/milvus-io/pymilvus/compare/v2.6.12...v3.0.0)

Updates `pyobvector` from 0.2.25 to 0.2.26
- [Release notes](https://github.com/oceanbase/pyobvector/releases)
- [Changelog](https://github.com/oceanbase/pyobvector/blob/main/RELEASE_NOTES.md)
- [Commits](https://github.com/oceanbase/pyobvector/compare/release-v0.2.25...release-v0.2.26)

Updates `oracledb` from 3.4.2 to 4.0.1
- [Release notes](https://github.com/oracle/python-oracledb/releases)
- [Commits](https://github.com/oracle/python-oracledb/compare/v3.4.2...v4.0.1)

Updates `tablestore` from 6.4.4 to 6.4.6
- [Release notes](https://github.com/aliyun/aliyun-tablestore-python-sdk/releases)
- [Changelog](https://github.com/aliyun/aliyun-tablestore-python-sdk/blob/master/CHANGELOG.md)
- [Commits](https://github.com/aliyun/aliyun-tablestore-python-sdk/commits)

Updates `weaviate-client` from 4.20.5 to 4.21.0
- [Release notes](https://github.com/weaviate/weaviate-python-client/releases)
- [Changelog](https://github.com/weaviate/weaviate-python-client/blob/main/docs/changelog.rst)
- [Commits](https://github.com/weaviate/weaviate-python-client/compare/v4.20.5...v4.21.0)

---
updated-dependencies:
- dependency-name: mysql-connector-python
  dependency-version: 9.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: vdb
- dependency-name: alibabacloud-gpdb20160503
  dependency-version: 5.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: vdb
- dependency-name: clickhouse-connect
  dependency-version: 1.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: vdb
- dependency-name: pymochow
  dependency-version: 2.4.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: vdb
- dependency-name: clickzetta-connector-python
  dependency-version: 0.8.109
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: vdb
- dependency-name: couchbase
  dependency-version: 4.6.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: vdb
- dependency-name: opensearch-py
  dependency-version: 3.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: vdb
- dependency-name: pymilvus
  dependency-version: 3.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: vdb
- dependency-name: pyobvector
  dependency-version: 0.2.26
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: vdb
- dependency-name: oracledb
  dependency-version: 4.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: vdb
- dependency-name: tablestore
  dependency-version: 6.4.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: vdb
- dependency-name: weaviate-client
  dependency-version: 4.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: vdb
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-25 02:21:23 +00:00
66 changed files with 2070 additions and 1783 deletions

59
api/uv.lock generated
View File

@ -1065,19 +1065,19 @@ wheels = [
[[package]]
name = "couchbase"
version = "4.6.0"
version = "4.6.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8d/be/1e6974158348dfa634ebbc32b76448f84945e15494852e0cea85607825b5/couchbase-4.6.0.tar.gz", hash = "sha256:61229d6112597f35f6aca687c255e12f495bde9051cd36063b4fddd532ab8f7f", size = 6697937, upload-time = "2026-03-31T23:29:50.602Z" }
sdist = { url = "https://files.pythonhosted.org/packages/99/8c/ecbf99eedbd8e39391d4eb44ff37517f3c5efb1a0879357ccc8ba7a0d106/couchbase-4.6.1.tar.gz", hash = "sha256:d15dd81c0789f5d3bda76e22c6636a0689afe065cf2db024ca074b6c208b79e4", size = 6712137, upload-time = "2026-04-29T21:27:59.694Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/84/dc/bea38235bfabd4fcf3d11e05955e38311869f173328475c369199a6b076b/couchbase-4.6.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:8d1244fd0581cc23aaf2fa3148e9c2d8cfba1d5489c123ee6bf975624d861f7a", size = 5521692, upload-time = "2026-03-31T23:29:07.933Z" },
{ url = "https://files.pythonhosted.org/packages/d1/18/cd1c751005cb67d3e2b090cd11626b8922b9d6a882516e57c1a3aedeed18/couchbase-4.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8efa57a86e35ceb7ae249cfa192e3f2c32a4a5b37098830196d3936994d55a67", size = 4667116, upload-time = "2026-03-31T23:29:10.706Z" },
{ url = "https://files.pythonhosted.org/packages/64/e9/1212bd59347e1cecdb02c6735704650e25f9195b634bf8df73d3382ffa14/couchbase-4.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7106e334acdacab64ae3530a181b8fabf0a1b91e7a1a1e41e259f995bdc78330", size = 5511873, upload-time = "2026-03-31T23:29:13.414Z" },
{ url = "https://files.pythonhosted.org/packages/86/a3/f676ee10f8ea2370700c1c4d03cbe8c3064a3e0cf887941a39333f3bdd97/couchbase-4.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c84e625f3e2ac895fafd2053fa50af2fbb63ab3cdd812eff2bc4171d9f934bde", size = 5782875, upload-time = "2026-03-31T23:29:16.258Z" },
{ url = "https://files.pythonhosted.org/packages/c5/34/45d167bc18d5d91b9ff95dcd4e24df60d424567611d48191a29bf19fdbc8/couchbase-4.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2619c966b308948900e51f1e4e1488e09ad50b119b1d5c31b697870aa82a6ce", size = 7234591, upload-time = "2026-03-31T23:29:19.148Z" },
{ url = "https://files.pythonhosted.org/packages/41/1f/cc4d1503463cf243959532424a30e79f34aadafde5bcb21754b19b2b9dde/couchbase-4.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:f64a017416958f10a07312a6d39c9b362827854de173fdef9bffdac71c8f3345", size = 4517477, upload-time = "2026-03-31T23:29:21.955Z" },
{ url = "https://files.pythonhosted.org/packages/fc/22/2dd059aa6bc912e4d2f62fbc722493d78582ae286c33fac7a78c3bba6af0/couchbase-4.6.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:35dfbee6f48b9f3eab9d2a07c80747f09d8b4b3d15b312190b3ae88e8e24cb6b", size = 5596715, upload-time = "2026-04-29T21:27:08.06Z" },
{ url = "https://files.pythonhosted.org/packages/28/77/00039e48470ca3413eba056b13f5c7d071b49e558fc8e8ec5ae84c072108/couchbase-4.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99afefbe46792cb45e55747dbd61ca64f806484fc0b1cdd1afa0b909d1a56744", size = 4724349, upload-time = "2026-04-29T21:27:10.272Z" },
{ url = "https://files.pythonhosted.org/packages/9d/f0/80207bdc94b441aae75db99799ec4439e1c483f3cc5b50b4fea0d23b04e8/couchbase-4.6.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:46073464a94a4767dc5888c9ede21c76c82054479ff12914026b6cbf0468c503", size = 5605918, upload-time = "2026-04-29T21:27:13.067Z" },
{ url = "https://files.pythonhosted.org/packages/30/44/2555e2823656bc9329e9bbe4b1ffb20ee5047fe7ffbb4eb2c55909a3fb1b/couchbase-4.6.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a37c8bfe4fcdf0089f40d1e306f9dff72802486a53c1cf530c5fe53031a548", size = 5870083, upload-time = "2026-04-29T21:27:15.449Z" },
{ url = "https://files.pythonhosted.org/packages/c4/92/143b000fbfa6443bf55644537d9b09c07c9ee3150d7b80c64e0164ee969a/couchbase-4.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8afdaeccec56308264fee90c4b53605d09b635154e3205824dbd4c5cb98deff7", size = 7335690, upload-time = "2026-04-29T21:27:18.291Z" },
{ url = "https://files.pythonhosted.org/packages/7a/13/4423cd492e306bef9c9f4d035c0061a906db7dd7961c208a7c6f37c4d3ad/couchbase-4.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:877fc6be2a59b7e851cb0790eccdcbb9fdfac7a951387518938ee67c727419af", size = 4544430, upload-time = "2026-04-29T21:27:20.241Z" },
]
[[package]]
@ -4052,16 +4052,15 @@ wheels = [
[[package]]
name = "mysql-connector-python"
version = "9.6.0"
version = "9.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6f/6e/c89babc7de3df01467d159854414659c885152579903a8220c8db02a3835/mysql_connector_python-9.6.0.tar.gz", hash = "sha256:c453bb55347174d87504b534246fb10c589daf5d057515bf615627198a3c7ef1", size = 12254999, upload-time = "2026-02-10T12:04:52.63Z" }
sdist = { url = "https://files.pythonhosted.org/packages/26/c9/a9446dbebbcdf7d828d0a3be9049607eab6eeffb4e46ef1ee8ac304baede/mysql_connector_python-9.7.0.tar.gz", hash = "sha256:933887e71c871b6e9d8908459fe8303ebcf8feb5cc1e1c49caa6490e525cf78e", size = 12254829, upload-time = "2026-04-30T07:55:39.797Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8f/d9/2a4b4d90b52f4241f0f71618cd4bd8779dd6d18db8058b0a4dd83ec0541c/mysql_connector_python-9.6.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9664e217c72dd6fb700f4c8512af90261f72d2f5d7c00c4e13e4c1e09bfa3d5e", size = 17585672, upload-time = "2026-02-10T12:03:52.955Z" },
{ url = "https://files.pythonhosted.org/packages/33/91/2495835733a054e716a17dc28404748b33f2dc1da1ae4396fb45574adf40/mysql_connector_python-9.6.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:1ed4b5c4761e5333035293e746683890e4ef2e818e515d14023fd80293bc31fa", size = 18452624, upload-time = "2026-02-10T12:03:56.153Z" },
{ url = "https://files.pythonhosted.org/packages/7a/69/e83abbbbf7f8eed855b5a5ff7285bc0afb1199418ac036c7691edf41e154/mysql_connector_python-9.6.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5095758dcb89a6bce2379f349da336c268c407129002b595c5dba82ce387e2a5", size = 34169154, upload-time = "2026-02-10T12:03:58.831Z" },
{ url = "https://files.pythonhosted.org/packages/82/44/67bb61c71f398fbc739d07e8dcadad94e2f655874cb32ae851454066bea0/mysql_connector_python-9.6.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4ae4e7780fad950a4f267dea5851048d160f5b71314a342cdbf30b154f1c74f7", size = 34542947, upload-time = "2026-02-10T12:04:02.408Z" },
{ url = "https://files.pythonhosted.org/packages/ba/39/994c4f7e9c59d3ca534a831d18442ac4c529865db20aeaa4fd94e2af5efd/mysql_connector_python-9.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c180e0b4100d7402e03993bfac5c97d18e01d7ca9d198d742fffc245077f8ffe", size = 16515709, upload-time = "2026-02-10T12:04:04.924Z" },
{ url = "https://files.pythonhosted.org/packages/15/dd/b3250826c29cee7816de4409a2fe5e469a68b9a89f6bfaa5eed74f05532c/mysql_connector_python-9.6.0-py2.py3-none-any.whl", hash = "sha256:44b0fb57207ebc6ae05b5b21b7968a9ed33b29187fe87b38951bad2a334d75d5", size = 480527, upload-time = "2026-02-10T12:04:36.176Z" },
{ url = "https://files.pythonhosted.org/packages/6b/2d/3fb86f8646c07c32b58e9f5bf0975472c62d07efd9e0918f2b5b64ef1f20/mysql_connector_python-9.7.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2eae45230b5cbd783d68bdfe8b05ad9b4ebd06799f8d302a6169d7f025572baf", size = 20270146, upload-time = "2026-04-30T07:54:12.214Z" },
{ url = "https://files.pythonhosted.org/packages/e8/87/f25195a824e8eb33f51113d85ff5bc3a3ec0430f6a537198d7d1a82fa1b4/mysql_connector_python-9.7.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:15106ed73d74487f86de6b1859ff7f362efca7c7f9c494497ccb7439d3139fe6", size = 19830228, upload-time = "2026-04-30T07:54:14.498Z" },
{ url = "https://files.pythonhosted.org/packages/2a/b0/f1ffcc88781a7099a8f32ec61a70d53f83432fd0c1f7be349d44810b65d5/mysql_connector_python-9.7.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:45491d4ce56722cb335e6d0bde2d4f4a98b7073421bd02a04ad5e6220d69e499", size = 21925372, upload-time = "2026-04-30T07:54:16.732Z" },
{ url = "https://files.pythonhosted.org/packages/ae/6a/5488a05a7c56eac2ef127f5f74ccf604cc659d33616a3d48eb82ce35f1f5/mysql_connector_python-9.7.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d5924a76b530159c02f2fe8da4d3c6377ce1f5e195827e8ecbd36124673651d3", size = 21695866, upload-time = "2026-04-30T07:54:19.031Z" },
{ url = "https://files.pythonhosted.org/packages/38/55/4ebb602d270108ea6e81ccdfe4aa0a511fedffdc3ef6c193ac9ff76402fe/mysql_connector_python-9.7.0-py2.py3-none-any.whl", hash = "sha256:af80b1e7179d5c2d983cf62470ad9b134a7e9ef05cf31108ae587f15873530cc", size = 480646, upload-time = "2026-04-30T07:54:43.82Z" },
]
[[package]]
@ -4690,23 +4689,23 @@ wheels = [
[[package]]
name = "pandas"
version = "3.0.2"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
{ name = "python-dateutil" },
{ name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" },
{ name = "pytz" },
{ name = "tzdata" },
]
sdist = { url = "https://files.pythonhosted.org/packages/da/99/b342345300f13440fe9fe385c3c481e2d9a595ee3bab4d3219247ac94e9a/pandas-3.0.2.tar.gz", hash = "sha256:f4753e73e34c8d83221ba58f232433fca2748be8b18dbca02d242ed153945043", size = 4645855, upload-time = "2026-03-31T06:48:30.816Z" }
sdist = { url = "https://files.pythonhosted.org/packages/72/51/48f713c4c728d7c55ef7444ba5ea027c26998d96d1a40953b346438602fc/pandas-2.3.0.tar.gz", hash = "sha256:34600ab34ebf1131a7613a260a61dbe8b62c188ec0ea4c296da7c9a06b004133", size = 4484490, upload-time = "2025-06-05T03:27:54.133Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f3/b0/c20bd4d6d3f736e6bd6b55794e9cd0a617b858eaad27c8f410ea05d953b7/pandas-3.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:232a70ebb568c0c4d2db4584f338c1577d81e3af63292208d615907b698a0f18", size = 10347921, upload-time = "2026-03-31T06:46:33.36Z" },
{ url = "https://files.pythonhosted.org/packages/35/d0/4831af68ce30cc2d03c697bea8450e3225a835ef497d0d70f31b8cdde965/pandas-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:970762605cff1ca0d3f71ed4f3a769ea8f85fc8e6348f6e110b8fea7e6eb5a14", size = 9888127, upload-time = "2026-03-31T06:46:36.253Z" },
{ url = "https://files.pythonhosted.org/packages/61/a9/16ea9346e1fc4a96e2896242d9bc674764fb9049b0044c0132502f7a771e/pandas-3.0.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aff4e6f4d722e0652707d7bcb190c445fe58428500c6d16005b02401764b1b3d", size = 10399577, upload-time = "2026-03-31T06:46:39.224Z" },
{ url = "https://files.pythonhosted.org/packages/c4/a8/3a61a721472959ab0ce865ef05d10b0d6bfe27ce8801c99f33d4fa996e65/pandas-3.0.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef8b27695c3d3dc78403c9a7d5e59a62d5464a7e1123b4e0042763f7104dc74f", size = 10880030, upload-time = "2026-03-31T06:46:42.412Z" },
{ url = "https://files.pythonhosted.org/packages/da/65/7225c0ea4d6ce9cb2160a7fb7f39804871049f016e74782e5dade4d14109/pandas-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f8d68083e49e16b84734eb1a4dcae4259a75c90fb6e2251ab9a00b61120c06ab", size = 11409468, upload-time = "2026-03-31T06:46:45.2Z" },
{ url = "https://files.pythonhosted.org/packages/fa/5b/46e7c76032639f2132359b5cf4c785dd8cf9aea5ea64699eac752f02b9db/pandas-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:32cc41f310ebd4a296d93515fcac312216adfedb1894e879303987b8f1e2b97d", size = 11936381, upload-time = "2026-03-31T06:46:48.293Z" },
{ url = "https://files.pythonhosted.org/packages/7b/8b/721a9cff6fa6a91b162eb51019c6243b82b3226c71bb6c8ef4a9bd65cbc6/pandas-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:a4785e1d6547d8427c5208b748ae2efb64659a21bd82bf440d4262d02bfa02a4", size = 9744993, upload-time = "2026-03-31T06:46:51.488Z" },
{ url = "https://files.pythonhosted.org/packages/d5/18/7f0bd34ae27b28159aa80f2a6799f47fda34f7fb938a76e20c7b7fe3b200/pandas-3.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:08504503f7101300107ecdc8df73658e4347586db5cfdadabc1592e9d7e7a0fd", size = 9056118, upload-time = "2026-03-31T06:46:54.548Z" },
{ url = "https://files.pythonhosted.org/packages/94/46/24192607058dd607dbfacdd060a2370f6afb19c2ccb617406469b9aeb8e7/pandas-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2eb4728a18dcd2908c7fccf74a982e241b467d178724545a48d0caf534b38ebf", size = 11573865, upload-time = "2025-06-05T03:26:46.774Z" },
{ url = "https://files.pythonhosted.org/packages/9f/cc/ae8ea3b800757a70c9fdccc68b67dc0280a6e814efcf74e4211fd5dea1ca/pandas-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9d8c3187be7479ea5c3d30c32a5d73d62a621166675063b2edd21bc47614027", size = 10702154, upload-time = "2025-06-05T16:50:14.439Z" },
{ url = "https://files.pythonhosted.org/packages/d8/ba/a7883d7aab3d24c6540a2768f679e7414582cc389876d469b40ec749d78b/pandas-2.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ff730713d4c4f2f1c860e36c005c7cefc1c7c80c21c0688fd605aa43c9fcf09", size = 11262180, upload-time = "2025-06-05T16:50:17.453Z" },
{ url = "https://files.pythonhosted.org/packages/01/a5/931fc3ad333d9d87b10107d948d757d67ebcfc33b1988d5faccc39c6845c/pandas-2.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba24af48643b12ffe49b27065d3babd52702d95ab70f50e1b34f71ca703e2c0d", size = 11991493, upload-time = "2025-06-05T03:26:51.813Z" },
{ url = "https://files.pythonhosted.org/packages/d7/bf/0213986830a92d44d55153c1d69b509431a972eb73f204242988c4e66e86/pandas-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:404d681c698e3c8a40a61d0cd9412cc7364ab9a9cc6e144ae2992e11a2e77a20", size = 12470733, upload-time = "2025-06-06T00:00:18.651Z" },
{ url = "https://files.pythonhosted.org/packages/a4/0e/21eb48a3a34a7d4bac982afc2c4eb5ab09f2d988bdf29d92ba9ae8e90a79/pandas-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6021910b086b3ca756755e86ddc64e0ddafd5e58e076c72cb1585162e5ad259b", size = 13212406, upload-time = "2025-06-05T03:26:55.992Z" },
{ url = "https://files.pythonhosted.org/packages/1f/d9/74017c4eec7a28892d8d6e31ae9de3baef71f5a5286e74e6b7aad7f8c837/pandas-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:094e271a15b579650ebf4c5155c05dcd2a14fd4fdd72cf4854b2f7ad31ea30be", size = 10976199, upload-time = "2025-06-05T03:26:59.594Z" },
]
[package.optional-dependencies]
@ -5288,7 +5287,7 @@ crypto = [
[[package]]
name = "pymilvus"
version = "2.6.12"
version = "2.6.14"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cachetools" },
@ -5300,9 +5299,9 @@ dependencies = [
{ name = "requests" },
{ name = "setuptools" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2c/d7/c5d1381248a33975ccc864a0f980f93270ecc35354de8646c8a16443cccb/pymilvus-2.6.12.tar.gz", hash = "sha256:8323e990dc305e607fef525498eb779e42940a69e0691dde009cd02d48845f7a", size = 1584521, upload-time = "2026-04-09T07:49:11.374Z" }
sdist = { url = "https://files.pythonhosted.org/packages/59/8c/ad0277685b6f7bc63a626ab77973e31f38e77e0d8a2af99733cf6b6cf138/pymilvus-2.6.14.tar.gz", hash = "sha256:078fb16731569b2fd8b82436e295f70ee2a682c8892ed0e9c919c9cbc9d0dfbd", size = 1604893, upload-time = "2026-05-15T14:11:47.052Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/5d/44b0fa94c91503381e6f12298277f84f8e7b0bb00715ab89fc273c4d681e/pymilvus-2.6.12-py3-none-any.whl", hash = "sha256:69051b8b62712f157b2b50aeb7bde7fd7cdb5940aac0122094eb3cd58bc20f0d", size = 315183, upload-time = "2026-04-09T07:49:09.013Z" },
{ url = "https://files.pythonhosted.org/packages/df/0e/2807bf330edb41537caabf483fa4128e429b8fa5bcfa9bf67effcafa722e/pymilvus-2.6.14-py3-none-any.whl", hash = "sha256:01694978c2bbbf4bcc5ef29dc788e480245154a38eae32da7184959ea4b501fb", size = 340221, upload-time = "2026-05-15T14:11:45.052Z" },
]
[[package]]

View File

@ -1571,6 +1571,19 @@
"count": 1
}
},
"web/app/components/base/pagination/index.tsx": {
"no-restricted-imports": {
"count": 1
},
"unicorn/prefer-number-properties": {
"count": 1
}
},
"web/app/components/base/pagination/type.ts": {
"ts/no-empty-object-type": {
"count": 1
}
},
"web/app/components/base/prompt-editor/index.stories.tsx": {
"no-console": {
"count": 1

View File

@ -40,16 +40,16 @@ Importing from `@langgenius/dify-ui` (no subpath) is intentionally not supported
## Primitives
| Category | Subpath | Notes |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| Actions | `./button` | Design-system CTA primitive with `cva` variants. |
| Feedback | `./meter`, `./toast` | Meter is inline status; Toast owns the `z-60` layer. |
| Form | `./form`, `./field`, `./fieldset`, `./input`, `./checkbox`, `./checkbox-group`, `./radio`, `./radio-group`, `./number-field`, `./select`, `./slider`, `./switch` | Native form boundary, field semantics, and controls. |
| Layout | `./scroll-area` | Custom-styled scrollbar over the host viewport. |
| Media | `./avatar` | Avatar root, image, and fallback primitives. |
| Navigation | `./pagination`, `./tabs`, `./toggle-group` | Pagination for page navigation; Tabs for panels; ToggleGroup for segmented modes. |
| Overlay / menu | `./alert-dialog`, `./context-menu`, `./dialog`, `./drawer`, `./dropdown-menu`, `./popover`, `./preview-card`, `./tooltip` | Portalled. See [Overlay & portal contract] below. |
| Search / pickers | `./autocomplete`, `./combobox`, `./select` | Search input, searchable picker, and closed picker. |
| Category | Subpath | Notes |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| Actions | `./button` | Design-system CTA primitive with `cva` variants. |
| Feedback | `./meter`, `./toast` | Meter is inline status; Toast owns the `z-60` layer. |
| Form | `./form`, `./field`, `./fieldset`, `./input`, `./checkbox`, `./checkbox-group`, `./radio`, `./radio-group`, `./number-field`, `./select`, `./slider`, `./switch` | Native form boundary, field semantics, and controls. |
| Layout | `./scroll-area` | Custom-styled scrollbar over the host viewport. |
| Media | `./avatar` | Avatar root, image, and fallback primitives. |
| Navigation | `./tabs`, `./toggle-group` | Tabs for panels; ToggleGroup for segmented modes. |
| Overlay / menu | `./alert-dialog`, `./context-menu`, `./dialog`, `./drawer`, `./dropdown-menu`, `./popover`, `./preview-card`, `./tooltip` | Portalled. See [Overlay & portal contract] below. |
| Search / pickers | `./autocomplete`, `./combobox`, `./select` | Search input, searchable picker, and closed picker. |
Utilities:

View File

@ -77,10 +77,6 @@
"types": "./src/number-field/index.tsx",
"import": "./src/number-field/index.tsx"
},
"./pagination": {
"types": "./src/pagination/index.tsx",
"import": "./src/pagination/index.tsx"
},
"./radio": {
"types": "./src/radio/index.tsx",
"import": "./src/radio/index.tsx"

View File

@ -1,293 +0,0 @@
import { render } from 'vitest-browser-react'
import {
Pagination,
PaginationContent,
PaginationNavigation,
PaginationNext,
PaginationPage,
PaginationPageJump,
PaginationPageList,
PaginationPageSize,
PaginationPrevious,
PaginationRoot,
PaginationSkeleton,
} from '../index'
const asHTMLElement = (element: HTMLElement | SVGElement) => element as HTMLElement
async function renderPagination({
page = 2,
totalPages = 200,
onPageChange = vi.fn(),
pageSize = 25,
onPageSizeChange = vi.fn(),
}: {
page?: number
totalPages?: number
onPageChange?: (page: number) => void
pageSize?: number
onPageSizeChange?: (pageSize: number) => void
} = {}) {
const screen = await render(
<PaginationRoot
page={page}
totalPages={totalPages}
onPageChange={onPageChange}
data-testid="pagination"
>
<PaginationContent data-testid="content">
<PaginationNavigation data-testid="controls">
<PaginationPrevious />
<PaginationPageJump />
<PaginationNext />
</PaginationNavigation>
<PaginationPageList data-testid="pages" />
<PaginationPageSize
value={pageSize}
options={[10, 25, 50]}
onValueChange={onPageSizeChange}
/>
</PaginationContent>
</PaginationRoot>,
)
return {
screen,
onPageChange,
onPageSizeChange,
}
}
describe('Pagination primitive', () => {
it('renders the Figma-aligned pagination structure with semantic navigation', async () => {
const { screen } = await renderPagination()
await expect.element(screen.getByRole('navigation', { name: 'Pagination' })).toHaveAttribute('data-page', '2')
await expect.element(screen.getByTestId('content')).toHaveClass('grid', 'grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)]')
await expect.element(screen.getByTestId('controls')).toHaveClass('justify-self-start', 'rounded-[10px]', 'bg-background-section-burn')
await expect.element(screen.getByRole('list')).toHaveClass('col-start-2', 'justify-self-center')
expect(screen.getByRole('group', { name: 'Items per page' }).element().parentElement).toHaveClass('col-start-3', 'justify-self-end')
await expect.element(screen.getByRole('button', { name: 'Previous page' })).toBeInTheDocument()
await expect.element(screen.getByRole('button', { name: 'Next page' })).toBeInTheDocument()
await expect.element(screen.getByRole('button', { name: 'Edit page number, current page 2 of 200' })).toHaveTextContent('2/200')
await expect.element(screen.getByRole('button', { name: 'Edit page number, current page 2 of 200' })).toHaveClass('h-7', 'px-2')
expect(screen.getByRole('button', { name: 'Edit page number, current page 2 of 200' }).element()).not.toHaveClass('min-w-14')
await expect.element(screen.getByRole('button', { name: 'Page 2, current page' })).toHaveAttribute('aria-current', 'page')
await expect.element(screen.getByRole('button', { name: 'Page 2, current page' })).toHaveClass('bg-components-button-tertiary-bg')
await expect.element(screen.getByText('…')).toBeInTheDocument()
})
it('uses one-based page changes for previous, next, and page buttons', async () => {
const { screen, onPageChange } = await renderPagination({ page: 4 })
asHTMLElement(screen.getByRole('button', { name: 'Previous page' }).element()).click()
asHTMLElement(screen.getByRole('button', { name: 'Next page' }).element()).click()
asHTMLElement(screen.getByRole('button', { name: 'Go to page 6' }).element()).click()
expect(onPageChange).toHaveBeenNthCalledWith(1, 3)
expect(onPageChange).toHaveBeenNthCalledWith(2, 5)
expect(onPageChange).toHaveBeenNthCalledWith(3, 6)
})
it('disables previous at the first page', async () => {
const { screen } = await renderPagination({ page: 1, totalPages: 10 })
await expect.element(screen.getByRole('button', { name: 'Previous page' })).toBeDisabled()
})
it('disables next at the last page', async () => {
const { screen } = await renderPagination({ page: 10, totalPages: 10 })
await expect.element(screen.getByRole('button', { name: 'Next page' })).toBeDisabled()
})
it('clamps invalid root page values without exposing invalid state', async () => {
const { screen } = await renderPagination({ page: 999, totalPages: 10 })
await expect.element(screen.getByRole('navigation', { name: 'Pagination' })).toHaveAttribute('data-page', '10')
await expect.element(screen.getByRole('button', { name: 'Page 10, current page' })).toHaveAttribute('aria-current', 'page')
})
it('switches the page summary into a selected labelled number field', async () => {
const { screen } = await renderPagination()
asHTMLElement(screen.getByRole('button', { name: 'Edit page number, current page 2 of 200' }).element()).click()
await expect.element(screen.getByRole('textbox', { name: 'Page number' })).toBeInTheDocument()
const input = asHTMLElement(screen.getByRole('textbox', { name: 'Page number' }).element()) as HTMLInputElement
await expect.element(screen.getByRole('textbox', { name: 'Page number' })).toHaveValue('2')
await expect.element(screen.getByRole('textbox', { name: 'Page number' })).toHaveClass('text-center', 'tabular-nums')
expect(input.parentElement?.parentElement?.parentElement).toHaveAttribute('data-page-summary', '2/200')
await vi.waitFor(() => {
expect(input.selectionStart).toBe(0)
expect(input.selectionEnd).toBe(1)
})
})
it('returns to the summary button when the page input loses focus', async () => {
const { screen } = await renderPagination()
asHTMLElement(screen.getByRole('button', { name: 'Edit page number, current page 2 of 200' }).element()).click()
await expect.element(screen.getByRole('textbox', { name: 'Page number' })).toBeInTheDocument()
asHTMLElement(screen.getByRole('textbox', { name: 'Page number' }).element()).blur()
await expect.element(screen.getByRole('button', { name: 'Edit page number, current page 2 of 200' })).toBeInTheDocument()
})
it('commits the page input editing mode with Enter', async () => {
const { screen } = await renderPagination()
asHTMLElement(screen.getByRole('button', { name: 'Edit page number, current page 2 of 200' }).element()).click()
await expect.element(screen.getByRole('textbox', { name: 'Page number' })).toBeInTheDocument()
const input = asHTMLElement(screen.getByRole('textbox', { name: 'Page number' }).element()) as HTMLInputElement
await vi.waitFor(() => {
expect(document.activeElement).toBe(input)
})
input.dispatchEvent(new KeyboardEvent('keydown', {
key: 'Enter',
bubbles: true,
cancelable: true,
}))
await expect.element(screen.getByRole('button', { name: 'Edit page number, current page 2 of 200' })).toBeInTheDocument()
})
it('cancels the page input editing mode with Escape', async () => {
const { screen, onPageChange } = await renderPagination()
asHTMLElement(screen.getByRole('button', { name: 'Edit page number, current page 2 of 200' }).element()).click()
await expect.element(screen.getByRole('textbox', { name: 'Page number' })).toBeInTheDocument()
const input = asHTMLElement(screen.getByRole('textbox', { name: 'Page number' }).element()) as HTMLInputElement
await vi.waitFor(() => {
expect(document.activeElement).toBe(input)
})
input.dispatchEvent(new KeyboardEvent('keydown', {
key: 'Escape',
bubbles: true,
cancelable: true,
}))
const summaryButton = screen.getByRole('button', { name: 'Edit page number, current page 2 of 200' })
await expect.element(summaryButton).toBeInTheDocument()
await vi.waitFor(() => {
expect(document.activeElement).toBe(summaryButton.element())
})
expect(onPageChange).not.toHaveBeenCalled()
})
it('uses Base UI ToggleGroup semantics for page size', async () => {
const { screen, onPageSizeChange } = await renderPagination()
await expect.element(screen.getByRole('group', { name: 'Items per page' })).toHaveClass('bg-components-segmented-control-bg-normal')
await expect.element(screen.getByText('Items per page')).toHaveClass('opacity-0', 'group-hover/page-size:opacity-100', 'group-focus-within/page-size:opacity-100')
await expect.element(screen.getByRole('button', { name: '25' })).toHaveAttribute('aria-pressed', 'true')
await expect.element(screen.getByRole('button', { name: '25' })).toHaveClass('data-pressed:text-text-primary')
asHTMLElement(screen.getByRole('button', { name: '50' }).element()).click()
expect(onPageSizeChange).toHaveBeenCalledWith(50)
})
it('renders the complete pagination bar with optional page size controls', async () => {
const onPageSizeChange = vi.fn()
const screen = await render(
<Pagination
page={2}
totalPages={10}
onPageChange={vi.fn()}
pageSize={{
value: 25,
options: [10, 25, 50],
onValueChange: onPageSizeChange,
}}
/>,
)
await expect.element(screen.getByRole('button', { name: 'Edit page number, current page 2 of 10' })).toBeInTheDocument()
await expect.element(screen.getByRole('group', { name: 'Items per page' })).toBeInTheDocument()
})
it('uses a localized action label for editing the page number', async () => {
const screen = await render(
<Pagination
page={2}
totalPages={10}
onPageChange={vi.fn()}
labels={{
editPageNumber: (page, totalPages) => `Change page, current page ${page} of ${totalPages}`,
}}
/>,
)
await expect.element(screen.getByRole('button', { name: 'Change page, current page 2 of 10' })).toBeInTheDocument()
})
it('keeps facade page numbers centered when page size controls are omitted', async () => {
const screen = await render(
<Pagination
page={2}
totalPages={10}
onPageChange={vi.fn()}
/>,
)
await expect.element(screen.getByRole('navigation', { name: 'Pagination' })).toBeInTheDocument()
expect(screen.container.querySelector('nav[aria-label="Pagination"] > div')).toHaveClass('grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)]')
await expect.element(screen.getByRole('list')).toHaveClass('col-start-2', 'justify-self-center')
})
it('does not expose invalid page controls when there are no pages', async () => {
const screen = await render(
<Pagination
page={1}
totalPages={0}
onPageChange={vi.fn()}
/>,
)
expect(screen.container.querySelector('nav[aria-label="Pagination"]')).not.toBeInTheDocument()
expect(screen.container.querySelector('button[aria-label*="current page 1 of 0"]')).not.toBeInTheDocument()
})
it('omits compound page jump and page list content for empty pagination state', async () => {
const { screen } = await renderPagination({ page: 1, totalPages: 0 })
await expect.element(screen.getByRole('navigation', { name: 'Pagination' })).toHaveAttribute('data-page', '1')
expect(screen.container.querySelector('button[aria-label*="current page 1 of 0"]')).not.toBeInTheDocument()
expect(screen.container.querySelector('button[aria-label="Previous page"]')).not.toBeInTheDocument()
expect(screen.container.querySelector('button[aria-label="Next page"]')).not.toBeInTheDocument()
expect(screen.container.querySelector('ol')).not.toBeInTheDocument()
})
it('allows custom page rendering while keeping the shared context', async () => {
const onPageChange = vi.fn()
const screen = await render(
<PaginationRoot page={3} totalPages={5} onPageChange={onPageChange}>
<ol>
<li>
<PaginationPage page={4} className="custom-page">
Four
</PaginationPage>
</li>
</ol>
</PaginationRoot>,
)
asHTMLElement(screen.getByRole('button', { name: 'Go to page 4' }).element()).click()
await expect.element(screen.getByRole('button', { name: 'Go to page 4' })).toHaveClass('custom-page')
expect(onPageChange).toHaveBeenCalledWith(4)
})
it('renders a non-interactive loading skeleton', async () => {
const screen = await render(<PaginationSkeleton data-testid="skeleton" />)
await expect.element(screen.getByTestId('skeleton')).toHaveAttribute('aria-hidden', 'true')
await expect.element(screen.getByTestId('skeleton')).toHaveClass('select-none')
})
})

View File

@ -1,93 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import type { ComponentProps } from 'react'
import { useState } from 'react'
import {
Pagination,
PaginationSkeleton,
} from '.'
function PaginationExample({
initialPage = 2,
initialPageSize = 25,
totalPages = 200,
}: {
initialPage?: number
initialPageSize?: number
totalPages?: number
}) {
const [page, setPage] = useState(initialPage)
const [pageSize, setPageSize] = useState(initialPageSize)
return (
<Pagination
page={page}
totalPages={totalPages}
onPageChange={setPage}
pageSize={{
value: pageSize,
options: [10, 25, 50],
onValueChange: setPageSize,
}}
/>
)
}
function PaginationDemo(props: ComponentProps<typeof PaginationExample>) {
return (
<div className="w-236 max-w-full bg-components-panel-bg px-16 py-10">
<PaginationExample {...props} />
</div>
)
}
function DesignSpecDemo() {
return (
<div className="flex w-236 max-w-full flex-col gap-6 bg-components-panel-bg px-16 py-10">
<PaginationExample />
<PaginationExample initialPage={2} initialPageSize={25} />
<PaginationExample initialPage={2} initialPageSize={25} />
<PaginationExample initialPage={2} initialPageSize={25} />
</div>
)
}
const meta = {
title: 'Base/UI/Pagination',
component: PaginationDemo,
parameters: {
layout: 'centered',
docs: {
description: {
component: 'Compound pagination primitive for list navigation. It combines semantic page buttons, a NumberField-backed page jump summary, and a ToggleGroup-backed page-size selector.',
},
},
},
args: {
initialPage: 2,
initialPageSize: 25,
totalPages: 200,
},
tags: ['autodocs'],
} satisfies Meta<typeof PaginationDemo>
export default meta
type Story = StoryObj<typeof meta>
export const Playground: Story = {
render: () => <PaginationDemo />,
}
export const DesignSpec: Story = {
render: () => <DesignSpecDemo />,
parameters: {
docs: {
description: {
story: 'Pagination rows with default, hover-like, focused, page-size, and skeleton examples.',
},
},
},
}
export const Loading: Story = {
render: () => <PaginationSkeleton />,
}

View File

@ -1,655 +0,0 @@
'use client'
import type { Button as BaseButtonNS } from '@base-ui/react/button'
import type { ReactNode } from 'react'
import { Button as BaseButton } from '@base-ui/react/button'
import { mergeProps } from '@base-ui/react/merge-props'
import { useRender } from '@base-ui/react/use-render'
import { createContext, useContext, useMemo, useRef, useState } from 'react'
import { cn } from '../cn'
import {
NumberField,
NumberFieldGroup,
NumberFieldInput,
} from '../number-field'
import {
ToggleGroup,
ToggleGroupItem,
} from '../toggle-group'
type PageItem = number | 'ellipsis-start' | 'ellipsis-end'
type PaginationContextValue = {
page: number
totalPages: number
hasPages: boolean
disabled: boolean
onPageChange: (page: number) => void
items: PageItem[]
}
const PaginationContext = createContext<PaginationContextValue | null>(null)
function usePaginationContext(component: string) {
const context = useContext(PaginationContext)
if (!context)
throw new Error(`${component} must be used inside PaginationRoot.`)
return context
}
function clampPage(page: number, totalPages: number) {
if (!Number.isFinite(page))
return 1
return Math.min(Math.max(Math.trunc(page), 1), Math.max(totalPages, 1))
}
function range(start: number, end: number) {
if (end < start)
return []
return Array.from({ length: end - start + 1 }, (_, index) => start + index)
}
type GetPageItemsOptions = {
page: number
totalPages: number
siblingCount: number
boundaryCount: number
visiblePageCount: number
}
function getPageItems({
page,
totalPages,
siblingCount,
boundaryCount,
visiblePageCount,
}: GetPageItemsOptions): PageItem[] {
if (totalPages <= 0)
return []
const normalizedPage = clampPage(page, totalPages)
const normalizedBoundaryCount = Math.max(Math.trunc(boundaryCount), 1)
const normalizedSiblingCount = Math.max(Math.trunc(siblingCount), 0)
const windowSize = Math.max(
Math.trunc(visiblePageCount),
normalizedSiblingCount * 2 + 1,
)
if (totalPages <= windowSize + normalizedBoundaryCount)
return range(1, totalPages)
const nearStartEnd = windowSize
const nearEndStart = totalPages - windowSize + 1
const middleStart = Math.max(
normalizedBoundaryCount + 1,
normalizedPage - normalizedSiblingCount,
)
const middleEnd = Math.min(
totalPages - normalizedBoundaryCount,
normalizedPage + normalizedSiblingCount,
)
const windowPages = normalizedPage <= nearStartEnd - normalizedSiblingCount
? range(1, nearStartEnd)
: normalizedPage >= nearEndStart + normalizedSiblingCount
? range(nearEndStart, totalPages)
: range(middleStart, middleEnd)
const pageSet = new Set([
...range(1, normalizedBoundaryCount),
...windowPages,
...range(totalPages - normalizedBoundaryCount + 1, totalPages),
])
const pages = Array.from(pageSet)
.filter(item => item >= 1 && item <= totalPages)
.sort((a, b) => a - b)
return pages.reduce<PageItem[]>((items, item, index) => {
const previous = pages[index - 1]
if (previous && item - previous === 2)
items.push(previous + 1)
else if (previous && item - previous > 2)
items.push(item < normalizedPage ? 'ellipsis-start' : 'ellipsis-end')
items.push(item)
return items
}, [])
}
type PaginationRootState = {
page: number
totalPages: number
hasPages: boolean
disabled: boolean
}
export type PaginationRootProps = Omit<
useRender.ComponentProps<'nav', PaginationRootState>,
'onChange'
> & {
page: number
totalPages: number
onPageChange: (page: number) => void
siblingCount?: number
boundaryCount?: number
visiblePageCount?: number
}
export function PaginationRoot({
page,
totalPages,
onPageChange,
siblingCount = 1,
boundaryCount = 1,
visiblePageCount = 8,
render,
children,
className,
...props
}: PaginationRootProps) {
const normalizedTotalPages = Math.max(Math.trunc(totalPages), 0)
const normalizedPage = clampPage(page, normalizedTotalPages)
const hasPages = normalizedTotalPages > 0
const disabled = normalizedTotalPages <= 1
const items = useMemo(() => getPageItems({
page: normalizedPage,
totalPages: normalizedTotalPages,
siblingCount,
boundaryCount,
visiblePageCount,
}), [
boundaryCount,
normalizedPage,
normalizedTotalPages,
siblingCount,
visiblePageCount,
])
const context = useMemo<PaginationContextValue>(() => ({
page: normalizedPage,
totalPages: normalizedTotalPages,
hasPages,
disabled,
onPageChange: nextPage => onPageChange(clampPage(nextPage, normalizedTotalPages)),
items,
}), [disabled, hasPages, items, normalizedPage, normalizedTotalPages, onPageChange])
const defaultProps: useRender.ElementProps<'nav'> = {
'aria-label': 'Pagination',
'className': cn('flex w-full min-w-0 items-center justify-between px-6 py-3 select-none', className),
'children': (
<PaginationContext.Provider value={context}>
{children}
</PaginationContext.Provider>
),
}
return useRender({
defaultTagName: 'nav',
render,
state: {
page: normalizedPage,
totalPages: normalizedTotalPages,
hasPages,
disabled,
},
props: mergeProps<'nav'>(defaultProps, props),
})
}
export type PaginationNavigationProps = useRender.ComponentProps<'div'>
export type PaginationContentProps = useRender.ComponentProps<'div'>
export function PaginationContent({
render,
className,
...props
}: PaginationContentProps) {
const defaultProps: useRender.ElementProps<'div'> = {
className: cn('grid w-full min-w-0 grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center gap-2', className),
}
return useRender({
defaultTagName: 'div',
render,
props: mergeProps<'div'>(defaultProps, props),
})
}
export function PaginationNavigation({
render,
className,
...props
}: PaginationNavigationProps) {
const defaultProps: useRender.ElementProps<'div'> = {
className: cn('flex shrink-0 items-center justify-self-start gap-0.5 rounded-[10px] bg-background-section-burn p-0.5', className),
}
return useRender({
defaultTagName: 'div',
render,
props: mergeProps<'div'>(defaultProps, props),
})
}
type PaginationButtonProps = Omit<BaseButtonNS.Props, 'children'> & {
children?: ReactNode
}
const paginationArrowButtonClassName = [
'inline-flex size-7 shrink-0 touch-manipulation items-center justify-center rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg text-components-button-secondary-text shadow-xs outline-hidden backdrop-blur-[10px] transition-[background-color,border-color,color,box-shadow]',
'hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover',
'focus-visible:ring-2 focus-visible:ring-components-input-border-hover',
'disabled:cursor-not-allowed disabled:border-components-button-secondary-border-disabled disabled:bg-components-button-secondary-bg-disabled disabled:text-components-button-secondary-text-disabled disabled:shadow-none',
'motion-reduce:transition-none',
]
export function PaginationPrevious({
className,
children,
'aria-label': ariaLabel,
...props
}: PaginationButtonProps) {
const pagination = usePaginationContext('PaginationPrevious')
if (!pagination.hasPages)
return null
const disabled = props.disabled || pagination.page <= 1 || pagination.disabled
return (
<BaseButton
{...props}
type="button"
aria-label={ariaLabel ?? 'Previous page'}
className={cn(paginationArrowButtonClassName, className)}
disabled={disabled}
onClick={(event) => {
props.onClick?.(event)
if (!event.defaultPrevented && !disabled)
pagination.onPageChange(pagination.page - 1)
}}
>
{children ?? <span className="i-ri-arrow-left-line size-4" aria-hidden="true" />}
</BaseButton>
)
}
export function PaginationNext({
className,
children,
'aria-label': ariaLabel,
...props
}: PaginationButtonProps) {
const pagination = usePaginationContext('PaginationNext')
if (!pagination.hasPages)
return null
const disabled = props.disabled || pagination.page >= pagination.totalPages || pagination.disabled
return (
<BaseButton
{...props}
type="button"
aria-label={ariaLabel ?? 'Next page'}
className={cn(paginationArrowButtonClassName, className)}
disabled={disabled}
onClick={(event) => {
props.onClick?.(event)
if (!event.defaultPrevented && !disabled)
pagination.onPageChange(pagination.page + 1)
}}
>
{children ?? <span className="i-ri-arrow-right-line size-4" aria-hidden="true" />}
</BaseButton>
)
}
export type PaginationPageJumpProps = Omit<BaseButtonNS.Props, 'children'> & {
inputLabel?: string
children?: ReactNode
}
export function PaginationPageJump({
className,
inputLabel = 'Page number',
children,
'aria-label': ariaLabel,
...props
}: PaginationPageJumpProps) {
const pagination = usePaginationContext('PaginationPageJump')
const [editing, setEditing] = useState(false)
const summaryButtonRef = useRef<HTMLButtonElement | null>(null)
if (!pagination.hasPages)
return null
if (editing) {
return (
<span
data-page-summary={`${pagination.page}/${pagination.totalPages}`}
className="inline-grid h-7 system-xs-medium tabular-nums after:invisible after:col-start-1 after:row-start-1 after:py-1.5 after:pr-3 after:pl-2 after:content-[attr(data-page-summary)]"
>
<NumberField
key={pagination.page}
className="col-start-1 row-start-1 w-full"
defaultValue={pagination.page}
min={1}
max={Math.max(pagination.totalPages, 1)}
onValueCommitted={(value) => {
if (value !== null)
pagination.onPageChange(value)
setEditing(false)
}}
>
<NumberFieldGroup
className="h-7 w-full min-w-0 rounded-lg border-[0.5px] border-components-input-border-active bg-components-input-bg-active shadow-xs"
>
<NumberFieldInput
aria-label={inputLabel}
autoFocus
className="px-2 py-1.5 text-center system-xs-medium tabular-nums"
onBlur={() => requestAnimationFrame(() => setEditing(false))}
onFocus={(event) => {
const input = event.currentTarget
requestAnimationFrame(() => input.select())
}}
onKeyDown={(event) => {
if (event.key === 'Enter') {
event.preventDefault()
event.currentTarget.blur()
return
}
if (event.key === 'Escape') {
event.preventDefault()
setEditing(false)
requestAnimationFrame(() => summaryButtonRef.current?.focus())
}
}}
/>
</NumberFieldGroup>
</NumberField>
</span>
)
}
return (
<BaseButton
{...props}
ref={summaryButtonRef}
type="button"
aria-label={ariaLabel ?? `Edit page number, current page ${pagination.page} of ${pagination.totalPages}`}
className={cn(
'inline-flex h-7 touch-manipulation items-center justify-center gap-0.5 rounded-lg px-2 py-1.5 system-xs-medium tabular-nums text-text-secondary outline-hidden transition-colors hover:cursor-text hover:bg-state-base-hover-alt focus-visible:ring-2 focus-visible:ring-components-input-border-hover motion-reduce:transition-none',
className,
)}
onClick={(event) => {
props.onClick?.(event)
if (!event.defaultPrevented)
setEditing(true)
}}
>
{children ?? (
<>
<span>{pagination.page}</span>
<span className="text-text-quaternary">/</span>
<span>{pagination.totalPages}</span>
</>
)}
</BaseButton>
)
}
export type PaginationPageListProps = useRender.ComponentProps<'ol'>
export function PaginationPageList({
render,
className,
...props
}: PaginationPageListProps) {
const pagination = usePaginationContext('PaginationPageList')
if (!pagination.hasPages)
return null
const defaultProps: useRender.ElementProps<'ol'> = {
className: cn('col-start-2 flex min-w-0 list-none items-center justify-self-center gap-0.5', className),
children: pagination.items.map(item => (
<li key={item}>
{typeof item === 'number'
? <PaginationPage page={item} />
: <PaginationEllipsis />}
</li>
)),
}
return useRender({
defaultTagName: 'ol',
render,
props: mergeProps<'ol'>(defaultProps, props),
})
}
export type PaginationPageProps = Omit<BaseButtonNS.Props, 'children'> & {
page: number
children?: ReactNode
}
export function PaginationPage({
page,
className,
children,
'aria-label': ariaLabel,
...props
}: PaginationPageProps) {
const pagination = usePaginationContext('PaginationPage')
const current = page === pagination.page
return (
<BaseButton
{...props}
type="button"
aria-current={current ? 'page' : undefined}
aria-label={ariaLabel ?? (current ? `Page ${page}, current page` : `Go to page ${page}`)}
className={cn(
'inline-flex h-8 min-w-8 touch-manipulation items-center justify-center rounded-lg px-1 py-2 system-sm-medium tabular-nums text-text-tertiary outline-hidden transition-colors hover:bg-components-button-ghost-bg-hover hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-components-input-border-hover',
current && 'bg-components-button-tertiary-bg text-components-button-tertiary-text hover:bg-components-button-ghost-bg-hover',
'motion-reduce:transition-none',
className,
)}
onClick={(event) => {
props.onClick?.(event)
if (!event.defaultPrevented)
pagination.onPageChange(page)
}}
>
{children ?? page}
</BaseButton>
)
}
export type PaginationEllipsisProps = useRender.ComponentProps<'span'>
export function PaginationEllipsis({
render,
className,
...props
}: PaginationEllipsisProps) {
const defaultProps: useRender.ElementProps<'span'> = {
'aria-hidden': true,
'className': cn('flex size-8 items-center justify-center px-1 py-2 system-sm-medium text-text-tertiary', className),
'children': '…',
}
return useRender({
defaultTagName: 'span',
render,
props: mergeProps<'span'>(defaultProps, props),
})
}
export type PaginationPageSizeProps<Value extends number = number> = {
'value': Value
'options': readonly Value[]
'onValueChange': (value: Value) => void
'label'?: ReactNode
'aria-label'?: string
'className'?: string
}
export function PaginationPageSize<Value extends number = number>({
value,
options,
onValueChange,
label = 'Items per page',
'aria-label': ariaLabel = 'Items per page',
className,
}: PaginationPageSizeProps<Value>) {
return (
<div className={cn('group/page-size col-start-3 flex shrink-0 items-center justify-end justify-self-end gap-2', className)}>
<div className="w-13 shrink-0 text-end system-2xs-regular-uppercase text-text-tertiary opacity-0 transition-opacity group-hover/page-size:opacity-100 group-focus-within/page-size:opacity-100 motion-reduce:transition-none">
{label}
</div>
<ToggleGroup
value={[String(value)]}
aria-label={ariaLabel}
onValueChange={(nextValue) => {
const [selectedValue] = nextValue
if (!selectedValue)
return
const selectedOption = options.find(option => String(option) === selectedValue)
if (selectedOption !== undefined)
onValueChange(selectedOption)
}}
>
{options.map(option => (
<ToggleGroupItem
key={option}
value={String(option)}
className="min-w-9 data-pressed:text-text-primary"
>
{option}
</ToggleGroupItem>
))}
</ToggleGroup>
</div>
)
}
export type PaginationLabels = {
previous?: string
next?: string
editPageNumber?: (page: number, totalPages: number) => string
pageNumberInput?: string
}
export type PaginationPageSizeConfig<Value extends number = number> = {
value: Value
options: readonly Value[]
onValueChange: (value: Value) => void
label?: ReactNode
ariaLabel?: string
}
export type PaginationProps<Value extends number = number> = Omit<PaginationRootProps, 'children'> & {
labels?: PaginationLabels
pageSize?: PaginationPageSizeConfig<Value>
}
export function Pagination<Value extends number = number>({
labels,
pageSize,
page,
totalPages,
onPageChange,
...props
}: PaginationProps<Value>) {
const normalizedTotalPages = Math.max(Math.trunc(totalPages), 0)
const normalizedPage = clampPage(page, normalizedTotalPages)
const editPageNumber = labels?.editPageNumber?.(normalizedPage, normalizedTotalPages)
if (normalizedTotalPages <= 0)
return null
return (
<PaginationRoot
page={page}
totalPages={totalPages}
onPageChange={onPageChange}
{...props}
>
<PaginationContent>
<PaginationNavigation>
<PaginationPrevious aria-label={labels?.previous} />
<PaginationPageJump
aria-label={editPageNumber}
inputLabel={labels?.pageNumberInput}
/>
<PaginationNext aria-label={labels?.next} />
</PaginationNavigation>
<PaginationPageList />
{pageSize && (
<PaginationPageSize
value={pageSize.value}
options={pageSize.options}
onValueChange={pageSize.onValueChange}
label={pageSize.label}
aria-label={pageSize.ariaLabel}
/>
)}
</PaginationContent>
</PaginationRoot>
)
}
export type PaginationSkeletonProps = useRender.ComponentProps<'div'>
export function PaginationSkeleton({
render,
className,
...props
}: PaginationSkeletonProps) {
const defaultProps: useRender.ElementProps<'div'> = {
'aria-hidden': true,
'className': cn('flex w-full min-w-0 items-center justify-between px-6 py-3 select-none', className),
'children': (
<div className="grid w-full min-w-0 grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center gap-2">
<div className="flex shrink-0 items-center justify-self-start gap-0.5 rounded-[10px] bg-background-section-burn p-0.5">
<div className="size-7 animate-pulse rounded-lg bg-state-base-hover motion-reduce:animate-none" />
<div className="h-7 min-w-14 animate-pulse rounded-lg bg-state-base-hover motion-reduce:animate-none" />
<div className="size-7 animate-pulse rounded-lg bg-state-base-hover motion-reduce:animate-none" />
</div>
<div className="col-start-2 flex items-center justify-self-center gap-0.5">
{range(1, 8).map(item => (
<div key={item} className="h-8 min-w-8 animate-pulse rounded-lg bg-state-base-hover motion-reduce:animate-none" />
))}
</div>
<div className="col-start-3 flex shrink-0 items-center justify-self-end">
<div className="h-8 w-28 animate-pulse rounded-[10px] bg-state-base-hover motion-reduce:animate-none" />
</div>
</div>
),
}
return useRender({
defaultTagName: 'div',
render,
props: mergeProps<'div'>(defaultProps, props),
})
}

View File

@ -207,16 +207,6 @@ describe('Select wrappers', () => {
expect(screen.getByRole('combobox', { name: 'city select' }).element().className).toContain('data-popup-open:bg-state-base-hover-alt')
})
it('should include keyboard focus ring classes', async () => {
const screen = await renderOpenSelect()
await expect.element(screen.getByRole('combobox', { name: 'city select' })).toHaveClass(
'focus-visible:ring-1',
'focus-visible:ring-components-input-border-active',
'focus-visible:ring-inset',
)
})
})
describe('SelectContent', () => {

View File

@ -24,7 +24,6 @@ const selectTriggerVariants = cva(
[
'group flex w-full items-center border-0 bg-components-input-bg-normal text-left text-components-input-text-filled outline-hidden',
'hover:bg-state-base-hover-alt focus-visible:bg-state-base-hover-alt data-popup-open:bg-state-base-hover-alt',
'focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:ring-inset',
'data-placeholder:text-components-input-text-placeholder',
'data-readonly:cursor-default data-readonly:bg-transparent data-readonly:hover:bg-transparent',
'data-disabled:cursor-not-allowed data-disabled:bg-components-input-bg-disabled data-disabled:text-components-input-text-filled-disabled data-disabled:hover:bg-components-input-bg-disabled',

View File

@ -49,19 +49,6 @@ describe('Switch', () => {
await expect.element(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'true')
})
it('should work in uncontrolled mode with defaultChecked prop', async () => {
const onCheckedChange = vi.fn()
const screen = await render(<Switch defaultChecked={false} onCheckedChange={onCheckedChange} />)
const switchElement = screen.getByRole('switch')
await expect.element(switchElement).toHaveAttribute('aria-checked', 'false')
asHTMLElement(switchElement.element()).click()
expect(onCheckedChange).toHaveBeenCalledWith(true)
await expect.element(switchElement).toHaveAttribute('aria-checked', 'true')
})
it('should not call onCheckedChange when disabled', async () => {
const onCheckedChange = vi.fn()
const screen = await render(<Switch checked={false} disabled onCheckedChange={onCheckedChange} />)
@ -155,24 +142,6 @@ describe('Switch', () => {
expect(screen.container.querySelector('span[aria-hidden="true"] i')).toBeInTheDocument()
})
it('should use checked data attributes to position spinner', async () => {
const screen = await render(<Switch checked={false} loading size="md" />)
const spinner = screen.container.querySelector('span[aria-hidden="true"]')
expect(spinner).toHaveClass(
'left-[calc(50%+6px)]',
'group-data-checked:left-[calc(50%-6px)]',
)
await screen.rerender(<Switch checked={true} loading size="md" />)
await expect.element(screen.getByRole('switch')).toHaveAttribute('data-checked', '')
expect(screen.container.querySelector('span[aria-hidden="true"]')).toHaveClass(
'left-[calc(50%+6px)]',
'group-data-checked:left-[calc(50%-6px)]',
)
})
it('should not show spinner for xs and sm sizes', async () => {
const screen = await render(<Switch checked={false} loading size="xs" />)
expect(screen.container.querySelector('span[aria-hidden="true"] i')).not.toBeInTheDocument()

View File

@ -2,11 +2,6 @@ import type { Meta, StoryObj } from '@storybook/react-vite'
import type { ComponentProps } from 'react'
import { useState, useTransition } from 'react'
import { Switch, SwitchSkeleton } from '.'
import {
FieldDescription,
FieldLabel,
FieldRoot,
} from '../field'
const meta = {
title: 'Base/Form/Switch',
@ -15,7 +10,7 @@ const meta = {
layout: 'centered',
docs: {
description: {
component: 'Toggle switch primitive with controlled and uncontrolled state support, loading state, and skeleton placeholder.',
component: 'Toggle switch built on Base UI with CVA variants, Figma-aligned design tokens, loading spinner, and skeleton placeholder. Import `Switch` and `SwitchSkeleton` from `@langgenius/dify-ui/switch`.',
},
},
},
@ -47,27 +42,20 @@ const meta = {
export default meta
type Story = StoryObj<typeof meta>
type SwitchDemoProps = Partial<Omit<ComponentProps<typeof Switch>, 'checked' | 'defaultChecked' | 'onCheckedChange'>> & {
checked?: boolean
}
const SwitchDemo = (args: SwitchDemoProps) => {
const SwitchDemo = (args: Partial<ComponentProps<typeof Switch>>) => {
const [enabled, setEnabled] = useState(args.checked ?? false)
return (
<FieldRoot name="autoRetry" className="w-72">
<FieldLabel className="flex items-center justify-between gap-3">
<span>Enable auto retry</span>
<Switch
{...args}
checked={enabled}
onCheckedChange={setEnabled}
/>
</FieldLabel>
<FieldDescription>
{enabled ? 'Failures will retry automatically.' : 'Failures require manual retry.'}
</FieldDescription>
</FieldRoot>
<div className="flex items-center justify-center gap-3">
<Switch
{...args}
checked={enabled}
onCheckedChange={setEnabled}
/>
<span className="text-sm text-gray-700">
{enabled ? 'On' : 'Off'}
</span>
</div>
)
}
@ -128,24 +116,24 @@ const AllStatesDemo = () => {
<td className="py-3 font-medium text-gray-900">{size}</td>
<td className="py-3">
<div className="flex gap-2">
<Switch size={size} checked={false} onCheckedChange={() => {}} aria-label={`${size} unchecked switch`} />
<Switch size={size} checked={true} onCheckedChange={() => {}} aria-label={`${size} checked switch`} />
<Switch size={size} checked={false} onCheckedChange={() => {}} />
<Switch size={size} checked={true} onCheckedChange={() => {}} />
</div>
</td>
<td className="py-3">
<div className="flex gap-2">
<Switch size={size} checked={false} disabled aria-label={`${size} disabled unchecked switch`} />
<Switch size={size} checked={true} disabled aria-label={`${size} disabled checked switch`} />
<Switch size={size} checked={false} disabled />
<Switch size={size} checked={true} disabled />
</div>
</td>
<td className="py-3">
<div className="flex gap-2">
<Switch size={size} checked={false} loading aria-label={`${size} loading unchecked switch`} />
<Switch size={size} checked={true} loading aria-label={`${size} loading checked switch`} />
<Switch size={size} checked={false} loading />
<Switch size={size} checked={true} loading />
</div>
</td>
<td className="py-3">
<SwitchSkeleton size={size} aria-hidden="true" />
<SwitchSkeleton size={size} />
</td>
</tr>
))}
@ -160,7 +148,7 @@ export const AllStates: Story = {
parameters: {
docs: {
description: {
story: 'Variant matrix for switch sizes and states.',
story: 'Complete variant matrix: all sizes × all states, matching Figma design spec (node 2144:1210).',
},
},
},
@ -176,30 +164,22 @@ const SizeComparisonDemo = () => {
return (
<div className="flex flex-col items-center space-y-4">
<FieldRoot name="extraSmallSwitch">
<FieldLabel className="flex items-center gap-3">
<Switch size="xs" checked={states.xs} onCheckedChange={v => setStates({ ...states, xs: v })} />
Extra Small (xs) - 14x10
</FieldLabel>
</FieldRoot>
<FieldRoot name="smallSwitch">
<FieldLabel className="flex items-center gap-3">
<Switch size="sm" checked={states.sm} onCheckedChange={v => setStates({ ...states, sm: v })} />
Small (sm) - 20x12
</FieldLabel>
</FieldRoot>
<FieldRoot name="regularSwitch">
<FieldLabel className="flex items-center gap-3">
<Switch size="md" checked={states.md} onCheckedChange={v => setStates({ ...states, md: v })} />
Regular (md) - 28x16
</FieldLabel>
</FieldRoot>
<FieldRoot name="largeSwitch">
<FieldLabel className="flex items-center gap-3">
<Switch size="lg" checked={states.lg} onCheckedChange={v => setStates({ ...states, lg: v })} />
Large (lg) - 36x20
</FieldLabel>
</FieldRoot>
<div className="flex items-center gap-3">
<Switch size="xs" checked={states.xs} onCheckedChange={v => setStates({ ...states, xs: v })} />
<span className="text-sm text-gray-700">Extra Small (xs) 14×10</span>
</div>
<div className="flex items-center gap-3">
<Switch size="sm" checked={states.sm} onCheckedChange={v => setStates({ ...states, sm: v })} />
<span className="text-sm text-gray-700">Small (sm) 20×12</span>
</div>
<div className="flex items-center gap-3">
<Switch size="md" checked={states.md} onCheckedChange={v => setStates({ ...states, md: v })} />
<span className="text-sm text-gray-700">Regular (md) 28×16</span>
</div>
<div className="flex items-center gap-3">
<Switch size="lg" checked={states.lg} onCheckedChange={v => setStates({ ...states, lg: v })} />
<span className="text-sm text-gray-700">Large (lg) 36×20</span>
</div>
</div>
)
}
@ -220,42 +200,30 @@ const LoadingDemo = () => {
{loading ? 'Stop Loading' : 'Start Loading'}
</button>
<div className="space-y-3">
<FieldRoot name="largeUncheckedLoading">
<FieldLabel className="flex items-center gap-3">
<Switch size="lg" checked={false} loading={loading} />
Large unchecked
</FieldLabel>
</FieldRoot>
<FieldRoot name="largeCheckedLoading">
<FieldLabel className="flex items-center gap-3">
<Switch size="lg" checked={true} loading={loading} />
Large checked
</FieldLabel>
</FieldRoot>
<FieldRoot name="regularUncheckedLoading">
<FieldLabel className="flex items-center gap-3">
<Switch size="md" checked={false} loading={loading} />
Regular unchecked
</FieldLabel>
</FieldRoot>
<FieldRoot name="regularCheckedLoading">
<FieldLabel className="flex items-center gap-3">
<Switch size="md" checked={true} loading={loading} />
Regular checked
</FieldLabel>
</FieldRoot>
<FieldRoot name="smallLoading">
<FieldLabel className="flex items-center gap-3">
<Switch size="sm" checked={false} loading={loading} />
Small
</FieldLabel>
</FieldRoot>
<FieldRoot name="extraSmallLoading">
<FieldLabel className="flex items-center gap-3">
<Switch size="xs" checked={false} loading={loading} />
Extra Small
</FieldLabel>
</FieldRoot>
<div className="flex items-center gap-3">
<Switch size="lg" checked={false} loading={loading} />
<span className="text-sm text-gray-700">Large unchecked</span>
</div>
<div className="flex items-center gap-3">
<Switch size="lg" checked={true} loading={loading} />
<span className="text-sm text-gray-700">Large checked</span>
</div>
<div className="flex items-center gap-3">
<Switch size="md" checked={false} loading={loading} />
<span className="text-sm text-gray-700">Regular unchecked</span>
</div>
<div className="flex items-center gap-3">
<Switch size="md" checked={true} loading={loading} />
<span className="text-sm text-gray-700">Regular checked</span>
</div>
<div className="flex items-center gap-3">
<Switch size="sm" checked={false} loading={loading} />
<span className="text-sm text-gray-700">Small (no spinner)</span>
</div>
<div className="flex items-center gap-3">
<Switch size="xs" checked={false} loading={loading} />
<span className="text-sm text-gray-700">Extra Small (no spinner)</span>
</div>
</div>
</div>
)
@ -266,7 +234,7 @@ export const Loading: Story = {
parameters: {
docs: {
description: {
story: 'Loading state disables interaction and shows a spinner for md and lg sizes.',
story: 'Loading state disables interaction and shows a spinning icon (i-ri-loader-2-line) for md/lg sizes. Spinner position mirrors the knob: appears on the opposite side of the checked state.',
},
},
},
@ -274,76 +242,61 @@ export const Loading: Story = {
const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
function useMockAutoRetrySettingQuery() {
const MutationLoadingDemo = () => {
const [enabled, setEnabled] = useState(false)
return {
data: {
enabled,
},
setData: setEnabled,
}
}
function useMockUpdateAutoRetrySettingMutation({
onSuccess,
}: {
onSuccess: (enabled: boolean) => void
}) {
const [requestCount, setRequestCount] = useState(0)
const [isPending, startTransition] = useTransition()
const mutate = (nextValue: boolean) => {
const handleChange = (nextValue: boolean) => {
if (isPending)
return
startTransition(async () => {
setRequestCount(current => current + 1)
await wait(1200)
onSuccess(nextValue)
setEnabled(nextValue)
})
}
return {
requestCount,
isPending,
mutate,
}
}
const MutationLoadingDemo = () => {
const autoRetrySetting = useMockAutoRetrySettingQuery()
const updateAutoRetrySetting = useMockUpdateAutoRetrySettingMutation({
onSuccess: autoRetrySetting.setData,
})
const statusText = updateAutoRetrySetting.isPending
? 'Saving changes...'
: autoRetrySetting.data.enabled
? 'Auto retry is enabled.'
: 'Auto retry is disabled.'
return (
<div className="grid w-90 gap-3 rounded-lg border border-components-panel-border bg-components-panel-bg p-4 shadow-sm">
<FieldRoot name="autoRetry">
<FieldLabel className="flex items-center justify-between gap-4">
<span className="system-sm-medium text-text-secondary">Enable auto retry</span>
<Switch
size="lg"
checked={autoRetrySetting.data.enabled}
loading={updateAutoRetrySetting.isPending}
onCheckedChange={updateAutoRetrySetting.mutate}
/>
</FieldLabel>
<FieldDescription>Retry failed workflow runs without manual intervention.</FieldDescription>
</FieldRoot>
<div className="w-[340px] space-y-4 rounded-2xl border border-components-panel-border bg-components-panel-bg p-4 shadow-sm">
<div className="space-y-1">
<p className="text-sm font-medium text-text-primary">Mutation Loading Guard</p>
<p className="text-xs text-text-tertiary">
Click once to start a simulated mutate call. While the request is pending, the switch enters
{' '}
<code className="rounded-sm bg-state-base-hover px-1 py-0.5 text-[11px]">loading</code>
{' '}
and rejects duplicate clicks.
</p>
</div>
<span className="text-xs text-text-tertiary" aria-live="polite">
{statusText}
{' '}
Save attempts:
{' '}
{updateAutoRetrySetting.requestCount}
</span>
<div className="flex items-center justify-between rounded-xl border border-components-panel-border-subtle bg-background-default-dodge px-3 py-2 shadow-sm">
<div className="space-y-1">
<p className="text-sm font-medium text-text-primary">Enable Auto Retry</p>
<p className="text-xs text-text-tertiary">
{isPending ? 'Saving…' : enabled ? 'Saved as on' : 'Saved as off'}
</p>
</div>
<Switch
size="lg"
checked={enabled}
loading={isPending}
onCheckedChange={handleChange}
aria-label="Enable Auto Retry"
/>
</div>
<div className="grid grid-cols-2 gap-2 text-xs text-text-tertiary">
<div className="rounded-lg bg-state-base-hover px-3 py-2">
<div className="font-medium text-text-secondary">Committed Value</div>
<div>{enabled ? 'On' : 'Off'}</div>
</div>
<div className="rounded-lg bg-state-base-hover px-3 py-2">
<div className="font-medium text-text-secondary">Mutate Count</div>
<div>{requestCount}</div>
</div>
</div>
</div>
)
}
@ -353,7 +306,7 @@ export const MutationLoadingGuard: Story = {
parameters: {
docs: {
description: {
story: 'Controlled switch that enters loading while the change is saved.',
story: 'Simulates a controlled switch backed by an async mutate call. The component keeps its previous committed value, sets `loading` during the request, and blocks duplicate clicks until the mutation resolves.',
},
},
},
@ -362,19 +315,19 @@ export const MutationLoadingGuard: Story = {
const SkeletonDemo = () => (
<div className="flex flex-col items-center space-y-4">
<div className="flex items-center gap-3">
<SwitchSkeleton size="xs" aria-hidden="true" />
<SwitchSkeleton size="xs" />
<span className="text-sm text-gray-700">Extra Small skeleton</span>
</div>
<div className="flex items-center gap-3">
<SwitchSkeleton size="sm" aria-hidden="true" />
<SwitchSkeleton size="sm" />
<span className="text-sm text-gray-700">Small skeleton</span>
</div>
<div className="flex items-center gap-3">
<SwitchSkeleton size="md" aria-hidden="true" />
<SwitchSkeleton size="md" />
<span className="text-sm text-gray-700">Regular skeleton</span>
</div>
<div className="flex items-center gap-3">
<SwitchSkeleton size="lg" aria-hidden="true" />
<SwitchSkeleton size="lg" />
<span className="text-sm text-gray-700">Large skeleton</span>
</div>
</div>
@ -385,7 +338,7 @@ export const Skeleton: Story = {
parameters: {
docs: {
description: {
story: 'Non-interactive placeholders for switch loading layouts.',
story: '`SwitchSkeleton` renders a non-interactive placeholder with `bg-text-quaternary opacity-20`. Exported from `@langgenius/dify-ui/switch` alongside `Switch`.',
},
},
},

View File

@ -45,34 +45,26 @@ const switchThumbVariants = cva(
export type SwitchSize = NonNullable<VariantProps<typeof switchRootVariants>['size']>
const switchSpinnerVariants = cva(
'absolute top-1/2 -translate-x-1/2 -translate-y-1/2',
{
variants: {
size: {
md: 'size-2 left-[calc(50%+6px)] group-data-checked:left-[calc(50%-6px)]',
lg: 'size-2.5 left-[calc(50%+8px)] group-data-checked:left-[calc(50%-8px)]',
},
},
const spinnerSizeConfig: Partial<Record<SwitchSize, {
icon: string
uncheckedPosition: string
checkedPosition: string
}>> = {
md: {
icon: 'size-2',
uncheckedPosition: 'left-[calc(50%+6px)]',
checkedPosition: 'left-[calc(50%-6px)]',
},
lg: {
icon: 'size-2.5',
uncheckedPosition: 'left-[calc(50%+8px)]',
checkedPosition: 'left-[calc(50%-8px)]',
},
)
type ControlledSwitchProps = {
checked: boolean
defaultChecked?: never
}
type UncontrolledSwitchProps = {
checked?: never
defaultChecked?: boolean
}
type SwitchControlProps = ControlledSwitchProps | UncontrolledSwitchProps
export type SwitchProps
= Omit<BaseSwitchNS.Root.Props, 'checked' | 'defaultChecked' | 'className' | 'size' | 'onCheckedChange'>
= Omit<BaseSwitchNS.Root.Props, 'className' | 'size' | 'onCheckedChange'>
& VariantProps<typeof switchRootVariants>
& SwitchControlProps
& {
onCheckedChange?: (checked: boolean) => void
loading?: boolean
@ -89,6 +81,7 @@ export function Switch({
...props
}: SwitchProps) {
const isDisabled = disabled || loading
const spinner = loading && size ? spinnerSizeConfig[size] : undefined
return (
<BaseSwitch.Root
@ -102,10 +95,14 @@ export function Switch({
<BaseSwitch.Thumb
className={switchThumbVariants({ size })}
/>
{loading && (size === 'md' || size === 'lg')
{spinner
? (
<span
className={switchSpinnerVariants({ size })}
className={cn(
'absolute top-1/2 -translate-x-1/2 -translate-y-1/2',
spinner.icon,
checked ? spinner.checkedPosition : spinner.uncheckedPosition,
)}
aria-hidden="true"
>
<i className="i-ri-loader-2-line size-full animate-spin text-text-tertiary motion-reduce:animate-none" />
@ -134,8 +131,11 @@ const switchSkeletonVariants = cva(
)
export type SwitchSkeletonProps
= HTMLAttributes<HTMLDivElement>
= Omit<HTMLAttributes<HTMLDivElement>, 'className'>
& VariantProps<typeof switchSkeletonVariants>
& {
className?: string
}
export function SwitchSkeleton({
size = 'md',

View File

@ -41,7 +41,6 @@ describe('@langgenius/dify-ui/toast', () => {
await expect.element(screen.getByRole('region', { name: 'Notifications' })).toHaveAttribute('aria-live', 'polite')
await expect.element(screen.getByRole('region', { name: 'Notifications' })).toHaveClass('z-60')
expect(screen.getByRole('region', { name: 'Notifications' }).element().firstElementChild).toHaveClass('top-4')
expect(screen.getByText('Saved').element().closest('[class*="transition-opacity"]')).toHaveClass('motion-reduce:transition-none')
expect(screen.getByRole('dialog').element()).not.toHaveClass('outline-hidden')
expect(document.body.querySelector('[aria-hidden="true"].i-ri-checkbox-circle-fill')).toBeInTheDocument()
expect(document.body.querySelector('button[aria-label="Close notification"][aria-hidden="true"]')).toBeInTheDocument()

View File

@ -171,7 +171,7 @@ function ToastCard({
aria-hidden="true"
className={cn('absolute -inset-px bg-linear-to-r opacity-40', getToneGradientClasses(toastType))}
/>
<BaseToast.Content className="relative flex items-start gap-1 overflow-hidden p-3 transition-opacity duration-200 data-behind:opacity-0 data-expanded:opacity-100 motion-reduce:transition-none">
<BaseToast.Content className="relative flex items-start gap-1 overflow-hidden p-3 transition-opacity duration-200 data-behind:opacity-0 data-expanded:opacity-100">
<div className="flex shrink-0 items-center justify-center p-0.5">
<ToastIcon type={toastType} />
</div>

View File

@ -10,11 +10,7 @@ export default defineConfig({
tsconfigPaths: true,
},
optimizeDeps: {
include: [
'@base-ui/react/form',
'@base-ui/react/merge-props',
'@base-ui/react/use-render',
],
include: ['@base-ui/react/form'],
},
test: {
globals: true,

View File

@ -5,7 +5,6 @@ import { useSuspenseQuery } from '@tanstack/react-query'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import DifyLogo from '@/app/components/base/logo/dify-logo'
import Link from '@/next/link'
import { useRouter } from '@/next/navigation'
import { systemFeaturesQueryOptions } from '@/service/system-features'
import Avatar from './avatar'
@ -18,26 +17,21 @@ const Header = () => {
const goToStudio = useCallback(() => {
router.push('/apps')
}, [router])
const logoLabel = systemFeatures.branding.enabled && systemFeatures.branding.application_title ? systemFeatures.branding.application_title : 'Dify'
return (
<div className="flex flex-1 items-center justify-between px-4">
<div className="flex items-center gap-3">
<Link
href="/apps"
className="flex items-center rounded-sm hover:opacity-80 focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
aria-label={logoLabel}
>
<div className="flex cursor-pointer items-center" onClick={goToStudio}>
{systemFeatures.branding.enabled && systemFeatures.branding.login_page_logo
? (
<img
src={systemFeatures.branding.login_page_logo}
className="block h-[22px] w-auto object-contain"
alt=""
alt="Dify logo"
/>
)
: <DifyLogo alt="" />}
</Link>
: <DifyLogo />}
</div>
<div className="h-4 w-px origin-center rotate-[11.31deg] bg-divider-regular" />
<p className="relative mt-[-2px] title-3xl-semi-bold text-text-primary">{t('account.account', { ns: 'common' })}</p>
</div>

View File

@ -1,4 +1,4 @@
import type { ReactElement } from 'react'
import type { ReactElement, ReactNode } from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { runCreateAppAttributionBootstrap } from '@/utils/create-app-tracking'
@ -28,7 +28,14 @@ vi.mock('@/next/headers', () => ({
const loadComponent = async () => {
const mod = await import('../create-app-attribution-bootstrap')
return mod.CreateAppAttributionBootstrap
const rawExport = mod.default as unknown
const renderer: (() => Promise<ReactNode>) | undefined
= typeof rawExport === 'function' ? rawExport as () => Promise<ReactNode> : (rawExport as { type?: () => Promise<ReactNode> }).type
if (!renderer)
throw new Error('CreateAppAttributionBootstrap component is not callable in tests')
return renderer
}
const runBootstrapScript = () => {

View File

@ -5,7 +5,6 @@ import type { AnnotationItem, AnnotationItemBasic } from './type'
import type { AnnotationReplyConfig } from '@/models/debug'
import type { App } from '@/types/app'
import { cn } from '@langgenius/dify-ui/cn'
import { Pagination } from '@langgenius/dify-ui/pagination'
import { Switch } from '@langgenius/dify-ui/switch'
import { toast } from '@langgenius/dify-ui/toast'
import { RiEqualizer2Line } from '@remixicon/react'
@ -17,6 +16,7 @@ import ActionButton from '@/app/components/base/action-button'
import ConfigParamModal from '@/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal'
import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication'
import Loading from '@/app/components/base/loading'
import Pagination from '@/app/components/base/pagination'
import AnnotationFullModal from '@/app/components/billing/annotation-full/modal'
import { APP_PAGE_LIMIT } from '@/config'
import { useProviderContext } from '@/context/provider-context'
@ -49,7 +49,6 @@ const Annotation: FC<Props> = (props) => {
const [limit, setLimit] = useState(APP_PAGE_LIMIT)
const [list, setList] = useState<AnnotationItem[]>([])
const [total, setTotal] = useState(0)
const totalPages = total ? Math.max(Math.ceil(total / limit), 1) : 1
const [isLoading, setIsLoading] = useState(false)
const [controlUpdateList, setControlUpdateList] = useState(() => Date.now())
const [currItem, setCurrItem] = useState<AnnotationItem | null>(null)
@ -218,22 +217,11 @@ const Annotation: FC<Props> = (props) => {
{(total && total > APP_PAGE_LIMIT)
? (
<Pagination
page={currPage + 1}
totalPages={totalPages}
onPageChange={page => setCurrPage(page - 1)}
labels={{
previous: t('pagination.previous', { ns: 'common' }),
next: t('pagination.next', { ns: 'common' }),
editPageNumber: (page, totalPages) => t('pagination.editPageNumber', { ns: 'common', page, totalPages }),
pageNumberInput: t('pagination.pageNumber', { ns: 'common' }),
}}
pageSize={{
value: limit,
options: [10, 25, 50],
onValueChange: setLimit,
label: t('pagination.perPage', { ns: 'common' }),
ariaLabel: t('pagination.perPage', { ns: 'common' }),
}}
current={currPage}
onChange={setCurrPage}
total={total}
limit={limit}
onLimitChange={setLimit}
/>
)
: null}

View File

@ -20,12 +20,12 @@ import {
DrawerTitle,
DrawerViewport,
} from '@langgenius/dify-ui/drawer'
import { Pagination } from '@langgenius/dify-ui/pagination'
import * as React from 'react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Badge from '@/app/components/base/badge'
import { MessageCheckRemove } from '@/app/components/base/icons/src/vender/line/communication'
import Pagination from '@/app/components/base/pagination'
import TabSlider from '@/app/components/base/tab-slider-plain'
import { APP_PAGE_LIMIT } from '@/config'
import useTimestamp from '@/hooks/use-timestamp'
@ -62,7 +62,6 @@ const ViewAnnotationModal: FC<Props> = ({
const { formatTime } = useTimestamp()
const [currPage, setCurrPage] = React.useState<number>(0)
const [total, setTotal] = useState(0)
const totalPages = total ? Math.max(Math.ceil(total / APP_PAGE_LIMIT), 1) : 1
const [hitHistoryList, setHitHistoryList] = useState<HitHistoryItem[]>([])
// Update local state when item prop changes (e.g., when modal is reopened with updated data)
@ -198,15 +197,10 @@ const ViewAnnotationModal: FC<Props> = ({
{(total && total > APP_PAGE_LIMIT)
? (
<Pagination
page={currPage + 1}
totalPages={totalPages}
onPageChange={page => setCurrPage(page - 1)}
labels={{
previous: t('pagination.previous', { ns: 'common' }),
next: t('pagination.next', { ns: 'common' }),
editPageNumber: (page, totalPages) => t('pagination.editPageNumber', { ns: 'common', page, totalPages }),
pageNumberInput: t('pagination.pageNumber', { ns: 'common' }),
}}
className="px-0"
current={currPage}
onChange={setCurrPage}
total={total}
/>
)
: null}

View File

@ -67,11 +67,9 @@ vi.mock('@/app/components/base/loading', () => ({
default: () => <div>loading-logs</div>,
}))
vi.mock('@langgenius/dify-ui/pagination', () => ({
Pagination: ({ onPageChange }: { onPageChange: (page: number) => void }) => (
<div>
<button onClick={() => onPageChange(2)}>go-to-page-2</button>
</div>
vi.mock('@/app/components/base/pagination', () => ({
default: ({ onChange }: { onChange: (page: number) => void }) => (
<button onClick={() => onChange(1)}>go-to-page-2</button>
),
}))

View File

@ -1,7 +1,6 @@
'use client'
import type { FC } from 'react'
import type { App } from '@/types/app'
import { Pagination } from '@langgenius/dify-ui/pagination'
import { useDebounce } from 'ahooks'
import dayjs from 'dayjs'
import { omit } from 'es-toolkit/object'
@ -9,6 +8,7 @@ import * as React from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Loading from '@/app/components/base/loading'
import Pagination from '@/app/components/base/pagination'
import { APP_PAGE_LIMIT } from '@/config'
import { usePathname, useRouter, useSearchParams } from '@/next/navigation'
import { useChatConversations, useCompletionConversations } from '@/service/use-log'
@ -98,7 +98,6 @@ const Logs: FC<ILogsProps> = ({ appDetail }) => {
})
const total = isChatMode ? chatConversations?.total : completionConversations?.total
const totalPages = total ? Math.max(Math.ceil(total / limit), 1) : 1
const handleQueryParamsChange = useCallback((next: QueryParam) => {
setCurrPage(0)
@ -131,22 +130,11 @@ const Logs: FC<ILogsProps> = ({ appDetail }) => {
{(total && total > APP_PAGE_LIMIT)
? (
<Pagination
page={currPage + 1}
totalPages={totalPages}
onPageChange={page => handlePageChange(page - 1)}
labels={{
previous: t('pagination.previous', { ns: 'common' }),
next: t('pagination.next', { ns: 'common' }),
editPageNumber: (page, totalPages) => t('pagination.editPageNumber', { ns: 'common', page, totalPages }),
pageNumberInput: t('pagination.pageNumber', { ns: 'common' }),
}}
pageSize={{
value: limit,
options: [10, 25, 50],
onValueChange: setLimit,
label: t('pagination.perPage', { ns: 'common' }),
ariaLabel: t('pagination.perPage', { ns: 'common' }),
}}
current={currPage}
onChange={handlePageChange}
total={total}
limit={limit}
onLimitChange={setLimit}
/>
)
: null}

View File

@ -1,7 +1,6 @@
'use client'
import type { FC } from 'react'
import type { App } from '@/types/app'
import { Pagination } from '@langgenius/dify-ui/pagination'
import { useDebounce } from 'ahooks'
import dayjs from 'dayjs'
import timezone from 'dayjs/plugin/timezone'
@ -12,6 +11,7 @@ import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import EmptyElement from '@/app/components/app/log/empty-element'
import Loading from '@/app/components/base/loading'
import Pagination from '@/app/components/base/pagination'
import { APP_PAGE_LIMIT } from '@/config'
import { useAppContext } from '@/context/app-context'
import { useWorkflowLogs } from '@/service/use-log'
@ -59,7 +59,6 @@ const Logs: FC<ILogsProps> = ({ appDetail }) => {
params: query,
})
const total = workflowLogs?.total
const totalPages = total ? Math.max(Math.ceil(total / limit), 1) : 1
return (
<div className="flex h-full flex-col">
@ -77,22 +76,11 @@ const Logs: FC<ILogsProps> = ({ appDetail }) => {
{(total && total > APP_PAGE_LIMIT)
? (
<Pagination
page={currPage + 1}
totalPages={totalPages}
onPageChange={page => setCurrPage(page - 1)}
labels={{
previous: t('pagination.previous', { ns: 'common' }),
next: t('pagination.next', { ns: 'common' }),
editPageNumber: (page, totalPages) => t('pagination.editPageNumber', { ns: 'common', page, totalPages }),
pageNumberInput: t('pagination.pageNumber', { ns: 'common' }),
}}
pageSize={{
value: limit,
options: [10, 25, 50],
onValueChange: setLimit,
label: t('pagination.perPage', { ns: 'common' }),
ariaLabel: t('pagination.perPage', { ns: 'common' }),
}}
current={currPage}
onChange={setCurrPage}
total={total}
limit={limit}
onLimitChange={setLimit}
/>
)
: null}

View File

@ -58,12 +58,6 @@ describe('DifyLogo', () => {
const img = screen.getByRole('img', { name: /dify logo/i })
expect(img).toHaveClass('custom-test-class')
})
it('applies custom alt text', () => {
const { container } = render(<DifyLogo alt="" />)
const img = container.querySelector('img')
expect(img).toHaveAttribute('alt', '')
})
})
describe('Theme behavior', () => {

View File

@ -23,14 +23,12 @@ type DifyLogoProps = {
style?: LogoStyle
size?: LogoSize
className?: string
alt?: string
}
const DifyLogo: FC<DifyLogoProps> = ({
style = 'default',
size = 'medium',
className,
alt = 'Dify logo',
}) => {
const { theme } = useTheme()
const themedStyle = (theme === 'dark' && style === 'default') ? 'monochromeWhite' : style
@ -39,7 +37,7 @@ const DifyLogo: FC<DifyLogoProps> = ({
<img
src={`${basePath}${logoPathMap[themedStyle]}`}
className={cn('block object-contain', logoSizeMap[size], className)}
alt={alt}
alt="Dify logo"
/>
)
}

View File

@ -0,0 +1,155 @@
import { renderHook } from '@testing-library/react'
import usePagination from '../hook'
const defaultProps = {
currentPage: 0,
setCurrentPage: vi.fn(),
totalPages: 10,
edgePageCount: 2,
middlePagesSiblingCount: 1,
truncableText: '...',
truncableClassName: 'truncable',
}
describe('usePagination', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('pages', () => {
it('should generate correct pages array', () => {
const { result } = renderHook(() => usePagination(defaultProps))
expect(result.current.pages).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
})
it('should generate empty pages for totalPages 0', () => {
const { result } = renderHook(() => usePagination({ ...defaultProps, totalPages: 0 }))
expect(result.current.pages).toEqual([])
})
it('should generate single page', () => {
const { result } = renderHook(() => usePagination({ ...defaultProps, totalPages: 1 }))
expect(result.current.pages).toEqual([1])
})
})
describe('hasPreviousPage / hasNextPage', () => {
it('should have no previous page on first page', () => {
const { result } = renderHook(() => usePagination({ ...defaultProps, currentPage: 1 }))
expect(result.current.hasPreviousPage).toBe(false)
})
it('should have previous page when not on first page', () => {
const { result } = renderHook(() => usePagination({ ...defaultProps, currentPage: 3 }))
expect(result.current.hasPreviousPage).toBe(true)
})
it('should have next page when not on last page', () => {
const { result } = renderHook(() => usePagination({ ...defaultProps, currentPage: 1 }))
expect(result.current.hasNextPage).toBe(true)
})
it('should have no next page on last page', () => {
const { result } = renderHook(() => usePagination({ ...defaultProps, currentPage: 10 }))
expect(result.current.hasNextPage).toBe(false)
})
})
describe('middlePages', () => {
it('should return correct middle pages when at start', () => {
const { result } = renderHook(() => usePagination({ ...defaultProps, currentPage: 0 }))
// isReachedToFirst: currentPage(0) <= middlePagesSiblingCount(1), so slice(0, 3)
expect(result.current.middlePages).toEqual([1, 2, 3])
})
it('should return correct middle pages when in the middle', () => {
const { result } = renderHook(() => usePagination({ ...defaultProps, currentPage: 5 }))
// Not at start or end, slice(5-1, 5+1+1) = slice(4, 7) = [5, 6, 7]
expect(result.current.middlePages).toEqual([5, 6, 7])
})
it('should return correct middle pages when at end', () => {
const { result } = renderHook(() => usePagination({ ...defaultProps, currentPage: 9 }))
// isReachedToLast: currentPage(9) + middlePagesSiblingCount(1) >= totalPages(10), so slice(-3)
expect(result.current.middlePages).toEqual([8, 9, 10])
})
})
describe('previousPages and nextPages', () => {
it('should return empty previousPages when at start', () => {
const { result } = renderHook(() => usePagination({ ...defaultProps, currentPage: 0 }))
expect(result.current.previousPages).toEqual([])
})
it('should return previousPages when in the middle', () => {
const { result } = renderHook(() => usePagination({ ...defaultProps, currentPage: 5 }))
// edgePageCount=2, so first 2 pages filtered by not in middlePages
expect(result.current.previousPages).toEqual([1, 2])
})
it('should return empty nextPages when at end', () => {
const { result } = renderHook(() => usePagination({ ...defaultProps, currentPage: 9 }))
expect(result.current.nextPages).toEqual([])
})
it('should return nextPages when in the middle', () => {
const { result } = renderHook(() => usePagination({ ...defaultProps, currentPage: 5 }))
// Last 2 pages: [9, 10], filtered by not in middlePages [5,6,7]
expect(result.current.nextPages).toEqual([9, 10])
})
})
describe('truncation', () => {
it('should be previous truncable when middle pages are far from edge', () => {
const { result } = renderHook(() => usePagination({ ...defaultProps, currentPage: 5 }))
// previousPages=[1,2], middlePages=[5,6,7], 5 > 2+1 = true
expect(result.current.isPreviousTruncable).toBe(true)
})
it('should not be previous truncable when pages are contiguous', () => {
const { result } = renderHook(() => usePagination({ ...defaultProps, currentPage: 2 }))
expect(result.current.isPreviousTruncable).toBe(false)
})
it('should be next truncable when middle pages are far from end edge', () => {
const { result } = renderHook(() => usePagination({ ...defaultProps, currentPage: 5 }))
// middlePages=[5,6,7], nextPages=[9,10], 7+1 < 9 = true
expect(result.current.isNextTruncable).toBe(true)
})
it('should not be next truncable when pages are contiguous', () => {
const { result } = renderHook(() => usePagination({ ...defaultProps, currentPage: 7 }))
expect(result.current.isNextTruncable).toBe(false)
})
})
describe('passthrough values', () => {
it('should pass through currentPage', () => {
const { result } = renderHook(() => usePagination({ ...defaultProps, currentPage: 5 }))
expect(result.current.currentPage).toBe(5)
})
it('should pass through setCurrentPage', () => {
const setCurrentPage = vi.fn()
const { result } = renderHook(() => usePagination({ ...defaultProps, setCurrentPage }))
result.current.setCurrentPage(3)
expect(setCurrentPage).toHaveBeenCalledWith(3)
})
it('should pass through truncableText', () => {
const { result } = renderHook(() => usePagination({ ...defaultProps, truncableText: '…' }))
expect(result.current.truncableText).toBe('…')
})
it('should pass through truncableClassName', () => {
const { result } = renderHook(() => usePagination({ ...defaultProps, truncableClassName: 'custom-trunc' }))
expect(result.current.truncableClassName).toBe('custom-trunc')
})
it('should use default truncableText', () => {
const { currentPage, setCurrentPage, totalPages, edgePageCount, middlePagesSiblingCount } = defaultProps
const { result } = renderHook(() => usePagination({ currentPage, setCurrentPage, totalPages, edgePageCount, middlePagesSiblingCount }))
expect(result.current.truncableText).toBe('...')
})
})
})

View File

@ -0,0 +1,444 @@
import { act, fireEvent, render, screen } from '@testing-library/react'
import CustomizedPagination from '../index'
describe('CustomizedPagination', () => {
const defaultProps = {
current: 0,
onChange: vi.fn(),
total: 100,
}
beforeEach(() => {
vi.clearAllMocks()
vi.useRealTimers()
})
describe('Rendering', () => {
it('should render without crashing', () => {
const { container } = render(<CustomizedPagination {...defaultProps} />)
expect(container)!.toBeInTheDocument()
})
it('should display current page and total pages', () => {
render(<CustomizedPagination {...defaultProps} current={0} total={100} limit={10} />)
// current + 1 = 1, totalPages = 10
// The page info display shows "1 / 10" and page buttons also show numbers
// current + 1 = 1, totalPages = 10
// The page info display shows "1 / 10" and page buttons also show numbers
expect(screen.getByText('/'))!.toBeInTheDocument()
expect(screen.getAllByText('1').length).toBeGreaterThanOrEqual(1)
})
it('should render prev and next buttons', () => {
render(<CustomizedPagination {...defaultProps} />)
const buttons = screen.getAllByRole('button')
expect(buttons.length).toBeGreaterThanOrEqual(2)
})
it('should render page number buttons', () => {
render(<CustomizedPagination {...defaultProps} total={50} limit={10} />)
// 5 pages total, should see page numbers
// 5 pages total, should see page numbers
expect(screen.getByText('2'))!.toBeInTheDocument()
expect(screen.getByText('3'))!.toBeInTheDocument()
})
it('should display slash separator between current page and total', () => {
render(<CustomizedPagination {...defaultProps} />)
expect(screen.getByText('/'))!.toBeInTheDocument()
})
})
describe('Props', () => {
it('should apply custom className', () => {
const { container } = render(<CustomizedPagination {...defaultProps} className="my-custom" />)
const wrapper = container.firstChild as HTMLElement
expect(wrapper)!.toHaveClass('my-custom')
})
it('should default limit to 10', () => {
render(<CustomizedPagination {...defaultProps} total={100} />)
// totalPages = 100 / 10 = 10, displayed in the page info area
expect(screen.getAllByText('10').length).toBeGreaterThanOrEqual(1)
})
it('should calculate total pages based on custom limit', () => {
render(<CustomizedPagination {...defaultProps} total={100} limit={25} />)
// totalPages = 100 / 25 = 4, displayed in the page info area
expect(screen.getAllByText('4').length).toBeGreaterThanOrEqual(1)
})
it('should disable prev button on first page', () => {
render(<CustomizedPagination {...defaultProps} current={0} />)
const buttons = screen.getAllByRole('button')
// First button is prev
// First button is prev
expect(buttons[0])!.toBeDisabled()
})
it('should disable next button on last page', () => {
render(<CustomizedPagination {...defaultProps} current={9} total={100} limit={10} />)
const buttons = screen.getAllByRole('button')
// Last button is next
// Last button is next
expect(buttons[buttons.length - 1])!.toBeDisabled()
})
it('should not render limit selector when onLimitChange is not provided', () => {
render(<CustomizedPagination {...defaultProps} />)
expect(screen.queryByText(/common\.pagination\.perPage/i)).not.toBeInTheDocument()
})
it('should render limit selector when onLimitChange is provided', () => {
const onLimitChange = vi.fn()
render(<CustomizedPagination {...defaultProps} onLimitChange={onLimitChange} />)
// Should show limit options 10, 25, 50
// Should show limit options 10, 25, 50
expect(screen.getByText('25'))!.toBeInTheDocument()
expect(screen.getByText('50'))!.toBeInTheDocument()
})
})
describe('User Interactions', () => {
it('should call onChange when next button is clicked', () => {
const onChange = vi.fn()
render(<CustomizedPagination {...defaultProps} current={0} onChange={onChange} />)
const buttons = screen.getAllByRole('button')
const nextButton = buttons[buttons.length - 1]
fireEvent.click(nextButton!)
expect(onChange).toHaveBeenCalledWith(1)
})
it('should call onChange when prev button is clicked', () => {
const onChange = vi.fn()
render(<CustomizedPagination {...defaultProps} current={5} onChange={onChange} />)
const buttons = screen.getAllByRole('button')
fireEvent.click(buttons[0]!)
expect(onChange).toHaveBeenCalledWith(4)
})
it('should show input when page display is clicked', () => {
render(<CustomizedPagination {...defaultProps} />)
// Click the current page display (the div containing "1 / 10")
fireEvent.click(screen.getByText('/'))
// Input should appear
// Input should appear
expect(screen.getByRole('textbox'))!.toBeInTheDocument()
})
it('should navigate to entered page on Enter key', () => {
vi.useFakeTimers()
const onChange = vi.fn()
render(<CustomizedPagination {...defaultProps} current={0} onChange={onChange} />)
fireEvent.click(screen.getByText('/'))
const input = screen.getByRole('textbox')
fireEvent.change(input, { target: { value: '5' } })
fireEvent.keyDown(input, { key: 'Enter' })
act(() => {
vi.advanceTimersByTime(500)
})
expect(onChange).toHaveBeenCalledWith(4) // 0-indexed
})
it('should cancel input on Escape key', () => {
render(<CustomizedPagination {...defaultProps} current={0} />)
fireEvent.click(screen.getByText('/'))
const input = screen.getByRole('textbox')
fireEvent.keyDown(input, { key: 'Escape' })
// Input should be hidden and page display should return
// Input should be hidden and page display should return
// Input should be hidden and page display should return
// Input should be hidden and page display should return
// Input should be hidden and page display should return
// Input should be hidden and page display should return
// Input should be hidden and page display should return
// Input should be hidden and page display should return
// Input should be hidden and page display should return
// Input should be hidden and page display should return
// Input should be hidden and page display should return
// Input should be hidden and page display should return
// Input should be hidden and page display should return
// Input should be hidden and page display should return
// Input should be hidden and page display should return
// Input should be hidden and page display should return
// Input should be hidden and page display should return
// Input should be hidden and page display should return
// Input should be hidden and page display should return
// Input should be hidden and page display should return
// Input should be hidden and page display should return
// Input should be hidden and page display should return
// Input should be hidden and page display should return
// Input should be hidden and page display should return
// Input should be hidden and page display should return
// Input should be hidden and page display should return
// Input should be hidden and page display should return
// Input should be hidden and page display should return
// Input should be hidden and page display should return
// Input should be hidden and page display should return
// Input should be hidden and page display should return
// Input should be hidden and page display should return
expect(screen.queryByRole('textbox')).not.toBeInTheDocument()
expect(screen.getByText('/'))!.toBeInTheDocument()
})
it('should confirm input on blur-sm', () => {
vi.useFakeTimers()
const onChange = vi.fn()
render(<CustomizedPagination {...defaultProps} current={0} onChange={onChange} />)
fireEvent.click(screen.getByText('/'))
const input = screen.getByRole('textbox')
fireEvent.change(input, { target: { value: '3' } })
fireEvent.blur(input)
act(() => {
vi.advanceTimersByTime(500)
})
expect(onChange).toHaveBeenCalledWith(2) // 0-indexed
})
it('should clamp page to max when input exceeds total pages', () => {
vi.useFakeTimers()
const onChange = vi.fn()
render(<CustomizedPagination {...defaultProps} current={0} total={100} limit={10} onChange={onChange} />)
fireEvent.click(screen.getByText('/'))
const input = screen.getByRole('textbox')
fireEvent.change(input, { target: { value: '999' } })
fireEvent.keyDown(input, { key: 'Enter' })
act(() => {
vi.advanceTimersByTime(500)
})
expect(onChange).toHaveBeenCalledWith(9) // last page (0-indexed)
})
it('should clamp page to min when input is less than 1', () => {
vi.useFakeTimers()
const onChange = vi.fn()
render(<CustomizedPagination {...defaultProps} current={5} onChange={onChange} />)
fireEvent.click(screen.getByText('/'))
const input = screen.getByRole('textbox')
fireEvent.change(input, { target: { value: '0' } })
fireEvent.keyDown(input, { key: 'Enter' })
act(() => {
vi.advanceTimersByTime(500)
})
expect(onChange).toHaveBeenCalledWith(0)
})
it('should ignore non-numeric input and empty input', () => {
render(<CustomizedPagination {...defaultProps} />)
fireEvent.click(screen.getByText('/'))
const input = screen.getByRole('textbox')
fireEvent.change(input, { target: { value: 'abc' } })
expect(input)!.toHaveValue('')
fireEvent.change(input, { target: { value: '' } })
expect(input)!.toHaveValue('')
})
it('should show per page tip on hover and hide on leave', () => {
const onLimitChange = vi.fn()
render(<CustomizedPagination {...defaultProps} onLimitChange={onLimitChange} />)
const container = screen.getByText('25').closest('.bg-components-segmented-control-bg-normal')!
fireEvent.mouseEnter(container)
// I18n mock returns ns.key
// I18n mock returns ns.key
expect(screen.getByText('common.pagination.perPage'))!.toBeInTheDocument()
fireEvent.mouseLeave(container)
expect(screen.queryByText('common.pagination.perPage')).not.toBeInTheDocument()
})
it('should call onLimitChange when limit option is clicked', () => {
const onLimitChange = vi.fn()
render(<CustomizedPagination {...defaultProps} onLimitChange={onLimitChange} />)
fireEvent.click(screen.getByText('25'))
expect(onLimitChange).toHaveBeenCalledWith(25)
})
it('should call onLimitChange with 10 when 10 option is clicked', () => {
const onLimitChange = vi.fn()
render(<CustomizedPagination {...defaultProps} onLimitChange={onLimitChange} />)
const container = screen.getByText('25').closest('.bg-components-segmented-control-bg-normal')!
const option10 = Array.from(container.children).find(el => el.textContent === '10')!
fireEvent.click(option10)
expect(onLimitChange).toHaveBeenCalledWith(10)
})
it('should call onLimitChange with 50 when 50 option is clicked', () => {
const onLimitChange = vi.fn()
render(<CustomizedPagination {...defaultProps} onLimitChange={onLimitChange} />)
fireEvent.click(screen.getByText('50'))
expect(onLimitChange).toHaveBeenCalledWith(50)
})
it('should call onChange when a page button is clicked', () => {
const onChange = vi.fn()
render(<CustomizedPagination {...defaultProps} current={0} total={50} limit={10} onChange={onChange} />)
fireEvent.click(screen.getByText('3'))
expect(onChange).toHaveBeenCalledWith(2) // 0-indexed
})
it('should correctly select active limit style for 25 and 50', () => {
// Test limit 25
const { container: containerA } = render(<CustomizedPagination current={0} total={100} limit={25} onChange={vi.fn()} onLimitChange={vi.fn()} />)
const wrapper25 = Array.from(containerA.querySelectorAll('div.system-sm-medium')).find(el => el.textContent === '25')!
expect(wrapper25)!.toHaveClass('bg-components-segmented-control-item-active-bg')
// Test limit 50
const { container: containerB } = render(<CustomizedPagination current={0} total={100} limit={50} onChange={vi.fn()} onLimitChange={vi.fn()} />)
const wrapper50 = Array.from(containerB.querySelectorAll('div.system-sm-medium')).find(el => el.textContent === '50')!
expect(wrapper50)!.toHaveClass('bg-components-segmented-control-item-active-bg')
})
})
describe('Edge Cases', () => {
it('should handle total of 0', () => {
const { container } = render(<CustomizedPagination {...defaultProps} total={0} />)
expect(container)!.toBeInTheDocument()
})
it('should handle confirm when input value is unchanged (covers false branch of empty string check)', () => {
vi.useFakeTimers()
const onChange = vi.fn()
render(<CustomizedPagination {...defaultProps} current={4} onChange={onChange} />)
fireEvent.click(screen.getByText('/'))
const input = screen.getByRole('textbox')
// Blur without changing anything
fireEvent.blur(input)
act(() => {
vi.advanceTimersByTime(500)
})
// onChange should NOT be called
expect(onChange).not.toHaveBeenCalled()
expect(screen.queryByRole('textbox')).not.toBeInTheDocument()
})
it('should ignore other keys in handleInputKeyDown (covers false branch of Escape check)', () => {
render(<CustomizedPagination {...defaultProps} current={4} />)
fireEvent.click(screen.getByText('/'))
const input = screen.getByRole('textbox')
fireEvent.keyDown(input, { key: 'a' })
expect(screen.getByRole('textbox'))!.toBeInTheDocument()
})
it('should trigger handleInputConfirm with empty string specifically on keydown Enter', async () => {
const { userEvent } = await import('@testing-library/user-event')
const user = userEvent.setup()
render(<CustomizedPagination {...defaultProps} current={4} />)
fireEvent.click(screen.getByText('/'))
const input = screen.getByRole('textbox')
await user.clear(input)
await user.type(input, '{Enter}')
// Wait for debounce 500ms
await new Promise(r => setTimeout(r, 600))
// Validates `inputValue === ''` path under `handleInputConfirm` triggered by Enter
// Validates `inputValue === ''` path under `handleInputConfirm` triggered by Enter
// Validates `inputValue === ''` path under `handleInputConfirm` triggered by Enter
// Validates `inputValue === ''` path under `handleInputConfirm` triggered by Enter
// Validates `inputValue === ''` path under `handleInputConfirm` triggered by Enter
// Validates `inputValue === ''` path under `handleInputConfirm` triggered by Enter
// Validates `inputValue === ''` path under `handleInputConfirm` triggered by Enter
// Validates `inputValue === ''` path under `handleInputConfirm` triggered by Enter
// Validates `inputValue === ''` path under `handleInputConfirm` triggered by Enter
// Validates `inputValue === ''` path under `handleInputConfirm` triggered by Enter
// Validates `inputValue === ''` path under `handleInputConfirm` triggered by Enter
// Validates `inputValue === ''` path under `handleInputConfirm` triggered by Enter
// Validates `inputValue === ''` path under `handleInputConfirm` triggered by Enter
// Validates `inputValue === ''` path under `handleInputConfirm` triggered by Enter
// Validates `inputValue === ''` path under `handleInputConfirm` triggered by Enter
// Validates `inputValue === ''` path under `handleInputConfirm` triggered by Enter
// Validates `inputValue === ''` path under `handleInputConfirm` triggered by Enter
// Validates `inputValue === ''` path under `handleInputConfirm` triggered by Enter
// Validates `inputValue === ''` path under `handleInputConfirm` triggered by Enter
// Validates `inputValue === ''` path under `handleInputConfirm` triggered by Enter
// Validates `inputValue === ''` path under `handleInputConfirm` triggered by Enter
// Validates `inputValue === ''` path under `handleInputConfirm` triggered by Enter
// Validates `inputValue === ''` path under `handleInputConfirm` triggered by Enter
// Validates `inputValue === ''` path under `handleInputConfirm` triggered by Enter
// Validates `inputValue === ''` path under `handleInputConfirm` triggered by Enter
// Validates `inputValue === ''` path under `handleInputConfirm` triggered by Enter
// Validates `inputValue === ''` path under `handleInputConfirm` triggered by Enter
// Validates `inputValue === ''` path under `handleInputConfirm` triggered by Enter
// Validates `inputValue === ''` path under `handleInputConfirm` triggered by Enter
// Validates `inputValue === ''` path under `handleInputConfirm` triggered by Enter
// Validates `inputValue === ''` path under `handleInputConfirm` triggered by Enter
// Validates `inputValue === ''` path under `handleInputConfirm` triggered by Enter
expect(screen.queryByRole('textbox')).not.toBeInTheDocument()
})
it('should explicitly trigger Escape key logic in handleInputKeyDown', async () => {
const { userEvent } = await import('@testing-library/user-event')
const user = userEvent.setup()
render(<CustomizedPagination {...defaultProps} current={4} />)
fireEvent.click(screen.getByText('/'))
const input = screen.getByRole('textbox')
await user.type(input, '{Escape}')
// Wait for debounce 500ms
await new Promise(r => setTimeout(r, 600))
expect(screen.queryByRole('textbox')).not.toBeInTheDocument()
})
it('should handle single page', () => {
render(<CustomizedPagination {...defaultProps} total={5} limit={10} />)
// totalPages = 1, both buttons should be disabled
const buttons = screen.getAllByRole('button')
expect(buttons[0])!.toBeDisabled()
expect(buttons[buttons.length - 1])!.toBeDisabled()
})
it('should restore input value when blurred with empty value', () => {
render(<CustomizedPagination {...defaultProps} current={4} />)
fireEvent.click(screen.getByText('/'))
const input = screen.getByRole('textbox')
fireEvent.change(input, { target: { value: '' } })
fireEvent.blur(input)
// Should close input without calling onChange, restoring to current + 1
// Should close input without calling onChange, restoring to current + 1
// Should close input without calling onChange, restoring to current + 1
// Should close input without calling onChange, restoring to current + 1
// Should close input without calling onChange, restoring to current + 1
// Should close input without calling onChange, restoring to current + 1
// Should close input without calling onChange, restoring to current + 1
// Should close input without calling onChange, restoring to current + 1
// Should close input without calling onChange, restoring to current + 1
// Should close input without calling onChange, restoring to current + 1
// Should close input without calling onChange, restoring to current + 1
// Should close input without calling onChange, restoring to current + 1
// Should close input without calling onChange, restoring to current + 1
// Should close input without calling onChange, restoring to current + 1
// Should close input without calling onChange, restoring to current + 1
// Should close input without calling onChange, restoring to current + 1
// Should close input without calling onChange, restoring to current + 1
// Should close input without calling onChange, restoring to current + 1
// Should close input without calling onChange, restoring to current + 1
// Should close input without calling onChange, restoring to current + 1
// Should close input without calling onChange, restoring to current + 1
// Should close input without calling onChange, restoring to current + 1
// Should close input without calling onChange, restoring to current + 1
// Should close input without calling onChange, restoring to current + 1
// Should close input without calling onChange, restoring to current + 1
// Should close input without calling onChange, restoring to current + 1
// Should close input without calling onChange, restoring to current + 1
// Should close input without calling onChange, restoring to current + 1
// Should close input without calling onChange, restoring to current + 1
// Should close input without calling onChange, restoring to current + 1
// Should close input without calling onChange, restoring to current + 1
// Should close input without calling onChange, restoring to current + 1
expect(screen.queryByRole('textbox')).not.toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,549 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { Pagination } from '../pagination'
// Helper to render Pagination with common defaults
function renderPagination({
currentPage = 0,
totalPages = 10,
setCurrentPage = vi.fn(),
edgePageCount = 2,
middlePagesSiblingCount = 1,
truncableText = '...',
truncableClassName = 'truncable',
children,
}: {
currentPage?: number
totalPages?: number
setCurrentPage?: (page: number) => void
edgePageCount?: number
middlePagesSiblingCount?: number
truncableText?: string
truncableClassName?: string
children?: React.ReactNode
} = {}) {
return render(
<Pagination
currentPage={currentPage}
totalPages={totalPages}
setCurrentPage={setCurrentPage}
edgePageCount={edgePageCount}
middlePagesSiblingCount={middlePagesSiblingCount}
truncableText={truncableText}
truncableClassName={truncableClassName}
>
{children}
</Pagination>,
)
}
describe('Pagination', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('Rendering', () => {
it('should render without crashing', () => {
const { container } = renderPagination()
expect(container).toBeInTheDocument()
})
it('should render children', () => {
renderPagination({ children: <span>child content</span> })
expect(screen.getByText(/child content/i)).toBeInTheDocument()
})
it('should apply className to wrapper div', () => {
const { container } = render(
<Pagination
currentPage={0}
totalPages={5}
setCurrentPage={vi.fn()}
edgePageCount={2}
middlePagesSiblingCount={1}
className="my-pagination"
>
<span>test</span>
</Pagination>,
)
expect(container.firstChild).toHaveClass('my-pagination')
})
it('should apply data-testid when provided', () => {
render(
<Pagination
currentPage={0}
totalPages={5}
setCurrentPage={vi.fn()}
edgePageCount={2}
middlePagesSiblingCount={1}
dataTestId="my-pagination"
>
<span>test</span>
</Pagination>,
)
expect(screen.getByTestId('my-pagination')).toBeInTheDocument()
})
})
describe('PrevButton', () => {
it('should render prev button', () => {
renderPagination({
currentPage: 3,
children: <Pagination.PrevButton>Prev</Pagination.PrevButton>,
})
expect(screen.getByText(/prev/i)).toBeInTheDocument()
})
it('should call setCurrentPage with previous page when clicked', () => {
const setCurrentPage = vi.fn()
renderPagination({
currentPage: 3,
setCurrentPage,
children: <Pagination.PrevButton>Prev</Pagination.PrevButton>,
})
fireEvent.click(screen.getByText(/prev/i))
expect(setCurrentPage).toHaveBeenCalledWith(2)
})
it('should not navigate below page 0', () => {
const setCurrentPage = vi.fn()
renderPagination({
currentPage: 0,
setCurrentPage,
children: <Pagination.PrevButton>Prev</Pagination.PrevButton>,
})
fireEvent.click(screen.getByText(/prev/i))
expect(setCurrentPage).not.toHaveBeenCalled()
})
it('should be disabled on first page', () => {
renderPagination({
currentPage: 0,
children: <Pagination.PrevButton>Prev</Pagination.PrevButton>,
})
expect(screen.getByText(/prev/i).closest('button')).toBeDisabled()
})
it('should navigate on Enter key press', () => {
const setCurrentPage = vi.fn()
renderPagination({
currentPage: 3,
setCurrentPage,
children: <Pagination.PrevButton>Prev</Pagination.PrevButton>,
})
fireEvent.keyDown(screen.getByText(/prev/i).closest('button')!, { key: 'Enter', code: 'Enter', keyCode: 13, which: 13 })
expect(setCurrentPage).toHaveBeenCalledWith(2)
})
it('should not navigate on Enter when disabled', () => {
const setCurrentPage = vi.fn()
renderPagination({
currentPage: 0,
setCurrentPage,
children: <Pagination.PrevButton>Prev</Pagination.PrevButton>,
})
fireEvent.keyDown(screen.getByText(/prev/i).closest('button')!, { key: 'Enter', code: 'Enter', keyCode: 13, which: 13 })
expect(setCurrentPage).not.toHaveBeenCalled()
})
it('should render with custom as element', () => {
renderPagination({
currentPage: 3,
children: <Pagination.PrevButton as={<div />}>Prev</Pagination.PrevButton>,
})
expect(screen.getByText(/prev/i)).toBeInTheDocument()
})
it('should apply dataTestId', () => {
renderPagination({
currentPage: 3,
children: <Pagination.PrevButton dataTestId="prev-btn">Prev</Pagination.PrevButton>,
})
expect(screen.getByTestId('prev-btn')).toBeInTheDocument()
})
})
describe('NextButton', () => {
it('should render next button', () => {
renderPagination({
currentPage: 0,
children: <Pagination.NextButton>Next</Pagination.NextButton>,
})
expect(screen.getByText(/next/i)).toBeInTheDocument()
})
it('should call setCurrentPage with next page when clicked', () => {
const setCurrentPage = vi.fn()
renderPagination({
currentPage: 0,
totalPages: 10,
setCurrentPage,
children: <Pagination.NextButton>Next</Pagination.NextButton>,
})
fireEvent.click(screen.getByText(/next/i))
expect(setCurrentPage).toHaveBeenCalledWith(1)
})
it('should not navigate beyond last page', () => {
const setCurrentPage = vi.fn()
renderPagination({
currentPage: 9,
totalPages: 10,
setCurrentPage,
children: <Pagination.NextButton>Next</Pagination.NextButton>,
})
fireEvent.click(screen.getByText(/next/i))
expect(setCurrentPage).not.toHaveBeenCalled()
})
it('should be disabled on last page', () => {
renderPagination({
currentPage: 9,
totalPages: 10,
children: <Pagination.NextButton>Next</Pagination.NextButton>,
})
expect(screen.getByText(/next/i).closest('button')).toBeDisabled()
})
it('should navigate on Enter key press', () => {
const setCurrentPage = vi.fn()
renderPagination({
currentPage: 0,
totalPages: 10,
setCurrentPage,
children: <Pagination.NextButton>Next</Pagination.NextButton>,
})
fireEvent.keyDown(screen.getByText(/next/i).closest('button')!, { key: 'Enter', code: 'Enter', keyCode: 13, which: 13 })
expect(setCurrentPage).toHaveBeenCalledWith(1)
})
it('should not navigate on Enter when disabled', () => {
const setCurrentPage = vi.fn()
renderPagination({
currentPage: 9,
totalPages: 10,
setCurrentPage,
children: <Pagination.NextButton>Next</Pagination.NextButton>,
})
fireEvent.keyDown(screen.getByText(/next/i).closest('button')!, { key: 'Enter', code: 'Enter', keyCode: 13, which: 13 })
expect(setCurrentPage).not.toHaveBeenCalled()
})
it('should apply dataTestId', () => {
renderPagination({
currentPage: 0,
children: <Pagination.NextButton dataTestId="next-btn">Next</Pagination.NextButton>,
})
expect(screen.getByTestId('next-btn')).toBeInTheDocument()
})
})
describe('PageButton', () => {
it('should render page number buttons', () => {
renderPagination({
currentPage: 0,
totalPages: 5,
children: (
<Pagination.PageButton
className="page-btn"
activeClassName="active"
inactiveClassName="inactive"
/>
),
})
expect(screen.getByText('1')).toBeInTheDocument()
expect(screen.getByText('5')).toBeInTheDocument()
})
it('should apply activeClassName to current page', () => {
renderPagination({
currentPage: 2,
totalPages: 5,
children: (
<Pagination.PageButton
className="page-btn"
activeClassName="active"
inactiveClassName="inactive"
/>
),
})
// current page is 2, so page 3 (1-indexed) should be active
expect(screen.getByText('3').closest('a')).toHaveClass('active')
})
it('should apply inactiveClassName to non-current pages', () => {
renderPagination({
currentPage: 2,
totalPages: 5,
children: (
<Pagination.PageButton
className="page-btn"
activeClassName="active"
inactiveClassName="inactive"
/>
),
})
expect(screen.getByText('1').closest('a')).toHaveClass('inactive')
})
it('should call setCurrentPage when a page button is clicked', () => {
const setCurrentPage = vi.fn()
renderPagination({
currentPage: 0,
totalPages: 5,
setCurrentPage,
children: (
<Pagination.PageButton
className="page-btn"
activeClassName="active"
inactiveClassName="inactive"
/>
),
})
fireEvent.click(screen.getByText('3'))
expect(setCurrentPage).toHaveBeenCalledWith(2) // 0-indexed
})
it('should navigate on Enter key press on a page button', () => {
const setCurrentPage = vi.fn()
renderPagination({
currentPage: 0,
totalPages: 5,
setCurrentPage,
children: (
<Pagination.PageButton
className="page-btn"
activeClassName="active"
inactiveClassName="inactive"
/>
),
})
fireEvent.keyDown(screen.getByText('4').closest('a')!, { key: 'Enter', code: 'Enter', keyCode: 13, which: 13 })
expect(setCurrentPage).toHaveBeenCalledWith(3) // 0-indexed
})
it('should render truncable text when pages are truncated', () => {
renderPagination({
currentPage: 5,
totalPages: 20,
edgePageCount: 2,
middlePagesSiblingCount: 1,
truncableText: '...',
children: (
<Pagination.PageButton
className="page-btn"
activeClassName="active"
inactiveClassName="inactive"
/>
),
})
// With 20 pages and current at 5, there should be truncation
expect(screen.getAllByText('...').length).toBeGreaterThanOrEqual(1)
})
})
describe('Edge Cases', () => {
it('should handle single page', () => {
const setCurrentPage = vi.fn()
renderPagination({
currentPage: 0,
totalPages: 1,
setCurrentPage,
children: (
<>
<Pagination.PrevButton>Prev</Pagination.PrevButton>
<Pagination.PageButton className="page-btn" activeClassName="active" inactiveClassName="inactive" />
<Pagination.NextButton>Next</Pagination.NextButton>
</>
),
})
expect(screen.getByText(/prev/i).closest('button')).toBeDisabled()
expect(screen.getByText(/next/i).closest('button')).toBeDisabled()
expect(screen.getByText('1')).toBeInTheDocument()
})
it('should handle zero total pages', () => {
const { container } = renderPagination({
currentPage: 0,
totalPages: 0,
children: (
<Pagination.PageButton className="page-btn" activeClassName="active" inactiveClassName="inactive" />
),
})
expect(container).toBeInTheDocument()
})
it('should cover undefined active/inactive dataTestIds', () => {
// Re-render PageButton without active/inactive data test ids to hit the undefined branch in cn() fallback
renderPagination({
currentPage: 1,
totalPages: 5,
children: (
<Pagination.PageButton
className="page-btn"
activeClassName="active"
inactiveClassName="inactive"
renderExtraProps={page => ({ 'aria-label': `Page ${page}` })}
/>
),
})
expect(screen.getByText('2')).toHaveAttribute('aria-label', 'Page 2')
})
it('should cover nextPages when edge pages fall perfectly into middle Pages', () => {
renderPagination({
currentPage: 5,
totalPages: 10,
edgePageCount: 8, // Very large edge page count to hit the filter(!middlePages.includes) branches
middlePagesSiblingCount: 1,
children: (
<Pagination.PageButton className="page-btn" activeClassName="active" inactiveClassName="inactive" />
),
})
expect(screen.getByText('1')).toBeInTheDocument()
expect(screen.getByText('10')).toBeInTheDocument()
})
it('should hide truncation element if truncable is false', () => {
renderPagination({
currentPage: 2,
totalPages: 5,
edgePageCount: 1,
middlePagesSiblingCount: 1,
// When we are at page 2, middle pages are [2, 3, 4] (if 0-indexed, wait, currentPage is 0-indexed in hook?)
// Let's just render the component which calls the internal TruncableElement, when previous/next are NOT truncable
children: (
<Pagination.PageButton className="page-btn" activeClassName="active" inactiveClassName="inactive" />
),
})
// Truncation only happens if middlePages > previousPages.last + 1
expect(screen.queryByText('...')).not.toBeInTheDocument()
})
it('should hit getAllPreviousPages with less than 1 element', () => {
renderPagination({
currentPage: 0,
totalPages: 10,
edgePageCount: 1,
middlePagesSiblingCount: 0,
children: <Pagination.PageButton className="btn" activeClassName="act" inactiveClassName="inact" />,
})
// With currentPage = 0, middlePages = [1], getAllPreviousPages() -> slice(0, 0) -> []
expect(screen.getByText('1')).toBeInTheDocument()
})
it('should fire previous() keyboard event even if it does nothing without crashing', () => {
// Line 38: pagination.currentPage + 1 > 1 check is usually guarded by disabled, but we can verify it explicitly.
const setCurrentPage = vi.fn()
// Use a span so that 'disabled' attribute doesn't prevent fireEvent.click from firing
renderPagination({
currentPage: 0,
setCurrentPage,
children: <Pagination.PrevButton as={<span />}>Prev</Pagination.PrevButton>,
})
fireEvent.click(screen.getByText('Prev'))
expect(setCurrentPage).not.toHaveBeenCalled()
})
it('should fire next() even if it does nothing without crashing', () => {
// Line 73: pagination.currentPage + 1 < pages.length verify
const setCurrentPage = vi.fn()
renderPagination({
currentPage: 10,
totalPages: 10,
setCurrentPage,
children: <Pagination.NextButton as={<span />}>Next</Pagination.NextButton>,
})
fireEvent.click(screen.getByText('Next'))
expect(setCurrentPage).not.toHaveBeenCalled()
})
it('should fall back to undefined when truncableClassName is empty', () => {
// Line 115: `<li className={truncableClassName || undefined}>{truncableText}</li>`
renderPagination({
currentPage: 5,
totalPages: 10,
truncableClassName: '',
children: (
<Pagination.PageButton className="page-btn" activeClassName="active" inactiveClassName="inactive" />
),
})
// Should not have a class attribute
const truncableElements = screen.getAllByText('...')
expect(truncableElements[0]).not.toHaveAttribute('class')
})
it('should handle dataTestIdActive and dataTestIdInactive completely', () => {
// Lines 137-144
renderPagination({
currentPage: 1, // 0-indexed, so page 2 is active
totalPages: 5,
children: (
<Pagination.PageButton
className="page-btn"
activeClassName="active"
inactiveClassName="inactive"
dataTestIdActive="active-test-id"
dataTestIdInactive="inactive-test-id"
/>
),
})
const activeBtn = screen.getByTestId('active-test-id')
expect(activeBtn).toHaveTextContent('2')
const inactiveBtn = screen.getByTestId('inactive-test-id-1') // page 1
expect(inactiveBtn).toHaveTextContent('1')
})
it('should hit getAllNextPages.length < 1 in hook', () => {
renderPagination({
currentPage: 2,
totalPages: 3,
edgePageCount: 1,
middlePagesSiblingCount: 0,
children: (
<Pagination.PageButton className="page-btn" activeClassName="active" inactiveClassName="inactive" />
),
})
// Current is 3 (index 2). middlePages = [3]. getAllNextPages = slice(3, 3) = []
// This will trigger the `getAllNextPages.length < 1` branch
expect(screen.getByText('3')).toBeInTheDocument()
})
it('should handle only dataTestIdInactive without dataTestIdActive', () => {
renderPagination({
currentPage: 1,
totalPages: 3,
children: (
<Pagination.PageButton
className="page-btn"
activeClassName="active"
inactiveClassName="inactive"
dataTestIdInactive="inactive-test-id"
/>
),
})
// Missing dataTestIdActive branch coverage on line 144
expect(screen.getByText('1')).toBeInTheDocument()
})
it('should handle only dataTestIdActive without dataTestIdInactive', () => {
renderPagination({
currentPage: 1, // page 2 is active
totalPages: 3,
children: (
<Pagination.PageButton
className="page-btn"
activeClassName="active"
inactiveClassName="inactive"
dataTestIdActive="active-test-id"
/>
),
})
// This hits the branch where dataTestIdActive exists but not dataTestIdInactive
expect(screen.getByTestId('active-test-id')).toHaveTextContent('2')
expect(screen.queryByTestId('inactive-test-id-1')).not.toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,94 @@
import type { IPaginationProps, IUsePagination } from './type'
import * as React from 'react'
import { useCallback } from 'react'
const usePagination = ({
currentPage,
setCurrentPage,
truncableText = '...',
truncableClassName = '',
totalPages,
edgePageCount,
middlePagesSiblingCount,
}: IPaginationProps): IUsePagination => {
const pages = React.useMemo(() => Array.from({ length: totalPages }, (_, i) => i + 1), [totalPages])
const hasPreviousPage = currentPage > 1
const hasNextPage = currentPage < totalPages
const isReachedToFirst = currentPage <= middlePagesSiblingCount
const isReachedToLast = currentPage + middlePagesSiblingCount >= totalPages
const middlePages = React.useMemo(() => {
const middlePageCount = middlePagesSiblingCount * 2 + 1
if (isReachedToFirst)
return pages.slice(0, middlePageCount)
if (isReachedToLast)
return pages.slice(-middlePageCount)
return pages.slice(
currentPage - middlePagesSiblingCount,
currentPage + middlePagesSiblingCount + 1,
)
}, [currentPage, isReachedToFirst, isReachedToLast, middlePagesSiblingCount, pages])
const getAllPreviousPages = useCallback(() => {
return pages.slice(0, middlePages[0]! - 1)
}, [middlePages, pages])
const previousPages = React.useMemo(() => {
if (isReachedToFirst || getAllPreviousPages().length < 1)
return []
return pages
.slice(0, edgePageCount)
.filter(p => !middlePages.includes(p))
}, [edgePageCount, getAllPreviousPages, isReachedToFirst, middlePages, pages])
const getAllNextPages = React.useMemo(() => {
return pages.slice(
middlePages[middlePages.length - 1],
pages[pages.length],
)
}, [pages, middlePages])
const nextPages = React.useMemo(() => {
if (isReachedToLast)
return []
if (getAllNextPages.length < 1)
return []
return pages
.slice(pages.length - edgePageCount, pages.length)
.filter(p => !middlePages.includes(p))
}, [edgePageCount, getAllNextPages.length, isReachedToLast, middlePages, pages])
const isPreviousTruncable = React.useMemo(() => {
// Is truncable if first value of middlePage is larger than last value of previousPages
return middlePages[0]! > previousPages[previousPages.length - 1]! + 1
}, [previousPages, middlePages])
const isNextTruncable = React.useMemo(() => {
// Is truncable if last value of middlePage is larger than first value of previousPages
return middlePages[middlePages.length - 1]! + 1 < nextPages[0]!
}, [nextPages, middlePages])
return {
currentPage,
setCurrentPage,
truncableText,
truncableClassName,
pages,
hasPreviousPage,
hasNextPage,
previousPages,
isPreviousTruncable,
middlePages,
isNextTruncable,
nextPages,
}
}
export default usePagination

View File

@ -0,0 +1,81 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import { useMemo, useState } from 'react'
import Pagination from '.'
const TOTAL_ITEMS = 120
const PaginationDemo = ({
initialPage = 0,
initialLimit = 10,
}: {
initialPage?: number
initialLimit?: number
}) => {
const [current, setCurrent] = useState(initialPage)
const [limit, setLimit] = useState(initialLimit)
const pageSummary = useMemo(() => {
const start = current * limit + 1
const end = Math.min((current + 1) * limit, TOTAL_ITEMS)
return `${start}-${end} of ${TOTAL_ITEMS}`
}, [current, limit])
return (
<div className="flex w-full max-w-3xl flex-col gap-4 rounded-2xl border border-divider-subtle bg-components-panel-bg p-6">
<div className="flex items-center justify-between text-xs tracking-[0.18em] text-text-tertiary uppercase">
<span>Log pagination</span>
<span className="rounded-md border border-divider-subtle bg-background-default px-2 py-1 font-medium text-text-secondary">
{pageSummary}
</span>
</div>
<Pagination
current={current}
total={TOTAL_ITEMS}
limit={limit}
onChange={setCurrent}
onLimitChange={(nextLimit) => {
setCurrent(0)
setLimit(nextLimit)
}}
/>
</div>
)
}
const meta = {
title: 'Base/Navigation/Pagination',
component: PaginationDemo,
parameters: {
layout: 'centered',
docs: {
description: {
component: 'Paginate long lists with optional per-page selector. Demonstrates the inline page jump input and quick limit toggles.',
},
},
},
args: {
initialPage: 0,
initialLimit: 10,
},
argTypes: {
initialPage: {
control: { type: 'number', min: 0, max: 9, step: 1 },
},
initialLimit: {
control: { type: 'radio' },
options: [10, 25, 50],
},
},
tags: ['autodocs'],
} satisfies Meta<typeof PaginationDemo>
export default meta
type Story = StoryObj<typeof meta>
export const Playground: Story = {}
export const StartAtMiddle: Story = {
args: {
initialPage: 4,
},
}

View File

@ -0,0 +1,201 @@
import type { FC } from 'react'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { RiArrowLeftLine, RiArrowRightLine } from '@remixicon/react'
import { useDebounceFn } from 'ahooks'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
import { Pagination } from './pagination'
export type Props = {
className?: string
current: number
onChange: (cur: number) => void
total: number
limit?: number
onLimitChange?: (limit: number) => void
}
const CustomizedPagination: FC<Props> = ({
className,
current,
onChange,
total,
limit = 10,
onLimitChange,
}) => {
const { t } = useTranslation()
const totalPages = Math.ceil(total / limit)
const inputRef = React.useRef<HTMLDivElement>(null)
const [showInput, setShowInput] = React.useState(false)
const [inputValue, setInputValue] = React.useState<string | number>(current + 1)
const [showPerPageTip, setShowPerPageTip] = React.useState(false)
const { run: handlePaging } = useDebounceFn((value: string) => {
if (Number.parseInt(value) > totalPages) {
setInputValue(totalPages)
onChange(totalPages - 1)
setShowInput(false)
return
}
if (Number.parseInt(value) < 1) {
setInputValue(1)
onChange(0)
setShowInput(false)
return
}
onChange(Number.parseInt(value) - 1)
setInputValue(Number.parseInt(value))
setShowInput(false)
}, { wait: 500 })
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
if (!value)
return setInputValue('')
if (isNaN(Number.parseInt(value)))
return setInputValue('')
setInputValue(Number.parseInt(value))
}
const handleInputConfirm = () => {
if (inputValue !== '' && String(inputValue) !== String(current + 1)) {
handlePaging(String(inputValue))
return
}
if (inputValue === '')
setInputValue(current + 1)
setShowInput(false)
}
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault()
handleInputConfirm()
}
else if (e.key === 'Escape') {
e.preventDefault()
setInputValue(current + 1)
setShowInput(false)
}
}
const handleInputBlur = () => {
handleInputConfirm()
}
return (
<Pagination
className={cn('flex w-full items-center px-6 py-3 select-none', className)}
currentPage={current}
edgePageCount={2}
middlePagesSiblingCount={1}
setCurrentPage={onChange}
totalPages={totalPages}
truncableClassName="flex items-center justify-center w-8 px-1 py-2 system-sm-medium text-text-tertiary"
truncableText="..."
>
<div className="flex items-center gap-0.5 rounded-[10px] bg-background-section-burn p-0.5">
<Pagination.PrevButton
as={<div></div>}
disabled={current === 0}
>
<Button
variant="secondary"
className="size-7 px-1.5"
disabled={current === 0}
>
<RiArrowLeftLine className="size-4" />
</Button>
</Pagination.PrevButton>
{!showInput && (
<div
ref={inputRef}
className="flex items-center gap-0.5 rounded-lg px-2 py-1.5 hover:cursor-text hover:bg-state-base-hover-alt"
onClick={() => setShowInput(true)}
>
<div className="system-xs-medium text-text-secondary">{current + 1}</div>
<div className="system-xs-medium text-text-quaternary">/</div>
<div className="system-xs-medium text-text-secondary">{totalPages}</div>
</div>
)}
{showInput && (
<Input
styleCss={{
height: '28px',
width: `${inputRef.current?.clientWidth}px`,
}}
placeholder=""
autoFocus
value={inputValue}
onChange={handleInputChange}
onKeyDown={handleInputKeyDown}
onBlur={handleInputBlur}
/>
)}
<Pagination.NextButton
as={<div></div>}
disabled={current === totalPages - 1}
>
<Button
variant="secondary"
className="size-7 px-1.5"
disabled={current === totalPages - 1}
>
<RiArrowRightLine className="size-4" />
</Button>
</Pagination.NextButton>
</div>
<div className={cn('flex grow list-none items-center justify-center gap-1')}>
<Pagination.PageButton
className="flex min-w-8 cursor-pointer items-center justify-center rounded-lg px-1 py-2 system-sm-medium hover:bg-components-button-ghost-bg-hover"
activeClassName="bg-components-button-tertiary-bg text-components-button-tertiary-text hover:bg-components-button-ghost-bg-hover"
inactiveClassName="text-text-tertiary"
/>
</div>
{onLimitChange && (
<div className="flex shrink-0 items-center gap-2">
<div className="w-[51px] shrink-0 text-end system-2xs-regular-uppercase text-text-tertiary">{showPerPageTip ? t('pagination.perPage', { ns: 'common' }) : ''}</div>
<div
className="flex items-center gap-px rounded-[10px] bg-components-segmented-control-bg-normal p-0.5"
onMouseEnter={() => setShowPerPageTip(true)}
onMouseLeave={() => setShowPerPageTip(false)}
>
<div
className={cn(
'cursor-pointer rounded-lg border-[0.5px] border-transparent px-2.5 py-1.5 system-sm-medium text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary',
limit === 10 && 'border-components-segmented-control-item-active-border bg-components-segmented-control-item-active-bg text-text-secondary shadow-xs hover:bg-components-segmented-control-item-active-bg',
)}
onClick={() => onLimitChange?.(10)}
>
10
</div>
<div
className={cn(
'cursor-pointer rounded-lg border-[0.5px] border-transparent px-2.5 py-1.5 system-sm-medium text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary',
limit === 25 && 'border-components-segmented-control-item-active-border bg-components-segmented-control-item-active-bg text-text-secondary shadow-xs hover:bg-components-segmented-control-item-active-bg',
)}
onClick={() => onLimitChange?.(25)}
>
25
</div>
<div
className={cn(
'cursor-pointer rounded-lg border-[0.5px] border-transparent px-2.5 py-1.5 system-sm-medium text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary',
limit === 50 && 'border-components-segmented-control-item-active-border bg-components-segmented-control-item-active-bg text-text-secondary shadow-xs hover:bg-components-segmented-control-item-active-bg',
)}
onClick={() => onLimitChange?.(50)}
>
50
</div>
</div>
</div>
)}
</Pagination>
)
}
export default CustomizedPagination

View File

@ -0,0 +1,190 @@
import type {
ButtonProps,
IPagination,
IPaginationProps,
PageButtonProps,
} from './type'
import { cn } from '@langgenius/dify-ui/cn'
import { noop } from 'es-toolkit/function'
import * as React from 'react'
import usePagination from './hook'
const defaultState: IPagination = {
currentPage: 0,
setCurrentPage: noop,
truncableText: '...',
truncableClassName: '',
pages: [],
hasPreviousPage: false,
hasNextPage: false,
previousPages: [],
isPreviousTruncable: false,
middlePages: [],
isNextTruncable: false,
nextPages: [],
}
const PaginationContext: React.Context<IPagination> = React.createContext<IPagination>(defaultState)
const PrevButton = ({
className,
children,
dataTestId,
as = <button type="button" />,
...buttonProps
}: ButtonProps) => {
const pagination = React.useContext(PaginationContext)
const previous = () => {
if (pagination.currentPage + 1 > 1)
pagination.setCurrentPage(pagination.currentPage - 1)
}
const disabled = pagination.currentPage === 0
return (
<as.type
{...buttonProps}
{...as.props}
className={cn(className, as.props.className)}
onClick={() => previous()}
tabIndex={disabled ? '-1' : 0}
disabled={disabled}
data-testid={dataTestId}
onKeyDown={(event: React.KeyboardEvent) => {
event.preventDefault()
if (event.key === 'Enter' && !disabled)
previous()
}}
>
{as.props.children ?? children}
</as.type>
)
}
const NextButton = ({
className,
children,
dataTestId,
as = <button type="button" />,
...buttonProps
}: ButtonProps) => {
const pagination = React.useContext(PaginationContext)
const next = () => {
if (pagination.currentPage + 1 < pagination.pages.length)
pagination.setCurrentPage(pagination.currentPage + 1)
}
const disabled = pagination.currentPage === pagination.pages.length - 1
return (
<as.type
{...buttonProps}
{...as.props}
className={cn(className, as.props.className)}
onClick={() => next()}
tabIndex={disabled ? '-1' : 0}
disabled={disabled}
data-testid={dataTestId}
onKeyDown={(event: React.KeyboardEvent) => {
event.preventDefault()
if (event.key === 'Enter' && !disabled)
next()
}}
>
{as.props.children ?? children}
</as.type>
)
}
type ITruncableElementProps = {
prev?: boolean
}
const TruncableElement = ({ prev }: ITruncableElementProps) => {
const pagination: IPagination = React.useContext(PaginationContext)
const {
isPreviousTruncable,
isNextTruncable,
truncableText,
truncableClassName,
} = pagination
return ((isPreviousTruncable && prev === true) || (isNextTruncable && !prev))
? (
<li className={truncableClassName || undefined}>{truncableText}</li>
)
: null
}
const PageButton = ({
as = <a />,
className,
dataTestIdActive,
dataTestIdInactive,
activeClassName,
inactiveClassName,
renderExtraProps,
}: PageButtonProps) => {
const pagination: IPagination = React.useContext(PaginationContext)
const renderPageButton = (page: number) => (
<li key={page}>
<as.type
data-testid={
cn({
[`${dataTestIdActive}`]:
dataTestIdActive && pagination.currentPage + 1 === page,
[`${dataTestIdInactive}-${page}`]:
dataTestIdActive && pagination.currentPage + 1 !== page,
}) || undefined
}
tabIndex={0}
onKeyDown={(event: React.KeyboardEvent) => {
if (event.key === 'Enter')
pagination.setCurrentPage(page - 1)
}}
onClick={() => pagination.setCurrentPage(page - 1)}
className={cn(
className,
pagination.currentPage + 1 === page
? activeClassName
: inactiveClassName,
)}
{...as.props}
{...(renderExtraProps ? renderExtraProps(page) : {})}
>
{page}
</as.type>
</li>
)
return (
<>
{pagination.previousPages.map(renderPageButton)}
<TruncableElement prev />
{pagination.middlePages.map(renderPageButton)}
<TruncableElement />
{pagination.nextPages.map(renderPageButton)}
</>
)
}
export const Pagination = ({
dataTestId,
...paginationProps
}: IPaginationProps & { dataTestId?: string }) => {
const pagination = usePagination(paginationProps)
return (
<PaginationContext.Provider value={pagination}>
<div className={paginationProps.className} data-testid={dataTestId}>
{paginationProps.children}
</div>
</PaginationContext.Provider>
)
}
Pagination.PrevButton = PrevButton
Pagination.NextButton = NextButton
Pagination.PageButton = PageButton

View File

@ -0,0 +1,64 @@
import type { ButtonHTMLAttributes } from 'react'
type ElementProps = {
className?: string
children?: React.ReactNode
[key: string]: unknown
}
type IBasePaginationProps = {
currentPage: number
setCurrentPage: (page: number) => void
truncableText?: string
truncableClassName?: string
}
type IPaginationProps = IBasePaginationProps & {
totalPages: number
edgePageCount: number
middlePagesSiblingCount: number
className?: string
children?: React.ReactNode
}
type IUsePagination = IBasePaginationProps & {
pages: number[]
hasPreviousPage: boolean
hasNextPage: boolean
previousPages: number[]
isPreviousTruncable: boolean
middlePages: number[]
isNextTruncable: boolean
nextPages: number[]
}
type IPagination = IUsePagination & {
setCurrentPage: (page: number) => void
}
type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
as?: React.ReactElement<ElementProps>
children?: string | React.ReactNode
className?: string
dataTestId?: string
}
type PageButtonProps = ButtonProps & {
/**
* Provide a custom ReactElement (e.g. Next/Link)
*/
as?: React.ReactElement<ElementProps>
activeClassName?: string
inactiveClassName?: string
dataTestIdActive?: string
dataTestIdInactive?: string
renderExtraProps?: (pageNum: number) => {}
}
export type {
ButtonProps,
IPagination,
IPaginationProps,
IUsePagination,
PageButtonProps,
}

View File

@ -1,17 +1,23 @@
import { memo } from 'react'
import { IS_PROD } from '@/config'
import { headers } from '@/next/headers'
import Script from '@/next/script'
import { buildCreateAppAttributionBootstrapScript } from '@/utils/create-app-tracking'
export async function CreateAppAttributionBootstrap() {
const nonce = IS_PROD ? (await headers()).get('x-nonce') ?? undefined : undefined
const CreateAppAttributionBootstrap = async () => {
const nonce = IS_PROD ? (await headers()).get('x-nonce') ?? '' : ''
/* v8 ignore next -- `nonce` is always a string (`''` or header value), so nullish fallback is unreachable in runtime. @preserve */
const scriptNonce = nonce ?? undefined
return (
<Script
id="create-app-attribution-bootstrap"
strategy="beforeInteractive"
nonce={nonce}
nonce={scriptNonce}
>
{buildCreateAppAttributionBootstrapScript()}
</Script>
)
}
export default memo(CreateAppAttributionBootstrap)

View File

@ -1,4 +1,5 @@
import type { ReactNode } from 'react'
import type { Props as PaginationProps } from '@/app/components/base/pagination'
import type { SimpleDocumentDetail } from '@/models/datasets'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { act, fireEvent, render, screen } from '@testing-library/react'
@ -8,14 +9,6 @@ import DocumentList from '../../list'
const mockPush = vi.fn()
type PaginationProps = {
current: number
onChange: (page: number) => void
total: number
limit?: number
onLimitChange?: (limit: number) => void
}
vi.mock('@/next/navigation', () => ({
useRouter: () => ({
push: mockPush,

View File

@ -1,11 +1,12 @@
'use client'
import type { Props as PaginationProps } from '@/app/components/base/pagination'
import type { SimpleDocumentDetail } from '@/models/datasets'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { CheckboxGroup } from '@langgenius/dify-ui/checkbox-group'
import { Pagination } from '@langgenius/dify-ui/pagination'
import { useBoolean } from 'ahooks'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Pagination from '@/app/components/base/pagination'
import EditMetadataBatchModal from '@/app/components/datasets/metadata/edit-metadata-batch/modal'
import useBatchEditDocumentMetadata from '@/app/components/datasets/metadata/hooks/use-batch-edit-document-metadata'
import { useDatasetDetailContextWithSelector as useDatasetDetailContext } from '@/context/dataset-detail'
@ -18,15 +19,6 @@ import RenameModal from './rename-modal'
type LocalDoc = SimpleDocumentDetail & { percent?: number }
type PaginationProps = {
className?: string
current: number
total: number
limit?: number
onChange: (page: number) => void
onLimitChange?: (limit: number) => void
}
type DocumentListProps = {
embeddingAvailable: boolean
documents: LocalDoc[]
@ -56,8 +48,6 @@ const DocumentList = ({
onSortChange,
}: DocumentListProps) => {
const { t } = useTranslation()
const pageSize = pagination.limit ?? 10
const totalPages = Math.max(Math.ceil(pagination.total / pageSize), 1)
const datasetConfig = useDatasetDetailContext(s => s.dataset)
const chunkingMode = datasetConfig?.doc_form
const isGeneralMode = chunkingMode !== ChunkingMode.parentChild
@ -208,25 +198,8 @@ const DocumentList = ({
{!!pagination.total && (
<Pagination
className="shrink-0"
page={pagination.current + 1}
totalPages={totalPages}
onPageChange={page => pagination.onChange(page - 1)}
labels={{
previous: t('pagination.previous', { ns: 'common' }),
next: t('pagination.next', { ns: 'common' }),
editPageNumber: (page, totalPages) => t('pagination.editPageNumber', { ns: 'common', page, totalPages }),
pageNumberInput: t('pagination.pageNumber', { ns: 'common' }),
}}
pageSize={pagination.onLimitChange
? {
value: pageSize,
options: [10, 25, 50],
onValueChange: pagination.onLimitChange,
label: t('pagination.perPage', { ns: 'common' }),
ariaLabel: t('pagination.perPage', { ns: 'common' }),
}
: undefined}
{...pagination}
className="w-full shrink-0"
/>
)}

View File

@ -203,22 +203,18 @@ vi.mock('@/app/components/base/divider', () => ({
default: () => <hr data-testid="divider" />,
}))
vi.mock('@langgenius/dify-ui/pagination', () => ({
Pagination: ({ page, totalPages, onPageChange, pageSize }: {
page: number
totalPages: number
onPageChange: (page: number) => void
pageSize?: {
onValueChange: (limit: number) => void
}
vi.mock('@/app/components/base/pagination', () => ({
default: ({ current, total, onChange, onLimitChange }: {
current: number
total: number
onChange: (page: number) => void
onLimitChange: (limit: number) => void
}) => (
<div data-testid="pagination">
<span data-testid="current-page">{page - 1}</span>
<span data-testid="total-pages">{totalPages}</span>
<button data-testid="next-page" onClick={() => onPageChange(page + 1)}>Next</button>
{pageSize && (
<button data-testid="change-limit" onClick={() => pageSize.onValueChange(20)}>Change Limit</button>
)}
<span data-testid="current-page">{current}</span>
<span data-testid="total-items">{total}</span>
<button data-testid="next-page" onClick={() => onChange(current + 1)}>Next</button>
<button data-testid="change-limit" onClick={() => onLimitChange(20)}>Change Limit</button>
</div>
),
}))
@ -1184,14 +1180,15 @@ describe('Inline callback and hook initialization coverage', () => {
})
})
it('should compute pagination pages from child chunk data in full-doc mode', () => {
// Covers paginationTotal in full-doc mode
it('should compute pagination total from child chunk data in full-doc mode', () => {
mockDocForm.current = ChunkingModeEnum.parentChild
mockParentMode.current = 'full-doc'
mockChildSegmentListData.total = 42
render(<Completed {...defaultProps} />, { wrapper: createWrapper() })
expect(screen.getByTestId('total-pages'))!.toHaveTextContent('5')
expect(screen.getByTestId('total-items'))!.toHaveTextContent('42')
})
// Covers search input change

View File

@ -3,10 +3,10 @@ import type { FC } from 'react'
import type { SegmentListContextValue } from './segment-list-context'
import type { SegmentImportStatus } from '@/types/dataset'
import { CheckboxGroup } from '@langgenius/dify-ui/checkbox-group'
import { Pagination } from '@langgenius/dify-ui/pagination'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
import Pagination from '@/app/components/base/pagination'
import {
useChunkListAllKey,
useChunkListDisabledKey,
@ -142,11 +142,10 @@ const Completed: FC<ICompletedProps> = ({
return childSegmentDataHook.childChunkListData?.total || 0
return segmentListDataHook.segmentListData?.total || 0
}, [segmentListDataHook.isFullDocMode, childSegmentDataHook.childChunkListData, segmentListDataHook.segmentListData])
const totalPages = Math.max(Math.ceil(paginationTotal / limit), 1)
// Handle page change
const handlePageChange = useCallback((page: number) => {
setCurrentPage(page)
setCurrentPage(page + 1)
}, [])
// Context value
@ -226,22 +225,12 @@ const Completed: FC<ICompletedProps> = ({
{/* Pagination */}
<Divider type="horizontal" className="mx-6 my-0 h-px w-auto bg-divider-subtle" />
<Pagination
page={currentPage}
totalPages={totalPages}
onPageChange={handlePageChange}
labels={{
previous: t('pagination.previous', { ns: 'common' }),
next: t('pagination.next', { ns: 'common' }),
editPageNumber: (page, totalPages) => t('pagination.editPageNumber', { ns: 'common', page, totalPages }),
pageNumberInput: t('pagination.pageNumber', { ns: 'common' }),
}}
pageSize={{
value: limit,
options: [10, 25, 50],
onValueChange: setLimit,
label: t('pagination.perPage', { ns: 'common' }),
ariaLabel: t('pagination.perPage', { ns: 'common' }),
}}
current={currentPage - 1}
onChange={handlePageChange}
total={paginationTotal}
limit={limit}
onLimitChange={setLimit}
className={segmentListDataHook.isFullDocMode ? 'px-3' : ''}
/>
{/* Drawer Group - only render when docForm is available */}

View File

@ -18,7 +18,6 @@ import {
DrawerPortal,
DrawerViewport,
} from '@langgenius/dify-ui/drawer'
import { Pagination } from '@langgenius/dify-ui/pagination'
import { useBoolean } from 'ahooks'
import * as React from 'react'
import { useCallback, useEffect, useState } from 'react'
@ -26,6 +25,7 @@ import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import FloatRightContainer from '@/app/components/base/float-right-container'
import Loading from '@/app/components/base/loading'
import Pagination from '@/app/components/base/pagination'
import docStyle from '@/app/components/datasets/documents/detail/completed/style.module.css'
import DatasetDetailContext from '@/context/dataset-detail'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
@ -63,7 +63,6 @@ const HitTestingPage: FC<Props> = ({ datasetId }: Props) => {
const { data: recordsRes, refetch: recordsRefetch, isLoading: isRecordsLoading } = useDatasetTestingRecords(datasetId, { limit, page: currPage + 1 })
const total = recordsRes?.total || 0
const totalPages = total ? Math.max(Math.ceil(total / limit), 1) : 1
const { dataset: currentDataset } = useContext(DatasetDetailContext)
const isExternal = currentDataset?.provider === 'external'
@ -152,19 +151,7 @@ const HitTestingPage: FC<Props> = ({ datasetId }: Props) => {
<>
<Records records={recordsRes?.data} onClickRecord={handleClickRecord} />
{(total && total > limit)
? (
<Pagination
page={currPage + 1}
totalPages={totalPages}
onPageChange={page => setCurrPage(page - 1)}
labels={{
previous: t('pagination.previous', { ns: 'common' }),
next: t('pagination.next', { ns: 'common' }),
editPageNumber: (page, totalPages) => t('pagination.editPageNumber', { ns: 'common', page, totalPages }),
pageNumberInput: t('pagination.pageNumber', { ns: 'common' }),
}}
/>
)
? <Pagination current={currPage} onChange={setCurrPage} total={total} limit={limit} />
: null}
</>
)}

View File

@ -1,4 +1,4 @@
import type { AnchorHTMLAttributes, ReactElement } from 'react'
import type { ReactElement } from 'react'
import { fireEvent, screen } from '@testing-library/react'
import { vi } from 'vitest'
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
@ -55,7 +55,7 @@ vi.mock('@/context/workspace-context-provider', () => ({
}))
vi.mock('@/next/link', () => ({
default: ({ children, href, ...props }: AnchorHTMLAttributes<HTMLAnchorElement> & { href?: string }) => <a href={href} {...props}>{children}</a>,
default: ({ children, href }: { children?: React.ReactNode, href?: string }) => <a href={href}>{children}</a>,
}))
let mockIsWorkspaceEditor = false
@ -122,9 +122,7 @@ describe('Header', () => {
it('should render header with main nav components', () => {
renderHeader()
expect(screen.getByRole('link', { name: 'Dify' })).toHaveAttribute('href', '/apps')
expect(screen.queryByRole('heading', { level: 1 })).not.toBeInTheDocument()
expect(screen.queryByRole('img', { name: /dify logo/i })).not.toBeInTheDocument()
expect(screen.getByRole('img', { name: /dify logo/i })).toBeInTheDocument()
expect(screen.getByTestId('workplace-selector')).toBeInTheDocument()
expect(screen.getByTestId('app-nav')).toBeInTheDocument()
expect(screen.getByTestId('account-dropdown')).toBeInTheDocument()
@ -168,7 +166,7 @@ describe('Header', () => {
mockMedia = 'mobile'
renderHeader()
expect(screen.getByRole('link', { name: 'Dify' })).toHaveAttribute('href', '/apps')
expect(screen.getByRole('img', { name: /dify logo/i })).toBeInTheDocument()
expect(screen.queryByTestId('env-nav')).not.toBeInTheDocument()
})
@ -179,8 +177,8 @@ describe('Header', () => {
renderHeader()
expect(screen.getByRole('link', { name: 'Acme Workspace' })).toHaveAttribute('href', '/apps')
expect(screen.queryByRole('img', { name: /logo/i })).not.toBeInTheDocument()
expect(screen.getByText('Acme Workspace')).toBeInTheDocument()
expect(screen.getByRole('img', { name: /logo/i })).toBeInTheDocument()
expect(screen.queryByRole('img', { name: /dify logo/i })).not.toBeInTheDocument()
})
@ -191,18 +189,18 @@ describe('Header', () => {
renderHeader()
expect(screen.getByRole('link', { name: 'Custom Title' })).toHaveAttribute('href', '/apps')
expect(screen.queryByRole('img', { name: /dify logo/i })).not.toBeInTheDocument()
expect(screen.getByText('Custom Title')).toBeInTheDocument()
expect(screen.getByRole('img', { name: /dify logo/i })).toBeInTheDocument()
})
it('should use default Dify link label when branding enabled but no application_title', () => {
it('should show default Dify text when branding enabled but no application_title', () => {
mockBrandingEnabled = true
mockBrandingTitle = null
mockBrandingLogo = null
renderHeader()
expect(screen.getByRole('link', { name: 'Dify' })).toHaveAttribute('href', '/apps')
expect(screen.getByText('Dify')).toBeInTheDocument()
})
it('should show dataset nav for editor who is not dataset operator', () => {

View File

@ -44,23 +44,21 @@ const Header = () => {
setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.BILLING })
}, [isFreePlan, setShowAccountSettingModal, setShowPricingModal])
const logoLabel = isBrandingEnabled && systemFeatures.branding.application_title ? systemFeatures.branding.application_title : 'Dify'
const renderLogo = () => (
<Link
href="/apps"
className="flex h-8 shrink-0 items-center justify-center overflow-hidden rounded-sm px-0.5 hover:opacity-80 focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
aria-label={logoLabel}
>
{systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
? (
<img
src={systemFeatures.branding.workspace_logo}
className="block h-[22px] w-auto object-contain"
alt=""
/>
)
: <DifyLogo alt="" />}
</Link>
<h1>
<Link href="/apps" className="flex h-8 shrink-0 items-center justify-center overflow-hidden px-0.5 indent-[-9999px] whitespace-nowrap">
{isBrandingEnabled && systemFeatures.branding.application_title ? systemFeatures.branding.application_title : 'Dify'}
{systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo
? (
<img
src={systemFeatures.branding.workspace_logo}
className="block h-[22px] w-auto object-contain"
alt="logo"
/>
)
: <DifyLogo />}
</Link>
</h1>
)
if (isMobile) {

View File

@ -10,7 +10,7 @@ import { getDatasetMap } from '@/env'
import { getLocaleOnServer } from '@/i18n-config/server'
import { headers } from '@/next/headers'
import PartnerStackCookieRecorder from './components/billing/partner-stack/cookie-recorder'
import { CreateAppAttributionBootstrap } from './components/create-app-attribution-bootstrap'
import CreateAppAttributionBootstrap from './components/create-app-attribution-bootstrap'
import { AgentationLoader } from './components/devtools/agentation-loader'
import { ReactScanLoader } from './components/devtools/react-scan/loader'
import { I18nServerProvider } from './components/provider/i18n-server'
@ -51,10 +51,11 @@ const LocaleLayout = async ({
<meta name="msapplication-config" content="/browserconfig.xml" />
<CreateAppAttributionBootstrap />
{/* <ReactGrabLoader /> */}
<ReactScanLoader />
</head>
<body
className="h-full"
className="h-full select-auto"
{...datasetMap}
>
<div className="isolate h-full">

View File

@ -21,15 +21,6 @@ describe('Trigger', () => {
expect(screen.getByText('Backend')).toBeInTheDocument()
expect(screen.queryByText('common.tag.addTag')).not.toBeInTheDocument()
})
it('should preserve tag label casing for mixed-case tags', () => {
render(<TagTrigger tags={['Prod', 'prod']} />)
expect(screen.getByText('Prod')).toHaveClass('system-2xs-medium')
expect(screen.getByText('Prod')).not.toHaveClass('system-2xs-medium-uppercase')
expect(screen.getByText('prod')).toHaveClass('system-2xs-medium')
expect(screen.getByText('prod')).not.toHaveClass('system-2xs-medium-uppercase')
})
})
// Prop-driven rendering updates.

View File

@ -34,7 +34,7 @@ export const TagTrigger = ({
className="flex max-w-30 min-w-0 shrink-0 items-center gap-x-0.5 rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-1.25 py-0.75"
>
<span aria-hidden="true" className="i-ri-price-tag-3-line size-3 shrink-0 text-text-quaternary" />
<div className="truncate system-2xs-medium text-text-tertiary">
<div className="truncate system-2xs-medium-uppercase text-text-tertiary">
{content}
</div>
</div>

View File

@ -194,7 +194,6 @@
"imageInput.browse": "تصفح",
"imageInput.dropImageHere": "أسقط صورتك هنا، أو",
"imageInput.supportedFormats": "يدعم PNG و JPG و JPEG و WEBP و GIF",
"imageUploader.imageList": "قائمة الصور",
"imageUploader.imageUpload": "تحميل الصورة",
"imageUploader.pasteImageLink": "لصق رابط الصورة",
"imageUploader.pasteImageLinkInputPlaceholder": "لصق رابط الصورة هنا",
@ -513,8 +512,6 @@
"operation.ok": "موافق",
"operation.openInNewTab": "فتح في علامة تبويب جديدة",
"operation.params": "معلمات",
"operation.pause": "إيقاف مؤقت",
"operation.play": "تشغيل",
"operation.refresh": "إعادة تشغيل",
"operation.regenerate": "إعادة إنشاء",
"operation.reload": "إعادة تحميل",
@ -522,7 +519,6 @@
"operation.rename": "إعادة تسمية",
"operation.reset": "إعادة تعيين",
"operation.resetKeywords": "إعادة تعيين الكلمات الرئيسية",
"operation.retry": "إعادة المحاولة",
"operation.save": "حفظ",
"operation.saveAndEnable": "حفظ وتمكين",
"operation.saveAndRegenerate": "حفظ وإعادة إنشاء القطع الفرعية",
@ -537,19 +533,13 @@
"operation.skip": "تخطي",
"operation.submit": "إرسال",
"operation.sure": "أنا متأكد",
"operation.toggleFullscreen": "تبديل ملء الشاشة",
"operation.toggleMute": "تبديل كتم الصوت",
"operation.view": "عرض",
"operation.viewDetails": "عرض التفاصيل",
"operation.viewMore": "عرض المزيد",
"operation.yes": "نعم",
"operation.zoomIn": "تكبير",
"operation.zoomOut": "تصغير",
"pagination.editPageNumber": "تعديل رقم الصفحة، الصفحة الحالية {{page}} من {{totalPages}}",
"pagination.next": "Next page",
"pagination.pageNumber": "Page number",
"pagination.perPage": "عناصر لكل صفحة",
"pagination.previous": "Previous page",
"placeholder.input": "يرجى الإدخال",
"placeholder.search": "بحث...",
"placeholder.select": "يرجى التحديد",
@ -687,6 +677,5 @@
"voiceInput.converting": "التحويل إلى نص...",
"voiceInput.notAllow": "الميكروفون غير مصرح به",
"voiceInput.speaking": "تحدث الآن...",
"voiceInput.start": "إدخال صوتي",
"you": "أنت"
}

View File

@ -194,7 +194,6 @@
"imageInput.browse": "blättern",
"imageInput.dropImageHere": "Laden Sie Ihr Bild hierher hoch oder",
"imageInput.supportedFormats": "Unterstützt PNG, JPG, JPEG, WEBP und GIF",
"imageUploader.imageList": "Bilderliste",
"imageUploader.imageUpload": "Bild-Upload",
"imageUploader.pasteImageLink": "Bildlink einfügen",
"imageUploader.pasteImageLinkInputPlaceholder": "Bildlink hier einfügen",
@ -513,8 +512,6 @@
"operation.ok": "OK",
"operation.openInNewTab": "In neuem Tab öffnen",
"operation.params": "Parameter",
"operation.pause": "Pausieren",
"operation.play": "Abspielen",
"operation.refresh": "Neustart",
"operation.regenerate": "Erneuern",
"operation.reload": "Neu laden",
@ -522,7 +519,6 @@
"operation.rename": "Umbenennen",
"operation.reset": "Zurücksetzen",
"operation.resetKeywords": "Schlüsselwörter zurücksetzen",
"operation.retry": "Erneut versuchen",
"operation.save": "Speichern",
"operation.saveAndEnable": "Speichern und Aktivieren",
"operation.saveAndRegenerate": "Speichern und Regenerieren von untergeordneten Chunks",
@ -537,19 +533,13 @@
"operation.skip": "Schiff",
"operation.submit": "Senden",
"operation.sure": "Ich bin sicher",
"operation.toggleFullscreen": "Vollbild umschalten",
"operation.toggleMute": "Stummschaltung umschalten",
"operation.view": "Ansehen",
"operation.viewDetails": "Details anzeigen",
"operation.viewMore": "MEHR SEHEN",
"operation.yes": "Ja",
"operation.zoomIn": "Vergrößern",
"operation.zoomOut": "Verkleinern",
"pagination.editPageNumber": "Seitennummer bearbeiten, aktuelle Seite {{page}} von {{totalPages}}",
"pagination.next": "Next page",
"pagination.pageNumber": "Page number",
"pagination.perPage": "Artikel pro Seite",
"pagination.previous": "Previous page",
"placeholder.input": "Bitte eingeben",
"placeholder.search": "Suchen...",
"placeholder.select": "Bitte auswählen",
@ -687,6 +677,5 @@
"voiceInput.converting": "Umwandlung in Text...",
"voiceInput.notAllow": "Mikrofon nicht autorisiert",
"voiceInput.speaking": "Sprechen Sie jetzt...",
"voiceInput.start": "Spracheingabe",
"you": "Du"
}

View File

@ -545,11 +545,7 @@
"operation.yes": "Yes",
"operation.zoomIn": "Zoom In",
"operation.zoomOut": "Zoom Out",
"pagination.editPageNumber": "Edit page number, current page {{page}} of {{totalPages}}",
"pagination.next": "Next page",
"pagination.pageNumber": "Page number",
"pagination.perPage": "Items per page",
"pagination.previous": "Previous page",
"placeholder.input": "Please enter",
"placeholder.search": "Search...",
"placeholder.select": "Please select",

View File

@ -194,7 +194,6 @@
"imageInput.browse": "navegar",
"imageInput.dropImageHere": "Deja tu imagen aquí, o",
"imageInput.supportedFormats": "Soporta PNG, JPG, JPEG, WEBP y GIF",
"imageUploader.imageList": "Lista de imágenes",
"imageUploader.imageUpload": "Carga de Imagen",
"imageUploader.pasteImageLink": "Pegar enlace de imagen",
"imageUploader.pasteImageLinkInputPlaceholder": "Pega el enlace de imagen aquí",
@ -513,8 +512,6 @@
"operation.ok": "OK",
"operation.openInNewTab": "Abrir en una nueva pestaña",
"operation.params": "Parámetros",
"operation.pause": "Pausar",
"operation.play": "Reproducir",
"operation.refresh": "Reiniciar",
"operation.regenerate": "Regenerar",
"operation.reload": "Recargar",
@ -522,7 +519,6 @@
"operation.rename": "Renombrar",
"operation.reset": "Restablecer",
"operation.resetKeywords": "Restablecer palabras clave",
"operation.retry": "Reintentar",
"operation.save": "Guardar",
"operation.saveAndEnable": "Guardar y habilitar",
"operation.saveAndRegenerate": "Guardar y regenerar fragmentos secundarios",
@ -537,19 +533,13 @@
"operation.skip": "Navío",
"operation.submit": "Enviar",
"operation.sure": "Estoy seguro",
"operation.toggleFullscreen": "Alternar pantalla completa",
"operation.toggleMute": "Alternar silencio",
"operation.view": "Vista",
"operation.viewDetails": "Ver detalles",
"operation.viewMore": "VER MÁS",
"operation.yes": "Sí",
"operation.zoomIn": "Acercar",
"operation.zoomOut": "Alejar",
"pagination.editPageNumber": "Editar número de página, página actual {{page}} de {{totalPages}}",
"pagination.next": "Next page",
"pagination.pageNumber": "Page number",
"pagination.perPage": "Elementos por página",
"pagination.previous": "Previous page",
"placeholder.input": "Por favor ingresa",
"placeholder.search": "Buscar...",
"placeholder.select": "Por favor selecciona",
@ -687,6 +677,5 @@
"voiceInput.converting": "Convirtiendo a texto...",
"voiceInput.notAllow": "micrófono no autorizado",
"voiceInput.speaking": "Habla ahora...",
"voiceInput.start": "Entrada de voz",
"you": "Tú"
}

View File

@ -194,7 +194,6 @@
"imageInput.browse": "مرورگر",
"imageInput.dropImageHere": "عکس خود را اینجا رها کنید، یا",
"imageInput.supportedFormats": "از فرمت‌های PNG، JPG، JPEG، WEBP و GIF پشتیبانی می‌کند",
"imageUploader.imageList": "فهرست تصاویر",
"imageUploader.imageUpload": "بارگذاری تصویر",
"imageUploader.pasteImageLink": "پیوند تصویر را بچسبانید",
"imageUploader.pasteImageLinkInputPlaceholder": "پیوند تصویر را اینجا بچسبانید",
@ -513,8 +512,6 @@
"operation.ok": "تایید",
"operation.openInNewTab": "باز کردن در برگه جدید",
"operation.params": "پارامترها",
"operation.pause": "مکث",
"operation.play": "پخش",
"operation.refresh": "شروع مجدد",
"operation.regenerate": "بازسازی",
"operation.reload": "بارگذاری مجدد",
@ -522,7 +519,6 @@
"operation.rename": "تغییر نام",
"operation.reset": "بازنشانی",
"operation.resetKeywords": "بازنشانی کلمات کلیدی",
"operation.retry": "تلاش دوباره",
"operation.save": "ذخیره",
"operation.saveAndEnable": "ذخیره و فعال سازی",
"operation.saveAndRegenerate": "ذخیره و بازسازی تکه های فرزند",
@ -537,19 +533,13 @@
"operation.skip": "کشتی",
"operation.submit": "ارسال",
"operation.sure": "مطمئن هستم",
"operation.toggleFullscreen": "تغییر حالت تمام‌صفحه",
"operation.toggleMute": "تغییر حالت بی‌صدا",
"operation.view": "مشاهده",
"operation.viewDetails": "دیدن جزئیات",
"operation.viewMore": "بیشتر ببینید",
"operation.yes": "بله",
"operation.zoomIn": "بزرگنمایی",
"operation.zoomOut": "کوچک نمایی",
"pagination.editPageNumber": "ویرایش شماره صفحه، صفحه فعلی {{page}} از {{totalPages}}",
"pagination.next": "Next page",
"pagination.pageNumber": "Page number",
"pagination.perPage": "موارد در هر صفحه",
"pagination.previous": "Previous page",
"placeholder.input": "لطفا وارد کنید",
"placeholder.search": "جستجو...",
"placeholder.select": "لطفا انتخاب کنید",
@ -687,6 +677,5 @@
"voiceInput.converting": "در حال تبدیل به متن...",
"voiceInput.notAllow": "میکروفون مجاز نیست",
"voiceInput.speaking": "اکنون صحبت کنید...",
"voiceInput.start": "ورودی صوتی",
"you": "تو"
}

View File

@ -194,7 +194,6 @@
"imageInput.browse": "naviguer",
"imageInput.dropImageHere": "Déposez votre image ici, ou",
"imageInput.supportedFormats": "Prend en charge PNG, JPG, JPEG, WEBP et GIF",
"imageUploader.imageList": "Liste des images",
"imageUploader.imageUpload": "Téléchargement d'image",
"imageUploader.pasteImageLink": "Collez le lien de l'image",
"imageUploader.pasteImageLinkInputPlaceholder": "Collez le lien de l'image ici",
@ -513,8 +512,6 @@
"operation.ok": "D'accord",
"operation.openInNewTab": "Ouvrir dans un nouvel onglet",
"operation.params": "Paramètres",
"operation.pause": "Pause",
"operation.play": "Lire",
"operation.refresh": "Redémarrer",
"operation.regenerate": "Régénérer",
"operation.reload": "Recharger",
@ -522,7 +519,6 @@
"operation.rename": "Renommer",
"operation.reset": "Réinitialiser",
"operation.resetKeywords": "Réinitialiser les mots-clés",
"operation.retry": "Réessayer",
"operation.save": "Enregistrer",
"operation.saveAndEnable": "Enregistrer et Activer",
"operation.saveAndRegenerate": "Enregistrer et régénérer des morceaux enfants",
@ -537,19 +533,13 @@
"operation.skip": "Bateau",
"operation.submit": "Envoyer",
"operation.sure": "Je suis sûr",
"operation.toggleFullscreen": "Basculer en plein écran",
"operation.toggleMute": "Activer/désactiver le son",
"operation.view": "Vue",
"operation.viewDetails": "Voir les détails",
"operation.viewMore": "VOIR PLUS",
"operation.yes": "Oui",
"operation.zoomIn": "Zoom avant",
"operation.zoomOut": "Zoom arrière",
"pagination.editPageNumber": "Modifier le numéro de page, page actuelle {{page}} sur {{totalPages}}",
"pagination.next": "Next page",
"pagination.pageNumber": "Page number",
"pagination.perPage": "Articles par page",
"pagination.previous": "Previous page",
"placeholder.input": "Veuillez entrer",
"placeholder.search": "Rechercher...",
"placeholder.select": "Veuillez sélectionner",
@ -687,6 +677,5 @@
"voiceInput.converting": "Conversion en texte...",
"voiceInput.notAllow": "microphone non autorisé",
"voiceInput.speaking": "Parle maintenant...",
"voiceInput.start": "Saisie vocale",
"you": "Vous"
}

View File

@ -194,7 +194,6 @@
"imageInput.browse": "ब्राउज़ करें",
"imageInput.dropImageHere": "अपनी छवि यहाँ छोड़ें, या",
"imageInput.supportedFormats": "PNG, JPG, JPEG, WEBP और GIF का समर्थन करता है",
"imageUploader.imageList": "छवि सूची",
"imageUploader.imageUpload": "छवि अपलोड",
"imageUploader.pasteImageLink": "छवि लिंक पेस्ट करें",
"imageUploader.pasteImageLinkInputPlaceholder": "छवि लिंक यहाँ पेस्ट करें",
@ -513,8 +512,6 @@
"operation.ok": "ठीक है",
"operation.openInNewTab": "नए टैब में खोलें",
"operation.params": "पैरामीटर",
"operation.pause": "रोकें",
"operation.play": "चलाएं",
"operation.refresh": "पुनः प्रारंभ करें",
"operation.regenerate": "पुनर्जन्म",
"operation.reload": "पुनः लोड करें",
@ -522,7 +519,6 @@
"operation.rename": "नाम बदलें",
"operation.reset": "रीसेट करें",
"operation.resetKeywords": "कीवर्ड रीसेट करें",
"operation.retry": "पुनः प्रयास करें",
"operation.save": "सहेजें",
"operation.saveAndEnable": "सहेजें और सक्षम करें",
"operation.saveAndRegenerate": "सहेजें और पुन: उत्पन्न करें बाल विखंडू",
@ -537,19 +533,13 @@
"operation.skip": "जहाज़",
"operation.submit": "जमा करें",
"operation.sure": "मुझे यकीन है",
"operation.toggleFullscreen": "फ़ुलस्क्रीन टॉगल करें",
"operation.toggleMute": "म्यूट टॉगल करें",
"operation.view": "देखना",
"operation.viewDetails": "विवरण देखें",
"operation.viewMore": "और देखें",
"operation.yes": "हाँ",
"operation.zoomIn": "ज़ूम इन करें",
"operation.zoomOut": "ज़ूम आउट करें",
"pagination.editPageNumber": "पृष्ठ संख्या संपादित करें, वर्तमान पृष्ठ {{page}} / {{totalPages}}",
"pagination.next": "Next page",
"pagination.pageNumber": "Page number",
"pagination.perPage": "प्रति पृष्ठ आइटम",
"pagination.previous": "Previous page",
"placeholder.input": "कृपया दर्ज करें",
"placeholder.search": "खोजें...",
"placeholder.select": "कृपया चयन करें",
@ -687,6 +677,5 @@
"voiceInput.converting": "पाठ में परिवर्तित हो रहा है...",
"voiceInput.notAllow": "माइक्रोफोन अधिकृत नहीं है",
"voiceInput.speaking": "अब बोलें...",
"voiceInput.start": "वॉइस इनपुट",
"you": "आप"
}

View File

@ -194,7 +194,6 @@
"imageInput.browse": "Telusuri",
"imageInput.dropImageHere": "Letakkan gambar Anda di sini, atau",
"imageInput.supportedFormats": "Mendukung PNG, JPG, JPEG, WEBP dan GIF",
"imageUploader.imageList": "Daftar gambar",
"imageUploader.imageUpload": "Unggah Gambar",
"imageUploader.pasteImageLink": "Tempel tautan gambar",
"imageUploader.pasteImageLinkInputPlaceholder": "Tempel tautan gambar di sini",
@ -513,8 +512,6 @@
"operation.ok": "OKE",
"operation.openInNewTab": "Buka di tab baru",
"operation.params": "Parameter",
"operation.pause": "Jeda",
"operation.play": "Putar",
"operation.refresh": "Segarkan",
"operation.regenerate": "Regenerasi",
"operation.reload": "Muat Ulang",
@ -522,7 +519,6 @@
"operation.rename": "Ubah nama",
"operation.reset": "Reset",
"operation.resetKeywords": "Atur ulang kata kunci",
"operation.retry": "Coba lagi",
"operation.save": "Simpan",
"operation.saveAndEnable": "Simpan & Aktifkan",
"operation.saveAndRegenerate": "Simpan & Buat Ulang Potongan Anak",
@ -537,19 +533,13 @@
"operation.skip": "Lewat",
"operation.submit": "Kirim",
"operation.sure": "Saya yakin",
"operation.toggleFullscreen": "Alihkan layar penuh",
"operation.toggleMute": "Alihkan bisu",
"operation.view": "Lihat",
"operation.viewDetails": "Lihat Detail",
"operation.viewMore": "LIHAT LEBIH BANYAK",
"operation.yes": "Ya",
"operation.zoomIn": "Perbesar",
"operation.zoomOut": "Perkecil",
"pagination.editPageNumber": "Edit nomor halaman, halaman saat ini {{page}} dari {{totalPages}}",
"pagination.next": "Next page",
"pagination.pageNumber": "Page number",
"pagination.perPage": "Item per halaman",
"pagination.previous": "Previous page",
"placeholder.input": "Silakan masuk",
"placeholder.search": "Cari...",
"placeholder.select": "Silakan pilih",
@ -687,6 +677,5 @@
"voiceInput.converting": "Mengonversi ke teks...",
"voiceInput.notAllow": "mikrofon tidak diizinkan",
"voiceInput.speaking": "Bicaralah sekarang...",
"voiceInput.start": "Input suara",
"you": "Kamu"
}

View File

@ -194,7 +194,6 @@
"imageInput.browse": "sfogliare",
"imageInput.dropImageHere": "Trascina la tua immagine qui, oppure",
"imageInput.supportedFormats": "Supporta PNG, JPG, JPEG, WEBP e GIF",
"imageUploader.imageList": "Elenco immagini",
"imageUploader.imageUpload": "Caricamento Immagine",
"imageUploader.pasteImageLink": "Incolla link immagine",
"imageUploader.pasteImageLinkInputPlaceholder": "Incolla qui il link immagine",
@ -513,8 +512,6 @@
"operation.ok": "OK",
"operation.openInNewTab": "Apri in una nuova scheda",
"operation.params": "Parametri",
"operation.pause": "Pausa",
"operation.play": "Riproduci",
"operation.refresh": "Riavvia",
"operation.regenerate": "Rigenerare",
"operation.reload": "Ricarica",
@ -522,7 +519,6 @@
"operation.rename": "Rinomina",
"operation.reset": "Reimposta",
"operation.resetKeywords": "Reimposta parole chiave",
"operation.retry": "Riprova",
"operation.save": "Salva",
"operation.saveAndEnable": "Salva & Abilita",
"operation.saveAndRegenerate": "Salva e rigenera i blocchi figlio",
@ -537,19 +533,13 @@
"operation.skip": "Nave",
"operation.submit": "Invia",
"operation.sure": "Sono sicuro",
"operation.toggleFullscreen": "Attiva/disattiva schermo intero",
"operation.toggleMute": "Attiva/disattiva muto",
"operation.view": "Vista",
"operation.viewDetails": "Visualizza dettagli",
"operation.viewMore": "SCOPRI DI PIÙ",
"operation.yes": "Sì",
"operation.zoomIn": "Ingrandisci",
"operation.zoomOut": "Zoom indietro",
"pagination.editPageNumber": "Modifica numero pagina, pagina corrente {{page}} di {{totalPages}}",
"pagination.next": "Next page",
"pagination.pageNumber": "Page number",
"pagination.perPage": "Articoli per pagina",
"pagination.previous": "Previous page",
"placeholder.input": "Per favore inserisci",
"placeholder.search": "Cerca...",
"placeholder.select": "Per favore seleziona",
@ -687,6 +677,5 @@
"voiceInput.converting": "Conversione in testo...",
"voiceInput.notAllow": "microfono non autorizzato",
"voiceInput.speaking": "Parla ora...",
"voiceInput.start": "Input vocale",
"you": "Tu"
}

View File

@ -194,7 +194,6 @@
"imageInput.browse": "ブラウズする",
"imageInput.dropImageHere": "ここに画像をドロップするか、",
"imageInput.supportedFormats": "PNG、JPG、JPEG、WEBP、および GIF をサポートしています。",
"imageUploader.imageList": "画像リスト",
"imageUploader.imageUpload": "画像アップロード",
"imageUploader.pasteImageLink": "画像リンクを貼り付ける",
"imageUploader.pasteImageLinkInputPlaceholder": "ここに画像リンクを貼り付けてください",
@ -513,8 +512,6 @@
"operation.ok": "OK",
"operation.openInNewTab": "新しいタブで開く",
"operation.params": "パラメータ",
"operation.pause": "一時停止",
"operation.play": "再生",
"operation.refresh": "リフレッシュ",
"operation.regenerate": "再生成",
"operation.reload": "再読み込み",
@ -522,7 +519,6 @@
"operation.rename": "名前の変更",
"operation.reset": "リセット",
"operation.resetKeywords": "キーワードをリセット",
"operation.retry": "再試行",
"operation.save": "保存",
"operation.saveAndEnable": "保存 & 有効に",
"operation.saveAndRegenerate": "保存して子チャンクを再生成",
@ -537,19 +533,13 @@
"operation.skip": "スキップ",
"operation.submit": "送信",
"operation.sure": "確認済み",
"operation.toggleFullscreen": "全画面表示を切り替え",
"operation.toggleMute": "ミュートを切り替え",
"operation.view": "表示",
"operation.viewDetails": "詳細を見る",
"operation.viewMore": "さらに表示",
"operation.yes": "はい",
"operation.zoomIn": "ズームインする",
"operation.zoomOut": "ズームアウト",
"pagination.editPageNumber": "ページ番号を編集、現在のページ {{page}} / {{totalPages}}",
"pagination.next": "Next page",
"pagination.pageNumber": "Page number",
"pagination.perPage": "ページあたりのアイテム数",
"pagination.previous": "Previous page",
"placeholder.input": "入力してください",
"placeholder.search": "検索...",
"placeholder.select": "選択してください",
@ -687,6 +677,5 @@
"voiceInput.converting": "テキストに変換中...",
"voiceInput.notAllow": "マイクが許可されていません",
"voiceInput.speaking": "今話しています...",
"voiceInput.start": "音声入力",
"you": "あなた"
}

View File

@ -194,7 +194,6 @@
"imageInput.browse": "찾아보기",
"imageInput.dropImageHere": "여기에 이미지를 드롭하거나",
"imageInput.supportedFormats": "PNG, JPG, JPEG, WEBP 및 GIF 를 지원합니다.",
"imageUploader.imageList": "이미지 목록",
"imageUploader.imageUpload": "이미지 업로드",
"imageUploader.pasteImageLink": "이미지 링크 붙여넣기",
"imageUploader.pasteImageLinkInputPlaceholder": "여기에 이미지 링크를 붙여넣으세요",
@ -513,8 +512,6 @@
"operation.ok": "확인",
"operation.openInNewTab": "새 탭에서 열기",
"operation.params": "매개변수",
"operation.pause": "일시 중지",
"operation.play": "재생",
"operation.refresh": "새로 고침",
"operation.regenerate": "재생성",
"operation.reload": "다시 불러오기",
@ -522,7 +519,6 @@
"operation.rename": "이름 바꾸기",
"operation.reset": "초기화",
"operation.resetKeywords": "키워드 재설정",
"operation.retry": "다시 시도",
"operation.save": "저장",
"operation.saveAndEnable": "저장 및 활성화",
"operation.saveAndRegenerate": "저장 및 자식 청크 재생성",
@ -537,19 +533,13 @@
"operation.skip": "건너뛰기",
"operation.submit": "전송",
"operation.sure": "확인",
"operation.toggleFullscreen": "전체 화면 전환",
"operation.toggleMute": "음소거 전환",
"operation.view": "보기",
"operation.viewDetails": "세부 정보보기",
"operation.viewMore": "더보기",
"operation.yes": "네",
"operation.zoomIn": "확대",
"operation.zoomOut": "축소",
"pagination.editPageNumber": "페이지 번호 편집, 현재 {{page}} / {{totalPages}} 페이지",
"pagination.next": "Next page",
"pagination.pageNumber": "Page number",
"pagination.perPage": "페이지당 항목 수",
"pagination.previous": "Previous page",
"placeholder.input": "입력해주세요",
"placeholder.search": "검색...",
"placeholder.select": "선택해주세요",
@ -687,6 +677,5 @@
"voiceInput.converting": "텍스트로 변환 중...",
"voiceInput.notAllow": "마이크가 허용되지 않았습니다",
"voiceInput.speaking": "지금 말하고 있습니다...",
"voiceInput.start": "음성 입력",
"you": "나"
}

View File

@ -194,7 +194,6 @@
"imageInput.browse": "browse",
"imageInput.dropImageHere": "Drop your image here, or",
"imageInput.supportedFormats": "Supports PNG, JPG, JPEG, WEBP and GIF",
"imageUploader.imageList": "Afbeeldingenlijst",
"imageUploader.imageUpload": "Image Upload",
"imageUploader.pasteImageLink": "Paste image link",
"imageUploader.pasteImageLinkInputPlaceholder": "Paste image link here",
@ -513,8 +512,6 @@
"operation.ok": "OK",
"operation.openInNewTab": "Open in new tab",
"operation.params": "Params",
"operation.pause": "Pauzeren",
"operation.play": "Afspelen",
"operation.refresh": "Restart",
"operation.regenerate": "Regenerate",
"operation.reload": "Reload",
@ -522,7 +519,6 @@
"operation.rename": "Rename",
"operation.reset": "Reset",
"operation.resetKeywords": "Reset keywords",
"operation.retry": "Opnieuw proberen",
"operation.save": "Save",
"operation.saveAndEnable": "Save & Enable",
"operation.saveAndRegenerate": "Save & Regenerate Child Chunks",
@ -537,19 +533,13 @@
"operation.skip": "Skip",
"operation.submit": "Submit",
"operation.sure": "I'm sure",
"operation.toggleFullscreen": "Volledig scherm schakelen",
"operation.toggleMute": "Dempen schakelen",
"operation.view": "View",
"operation.viewDetails": "View Details",
"operation.viewMore": "VIEW MORE",
"operation.yes": "Yes",
"operation.zoomIn": "Zoom In",
"operation.zoomOut": "Zoom Out",
"pagination.editPageNumber": "Paginanummer bewerken, huidige pagina {{page}} van {{totalPages}}",
"pagination.next": "Next page",
"pagination.pageNumber": "Page number",
"pagination.perPage": "Items per page",
"pagination.previous": "Previous page",
"placeholder.input": "Please enter",
"placeholder.search": "Search...",
"placeholder.select": "Please select",
@ -687,6 +677,5 @@
"voiceInput.converting": "Converting to text...",
"voiceInput.notAllow": "microphone not authorized",
"voiceInput.speaking": "Speak now...",
"voiceInput.start": "Spraakinvoer",
"you": "You"
}

View File

@ -194,7 +194,6 @@
"imageInput.browse": "przeglądaj",
"imageInput.dropImageHere": "Upuść swój obraz tutaj, lub",
"imageInput.supportedFormats": "Obsługuje PNG, JPG, JPEG, WEBP i GIF",
"imageUploader.imageList": "Lista obrazów",
"imageUploader.imageUpload": "Przesyłanie obrazu",
"imageUploader.pasteImageLink": "Wklej link do obrazu",
"imageUploader.pasteImageLinkInputPlaceholder": "Wklej tutaj link do obrazu",
@ -513,8 +512,6 @@
"operation.ok": "OK",
"operation.openInNewTab": "Otwórz w nowej karcie",
"operation.params": "Parametry",
"operation.pause": "Pauza",
"operation.play": "Odtwórz",
"operation.refresh": "Odśwież",
"operation.regenerate": "Ponownie wygenerować",
"operation.reload": "Przeładuj",
@ -522,7 +519,6 @@
"operation.rename": "Zmień nazwę",
"operation.reset": "Resetuj",
"operation.resetKeywords": "Resetuj słowa kluczowe",
"operation.retry": "Spróbuj ponownie",
"operation.save": "Zapisz",
"operation.saveAndEnable": "Zapisz i Włącz",
"operation.saveAndRegenerate": "Zapisywanie i regeneracja fragmentów podrzędnych",
@ -537,19 +533,13 @@
"operation.skip": "Statek",
"operation.submit": "Prześlij",
"operation.sure": "Jestem pewien",
"operation.toggleFullscreen": "Przełącz pełny ekran",
"operation.toggleMute": "Przełącz wyciszenie",
"operation.view": "Widok",
"operation.viewDetails": "Wyświetl szczegóły",
"operation.viewMore": "ZOBACZ WIĘCEJ",
"operation.yes": "Tak",
"operation.zoomIn": "Powiększenie",
"operation.zoomOut": "Pomniejszanie",
"pagination.editPageNumber": "Edytuj numer strony, bieżąca strona {{page}} z {{totalPages}}",
"pagination.next": "Next page",
"pagination.pageNumber": "Page number",
"pagination.perPage": "Ilość elementów na stronie",
"pagination.previous": "Previous page",
"placeholder.input": "Proszę wprowadzić",
"placeholder.search": "Szukaj...",
"placeholder.select": "Proszę wybrać",
@ -687,6 +677,5 @@
"voiceInput.converting": "Konwertowanie na tekst...",
"voiceInput.notAllow": "mikrofon nieautoryzowany",
"voiceInput.speaking": "Mów teraz...",
"voiceInput.start": "Wprowadzanie głosowe",
"you": "Ty"
}

View File

@ -194,7 +194,6 @@
"imageInput.browse": "navegar",
"imageInput.dropImageHere": "Arraste sua imagem aqui, ou",
"imageInput.supportedFormats": "Suporta PNG, JPG, JPEG, WEBP e GIF",
"imageUploader.imageList": "Lista de imagens",
"imageUploader.imageUpload": "Enviar Imagem",
"imageUploader.pasteImageLink": "Colar link da imagem",
"imageUploader.pasteImageLinkInputPlaceholder": "Cole o link da imagem aqui",
@ -513,8 +512,6 @@
"operation.ok": "OK",
"operation.openInNewTab": "Abrir em nova guia",
"operation.params": "Parâmetros",
"operation.pause": "Pausar",
"operation.play": "Reproduzir",
"operation.refresh": "Reiniciar",
"operation.regenerate": "Regenerar",
"operation.reload": "Recarregar",
@ -522,7 +519,6 @@
"operation.rename": "Renomear",
"operation.reset": "Redefinir",
"operation.resetKeywords": "Redefinir palavras-chave",
"operation.retry": "Tentar novamente",
"operation.save": "Salvar",
"operation.saveAndEnable": "Salvar e Ativar",
"operation.saveAndRegenerate": "Salvar e regenerar pedaços filhos",
@ -537,19 +533,13 @@
"operation.skip": "Navio",
"operation.submit": "Enviar",
"operation.sure": "Tenho certeza",
"operation.toggleFullscreen": "Alternar tela cheia",
"operation.toggleMute": "Alternar mudo",
"operation.view": "Vista",
"operation.viewDetails": "Ver detalhes",
"operation.viewMore": "VER MAIS",
"operation.yes": "Sim",
"operation.zoomIn": "Ampliar",
"operation.zoomOut": "Diminuir o zoom",
"pagination.editPageNumber": "Editar número da página, página atual {{page}} de {{totalPages}}",
"pagination.next": "Next page",
"pagination.pageNumber": "Page number",
"pagination.perPage": "Itens por página",
"pagination.previous": "Previous page",
"placeholder.input": "Por favor, insira",
"placeholder.search": "Pesquisar...",
"placeholder.select": "Por favor, selecione",
@ -687,6 +677,5 @@
"voiceInput.converting": "Convertendo para texto...",
"voiceInput.notAllow": "microfone não autorizado",
"voiceInput.speaking": "Fale agora...",
"voiceInput.start": "Entrada por voz",
"you": "Você"
}

View File

@ -194,7 +194,6 @@
"imageInput.browse": "naviga",
"imageInput.dropImageHere": "Trageți imaginea aici sau",
"imageInput.supportedFormats": "Suportă PNG, JPG, JPEG, WEBP și GIF",
"imageUploader.imageList": "Listă de imagini",
"imageUploader.imageUpload": "Încărcare imagine",
"imageUploader.pasteImageLink": "Inserați link-ul imaginii",
"imageUploader.pasteImageLinkInputPlaceholder": "Inserați link-ul imaginii aici",
@ -513,8 +512,6 @@
"operation.ok": "OK",
"operation.openInNewTab": "Deschide într-o filă nouă",
"operation.params": "Parametri",
"operation.pause": "Pauză",
"operation.play": "Redare",
"operation.refresh": "Reîncarcă",
"operation.regenerate": "Regenera",
"operation.reload": "Reîncarcă",
@ -522,7 +519,6 @@
"operation.rename": "Redenumește",
"operation.reset": "Resetează",
"operation.resetKeywords": "Resetează cuvintele cheie",
"operation.retry": "Reîncercați",
"operation.save": "Salvează",
"operation.saveAndEnable": "Salvează și Activează",
"operation.saveAndRegenerate": "Salvați și regenerați bucățile secundare",
@ -537,19 +533,13 @@
"operation.skip": "Navă",
"operation.submit": "Prezinte",
"operation.sure": "Sunt sigur",
"operation.toggleFullscreen": "Comută ecran complet",
"operation.toggleMute": "Comută sunetul",
"operation.view": "Vedere",
"operation.viewDetails": "Vezi detalii",
"operation.viewMore": "VEZI MAI MULT",
"operation.yes": "Da",
"operation.zoomIn": "Măriți",
"operation.zoomOut": "Micșorare",
"pagination.editPageNumber": "Editați numărul paginii, pagina curentă {{page}} din {{totalPages}}",
"pagination.next": "Next page",
"pagination.pageNumber": "Page number",
"pagination.perPage": "Articole pe pagină",
"pagination.previous": "Previous page",
"placeholder.input": "Vă rugăm să introduceți",
"placeholder.search": "Caută...",
"placeholder.select": "Vă rugăm să selectați",
@ -687,6 +677,5 @@
"voiceInput.converting": "Se convertește la text...",
"voiceInput.notAllow": "microfonul nu este autorizat",
"voiceInput.speaking": "Vorbiți acum...",
"voiceInput.start": "Introducere vocală",
"you": "Tu"
}

View File

@ -194,7 +194,6 @@
"imageInput.browse": "просмотр",
"imageInput.dropImageHere": "Перетащите ваше изображение сюда или",
"imageInput.supportedFormats": "Поддерживает PNG, JPG, JPEG, WEBP и GIF",
"imageUploader.imageList": "Список изображений",
"imageUploader.imageUpload": "Загрузка изображения",
"imageUploader.pasteImageLink": "Вставить ссылку на изображение",
"imageUploader.pasteImageLinkInputPlaceholder": "Вставьте ссылку на изображение здесь",
@ -513,8 +512,6 @@
"operation.ok": "ОК",
"operation.openInNewTab": "Открыть в новой вкладке",
"operation.params": "Параметры",
"operation.pause": "Пауза",
"operation.play": "Воспроизвести",
"operation.refresh": "Перезапустить",
"operation.regenerate": "Регенерировать",
"operation.reload": "Перезагрузить",
@ -522,7 +519,6 @@
"operation.rename": "Переименовать",
"operation.reset": "Сбросить",
"operation.resetKeywords": "Сбросить ключевые слова",
"operation.retry": "Повторить",
"operation.save": "Сохранить",
"operation.saveAndEnable": "Сохранить и включить",
"operation.saveAndRegenerate": "Сохранение и повторное создание дочерних блоков",
@ -537,19 +533,13 @@
"operation.skip": "Корабль",
"operation.submit": "Отправить",
"operation.sure": "Я уверен",
"operation.toggleFullscreen": "Переключить полноэкранный режим",
"operation.toggleMute": "Переключить звук",
"operation.view": "Вид",
"operation.viewDetails": "Подробнее",
"operation.viewMore": "ПОДРОБНЕЕ",
"operation.yes": "Да",
"operation.zoomIn": "Увеличить",
"operation.zoomOut": "Уменьшение масштаба",
"pagination.editPageNumber": "Изменить номер страницы, текущая страница {{page}} из {{totalPages}}",
"pagination.next": "Next page",
"pagination.pageNumber": "Page number",
"pagination.perPage": "Элементов на странице",
"pagination.previous": "Previous page",
"placeholder.input": "Пожалуйста, введите",
"placeholder.search": "Поиск...",
"placeholder.select": "Пожалуйста, выберите",
@ -687,6 +677,5 @@
"voiceInput.converting": "Преобразование в текст...",
"voiceInput.notAllow": "микрофон не авторизован",
"voiceInput.speaking": "Говорите сейчас...",
"voiceInput.start": "Голосовой ввод",
"you": "Ты"
}

View File

@ -194,7 +194,6 @@
"imageInput.browse": "brskati",
"imageInput.dropImageHere": "Tukaj spustite svojo sliko ali",
"imageInput.supportedFormats": "Podpira PNG, JPG, JPEG, WEBP in GIF",
"imageUploader.imageList": "Seznam slik",
"imageUploader.imageUpload": "Nalaganje slik",
"imageUploader.pasteImageLink": "Prilepi povezavo do slike",
"imageUploader.pasteImageLinkInputPlaceholder": "Tukaj prilepi povezavo do slike",
@ -513,8 +512,6 @@
"operation.ok": "V redu",
"operation.openInNewTab": "Odpri v novem zavihku",
"operation.params": "Parametri",
"operation.pause": "Premor",
"operation.play": "Predvajaj",
"operation.refresh": "Osveži",
"operation.regenerate": "Regeneracijo",
"operation.reload": "Ponovno naloži",
@ -522,7 +519,6 @@
"operation.rename": "Preimenuj",
"operation.reset": "Ponastavi",
"operation.resetKeywords": "Ponastavi ključne besede",
"operation.retry": "Poskusi znova",
"operation.save": "Shrani",
"operation.saveAndEnable": "Shrani in omogoči",
"operation.saveAndRegenerate": "Shranite in regenerirajte otroške koščke",
@ -537,19 +533,13 @@
"operation.skip": "Ladja",
"operation.submit": "Predložiti",
"operation.sure": "Prepričan sem",
"operation.toggleFullscreen": "Preklopi celozaslonski način",
"operation.toggleMute": "Preklopi utišanje",
"operation.view": "Pogled",
"operation.viewDetails": "Poglej podrobnosti",
"operation.viewMore": "POGLEJ VEČ",
"operation.yes": "Da",
"operation.zoomIn": "Povečava",
"operation.zoomOut": "Pomanjšanje",
"pagination.editPageNumber": "Uredi številko strani, trenutna stran {{page}} od {{totalPages}}",
"pagination.next": "Next page",
"pagination.pageNumber": "Page number",
"pagination.perPage": "Elementi na stran",
"pagination.previous": "Previous page",
"placeholder.input": "Vnesite prosim",
"placeholder.search": "Išči...",
"placeholder.select": "Izberite prosim",
@ -687,6 +677,5 @@
"voiceInput.converting": "Pretvorba v besedilo ...",
"voiceInput.notAllow": "Mikrofon ni pooblaščen",
"voiceInput.speaking": "Spregovorite zdaj ...",
"voiceInput.start": "Glasovni vnos",
"you": "Ti"
}

View File

@ -194,7 +194,6 @@
"imageInput.browse": "ท่องเว็บ",
"imageInput.dropImageHere": "วางภาพของคุณที่นี่ หรือ",
"imageInput.supportedFormats": "รองรับ PNG, JPG, JPEG, WEBP และ GIF",
"imageUploader.imageList": "รายการรูปภาพ",
"imageUploader.imageUpload": "อัปโหลดรูปภาพ",
"imageUploader.pasteImageLink": "วางลิงก์รูปภาพ",
"imageUploader.pasteImageLinkInputPlaceholder": "วางลิงค์รูปภาพที่นี่",
@ -513,8 +512,6 @@
"operation.ok": "ตกลง, ได้",
"operation.openInNewTab": "เปิดในแท็บใหม่",
"operation.params": "พารามิเตอร์",
"operation.pause": "หยุดชั่วคราว",
"operation.play": "เล่น",
"operation.refresh": "เริ่มใหม่",
"operation.regenerate": "สร้างใหม่",
"operation.reload": "โหลด",
@ -522,7 +519,6 @@
"operation.rename": "ตั้งชื่อใหม่",
"operation.reset": "รี เซ็ต",
"operation.resetKeywords": "รีเซ็ตคำสำคัญ",
"operation.retry": "ลองอีกครั้ง",
"operation.save": "ประหยัด",
"operation.saveAndEnable": "บันทึกและเปิดใช้งาน",
"operation.saveAndRegenerate": "บันทึกและสร้างก้อนย่อยใหม่",
@ -537,19 +533,13 @@
"operation.skip": "เรือ",
"operation.submit": "ส่ง",
"operation.sure": "ฉันแน่ใจ",
"operation.toggleFullscreen": "สลับเต็มหน้าจอ",
"operation.toggleMute": "สลับปิดเสียง",
"operation.view": "ทิวทัศน์",
"operation.viewDetails": "ดูรายละเอียด",
"operation.viewMore": "ดูเพิ่มเติม",
"operation.yes": "ใช่",
"operation.zoomIn": "ซูมเข้า",
"operation.zoomOut": "ซูมออก",
"pagination.editPageNumber": "แก้ไขหมายเลขหน้า หน้าปัจจุบัน {{page}} จาก {{totalPages}}",
"pagination.next": "Next page",
"pagination.pageNumber": "Page number",
"pagination.perPage": "รายการต่อหน้า",
"pagination.previous": "Previous page",
"placeholder.input": "กรุณากรอก",
"placeholder.search": "ค้นหา...",
"placeholder.select": "กรุณาเลือก",
@ -687,6 +677,5 @@
"voiceInput.converting": "กําลังแปลงเป็นข้อความ...",
"voiceInput.notAllow": "ไม่ได้รับอนุญาตไมโครโฟน",
"voiceInput.speaking": "พูดเดี๋ยวนี้...",
"voiceInput.start": "ป้อนข้อมูลด้วยเสียง",
"you": "คุณ"
}

View File

@ -194,7 +194,6 @@
"imageInput.browse": "göz atın",
"imageInput.dropImageHere": "Görüntünüzü buraya bırakın veya",
"imageInput.supportedFormats": "PNG, JPG, JPEG, WEBP ve GIF'i destekler",
"imageUploader.imageList": "Görsel listesi",
"imageUploader.imageUpload": "Görüntü Yükleme",
"imageUploader.pasteImageLink": "Görüntü bağlantısını yapıştır",
"imageUploader.pasteImageLinkInputPlaceholder": "Görüntü bağlantısını buraya yapıştırın",
@ -513,8 +512,6 @@
"operation.ok": "Tamam",
"operation.openInNewTab": "Yeni sekmede aç",
"operation.params": "Parametreler",
"operation.pause": "Duraklat",
"operation.play": "Oynat",
"operation.refresh": "Yeniden Başlat",
"operation.regenerate": "Yeniden Oluştur",
"operation.reload": "Yeniden Yükle",
@ -522,7 +519,6 @@
"operation.rename": "Yeniden Adlandır",
"operation.reset": "Sıfırla",
"operation.resetKeywords": "Anahtar kelimeleri sıfırla",
"operation.retry": "Tekrar dene",
"operation.save": "Kaydet",
"operation.saveAndEnable": "Kaydet ve Etkinleştir",
"operation.saveAndRegenerate": "Alt Parçaları Kaydetme ve Yeniden Oluşturma",
@ -537,19 +533,13 @@
"operation.skip": "Atla",
"operation.submit": "Gönder",
"operation.sure": "Eminim",
"operation.toggleFullscreen": "Tam ekranı aç/kapat",
"operation.toggleMute": "Sessize al/aç",
"operation.view": "Görüntüle",
"operation.viewDetails": "Detayları Görüntüle",
"operation.viewMore": "DAHA FAZLA GÖSTER",
"operation.yes": "Evet",
"operation.zoomIn": "Yakınlaştırma",
"operation.zoomOut": "Uzaklaştırma",
"pagination.editPageNumber": "Sayfa numarasını düzenle, geçerli sayfa {{page}} / {{totalPages}}",
"pagination.next": "Next page",
"pagination.pageNumber": "Page number",
"pagination.perPage": "Sayfa başına öğe sayısı",
"pagination.previous": "Previous page",
"placeholder.input": "Lütfen girin",
"placeholder.search": "Ara...",
"placeholder.select": "Lütfen seçin",
@ -687,6 +677,5 @@
"voiceInput.converting": "Metne dönüştürülüyor...",
"voiceInput.notAllow": "mikrofon yetkilendirilmedi",
"voiceInput.speaking": "Şimdi konuş...",
"voiceInput.start": "Sesli giriş",
"you": "Sen"
}

View File

@ -194,7 +194,6 @@
"imageInput.browse": "перегляд",
"imageInput.dropImageHere": "Перетягніть зображення сюди або",
"imageInput.supportedFormats": "Підтримує PNG, JPG, JPEG, WEBP і GIF",
"imageUploader.imageList": "Список зображень",
"imageUploader.imageUpload": "Завантаження зображення",
"imageUploader.pasteImageLink": "Вставити посилання на зображення",
"imageUploader.pasteImageLinkInputPlaceholder": "Вставте посилання на зображення тут",
@ -513,8 +512,6 @@
"operation.ok": "ОК",
"operation.openInNewTab": "Відкрити в новій вкладці",
"operation.params": "Параметри",
"operation.pause": "Пауза",
"operation.play": "Відтворити",
"operation.refresh": "Перезапустити",
"operation.regenerate": "Відновити",
"operation.reload": "Перезавантажити",
@ -522,7 +519,6 @@
"operation.rename": "Перейменувати",
"operation.reset": "Скинути",
"operation.resetKeywords": "Скинути ключові слова",
"operation.retry": "Повторити",
"operation.save": "Зберегти",
"operation.saveAndEnable": "Зберегти та Увімкнути",
"operation.saveAndRegenerate": "Збереження та регенерація дочірніх фрагментів",
@ -537,19 +533,13 @@
"operation.skip": "Корабель",
"operation.submit": "Представити",
"operation.sure": "Я впевнений",
"operation.toggleFullscreen": "Перемкнути повноекранний режим",
"operation.toggleMute": "Перемкнути звук",
"operation.view": "Вид",
"operation.viewDetails": "Перегляд докладних відомостей",
"operation.viewMore": "ДИВИТИСЬ БІЛЬШЕ",
"operation.yes": "Так",
"operation.zoomIn": "Збільшити масштаб",
"operation.zoomOut": "Зменшити масштаб",
"pagination.editPageNumber": "Редагувати номер сторінки, поточна сторінка {{page}} з {{totalPages}}",
"pagination.next": "Next page",
"pagination.pageNumber": "Page number",
"pagination.perPage": "Елементів на сторінці",
"pagination.previous": "Previous page",
"placeholder.input": "Будь ласка, введіть текст",
"placeholder.search": "Пошук...",
"placeholder.select": "Будь ласка, оберіть параметр",
@ -687,6 +677,5 @@
"voiceInput.converting": "Перетворення на текст...",
"voiceInput.notAllow": "мікрофон не авторизований",
"voiceInput.speaking": "Говоріть зараз...",
"voiceInput.start": "Голосове введення",
"you": "Ти"
}

View File

@ -194,7 +194,6 @@
"imageInput.browse": "duyệt",
"imageInput.dropImageHere": "Kéo hình ảnh của bạn vào đây, hoặc",
"imageInput.supportedFormats": "Hỗ trợ PNG, JPG, JPEG, WEBP và GIF",
"imageUploader.imageList": "Danh sách hình ảnh",
"imageUploader.imageUpload": "Tải ảnh lên",
"imageUploader.pasteImageLink": "Dán liên kết ảnh",
"imageUploader.pasteImageLinkInputPlaceholder": "Dán liên kết ảnh ở đây",
@ -513,8 +512,6 @@
"operation.ok": "OK",
"operation.openInNewTab": "Mở trong tab mới",
"operation.params": "Tham số",
"operation.pause": "Tạm dừng",
"operation.play": "Phát",
"operation.refresh": "Làm mới",
"operation.regenerate": "Tái tạo",
"operation.reload": "Tải lại",
@ -522,7 +519,6 @@
"operation.rename": "Đổi tên",
"operation.reset": "Đặt lại",
"operation.resetKeywords": "Đặt lại từ khóa",
"operation.retry": "Thử lại",
"operation.save": "Lưu",
"operation.saveAndEnable": "Lưu & Kích hoạt",
"operation.saveAndRegenerate": "Lưu và tạo lại các phần con",
@ -537,19 +533,13 @@
"operation.skip": "Tàu",
"operation.submit": "Trình",
"operation.sure": "Tôi chắc chắn",
"operation.toggleFullscreen": "Chuyển đổi toàn màn hình",
"operation.toggleMute": "Bật/tắt tiếng",
"operation.view": "Cảnh",
"operation.viewDetails": "Xem chi tiết",
"operation.viewMore": "XEM THÊM",
"operation.yes": "Vâng",
"operation.zoomIn": "Phóng to",
"operation.zoomOut": "Thu nhỏ",
"pagination.editPageNumber": "Chỉnh sửa số trang, trang hiện tại {{page}} trên {{totalPages}}",
"pagination.next": "Next page",
"pagination.pageNumber": "Page number",
"pagination.perPage": "Mục trên mỗi trang",
"pagination.previous": "Previous page",
"placeholder.input": "Vui lòng nhập",
"placeholder.search": "Tìm kiếm...",
"placeholder.select": "Vui lòng chọn",
@ -687,6 +677,5 @@
"voiceInput.converting": "Chuyển đổi thành văn bản...",
"voiceInput.notAllow": "micro không được ủy quyền",
"voiceInput.speaking": "Hãy nói...",
"voiceInput.start": "Nhập bằng giọng nói",
"you": "Bạn"
}

View File

@ -194,7 +194,6 @@
"imageInput.browse": "浏览",
"imageInput.dropImageHere": "将图片拖放到此处,或",
"imageInput.supportedFormats": "支持 PNG、JPG、JPEG、WEBP 和 GIF 格式",
"imageUploader.imageList": "图片列表",
"imageUploader.imageUpload": "图片上传",
"imageUploader.pasteImageLink": "粘贴图片链接",
"imageUploader.pasteImageLinkInputPlaceholder": "将图像链接粘贴到此处",
@ -513,8 +512,6 @@
"operation.ok": "好的",
"operation.openInNewTab": "在新标签页打开",
"operation.params": "参数设置",
"operation.pause": "暂停",
"operation.play": "播放",
"operation.refresh": "重新开始",
"operation.regenerate": "重新生成",
"operation.reload": "刷新",
@ -522,7 +519,6 @@
"operation.rename": "重命名",
"operation.reset": "重置",
"operation.resetKeywords": "重置关键词",
"operation.retry": "重试",
"operation.save": "保存",
"operation.saveAndEnable": "保存并启用",
"operation.saveAndRegenerate": "保存并重新生成子分段",
@ -537,19 +533,13 @@
"operation.skip": "跳过",
"operation.submit": "提交",
"operation.sure": "我确定",
"operation.toggleFullscreen": "切换全屏",
"operation.toggleMute": "切换静音",
"operation.view": "查看",
"operation.viewDetails": "查看详情",
"operation.viewMore": "查看更多",
"operation.yes": "是",
"operation.zoomIn": "放大",
"operation.zoomOut": "缩小",
"pagination.editPageNumber": "编辑页码,当前第 {{page}} 页,共 {{totalPages}} 页",
"pagination.next": "下一页",
"pagination.pageNumber": "页码",
"pagination.perPage": "每页显示",
"pagination.previous": "上一页",
"placeholder.input": "请输入",
"placeholder.search": "搜索...",
"placeholder.select": "请选择",
@ -687,6 +677,5 @@
"voiceInput.converting": "正在转换为文本...",
"voiceInput.notAllow": "麦克风未授权",
"voiceInput.speaking": "现在讲...",
"voiceInput.start": "语音输入",
"you": "你"
}

View File

@ -194,7 +194,6 @@
"imageInput.browse": "瀏覽",
"imageInput.dropImageHere": "將您的圖片放在這裡,或",
"imageInput.supportedFormats": "支援 PNG、JPG、JPEG、WEBP 和 GIF",
"imageUploader.imageList": "圖片列表",
"imageUploader.imageUpload": "圖片上傳",
"imageUploader.pasteImageLink": "貼上圖片連結",
"imageUploader.pasteImageLinkInputPlaceholder": "將影象連結貼上到此處",
@ -513,8 +512,6 @@
"operation.ok": "好的",
"operation.openInNewTab": "在新選項卡中打開",
"operation.params": "引數設定",
"operation.pause": "暫停",
"operation.play": "播放",
"operation.refresh": "重新開始",
"operation.regenerate": "再生",
"operation.reload": "重新整理",
@ -522,7 +519,6 @@
"operation.rename": "重新命名",
"operation.reset": "重置",
"operation.resetKeywords": "重置關鍵字",
"operation.retry": "重試",
"operation.save": "儲存",
"operation.saveAndEnable": "儲存並啟用",
"operation.saveAndRegenerate": "保存並重新生成子塊",
@ -537,19 +533,13 @@
"operation.skip": "船",
"operation.submit": "提交",
"operation.sure": "我確定",
"operation.toggleFullscreen": "切換全螢幕",
"operation.toggleMute": "切換靜音",
"operation.view": "視圖",
"operation.viewDetails": "查看詳情",
"operation.viewMore": "查看更多",
"operation.yes": "是",
"operation.zoomIn": "放大",
"operation.zoomOut": "縮小",
"pagination.editPageNumber": "編輯頁碼,目前第 {{page}} 頁,共 {{totalPages}} 頁",
"pagination.next": "下一頁",
"pagination.pageNumber": "頁碼",
"pagination.perPage": "每頁項目數",
"pagination.previous": "上一頁",
"placeholder.input": "請輸入",
"placeholder.search": "搜尋...",
"placeholder.select": "請選擇",
@ -687,6 +677,5 @@
"voiceInput.converting": "正在轉換為文字...",
"voiceInput.notAllow": "麥克風未授權",
"voiceInput.speaking": "現在講...",
"voiceInput.start": "語音輸入",
"you": "你"
}