bito-code-review[bot] commented on code in PR #37805:
URL: https://github.com/apache/superset/pull/37805#discussion_r2783139721


##########
superset/static/service-worker.js:
##########
@@ -1,27 +1,1471 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
+/*
+ * ATTENTION: An "eval-source-map" devtool has been used.
+ * This devtool is neither made for production nor for readable output files.
+ * It uses "eval()" calls to create a separate source file with attached 
SourceMaps in the browser devtools.
+ * If you are trying to read the output file, select a different devtool 
(https://webpack.js.org/configuration/devtool/)
+ * or disable the default devtool with "devtool: false".
+ * If you are looking for production-ready output files, see mode: 
"production" (https://webpack.js.org/configuration/mode/).
  */
+/******/ (() => { // webpackBootstrap
+/******/       "use strict";
+/******/       var __webpack_modules__ = ({
 
-// Minimal service worker for PWA file handling support
-self.addEventListener('install', event => {
-  event.waitUntil(self.skipWaiting());
-});
+/***/ "./src/service-worker.ts"
+/*!*******************************!*\
+  !*** ./src/service-worker.ts ***!
+  \*******************************/
+() {
 
-self.addEventListener('activate', event => {
-  event.waitUntil(self.clients.claim());
-});
+eval("{/**\n * Licensed to the Apache Software Foundation (ASF) under one\n * 
or more contributor license agreements.  See the NOTICE file\n * distributed 
with this work for additional information\n * regarding copyright ownership.  
The ASF licenses this file\n * to you under the Apache License, Version 2.0 
(the\n * \"License\"); you may not use this file except in compliance\n * with 
the License.  You may obtain a copy of the License at\n *\n *   
http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by 
applicable law or agreed to in writing,\n * software distributed under the 
License is distributed on an\n * \"AS IS\" BASIS, WITHOUT WARRANTIES OR 
CONDITIONS OF ANY\n * KIND, either express or implied.  See the License for 
the\n * specific language governing permissions and limitations\n * under the 
License.\n */ // Service Worker types (declared locally to avoid polluting 
global scope)\nself.addEventListener('install', (event)=>{\n    
event.waitUntil(self.skipWaiting())
 ;\n});\nself.addEventListener('activate', (event)=>{\n    
event.waitUntil(self.clients.claim());\n});\n\n//# sourceURL=[module]\n//# 
sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiLi9zcmMvc2VydmljZS13b3JrZXIudHMiLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7Ozs7Ozs7Ozs7O0FBaUJBO0FBWUE7QUFDQTtBQUNBO0FBRUE7QUFDQTtBQUNBO0FBRUEiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9zdXBlcnNldC8uL3NyYy9zZXJ2aWNlLXdvcmtlci50cz83ZjU4Il0sInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogTGljZW5zZWQgdG8gdGhlIEFwYWNoZSBTb2Z0d2FyZSBGb3VuZGF0aW9uIChBU0YpIHVuZGVyIG9uZVxuICogb3IgbW9yZSBjb250cmlidXRvciBsaWNlbnNlIGFncmVlbWVudHMuICBTZWUgdGhlIE5PVElDRSBmaWxlXG4gKiBkaXN0cmlidXRlZCB3aXRoIHRoaXMgd29yayBmb3IgYWRkaXRpb25hbCBpbmZvcm1hdGlvblxuICogcmVnYXJkaW5nIGNvcHlyaWdodCBvd25lcnNoaXAuICBUaGUgQVNGIGxpY2Vuc2VzIHRoaXMgZmlsZVxuICogdG8geW91IHVuZGVyIHRoZSBBcGFjaGUgTGljZW5zZSwgVmVyc2lvbiAyLjAgKHRoZVxuICogXCJMaWNlbnNlXCIpOyB5b3UgbWF5IG5vdCB1c2UgdGhpcyBmaWxlIGV4Y2VwdCBpbiBjb21wbGlhbmNlXG4gKiB3aXRoIHRoZSBMaWNlbnNlLiAgWW91IG1h
 
eSBvYnRhaW4gYSBjb3B5IG9mIHRoZSBMaWNlbnNlIGF0XG4gKlxuICogICBodHRwOi8vd3d3LmFwYWNoZS5vcmcvbGljZW5zZXMvTElDRU5TRS0yLjBcbiAqXG4gKiBVbmxlc3MgcmVxdWlyZWQgYnkgYXBwbGljYWJsZSBsYXcgb3IgYWdyZWVkIHRvIGluIHdyaXRpbmcsXG4gKiBzb2Z0d2FyZSBkaXN0cmlidXRlZCB1bmRlciB0aGUgTGljZW5zZSBpcyBkaXN0cmlidXRlZCBvbiBhblxuICogXCJBUyBJU1wiIEJBU0lTLCBXSVRIT1VUIFdBUlJBTlRJRVMgT1IgQ09ORElUSU9OUyBPRiBBTllcbiAqIEtJTkQsIGVpdGhlciBleHByZXNzIG9yIGltcGxpZWQuICBTZWUgdGhlIExpY2Vuc2UgZm9yIHRoZVxuICogc3BlY2lmaWMgbGFuZ3VhZ2UgZ292ZXJuaW5nIHBlcm1pc3Npb25zIGFuZCBsaW1pdGF0aW9uc1xuICogdW5kZXIgdGhlIExpY2Vuc2UuXG4gKi9cblxuLy8gU2VydmljZSBXb3JrZXIgdHlwZXMgKGRlY2xhcmVkIGxvY2FsbHkgdG8gYXZvaWQgcG9sbHV0aW5nIGdsb2JhbCBzY29wZSlcbmRlY2xhcmUgY29uc3Qgc2VsZjoge1xuICBza2lwV2FpdGluZygpOiBQcm9taXNlPHZvaWQ+O1xuICBjbGllbnRzOiB7IGNsYWltKCk6IFByb21pc2U8dm9pZD4gfTtcbiAgYWRkRXZlbnRMaXN0ZW5lcihcbiAgICB0eXBlOiAnaW5zdGFsbCcgfCAnYWN0aXZhdGUnLFxuICAgIGxpc3RlbmVyOiAoZXZlbnQ6IHsgd2FpdFVudGlsKHByb21pc2U6IFByb21pc2U8dW5rbm93bj4pOiB2b2lkIH0pID0+IHZvaWQsXG4gICk6IHZva
 
WQ7XG59O1xuXG5zZWxmLmFkZEV2ZW50TGlzdGVuZXIoJ2luc3RhbGwnLCBldmVudCA9PiB7XG4gIGV2ZW50LndhaXRVbnRpbChzZWxmLnNraXBXYWl0aW5nKCkpO1xufSk7XG5cbnNlbGYuYWRkRXZlbnRMaXN0ZW5lcignYWN0aXZhdGUnLCBldmVudCA9PiB7XG4gIGV2ZW50LndhaXRVbnRpbChzZWxmLmNsaWVudHMuY2xhaW0oKSk7XG59KTtcblxuZXhwb3J0IHt9O1xuIl0sIm5hbWVzIjpbXSwiaWdub3JlTGlzdCI6W10sInNvdXJjZVJvb3QiOiIifQ==\n//#
 sourceURL=webpack-internal:///./src/service-worker.ts\n\n}");
+
+/***/ }
+
+/******/       });
+/************************************************************************/
+/******/       // The module cache
+/******/       var __webpack_module_cache__ = {};
+/******/
+/******/       // The require function
+/******/       function __webpack_require__(moduleId) {
+/******/               // Check if module is in cache
+/******/               var cachedModule = __webpack_module_cache__[moduleId];
+/******/               if (cachedModule !== undefined) {
+/******/                       return cachedModule.exports;
+/******/               }
+/******/               // Check if module exists (development only)
+/******/               if (__webpack_modules__[moduleId] === undefined) {
+/******/                       var e = new Error("Cannot find module '" + 
moduleId + "'");
+/******/                       e.code = 'MODULE_NOT_FOUND';
+/******/                       throw e;
+/******/               }
+/******/               // Create a new module (and put it into the cache)
+/******/               var module = __webpack_module_cache__[moduleId] = {
+/******/                       id: moduleId,
+/******/                       loaded: false,
+/******/                       exports: {}
+/******/               };
+/******/
+/******/               // Execute the module function
+/******/               var execOptions = { id: moduleId, module: module, 
factory: __webpack_modules__[moduleId], require: __webpack_require__ };
+/******/               __webpack_require__.i.forEach(function(handler) { 
handler(execOptions); });
+/******/               module = execOptions.module;
+/******/               execOptions.factory.call(module.exports, module, 
module.exports, execOptions.require);
+/******/
+/******/               // Flag the module as loaded
+/******/               module.loaded = true;
+/******/
+/******/               // Return the exports of the module
+/******/               return module.exports;
+/******/       }
+/******/
+/******/       // expose the modules object (__webpack_modules__)
+/******/       __webpack_require__.m = __webpack_modules__;
+/******/
+/******/       // expose the module cache
+/******/       __webpack_require__.c = __webpack_module_cache__;
+/******/
+/******/       // expose the module execution interceptor
+/******/       __webpack_require__.i = [];
+/******/
+/************************************************************************/
+/******/       /* webpack/runtime/chunk loaded */
+/******/       (() => {
+/******/               var deferred = [];
+/******/               __webpack_require__.O = (result, chunkIds, fn, 
priority) => {
+/******/                       if(chunkIds) {
+/******/                               priority = priority || 0;
+/******/                               for(var i = deferred.length; i > 0 && 
deferred[i - 1][2] > priority; i--) deferred[i] = deferred[i - 1];
+/******/                               deferred[i] = [chunkIds, fn, priority];
+/******/                               return;
+/******/                       }
+/******/                       var notFulfilled = Infinity;
+/******/                       for (var i = 0; i < deferred.length; i++) {
+/******/                               var [chunkIds, fn, priority] = 
deferred[i];
+/******/                               var fulfilled = true;
+/******/                               for (var j = 0; j < chunkIds.length; 
j++) {
+/******/                                       if ((priority & 1 === 0 || 
notFulfilled >= priority) && Object.keys(__webpack_require__.O).every((key) => 
(__webpack_require__.O[key](chunkIds[j])))) {
+/******/                                               chunkIds.splice(j--, 1);
+/******/                                       } else {
+/******/                                               fulfilled = false;
+/******/                                               if(priority < 
notFulfilled) notFulfilled = priority;
+/******/                                       }
+/******/                               }
+/******/                               if(fulfilled) {
+/******/                                       deferred.splice(i--, 1)
+/******/                                       var r = fn();
+/******/                                       if (r !== undefined) result = r;
+/******/                               }
+/******/                       }
+/******/                       return result;
+/******/               };
+/******/       })();
+/******/
+/******/       /* webpack/runtime/compat get default export */
+/******/       (() => {
+/******/               // getDefaultExport function for compatibility with 
non-harmony modules
+/******/               __webpack_require__.n = (module) => {
+/******/                       var getter = module && module.__esModule ?
+/******/                               () => (module['default']) :
+/******/                               () => (module);
+/******/                       __webpack_require__.d(getter, { a: getter });
+/******/                       return getter;
+/******/               };
+/******/       })();
+/******/
+/******/       /* webpack/runtime/create fake namespace object */
+/******/       (() => {
+/******/               var getProto = Object.getPrototypeOf ? (obj) => 
(Object.getPrototypeOf(obj)) : (obj) => (obj.__proto__);
+/******/               var leafPrototypes;
+/******/               // create a fake namespace object
+/******/               // mode & 1: value is a module id, require it
+/******/               // mode & 2: merge all properties of value into the ns
+/******/               // mode & 4: return value when already ns object
+/******/               // mode & 16: return value when it's Promise-like
+/******/               // mode & 8|1: behave like require
+/******/               __webpack_require__.t = function(value, mode) {
+/******/                       if(mode & 1) value = this(value);
+/******/                       if(mode & 8) return value;
+/******/                       if(typeof value === 'object' && value) {
+/******/                               if((mode & 4) && value.__esModule) 
return value;
+/******/                               if((mode & 16) && typeof value.then === 
'function') return value;
+/******/                       }
+/******/                       var ns = Object.create(null);
+/******/                       __webpack_require__.r(ns);
+/******/                       var def = {};
+/******/                       leafPrototypes = leafPrototypes || [null, 
getProto({}), getProto([]), getProto(getProto)];
+/******/                       for(var current = mode & 2 && value; (typeof 
current == 'object' || typeof current == 'function') && 
!~leafPrototypes.indexOf(current); current = getProto(current)) {
+/******/                               
Object.getOwnPropertyNames(current).forEach((key) => (def[key] = () => 
(value[key])));
+/******/                       }
+/******/                       def['default'] = () => (value);
+/******/                       __webpack_require__.d(ns, def);
+/******/                       return ns;
+/******/               };
+/******/       })();
+/******/
+/******/       /* webpack/runtime/define property getters */
+/******/       (() => {
+/******/               // define getter functions for harmony exports
+/******/               __webpack_require__.d = (exports, definition) => {
+/******/                       for(var key in definition) {
+/******/                               if(__webpack_require__.o(definition, 
key) && !__webpack_require__.o(exports, key)) {
+/******/                                       Object.defineProperty(exports, 
key, { enumerable: true, get: definition[key] });
+/******/                               }
+/******/                       }
+/******/               };
+/******/       })();
+/******/
+/******/       /* webpack/runtime/get javascript update chunk filename */
+/******/       (() => {
+/******/               // This function allow to reference all chunks
+/******/               __webpack_require__.hu = (chunkId) => {
+/******/                       // return url for filenames based on template
+/******/                       return "" + chunkId + "." + 
__webpack_require__.h() + ".hot-update.js";
+/******/               };
+/******/       })();
+/******/
+/******/       /* webpack/runtime/get update manifest filename */
+/******/       (() => {
+/******/               __webpack_require__.hmrF = () => ("service-worker." + 
__webpack_require__.h() + ".hot-update.json");
+/******/       })();
+/******/
+/******/       /* webpack/runtime/getFullHash */
+/******/       (() => {
+/******/               __webpack_require__.h = () => ("f4461569ed3749bcd919")
+/******/       })();
+/******/
+/******/       /* webpack/runtime/harmony module decorator */
+/******/       (() => {
+/******/               __webpack_require__.hmd = (module) => {
+/******/                       module = Object.create(module);
+/******/                       if (!module.children) module.children = [];
+/******/                       Object.defineProperty(module, 'exports', {
+/******/                               enumerable: true,
+/******/                               set: () => {
+/******/                                       throw new Error('ES Modules may 
not assign module.exports or exports.*, Use ESM export syntax, instead: ' + 
module.id);
+/******/                               }
+/******/                       });
+/******/                       return module;
+/******/               };
+/******/       })();
+/******/
+/******/       /* webpack/runtime/hasOwnProperty shorthand */
+/******/       (() => {
+/******/               __webpack_require__.o = (obj, prop) => 
(Object.prototype.hasOwnProperty.call(obj, prop))
+/******/       })();
+/******/
+/******/       /* webpack/runtime/load script */
+/******/       (() => {
+/******/               var inProgress = {};
+/******/               var dataWebpackPrefix = "superset:";
+/******/               // loadScript function to load a script via script tag
+/******/               __webpack_require__.l = (url, done, key, chunkId) => {
+/******/                       if(inProgress[url]) { 
inProgress[url].push(done); return; }
+/******/                       var script, needAttach;
+/******/                       if(key !== undefined) {
+/******/                               var scripts = 
document.getElementsByTagName("script");
+/******/                               for(var i = 0; i < scripts.length; i++) 
{
+/******/                                       var s = scripts[i];
+/******/                                       if(s.getAttribute("src") == url 
|| s.getAttribute("data-webpack") == dataWebpackPrefix + key) { script = s; 
break; }
+/******/                               }
+/******/                       }
+/******/                       if(!script) {
+/******/                               needAttach = true;
+/******/                               script = 
document.createElement('script');
+/******/
+/******/                               script.charset = 'utf-8';
+/******/                               if (__webpack_require__.nc) {
+/******/                                       script.setAttribute("nonce", 
__webpack_require__.nc);

Review Comment:
   <div>
   
   
   <div id="suggestion">
   <div id="issue"><b>Production Build Uses Dev Devtool</b></div>
   <div id="fix">
   
   The service-worker.js is now built with webpack using 'eval-source-map' 
devtool, which is intended for development only. This exposes source code via 
eval() and increases bundle size, unsuitable for production. Consider using 
'source-map' for production debugging or disabling devtool for service workers.
   </div>
   
   
   </div>
   
   
   
   
   <small><i>Code Review Run #995e5e</i></small>
   </div>
   
   ---
   Should Bito avoid suggestions like this for future reviews? (<a 
href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
   - [ ] Yes, avoid them



##########
superset-frontend/src/theme/accessibility.ts:
##########
@@ -0,0 +1,542 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * WCAG 2.1 Accessibility utilities for theme contrast analysis.
+ *
+ * This module provides functions to analyze theme configurations for
+ * accessibility compliance, specifically contrast ratios between
+ * text and background colors.
+ *
+ * @see https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html
+ */
+
+// eslint-disable-next-line no-restricted-syntax
+import { theme as antdTheme } from 'antd';
+import type { ThemeConfig } from 'antd';
+
+/**
+ * WCAG contrast ratio requirements
+ */
+export const WCAG_REQUIREMENTS = {
+  AA_NORMAL_TEXT: 4.5,
+  AA_LARGE_TEXT: 3.0,
+  AAA_NORMAL_TEXT: 7.0,
+  AAA_LARGE_TEXT: 4.5,
+} as const;
+
+/**
+ * Contrast issue severity levels
+ */
+export type ContrastSeverity = 'error' | 'warning';
+
+/**
+ * Represents a single contrast issue found during analysis
+ */
+export interface ContrastIssue {
+  /** Token name of the foreground color (e.g., 'colorText') */
+  foreground: string;
+  /** Token name of the background color (e.g., 'colorBgBase') */
+  background: string;
+  /** Actual hex color value of the foreground */
+  foregroundColor: string;
+  /** Actual hex color value of the background */
+  backgroundColor: string;
+  /** Calculated contrast ratio */
+  ratio: number;
+  /** Required ratio for AA compliance */
+  required: number;
+  /** Severity: error (fails AA) or warning (fails AAA) */
+  severity: ContrastSeverity;
+  /** Human-readable description of the issue */
+  description: string;
+}
+
+/**
+ * WCAG compliance level
+ */
+export type WCAGLevel = 'AAA' | 'AA' | 'A' | 'Fail';
+
+/**
+ * Complete accessibility analysis result
+ */
+export interface AccessibilityAnalysis {
+  /** Overall score from 0-100 */
+  score: number;
+  /** WCAG compliance level achieved */
+  level: WCAGLevel;
+  /** List of contrast issues found */
+  issues: ContrastIssue[];
+  /** Number of checks that passed AA requirements */
+  passedChecks: number;
+  /** Total number of contrast checks performed */
+  totalChecks: number;
+}
+
+/**
+ * Contrast pairs to check in theme analysis.
+ * Each pair represents a foreground/background color combination
+ * that should meet WCAG contrast requirements.
+ */
+const CONTRAST_PAIRS: Array<{
+  foreground: string;
+  background: string;
+  description: string;
+  isLargeText?: boolean;
+}> = [
+  {
+    foreground: 'colorText',
+    background: 'colorBgBase',
+    description: 'Primary text on base background',
+  },
+  {
+    foreground: 'colorText',
+    background: 'colorBgContainer',
+    description: 'Primary text on container background',
+  },
+  {
+    foreground: 'colorTextSecondary',
+    background: 'colorBgBase',
+    description: 'Secondary text on base background',
+  },
+  {
+    foreground: 'colorTextSecondary',
+    background: 'colorBgContainer',
+    description: 'Secondary text on container background',
+  },
+  {
+    foreground: 'colorTextDescription',
+    background: 'colorBgBase',
+    description: 'Description text on base background',
+  },
+  {
+    foreground: 'colorPrimary',
+    background: 'colorBgBase',
+    description: 'Primary color (buttons/links) on base background',
+  },
+  {
+    foreground: 'colorPrimary',
+    background: 'colorBgContainer',
+    description: 'Primary color on container background',
+  },
+  {
+    foreground: 'colorError',
+    background: 'colorBgBase',
+    description: 'Error text on base background',
+  },
+  {
+    foreground: 'colorError',
+    background: 'colorBgContainer',
+    description: 'Error text on container background',
+  },
+  {
+    foreground: 'colorWarning',
+    background: 'colorBgBase',
+    description: 'Warning text on base background',
+  },
+  {
+    foreground: 'colorSuccess',
+    background: 'colorBgBase',
+    description: 'Success text on base background',
+  },
+  {
+    foreground: 'colorLink',
+    background: 'colorBgBase',
+    description: 'Link text on base background',
+  },
+  {
+    foreground: 'colorTextHeading',
+    background: 'colorBgBase',
+    description: 'Heading text on base background',
+    isLargeText: true,
+  },
+];
+
+/**
+ * Converts a hex color string to RGB values (0-255).
+ * Supports 3-char, 6-char, and 8-char (with alpha) hex formats.
+ */
+function hexToRgb(hex: string): [number, number, number] | null {
+  const cleanHex = hex.replace(/^#/, '');
+
+  let fullHex = cleanHex;
+
+  // Handle shorthand (#fff -> #ffffff)
+  if (cleanHex.length === 3) {
+    fullHex = cleanHex
+      .split('')
+      .map(c => c + c)
+      .join('');
+  }
+
+  // Handle 8-char hex (with alpha) - just take the first 6 chars
+  if (fullHex.length === 8) {
+    fullHex = fullHex.substring(0, 6);
+  }
+
+  if (fullHex.length !== 6) {
+    return null;
+  }
+
+  const r = parseInt(fullHex.substring(0, 2), 16);
+  const g = parseInt(fullHex.substring(2, 4), 16);
+  const b = parseInt(fullHex.substring(4, 6), 16);
+
+  if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) {
+    return null;
+  }
+
+  return [r, g, b];
+}
+
+/**
+ * Parses an rgba() color string to RGB values.
+ */
+function rgbaToRgb(rgba: string): [number, number, number] | null {
+  const match = rgba.match(
+    /rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*[\d.]+\s*)?\)/,
+  );
+  if (!match) return null;
+  return [
+    parseInt(match[1], 10),
+    parseInt(match[2], 10),
+    parseInt(match[3], 10),
+  ];
+}
+
+/**
+ * Converts any color format to a normalized hex string.
+ * Handles hex (3, 6, or 8 char), rgb(), and rgba() formats.
+ */
+function normalizeColor(color: string): string | null {
+  if (!color || typeof color !== 'string') return null;
+
+  const trimmed = color.trim();
+
+  // Handle hex colors
+  if (trimmed.startsWith('#')) {
+    const rgb = hexToRgb(trimmed);
+    if (!rgb) return null;
+    return `#${rgb.map(c => c.toString(16).padStart(2, '0')).join('')}`;
+  }
+
+  // Handle rgb/rgba colors
+  if (trimmed.startsWith('rgb')) {
+    const rgb = rgbaToRgb(trimmed);
+    if (!rgb) return null;
+    return `#${rgb.map(c => c.toString(16).padStart(2, '0')).join('')}`;
+  }
+
+  return null;
+}
+
+/**
+ * Converts algorithm string to Ant Design algorithm function(s).
+ */
+function getAlgorithm(
+  algorithmConfig: string | string[] | undefined,
+): ThemeConfig['algorithm'] {
+  const { darkAlgorithm, compactAlgorithm, defaultAlgorithm } = antdTheme;
+
+  if (!algorithmConfig) {
+    return defaultAlgorithm;
+  }
+
+  const algorithms = Array.isArray(algorithmConfig)
+    ? algorithmConfig
+    : [algorithmConfig];
+
+  const algorithmFns = algorithms
+    .map(alg => {
+      switch (alg) {
+        case 'dark':
+          return darkAlgorithm;
+        case 'compact':
+          return compactAlgorithm;
+        default:
+          return defaultAlgorithm;
+      }
+    })
+    .filter(Boolean);
+
+  return algorithmFns.length === 1 ? algorithmFns[0] : algorithmFns;
+}
+
+/**
+ * Flattens nested color tokens from custom theme structures.
+ * Handles formats like { neutrals: { colorText: '#000' }, brand: { 
colorPrimary: '#00f' } }
+ */
+function flattenColorTokens(
+  themeConfig: Record<string, unknown>,
+): Record<string, unknown> {
+  const flattened: Record<string, unknown> = {};
+
+  // First, add any root-level color tokens
+  for (const [key, value] of Object.entries(themeConfig)) {
+    if (key.startsWith('color') && typeof value === 'string') {
+      flattened[key] = value;
+    }
+  }
+
+  // Then, flatten nested objects that might contain color tokens
+  for (const [, value] of Object.entries(themeConfig)) {
+    if (value && typeof value === 'object' && !Array.isArray(value)) {
+      const nestedObj = value as Record<string, unknown>;
+      for (const [nestedKey, nestedValue] of Object.entries(nestedObj)) {
+        if (nestedKey.startsWith('color') && typeof nestedValue === 'string') {
+          // Don't overwrite if already exists (root takes precedence)
+          if (!(nestedKey in flattened)) {
+            flattened[nestedKey] = nestedValue;
+          }
+        }
+      }
+    }
+  }
+
+  return flattened;
+}
+
+/**
+ * Resolves a theme configuration to computed design tokens using Ant Design.
+ * This allows us to analyze themes that only define seed colors.
+ * Also handles custom nested structures by flattening color tokens first.
+ */
+function resolveThemeTokens(
+  themeConfig: Record<string, unknown>,
+): Record<string, unknown> {
+  // First, flatten any custom nested color structures
+  const flattenedColors = flattenColorTokens(themeConfig);
+
+  try {
+    // Build Ant Design ThemeConfig from the user's config
+    // Merge flattened colors with existing token object
+    const tokenConfig = {
+      ...(themeConfig.token as Record<string, unknown>),
+      ...flattenedColors,
+    };
+
+    const antdConfig: ThemeConfig = {
+      token: tokenConfig as ThemeConfig['token'],
+      algorithm: getAlgorithm(themeConfig.algorithm as string | string[]),
+    };
+
+    // Use Ant Design's getDesignToken to compute all tokens
+    const resolvedTokens = antdTheme.getDesignToken(antdConfig);
+
+    // Merge flattened colors back in case Ant Design didn't recognize them
+    return {
+      ...(resolvedTokens as unknown as Record<string, unknown>),
+      ...flattenedColors,
+    };
+  } catch {
+    // If resolution fails, return the flattened colors plus original token 
object
+    return {
+      ...(themeConfig.token as Record<string, unknown>),
+      ...flattenedColors,
+    };
+  }
+}
+
+/**
+ * Calculates relative luminance per WCAG 2.1 specification.
+ * @see https://www.w3.org/WAI/GL/wiki/Relative_luminance
+ */
+export function getLuminance(r: number, g: number, b: number): number {
+  const [rs, gs, bs] = [r, g, b].map(c => {
+    const sRGB = c / 255;
+    return sRGB <= 0.03928 ? sRGB / 12.92 : ((sRGB + 0.055) / 1.055) ** 2.4;
+  });
+  return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
+}
+
+/**
+ * Calculates WCAG contrast ratio between two colors.
+ * @see https://www.w3.org/WAI/GL/wiki/Contrast_ratio
+ *
+ * @param color1 - First color in hex format
+ * @param color2 - Second color in hex format
+ * @returns Contrast ratio (1 to 21), or 0 if colors are invalid
+ */
+export function getContrastRatio(color1: string, color2: string): number {
+  const rgb1 = hexToRgb(color1);
+  const rgb2 = hexToRgb(color2);
+
+  if (!rgb1 || !rgb2) {
+    return 0;
+  }
+
+  const l1 = getLuminance(rgb1[0], rgb1[1], rgb1[2]);
+  const l2 = getLuminance(rgb2[0], rgb2[1], rgb2[2]);
+
+  const lighter = Math.max(l1, l2);
+  const darker = Math.min(l1, l2);
+
+  return (lighter + 0.05) / (darker + 0.05);
+}
+
+/**
+ * Checks if a contrast ratio meets WCAG requirements.
+ *
+ * @param ratio - The contrast ratio to check
+ * @param level - WCAG level ('AA' or 'AAA')
+ * @param isLargeText - Whether the text is large (14pt bold or 18pt+)
+ * @returns True if the ratio meets requirements
+ */
+export function meetsWCAG(
+  ratio: number,
+  level: 'AA' | 'AAA',
+  isLargeText = false,
+): boolean {
+  if (level === 'AAA') {
+    return ratio >= (isLargeText ? 4.5 : 7.0);
+  }
+  return ratio >= (isLargeText ? 3.0 : 4.5);
+}
+
+/**
+ * Gets a color value from resolved tokens.
+ * Normalizes the color to hex format.
+ */
+function getColorFromTokens(
+  tokens: Record<string, unknown>,
+  tokenName: string,
+): string | null {
+  const value = tokens[tokenName];
+  if (typeof value === 'string') {
+    return normalizeColor(value);
+  }
+  return null;
+}
+
+/**
+ * Checks if a string looks like a valid hex color.
+ */
+function isValidHexColor(color: string | null): color is string {
+  if (!color) return false;
+  return /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/.test(color);
+}
+
+/**
+ * Analyzes a theme configuration for WCAG accessibility compliance.
+ * Uses Ant Design's getDesignToken() to resolve computed colors,
+ * allowing analysis even when only seed colors are defined.
+ *
+ * @param themeConfig - The theme configuration object to analyze
+ * @returns Complete accessibility analysis with score, level, and issues
+ */
+export function analyzeThemeAccessibility(
+  themeConfig: Record<string, unknown>,
+): AccessibilityAnalysis {
+  // Resolve theme tokens using Ant Design's algorithm
+  const resolvedTokens = resolveThemeTokens(themeConfig);
+
+  const issues: ContrastIssue[] = [];
+  let passedChecks = 0;
+  let totalChecks = 0;
+  let passedAAA = 0;
+
+  for (const pair of CONTRAST_PAIRS) {
+    const fgColor = getColorFromTokens(resolvedTokens, pair.foreground);
+    const bgColor = getColorFromTokens(resolvedTokens, pair.background);
+
+    // Skip if colors are not defined or invalid
+    if (!isValidHexColor(fgColor) || !isValidHexColor(bgColor)) {
+      continue;
+    }
+
+    totalChecks += 1;
+    const ratio = getContrastRatio(fgColor, bgColor);
+    const requiredAA = pair.isLargeText
+      ? WCAG_REQUIREMENTS.AA_LARGE_TEXT
+      : WCAG_REQUIREMENTS.AA_NORMAL_TEXT;
+    const requiredAAA = pair.isLargeText
+      ? WCAG_REQUIREMENTS.AAA_LARGE_TEXT
+      : WCAG_REQUIREMENTS.AAA_NORMAL_TEXT;
+
+    if (ratio >= requiredAA) {
+      passedChecks += 1;
+      if (ratio >= requiredAAA) {
+        passedAAA += 1;
+      }
+    } else {
+      issues.push({
+        foreground: pair.foreground,
+        background: pair.background,
+        foregroundColor: fgColor,
+        backgroundColor: bgColor,
+        ratio: Math.round(ratio * 100) / 100,
+        required: requiredAA,
+        severity: 'error',
+        description: pair.description,
+      });
+    }

Review Comment:
   <div>
   
   
   <div id="suggestion">
   <div id="issue"><b>Missing AAA failure warnings</b></div>
   <div id="fix">
   
   Also report a warning when the contrast ratio passes AA but fails AAA to 
ensure full WCAG level coverage.
   </div>
   
   
   </div>
   
   
   
   
   <small><i>Code Review Run #995e5e</i></small>
   </div>
   
   ---
   Should Bito avoid suggestions like this for future reviews? (<a 
href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
   - [ ] Yes, avoid them



##########
superset-frontend/src/features/themes/AccessibilityScore.tsx:
##########
@@ -0,0 +1,534 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { useMemo, useState, useCallback } from 'react';
+import { t } from '@apache-superset/core';
+import { css, styled, useTheme } from '@apache-superset/core/ui';
+import {
+  Progress,
+  Collapse,
+  Tooltip,
+  Button,
+} from '@superset-ui/core/components';
+import { Icons } from '@superset-ui/core/components/Icons';
+import { Typography } from '@superset-ui/core/components/Typography';
+import {
+  analyzeThemeAccessibility,
+  formatContrastRatio,
+  getScoreColor,
+  type AccessibilityAnalysis,
+  type ContrastIssue,
+  WCAG_REQUIREMENTS,
+} from 'src/theme/accessibility';
+
+interface AccessibilityScoreProps {
+  /** JSON string of the theme configuration */
+  themeJson: string | undefined;
+}
+
+const StyledContainer = styled.div`
+  ${({ theme }) => css`
+    margin-top: ${theme.sizeUnit * 4}px;
+    padding: ${theme.sizeUnit * 3}px;
+    background: ${theme.colorBgContainer};
+    border: 1px solid ${theme.colorBorderSecondary};
+    border-radius: ${theme.borderRadius}px;
+  `}
+`;
+
+const StyledHeader = styled.div`
+  ${({ theme }) => css`
+    display: flex;
+    align-items: center;
+    gap: ${theme.sizeUnit * 2}px;
+    margin-bottom: ${theme.sizeUnit * 2}px;
+
+    .header-icon {
+      font-size: ${theme.fontSizeLG}px;
+    }
+
+    .score-text {
+      font-weight: ${theme.fontWeightStrong};
+    }
+
+    .level-badge {
+      padding: 0 ${theme.sizeUnit}px;
+      border-radius: ${theme.borderRadiusSM}px;
+      font-size: ${theme.fontSizeSM}px;
+      font-weight: ${theme.fontWeightStrong};
+
+      &.level-aaa {
+        background: ${theme.colorSuccessBg};
+        color: ${theme.colorSuccess};
+      }
+      &.level-aa {
+        background: ${theme.colorSuccessBg};
+        color: ${theme.colorSuccess};
+      }
+      &.level-a {
+        background: ${theme.colorWarningBg};
+        color: ${theme.colorWarning};
+      }
+      &.level-fail {
+        background: ${theme.colorErrorBg};
+        color: ${theme.colorError};
+      }
+    }
+  `}
+`;
+
+const StyledProgressWrapper = styled.div`
+  ${({ theme }) => css`
+    margin-bottom: ${theme.sizeUnit * 2}px;
+  `}
+`;
+
+const StyledIssuesList = styled.div`
+  ${({ theme }) => css`
+    .issue-item {
+      display: flex;
+      align-items: flex-start;
+      gap: ${theme.sizeUnit}px;
+      padding: ${theme.sizeUnit}px 0;
+      font-size: ${theme.fontSizeSM}px;
+
+      &:not(:last-child) {
+        border-bottom: 1px solid ${theme.colorBorderSecondary};
+      }
+
+      .issue-icon {
+        flex-shrink: 0;
+        margin-top: 2px;
+      }
+
+      .issue-icon-error {
+        color: ${theme.colorError};
+      }
+
+      .issue-icon-warning {
+        color: ${theme.colorWarning};
+      }
+
+      .issue-content {
+        flex: 1;
+      }
+
+      .issue-tokens {
+        font-family: ${theme.fontFamilyCode};
+        color: ${theme.colorTextSecondary};
+      }
+
+      .issue-ratio {
+        font-weight: ${theme.fontWeightStrong};
+        color: ${theme.colorError};
+      }
+
+      .issue-required {
+        color: ${theme.colorTextTertiary};
+      }
+
+      .color-swatch {
+        display: inline-block;
+        width: 12px;
+        height: 12px;
+        border-radius: 2px;
+        border: 1px solid ${theme.colorBorder};
+        vertical-align: middle;
+        margin: 0 2px;
+      }
+    }
+
+    .ant-collapse {
+      background: transparent;
+      border: none;
+    }
+
+    .ant-collapse-item {
+      border: none !important;
+    }
+
+    .ant-collapse-header {
+      padding: ${theme.sizeUnit}px 0 !important;
+      color: ${theme.colorText} !important;
+    }
+
+    .ant-collapse-content {
+      border: none !important;
+    }
+
+    .ant-collapse-content-box {
+      padding: 0 !important;
+    }
+  `}
+`;
+
+const StyledEmptyState = styled.div`
+  ${({ theme }) => css`
+    text-align: center;
+    color: ${theme.colorTextSecondary};
+    padding: ${theme.sizeUnit * 2}px;
+  `}
+`;
+
+/**
+ * Renders a single contrast issue item
+ */
+function IssueItem({ issue }: { issue: ContrastIssue }) {
+  const IconComponent =
+    issue.severity === 'error'
+      ? Icons.CloseCircleOutlined
+      : Icons.ExclamationCircleOutlined;
+
+  return (
+    <div className="issue-item">
+      <IconComponent
+        className={`issue-icon issue-icon-${issue.severity}`}
+        iconSize="s"
+      />
+      <div className="issue-content">
+        <div>
+          <span className="issue-tokens">
+            {issue.foreground}
+            <span
+              className="color-swatch"
+              style={{ backgroundColor: issue.foregroundColor }}
+              title={issue.foregroundColor}
+            />
+          </span>
+          {' vs '}
+          <span className="issue-tokens">
+            {issue.background}
+            <span
+              className="color-swatch"
+              style={{ backgroundColor: issue.backgroundColor }}
+              title={issue.backgroundColor}
+            />
+          </span>
+        </div>
+        <div>
+          <span className="issue-ratio">
+            {formatContrastRatio(issue.ratio)}
+          </span>
+          <span className="issue-required">
+            {' '}
+            ({t('need')} {formatContrastRatio(issue.required)})
+          </span>
+        </div>
+        <Typography.Text type="secondary">{issue.description}</Typography.Text>
+      </div>
+    </div>
+  );
+}
+
+/**
+ * Checks if a key looks like a color token.
+ */
+function isColorKey(key: string): boolean {
+  return (
+    key.startsWith('color') ||
+    key.startsWith('colorBg') ||
+    key.startsWith('colorText')
+  );
+}
+
+/**
+ * Checks if a theme config has any meaningful color tokens defined.
+ * Returns false for empty objects or objects with no color-related tokens.
+ * Searches recursively through nested objects.
+ */
+function hasColorTokens(themeConfig: Record<string, unknown>): boolean {
+  // Check if there's a token object with any color-related keys
+  const token = themeConfig.token as Record<string, unknown> | undefined;
+  if (token && typeof token === 'object') {
+    const colorKeys = Object.keys(token).filter(isColorKey);
+    if (colorKeys.length > 0) {
+      return true;
+    }
+  }
+
+  // Check root level for color keys
+  const rootColorKeys = Object.keys(themeConfig).filter(isColorKey);
+  if (rootColorKeys.length > 0) {
+    return true;
+  }
+
+  // Check nested objects (e.g., neutrals, brand, semantic)
+  for (const value of Object.values(themeConfig)) {
+    if (value && typeof value === 'object' && !Array.isArray(value)) {
+      const nestedObj = value as Record<string, unknown>;
+      const nestedColorKeys = Object.keys(nestedObj).filter(isColorKey);
+      if (nestedColorKeys.length > 0) {
+        return true;
+      }
+    }
+  }
+
+  return false;
+}
+
+export interface ThemeAnalysisResult {
+  analysis: AccessibilityAnalysis | null;
+  hasColors: boolean;
+  isValidJson: boolean;
+  runAnalysis: () => void;
+  resetAnalysis: () => void;
+}
+
+/**
+ * Parses theme JSON synchronously (for immediate validation).
+ */
+function parseThemeJson(themeJson: string | undefined): {
+  themeConfig: Record<string, unknown> | null;
+  hasColors: boolean;
+  isValidJson: boolean;
+} {
+  if (!themeJson || themeJson.trim() === '') {
+    return { themeConfig: null, hasColors: false, isValidJson: false };
+  }
+
+  try {
+    const themeConfig = JSON.parse(themeJson);
+    if (typeof themeConfig !== 'object' || themeConfig === null) {
+      return { themeConfig: null, hasColors: false, isValidJson: false };
+    }
+
+    const hasColors = hasColorTokens(themeConfig);
+    return { themeConfig, hasColors, isValidJson: true };
+  } catch {
+    return { themeConfig: null, hasColors: false, isValidJson: false };
+  }
+}
+
+/**
+ * Parses theme JSON and provides a function to trigger analysis.
+ * Analysis is triggered manually via the runAnalysis function.
+ */
+export function useThemeAnalysis(
+  themeJson: string | undefined,
+): ThemeAnalysisResult {
+  const [analysis, setAnalysis] = useState<AccessibilityAnalysis | null>(null);
+
+  // Immediate parsing for validation
+  const parsed = useMemo(() => parseThemeJson(themeJson), [themeJson]);
+
+  const runAnalysis = useCallback(() => {
+    const { themeConfig, hasColors, isValidJson } = parsed;
+
+    if (!isValidJson || !hasColors || !themeConfig) {
+      return;
+    }
+
+    const result = analyzeThemeAccessibility(themeConfig);
+    setAnalysis(result);
+  }, [parsed]);
+
+  const resetAnalysis = useCallback(() => {
+    setAnalysis(null);
+  }, []);
+
+  return {
+    analysis,
+    hasColors: parsed.hasColors,
+    isValidJson: parsed.isValidJson,
+    runAnalysis,
+    resetAnalysis,
+  };
+}
+
+interface AccessibilityScoreResultsProps {
+  analysis: AccessibilityAnalysis;
+  onReanalyze: () => void;
+}
+
+/**
+ * AccessibilityScoreResults component displays WCAG contrast analysis results.
+ * Only renders when there's an analysis to show.
+ *
+ * Features:
+ * - Score from 0-100 based on contrast checks
+ * - WCAG compliance level (AAA, AA, A, Fail)
+ * - List of specific contrast issues with color swatches
+ * - Collapsible issues list for clean UI
+ */
+export function AccessibilityScoreResults({
+  analysis,
+  onReanalyze,
+}: AccessibilityScoreResultsProps): JSX.Element | null {
+  const theme = useTheme();
+
+  // Show message when no checkable pairs found
+  if (analysis.totalChecks === 0) {
+    return (
+      <StyledContainer>
+        <StyledHeader>
+          <Icons.InfoCircleOutlined className="header-icon" />
+          <Typography.Text className="score-text">
+            {t('Accessibility')}
+          </Typography.Text>
+        </StyledHeader>
+        <StyledEmptyState>
+          <Typography.Text type="secondary">
+            {t('No contrast pairs found to analyze.')}
+          </Typography.Text>
+          <Typography.Text
+            type="secondary"
+            css={css`
+              display: block;
+              margin-top: 4px;
+              font-size: 12px;
+            `}
+          >
+            {t('Try adding: colorText, colorBgBase, colorPrimary')}
+          </Typography.Text>
+          <Button
+            onClick={onReanalyze}
+            icon={<Icons.ReloadOutlined />}
+            buttonStyle="tertiary"
+            css={css`
+              margin-top: 8px;
+            `}
+          >
+            {t('Re-analyze')}
+          </Button>
+        </StyledEmptyState>
+      </StyledContainer>
+    );
+  }
+
+  const { score, level, issues, passedChecks, totalChecks } = analysis;
+  const scoreColor = getScoreColor(score);
+  const hasIssues = issues.length > 0;
+
+  const levelClassName = `level-${level.toLowerCase()}`;
+
+  const collapseItems = hasIssues
+    ? [
+        {
+          key: 'issues',
+          label: (
+            <span>
+              <Icons.WarningOutlined
+                css={css`
+                  margin-right: 4px;
+                `}
+              />
+              {t('%s contrast issue(s)', issues.length)}
+            </span>
+          ),
+          children: (
+            <>
+              {issues.map((issue, index) => (
+                <IssueItem
+                  key={`${issue.foreground}-${issue.background}-${index}`}
+                  issue={issue}
+                />
+              ))}
+            </>
+          ),
+        },
+      ]
+    : [];
+
+  return (
+    <StyledContainer>
+      <StyledHeader>
+        <Tooltip
+          title={t(
+            'WCAG 2.1 contrast analysis. AA requires %s:1 for normal text.',
+            WCAG_REQUIREMENTS.AA_NORMAL_TEXT,
+          )}
+        >
+          <Icons.InfoCircleOutlined className="header-icon" />
+        </Tooltip>
+        <Typography.Text className="score-text">
+          {t('Accessibility')}:
+        </Typography.Text>
+        <Typography.Text className="score-text">{score}/100</Typography.Text>
+        <span className={`level-badge ${levelClassName}`}>{level}</span>
+        <Typography.Text type="secondary">
+          ({passedChecks}/{totalChecks} {t('checks passed')})
+        </Typography.Text>
+        <Tooltip title={t('Re-analyze')}>
+          <Button
+            onClick={onReanalyze}
+            icon={<Icons.ReloadOutlined />}
+            buttonStyle="link"
+            buttonSize="xsmall"
+          />

Review Comment:
   <div>
   
   
   <div id="suggestion">
   <div id="issue"><b>Missing aria-label on icon button</b></div>
   <div id="fix">
   
   The icon-only button for re-analysis lacks an accessible name for screen 
readers, violating WCAG guidelines. While the Tooltip provides a title, it may 
not be read by all assistive technologies. Add aria-label with the translated 
text to ensure accessibility.
   </div>
   
   
   <details>
   <summary>
   <b>Code suggestion</b>
   </summary>
   <blockquote>Check the AI-generated fix before applying</blockquote>
   <div id="code">
   
   
   ````suggestion
           <Tooltip title={t('Re-analyze')}>
             <Button
               onClick={onReanalyze}
               icon={<Icons.ReloadOutlined />}
               buttonStyle="link"
               buttonSize="xsmall"
               aria-label={t('Re-analyze')}
             />
   ````
   
   </div>
   </details>
   
   
   
   </div>
   
   
   
   
   <small><i>Code Review Run #995e5e</i></small>
   </div>
   
   ---
   Should Bito avoid suggestions like this for future reviews? (<a 
href=https://alpha.bito.ai/home/ai-agents/review-rules>Manage Rules</a>)
   - [ ] Yes, avoid them



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to