Initial commit: Open sourcing all of the Maple Open Technologies code.

This commit is contained in:
Bartlomiej Mika 2025-12-02 14:33:08 -05:00
commit 755d54a99d
2010 changed files with 448675 additions and 0 deletions

View file

@ -0,0 +1,153 @@
/**
* Color Utilities for Tag Visualization
*
* Provides utilities for blending multiple tag colors and creating
* visual indicators for collections with multiple tags.
*/
/**
* Convert hex color to RGB
* @param {string} hex - Hex color code (e.g., "#FF5733")
* @returns {{r: number, g: number, b: number}} RGB object
*/
function hexToRgb(hex) {
// Remove # if present
hex = hex.replace(/^#/, '');
// Parse hex values
const bigint = parseInt(hex, 16);
const r = (bigint >> 16) & 255;
const g = (bigint >> 8) & 255;
const b = bigint & 255;
return { r, g, b };
}
/**
* Convert RGB to hex color
* @param {number} r - Red (0-255)
* @param {number} g - Green (0-255)
* @param {number} b - Blue (0-255)
* @returns {string} Hex color code
*/
function rgbToHex(r, g, b) {
return "#" + ((1 << 24) + (Math.round(r) << 16) + (Math.round(g) << 8) + Math.round(b))
.toString(16)
.slice(1)
.toUpperCase();
}
/**
* Blend multiple colors using average RGB values
* @param {string[]} colors - Array of hex color codes
* @returns {string} Blended hex color
*/
export function blendColors(colors) {
if (!colors || colors.length === 0) {
return '#9CA3AF'; // Default gray color
}
if (colors.length === 1) {
return colors[0];
}
// Convert all colors to RGB
const rgbColors = colors.map(hexToRgb);
// Calculate average RGB values
const avgR = rgbColors.reduce((sum, c) => sum + c.r, 0) / rgbColors.length;
const avgG = rgbColors.reduce((sum, c) => sum + c.g, 0) / rgbColors.length;
const avgB = rgbColors.reduce((sum, c) => sum + c.b, 0) / rgbColors.length;
// Convert back to hex
return rgbToHex(avgR, avgG, avgB);
}
/**
* Create a gradient string from multiple colors
* @param {string[]} colors - Array of hex color codes
* @returns {string} CSS linear gradient string
*/
export function createGradientFromColors(colors) {
if (!colors || colors.length === 0) {
return 'linear-gradient(135deg, #9CA3AF, #6B7280)';
}
if (colors.length === 1) {
const color = colors[0];
// Create a subtle gradient with the same color (lighter to darker)
return `linear-gradient(135deg, ${color}E6, ${color})`;
}
if (colors.length === 2) {
return `linear-gradient(135deg, ${colors[0]}, ${colors[1]})`;
}
// For 3+ colors, create evenly spaced gradient stops
const stops = colors.map((color, index) => {
const percentage = (index / (colors.length - 1)) * 100;
return `${color} ${percentage}%`;
}).join(', ');
return `linear-gradient(135deg, ${stops})`;
}
/**
* Lighten a color by a percentage
* @param {string} color - Hex color code
* @param {number} percent - Percentage to lighten (0-100)
* @returns {string} Lightened hex color
*/
export function lightenColor(color, percent) {
const rgb = hexToRgb(color);
const factor = 1 + (percent / 100);
const r = Math.min(255, rgb.r * factor);
const g = Math.min(255, rgb.g * factor);
const b = Math.min(255, rgb.b * factor);
return rgbToHex(r, g, b);
}
/**
* Darken a color by a percentage
* @param {string} color - Hex color code
* @param {number} percent - Percentage to darken (0-100)
* @returns {string} Darkened hex color
*/
export function darkenColor(color, percent) {
const rgb = hexToRgb(color);
const factor = 1 - (percent / 100);
const r = Math.max(0, rgb.r * factor);
const g = Math.max(0, rgb.g * factor);
const b = Math.max(0, rgb.b * factor);
return rgbToHex(r, g, b);
}
/**
* Add opacity to a hex color (returns rgba)
* @param {string} color - Hex color code
* @param {number} opacity - Opacity (0-1)
* @returns {string} RGBA color string
*/
export function addOpacity(color, opacity) {
const rgb = hexToRgb(color);
return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${opacity})`;
}
/**
* Get contrasting text color (black or white) for a background color
* @param {string} bgColor - Background hex color
* @returns {string} '#000000' or '#FFFFFF'
*/
export function getContrastColor(bgColor) {
const rgb = hexToRgb(bgColor);
// Calculate relative luminance
const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255;
// Return black for light backgrounds, white for dark backgrounds
return luminance > 0.5 ? '#000000' : '#FFFFFF';
}

View file

@ -0,0 +1,168 @@
/**
* RFC 9457 Problem Details Parser
*
* This utility parses RFC 9457 "Problem Details for HTTP APIs" responses
* from the backend and converts them into a format suitable for display
* in React components.
*
* RFC 9457 Specification: https://www.rfc-editor.org/rfc/rfc9457.html
*/
/**
* Parse RFC 9457 Problem Detail from API response or pre-parsed JSON data
*
* @param {Response|Object} responseOrData - Fetch API response object or pre-parsed JSON data
* @returns {ParsedError|null} Parsed error object, or null if not RFC 9457 format
*
* @typedef {Object} ParsedError
* @property {Object<string, string>} fieldErrors - Field-specific error messages (key = field name, value = error message)
* @property {string} generalError - General error message to display
* @property {Object} problem - Full RFC 9457 problem detail for debugging
* @property {string} type - Problem type URI
* @property {number} status - HTTP status code
* @property {string} instance - Request path that caused the error
* @property {string} timestamp - ISO 8601 timestamp
* @property {string} traceId - Request trace ID for debugging
*/
export function parseRFC9457Error(responseOrData) {
try {
let problem;
// Check if this is already parsed JSON data (object) or a Response object
if (responseOrData && typeof responseOrData === 'object' && !responseOrData.json) {
// Already parsed JSON data
problem = responseOrData;
} else {
// This shouldn't happen in normal usage anymore, but handle it for safety
console.warn("[RFC9457Parser] Received Response object instead of parsed data");
return null;
}
// Check if this looks like an RFC 9457 problem detail
// RFC 9457 requires at least 'type' and 'status' fields
if (!problem.type && !problem.status && !problem.title && !problem.detail && !problem.errors) {
// Not an RFC 9457 format, return null to let caller handle legacy format
return null;
}
// All errors from our API are RFC 9457 format
return {
fieldErrors: problem.errors || {},
generalError: problem.detail || problem.title || "An error occurred",
problem: problem, // Full problem detail for debugging/logging
type: problem.type,
status: problem.status,
instance: problem.instance,
timestamp: problem.timestamp,
traceId: problem.trace_id,
};
} catch (err) {
// If parsing fails, return null to let caller handle it
console.error("[RFC9457Parser] Failed to parse error response:", err);
return null;
}
}
/**
* Check if HTTP response is an error
*
* @param {Response} response - Fetch API response object
* @returns {boolean} True if response indicates an error (status >= 400)
*/
export function isErrorResponse(response) {
return !response.ok && response.status >= 400;
}
/**
* Format field errors for display in forms
*
* Converts the fieldErrors object into an array suitable for mapping
* in React components.
*
* @param {Object<string, string>} fieldErrors - Field errors from parsed error
* @returns {Array<{field: string, message: string}>} Array of field error objects
*
* @example
* const fieldErrorArray = formatFieldErrors({
* email: "Email is required",
* password: "Password must be at least 8 characters"
* });
* // Returns: [
* // { field: "email", message: "Email is required" },
* // { field: "password", message: "Password must be at least 8 characters" }
* // ]
*/
export function formatFieldErrors(fieldErrors) {
return Object.entries(fieldErrors || {}).map(([field, message]) => ({
field,
message,
}));
}
/**
* Get user-friendly error title based on problem type
*
* @param {string} type - Problem type URI
* @returns {string} User-friendly title
*/
export function getProblemTypeTitle(type) {
if (!type) return "Error";
// Extract the last part of the URI
const parts = type.split("/");
const lastPart = parts[parts.length - 1];
// Convert kebab-case to Title Case
return lastPart
.split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
}
/**
* Log error details for debugging
*
* Logs the full RFC 9457 problem detail to console for debugging purposes.
* Should only be called in development mode.
*
* @param {ParsedError} parsedError - Parsed error from parseRFC9457Error
*/
export function logErrorDetails(parsedError) {
if (import.meta.env.DEV && parsedError.problem) {
console.group(
`[RFC9457] ${parsedError.problem.title} (${parsedError.status})`,
);
console.log("Type:", parsedError.type);
console.log("Detail:", parsedError.generalError);
console.log("Instance:", parsedError.instance);
console.log("Trace ID:", parsedError.traceId);
console.log("Timestamp:", parsedError.timestamp);
if (parsedError.fieldErrors && Object.keys(parsedError.fieldErrors).length > 0) {
console.log("Field Errors:", parsedError.fieldErrors);
}
console.log("Full Problem Detail:", parsedError.problem);
console.groupEnd();
}
}
/**
* Create a structured Error object from parsed RFC 9457 response
*
* This is useful for throwing errors that can be caught and displayed
* in error boundaries or catch blocks.
*
* @param {ParsedError} parsedError - Parsed error from parseRFC9457Error
* @returns {Error} Error object with additional properties
*/
export function createErrorFromProblem(parsedError) {
const error = new Error(parsedError.generalError);
error.fieldErrors = parsedError.fieldErrors;
error.problem = parsedError.problem;
error.status = parsedError.status;
error.type = parsedError.type;
error.instance = parsedError.instance;
error.traceId = parsedError.traceId;
return error;
}