diff --git a/app/components/Viewer/ContextMenu/InfoCard.vue b/app/components/Viewer/ContextMenu/InfoCard.vue index 6871cf72..28c6afa2 100644 --- a/app/components/Viewer/ContextMenu/InfoCard.vue +++ b/app/components/Viewer/ContextMenu/InfoCard.vue @@ -1,7 +1,7 @@ diff --git a/app/composables/copy_to_clipboard.ts b/app/composables/copy_to_clipboard.ts new file mode 100644 index 00000000..c17bd899 --- /dev/null +++ b/app/composables/copy_to_clipboard.ts @@ -0,0 +1,7 @@ +import { useClipboard } from "@vueuse/core"; + +const COPIED_FEEDBACK_TIMEOUT = 1500; + +export function useCopyToClipboard(): ReturnType { + return useClipboard({ copiedDuring: COPIED_FEEDBACK_TIMEOUT }); +} diff --git a/app/stores/app.ts b/app/stores/app.ts index ba6db799..54753654 100644 --- a/app/stores/app.ts +++ b/app/stores/app.ts @@ -170,6 +170,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 + // oxlint-disable-next-line unicorn/consistent-function-scoping async function request( { schema, params }: { schema: JsonRpcSchema; params?: Record }, callbacks: RequestHandlers = {}, diff --git a/app/utils/color_picker.js b/app/utils/color_picker.js new file mode 100644 index 00000000..ef6316c7 --- /dev/null +++ b/app/utils/color_picker.js @@ -0,0 +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 || typeof colorText !== "string") { + return undefined; + } + + const numbersMatch = colorText.match(/[\d.]+%?/gu); + if (!numbersMatch || (numbersMatch.length !== 3 && numbersMatch.length !== 4)) { + return undefined; + } + + 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 (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)); + } + + 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(${roundRed}, ${roundGreen}, ${roundBlue})` + : `rgba(${roundRed}, ${roundGreen}, ${roundBlue}, ${alpha})`; +} + +export { parseColorString, formatColorString };