diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index dbbb26b0c8..be5afe0e33 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -148,7 +148,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up JDK 25 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: java-version: "25" distribution: "temurin" diff --git a/app/core/src/main/resources/static/3rdPartyLicenses.json b/app/core/src/main/resources/static/3rdPartyLicenses.json index fbdce0a158..c411735489 100644 --- a/app/core/src/main/resources/static/3rdPartyLicenses.json +++ b/app/core/src/main/resources/static/3rdPartyLicenses.json @@ -527,27 +527,45 @@ "moduleLicense": "The BSD License", "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" }, + { + "moduleName": "com.twelvemonkeys.common:common-image", + "moduleVersion": "3.14.0", + "moduleLicense": "The BSD License", + "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" + }, { "moduleName": "com.twelvemonkeys.common:common-io", "moduleVersion": "3.13.1", "moduleLicense": "The BSD License", "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" }, + { + "moduleName": "com.twelvemonkeys.common:common-io", + "moduleVersion": "3.14.0", + "moduleLicense": "The BSD License", + "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" + }, { "moduleName": "com.twelvemonkeys.common:common-lang", "moduleVersion": "3.13.1", "moduleLicense": "The BSD License", "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" }, + { + "moduleName": "com.twelvemonkeys.common:common-lang", + "moduleVersion": "3.14.0", + "moduleLicense": "The BSD License", + "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" + }, { "moduleName": "com.twelvemonkeys.imageio:imageio-batik", - "moduleVersion": "3.13.1", + "moduleVersion": "3.14.0", "moduleLicense": "The BSD License", "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" }, { "moduleName": "com.twelvemonkeys.imageio:imageio-bmp", - "moduleVersion": "3.13.1", + "moduleVersion": "3.14.0", "moduleLicense": "The BSD License", "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" }, @@ -557,9 +575,15 @@ "moduleLicense": "The BSD License", "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" }, + { + "moduleName": "com.twelvemonkeys.imageio:imageio-core", + "moduleVersion": "3.14.0", + "moduleLicense": "The BSD License", + "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" + }, { "moduleName": "com.twelvemonkeys.imageio:imageio-jpeg", - "moduleVersion": "3.13.1", + "moduleVersion": "3.14.0", "moduleLicense": "The BSD License", "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" }, @@ -569,9 +593,15 @@ "moduleLicense": "The BSD License", "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" }, + { + "moduleName": "com.twelvemonkeys.imageio:imageio-metadata", + "moduleVersion": "3.14.0", + "moduleLicense": "The BSD License", + "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" + }, { "moduleName": "com.twelvemonkeys.imageio:imageio-psd", - "moduleVersion": "3.13.1", + "moduleVersion": "3.14.0", "moduleLicense": "The BSD License", "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" }, @@ -581,12 +611,24 @@ "moduleLicense": "The BSD License", "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" }, + { + "moduleName": "com.twelvemonkeys.imageio:imageio-tiff", + "moduleVersion": "3.14.0", + "moduleLicense": "The BSD License", + "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" + }, { "moduleName": "com.twelvemonkeys.imageio:imageio-webp", "moduleVersion": "3.13.1", "moduleLicense": "The BSD License", "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" }, + { + "moduleName": "com.twelvemonkeys.imageio:imageio-webp", + "moduleVersion": "3.14.0", + "moduleLicense": "The BSD License", + "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" + }, { "moduleName": "com.vladsch.flexmark:flexmark", "moduleVersion": "0.64.8", @@ -2262,7 +2304,7 @@ }, { "moduleName": "org.simplejavamail:core-module", - "moduleVersion": "9.2.0", + "moduleVersion": "9.3.1", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, @@ -2275,13 +2317,13 @@ }, { "moduleName": "org.simplejavamail:outlook-module", - "moduleVersion": "9.2.0", + "moduleVersion": "9.3.1", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, { "moduleName": "org.simplejavamail:simple-java-mail", - "moduleVersion": "9.2.0", + "moduleVersion": "9.3.1", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, diff --git a/app/proprietary/build.gradle b/app/proprietary/build.gradle index b884cb18be..e66288238a 100644 --- a/app/proprietary/build.gradle +++ b/app/proprietary/build.gradle @@ -27,7 +27,7 @@ dependencies { api 'org.springframework.boot:spring-boot-starter-cache' api 'com.github.ben-manes.caffeine:caffeine' implementation 'org.springframework.boot:spring-boot-starter-data-redis' - api 'io.swagger.core.v3:swagger-core-jakarta:2.2.46' + api 'io.swagger.core.v3:swagger-core-jakarta:2.2.53' implementation "com.bucket4j:bucket4j_jdk17-core:${bucket4jVersion}" // Lettuce-backed Bucket4j ProxyManager used by ValkeyRateLimitStore for cluster-wide // token-bucket rate limiting (parity with in-process Bucket4j semantics; no fixed-window diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/service/ua/PdfUaBenchmarkTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/service/ua/PdfUaBenchmarkTest.java deleted file mode 100644 index 2c4ca6db2a..0000000000 --- a/app/proprietary/src/test/java/stirling/software/proprietary/service/ua/PdfUaBenchmarkTest.java +++ /dev/null @@ -1,341 +0,0 @@ -package stirling.software.proprietary.service.ua; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; - -import org.apache.pdfbox.Loader; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.PDPage; -import org.apache.pdfbox.pdmodel.PDPageContentStream; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.apache.pdfbox.pdmodel.font.PDFont; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import stirling.software.common.service.CustomPDFDocumentFactory; -import stirling.software.common.service.PdfMetadataService; -import stirling.software.proprietary.pdf.ua.DocumentStructure; -import stirling.software.proprietary.pdf.ua.LayoutAnalyzer; -import stirling.software.proprietary.pdf.ua.PageContent; -import stirling.software.proprietary.pdf.ua.PdfUaProfile; -import stirling.software.proprietary.pdf.ua.PdfUaTagger; -import stirling.software.proprietary.pdf.ua.TaggedContentExtractor; -import stirling.software.proprietary.pdf.ua.TaggingOptions; - -/** - * Measures where conversion time and memory go; meant to be read, not to gate CI. Assertions catch - * only order-of-magnitude regressions - wall-clock numbers are no contract. - */ -class PdfUaBenchmarkTest { - - private static PdfUaConversionService service; - private static PdfUaValidationService validation; - - @BeforeAll - static void setUp() { - validation = new PdfUaValidationService(); - validation.initialise(); - service = - new PdfUaConversionService( - validation, - new FontEmbeddingService(), - new CustomPDFDocumentFactory( - org.mockito.Mockito.mock(PdfMetadataService.class))); - } - - /** A realistic page: heading, prose, a small table, a bullet list. */ - private static byte[] document(int pages) throws IOException { - try (PDDocument document = new PDDocument()) { - PDFont font = null; - for (int p = 0; p < pages; p++) { - PDPage page = new PDPage(PDRectangle.A4); - document.addPage(page); - if (font == null) { - font = PdfUaTestDocuments.font(document); - } - try (PDPageContentStream cs = new PDPageContentStream(document, page)) { - float y = 790; - write(cs, font, 9, 60, 810, "Benchmark Corpus Running Head"); - write(cs, font, 18, 60, y, "Section " + (p + 1)); - y -= 30; - for (int line = 0; line < 22; line++) { - write( - cs, - font, - 11, - 60, - y, - "Body line " + line + " of section " + (p + 1) + " with prose."); - y -= 15; - } - for (int row = 0; row < 4; row++) { - cs.beginText(); - cs.setFont(font, 11); - cs.newLineAtOffset(60, y); - cs.showText("Row " + row); - cs.newLineAtOffset(160, 0); - cs.showText(String.valueOf(row * 120)); - cs.newLineAtOffset(140, 0); - cs.showText(String.valueOf(row * 480)); - cs.endText(); - y -= 16; - } - write(cs, font, 11, 60, y - 10, "• First bullet point"); - write(cs, font, 11, 60, y - 25, "• Second bullet point"); - write(cs, font, 9, 300, 30, "Page " + (p + 1)); - } - } - ByteArrayOutputStream out = new ByteArrayOutputStream(); - document.save(out); - return out.toByteArray(); - } - } - - private static void write( - PDPageContentStream cs, PDFont font, float size, float x, float y, String text) - throws IOException { - cs.beginText(); - cs.setFont(font, size); - cs.newLineAtOffset(x, y); - cs.showText(text); - cs.endText(); - } - - private static TaggingOptions options() { - return TaggingOptions.builder() - .profile(PdfUaProfile.UA1) - .language("en-GB") - .title("Benchmark") - .embedFonts(false) - .existingTags(TaggingOptions.ExistingTags.REBUILD) - .build(); - } - - private static long usedHeap() { - Runtime runtime = Runtime.getRuntime(); - System.gc(); - return runtime.totalMemory() - runtime.freeMemory(); - } - - @Test - @DisplayName("reports throughput and memory across document sizes") - void throughputAcrossSizes() throws Exception { - int[] sizes = {1, 10, 50, 150}; - StringBuilder report = - new StringBuilder("\nPDF/UA conversion throughput\n") - .append( - String.format( - " %-7s %-10s %-12s %-12s %-10s %s%n", - "pages", - "input", - "convert ms", - "ms/page", - "pages/s", - "heap MB")); - - // Warm up so the first timed run is not measuring class loading and JIT. - service.convert(document(5), options()); - - for (int pages : sizes) { - byte[] input = document(pages); - long heapBefore = usedHeap(); - long start = System.nanoTime(); - var outcome = service.convert(input, options()); - long elapsedMs = (System.nanoTime() - start) / 1_000_000; - long heapDelta = (usedHeap() - heapBefore) / (1024 * 1024); - - assertTrue(outcome.pdfBytes().length > 0); - report.append( - String.format( - Locale.ROOT, - " %-7d %-10s %-12d %-12.2f %-10.1f %d%n", - pages, - humanBytes(input.length), - elapsedMs, - elapsedMs / (double) pages, - pages * 1000.0 / Math.max(elapsedMs, 1), - Math.max(heapDelta, 0))); - } - System.out.println(report); - } - - @Test - @DisplayName("breaks conversion down by phase so optimisation has a target") - void phaseBreakdown() throws Exception { - byte[] input = document(60); - - // Warm up. - try (PDDocument warm = Loader.loadPDF(input)) { - new TaggedContentExtractor().extract(warm); - } - - long parseMs; - long extractMs; - long analyseMs; - long tagMs; - List pages; - DocumentStructure structure; - - long t0 = System.nanoTime(); - try (PDDocument document = Loader.loadPDF(input)) { - parseMs = ms(t0); - - long t1 = System.nanoTime(); - pages = new TaggedContentExtractor().extract(document); - extractMs = ms(t1); - - long t2 = System.nanoTime(); - structure = new LayoutAnalyzer().analyse(pages); - analyseMs = ms(t2); - } - - long t3 = System.nanoTime(); - try (PDDocument document = Loader.loadPDF(input)) { - new PdfUaTagger().tag(document, options()); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - document.save(out); - } - tagMs = ms(t3); - - long t4 = System.nanoTime(); - var outcome = service.convert(input, options()); - long totalMs = ms(t4); - - long t5 = System.nanoTime(); - validation.validate(outcome.pdfBytes(), PdfUaProfile.UA1); - long validateMs = ms(t5); - - System.out.printf( - Locale.ROOT, - "%nPhase breakdown over %d pages (%d blocks)%n" - + " parse %5d ms%n" - + " extract %5d ms (text pass + token scan)%n" - + " analyse %5d ms%n" - + " tag end-to-end %5d ms (includes parse, extract, analyse, inject, write)%n" - + " validate %5d ms (veraPDF)%n" - + " full convert %5d ms (tag + declare + validate)%n", - 60, - structure.getBlocks().size(), - parseMs, - extractMs, - analyseMs, - tagMs, - validateMs, - totalMs); - - assertTrue(pages.size() == 60, "extractor lost pages"); - } - - @Test - @DisplayName("splits the tagging pass into its own sub-phases") - void taggingSubPhases() throws Exception { - byte[] input = document(60); - try (PDDocument warm = Loader.loadPDF(input)) { - new TaggedContentExtractor().extract(warm); - } - - long extractMs; - long analyseMs; - long injectMs; - long treeMs; - long saveMs; - - try (PDDocument document = Loader.loadPDF(input)) { - long t = System.nanoTime(); - List pages = new TaggedContentExtractor().extract(document); - extractMs = ms(t); - - t = System.nanoTime(); - DocumentStructure structure = new LayoutAnalyzer().analyse(pages); - analyseMs = ms(t); - - t = System.nanoTime(); - var injector = new stirling.software.proprietary.pdf.ua.MarkedContentInjector(); - var byPage = - new java.util.LinkedHashMap< - Integer, List>(); - structure - .getBlocks() - .forEach( - b -> - byPage.computeIfAbsent(b.getPageIndex(), k -> new ArrayList<>()) - .add(b)); - for (int p = 0; p < document.getNumberOfPages(); p++) { - injector.inject( - document, document.getPage(p), byPage.getOrDefault(p, List.of()), 0, true); - } - injectMs = ms(t); - - t = System.nanoTime(); - new stirling.software.proprietary.pdf.ua.StructTreeWriter() - .write(document, structure, PdfUaProfile.UA1); - treeMs = ms(t); - - t = System.nanoTime(); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - document.save(out); - saveMs = ms(t); - } - - System.out.printf( - Locale.ROOT, - "%nTagging sub-phases over 60 pages%n" - + " extract %5d ms%n" - + " analyse %5d ms%n" - + " inject %5d ms%n" - + " struct tree %5d ms%n" - + " save %5d ms%n", - extractMs, - analyseMs, - injectMs, - treeMs, - saveMs); - } - - @Test - @DisplayName("memory stays proportional to document size, not quadratic") - void memoryScales() throws Exception { - List rows = new ArrayList<>(); - long previousPerPage = 0; - boolean blewUp = false; - - for (int pages : new int[] {20, 80, 200}) { - byte[] input = document(pages); - long before = usedHeap(); - var outcome = service.convert(input, options()); - long after = usedHeap(); - long perPageKb = Math.max(after - before, 0) / 1024 / pages; - rows.add( - String.format( - Locale.ROOT, - " %-6d pages in %-9s out %-9s ~%d KB/page retained", - pages, - humanBytes(input.length), - humanBytes(outcome.pdfBytes().length), - perPageKb)); - // Per-page cost should stay roughly flat; a big jump means something accumulates. - if (previousPerPage > 0 && perPageKb > previousPerPage * 4 && perPageKb > 200) { - blewUp = true; - } - previousPerPage = Math.max(perPageKb, 1); - } - System.out.println("\nMemory scaling\n" + String.join("\n", rows)); - assertTrue(!blewUp, "per-page memory grew superlinearly: " + rows); - } - - private static long ms(long startNanos) { - return (System.nanoTime() - startNanos) / 1_000_000; - } - - private static String humanBytes(int bytes) { - return bytes < 1024 * 1024 - ? (bytes / 1024) + " KB" - : String.format(Locale.ROOT, "%.1f MB", bytes / 1024.0 / 1024.0); - } -} diff --git a/docker/backend/Dockerfile b/docker/backend/Dockerfile index a455da4d1e..0d5a310c45 100644 --- a/docker/backend/Dockerfile +++ b/docker/backend/Dockerfile @@ -4,7 +4,7 @@ ARG BASE_VERSION=1.0.2@sha256:c7698687f486707ddef9e0298587ca8b44c4e96185e1bdb0c3 ARG BASE_IMAGE=stirlingtools/stirling-pdf-base:${BASE_VERSION} # Stage 1: Build the Java application (backend only, no frontend) -FROM gradle:9.7.0-jdk25@sha256:7d4e63b32991e679b183645680ff81762b6f1ef137850d8c2750b362eb994d08 AS app-build +FROM gradle:9.7.1-jdk25@sha256:a80276ab804c348989df46016e2b5d58cad07c5b29e06f2112434d28ca5b2844 AS app-build # JDK 25+: --add-exports is no longer accepted via JAVA_TOOL_OPTIONS; use JDK_JAVA_OPTIONS instead ENV JDK_JAVA_OPTIONS="--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \ diff --git a/docker/embedded/Dockerfile b/docker/embedded/Dockerfile index d39b7178b6..fb9e1619b9 100644 --- a/docker/embedded/Dockerfile +++ b/docker/embedded/Dockerfile @@ -5,7 +5,7 @@ ARG BASE_VERSION=1.0.2@sha256:c7698687f486707ddef9e0298587ca8b44c4e96185e1bdb0c3 ARG BASE_IMAGE=stirlingtools/stirling-pdf-base:${BASE_VERSION} # Stage 1: Build the Java application and frontend -FROM gradle:9.7.0-jdk25@sha256:7d4e63b32991e679b183645680ff81762b6f1ef137850d8c2750b362eb994d08 AS app-build +FROM gradle:9.7.1-jdk25@sha256:a80276ab804c348989df46016e2b5d58cad07c5b29e06f2112434d28ca5b2844 AS app-build ARG TASK_VERSION=3.53.1 RUN apt-get update \ diff --git a/docker/embedded/Dockerfile.fat b/docker/embedded/Dockerfile.fat index 978f7b09f3..7042adb1c4 100644 --- a/docker/embedded/Dockerfile.fat +++ b/docker/embedded/Dockerfile.fat @@ -8,7 +8,7 @@ ARG BASE_VERSION=1.0.2@sha256:c7698687f486707ddef9e0298587ca8b44c4e96185e1bdb0c3 ARG BASE_IMAGE=stirlingtools/stirling-pdf-base:${BASE_VERSION} # Stage 1: Build the Java application and frontend -FROM gradle:9.7.0-jdk25@sha256:7d4e63b32991e679b183645680ff81762b6f1ef137850d8c2750b362eb994d08 AS app-build +FROM gradle:9.7.1-jdk25@sha256:a80276ab804c348989df46016e2b5d58cad07c5b29e06f2112434d28ca5b2844 AS app-build ARG TASK_VERSION=3.53.1 RUN apt-get update \ diff --git a/docker/embedded/Dockerfile.ultra-lite b/docker/embedded/Dockerfile.ultra-lite index aa611d36d5..4f6df41c47 100644 --- a/docker/embedded/Dockerfile.ultra-lite +++ b/docker/embedded/Dockerfile.ultra-lite @@ -4,7 +4,7 @@ # Single JAR contains both frontend and backend with minimal dependencies # Stage 1: Build application with embedded frontend -FROM gradle:9.7.0-jdk25@sha256:7d4e63b32991e679b183645680ff81762b6f1ef137850d8c2750b362eb994d08 AS build +FROM gradle:9.7.1-jdk25@sha256:a80276ab804c348989df46016e2b5d58cad07c5b29e06f2112434d28ca5b2844 AS build # Install Node.js and npm for frontend build ARG TASK_VERSION=3.53.1 diff --git a/engine/pyproject.toml b/engine/pyproject.toml index 8071f45bab..0e4ffb1586 100644 --- a/engine/pyproject.toml +++ b/engine/pyproject.toml @@ -49,7 +49,7 @@ tools = [ "deep-translator>=1.11.4", "defusedxml>=0.7.1", "fonttools>=4.63.0", - "fpdf2>=2.8.7", + "fpdf2>=2.8.8", "openai>=2.53.0", "requests>=2.34.2", "tomli-w>=1.2.0", diff --git a/engine/uv.lock b/engine/uv.lock index 85162cdb5f..bb5b812fce 100644 --- a/engine/uv.lock +++ b/engine/uv.lock @@ -726,7 +726,7 @@ tools = [ { name = "deep-translator", specifier = ">=1.11.4" }, { name = "defusedxml", specifier = ">=0.7.1" }, { name = "fonttools", specifier = ">=4.63.0" }, - { name = "fpdf2", specifier = ">=2.8.7" }, + { name = "fpdf2", specifier = ">=2.8.8" }, { name = "openai", specifier = ">=2.53.0" }, { name = "requests", specifier = ">=2.34.2" }, { name = "tomli-w", specifier = ">=1.2.0" }, @@ -898,16 +898,16 @@ woff = [ [[package]] name = "fpdf2" -version = "2.8.7" +version = "2.8.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "defusedxml" }, { name = "fonttools" }, { name = "pillow" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/27/f2/72feae0b2827ed38013e4307b14f95bf0b3d124adfef4d38a7d57533f7be/fpdf2-2.8.7.tar.gz", hash = "sha256:7060ccee5a9c7ab0a271fb765a36a23639f83ef8996c34e3d46af0a17ede57f9", size = 362351, upload-time = "2026-02-28T05:39:16.456Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/bc/8fd4321aed40cadadddc8f311c65b6082346b252bca048f7b476d8f35d72/fpdf2-2.8.8.tar.gz", hash = "sha256:9e94e155e85e8053329a9a1fce8b566fd7a7c5bb79e98a1a3952d379b947c5b9", size = 374689, upload-time = "2026-08-09T23:32:45.334Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/66/0a/cf50ecffa1e3747ed9380a3adfc829259f1f86b3fdbd9e505af789003141/fpdf2-2.8.7-py3-none-any.whl", hash = "sha256:d391fc508a3ce02fc43a577c830cda4fe6f37646f2d143d489839940932fbc19", size = 327056, upload-time = "2026-02-28T05:39:14.619Z" }, + { url = "https://files.pythonhosted.org/packages/f5/be/af012eda9507494f28b99b077423806c43a11573eb6225dd46f19ae2d263/fpdf2-2.8.8-py3-none-any.whl", hash = "sha256:3557a478fc577a929c94aace9666aed4dcc432b5ab6764232e6a59f1ccd75f17", size = 337000, upload-time = "2026-08-09T23:32:43.728Z" }, ] [[package]] diff --git a/frontend/editor/src/core/components/shared/TextInput.test.tsx b/frontend/editor/src/core/components/shared/TextInput.test.tsx new file mode 100644 index 0000000000..d0dd2c9564 --- /dev/null +++ b/frontend/editor/src/core/components/shared/TextInput.test.tsx @@ -0,0 +1,79 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import { describe, it, expect, vi } from "vitest"; +import { TextInput } from "@app/components/shared/TextInput"; +import { MantineProvider } from "@mantine/core"; + +function renderWithMantine(ui: React.ReactElement) { + return render({ui}); +} + +describe("TextInput", () => { + it("renders input with value and placeholder", () => { + renderWithMantine( + , + ); + + const input = screen.getByPlaceholderText("Type here..."); + expect(input).toBeInTheDocument(); + expect(input).toHaveValue("hello"); + }); + + it("shows clear button when there is a value and triggers clear on click", () => { + const onChange = vi.fn(); + renderWithMantine( + , + ); + + const clearButton = screen.getByRole("button", { + name: /(?:clear|textInput\.clear)/i, + }); + expect(clearButton).toBeInTheDocument(); + + fireEvent.click(clearButton); + expect(onChange).toHaveBeenCalledWith(""); + }); + + it("does not show clear button when value is empty", () => { + renderWithMantine( + , + ); + + expect( + screen.queryByRole("button", { name: /clear input/i }), + ).not.toBeInTheDocument(); + }); + + it("does not show clear button when showClearButton is false", () => { + renderWithMantine( + , + ); + + expect( + screen.queryByRole("button", { name: /clear input/i }), + ).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/editor/src/core/components/shared/TextInput.tsx b/frontend/editor/src/core/components/shared/TextInput.tsx index 7d927bb011..41a4223a4c 100644 --- a/frontend/editor/src/core/components/shared/TextInput.tsx +++ b/frontend/editor/src/core/components/shared/TextInput.tsx @@ -119,12 +119,25 @@ export const TextInput = forwardRef( /> {shouldShowClearButton && ( - + )} diff --git a/frontend/editor/src/core/components/shared/superSearch/SuperSearch.css b/frontend/editor/src/core/components/shared/superSearch/SuperSearch.css index 6920c37be4..a815d88fca 100644 --- a/frontend/editor/src/core/components/shared/superSearch/SuperSearch.css +++ b/frontend/editor/src/core/components/shared/superSearch/SuperSearch.css @@ -209,9 +209,6 @@ } .super-search-item { - display: flex; - align-items: center; - gap: 0.5rem; width: 100%; padding: 0.45rem 0.5rem; border: none; @@ -227,6 +224,18 @@ background: var(--c-hover); } +.super-search-item .mantine-Button-inner { + width: 100%; +} + +.super-search-item .mantine-Button-label { + display: flex; + align-items: center; + gap: 0.75rem; + width: 100%; + overflow: visible; +} + .super-search-item-icon { display: flex; align-items: center; @@ -241,6 +250,7 @@ display: flex; flex-direction: column; min-width: 0; + flex: 1 1 auto; } .super-search-item-title { diff --git a/frontend/editor/src/core/components/shared/textInput/TextInput.module.css b/frontend/editor/src/core/components/shared/textInput/TextInput.module.css index b5131b014f..0b6a38c7af 100644 --- a/frontend/editor/src/core/components/shared/textInput/TextInput.module.css +++ b/frontend/editor/src/core/components/shared/textInput/TextInput.module.css @@ -52,22 +52,29 @@ .clearButton { position: absolute; - right: 8px; + right: 6px; top: 50%; transform: translateY(-50%); - background: none; - border: none; - cursor: pointer; - padding: 4px; - border-radius: 4px; - display: flex; + --ai-size: 1.35rem !important; + width: 1.35rem !important; + height: 1.35rem !important; + min-width: 1.35rem !important; + min-height: 1.35rem !important; + padding: 0 !important; + display: inline-flex; align-items: center; justify-content: center; - font-size: 16px; - transition: background-color 0.2s ease; + border-radius: var(--radius-full, 9999px); + background: transparent; + border: none; + cursor: pointer; color: var(--c-text-subtle); + transition: + background-color 0.15s ease, + color 0.15s ease; } .clearButton:hover { - background-color: var(--c-hover); + color: var(--c-text); + background-color: var(--c-hover) !important; } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 69dd0d0404..ad7845be30 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500