Compare commits
1 Commits
v1.5.3
...
renovate/h
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d8dcbca22 |
@@ -18,7 +18,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: https://git.quad4.io/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
uses: https://git.quad4.io/actions/checkout@a5b3063b1edaa6ba4911c8a1b1d5e1656fba3ea5 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -27,11 +27,8 @@ jobs:
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT
|
||||
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
|
||||
echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Setup Node.js
|
||||
@@ -53,11 +50,9 @@ 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
|
||||
uses: https://git.quad4.io/actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
|
||||
with:
|
||||
go-version: '1.25.5'
|
||||
|
||||
@@ -73,16 +68,6 @@ 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
|
||||
@@ -95,6 +80,14 @@ 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:
|
||||
@@ -104,7 +97,7 @@ jobs:
|
||||
tag: ${{ steps.version.outputs.version }}
|
||||
body: |
|
||||
Release ${{ steps.version.outputs.version }}
|
||||
|
||||
|
||||
## Assets
|
||||
- Server binaries (Linux AMD64, Windows AMD64)
|
||||
- Desktop applications (Linux AMD64, Windows AMD64)
|
||||
@@ -112,3 +105,4 @@ jobs:
|
||||
files: release-assets/*
|
||||
draft: false
|
||||
prerelease: false
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: https://git.quad4.io/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
uses: https://git.quad4.io/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- name: Setup Node.js
|
||||
uses: https://git.quad4.io/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
@@ -29,16 +29,8 @@ 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:
|
||||
@@ -49,7 +41,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: https://git.quad4.io/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
uses: https://git.quad4.io/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- name: Setup Go
|
||||
uses: https://git.quad4.io/actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
|
||||
with:
|
||||
@@ -62,7 +54,7 @@ jobs:
|
||||
needs: [build-frontend, scan-backend]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: https://git.quad4.io/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
uses: https://git.quad4.io/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- name: Download frontend assets
|
||||
uses: https://git.quad4.io/actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
|
||||
with:
|
||||
|
||||
@@ -22,19 +22,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
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
|
||||
uses: https://git.quad4.io/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: https://git.quad4.io/actions/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
@@ -74,5 +62,3 @@ 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 }}
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: https://git.quad4.io/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
uses: https://git.quad4.io/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: https://git.quad4.io/actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: https://git.quad4.io/actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
uses: https://git.quad4.io/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: https://git.quad4.io/actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
|
||||
|
||||
63
.gitea/workflows/sbom.yml
Normal file
63
.gitea/workflows/sbom.yml
Normal file
@@ -0,0 +1,63 @@
|
||||
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 }}
|
||||
|
||||
34
CHANGELOG.md
34
CHANGELOG.md
@@ -1,36 +1,5 @@
|
||||
# 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
|
||||
|
||||
- **Mobile Enhancements**:
|
||||
- Added pinch-to-zoom support for graph navigation on touch devices.
|
||||
- Redesigned mobile toolbar into a single row with a collapsible "More" menu.
|
||||
- Added a responsive expand/collapse toggle for the mobile toolbar using chevron icons.
|
||||
- Moved the "Add Node" action to a floating sticky button in the bottom-right on mobile for better accessibility.
|
||||
- Optimized toolbar width and spacing for mobile screens.
|
||||
- **UI/UX**:
|
||||
- Removed top navbar/header to maximize workspace area.
|
||||
- Simplified layout with a minimal footer.
|
||||
- Updated footer branding to include "Linking Tool".
|
||||
|
||||
### Fixes
|
||||
|
||||
- Improved click-outside handling for mobile menus.
|
||||
- Fixed various mobile layout and justification constraints.
|
||||
|
||||
## 1.5.1 - 2025-12-29
|
||||
|
||||
### Features
|
||||
@@ -58,7 +27,7 @@
|
||||
- Mass selection improvements (moving and linking multiple nodes at once).
|
||||
- Codebase refactor to use Svelte 5 Runes.
|
||||
- Mobile improvements
|
||||
- Added SBOM generation as release assets
|
||||
- Added SBOM generation, see `/sbom/` for the generated SBOMs.
|
||||
|
||||
### Dependency Updates
|
||||
|
||||
@@ -70,6 +39,7 @@
|
||||
- `vite`: ^7.2.6 -> ^7.3.0
|
||||
- Added `eslint-plugin-security`: ^3.0.1
|
||||
|
||||
|
||||
### Major Codebase Changes
|
||||
|
||||
- Moved from `npm` to `pnpm`
|
||||
|
||||
37
Taskfile.yml
37
Taskfile.yml
@@ -19,19 +19,7 @@ tasks:
|
||||
build:frontend:
|
||||
desc: Build frontend only
|
||||
cmds:
|
||||
- |
|
||||
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
|
||||
- pnpm run build
|
||||
|
||||
build:backend:
|
||||
desc: Build backend binary only
|
||||
@@ -43,19 +31,7 @@ tasks:
|
||||
desc: Build the single binary web server
|
||||
cmds:
|
||||
- pnpm install
|
||||
- |
|
||||
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
|
||||
- pnpm run build
|
||||
- mkdir -p {{.BUILD_DIR}}
|
||||
- CGO_ENABLED=0 go build -ldflags="-s -w" -o {{.BUILD_DIR}}/{{.BINARY_NAME}} main.go
|
||||
|
||||
@@ -117,14 +93,9 @@ tasks:
|
||||
- GOOS=freebsd GOARCH=amd64 go build -o {{.BUILD_DIR}}/{{.BINARY_NAME}}-freebsd-amd64 main.go
|
||||
|
||||
docker-build:
|
||||
desc: Build Docker image (VITE_APP_VERSION env var will be passed as build arg if set)
|
||||
desc: Build Docker image
|
||||
cmds:
|
||||
- |
|
||||
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 build -f docker/Dockerfile -t {{.BINARY_NAME}} .
|
||||
|
||||
docker-run:
|
||||
desc: Run Docker container
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# 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
|
||||
@@ -8,7 +7,7 @@ USER node
|
||||
COPY --chown=node:node package.json pnpm-lock.yaml ./
|
||||
RUN pnpm install --frozen-lockfile
|
||||
COPY --chown=node:node . .
|
||||
RUN VITE_APP_VERSION=${VITE_APP_VERSION} pnpm run build
|
||||
RUN pnpm run build
|
||||
|
||||
# Stage 2: Build the Go binary with embedded assets
|
||||
FROM cgr.dev/chainguard/go:latest-dev AS go-builder
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@quad4/linking-tool",
|
||||
"version": "1.5.3",
|
||||
"version": "1.5.1",
|
||||
"license": "BSD-3-Clause",
|
||||
"author": "Quad4",
|
||||
"type": "module",
|
||||
@@ -27,9 +27,9 @@
|
||||
"LICENSE"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "VITE_APP_VERSION=dev vite dev",
|
||||
"dev": "VITE_APP_VERSION=$(node -p \"require('./package.json').version\") vite dev",
|
||||
"prebuild": "node scripts/inject-sw-version.js",
|
||||
"build": "VITE_APP_VERSION=${VITE_APP_VERSION:-$(node -p \"require('./package.json').version\")} vite build",
|
||||
"build": "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",
|
||||
|
||||
1790
pnpm-lock.yaml
generated
1790
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
8856
sbom/sbom.cyclonedx.json
Normal file
8856
sbom/sbom.cyclonedx.json
Normal file
File diff suppressed because it is too large
Load Diff
12578
sbom/sbom.spdx.json
Normal file
12578
sbom/sbom.spdx.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -13,15 +13,11 @@
|
||||
X,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Share2,
|
||||
Search,
|
||||
HelpCircle,
|
||||
Moon,
|
||||
Sun,
|
||||
MoreVertical,
|
||||
Settings,
|
||||
} from 'lucide-svelte';
|
||||
import {
|
||||
DB_NAME,
|
||||
@@ -375,7 +371,6 @@
|
||||
let newNodeImageError = $state('');
|
||||
let controlsCollapsed = $state(false);
|
||||
let isMobile = $state(false);
|
||||
let mobileToolbarCollapsed = $state(false);
|
||||
let copiedNodes = $state<Node[]>([]);
|
||||
let searchQuery = $state('');
|
||||
let searchInput = $state<HTMLInputElement | null>(null);
|
||||
@@ -384,20 +379,10 @@
|
||||
let addNodeInput = $state<HTMLInputElement | null>(null);
|
||||
let theme = $state<'dark' | 'light'>('dark');
|
||||
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 });
|
||||
let isLongPressing = $state(false);
|
||||
let isPinching = $state(false);
|
||||
let pinchStartDistance = $state(0);
|
||||
let pinchStartScale = $state(1);
|
||||
let pinchCenter = $state({ x: 0, y: 0 });
|
||||
|
||||
let panelClass = $derived(
|
||||
isLight
|
||||
@@ -406,8 +391,8 @@
|
||||
);
|
||||
let iconButtonClass = $derived(
|
||||
isLight
|
||||
? 'p-3 md:p-2 mobile-landscape:p-1 rounded text-neutral-600 hover:text-neutral-900 hover:bg-amber-100'
|
||||
: 'p-3 md:p-2 mobile-landscape:p-1 hover:bg-neutral-800 rounded text-neutral-400 hover:text-white'
|
||||
? 'p-2 md:p-2 mobile-landscape:p-1 rounded text-neutral-600 hover:text-neutral-900 hover:bg-amber-100'
|
||||
: 'p-2 md:p-2 mobile-landscape:p-1 hover:bg-neutral-800 rounded text-neutral-400 hover:text-white'
|
||||
);
|
||||
let dividerClass = $derived(
|
||||
isLight
|
||||
@@ -426,35 +411,7 @@
|
||||
);
|
||||
let modalBackdropClass = $derived(isLight ? 'bg-black/30' : 'bg-black/60');
|
||||
let canvasBgClass = $derived(isLight ? 'bg-amber-50' : 'bg-neutral-950');
|
||||
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 gridStroke = $derived(isLight ? '#e5e7eb' : '#262626');
|
||||
let mutedTextClass = $derived(isLight ? 'text-neutral-600' : 'text-neutral-500');
|
||||
|
||||
let filteredNodes = $derived(
|
||||
@@ -1314,10 +1271,6 @@
|
||||
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
|
||||
@@ -1327,10 +1280,6 @@
|
||||
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;
|
||||
}
|
||||
@@ -1391,46 +1340,11 @@
|
||||
});
|
||||
}
|
||||
|
||||
function getTouchDistance(touches: TouchEvent['touches']): number {
|
||||
if (touches.length < 2) return 0;
|
||||
const dx = touches[0].clientX - touches[1].clientX;
|
||||
const dy = touches[0].clientY - touches[1].clientY;
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
function getTouchCenter(touches: TouchEvent['touches']): { x: number; y: number } {
|
||||
if (touches.length === 0) return { x: 0, y: 0 };
|
||||
if (touches.length === 1) {
|
||||
return { x: touches[0].clientX, y: touches[0].clientY };
|
||||
}
|
||||
return {
|
||||
x: (touches[0].clientX + touches[1].clientX) / 2,
|
||||
y: (touches[0].clientY + touches[1].clientY) / 2,
|
||||
};
|
||||
}
|
||||
|
||||
function handleTouchStart(e: TouchEvent) {
|
||||
if (e.cancelable) {
|
||||
e.preventDefault();
|
||||
}
|
||||
if (e.touches.length === 0) return;
|
||||
|
||||
if (e.touches.length === 2) {
|
||||
isPinching = true;
|
||||
pinchStartDistance = getTouchDistance(e.touches);
|
||||
pinchStartScale = transform.k;
|
||||
const center = getTouchCenter(e.touches);
|
||||
const rect = containerElement!.getBoundingClientRect();
|
||||
pinchCenter = {
|
||||
x: center.x - rect.left,
|
||||
y: center.y - rect.top,
|
||||
};
|
||||
isPanning = false;
|
||||
isDragging = false;
|
||||
clearTouchHold();
|
||||
return;
|
||||
}
|
||||
|
||||
const touch = e.touches[0];
|
||||
handleMouseDown(touchToMouseEvent(touch, 'mousedown'));
|
||||
}
|
||||
@@ -1440,34 +1354,16 @@
|
||||
e.preventDefault();
|
||||
}
|
||||
if (e.touches.length === 0) return;
|
||||
|
||||
if (e.touches.length === 2 && isPinching) {
|
||||
const currentDistance = getTouchDistance(e.touches);
|
||||
if (pinchStartDistance > 0) {
|
||||
const scaleChange = currentDistance / pinchStartDistance;
|
||||
const newScale = Math.min(Math.max(0.1, pinchStartScale * scaleChange), 5);
|
||||
|
||||
const worldBefore = screenToWorld(pinchCenter.x, pinchCenter.y);
|
||||
transform.k = newScale;
|
||||
const worldAfter = screenToWorld(pinchCenter.x, pinchCenter.y);
|
||||
transform.x += (worldAfter.x - worldBefore.x) * newScale;
|
||||
transform.y += (worldAfter.y - worldBefore.y) * newScale;
|
||||
const touch = e.touches[0];
|
||||
if (touchHoldTimeout && !isLongPressing) {
|
||||
const dx = touch.clientX - touchHoldStart.x;
|
||||
const dy = touch.clientY - touchHoldStart.y;
|
||||
if (Math.hypot(dx, dy) > 10) {
|
||||
clearTouchHold();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.touches.length === 1) {
|
||||
const touch = e.touches[0];
|
||||
if (touchHoldTimeout && !isLongPressing) {
|
||||
const dx = touch.clientX - touchHoldStart.x;
|
||||
const dy = touch.clientY - touchHoldStart.y;
|
||||
if (Math.hypot(dx, dy) > 10) {
|
||||
clearTouchHold();
|
||||
}
|
||||
}
|
||||
if ((isLongPressing && touchHoldNodeId) || isPanning) {
|
||||
handleMouseMove(touchToMouseEvent(touch, 'mousemove'));
|
||||
}
|
||||
if ((isLongPressing && touchHoldNodeId) || isPanning) {
|
||||
handleMouseMove(touchToMouseEvent(touch, 'mousemove'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1475,15 +1371,6 @@
|
||||
if (e.cancelable) {
|
||||
e.preventDefault();
|
||||
}
|
||||
if (isPinching && e.touches.length < 2) {
|
||||
isPinching = false;
|
||||
pinchStartDistance = 0;
|
||||
pinchStartScale = 1;
|
||||
if (e.touches.length === 0) {
|
||||
handleMouseUp();
|
||||
}
|
||||
return;
|
||||
}
|
||||
clearTouchHold();
|
||||
handleMouseUp();
|
||||
}
|
||||
@@ -1867,32 +1754,11 @@
|
||||
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(() => {
|
||||
@@ -1961,26 +1827,6 @@
|
||||
}, 10);
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (showMoreMenu) {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (moreMenuRef && e.target instanceof Element) {
|
||||
const target = e.target;
|
||||
if (!moreMenuRef.contains(target) && !target.closest('[data-more-menu-button]')) {
|
||||
showMoreMenu = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
const timeoutId = setTimeout(() => {
|
||||
document.addEventListener('click', handleClickOutside, true);
|
||||
}, 10);
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
document.removeEventListener('click', handleClickOutside, true);
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -1992,215 +1838,71 @@
|
||||
bind:this={containerElement}
|
||||
>
|
||||
<div
|
||||
class="absolute z-10 pointer-events-none flex flex-col gap-1 md:gap-2 top-1 md:top-2 md:left-4 md:translate-x-0 md:top-4 max-h-[calc(100vh-120px)] md:max-h-none mobile-landscape:flex-row mobile-landscape:left-1/2 mobile-landscape:-translate-x-1/2 mobile-landscape:top-auto mobile-landscape:bottom-2 mobile-landscape:max-h-none mobile-landscape:gap-1 mobile-landscape:max-w-[calc(100vw-1rem)] mobile-landscape:w-auto transition-all duration-300 {mobileToolbarCollapsed
|
||||
? 'right-2 md:left-4 md:right-auto'
|
||||
: 'left-1/2 -translate-x-1/2 md:left-4 md:translate-x-0 w-[calc(100vw-0.25rem)] md:w-auto'}"
|
||||
class="absolute z-10 pointer-events-none flex flex-col gap-1 md:gap-2 top-1 md:top-2 left-1/2 -translate-x-1/2 md:left-4 md:translate-x-0 md:top-4 max-w-[calc(100vw-1rem)] md:max-w-none max-h-[calc(100vh-120px)] md:max-h-none mobile-landscape:flex-row mobile-landscape:left-1/2 mobile-landscape:-translate-x-1/2 mobile-landscape:top-auto mobile-landscape:bottom-2 mobile-landscape:max-h-none mobile-landscape:gap-1"
|
||||
>
|
||||
{#if !mobileToolbarCollapsed}
|
||||
<div
|
||||
class={`rounded-lg p-1 mobile-landscape:p-1 md:p-2 pointer-events-auto shadow-lg border ${panelClass} max-h-full overflow-y-auto mobile-landscape:max-h-none mobile-landscape:overflow-visible`}
|
||||
>
|
||||
<div
|
||||
class={`rounded-lg p-2 mobile-landscape:p-1 md:p-2 pointer-events-auto shadow-lg border ${panelClass} max-h-full overflow-visible mobile-landscape:max-h-none mobile-landscape:overflow-visible w-full md:w-auto transition-all`}
|
||||
class="flex flex-row flex-wrap md:flex-col md:flex-nowrap mobile-landscape:flex-row mobile-landscape:flex-nowrap mobile-landscape:flex-wrap gap-1.5 md:gap-1.5 mobile-landscape:gap-1 justify-center w-full md:w-auto mobile-landscape:w-auto"
|
||||
>
|
||||
<div
|
||||
class="flex flex-row flex-nowrap md:flex-col md:flex-nowrap mobile-landscape:flex-row mobile-landscape:flex-nowrap mobile-landscape:flex-wrap gap-2 md:gap-1.5 mobile-landscape:gap-1 justify-start md:justify-center mobile-landscape:justify-center w-full md:w-auto mobile-landscape:w-auto overflow-visible md:overflow-visible items-center"
|
||||
<button class={iconButtonClass} title="Toggle Theme" onclick={toggleTheme}>
|
||||
{#if theme === 'dark'}
|
||||
<Sun size={16} class="md:w-4 md:h-4 mobile-landscape:w-3 mobile-landscape:h-3" />
|
||||
{:else}
|
||||
<Moon size={16} class="md:w-4 md:h-4 mobile-landscape:w-3 mobile-landscape:h-3" />
|
||||
{/if}
|
||||
</button>
|
||||
<div class={dividerClass}></div>
|
||||
<button class={iconButtonClass} title="Add Node" onclick={() => (showAddModal = true)}>
|
||||
<Plus size={16} class="md:w-4 md:h-4 mobile-landscape:w-3 mobile-landscape:h-3" />
|
||||
</button>
|
||||
<div class={dividerClass}></div>
|
||||
<button class={iconButtonClass} title="Import Graph" onclick={importGraph}>
|
||||
<Upload size={16} class="md:w-4 md:h-4 mobile-landscape:w-3 mobile-landscape:h-3" />
|
||||
</button>
|
||||
<button class={iconButtonClass} title="Export JSON" onclick={exportGraph}>
|
||||
<Download size={16} class="md:w-4 md:h-4 mobile-landscape:w-3 mobile-landscape:h-3" />
|
||||
</button>
|
||||
<button class={iconButtonClass} title="Share Link" onclick={shareGraph}>
|
||||
<Share2 size={16} class="md:w-4 md:h-4 mobile-landscape:w-3 mobile-landscape:h-3" />
|
||||
</button>
|
||||
<div class={dividerClass}></div>
|
||||
<button
|
||||
class={iconButtonClass}
|
||||
title="Keyboard Shortcuts (?)"
|
||||
onclick={() => (showShortcutsModal = true)}
|
||||
>
|
||||
<button class={iconButtonClass} title="Toggle Theme" onclick={toggleTheme}>
|
||||
{#if theme === 'dark'}
|
||||
<Sun size={18} class="md:w-4 md:h-4 mobile-landscape:w-3 mobile-landscape:h-3" />
|
||||
{:else}
|
||||
<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`}
|
||||
title="Add Node"
|
||||
onclick={() => (showAddModal = true)}
|
||||
>
|
||||
<Plus size={18} class="md:w-4 md:h-4 mobile-landscape:w-3 mobile-landscape:h-3" />
|
||||
</button>
|
||||
<div class={`${dividerClass} hidden md:block mobile-landscape:block`}></div>
|
||||
<button
|
||||
class={`${iconButtonClass} hidden md:block mobile-landscape:block`}
|
||||
title="Import Graph"
|
||||
onclick={importGraph}
|
||||
>
|
||||
<Upload size={18} class="md:w-4 md:h-4 mobile-landscape:w-3 mobile-landscape:h-3" />
|
||||
</button>
|
||||
<button
|
||||
class={`${iconButtonClass} hidden md:block mobile-landscape:block`}
|
||||
title="Export JSON"
|
||||
onclick={exportGraph}
|
||||
>
|
||||
<Download size={18} class="md:w-4 md:h-4 mobile-landscape:w-3 mobile-landscape:h-3" />
|
||||
</button>
|
||||
<button
|
||||
class={`${iconButtonClass} hidden md:block mobile-landscape:block`}
|
||||
title="Share Link"
|
||||
onclick={shareGraph}
|
||||
>
|
||||
<Share2 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`}
|
||||
title="Keyboard Shortcuts (?)"
|
||||
onclick={() => (showShortcutsModal = true)}
|
||||
>
|
||||
<HelpCircle 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}
|
||||
title="Undo (Ctrl+Z)"
|
||||
onclick={undo}
|
||||
disabled={undoCount === 0}
|
||||
class:opacity-50={undoCount === 0}
|
||||
>
|
||||
<Undo2 size={18} class="md:w-4 md:h-4 mobile-landscape:w-3 mobile-landscape:h-3" />
|
||||
</button>
|
||||
<button
|
||||
class={iconButtonClass}
|
||||
title="Redo (Ctrl+Y)"
|
||||
onclick={redo}
|
||||
disabled={redoCount === 0}
|
||||
class:opacity-50={redoCount === 0}
|
||||
>
|
||||
<Redo2 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} title="Fit to Screen" onclick={centerView}>
|
||||
<Maximize size={18} class="md:w-4 md:h-4 mobile-landscape:w-3 mobile-landscape:h-3" />
|
||||
</button>
|
||||
<button
|
||||
class={`${iconButtonClass} hidden md:block mobile-landscape:block`}
|
||||
title="Clear Graph"
|
||||
onclick={clearGraph}
|
||||
>
|
||||
<Trash2 size={18} class="md:w-4 md:h-4 mobile-landscape:w-3 mobile-landscape:h-3" />
|
||||
</button>
|
||||
<div class={`${dividerClass} hidden md:block mobile-landscape:block`}></div>
|
||||
<div class="relative md:hidden mobile-landscape:hidden">
|
||||
<button
|
||||
class={iconButtonClass}
|
||||
title="More options"
|
||||
data-more-menu-button
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
showMoreMenu = !showMoreMenu;
|
||||
}}
|
||||
>
|
||||
<MoreVertical size={18} />
|
||||
</button>
|
||||
{#if showMoreMenu}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
bind:this={moreMenuRef}
|
||||
class={`absolute top-full right-0 mt-1 rounded-lg shadow-lg border ${panelClass} z-[100] min-w-[180px] pointer-events-auto`}
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
role="menu"
|
||||
tabindex="-1"
|
||||
>
|
||||
<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={() => {
|
||||
showAddModal = true;
|
||||
showMoreMenu = false;
|
||||
}}
|
||||
>
|
||||
<Plus size={16} />
|
||||
Add Node
|
||||
</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={() => {
|
||||
importGraph();
|
||||
showMoreMenu = false;
|
||||
}}
|
||||
>
|
||||
<Upload size={16} />
|
||||
Import Graph
|
||||
</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={() => {
|
||||
exportGraph();
|
||||
showMoreMenu = false;
|
||||
}}
|
||||
>
|
||||
<Download size={16} />
|
||||
Export JSON
|
||||
</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={() => {
|
||||
shareGraph();
|
||||
showMoreMenu = false;
|
||||
}}
|
||||
>
|
||||
<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'
|
||||
}`}
|
||||
onclick={() => {
|
||||
clearGraph();
|
||||
showMoreMenu = false;
|
||||
}}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
Clear Graph
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<button
|
||||
class={`${iconButtonClass} md:hidden mobile-landscape:hidden ml-auto`}
|
||||
title="Collapse toolbar"
|
||||
onclick={() => (mobileToolbarCollapsed = !mobileToolbarCollapsed)}
|
||||
>
|
||||
<ChevronLeft size={18} />
|
||||
</button>
|
||||
</div>
|
||||
<HelpCircle size={16} class="md:w-4 md:h-4 mobile-landscape:w-3 mobile-landscape:h-3" />
|
||||
</button>
|
||||
<div class={dividerClass}></div>
|
||||
<button
|
||||
class={iconButtonClass}
|
||||
title="Undo (Ctrl+Z)"
|
||||
onclick={undo}
|
||||
disabled={undoCount === 0}
|
||||
class:opacity-50={undoCount === 0}
|
||||
>
|
||||
<Undo2 size={16} class="md:w-4 md:h-4 mobile-landscape:w-3 mobile-landscape:h-3" />
|
||||
</button>
|
||||
<button
|
||||
class={iconButtonClass}
|
||||
title="Redo (Ctrl+Y)"
|
||||
onclick={redo}
|
||||
disabled={redoCount === 0}
|
||||
class:opacity-50={redoCount === 0}
|
||||
>
|
||||
<Redo2 size={16} class="md:w-4 md:h-4 mobile-landscape:w-3 mobile-landscape:h-3" />
|
||||
</button>
|
||||
<div class={dividerClass}></div>
|
||||
<button class={iconButtonClass} title="Fit to Screen" onclick={centerView}>
|
||||
<Maximize size={16} class="md:w-4 md:h-4 mobile-landscape:w-3 mobile-landscape:h-3" />
|
||||
</button>
|
||||
<button class={iconButtonClass} title="Clear Graph" onclick={clearGraph}>
|
||||
<Trash2 size={16} class="md:w-4 md:h-4 mobile-landscape:w-3 mobile-landscape:h-3" />
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
class={`${iconButtonClass} md:hidden mobile-landscape:hidden pointer-events-auto rounded-lg p-2 shadow-lg border ${panelClass} w-auto`}
|
||||
title="Expand toolbar"
|
||||
onclick={() => (mobileToolbarCollapsed = !mobileToolbarCollapsed)}
|
||||
>
|
||||
<ChevronRight size={18} />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showSearch}
|
||||
@@ -2244,24 +1946,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="fixed bottom-14 right-4 z-20 pointer-events-none md:hidden mobile-landscape:hidden">
|
||||
<button
|
||||
class={`rounded-full p-4 pointer-events-auto shadow-lg border-2 transition-transform hover:scale-110 active:scale-95 ${
|
||||
isLight
|
||||
? 'bg-rose-600 border-rose-700 text-white hover:bg-rose-500'
|
||||
: 'bg-rose-600 border-rose-700 text-white hover:bg-rose-500'
|
||||
}`}
|
||||
title="Add Node"
|
||||
onclick={() => (showAddModal = true)}
|
||||
aria-label="Add Node"
|
||||
>
|
||||
<Plus size={24} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="absolute bottom-4 right-4 z-10 pointer-events-none hidden md:block mobile-landscape:block"
|
||||
>
|
||||
<div class="absolute bottom-4 right-4 z-10 pointer-events-none">
|
||||
<div class={`backdrop-blur rounded-lg pointer-events-auto shadow-lg border ${panelClass}`}>
|
||||
<button
|
||||
class={`w-full flex items-center justify-between px-3 py-2 text-[10px] uppercase tracking-wider font-semibold transition-colors ${
|
||||
@@ -2318,7 +2003,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; ${gridStyle()}`}
|
||||
style="touch-action: none;"
|
||||
bind:this={canvasElement}
|
||||
onmousedown={handleMouseDown}
|
||||
onwheel={handleWheel}
|
||||
@@ -2332,6 +2017,25 @@
|
||||
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)}
|
||||
@@ -2975,128 +2679,6 @@
|
||||
</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
|
||||
|
||||
@@ -1,17 +1,7 @@
|
||||
<script lang="ts">
|
||||
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;
|
||||
import { Link as LinkIcon, GitBranch } from 'lucide-svelte';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -31,40 +21,42 @@
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex flex-col h-screen bg-bg-primary text-text-primary">
|
||||
<main class="flex-1 relative overflow-hidden bg-bg-primary p-0 sm:p-4">
|
||||
<IdentityGraph />
|
||||
</main>
|
||||
<footer class="bg-neutral-950 border-t border-neutral-800 px-4 py-2 flex-shrink-0">
|
||||
<div class="text-text-secondary text-xs flex items-center justify-center gap-2">
|
||||
<header
|
||||
class="bg-neutral-950 border-b border-neutral-800 px-2 sm:px-6 py-1.5 sm:py-3 flex flex-col sm:flex-row justify-between items-center gap-1 sm:gap-2 flex-shrink-0 shadow-lg"
|
||||
>
|
||||
<a
|
||||
href="https://git.quad4.io/Quad4-Software/Linking-Tool"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-sm sm:text-xl font-semibold text-accent-red-light flex items-center gap-1.5 sm:gap-2 hover:text-accent-red-dark transition-colors"
|
||||
>
|
||||
<div
|
||||
class="h-4 w-4 sm:h-5 sm:w-5 rounded border border-accent-red-light flex items-center justify-center bg-neutral-900"
|
||||
>
|
||||
<LinkIcon size={12} class="sm:w-[14px] sm:h-[14px] text-accent-red-light" />
|
||||
</div>
|
||||
Linking Tool
|
||||
</a>
|
||||
<div class="text-text-secondary text-[10px] sm:text-sm flex items-center gap-1 sm:gap-2">
|
||||
<span
|
||||
>Linking Tool - Created by <a
|
||||
>Created by <a
|
||||
href="https://quad4.io"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-accent-red-light hover:text-accent-red-dark transition-colors">Quad4</a
|
||||
>
|
||||
-
|
||||
<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>
|
||||
<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
|
||||
>
|
||||
</div>
|
||||
</footer>
|
||||
</header>
|
||||
<main class="flex-1 relative overflow-hidden bg-bg-primary p-0 sm:p-4">
|
||||
<IdentityGraph />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const CACHE_VERSION = '1.5.3';
|
||||
const CACHE_VERSION = '1.5.1';
|
||||
const CACHE_NAME = `quad4-linking-tool-${CACHE_VERSION}`;
|
||||
const urlsToCache = ['/', '/favicon.svg', '/manifest.json'];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user