diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index 2016748..bff58b2 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -9,6 +9,8 @@ configure_file( add_library(pdfengine STATIC src/core/engine_info.cpp + src/core/graphics_state.cpp + src/core/display_list.cpp src/parser/pdfium_loader.cpp src/fonts/font_face.cpp src/fonts/hb_shaper.cpp diff --git a/engine/include/pdfengine/display_list.hpp b/engine/include/pdfengine/display_list.hpp new file mode 100644 index 0000000..d62b6f8 --- /dev/null +++ b/engine/include/pdfengine/display_list.hpp @@ -0,0 +1,88 @@ +#pragma once + +#include +#include +#include +#include + +namespace pdfengine { + +class CommandVisitor; + +// Base class for all drawing commands +struct Command { + virtual ~Command() = default; + virtual void accept(CommandVisitor& visitor) const = 0; +}; + +// --- Specific Command Types --- + +struct SaveStateCommand : public Command { + void accept(CommandVisitor& visitor) const override; +}; + +struct RestoreStateCommand : public Command { + void accept(CommandVisitor& visitor) const override; +}; + +struct SetTransformCommand : public Command { + Matrix matrix; + explicit SetTransformCommand(const Matrix& m) : matrix(m) {} + void accept(CommandVisitor& visitor) const override; +}; + +struct FillRectCommand : public Command { + float x, y, width, height; + FillRectCommand(float x, float y, float w, float h) : x(x), y(y), width(w), height(h) {} + void accept(CommandVisitor& visitor) const override; +}; + +struct DrawTextCommand : public Command { + std::string text; + float x, y; + // We would eventually have a font reference here too + DrawTextCommand(std::string text, float x, float y) : text(std::move(text)), x(x), y(y) {} + void accept(CommandVisitor& visitor) const override; +}; + +// --- Visitor Interface --- + +// The visitor interface that the renderer (or replay engine) implements +class CommandVisitor { +public: + virtual ~CommandVisitor() = default; + virtual void visit(const SaveStateCommand& cmd) = 0; + virtual void visit(const RestoreStateCommand& cmd) = 0; + virtual void visit(const SetTransformCommand& cmd) = 0; + virtual void visit(const FillRectCommand& cmd) = 0; + virtual void visit(const DrawTextCommand& cmd) = 0; +}; + +// --- Display List Container --- + +// A container that stores a sequence of drawing commands. +class DisplayList { +public: + DisplayList() = default; + + // Add commands directly + void addCommand(std::unique_ptr cmd); + + // Replay the commands to a visitor (renderer) + void replay(CommandVisitor& visitor) const; + + // Helper methods to easily append common commands + void saveState(); + void restoreState(); + void setTransform(const Matrix& m); + void fillRect(float x, float y, float w, float h); + void drawText(const std::string& text, float x, float y); + + [[nodiscard]] size_t size() const noexcept { return m_commands.size(); } + void clear() { m_commands.clear(); } + +private: + std::vector> m_commands; +}; + +} // namespace pdfengine diff --git a/engine/include/pdfengine/graphics_state.hpp b/engine/include/pdfengine/graphics_state.hpp new file mode 100644 index 0000000..1e2012d --- /dev/null +++ b/engine/include/pdfengine/graphics_state.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include +#include + +namespace pdfengine { + +// 2D Affine Transformation Matrix (3x3 matrix optimized for 2D) +// [ a b 0 ] +// [ c d 0 ] +// [ e f 1 ] +struct Matrix { + float a = 1.0f, b = 0.0f; + float c = 0.0f, d = 1.0f; + float e = 0.0f, f = 0.0f; + + Matrix() = default; + Matrix(float a, float b, float c, float d, float e, float f) + : a(a), b(b), c(c), d(d), e(e), f(f) {} + + // Multiply this matrix by another matrix + [[nodiscard]] Matrix multiply(const Matrix& other) const noexcept; + + // Transform a 2D point using this matrix + void transform(float& x, float& y) const noexcept; +}; + +struct Color { + float r = 0.0f; + float g = 0.0f; + float b = 0.0f; +}; + +// Represents the current graphics state in a PDF document +struct GraphicsState { + Matrix ctm; // Current Transformation Matrix + Color fillColor; + Color strokeColor; + float lineWidth = 1.0f; + + // In the future, this will also hold: + // - Clipping paths + // - Font state (current font, font size) + // - Dash patterns + // - Line cap/join styles +}; + +// Manages the q/Q stack of graphics states +class GraphicsStateStack { +public: + GraphicsStateStack(); + + // Corresponds to the 'q' operator (save graphics state) + void push(); + + // Corresponds to the 'Q' operator (restore graphics state) + void pop(); + + // Access the current active graphics state + [[nodiscard]] GraphicsState& current(); + [[nodiscard]] const GraphicsState& current() const; + +private: + std::vector m_stack; +}; + +} // namespace pdfengine diff --git a/engine/src/core/display_list.cpp b/engine/src/core/display_list.cpp new file mode 100644 index 0000000..aac060a --- /dev/null +++ b/engine/src/core/display_list.cpp @@ -0,0 +1,43 @@ +#include + +namespace pdfengine { + +void SaveStateCommand::accept(CommandVisitor& visitor) const { visitor.visit(*this); } +void RestoreStateCommand::accept(CommandVisitor& visitor) const { visitor.visit(*this); } +void SetTransformCommand::accept(CommandVisitor& visitor) const { visitor.visit(*this); } +void FillRectCommand::accept(CommandVisitor& visitor) const { visitor.visit(*this); } +void DrawTextCommand::accept(CommandVisitor& visitor) const { visitor.visit(*this); } + +void DisplayList::addCommand(std::unique_ptr cmd) { + if (cmd) { + m_commands.push_back(std::move(cmd)); + } +} + +void DisplayList::replay(CommandVisitor& visitor) const { + for (const auto& cmd : m_commands) { + cmd->accept(visitor); + } +} + +void DisplayList::saveState() { + addCommand(std::make_unique()); +} + +void DisplayList::restoreState() { + addCommand(std::make_unique()); +} + +void DisplayList::setTransform(const Matrix& m) { + addCommand(std::make_unique(m)); +} + +void DisplayList::fillRect(float x, float y, float w, float h) { + addCommand(std::make_unique(x, y, w, h)); +} + +void DisplayList::drawText(const std::string& text, float x, float y) { + addCommand(std::make_unique(text, x, y)); +} + +} // namespace pdfengine diff --git a/engine/src/core/graphics_state.cpp b/engine/src/core/graphics_state.cpp new file mode 100644 index 0000000..a6a4004 --- /dev/null +++ b/engine/src/core/graphics_state.cpp @@ -0,0 +1,57 @@ +#include + +namespace pdfengine { + +Matrix Matrix::multiply(const Matrix& other) const noexcept { + return Matrix( + a * other.a + b * other.c, + a * other.b + b * other.d, + c * other.a + d * other.c, + c * other.b + d * other.d, + e * other.a + f * other.c + other.e, + e * other.b + f * other.d + other.f + ); +} + +void Matrix::transform(float& x, float& y) const noexcept { + float newX = a * x + c * y + e; + float newY = b * x + d * y + f; + x = newX; + y = newY; +} + +GraphicsStateStack::GraphicsStateStack() { + // A PDF always starts with one default graphics state on the stack. + m_stack.emplace_back(); +} + +void GraphicsStateStack::push() { + // Duplicate the current state and push it onto the stack. + if (!m_stack.empty()) { + m_stack.push_back(m_stack.back()); + } else { + m_stack.emplace_back(); + } +} + +void GraphicsStateStack::pop() { + // The stack should never be empty, but we must protect against popping the initial state + // if the PDF is malformed (e.g. more 'Q' operators than 'q' operators). + if (m_stack.size() > 1) { + m_stack.pop_back(); + } else { + // We could throw an exception or just ignore the invalid pop. + // For a resilient engine, ignoring is often better, but we could log a warning. + // For now, we do nothing to prevent crashing on the root state. + } +} + +GraphicsState& GraphicsStateStack::current() { + return m_stack.back(); +} + +const GraphicsState& GraphicsStateStack::current() const { + return m_stack.back(); +} + +} // namespace pdfengine diff --git a/engine/tests/CMakeLists.txt b/engine/tests/CMakeLists.txt index 08f8f19..7e8f1c5 100644 --- a/engine/tests/CMakeLists.txt +++ b/engine/tests/CMakeLists.txt @@ -3,6 +3,8 @@ add_executable(pdfengine_smoke smoke_test.cpp fonts_test.cpp + graphics_state_test.cpp + display_list_test.cpp ) target_link_libraries(pdfengine_smoke diff --git a/engine/tests/display_list_test.cpp b/engine/tests/display_list_test.cpp new file mode 100644 index 0000000..e2dd917 --- /dev/null +++ b/engine/tests/display_list_test.cpp @@ -0,0 +1,54 @@ +#include +#include +#include +#include + +using namespace pdfengine; + +// A simple visitor for testing that just records the sequence of visited commands. +class MockVisitor : public CommandVisitor { +public: + std::vector calls; + + void visit(const SaveStateCommand&) override { calls.push_back("SaveState"); } + void visit(const RestoreStateCommand&) override { calls.push_back("RestoreState"); } + void visit(const SetTransformCommand& cmd) override { calls.push_back("SetTransform(" + std::to_string(cmd.matrix.a) + ")"); } + void visit(const FillRectCommand& cmd) override { calls.push_back("FillRect(" + std::to_string(cmd.width) + ")"); } + void visit(const DrawTextCommand& cmd) override { calls.push_back("DrawText(" + cmd.text + ")"); } +}; + +TEST(DisplayListTest, RecordAndReplay) { + DisplayList list; + + EXPECT_EQ(list.size(), 0); + + // Record some commands + list.saveState(); + list.setTransform(Matrix(2.0f, 0.0f, 0.0f, 2.0f, 0.0f, 0.0f)); + list.fillRect(10.0f, 10.0f, 100.0f, 50.0f); + list.drawText("Hello World", 20.0f, 30.0f); + list.restoreState(); + + EXPECT_EQ(list.size(), 5); + + // Replay to the mock visitor + MockVisitor visitor; + list.replay(visitor); + + ASSERT_EQ(visitor.calls.size(), 5); + EXPECT_EQ(visitor.calls[0], "SaveState"); + EXPECT_EQ(visitor.calls[1], "SetTransform(2.000000)"); + EXPECT_EQ(visitor.calls[2], "FillRect(100.000000)"); + EXPECT_EQ(visitor.calls[3], "DrawText(Hello World)"); + EXPECT_EQ(visitor.calls[4], "RestoreState"); +} + +TEST(DisplayListTest, Clear) { + DisplayList list; + list.saveState(); + list.fillRect(0, 0, 10, 10); + EXPECT_EQ(list.size(), 2); + + list.clear(); + EXPECT_EQ(list.size(), 0); +} diff --git a/engine/tests/graphics_state_test.cpp b/engine/tests/graphics_state_test.cpp new file mode 100644 index 0000000..95a7ee4 --- /dev/null +++ b/engine/tests/graphics_state_test.cpp @@ -0,0 +1,64 @@ +#include +#include + +using namespace pdfengine; + +TEST(MatrixTest, DefaultIdentity) { + Matrix m; + EXPECT_FLOAT_EQ(m.a, 1.0f); + EXPECT_FLOAT_EQ(m.b, 0.0f); + EXPECT_FLOAT_EQ(m.c, 0.0f); + EXPECT_FLOAT_EQ(m.d, 1.0f); + EXPECT_FLOAT_EQ(m.e, 0.0f); + EXPECT_FLOAT_EQ(m.f, 0.0f); +} + +TEST(MatrixTest, Multiply) { + Matrix m1(2.0f, 0.0f, 0.0f, 2.0f, 10.0f, 20.0f); + Matrix m2(0.5f, 0.0f, 0.0f, 0.5f, -5.0f, -10.0f); + Matrix result = m1.multiply(m2); + + EXPECT_FLOAT_EQ(result.a, 1.0f); + EXPECT_FLOAT_EQ(result.b, 0.0f); + EXPECT_FLOAT_EQ(result.c, 0.0f); + EXPECT_FLOAT_EQ(result.d, 1.0f); + EXPECT_FLOAT_EQ(result.e, 0.0f); + EXPECT_FLOAT_EQ(result.f, 0.0f); +} + +TEST(MatrixTest, TransformPoint) { + Matrix m(2.0f, 0.0f, 0.0f, 3.0f, 10.0f, 20.0f); + float x = 5.0f; + float y = 5.0f; + m.transform(x, y); + + EXPECT_FLOAT_EQ(x, 20.0f); // 2*5 + 10 + EXPECT_FLOAT_EQ(y, 35.0f); // 3*5 + 20 +} + +TEST(GraphicsStateStackTest, PushPop) { + GraphicsStateStack stack; + + // Initial state + stack.current().lineWidth = 5.0f; + + // Push new state + stack.push(); + EXPECT_FLOAT_EQ(stack.current().lineWidth, 5.0f); + + // Modify current state + stack.current().lineWidth = 10.0f; + EXPECT_FLOAT_EQ(stack.current().lineWidth, 10.0f); + + // Pop back to initial + stack.pop(); + EXPECT_FLOAT_EQ(stack.current().lineWidth, 5.0f); +} + +TEST(GraphicsStateStackTest, PopEmptyProtection) { + GraphicsStateStack stack; + // Attempting to pop the root state should be safe (ignored) + stack.pop(); + stack.current().lineWidth = 2.0f; // Should still be valid + EXPECT_FLOAT_EQ(stack.current().lineWidth, 2.0f); +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 390cc02..2ec3aeb 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,8 +8,10 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "@tailwindcss/vite": "^4.3.0", "react": "^19.2.6", - "react-dom": "^19.2.6" + "react-dom": "^19.2.6", + "tailwindcss": "^4.3.0" }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -57,7 +59,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -267,11 +268,31 @@ "node": ">=6.9.0" } }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -476,7 +497,6 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -487,7 +507,6 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -498,7 +517,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -508,14 +526,12 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -526,7 +542,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -545,7 +560,6 @@ "version": "0.130.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz", "integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" @@ -558,7 +572,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -575,7 +588,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -592,7 +604,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -609,7 +620,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -626,7 +636,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -643,7 +652,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -660,7 +668,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -677,7 +684,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -694,7 +700,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -711,7 +716,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -728,7 +732,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -745,7 +748,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -762,7 +764,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -781,7 +782,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -798,7 +798,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -812,14 +811,269 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "dev": true, "license": "MIT" }, + "node_modules/@tailwindcss/node": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", + "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.21.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz", + "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-x64": "4.3.0", + "@tailwindcss/oxide-freebsd-x64": "4.3.0", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-x64-musl": "4.3.0", + "@tailwindcss/oxide-wasm32-wasi": "4.3.0", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", + "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", + "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", + "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", + "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", + "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", + "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz", + "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", + "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz", + "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz", + "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz", + "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz", + "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.0.tgz", + "integrity": "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.0", + "@tailwindcss/oxide": "4.3.0", + "tailwindcss": "4.3.0" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -851,9 +1105,8 @@ "version": "24.12.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz", "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==", - "dev": true, + "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -864,7 +1117,6 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -924,7 +1176,6 @@ "integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/types": "8.59.3", @@ -1155,7 +1406,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1246,7 +1496,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -1340,7 +1589,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -1353,6 +1601,19 @@ "dev": true, "license": "ISC" }, + "node_modules/enhanced-resolve": { + "version": "5.21.6", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", + "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -1382,7 +1643,6 @@ "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -1584,7 +1844,6 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -1653,7 +1912,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -1700,6 +1958,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -1767,6 +2031,15 @@ "dev": true, "license": "ISC" }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -1849,7 +2122,6 @@ "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "dev": true, "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -1882,7 +2154,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1903,7 +2174,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1924,7 +2194,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1945,7 +2214,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1966,7 +2234,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1987,7 +2254,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2008,7 +2274,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2029,7 +2294,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2050,7 +2314,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2071,7 +2334,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2092,7 +2354,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2132,6 +2393,15 @@ "yallist": "^3.0.2" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", @@ -2159,7 +2429,6 @@ "version": "3.3.12", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", - "dev": true, "funding": [ { "type": "github", @@ -2262,16 +2531,13 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -2283,7 +2549,6 @@ "version": "8.5.14", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -2333,7 +2598,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -2354,7 +2618,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz", "integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==", - "dev": true, "license": "MIT", "dependencies": { "@oxc-project/types": "=0.130.0", @@ -2427,17 +2690,34 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, + "node_modules/tailwindcss": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", + "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -2467,7 +2747,6 @@ "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", "optional": true }, @@ -2490,7 +2769,6 @@ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2527,7 +2805,7 @@ "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/update-browserslist-db": { @@ -2575,9 +2853,7 @@ "version": "8.0.13", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz", "integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", @@ -2702,7 +2978,6 @@ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/frontend/package.json b/frontend/package.json index 70b0913..5161e44 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,8 +10,10 @@ "preview": "vite preview" }, "dependencies": { + "@tailwindcss/vite": "^4.3.0", "react": "^19.2.6", - "react-dom": "^19.2.6" + "react-dom": "^19.2.6", + "tailwindcss": "^4.3.0" }, "devDependencies": { "@eslint/js": "^10.0.1", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 45f3316..6282708 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,7 +1,8 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { Toolbar } from './components/Toolbar'; import { Sidebar } from './components/Sidebar'; import { PDFViewer } from './viewer/PDFViewer'; +import type { PDFViewerRef } from './viewer/PDFViewer'; import type { Annotation } from './viewer/AnnotationLayer'; import { gatewayService } from './lib/gatewayService'; import type { DocumentInfo } from './lib/gatewayService'; @@ -9,6 +10,7 @@ import { wasmLoader } from './lib/wasmLoader'; import './App.css'; function App() { + const viewerRef = useRef(null); const [documents, setDocuments] = useState([]); const [selectedDocId, setSelectedDocId] = useState('sample-doc-1'); const [activeDoc, setActiveDoc] = useState(null); @@ -25,6 +27,35 @@ function App() { const [sidebarTab, setSidebarTab] = useState<'documents' | 'annotations' | 'outline'>('documents'); const [isLoading, setIsLoading] = useState(true); + // Search State + const [searchQuery, setSearchQuery] = useState(''); + const [searchResultCount, setSearchResultCount] = useState(0); + const [searchCurrentMatch, setSearchCurrentMatch] = useState(0); + + const handleSearch = (query: string) => { + setSearchQuery(query); + // Mock search results for Phase 0 + if (query) { + setSearchResultCount(5); + setSearchCurrentMatch(0); + } else { + setSearchResultCount(0); + setSearchCurrentMatch(0); + } + }; + + const handleSearchNext = () => { + if (searchResultCount > 0) { + setSearchCurrentMatch((prev) => (prev + 1) % searchResultCount); + } + }; + + const handleSearchPrev = () => { + if (searchResultCount > 0) { + setSearchCurrentMatch((prev) => (prev - 1 + searchResultCount) % searchResultCount); + } + }; + // Check Gateway Health on mount useEffect(() => { const checkHealth = async () => { @@ -131,6 +162,11 @@ function App() { totalPages={activeDoc?.totalPages || 1} onUploadStart={handleUploadStart} backendHealthy={backendHealthy} + onSearch={handleSearch} + searchResultCount={searchResultCount} + searchCurrentMatch={searchCurrentMatch} + onSearchNext={handleSearchNext} + onSearchPrev={handleSearchPrev} />
@@ -143,6 +179,9 @@ function App() { totalPages={activeDoc?.totalPages || 0} activeTab={sidebarTab} setActiveTab={setSidebarTab} + onNavigateToPage={(pageIndex) => { + viewerRef.current?.scrollToPage(pageIndex); + }} /> {/* Main PDF Scroll Viewer Area */} @@ -156,12 +195,14 @@ function App() {
) : activeDoc ? ( diff --git a/frontend/src/components/SearchBar.tsx b/frontend/src/components/SearchBar.tsx new file mode 100644 index 0000000..04fb0e4 --- /dev/null +++ b/frontend/src/components/SearchBar.tsx @@ -0,0 +1,58 @@ +import React, { useState } from 'react'; + +interface SearchBarProps { + onSearch: (query: string) => void; + resultCount: number; + currentMatch: number; + onNext: () => void; + onPrev: () => void; +} + +export const SearchBar: React.FC = ({ + onSearch, + resultCount, + currentMatch, + onNext, + onPrev, +}) => { + const [query, setQuery] = useState(''); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + onSearch(query); + } + }; + + return ( +
+ + + + setQuery(e.target.value)} + onKeyDown={handleKeyDown} + className="bg-transparent border-none outline-none text-slate-100 text-sm w-32 placeholder-slate-500" + /> + {query && ( +
+ {resultCount > 0 ? `${currentMatch + 1}/${resultCount}` : '0/0'} +
+ + +
+
+ )} +
+ ); +}; diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 1a2e987..0dd77a8 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -1,6 +1,7 @@ import React from 'react'; import type { DocumentInfo } from '../lib/gatewayService'; import type { Annotation } from '../viewer/AnnotationLayer'; +import { Thumbnail } from './Thumbnail'; interface SidebarProps { documents: DocumentInfo[]; @@ -10,6 +11,7 @@ interface SidebarProps { totalPages: number; activeTab: 'documents' | 'annotations' | 'outline'; setActiveTab: (tab: 'documents' | 'annotations' | 'outline') => void; + onNavigateToPage?: (pageIndex: number) => void; } export const Sidebar: React.FC = ({ @@ -20,6 +22,7 @@ export const Sidebar: React.FC = ({ totalPages, activeTab, setActiveTab, + onNavigateToPage, }) => { const formatBytes = (bytes: number) => { if (bytes === 0) return '0 Bytes'; @@ -104,14 +107,12 @@ export const Sidebar: React.FC = ({ {activeTab === 'outline' && (
{Array.from({ length: totalPages }).map((_, idx) => ( -
-
- - - - Page {idx + 1} -
-
+ onNavigateToPage?.(idx)} + /> ))}
)} diff --git a/frontend/src/components/Thumbnail.tsx b/frontend/src/components/Thumbnail.tsx new file mode 100644 index 0000000..2827fc5 --- /dev/null +++ b/frontend/src/components/Thumbnail.tsx @@ -0,0 +1,75 @@ +import React, { useEffect, useState } from 'react'; +import { gatewayService } from '../lib/gatewayService'; + +interface ThumbnailProps { + documentId: string; + pageIndex: number; + onClick: () => void; +} + +export const Thumbnail: React.FC = ({ + documentId, + pageIndex, + onClick, +}) => { + const [imageUrl, setImageUrl] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + let active = true; + + const fetchThumbnail = async () => { + try { + setLoading(true); + // Request a lower resolution/zoom image for the thumbnail + const url = await gatewayService.renderPage({ + documentId, + pageIndex, + zoom: 0.2, // Small zoom for thumbnail size + rotation: 0, + }); + + if (active) { + setImageUrl(url); + } + } catch (err) { + console.error(`Failed to load thumbnail for page ${pageIndex}`, err); + } finally { + if (active) setLoading(false); + } + }; + + fetchThumbnail(); + + return () => { + active = false; + }; + }, [documentId, pageIndex]); + + return ( +
+
+ {loading ? ( +
+
+ Loading... +
+ ) : imageUrl ? ( + {`Page + ) : ( +
+ + + + Error +
+ )} +
+ Page {pageIndex + 1} +
+ ); +}; diff --git a/frontend/src/components/Toolbar.tsx b/frontend/src/components/Toolbar.tsx index 112deae..913594c 100644 --- a/frontend/src/components/Toolbar.tsx +++ b/frontend/src/components/Toolbar.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { SearchBar } from './SearchBar'; interface ToolbarProps { zoom: number; @@ -13,6 +14,11 @@ interface ToolbarProps { backendHealthy: boolean | null; wasmEngineInfo?: string; wasmHasSkia?: boolean; + onSearch: (query: string) => void; + searchResultCount: number; + searchCurrentMatch: number; + onSearchNext: () => void; + onSearchPrev: () => void; } export const Toolbar: React.FC = ({ @@ -26,8 +32,11 @@ export const Toolbar: React.FC = ({ totalPages, onUploadStart, backendHealthy, - wasmEngineInfo, - wasmHasSkia, + onSearch, + searchResultCount, + searchCurrentMatch, + onSearchNext, + onSearchPrev, }) => { const handleZoomPercentSelect = (e: React.ChangeEvent) => { @@ -125,6 +134,14 @@ export const Toolbar: React.FC = ({ {/* Right Tools (Active tool highlights, Upload) */}
+ +
+ {/* SEARCH BAR */} + +