From 88553559b8666afe8e3e1820c3f94059b28a56b6 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Thu, 10 Sep 2026 16:31:59 +0200 Subject: [PATCH 1/9] push vuetify version [skip ci] --- app/components/Viewer/Options/ColorPicker.vue | 146 +++++++++++++----- app/utils/color_picker.js | 35 +++++ 2 files changed, 144 insertions(+), 37 deletions(-) create mode 100644 app/utils/color_picker.js diff --git a/app/components/Viewer/Options/ColorPicker.vue b/app/components/Viewer/Options/ColorPicker.vue index b88e1fee3..a4ff4ef63 100644 --- a/app/components/Viewer/Options/ColorPicker.vue +++ b/app/components/Viewer/Options/ColorPicker.vue @@ -1,6 +1,11 @@ diff --git a/app/utils/color_picker.js b/app/utils/color_picker.js new file mode 100644 index 000000000..33ba2bf84 --- /dev/null +++ b/app/utils/color_picker.js @@ -0,0 +1,35 @@ +const RGB_MAX = 255; +const PERCENT_MAX = 100; + +function parseColorString(colorText) { + if (!colorText) { + return undefined; + } + const rgbMatch = colorText + .trim() + .match( + /^(?:rgba?\()?(?\d{1,3})[\s,]+(?\d{1,3})[\s,]+(?\d{1,3})(?:[\s,/]+(?[\d.%]+))?\)?$/iu, + ); + if (!rgbMatch) { + return undefined; + } + + const { red, green, blue, alpha: rawAlpha } = rgbMatch.groups; + let alpha = 1; + if (rawAlpha) { + alpha = rawAlpha.endsWith("%") ? Number(rawAlpha.slice(0, -1)) / PERCENT_MAX : Number(rawAlpha); + if (alpha > 1) { + alpha /= RGB_MAX; + } + } + return { red: Number(red), green: Number(green), blue: Number(blue), alpha }; +} + +function formatColorString(color, mode) { + const { red, green, blue, alpha } = color; + return mode === "rgb" + ? `rgb(${red}, ${green}, ${blue})` + : `rgba(${red}, ${green}, ${blue}, ${alpha})`; +} + +export { parseColorString, formatColorString }; From a4143852f3fdd147083aa8eb25f188040016d672 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Fri, 11 Sep 2026 11:18:37 +0200 Subject: [PATCH 2/9] feat(colorpicker): Add input for color and copy to clipboard --- app/components/Viewer/Options/ColorPicker.vue | 254 ++++++++++++------ app/utils/color_picker.js | 51 ++-- 2 files changed, 209 insertions(+), 96 deletions(-) diff --git a/app/components/Viewer/Options/ColorPicker.vue b/app/components/Viewer/Options/ColorPicker.vue index a4ff4ef63..3e4b5c31b 100644 --- a/app/components/Viewer/Options/ColorPicker.vue +++ b/app/components/Viewer/Options/ColorPicker.vue @@ -1,155 +1,249 @@ diff --git a/app/utils/color_picker.js b/app/utils/color_picker.js index 33ba2bf84..ef6316c7e 100644 --- a/app/utils/color_picker.js +++ b/app/utils/color_picker.js @@ -1,35 +1,54 @@ const RGB_MAX = 255; const PERCENT_MAX = 100; +function parseComponent(val, max = RGB_MAX) { + if (val.endsWith("%")) { + return (Number(val.slice(0, -1)) / PERCENT_MAX) * max; + } + return Number(val); +} + function parseColorString(colorText) { - if (!colorText) { + if (!colorText || typeof colorText !== "string") { return undefined; } - const rgbMatch = colorText - .trim() - .match( - /^(?:rgba?\()?(?\d{1,3})[\s,]+(?\d{1,3})[\s,]+(?\d{1,3})(?:[\s,/]+(?[\d.%]+))?\)?$/iu, - ); - if (!rgbMatch) { + + const numbersMatch = colorText.match(/[\d.]+%?/gu); + if (!numbersMatch || (numbersMatch.length !== 3 && numbersMatch.length !== 4)) { return undefined; } - const { red, green, blue, alpha: rawAlpha } = rgbMatch.groups; + const rawRed = parseComponent(numbersMatch[0]); + const rawGreen = parseComponent(numbersMatch[1]); + const rawBlue = parseComponent(numbersMatch[2]); + + const red = Math.min(RGB_MAX, Math.max(0, Math.round(rawRed))); + const green = Math.min(RGB_MAX, Math.max(0, Math.round(rawGreen))); + const blue = Math.min(RGB_MAX, Math.max(0, Math.round(rawBlue))); + let alpha = 1; - if (rawAlpha) { - alpha = rawAlpha.endsWith("%") ? Number(rawAlpha.slice(0, -1)) / PERCENT_MAX : Number(rawAlpha); - if (alpha > 1) { - alpha /= RGB_MAX; - } + if (numbersMatch.length === 4) { + const rawAlpha = parseComponent(numbersMatch[3], 1); + const calculatedAlpha = rawAlpha > 1 ? rawAlpha / RGB_MAX : rawAlpha; + const roundedAlpha = Number(calculatedAlpha.toFixed(2)); + alpha = Math.min(1, Math.max(0, roundedAlpha)); } - return { red: Number(red), green: Number(green), blue: Number(blue), alpha }; + + if (Number.isNaN(red) || Number.isNaN(green) || Number.isNaN(blue) || Number.isNaN(alpha)) { + return undefined; + } + + return { red, green, blue, alpha }; } function formatColorString(color, mode) { const { red, green, blue, alpha } = color; + const roundRed = Math.round(red); + const roundGreen = Math.round(green); + const roundBlue = Math.round(blue); return mode === "rgb" - ? `rgb(${red}, ${green}, ${blue})` - : `rgba(${red}, ${green}, ${blue}, ${alpha})`; + ? `rgb(${roundRed}, ${roundGreen}, ${roundBlue})` + : `rgba(${roundRed}, ${roundGreen}, ${roundBlue}, ${alpha})`; } export { parseColorString, formatColorString }; From d6ad194187d972f2c2f8a23511489d82f6b1264b Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Fri, 11 Sep 2026 17:25:51 +0200 Subject: [PATCH 3/9] data-testid --- app/components/Viewer/Options/ColorPicker.vue | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/components/Viewer/Options/ColorPicker.vue b/app/components/Viewer/Options/ColorPicker.vue index 3e4b5c31b..2095b0aaa 100644 --- a/app/components/Viewer/Options/ColorPicker.vue +++ b/app/components/Viewer/Options/ColorPicker.vue @@ -56,9 +56,9 @@ function toggleMode() { updateInputTextFromColor(red, green, blue, alpha); } -function copyToClipboard() { +async function copyToClipboard() { const { r: red, g: green, b: blue, a: alpha } = vuetifyColor.value; - copy(formatColorString({ red, green, blue, alpha }, currentMode.value)); + await copy(formatColorString({ red, green, blue, alpha }, currentMode.value)); } function parseAndApplyText(text) { @@ -165,6 +165,7 @@ watch( Date: Mon, 21 Sep 2026 07:47:55 +0000 Subject: [PATCH 4/9] Apply prepare changes --- app/components/Viewer/Options/ColorPicker.vue | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/app/components/Viewer/Options/ColorPicker.vue b/app/components/Viewer/Options/ColorPicker.vue index c77f4e124..a881fe9b3 100644 --- a/app/components/Viewer/Options/ColorPicker.vue +++ b/app/components/Viewer/Options/ColorPicker.vue @@ -27,12 +27,7 @@ const vuetifyColor = ref({ a: initialColor.alpha, }); -function updateInputTextFromColor( - red: number, - green: number, - blue: number, - alpha: number, -) { +function updateInputTextFromColor(red: number, green: number, blue: number, alpha: number) { colorInputText.value = disabledAlpha || currentMode.value === "rgb" ? `${red}, ${green}, ${blue}` From 00eae79fe7cdfbfa8d9da119bf98cc912426d7d9 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Tue, 22 Sep 2026 14:00:57 +0200 Subject: [PATCH 5/9] fix oxlint --- app/components/csv-preview/CsvPreviewer.vue | 15 +++++++-------- app/stores/app.ts | 2 ++ app/stores/viewer.ts | 1 + internal/stores/hybrid_viewer/filters.ts | 10 ++-------- internal/stores/hybrid_viewer/viewport.ts | 10 ++-------- 5 files changed, 14 insertions(+), 24 deletions(-) diff --git a/app/components/csv-preview/CsvPreviewer.vue b/app/components/csv-preview/CsvPreviewer.vue index c62b6fd15..e797c87f4 100644 --- a/app/components/csv-preview/CsvPreviewer.vue +++ b/app/components/csv-preview/CsvPreviewer.vue @@ -77,14 +77,7 @@ function autoDetectSeparator(content: string): string { return best; } -function parseContent(): string[] { - if (!rawContent.value) { - return []; - } - - const allLines = rawContent.value.split(/\r?\n/u).filter((line) => line.trim() !== ""); - - function splitLine(line: string): string[] { +function splitLine(line: string): string[] { if (!separator.value) { return [line]; } @@ -105,6 +98,12 @@ function parseContent(): string[] { return result; } +function parseContent(): string[] { + if (!rawContent.value) { + return []; + } + + const allLines = rawContent.value.split(/\r?\n/u).filter((line) => line.trim() !== ""); const headerLine = allLines[headerRow.value]; const rawHeaders = headerLine ? splitLine(headerLine) : []; diff --git a/app/stores/app.ts b/app/stores/app.ts index 910d06b8b..12bde2496 100644 --- a/app/stores/app.ts +++ b/app/stores/app.ts @@ -142,6 +142,7 @@ export const useAppStore = defineStore("app", () => { getExtensionEnabled, } = useAppExtensions(); +// oxlint-disable-next-line unicorn/consistent-function-scoping async function upload(file: Readonly, callbacks: RequestHandlers = {}): Promise { const store = useAppStore(); const schema = opengeodeweb_front_schemas.api.local.extensions.upload; @@ -163,6 +164,7 @@ export const useAppStore = defineStore("app", () => { } // `TResult` is asserted, not verified, at the single `return result as TResult` boundary below: the backend response is only checked against `schema` at runtime, so callers' `TResult` is a contract with the schema, not something this function can prove. + // oxlint-disable-next-line unicorn/consistent-function-scoping async function request( { schema, diff --git a/app/stores/viewer.ts b/app/stores/viewer.ts index 51dcbf734..52f82a7b0 100644 --- a/app/stores/viewer.ts +++ b/app/stores/viewer.ts @@ -62,6 +62,7 @@ export const useViewerStore = defineStore( function toggle_picking_mode(value: boolean): void { picking_mode.value = value; } + // oxlint-disable-next-line unicorn/consistent-function-scoping async function request( { schema, diff --git a/internal/stores/hybrid_viewer/filters.ts b/internal/stores/hybrid_viewer/filters.ts index d820ef692..6933dd919 100644 --- a/internal/stores/hybrid_viewer/filters.ts +++ b/internal/stores/hybrid_viewer/filters.ts @@ -34,15 +34,9 @@ function useHybridViewerFilters(): { setClippingPlanes: (ids: string[], planes: unknown) => Promise; setShrink: (ids: string[], shrink_factor: number) => Promise; } { - async function setClippingPlanes(ids: string[], planes: unknown): Promise { - await performSetClippingPlanes(ids, planes); - } - async function setShrink(ids: string[], shrink_factor: number): Promise { - await performSetShrink(ids, shrink_factor); - } return { - setClippingPlanes, - setShrink, + setClippingPlanes: performSetClippingPlanes, + setShrink: performSetShrink, }; } export { performSetClippingPlanes, performSetShrink, useHybridViewerFilters }; diff --git a/internal/stores/hybrid_viewer/viewport.ts b/internal/stores/hybrid_viewer/viewport.ts index e02e44a04..38ab6778e 100644 --- a/internal/stores/hybrid_viewer/viewport.ts +++ b/internal/stores/hybrid_viewer/viewport.ts @@ -167,15 +167,9 @@ function performSetContainer(container: ContainerRef | undefined): void { } function useHybridViewerViewport(): HybridViewerViewport { const { viewStream } = useHybridViewerCore(); - function setContainer(container: ContainerRef | undefined): void { - performSetContainer(container); - } - async function resize(width: number, height: number): Promise { - await performResize(width, height); - } return { - setContainer, - resize, + setContainer: performSetContainer, + resize: performResize, viewStream, }; } From fe5c6302e1c0a6ef8b0657ccb4d5c14ba54528b1 Mon Sep 17 00:00:00 2001 From: MaxNumerique <144453705+MaxNumerique@users.noreply.github.com> Date: Tue, 22 Sep 2026 12:58:04 +0000 Subject: [PATCH 6/9] Apply prepare changes --- app/components/csv-preview/CsvPreviewer.vue | 34 ++++++++++----------- app/stores/app.ts | 2 +- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/app/components/csv-preview/CsvPreviewer.vue b/app/components/csv-preview/CsvPreviewer.vue index e797c87f4..34e644e6e 100644 --- a/app/components/csv-preview/CsvPreviewer.vue +++ b/app/components/csv-preview/CsvPreviewer.vue @@ -78,25 +78,25 @@ function autoDetectSeparator(content: string): string { } function splitLine(line: string): string[] { - if (!separator.value) { - return [line]; - } - const result = []; - let current = ""; - let inQuotes = false; - for (const char of line) { - if (char === '"') { - inQuotes = !inQuotes; - } else if (char === separator.value && !inQuotes) { - result.push(current.trim()); - current = ""; - } else { - current += char; - } + if (!separator.value) { + return [line]; + } + const result = []; + let current = ""; + let inQuotes = false; + for (const char of line) { + if (char === '"') { + inQuotes = !inQuotes; + } else if (char === separator.value && !inQuotes) { + result.push(current.trim()); + current = ""; + } else { + current += char; } - result.push(current.trim()); - return result; } + result.push(current.trim()); + return result; +} function parseContent(): string[] { if (!rawContent.value) { diff --git a/app/stores/app.ts b/app/stores/app.ts index 12bde2496..7c686abc3 100644 --- a/app/stores/app.ts +++ b/app/stores/app.ts @@ -142,7 +142,7 @@ export const useAppStore = defineStore("app", () => { getExtensionEnabled, } = useAppExtensions(); -// oxlint-disable-next-line unicorn/consistent-function-scoping + // oxlint-disable-next-line unicorn/consistent-function-scoping async function upload(file: Readonly, callbacks: RequestHandlers = {}): Promise { const store = useAppStore(); const schema = opengeodeweb_front_schemas.api.local.extensions.upload; From 49a85b79ab23c0633d64f8b49da8bdf70e04fa41 Mon Sep 17 00:00:00 2001 From: MaxNumerique Date: Tue, 22 Sep 2026 15:07:00 +0200 Subject: [PATCH 7/9] copy to clipboard refacto & performance update --- .../Viewer/ContextMenu/InfoCard.vue | 5 ++-- app/components/Viewer/Options/ColorPicker.vue | 27 ++++++++++++++----- app/composables/copy_to_clipboard.ts | 7 +++++ 3 files changed, 29 insertions(+), 10 deletions(-) create mode 100644 app/composables/copy_to_clipboard.ts diff --git a/app/components/Viewer/ContextMenu/InfoCard.vue b/app/components/Viewer/ContextMenu/InfoCard.vue index 6871cf727..28c6afa22 100644 --- a/app/components/Viewer/ContextMenu/InfoCard.vue +++ b/app/components/Viewer/ContextMenu/InfoCard.vue @@ -1,7 +1,7 @@