Restructure/frontend editor (#6404)

## Move editor under `frontend/editor/`

Pure restructure: `frontend/` becomes the workspace, `frontend/editor/`
holds
  the PDF editor. 1775 file renames + 40 wiring edits. No logic changes.

  ### Why

`frontend/` is currently the editor — its `src/`, `public/`,
`src-tauri/`,
  config files all sit at the root. Promoting `frontend/` to a
workspace and putting the editor in a sibling folder leaves room for
future
apps to drop in alongside it, sharing one `package.json` /
`node_modules` /
  lint config / Storybook.

  ### What moves

  frontend/
  ├── editor/                ← NEW: everything editor-specific
  │   ├── src/               ← was frontend/src/
  │   ├── public/            ← was frontend/public/
  │   ├── src-tauri/         ← was frontend/src-tauri/
│ ├── index.html, vite.config.ts, vitest.config.ts, playwright.config.ts
  │   ├── tsconfig*.json, tailwind.config.js, postcss.config.js
  │   ├── scripts/
  │   ├── .env, .env.desktop, .env.saas
  │   └── DeveloperGuide.md
├── package.json, package-lock.json, node_modules/ ← workspace install
  ├── eslint.config.mjs, .prettierrc, .prettierignore ← shared tooling
  ├── .gitignore
  └── README.md

  ### Wiring edits (40 files)

  - `.taskfiles/frontend.yml`, `desktop.yml`, `e2e.yml`
  - `build.gradle`, `app/core/build.gradle`
- `eslint.config.mjs`, `frontend/package.json`, `.gitignore`,
`.prettierignore`
  - `docker/frontend/Dockerfile`
  - 8 `.github/workflows/*.yml`, plus `.github/dependabot.yml`,
    `.github/config/.files.yaml`, `.github/labeler-config-srvaroa.yml`
  - `scripts/translations/**`
- Docs: `AGENTS.md`, `CLAUDE.md`, `ADDING_TOOLS.md`,
`DeveloperGuide.md`,
`WINDOWS_SIGNING.md`, `devGuide/HowToAddNewLanguage.md`,
`frontend/README.md`,
    `frontend/editor/DeveloperGuide.md`

Plus 3 renamed + edited: `editor/vite.config.ts` (env path +
node_modules
  walk-up), `editor/scripts/setup-env.mts` (renamed from `.ts` for
`import.meta.url`), `editor/scripts/build-provisioner.mjs` (resolve
src-tauri
  relative to script).

  ### Verification

  | Check | Result |
  |---|---|
  | `task frontend:typecheck:all` (6 variants) | exit 0 |
  | `task frontend:lint` (eslint + dpdm) | exit 0 |
  | `task frontend:format:check` | exit 0 |
  | `task frontend:test` | 657 tests pass, 50 files |
| `task frontend:build:{core,proprietary,saas,desktop,prototypes}` | all
green |
| `task desktop:build` | full Tauri pipeline →
`Stirling-PDF_2.11.0_x64_en-US.msi` |
  | `playwright test --list --project=stubbed` | 172 tests discovered |

`task desktop:build` exercises the heaviest path — Rust + WiX + MSI
bundle
against the moved `editor/src-tauri/`. If anything in the restructure
was
  wrong it wouldn't have built.

  ### Test plan

  - [ ] `frontend-validation.yml` green
  - [ ] `e2e-stubbed.yml` green
  - [ ] `tauri-build.yml` green on at least one platform
  - [ ] `check_toml.yml` runs on a translation-touching PR

---------

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
Reece Browne
2026-05-22 13:40:34 +01:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 48027ee9d6
commit 0a50e765b7
1820 changed files with 300 additions and 226 deletions
@@ -0,0 +1,26 @@
// Utility helpers to open the settings/config modal programmatically
// and optionally navigate to a specific section (e.g., 'plan').
import type { NavKey } from "@app/components/shared/config/types";
export function openAppSettings(targetKey?: NavKey, notice?: string) {
try {
const detail: { key?: NavKey; notice?: string } = {};
if (targetKey) detail.key = targetKey;
if (notice) detail.notice = notice;
// Ask the UI to open the App Config modal
window.dispatchEvent(new CustomEvent("appConfig:open", { detail }));
// If a specific section is requested, navigate there once modal mounts
if (targetKey) {
window.dispatchEvent(
new CustomEvent("appConfig:navigate", { detail: { key: targetKey } }),
);
}
} catch (_e) {
// no-op on SSR or test environments
}
}
export function openPlanSettings(notice?: string) {
openAppSettings("plan", notice);
}
@@ -0,0 +1,84 @@
/**
* Crops an image based on the provided pixel crop area using HTML5 Canvas API.
* Returns a PNG blob ready for upload.
*/
export interface Area {
x: number;
y: number;
width: number;
height: number;
}
/**
* Creates a cropped image blob from the source image and crop area.
*
* @param imageSrc - Data URL or blob URL of the source image
* @param pixelCrop - Pixel coordinates and dimensions of the crop area
* @returns Promise that resolves to a PNG Blob of the cropped image
*/
export async function getCroppedImage(
imageSrc: string,
pixelCrop: Area,
): Promise<Blob> {
return new Promise((resolve, reject) => {
const image = new Image();
image.onload = () => {
try {
// Create canvas with crop dimensions
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
if (!ctx) {
reject(new Error("Failed to get canvas context"));
return;
}
// Set canvas size to crop dimensions
canvas.width = pixelCrop.width;
canvas.height = pixelCrop.height;
// Draw the cropped region
// drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh)
// sx, sy: source x, y coordinates
// sw, sh: source width, height
// dx, dy: destination x, y coordinates (0, 0 for top-left)
// dw, dh: destination width, height
ctx.drawImage(
image,
pixelCrop.x,
pixelCrop.y,
pixelCrop.width,
pixelCrop.height,
0,
0,
pixelCrop.width,
pixelCrop.height,
);
// Convert canvas to PNG blob
canvas.toBlob(
(blob) => {
if (!blob) {
reject(new Error("Failed to create blob from canvas"));
return;
}
resolve(blob);
},
"image/png",
1.0, // Maximum quality
);
} catch (error) {
reject(error);
}
};
image.onerror = () => {
reject(new Error("Failed to load image"));
};
// Start loading the image
image.src = imageSrc;
});
}
@@ -0,0 +1,48 @@
import { URL_TO_TOOL_MAP } from "@app/utils/urlMapping";
const SUBPATH = import.meta.env.VITE_RUN_SUBPATH.replace(/^\/|\/$/g, ""); // "app" or ""
/**
* Normalize pathname by stripping subpath prefix and trailing slashes
*/
export function normalizePath(pathname: string): string {
// Ensure leading slash, strip subpath prefix if configured
let p = pathname.startsWith("/") ? pathname : `/${pathname}`;
if (SUBPATH && p.startsWith(`/${SUBPATH}/`)) {
p = p.slice(SUBPATH.length + 1); // remove "/app"
} else if (SUBPATH && p === `/${SUBPATH}`) {
p = "/";
}
// Strip trailing slash except root
if (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
return p;
}
/**
* Check if pathname is an auth route
*/
export function isAuthRoute(pathname: string): boolean {
const p = normalizePath(pathname);
return p === "/login" || p === "/signup" || p === "/auth/callback";
}
/**
* Check if pathname is home route
*/
export function isHomeRoute(pathname: string): boolean {
return normalizePath(pathname) === "/";
}
/**
* Check if pathname is a tool route
*/
export function isToolRoute(pathname: string): boolean {
const p = normalizePath(pathname);
// direct match or try without trailing slash variants if your map uses them
if (URL_TO_TOOL_MAP[p] !== undefined) return true;
// Fallback: try adding/removing trailing slash
if (URL_TO_TOOL_MAP[`${p}/`] !== undefined) return true;
if (p.endsWith("/") && URL_TO_TOOL_MAP[p.slice(0, -1)] !== undefined)
return true;
return false;
}