diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index b547346..eeb62da 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,7 +1,7 @@ # 🤝 Contributing to XenSQL Thanks for considering contributing to **XenSQL**! 🎉 -We welcome all kinds of contributions - bug reports, feature ideas, documentation, code, translations, and more. +We welcome all kinds of contributions - bug reports, feature ideas, documentation, code, translations and more. --- @@ -11,7 +11,7 @@ We welcome all kinds of contributions - bug reports, feature ideas, documentatio 2. Follow the **[Development Setup](#-development-setup)** below 3. Create a branch: `git checkout -b feature/amazing-thing` 4. Make your changes -5. Test thoroughly (especially across SQLite, PostgreSQL, and MySQL) +5. Test thoroughly (especially across SQLite, PostgreSQL and MySQL) 6. Open a **Pull Request** using our [PR template](.github/PULL_REQUEST_TEMPLATE.md) --- @@ -62,7 +62,7 @@ task e2e:go:all task e2e:ui:all ``` -CI runs the frontend, API E2E (Go), and Go unit suites on every pull request. +CI runs the frontend, API E2E (Go) and Go unit suites on every pull request. **Playwright UI tests are not in CI** - run them locally before merging UI-facing work. See **[TESTING.md](./TESTING.md#ui-end-to-end-tests-playwright)** for details. diff --git a/.github/TESTING.md b/.github/TESTING.md index c834227..7b90355 100644 --- a/.github/TESTING.md +++ b/.github/TESTING.md @@ -114,14 +114,14 @@ Every test runs against all three engines (`postgres`, `mysql`, `mariadb`): - **Query execution** - DDL / DML / SELECT lifecycle, affected-row counts, empty result sets, error surfacing, value normalization (bigints past 2⁵³ → string, `NULL` → nil, binary → hex, timestamps → RFC3339), `RETURNING` -- **Results grid** - browse with pagination, sort, filter, and rejection of +- **Results grid** - browse with pagination, sort, filter and rejection of injection-style filters - **Editing data** - `InsertRow` (returns the generated key), `UpdateRow`, - `DeleteRows`, and the no-primary-key safety rule + `DeleteRows` and the no-primary-key safety rule - **Streaming** - batched row delivery, streaming table browse, multi-statement - scripts (one result set per statement), and stop-on-first-error + scripts (one result set per statement) and stop-on-first-error - **Transactions** - per-tab begin / commit / rollback, isolation from other - connections, guard rails, and independent concurrent transactions per tab + connections, guard rails and independent concurrent transactions per tab - **Query history** - success/error recording and clearing - **Export** - CSV / JSON / Markdown / SQL of live query results @@ -143,8 +143,8 @@ Every test runs against all three engines (`postgres`, `mysql`, `mariadb`): The Playwright suite drives the **real XenSQL UI in a browser** against the **real Go backend** in Wails v3 server mode (HTTP + WebSocket, no native window). It lives entirely under [`e2e/`](../e2e/) and covers connections, schema, queries, -transactions, table view, results grid, editor autocomplete, and app-shell toggles -across **PostgreSQL**, **MySQL**, **MariaDB**, and **SQLite**. +transactions, table view, results grid, editor autocomplete and app-shell toggles +across **PostgreSQL**, **MySQL**, **MariaDB** and **SQLite**. ### Requirements @@ -213,7 +213,7 @@ the matrix suites replay it on all four drivers. - **Editor tabs** - open (`+`), switch (`Ctrl+Tab` / `Ctrl+Shift+Tab`), close (`Ctrl+W` / ✕) - **Table view (browse)** - browse data; sort; scroll-paginate past the first 100 rows - **Table view (edit)** - inline edit, set NULL, mark row for delete, Reset / Apply - (verified against the DB), and undo / redo (`Ctrl+Z` / `Ctrl+Shift+Z`) + (verified against the DB) and undo / redo (`Ctrl+Z` / `Ctrl+Shift+Z`) - **Table view (data ops)** - filter by a condition; add a row via the Add-row dialog - **Cell viewer** - open a JSON cell with `Shift+Enter`, Beautify / Minify, Set to NULL - **JSON viewer** - `Ctrl+J` toggle, mirrors the focused row, key filter, JSON nesting diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a91f478..3ce3fd7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -66,7 +66,7 @@ jobs: # single top-level entry, so each archive is named - and # holds exactly one entry: the binary (linux/windows) or XenSQL.app (macOS). # Plain XenSQL / XenSQL.exe (no platform tokens), distro packages - # (XenSQL.deb, XenSQL.rpm, XenSQL.pkg.tar.zst, xensql-x86_64.AppImage), and + # (XenSQL.deb, XenSQL.rpm, XenSQL.pkg.tar.zst, xensql-x86_64.AppImage) and # XenSQL-amd64-installer.exe are manual-download only. - name: Build & package (Linux) diff --git a/README.md b/README.md index 1db8e79..b24a13a 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ wails3 dev ## ⚡ SQL tools are usually overkill. XenSQL isn’t. -Work with **SQLite**, **PostgreSQL**, and **MySQL / MariaDB** in a single fast desktop app that runs entirely on your machine. +Work with **SQLite**, **PostgreSQL** and **MySQL / MariaDB** in a single fast desktop app that runs entirely on your machine. 🧳 Portable ⚡ Fast startup @@ -96,7 +96,7 @@ Work with **SQLite**, **PostgreSQL**, and **MySQL / MariaDB** in a single fast d ## ⚡ What XenSQL is -XenSQL is a **desktop SQL client built for developers who want speed, clarity, and control**. +XenSQL is a **desktop SQL client built for developers who want speed, clarity and control**. It combines: @@ -137,7 +137,7 @@ XenSQL focuses on one thing: ## 🔌 Connections -- Create, edit, test, and manage database connections +- Create, edit, test and manage database connections - Organize into **folders** with drag-and-drop reorder - Per-connection **tab colors** - **Read-only mode** with defense-in-depth - blocked at both the Wails layer and inside each driver @@ -155,7 +155,7 @@ XenSQL focuses on one thing: - Driver-correct identifier quoting (PostgreSQL, MySQL, SQLite) - **Run selection** (`Ctrl+Enter`) / **run all** (`Ctrl+Shift+Enter`) / **stop** long-running queries - **Streaming results** - rows render as the driver yields them -- **Multi-statement scripts** - run several `;`-separated statements at once; they execute in order on one connection, so temp tables, `SET`, and scripted `BEGIN` / `COMMIT` hold +- **Multi-statement scripts** - run several `;`-separated statements at once; they execute in order on one connection, so temp tables, `SET` and scripted `BEGIN` / `COMMIT` hold - **Multiple result outputs** - a script or stored procedure that returns several result sets shows each in its own switchable result tab; a failing statement reports its error and stops the run - **Pinned transactions** per tab - run `BEGIN` / `COMMIT` / `ROLLBACK` as SQL or from the toolbar; queries run inside the open transaction until you commit or roll back - `UPDATE` / `DELETE` / `INSERT` with **`RETURNING`** flow back to the Results Grid @@ -195,7 +195,7 @@ View and modify table data directly in the grid - no hand-written `UPDATE` / `DE - **Browse** any table's rows (`Ctrl+double-click` a table in the Schema Explorer) - **Inline edit** cells in place - changes are staged, then applied on demand - **Insert** new rows and **bulk-delete** selected ones -- Safe by design: edits require a primary key, and **read-only** connections are blocked at both the Wails layer and inside the driver +- Safe by design: edits require a primary key and **read-only** connections are blocked at both the Wails layer and inside the driver - `INSERT` / `UPDATE` / `DELETE … RETURNING` results flow straight back into the grid --- @@ -203,7 +203,7 @@ View and modify table data directly in the grid - no hand-written `UPDATE` / `DE ## 📚 Query Library & History - **Saved queries** - name, filter, sort, link tabs with dirty-state tracking -- Save, update, rename, and delete from the sidebar or toolbar +- Save, update, rename and delete from the sidebar or toolbar - **Per-connection query history** with success/error and duration - Clear history per connection or delete individual entries @@ -223,18 +223,18 @@ View and modify table data directly in the grid - no hand-written `UPDATE` / `DE - Automatically checks for new versions on startup - Clean modal with changelog highlights - One-click download and update -- Supports Windows (MSI), macOS, and Linux builds +- Supports Windows (MSI), macOS and Linux builds --- ## 🌍 UX - **Dark & light** themes -- **English**, **Deutsch**, and **Български** +- **English**, **Deutsch** and **Български** - **Quick Search palette** (`Ctrl+P`) - jump to connections, saved queries, history, tabs - Custom shortcuts editor + keyboard tips - Frameless native title bar -- **Window state persistence** - size, position, and maximized state restored between sessions +- **Window state persistence** - size, position and maximized state restored between sessions --- @@ -273,7 +273,7 @@ XenSQL-data/ settings.json ``` -`settings.json` keeps your UI preferences - theme, language, layout, and keyboard shortcuts. +`settings.json` keeps your UI preferences - theme, language, layout and keyboard shortcuts. When the app sits somewhere writable, that folder is created **right next to the executable** (beside the `.app` bundle on macOS) - move it to a USB stick, network drive, or another PC and it just works. @@ -313,7 +313,7 @@ XenSQL uses **[Wails v3](https://v3.wails.io/)**, which embeds a web UI into a n ```bash go install github.com/wailsapp/wails/v3/cmd/wails3@latest -# wails3 dev installs frontend deps, generates bindings, and launches the app +# wails3 dev installs frontend deps, generates bindings and launches the app wails3 dev ``` diff --git a/Taskfile.yml b/Taskfile.yml index cb72824..58ca82e 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -162,8 +162,8 @@ tasks: summary: Run the Playwright UI suite (drives the app in Wails server mode; data dir ./e2e/XenSQL-data) dir: e2e cmds: - # e2e/playwright.config.ts builds the frontend, starts the server, and brings the - # database stack up (global-setup) on its own. + # e2e/playwright.config.ts builds the frontend, + # starts the server and brings the database stack up (global-setup) on its own. - npx playwright test e2e:ui:all: diff --git a/build/config.yml b/build/config.yml index 799d3e1..a3675d8 100644 --- a/build/config.yml +++ b/build/config.yml @@ -7,11 +7,11 @@ version: '3' info: companyName: "Bare7a" # The name of the company productName: "XenSQL" # The name of the application - productIdentifier: "com.bare7a.xensql" # The unique product identifier + productIdentifier: "eu.bare7a.xensql" # The unique product identifier description: "A fast, native SQL client built with Go, Wails and React." # The application description copyright: "(c) 2026, Bare7a" # Copyright text comments: "A fast, native SQL client built with Go, Wails and React." # Comments - version: "1.4.1" # The application version + version: "1.4.2" # The application version # cfBundleIconName: "appicon" # The macOS icon name in Assets.car icon bundles (optional) # # Should match the name of your .icon file without the extension # # If not set and Assets.car exists, defaults to "appicon" diff --git a/build/darwin/Info.dev.plist b/build/darwin/Info.dev.plist index 905ac51..df5da25 100644 --- a/build/darwin/Info.dev.plist +++ b/build/darwin/Info.dev.plist @@ -72,15 +72,15 @@ CFBundleIconFile icons CFBundleIdentifier - com.bare7a.xensql + eu.bare7a.xensql CFBundleName XenSQL CFBundlePackageType APPL CFBundleShortVersionString - 1.4.1 + 1.4.2 CFBundleVersion - 1.4.1 + 1.4.2 LSMinimumSystemVersion 12.0.0 NSAppTransportSecurity diff --git a/build/darwin/Info.plist b/build/darwin/Info.plist index 1b40743..0656e0a 100644 --- a/build/darwin/Info.plist +++ b/build/darwin/Info.plist @@ -72,15 +72,15 @@ CFBundleIconFile icons CFBundleIdentifier - com.bare7a.xensql + eu.bare7a.xensql CFBundleName XenSQL CFBundlePackageType APPL CFBundleShortVersionString - 1.4.1 + 1.4.2 CFBundleVersion - 1.4.1 + 1.4.2 LSMinimumSystemVersion 12.0.0 NSHighResolutionCapable diff --git a/build/ios/Info.dev.plist b/build/ios/Info.dev.plist index beacbef..72fce0a 100644 --- a/build/ios/Info.dev.plist +++ b/build/ios/Info.dev.plist @@ -9,15 +9,15 @@ CFBundleGetInfoString A fast, native SQL client built with Go, Wails and React. CFBundleIdentifier - com.bare7a.xensql.dev + eu.bare7a.xensql.dev CFBundleName XenSQL (Dev) CFBundlePackageType APPL CFBundleShortVersionString - 1.4.1-dev + 1.4.2-dev CFBundleVersion - 1.4.1 + 1.4.2 LSRequiresIPhoneOS MinimumOSVersion diff --git a/build/ios/Info.plist b/build/ios/Info.plist index 0c2fe22..a0e09f5 100644 --- a/build/ios/Info.plist +++ b/build/ios/Info.plist @@ -9,15 +9,15 @@ CFBundleGetInfoString A fast, native SQL client built with Go, Wails and React. CFBundleIdentifier - com.bare7a.xensql + eu.bare7a.xensql CFBundleName XenSQL CFBundlePackageType APPL CFBundleShortVersionString - 1.4.1 + 1.4.2 CFBundleVersion - 1.4.1 + 1.4.2 LSRequiresIPhoneOS MinimumOSVersion diff --git a/build/ios/build.sh b/build/ios/build.sh index 36df590..1558f79 100644 --- a/build/ios/build.sh +++ b/build/ios/build.sh @@ -3,7 +3,7 @@ set -e # Build configuration APP_NAME="XenSQL" -BUNDLE_ID="com.bare7a.xensql" +BUNDLE_ID="eu.bare7a.xensql" VERSION="1.2.1" BUILD_NUMBER="1.2.1" BUILD_DIR="build/ios" @@ -69,4 +69,4 @@ if [ "$TARGET" = "simulator" ]; then xcrun simctl install booted "$APP_BUNDLE" xcrun simctl launch booted "$BUNDLE_ID" echo "App launched on simulator" -fi \ No newline at end of file +fi diff --git a/build/ios/project.pbxproj b/build/ios/project.pbxproj index 54fa3ab..648fc8a 100644 --- a/build/ios/project.pbxproj +++ b/build/ios/project.pbxproj @@ -176,7 +176,7 @@ buildSettings = { INFOPLIST_FILE = main/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.bare7a.xensql"; + PRODUCT_BUNDLE_IDENTIFIER = "eu.bare7a.xensql"; PRODUCT_NAME = "XenSQL"; CODE_SIGNING_ALLOWED = NO; SDKROOT = iphoneos; @@ -188,7 +188,7 @@ buildSettings = { INFOPLIST_FILE = main/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.bare7a.xensql"; + PRODUCT_BUNDLE_IDENTIFIER = "eu.bare7a.xensql"; PRODUCT_NAME = "XenSQL"; CODE_SIGNING_ALLOWED = NO; SDKROOT = iphoneos; diff --git a/build/linux/nfpm/nfpm.yaml b/build/linux/nfpm/nfpm.yaml index ffb534d..f4e54ac 100644 --- a/build/linux/nfpm/nfpm.yaml +++ b/build/linux/nfpm/nfpm.yaml @@ -6,7 +6,7 @@ name: "XenSQL" arch: ${GOARCH} platform: "linux" -version: "1.4.1" +version: "1.4.2" section: "default" priority: "extra" maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}> diff --git a/build/windows/info.json b/build/windows/info.json index cba5755..29e5922 100644 --- a/build/windows/info.json +++ b/build/windows/info.json @@ -1,10 +1,10 @@ { "fixed": { - "file_version": "1.4.1" + "file_version": "1.4.2" }, "info": { "0000": { - "ProductVersion": "1.4.1", + "ProductVersion": "1.4.2", "CompanyName": "Bare7a", "FileDescription": "A fast, native SQL client built with Go, Wails and React.", "LegalCopyright": "(c) 2026, Bare7a", diff --git a/build/windows/msix/app_manifest.xml b/build/windows/msix/app_manifest.xml index 10e4d72..a969e3c 100644 --- a/build/windows/msix/app_manifest.xml +++ b/build/windows/msix/app_manifest.xml @@ -8,7 +8,7 @@ IgnorableNamespaces="uap3"> @@ -29,7 +29,7 @@ - + - + diff --git a/cmd/bump-version/main.go b/cmd/bump-version/main.go index b4f410a..4b05df3 100644 --- a/cmd/bump-version/main.go +++ b/cmd/bump-version/main.go @@ -36,7 +36,7 @@ var ( func main() { flag.Usage = func() { fmt.Fprintf(os.Stderr, "Usage: go run ./cmd/bump-version [flags] [version]\n\n") - fmt.Fprintf(os.Stderr, "Updates version.go, build/config.yml, and frontend metadata, then runs\n") + fmt.Fprintf(os.Stderr, "Updates version.go, build/config.yml and frontend metadata, then runs\n") fmt.Fprintf(os.Stderr, "wails3 task common:update:build-assets to refresh everything under build/.\n\n") fmt.Fprintf(os.Stderr, "Examples:\n") fmt.Fprintf(os.Stderr, " go run ./cmd/bump-version -minor\n") diff --git a/docker-compose.yml b/docker-compose.yml index c09e1b9..f8eb8ac 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,7 +36,7 @@ services: timeout: 5s retries: 20 tmpfs: - # Keep the data in RAM: faster, and nothing to clean up between runs. + # Keep the data in RAM: faster and nothing to clean up between runs. - /var/lib/postgresql/data mysql: diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..5162c22 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,380 @@ + + + + + + XenSQL - A fast, local-first SQL workbench + + + + + + + + + + + + + + + + +
+ + +
+
+
+ Go 1.26+ + Wails v3 + React + TypeScript + GPL-3.0 +
+

A fast, local-first SQL workbench

+

+ No cloud. No accounts. No telemetry.
+ Just a focused, native desktop tool for working with
+ SQLite, PostgreSQL and MySQL / MariaDB. +

+ +

Free & open source · Windows · macOS · Linux

+
+ XenSQL editor with schema-aware autocomplete, streaming results grid and JSON viewer +
+
+
+ + +
+
+
🧳 Portable
+
Fast startup
+
🔒 Local-first
+
🧠 Developer-focused
+
🔄 Auto-updating
+
+
+ + +
+
+

SQL tools are usually overkill. XenSQL isn't.

+

+ Most SQL tools today are heavy Electron apps, cloud-connected by default, tied to subscriptions, + and overloaded with features you don't use. XenSQL focuses on one thing: + a fast, local environment for working with databases. +

+
+
+

⚡ Native, not Electron

+

Built with Go and Wails v3, the UI runs in the OS webview - WebView2 on Windows, WebKit on macOS, WebKitGTK on Linux. No bundled Chromium.

+
+
+

🔒 Yours, entirely

+

Everything runs and stays on your machine. No account, no sign-in, no telemetry - your connections and queries never leave your computer.

+
+
+

🧳 Portable by design

+

All data lives in a single XenSQL-data/ folder next to the executable. Move it to a USB stick or another PC and it just works.

+
+
+

🧪 Battle-tested

+

Backed by an extensive Playwright end-to-end suite covering the editor, transactions, results grid, exports, schema explorer and table editing against real PostgreSQL, MySQL and MariaDB instances.

+
+
+
+
+ + +
+
+

Supported databases

+
+
+
🐘 PostgreSQL
+
    +
  • Read & write + read-only mode
  • +
  • SSL: require / verify-full
  • +
  • Powered by pgx
  • +
+
+
+
🐬 MySQL / MariaDB
+
    +
  • Read & write + read-only mode
  • +
  • TLS transport
  • +
  • Powered by go-sql-driver
  • +
+
+
+
🪶 SQLite
+
    +
  • Read & write + read-only mode
  • +
  • Local file with picker workflow
  • +
  • Pure-Go driver (modernc.org/sqlite)
  • +
+
+
+
+
+ + +
+
+

Everything you need. Nothing you don't.

+
+
+

🧠 SQL Editor

+

Tabbed Monaco-powered workspace with session restore, smart schema-aware autocomplete, snippets, SQL formatting, gutter run icons and remappable shortcuts.

+
+
+

📜 Multi-statement scripts

+

Run several ;-separated statements at once on a single connection - temp tables, SET and scripted transactions hold. Each result set gets its own tab.

+
+
+

📌 Pinned transactions

+

Run BEGIN / COMMIT / ROLLBACK as SQL or from the toolbar. Queries run inside the open transaction, per tab, until you commit or roll back.

+
+
+

🌊 Streaming results

+

Rows render as the driver yields them, in a virtualized grid that scrolls smoothly over thousands of rows. Stop long-running queries with one click.

+
+
+

🗃️ Schema Explorer

+

Tree view of schemas, tables and columns with instant search. Double-click a table for a SELECT, Ctrl+double-click to browse and edit its data.

+
+
+

✏️ Edit data in the grid

+

Inline-edit cells, insert rows and bulk-delete - no hand-written UPDATE. Changes are staged and applied on demand, guarded by primary keys and read-only mode.

+
+
+

🔍 Cell & JSON viewers

+

A cell editor for large values - JSON, XML, HTML, or plain text with beautify/minify - plus a side JSON row viewer with filter and regex search.

+
+
+

📤 One-click export

+

CSV, JSON, Markdown, or SQL INSERT. Export all or selected rows and columns, copy to clipboard or save to file. Remembers your last format.

+
+
+

📚 Query library & history

+

Save, rename, filter and reuse queries with dirty-state tracking, plus per-connection history with success/error status and duration.

+
+
+

🔌 Connection manager

+

Create, test and organize connections into drag-and-drop folders, with per-connection tab colors and a defense-in-depth read-only mode.

+
+
+

⌨️ Keyboard-first

+

Quick Search palette (Ctrl+P) jumps to connections, saved queries, history and tabs. Every shortcut is remappable in the in-app editor.

+
+
+

🌍 Polished UX

+

Dark & light themes, English / Deutsch / Български, frameless native title bar, window state persistence and one-click auto-updates with changelogs.

+
+
+
+
+ + +
+
+

Screenshots

+

Click any screenshot to view it full size.

+ +
+
+ + +
+
+

Download

+

+ No account required - just download and run. +

+
+
+
+ +

Windows

+
+ Portable .zip + Installer .exe +
+ Other options + +
+
+
+
+ +

macOS

+
+ Apple Silicon .zip + Intel .zip +
+ Other options + +
+
+ +
+

+ All builds and checksums are on the + latest release page. + Prefer to build it yourself? wails3 task build - see the + README. +

+
+
+ + +
+
+
+

🧳 Portable by design

+

+ Everything lives in a single XenSQL-data/ folder created right next to the + executable - connections, editor session, history, saved queries and settings. + Move it to a USB stick, a network drive, or another PC and it just works. +

+

+ If the app lives in a read-only location, it falls back to the OS per-user data + directory. You can always override the location with XENSQL_DATA_DIR. +

+
+
XenSQL(.exe)
+XenSQL-data/
+  connections.json
+  editor_session.json
+  query_history.json
+  saved_queries.json
+  settings.json
+
+
+ + +
+ +
+ + +
+
+

Ready to try XenSQL?

+

Free, open source and yours to keep - on Windows, macOS and Linux.

+ +
+
+ +
+ + + + + + + + + diff --git a/docs/script.js b/docs/script.js new file mode 100644 index 0000000..aec0257 --- /dev/null +++ b/docs/script.js @@ -0,0 +1,57 @@ +// Theme toggle (dark is the default; choice persists in localStorage) +(function () { + const root = document.documentElement; + const toggle = document.getElementById("theme-toggle"); + + toggle.addEventListener("click", function () { + const next = root.dataset.theme === "dark" ? "light" : "dark"; + root.dataset.theme = next; + try { + localStorage.setItem("xensql-theme", next); + } catch (e) {} + }); +})(); + +// Detect the visitor's OS: point the hero button at the right download +// and highlight the matching card in the download section. +(function () { + const ua = navigator.userAgent; + const os = /Mac|iPhone|iPad/i.test(ua) ? "macos" : /Linux/i.test(ua) && !/Android/i.test(ua) ? "linux" : /Win/i.test(ua) ? "windows" : null; + if (!os) return; + + const labels = { windows: "Download for Windows", macos: "Download for macOS", linux: "Download for Linux" }; + const label = document.getElementById("hero-download-label"); + if (label) label.textContent = labels[os]; + + const card = document.getElementById("dl-" + os); + if (card) card.classList.add("dl-detected"); +})(); + +// Screenshot lightbox +(function () { + const lightbox = document.getElementById("lightbox"); + const img = lightbox.querySelector("img"); + const caption = lightbox.querySelector(".lightbox-caption"); + + document.querySelectorAll(".gallery figure").forEach(function (fig) { + fig.querySelector("img").addEventListener("click", function (e) { + img.src = e.target.src; + img.alt = e.target.alt; + const cap = fig.querySelector("figcaption"); + caption.textContent = cap ? cap.textContent : ""; + lightbox.hidden = false; + document.body.style.overflow = "hidden"; + }); + }); + + function close() { + lightbox.hidden = true; + img.src = ""; + document.body.style.overflow = ""; + } + + lightbox.addEventListener("click", close); + document.addEventListener("keydown", function (e) { + if (e.key === "Escape" && !lightbox.hidden) close(); + }); +})(); diff --git a/docs/styles.css b/docs/styles.css new file mode 100644 index 0000000..eb1ae40 --- /dev/null +++ b/docs/styles.css @@ -0,0 +1,568 @@ +/* ---------- Theme tokens ---------- */ + +:root { + --font-sans: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif; + --font-mono: "JetBrains Mono", ui-monospace, "Cascadia Code", Consolas, monospace; + --accent: #3b82f6; + --accent-hover: #2563eb; + --radius: 12px; + --container: 1120px; +} + +html[data-theme="dark"] { + color-scheme: dark; + --bg: #0b0e14; + --bg-alt: #10141d; + --bg-card: #141926; + --bg-code: #0d1119; + --border: #232a3a; + --text: #e6e9f0; + --text-muted: #9aa3b5; + --header-bg: rgba(11, 14, 20, 0.85); + --shadow: 0 10px 40px rgba(0, 0, 0, 0.5); + --hero-glow: radial-gradient(ellipse 70% 50% at 50% -10%, rgba(59, 130, 246, 0.18), transparent); +} + +html[data-theme="light"] { + color-scheme: light; + --bg: #ffffff; + --bg-alt: #f5f7fb; + --bg-card: #ffffff; + --bg-code: #f0f3f8; + --border: #dde3ee; + --text: #1a2233; + --text-muted: #5a6579; + --header-bg: rgba(255, 255, 255, 0.85); + --shadow: 0 10px 40px rgba(30, 50, 100, 0.12); + --hero-glow: radial-gradient(ellipse 70% 50% at 50% -10%, rgba(59, 130, 246, 0.1), transparent); +} + +/* ---------- Base ---------- */ + +* { box-sizing: border-box; } + +html { scroll-behavior: smooth; } + +body { + margin: 0; + font-family: var(--font-sans); + background: var(--bg); + color: var(--text); + line-height: 1.6; + transition: background 0.25s ease, color 0.25s ease; +} + +img { max-width: 100%; display: block; } + +a { color: var(--accent); text-decoration: none; } +a:hover { text-decoration: underline; } + +code { + font-family: var(--font-mono); + font-size: 0.875em; + background: var(--bg-code); + border: 1px solid var(--border); + border-radius: 5px; + padding: 0.1em 0.4em; +} + +.container { + max-width: var(--container); + margin: 0 auto; + padding: 0 24px; +} + +h1, h2, h3 { line-height: 1.2; margin: 0 0 0.5em; text-wrap: balance; } +h2 { font-size: clamp(1.6rem, 3.5vw, 2.2rem); font-weight: 800; text-align: center; } + +/* Avoid single dangling words at the end of paragraphs */ +p, figcaption, li { text-wrap: pretty; } + +.accent { color: var(--accent); } + +.section { padding: 80px 0; } +.section-alt { background: var(--bg-alt); } + +.section-sub { + max-width: 720px; + margin: 0 auto 40px; + text-align: center; + color: var(--text-muted); + font-size: 1.05rem; + text-wrap: balance; +} + +/* ---------- Header ---------- */ + +.site-header { + position: sticky; + top: 0; + z-index: 50; + background: var(--header-bg); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border-bottom: 1px solid var(--border); +} + +.nav { + display: flex; + align-items: center; + justify-content: space-between; + height: 60px; +} + +.brand { + display: flex; + align-items: center; + gap: 10px; + font-weight: 800; + font-size: 1.15rem; + color: var(--text); +} +.brand:hover { text-decoration: none; } + +.nav-links { + display: flex; + align-items: center; + gap: 22px; +} + +.nav-links > a { + color: var(--text-muted); + font-weight: 500; + font-size: 0.95rem; +} +.nav-links > a:hover { color: var(--text); text-decoration: none; } + +.nav-github { + display: inline-flex; + align-items: center; + gap: 7px; +} + +.theme-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + width: 38px; + height: 38px; + border-radius: 10px; + border: 1px solid var(--border); + background: var(--bg-card); + color: var(--text); + cursor: pointer; + transition: border-color 0.15s ease; +} +.theme-toggle:hover { border-color: var(--accent); } + +html[data-theme="dark"] .icon-moon { display: none; } +html[data-theme="light"] .icon-sun { display: none; } + +/* ---------- Hero ---------- */ + +.hero { + position: relative; + padding: 90px 0 0; + background: var(--hero-glow); + text-align: center; + overflow: hidden; +} + +.hero-badges { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 10px; + margin-bottom: 28px; +} + +.badge { + font-family: var(--font-mono); + font-size: 0.78rem; + font-weight: 600; + padding: 5px 12px; + border-radius: 999px; + border: 1px solid var(--border); + background: var(--bg-card); + color: var(--text-muted); +} +.badge-license { color: var(--accent); border-color: var(--accent); } + +.hero h1 { + font-size: clamp(2.2rem, 6vw, 3.6rem); + font-weight: 800; + letter-spacing: -0.02em; + max-width: 800px; + margin: 0 auto 20px; +} + +.hero-sub { + font-size: clamp(1.05rem, 2vw, 1.25rem); + color: var(--text-muted); + max-width: 640px; + margin: 0 auto 32px; + text-wrap: balance; +} + +.hero-cta { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 14px; +} + +.hero-note { + margin-top: 16px; + font-size: 0.88rem; + color: var(--text-muted); +} + +.hero-shot { + margin-top: 56px; + padding: 0 12px; +} + +.hero-shot img { + margin: 0 auto; + max-width: 980px; + width: 100%; + border-radius: var(--radius) var(--radius) 0 0; + border: 1px solid var(--border); + border-bottom: none; + box-shadow: var(--shadow); +} + +/* ---------- Buttons ---------- */ + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 9px; + padding: 12px 26px; + border-radius: 10px; + font-weight: 600; + font-size: 1rem; + border: 1px solid transparent; + cursor: pointer; + transition: background 0.15s ease, border-color 0.15s ease, transform 0.1s ease; +} +.btn:hover { text-decoration: none; transform: translateY(-1px); } + +.btn-primary { + background: var(--accent); + color: #fff; +} +.btn-primary:hover { background: var(--accent-hover); } + +.btn-secondary { + background: var(--bg-card); + border-color: var(--border); + color: var(--text); +} +.btn-secondary:hover { border-color: var(--accent); } + +/* ---------- Strip ---------- */ + +.strip { + border-top: 1px solid var(--border); + border-bottom: 1px solid var(--border); + background: var(--bg-alt); +} + +.strip-inner { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px 40px; + padding: 20px 24px; +} + +.strip-item { + display: flex; + align-items: center; + gap: 9px; + font-weight: 600; + color: var(--text-muted); +} + +/* ---------- Why / feature cards ---------- */ + +.why-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 20px; +} + +.why-card, .feature-card, .db-card, .dl-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 24px; + transition: border-color 0.15s ease, transform 0.15s ease; +} + +.why-card:hover, .feature-card:hover, .db-card:hover { + border-color: var(--accent); + transform: translateY(-2px); +} + +.why-card h3, .feature-card h3 { font-size: 1.05rem; margin-bottom: 8px; } +.why-card p, .feature-card p { margin: 0; color: var(--text-muted); font-size: 0.94rem; } + +.feature-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 20px; +} + +/* ---------- Databases ---------- */ + +.db-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 20px; +} + +.db-name { font-weight: 700; font-size: 1.15rem; margin-bottom: 12px; } + +.db-card ul { + margin: 0; + padding-left: 20px; + color: var(--text-muted); + font-size: 0.94rem; +} +.db-card li { margin-bottom: 6px; } + +/* ---------- Gallery ---------- */ + +.gallery { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 20px; +} + +.gallery figure { margin: 0; } + +.gallery img { + width: 100%; + border-radius: 10px; + border: 1px solid var(--border); + cursor: zoom-in; + transition: transform 0.15s ease, border-color 0.15s ease; +} +.gallery img:hover { transform: scale(1.02); border-color: var(--accent); } + +.gallery figcaption { + margin-top: 8px; + text-align: center; + font-size: 0.88rem; + color: var(--text-muted); +} + +/* ---------- Download ---------- */ + +.dl-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 20px; +} + +.dl-card { display: flex; flex-direction: column; gap: 12px; } + +.dl-card.dl-detected { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); } + +.dl-head { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 4px; +} +.dl-head h3 { margin: 0; font-size: 1.2rem; } +.dl-head svg { color: var(--accent); flex-shrink: 0; } + +.dl-main { width: 100%; } + +/* Negative margins let the dividers span the card's full width */ +.dl-other { + margin: 6px -24px -14px; + padding-top: 14px; + border-top: 1px solid var(--border); +} + +.dl-other-title { + display: block; + font-size: 0.78rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text-muted); + margin-bottom: 8px; + padding: 0 24px; +} + +.dl-other ul { margin: 0; padding: 0; list-style: none; } + +.dl-other li + li { border-top: 1px solid var(--border); } + +.dl-other a { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 9px 24px; + font-size: 0.92rem; + color: var(--text); +} +.dl-other a:hover { color: var(--accent); background: var(--bg-alt); text-decoration: none; } + +.dl-other a span { + font-family: var(--font-mono); + font-size: 0.78rem; + color: var(--text-muted); + white-space: nowrap; +} +.dl-other a:hover span { color: var(--accent); } + +.dl-all { + margin-top: 32px; + text-align: center; + color: var(--text-muted); + font-size: 0.95rem; + text-wrap: balance; +} + +/* ---------- Portable ---------- */ + +.portable { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 48px; + align-items: center; +} + +.portable h2 { text-align: left; } +.portable-text p { color: var(--text-muted); } + +.code-block { + margin: 0; + padding: 24px; + background: var(--bg-code); + border: 1px solid var(--border); + border-radius: var(--radius); + font-family: var(--font-mono); + font-size: 0.9rem; + line-height: 1.7; + overflow-x: auto; +} +.code-block code { background: none; border: none; padding: 0; } + +@media (max-width: 800px) { + .portable { grid-template-columns: 1fr; gap: 28px; } + .portable h2 { text-align: center; } +} + +/* ---------- Tech stack ---------- */ + +.stack-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 14px; +} + +.stack-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 3px; + padding: 18px 12px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 10px; + color: var(--text); + text-align: center; + transition: border-color 0.15s ease, transform 0.15s ease; +} +.stack-item:hover { border-color: var(--accent); transform: translateY(-2px); text-decoration: none; } +.stack-item strong { font-size: 0.98rem; } +.stack-item span { font-size: 0.8rem; color: var(--text-muted); } + +/* ---------- Final CTA ---------- */ + +.cta-final { text-align: center; } + +/* ---------- Footer ---------- */ + +.site-footer { + border-top: 1px solid var(--border); + background: var(--bg-alt); + padding: 28px 0; +} + +.footer-inner { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.footer-brand { + display: flex; + align-items: center; + gap: 10px; + font-weight: 700; +} + +.footer-links { display: flex; flex-wrap: wrap; gap: 20px; } +.footer-links a { color: var(--text-muted); font-size: 0.92rem; } +.footer-links a:hover { color: var(--accent); } + +/* ---------- Lightbox ---------- */ + +.lightbox { + position: fixed; + inset: 0; + z-index: 100; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 14px; + padding: 40px; + background: rgba(5, 8, 14, 0.92); + cursor: zoom-out; +} + +.lightbox[hidden] { display: none; } + +.lightbox img { + max-width: 95vw; + max-height: 85vh; + border-radius: 8px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6); +} + +.lightbox-caption { color: #cbd3e1; margin: 0; font-size: 0.95rem; } + +.lightbox-close { + position: absolute; + top: 18px; + right: 26px; + background: none; + border: none; + color: #fff; + font-size: 2.4rem; + line-height: 1; + cursor: pointer; +} + +/* ---------- Responsive ---------- */ + +@media (max-width: 640px) { + .nav-links { gap: 14px; } + .nav-links > a:not(.nav-github) { display: none; } + .nav-github span { display: none; } + .section { padding: 56px 0; } + .hero { padding-top: 56px; } +} diff --git a/e2e/specs/editor/autocomplete.spec.ts b/e2e/specs/editor/autocomplete.spec.ts index 543c725..9181c81 100644 --- a/e2e/specs/editor/autocomplete.spec.ts +++ b/e2e/specs/editor/autocomplete.spec.ts @@ -15,7 +15,7 @@ test.describe('Editor autocomplete', () => { }); // SQLite so the test needs no database container. - test('suggests a seeded table, its columns, and the FK join condition', async ({ + test('suggests a seeded table, its columns and the FK join condition', async ({ connections, editor, schema, diff --git a/e2e/specs/editor/query-errors.spec.ts b/e2e/specs/editor/query-errors.spec.ts index 2fcbfc1..32dc8a8 100644 --- a/e2e/specs/editor/query-errors.spec.ts +++ b/e2e/specs/editor/query-errors.spec.ts @@ -1,7 +1,7 @@ import { POSTGRES } from '@support/databases'; import { expect, test } from '@support/fixtures'; -// Postgres-specific: only pgconn reports an error position, and these codes are Postgres SQLSTATEs. +// Postgres-specific: only pgconn reports an error position and these codes are Postgres SQLSTATEs. // The cross-driver smoke check lives in query-execution.spec.ts. test.describe('Query errors', () => { test('shows a structured error card: code, message and hint', async ({ app, connections, editor, results }) => { diff --git a/frontend/bindings/xensql/internal/app/models.ts b/frontend/bindings/xensql/internal/app/models.ts index 36ad6f6..7c28a22 100644 --- a/frontend/bindings/xensql/internal/app/models.ts +++ b/frontend/bindings/xensql/internal/app/models.ts @@ -10,6 +10,7 @@ export class AppInfo { "version": string; "author": string; "email": string; + "website": string; "repository": string; "description": string; @@ -27,6 +28,9 @@ export class AppInfo { if (!("email" in $$source)) { this["email"] = ""; } + if (!("website" in $$source)) { + this["website"] = ""; + } if (!("repository" in $$source)) { this["repository"] = ""; } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index fc114ee..1275739 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "xensql-frontend", - "version": "1.4.1", + "version": "1.4.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "xensql-frontend", - "version": "1.4.1", + "version": "1.4.2", "dependencies": { "@fontsource/fira-code": "^5.2.7", "@fontsource/inter": "^5.2.8", diff --git a/frontend/package.json b/frontend/package.json index 2497d2e..45da949 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,8 @@ { "name": "xensql-frontend", "private": true, - "version": "1.4.1", + "version": "1.4.2", + "homepage": "https://xensql.bare7a.eu", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/features/editor/lib/monacoTheme.ts b/frontend/src/features/editor/lib/monacoTheme.ts index 6c888ca..f23cf81 100644 --- a/frontend/src/features/editor/lib/monacoTheme.ts +++ b/frontend/src/features/editor/lib/monacoTheme.ts @@ -90,7 +90,7 @@ export function getMonacoThemeName(theme: AppTheme): string { return theme === 'light' ? XENSQL_MONACO_THEME_LIGHT : XENSQL_MONACO_THEME_DARK; } -/** Apply theme, layout, and re-tokenize after mount or when a panel becomes visible. */ +/** Apply theme, layout and re-tokenize after mount or when a panel becomes visible. */ export function syncMonacoEditorView(ed: editor.IStandaloneCodeEditor, theme: string): void { requestAnimationFrame(() => { monaco.editor.setTheme(theme); diff --git a/frontend/src/features/editor/lib/sqlCompletion.test.ts b/frontend/src/features/editor/lib/sqlCompletion.test.ts index 1aa5e6b..e8f02ef 100644 --- a/frontend/src/features/editor/lib/sqlCompletion.test.ts +++ b/frontend/src/features/editor/lib/sqlCompletion.test.ts @@ -161,7 +161,7 @@ describe('existing contexts are unchanged', () => { expect(columnLabels('UPDATE users SET ')).toEqual(expect.arrayContaining(ALL_COLS)); }); - it('after a completed SET assignment offers WHERE, and columns again after a comma', () => { + it('after a completed SET assignment offers WHERE and columns again after a comma', () => { expect(columnLabels('UPDATE users SET name = 1 ')).toEqual([]); expect(labelsOf('UPDATE users SET name = 1 ')).toContain('WHERE'); expect(columnLabels('UPDATE users SET name = 1, ')).toEqual(expect.arrayContaining(ALL_COLS)); @@ -177,7 +177,7 @@ describe('existing contexts are unchanged', () => { }); // Regression: a quoted table in FROM (`FROM "Users"`) broke WHERE completion - the range matched -// from its closing quote (hiding everything), and the table-ref needs a quoted filterText to match. +// from its closing quote (hiding everything) and the table-ref needs a quoted filterText to match. describe('capital/quoted table works in WHERE (range + filterText)', () => { const capTables: TableInfo[] = [{ schema: 'public', name: 'Users', type: 'table' }]; const capCtx = (): CompletionContext => ({ diff --git a/frontend/src/features/editor/lib/sqlSuggestions.ts b/frontend/src/features/editor/lib/sqlSuggestions.ts index bc91d89..77dbd8c 100644 --- a/frontend/src/features/editor/lib/sqlSuggestions.ts +++ b/frontend/src/features/editor/lib/sqlSuggestions.ts @@ -23,7 +23,7 @@ export interface CompletionItem { sortText?: string; // lower lex = higher in list } -// One rule per keyword: shape gate, engines, and whether it survives inside WHERE. +// One rule per keyword: shape gate, engines and whether it survives inside WHERE. interface KeywordRule { kw: string; drivers?: readonly DriverType[]; diff --git a/frontend/src/features/editor/lib/transactionControl.ts b/frontend/src/features/editor/lib/transactionControl.ts index a3cc731..36f8a25 100644 --- a/frontend/src/features/editor/lib/transactionControl.ts +++ b/frontend/src/features/editor/lib/transactionControl.ts @@ -4,7 +4,7 @@ export type TxnControlAction = 'begin' | 'commit' | 'rollback'; // Classifies `sql` when it is exactly one transaction-control statement - BEGIN / START TRANSACTION // / COMMIT / ROLLBACK and their WORK/TRANSACTION variants - ignoring comments, surrounding -// whitespace, and a single trailing semicolon. Returns null for anything else, so ordinary queries +// whitespace and a single trailing semicolon. Returns null for anything else, so ordinary queries // (and a control keyword bundled with other statements, or `ROLLBACK TO SAVEPOINT ...`, which must // run inside the transaction rather than end it) fall through to normal execution. export function detectTransactionControl(sql: string): TxnControlAction | null { diff --git a/frontend/src/features/sidebar/hooks/useSchemaTree.ts b/frontend/src/features/sidebar/hooks/useSchemaTree.ts index 74dc36d..7c5867d 100644 --- a/frontend/src/features/sidebar/hooks/useSchemaTree.ts +++ b/frontend/src/features/sidebar/hooks/useSchemaTree.ts @@ -198,7 +198,7 @@ export function useSchemaTree({ connId, connConnected, schemaList, schemaSearch fetchTableColumns, ]); - // Pre-warm while the user types: tables for every schema (so name matches surface), and columns + // Pre-warm while the user types: tables for every schema (so name matches surface) and columns // for non-matching table names (so column matches surface). Loaded/loading guards make re-runs no-ops. useEffect(() => { if (!schemaSearch || !connId) return; diff --git a/frontend/src/features/table-view/hooks/useTableViewPendingCells.ts b/frontend/src/features/table-view/hooks/useTableViewPendingCells.ts index e0504c4..b671145 100644 --- a/frontend/src/features/table-view/hooks/useTableViewPendingCells.ts +++ b/frontend/src/features/table-view/hooks/useTableViewPendingCells.ts @@ -13,7 +13,7 @@ interface UseTableViewPendingCellsArgs { /** * Pending-edit bookkeeping for the table-view grid: primary-key lookups, the cell display value - * (pending edit over raw), and the optimistic "edited" tint kept in lockstep with committed edits. + * (pending edit over raw) and the optimistic "edited" tint kept in lockstep with committed edits. */ export function useTableViewPendingCells({ rows, diff --git a/frontend/src/features/table-view/hooks/useTableViewPendingMutations.ts b/frontend/src/features/table-view/hooks/useTableViewPendingMutations.ts index dcff863..ce84fa7 100644 --- a/frontend/src/features/table-view/hooks/useTableViewPendingMutations.ts +++ b/frontend/src/features/table-view/hooks/useTableViewPendingMutations.ts @@ -52,7 +52,7 @@ interface UseTableViewPendingMutationsArgs { } /** - * Pending-change mutations for a table-view tab: cell edits, paste batches, and row delete toggles, + * Pending-change mutations for a table-view tab: cell edits, paste batches and row delete toggles, * each recorded as one undoable snapshot on shared undo/redo stacks. */ export function useTableViewPendingMutations({ diff --git a/frontend/src/features/table-view/lib/tableViewClipboard.test.ts b/frontend/src/features/table-view/lib/tableViewClipboard.test.ts index d11e4ee..b54f597 100644 --- a/frontend/src/features/table-view/lib/tableViewClipboard.test.ts +++ b/frontend/src/features/table-view/lib/tableViewClipboard.test.ts @@ -34,7 +34,7 @@ describe('parseClipboardGrid', () => { ]); }); - it('honors quoted fields containing the delimiter, quotes, and newlines', () => { + it('honors quoted fields containing the delimiter, quotes and newlines', () => { expect(parseClipboardGrid('"a,b","c""d","e\nf"')).toEqual([['a,b', 'c"d', 'e\nf']]); }); diff --git a/frontend/src/features/table-view/lib/tableViewClipboard.ts b/frontend/src/features/table-view/lib/tableViewClipboard.ts index 4cde98e..51e9ca5 100644 --- a/frontend/src/features/table-view/lib/tableViewClipboard.ts +++ b/frontend/src/features/table-view/lib/tableViewClipboard.ts @@ -19,9 +19,9 @@ export interface PasteCellEdit { * Parse clipboard text into a 2D grid of raw field strings. * * Delimiter detection prefers TAB - the de-facto spreadsheet clipboard format produced by Excel, - * Google Sheets, and our own "text" export - and only falls back to comma (our default CSV copy + * Google Sheets and our own "text" export - and only falls back to comma (our default CSV copy * format) when no tab is present. Both delimiters are parsed quote-aware in the RFC-4180 style: a - * field may be wrapped in double quotes to contain the delimiter or newlines, and `""` is an escaped + * field may be wrapped in double quotes to contain the delimiter or newlines and `""` is an escaped * quote. A single trailing blank line (from a trailing newline) is dropped. * * Empty fields are returned as `''`; NULL semantics are decided later in {@link computePasteEdits}. @@ -79,7 +79,7 @@ export function parseClipboardGrid(text: string): string[][] { /** * Offset a parsed clipboard grid onto target cells anchored at (anchorRow, anchorColPos) in display * coordinates - e.g. a 3×3 copy pasted at B2 fills B2:D4. Cells that fall past the loaded rows or the - * visible columns are dropped: we can't create rows here, and there's nothing to the right to fill. + * visible columns are dropped: we can't create rows here and there's nothing to the right to fill. * * Accepts the parsed clipboard (`string[][]`) or the captured copy buffer (`(string | null)[][]`, * NULLs preserved); a null or empty field both paste as NULL, matching single-cell paste. diff --git a/frontend/src/i18n/locales/bg.json b/frontend/src/i18n/locales/bg.json index c793598..67d653b 100644 --- a/frontend/src/i18n/locales/bg.json +++ b/frontend/src/i18n/locales/bg.json @@ -480,6 +480,7 @@ "title": "За {{name}}", "version": "Версия", "createdBy": "Създадено от", + "website": "Уебсайт", "repository": "Хранилище с изходен код", "stack": "Създадено с Go, Wails, React и Monaco Editor.", "checkUpdates": "Проверка за актуализации" diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index 0bccdb3..5c404cb 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -480,6 +480,7 @@ "title": "Über {{name}}", "version": "Version", "createdBy": "Erstellt von", + "website": "Webseite", "repository": "Quellcode-Repository", "stack": "Erstellt mit Go, Wails, React und Monaco Editor.", "checkUpdates": "Nach Updates suchen" diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 5871cce..d0eb49c 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -124,7 +124,7 @@ "cellViewerKind": "View content as", "copyResults": "Copy results to clipboard", "exportFile": "Save results to file", - "exportOptions": "Export with row, column, and format options", + "exportOptions": "Export with row, column and format options", "showHideColumns": "Show or hide columns", "resultsColumnHeader": "Click to sort · Ctrl+click to select column · Shift+click for range", "resultsRowGutter": "Click to focus row · Arrow keys move focus · Shift+arrows select rows · Ctrl+click to select", @@ -373,7 +373,7 @@ "postgres": "PostgreSQL", "mysql": "MySQL / MariaDB", "readOnly": "Read only", - "readOnlyHint": "Blocks INSERT, UPDATE, DELETE, DDL, and editing rows in the results grid.", + "readOnlyHint": "Blocks INSERT, UPDATE, DELETE, DDL and editing rows in the results grid.", "tabColor": "Tab Color", "file": "Database File", "filePlaceholder": "C:\\path\\to\\database.db", @@ -382,7 +382,7 @@ "databaseName": "Database name", "databasePlaceholder": "blog", "databasePlaceholderMysql": "myapp", - "databaseHint": "PostgreSQL database to open (e.g. blog). Not the connection name above, and not the default postgres admin database unless you mean that one.", + "databaseHint": "PostgreSQL database to open (e.g. blog). Not the connection name above and not the default postgres admin database unless you mean that one.", "databaseHintMysql": "MySQL or MariaDB database to connect to on the server.", "username": "Username", "password": "Password", @@ -480,6 +480,7 @@ "title": "About {{name}}", "version": "Version", "createdBy": "Created by", + "website": "Website", "repository": "Source repository", "stack": "Built with Go, Wails, React and Monaco Editor.", "checkUpdates": "Check for Updates" @@ -526,14 +527,14 @@ "editorRunAll": "Run the entire editor contents", "editorSave": "Save the active tab to the query library", "editorGutter": "Click the ▶ icon in the gutter to run a single statement", - "editorContext": "Right-click for cut, copy, paste, find, and format", + "editorContext": "Right-click for cut, copy, paste, find and format", "resultsTitle": "Results grid", "resultsCopy": "Copy selection, or the focused cell when nothing is selected", "resultsSort": "Click a column header to sort; Ctrl+click to select columns; Shift+click for a range", "resultsRow": "Click a row number to focus; arrow keys move focus; Shift+arrows select rows", "resultsCtrlClick": "Ctrl+click a row or cell to toggle row selection", "resultsDoubleClick": "Double-click a cell to view or edit the full value", - "resultsContext": "Right-click for copy, export, clear selection, and more", + "resultsContext": "Right-click for copy, export, clear selection and more", "resultsEsc": "Clear row/column selection", "schemaTitle": "Schema browser", "schemaClick": "Click a table to expand columns", diff --git a/frontend/src/shared/components/AboutDialog.tsx b/frontend/src/shared/components/AboutDialog.tsx index 4a542f0..ee39a4d 100644 --- a/frontend/src/shared/components/AboutDialog.tsx +++ b/frontend/src/shared/components/AboutDialog.tsx @@ -12,11 +12,11 @@ interface Props { export function AboutDialog({ info, onClose }: Props) { const { t } = useTranslation(); - const openRepo = () => { + const openUrl = (url: string) => { try { - Browser.OpenURL(info.repository); + Browser.OpenURL(url); } catch { - window.open(info.repository, '_blank', 'noopener,noreferrer'); + window.open(url, '_blank', 'noopener,noreferrer'); } }; @@ -43,10 +43,18 @@ export function AboutDialog({ info, onClose }: Props) { ) : null} +
+
{t('about.website')}
+
+ +
+
{t('about.repository')}
-
diff --git a/frontend/src/shared/hooks/useListKeyboardNav.ts b/frontend/src/shared/hooks/useListKeyboardNav.ts index 0993238..3910a69 100644 --- a/frontend/src/shared/hooks/useListKeyboardNav.ts +++ b/frontend/src/shared/hooks/useListKeyboardNav.ts @@ -57,7 +57,7 @@ export function useListKeyboardNav(): { return { onKeyDown }; } -// Activates a focusable row (role="button") on Enter/Space by triggering its onClick, and stops +// Activates a focusable row (role="button") on Enter/Space by triggering its onClick and stops // propagation so an enclosing useListKeyboardNav container does not also fire Enter (double activation). export function rowActivateKeyDown(e: KeyboardEvent) { if (e.key === 'Enter' || e.key === ' ') { diff --git a/frontend/src/shared/lib/appInfo.ts b/frontend/src/shared/lib/appInfo.ts index 859e135..81eb880 100644 --- a/frontend/src/shared/lib/appInfo.ts +++ b/frontend/src/shared/lib/appInfo.ts @@ -3,6 +3,7 @@ export interface AppInfo { version: string; author: string; email: string; + website: string; repository: string; description: string; } @@ -10,9 +11,10 @@ export interface AppInfo { /** Fallback when Go binding is unavailable (dev in browser). */ export const DEFAULT_APP_INFO: AppInfo = { name: 'XenSQL', - version: '1.4.1', + version: '1.4.2', author: 'Bare7a', email: 'bare7a@gmail.com', + website: 'https://xensql.bare7a.eu', repository: 'https://github.com/Bare7a/XenSQL', description: 'A fast, native SQL client built with Go, Wails and React.', }; diff --git a/frontend/src/shared/lib/exportResult.test.ts b/frontend/src/shared/lib/exportResult.test.ts index b295785..472efdb 100644 --- a/frontend/src/shared/lib/exportResult.test.ts +++ b/frontend/src/shared/lib/exportResult.test.ts @@ -95,7 +95,7 @@ describe('exportResultToText - csv', () => { }); describe('exportResultToText - markdown', () => { - it('writes header, separator, and escaped pipes', () => { + it('writes header, separator and escaped pipes', () => { const r = sample(); r.rows.push([4, 'a|b', 'c']); const lines = exportResultToText(r, 'markdown').split('\n'); @@ -377,7 +377,7 @@ describe('formatCellCopyValue', () => { expect(formatCellCopyValue(undefined)).toBe(''); }); - it('stringifies numbers, booleans, and strings', () => { + it('stringifies numbers, booleans and strings', () => { expect(formatCellCopyValue(0)).toBe('0'); expect(formatCellCopyValue(false)).toBe('false'); expect(formatCellCopyValue('hi')).toBe('hi'); diff --git a/frontend/src/shared/lib/normalize.test.ts b/frontend/src/shared/lib/normalize.test.ts index 62196f5..6d2ff4c 100644 --- a/frontend/src/shared/lib/normalize.test.ts +++ b/frontend/src/shared/lib/normalize.test.ts @@ -175,7 +175,7 @@ describe('normalizeQueryResult disambiguates duplicate columns', () => { }); describe('formatError', () => { - it('handles strings, Errors, and objects with .message', () => { + it('handles strings, Errors and objects with .message', () => { expect(formatError('boom')).toBe('boom'); expect(formatError(new Error('nope'))).toBe('nope'); expect(formatError({ message: 'huh' })).toBe('huh'); diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 91b91ad..f63c16d 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -11,7 +11,7 @@ export default defineConfig({ resolve: { alias: { // `@/` resolves to src/ - mirrors the tsconfig paths entry so editor, - // type checker, vite dev server, and the production build all agree + // type checker, vite dev server and the production build all agree // on what `@/features/...` means. '@': path.resolve(__dirname, 'src'), // `@bindings/` points at the Wails v3 generated bindings outside src/. diff --git a/internal/app/app.go b/internal/app/app.go index ba6aafb..ec07cef 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -158,6 +158,7 @@ type AppInfo struct { Version string `json:"version"` Author string `json:"author"` Email string `json:"email"` + Website string `json:"website"` Repository string `json:"repository"` Description string `json:"description"` } @@ -168,6 +169,7 @@ func (a *App) GetAppInfo() AppInfo { Version: Version, Author: "Bare7a", Email: "bare7a@gmail.com", + Website: "https://xensql.bare7a.eu", Repository: "https://github.com/Bare7a/XenSQL", Description: "A fast, native SQL client built with Go and Wails.", } diff --git a/internal/app/app_updater.go b/internal/app/app_updater.go index 4551f02..2751ba6 100644 --- a/internal/app/app_updater.go +++ b/internal/app/app_updater.go @@ -37,7 +37,7 @@ func (a *App) startBackgroundUpdateCheck() { // runBackgroundUpdateCheck checks without opening any window. When a newer // release is found it emits "update-available" so the UI can show a dismissible -// toast; offline, errored, up-to-date, and skipped cases all stay silent. +// toast; offline, errored, up-to-date and skipped cases all stay silent. func (a *App) runBackgroundUpdateCheck() { select { case <-time.After(startupCheckDelay): diff --git a/internal/app/e2e_connections_test.go b/internal/app/e2e_connections_test.go index 64a0d4e..56d8bcf 100644 --- a/internal/app/e2e_connections_test.go +++ b/internal/app/e2e_connections_test.go @@ -9,7 +9,7 @@ import ( ) // TestE2ETestConnection covers the "Test connection" button: a good config pings -// successfully, and bad host/credentials fail fast instead of hanging. +// successfully and bad host/credentials fail fast instead of hanging. func TestE2ETestConnection(t *testing.T) { for _, e := range allEngines() { e := e diff --git a/internal/app/e2e_grid_test.go b/internal/app/e2e_grid_test.go index 57b27a3..023483a 100644 --- a/internal/app/e2e_grid_test.go +++ b/internal/app/e2e_grid_test.go @@ -18,7 +18,7 @@ func queryTable(t *testing.T, a *App, connID string, req database.TableDataReque } // TestE2EQueryTable covers the Results Grid "browse table" path: pagination, -// sorting, filtering, and the primary-key metadata that decides whether the grid +// sorting, filtering and the primary-key metadata that decides whether the grid // is editable. func TestE2EQueryTable(t *testing.T) { forEachEngine(t, func(t *testing.T, a *App, e engine, connID string) { @@ -98,7 +98,7 @@ func TestE2EQueryTable(t *testing.T) { } // TestE2EGridEditing covers the no-SQL editing flow: InsertRow (which returns the -// full row including the generated key), UpdateRow, and DeleteRows. +// full row including the generated key), UpdateRow and DeleteRows. func TestE2EGridEditing(t *testing.T) { forEachEngine(t, func(t *testing.T, a *App, e engine, connID string) { table := uniqueTable("edit") diff --git a/internal/app/e2e_helpers_test.go b/internal/app/e2e_helpers_test.go index 7110742..e2527ea 100644 --- a/internal/app/e2e_helpers_test.go +++ b/internal/app/e2e_helpers_test.go @@ -169,7 +169,7 @@ var ( // reachable reports whether the engine's server accepts a connection, so the // suite can skip engines that aren't up instead of failing. The result is probed // once per engine and cached: the first call absorbs a slow-starting server (a -// few retries), and every later call across the suite is instant. +// few retries) and every later call across the suite is instant. func reachable(e engine) error { reachMu.Lock() defer reachMu.Unlock() diff --git a/internal/app/e2e_history_export_test.go b/internal/app/e2e_history_export_test.go index e59c305..7118b78 100644 --- a/internal/app/e2e_history_export_test.go +++ b/internal/app/e2e_history_export_test.go @@ -8,7 +8,7 @@ import ( ) // TestE2EQueryHistory checks that ExecuteQuery records per-connection history with -// success/error state, and that history can be cleared - the Query History panel. +// success/error state and that history can be cleared - the Query History panel. func TestE2EQueryHistory(t *testing.T) { forEachEngine(t, func(t *testing.T, a *App, e engine, connID string) { // Clear any history left by other subtests on this fresh app+connection. diff --git a/internal/app/e2e_query_test.go b/internal/app/e2e_query_test.go index 877a9c3..59a9696 100644 --- a/internal/app/e2e_query_test.go +++ b/internal/app/e2e_query_test.go @@ -104,7 +104,7 @@ func TestE2EErrorSurfacing(t *testing.T) { } // TestE2EValueNormalization checks the type coercions that protect the frontend: -// integers past JS's safe range become strings, NULL becomes nil, and binary is +// integers past JS's safe range become strings, NULL becomes nil and binary is // hex-encoded. Timestamps and JSON are checked per-driver where syntax differs. func TestE2EValueNormalization(t *testing.T) { forEachEngine(t, func(t *testing.T, a *App, e engine, connID string) { diff --git a/internal/app/e2e_schema_test.go b/internal/app/e2e_schema_test.go index 170cf44..e0d514c 100644 --- a/internal/app/e2e_schema_test.go +++ b/internal/app/e2e_schema_test.go @@ -9,9 +9,9 @@ import ( ) // TestE2ESchemaExplorer covers the Schema Explorer surface: LoadSchemaData (the -// single call the UI makes on connect), ListSchemas/ListTables/ListColumns, and +// single call the UI makes on connect), ListSchemas/ListTables/ListColumns and // primary/foreign-key + nullable detection - the metadata that drives the tree, -// editable-grid gating, and autocomplete. +// editable-grid gating and autocomplete. func TestE2ESchemaExplorer(t *testing.T) { forEachEngine(t, func(t *testing.T, a *App, e engine, connID string) { parent := uniqueTable("authors") diff --git a/internal/app/e2e_txn_test.go b/internal/app/e2e_txn_test.go index 3eba286..2fd55c1 100644 --- a/internal/app/e2e_txn_test.go +++ b/internal/app/e2e_txn_test.go @@ -94,7 +94,7 @@ func TestE2ETransactionRollback(t *testing.T) { } // TestE2ETransactionGuards checks the guard rails: a tab id is required, a tab can -// only hold one transaction, commit needs an open transaction, and closing a tab +// only hold one transaction, commit needs an open transaction and closing a tab // rolls back its open transaction. func TestE2ETransactionGuards(t *testing.T) { forEachEngine(t, func(t *testing.T, a *App, e engine, connID string) { diff --git a/internal/app/version.go b/internal/app/version.go index eb1ffa7..4ab8023 100644 --- a/internal/app/version.go +++ b/internal/app/version.go @@ -3,4 +3,4 @@ package app // Version is the source of truth for the application version. // In order to update it use the following command: // go run ./cmd/bump-version [-major, -minor, -patch, 1.2.0] -const Version = "1.4.1" +const Version = "1.4.2" diff --git a/internal/database/driver.go b/internal/database/driver.go index 4c27e60..50dfdc9 100644 --- a/internal/database/driver.go +++ b/internal/database/driver.go @@ -18,7 +18,7 @@ type Session interface { Execute(ctx context.Context, sql string) (*QueryResult, error) // Returned QueryResult carries metadata only; rows are delivered via OnBatch. Non-SELECT acts like Execute. ExecuteStream(ctx context.Context, sql string, opts StreamOpts) (*QueryResult, error) - // BeginTxn checks out a dedicated connection, issues BEGIN, and returns a PinnedTxn that must be + // BeginTxn checks out a dedicated connection, issues BEGIN and returns a PinnedTxn that must be // Committed or Rolled back by the caller, then Closed. BeginTxn(ctx context.Context) (PinnedTxn, error) // PinnedConn checks out a dedicated connection (with the session's setup applied) for running a diff --git a/internal/database/readonly.go b/internal/database/readonly.go index aeeb66d..89fc246 100644 --- a/internal/database/readonly.go +++ b/internal/database/readonly.go @@ -107,7 +107,7 @@ func IsReadOnlySQL(sql string) bool { return IsReadOnlySQLFor("", sql) } -// IsReadOnlySQLFor strips # comments only for MySQL - on Postgres # is an operator, and stripping +// IsReadOnlySQLFor strips # comments only for MySQL - on Postgres # is an operator and stripping // to end-of-line could hide a following write from the classifier. func IsReadOnlySQLFor(driver DriverType, sql string) bool { cleaned := maskStringLiterals(stripSQLComments(sql, driver == DriverMySQL)) @@ -290,7 +290,7 @@ func emitSpan(sql string, lo, hi int, mask bool, b *strings.Builder) { } // scanQuoted consumes the quoted span at sql[i] (doubled-quote escape), writing spaces when mask is -// true else copying verbatim, and returns the index past the closing quote. +// true else copying verbatim and returns the index past the closing quote. func scanQuoted(sql string, i int, quote byte, mask bool, b *strings.Builder) int { end := quotedSpanEnd(sql, i, quote) emitSpan(sql, i, end, mask, b) diff --git a/internal/database/session_base.go b/internal/database/session_base.go index efb335a..5b2db86 100644 --- a/internal/database/session_base.go +++ b/internal/database/session_base.go @@ -7,9 +7,9 @@ import ( ) // SessionBase implements the Session methods whose behaviour is identical across drivers: -// lifecycle, read-only gating, schema defaulting, statement execution, table browsing, and row +// lifecycle, read-only gating, schema defaulting, statement execution, table browsing and row // mutations. Driver sessions embed it and keep only catalog discovery (ListSchemas / ListTables / -// ListColumns), InsertRow, and ConnectionInfo driver-specific. +// ListColumns), InsertRow and ConnectionInfo driver-specific. type SessionBase struct { DB *sql.DB Driver DriverType diff --git a/internal/database/util.go b/internal/database/util.go index 7e835f1..3e7c185 100644 --- a/internal/database/util.go +++ b/internal/database/util.go @@ -82,7 +82,7 @@ func collectStream(run func(StreamOpts) (*QueryResult, error)) (*QueryResult, er } // ScanRowsStream is the single row-scan loop every read path funnels through: it normalizes -// values, checks ctx periodically, and delivers rows to OnBatch in BatchSize chunks. +// values, checks ctx periodically and delivers rows to OnBatch in BatchSize chunks. func ScanRowsStream(ctx context.Context, rows *sql.Rows, opts StreamOpts) (int64, error) { batchSize := opts.BatchSize if batchSize <= 0 {