Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc2e6f6cff | ||
|
|
aba27cf9d3 | ||
|
|
a098db62c3 | ||
|
|
c4886127a6 | ||
|
|
343d671575 | ||
|
|
06a0411f84 | ||
|
|
ffe3d45017 | ||
|
|
16005be3e6 | ||
|
|
005adb10ce | ||
|
|
0a7d9e6a25 | ||
|
|
0edfb29851 | ||
|
|
52f567fded | ||
|
|
116b1a3be8 | ||
|
|
84cc6fcf60 | ||
|
|
1f4266ec95 | ||
|
|
c85b3ccd24 | ||
|
|
7687943ecf | ||
|
|
587d25bb40 | ||
|
|
5561d37ce8 | ||
|
|
fffbea22af | ||
|
|
934d45617a | ||
|
|
ccb6f2208c | ||
|
|
d9e4383b22 | ||
|
|
cbd8458776 | ||
|
|
3352b66a26 | ||
|
|
9b5ceed501 | ||
|
|
3687a0807d | ||
|
|
bda9966330 | ||
|
|
f0be7b6af6 | ||
|
|
3b0f0ddf1e | ||
|
|
85526b45dd | ||
|
|
a6a7aeb97f | ||
|
|
edf54d3b72 | ||
|
|
7a48102df4 | ||
|
|
a16d03d5a6 | ||
|
|
8410df4fa9 | ||
|
|
295fcf8701 | ||
|
|
ba4f1c9db7 | ||
|
|
bde5c93740 | ||
|
|
2421e4806a | ||
|
|
27a9cf3d58 | ||
|
|
cddbb1c876 | ||
|
|
2ab9d2f450 | ||
|
|
750d66c4ae | ||
|
|
844c01dc75 | ||
|
|
154082e6bd | ||
|
|
5d90858fa3 | ||
|
|
0ae505fd32 | ||
|
|
7b0c6ebe66 | ||
|
|
591083435d | ||
|
|
d23959a9b5 | ||
|
|
1799768389 | ||
|
|
250547f329 | ||
|
|
4f52c445fc | ||
|
|
0c70890258 | ||
|
|
bc656de886 | ||
|
|
fb6e6f6f13 | ||
|
|
a0d79ff9b1 | ||
|
|
f5dfd8726a | ||
|
|
0095cc8d37 | ||
|
|
16fcc21346 | ||
|
|
f5e4f14159 | ||
|
|
0c1d47ef10 | ||
|
|
2d82e85491 | ||
|
|
87e9c79e43 | ||
|
|
c3042d5aa4 | ||
|
|
d06575af9c | ||
|
|
90eb0520a7 | ||
|
|
dd2869f4bd | ||
|
|
07e0c63b6a | ||
|
|
4253c640cf | ||
|
|
298ce286f1 | ||
|
|
34c377a17e | ||
|
|
9551beca7a | ||
|
|
dfbb2846a5 | ||
|
|
873861c759 | ||
|
|
d4e8329528 | ||
|
|
08346ebc12 | ||
|
|
d093301983 | ||
|
|
8dcc92f7b6 | ||
|
|
d29550667b | ||
|
|
3c2e4fb646 | ||
|
|
f286926526 | ||
|
|
9dc0863e3a | ||
|
|
63f31d0a08 | ||
|
|
8bce3f2357 | ||
|
|
f06b769cfc | ||
|
|
2aca70043e | ||
|
|
bcbaae3fb4 | ||
|
|
bc4d83cbbb | ||
|
|
dd1947beaa | ||
|
|
4aa6ccf495 | ||
|
|
4dd8ddbefd | ||
|
|
08e6d9631f | ||
|
|
143cbb0d88 | ||
|
|
efec971177 | ||
|
|
edc671f428 | ||
|
|
7f91dbc207 | ||
|
|
1cc6f6f308 | ||
|
|
02a8226196 | ||
|
|
42da37830f | ||
|
|
3714d7d3a5 | ||
|
|
b51ac6bd38 | ||
|
|
e819af4f90 | ||
|
|
584b27eea2 | ||
|
|
9c3cc17bd9 | ||
|
|
e1a05496cb | ||
|
|
947dcb367e | ||
|
|
b4557ae3a2 | ||
|
|
63acb114df | ||
|
|
a3d8e92b5c | ||
|
|
8681d5f5f5 | ||
|
|
3a4a4c53ae | ||
|
|
600833c068 | ||
|
|
164b59b37a | ||
|
|
b8952c87b8 | ||
|
|
e961d7cda7 | ||
|
|
348d6b3161 | ||
|
|
9ab000d604 | ||
|
|
a4532dd430 | ||
|
|
1d21c677ab | ||
|
|
d330b60f35 | ||
|
|
584e5bb260 | ||
|
|
71f77186c4 | ||
|
|
fe6174755b | ||
|
|
d9ba55df54 | ||
|
|
70381f38f9 | ||
|
|
d9a5ea053b | ||
|
|
06e2aa9f0e | ||
|
|
219ab7c22d | ||
|
|
cdb65a8caa | ||
|
|
bd6b4112b0 | ||
|
|
34f4063114 | ||
|
|
6c44f8c432 | ||
|
|
34ff5a34cb | ||
|
|
cb350cc56e | ||
|
|
6dc0b6d8a4 | ||
|
|
655613ccf1 | ||
|
|
e87e3397e2 | ||
|
|
3c1766cd14 | ||
|
|
039407b9db | ||
|
|
e02300e34d | ||
|
|
295ce2d46a | ||
|
|
36102cdee5 | ||
|
|
c532298eba | ||
|
|
254da45290 | ||
|
|
2c3b44cbdf | ||
|
|
05b0cddab9 | ||
|
|
a118e1c33d | ||
|
|
cc2a8dab63 | ||
|
|
080317d710 | ||
|
|
a4102b28dd | ||
|
|
d40e46739a | ||
|
|
f4ba5065c0 | ||
|
|
50dc9121b6 | ||
|
|
901854f9d1 | ||
|
|
d7d02f2c93 | ||
|
|
12ff40361d | ||
|
|
8d7165b47b | ||
|
|
c7e19a5780 | ||
|
|
0ac2bc7346 | ||
|
|
5a7b84c075 | ||
|
|
377a77dd58 | ||
|
|
2561583021 | ||
|
|
4a2c4749f2 | ||
|
|
179c73424a | ||
|
|
0b924da666 | ||
|
|
24117dbf3f | ||
|
|
bb23a2d587 | ||
|
|
e93a3d1e62 | ||
|
|
0f7477e2b6 | ||
|
|
df49cef865 | ||
|
|
356950bb78 | ||
|
|
0aafa78926 | ||
|
|
d3ed80219e | ||
|
|
04388a7cfe | ||
|
|
0fea710705 | ||
|
|
56d296558e | ||
|
|
87b0087e52 | ||
|
|
b79287d327 | ||
|
|
4cdfa5b07f | ||
|
|
f542534fdc | ||
|
|
b06f28bd83 |
@@ -1,29 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve CloudBeaver
|
||||
title: ''
|
||||
labels: bug, wait for review
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here, e.g. error log.
|
||||
@@ -0,0 +1,61 @@
|
||||
name: 🐛 Bug Report
|
||||
description: Create a report to help us improve our product
|
||||
labels: [wait for review]
|
||||
type: Bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for reporting a bug in CloudBeaver!
|
||||
To ensure we can address the issue as quickly and efficiently as possible, please provide as much detail as you can in the fields below.
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: |
|
||||
A clear and concise description of the issue being addressed.
|
||||
- Describe the actual problematic behavior.
|
||||
- Ensure private information is redacted.
|
||||
- Include console output or log files if relevant.
|
||||
- If applicable, add screenshots or screen recordings to help explain your problem.
|
||||
placeholder: |
|
||||
Please enter a description of the issue. Here you can also attach log files, screenshots or a video
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Describe the steps to reproduce the bug
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected/Desired Behavior
|
||||
description: |
|
||||
A clear and concise description of what happens in the software **after** a fix is created and merged.
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: CloudBeaver Version
|
||||
description: What version of CloudBeaver are you running?
|
||||
placeholder: ex. CloudBeaver Community 25.0.0
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Any additional context about the environment
|
||||
placeholder: |
|
||||
Example:
|
||||
1. OS: [e.g. iOS, Ubuntu, Windows 11]
|
||||
2. Browser: [e.g. Chrome, Safari]
|
||||
validations:
|
||||
required: false
|
||||
@@ -0,0 +1,14 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Read our documentations
|
||||
url: https://dbeaver.com/docs/cloudbeaver
|
||||
about: Many articles about product deployment and features.
|
||||
- name: Try advanced database management features in DBeaver PRO or CloudBeaver EE
|
||||
url: https://dbeaver.com/download/?utm_source=github&utm_medium=social&utm_campaign=issue
|
||||
about: Download a 14-day free trial on dbeaver.com. No credit card is needed.
|
||||
- name: Learn how to use CloudBeaver effectively with our tutorial videos
|
||||
url: https://www.youtube.com/channel/UC-yOjsQLSaJVEghg6UB3N7A
|
||||
about: Subscribe to our YouTube channel and watch new tutorials every week.
|
||||
- name: Follow us on X (ex. Twitter)
|
||||
url: https://x.com/dbeaver_news
|
||||
about: Get everyday tips, fresh news, and DBeaver/CloudBeaver feature overviews.
|
||||
@@ -1,8 +1,9 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for CloudBeaver
|
||||
name: Feature Request
|
||||
about: Provide a description of your feature request for CloudBeaver. Include the problem you are trying to solve, why it is important, and any specific functionality you believe would improve the platform.
|
||||
title: ''
|
||||
labels: feature request, wait for review
|
||||
labels: wait for review
|
||||
type: Feature
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
@@ -12,3 +13,9 @@ A clear and concise description of what the problem is. Ex. I'm always frustrate
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
---
|
||||
name: Question
|
||||
about: Is there something unclear?
|
||||
about: Is there something unclear? Ask your question.
|
||||
title: ''
|
||||
labels: question, wait for review
|
||||
labels: wait for review
|
||||
type: Question
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
- name: Clone Deps Repositories
|
||||
uses: dbeaver/github-actions/clone-repositories@devel
|
||||
with:
|
||||
project_deps_path: './cloudbeaver/project.deps'
|
||||
project_deps_path: "./cloudbeaver/project.deps"
|
||||
token: ${{ secrets.DEVOPS_ISSUE_RO_TOKEN }}
|
||||
|
||||
- run: corepack enable
|
||||
@@ -48,7 +48,17 @@ jobs:
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
cache: "yarn"
|
||||
cache-dependency-path: './cloudbeaver/webapp/yarn.lock'
|
||||
cache-dependency-path: |
|
||||
cloudbeaver/common-*/yarn.lock
|
||||
cloudbeaver/webapp/yarn.lock
|
||||
|
||||
- name: restore cloudbeaver-common typescript cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: "cloudbeaver/common-*/**/*/lib"
|
||||
key: ${{ runner.os }}-common-dist-${{ hashFiles('cloudbeaver/common-*/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-common-dist-
|
||||
|
||||
- name: restore typescript cache
|
||||
uses: actions/cache@v4
|
||||
@@ -61,11 +71,23 @@ jobs:
|
||||
- if: inputs.clean-cache
|
||||
run: yarn clear
|
||||
|
||||
- run: yarn install --immutable
|
||||
working-directory: ./cloudbeaver/common-typescript
|
||||
|
||||
- run: yarn install --immutable
|
||||
working-directory: ./cloudbeaver/common-react
|
||||
|
||||
- run: yarn install --immutable
|
||||
|
||||
- run: yarn bundle
|
||||
working-directory: ./cloudbeaver/webapp/packages/product-default
|
||||
|
||||
- run: yarn test
|
||||
working-directory: ./cloudbeaver/common-typescript
|
||||
|
||||
# - run: yarn test
|
||||
# working-directory: ./cloudbeaver/common-react
|
||||
|
||||
- run: yarn test
|
||||
|
||||
# - name: Archive build artifacts
|
||||
|
||||
@@ -29,6 +29,12 @@ jobs:
|
||||
cache: "yarn"
|
||||
cache-dependency-path: "./cloudbeaver/webapp/yarn.lock"
|
||||
|
||||
# - run: yarn install --immutable
|
||||
# working-directory: ./cloudbeaver/common-typescript
|
||||
|
||||
# - run: yarn install --immutable
|
||||
# working-directory: ./cloudbeaver/common-react
|
||||
|
||||
- run: |
|
||||
yarn install --immutable
|
||||
git fetch origin "${{ github.base_ref }}" --depth=1
|
||||
|
||||
@@ -10,7 +10,7 @@ on:
|
||||
branches: [devel]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || 'push-pr-devel' }}
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"yzhang.markdown-all-in-one",
|
||||
"GraphQL.vscode-graphql-syntax",
|
||||
"mrmlnc.vscode-json5",
|
||||
"esbenp.prettier-vscode"
|
||||
"esbenp.prettier-vscode",
|
||||
"devnaumov.licensify"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"license": {
|
||||
"scope": "typescript,typescriptreact,javascript,javascriptreact,css,scss",
|
||||
"prefix": "license",
|
||||
"body": [
|
||||
"/*",
|
||||
" * CloudBeaver - Cloud Database Manager",
|
||||
" * Copyright (C) 2020-2025 DBeaver Corp and others",
|
||||
" *",
|
||||
" * Licensed under the Apache License, Version 2.0.",
|
||||
" * you may not use this file except in compliance with the License.",
|
||||
" */"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -2,21 +2,18 @@
|
||||
"editor.tabSize": 2,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.linkedEditing": true,
|
||||
|
||||
"task.autoDetect": "off",
|
||||
|
||||
"java.configuration.updateBuildConfiguration": "automatic",
|
||||
"java.jdt.ls.vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx16G -Xms100m -Xlog:disable",
|
||||
"java.compile.nullAnalysis.mode": "automatic",
|
||||
|
||||
"typescript.preferences.useAliasesForRenames": false,
|
||||
"typescript.preferences.importModuleSpecifier": "relative",
|
||||
"typescript.preferences.includePackageJsonAutoImports": "on",
|
||||
"typescript.preferences.quoteStyle": "single",
|
||||
"typescript.tsdk": "webapp/.yarn/sdks/typescript/lib",
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||
"prettier.prettierPath": "webapp/.yarn/sdks/prettier/index.cjs",
|
||||
"eslint.nodePath": "webapp/.yarn/sdks",
|
||||
|
||||
"eslint.enable": true,
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
@@ -24,8 +21,9 @@
|
||||
"typescript",
|
||||
"typescriptreact"
|
||||
],
|
||||
"eslint.workingDirectories": ["webapp"],
|
||||
|
||||
"eslint.workingDirectories": [
|
||||
"webapp"
|
||||
],
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
@@ -42,12 +40,10 @@
|
||||
"[java]": {
|
||||
"editor.defaultFormatter": "redhat.java"
|
||||
},
|
||||
|
||||
"files.eol": "\n",
|
||||
"files.associations": {
|
||||
"*.css": "postcss"
|
||||
},
|
||||
|
||||
"java.checkstyle.configuration": "${workspaceFolder}/../dbeaver-common/.github/dbeaver-checkstyle-config.xml",
|
||||
"java.checkstyle.version": "10.12.0"
|
||||
}
|
||||
}
|
||||
@@ -92,18 +92,10 @@
|
||||
"clear": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Update TS references CE",
|
||||
"type": "shell",
|
||||
"command": "yarn run update-ts-references",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/webapp"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Clean CE",
|
||||
"type": "shell",
|
||||
"command": "yarn clean",
|
||||
"command": "yarn clear",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/webapp"
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ CloudBeaver is a web server that provides a rich web interface. The server itsel
|
||||
It is free to use and open-source (licensed under [Apache 2](https://github.com/dbeaver/cloudbeaver/blob/devel/LICENSE) license).
|
||||
See our [WIKI](https://github.com/dbeaver/cloudbeaver/wiki) for more details.
|
||||
|
||||
<img src="https://github.com/dbeaver/cloudbeaver/wiki/images/connection-creation-demo.png" width="400"/>
|
||||
<a><img src="https://github.com/dbeaver/cloudbeaver/wiki/images/connection-creation-demo.png" width="400"/></a>
|
||||
<img src="https://github.com/dbeaver/cloudbeaver/wiki/images/gis-demo.png" width="400"/>
|
||||
<img src="https://github.com/dbeaver/cloudbeaver/wiki/images/data-transfer-demo.png" width="400"/>
|
||||
<img src="https://github.com/dbeaver/cloudbeaver/wiki/images/sql-editor-demo.png" width="400"/>
|
||||
@@ -15,7 +15,7 @@ See our [WIKI](https://github.com/dbeaver/cloudbeaver/wiki) for more details.
|
||||
## Run in Docker
|
||||
|
||||
- [Official Docker repository](https://hub.docker.com/r/dbeaver/cloudbeaver)
|
||||
- [Running instructions](https://github.com/dbeaver/cloudbeaver/wiki/Run-Docker-Container)
|
||||
- [Deployment instructions](https://github.com/dbeaver/cloudbeaver/wiki/CloudBeaver-Deployment)
|
||||
|
||||
## Demo server
|
||||
|
||||
@@ -25,67 +25,70 @@ You can see a live demo of CloudBeaver here: https://demo.cloudbeaver.io
|
||||
|
||||
## Changelog
|
||||
|
||||
### 24.3.4. 2025-02-03
|
||||
- Connections:
|
||||
- Added a possibility to switch a database connection to the read-only mode. This mode can be activated in the connection dialog. Administrators can set this mode for any connections, the regular users can set it for their own private connections only;
|
||||
- An ability to use advanced database settings in the URL connection mode was added for Oracle and SQL Server connections.
|
||||
- General:
|
||||
- Introduced auto-suggestions for column names in the Data Editor filter field to avoid typing mistakes;
|
||||
- Fixed the display of objects with long names (more than 32 characters) in the Navigator tree - now, such names will be displayed entirely and not in a cropped format;
|
||||
- Added the ability to use default spreadsheet names (e.g. Sheet0, Sheet1) for export to XLSX.
|
||||
- Deployment:
|
||||
- CloudBeaver base Java image was switched to the JDK 21 version. The new Java version will be applied automatically after the product update if you use standard deployment and upgrade scripts;
|
||||
- Changed the permissions of the Docker volumes directory from the "root" user to the "dbeaver" user (uid=8978). To enhance security and prevent any insecure actions by the "root" user. These changes will only affect existing deployments.
|
||||
### 25.0.5 2025-05-19
|
||||
- Added the ability to configure the default database or schema in the connection configuration;
|
||||
- A new setting to autosave scripts in the SQL editor appeared in global and users' preferences;
|
||||
- SQLite: added information about a table Strict mode to the table metadata section (thanks to @eusebe-cda);
|
||||
- Vietnamese localization has been added (thanks to @0xhanh).
|
||||
|
||||
### 24.3.3. 2025-01-20
|
||||
- Administration:
|
||||
- Added an ability to match users from LDAP configuration with CloudBeaver users;
|
||||
- New environment variables were introduced for theme styling, SQL editor, and log viewer settings. You can use them for quick setup during the initial Docker configuration stage.
|
||||
### 25.0.4 2025-05-05
|
||||
- Important:
|
||||
- Connection templates were removed from the application. You can use the Connection Editor to create new connections.
|
||||
- Databases and drivers:
|
||||
- LibSQL: added the ability to use token authentication for connecting;
|
||||
- Clickhouse: driver has been updated to version 0.8.5.
|
||||
|
||||
### 25.0.3 2025-04-21
|
||||
- Administration: log records now capture create, update, and delete actions for users, teams, and connections for improved tracking and transparency;
|
||||
- SQL Editor: auto-completion has been improved for aliases and camelCase entities;
|
||||
- Users DDL in Oracle is displayed in metadata editor on corresponding tab for users with Oracle administration permissions;
|
||||
- Different bug fixes and enhancements have been made.
|
||||
|
||||
### 25.0.2 2025-04-07
|
||||
- Data Editor: improved table rendering to scale column width depending on the content.
|
||||
- Improved Navigation tree performance: lost connection does not lead to the application freezing anymore.
|
||||
- Different bug fixes and enhancements have been made.
|
||||
|
||||
### 25.0.1 2025-03-24
|
||||
- Improved performance of the export/import functionality by optimizing disk memory consumption;
|
||||
- Enhanced initial configuration on stage security: if the initial setup time exceeds 1 hour, the server must be restarted.
|
||||
|
||||
### 25.0 2025-03-03
|
||||
### Changes since 24.3.0:
|
||||
- General:
|
||||
- An ability to show metadata object comments was added in the Navigator tree. You can enable it in the database navigator settings panel;
|
||||
- Added transaction log viewing for connections with manual commit mode. This allows users to see all modifying statements before committing;
|
||||
- Added an ability to import data in tables without unique keys;
|
||||
- Added an ability to insert data in tables without unique keys;
|
||||
- Added Ctrl + Shift + S (Cmd + Shift + S on Mac) shortcut for "Save As Script" action.
|
||||
|
||||
### 24.3.2. 2025-01-06
|
||||
- Added an ability to specify a user login as an attribute parameter for LDAP providers;
|
||||
- The collapse of the grouping panel doesn't lead to the full panel cleaning anymore.
|
||||
|
||||
### 24.3.1. 2024-12-23
|
||||
- Added an ability to reconnect for all editors if a connection has been lost;
|
||||
- Added an option to replace line break characters on any custom value when exporting to CSV;
|
||||
- Added an ability to create connections in the Navigation tree not only on the initial level but in folders and sub-folders directly;
|
||||
- Updated list of available shortcuts for MacOS.
|
||||
|
||||
### 24.3.0. 2024-12-02
|
||||
### Changes since 24.2.0:
|
||||
- General:
|
||||
- The connections Templates feature is declared obsolete and will be removed in future releases;
|
||||
- Data export: Added the ability to export JSON values as embedded JSON;
|
||||
- Fixed a proxy issue that excluded the Content-Type header in responses;
|
||||
- Data editor enhancements: Rows with focused cells are specially marked to make it easier to locate a position in large tables;
|
||||
- Keyboard navigation has been enhanced. You can now use the arrow keys to move through navigator tree elements and the tab key to switch between editors' tabs;
|
||||
- Sample SQLite database was removed;
|
||||
- Chinese localization has been improved (thanks to @cashlifei);
|
||||
- Brazilian Portuguese localization was enhanced (thanks to @brlarini).
|
||||
- Added an ability for all editors to reconnect if a connection has been lost;
|
||||
- Tooltips for metadata objects and SQL Editor tabs now include additional details about catalogs, schemas, and connections;
|
||||
- Added Ctrl + Shift + S (Cmd + Shift + S on Mac) shortcut for the "Save As Script" action;
|
||||
- Updated list of available shortcuts for macOS.
|
||||
- Administration:
|
||||
- Updated user storage mechanism: New user logins are now stored in lowercase to prevent duplicate entries (e.g., "ADMIN" and "admin"). Note: This update does not affect previously created user logins;
|
||||
- Added an ability to change a user password even if the user is disabled in a system;
|
||||
- Refreshed design for the User and Teams tab in the Administration panel;
|
||||
- A new setting in Global Preferences was added to restrict data import for non-admin users;
|
||||
- A search option was added for preferences in the Administration part;
|
||||
- Added detailed logging of GraphQL queries in the server logs, including all provided variables, for improved debugging and monitoring;
|
||||
- Environment variables configuration has been improved - now you can configure more variables on the initial stage of the Docker setup.
|
||||
- Databases:
|
||||
- PostgreSQL, Greenplum, CockroachDB: The URL mode now supports connecting to multiple databases;
|
||||
- PostgreSQL, H2, and SQL Server: Schemas were added to the SQL autocompletion;
|
||||
- MySQL: CloudBeaver can now correctly display negative dates;
|
||||
- DuckDB: The issue with displaying BLOB data types has been resolved.
|
||||
- Added system information data to the Product Information tab in the Administration panel. This provides administrators with quick access to essential system details, such as the server deployment type (e.g., Docker, Kubernetes), install path, Java version, and Java parameters;
|
||||
- The global access server URL has been added to the console log, making it easier to locate server installation information directly within the logs;
|
||||
- New environment variables were introduced for theme styling, SQL editor, and log viewer settings. You can use them for quick setup during the initial Docker configuration stage.
|
||||
- LDAP authorization:
|
||||
- Added the ability to match users from LDAP configuration with CloudBeaver users;
|
||||
- Added an option to specify a user login as an attribute parameter for LDAP providers;
|
||||
- CloudBeaver now uses EntityID or objectGUID to create new LDAP users in CloudBeaver to ensure uniqueness. This change enhances user identification and prevents conflicts while maintaining backward compatibility for existing users.
|
||||
- Deployment:
|
||||
- CloudBeaver now uses JDK 21 as its base Java image. If you use standard deployment and upgrade scripts, the new Java version will be applied automatically after the product update;
|
||||
- Changed the permission of the Docker volumes directory to enhance security and prevent any insecure actions by the "root" user. All new deployments will use the "dbeaver" user by default. For existing deployments running as the "root" user, this update will automatically adjust file permissions to ensure they are compatible with the new security model. There will be no manual intervention, and the service will continue to run as expected with the updated permissions.
|
||||
- Connections:
|
||||
- Enabled the possibility of switching a database connection to read-only mode. This mode can be activated in the connection dialog. Administrators can set this mode for any connection, while regular users can set it for their private connections only;
|
||||
- For Oracle and SQL Server connections, advanced database settings can now be used in the URL connection mode.
|
||||
- Data Editor:
|
||||
- Auto-suggestions for column names in the Data Editor filter field were introduced to avoid typing mistakes;
|
||||
- Introduced the ability to insert data in tables without unique keys: you can import data from CSV files or insert entire rows directly via UI;
|
||||
- The collapse of the grouping panel doesn't lead to the complete panel cleaning anymore.
|
||||
- Data transfer:
|
||||
- Added the ability to use default spreadsheet names (e.g. Sheet0, Sheet1) for exporting to XLSX;
|
||||
- An option to replace line break characters on any custom value was added when exporting to CSV.
|
||||
- Navigator tree:
|
||||
- An option to show metadata object comments was added. You can enable it in the database navigator settings panel;
|
||||
- Introduced the ability to create connections not only on the initial level but in folders and sub-folders directly;
|
||||
- Fixed the display of objects with long names (more than 32 characters) - now, such names will be displayed entirely and not in a cropped format.
|
||||
- Drivers:
|
||||
- A new LibSQL/Turso driver was added;
|
||||
- SQL Server driver has been updated to version 12.8.0;
|
||||
- DB2i driver has been updated to version 20.0.7.
|
||||
- The MariaDB driver has been updated to version 3.5.1;
|
||||
- The SQLite driver has been updated to version 3.48.0.0.
|
||||
|
||||
## Contribution
|
||||
As a community-driven open-source project, we warmly welcome contributions through GitHub pull requests.
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
# artifacts
|
||||
lib
|
||||
out
|
||||
dist
|
||||
node_modules
|
||||
|
||||
# Yarn
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
!.yarn/sdks/**
|
||||
|
||||
# Swap the comments on the following lines if you wish to use zero-installs
|
||||
# In that case, don't forget to run `yarn config set enableGlobalCache false`!
|
||||
@@ -11,9 +19,3 @@
|
||||
|
||||
#!.yarn/cache
|
||||
.pnp.*
|
||||
|
||||
# artifacts
|
||||
lib
|
||||
out
|
||||
dist
|
||||
node_modules
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "prettier",
|
||||
"version": "3.4.2-sdk",
|
||||
"version": "3.5.3-sdk",
|
||||
"main": "./index.cjs",
|
||||
"type": "commonjs",
|
||||
"bin": "./bin/prettier.cjs"
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"name": "typescript",
|
||||
"version": "5.7.3-sdk",
|
||||
"main": "./lib/typescript.js",
|
||||
"type": "commonjs",
|
||||
"bin": {
|
||||
"tsc": "./bin/tsc",
|
||||
"tsserver": "./bin/tsserver"
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,10 @@
|
||||
enableTelemetry: false
|
||||
|
||||
preferReuse: true
|
||||
|
||||
# pnpIgnorePatterns:
|
||||
# - "**/cloudbeaver/common-typescript/**"
|
||||
|
||||
plugins:
|
||||
- checksum: 50414773926286d8c0fc32e9936a799ad23296281046f03674bc33517b597e1ff7856092c3fcf8d5460a29549ef8b3dca3f59a1732b650a02bf523b255d726e7
|
||||
path: .yarn/plugins/@yarnpkg/plugin-ts-project-linker.cjs
|
||||
spec: "https://raw.githubusercontent.com/Wroud/foundation/refs/heads/main/packages/yarn-plugin-ts-project-linker/bundles/%40yarnpkg/plugin-ts-project-linker.js"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
!lib
|
||||
@@ -1,11 +1,10 @@
|
||||
import { JSX as JSX_2 } from 'react/jsx-runtime';
|
||||
import { Context } from 'react';
|
||||
import { JSX } from 'react/jsx-runtime';
|
||||
import type { Key } from 'react';
|
||||
import { Provider } from 'react';
|
||||
import type { ReactElement } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import { RefAttributes } from 'react';
|
||||
|
||||
declare interface BaseRenderRowProps<TRow, TSummaryRow = unknown> extends Omit_2<React.HTMLAttributes<HTMLDivElement>, 'style' | 'children'>, Pick<DataGridProps<TRow, TSummaryRow>, 'onCellClick' | 'onCellDoubleClick' | 'onCellContextMenu'> {
|
||||
declare interface BaseRenderRowProps<TRow, TSummaryRow = unknown> extends Omit_2<React.ComponentProps<'div'>, 'style' | 'children'>, Pick<DataGridProps<TRow, TSummaryRow>, 'onCellClick' | 'onCellDoubleClick' | 'onCellContextMenu'> {
|
||||
viewportColumns: readonly CalculatedColumn<TRow, TSummaryRow>[];
|
||||
rowIdx: number;
|
||||
selectedCellIdx: number | undefined;
|
||||
@@ -27,6 +26,7 @@ export declare interface CalculatedColumn<TRow, TSummaryRow = unknown> extends C
|
||||
readonly draggable: boolean;
|
||||
readonly frozen: boolean;
|
||||
readonly renderCell: (props: RenderCellProps<TRow, TSummaryRow>) => ReactNode;
|
||||
readonly renderHeaderCell: (props: RenderHeaderCellProps<TRow, TSummaryRow>) => ReactNode;
|
||||
}
|
||||
|
||||
export declare type CalculatedColumnOrColumnGroup<R, SR> = CalculatedColumnParent<R, SR> | CalculatedColumn<R, SR>;
|
||||
@@ -40,9 +40,10 @@ export declare interface CalculatedColumnParent<R, SR> {
|
||||
readonly headerCellClass?: Maybe<string>;
|
||||
}
|
||||
|
||||
export declare const Cell: <R, SR>(props: CellRendererProps<R, SR> & RefAttributes<HTMLDivElement>) => JSX.Element;
|
||||
export declare const Cell: <R, SR>(props: CellRendererProps<R, SR>) => React.JSX.Element;
|
||||
|
||||
export declare interface CellClickArgs<TRow, TSummaryRow = unknown> {
|
||||
rowIdx: number;
|
||||
row: TRow;
|
||||
column: CalculatedColumn<TRow, TSummaryRow>;
|
||||
selectCell: (enableEditor?: boolean) => void;
|
||||
@@ -59,7 +60,7 @@ export declare type CellKeyDownArgs<TRow, TSummaryRow = unknown> = SelectCellKey
|
||||
|
||||
export declare type CellMouseEvent = CellEvent<React.MouseEvent<HTMLDivElement>>;
|
||||
|
||||
export declare interface CellRendererProps<TRow, TSummaryRow> extends Pick<RenderRowProps<TRow, TSummaryRow>, 'row' | 'rowIdx' | 'selectCell'>, Omit_2<React.HTMLAttributes<HTMLDivElement>, 'style' | 'children' | 'onClick' | 'onDoubleClick' | 'onContextMenu'> {
|
||||
export declare interface CellRendererProps<TRow, TSummaryRow> extends Pick<RenderRowProps<TRow, TSummaryRow>, 'row' | 'rowIdx' | 'selectCell'>, Omit_2<React.ComponentProps<'div'>, 'children' | 'onClick' | 'onDoubleClick' | 'onContextMenu'> {
|
||||
column: CalculatedColumn<TRow, TSummaryRow>;
|
||||
colSpan: number | undefined;
|
||||
isCopied: boolean;
|
||||
@@ -73,7 +74,7 @@ export declare interface CellRendererProps<TRow, TSummaryRow> extends Pick<Rende
|
||||
|
||||
export declare interface CellSelectArgs<TRow, TSummaryRow = unknown> {
|
||||
rowIdx: number;
|
||||
row: TRow;
|
||||
row: TRow | undefined;
|
||||
column: CalculatedColumn<TRow, TSummaryRow>;
|
||||
}
|
||||
|
||||
@@ -92,19 +93,25 @@ export declare interface Column<TRow, TSummaryRow = unknown> {
|
||||
readonly name: string | ReactElement;
|
||||
/** A unique key to distinguish each column */
|
||||
readonly key: string;
|
||||
/** Column width. If not specified, it will be determined automatically based on grid width and specified widths of other columns */
|
||||
/**
|
||||
* Column width. If not specified, it will be determined automatically based on grid width and specified widths of other columns
|
||||
* @default 'auto'
|
||||
*/
|
||||
readonly width?: Maybe<number | string>;
|
||||
/** Minimum column width in px. */
|
||||
/**
|
||||
* Minimum column width in px
|
||||
* @default '50px'
|
||||
*/
|
||||
readonly minWidth?: Maybe<number>;
|
||||
/** Maximum column width in px. */
|
||||
readonly maxWidth?: Maybe<number>;
|
||||
readonly cellClass?: Maybe<string | ((row: TRow) => Maybe<string>)>;
|
||||
readonly headerCellClass?: Maybe<string>;
|
||||
readonly summaryCellClass?: Maybe<string | ((row: TSummaryRow) => Maybe<string>)>;
|
||||
/** Render function used to render the content of the column's header cell */
|
||||
readonly renderHeaderCell?: Maybe<(props: RenderHeaderCellProps<TRow, TSummaryRow>) => ReactNode>;
|
||||
/** Render function used to render the content of cells */
|
||||
readonly renderCell?: Maybe<(props: RenderCellProps<TRow, TSummaryRow>) => ReactNode>;
|
||||
/** Render function used to render the content of the column's header cell */
|
||||
readonly renderHeaderCell?: Maybe<(props: RenderHeaderCellProps<TRow, TSummaryRow>) => ReactNode>;
|
||||
/** Render function used to render the content of summary cells */
|
||||
readonly renderSummaryCell?: Maybe<(props: RenderSummaryCellProps<TSummaryRow, TRow>) => ReactNode>;
|
||||
/** Render function used to render the content of group cells */
|
||||
@@ -151,16 +158,25 @@ export declare interface CopyEvent<TRow> {
|
||||
sourceRow: TRow;
|
||||
}
|
||||
|
||||
export declare const DataGridDefaultRenderersProvider: Provider<Maybe<Renderers<any, any>>>;
|
||||
/**
|
||||
* Main API Component to render a data grid of rows and columns
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* <DataGrid columns={columns} rows={rows} />
|
||||
*/
|
||||
export declare function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridProps<R, SR, K>): JSX.Element;
|
||||
|
||||
export declare const DataGridDefaultRenderersContext: Context<Maybe<Renderers<any, any>>>;
|
||||
|
||||
export declare interface DataGridHandle {
|
||||
element: HTMLDivElement | null;
|
||||
selectedCell: Position;
|
||||
scrollToCell: (position: PartialPosition) => void;
|
||||
selectCell: (position: Position, enableEditor?: Maybe<boolean>) => void;
|
||||
}
|
||||
|
||||
export declare interface DataGridProps<R, SR = unknown, K extends Key = Key> extends SharedDivProps {
|
||||
ref?: Maybe<React.Ref<DataGridHandle>>;
|
||||
/**
|
||||
* Grid and data Props
|
||||
*/
|
||||
@@ -228,7 +244,7 @@ export declare interface DataGridProps<R, SR = unknown, K extends Key = Key> ext
|
||||
/** Called when the grid is scrolled */
|
||||
onScroll?: Maybe<(event: React.UIEvent<HTMLDivElement>) => void>;
|
||||
/** Called when a column is resized */
|
||||
onColumnResize?: Maybe<(idx: number, width: number) => void>;
|
||||
onColumnResize?: Maybe<(column: CalculatedColumn<R, SR>, width: number) => void>;
|
||||
/** Called when a column is reordered */
|
||||
onColumnsReorder?: Maybe<(sourceColumnKey: string, targetColumnKey: string) => void>;
|
||||
/**
|
||||
@@ -236,6 +252,10 @@ export declare interface DataGridProps<R, SR = unknown, K extends Key = Key> ext
|
||||
*/
|
||||
/** @default true */
|
||||
enableVirtualization?: Maybe<boolean>;
|
||||
/**
|
||||
* The minimum number of rows to render when virtualization is enabled
|
||||
*/
|
||||
minimumRowsToRender?: Maybe<number>;
|
||||
/**
|
||||
* Miscellaneous
|
||||
*/
|
||||
@@ -244,12 +264,10 @@ export declare interface DataGridProps<R, SR = unknown, K extends Key = Key> ext
|
||||
/** @default 'ltr' */
|
||||
direction?: Maybe<Direction>;
|
||||
'data-testid'?: Maybe<string>;
|
||||
'data-cy'?: Maybe<string>;
|
||||
}
|
||||
|
||||
declare const _default: <R, SR = unknown, K extends Key = Key>(props: DataGridProps<R, SR, K> & RefAttributes<DataGridHandle>) => React.JSX.Element;
|
||||
export default _default;
|
||||
|
||||
declare type DefaultColumnOptions<R, SR> = Pick<Column<R, SR>, 'renderCell' | 'width' | 'minWidth' | 'maxWidth' | 'resizable' | 'sortable' | 'draggable'>;
|
||||
export declare type DefaultColumnOptions<R, SR> = Pick<Column<R, SR>, 'renderCell' | 'renderHeaderCell' | 'width' | 'minWidth' | 'maxWidth' | 'resizable' | 'sortable' | 'draggable'>;
|
||||
|
||||
declare type Direction = 'ltr' | 'rtl';
|
||||
|
||||
@@ -296,7 +314,7 @@ export declare interface PasteEvent<TRow> {
|
||||
targetRow: TRow;
|
||||
}
|
||||
|
||||
export declare interface Position {
|
||||
declare interface Position {
|
||||
readonly idx: number;
|
||||
readonly rowIdx: number;
|
||||
}
|
||||
@@ -310,9 +328,9 @@ export declare interface RenderCellProps<TRow, TSummaryRow = unknown> {
|
||||
onRowChange: (row: TRow) => void;
|
||||
}
|
||||
|
||||
export declare function renderCheckbox({ onChange, indeterminate, ...props }: RenderCheckboxProps): JSX_2.Element;
|
||||
export declare function renderCheckbox({ onChange, indeterminate, ...props }: RenderCheckboxProps): JSX.Element;
|
||||
|
||||
export declare interface RenderCheckboxProps extends Pick<React.InputHTMLAttributes<HTMLInputElement>, 'aria-label' | 'aria-labelledby' | 'checked' | 'tabIndex' | 'disabled'> {
|
||||
export declare interface RenderCheckboxProps extends Pick<React.ComponentProps<'input'>, 'aria-label' | 'aria-labelledby' | 'checked' | 'tabIndex' | 'disabled'> {
|
||||
indeterminate?: boolean | undefined;
|
||||
onChange: (checked: boolean, shift: boolean) => void;
|
||||
}
|
||||
@@ -326,10 +344,10 @@ export declare interface RenderEditCellProps<TRow, TSummaryRow = unknown> {
|
||||
}
|
||||
|
||||
export declare interface Renderers<TRow, TSummaryRow> {
|
||||
renderCell?: Maybe<(key: Key, props: CellRendererProps<TRow, TSummaryRow>) => ReactNode>;
|
||||
renderCheckbox?: Maybe<(props: RenderCheckboxProps) => ReactNode>;
|
||||
renderRow?: Maybe<(key: Key, props: RenderRowProps<TRow, TSummaryRow>) => ReactNode>;
|
||||
renderSortStatus?: Maybe<(props: RenderSortStatusProps) => ReactNode>;
|
||||
renderCell?: Maybe<(key: Key, props: CellRendererProps<TRow, TSummaryRow>) => ReactNode>;
|
||||
noRowsFallback?: Maybe<ReactNode>;
|
||||
}
|
||||
|
||||
@@ -343,7 +361,7 @@ export declare interface RenderGroupCellProps<TRow, TSummaryRow = unknown> {
|
||||
toggleGroup: () => void;
|
||||
}
|
||||
|
||||
export declare function renderHeaderCell<R, SR>({ column, sortDirection, priority }: RenderHeaderCellProps<R, SR>): string | JSX_2.Element;
|
||||
export declare function renderHeaderCell<R, SR>({ column, sortDirection, priority }: RenderHeaderCellProps<R, SR>): string | JSX.Element;
|
||||
|
||||
export declare interface RenderHeaderCellProps<TRow, TSummaryRow = unknown> {
|
||||
column: CalculatedColumn<TRow, TSummaryRow>;
|
||||
@@ -363,7 +381,7 @@ export declare interface RenderRowProps<TRow, TSummaryRow = unknown> extends Bas
|
||||
setDraggedOverRowIdx: ((overRowIdx: number) => void) | undefined;
|
||||
}
|
||||
|
||||
export declare function renderSortIcon({ sortDirection }: RenderSortIconProps): JSX_2.Element | null;
|
||||
export declare function renderSortIcon({ sortDirection }: RenderSortIconProps): JSX.Element | null;
|
||||
|
||||
export declare interface RenderSortIconProps {
|
||||
sortDirection: SortDirection | undefined;
|
||||
@@ -384,11 +402,11 @@ export declare interface RenderSummaryCellProps<TSummaryRow, TRow = unknown> {
|
||||
tabIndex: number;
|
||||
}
|
||||
|
||||
export declare function renderToggleGroup<R, SR>(props: RenderGroupCellProps<R, SR>): JSX_2.Element;
|
||||
export declare function renderToggleGroup<R, SR>(props: RenderGroupCellProps<R, SR>): JSX.Element;
|
||||
|
||||
export declare function renderValue<R, SR>(props: RenderCellProps<R, SR>): ReactNode;
|
||||
|
||||
export declare const Row: <R, SR>(props: RenderRowProps<R, SR> & RefAttributes<HTMLDivElement>) => React.JSX.Element;
|
||||
export declare const Row: <R, SR>(props: RenderRowProps<R, SR>) => React.JSX.Element;
|
||||
|
||||
export declare type RowHeightArgs<TRow> = {
|
||||
type: 'ROW';
|
||||
@@ -431,7 +449,7 @@ export declare interface SelectRowEvent<TRow> {
|
||||
isShiftClick: boolean;
|
||||
}
|
||||
|
||||
declare type SharedDivProps = Pick<React.HTMLAttributes<HTMLDivElement>, 'role' | 'aria-label' | 'aria-labelledby' | 'aria-describedby' | 'aria-rowcount' | 'className' | 'style'>;
|
||||
declare type SharedDivProps = Pick<React.ComponentProps<'div'>, 'role' | 'aria-label' | 'aria-labelledby' | 'aria-description' | 'aria-describedby' | 'aria-rowcount' | 'className' | 'style'>;
|
||||
|
||||
declare type SharedInputProps = Pick<RenderCheckboxProps, 'disabled' | 'tabIndex' | 'aria-label' | 'aria-labelledby' | 'indeterminate' | 'onChange'>;
|
||||
|
||||
@@ -442,11 +460,11 @@ export declare interface SortColumn {
|
||||
|
||||
export declare type SortDirection = 'ASC' | 'DESC';
|
||||
|
||||
export declare function textEditor<TRow, TSummaryRow>({ row, column, onRowChange, onClose }: RenderEditCellProps<TRow, TSummaryRow>): JSX_2.Element;
|
||||
export declare function textEditor<TRow, TSummaryRow>({ row, column, onRowChange, onClose }: RenderEditCellProps<TRow, TSummaryRow>): JSX.Element;
|
||||
|
||||
export declare function ToggleGroup<R, SR>({ groupKey, isExpanded, tabIndex, toggleGroup }: RenderGroupCellProps<R, SR>): JSX_2.Element;
|
||||
export declare function ToggleGroup<R, SR>({ groupKey, isExpanded, tabIndex, toggleGroup }: RenderGroupCellProps<R, SR>): JSX.Element;
|
||||
|
||||
export declare const TreeDataGrid: <R, SR = unknown, K extends Key = Key>(props: TreeDataGridProps<R, SR, K> & RefAttributes<DataGridHandle>) => React.JSX.Element;
|
||||
export declare function TreeDataGrid<R, SR = unknown, K extends Key = Key>({ columns: rawColumns, rows: rawRows, rowHeight: rawRowHeight, rowKeyGetter: rawRowKeyGetter, onCellKeyDown: rawOnCellKeyDown, onRowsChange, selectedRows: rawSelectedRows, onSelectedRowsChange: rawOnSelectedRowsChange, renderers, groupBy: rawGroupBy, rowGrouper, expandedGroupIds, onExpandedGroupIdsChange, ...props }: TreeDataGridProps<R, SR, K>): JSX.Element;
|
||||
|
||||
export declare interface TreeDataGridProps<R, SR = unknown, K extends Key = Key> extends Omit_2<DataGridProps<R, SR, K>, 'columns' | 'role' | 'aria-rowcount' | 'rowHeight' | 'onFill' | 'isRowSelectionDisabled'> {
|
||||
columns: readonly Column<NoInfer<R>, NoInfer<SR>>[];
|
||||
@@ -15,7 +15,7 @@
|
||||
Root;
|
||||
}
|
||||
|
||||
.mlln6zg7-0-0-beta-47 {
|
||||
.mlln6zg7-0-0-beta-50 {
|
||||
@layer rdg.MeasuringCell {
|
||||
contain: strict;
|
||||
grid-row: 1;
|
||||
@@ -24,13 +24,13 @@
|
||||
}
|
||||
|
||||
|
||||
.cj343x07-0-0-beta-47 {
|
||||
.cj343x07-0-0-beta-50 {
|
||||
@layer rdg.Cell {
|
||||
/* max-content does not work with size containment
|
||||
* dynamically switching between different containment styles incurs a heavy relayout penalty
|
||||
* Chromium bug: at odd zoom levels or subpixel positioning,
|
||||
* layout/paint/style containment can make cell borders disappear
|
||||
* https://bugs.chromium.org/p/chromium/issues/detail?id=1326946
|
||||
* https://issues.chromium.org/issues/40840864
|
||||
*/
|
||||
position: relative; /* needed for absolute positioning to work */
|
||||
padding-block: 0;
|
||||
@@ -53,7 +53,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.csofj7r7-0-0-beta-47 {
|
||||
.csofj7r7-0-0-beta-50 {
|
||||
@layer rdg.Cell {
|
||||
position: sticky;
|
||||
/* Should have a higher value than 0 to show up above unfrozen cells */
|
||||
@@ -67,7 +67,7 @@
|
||||
}
|
||||
|
||||
|
||||
.c1bn88vv7-0-0-beta-47 {
|
||||
.c1bn88vv7-0-0-beta-50 {
|
||||
@layer rdg.CheckboxInput {
|
||||
display: block;
|
||||
margin: auto;
|
||||
@@ -86,13 +86,13 @@
|
||||
}
|
||||
|
||||
|
||||
.g1s9ylgp7-0-0-beta-47 {
|
||||
.g1s9ylgp7-0-0-beta-50 {
|
||||
@layer rdg.GroupCellContent {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.cz54e4y7-0-0-beta-47 {
|
||||
.cz54e4y7-0-0-beta-50 {
|
||||
@layer rdg.GroupCellCaret {
|
||||
margin-inline-start: 4px;
|
||||
stroke: currentColor;
|
||||
@@ -107,24 +107,39 @@
|
||||
}
|
||||
|
||||
|
||||
.c6ra8a37-0-0-beta-47 {
|
||||
.h44jtk67-0-0-beta-50 {
|
||||
@layer rdg.SortableHeaderCell {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.hcgkhxz7-0-0-beta-50 {
|
||||
@layer rdg.SortableHeaderCellName {
|
||||
flex-grow: 1;
|
||||
overflow: clip;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.c6ra8a37-0-0-beta-50 {
|
||||
@layer rdg.Cell {
|
||||
background-color: #ccccff;
|
||||
}
|
||||
}
|
||||
|
||||
.cq910m07-0-0-beta-47 {
|
||||
.cq910m07-0-0-beta-50 {
|
||||
@layer rdg.Cell {
|
||||
background-color: #ccccff;
|
||||
|
||||
&.c6ra8a37-0-0-beta-47 {
|
||||
&.c6ra8a37-0-0-beta-50 {
|
||||
background-color: #9999ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.c1w9bbhr7-0-0-beta-47 {
|
||||
.c1w9bbhr7-0-0-beta-50 {
|
||||
@layer rdg.DragHandle {
|
||||
--rdg-drag-handle-size: 8px;
|
||||
z-index: 0;
|
||||
@@ -142,7 +157,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.c1creorc7-0-0-beta-47 {
|
||||
.c1creorc7-0-0-beta-50 {
|
||||
@layer rdg.DragHandle {
|
||||
z-index: 1;
|
||||
position: sticky;
|
||||
@@ -150,41 +165,26 @@
|
||||
}
|
||||
|
||||
|
||||
.cis5rrm7-0-0-beta-47 {
|
||||
.cis5rrm7-0-0-beta-50 {
|
||||
@layer rdg.EditCell {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.h44jtk67-0-0-beta-47 {
|
||||
@layer rdg.SortableHeaderCell {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.hcgkhxz7-0-0-beta-47 {
|
||||
@layer rdg.SortableHeaderCellName {
|
||||
flex-grow: 1;
|
||||
overflow: clip;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.c6l2wv17-0-0-beta-47 {
|
||||
.c6l2wv17-0-0-beta-50 {
|
||||
@layer rdg.HeaderCell {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.c1kqdw7y7-0-0-beta-47 {
|
||||
.c1kqdw7y7-0-0-beta-50 {
|
||||
@layer rdg.HeaderCell {
|
||||
touch-action: none;
|
||||
}
|
||||
}
|
||||
|
||||
.r1y6ywlx7-0-0-beta-47 {
|
||||
.r1y6ywlx7-0-0-beta-50 {
|
||||
@layer rdg.HeaderCell {
|
||||
cursor: col-resize;
|
||||
position: absolute;
|
||||
@@ -195,16 +195,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.c1bezg5o7-0-0-beta-47 {
|
||||
.c1bezg5o7-0-0-beta-50 {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.c1vc96037-0-0-beta-47 {
|
||||
.c1vc96037-0-0-beta-50 {
|
||||
background-color: var(--rdg-header-draggable-background-color);
|
||||
}
|
||||
|
||||
|
||||
.r1upfr807-0-0-beta-47 {
|
||||
.r1upfr807-0-0-beta-50 {
|
||||
@layer rdg.Row {
|
||||
display: contents;
|
||||
background-color: var(--rdg-background-color);
|
||||
@@ -223,14 +223,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.r190mhd37-0-0-beta-47 {
|
||||
.r190mhd37-0-0-beta-50 {
|
||||
@layer rdg.FocusSink {
|
||||
outline: 2px solid var(--rdg-selection-color);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
.r139qu9m7-0-0-beta-47 {
|
||||
.r139qu9m7-0-0-beta-50 {
|
||||
@layer rdg.FocusSink {
|
||||
&::before {
|
||||
content: '';
|
||||
@@ -244,26 +244,26 @@
|
||||
}
|
||||
|
||||
|
||||
.h10tskcx7-0-0-beta-47 {
|
||||
.h10tskcx7-0-0-beta-50 {
|
||||
@layer rdg.HeaderRow {
|
||||
display: contents;
|
||||
background-color: var(--rdg-header-background-color);
|
||||
font-weight: bold;
|
||||
|
||||
& > .cj343x07-0-0-beta-47 {
|
||||
& > .cj343x07-0-0-beta-50 {
|
||||
/* Should have a higher value than 1 to show up above regular cells and the focus sink */
|
||||
z-index: 2;
|
||||
position: sticky;
|
||||
}
|
||||
|
||||
& > .csofj7r7-0-0-beta-47 {
|
||||
& > .csofj7r7-0-0-beta-50 {
|
||||
z-index: 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.a3ejtar7-0-0-beta-47 {
|
||||
.a3ejtar7-0-0-beta-50 {
|
||||
@layer rdg.SortIcon {
|
||||
fill: currentColor;
|
||||
|
||||
@@ -274,7 +274,7 @@
|
||||
}
|
||||
|
||||
|
||||
.rnvodz57-0-0-beta-47 {
|
||||
.rnvodz57-0-0-beta-50 {
|
||||
@layer rdg.Defaults {
|
||||
*,
|
||||
*::before,
|
||||
@@ -334,30 +334,30 @@
|
||||
}
|
||||
|
||||
> :nth-last-child(1 of .rdg-top-summary-row) {
|
||||
> .cj343x07-0-0-beta-47 {
|
||||
> .cj343x07-0-0-beta-50 {
|
||||
border-block-end: 2px solid var(--rdg-summary-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
> :nth-child(1 of .rdg-bottom-summary-row) {
|
||||
> .cj343x07-0-0-beta-47 {
|
||||
> .cj343x07-0-0-beta-50 {
|
||||
border-block-start: 2px solid var(--rdg-summary-border-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vlqv91k7-0-0-beta-47 {
|
||||
.vlqv91k7-0-0-beta-50 {
|
||||
@layer rdg.Root {
|
||||
user-select: none;
|
||||
|
||||
& .r1upfr807-0-0-beta-47 {
|
||||
& .r1upfr807-0-0-beta-50 {
|
||||
cursor: move;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.f1lsfrzw7-0-0-beta-47 {
|
||||
.f1lsfrzw7-0-0-beta-50 {
|
||||
@layer rdg.FocusSink {
|
||||
grid-column: 1/-1;
|
||||
pointer-events: none;
|
||||
@@ -366,7 +366,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.f1cte0lg7-0-0-beta-47 {
|
||||
.f1cte0lg7-0-0-beta-50 {
|
||||
@layer rdg.FocusSink {
|
||||
/* Should have a higher value than 3 to show up above header and summary rows */
|
||||
z-index: 3;
|
||||
@@ -374,7 +374,7 @@
|
||||
}
|
||||
|
||||
|
||||
.s8wc6fl7-0-0-beta-47 {
|
||||
.s8wc6fl7-0-0-beta-50 {
|
||||
@layer rdg.SummaryCell {
|
||||
inset-block-start: var(--rdg-summary-row-top);
|
||||
inset-block-end: var(--rdg-summary-row-bottom);
|
||||
@@ -382,42 +382,42 @@
|
||||
}
|
||||
|
||||
|
||||
.skuhp557-0-0-beta-47 {
|
||||
.skuhp557-0-0-beta-50 {
|
||||
@layer rdg.SummaryRow {
|
||||
> .cj343x07-0-0-beta-47 {
|
||||
> .cj343x07-0-0-beta-50 {
|
||||
position: sticky;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tf8l5ub7-0-0-beta-47 {
|
||||
.tf8l5ub7-0-0-beta-50 {
|
||||
@layer rdg.SummaryRow {
|
||||
> .cj343x07-0-0-beta-47 {
|
||||
> .cj343x07-0-0-beta-50 {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
> .csofj7r7-0-0-beta-47 {
|
||||
> .csofj7r7-0-0-beta-50 {
|
||||
z-index: 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.g1yxluv37-0-0-beta-47 {
|
||||
.g1yxluv37-0-0-beta-50 {
|
||||
@layer rdg.GroupedRow {
|
||||
&:not([aria-selected='true']) {
|
||||
background-color: var(--rdg-header-background-color);
|
||||
}
|
||||
|
||||
> .cj343x07-0-0-beta-47:not(:last-child, .csofj7r7-0-0-beta-47),
|
||||
> :nth-last-child(n + 2 of .csofj7r7-0-0-beta-47) {
|
||||
> .cj343x07-0-0-beta-50:not(:last-child, .csofj7r7-0-0-beta-50),
|
||||
> :nth-last-child(n + 2 of .csofj7r7-0-0-beta-50) {
|
||||
border-inline-end: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.t7vyx3i7-0-0-beta-47 {
|
||||
.t7vyx3i7-0-0-beta-50 {
|
||||
@layer rdg.TextEditor {
|
||||
appearance: none;
|
||||
|
||||
@@ -446,3 +446,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*# sourceMappingURL=styles.css.map*/
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "react-data-grid",
|
||||
"version": "7.0.0-beta.50",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./lib/styles.css": "./lib/styles.css",
|
||||
".": {
|
||||
"types": "./lib/index.d.ts",
|
||||
"default": "./lib/bundle.js"
|
||||
}
|
||||
},
|
||||
"browser": "./lib/bundle.js",
|
||||
"main": "./lib/bundle.js",
|
||||
"module": "./lib/bundle.js",
|
||||
"types": "./lib/index.d.ts",
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"sideEffects": [
|
||||
"**/*.css"
|
||||
],
|
||||
"dependencies": {
|
||||
"clsx": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.0",
|
||||
"react-dom": "^19.0"
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
"sideEffects": [],
|
||||
"exports": {
|
||||
".": "./lib/index.js",
|
||||
"./*.css": "./src/*.css",
|
||||
"./*": "./lib/*.js"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -32,8 +33,10 @@
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dbeaver/react-dnd": "workspace:^",
|
||||
"@dbeaver/ui-kit": "workspace:^",
|
||||
"react": "^19",
|
||||
"react-data-grid": "7.0.0-beta.47",
|
||||
"react-data-grid": "file:./artifacts",
|
||||
"react-dom": "^19"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { memo, use, useMemo } from 'react';
|
||||
import { Cell, type CellRendererProps } from 'react-data-grid';
|
||||
import { DataGridCellContext, type IDataGridCellRenderer } from './DataGridCellContext.js';
|
||||
import { createCellMouseEvent } from './eventUtils.js';
|
||||
import { DataGridCellInnerContext, type IDataGridCellInnerContext } from './DataGridCellInnerContext.js';
|
||||
import { useGridReactiveValue } from './useGridReactiveValue.js';
|
||||
import { HeaderDnDContext } from './useHeaderDnD.js';
|
||||
|
||||
export const BaseCell = memo(function BaseCell<TRow, TSummaryRow>(props: CellRendererProps<TRow, TSummaryRow>) {
|
||||
const cellContext = use(DataGridCellContext);
|
||||
const dndContext = use(HeaderDnDContext)!;
|
||||
const virtualColIdx = props.column.idx;
|
||||
const dataColIdx = dndContext.getDataColIdx(props.column.idx);
|
||||
const rowIdx = props.rowIdx;
|
||||
const tooltip = useGridReactiveValue(cellContext?.cellTooltip, rowIdx, dataColIdx);
|
||||
|
||||
function handleClick(event: React.MouseEvent<HTMLDivElement>) {
|
||||
props.onClick?.(
|
||||
{
|
||||
rowIdx,
|
||||
row: props.row,
|
||||
column: props.column,
|
||||
selectCell(enableEditor) {
|
||||
props.selectCell({ rowIdx, idx: virtualColIdx }, enableEditor);
|
||||
},
|
||||
},
|
||||
createCellMouseEvent(event),
|
||||
);
|
||||
}
|
||||
|
||||
function handleDoubleClick(event: React.MouseEvent<HTMLDivElement>) {
|
||||
props.onDoubleClick?.(
|
||||
{
|
||||
rowIdx,
|
||||
row: props.row,
|
||||
column: props.column,
|
||||
selectCell(enableEditor) {
|
||||
props.selectCell({ rowIdx, idx: virtualColIdx }, enableEditor);
|
||||
},
|
||||
},
|
||||
createCellMouseEvent(event),
|
||||
);
|
||||
}
|
||||
|
||||
function handleContextMenu(event: React.MouseEvent<HTMLDivElement>) {
|
||||
props.onContextMenu?.(
|
||||
{
|
||||
rowIdx,
|
||||
row: props.row,
|
||||
column: props.column,
|
||||
selectCell(enableEditor) {
|
||||
props.selectCell({ rowIdx, idx: virtualColIdx }, enableEditor);
|
||||
},
|
||||
},
|
||||
createCellMouseEvent(event),
|
||||
);
|
||||
}
|
||||
|
||||
const mappedProps = useMemo(
|
||||
() => ({ ...props, isFocused: props.isCellSelected, onClick: handleClick, onDoubleClick: handleDoubleClick, onContextMenu: handleContextMenu }),
|
||||
Object.values(props),
|
||||
);
|
||||
|
||||
const renderDefaultCell = useMemo<IDataGridCellRenderer>(
|
||||
() =>
|
||||
({ onClick, onDoubleClick, onContextMenu, ...rest }) => {
|
||||
return (
|
||||
<Cell
|
||||
{...props}
|
||||
title={tooltip}
|
||||
onClick={(_, event) => onClick?.(event)}
|
||||
onDoubleClick={(_, event) => onDoubleClick?.(event)}
|
||||
onContextMenu={(_, event) => onContextMenu?.(event)}
|
||||
{...rest}
|
||||
isCellSelected={props.isCellSelected || rest.isFocused || false}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[...Object.values(props), tooltip],
|
||||
);
|
||||
|
||||
const cellElement = useGridReactiveValue(cellContext?.cellElement, rowIdx, dataColIdx, mappedProps, renderDefaultCell);
|
||||
|
||||
const innerCellContext = useMemo<IDataGridCellInnerContext>(() => ({ isFocused: props.isCellSelected }), [props.isCellSelected]);
|
||||
return <DataGridCellInnerContext value={innerCellContext}>{cellElement ?? <Cell title={tooltip} {...props} />}</DataGridCellInnerContext>;
|
||||
}) as <TRow, TSummaryRow>(props: CellRendererProps<TRow, TSummaryRow>) => React.ReactNode;
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Row, type RenderRowProps } from 'react-data-grid';
|
||||
import { DataGridRowContext } from './DataGridRowContext.js';
|
||||
import { memo, use, useEffect } from 'react';
|
||||
import { useGridReactiveValue } from './useGridReactiveValue.js';
|
||||
|
||||
export const BaseRow = memo(function BaseRow<TRow, TSummaryRow>(props: RenderRowProps<TRow, TSummaryRow>) {
|
||||
const rowContext = use(DataGridRowContext);
|
||||
const rowsCount = useGridReactiveValue(rowContext?.onScrollToBottom ? rowContext?.rowCount : undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (rowsCount === props.rowIdx + 1) {
|
||||
rowContext?.onScrollToBottom?.();
|
||||
}
|
||||
}, [rowsCount]);
|
||||
return <Row {...props} />;
|
||||
}) as <TRow, TSummaryRow>(props: RenderRowProps<TRow, TSummaryRow>) => React.ReactNode;
|
||||
@@ -0,0 +1,33 @@
|
||||
@import url('@dbeaver/ui-kit/index.css');
|
||||
@import url('react-data-grid/lib/styles.css') layer(components);
|
||||
|
||||
@layer components.rdg.MeasuringCell {
|
||||
[data-measuring-cell] {
|
||||
max-width: var(--rdg-max-auto-size, 350px);
|
||||
}
|
||||
}
|
||||
|
||||
@layer components.rdg.Cell {
|
||||
.rdg {
|
||||
--dbv-kit-icon-btn-outline-width: 1px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.rdg-cell-frozen {
|
||||
&:nth-last-child(1 of &) {
|
||||
box-shadow: none;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
background-color: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +1,172 @@
|
||||
// import { useSyncExternalStore } from 'react';
|
||||
import DataGridBase, { type ColumnOrColumnGroup } from 'react-data-grid';
|
||||
import 'react-data-grid/lib/styles.css';
|
||||
import { forwardRef, useImperativeHandle, useMemo, useRef } from 'react';
|
||||
import { DataGrid as DataGridBase, type ColumnOrColumnGroup, type CellSelectArgs, type DataGridHandle } from 'react-data-grid';
|
||||
import { rowRenderer } from './renderers/rowRenderer.js';
|
||||
import { cellRenderer } from './renderers/cellRenderer.js';
|
||||
import { DataGridCellHeaderContext, type IDataGridHeaderCellContext } from './DataGridHeaderCellContext.js';
|
||||
import { DataGridCellContext, type IDataGridCellContext } from './DataGridCellContext.js';
|
||||
import type { IInnerRow } from './IInnerRow.js';
|
||||
import type { IGridReactiveValue } from './IGridReactiveValue.js';
|
||||
import { useGridReactiveValue } from './useGridReactiveValue.js';
|
||||
import { mapCellContentRenderer } from './mapCellContentRenderer.js';
|
||||
import { mapRenderHeaderCell } from './mapRenderHeaderCell.js';
|
||||
import { mapEditCellRenderer } from './mapEditCellRenderer.js';
|
||||
import { DataGridRowContext, type IDataGridRowContext } from './DataGridRowContext.js';
|
||||
import './DataGrid.css';
|
||||
import { HeaderDnDContext, useHeaderDnD } from './useHeaderDnD.js';
|
||||
|
||||
export interface DataGridProps {
|
||||
getHeaderHeight?: () => number;
|
||||
export interface ICellPosition {
|
||||
rowIdx: number;
|
||||
colIdx: number;
|
||||
}
|
||||
|
||||
export interface DataGridCellKeyboardEvent extends React.KeyboardEvent<HTMLDivElement> {
|
||||
preventGridDefault: () => void;
|
||||
isGridDefaultPrevented: () => boolean;
|
||||
}
|
||||
|
||||
export interface DataGridProps extends IDataGridCellContext, IDataGridRowContext, IDataGridHeaderCellContext, React.PropsWithChildren {
|
||||
getRowHeight?: (rowIdx: number) => number;
|
||||
getHeaderText: (colIdx: number) => string;
|
||||
getCellText: (rowIdx: number, colIdx: number) => string;
|
||||
getColumnCount: () => number;
|
||||
getRowCount: () => number;
|
||||
subscribeToStore: (onStoreChange: () => void) => () => void;
|
||||
getRowId?: (rowIdx: number) => React.Key;
|
||||
columnCount: IGridReactiveValue<number, []>;
|
||||
getColumnKey?: (colIdx: number) => string;
|
||||
onScroll?: (event: React.UIEvent<HTMLDivElement>) => void;
|
||||
onFocus?: (position: ICellPosition) => void;
|
||||
onEditorOpen?: (position: ICellPosition) => void;
|
||||
onCellKeyDown?: (position: ICellPosition, event: DataGridCellKeyboardEvent) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const DataGrid: React.FC<DataGridProps> = function DataGrid({
|
||||
getHeaderText,
|
||||
getCellText,
|
||||
getColumnCount,
|
||||
getRowCount,
|
||||
getHeaderHeight,
|
||||
getRowHeight,
|
||||
subscribeToStore,
|
||||
className,
|
||||
}) {
|
||||
// const store = useSyncExternalStore(subscribeToStore, () => {
|
||||
// return {
|
||||
// columns: getColumnCount(),
|
||||
// rows: getRowCount(),
|
||||
// };
|
||||
// });
|
||||
export interface DataGridRef {
|
||||
selectCell: (position: ICellPosition) => void;
|
||||
scrollToCell: (position: Partial<ICellPosition>) => void;
|
||||
openEditor: (position: ICellPosition) => void;
|
||||
}
|
||||
|
||||
const columns = new Array<ColumnOrColumnGroup<any, unknown>>(getColumnCount()).fill(null as any).map(
|
||||
(_, i) =>
|
||||
({
|
||||
key: i + '',
|
||||
name: getHeaderText(i),
|
||||
renderCell: ({ rowIdx }) => getCellText(rowIdx, i),
|
||||
}) as ColumnOrColumnGroup<any, unknown>,
|
||||
export const DataGrid = forwardRef<DataGridRef, DataGridProps>(function DataGrid(
|
||||
{
|
||||
headerElement,
|
||||
getHeaderWidth,
|
||||
headerText,
|
||||
getHeaderOrder,
|
||||
getHeaderResizable,
|
||||
columnSortable,
|
||||
getHeaderHeight,
|
||||
getHeaderPinned,
|
||||
columnSortingState,
|
||||
getHeaderDnD,
|
||||
cell,
|
||||
cellText,
|
||||
cellElement,
|
||||
cellTooltip,
|
||||
getCellEditable,
|
||||
columnCount,
|
||||
getColumnKey,
|
||||
rowCount,
|
||||
getRowId,
|
||||
getRowHeight,
|
||||
onHeaderReorder,
|
||||
onScroll,
|
||||
onScrollToBottom,
|
||||
onFocus,
|
||||
onCellChange,
|
||||
onColumnSort,
|
||||
children,
|
||||
className,
|
||||
onCellKeyDown,
|
||||
},
|
||||
ref,
|
||||
) {
|
||||
const rowsCount = useGridReactiveValue(rowCount);
|
||||
const columnsCount = useGridReactiveValue(columnCount);
|
||||
|
||||
const gridKey = useRef(0);
|
||||
const rowsCountRef = useRef(rowsCount);
|
||||
const innerGridRef = useRef<DataGridHandle>(null);
|
||||
const columns = new Array<ColumnOrColumnGroup<IInnerRow, unknown>>(columnsCount)
|
||||
.fill(null as any)
|
||||
.map((_, i): ColumnOrColumnGroup<IInnerRow, unknown> => {
|
||||
const width = getHeaderWidth?.(i) ?? 'max-content';
|
||||
return {
|
||||
key: getColumnKey?.(i) ?? String(i),
|
||||
name: '',
|
||||
resizable: getHeaderResizable?.(i) ?? true,
|
||||
width,
|
||||
minWidth: 26,
|
||||
editable: row => getCellEditable?.(row.idx, i) ?? false,
|
||||
frozen: getHeaderPinned?.(i),
|
||||
renderHeaderCell: mapRenderHeaderCell(i),
|
||||
renderCell: mapCellContentRenderer(i),
|
||||
renderEditCell: mapEditCellRenderer(i),
|
||||
};
|
||||
});
|
||||
|
||||
const dndHeaderContext = useHeaderDnD({ columns, onReorder: onHeaderReorder, getCanDrag: getHeaderDnD, getHeaderOrder });
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
selectCell: (position: ICellPosition) => {
|
||||
innerGridRef.current?.selectCell({ idx: dndHeaderContext.getDataColIdx(position.colIdx), rowIdx: position.rowIdx });
|
||||
},
|
||||
scrollToCell: (position: Partial<ICellPosition>) => {
|
||||
innerGridRef.current?.scrollToCell({ idx: position.colIdx && dndHeaderContext.getDataColIdx(position.colIdx), rowIdx: position.rowIdx });
|
||||
},
|
||||
openEditor: (position: ICellPosition) => {
|
||||
innerGridRef.current?.selectCell({ idx: dndHeaderContext.getDataColIdx(position.colIdx), rowIdx: position.rowIdx }, true);
|
||||
},
|
||||
}));
|
||||
|
||||
if (rowsCountRef.current !== rowsCount) {
|
||||
const previousRowCount = rowsCountRef.current;
|
||||
rowsCountRef.current = rowsCount;
|
||||
|
||||
if (previousRowCount === 0) {
|
||||
// we trigger columns size recalculation when rows are added
|
||||
gridKey.current++;
|
||||
}
|
||||
}
|
||||
|
||||
const rows = useMemo(
|
||||
() =>
|
||||
new Array<IInnerRow>(rowsCount).fill({ idx: 0 }).map((_, i) => ({
|
||||
idx: i,
|
||||
})),
|
||||
[rowsCount],
|
||||
);
|
||||
|
||||
const rows = new Array(getRowCount()).fill({});
|
||||
function handleCellFocus(args: CellSelectArgs<IInnerRow, unknown>) {
|
||||
onFocus?.({ colIdx: dndHeaderContext.getDataColIdx(args.column.idx), rowIdx: args.rowIdx });
|
||||
}
|
||||
|
||||
return <DataGridBase columns={columns} rows={rows} className={className} headerRowHeight={getHeaderHeight?.()} rowHeight={getRowHeight} />;
|
||||
};
|
||||
function handleCellKeyDown(args: CellSelectArgs<IInnerRow, unknown>, event: DataGridCellKeyboardEvent) {
|
||||
onCellKeyDown?.({ colIdx: dndHeaderContext.getDataColIdx(args.column.idx), rowIdx: args.rowIdx }, event);
|
||||
}
|
||||
|
||||
return (
|
||||
<HeaderDnDContext value={dndHeaderContext}>
|
||||
<DataGridRowContext value={{ rowCount, onScrollToBottom }}>
|
||||
<DataGridCellContext value={{ cell, cellText, cellElement, cellTooltip, onCellChange }}>
|
||||
<DataGridCellHeaderContext value={{ headerElement, headerText, getHeaderDnD, columnSortable, onColumnSort, columnSortingState }}>
|
||||
<DataGridBase
|
||||
key={gridKey.current}
|
||||
ref={innerGridRef}
|
||||
columns={dndHeaderContext.columns}
|
||||
rows={rows}
|
||||
className={className}
|
||||
headerRowHeight={getHeaderHeight?.()}
|
||||
onScroll={onScroll}
|
||||
rowHeight={getRowHeight ? row => getRowHeight(row.idx) : undefined}
|
||||
rowKeyGetter={getRowId ? row => getRowId(row.idx) : undefined}
|
||||
onSelectedCellChange={handleCellFocus}
|
||||
onCellKeyDown={handleCellKeyDown}
|
||||
renderers={{
|
||||
renderRow: rowRenderer,
|
||||
renderCell: cellRenderer,
|
||||
noRowsFallback: children,
|
||||
}}
|
||||
minimumRowsToRender={100}
|
||||
/>
|
||||
</DataGridCellHeaderContext>
|
||||
</DataGridCellContext>
|
||||
</DataGridRowContext>
|
||||
</HeaderDnDContext>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { createContext } from 'react';
|
||||
import type { IGridReactiveValue } from './IGridReactiveValue.js';
|
||||
|
||||
export interface IDataGridCellProps extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
|
||||
isFocused: boolean;
|
||||
}
|
||||
|
||||
export interface IDataGridCellRenderer {
|
||||
(propsOverride: Partial<IDataGridCellProps> & { [key: string]: any }): React.ReactNode;
|
||||
}
|
||||
|
||||
export interface IDataGridCellContext {
|
||||
cellElement?: IGridReactiveValue<
|
||||
React.ReactNode,
|
||||
[rowIdx: number, colIdx: number, props: IDataGridCellProps, renderDefaultCell: IDataGridCellRenderer]
|
||||
>;
|
||||
cell?: IGridReactiveValue<React.ReactNode, [rowIdx: number, colIdx: number]>;
|
||||
cellText?: IGridReactiveValue<string, [rowIdx: number, colIdx: number]>;
|
||||
cellTooltip?: IGridReactiveValue<string, [rowIdx: number, colIdx: number]>;
|
||||
getCellEditable?: (rowIdx: number, colIdx: number) => boolean;
|
||||
onCellChange?: (rowIdx: number, colIdx: number, value: any) => void;
|
||||
}
|
||||
|
||||
export const DataGridCellContext = createContext<IDataGridCellContext | null>(null);
|
||||
@@ -0,0 +1,7 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export interface IDataGridCellInnerContext {
|
||||
isFocused: boolean;
|
||||
}
|
||||
|
||||
export const DataGridCellInnerContext = createContext<IDataGridCellInnerContext | null>(null);
|
||||
@@ -0,0 +1,19 @@
|
||||
import { createContext } from 'react';
|
||||
import type { IGridReactiveValue } from './IGridReactiveValue.js';
|
||||
|
||||
export interface IDataGridHeaderCellContext {
|
||||
headerElement?: IGridReactiveValue<React.ReactNode, [colIdx: number]>;
|
||||
headerText?: IGridReactiveValue<string, [colIdx: number]>;
|
||||
getHeaderOrder?: () => number[];
|
||||
getHeaderWidth?: (colIdx: number) => number | string | null;
|
||||
getHeaderResizable?: (colIdx: number) => boolean;
|
||||
getHeaderHeight?: () => number;
|
||||
getHeaderPinned?: (colIdx: number) => boolean;
|
||||
getHeaderDnD?: (colIdx: number) => boolean;
|
||||
onHeaderReorder?: (from: number, to: number) => void;
|
||||
columnSortingState?: IGridReactiveValue<'asc' | 'desc' | undefined | null, [colIdx: number]>;
|
||||
columnSortable?: IGridReactiveValue<boolean, [colIdx: number]>;
|
||||
onColumnSort?: (colIdx: number, order: 'asc' | 'desc' | null, isMultiple: boolean) => void;
|
||||
}
|
||||
|
||||
export const DataGridCellHeaderContext = createContext<IDataGridHeaderCellContext | null>(null);
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createContext } from 'react';
|
||||
import type { IGridReactiveValue } from './IGridReactiveValue.js';
|
||||
|
||||
export interface IDataGridRowContext {
|
||||
rowCount: IGridReactiveValue<number, []>;
|
||||
onScrollToBottom?: () => void;
|
||||
}
|
||||
|
||||
export const DataGridRowContext = createContext<IDataGridRowContext | null>(null);
|
||||
@@ -0,0 +1,6 @@
|
||||
export type IGridReactiveValueSubscribe<TArgs extends any[]> = (onValueChange: () => void, ...args: TArgs) => () => void;
|
||||
|
||||
export interface IGridReactiveValue<T, TArgs extends any[]> {
|
||||
get(...args: TArgs): T;
|
||||
subscribe: IGridReactiveValueSubscribe<TArgs>;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface IInnerRow {
|
||||
idx: number;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
:global(.rdg-cell) .editor {
|
||||
appearance: none;
|
||||
|
||||
box-sizing: border-box;
|
||||
min-height: initial;
|
||||
inline-size: 100%;
|
||||
block-size: 100%;
|
||||
padding-block: 0;
|
||||
padding-inline: 6px;
|
||||
border: 2px solid #ccc;
|
||||
vertical-align: top;
|
||||
color: var(--rdg-color);
|
||||
background-color: var(--rdg-background-color);
|
||||
|
||||
font: inherit;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
font-weight: inherit;
|
||||
letter-spacing: inherit;
|
||||
text-decoration: inherit;
|
||||
text-transform: inherit;
|
||||
|
||||
&:focus {
|
||||
border-color: var(--rdg-selection-color);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #999;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { use } from 'react';
|
||||
import classes from './TextEditor.module.css';
|
||||
import { DataGridCellContext } from '../DataGridCellContext.js';
|
||||
import { useGridReactiveValue } from '../useGridReactiveValue.js';
|
||||
|
||||
function autoFocusAndSelect(input: HTMLInputElement | null) {
|
||||
input?.focus();
|
||||
input?.select();
|
||||
}
|
||||
|
||||
export interface IProps {
|
||||
rowIdx: number;
|
||||
colIdx: number;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function TextEditor({ rowIdx, colIdx, onClose }: IProps) {
|
||||
const cellContext = use(DataGridCellContext);
|
||||
|
||||
const value = useGridReactiveValue(cellContext?.cellText, rowIdx, colIdx) ?? '';
|
||||
|
||||
return (
|
||||
<input
|
||||
className={classes['editor']}
|
||||
ref={autoFocusAndSelect}
|
||||
value={value}
|
||||
onChange={event => cellContext?.onCellChange?.(rowIdx, colIdx, event.target.value)}
|
||||
onBlur={() => onClose()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import type { CellMouseEvent } from 'react-data-grid';
|
||||
|
||||
export function createCellMouseEvent<E extends React.MouseEvent<HTMLDivElement>>(event: E): CellMouseEvent {
|
||||
let defaultPrevented = false;
|
||||
const cellEvent = {
|
||||
...event,
|
||||
preventGridDefault() {
|
||||
defaultPrevented = true;
|
||||
},
|
||||
isGridDefaultPrevented() {
|
||||
return defaultPrevented;
|
||||
},
|
||||
};
|
||||
|
||||
Object.setPrototypeOf(cellEvent, Object.getPrototypeOf(event));
|
||||
|
||||
return cellEvent;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { clsx } from '@dbeaver/ui-kit';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
onClick?: React.MouseEventHandler<HTMLElement>;
|
||||
}
|
||||
|
||||
export function BlobFormatter({ className, onClick }: Props) {
|
||||
return (
|
||||
<span className={clsx('tw:uppercase', className)} onClick={onClick}>
|
||||
[blob]
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { NullFormatter } from './NullFormatter.js';
|
||||
import { Checkbox, clsx, Focusable } from '@dbeaver/ui-kit';
|
||||
|
||||
interface Props {
|
||||
value: boolean | null;
|
||||
className?: string;
|
||||
onClick?: React.MouseEventHandler<HTMLElement>;
|
||||
onKeyDown?: React.KeyboardEventHandler<HTMLElement>;
|
||||
}
|
||||
|
||||
export function BooleanFormatter({ value, className, onClick, onKeyDown }: Props) {
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
|
||||
if (event.code === 'Enter' || event.code === 'Space') {
|
||||
event.preventDefault();
|
||||
onKeyDown?.(event);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Focusable
|
||||
className={clsx('tw:flex tw:items-center tw:outline-none tw:hover:cursor-pointer', className)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onClick={onClick}
|
||||
>
|
||||
{value === null ? <NullFormatter /> : <Checkbox className="tw:data-disabled:opacity-100!" disabled size="small" checked={value} />}
|
||||
</Focusable>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { NullFormatter } from './NullFormatter.js';
|
||||
|
||||
interface Props {
|
||||
value: Date | null;
|
||||
type?: 'time' | 'date' | 'datetime';
|
||||
onClick?: React.MouseEventHandler<HTMLElement>;
|
||||
}
|
||||
|
||||
export function DateFormatter({ value, type = 'datetime', onClick }: Props) {
|
||||
if (value === null) {
|
||||
return <NullFormatter />;
|
||||
}
|
||||
return (
|
||||
<div onClick={onClick}>
|
||||
{value.toLocaleString(undefined, {
|
||||
dateStyle: type === 'date' ? 'long' : undefined,
|
||||
timeStyle: type === 'time' ? 'long' : undefined,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { clsx } from '@dbeaver/ui-kit';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
onClick?: React.MouseEventHandler<HTMLElement>;
|
||||
}
|
||||
|
||||
export function NullFormatter({ className, onClick }: Props) {
|
||||
return (
|
||||
<span className={clsx('tw:uppercase', 'tw:opacity-65', className)} onClick={onClick}>
|
||||
[NULL]
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { NullFormatter } from './NullFormatter.js';
|
||||
|
||||
interface Props {
|
||||
value: number | null;
|
||||
className?: string;
|
||||
onClick?: React.MouseEventHandler<HTMLElement>;
|
||||
}
|
||||
|
||||
export function NumberFormatter({ value, className, onClick }: Props) {
|
||||
if (value === null) {
|
||||
return <NullFormatter className={className} onClick={onClick} />;
|
||||
}
|
||||
return (
|
||||
<div style={{ textAlign: 'right' }} className={className} onClick={onClick}>
|
||||
{value.toLocaleString()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1 +1,13 @@
|
||||
export * from './DataGrid.js';
|
||||
export * from './DataGridCellContext.js';
|
||||
export * from './DataGridCellInnerContext.js';
|
||||
export * from './IGridReactiveValue.js';
|
||||
export * from './useCreateGridReactiveValue.js';
|
||||
export * from './useGridReactiveValue.js';
|
||||
export * from './reorderArray.js';
|
||||
|
||||
export * from './formatters/NullFormatter.js';
|
||||
export * from './formatters/BooleanFormatter.js';
|
||||
export * from './formatters/NumberFormatter.js';
|
||||
export * from './formatters/DateFormatter.js';
|
||||
export * from './formatters/BlobFormatter.js';
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { RenderCellProps } from 'react-data-grid';
|
||||
import type { IInnerRow } from './IInnerRow.js';
|
||||
import { CellContentRenderer } from './renderers/CellContentRenderer.js';
|
||||
|
||||
export function mapCellContentRenderer(colIdx: number) {
|
||||
return function RenderCell({ rowIdx }: RenderCellProps<IInnerRow, unknown>) {
|
||||
return <CellContentRenderer rowIdx={rowIdx} colIdx={colIdx} />;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { RenderEditCellProps } from 'react-data-grid';
|
||||
import type { IInnerRow } from './IInnerRow.js';
|
||||
import { TextEditor } from './editors/TextEditor.js';
|
||||
|
||||
export function mapEditCellRenderer(colIdx: number) {
|
||||
return function RenderEditCell({ rowIdx, onClose }: RenderEditCellProps<IInnerRow, unknown>) {
|
||||
return <TextEditor rowIdx={rowIdx} colIdx={colIdx} onClose={() => onClose()} />;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { RenderHeaderCellProps } from 'react-data-grid';
|
||||
import type { IInnerRow } from './IInnerRow.js';
|
||||
import { HeaderCellContentRenderer } from './renderers/HeaderCellContentRenderer.js';
|
||||
|
||||
export function mapRenderHeaderCell(colIdx: number) {
|
||||
return function RenderHeaderCell({ tabIndex }: RenderHeaderCellProps<IInnerRow, unknown>) {
|
||||
return <HeaderCellContentRenderer colIdx={colIdx} tabIndex={tabIndex} />;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { use } from 'react';
|
||||
import { DataGridCellContext } from '../DataGridCellContext.js';
|
||||
import { useGridReactiveValue } from '../useGridReactiveValue.js';
|
||||
|
||||
export interface Props {
|
||||
rowIdx: number;
|
||||
colIdx: number;
|
||||
}
|
||||
|
||||
export function CellContentRenderer({ rowIdx, colIdx }: Props) {
|
||||
const cellContext = use(DataGridCellContext);
|
||||
return useGridReactiveValue(cellContext?.cell, rowIdx, colIdx);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { memo, use, useRef } from 'react';
|
||||
import { useDrag, useDrop, type DnDStoreProvider } from '@dbeaver/react-dnd';
|
||||
import { DataGridCellHeaderContext } from '../DataGridHeaderCellContext.js';
|
||||
import { useGridReactiveValue } from '../useGridReactiveValue.js';
|
||||
import { HeaderDnDContext } from '../useHeaderDnD.js';
|
||||
import { OrderButton } from './OrderButton.js';
|
||||
|
||||
interface Props {
|
||||
colIdx: number;
|
||||
tabIndex?: number;
|
||||
}
|
||||
export const HeaderCellContentRenderer = memo(function HeaderCellContentRenderer({ colIdx, tabIndex }: Props) {
|
||||
const dndHeaderContext = use(HeaderDnDContext);
|
||||
const cellHeaderContext = use(DataGridCellHeaderContext);
|
||||
const headerElement = useGridReactiveValue(cellHeaderContext?.headerElement, colIdx);
|
||||
const getHeaderText = useGridReactiveValue(headerElement ? undefined : cellHeaderContext?.headerText, colIdx);
|
||||
const isColumnSortable = useGridReactiveValue(cellHeaderContext?.columnSortable, colIdx);
|
||||
const onColumnSort = cellHeaderContext?.onColumnSort;
|
||||
const sortingState = useGridReactiveValue(cellHeaderContext?.columnSortingState, colIdx);
|
||||
|
||||
const orderButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const draggable = dndHeaderContext?.getCanDrag?.(colIdx) ?? false;
|
||||
const drag = useDrag({
|
||||
draggable,
|
||||
onDragStart(event, store) {
|
||||
if (dndHeaderContext?.id) {
|
||||
store.setData('dbeaver-react-data-grid-id', dndHeaderContext?.id);
|
||||
}
|
||||
store.setData('dbeaver-react-data-grid-col-idx', colIdx);
|
||||
},
|
||||
onDragEnd() {
|
||||
dndHeaderContext?.onDragEnd(colIdx);
|
||||
},
|
||||
});
|
||||
|
||||
function canDrop(event: React.DragEvent<HTMLElement>, store: DnDStoreProvider | null) {
|
||||
const gridId = store?.getData('dbeaver-react-data-grid-id');
|
||||
return gridId === dndHeaderContext?.id;
|
||||
}
|
||||
const drop = useDrop({
|
||||
canDrop,
|
||||
onDragOver(event, store) {
|
||||
if (!canDrop(event, store)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dragColIdx = store?.getData('dbeaver-react-data-grid-col-idx');
|
||||
const position = event.clientX - event.currentTarget.getBoundingClientRect().left;
|
||||
const width = event.currentTarget.offsetWidth;
|
||||
const isAfter = position > width / 2;
|
||||
|
||||
dndHeaderContext?.onDragOver(dragColIdx, colIdx, isAfter);
|
||||
},
|
||||
});
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Tab' && !e.shiftKey && isColumnSortable && onColumnSort && orderButtonRef.current !== document.activeElement) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
containerRef.current?.parentElement?.setAttribute('aria-selected', 'false');
|
||||
orderButtonRef.current?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
tabIndex={tabIndex}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="tw:w-full tw:h-full tw:content-center tw:flex tw:items-center tw:justify-between tw:gap-1 tw:outline-none tw:group"
|
||||
{...drag.props}
|
||||
{...drop.props}
|
||||
>
|
||||
<span className="tw:overflow-hidden tw:text-ellipsis">{headerElement ?? getHeaderText ?? ''}</span>
|
||||
{isColumnSortable && onColumnSort && <OrderButton ref={orderButtonRef} colIdx={colIdx} sortState={sortingState} onSort={onColumnSort} />}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { clsx, IconButton } from '@dbeaver/ui-kit';
|
||||
|
||||
interface OrderButtonProps {
|
||||
colIdx: number;
|
||||
sortState?: 'asc' | 'desc' | null;
|
||||
onSort: (attributePosition: number, order: 'asc' | 'desc' | null, isMultiple: boolean) => void;
|
||||
tabIndex?: number;
|
||||
ref: React.Ref<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
export function OrderButton({ colIdx, sortState, onSort, tabIndex, ref }: OrderButtonProps) {
|
||||
function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
|
||||
const nextSortState = sortState === 'asc' ? 'desc' : sortState === 'desc' ? null : 'asc';
|
||||
onSort(colIdx, nextSortState, e.ctrlKey || e.metaKey);
|
||||
}
|
||||
|
||||
const svgSortAsc = '#icon-sort-asc';
|
||||
const svgSortDesc = '#icon-sort-desc';
|
||||
const svgSortUnknown = '#icon-sort-unknown';
|
||||
|
||||
const iconSrc = sortState === 'asc' ? svgSortAsc : sortState === 'desc' ? svgSortDesc : svgSortUnknown;
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={handleClick}
|
||||
tabIndex={tabIndex}
|
||||
ref={ref}
|
||||
title="Sort by column"
|
||||
aria-label="Sort by column"
|
||||
className={clsx(
|
||||
'tw:opacity-0 tw:group-focus:opacity-100 tw:focus:opacity-100 tw:group-hover:opacity-100 tw:hover:opacity-100 tw:outline-offset-0',
|
||||
sortState && 'tw:opacity-100',
|
||||
)}
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16">
|
||||
<use href={iconSrc} />
|
||||
</svg>
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { CellRendererProps } from 'react-data-grid';
|
||||
import { BaseCell } from '../BaseCell.js';
|
||||
|
||||
export function cellRenderer<TRow, TSummaryRow>(key: React.Key, props: CellRendererProps<TRow, TSummaryRow>) {
|
||||
return <BaseCell key={key} {...props} />;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { RenderRowProps } from 'react-data-grid';
|
||||
import { BaseRow } from '../BaseRow.js';
|
||||
|
||||
export function rowRenderer<TRow, TSummaryRow>(key: React.Key, props: RenderRowProps<TRow, TSummaryRow>) {
|
||||
return <BaseRow key={key} {...props} />;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export function reorderArray<T>(array: T[], from: number, to: number): T[] {
|
||||
const newArray = [...array];
|
||||
if (from < to) {
|
||||
newArray.splice(to, 0, array[from]!);
|
||||
newArray.splice(from, 1);
|
||||
} else {
|
||||
newArray.splice(from, 1);
|
||||
newArray.splice(to, 0, array[from]!);
|
||||
}
|
||||
return newArray;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useMemo } from 'react';
|
||||
import type { IGridReactiveValue, IGridReactiveValueSubscribe } from './IGridReactiveValue.js';
|
||||
|
||||
type NoInfer<T> = intrinsic;
|
||||
export function useCreateGridReactiveValue<T, TArgs extends any[]>(
|
||||
get: (...args: TArgs) => T,
|
||||
subscribe: NoInfer<IGridReactiveValueSubscribe<TArgs>> | null,
|
||||
deps: React.DependencyList,
|
||||
): IGridReactiveValue<T, TArgs> {
|
||||
const value = useMemo<IGridReactiveValue<T, TArgs>>(
|
||||
() => ({
|
||||
get,
|
||||
subscribe: subscribe ?? (() => () => {}),
|
||||
}),
|
||||
deps,
|
||||
);
|
||||
|
||||
return value;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useMemo, useSyncExternalStore } from 'react';
|
||||
import type { IGridReactiveValue } from './IGridReactiveValue.js';
|
||||
|
||||
export function useGridReactiveValue<P extends IGridReactiveValue<any, any[]> | undefined>(
|
||||
value: P,
|
||||
...args: P extends IGridReactiveValue<any, infer T> ? T : never[]
|
||||
): P extends IGridReactiveValue<infer T, any[]> ? T : undefined {
|
||||
const mappedValue = useMemo(
|
||||
() => ({
|
||||
cache: null as any,
|
||||
cacheInvalidated: true,
|
||||
subscribe: (onValueChange: () => void) => {
|
||||
if (value) {
|
||||
return value.subscribe(
|
||||
() => {
|
||||
mappedValue.cacheInvalidated = true;
|
||||
return onValueChange();
|
||||
},
|
||||
...args,
|
||||
);
|
||||
}
|
||||
|
||||
return () => {};
|
||||
},
|
||||
get: () => {
|
||||
if (mappedValue.cacheInvalidated) {
|
||||
mappedValue.cache = value?.get(...args);
|
||||
mappedValue.cacheInvalidated = false;
|
||||
}
|
||||
return mappedValue.cache;
|
||||
},
|
||||
}),
|
||||
[value, ...args],
|
||||
);
|
||||
return useSyncExternalStore(mappedValue.subscribe, mappedValue.get);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import type { ColumnOrColumnGroup } from 'react-data-grid';
|
||||
import type { IInnerRow } from './IInnerRow.js';
|
||||
import { createContext, useId, useMemo, useState } from 'react';
|
||||
import { reorderArray } from './reorderArray.js';
|
||||
|
||||
interface IHeaderDnDOptions {
|
||||
columns: Array<ColumnOrColumnGroup<IInnerRow, unknown>>;
|
||||
getCanDrag?: (colIdx: number) => boolean;
|
||||
onReorder?: (from: number, to: number) => void;
|
||||
getHeaderOrder?: () => number[];
|
||||
}
|
||||
|
||||
interface IHeaderDnD {
|
||||
id: string;
|
||||
columns: Array<ColumnOrColumnGroup<IInnerRow, unknown>>;
|
||||
getDataColIdx: (virtualColIdx: number) => number;
|
||||
getVirtualColIdx: (dataColIdx: number) => number;
|
||||
getCanDrag?: (colIdx: number) => boolean;
|
||||
onDragOver: (dragColIdx: number, dropColIdx: number, isAfter: boolean) => void;
|
||||
onDragEnd: (colIdx: number) => void;
|
||||
}
|
||||
|
||||
export const HeaderDnDContext = createContext<IHeaderDnD | null>(null);
|
||||
|
||||
export function useHeaderDnD({ columns, getCanDrag, onReorder, getHeaderOrder }: IHeaderDnDOptions): IHeaderDnD {
|
||||
const id = useId();
|
||||
const [activeDnDElement, setActiveDnDElement] = useState<number | null>(null);
|
||||
const [dragOverElement, setDragOverElement] = useState<[number, boolean] | null>(null);
|
||||
|
||||
function getDataColIdx(virtualColIdx: number) {
|
||||
// Given a visible (ordered) column index, return the original data column index
|
||||
const order = getHeaderOrder?.();
|
||||
if (order) {
|
||||
return order[virtualColIdx] ?? virtualColIdx;
|
||||
}
|
||||
return virtualColIdx;
|
||||
}
|
||||
|
||||
function getVirtualColIdx(dataColIdx: number) {
|
||||
// Given the original data column index, return its visible (ordered) index
|
||||
const order = getHeaderOrder?.();
|
||||
if (order) {
|
||||
return order.indexOf(dataColIdx);
|
||||
}
|
||||
return dataColIdx;
|
||||
}
|
||||
|
||||
function onDragEnd(dragColIdx: number) {
|
||||
setActiveDnDElement(null);
|
||||
setDragOverElement(null);
|
||||
|
||||
if (dragOverElement) {
|
||||
const isAfter = dragOverElement?.[1];
|
||||
let to = dragOverElement?.[0];
|
||||
|
||||
if (isAfter) {
|
||||
to = getDataColIdx(getVirtualColIdx(to) + 1);
|
||||
}
|
||||
|
||||
onReorder?.(dragColIdx, to);
|
||||
}
|
||||
}
|
||||
|
||||
function onDragOver(dragColIdx: number, dropColIdx: number, isAfter: boolean) {
|
||||
setActiveDnDElement(dragColIdx);
|
||||
if (dragColIdx !== dropColIdx) {
|
||||
setDragOverElement([dropColIdx, isAfter]);
|
||||
}
|
||||
}
|
||||
|
||||
columns = useMemo(() => columns.map((col, idx) => columns[getDataColIdx(idx)]!), [columns, getHeaderOrder]);
|
||||
|
||||
if (activeDnDElement !== null && dragOverElement !== null) {
|
||||
const from = getVirtualColIdx(activeDnDElement);
|
||||
let to = getVirtualColIdx(dragOverElement[0]);
|
||||
const isAfter = dragOverElement[1];
|
||||
|
||||
if (isAfter) {
|
||||
to++;
|
||||
}
|
||||
|
||||
columns = reorderArray(columns, from, to);
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
columns,
|
||||
getDataColIdx,
|
||||
getVirtualColIdx,
|
||||
getCanDrag,
|
||||
onDragOver,
|
||||
onDragEnd,
|
||||
};
|
||||
}
|
||||
@@ -12,5 +12,13 @@
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../react-dnd"
|
||||
},
|
||||
{
|
||||
"path": "../ui-kit"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# @dbeaver/react-dnd
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "@dbeaver/react-dnd",
|
||||
"type": "module",
|
||||
"sideEffects": [],
|
||||
"exports": {
|
||||
".": "./lib/index.js",
|
||||
"./*": "./lib/*.js"
|
||||
},
|
||||
"scripts": {
|
||||
"clear": "rimraf lib"
|
||||
},
|
||||
"files": [
|
||||
"package.json",
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"CHANGELOG.md",
|
||||
"lib",
|
||||
"!lib/**/*.d.ts.map",
|
||||
"!lib/**/*.test.js",
|
||||
"!lib/**/*.test.d.ts",
|
||||
"!lib/**/*.test.d.ts.map",
|
||||
"!lib/**/*.test.js.map",
|
||||
"!lib/tests",
|
||||
"!.tsbuildinfo"
|
||||
],
|
||||
"packageManager": "yarn@4.6.0",
|
||||
"devDependencies": {
|
||||
"@dbeaver/tsconfig": "workspace:^",
|
||||
"@types/react": "^19",
|
||||
"rimraf": "^6",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './useDrag.js';
|
||||
export * from './useDrop.js';
|
||||
export { type DnDStoreProvider } from './store.js';
|
||||
@@ -0,0 +1,46 @@
|
||||
export const DND_STORE_PREFIX = 'application/dbeaver-react-dnd-';
|
||||
|
||||
export interface DnDStoreProvider {
|
||||
getData: (key: string) => any;
|
||||
setData: (key: string, value: any) => void;
|
||||
removeData: () => void;
|
||||
}
|
||||
|
||||
class DnDStore {
|
||||
private data: Map<string, Record<string, any>> = new Map();
|
||||
setData(id: string, key: string, value: any) {
|
||||
if (!this.data.has(id)) {
|
||||
this.data.set(id, {});
|
||||
}
|
||||
this.data.get(id)![key] = value;
|
||||
}
|
||||
getData(id: string, key: string) {
|
||||
return this.data.get(id)?.[key];
|
||||
}
|
||||
removeData(id: string) {
|
||||
this.data.delete(id);
|
||||
}
|
||||
|
||||
getProvider(id: string) {
|
||||
return {
|
||||
getData: (key: string) => this.getData(id, key),
|
||||
setData: (key: string, value: any) => {
|
||||
this.setData(id, key, value);
|
||||
},
|
||||
removeData: () => {
|
||||
this.removeData(id);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function getStoreProvider(event: React.DragEvent<HTMLElement>): DnDStoreProvider | null {
|
||||
const id = event.dataTransfer.types.find(type => type.startsWith(DND_STORE_PREFIX))?.slice(DND_STORE_PREFIX.length);
|
||||
|
||||
return id ? globalDnDStore.getProvider(id) : null;
|
||||
}
|
||||
|
||||
// TODO: do we want to use react context here?
|
||||
// the main idea was to use native DataTransfer, but it's available only for drag start and drop events
|
||||
// so we need to use a custom store
|
||||
export const globalDnDStore = new DnDStore();
|
||||
@@ -0,0 +1,67 @@
|
||||
import { useId, useMemo, useRef, useState } from 'react';
|
||||
import { DND_STORE_PREFIX, globalDnDStore, type DnDStoreProvider } from './store.js';
|
||||
|
||||
interface IDragOptions {
|
||||
draggable?: boolean;
|
||||
onDrag?: (event: React.DragEvent<HTMLElement>, provider: DnDStoreProvider) => void;
|
||||
onDragStart?: (event: React.DragEvent<HTMLElement>, provider: DnDStoreProvider) => void;
|
||||
onDragEnd?: (event: React.DragEvent<HTMLElement>, provider: DnDStoreProvider) => void;
|
||||
}
|
||||
interface IDrag {
|
||||
isDragging: boolean;
|
||||
props: {
|
||||
draggable?: boolean;
|
||||
onDrag: (event: React.DragEvent<HTMLElement>) => void;
|
||||
onDragStart: (event: React.DragEvent<HTMLElement>) => void;
|
||||
onDragEnd: (event: React.DragEvent<HTMLElement>) => void;
|
||||
};
|
||||
}
|
||||
|
||||
export function useDrag({ draggable, onDragStart, onDrag, onDragEnd }: IDragOptions = {}): IDrag {
|
||||
const id = useId();
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const optionsRef = useRef<IDragOptions>({ draggable, onDragStart, onDrag, onDragEnd });
|
||||
optionsRef.current = { draggable, onDragStart, onDrag, onDragEnd };
|
||||
|
||||
function handleDragStart(event: React.DragEvent<HTMLElement>) {
|
||||
if (!optionsRef.current.draggable) {
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
event.dataTransfer.setData(`${DND_STORE_PREFIX}${id}`, id);
|
||||
setDragging(true);
|
||||
optionsRef.current.onDragStart?.(event, globalDnDStore.getProvider(id));
|
||||
}
|
||||
|
||||
function handleDrag(event: React.DragEvent<HTMLElement>) {
|
||||
if (!optionsRef.current.draggable) {
|
||||
return;
|
||||
}
|
||||
optionsRef.current.onDrag?.(event, globalDnDStore.getProvider(id));
|
||||
}
|
||||
|
||||
function handleDragEnd(event: React.DragEvent<HTMLElement>) {
|
||||
if (!optionsRef.current.draggable) {
|
||||
return;
|
||||
}
|
||||
setDragging(false);
|
||||
optionsRef.current.onDragEnd?.(event, globalDnDStore.getProvider(id));
|
||||
globalDnDStore.removeData(id);
|
||||
}
|
||||
|
||||
return {
|
||||
isDragging: dragging,
|
||||
props: {
|
||||
draggable,
|
||||
...useMemo(
|
||||
() => ({
|
||||
onDrag: handleDrag,
|
||||
onDragStart: handleDragStart,
|
||||
onDragEnd: handleDragEnd,
|
||||
}),
|
||||
[],
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { getStoreProvider, type DnDStoreProvider } from './store.js';
|
||||
|
||||
interface IDropOptions {
|
||||
canDrop?: (event: React.DragEvent<HTMLElement>, provider: DnDStoreProvider | null) => boolean;
|
||||
onDrop?: (event: React.DragEvent<HTMLElement>, provider: DnDStoreProvider | null) => void;
|
||||
onDragOver?: (event: React.DragEvent<HTMLElement>, provider: DnDStoreProvider | null) => void;
|
||||
onDragEnter?: (event: React.DragEvent<HTMLElement>, provider: DnDStoreProvider | null) => void;
|
||||
onDragLeave?: (event: React.DragEvent<HTMLElement>, provider: DnDStoreProvider | null) => void;
|
||||
}
|
||||
|
||||
interface IDrop {
|
||||
props: {
|
||||
onDrop: (event: React.DragEvent<HTMLElement>) => void;
|
||||
onDragOver: (event: React.DragEvent<HTMLElement>) => void;
|
||||
onDragEnter: (event: React.DragEvent<HTMLElement>) => void;
|
||||
onDragLeave: (event: React.DragEvent<HTMLElement>) => void;
|
||||
};
|
||||
}
|
||||
|
||||
export function useDrop({ canDrop, onDrop, onDragOver, onDragEnter, onDragLeave }: IDropOptions = {}): IDrop {
|
||||
const optionsRef = useRef<IDropOptions>({ canDrop, onDrop, onDragOver, onDragEnter, onDragLeave });
|
||||
optionsRef.current = { canDrop, onDrop, onDragOver, onDragEnter, onDragLeave };
|
||||
|
||||
function handleOver(event: React.DragEvent<HTMLElement>) {
|
||||
const provider = getStoreProvider(event);
|
||||
const isDroppable = optionsRef.current.canDrop?.(event, provider);
|
||||
if (isDroppable) {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
} else {
|
||||
event.dataTransfer.dropEffect = 'none';
|
||||
}
|
||||
optionsRef.current.onDragOver?.(event, provider);
|
||||
}
|
||||
|
||||
function handleEnter(event: React.DragEvent<HTMLElement>) {
|
||||
const provider = getStoreProvider(event);
|
||||
optionsRef.current.onDragEnter?.(event, provider);
|
||||
}
|
||||
function handleLeave(event: React.DragEvent<HTMLElement>) {
|
||||
const provider = getStoreProvider(event);
|
||||
optionsRef.current.onDragLeave?.(event, provider);
|
||||
}
|
||||
function handleDrop(event: React.DragEvent<HTMLElement>) {
|
||||
const provider = getStoreProvider(event);
|
||||
optionsRef.current.onDrop?.(event, provider);
|
||||
}
|
||||
|
||||
return {
|
||||
props: useMemo(
|
||||
() => ({
|
||||
onDrop: handleDrop,
|
||||
onDragOver: handleOver,
|
||||
onDragEnter: handleEnter,
|
||||
onDragLeave: handleLeave,
|
||||
}),
|
||||
[],
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "@dbeaver/tsconfig/tsconfig.webview.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./lib/.tsbuildinfo",
|
||||
"rootDir": "src",
|
||||
"rootDirs": [
|
||||
"src"
|
||||
],
|
||||
"outDir": "lib",
|
||||
"incremental": true,
|
||||
"composite": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
!lib/
|
||||
!lib/lib
|
||||
/lib
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
@@ -16,3 +19,5 @@
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
tsconfig.tsbuildinfo
|
||||
@@ -0,0 +1,2 @@
|
||||
import '../src/index.css';
|
||||
import './global.css';
|
||||
@@ -0,0 +1,8 @@
|
||||
/** @type {import('@ladle/react').UserConfig} */
|
||||
export default {
|
||||
addons: {
|
||||
a11y: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,132 @@
|
||||
@import 'tailwindcss' layer(base) prefix(tw);
|
||||
|
||||
@layer theme {
|
||||
main {
|
||||
color: var(--dbv-kit-color-foreground);
|
||||
background-color: var(--tw-color-gray-50);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--tw-color-blue-600);
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover {
|
||||
color: var(--tw-color-blue-700);
|
||||
text-decoration-thickness: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
color: var(--tw-color-orange-700);
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h3,
|
||||
h5 {
|
||||
font-weight: var(--tw-font-weight-semibold);
|
||||
margin-block-start: 1em;
|
||||
margin-block-end: 0.5em;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: var(--tw-text-2xl);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: var(--tw-text-xl);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: var(--tw-text-lg);
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: var(--tw-text-base);
|
||||
margin-block-start: 0.5em;
|
||||
margin-block-end: 0.5em;
|
||||
}
|
||||
|
||||
kbd {
|
||||
background-color: #eee;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #b4b4b4;
|
||||
box-shadow:
|
||||
0 1px 1px rgba(0, 0, 0, 0.2),
|
||||
0 2px 0 0 rgba(255, 255, 255, 0.7) inset;
|
||||
color: #333;
|
||||
display: inline-block;
|
||||
font-size: 0.85em;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
padding: 2px 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.codeblock {
|
||||
background-color: var(--tw-color-gray-100);
|
||||
padding: 1em;
|
||||
border-radius: 0.25em;
|
||||
margin-block-start: 1em;
|
||||
margin-block-end: 1em;
|
||||
}
|
||||
|
||||
.comment {
|
||||
color: var(--tw-color-gray-500);
|
||||
}
|
||||
|
||||
.color-sample {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
border-radius: 0.25em;
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] {
|
||||
--dbv-kit-color-foreground: var(--tw-color-gray-200);
|
||||
--dbv-kit-color-background: var(--tw-color-gray-900);
|
||||
--dbv-kit-btn-loader-base-color: var(--tw-color-gray-900);
|
||||
|
||||
/* Primary Button */
|
||||
--dbv-kit-btn-primary-foreground: var(--tw-color-gray-100);
|
||||
--dbv-kit-btn-primary-background: var(--dbv-kit-color-primary-700);
|
||||
--dbv-kit-btn-primary-background-hover: var(--dbv-kit-color-primary-600);
|
||||
--dbv-kit-btn-primary-background-active: var(--dbv-kit-color-primary-500);
|
||||
|
||||
/* Secondary Button */
|
||||
--dbv-kit-btn-secondary-foreground: var(--tw-color-gray-100);
|
||||
--dbv-kit-btn-secondary-background: transparent;
|
||||
--dbv-kit-btn-secondary-background-hover: var(--tw-color-gray-800);
|
||||
--dbv-kit-btn-secondary-background-active: var(--tw-color-gray-700);
|
||||
--dbv-kit-btn-secondary-border-color: var(--tw-color-gray-200);
|
||||
|
||||
/* Danger Button */
|
||||
--dbv-kit-btn-danger-foreground: var(--tw-color-gray-100);
|
||||
--dbv-kit-btn-danger-background: var(--tw-color-red-700);
|
||||
--dbv-kit-btn-danger-background-hover: var(--tw-color-red-600);
|
||||
--dbv-kit-btn-danger-background-active: var(--tw-color-red-500);
|
||||
|
||||
/* Input */
|
||||
--dbv-kit-input-background: var(--tw-color-gray-800);
|
||||
--dbv-kit-input-border-color: var(--tw-color-gray-700);
|
||||
--dbv-kit-input-color: var(--tw-color-gray-100);
|
||||
--dbv-kit-input-placeholder-foreground: var(--tw-color-gray-400);
|
||||
|
||||
main {
|
||||
background-color: var(--tw-color-gray-900);
|
||||
}
|
||||
|
||||
code {
|
||||
color: var(--tw-color-orange-400);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--tw-color-blue-300);
|
||||
}
|
||||
|
||||
.codeblock {
|
||||
background-color: var(--tw-color-gray-800);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
# DBeaver UI Kit
|
||||
|
||||
#### The DBeaver UI Kit is a collection of reusable React components designed to help developers build consistent and visually appealing user interfaces.
|
||||
|
||||
## Using Ladle for Component Development
|
||||
|
||||
[Ladle](https://ladle.dev/) is a tool that allows you to develop, test, and document your React components in isolation. We use it to create a sandbox environment where you can interact with your components and ensure they work as expected.
|
||||
|
||||
To get started with Ladle in the DBeaver UI Kit, write in terminal:
|
||||
|
||||
```bash
|
||||
yarn docs
|
||||
```
|
||||
|
||||
Then open the provided URL to see components.
|
||||
|
||||
## How to Customize
|
||||
|
||||
DBeaver UI Kit components have basic styles and use some defaults from Tailwind CSS. To change styling, you can use different approaches.
|
||||
|
||||
### Global CSS Tokens
|
||||
|
||||
The UI kit uses several global CSS tokens described on the Global Tokens page. These tokens help maintain consistency across the UI components. All DBeaver UI tokens are prefixed with `--dbv-kit-`. You can use these tokens to customize the UI kit to fit your application's design. For example, changing the `--dbv-kit-font-size-base` token will change the base font size for text in the UI kit. Changing the `--dbv-kit-control-height-base` token will change all control heights. If you don't want to change all controls, you can change `--dbv-kit-control-height-small`, `--dbv-kit-control-height-medium`, or other tokens separately. They will affect only specific components.
|
||||
|
||||
### Component CSS Tokens
|
||||
|
||||
Some components have their own tokens. For example, the Button component has tokens for button sizes, padding, and colors. You can change these tokens to customize the Button component. To see the list of tokens, go to sources or start Ladle.
|
||||
|
||||
### Component CSS Classes
|
||||
|
||||
Each component in the UI kit has a default CSS class name following the `dbv-kit-[component name(in lowercase)]` naming convention that you can use to customize the component. You can find the default class name in the component's documentation.
|
||||
|
||||
For button:
|
||||
|
||||
```css
|
||||
.dbv-kit-button {
|
||||
/* Your custom styles */
|
||||
}
|
||||
```
|
||||
|
||||
For input:
|
||||
|
||||
```css
|
||||
.dbv-kit-input {
|
||||
/* Your custom styles */
|
||||
}
|
||||
```
|
||||
|
||||
Some components have additional classes for different states or sizes. For example, the Button component has classes for different sizes: `.dbv-kit-button--small`, `.dbv-kit-button--large`. We use the BEM convention for class names. If a component has a modifier, it will be separated by two dashes. If a component has a child element, it will be separated by two underscores, for example:
|
||||
|
||||
- `.dbv-kit-button__icon` - icon is a child element of the button
|
||||
- `.dbv-kit-button--large` - large is a size modifier
|
||||
- `.dbv-kit-button__icon--start` - icon is a child element of the button and has a start placement
|
||||
|
||||
Each component accepts the standard `className` and `style` props which enable using vanilla CSS, utility classes (e.g., Tailwind), CSS-in-JS (e.g., Styled Components), etc. A custom `className` can be specified and will be appended to a class list of a component:
|
||||
|
||||
```jsx
|
||||
<Button className="my-custom-button">Button</Button>
|
||||
```
|
||||
|
||||
```jsx
|
||||
<Button style={{ background: 'red' }}>Button</Button>
|
||||
```
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="currentColor">
|
||||
<polygon points="2 10 12 10 7 4 2 10" />
|
||||
<rect x="6" y="6" width="2" height="22" />
|
||||
<rect x="14" y="6" width="16" height="2" />
|
||||
<rect x="14" y="15" width="12" height="2" />
|
||||
<rect x="14" y="24" width="8" height="2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 353 B |
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<polygon points="2 8.14 9.14 8.14 5.57 3.86 2 8.14" />
|
||||
<rect x="4.86" y="5.29" width="1.43" height="15.71" />
|
||||
<rect x="10.57" y="5.29" width="11.43" height="1.43" />
|
||||
<rect x="10.57" y="11.71" width="8.57" height="1.43" />
|
||||
<rect x="10.57" y="18.14" width="5.71" height="1.43" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 414 B |
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor">
|
||||
<polygon points="0 4 5 4 2.5 1 0 4" />
|
||||
<rect x="2" y="4" width="1" height="11" />
|
||||
<rect x="6" y="3" width="10" height="1" />
|
||||
<rect x="6" y="8" width="7" height="1" />
|
||||
<rect x="6" y="13" width="4" height="1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 346 B |
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="currentColor">
|
||||
<polygon points="12 22 2 22 7 28 12 22" />
|
||||
<rect x="6" y="4" width="2" height="22" />
|
||||
<rect x="14" y="6" width="16" height="2" />
|
||||
<rect x="14" y="15" width="12" height="2" />
|
||||
<rect x="14" y="24" width="8" height="2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 355 B |
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<polygon points="9.14 16.71 2 16.71 5.57 21 9.14 16.71" />
|
||||
<rect x="4.86" y="3.86" width="1.43" height="15.71" />
|
||||
<rect x="10.57" y="5.29" width="11.43" height="1.43" />
|
||||
<rect x="10.57" y="11.71" width="8.57" height="1.43" />
|
||||
<rect x="10.57" y="18.14" width="5.71" height="1.43" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 418 B |
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor">
|
||||
<polygon points="5 12 0 12 2.5 15 5 12" />
|
||||
<rect x="2" y="1" width="1" height="12" />
|
||||
<rect x="6" y="3" width="10" height="1" />
|
||||
<rect x="6" y="8" width="7" height="1" />
|
||||
<rect x="6" y="13" width="4" height="1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 350 B |
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="currentColor">
|
||||
<polygon points="4 10 14 10 9 4 4 10" />
|
||||
<rect x="8" y="6" width="2" height="22" />
|
||||
<polygon points="28 22 18 22 23 28 28 22" />
|
||||
<rect x="22" y="4" width="2" height="22" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 307 B |
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<polygon points="3.4 8.29 10.55 8.29 6.98 4 3.4 8.29" />
|
||||
<rect x="6.26" y="5.43" width="1.43" height="15.71" />
|
||||
<polygon points="20.55 16.86 13.4 16.86 16.98 21.14 20.55 16.86" />
|
||||
<rect x="16.26" y="4" width="1.43" height="15.71" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 367 B |
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor">
|
||||
<polygon points="2 4 7 4 4.5 1 2 4" />
|
||||
<rect x="4" y="4" width="1" height="11" />
|
||||
<polygon points="14 12 9 12 11.5 15 14 12" />
|
||||
<rect x="11" y="1" width="1" height="11" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 306 B |
@@ -0,0 +1,28 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<symbol id="icon-sort-unknown" viewBox="0 0 24 24" fill="currentColor">
|
||||
<polygon points="3.4 8.29 10.55 8.29 6.98 4 3.4 8.29" />
|
||||
<rect x="6.26" y="5.43" width="1.43" height="15.71" />
|
||||
<polygon points="20.55 16.86 13.4 16.86 16.98 21.14 20.55 16.86" />
|
||||
<rect x="16.26" y="4" width="1.43" height="15.71" />
|
||||
</symbol>
|
||||
<symbol id="icon-sort-desc" viewBox="0 0 24 24"
|
||||
fill="currentColor">
|
||||
<polygon points="9.14 16.71 2 16.71 5.57 21 9.14 16.71" />
|
||||
<rect x="4.86" y="3.86" width="1.43" height="15.71" />
|
||||
<rect x="10.57" y="5.29" width="11.43" height="1.43" />
|
||||
<rect x="10.57" y="11.71" width="8.57" height="1.43" />
|
||||
<rect x="10.57" y="18.14" width="5.71" height="1.43" />
|
||||
</symbol>
|
||||
<symbol id="icon-sort-asc" viewBox="0 0 24 24" fill="currentColor">
|
||||
<polygon points="2 8.14 9.14 8.14 5.57 3.86 2 8.14" />
|
||||
<rect x="4.86" y="5.29" width="1.43" height="15.71" />
|
||||
<rect x="10.57" y="5.29" width="11.43" height="1.43" />
|
||||
<rect x="10.57" y="11.71" width="8.57" height="1.43" />
|
||||
<rect x="10.57" y="18.14" width="5.71" height="1.43" />
|
||||
</symbol>
|
||||
<symbol id="icon-cross" viewBox="0 0 24 24">
|
||||
<path xmlns="http://www.w3.org/2000/svg" fill="currentColor" fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M3.31579 3.31579C2.89474 3.73684 2.89474 4.42105 3.31579 4.84211L10.4737 12L3.31579 19.1579C2.89474 19.5789 2.89474 20.2632 3.31579 20.6842C3.73684 21.1053 4.42105 21.1053 4.84211 20.6842L12 13.5263L19.1579 20.6842C19.5789 21.1053 20.2632 21.1053 20.6842 20.6842C21.1053 20.2632 21.1053 19.5789 20.6842 19.1579L13.5263 12L20.6842 4.84211C21.1053 4.42105 21.1053 3.73684 20.6842 3.31579C20.2632 2.89474 19.5789 2.89474 19.1579 3.31579L12 10.4737L4.84211 3.31579C4.42105 2.89474 3.73684 2.89474 3.31579 3.31579Z" />
|
||||
</symbol>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@dbeaver/ui-kit",
|
||||
"type": "module",
|
||||
"version": "0.1.0",
|
||||
"description": "",
|
||||
"license": "Apache-2.0",
|
||||
"exports": {
|
||||
".": "./lib/index.js",
|
||||
"./*.css": "./src/*.css",
|
||||
"./*": "./lib/*.js",
|
||||
"./assets/icons/*.svg": "./assets/icons/*.svg"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -b",
|
||||
"docs": "ladle serve",
|
||||
"clean": "rimraf --glob lib"
|
||||
},
|
||||
"packageManager": "yarn@4.6.0",
|
||||
"devDependencies": {
|
||||
"@dbeaver/tsconfig": "workspace:^",
|
||||
"@ladle/react": "^5",
|
||||
"@tailwindcss/vite": "4.0.7",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@wroud/vite-plugin-asset-resolver": "^0",
|
||||
"@wroud/vite-plugin-tsc": "^0.11.6",
|
||||
"rimraf": "^6",
|
||||
"tailwindcss": "4.0.7",
|
||||
"typescript": "^5",
|
||||
"vite": "^6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ariakit/react": "^0",
|
||||
"clsx": "^2",
|
||||
"react": "^19",
|
||||
"react-dom": "^19"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export default function template(files) {
|
||||
const exportLines = files.map((file) => {
|
||||
const { path } = file;
|
||||
|
||||
const fileName = path.split('/').pop();
|
||||
const componentNameWithExt = fileName.replace(/\.tsx$/, '.js');
|
||||
const componentName = fileName.replace(/\.tsx$/, '');
|
||||
|
||||
return `export { default as ${componentName} } from './${componentNameWithExt}';`;
|
||||
});
|
||||
|
||||
|
||||
const header = '/* This file is automatically generated by the build:icons script. Do not modify it manually.*/\n\n ';
|
||||
|
||||
return header + exportLines.join('\n');
|
||||
};
|
||||
@@ -0,0 +1,176 @@
|
||||
@import './_base.css';
|
||||
@import './_sizes.css';
|
||||
@import './_variants.css';
|
||||
|
||||
@layer base {
|
||||
.dbv-kit-button {
|
||||
height: var(--dbv-kit-btn-height);
|
||||
padding-inline: var(--dbv-kit-btn-padding-inline);
|
||||
|
||||
background-color: var(--dbv-kit-btn-background);
|
||||
color: var(--dbv-kit-btn-foreground);
|
||||
|
||||
border-radius: var(--dbv-kit-btn-border-radius);
|
||||
border-width: var(--dbv-kit-btn-border-width);
|
||||
border-color: var(--dbv-kit-btn-border-color);
|
||||
border-style: var(--dbv-kit-btn-border-style);
|
||||
|
||||
font-size: var(--dbv-kit-btn-font-size);
|
||||
font-weight: var(--dbv-kit-btn-font-weight);
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
&:focus-visible,
|
||||
&[data-focus-visible] {
|
||||
outline: var(--dbv-kit-control-outline-width) solid var(--dbv-kit-btn-outline-color);
|
||||
outline-offset: var(--dbv-kit-btn-outline-offset);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
color: var(--dbv-kit-btn-foreground-hover);
|
||||
background-color: var(--dbv-kit-btn-background-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&:active,
|
||||
&[data-active] {
|
||||
color: var(--dbv-kit-btn-foreground-active);
|
||||
background-color: var(--dbv-kit-btn-background-active);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------- */
|
||||
/* Sizes */
|
||||
|
||||
.dbv-kit-button--small {
|
||||
--dbv-kit-btn-padding-inline: var(--dbv-kit-btn-small-padding-inline);
|
||||
--dbv-kit-btn-font-size: var(--dbv-kit-btn-small-font-size);
|
||||
--dbv-kit-btn-height: var(--dbv-kit-btn-small-height);
|
||||
--dbv-kit-btn-gap: var(--dbv-kit-btn-small-gap);
|
||||
|
||||
--dbv-kit-btn-icon-margin-inline: calc(var(--dbv-kit-btn-small-padding-inline) * -0.25);
|
||||
}
|
||||
|
||||
.dbv-kit-button--medium {
|
||||
--dbv-kit-btn-padding-inline: var(--dbv-kit-btn-medium-padding-inline);
|
||||
--dbv-kit-btn-font-size: var(--dbv-kit-btn-medium-font-size);
|
||||
--dbv-kit-btn-height: var(--dbv-kit-btn-medium-height);
|
||||
--dbv-kit-btn-gap: var(--dbv-kit-btn-medium-gap);
|
||||
|
||||
--dbv-kit-btn-icon-margin-inline: calc(var(--dbv-kit-btn-medium-padding-inline) * -0.25);
|
||||
}
|
||||
|
||||
.dbv-kit-button--large {
|
||||
--dbv-kit-btn-padding-inline: var(--dbv-kit-btn-large-padding-inline);
|
||||
--dbv-kit-btn-font-size: var(--dbv-kit-btn-large-font-size);
|
||||
--dbv-kit-btn-height: var(--dbv-kit-btn-large-height);
|
||||
--dbv-kit-btn-gap: var(--dbv-kit-btn-large-gap);
|
||||
|
||||
--dbv-kit-btn-icon-margin-inline: calc(var(--dbv-kit-btn-large-padding-inline) * -0.25);
|
||||
}
|
||||
|
||||
.dbv-kit-button--xlarge {
|
||||
--dbv-kit-btn-padding-inline: var(--dbv-kit-btn-xlarge-padding-inline);
|
||||
--dbv-kit-btn-font-size: var(--dbv-kit-btn-xlarge-font-size);
|
||||
--dbv-kit-btn-height: var(--dbv-kit-btn-xlarge-height);
|
||||
--dbv-kit-btn-gap: var(--dbv-kit-btn-xlarge-gap);
|
||||
|
||||
--dbv-kit-btn-icon-margin-inline: calc(var(--dbv-kit-btn-xlarge-padding-inline) * -0.25);
|
||||
}
|
||||
|
||||
/* ---------------------------- */
|
||||
/* Variants */
|
||||
|
||||
.dbv-kit-button--primary {
|
||||
--dbv-kit-btn-foreground: var(--dbv-kit-btn-primary-foreground);
|
||||
--dbv-kit-btn-foreground-hover: var(--dbv-kit-btn-primary-foreground-hover);
|
||||
--dbv-kit-btn-foreground-active: var(--dbv-kit-btn-primary-foreground-active);
|
||||
|
||||
--dbv-kit-btn-background: var(--dbv-kit-btn-primary-background);
|
||||
--dbv-kit-btn-background-hover: var(--dbv-kit-btn-primary-background-hover);
|
||||
--dbv-kit-btn-background-active: var(--dbv-kit-btn-primary-background-active);
|
||||
|
||||
--dbv-kit-btn-loader-color: var(--dbv-kit-btn-primary-foreground);
|
||||
|
||||
&:disabled {
|
||||
background-color: var(--dbv-kit-control-disabled-background);
|
||||
color: var(--dbv-kit-control-disabled-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.dbv-kit-button--secondary {
|
||||
--dbv-kit-btn-foreground: var(--dbv-kit-btn-secondary-foreground);
|
||||
--dbv-kit-btn-foreground-hover: var(--dbv-kit-btn-secondary-foreground-hover);
|
||||
--dbv-kit-btn-foreground-active: var(--dbv-kit-btn-secondary-foreground-active);
|
||||
|
||||
--dbv-kit-btn-background: var(--dbv-kit-btn-secondary-background);
|
||||
--dbv-kit-btn-background-hover: var(--dbv-kit-btn-secondary-background-hover);
|
||||
--dbv-kit-btn-background-active: var(--dbv-kit-btn-secondary-background-active);
|
||||
--dbv-kit-btn-border-color: var(--dbv-kit-btn-secondary-border-color);
|
||||
|
||||
--dbv-kit-btn-loader-color: var(--dbv-kit-btn-secondary-foreground);
|
||||
|
||||
&:disabled {
|
||||
color: var(--dbv-kit-control-disabled-foreground);
|
||||
border-color: var(--dbv-kit-control-disabled-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.dbv-kit-button--danger {
|
||||
--dbv-kit-btn-foreground: var(--dbv-kit-btn-danger-foreground);
|
||||
--dbv-kit-btn-foreground-hover: var(--dbv-kit-btn-danger-foreground);
|
||||
--dbv-kit-btn-foreground-active: var(--dbv-kit-btn-danger-foreground-active);
|
||||
|
||||
--dbv-kit-btn-background: var(--dbv-kit-btn-danger-background);
|
||||
--dbv-kit-btn-background-hover: var(--dbv-kit-btn-danger-background-hover);
|
||||
--dbv-kit-btn-background-active: var(--dbv-kit-btn-danger-background-active);
|
||||
--dbv-kit-btn-border-color: var(--dbv-kit-btn-danger-border-color);
|
||||
|
||||
--dbv-kit-btn-loader-color: var(--dbv-kit-btn-danger-foreground);
|
||||
--dbv-kit-btn-outline-color: var(--dbv-kit-btn-danger-color-outline);
|
||||
|
||||
&:disabled {
|
||||
background-color: var(--dbv-kit-control-disabled-background);
|
||||
color: var(--dbv-kit-control-disabled-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------- */
|
||||
/* Loader */
|
||||
|
||||
.dbv-kit-button[data-loading='true'] {
|
||||
background-color: var(--dbv-kit-btn-background-hover);
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&[data-active] {
|
||||
background-color: var(--dbv-kit-btn-background-active);
|
||||
}
|
||||
}
|
||||
|
||||
.dbv-kit-button .dbv-kit-spinner {
|
||||
--dbv-kit-spinner-stroke-color: var(--dbv-kit-btn-loader-color);
|
||||
--dbv-kit-spinner-height: calc(var(--dbv-kit-btn-height) * 0.6);
|
||||
--dbv-kit-spinner-stroke-width: 3px;
|
||||
}
|
||||
|
||||
/* ---------------------------- */
|
||||
/* Icon */
|
||||
|
||||
.dbv-kit-button__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.dbv-kit-button__icon--start {
|
||||
margin-inline-start: var(--dbv-kit-btn-icon-margin-inline);
|
||||
}
|
||||
|
||||
.dbv-kit-button__icon--end {
|
||||
margin-inline-end: var(--dbv-kit-btn-icon-margin-inline);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import clsx from 'clsx';
|
||||
import type { ControlSize } from '../types/controls.js';
|
||||
import { componentProviderWrapper } from '../componentProviderWrapper.js';
|
||||
import { ButtonRoot, type ButtonRootProps } from './ButtonRoot.js';
|
||||
import './Button.css';
|
||||
|
||||
export interface ButtonProps extends ButtonRootProps {
|
||||
variant?: 'primary' | 'secondary' | 'danger';
|
||||
size?: ControlSize;
|
||||
}
|
||||
|
||||
export function ButtonBase({ className, variant = 'primary', size = 'medium', ...props }: ButtonProps) {
|
||||
const classToApply = clsx('dbv-kit-button', `dbv-kit-button--${variant}`, `dbv-kit-button--${size}`, className);
|
||||
|
||||
return <ButtonRoot className={classToApply} {...props} />;
|
||||
}
|
||||
export interface ButtonIconProps extends React.HTMLAttributes<HTMLSpanElement> {
|
||||
placement?: 'start' | 'end';
|
||||
}
|
||||
|
||||
/**
|
||||
* Button Icon component - used to place an icon inside a button, renders span element.;
|
||||
*
|
||||
* @param props.placement This property is needed to adjust icon placement inside a button. The icon with placement="start" will cut the inline-start padding. placement="end" will affect the padding-inline-end accordingly. This property supports RTL and LTR, so you don't need to think about it.
|
||||
*/
|
||||
export function ButtonBaseIcon({ className, children, placement }: ButtonIconProps) {
|
||||
const classToApply = clsx('dbv-kit-button__icon', placement && `dbv-kit-button__icon--${placement}`, className);
|
||||
return <span className={classToApply}>{children}</span>;
|
||||
}
|
||||
|
||||
export const Button = componentProviderWrapper('Button', ButtonBase);
|
||||
export const ButtonIcon = componentProviderWrapper('ButtonIcon', ButtonBaseIcon);
|
||||
@@ -0,0 +1,25 @@
|
||||
@layer base {
|
||||
.dbv-kit-button__loader {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dbv-kit-button__content {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
gap: var(--dbv-kit-btn-gap);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Button as AriaButton, type ButtonProps as AriaKitButtonProps } from '@ariakit/react';
|
||||
import { Spinner } from '../Spinner/Spinner.js';
|
||||
import { clsx } from '../utils/clsx.js';
|
||||
import './ButtonRoot.css';
|
||||
|
||||
export interface ButtonRootProps extends Omit<AriaKitButtonProps, 'clickOnEnter' | 'clickOnSpace'> {
|
||||
loading?: boolean;
|
||||
loader?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ButtonRoot({ loading, loader, children, onClick, className, ...props }: ButtonRootProps) {
|
||||
if (loading) {
|
||||
props['aria-busy'] = true;
|
||||
props['data-loading'] = true;
|
||||
}
|
||||
|
||||
return (
|
||||
<AriaButton onClick={loading ? () => null : onClick} className={clsx(className, 'tw:relative')} {...props}>
|
||||
<div className={clsx('dbv-kit-button__content', loading && 'tw:opacity-0')}>{children}</div>
|
||||
{loading && <div className="dbv-kit-button__loader">{loader ? loader : <Spinner />}</div>}
|
||||
</AriaButton>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
@layer base {
|
||||
:root {
|
||||
--dbv-kit-btn-height: var(--dbv-kit-control-height-medium);
|
||||
--dbv-kit-btn-padding-inline: calc(var(--tw-spacing) * 2.5);
|
||||
--dbv-kit-btn-gap: calc(var(--dbv-kit-btn-padding-inline) / 2);
|
||||
|
||||
--dbv-kit-btn-outline-offset: var(--dbv-kit-control-outline-offset);
|
||||
--dbv-kit-btn-outline-color: var(--dbv-kit-control-outline-color);
|
||||
|
||||
--dbv-kit-btn-foreground: var(--tw-color-white);
|
||||
--dbv-kit-btn-foreground-hover: var(--tw-color-white);
|
||||
--dbv-kit-btn-foreground-active: var(--tw-color-white);
|
||||
|
||||
--dbv-kit-btn-background: var(--dbv-kit-color-primary-600);
|
||||
--dbv-kit-btn-background-hover: var(--dbv-kit-color-primary-700);
|
||||
--dbv-kit-btn-background-active: var(--dbv-kit-color-primary-800);
|
||||
|
||||
--dbv-kit-btn-border-width: var(--dbv-kit-control-border-width);
|
||||
--dbv-kit-btn-border-color: transparent;
|
||||
--dbv-kit-btn-border-style: solid;
|
||||
--dbv-kit-btn-border-radius: var(--dbv-kit-control-border-radius);
|
||||
|
||||
--dbv-kit-btn-font-weight: var(--tw-font-weight-normal);
|
||||
--dbv-kit-btn-font-size: calc(var(--dbv-kit-font-size-base) * 0.875);
|
||||
|
||||
--dbv-kit-btn-loader-base-color: var(--dbv-kit-btn-foreground);
|
||||
--dbv-kit-btn-loader-height: calc(var(--dbv-kit-btn-height) * 0.6);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
@layer base {
|
||||
:root {
|
||||
/*Small button*/
|
||||
--dbv-kit-btn-small-padding-inline: calc(var(--dbv-kit-btn-padding-inline) * 0.5);
|
||||
--dbv-kit-btn-small-font-size: calc(var(--dbv-kit-btn-font-size) * 0.875);
|
||||
--dbv-kit-btn-small-height: var(--dbv-kit-control-height-small);
|
||||
--dbv-kit-btn-small-gap: calc(var(--dbv-kit-btn-gap) * 0.5);
|
||||
|
||||
/*Medium button*/
|
||||
--dbv-kit-btn-medium-padding-inline: var(--dbv-kit-btn-padding-inline);
|
||||
--dbv-kit-btn-medium-font-size: var(--dbv-kit-btn-font-size);
|
||||
--dbv-kit-btn-medium-height: var(--dbv-kit-control-height-medium);
|
||||
--dbv-kit-btn-medium-gap: var(--dbv-kit-btn-gap);
|
||||
|
||||
/*Large button*/
|
||||
--dbv-kit-btn-large-padding-inline: calc(var(--dbv-kit-btn-padding-inline) * 1.25);
|
||||
--dbv-kit-btn-large-font-size: calc(var(--dbv-kit-btn-font-size) * 1.125);
|
||||
--dbv-kit-btn-large-height: var(--dbv-kit-control-height-large);
|
||||
--dbv-kit-btn-large-gap: calc(var(--dbv-kit-btn-gap) * 1.25);
|
||||
|
||||
/*Extra large button*/
|
||||
--dbv-kit-btn-xlarge-padding-inline: calc(var(--dbv-kit-btn-padding-inline) * 1.5);
|
||||
--dbv-kit-btn-xlarge-font-size: calc(var(--dbv-kit-btn-font-size) * 1.25);
|
||||
--dbv-kit-btn-xlarge-height: var(--dbv-kit-control-height-xlarge);
|
||||
--dbv-kit-btn-xlarge-gap: calc(var(--dbv-kit-btn-gap) * 1.5);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
@layer base {
|
||||
:root {
|
||||
/* Primary Button */
|
||||
--dbv-kit-btn-primary-foreground: var(--tw-color-white);
|
||||
--dbv-kit-btn-primary-foreground-hover: var(--tw-color-white);
|
||||
--dbv-kit-btn-primary-foreground-active: var(--tw-color-white);
|
||||
--dbv-kit-btn-primary-background: var(--dbv-kit-color-primary-600);
|
||||
--dbv-kit-btn-primary-background-hover: var(--dbv-kit-color-primary-700);
|
||||
--dbv-kit-btn-primary-background-active: var(--dbv-kit-color-primary-800);
|
||||
|
||||
/* Secondary Button */
|
||||
--dbv-kit-btn-secondary-foreground: var(--dbv-kit-color-primary-600);
|
||||
--dbv-kit-btn-secondary-foreground-hover: var(--dbv-kit-color-primary-600);
|
||||
--dbv-kit-btn-secondary-foreground-active: var(--dbv-kit-color-primary-600);
|
||||
--dbv-kit-btn-secondary-background: transparent;
|
||||
--dbv-kit-btn-secondary-background-hover: var(--dbv-kit-color-primary-50);
|
||||
--dbv-kit-btn-secondary-background-active: var(--dbv-kit-color-primary-100);
|
||||
--dbv-kit-btn-secondary-border-color: var(--dbv-kit-color-primary-600);
|
||||
--dbv-kit-btn-secondary-border-color-hover: var(--dbv-kit-color-primary-600);
|
||||
|
||||
/* Danger Button */
|
||||
--dbv-kit-btn-danger-foreground: var(--tw-color-white);
|
||||
--dbv-kit-btn-danger-foreground-hover: var(--tw-color-white);
|
||||
--dbv-kit-btn-danger-foreground-active: var(--tw-color-white);
|
||||
--dbv-kit-btn-danger-background: var(--tw-color-red-600);
|
||||
--dbv-kit-btn-danger-background-hover: var(--tw-color-red-700);
|
||||
--dbv-kit-btn-danger-background-active: var(--tw-color-red-800);
|
||||
--dbv-kit-btn-danger-border-color: transparent;
|
||||
--dbv-kit-btn-danger-color-outline: var(--tw-color-red-300);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
@import './_base.css';
|
||||
@import './_sizes.css';
|
||||
|
||||
@layer base {
|
||||
.dbv-kit-checkbox {
|
||||
display: inline-flex;
|
||||
user-select: none;
|
||||
align-items: start;
|
||||
min-height: var(--dbv-kit-checkbox-height);
|
||||
gap: var(--dbv-kit-checkbox-gap);
|
||||
line-height: var(--dbv-kit-checkbox-height);
|
||||
font-size: var(--dbv-kit-checkbox-font-size);
|
||||
color: var(--dbv-kit-checkbox-foreground);
|
||||
outline: var(--dbv-kit-control-outline-color);
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
||||
.dbv-kit-checkbox__check {
|
||||
box-shadow: 0 0 0 2px var(--dbv-kit-checkbox-hover-shadow-color);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-checked='true'] {
|
||||
border-color: var(--dbv-kit-checkbox-border-color-checked);
|
||||
}
|
||||
|
||||
&[data-disabled='true'] {
|
||||
pointer-events: none;
|
||||
opacity: var(--dbv-kit-control-disabled-opacity);
|
||||
}
|
||||
}
|
||||
|
||||
.dbv-kit-checkbox__check {
|
||||
--check-size: round(calc(var(--dbv-kit-checkbox-height) * var(--dbv-kit-checkbox-check-size)), 2px);
|
||||
margin-top: calc(var(--dbv-kit-checkbox-height) * (calc(1 - var(--dbv-kit-checkbox-check-size)) / 2));
|
||||
height: var(--check-size);
|
||||
line-height: var(--check-size);
|
||||
width: var(--check-size);
|
||||
min-width: var(--check-size);
|
||||
border-radius: var(--dbv-kit-checkbox-border-radius);
|
||||
background-color: var(--dbv-kit-checkbox-background);
|
||||
border-color: var(--dbv-kit-checkbox-border-color);
|
||||
border-width: var(--dbv-kit-checkbox-border-width);
|
||||
border-style: var(--dbv-kit-checkbox-border-style);
|
||||
color: var(--dbv-kit-checkbox-icon-color);
|
||||
outline-offset: var(--dbv-kit-control-outline-offset);
|
||||
outline-width: var(--dbv-kit-control-outline-width);
|
||||
outline-color: var(--dbv-kit-control-outline-color);
|
||||
transition-property: stroke-dashoffset;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
stroke-dasharray: 20;
|
||||
stroke-dashoffset: 20;
|
||||
|
||||
&[data-focus-visible] {
|
||||
outline-style: solid;
|
||||
}
|
||||
|
||||
&[data-checked='true'],
|
||||
&[data-indeterminate='true'] {
|
||||
background-color: var(--dbv-kit-checkbox-background-checked);
|
||||
border-color: var(--dbv-kit-checkbox-border-color-checked);
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------- */
|
||||
/* Sizes */
|
||||
|
||||
.dbv-kit-checkbox--small {
|
||||
--dbv-kit-checkbox-font-size: var(--dbv-kit-checkbox-small-font-size);
|
||||
--dbv-kit-checkbox-height: var(--dbv-kit-checkbox-small-height);
|
||||
--dbv-kit-checkbox-gap: var(--dbv-kit-checkbox-small-gap);
|
||||
}
|
||||
|
||||
.dbv-kit-checkbox--medium {
|
||||
--dbv-kit-checkbox-font-size: var(--dbv-kit-checkbox-medium-font-size);
|
||||
--dbv-kit-checkbox-height: var(--dbv-kit-checkbox-medium-height);
|
||||
--dbv-kit-checkbox-gap: var(--dbv-kit-checkbox-medium-gap);
|
||||
}
|
||||
|
||||
.dbv-kit-checkbox--large {
|
||||
--dbv-kit-checkbox-font-size: var(--dbv-kit-checkbox-large-font-size);
|
||||
--dbv-kit-checkbox-height: var(--dbv-kit-checkbox-large-height);
|
||||
--dbv-kit-checkbox-gap: var(--dbv-kit-checkbox-large-gap);
|
||||
}
|
||||
|
||||
.dbv-kit-checkbox--xlarge {
|
||||
--dbv-kit-checkbox-font-size: var(--dbv-kit-checkbox-xlarge-font-size);
|
||||
--dbv-kit-checkbox-height: var(--dbv-kit-checkbox-xlarge-height);
|
||||
--dbv-kit-checkbox-gap: var(--dbv-kit-checkbox-xlarge-gap);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
import { Checkbox as AriaCheckbox, VisuallyHidden, type CheckboxProps as AriaKitCheckboxProps } from '@ariakit/react';
|
||||
import { useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import './Checkbox.css';
|
||||
import type { ControlSize } from '../types/controls.js';
|
||||
import { componentProviderWrapper } from '../componentProviderWrapper.js';
|
||||
|
||||
export interface CheckboxProps extends Omit<AriaKitCheckboxProps, 'render' | 'size'> {
|
||||
size?: ControlSize;
|
||||
icon?: React.ReactNode;
|
||||
indeterminate?: boolean;
|
||||
indeterminateIcon?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function CheckboxBase({ children, className, icon, indeterminate, indeterminateIcon, size = 'medium', ...props }: CheckboxProps) {
|
||||
const [innerChecked, setInnerChecked] = useState(props.defaultChecked ?? false);
|
||||
const checked = props.checked ?? innerChecked;
|
||||
|
||||
const [focusVisible, setFocusVisible] = useState(false);
|
||||
return (
|
||||
<label
|
||||
className={clsx('dbv-kit-checkbox', `dbv-kit-checkbox--${size}`, className)}
|
||||
data-disabled={props.disabled || undefined}
|
||||
data-checked={checked}
|
||||
data-focus-visible={focusVisible || undefined}
|
||||
>
|
||||
<VisuallyHidden>
|
||||
<AriaCheckbox
|
||||
{...props}
|
||||
className="dbv-kit-checkbox__input"
|
||||
clickOnEnter
|
||||
onFocusVisible={() => setFocusVisible(true)}
|
||||
onBlur={() => setFocusVisible(false)}
|
||||
onChange={event => {
|
||||
setInnerChecked(event.target.checked);
|
||||
props.onChange?.(event);
|
||||
}}
|
||||
/>
|
||||
</VisuallyHidden>
|
||||
|
||||
<div
|
||||
className="dbv-kit-checkbox__check"
|
||||
data-indeterminate={indeterminate}
|
||||
data-checked={checked}
|
||||
data-focus-visible={focusVisible || undefined}
|
||||
>
|
||||
{checked &&
|
||||
(icon ?? (
|
||||
<svg stroke="currentColor" strokeWidth="2" viewBox="0 0 16 16">
|
||||
<polyline fill="none" points="3,7 7,11 13,4" />
|
||||
</svg>
|
||||
))}
|
||||
{indeterminate &&
|
||||
!checked &&
|
||||
(indeterminateIcon ?? (
|
||||
<svg fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 16 16">
|
||||
<line x1="3" y1="8" x2="13" y2="8" />
|
||||
</svg>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<span className="dbv-kit-checkbox__text">{children}</span>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
export const Checkbox = componentProviderWrapper('Checkbox', CheckboxBase);
|
||||
@@ -0,0 +1,27 @@
|
||||
@layer base {
|
||||
:root {
|
||||
/* Checkbox sizes */
|
||||
--dbv-kit-checkbox-height: var(--dbv-kit-control-height-medium);
|
||||
--dbv-kit-checkbox-gap: calc(var(--tw-spacing) * 1.25);
|
||||
--dbv-kit-checkbox-check-size: 0.5625;
|
||||
|
||||
/* Checkbox colors */
|
||||
--dbv-kit-checkbox-foreground: var(--tw-color-black);
|
||||
--dbv-kit-checkbox-background: transparent;
|
||||
--dbv-kit-checkbox-background-checked: var(--dbv-kit-color-primary-600);
|
||||
|
||||
--dbv-kit-checkbox-hover-shadow-color: var(--dbv-kit-color-primary-200);
|
||||
--dbv-kit-checkbox-icon-color: var(--tw-color-white);
|
||||
|
||||
/* Checkbox border */
|
||||
--dbv-kit-checkbox-border-width: calc(var(--dbv-kit-control-border-width) * 2);
|
||||
--dbv-kit-checkbox-border-radius: var(--dbv-kit-control-border-radius);
|
||||
--dbv-kit-checkbox-border-style: solid;
|
||||
--dbv-kit-checkbox-border-color: var(--dbv-kit-color-control-inactive);
|
||||
--dbv-kit-checkbox-border-color-checked: var(--dbv-kit-color-primary-600);
|
||||
|
||||
/* Checkbox label font */
|
||||
--dbv-kit-checkbox-font-weight: var(--tw-font-weight-normal);
|
||||
--dbv-kit-checkbox-font-size: var(--dbv-kit-font-size-base);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
@layer base {
|
||||
:root {
|
||||
/* Small checkbox */
|
||||
--dbv-kit-checkbox-small-font-size: calc(var(--dbv-kit-checkbox-font-size) * 0.875);
|
||||
--dbv-kit-checkbox-small-height: var(--dbv-kit-control-height-small);
|
||||
--dbv-kit-checkbox-small-gap: calc(var(--dbv-kit-checkbox-gap) * 0.5);
|
||||
|
||||
/* Medium checkbox */
|
||||
--dbv-kit-checkbox-medium-font-size: var(--dbv-kit-checkbox-font-size);
|
||||
--dbv-kit-checkbox-medium-height: var(--dbv-kit-control-height-medium);
|
||||
--dbv-kit-checkbox-medium-gap: var(--dbv-kit-checkbox-gap);
|
||||
|
||||
/* Large checkbox */
|
||||
--dbv-kit-checkbox-large-font-size: calc(var(--dbv-kit-checkbox-font-size) * 1.125);
|
||||
--dbv-kit-checkbox-large-height: var(--dbv-kit-control-height-large);
|
||||
--dbv-kit-checkbox-large-gap: calc(var(--dbv-kit-checkbox-gap) * 1.25);
|
||||
|
||||
/* Extra large checkbox */
|
||||
--dbv-kit-checkbox-xlarge-font-size: calc(var(--dbv-kit-checkbox-font-size) * 1.25);
|
||||
--dbv-kit-checkbox-xlarge-height: var(--dbv-kit-control-height-xlarge);
|
||||
--dbv-kit-checkbox-xlarge-gap: calc(var(--dbv-kit-checkbox-gap) * 1.5);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { createContext } from 'react';
|
||||
import type { ButtonBase, ButtonBaseIcon } from './Button/Button.js';
|
||||
import type { IconButtonBase } from './IconButton/IconButton.js';
|
||||
import type { CheckboxBase } from './Checkbox/Checkbox.js';
|
||||
import type { Radio } from './Radio/Radio.js';
|
||||
import type { InputBase } from './Input/Input.js';
|
||||
import type { RadioGroup } from './Radio/RadioGroup.js';
|
||||
import type { Select } from './Select/Select.js';
|
||||
import type { SelectField } from './Select/SelectField.js';
|
||||
|
||||
export interface IComponentProvider {
|
||||
Button?: typeof ButtonBase;
|
||||
ButtonIcon?: typeof ButtonBaseIcon;
|
||||
IconButton?: typeof IconButtonBase;
|
||||
Input?: typeof InputBase;
|
||||
Checkbox?: typeof CheckboxBase;
|
||||
Radio?: typeof Radio;
|
||||
RadioGroup?: typeof RadioGroup;
|
||||
Select?: typeof Select;
|
||||
SelectField?: typeof SelectField;
|
||||
}
|
||||
export const ComponentProvider = createContext<IComponentProvider>({});
|
||||
@@ -0,0 +1,110 @@
|
||||
@import './_base.css';
|
||||
@import './_sizes.css';
|
||||
@import './_variants.css';
|
||||
|
||||
@layer base {
|
||||
.dbv-kit-icon-button {
|
||||
min-width: var(--dbv-kit-icon-btn-size);
|
||||
width: var(--dbv-kit-icon-btn-size);
|
||||
height: var(--dbv-kit-icon-btn-size);
|
||||
font-size: calc(var(--dbv-kit-icon-btn-size) * 0.75);
|
||||
|
||||
border-radius: var(--dbv-kit-icon-btn-border-radius);
|
||||
border-color: var(--dbv-kit-icon-btn-border-color);
|
||||
border-width: var(--dbv-kit-icon-btn-border-width);
|
||||
border-style: var(--dbv-kit-icon-btn-border-style);
|
||||
|
||||
color: var(--dbv-kit-icon-btn-foreground);
|
||||
background-color: var(--dbv-kit-icon-btn-background);
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
&:focus-visible,
|
||||
&[data-focus-visible] {
|
||||
outline: var(--dbv-kit-icon-btn-outline-width) solid var(--dbv-kit-icon-btn-outline-color);
|
||||
outline-offset: var(--dbv-kit-icon-btn-outline-offset);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
color: var(--dbv-kit-icon-btn-foreground-hover);
|
||||
background-color: var(--dbv-kit-icon-btn-background-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&:active,
|
||||
&[data-active] {
|
||||
color: var(--dbv-kit-icon-btn-foreground-active);
|
||||
background-color: var(--dbv-kit-icon-btn-background-active);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: var(--dbv-kit-control-disabled-opacity);
|
||||
}
|
||||
|
||||
&[data-loading='true'] {
|
||||
background-color: var(--dbv-kit-icon-btn-background);
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&[data-active] {
|
||||
background-color: var(--dbv-kit-icon-btn-background-active);
|
||||
}
|
||||
}
|
||||
|
||||
.dbv-kit-spinner {
|
||||
--dbv-kit-spinner-stroke-color: var(--dbv-kit-icon-btn-loader-color);
|
||||
--dbv-kit-spinner-height: calc(var(--dbv-kit-icon-btn-size) * 0.8);
|
||||
--dbv-kit-spinner-stroke-width: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Size variants */
|
||||
.dbv-kit-icon-button--small {
|
||||
--dbv-kit-icon-btn-size: var(--dbv-kit-icon-btn-small-size);
|
||||
}
|
||||
|
||||
.dbv-kit-icon-button--medium {
|
||||
--dbv-kit-icon-btn-size: var(--dbv-kit-icon-btn-medium-size);
|
||||
}
|
||||
|
||||
.dbv-kit-icon-button--large {
|
||||
--dbv-kit-icon-btn-size: var(--dbv-kit-icon-btn-large-size);
|
||||
}
|
||||
|
||||
.dbv-kit-icon-button--xlarge {
|
||||
--dbv-kit-icon-btn-size: var(--dbv-kit-icon-btn-xlarge-size);
|
||||
}
|
||||
|
||||
/* Variant styles */
|
||||
.dbv-kit-icon-button--primary {
|
||||
--dbv-kit-icon-btn-foreground: var(--dbv-kit-icon-btn-primary-foreground);
|
||||
--dbv-kit-icon-btn-foreground-hover: var(--dbv-kit-icon-btn-primary-foreground-hover);
|
||||
--dbv-kit-icon-btn-foreground-active: var(--dbv-kit-icon-btn-primary-foreground-active);
|
||||
--dbv-kit-icon-btn-background: var(--dbv-kit-icon-btn-primary-background);
|
||||
--dbv-kit-icon-btn-background-hover: var(--dbv-kit-icon-btn-primary-background-hover);
|
||||
--dbv-kit-icon-btn-background-active: var(--dbv-kit-icon-btn-primary-background-active);
|
||||
--dbv-kit-icon-btn-loader-color: var(--dbv-kit-icon-btn-primary-foreground);
|
||||
}
|
||||
|
||||
.dbv-kit-icon-button--secondary {
|
||||
--dbv-kit-icon-btn-foreground: var(--dbv-kit-icon-btn-secondary-foreground);
|
||||
--dbv-kit-icon-btn-foreground-hover: var(--dbv-kit-icon-btn-secondary-foreground-hover);
|
||||
--dbv-kit-icon-btn-foreground-active: var(--dbv-kit-icon-btn-secondary-foreground-active);
|
||||
--dbv-kit-icon-btn-background: var(--dbv-kit-icon-btn-secondary-background);
|
||||
--dbv-kit-icon-btn-background-hover: var(--dbv-kit-icon-btn-secondary-background-hover);
|
||||
--dbv-kit-icon-btn-background-active: var(--dbv-kit-icon-btn-secondary-background-active);
|
||||
--dbv-kit-icon-btn-border-color: var(--dbv-kit-icon-btn-secondary-border-color);
|
||||
--dbv-kit-icon-btn-loader-color: var(--dbv-kit-icon-btn-secondary-loader-color);
|
||||
}
|
||||
|
||||
.dbv-kit-icon-button--danger {
|
||||
--dbv-kit-icon-btn-foreground: var(--dbv-kit-icon-btn-danger-foreground);
|
||||
--dbv-kit-icon-btn-foreground-hover: var(--dbv-kit-icon-btn-danger-foreground-hover);
|
||||
--dbv-kit-icon-btn-foreground-active: var(--dbv-kit-icon-btn-danger-foreground-active);
|
||||
--dbv-kit-icon-btn-background: var(--dbv-kit-icon-btn-danger-background);
|
||||
--dbv-kit-icon-btn-background-hover: var(--dbv-kit-icon-btn-danger-background-hover);
|
||||
--dbv-kit-icon-btn-background-active: var(--dbv-kit-icon-btn-danger-background-active);
|
||||
--dbv-kit-icon-btn-loader-color: var(--dbv-kit-icon-btn-danger-loader-color);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2025 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
import { componentProviderWrapper } from '../componentProviderWrapper.js';
|
||||
import { ButtonRoot, type ButtonRootProps } from '../Button/ButtonRoot.js';
|
||||
import type { ControlSize } from '../types/controls.js';
|
||||
|
||||
import './IconButton.css';
|
||||
|
||||
export interface IconButtonProps extends ButtonRootProps {
|
||||
'aria-label': string;
|
||||
variant?: 'primary' | 'secondary' | 'danger';
|
||||
size?: ControlSize;
|
||||
}
|
||||
|
||||
export function IconButtonBase({ className, variant = 'primary', size = 'medium', children, ...props }: IconButtonProps) {
|
||||
const classToApply = clsx('dbv-kit-icon-button', `dbv-kit-icon-button--${variant}`, `dbv-kit-icon-button--${size}`, className);
|
||||
|
||||
return (
|
||||
<ButtonRoot className={classToApply} {...props}>
|
||||
{children}
|
||||
</ButtonRoot>
|
||||
);
|
||||
}
|
||||
|
||||
export const IconButton = componentProviderWrapper('IconButton', IconButtonBase);
|
||||
@@ -0,0 +1,30 @@
|
||||
@layer base {
|
||||
:root {
|
||||
/* Icon Button border variables */
|
||||
--dbv-kit-icon-btn-border-radius: var(--dbv-kit-btn-border-radius);
|
||||
--dbv-kit-icon-btn-border-width: var(--dbv-kit-btn-border-width);
|
||||
--dbv-kit-icon-btn-border-style: var(--dbv-kit-btn-border-style);
|
||||
--dbv-kit-icon-btn-border-color: var(--dbv-kit-btn-border-color);
|
||||
|
||||
/* Icon Button outline variables */
|
||||
--dbv-kit-icon-btn-outline-width: var(--dbv-kit-control-outline-width);
|
||||
--dbv-kit-icon-btn-outline-style: var(--dbv-kit-control-outline-style);
|
||||
--dbv-kit-icon-btn-outline-color: var(--dbv-kit-control-outline-color);
|
||||
--dbv-kit-icon-btn-outline-offset: var(--dbv-kit-control-outline-offset);
|
||||
|
||||
/* Default size */
|
||||
--dbv-kit-icon-btn-size: var(--dbv-kit-control-height-medium);
|
||||
|
||||
/* Default colors */
|
||||
--dbv-kit-icon-btn-foreground: var(--tw-color-gray-600);
|
||||
--dbv-kit-icon-btn-foreground-hover: var(--tw-color-gray-600);
|
||||
--dbv-kit-icon-btn-foreground-active: var(--tw-color-gray-700);
|
||||
--dbv-kit-icon-btn-background: transparent;
|
||||
--dbv-kit-icon-btn-background-hover: var(--tw-color-gray-100);
|
||||
--dbv-kit-icon-btn-background-active: var(--tw-color-gray-200);
|
||||
|
||||
/* Loader color */
|
||||
--dbv-kit-btn-loader-color: var(--dbv-kit-btn-foreground-hover);
|
||||
--dbv-kit-btn-loader-base-color: transparent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
@layer base {
|
||||
:root {
|
||||
--dbv-kit-icon-btn-small-size: var(--dbv-kit-control-height-small);
|
||||
--dbv-kit-icon-btn-medium-size: var(--dbv-kit-control-height-medium);
|
||||
--dbv-kit-icon-btn-large-size: var(--dbv-kit-control-height-large);
|
||||
--dbv-kit-icon-btn-xlarge-size: var(--dbv-kit-control-height-xlarge);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
@layer base {
|
||||
:root {
|
||||
/* Primary */
|
||||
--dbv-kit-icon-btn-primary-foreground: var(--tw-color-gray-600);
|
||||
--dbv-kit-icon-btn-primary-foreground-hover: var(--dbv-kit-color-primary-600);
|
||||
--dbv-kit-icon-btn-primary-foreground-active: var(--dbv-kit-color-primary-700);
|
||||
--dbv-kit-icon-btn-primary-background: transparent;
|
||||
--dbv-kit-icon-btn-primary-background-hover: var(--tw-color-gray-100);
|
||||
--dbv-kit-icon-btn-primary-background-active: var(--tw-color-gray-200);
|
||||
--dbv-kit-icon-btn-primary-loader-color: var(--tw-color-gray-300);
|
||||
|
||||
/* Secondary */
|
||||
--dbv-kit-icon-btn-secondary-foreground: var(--tw-color-gray-500);
|
||||
--dbv-kit-icon-btn-secondary-foreground-hover: var(--dbv-kit-color-primary-600);
|
||||
--dbv-kit-icon-btn-secondary-foreground-active: var(--dbv-kit-color-primary-700);
|
||||
--dbv-kit-icon-btn-secondary-background: transparent;
|
||||
--dbv-kit-icon-btn-secondary-background-hover: transparent;
|
||||
--dbv-kit-icon-btn-secondary-background-active: transparent;
|
||||
--dbv-kit-icon-btn-secondary-border-color: transparent;
|
||||
--dbv-kit-icon-btn-secondary-loader-color: var(--tw-color-gray-300);
|
||||
|
||||
/* Danger */
|
||||
--dbv-kit-icon-btn-danger-foreground: var(--tw-color-red-600);
|
||||
--dbv-kit-icon-btn-danger-foreground-hover: var(--tw-color-red-700);
|
||||
--dbv-kit-icon-btn-danger-foreground-active: var(--tw-color-red-800);
|
||||
--dbv-kit-icon-btn-danger-background: transparent;
|
||||
--dbv-kit-icon-btn-danger-background-hover: var(--tw-color-red-100);
|
||||
--dbv-kit-icon-btn-danger-background-active: var(--tw-color-red-200);
|
||||
--dbv-kit-icon-btn-danger-loader-color: var(--tw-color-red-300);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
@import './_base.css';
|
||||
@import './_sizes.css';
|
||||
@import './_variants.css';
|
||||
|
||||
@layer base {
|
||||
.dbv-kit-input-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dbv-kit-input {
|
||||
background: var(--dbv-kit-input-background);
|
||||
border-radius: var(--dbv-kit-input-radius);
|
||||
border-color: var(--dbv-kit-input-border-color);
|
||||
border-width: var(--dbv-kit-input-border-width);
|
||||
border-style: var(--dbv-kit-input-border-style);
|
||||
color: var(--dbv-kit-input-foreground);
|
||||
padding-inline: var(--dbv-kit-input-padding);
|
||||
font-size: var(--dbv-kit-input-font-size);
|
||||
height: var(--dbv-kit-input-height);
|
||||
|
||||
&:not(:disabled):hover {
|
||||
border-color: var(--dbv-kit-color-control-hover);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: var(--dbv-kit-control-disabled-opacity);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--dbv-kit-input-placeholder-foreground);
|
||||
}
|
||||
|
||||
&:focus-visible,
|
||||
&[data-focus-visible] {
|
||||
outline: var(--dbv-kit-control-outline-width) solid var(--dbv-kit-control-outline-color);
|
||||
outline-offset: var(--dbv-kit-control-outline-offset);
|
||||
}
|
||||
}
|
||||
|
||||
.dbv-kit-input__title {
|
||||
color: var(--dbv-kit-input-label-foreground);
|
||||
padding-inline-start: var(--dbv-kit-input-label-padding);
|
||||
margin-block-end: var(--tw-spacing);
|
||||
}
|
||||
|
||||
.dbv-kit-input__title--required {
|
||||
&:after {
|
||||
content: '*';
|
||||
color: var(--dbv-kit-color-control-required);
|
||||
margin-inline-start: 0.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------- */
|
||||
/* Sizes */
|
||||
|
||||
.dbv-kit-input--small {
|
||||
--dbv-kit-input-height: var(--dbv-kit-input-small-height);
|
||||
--dbv-kit-input-padding: var(--dbv-kit-input-small-padding);
|
||||
--dbv-kit-input-font-size: var(--dbv-kit-input-small-font-size);
|
||||
}
|
||||
|
||||
.dbv-kit-input--large {
|
||||
--dbv-kit-input-height: var(--dbv-kit-input-large-height);
|
||||
--dbv-kit-input-padding: var(--dbv-kit-input-large-padding);
|
||||
--dbv-kit-input-font-size: var(--dbv-kit-input-large-font-size);
|
||||
}
|
||||
|
||||
.dbv-kit-input--xlarge {
|
||||
--dbv-kit-input-height: var(--dbv-kit-input-xlarge-height);
|
||||
--dbv-kit-input-padding: var(--dbv-kit-input-xlarge-padding);
|
||||
--dbv-kit-input-font-size: var(--dbv-kit-input-xlarge-font-size);
|
||||
}
|
||||
}
|
||||