mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 03:20:46 +02:00
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:
co-authored by
Claude Opus 4.7
parent
48027ee9d6
commit
0a50e765b7
@@ -1,56 +0,0 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync, mkdirSync, copyFileSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
|
||||
if (process.platform !== "win32") {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const frontendDir = process.cwd();
|
||||
const tauriDir = resolve(frontendDir, "src-tauri");
|
||||
const provisionerManifest = join(tauriDir, "provisioner", "Cargo.toml");
|
||||
|
||||
execFileSync(
|
||||
"cargo",
|
||||
["build", "--release", "--manifest-path", provisionerManifest],
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
|
||||
const provisionerExe = join(
|
||||
tauriDir,
|
||||
"provisioner",
|
||||
"target",
|
||||
"release",
|
||||
"stirling-provisioner.exe",
|
||||
);
|
||||
if (!existsSync(provisionerExe)) {
|
||||
throw new Error(`Provisioner binary not found at ${provisionerExe}`);
|
||||
}
|
||||
|
||||
const wixDir = join(tauriDir, "windows", "wix");
|
||||
mkdirSync(wixDir, { recursive: true });
|
||||
|
||||
const destExe = join(wixDir, "stirling-provision.exe");
|
||||
copyFileSync(provisionerExe, destExe);
|
||||
|
||||
// --- Thumbnail handler DLL ---
|
||||
const thumbManifest = join(tauriDir, "thumbnail-handler", "Cargo.toml");
|
||||
|
||||
execFileSync(
|
||||
"cargo",
|
||||
["build", "--release", "--manifest-path", thumbManifest],
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
|
||||
const thumbDll = join(
|
||||
tauriDir,
|
||||
"thumbnail-handler",
|
||||
"target",
|
||||
"release",
|
||||
"stirling_thumbnail_handler.dll",
|
||||
);
|
||||
if (!existsSync(thumbDll)) {
|
||||
throw new Error(`Thumbnail handler DLL not found at ${thumbDll}`);
|
||||
}
|
||||
|
||||
copyFileSync(thumbDll, join(wixDir, "stirling_thumbnail_handler.dll"));
|
||||
@@ -1,108 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build a universal (arm64 + x86_64) JRE bundle for the macOS Tauri app.
|
||||
#
|
||||
# jlink can only emit a single-architecture runtime, but the Tauri shell is
|
||||
# universal-apple-darwin. Without this merge the bundled JRE would only run
|
||||
# on whichever arch the runner used. We jlink twice (once per target JDK's
|
||||
# jmods) and lipo-fatten every Mach-O file in the result.
|
||||
#
|
||||
# Inputs (env):
|
||||
# AARCH64_JAVA_HOME path to an aarch64 JDK with jmods/
|
||||
# X64_JAVA_HOME path to an x86_64 JDK with jmods/
|
||||
# JLINK_MODULES comma-separated module list (matches desktop.yml)
|
||||
# OUTPUT_DIR target directory (will be wiped); defaults to
|
||||
# frontend/src-tauri/runtime/jre
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
: "${AARCH64_JAVA_HOME:?AARCH64_JAVA_HOME must be set}"
|
||||
: "${X64_JAVA_HOME:?X64_JAVA_HOME must be set}"
|
||||
: "${JLINK_MODULES:?JLINK_MODULES must be set}"
|
||||
|
||||
OUTPUT_DIR="${OUTPUT_DIR:-frontend/src-tauri/runtime/jre}"
|
||||
|
||||
if [[ "$(uname -s)" != "Darwin" ]]; then
|
||||
echo "build-universal-mac-jre.sh only runs on macOS" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# x86_64 jlink runs under Rosetta on Apple Silicon. If Rosetta isn't
|
||||
# installed the failure mode is a cryptic "Bad CPU type" from exec, so
|
||||
# fail loudly up front when running on arm64 without it.
|
||||
if [[ "$(uname -m)" == "arm64" ]] && ! arch -x86_64 /usr/bin/true >/dev/null 2>&1; then
|
||||
echo "Rosetta 2 is required to run x86_64 jlink on Apple Silicon. Install with: softwareupdate --install-rosetta --agree-to-license" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WORK_DIR="$(mktemp -d -t universal-jre)"
|
||||
trap 'rm -rf "$WORK_DIR"' EXIT INT TERM
|
||||
|
||||
ARM_JRE="$WORK_DIR/jre-aarch64"
|
||||
X64_JRE="$WORK_DIR/jre-x86_64"
|
||||
|
||||
# Each jlink invocation must run on the native arch of its JDK: jlink
|
||||
# stamps out launcher executables (bin/java, bin/rmiregistry, etc.)
|
||||
# matching the host binary, regardless of --module-path. So we run the
|
||||
# arm64 jlink for the arm64 JRE and the x86_64 jlink for the x86_64
|
||||
# JRE.
|
||||
run_jlink() {
|
||||
local java_home="$1"
|
||||
local out="$2"
|
||||
"$java_home/bin/jlink" \
|
||||
--module-path "$java_home/jmods" \
|
||||
--add-modules "$JLINK_MODULES" \
|
||||
--strip-debug \
|
||||
--compress=zip-6 \
|
||||
--no-header-files \
|
||||
--no-man-pages \
|
||||
--output "$out"
|
||||
}
|
||||
|
||||
echo "Building aarch64 JRE from $AARCH64_JAVA_HOME"
|
||||
run_jlink "$AARCH64_JAVA_HOME" "$ARM_JRE"
|
||||
|
||||
echo "Building x86_64 JRE from $X64_JAVA_HOME (runs under Rosetta on Apple Silicon)"
|
||||
run_jlink "$X64_JAVA_HOME" "$X64_JRE"
|
||||
|
||||
rm -rf "$OUTPUT_DIR"
|
||||
mkdir -p "$(dirname "$OUTPUT_DIR")"
|
||||
cp -R "$ARM_JRE" "$OUTPUT_DIR"
|
||||
|
||||
# Replace every Mach-O file in the copied tree with a fat binary.
|
||||
# Files that aren't Mach-O (text, modules archive, etc.) are left as-is.
|
||||
merged=0
|
||||
skipped=0
|
||||
while IFS= read -r -d '' arm_file; do
|
||||
rel="${arm_file#"$ARM_JRE"/}"
|
||||
x64_file="$X64_JRE/$rel"
|
||||
out_file="$OUTPUT_DIR/$rel"
|
||||
|
||||
if [[ ! -f "$x64_file" ]]; then
|
||||
skipped=$((skipped + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! file "$arm_file" | grep -q "Mach-O"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
lipo -create "$arm_file" "$x64_file" -output "$out_file"
|
||||
merged=$((merged + 1))
|
||||
done < <(find "$ARM_JRE" -type f -print0)
|
||||
|
||||
echo "Universal JRE written to $OUTPUT_DIR (merged $merged Mach-O files, skipped $skipped arm-only files)"
|
||||
|
||||
if [[ "$merged" -eq 0 ]]; then
|
||||
echo "Refusing to ship a JRE with zero merged Mach-O binaries" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Sanity-check: the launcher must be a fat binary or the app crashes on the
|
||||
# arch we didn't lipo for. `lipo -archs` prints just the arch names space-
|
||||
# separated (avoiding fragile grep alternation across BSD/GNU grep).
|
||||
java_archs="$(lipo -archs "$OUTPUT_DIR/bin/java" 2>/dev/null || true)"
|
||||
if [[ "$java_archs" != *arm64* || "$java_archs" != *x86_64* ]]; then
|
||||
echo "bin/java is not a universal binary (archs: ${java_archs:-unknown})" >&2
|
||||
lipo -info "$OUTPUT_DIR/bin/java" >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,249 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { icons } = require("@iconify-json/material-symbols");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
// Check for verbose flag
|
||||
const isVerbose =
|
||||
process.argv.includes("--verbose") || process.argv.includes("-v");
|
||||
|
||||
// Logging functions
|
||||
const info = (message) => console.log(message);
|
||||
const debug = (message) => {
|
||||
if (isVerbose) {
|
||||
console.log(message);
|
||||
}
|
||||
};
|
||||
|
||||
// Function to scan codebase for LocalIcon usage
|
||||
function scanForUsedIcons() {
|
||||
const usedIcons = new Set();
|
||||
const srcDir = path.join(__dirname, "..", "src");
|
||||
|
||||
info("🔍 Scanning codebase for LocalIcon usage...");
|
||||
|
||||
if (!fs.existsSync(srcDir)) {
|
||||
console.error("❌ Source directory not found:", srcDir);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Recursively scan all .tsx and .ts files
|
||||
function scanDirectory(dir) {
|
||||
const files = fs.readdirSync(dir);
|
||||
|
||||
files.forEach((file) => {
|
||||
const filePath = path.join(dir, file);
|
||||
const stat = fs.statSync(filePath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
scanDirectory(filePath);
|
||||
} else if (file.endsWith(".tsx") || file.endsWith(".ts")) {
|
||||
const content = fs.readFileSync(filePath, "utf8");
|
||||
|
||||
// Match LocalIcon usage: <LocalIcon icon="icon-name" ...>
|
||||
const localIconMatches = content.match(
|
||||
/<LocalIcon\s+[^>]*icon="([^"]+)"/g,
|
||||
);
|
||||
if (localIconMatches) {
|
||||
localIconMatches.forEach((match) => {
|
||||
const iconMatch = match.match(/icon="([^"]+)"/);
|
||||
if (iconMatch) {
|
||||
usedIcons.add(iconMatch[1]);
|
||||
debug(
|
||||
` Found: ${iconMatch[1]} in ${path.relative(srcDir, filePath)}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Match LocalIcon usage: <LocalIcon icon='icon-name' ...>
|
||||
const localIconSingleQuoteMatches = content.match(
|
||||
/<LocalIcon\s+[^>]*icon='([^']+)'/g,
|
||||
);
|
||||
if (localIconSingleQuoteMatches) {
|
||||
localIconSingleQuoteMatches.forEach((match) => {
|
||||
const iconMatch = match.match(/icon='([^']+)'/);
|
||||
if (iconMatch) {
|
||||
usedIcons.add(iconMatch[1]);
|
||||
debug(
|
||||
` Found: ${iconMatch[1]} in ${path.relative(srcDir, filePath)}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Match old material-symbols-rounded spans: <span className="material-symbols-rounded">icon-name</span>
|
||||
const spanMatches = content.match(
|
||||
/<span[^>]*className="[^"]*material-symbols-rounded[^"]*"[^>]*>([^<]+)<\/span>/g,
|
||||
);
|
||||
if (spanMatches) {
|
||||
spanMatches.forEach((match) => {
|
||||
const iconMatch = match.match(/>([^<]+)<\/span>/);
|
||||
if (iconMatch && iconMatch[1].trim()) {
|
||||
const iconName = iconMatch[1].trim();
|
||||
usedIcons.add(iconName);
|
||||
debug(
|
||||
` Found (legacy): ${iconName} in ${path.relative(srcDir, filePath)}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Match Icon component usage: <Icon icon="material-symbols:icon-name" ...>
|
||||
const iconMatches = content.match(
|
||||
/<Icon\s+[^>]*icon="material-symbols:([^"]+)"/g,
|
||||
);
|
||||
if (iconMatches) {
|
||||
iconMatches.forEach((match) => {
|
||||
const iconMatch = match.match(/icon="material-symbols:([^"]+)"/);
|
||||
if (iconMatch) {
|
||||
usedIcons.add(iconMatch[1]);
|
||||
debug(
|
||||
` Found (Icon): ${iconMatch[1]} in ${path.relative(srcDir, filePath)}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Match icon config usage: icon: 'icon-name' or icon: "icon-name"
|
||||
const iconPropertyMatches = content.match(
|
||||
/icon:\s*(['"])([a-z0-9-]+)\1/g,
|
||||
);
|
||||
if (iconPropertyMatches) {
|
||||
iconPropertyMatches.forEach((match) => {
|
||||
const iconMatch = match.match(/icon:\s*(['"])([a-z0-9-]+)\1/);
|
||||
if (iconMatch) {
|
||||
usedIcons.add(iconMatch[2]);
|
||||
debug(
|
||||
` Found (config): ${iconMatch[2]} in ${path.relative(srcDir, filePath)}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
scanDirectory(srcDir);
|
||||
|
||||
const iconArray = Array.from(usedIcons).sort();
|
||||
info(`📋 Found ${iconArray.length} unique icons across codebase`);
|
||||
|
||||
return iconArray;
|
||||
}
|
||||
|
||||
// Main async function
|
||||
async function main() {
|
||||
// Auto-detect used icons
|
||||
const usedIcons = scanForUsedIcons();
|
||||
|
||||
// Check if we need to regenerate (compare with existing)
|
||||
const outputPath = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"src",
|
||||
"assets",
|
||||
"material-symbols-icons.json",
|
||||
);
|
||||
let needsRegeneration = true;
|
||||
|
||||
if (fs.existsSync(outputPath)) {
|
||||
try {
|
||||
const existingSet = JSON.parse(fs.readFileSync(outputPath, "utf8"));
|
||||
const existingIcons = Object.keys(existingSet.icons || {}).sort();
|
||||
const currentIcons = [...usedIcons].sort();
|
||||
|
||||
if (JSON.stringify(existingIcons) === JSON.stringify(currentIcons)) {
|
||||
needsRegeneration = false;
|
||||
info(
|
||||
`✅ Icon set already up-to-date (${usedIcons.length} icons, ${Math.round(fs.statSync(outputPath).size / 1024)}KB)`,
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// If we can't parse existing file, regenerate
|
||||
needsRegeneration = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!needsRegeneration) {
|
||||
info("🎉 No regeneration needed!");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
info(`🔍 Extracting ${usedIcons.length} icons from Material Symbols...`);
|
||||
|
||||
// Dynamic import of ES module
|
||||
const { getIcons } = await import("@iconify/utils");
|
||||
|
||||
// Extract only our used icons from the full set
|
||||
const extractedIcons = getIcons(icons, usedIcons);
|
||||
|
||||
if (!extractedIcons) {
|
||||
console.error("❌ Failed to extract icons");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Check for missing icons
|
||||
const extractedIconNames = Object.keys(extractedIcons.icons || {});
|
||||
const missingIcons = usedIcons.filter(
|
||||
(icon) => !extractedIconNames.includes(icon),
|
||||
);
|
||||
|
||||
if (missingIcons.length > 0) {
|
||||
info(
|
||||
`⚠️ Missing icons (${missingIcons.length}): ${missingIcons.join(", ")}`,
|
||||
);
|
||||
info(
|
||||
"💡 These icons don't exist in Material Symbols. Please use available alternatives.",
|
||||
);
|
||||
}
|
||||
|
||||
// Create output directory
|
||||
const outputDir = path.join(__dirname, "..", "src", "assets");
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Write the extracted icon set to a file (outputPath already defined above)
|
||||
fs.writeFileSync(outputPath, JSON.stringify(extractedIcons, null, 2));
|
||||
|
||||
info(
|
||||
`✅ Successfully extracted ${Object.keys(extractedIcons.icons || {}).length} icons`,
|
||||
);
|
||||
info(
|
||||
`📦 Bundle size: ${Math.round(JSON.stringify(extractedIcons).length / 1024)}KB`,
|
||||
);
|
||||
info(`💾 Saved to: ${outputPath}`);
|
||||
|
||||
// Generate TypeScript types
|
||||
const typesContent = `// Auto-generated icon types
|
||||
// This file is automatically generated by scripts/generate-icons.js
|
||||
// Do not edit manually - changes will be overwritten
|
||||
|
||||
export type MaterialSymbolIcon = ${usedIcons.map((icon) => `'${icon}'`).join(" | ")};
|
||||
|
||||
export interface IconSet {
|
||||
prefix: string;
|
||||
icons: Record<string, any>;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
// Re-export the icon set as the default export with proper typing
|
||||
declare const iconSet: IconSet;
|
||||
export default iconSet;
|
||||
`;
|
||||
|
||||
const typesPath = path.join(outputDir, "material-symbols-icons.d.ts");
|
||||
fs.writeFileSync(typesPath, typesContent);
|
||||
|
||||
info(`📝 Generated types: ${typesPath}`);
|
||||
info(`🎉 Icon extraction complete!`);
|
||||
}
|
||||
|
||||
// Run the main function
|
||||
main().catch((error) => {
|
||||
console.error("❌ Script failed:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,518 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { execSync } = require("node:child_process");
|
||||
const {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
writeFileSync,
|
||||
readFileSync,
|
||||
} = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const { argv } = require("node:process");
|
||||
const inputIdx = argv.indexOf("--input");
|
||||
const INPUT_FILE = inputIdx > -1 ? argv[inputIdx + 1] : null;
|
||||
const POSTPROCESS_ONLY = !!INPUT_FILE;
|
||||
|
||||
// __dirname is available in CommonJS by default
|
||||
|
||||
/**
|
||||
* Generate 3rd party licenses for frontend dependencies
|
||||
* This script creates a JSON file similar to the Java backend's 3rdPartyLicenses.json
|
||||
*/
|
||||
|
||||
const OUTPUT_FILE = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"src",
|
||||
"assets",
|
||||
"3rdPartyLicenses.json",
|
||||
);
|
||||
const PACKAGE_JSON = path.join(__dirname, "..", "package.json");
|
||||
|
||||
// Ensure the output directory exists
|
||||
const outputDir = path.dirname(OUTPUT_FILE);
|
||||
if (!existsSync(outputDir)) {
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
console.log("🔍 Generating frontend license report...");
|
||||
|
||||
try {
|
||||
// Safety guard: don't run this script on fork PRs (workflow setzt PR_IS_FORK)
|
||||
if (process.env.PR_IS_FORK === "true" && !POSTPROCESS_ONLY) {
|
||||
console.error(
|
||||
"Fork PR detected: only --input (postprocess-only) mode is allowed.",
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
let licenseData;
|
||||
// Generate license report using pinned license-checker; disable lifecycle scripts
|
||||
if (POSTPROCESS_ONLY) {
|
||||
if (!INPUT_FILE || !existsSync(INPUT_FILE)) {
|
||||
console.error("❌ --input file missing or not found");
|
||||
process.exit(1);
|
||||
}
|
||||
licenseData = JSON.parse(readFileSync(INPUT_FILE, "utf8"));
|
||||
} else {
|
||||
const licenseReport = execSync(
|
||||
// 'npx --yes [email protected] --production --json',
|
||||
"npx --yes license-report --only=prod --output=json",
|
||||
{
|
||||
encoding: "utf8",
|
||||
cwd: path.dirname(PACKAGE_JSON),
|
||||
env: { ...process.env, NPM_CONFIG_IGNORE_SCRIPTS: "true" },
|
||||
},
|
||||
);
|
||||
try {
|
||||
licenseData = JSON.parse(licenseReport);
|
||||
} catch (parseError) {
|
||||
console.error("❌ Failed to parse license data:", parseError.message);
|
||||
console.error("Raw output:", licenseReport.substring(0, 500) + "...");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (!Array.isArray(licenseData)) {
|
||||
console.error("❌ Invalid license data structure");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Convert license-checker format to array
|
||||
const licenseArray = licenseData.map((dep) => {
|
||||
let licenseType = dep.licenseType;
|
||||
|
||||
// Handle missing or null licenses
|
||||
if (!licenseType || licenseType === null || licenseType === undefined) {
|
||||
licenseType = "Unknown";
|
||||
}
|
||||
|
||||
// Handle empty string licenses
|
||||
if (licenseType === "") {
|
||||
licenseType = "Unknown";
|
||||
}
|
||||
|
||||
// Handle array licenses (rare but possible)
|
||||
if (Array.isArray(licenseType)) {
|
||||
licenseType = licenseType.join(" AND ");
|
||||
}
|
||||
|
||||
// Handle object licenses (fallback)
|
||||
if (typeof licenseType === "object" && licenseType !== null) {
|
||||
licenseType = "Unknown";
|
||||
}
|
||||
|
||||
if (
|
||||
"posthog-js" === dep.name &&
|
||||
licenseType.startsWith("SEE LICENSE IN LICENSE")
|
||||
) {
|
||||
licenseType =
|
||||
"SEE LICENSE IN LICENSE https://github.com/PostHog/posthog-js/blob/main/LICENSE";
|
||||
}
|
||||
|
||||
return {
|
||||
name: dep.name,
|
||||
version:
|
||||
dep.installedVersion ||
|
||||
dep.definedVersion ||
|
||||
dep.remoteVersion ||
|
||||
"unknown",
|
||||
licenseType: licenseType,
|
||||
repository: dep.link,
|
||||
url: dep.link,
|
||||
link: dep.link,
|
||||
};
|
||||
});
|
||||
|
||||
// Transform to match Java backend format
|
||||
const transformedData = {
|
||||
dependencies: licenseArray.map((dep) => {
|
||||
const licenseType = Array.isArray(dep.licenseType)
|
||||
? dep.licenseType.join(", ")
|
||||
: dep.licenseType || "Unknown";
|
||||
const licenseUrl = dep.link || getLicenseUrl(licenseType);
|
||||
|
||||
return {
|
||||
moduleName: dep.name,
|
||||
moduleUrl:
|
||||
dep.repository ||
|
||||
dep.url ||
|
||||
`https://www.npmjs.com/package/${dep.name}`,
|
||||
moduleVersion: dep.version,
|
||||
moduleLicense: licenseType,
|
||||
moduleLicenseUrl: licenseUrl,
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
// Log summary of license types found
|
||||
const licenseSummary = licenseArray.reduce((acc, dep) => {
|
||||
const license = Array.isArray(dep.licenseType)
|
||||
? dep.licenseType.join(", ")
|
||||
: dep.licenseType || "Unknown";
|
||||
acc[license] = (acc[license] || 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
console.log("📊 License types found:");
|
||||
Object.entries(licenseSummary).forEach(([license, count]) => {
|
||||
console.log(` ${license}: ${count} packages`);
|
||||
});
|
||||
|
||||
// Log any complex or unusual license formats for debugging
|
||||
const complexLicenses = licenseArray.filter(
|
||||
(dep) =>
|
||||
dep.licenseType &&
|
||||
(dep.licenseType.includes("AND") ||
|
||||
dep.licenseType.includes("OR") ||
|
||||
dep.licenseType === "Unknown" ||
|
||||
dep.licenseType.includes("SEE LICENSE")),
|
||||
);
|
||||
|
||||
if (complexLicenses.length > 0) {
|
||||
console.log("\n🔍 Complex/Edge case licenses detected:");
|
||||
complexLicenses.forEach((dep) => {
|
||||
console.log(` ${dep.name}@${dep.version}: "${dep.licenseType}"`);
|
||||
});
|
||||
}
|
||||
|
||||
// Check for potentially problematic licenses
|
||||
const problematicLicenses = checkLicenseCompatibility(
|
||||
licenseSummary,
|
||||
licenseArray,
|
||||
);
|
||||
if (problematicLicenses.length > 0) {
|
||||
console.log("\n⚠️ License compatibility warnings:");
|
||||
problematicLicenses.forEach((warning) => {
|
||||
console.log(` ${warning.message}`);
|
||||
});
|
||||
|
||||
// Write license warnings to a separate file for CI/CD
|
||||
const warningsFile = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"src",
|
||||
"assets",
|
||||
"license-warnings.json",
|
||||
);
|
||||
writeFileSync(
|
||||
warningsFile,
|
||||
JSON.stringify(
|
||||
{
|
||||
warnings: problematicLicenses,
|
||||
generated: new Date().toISOString(),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
console.log(`⚠️ License warnings saved to: ${warningsFile}`);
|
||||
} else {
|
||||
console.log("\n✅ All licenses appear to be corporate-friendly");
|
||||
}
|
||||
|
||||
// Write to file
|
||||
writeFileSync(OUTPUT_FILE, JSON.stringify(transformedData, null, 2) + "\n");
|
||||
|
||||
console.log(`✅ License report generated successfully!`);
|
||||
console.log(`📄 Found ${transformedData.dependencies.length} dependencies`);
|
||||
console.log(`💾 Saved to: ${OUTPUT_FILE}`);
|
||||
} catch (error) {
|
||||
console.error("❌ Error generating license report:", error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get standard license URLs for common licenses
|
||||
*/
|
||||
function getLicenseUrl(licenseType) {
|
||||
if (!licenseType || licenseType === "Unknown") return "";
|
||||
|
||||
const licenseUrls = {
|
||||
MIT: "https://opensource.org/licenses/MIT",
|
||||
"MIT*": "https://opensource.org/licenses/MIT",
|
||||
"Apache-2.0": "https://www.apache.org/licenses/LICENSE-2.0",
|
||||
"Apache License 2.0": "https://www.apache.org/licenses/LICENSE-2.0",
|
||||
"BSD-3-Clause": "https://opensource.org/licenses/BSD-3-Clause",
|
||||
"BSD-2-Clause": "https://opensource.org/licenses/BSD-2-Clause",
|
||||
BSD: "https://opensource.org/licenses/BSD-3-Clause",
|
||||
"GPL-3.0": "https://www.gnu.org/licenses/gpl-3.0.html",
|
||||
"GPL-2.0": "https://www.gnu.org/licenses/gpl-2.0.html",
|
||||
"LGPL-2.1": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html",
|
||||
"LGPL-3.0": "https://www.gnu.org/licenses/lgpl-3.0.html",
|
||||
ISC: "https://opensource.org/licenses/ISC",
|
||||
"CC0-1.0": "https://creativecommons.org/publicdomain/zero/1.0/",
|
||||
Unlicense: "https://unlicense.org/",
|
||||
"MPL-2.0": "https://www.mozilla.org/en-US/MPL/2.0/",
|
||||
WTFPL: "http://www.wtfpl.net/",
|
||||
Zlib: "https://opensource.org/licenses/Zlib",
|
||||
"Artistic-2.0": "https://opensource.org/licenses/Artistic-2.0",
|
||||
"EPL-1.0": "https://www.eclipse.org/legal/epl-v10.html",
|
||||
"EPL-2.0": "https://www.eclipse.org/legal/epl-2.0/",
|
||||
"CDDL-1.0": "https://opensource.org/licenses/CDDL-1.0",
|
||||
Ruby: "https://www.ruby-lang.org/en/about/license.txt",
|
||||
"Python-2.0": "https://www.python.org/download/releases/2.0/license/",
|
||||
"Public Domain": "https://creativecommons.org/publicdomain/zero/1.0/",
|
||||
UNLICENSED: "",
|
||||
};
|
||||
|
||||
// Try exact match first
|
||||
if (licenseUrls[licenseType]) {
|
||||
return licenseUrls[licenseType];
|
||||
}
|
||||
|
||||
// Try case-insensitive match
|
||||
const lowerType = licenseType.toLowerCase();
|
||||
for (const [key, url] of Object.entries(licenseUrls)) {
|
||||
if (key.toLowerCase() === lowerType) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle complex SPDX expressions like "(MIT AND Zlib)" or "(MIT OR CC0-1.0)"
|
||||
if (licenseType.includes("AND") || licenseType.includes("OR")) {
|
||||
// Extract the first license from compound expressions for URL
|
||||
const match = licenseType.match(/\(?\s*([A-Za-z0-9\-.]+)/);
|
||||
if (match && licenseUrls[match[1]]) {
|
||||
return licenseUrls[match[1]];
|
||||
}
|
||||
}
|
||||
|
||||
// For non-standard licenses, return empty string (will use package link if available)
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for potentially problematic licenses that may not be MIT/corporate compatible
|
||||
*/
|
||||
function checkLicenseCompatibility(licenseSummary, licenseArray) {
|
||||
const warnings = [];
|
||||
|
||||
// Define problematic license patterns
|
||||
const problematicLicenses = {
|
||||
// Copyleft licenses
|
||||
"GPL-2.0": "Strong copyleft license - requires derivative works to be GPL",
|
||||
"GPL-3.0": "Strong copyleft license - requires derivative works to be GPL",
|
||||
"LGPL-2.1":
|
||||
"Weak copyleft license - may require source disclosure for modifications",
|
||||
"LGPL-3.0":
|
||||
"Weak copyleft license - may require source disclosure for modifications",
|
||||
"AGPL-3.0":
|
||||
"Network copyleft license - requires source disclosure for network use",
|
||||
"AGPL-1.0":
|
||||
"Network copyleft license - requires source disclosure for network use",
|
||||
|
||||
// Other potentially problematic licenses
|
||||
WTFPL: "Potentially problematic license - legal uncertainty",
|
||||
"CC-BY-SA-4.0":
|
||||
"ShareAlike license - requires derivative works to use same license",
|
||||
"CC-BY-SA-3.0":
|
||||
"ShareAlike license - requires derivative works to use same license",
|
||||
"CC-BY-NC-4.0": "Non-commercial license - prohibits commercial use",
|
||||
"CC-BY-NC-3.0": "Non-commercial license - prohibits commercial use",
|
||||
"OSL-3.0": "Copyleft license - requires derivative works to be OSL",
|
||||
"EPL-1.0": "Weak copyleft license - may require source disclosure",
|
||||
"EPL-2.0": "Weak copyleft license - may require source disclosure",
|
||||
"CDDL-1.0": "Weak copyleft license - may require source disclosure",
|
||||
"CDDL-1.1": "Weak copyleft license - may require source disclosure",
|
||||
"CPL-1.0": "Weak copyleft license - may require source disclosure",
|
||||
"MPL-1.1": "Weak copyleft license - may require source disclosure",
|
||||
"EUPL-1.1": "Copyleft license - requires derivative works to be EUPL",
|
||||
"EUPL-1.2": "Copyleft license - requires derivative works to be EUPL",
|
||||
UNLICENSED: "No license specified - usage rights unclear",
|
||||
Unknown: "License not detected - manual review required",
|
||||
};
|
||||
|
||||
// Known good licenses (no warnings needed)
|
||||
const goodLicenses = new Set([
|
||||
"MIT",
|
||||
"MIT*",
|
||||
"Apache-2.0",
|
||||
"Apache License 2.0",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"BSD",
|
||||
"ISC",
|
||||
"CC0-1.0",
|
||||
"Public Domain",
|
||||
"Unlicense",
|
||||
"0BSD",
|
||||
"BlueOak-1.0.0",
|
||||
"Zlib",
|
||||
"Artistic-2.0",
|
||||
"Python-2.0",
|
||||
"Ruby",
|
||||
"MPL-2.0",
|
||||
"CC-BY-4.0",
|
||||
"SEE LICENSE IN https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/refs/heads/main/proprietary/LICENSE",
|
||||
"SEE LICENSE IN LICENSE https://github.com/PostHog/posthog-js/blob/main/LICENSE",
|
||||
]);
|
||||
|
||||
// Helper function to normalize license names for comparison
|
||||
function normalizeLicense(license) {
|
||||
return license
|
||||
.replace(/-or-later$/, "") // Remove -or-later suffix
|
||||
.replace(/\+$/, "") // Remove + suffix
|
||||
.trim();
|
||||
}
|
||||
|
||||
// Check each license type
|
||||
Object.entries(licenseSummary).forEach(([license, count]) => {
|
||||
// Skip known good licenses
|
||||
if (goodLicenses.has(license)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this license only affects our own packages
|
||||
const affectedPackages = licenseArray.filter((dep) => {
|
||||
const depLicense = Array.isArray(dep.licenseType)
|
||||
? dep.licenseType.join(", ")
|
||||
: dep.licenseType;
|
||||
return depLicense === license;
|
||||
});
|
||||
|
||||
const isOnlyOurPackages = affectedPackages.every(
|
||||
(dep) =>
|
||||
dep.name === "frontend" ||
|
||||
dep.name.toLowerCase().includes("stirling-pdf") ||
|
||||
dep.name.toLowerCase().includes("stirling_pdf") ||
|
||||
dep.name.toLowerCase().includes("stirlingpdf"),
|
||||
);
|
||||
|
||||
if (
|
||||
isOnlyOurPackages &&
|
||||
(license === "UNLICENSED" || license.startsWith("SEE LICENSE IN"))
|
||||
) {
|
||||
return; // Skip warnings for our own Stirling-PDF packages
|
||||
}
|
||||
|
||||
// Check for compound licenses like "(MIT AND Zlib)" or "(MIT OR CC0-1.0)"
|
||||
if (license.includes("AND") || license.includes("OR")) {
|
||||
// For OR licenses, check if there's at least one acceptable license option
|
||||
if (license.includes("OR")) {
|
||||
// Extract license components from OR expression
|
||||
const orComponents = license
|
||||
.replace(/[()]/g, "") // Remove parentheses
|
||||
.split(" OR ")
|
||||
.map((component) => component.trim());
|
||||
|
||||
// Check if any component is in the goodLicenses set (with normalization)
|
||||
const hasGoodLicense = orComponents.some((component) => {
|
||||
const normalized = normalizeLicense(component);
|
||||
return goodLicenses.has(component) || goodLicenses.has(normalized);
|
||||
});
|
||||
|
||||
if (hasGoodLicense) {
|
||||
return; // Skip warning - can use the good license option
|
||||
}
|
||||
}
|
||||
|
||||
// For AND licenses or OR licenses with no good options, check for problematic components
|
||||
const hasProblematicComponent = Object.keys(problematicLicenses).some(
|
||||
(problematic) => license.includes(problematic),
|
||||
);
|
||||
|
||||
if (hasProblematicComponent) {
|
||||
const affectedPackages = licenseArray
|
||||
.filter((dep) => {
|
||||
const depLicense = Array.isArray(dep.licenseType)
|
||||
? dep.licenseType.join(", ")
|
||||
: dep.licenseType;
|
||||
return depLicense === license;
|
||||
})
|
||||
.map((dep) => ({
|
||||
name: dep.name,
|
||||
version: dep.version,
|
||||
url:
|
||||
dep.repository ||
|
||||
dep.url ||
|
||||
`https://www.npmjs.com/package/${dep.name}`,
|
||||
}));
|
||||
|
||||
const licenseType = license.includes("AND") ? "AND" : "OR";
|
||||
const reason =
|
||||
licenseType === "AND"
|
||||
? "Compound license with AND requirement - all components must be compatible"
|
||||
: "Compound license with potentially problematic components and no good fallback options";
|
||||
|
||||
warnings.push({
|
||||
message: `📋 This PR contains ${count} package${count > 1 ? "s" : ""} with compound license "${license}" - manual review recommended`,
|
||||
licenseType: license,
|
||||
licenseUrl: "",
|
||||
reason: reason,
|
||||
packageCount: count,
|
||||
affectedDependencies: affectedPackages,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for exact matches with problematic licenses
|
||||
if (problematicLicenses[license]) {
|
||||
const affectedPackages = licenseArray
|
||||
.filter((dep) => {
|
||||
const depLicense = Array.isArray(dep.licenseType)
|
||||
? dep.licenseType.join(", ")
|
||||
: dep.licenseType;
|
||||
return depLicense === license;
|
||||
})
|
||||
.map((dep) => ({
|
||||
name: dep.name,
|
||||
version: dep.version,
|
||||
url:
|
||||
dep.repository ||
|
||||
dep.url ||
|
||||
`https://www.npmjs.com/package/${dep.name}`,
|
||||
}));
|
||||
|
||||
const packageList =
|
||||
affectedPackages
|
||||
.map((pkg) => pkg.name)
|
||||
.slice(0, 5)
|
||||
.join(", ") +
|
||||
(affectedPackages.length > 5
|
||||
? `, and ${affectedPackages.length - 5} more`
|
||||
: "");
|
||||
const licenseUrl =
|
||||
getLicenseUrl(license) || "https://opensource.org/licenses";
|
||||
|
||||
warnings.push({
|
||||
message: `⚠️ This PR contains ${count} package${count > 1 ? "s" : ""} with license type [${license}](${licenseUrl}) - ${problematicLicenses[license]}. Affected packages: ${packageList}`,
|
||||
licenseType: license,
|
||||
licenseUrl: licenseUrl,
|
||||
reason: problematicLicenses[license],
|
||||
packageCount: count,
|
||||
affectedDependencies: affectedPackages,
|
||||
});
|
||||
} else {
|
||||
// Unknown license type - flag for manual review
|
||||
const affectedPackages = licenseArray
|
||||
.filter((dep) => {
|
||||
const depLicense = Array.isArray(dep.licenseType)
|
||||
? dep.licenseType.join(", ")
|
||||
: dep.licenseType;
|
||||
return depLicense === license;
|
||||
})
|
||||
.map((dep) => ({
|
||||
name: dep.name,
|
||||
version: dep.version,
|
||||
url:
|
||||
dep.repository ||
|
||||
dep.url ||
|
||||
`https://www.npmjs.com/package/${dep.name}`,
|
||||
}));
|
||||
|
||||
warnings.push({
|
||||
message: `❓ This PR contains ${count} package${count > 1 ? "s" : ""} with unknown license type "${license}" - manual review required`,
|
||||
licenseType: license,
|
||||
licenseUrl: "",
|
||||
reason: "Unknown license type",
|
||||
packageCount: count,
|
||||
affectedDependencies: affectedPackages,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return warnings;
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Stirling PDF Sample Document Generator
|
||||
*
|
||||
* This script uses Puppeteer to generate a sample PDF from a HTML template.
|
||||
* The output is used in the onboarding tour and as a demo document
|
||||
* for users to experiment with Stirling PDF's features.
|
||||
*/
|
||||
|
||||
import puppeteer from "puppeteer";
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname, join } from "path";
|
||||
import { existsSync, mkdirSync, statSync } from "fs";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const TEMPLATE_PATH = join(__dirname, "template.html");
|
||||
const OUTPUT_DIR = join(__dirname, "../../public/samples");
|
||||
const OUTPUT_PATH = join(OUTPUT_DIR, "Sample.pdf");
|
||||
|
||||
async function generatePDF() {
|
||||
console.log("🚀 Starting Stirling PDF sample document generation...\n");
|
||||
|
||||
// Ensure output directory exists
|
||||
if (!existsSync(OUTPUT_DIR)) {
|
||||
mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
console.log(`✅ Created output directory: ${OUTPUT_DIR}`);
|
||||
}
|
||||
|
||||
// Check if template exists
|
||||
if (!existsSync(TEMPLATE_PATH)) {
|
||||
console.error(`❌ Template file not found: ${TEMPLATE_PATH}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`📄 Reading template: ${TEMPLATE_PATH}`);
|
||||
|
||||
let browser;
|
||||
try {
|
||||
// Launch Puppeteer
|
||||
console.log("🌐 Launching browser...");
|
||||
browser = await puppeteer.launch({
|
||||
headless: "new",
|
||||
args: ["--no-sandbox", "--disable-setuid-sandbox"],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
// Set viewport to match A4 proportions
|
||||
await page.setViewport({
|
||||
width: 794, // A4 width in pixels at 96 DPI
|
||||
height: 1123, // A4 height in pixels at 96 DPI
|
||||
deviceScaleFactor: 2, // Higher quality rendering
|
||||
});
|
||||
|
||||
// Navigate to the template file
|
||||
const fileUrl = `file://${TEMPLATE_PATH}`;
|
||||
console.log("📖 Loading HTML template...");
|
||||
await page.goto(fileUrl, {
|
||||
waitUntil: "networkidle0", // Wait for all resources to load
|
||||
});
|
||||
|
||||
// Generate PDF with A4 dimensions
|
||||
console.log("📝 Generating PDF...");
|
||||
await page.pdf({
|
||||
path: OUTPUT_PATH,
|
||||
format: "A4",
|
||||
printBackground: true,
|
||||
margin: {
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
},
|
||||
preferCSSPageSize: true,
|
||||
});
|
||||
|
||||
console.log("\n✅ PDF generated successfully!");
|
||||
console.log(`📦 Output: ${OUTPUT_PATH}`);
|
||||
|
||||
// Get file size
|
||||
const stats = statSync(OUTPUT_PATH);
|
||||
const fileSizeInKB = (stats.size / 1024).toFixed(2);
|
||||
console.log(`📊 File size: ${fileSizeInKB} KB`);
|
||||
} catch (error) {
|
||||
console.error("\n❌ Error generating PDF:", error.message);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
if (browser) {
|
||||
await browser.close();
|
||||
console.log("🔒 Browser closed.");
|
||||
}
|
||||
}
|
||||
|
||||
console.log("\n🎉 Done! Sample PDF is ready for use in Stirling PDF.\n");
|
||||
}
|
||||
|
||||
// Run the generator
|
||||
generatePDF().catch((error) => {
|
||||
console.error("Fatal error:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,433 +0,0 @@
|
||||
/* Stirling PDF Sample Document Styles */
|
||||
|
||||
:root {
|
||||
/* Brand Colors */
|
||||
--brand-red: #8e3231;
|
||||
--brand-blue: #3b82f6;
|
||||
|
||||
/* Category Colors */
|
||||
--color-general: #3b82f6;
|
||||
--color-security: #f59e0b;
|
||||
--color-formatting: #8b5cf6;
|
||||
--color-automation: #ec4899;
|
||||
|
||||
/* Neutral Colors */
|
||||
--color-black: #111827;
|
||||
--color-gray-dark: #4b5563;
|
||||
--color-gray-medium: #6b7280;
|
||||
--color-gray-light: #e5e7eb;
|
||||
--color-gray-lighter: #f3f4f6;
|
||||
--color-white: #ffffff;
|
||||
|
||||
/* Font Stack */
|
||||
--font-family:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu",
|
||||
"Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-family);
|
||||
color: var(--color-black);
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Page Structure - A4 Dimensions */
|
||||
.page {
|
||||
width: 210mm;
|
||||
height: 297mm;
|
||||
background: white;
|
||||
page-break-after: always;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.page:last-child {
|
||||
page-break-after: auto;
|
||||
}
|
||||
|
||||
/* Page 1: Hero / Cover */
|
||||
.page-1 {
|
||||
background: var(--brand-red);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Decorative shapes container */
|
||||
.decorative-shapes {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.shape {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
/* Logo SVG shape - top-right */
|
||||
.shape-1 {
|
||||
top: -120px;
|
||||
right: -100px;
|
||||
width: 450px;
|
||||
height: auto;
|
||||
opacity: 0.12;
|
||||
}
|
||||
|
||||
/* Logo SVG shape - top-left */
|
||||
.shape-2 {
|
||||
top: -80px;
|
||||
left: -80px;
|
||||
width: 350px;
|
||||
height: auto;
|
||||
opacity: 0.08;
|
||||
}
|
||||
|
||||
/* Logo SVG shape - bottom-left */
|
||||
.shape-3 {
|
||||
bottom: -180px;
|
||||
left: -150px;
|
||||
width: 550px;
|
||||
height: auto;
|
||||
opacity: 0.15;
|
||||
}
|
||||
|
||||
/* Logo SVG shape - bottom-right */
|
||||
.shape-4 {
|
||||
bottom: -100px;
|
||||
right: -120px;
|
||||
width: 400px;
|
||||
height: auto;
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
/* Small accent shape center-right */
|
||||
.shape-5 {
|
||||
top: 50%;
|
||||
right: -30px;
|
||||
width: 200px;
|
||||
height: auto;
|
||||
opacity: 0.08;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
text-align: center;
|
||||
padding: 60px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
margin-bottom: 48px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hero-logo {
|
||||
width: 280px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.hero-tagline {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: var(--color-white);
|
||||
margin-bottom: 32px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.hero-stats {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.stat-badge {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 24px 48px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 48px;
|
||||
font-weight: 700;
|
||||
color: var(--brand-red);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 18px;
|
||||
color: var(--color-gray-dark);
|
||||
margin-top: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.hero-features {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.feature-pill {
|
||||
padding: 12px 24px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
border-radius: 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
/* Page 2: What is Stirling PDF */
|
||||
.page-2 {
|
||||
padding: 60px;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
color: var(--brand-red);
|
||||
margin-bottom: 24px;
|
||||
border-bottom: 4px solid var(--brand-red);
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
font-size: 16px;
|
||||
color: var(--color-gray-dark);
|
||||
margin-bottom: 48px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.value-props {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.value-prop {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.value-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
color: var(--brand-red);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.value-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.value-prop h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.value-prop p {
|
||||
font-size: 14px;
|
||||
color: var(--color-gray-dark);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Page 3: Key Features */
|
||||
.page-3 {
|
||||
padding: 60px;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: white;
|
||||
border: 2px solid var(--color-gray-light);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.feature-card[data-category="general"] {
|
||||
border-color: var(--color-general);
|
||||
}
|
||||
|
||||
.feature-card[data-category="security"] {
|
||||
border-color: var(--color-security);
|
||||
}
|
||||
|
||||
.feature-card[data-category="formatting"] {
|
||||
border-color: var(--color-formatting);
|
||||
}
|
||||
|
||||
.feature-card[data-category="automation"] {
|
||||
border-color: var(--color-automation);
|
||||
}
|
||||
|
||||
.feature-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.feature-icon-large {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.feature-card[data-category="general"] .feature-icon-large {
|
||||
color: var(--color-general);
|
||||
}
|
||||
|
||||
.feature-card[data-category="security"] .feature-icon-large {
|
||||
color: var(--color-security);
|
||||
}
|
||||
|
||||
.feature-card[data-category="formatting"] .feature-icon-large {
|
||||
color: var(--color-formatting);
|
||||
}
|
||||
|
||||
.feature-card[data-category="automation"] .feature-icon-large {
|
||||
color: var(--color-automation);
|
||||
}
|
||||
|
||||
.feature-icon-large svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.feature-card h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.feature-list li {
|
||||
font-size: 14px;
|
||||
color: var(--color-gray-dark);
|
||||
padding: 6px 0;
|
||||
padding-left: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.feature-list li::before {
|
||||
content: "•";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--brand-red);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.additional-features {
|
||||
background: white;
|
||||
border: 2px solid var(--brand-red);
|
||||
padding: 24px;
|
||||
border-radius: 12px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.additional-features-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.additional-features-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
color: var(--brand-red);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.additional-features-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.additional-features h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--color-black);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.additional-features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.additional-features-grid ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.additional-features-grid li {
|
||||
font-size: 15px;
|
||||
color: var(--color-gray-dark);
|
||||
padding: 4px 0;
|
||||
padding-left: 24px;
|
||||
position: relative;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.additional-features-grid li::before {
|
||||
content: "•";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--brand-red);
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* Print Styles */
|
||||
@media print {
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page {
|
||||
margin: 0;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
@@ -1,325 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Stirling PDF - Sample Document</title>
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- Page 1: Hero / Cover Page -->
|
||||
<div class="page page-1">
|
||||
<div class="decorative-shapes">
|
||||
<img
|
||||
src="../../public/modern-logo/StirlingPDFLogoNoTextLight.svg"
|
||||
class="shape shape-1"
|
||||
alt=""
|
||||
/>
|
||||
<img
|
||||
src="../../public/modern-logo/StirlingPDFLogoNoTextDark.svg"
|
||||
class="shape shape-2"
|
||||
alt=""
|
||||
/>
|
||||
<img
|
||||
src="../../public/modern-logo/StirlingPDFLogoNoTextLight.svg"
|
||||
class="shape shape-3"
|
||||
alt=""
|
||||
/>
|
||||
<img
|
||||
src="../../public/modern-logo/StirlingPDFLogoNoTextDark.svg"
|
||||
class="shape shape-4"
|
||||
alt=""
|
||||
/>
|
||||
<img
|
||||
src="../../public/modern-logo/StirlingPDFLogoNoTextLight.svg"
|
||||
class="shape shape-5"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div class="hero-content">
|
||||
<div class="logo-container">
|
||||
<img
|
||||
src="../../public/modern-logo/StirlingPDFLogoWhiteText.svg"
|
||||
alt="Stirling PDF"
|
||||
class="hero-logo"
|
||||
/>
|
||||
</div>
|
||||
<h1 class="hero-tagline">The Free Adobe Acrobat Alternative</h1>
|
||||
<div class="hero-stats">
|
||||
<div class="stat-badge">
|
||||
<span class="stat-number">10M+</span>
|
||||
<span class="stat-label">Downloads</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-features">
|
||||
<div class="feature-pill">Open Source</div>
|
||||
<div class="feature-pill">Privacy First</div>
|
||||
<div class="feature-pill">Self-Hosted</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Page 2: What is Stirling PDF -->
|
||||
<div class="page page-2">
|
||||
<div class="content-wrapper">
|
||||
<h2 class="page-title">What is Stirling PDF?</h2>
|
||||
<p class="intro-text">
|
||||
Stirling PDF is a robust, web-based PDF manipulation tool. It enables
|
||||
you to carry out various operations on PDF files, including splitting,
|
||||
merging, converting, rearranging, adding images, rotating,
|
||||
compressing, and more.
|
||||
</p>
|
||||
|
||||
<div class="value-props">
|
||||
<div class="value-prop">
|
||||
<div class="value-icon">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>50+ PDF Operations</h3>
|
||||
<p>
|
||||
Comprehensive toolkit covering all your PDF needs. From basic
|
||||
operations to advanced processing.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="value-prop">
|
||||
<div class="value-icon">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M7.4 17.25q-1.05.875-2.187.8t-1.988-.775t-1.162-1.837t.412-2.338L4.35 10q-.625-.55-.987-1.325T3 7q0-1.65 1.175-2.825T7 3t2.825 1.175T11 7T9.825 9.825T7 11q-.225 0-.45-.025t-.425-.075L4.2 14.15q-.275.45-.175.888t.425.712t.775.313t.875-.313l10.5-9.025q1.05-.875 2.2-.788t2 .788t1.15 1.838t-.425 2.337L19.65 14q.625.55.988 1.325T21 17q0 1.65-1.175 2.825T17 21t-2.825-1.175T13 17t1.175-2.825T17 13q.225 0 .438.025t.412.075l1.95-3.25q.275-.45.175-.888t-.425-.712t-.775-.312t-.875.312zM7 9q.825 0 1.413-.587T9 7t-.587-1.412T7 5t-1.412.588T5 7t.588 1.413T7 9m10 10q.825 0 1.413-.587T19 17t-.587-1.412T17 15t-1.412.588T15 17t.588 1.413T17 19m0-2"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Workflow Automation</h3>
|
||||
<p>
|
||||
Chain multiple operations together and save them as reusable
|
||||
workflows. Perfect for recurring tasks.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="value-prop">
|
||||
<div class="value-icon">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<line x1="2" y1="12" x2="22" y2="12" />
|
||||
<path
|
||||
d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Multi-Language Support</h3>
|
||||
<p>
|
||||
Available in over 30 languages with community-contributed
|
||||
translations. Accessible to users worldwide.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="value-prop">
|
||||
<div class="value-icon">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<rect x="5" y="11" width="14" height="10" rx="2" />
|
||||
<circle cx="12" cy="16" r="1" />
|
||||
<path d="M8 11V7a4 4 0 0 1 8 0v4" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Privacy First</h3>
|
||||
<p>
|
||||
Self-hosted solution means your data stays on your infrastructure.
|
||||
You have full control over your documents.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="value-prop">
|
||||
<div class="value-icon">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="m9 12 2 2 4-4" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Open Source</h3>
|
||||
<p>
|
||||
Transparent, community-driven development. Inspect the code,
|
||||
contribute features, and adapt as needed.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="value-prop">
|
||||
<div class="value-icon">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<polyline points="16 18 22 12 16 6" />
|
||||
<polyline points="8 6 2 12 8 18" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3>API Access</h3>
|
||||
<p>
|
||||
RESTful API for integration with external tools and scripts.
|
||||
Automate PDF operations programmatically.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Page 3: Key Features -->
|
||||
<div class="page page-3">
|
||||
<div class="content-wrapper">
|
||||
<h2 class="page-title">Key Features</h2>
|
||||
|
||||
<div class="features-grid">
|
||||
<div class="feature-card" data-category="general">
|
||||
<div class="feature-header">
|
||||
<div class="feature-icon-large">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"
|
||||
/>
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
<line x1="16" y1="13" x2="8" y2="13" />
|
||||
<line x1="16" y1="17" x2="8" y2="17" />
|
||||
<polyline points="10 9 9 9 8 9" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Page Operations</h3>
|
||||
</div>
|
||||
<ul class="feature-list">
|
||||
<li>Merge & split PDFs</li>
|
||||
<li>Rearrange pages</li>
|
||||
<li>Rotate & crop</li>
|
||||
<li>Extract pages</li>
|
||||
<li>Multi-page layout</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="feature-card" data-category="security">
|
||||
<div class="feature-header">
|
||||
<div class="feature-icon-large">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Security & Signing</h3>
|
||||
</div>
|
||||
<ul class="feature-list">
|
||||
<li>Password protection</li>
|
||||
<li>Digital signatures</li>
|
||||
<li>Watermarks</li>
|
||||
<li>Permission controls</li>
|
||||
<li>Redaction tools</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="feature-card" data-category="formatting">
|
||||
<div class="feature-header">
|
||||
<div class="feature-icon-large">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="m5.825 17l1.9 1.9q.3.3.288.7t-.313.7q-.3.275-.7.288t-.7-.288l-3.6-3.6q-.15-.15-.213-.325T2.426 16t.063-.375t.212-.325l3.6-3.6q.275-.275.688-.275t.712.275q.3.3.3.713t-.3.712L5.825 15H20q.425 0 .713.288T21 16t-.288.713T20 17zm12.35-8H4q-.425 0-.712-.288T3 8t.288-.712T4 7h14.175l-1.9-1.9q-.3-.3-.287-.7t.312-.7q.3-.275.7-.288t.7.288l3.6 3.6q.15.15.213.325t.062.375t-.062.375t-.213.325l-3.6 3.6q-.275.275-.687.275T16.3 12.3q-.3-.3-.3-.712t.3-.713z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>File Conversions</h3>
|
||||
</div>
|
||||
<ul class="feature-list">
|
||||
<li>PDF to/from images</li>
|
||||
<li>Office documents</li>
|
||||
<li>HTML to PDF</li>
|
||||
<li>Markdown to PDF</li>
|
||||
<li>PDF to Word/Excel</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="feature-card" data-category="automation">
|
||||
<div class="feature-header">
|
||||
<div class="feature-icon-large">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M7.4 17.25q-1.05.875-2.187.8t-1.988-.775t-1.162-1.837t.412-2.338L4.35 10q-.625-.55-.987-1.325T3 7q0-1.65 1.175-2.825T7 3t2.825 1.175T11 7T9.825 9.825T7 11q-.225 0-.45-.025t-.425-.075L4.2 14.15q-.275.45-.175.888t.425.712t.775.313t.875-.313l10.5-9.025q1.05-.875 2.2-.788t2 .788t1.15 1.838t-.425 2.337L19.65 14q.625.55.988 1.325T21 17q0 1.65-1.175 2.825T17 21t-2.825-1.175T13 17t1.175-2.825T17 13q.225 0 .438.025t.412.075l1.95-3.25q.275-.45.175-.888t-.425-.712t-.775-.312t-.875.312zM7 9q.825 0 1.413-.587T9 7t-.587-1.412T7 5t-1.412.588T5 7t.588 1.413T7 9m10 10q.825 0 1.413-.587T19 17t-.587-1.412T17 15t-1.412.588T15 17t.588 1.413T17 19m0-2"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Automation</h3>
|
||||
</div>
|
||||
<ul class="feature-list">
|
||||
<li>Multi-step workflows</li>
|
||||
<li>Chain PDF operations</li>
|
||||
<li>Save recurring tasks</li>
|
||||
<li>Batch file processing</li>
|
||||
<li>API integration</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="additional-features">
|
||||
<div class="additional-features-header">
|
||||
<div class="additional-features-icon">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M6 20q-.825 0-1.4125-.5875T4 18t.5875-1.4125T6 16t1.4125.5875T8 18t-.5875 1.4125T6 20m6 0q-.825 0-1.4125-.5875T10 18t.5875-1.4125T12 16t1.4125.5875T14 18t-.5875 1.4125T12 20m6 0q-.825 0-1.4125-.5875T16 18t.5875-1.4125T18 16t1.4125.5875T20 18t-.5875 1.4125T18 20M6 14q-.825 0-1.4125-.5875T4 12t.5875-1.4125T6 10t1.4125.5875T8 12t-.5875 1.4125T6 14m6 0q-.825 0-1.4125-.5875T10 12t.5875-1.4125T12 10t1.4125.5875T14 12t-.5875 1.4125T12 14m6 0q-.825 0-1.4125-.5875T16 12t.5875-1.4125T18 10t1.4125.5875T20 12t-.5875 1.4125T18 14M6 8q-.825 0-1.4125-.5875T4 6t.5875-1.4125T6 4t1.4125.5875T8 6t-.5875 1.4125T6 8m6 0q-.825 0-1.4125-.5875T10 6t.5875-1.4125T12 4t1.4125.5875T14 6t-.5875 1.4125T12 8m6 0q-.825 0-1.4125-.5875T16 6t.5875-1.4125T18 4t1.4125.5875T20 6t-.5875 1.4125T18 8"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Plus Many More</h3>
|
||||
</div>
|
||||
<div class="additional-features-grid">
|
||||
<ul>
|
||||
<li>OCR text recognition</li>
|
||||
<li>Compress PDFs</li>
|
||||
<li>Add images & stamps</li>
|
||||
<li>Detect blank pages</li>
|
||||
<li>Extract images</li>
|
||||
<li>Edit metadata</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>Flatten forms</li>
|
||||
<li>PDF/A conversion</li>
|
||||
<li>Add page numbers</li>
|
||||
<li>Remove pages</li>
|
||||
<li>Repair PDFs</li>
|
||||
<li>And 40+ more tools</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,44 +0,0 @@
|
||||
/**
|
||||
* Ensures `.env.local` (and mode-specific `.env.desktop.local` / `.env.saas.local`)
|
||||
* files exist so developers have a place to put overrides (API keys, machine-specific
|
||||
* settings) without touching the committed `.env` / `.env.desktop` / `.env.saas` files.
|
||||
*
|
||||
* Vite automatically layers these `.local` files on top of the committed ones.
|
||||
*
|
||||
* Usage:
|
||||
* tsx scripts/setup-env.ts # ensures .env.local
|
||||
* tsx scripts/setup-env.ts --desktop # also ensures .env.desktop.local
|
||||
* tsx scripts/setup-env.ts --saas # also ensures .env.saas.local
|
||||
*/
|
||||
|
||||
import { existsSync, writeFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
// npm scripts run from the directory containing package.json (frontend/)
|
||||
const root = process.cwd();
|
||||
const args = process.argv.slice(2);
|
||||
const isDesktop = args.includes("--desktop");
|
||||
const isSaas = args.includes("--saas");
|
||||
|
||||
function template(parent: string): string {
|
||||
return [
|
||||
"###############################################################################",
|
||||
`# Local overrides for \`frontend/${parent}\``,
|
||||
"# Put API keys and machine-specific settings here. Any variable defined here",
|
||||
`# takes precedence over the committed \`${parent}\``,
|
||||
"###############################################################################",
|
||||
"",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function ensureLocalFile(localFile: string, parentFile: string): void {
|
||||
const localPath = join(root, localFile);
|
||||
if (!existsSync(localPath)) {
|
||||
writeFileSync(localPath, template(parentFile));
|
||||
console.log(`setup-env: created empty ${localFile} for local overrides`);
|
||||
}
|
||||
}
|
||||
|
||||
ensureLocalFile(".env.local", ".env");
|
||||
if (isDesktop) ensureLocalFile(".env.desktop.local", ".env.desktop");
|
||||
if (isSaas) ensureLocalFile(".env.saas.local", ".env.saas");
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "node16",
|
||||
"moduleResolution": "node16",
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["./**/*.ts"],
|
||||
"exclude": []
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Cross-platform update:minor script
|
||||
* Calculates date from 7 days ago and runs npm update/audit with that date
|
||||
*/
|
||||
|
||||
const { spawn } = require("child_process");
|
||||
|
||||
// Calculate date from 7 days ago in YYYY-MM-DD format
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - 7);
|
||||
const beforeDate = date.toISOString().split("T")[0];
|
||||
|
||||
console.log(`Updating packages modified before: ${beforeDate}`);
|
||||
|
||||
let lastExitCode = 0;
|
||||
|
||||
// Run npm outdated first
|
||||
const outdated = spawn("npm", ["outdated"], { stdio: "inherit", shell: true });
|
||||
|
||||
outdated.on("close", (_code) => {
|
||||
// npm outdated returns exit code 1 if updates are available, so we ignore it
|
||||
|
||||
// Run npm update with before date
|
||||
const update = spawn("npm", ["update", `--before=${beforeDate}`], {
|
||||
stdio: "inherit",
|
||||
shell: true,
|
||||
});
|
||||
|
||||
update.on("close", (updateCode) => {
|
||||
// Track update failures
|
||||
if (updateCode !== 0) {
|
||||
lastExitCode = updateCode;
|
||||
}
|
||||
|
||||
// Run npm audit fix with before date
|
||||
const audit = spawn("npm", ["audit", "fix", `--before=${beforeDate}`], {
|
||||
stdio: "inherit",
|
||||
shell: true,
|
||||
});
|
||||
|
||||
audit.on("close", (auditCode) => {
|
||||
// Track audit failures (but don't override critical update failures)
|
||||
if (auditCode !== 0 && lastExitCode === 0) {
|
||||
lastExitCode = auditCode;
|
||||
}
|
||||
|
||||
// Update complete - report with tracked exit code
|
||||
console.log("\nPackage update complete!");
|
||||
process.exit(lastExitCode);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user