mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-11-16 01:21:16 +01:00
Add onboarding flow using Reactour (#4635)
# Description of Changes Add onboarding flow
This commit is contained in:
parent
3e6236d957
commit
3e23dc59b6
723
frontend/package-lock.json
generated
723
frontend/package-lock.json
generated
@ -37,6 +37,7 @@
|
||||
"@mantine/hooks": "^8.3.1",
|
||||
"@mui/icons-material": "^7.3.2",
|
||||
"@mui/material": "^7.3.2",
|
||||
"@reactour/tour": "^3.8.0",
|
||||
"@tailwindcss/postcss": "^4.1.13",
|
||||
"@tanstack/react-virtual": "^3.13.12",
|
||||
"autoprefixer": "^10.4.21",
|
||||
@ -87,6 +88,7 @@
|
||||
"postcss-cli": "^11.0.1",
|
||||
"postcss-preset-mantine": "^1.18.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"puppeteer": "^24.25.0",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint": "^8.44.1",
|
||||
"vite": "^7.1.7",
|
||||
@ -2554,6 +2556,79 @@
|
||||
"integrity": "sha512-igElrcnRPJh2nWYACschjH4OwGwzSa6xVFzRDVzpnjirUivdJ8nv4hE+H31nvwE56MFhvvglfHuotnWLMcRW7w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@puppeteer/browsers": {
|
||||
"version": "2.10.12",
|
||||
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.12.tgz",
|
||||
"integrity": "sha512-mP9iLFZwH+FapKJLeA7/fLqOlSUwYpMwjR1P5J23qd4e7qGJwecJccJqHYrjw33jmIZYV4dtiTHPD/J+1e7cEw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.3",
|
||||
"extract-zip": "^2.0.1",
|
||||
"progress": "^2.0.3",
|
||||
"proxy-agent": "^6.5.0",
|
||||
"semver": "^7.7.3",
|
||||
"tar-fs": "^3.1.1",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"browsers": "lib/cjs/main-cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@reactour/mask": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@reactour/mask/-/mask-1.2.0.tgz",
|
||||
"integrity": "sha512-XLgBLWfKJybtZjNTSO5lt/SIvRlCZBadB6JfE/hO1ErqURRjYhnv+edC0Ki1haUCqMGFppWk3lwcPCjmK0xNog==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@reactour/utils": "*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "16.x || 17.x || 18.x || 19.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@reactour/popover": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@reactour/popover/-/popover-1.3.0.tgz",
|
||||
"integrity": "sha512-YdyjSmHPvEeQEcJM4gcGFa5pI/Yf4nZGqwG4JnT+rK1SyUJBIPnm4Gkl/h7/+1g0KCFMkwNwagS3ZiXvZB7ThA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@reactour/utils": "*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "16.x || 17.x || 18.x || 19.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@reactour/tour": {
|
||||
"version": "3.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@reactour/tour/-/tour-3.8.0.tgz",
|
||||
"integrity": "sha512-KZTFi1pAvoTVKKRdBN5+XCYxXBp4k4Ql/acZcXyPvec8VU24fkMSEeV+v8krfYQpoVcewxIu3gM6xWZZLjxi7w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@reactour/mask": "*",
|
||||
"@reactour/popover": "*",
|
||||
"@reactour/utils": "*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "16.x || 17.x || 18.x || 19.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@reactour/utils": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@reactour/utils/-/utils-0.6.0.tgz",
|
||||
"integrity": "sha512-GqaLjQi7MJsgtAKjdiw2Eak1toFkADoLRnm1+HZpaD+yl+DkaHpC1N7JAl+kVOO5I17bWInPA+OFbXjO9Co8Qg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rooks/use-mutation-observer": "^4.11.2",
|
||||
"resize-observer-polyfill": "^1.5.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "16.x || 17.x || 18.x || 19.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/pluginutils": {
|
||||
"version": "1.0.0-beta.35",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.35.tgz",
|
||||
@ -2869,6 +2944,15 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rooks/use-mutation-observer": {
|
||||
"version": "4.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@rooks/use-mutation-observer/-/use-mutation-observer-4.11.2.tgz",
|
||||
"integrity": "sha512-vpsdrZdr6TkB1zZJcHx+fR1YC/pHs2BaqcuYiEGjBVbwY5xcC49+h0hAUtQKHth3oJqXfIX/Ng8S7s5HFHdM/A==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sec-ant/readable-stream": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
|
||||
@ -3502,6 +3586,13 @@
|
||||
"@testing-library/dom": ">=7.21.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@tootallnate/quickjs-emscripten": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
|
||||
"integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@ts-graphviz/adapter": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@ts-graphviz/adapter/-/adapter-2.0.6.tgz",
|
||||
@ -3734,6 +3825,17 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/yauzl": {
|
||||
"version": "2.10.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
|
||||
"integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.44.1.tgz",
|
||||
@ -4428,6 +4530,26 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/ast-types": {
|
||||
"version": "0.13.4",
|
||||
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
|
||||
"integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/ast-types/node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/ast-v8-to-istanbul": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.5.tgz",
|
||||
@ -4510,6 +4632,21 @@
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/b4a": {
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz",
|
||||
"integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"react-native-b4a": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-native-b4a": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/babel-plugin-macros": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
|
||||
@ -4532,6 +4669,103 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bare-events": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.0.tgz",
|
||||
"integrity": "sha512-AOhh6Bg5QmFIXdViHbMc2tLDsBIRxdkIaIddPslJF9Z5De3APBScuqGP2uThXnIpqFrgoxMNC6km7uXNIMLHXA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"bare-abort-controller": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-abort-controller": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-fs": {
|
||||
"version": "4.4.11",
|
||||
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.4.11.tgz",
|
||||
"integrity": "sha512-Bejmm9zRMvMTRoHS+2adgmXw1ANZnCNx+B5dgZpGwlP1E3x6Yuxea8RToddHUbWtVV0iUMWqsgZr8+jcgUI2SA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"bare-events": "^2.5.4",
|
||||
"bare-path": "^3.0.0",
|
||||
"bare-stream": "^2.6.4",
|
||||
"bare-url": "^2.2.2",
|
||||
"fast-fifo": "^1.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"bare": ">=1.16.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bare-buffer": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-buffer": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-os": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz",
|
||||
"integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"bare": ">=1.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
|
||||
"integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"bare-os": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-stream": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz",
|
||||
"integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"streamx": "^2.21.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bare-buffer": "*",
|
||||
"bare-events": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-buffer": {
|
||||
"optional": true
|
||||
},
|
||||
"bare-events": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-url": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.0.tgz",
|
||||
"integrity": "sha512-c+RCqMSZbkz97Mw1LWR0gcOqwK82oyYKfLoHJ8k13ybi1+I80ffdDzUy0TdAburdrR/kI0/VuN8YgEnJqX+Nyw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"bare-path": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
@ -4562,6 +4796,16 @@
|
||||
"baseline-browser-mapping": "dist/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/basic-ftp": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz",
|
||||
"integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bidi-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
|
||||
@ -4699,6 +4943,16 @@
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-crc32": {
|
||||
"version": "0.2.13",
|
||||
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
|
||||
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/cac": {
|
||||
"version": "6.7.14",
|
||||
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
||||
@ -4879,6 +5133,20 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/chromium-bidi": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-9.1.0.tgz",
|
||||
"integrity": "sha512-rlUzQ4WzIAWdIbY/viPShhZU2n21CxDUgazXVbw4Hu1MwaeUSEksSeM6DqPgpRjCLXRk702AVRxJxoOz0dw4OA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"mitt": "^3.0.1",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"devtools-protocol": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-cursor": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
|
||||
@ -5138,6 +5406,16 @@
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/data-uri-to-buffer": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
|
||||
"integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz",
|
||||
@ -5269,6 +5547,21 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/degenerator": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz",
|
||||
"integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ast-types": "^0.13.4",
|
||||
"escodegen": "^2.1.0",
|
||||
"esprima": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
@ -5483,6 +5776,13 @@
|
||||
"typescript": "^5.4.4"
|
||||
}
|
||||
},
|
||||
"node_modules/devtools-protocol": {
|
||||
"version": "0.0.1508733",
|
||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1508733.tgz",
|
||||
"integrity": "sha512-QJ1R5gtck6nDcdM+nlsaJXcelPEI7ZxSMw1ujHpO1c4+9l+Nue5qlebi9xO1Z2MGr92bFOQTW7/rrheh5hHxDg==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/dezalgo": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
|
||||
@ -5545,6 +5845,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.18.3",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
|
||||
@ -5571,6 +5881,16 @@
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/env-paths": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
|
||||
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/eol": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/eol/-/eol-0.10.0.tgz",
|
||||
@ -5989,6 +6309,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/events-universal": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
|
||||
"integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bare-events": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/expect-type": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz",
|
||||
@ -6006,6 +6336,43 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/extract-zip": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
||||
"integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1",
|
||||
"get-stream": "^5.1.0",
|
||||
"yauzl": "^2.10.0"
|
||||
},
|
||||
"bin": {
|
||||
"extract-zip": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.17.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@types/yauzl": "^2.9.1"
|
||||
}
|
||||
},
|
||||
"node_modules/extract-zip/node_modules/get-stream": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
|
||||
"integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pump": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
@ -6013,6 +6380,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-fifo": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
|
||||
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||
@ -6067,6 +6441,16 @@
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/fd-slicer": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
|
||||
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pend": "~1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.4.8",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
|
||||
@ -6445,6 +6829,21 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/get-uri": {
|
||||
"version": "6.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz",
|
||||
"integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"basic-ftp": "^5.0.2",
|
||||
"data-uri-to-buffer": "^6.0.2",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
@ -6900,6 +7299,16 @@
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
|
||||
"integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/is-arrayish": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
|
||||
@ -8056,6 +8465,13 @@
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/mitt": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
||||
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||
@ -8178,6 +8594,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/netmask": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz",
|
||||
"integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/no-case": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
|
||||
@ -8480,6 +8906,40 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/pac-proxy-agent": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz",
|
||||
"integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tootallnate/quickjs-emscripten": "^0.23.0",
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "^4.3.4",
|
||||
"get-uri": "^6.0.1",
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"pac-resolver": "^7.0.1",
|
||||
"socks-proxy-agent": "^8.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/pac-resolver": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz",
|
||||
"integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"degenerator": "^5.0.0",
|
||||
"netmask": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/package-json-from-dist": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
@ -8663,6 +9123,13 @@
|
||||
"@napi-rs/canvas": "^0.1.77"
|
||||
}
|
||||
},
|
||||
"node_modules/pend": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@ -9178,6 +9645,16 @@
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/progress": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
@ -9195,12 +9672,53 @@
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-agent": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz",
|
||||
"integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "^4.3.4",
|
||||
"http-proxy-agent": "^7.0.1",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
"lru-cache": "^7.14.1",
|
||||
"pac-proxy-agent": "^7.1.0",
|
||||
"proxy-from-env": "^1.1.0",
|
||||
"socks-proxy-agent": "^8.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-agent/node_modules/lru-cache": {
|
||||
"version": "7.18.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
|
||||
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
|
||||
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
@ -9211,6 +9729,74 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/puppeteer": {
|
||||
"version": "24.25.0",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.25.0.tgz",
|
||||
"integrity": "sha512-P3rUaom2w/Ubrnz3v3kSbxGkN7SpbtQeGRPb7iO86Bv/dAz2WUmGQBHr37W/Rp1fbAocMvu0rHFbCIJvjiNhGw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@puppeteer/browsers": "2.10.12",
|
||||
"chromium-bidi": "9.1.0",
|
||||
"cosmiconfig": "^9.0.0",
|
||||
"devtools-protocol": "0.0.1508733",
|
||||
"puppeteer-core": "24.25.0",
|
||||
"typed-query-selector": "^2.12.0"
|
||||
},
|
||||
"bin": {
|
||||
"puppeteer": "lib/cjs/puppeteer/node/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/puppeteer-core": {
|
||||
"version": "24.25.0",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.25.0.tgz",
|
||||
"integrity": "sha512-8Xs6q3Ut+C8y7sAaqjIhzv1QykGWG4gc2mEZ2mYE7siZFuRp4xQVehOf8uQKSQAkeL7jXUs3mknEeiqnRqUKvQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@puppeteer/browsers": "2.10.12",
|
||||
"chromium-bidi": "9.1.0",
|
||||
"debug": "^4.4.3",
|
||||
"devtools-protocol": "0.0.1508733",
|
||||
"typed-query-selector": "^2.12.0",
|
||||
"webdriver-bidi-protocol": "0.3.7",
|
||||
"ws": "^8.18.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/puppeteer/node_modules/cosmiconfig": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz",
|
||||
"integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"env-paths": "^2.2.1",
|
||||
"import-fresh": "^3.3.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"parse-json": "^5.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/d-fischer"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.9.5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/quansync": {
|
||||
"version": "0.2.11",
|
||||
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
|
||||
@ -9699,6 +10285,12 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/resize-observer-polyfill": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
||||
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.10",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
|
||||
@ -9917,9 +10509,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@ -10023,6 +10615,47 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/smart-buffer": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socks": {
|
||||
"version": "2.8.7",
|
||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
|
||||
"integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "^10.0.1",
|
||||
"smart-buffer": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socks-proxy-agent": {
|
||||
"version": "8.0.5",
|
||||
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
|
||||
"integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "^4.3.4",
|
||||
"socks": "^2.8.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||
@ -10145,6 +10778,18 @@
|
||||
"any-promise": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/streamx": {
|
||||
"version": "2.23.0",
|
||||
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
|
||||
"integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"events-universal": "^1.0.0",
|
||||
"fast-fifo": "^1.3.2",
|
||||
"text-decoder": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
@ -10424,6 +11069,33 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz",
|
||||
"integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pump": "^3.0.0",
|
||||
"tar-stream": "^3.1.5"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bare-fs": "^4.0.1",
|
||||
"bare-path": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-stream": {
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
|
||||
"integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"b4a": "^1.6.4",
|
||||
"fast-fifo": "^1.2.0",
|
||||
"streamx": "^2.15.0"
|
||||
}
|
||||
},
|
||||
"node_modules/test-exclude": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz",
|
||||
@ -10460,6 +11132,16 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/text-decoder": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz",
|
||||
"integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"b4a": "^1.6.4"
|
||||
}
|
||||
},
|
||||
"node_modules/text-table": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||
@ -10719,6 +11401,13 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/typed-query-selector": {
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz",
|
||||
"integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
||||
@ -11273,6 +11962,13 @@
|
||||
"integrity": "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/webdriver-bidi-protocol": {
|
||||
"version": "0.3.7",
|
||||
"resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.3.7.tgz",
|
||||
"integrity": "sha512-wIx5Gu/LLTeexxilpk8WxU2cpGAKlfbWRO5h+my6EMD1k5PYqM1qQO1MHUFf4f3KRnhBvpbZU7VkizAgeSEf7g==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz",
|
||||
@ -11507,6 +12203,17 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/yauzl": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
|
||||
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-crc32": "~0.2.3",
|
||||
"fd-slicer": "~1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
@ -11519,6 +12226,16 @@
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,6 +33,7 @@
|
||||
"@mantine/hooks": "^8.3.1",
|
||||
"@mui/icons-material": "^7.3.2",
|
||||
"@mui/material": "^7.3.2",
|
||||
"@reactour/tour": "^3.8.0",
|
||||
"@tailwindcss/postcss": "^4.1.13",
|
||||
"@tanstack/react-virtual": "^3.13.12",
|
||||
"autoprefixer": "^10.4.21",
|
||||
@ -66,6 +67,7 @@
|
||||
"generate-licenses": "node scripts/generate-licenses.js",
|
||||
"generate-icons": "node scripts/generate-icons.js",
|
||||
"generate-icons:verbose": "node scripts/generate-icons.js --verbose",
|
||||
"generate-sample-pdf": "node scripts/sample-pdf/generate.mjs",
|
||||
"test": "vitest",
|
||||
"test:run": "vitest run",
|
||||
"test:watch": "vitest --watch",
|
||||
@ -126,6 +128,7 @@
|
||||
"postcss-cli": "^11.0.1",
|
||||
"postcss-preset-mantine": "^1.18.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"puppeteer": "^24.25.0",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint": "^8.44.1",
|
||||
"vite": "^7.1.7",
|
||||
|
||||
@ -94,8 +94,8 @@
|
||||
"save": "Save",
|
||||
"saveToBrowser": "Save to Browser",
|
||||
"download": "Download",
|
||||
"pin": "Pin",
|
||||
"unpin": "Unpin",
|
||||
"pin": "Pin File (keep active after tool run)",
|
||||
"unpin": "Unpin File (replace after tool run)",
|
||||
"undoOperationTooltip": "Click to undo the last operation and restore the original files",
|
||||
"undo": "Undo",
|
||||
"moreOptions": "More Options",
|
||||
@ -455,6 +455,9 @@
|
||||
"alphabetical": "Alphabetical",
|
||||
"globalPopularity": "Global Popularity",
|
||||
"sortBy": "Sort by:",
|
||||
"mobile": {
|
||||
"brandAlt": "Stirling PDF logo"
|
||||
},
|
||||
"multiTool": {
|
||||
"tags": "multiple,tools",
|
||||
"title": "PDF Multi Tool",
|
||||
@ -3469,6 +3472,7 @@
|
||||
"automate": "Automate",
|
||||
"files": "Files",
|
||||
"activity": "Activity",
|
||||
"help": "Help",
|
||||
"account": "Account",
|
||||
"config": "Config",
|
||||
"allTools": "All Tools"
|
||||
@ -3969,5 +3973,38 @@
|
||||
"undoQuotaError": "Cannot undo: insufficient storage space",
|
||||
"undoStorageError": "Undo completed but some files could not be saved to storage",
|
||||
"undoSuccess": "Operation undone successfully",
|
||||
"unsupported": "Unsupported"
|
||||
"unsupported": "Unsupported",
|
||||
"onboarding": {
|
||||
"welcomeModal": {
|
||||
"title": "Welcome to Stirling PDF!",
|
||||
"description": "Would you like to take a quick 1-minute tour to learn the key features and how to get started?",
|
||||
"helpHint": "You can always access this tour later from the <strong>Help</strong> button in the bottom left.",
|
||||
"startTour": "Start Tour",
|
||||
"maybeLater": "Maybe Later",
|
||||
"dontShowAgain": "Don't Show Again"
|
||||
},
|
||||
"allTools": "This is the <strong>All Tools</strong> panel, where you can browse and select from all available PDF tools.",
|
||||
"selectCropTool": "Let's select the <strong>Crop</strong> tool to demonstrate how to use one of the tools.",
|
||||
"toolInterface": "This is the <strong>Crop</strong> tool interface. As you can see, there's not much there because we haven't added any PDF files to work with yet.",
|
||||
"filesButton": "The <strong>Files</strong> button on the Quick Access bar allows you to upload PDFs to use the tools on.",
|
||||
"fileSources": "You can upload new files or access recent files from here. For the tour, we'll just use a sample file.",
|
||||
"workbench": "This is the <strong>Workbench</strong> - the main area where you view and edit your PDFs.",
|
||||
"viewSwitcher": "Use these controls to select how you want to view your PDFs.",
|
||||
"viewer": "The <strong>Viewer</strong> lets you read and annotate your PDFs.",
|
||||
"pageEditor": "The <strong>Page Editor</strong> allows you to do various operations on the pages within your PDFs, such as reordering, rotating and deleting.",
|
||||
"activeFiles": "The <strong>Active Files</strong> view shows all of the PDFs you have loaded into the tool, and allows you to select which ones to process.",
|
||||
"fileCheckbox": "Clicking one of the files selects it for processing. You can select multiple files for batch operations.",
|
||||
"selectControls": "The <strong>Right Rail</strong> contains buttons to quickly select/deselect all of your active PDFs, along with buttons to change the app's theme or language.",
|
||||
"cropSettings": "Now that we've selected the file we want crop, we can configure the Crop tool to choose the area that we want to crop the PDF to.",
|
||||
"runButton": "Once the tool has been configured, this button allows you to run the tool on all the selected PDFs.",
|
||||
"results": "After the tool has finished running, the <strong>Review</strong> step will show a preview of the results in this panel, and allow you to undo the operation or download the file. ",
|
||||
"fileReplacement": "The modified file will replace the original file in the Workbench automatically, allowing you to easily run it through more tools.",
|
||||
"pinButton": "You can use the <strong>Pin</strong> button if you'd rather your files stay active after running tools on them.",
|
||||
"wrapUp": "You're all set! You've learnt about the main areas of the app and how to use them. Click the <strong>Help</strong> button whenever you like to see this tour again.",
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"finish": "Finish",
|
||||
"startTour": "Start Tour",
|
||||
"startTourDescription": "Take a guided tour of Stirling PDF's key features"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
frontend/public/samples/Sample.pdf
Normal file
BIN
frontend/public/samples/Sample.pdf
Normal file
Binary file not shown.
105
frontend/scripts/sample-pdf/generate.mjs
Executable file
105
frontend/scripts/sample-pdf/generate.mjs
Executable file
@ -0,0 +1,105 @@
|
||||
#!/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);
|
||||
});
|
||||
432
frontend/scripts/sample-pdf/styles.css
Normal file
432
frontend/scripts/sample-pdf/styles.css
Normal file
@ -0,0 +1,432 @@
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
234
frontend/scripts/sample-pdf/template.html
Normal file
234
frontend/scripts/sample-pdf/template.html
Normal file
@ -0,0 +1,234 @@
|
||||
<!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/branding/StirlingPDFLogoNoTextLight.svg" class="shape shape-1" alt="">
|
||||
<img src="../../public/branding/StirlingPDFLogoNoTextDark.svg" class="shape shape-2" alt="">
|
||||
<img src="../../public/branding/StirlingPDFLogoNoTextLight.svg" class="shape shape-3" alt="">
|
||||
<img src="../../public/branding/StirlingPDFLogoNoTextDark.svg" class="shape shape-4" alt="">
|
||||
<img src="../../public/branding/StirlingPDFLogoNoTextLight.svg" class="shape shape-5" alt="">
|
||||
</div>
|
||||
<div class="hero-content">
|
||||
<div class="logo-container">
|
||||
<img src="../../public/branding/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>
|
||||
@ -7,8 +7,11 @@ import { ToolWorkflowProvider } from "./contexts/ToolWorkflowContext";
|
||||
import { HotkeyProvider } from "./contexts/HotkeyContext";
|
||||
import { SidebarProvider } from "./contexts/SidebarContext";
|
||||
import { PreferencesProvider } from "./contexts/PreferencesContext";
|
||||
import { OnboardingProvider } from "./contexts/OnboardingContext";
|
||||
import { TourOrchestrationProvider } from "./contexts/TourOrchestrationContext";
|
||||
import ErrorBoundary from "./components/shared/ErrorBoundary";
|
||||
import HomePage from "./pages/HomePage";
|
||||
import OnboardingTour from "./components/onboarding/OnboardingTour";
|
||||
|
||||
// Import global styles
|
||||
import "./styles/tailwind.css";
|
||||
@ -43,25 +46,30 @@ export default function App() {
|
||||
<PreferencesProvider>
|
||||
<RainbowThemeProvider>
|
||||
<ErrorBoundary>
|
||||
<FileContextProvider enableUrlSync={true} enablePersistence={true}>
|
||||
<NavigationProvider>
|
||||
<FilesModalProvider>
|
||||
<ToolWorkflowProvider>
|
||||
<HotkeyProvider>
|
||||
<SidebarProvider>
|
||||
<ViewerProvider>
|
||||
<SignatureProvider>
|
||||
<RightRailProvider>
|
||||
<HomePage />
|
||||
</RightRailProvider>
|
||||
</SignatureProvider>
|
||||
</ViewerProvider>
|
||||
</SidebarProvider>
|
||||
</HotkeyProvider>
|
||||
</ToolWorkflowProvider>
|
||||
</FilesModalProvider>
|
||||
</NavigationProvider>
|
||||
</FileContextProvider>
|
||||
<OnboardingProvider>
|
||||
<FileContextProvider enableUrlSync={true} enablePersistence={true}>
|
||||
<NavigationProvider>
|
||||
<FilesModalProvider>
|
||||
<ToolWorkflowProvider>
|
||||
<HotkeyProvider>
|
||||
<SidebarProvider>
|
||||
<ViewerProvider>
|
||||
<SignatureProvider>
|
||||
<RightRailProvider>
|
||||
<TourOrchestrationProvider>
|
||||
<HomePage />
|
||||
<OnboardingTour />
|
||||
</TourOrchestrationProvider>
|
||||
</RightRailProvider>
|
||||
</SignatureProvider>
|
||||
</ViewerProvider>
|
||||
</SidebarProvider>
|
||||
</HotkeyProvider>
|
||||
</ToolWorkflowProvider>
|
||||
</FilesModalProvider>
|
||||
</NavigationProvider>
|
||||
</FileContextProvider>
|
||||
</OnboardingProvider>
|
||||
</ErrorBoundary>
|
||||
</RainbowThemeProvider>
|
||||
</PreferencesProvider>
|
||||
|
||||
@ -247,6 +247,7 @@ const FileEditorThumbnail = ({
|
||||
ref={fileElementRef}
|
||||
data-file-id={file.id}
|
||||
data-testid="file-thumbnail"
|
||||
data-tour="file-card-checkbox"
|
||||
data-selected={isSelected}
|
||||
data-supported={isSupported}
|
||||
className={`${styles.card} w-[18rem] h-[22rem] select-none flex flex-col shadow-sm transition-all relative`}
|
||||
@ -293,11 +294,12 @@ const FileEditorThumbnail = ({
|
||||
{/* Action buttons group */}
|
||||
<div className={styles.headerActions}>
|
||||
{/* Pin/Unpin icon */}
|
||||
<Tooltip label={isPinned ? t('unpin', 'Unpin') : t('pin', 'Pin')}>
|
||||
<Tooltip label={isPinned ? t('unpin', 'Unpin File (replace after tool run)') : t('pin', 'Pin File (keep active after tool run)')}>
|
||||
<ActionIcon
|
||||
aria-label={isPinned ? t('unpin', 'Unpin') : t('pin', 'Pin')}
|
||||
aria-label={isPinned ? t('unpin', 'Unpin File (replace after tool run)') : t('pin', 'Pin File (keep active after tool run)')}
|
||||
variant="subtle"
|
||||
className={isPinned ? styles.pinned : styles.headerIconButton}
|
||||
data-tour="file-card-pin"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (actualFile) {
|
||||
|
||||
@ -23,7 +23,7 @@ const DesktopLayout: React.FC = () => {
|
||||
width: '13.625rem',
|
||||
flexShrink: 0,
|
||||
height: '100%',
|
||||
}}>
|
||||
}} data-tour="file-sources">
|
||||
<FileSourceButtons />
|
||||
</Grid.Col>
|
||||
|
||||
|
||||
@ -153,6 +153,7 @@ export default function Workbench() {
|
||||
return (
|
||||
<Box
|
||||
className="flex-1 h-full min-w-80 relative flex flex-col"
|
||||
data-tour="workbench"
|
||||
style={
|
||||
isRainbowMode
|
||||
? {} // No background color in rainbow mode
|
||||
|
||||
8
frontend/src/components/onboarding/OnboardingTour.css
Normal file
8
frontend/src/components/onboarding/OnboardingTour.css
Normal file
@ -0,0 +1,8 @@
|
||||
/* Glow effect for tour highlighted area */
|
||||
.tour-highlight-glow {
|
||||
stroke: var(--mantine-primary-color-filled);
|
||||
stroke-width: 3px;
|
||||
rx: 8px;
|
||||
ry: 8px;
|
||||
filter: drop-shadow(0 0 10px var(--mantine-primary-color-filled));
|
||||
}
|
||||
331
frontend/src/components/onboarding/OnboardingTour.tsx
Normal file
331
frontend/src/components/onboarding/OnboardingTour.tsx
Normal file
@ -0,0 +1,331 @@
|
||||
import React from "react";
|
||||
import { TourProvider, useTour, type StepType } from '@reactour/tour';
|
||||
import { useOnboarding } from '../../contexts/OnboardingContext';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CloseButton, ActionIcon } from '@mantine/core';
|
||||
import { useFilesModalContext } from '../../contexts/FilesModalContext';
|
||||
import { useTourOrchestration } from '../../contexts/TourOrchestrationContext';
|
||||
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
import TourWelcomeModal from './TourWelcomeModal';
|
||||
import './OnboardingTour.css';
|
||||
|
||||
// Enum case order defines order steps will appear
|
||||
enum TourStep {
|
||||
ALL_TOOLS,
|
||||
SELECT_CROP_TOOL,
|
||||
TOOL_INTERFACE,
|
||||
FILES_BUTTON,
|
||||
FILE_SOURCES,
|
||||
WORKBENCH,
|
||||
VIEW_SWITCHER,
|
||||
VIEWER,
|
||||
PAGE_EDITOR,
|
||||
ACTIVE_FILES,
|
||||
FILE_CHECKBOX,
|
||||
SELECT_CONTROLS,
|
||||
CROP_SETTINGS,
|
||||
RUN_BUTTON,
|
||||
RESULTS,
|
||||
FILE_REPLACEMENT,
|
||||
PIN_BUTTON,
|
||||
WRAP_UP,
|
||||
}
|
||||
|
||||
function TourContent() {
|
||||
const { isOpen } = useOnboarding();
|
||||
const { setIsOpen, setCurrentStep } = useTour();
|
||||
const previousIsOpenRef = React.useRef(isOpen);
|
||||
|
||||
// Sync tour open state with context and reset to step 0 when reopening
|
||||
React.useEffect(() => {
|
||||
const wasClosedNowOpen = !previousIsOpenRef.current && isOpen;
|
||||
previousIsOpenRef.current = isOpen;
|
||||
|
||||
if (wasClosedNowOpen) {
|
||||
// Tour is being opened (Help button pressed), reset to first step
|
||||
setCurrentStep(0);
|
||||
}
|
||||
setIsOpen(isOpen);
|
||||
}, [isOpen, setIsOpen, setCurrentStep]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function OnboardingTour() {
|
||||
const { t } = useTranslation();
|
||||
const { completeTour, showWelcomeModal, setShowWelcomeModal, startTour } = useOnboarding();
|
||||
const { openFilesModal, closeFilesModal } = useFilesModalContext();
|
||||
const {
|
||||
saveWorkbenchState,
|
||||
restoreWorkbenchState,
|
||||
backToAllTools,
|
||||
selectCropTool,
|
||||
loadSampleFile,
|
||||
switchToViewer,
|
||||
switchToPageEditor,
|
||||
switchToActiveFiles,
|
||||
selectFirstFile,
|
||||
pinFile,
|
||||
modifyCropSettings,
|
||||
executeTool,
|
||||
} = useTourOrchestration();
|
||||
|
||||
// Define steps as object keyed by enum - TypeScript ensures all keys are present
|
||||
const stepsConfig: Record<TourStep, StepType> = {
|
||||
[TourStep.ALL_TOOLS]: {
|
||||
selector: '[data-tour="tool-panel"]',
|
||||
content: t('onboarding.allTools', 'This is the <strong>All Tools</strong> panel, where you can browse and select from all available PDF tools.'),
|
||||
position: 'center',
|
||||
padding: 0,
|
||||
action: () => {
|
||||
saveWorkbenchState();
|
||||
closeFilesModal();
|
||||
backToAllTools();
|
||||
},
|
||||
},
|
||||
[TourStep.SELECT_CROP_TOOL]: {
|
||||
selector: '[data-tour="tool-button-crop"]',
|
||||
content: t('onboarding.selectCropTool', "Let's select the <strong>Crop</strong> tool to demonstrate how to use one of the tools."),
|
||||
position: 'right',
|
||||
padding: 0,
|
||||
actionAfter: () => selectCropTool(),
|
||||
},
|
||||
[TourStep.TOOL_INTERFACE]: {
|
||||
selector: '[data-tour="tool-panel"]',
|
||||
content: t('onboarding.toolInterface', "This is the <strong>Crop</strong> tool interface. As you can see, there's not much there because we haven't added any PDF files to work with yet."),
|
||||
position: 'center',
|
||||
padding: 0,
|
||||
},
|
||||
[TourStep.FILES_BUTTON]: {
|
||||
selector: '[data-tour="files-button"]',
|
||||
content: t('onboarding.filesButton', "The <strong>Files</strong> button on the Quick Access bar allows you to upload PDFs to use the tools on."),
|
||||
position: 'right',
|
||||
padding: 10,
|
||||
action: () => openFilesModal(),
|
||||
},
|
||||
[TourStep.FILE_SOURCES]: {
|
||||
selector: '[data-tour="file-sources"]',
|
||||
content: t('onboarding.fileSources', "You can upload new files or access recent files from here. For the tour, we'll just use a sample file."),
|
||||
position: 'right',
|
||||
padding: 0,
|
||||
actionAfter: () => {
|
||||
loadSampleFile();
|
||||
closeFilesModal();
|
||||
}
|
||||
},
|
||||
[TourStep.WORKBENCH]: {
|
||||
selector: '[data-tour="workbench"]',
|
||||
content: t('onboarding.workbench', 'This is the <strong>Workbench</strong> - the main area where you view and edit your PDFs.'),
|
||||
position: 'center',
|
||||
padding: 0,
|
||||
},
|
||||
[TourStep.VIEW_SWITCHER]: {
|
||||
selector: '[data-tour="view-switcher"]',
|
||||
content: t('onboarding.viewSwitcher', 'Use these controls to select how you want to view your PDFs.'),
|
||||
position: 'bottom',
|
||||
padding: 0,
|
||||
},
|
||||
[TourStep.VIEWER]: {
|
||||
selector: '[data-tour="workbench"]',
|
||||
content: t('onboarding.viewer', "The <strong>Viewer</strong> lets you read and annotate your PDFs."),
|
||||
position: 'center',
|
||||
padding: 0,
|
||||
action: () => switchToViewer(),
|
||||
},
|
||||
[TourStep.PAGE_EDITOR]: {
|
||||
selector: '[data-tour="workbench"]',
|
||||
content: t('onboarding.pageEditor', "The <strong>Page Editor</strong> allows you to do various operations on the pages within your PDFs, such as reordering, rotating and deleting."),
|
||||
position: 'center',
|
||||
padding: 0,
|
||||
action: () => switchToPageEditor(),
|
||||
},
|
||||
[TourStep.ACTIVE_FILES]: {
|
||||
selector: '[data-tour="workbench"]',
|
||||
content: t('onboarding.activeFiles', "The <strong>Active Files</strong> view shows all of the PDFs you have loaded into the tool, and allows you to select which ones to process."),
|
||||
position: 'center',
|
||||
padding: 0,
|
||||
action: () => switchToActiveFiles(),
|
||||
},
|
||||
[TourStep.FILE_CHECKBOX]: {
|
||||
selector: '[data-tour="file-card-checkbox"]',
|
||||
content: t('onboarding.fileCheckbox', "Clicking one of the files selects it for processing. You can select multiple files for batch operations."),
|
||||
position: 'top',
|
||||
padding: 10,
|
||||
},
|
||||
[TourStep.SELECT_CONTROLS]: {
|
||||
selector: '[data-tour="right-rail-controls"]',
|
||||
highlightedSelectors: ['[data-tour="right-rail-controls"]', '[data-tour="right-rail-settings"]'],
|
||||
content: t('onboarding.selectControls', "The <strong>Right Rail</strong> contains buttons to quickly select/deselect all of your active PDFs, along with buttons to change the app's theme or language."),
|
||||
position: 'left',
|
||||
padding: 5,
|
||||
action: () => selectFirstFile(),
|
||||
},
|
||||
[TourStep.CROP_SETTINGS]: {
|
||||
selector: '[data-tour="crop-settings"]',
|
||||
content: t('onboarding.cropSettings', "Now that we've selected the file we want crop, we can configure the <strong>Crop</strong> tool to choose the area that we want to crop the PDF to."),
|
||||
position: 'left',
|
||||
padding: 10,
|
||||
action: () => modifyCropSettings(),
|
||||
},
|
||||
[TourStep.RUN_BUTTON]: {
|
||||
selector: '[data-tour="run-button"]',
|
||||
content: t('onboarding.runButton', "Once the tool has been configured, this button allows you to run the tool on all the selected PDFs."),
|
||||
position: 'top',
|
||||
padding: 10,
|
||||
actionAfter: () => executeTool(),
|
||||
},
|
||||
[TourStep.RESULTS]: {
|
||||
selector: '[data-tour="tool-panel"]',
|
||||
content: t('onboarding.results', "After the tool has finished running, the <strong>Review</strong> step will show a preview of the results in this panel, and allow you to undo the operation or download the file. "),
|
||||
position: 'center',
|
||||
padding: 0,
|
||||
},
|
||||
[TourStep.FILE_REPLACEMENT]: {
|
||||
selector: '[data-tour="file-card-checkbox"]',
|
||||
content: t('onboarding.fileReplacement', "The modified file will replace the original file in the Workbench automatically, allowing you to easily run it through more tools."),
|
||||
position: 'left',
|
||||
padding: 10,
|
||||
},
|
||||
[TourStep.PIN_BUTTON]: {
|
||||
selector: '[data-tour="file-card-pin"]',
|
||||
content: t('onboarding.pinButton', "You can use the <strong>Pin</strong> button if you'd rather your files stay active after running tools on them."),
|
||||
position: 'left',
|
||||
padding: 10,
|
||||
action: () => pinFile(),
|
||||
},
|
||||
[TourStep.WRAP_UP]: {
|
||||
selector: '[data-tour="help-button"]',
|
||||
content: t('onboarding.wrapUp', "You're all set! You've learnt about the main areas of the app and how to use them. Click the <strong>Help</strong> button whenever you like to see this tour again."),
|
||||
position: 'right',
|
||||
padding: 10,
|
||||
},
|
||||
};
|
||||
|
||||
// Convert to array using enum's numeric ordering
|
||||
const steps = Object.values(stepsConfig);
|
||||
|
||||
const advanceTour = ({ setCurrentStep, currentStep, steps, setIsOpen }: {
|
||||
setCurrentStep: (value: number | ((prev: number) => number)) => void;
|
||||
currentStep: number;
|
||||
steps?: StepType[];
|
||||
setIsOpen: (value: boolean) => void;
|
||||
}) => {
|
||||
if (steps && currentStep === steps.length - 1) {
|
||||
setIsOpen(false);
|
||||
restoreWorkbenchState();
|
||||
completeTour();
|
||||
} else if (steps) {
|
||||
setCurrentStep((s) => (s === steps.length - 1 ? 0 : s + 1));
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseTour = ({ setIsOpen }: { setIsOpen: (value: boolean) => void }) => {
|
||||
setIsOpen(false);
|
||||
restoreWorkbenchState();
|
||||
completeTour();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TourWelcomeModal
|
||||
opened={showWelcomeModal}
|
||||
onStartTour={() => {
|
||||
setShowWelcomeModal(false);
|
||||
startTour();
|
||||
}}
|
||||
onMaybeLater={() => {
|
||||
setShowWelcomeModal(false);
|
||||
}}
|
||||
onDontShowAgain={() => {
|
||||
setShowWelcomeModal(false);
|
||||
completeTour();
|
||||
}}
|
||||
/>
|
||||
<TourProvider
|
||||
steps={steps}
|
||||
onClickClose={handleCloseTour}
|
||||
onClickMask={advanceTour}
|
||||
onClickHighlighted={(e, clickProps) => {
|
||||
e.stopPropagation();
|
||||
advanceTour(clickProps);
|
||||
}}
|
||||
keyboardHandler={(e, clickProps, status) => {
|
||||
// Handle right arrow key to advance tour
|
||||
if (e.key === 'ArrowRight' && !status?.isRightDisabled && clickProps) {
|
||||
e.preventDefault();
|
||||
advanceTour(clickProps);
|
||||
}
|
||||
// Handle escape key to close tour
|
||||
else if (e.key === 'Escape' && !status?.isEscDisabled && clickProps) {
|
||||
e.preventDefault();
|
||||
handleCloseTour(clickProps);
|
||||
}
|
||||
}}
|
||||
styles={{
|
||||
popover: (base) => ({
|
||||
...base,
|
||||
backgroundColor: 'var(--mantine-color-body)',
|
||||
color: 'var(--mantine-color-text)',
|
||||
borderRadius: '8px',
|
||||
padding: '20px',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
||||
maxWidth: '400px',
|
||||
}),
|
||||
maskArea: (base) => ({
|
||||
...base,
|
||||
rx: 8,
|
||||
}),
|
||||
badge: (base) => ({
|
||||
...base,
|
||||
backgroundColor: 'var(--mantine-primary-color-filled)',
|
||||
}),
|
||||
controls: (base) => ({
|
||||
...base,
|
||||
justifyContent: 'center',
|
||||
}),
|
||||
}}
|
||||
highlightedMaskClassName="tour-highlight-glow"
|
||||
showNavigation={true}
|
||||
showBadge={false}
|
||||
showCloseButton={true}
|
||||
disableInteraction={true}
|
||||
disableDotsNavigation={true}
|
||||
prevButton={() => null}
|
||||
nextButton={({ currentStep, stepsLength, setCurrentStep, setIsOpen }) => {
|
||||
const isLast = currentStep === stepsLength - 1;
|
||||
|
||||
return (
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
advanceTour({ setCurrentStep, currentStep, steps, setIsOpen });
|
||||
}}
|
||||
variant="subtle"
|
||||
size="lg"
|
||||
aria-label={isLast ? t('onboarding.finish', 'Finish') : t('onboarding.next', 'Next')}
|
||||
>
|
||||
{isLast ? <CheckIcon /> : <ArrowForwardIcon />}
|
||||
</ActionIcon>
|
||||
);
|
||||
}}
|
||||
components={{
|
||||
Close: ({ onClick }) => (
|
||||
<CloseButton
|
||||
onClick={onClick}
|
||||
size="md"
|
||||
style={{ position: 'absolute', top: '8px', right: '8px' }}
|
||||
/>
|
||||
),
|
||||
Content: ({ content } : {content: string}) => (
|
||||
<div
|
||||
style={{ paddingRight: '16px' /* Ensure text doesn't overlap with close button */ }}
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
>
|
||||
<TourContent />
|
||||
</TourProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
82
frontend/src/components/onboarding/TourWelcomeModal.tsx
Normal file
82
frontend/src/components/onboarding/TourWelcomeModal.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import { Modal, Title, Text, Button, Stack, Group } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Z_INDEX_OVER_FULLSCREEN_SURFACE } from '../../styles/zIndex';
|
||||
|
||||
interface TourWelcomeModalProps {
|
||||
opened: boolean;
|
||||
onStartTour: () => void;
|
||||
onMaybeLater: () => void;
|
||||
onDontShowAgain: () => void;
|
||||
}
|
||||
|
||||
export default function TourWelcomeModal({
|
||||
opened,
|
||||
onStartTour,
|
||||
onMaybeLater,
|
||||
onDontShowAgain,
|
||||
}: TourWelcomeModalProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={onMaybeLater}
|
||||
centered
|
||||
size="md"
|
||||
radius="lg"
|
||||
withCloseButton={false}
|
||||
zIndex={Z_INDEX_OVER_FULLSCREEN_SURFACE}
|
||||
>
|
||||
<Stack gap="lg">
|
||||
<Stack gap="xs">
|
||||
<Title order={2}>
|
||||
{t('onboarding.welcomeModal.title', 'Welcome to Stirling PDF!')}
|
||||
</Title>
|
||||
<Text size="md" c="dimmed">
|
||||
{t('onboarding.welcomeModal.description',
|
||||
"Would you like to take a quick 1-minute tour to learn the key features and how to get started?"
|
||||
)}
|
||||
</Text>
|
||||
<Text
|
||||
size="md"
|
||||
c="dimmed"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: t('onboarding.welcomeModal.helpHint',
|
||||
'You can always access this tour later from the <strong>Help</strong> button in the bottom left.'
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Stack gap="sm">
|
||||
<Button
|
||||
onClick={onStartTour}
|
||||
size="md"
|
||||
variant="filled"
|
||||
fullWidth
|
||||
>
|
||||
{t('onboarding.welcomeModal.startTour', 'Start Tour')}
|
||||
</Button>
|
||||
|
||||
<Group grow>
|
||||
<Button
|
||||
onClick={onMaybeLater}
|
||||
size="md"
|
||||
variant="light"
|
||||
>
|
||||
{t('onboarding.welcomeModal.maybeLater', 'Maybe Later')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={onDontShowAgain}
|
||||
size="md"
|
||||
variant="light"
|
||||
>
|
||||
{t('onboarding.welcomeModal.dontShowAgain', "Don't Show Again")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@ -155,4 +155,4 @@ const AppConfigModal: React.FC<AppConfigModalProps> = ({ opened, onClose }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default AppConfigModal;
|
||||
export default AppConfigModal;
|
||||
|
||||
@ -51,6 +51,7 @@ const FileCard = ({ file, fileStub, onRemove, onDoubleClick, onView, onEdit, isS
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
onClick={onSelect}
|
||||
data-testid="file-card"
|
||||
data-tour="file-card-checkbox"
|
||||
>
|
||||
<Stack gap={6} align="center">
|
||||
<Box
|
||||
|
||||
@ -14,6 +14,7 @@ import AllToolsNavButton from './AllToolsNavButton';
|
||||
import ActiveToolButton from "./quickAccessBar/ActiveToolButton";
|
||||
import AppConfigModal from './AppConfigModal';
|
||||
import { useAppConfig } from '../../hooks/useAppConfig';
|
||||
import { useOnboarding } from '../../contexts/OnboardingContext';
|
||||
import {
|
||||
isNavButtonActive,
|
||||
getNavButtonStyle,
|
||||
@ -27,6 +28,7 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
const { handleReaderToggle, handleToolSelect, selectedToolKey, leftPanelView, toolRegistry, readerMode, resetTool } = useToolWorkflow();
|
||||
const { getToolNavigation } = useSidebarNavigation();
|
||||
const { config } = useAppConfig();
|
||||
const { startTour } = useOnboarding();
|
||||
const [configModalOpen, setConfigModalOpen] = useState(false);
|
||||
const [activeButton, setActiveButton] = useState<string>('tools');
|
||||
const scrollableRef = useRef<HTMLDivElement>(null);
|
||||
@ -60,7 +62,12 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
|
||||
// Render navigation button with conditional URL support
|
||||
return (
|
||||
<div key={config.id} className="flex flex-col items-center gap-1" style={{ marginTop: index === 0 ? '0.5rem' : "0rem" }}>
|
||||
<div
|
||||
key={config.id}
|
||||
className="flex flex-col items-center gap-1"
|
||||
style={{ marginTop: index === 0 ? '0.5rem' : "0rem" }}
|
||||
data-tour={`${config.id}-button`}
|
||||
>
|
||||
<ActionIcon
|
||||
{...(navProps ? {
|
||||
component: "a" as const,
|
||||
@ -88,8 +95,7 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const buttonConfigs: ButtonConfig[] = [
|
||||
const mainButtons: ButtonConfig[] = [
|
||||
{
|
||||
id: 'read',
|
||||
name: t("quickAccess.read", "Read"),
|
||||
@ -131,6 +137,9 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
}
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
const middleButtons: ButtonConfig[] = [
|
||||
{
|
||||
id: 'files',
|
||||
name: t("quickAccess.files", "Files"),
|
||||
@ -150,6 +159,20 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
// type: 'navigation',
|
||||
// onClick: () => setActiveButton('activity')
|
||||
//},
|
||||
];
|
||||
|
||||
const bottomButtons: ButtonConfig[] = [
|
||||
{
|
||||
id: 'help',
|
||||
name: t("quickAccess.help", "Help"),
|
||||
icon: <LocalIcon icon="help-rounded" width="1.5rem" height="1.5rem" />,
|
||||
isRound: true,
|
||||
size: 'lg',
|
||||
type: 'action',
|
||||
onClick: () => {
|
||||
startTour();
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'config',
|
||||
name: config?.enableLogin ? t("quickAccess.account", "Account") : t("quickAccess.config", "Config"),
|
||||
@ -162,8 +185,6 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
@ -198,49 +219,41 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
}}
|
||||
>
|
||||
<div className="scrollable-content">
|
||||
{/* Top section with main buttons */}
|
||||
{/* Main navigation section */}
|
||||
<Stack gap="lg" align="center">
|
||||
{buttonConfigs.slice(0, -1).map((config, index) => (
|
||||
{mainButtons.map((config, index) => (
|
||||
<React.Fragment key={config.id}>
|
||||
{renderNavButton(config, index)}
|
||||
|
||||
{/* Add divider after Automate button (index 1) and Files button (index 2) */}
|
||||
{index === 1 && (
|
||||
<Divider
|
||||
size="xs"
|
||||
className="content-divider"
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
{/* Spacer to push Config button to bottom */}
|
||||
{/* Divider after main buttons */}
|
||||
<Divider
|
||||
size="xs"
|
||||
className="content-divider"
|
||||
/>
|
||||
|
||||
{/* Middle section */}
|
||||
<Stack gap="lg" align="center">
|
||||
{middleButtons.map((config, index) => (
|
||||
<React.Fragment key={config.id}>
|
||||
{renderNavButton(config, index)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
{/* Spacer to push bottom buttons to bottom */}
|
||||
<div className="spacer" />
|
||||
|
||||
{/* Config button at the bottom */}
|
||||
{buttonConfigs
|
||||
.filter(config => config.id === 'config')
|
||||
.map(config => (
|
||||
<div key={config.id} className="flex flex-col items-center gap-1">
|
||||
<ActionIcon
|
||||
size={config.size || 'lg'}
|
||||
variant="subtle"
|
||||
onClick={config.onClick}
|
||||
style={getNavButtonStyle(config, activeButton, isFilesModalOpen, configModalOpen, selectedToolKey, leftPanelView)}
|
||||
className={isNavButtonActive(config, activeButton, isFilesModalOpen, configModalOpen, selectedToolKey, leftPanelView) ? 'activeIconScale' : ''}
|
||||
aria-label={config.name}
|
||||
data-testid={`${config.id}-button`}
|
||||
>
|
||||
<span className="iconContainer">
|
||||
{config.icon}
|
||||
</span>
|
||||
</ActionIcon>
|
||||
<span className={`button-text ${isNavButtonActive(config, activeButton, isFilesModalOpen, configModalOpen, selectedToolKey, leftPanelView) ? 'active' : 'inactive'}`}>
|
||||
{config.name}
|
||||
</span>
|
||||
</div>
|
||||
{/* Bottom section */}
|
||||
<Stack gap="lg" align="center">
|
||||
{bottomButtons.map((config, index) => (
|
||||
<React.Fragment key={config.id}>
|
||||
{renderNavButton(config, index)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Stack>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -168,7 +168,7 @@ export default function RightRail() {
|
||||
<div className="right-rail-inner">
|
||||
{sectionsWithButtons.map(({ section, buttons: sectionButtons }) => (
|
||||
<React.Fragment key={section}>
|
||||
<div className="right-rail-section">
|
||||
<div className="right-rail-section" data-tour="right-rail-controls">
|
||||
{sectionButtons.map((btn, index) => {
|
||||
const content = renderButton(btn);
|
||||
if (!content) return null;
|
||||
@ -186,7 +186,7 @@ export default function RightRail() {
|
||||
<Divider className="right-rail-divider" />
|
||||
</React.Fragment>
|
||||
))}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1rem' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1rem' }} data-tour="right-rail-settings">
|
||||
{renderWithTooltip(
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
|
||||
@ -104,7 +104,7 @@ const createViewOptions = (
|
||||
fileEditorOption,
|
||||
];
|
||||
|
||||
const customOptions = (customViews ?? [])
|
||||
const customOptions = (customViews ?? [])
|
||||
.filter((view) => view.data != null)
|
||||
.map((view) => ({
|
||||
label: (
|
||||
@ -169,6 +169,7 @@ const TopControls = ({
|
||||
<div className="absolute left-0 w-full top-0 z-[100] pointer-events-none">
|
||||
<div className="flex justify-center mt-[0.5rem]">
|
||||
<SegmentedControl
|
||||
data-tour="view-switcher"
|
||||
data={createViewOptions(currentView, switchingTo, activeFiles, currentFileIndex, onFileSelect, customViews)}
|
||||
value={currentView}
|
||||
onChange={handleViewChange}
|
||||
|
||||
@ -27,7 +27,7 @@ export interface ConfigColors {
|
||||
|
||||
export const createConfigNavSections = (
|
||||
Overview: React.ComponentType<{ onLogoutClick: () => void }>,
|
||||
onLogoutClick: () => void
|
||||
onLogoutClick: () => void,
|
||||
): ConfigNavSection[] => {
|
||||
const sections: ConfigNavSection[] = [
|
||||
{
|
||||
@ -61,4 +61,4 @@ export const createConfigNavSections = (
|
||||
];
|
||||
|
||||
return sections;
|
||||
};
|
||||
};
|
||||
|
||||
@ -145,6 +145,7 @@
|
||||
.content-divider {
|
||||
width: 3.75rem;
|
||||
border-color: var(--color-gray-300);
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
/* Spacer */
|
||||
|
||||
@ -94,6 +94,7 @@ const FullscreenToolSurface = ({
|
||||
style={style}
|
||||
role="region"
|
||||
aria-label={t('toolPanel.fullscreen.heading', 'All tools (fullscreen view)')}
|
||||
data-tour="tool-panel"
|
||||
>
|
||||
<div
|
||||
ref={surfaceRef}
|
||||
|
||||
@ -103,6 +103,7 @@ export default function ToolPanel() {
|
||||
<div
|
||||
ref={toolPanelRef}
|
||||
data-sidebar="tool-panel"
|
||||
data-tour={fullscreenExpanded ? undefined : "tool-panel"}
|
||||
className={`tool-panel flex flex-col ${fullscreenExpanded ? 'tool-panel--fullscreen-active' : 'overflow-hidden'} bg-[var(--bg-toolbar)] border-r border-[var(--border-subtle)] transition-all duration-300 ease-out ${
|
||||
isRainbowMode ? rainbowStyles.rainbowPaper : ''
|
||||
} ${isMobile ? 'h-full border-r-0' : 'h-screen'} ${fullscreenExpanded ? 'tool-panel--fullscreen' : ''}`}
|
||||
@ -135,7 +136,7 @@ export default function ToolPanel() {
|
||||
mode="filter"
|
||||
/>
|
||||
{!isMobile && leftPanelView === 'toolPicker' && (
|
||||
<Tooltip
|
||||
<Tooltip
|
||||
content={toggleLabel}
|
||||
position="bottom"
|
||||
arrow={true}
|
||||
|
||||
@ -172,7 +172,8 @@ const CropAreaSelector: React.FC<CropAreaSelectorProps> = ({
|
||||
border: `2px solid ${theme.other.crop.overlayBorder}`,
|
||||
backgroundColor: theme.other.crop.overlayBackground,
|
||||
cursor: 'move',
|
||||
pointerEvents: 'auto'
|
||||
pointerEvents: 'auto',
|
||||
transition: (isDragging || isResizing) ? undefined : 'all 1s ease-in-out'
|
||||
}}
|
||||
onMouseDown={handleOverlayMouseDown}
|
||||
>
|
||||
|
||||
@ -93,6 +93,19 @@ const CropSettings = ({ parameters, disabled = false }: CropSettingsProps) => {
|
||||
loadPDFDimensions();
|
||||
}, [selectedStub, selectedFile, parameters]);
|
||||
|
||||
// Listen for tour events to set crop area
|
||||
useEffect(() => {
|
||||
const handleSetCropArea = (event: Event) => {
|
||||
const customEvent = event as CustomEvent<Rectangle>;
|
||||
if (customEvent.detail && pdfBounds) {
|
||||
parameters.setCropArea(customEvent.detail, pdfBounds);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('tour:setCropArea', handleSetCropArea);
|
||||
return () => window.removeEventListener('tour:setCropArea', handleSetCropArea);
|
||||
}, [parameters, pdfBounds]);
|
||||
|
||||
// Current crop area
|
||||
const cropArea = parameters.getCropArea();
|
||||
|
||||
@ -137,7 +150,7 @@ const CropSettings = ({ parameters, disabled = false }: CropSettingsProps) => {
|
||||
const isFullCrop = parameters.isFullPDFCrop(pdfBounds);
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
<Stack gap="md" data-tour="crop-settings">
|
||||
{/* PDF Preview with Crop Selector */}
|
||||
<Stack gap="xs">
|
||||
<Group justify="space-between" align="center">
|
||||
|
||||
@ -42,6 +42,7 @@ const CompactToolItem: React.FC<CompactToolItemProps> = ({ id, tool, isSelected,
|
||||
onClick={onClick}
|
||||
aria-disabled={disabled}
|
||||
disabled={disabled}
|
||||
data-tour={`tool-button-${id}`}
|
||||
>
|
||||
{tool.icon ? (
|
||||
<span
|
||||
|
||||
@ -41,6 +41,7 @@ const DetailedToolItem: React.FC<DetailedToolItemProps> = ({ id, tool, isSelecte
|
||||
onClick={onClick}
|
||||
aria-disabled={disabled}
|
||||
disabled={disabled}
|
||||
data-tour={`tool-button-${id}`}
|
||||
>
|
||||
{tool.icon ? (
|
||||
<span
|
||||
|
||||
@ -13,6 +13,7 @@ export interface OperationButtonProps {
|
||||
mt?: string;
|
||||
type?: 'button' | 'submit' | 'reset';
|
||||
'data-testid'?: string;
|
||||
'data-tour'?: string;
|
||||
}
|
||||
|
||||
const OperationButton = ({
|
||||
@ -26,7 +27,8 @@ const OperationButton = ({
|
||||
fullWidth = false,
|
||||
mt = 'md',
|
||||
type = 'button',
|
||||
'data-testid': dataTestId
|
||||
'data-testid': dataTestId,
|
||||
'data-tour': dataTour
|
||||
}: OperationButtonProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -43,6 +45,7 @@ const OperationButton = ({
|
||||
variant={variant}
|
||||
color={color}
|
||||
data-testid={dataTestId}
|
||||
data-tour={dataTour}
|
||||
style={{ minHeight: '2.5rem' }}
|
||||
>
|
||||
{isLoading
|
||||
|
||||
@ -105,6 +105,7 @@ export function createToolFlow(config: ToolFlowConfig) {
|
||||
loadingText={config.executeButton.loadingText}
|
||||
submitText={config.executeButton.text}
|
||||
data-testid={config.executeButton.testId}
|
||||
data-tour="run-button"
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@ -112,10 +112,11 @@ const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect,
|
||||
fullWidth
|
||||
justify="flex-start"
|
||||
className="tool-button"
|
||||
styles={{
|
||||
root: {
|
||||
borderRadius: 0,
|
||||
color: "var(--tools-text-and-icon-color)",
|
||||
data-tour={`tool-button-${id}`}
|
||||
styles={{
|
||||
root: {
|
||||
borderRadius: 0,
|
||||
color: "var(--tools-text-and-icon-color)",
|
||||
overflow: 'visible'
|
||||
},
|
||||
label: { overflow: 'visible' }
|
||||
@ -137,10 +138,11 @@ const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect,
|
||||
fullWidth
|
||||
justify="flex-start"
|
||||
className="tool-button"
|
||||
styles={{
|
||||
root: {
|
||||
borderRadius: 0,
|
||||
color: "var(--tools-text-and-icon-color)",
|
||||
data-tour={`tool-button-${id}`}
|
||||
styles={{
|
||||
root: {
|
||||
borderRadius: 0,
|
||||
color: "var(--tools-text-and-icon-color)",
|
||||
overflow: 'visible'
|
||||
},
|
||||
label: { overflow: 'visible' }
|
||||
@ -159,14 +161,15 @@ const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect,
|
||||
justify="flex-start"
|
||||
className="tool-button"
|
||||
aria-disabled={isUnavailable}
|
||||
data-tour={`tool-button-${id}`}
|
||||
styles={{
|
||||
root: {
|
||||
borderRadius: 0,
|
||||
color: "var(--tools-text-and-icon-color)",
|
||||
cursor: isUnavailable ? 'not-allowed' : undefined,
|
||||
root: {
|
||||
borderRadius: 0,
|
||||
color: "var(--tools-text-and-icon-color)",
|
||||
cursor: isUnavailable ? 'not-allowed' : undefined,
|
||||
overflow: 'visible'
|
||||
},
|
||||
label: { overflow: 'visible' }
|
||||
},
|
||||
label: { overflow: 'visible' }
|
||||
}}
|
||||
>
|
||||
{buttonContent}
|
||||
|
||||
80
frontend/src/contexts/OnboardingContext.tsx
Normal file
80
frontend/src/contexts/OnboardingContext.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
||||
import { usePreferences } from './PreferencesContext';
|
||||
import { useMediaQuery } from '@mantine/hooks';
|
||||
|
||||
interface OnboardingContextValue {
|
||||
isOpen: boolean;
|
||||
currentStep: number;
|
||||
setCurrentStep: (step: number) => void;
|
||||
startTour: () => void;
|
||||
closeTour: () => void;
|
||||
completeTour: () => void;
|
||||
resetTour: () => void;
|
||||
showWelcomeModal: boolean;
|
||||
setShowWelcomeModal: (show: boolean) => void;
|
||||
}
|
||||
|
||||
const OnboardingContext = createContext<OnboardingContextValue | undefined>(undefined);
|
||||
|
||||
export const OnboardingProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const { preferences, updatePreference } = usePreferences();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [showWelcomeModal, setShowWelcomeModal] = useState(false);
|
||||
const isMobile = useMediaQuery("(max-width: 1024px)");
|
||||
|
||||
// Auto-show welcome modal for first-time users after preferences load
|
||||
// Only show after user has seen the tool panel mode prompt
|
||||
// Also, don't show tour on mobile devices because it feels clunky
|
||||
useEffect(() => {
|
||||
if (!preferences.hasCompletedOnboarding && preferences.toolPanelModePromptSeen && !isMobile) {
|
||||
setShowWelcomeModal(true);
|
||||
}
|
||||
}, [preferences.hasCompletedOnboarding, preferences.toolPanelModePromptSeen, isMobile]);
|
||||
|
||||
const startTour = useCallback(() => {
|
||||
setCurrentStep(0);
|
||||
setIsOpen(true);
|
||||
}, []);
|
||||
|
||||
const closeTour = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
}, []);
|
||||
|
||||
const completeTour = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
updatePreference('hasCompletedOnboarding', true);
|
||||
}, [updatePreference]);
|
||||
|
||||
const resetTour = useCallback(() => {
|
||||
updatePreference('hasCompletedOnboarding', false);
|
||||
setCurrentStep(0);
|
||||
setIsOpen(true);
|
||||
}, [updatePreference]);
|
||||
|
||||
return (
|
||||
<OnboardingContext.Provider
|
||||
value={{
|
||||
isOpen,
|
||||
currentStep,
|
||||
setCurrentStep,
|
||||
startTour,
|
||||
closeTour,
|
||||
completeTour,
|
||||
resetTour,
|
||||
showWelcomeModal,
|
||||
setShowWelcomeModal,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</OnboardingContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useOnboarding = (): OnboardingContextValue => {
|
||||
const context = useContext(OnboardingContext);
|
||||
if (!context) {
|
||||
throw new Error('useOnboarding must be used within an OnboardingProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
207
frontend/src/contexts/TourOrchestrationContext.tsx
Normal file
207
frontend/src/contexts/TourOrchestrationContext.tsx
Normal file
@ -0,0 +1,207 @@
|
||||
import React, { createContext, useContext, useCallback, useRef } from 'react';
|
||||
import { useFileHandler } from '../hooks/useFileHandler';
|
||||
import { useFilesModalContext } from './FilesModalContext';
|
||||
import { useNavigationActions } from './NavigationContext';
|
||||
import { useToolWorkflow } from './ToolWorkflowContext';
|
||||
import { useAllFiles, useFileManagement } from './FileContext';
|
||||
import { StirlingFile } from '../types/fileContext';
|
||||
import { fileStorage } from '../services/fileStorage';
|
||||
|
||||
interface TourOrchestrationContextType {
|
||||
// State management
|
||||
saveWorkbenchState: () => void;
|
||||
restoreWorkbenchState: () => Promise<void>;
|
||||
|
||||
// Tool deselection
|
||||
backToAllTools: () => void;
|
||||
|
||||
// Tool selection
|
||||
selectCropTool: () => void;
|
||||
|
||||
// File operations
|
||||
loadSampleFile: () => Promise<void>;
|
||||
|
||||
// View switching
|
||||
switchToViewer: () => void;
|
||||
switchToPageEditor: () => void;
|
||||
switchToActiveFiles: () => void;
|
||||
|
||||
// File operations
|
||||
selectFirstFile: () => void;
|
||||
pinFile: () => void;
|
||||
|
||||
// Crop settings (placeholder for now)
|
||||
modifyCropSettings: () => void;
|
||||
|
||||
// Tool execution
|
||||
executeTool: () => void;
|
||||
}
|
||||
|
||||
const TourOrchestrationContext = createContext<TourOrchestrationContextType | undefined>(undefined);
|
||||
|
||||
export const TourOrchestrationProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const { addFiles } = useFileHandler();
|
||||
const { closeFilesModal } = useFilesModalContext();
|
||||
const { actions: navActions } = useNavigationActions();
|
||||
const { handleToolSelect, handleBackToTools } = useToolWorkflow();
|
||||
const { files } = useAllFiles();
|
||||
const { clearAllFiles } = useFileManagement();
|
||||
|
||||
// Store the user's files before tour starts
|
||||
const savedFilesRef = useRef<StirlingFile[]>([]);
|
||||
|
||||
// Keep a ref to always have the latest files
|
||||
const filesRef = useRef<StirlingFile[]>(files);
|
||||
React.useEffect(() => {
|
||||
filesRef.current = files;
|
||||
}, [files]);
|
||||
|
||||
const saveWorkbenchState = useCallback(() => {
|
||||
// Get fresh files from ref
|
||||
const currentFiles = filesRef.current;
|
||||
console.log('Saving workbench state, files count:', currentFiles.length);
|
||||
savedFilesRef.current = [...currentFiles];
|
||||
// Clear all files for clean demo
|
||||
clearAllFiles();
|
||||
}, [clearAllFiles]);
|
||||
|
||||
const restoreWorkbenchState = useCallback(async () => {
|
||||
console.log('Restoring workbench state, saved files count:', savedFilesRef.current.length);
|
||||
|
||||
// Go back to All Tools
|
||||
handleBackToTools();
|
||||
|
||||
// Clear all files (including tour sample)
|
||||
clearAllFiles();
|
||||
|
||||
// Delete all active files from storage (they're just the ones from the tour)
|
||||
const currentFiles = filesRef.current;
|
||||
if (currentFiles.length > 0) {
|
||||
try {
|
||||
await Promise.all(currentFiles.map(file => fileStorage.deleteStirlingFile(file.fileId)));
|
||||
console.log(`Deleted ${currentFiles.length} file(s) from storage`);
|
||||
} catch (error) {
|
||||
console.error('Failed to delete files from storage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore saved files
|
||||
if (savedFilesRef.current.length > 0) {
|
||||
// Create fresh File objects from StirlingFile to avoid ID conflicts
|
||||
const filesToRestore = await Promise.all(
|
||||
savedFilesRef.current.map(async (sf) => {
|
||||
const buffer = await sf.arrayBuffer();
|
||||
return new File([buffer], sf.name, { type: sf.type, lastModified: sf.lastModified });
|
||||
})
|
||||
);
|
||||
console.log('Restoring files:', filesToRestore.map(f => f.name));
|
||||
await addFiles(filesToRestore);
|
||||
savedFilesRef.current = [];
|
||||
}
|
||||
}, [clearAllFiles, addFiles, handleBackToTools]);
|
||||
|
||||
const backToAllTools = useCallback(() => {
|
||||
handleBackToTools();
|
||||
}, [handleBackToTools]);
|
||||
|
||||
const selectCropTool = useCallback(() => {
|
||||
handleToolSelect('crop');
|
||||
}, [handleToolSelect]);
|
||||
|
||||
const loadSampleFile = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch('/samples/Sample.pdf');
|
||||
const blob = await response.blob();
|
||||
const file = new File([blob], 'Sample.pdf', { type: 'application/pdf' });
|
||||
|
||||
await addFiles([file]);
|
||||
closeFilesModal();
|
||||
} catch (error) {
|
||||
console.error('Failed to load sample file:', error);
|
||||
}
|
||||
}, [addFiles, closeFilesModal]);
|
||||
|
||||
const switchToViewer = useCallback(() => {
|
||||
navActions.setWorkbench('viewer');
|
||||
}, [navActions]);
|
||||
|
||||
const switchToPageEditor = useCallback(() => {
|
||||
navActions.setWorkbench('pageEditor');
|
||||
}, [navActions]);
|
||||
|
||||
const switchToActiveFiles = useCallback(() => {
|
||||
navActions.setWorkbench('fileEditor');
|
||||
}, [navActions]);
|
||||
|
||||
const selectFirstFile = useCallback(() => {
|
||||
// File selection is handled by FileCard onClick
|
||||
// This function could trigger a click event on the first file card
|
||||
const firstFileCard = document.querySelector('[data-tour="file-card-checkbox"]') as HTMLElement;
|
||||
if (firstFileCard) {
|
||||
// Check if already selected (data-selected attribute)
|
||||
const isSelected = firstFileCard.getAttribute('data-selected') === 'true';
|
||||
// Only click if not already selected (to avoid toggling off)
|
||||
if (!isSelected) {
|
||||
firstFileCard.click();
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const pinFile = useCallback(() => {
|
||||
// Click the pin button directly
|
||||
const pinButton = document.querySelector('[data-tour="file-card-pin"]') as HTMLElement;
|
||||
if (pinButton) {
|
||||
pinButton.click();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const modifyCropSettings = useCallback(() => {
|
||||
// Dispatch a custom event to modify crop settings
|
||||
const event = new CustomEvent('tour:setCropArea', {
|
||||
detail: {
|
||||
x: 80,
|
||||
y: 435,
|
||||
width: 440,
|
||||
height: 170,
|
||||
}
|
||||
});
|
||||
window.dispatchEvent(event);
|
||||
}, []);
|
||||
|
||||
const executeTool = useCallback(() => {
|
||||
// Trigger the run button click
|
||||
const runButton = document.querySelector('[data-tour="run-button"]') as HTMLElement;
|
||||
if (runButton) {
|
||||
runButton.click();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const value: TourOrchestrationContextType = {
|
||||
saveWorkbenchState,
|
||||
restoreWorkbenchState,
|
||||
backToAllTools,
|
||||
selectCropTool,
|
||||
loadSampleFile,
|
||||
switchToViewer,
|
||||
switchToPageEditor,
|
||||
switchToActiveFiles,
|
||||
selectFirstFile,
|
||||
pinFile,
|
||||
modifyCropSettings,
|
||||
executeTool,
|
||||
};
|
||||
|
||||
return (
|
||||
<TourOrchestrationContext.Provider value={value}>
|
||||
{children}
|
||||
</TourOrchestrationContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useTourOrchestration = (): TourOrchestrationContextType => {
|
||||
const context = useContext(TourOrchestrationContext);
|
||||
if (!context) {
|
||||
throw new Error('useTourOrchestration must be used within TourOrchestrationProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@ -8,6 +8,7 @@ export interface UserPreferences {
|
||||
theme: ThemeMode;
|
||||
toolPanelModePromptSeen: boolean;
|
||||
showLegacyToolDescriptions: boolean;
|
||||
hasCompletedOnboarding: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_PREFERENCES: UserPreferences = {
|
||||
@ -17,6 +18,7 @@ export const DEFAULT_PREFERENCES: UserPreferences = {
|
||||
theme: getSystemTheme(),
|
||||
toolPanelModePromptSeen: false,
|
||||
showLegacyToolDescriptions: false,
|
||||
hasCompletedOnboarding: false,
|
||||
};
|
||||
|
||||
const STORAGE_KEY = 'stirlingpdf_preferences';
|
||||
|
||||
@ -28,7 +28,7 @@ const Crop = (props: BaseToolProps) => {
|
||||
steps: [
|
||||
{
|
||||
title: t("crop.steps.selectArea", "Select Crop Area"),
|
||||
isCollapsed: !base.hasFiles, // Collapsed until files selected
|
||||
isCollapsed: base.settingsCollapsed,
|
||||
onCollapsedClick: base.hasResults ? base.handleSettingsReset : undefined,
|
||||
tooltip: tooltips,
|
||||
content: (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user