14 Commits

Author SHA1 Message Date
81869eb6d7 docs: update CHANGELOG for version 1.5.3, detailing CI/CD updates and UI/UX improvements
Some checks failed
OSV-Scanner Scheduled Scan / scan-scheduled (push) Successful in 21s
CI / build-frontend (push) Successful in 46s
Build and Release / build (push) Has been cancelled
CI / scan-backend (push) Successful in 9m19s
CI / build-backend (push) Successful in 24s
Build and Publish Docker Image / build (push) Successful in 10m37s
2025-12-31 15:37:53 -06:00
ab39ddca15 fix: pass VITE_APP_VERSION as an argument during frontend build in Dockerfile 2025-12-31 15:37:44 -06:00
579dc721bc chore: update version to 1.5.3 in package.json and service worker 2025-12-31 15:37:38 -06:00
4c83b97d60 feat: implement version determination logic for frontend and Docker workflows, enhancing VITE_APP_VERSION usage 2025-12-31 15:36:57 -06:00
a5e7784048 feat: enhance VITE_APP_VERSION handling in Taskfile for frontend, backend, and Docker builds 2025-12-31 15:36:50 -06:00
298b62fdc4 chore: simplify VITE_APP_VERSION handling in package.json scripts 2025-12-31 15:36:40 -06:00
936a7e51c3 feat: improve footer to display version information with conditional link to repository 2025-12-31 15:36:35 -06:00
e6cf656556 chore: update dependencies in pnpm-lock.yaml to latest versions
All checks were successful
OSV-Scanner Scheduled Scan / scan-scheduled (push) Successful in 21s
CI / build-frontend (push) Successful in 43s
CI / scan-backend (push) Successful in 9m21s
CI / build-backend (push) Successful in 22s
2025-12-31 15:18:23 -06:00
74e0bd403e feat: add Trivy download and SBOM generation to build workflow 2025-12-31 15:16:46 -06:00
c14dc18a65 chore: remove SBOM generation workflow and associated output files 2025-12-31 15:16:38 -06:00
e9eb07ef52 Merge pull request 'Update https://git.quad4.io/actions/checkout action to v6' (#18) from renovate/https-git.quad4.io-actions-checkout-6.x into master
All checks were successful
CI / scan-backend (push) Successful in 12s
OSV-Scanner Scheduled Scan / scan-scheduled (push) Successful in 9m23s
CI / build-frontend (push) Successful in 9m38s
CI / build-backend (push) Successful in 9m25s
Reviewed-on: #18
2025-12-31 21:06:51 +00:00
5ccc6846a7 feat: add settings modal and grid customization options to IdentityGraph component
All checks were successful
OSV-Scanner Scheduled Scan / scan-scheduled (push) Successful in 22s
CI / build-frontend (push) Successful in 43s
CI / scan-backend (push) Successful in 9m18s
CI / build-backend (push) Successful in 21s
2025-12-31 08:58:22 -06:00
7e3c8e2b79 Auto-update SBOM [skip ci] 2025-12-31 14:46:32 +00:00
Renovate Bot
232d62e5f9 Update https://git.quad4.io/actions/checkout action to v6
All checks were successful
OSV-Scanner PR Scan / scan-pr (pull_request) Successful in 17s
2025-12-31 00:01:37 +00:00
16 changed files with 347 additions and 21566 deletions

View File

@@ -18,7 +18,7 @@ jobs:
contents: write
steps:
- name: Checkout
uses: https://git.quad4.io/actions/checkout@a5b3063b1edaa6ba4911c8a1b1d5e1656fba3ea5 # v4
uses: https://git.quad4.io/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with:
fetch-depth: 0
@@ -27,8 +27,11 @@ jobs:
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT
else
elif [[ "${{ github.ref }}" == refs/tags/* ]]; then
echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
else
SHORT_SHA=$(git rev-parse --short HEAD)
echo "version=${SHORT_SHA}" >> $GITHUB_OUTPUT
fi
- name: Setup Node.js
@@ -50,6 +53,8 @@ jobs:
- name: Build frontend
run: task build:frontend
env:
VITE_APP_VERSION: ${{ steps.version.outputs.version }}
- name: Setup Go
uses: https://git.quad4.io/actions/setup-go@f50900cd786a0c549eed5a472b4f2c371ae8589f # v5
@@ -68,6 +73,16 @@ jobs:
- name: Build desktop Windows
run: task desktop-windows
- name: Download Trivy
run: |
curl -L -o /tmp/trivy.deb https://git.quad4.io/Quad4-Extra/assets/raw/commit/90fdcea1bb71d91df2de6ff2e3897f278413f300/bin/trivy_0.68.2_Linux-64bit.deb
sudo dpkg -i /tmp/trivy.deb || sudo apt-get install -f -y
- name: Generate SBOM (CycloneDX)
run: |
mkdir -p release-assets
trivy fs --format cyclonedx --include-dev-deps --output release-assets/sbom.cyclonedx.json .
- name: Prepare release assets
run: |
mkdir -p release-assets
@@ -80,14 +95,6 @@ jobs:
cp desktop/build/bin/linking-tool.exe release-assets/linking-tool-desktop-windows-amd64.exe
fi
- name: Download SBOM
run: |
git fetch origin master:master || true
git checkout master -- sbom/ || git checkout ${{ github.sha }} -- sbom/ || true
if [ -d sbom ]; then
cp sbom/*.json release-assets/ || true
fi
- name: Create Release
uses: https://git.quad4.io/actions/gitea-release-action@4875285c0950474efb7ca2df55233c51333eeb74 # v1
with:

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: https://git.quad4.io/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: https://git.quad4.io/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Setup Node.js
uses: https://git.quad4.io/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
@@ -29,8 +29,16 @@ jobs:
run: task lint
- name: Frontend checks
run: task check
- name: Determine version
id: version
run: |
SHORT_SHA=$(git rev-parse --short HEAD)
echo "version=${SHORT_SHA}" >> $GITHUB_OUTPUT
- name: Build frontend
run: task build:frontend
env:
VITE_APP_VERSION: ${{ steps.version.outputs.version }}
- name: Upload frontend assets
uses: https://git.quad4.io/actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5 # v3.2.1
with:
@@ -41,7 +49,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: https://git.quad4.io/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: https://git.quad4.io/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Setup Go
uses: https://git.quad4.io/actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
@@ -54,7 +62,7 @@ jobs:
needs: [build-frontend, scan-backend]
steps:
- name: Checkout
uses: https://git.quad4.io/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: https://git.quad4.io/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Download frontend assets
uses: https://git.quad4.io/actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
with:

View File

@@ -22,7 +22,19 @@ jobs:
steps:
- name: Checkout repository
uses: https://git.quad4.io/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: https://git.quad4.io/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0
- name: Determine version
id: version
run: |
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
else
SHORT_SHA=$(git rev-parse --short HEAD)
echo "version=${SHORT_SHA}" >> $GITHUB_OUTPUT
fi
- name: Set up QEMU
uses: https://git.quad4.io/actions/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
@@ -62,3 +74,5 @@ jobs:
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
VITE_APP_VERSION=${{ steps.version.outputs.version }}

View File

@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: https://git.quad4.io/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: https://git.quad4.io/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Setup Go
uses: https://git.quad4.io/actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5

View File

@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: https://git.quad4.io/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: https://git.quad4.io/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Setup Go
uses: https://git.quad4.io/actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5

View File

@@ -1,62 +0,0 @@
name: Generate SBOM
on:
push:
tags:
- 'v*'
workflow_dispatch:
jobs:
generate-sbom:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: https://git.quad4.io/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0
ref: ${{ github.ref }}
- name: Setup Node.js
uses: https://git.quad4.io/actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
node-version: 22
cache: pnpm
- name: Setup Go
uses: https://git.quad4.io/actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: '1.25.5'
- name: Setup Task
uses: https://git.quad4.io/actions/setup-task@0ab1b2a65bc55236a3bc64cde78f80e20e8885c2 # v1
with:
version: '3.46.3'
- name: Setup environment
run: task setup
- name: Install dependencies
run: task install:ci
- name: Download Trivy
run: |
curl -L -o /tmp/trivy.deb https://git.quad4.io/Quad4-Extra/assets/raw/commit/90fdcea1bb71d91df2de6ff2e3897f278413f300/bin/trivy_0.68.2_Linux-64bit.deb
sudo dpkg -i /tmp/trivy.deb || sudo apt-get install -f -y
- name: Generate SBOM
run: |
mkdir -p sbom
trivy fs --format spdx-json --include-dev-deps --output sbom/sbom.spdx.json .
trivy fs --format cyclonedx --include-dev-deps --output sbom/sbom.cyclonedx.json .
- name: Commit and Push Changes
run: |
git config --global user.name "Gitea Action"
git config --global user.email "actions@noreply.quad4.io"
git remote set-url origin https://${{ secrets.GITEA_TOKEN }}@git.quad4.io/${{ github.repository }}.git
git fetch origin master
git checkout master
git add sbom/
git diff --quiet && git diff --staged --quiet || (git commit -m "Auto-update SBOM [skip ci]" && git push origin master)
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}

View File

@@ -1,5 +1,16 @@
# Changelog
## 1.5.3 - 2025-12-31
### CI/CD Updates
- Moved SBOM generation from `sbom.yml` workflow to `build.yml` workflow as release assets instead of auto-committing to source code
- Removed SPDX format, now only generating CycloneDX SBOM format (more popular and security-focused)
### UI/UX
- Updated version display logic: tag builds show tag version (e.g., `v1.5.2`), branch builds show commit SHA (e.g., `abc1234`), local dev shows `dev`
## 1.5.2 - 2025-12-31
### Features
@@ -47,7 +58,7 @@
- Mass selection improvements (moving and linking multiple nodes at once).
- Codebase refactor to use Svelte 5 Runes.
- Mobile improvements
- Added SBOM generation, see `/sbom/` for the generated SBOMs.
- Added SBOM generation as release assets
### Dependency Updates

View File

@@ -19,7 +19,19 @@ tasks:
build:frontend:
desc: Build frontend only
cmds:
- pnpm run build
- |
if [ -z "$VITE_APP_VERSION" ]; then
if git rev-parse --git-dir > /dev/null 2>&1; then
if git describe --tags --exact-match HEAD > /dev/null 2>&1; then
VITE_APP_VERSION=$(git describe --tags --exact-match HEAD)
else
VITE_APP_VERSION=$(git rev-parse --short HEAD)
fi
else
VITE_APP_VERSION=$(node -p "require('./package.json').version")
fi
fi
VITE_APP_VERSION="$VITE_APP_VERSION" pnpm run build
build:backend:
desc: Build backend binary only
@@ -31,7 +43,19 @@ tasks:
desc: Build the single binary web server
cmds:
- pnpm install
- pnpm run build
- |
if [ -z "$VITE_APP_VERSION" ]; then
if git rev-parse --git-dir > /dev/null 2>&1; then
if git describe --tags --exact-match HEAD > /dev/null 2>&1; then
VITE_APP_VERSION=$(git describe --tags --exact-match HEAD)
else
VITE_APP_VERSION=$(git rev-parse --short HEAD)
fi
else
VITE_APP_VERSION=$(node -p "require('./package.json').version")
fi
fi
VITE_APP_VERSION="$VITE_APP_VERSION" pnpm run build
- mkdir -p {{.BUILD_DIR}}
- CGO_ENABLED=0 go build -ldflags="-s -w" -o {{.BUILD_DIR}}/{{.BINARY_NAME}} main.go
@@ -93,9 +117,14 @@ tasks:
- GOOS=freebsd GOARCH=amd64 go build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-freebsd-amd64 main.go
docker-build:
desc: Build Docker image
desc: Build Docker image (VITE_APP_VERSION env var will be passed as build arg if set)
cmds:
- docker build -f docker/Dockerfile -t {{.BINARY_NAME}} .
- |
if [ -n "$VITE_APP_VERSION" ]; then
docker build --build-arg VITE_APP_VERSION="$VITE_APP_VERSION" -f docker/Dockerfile -t {{.BINARY_NAME}} .
else
docker build -f docker/Dockerfile -t {{.BINARY_NAME}} .
fi
docker-run:
desc: Run Docker container

View File

@@ -1,5 +1,6 @@
# Stage 1: Build the frontend
FROM cgr.dev/chainguard/node:latest-dev AS node-builder
ARG VITE_APP_VERSION
WORKDIR /app
USER root
RUN corepack enable && corepack prepare pnpm@10.25.0 --activate
@@ -7,7 +8,7 @@ USER node
COPY --chown=node:node package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY --chown=node:node . .
RUN pnpm run build
RUN VITE_APP_VERSION=${VITE_APP_VERSION} pnpm run build
# Stage 2: Build the Go binary with embedded assets
FROM cgr.dev/chainguard/go:latest-dev AS go-builder

View File

@@ -1,6 +1,6 @@
{
"name": "@quad4/linking-tool",
"version": "1.5.2",
"version": "1.5.3",
"license": "BSD-3-Clause",
"author": "Quad4",
"type": "module",
@@ -27,9 +27,9 @@
"LICENSE"
],
"scripts": {
"dev": "VITE_APP_VERSION=$(node -p \"require('./package.json').version\") vite dev",
"dev": "VITE_APP_VERSION=dev vite dev",
"prebuild": "node scripts/inject-sw-version.js",
"build": "VITE_APP_VERSION=$(node -p \"require('./package.json').version\") vite build",
"build": "VITE_APP_VERSION=${VITE_APP_VERSION:-$(node -p \"require('./package.json').version\")} vite build",
"preview": "VITE_APP_VERSION=$(node -p \"require('./package.json').version\") vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",

30
pnpm-lock.yaml generated
View File

@@ -314,10 +314,10 @@ packages:
cpu: [x64]
os: [win32]
'@eslint-community/eslint-utils@4.9.0':
'@eslint-community/eslint-utils@4.9.1':
resolution:
{
integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==,
integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==,
}
engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 }
peerDependencies:
@@ -946,10 +946,10 @@ packages:
}
engines: { node: '>= 6' }
caniuse-lite@1.0.30001761:
caniuse-lite@1.0.30001762:
resolution:
{
integrity: sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==,
integrity: sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==,
}
chalk@4.1.2:
@@ -1166,10 +1166,10 @@ packages:
}
engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 }
esquery@1.6.0:
esquery@1.7.0:
resolution:
{
integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==,
integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==,
}
engines: { node: '>=0.10' }
@@ -2286,7 +2286,7 @@ snapshots:
'@esbuild/win32-x64@0.27.2':
optional: true
'@eslint-community/eslint-utils@4.9.0(eslint@9.39.2(jiti@1.21.7))':
'@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@1.21.7))':
dependencies:
eslint: 9.39.2(jiti@1.21.7)
eslint-visitor-keys: 3.4.3
@@ -2575,7 +2575,7 @@ snapshots:
'@typescript-eslint/utils@8.51.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
dependencies:
'@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@1.21.7))
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@1.21.7))
'@typescript-eslint/scope-manager': 8.51.0
'@typescript-eslint/types': 8.51.0
'@typescript-eslint/typescript-estree': 8.51.0(typescript@5.9.3)
@@ -2622,7 +2622,7 @@ snapshots:
autoprefixer@10.4.23(postcss@8.5.6):
dependencies:
browserslist: 4.28.1
caniuse-lite: 1.0.30001761
caniuse-lite: 1.0.30001762
fraction.js: 5.3.4
picocolors: 1.1.1
postcss: 8.5.6
@@ -2652,7 +2652,7 @@ snapshots:
browserslist@4.28.1:
dependencies:
baseline-browser-mapping: 2.9.11
caniuse-lite: 1.0.30001761
caniuse-lite: 1.0.30001762
electron-to-chromium: 1.5.267
node-releases: 2.0.27
update-browserslist-db: 1.2.3(browserslist@4.28.1)
@@ -2661,7 +2661,7 @@ snapshots:
camelcase-css@2.0.1: {}
caniuse-lite@1.0.30001761: {}
caniuse-lite@1.0.30001762: {}
chalk@4.1.2:
dependencies:
@@ -2761,7 +2761,7 @@ snapshots:
eslint-plugin-svelte@3.13.1(eslint@9.39.2(jiti@1.21.7))(svelte@5.46.1):
dependencies:
'@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@1.21.7))
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@1.21.7))
'@jridgewell/sourcemap-codec': 1.5.5
eslint: 9.39.2(jiti@1.21.7)
esutils: 2.0.3
@@ -2788,7 +2788,7 @@ snapshots:
eslint@9.39.2(jiti@1.21.7):
dependencies:
'@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@1.21.7))
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@1.21.7))
'@eslint-community/regexpp': 4.12.2
'@eslint/config-array': 0.21.1
'@eslint/config-helpers': 0.4.2
@@ -2808,7 +2808,7 @@ snapshots:
eslint-scope: 8.4.0
eslint-visitor-keys: 4.2.1
espree: 10.4.0
esquery: 1.6.0
esquery: 1.7.0
esutils: 2.0.3
fast-deep-equal: 3.1.3
file-entry-cache: 8.0.0
@@ -2835,7 +2835,7 @@ snapshots:
acorn-jsx: 5.3.2(acorn@8.15.0)
eslint-visitor-keys: 4.2.1
esquery@1.6.0:
esquery@1.7.0:
dependencies:
estraverse: 5.3.0

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -21,6 +21,7 @@
Moon,
Sun,
MoreVertical,
Settings,
} from 'lucide-svelte';
import {
DB_NAME,
@@ -385,6 +386,10 @@
let isLight = $derived(theme === 'light');
let showMoreMenu = $state(false);
let moreMenuRef = $state<HTMLDivElement | null>(null);
let showSettingsModal = $state(false);
let showGrid = $state(true);
let gridOpacityMultiplier = $state(1);
let snapToGrid = $state(false);
let touchHoldTimeout: ReturnType<typeof setTimeout> | null = null;
let touchHoldNodeId = $state<string | null>(null);
let touchHoldStart = $state({ x: 0, y: 0 });
@@ -421,7 +426,35 @@
);
let modalBackdropClass = $derived(isLight ? 'bg-black/30' : 'bg-black/60');
let canvasBgClass = $derived(isLight ? 'bg-amber-50' : 'bg-neutral-950');
let gridStroke = $derived(isLight ? '#e5e7eb' : '#262626');
let gridStyle = $derived(() => {
if (!showGrid) return 'background-image: none;';
const k = transform.k;
const s1 = 40 * k;
const s2 = 200 * k;
const s3 = 1000 * k;
// Extremely subtle opacities
const o1 = Math.min(0.03, Math.max(0, (s1 - 15) / 100)) * gridOpacityMultiplier;
const o2 = Math.min(0.06, Math.max(0, (s2 - 15) / 100)) * gridOpacityMultiplier;
const o3 = Math.min(0.1, k < 0.1 ? 0.1 : 0.05) * gridOpacityMultiplier;
const color = isLight ? '0, 0, 0' : '255, 255, 255';
return `
background-image:
linear-gradient(to right, rgba(${color}, ${o1}) 1px, transparent 1px),
linear-gradient(to bottom, rgba(${color}, ${o1}) 1px, transparent 1px),
linear-gradient(to right, rgba(${color}, ${o2}) 1px, transparent 1px),
linear-gradient(to bottom, rgba(${color}, ${o2}) 1px, transparent 1px),
linear-gradient(to right, rgba(${color}, ${o3}) 1px, transparent 1px),
linear-gradient(to bottom, rgba(${color}, ${o3}) 1px, transparent 1px);
background-size:
${s1}px ${s1}px, ${s1}px ${s1}px,
${s2}px ${s2}px, ${s2}px ${s2}px,
${s3}px ${s3}px, ${s3}px ${s3}px;
background-position: ${transform.x}px ${transform.y}px;
`;
});
let mutedTextClass = $derived(isLight ? 'text-neutral-600' : 'text-neutral-500');
let filteredNodes = $derived(
@@ -1281,6 +1314,10 @@
if (node) {
node.x += dx / transform.k;
node.y += dy / transform.k;
if (snapToGrid) {
node.x = Math.round(node.x / 40) * 40;
node.y = Math.round(node.y / 40) * 40;
}
}
});
// eslint-disable-next-line no-self-assign
@@ -1290,6 +1327,10 @@
if (node) {
node.x += dx / transform.k;
node.y += dy / transform.k;
if (snapToGrid) {
node.x = Math.round(node.x / 40) * 40;
node.y = Math.round(node.y / 40) * 40;
}
// eslint-disable-next-line no-self-assign
nodes = nodes;
}
@@ -1826,11 +1867,32 @@
await saveSetting('theme', theme);
}
async function updateGridSettings() {
await saveSetting('showGrid', showGrid);
await saveSetting('gridOpacityMultiplier', gridOpacityMultiplier);
await saveSetting('snapToGrid', snapToGrid);
}
async function loadTheme() {
const saved = await loadSetting('theme');
if (saved === 'light' || saved === 'dark') {
theme = saved;
}
const savedShowGrid = await loadSetting('showGrid');
if (typeof savedShowGrid === 'boolean') {
showGrid = savedShowGrid;
}
const savedGridOpacity = await loadSetting('gridOpacityMultiplier');
if (typeof savedGridOpacity === 'number') {
gridOpacityMultiplier = savedGridOpacity;
}
const savedSnapToGrid = await loadSetting('snapToGrid');
if (typeof savedSnapToGrid === 'boolean') {
snapToGrid = savedSnapToGrid;
}
}
$effect(() => {
@@ -1948,6 +2010,13 @@
<Moon size={18} class="md:w-4 md:h-4 mobile-landscape:w-3 mobile-landscape:h-3" />
{/if}
</button>
<button
class={iconButtonClass}
title="Settings"
onclick={() => (showSettingsModal = true)}
>
<Settings size={18} class="md:w-4 md:h-4 mobile-landscape:w-3 mobile-landscape:h-3" />
</button>
<div class={dividerClass}></div>
<button
class={`${iconButtonClass} hidden md:block mobile-landscape:block`}
@@ -2087,6 +2156,18 @@
<Share2 size={16} />
Share Link
</button>
<button
class={`w-full text-left px-4 py-2.5 text-sm flex items-center gap-2 hover:bg-neutral-800/50 transition-colors ${
isLight ? 'text-neutral-700 hover:bg-amber-100' : 'text-neutral-200'
}`}
onclick={() => {
showSettingsModal = true;
showMoreMenu = false;
}}
>
<Settings size={16} />
Settings
</button>
<button
class={`w-full text-left px-4 py-2.5 text-sm flex items-center gap-2 hover:bg-neutral-800/50 transition-colors ${
isLight ? 'text-neutral-700 hover:bg-amber-100' : 'text-neutral-200'
@@ -2237,7 +2318,7 @@
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<div
class={`flex-1 w-full h-full cursor-default outline-none ${canvasBgClass}`}
style="touch-action: none;"
style={`touch-action: none; ${gridStyle()}`}
bind:this={canvasElement}
onmousedown={handleMouseDown}
onwheel={handleWheel}
@@ -2251,25 +2332,6 @@
aria-roledescription="Interactive graph canvas"
>
<svg bind:this={svgElement} class="w-full h-full block">
<defs>
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
<path d="M 40 0 L 0 0 0 40" fill="none" stroke={gridStroke} stroke-width="1" />
</pattern>
</defs>
<g
transform="translate({transform.x % (40 * transform.k)}, {transform.y %
(40 * transform.k)}) scale({transform.k})"
>
<rect
x={-containerElement?.clientWidth / transform.k || -1000}
y={-containerElement?.clientHeight / transform.k || -1000}
width="400%"
height="400%"
fill="url(#grid)"
/>
</g>
<g transform="translate({transform.x}, {transform.y}) scale({transform.k})">
{#each filteredLinks as link (link.id)}
{@const source = nodes.find((n) => n.id === link.source)}
@@ -2913,6 +2975,128 @@
</div>
{/if}
{#if showSettingsModal}
<!-- svelte-ignore a11y_interactive_supports_focus -->
<div
class={`absolute inset-0 z-50 flex items-center justify-center backdrop-blur-sm p-4 ${modalBackdropClass}`}
onclick={(e) => {
if (e.target === e.currentTarget) {
showSettingsModal = false;
}
}}
onkeydown={(e) => e.key === 'Escape' && (showSettingsModal = false)}
role="dialog"
tabindex="-1"
aria-modal="true"
aria-label="Settings"
>
<div class={`w-full max-w-md rounded-xl border p-6 shadow-2xl ${surfaceClass}`}>
<div class="flex items-center justify-between mb-6">
<h4 class={`text-lg font-semibold ${isLight ? 'text-neutral-900' : 'text-gray-100'}`}>
Settings
</h4>
<button
class={`p-1 rounded transition-colors ${
isLight
? 'text-neutral-600 hover:text-neutral-900 hover:bg-amber-100'
: 'text-neutral-400 hover:text-white hover:bg-neutral-800'
}`}
onclick={() => (showSettingsModal = false)}
>
<X size={20} />
</button>
</div>
<div class="space-y-6">
<div class="space-y-3">
<div class="text-xs font-semibold uppercase tracking-[0.2em] text-neutral-500">
Grid Settings
</div>
<label class="flex items-center justify-between cursor-pointer">
<span class={`text-sm ${isLight ? 'text-neutral-700' : 'text-neutral-300'}`}
>Show Grid Squares</span
>
<input
type="checkbox"
bind:checked={showGrid}
onchange={updateGridSettings}
class={`rounded text-rose-500 focus:ring-rose-500 h-5 w-5 ${
isLight ? 'border-amber-300 bg-amber-50' : 'border-neutral-700 bg-neutral-800'
}`}
/>
</label>
<div class="space-y-2">
<div class="flex justify-between items-center">
<span class={`text-sm ${isLight ? 'text-neutral-700' : 'text-neutral-300'}`}
>Grid Opacity</span
>
<span class="text-xs font-mono text-neutral-500"
>{Math.round(gridOpacityMultiplier * 100)}%</span
>
</div>
<input
type="range"
min="0"
max="2"
step="0.1"
bind:value={gridOpacityMultiplier}
oninput={updateGridSettings}
class="w-full h-2 bg-neutral-800 rounded-lg appearance-none cursor-pointer accent-rose-500"
/>
</div>
<label class="flex items-center justify-between cursor-pointer">
<span class={`text-sm ${isLight ? 'text-neutral-700' : 'text-neutral-300'}`}
>Snap to Grid (40px)</span
>
<input
type="checkbox"
bind:checked={snapToGrid}
onchange={updateGridSettings}
class={`rounded text-rose-500 focus:ring-rose-500 h-5 w-5 ${
isLight ? 'border-amber-300 bg-amber-50' : 'border-neutral-700 bg-neutral-800'
}`}
/>
</label>
</div>
<div class="space-y-3 pt-4 border-t border-neutral-800">
<div class="text-xs font-semibold uppercase tracking-[0.2em] text-neutral-500">
Theme
</div>
<button
class={`w-full flex items-center justify-between px-4 py-2 rounded-lg border transition-colors ${
isLight
? 'border-amber-300 bg-amber-50 text-neutral-700 hover:bg-amber-100'
: 'border-neutral-800 bg-neutral-900 text-neutral-300 hover:bg-neutral-800'
}`}
onclick={toggleTheme}
>
<span class="text-sm"
>Active Theme: {theme.charAt(0).toUpperCase() + theme.slice(1)}</span
>
{#if theme === 'dark'}
<Sun size={16} />
{:else}
<Moon size={16} />
{/if}
</button>
</div>
</div>
<div class="flex justify-end mt-8">
<button
class="rounded-lg bg-rose-600 px-6 py-2 text-sm font-medium text-white hover:bg-rose-500 shadow-lg shadow-rose-900/20"
onclick={() => (showSettingsModal = false)}
>
Close
</button>
</div>
</div>
</div>
{/if}
{#if showAddModal}
<!-- svelte-ignore a11y_interactive_supports_focus -->
<div

View File

@@ -2,6 +2,16 @@
import IdentityGraph from '../components/IdentityGraph.svelte';
import { APP_VERSION } from '$lib/version';
import { GitBranch } from 'lucide-svelte';
const REPO_URL = 'https://git.quad4.io/Quad4-Software/Linking-Tool';
const isCommitSha = /^[a-f0-9]{7,}$/i.test(APP_VERSION);
const displayVersion =
APP_VERSION.startsWith('v') || APP_VERSION === 'dev' || isCommitSha
? APP_VERSION
: `v${APP_VERSION}`;
const versionUrl = isCommitSha ? `${REPO_URL}/commit/${APP_VERSION}` : REPO_URL;
</script>
<svelte:head>
@@ -34,14 +44,27 @@
class="text-accent-red-light hover:text-accent-red-dark transition-colors">Quad4</a
>
-
<a
href="https://git.quad4.io/Quad4-Software/Linking-Tool"
target="_blank"
rel="noopener noreferrer"
class="text-accent-red-light hover:text-accent-red-dark transition-colors inline-flex items-center gap-1"
>v{APP_VERSION} <GitBranch size={12} /></a
></span
>
<span class="inline-flex items-center gap-1">
{#if isCommitSha}
<a
href={versionUrl}
target="_blank"
rel="noopener noreferrer"
class="text-accent-red-light hover:text-accent-red-dark transition-colors"
>{displayVersion}</a
>
{:else}
<span>{displayVersion}</span>
{/if}
<a
href={REPO_URL}
target="_blank"
rel="noopener noreferrer"
class="text-accent-red-light hover:text-accent-red-dark transition-colors"
><GitBranch size={12} /></a
>
</span>
</span>
</div>
</footer>
</div>

View File

@@ -1,4 +1,4 @@
const CACHE_VERSION = '1.5.2';
const CACHE_VERSION = '1.5.3';
const CACHE_NAME = `quad4-linking-tool-${CACHE_VERSION}`;
const urlsToCache = ['/', '/favicon.svg', '/manifest.json'];