From 0581e16edf9dce1c81b2edbb2d30ea8dcaf4a89b Mon Sep 17 00:00:00 2001 From: Jurriaan Roelofs Date: Tue, 27 Jan 2026 13:07:26 +0100 Subject: [PATCH] feat(ux): add animated gradient border branding for RL experiments - Add experiment-highlight CSS library with animated gradient border - Attach library for users with 'view rl reports' permission - Add .rl-experiment class for integrating modules to apply branding - Style contextual links and page titles on RL admin pages - Add body class 'rl-admin-page' on RL routes for targeted styling - Add dynamic page title showing experiment name on detail page The gradient border uses a 10-color palette (blue to green to yellow) with a 10s animation cycle. Integrating modules can simply add the 'rl-experiment' class to their elements to receive the branding. --- css/experiment-highlight.css | 50 ++++++++++++++++++++++++++++ rl.libraries.yml | 6 ++++ rl.module | 21 ++++++++++++ rl.routing.yml | 2 +- src/Controller/ReportsController.php | 15 +++++++++ 5 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 css/experiment-highlight.css diff --git a/css/experiment-highlight.css b/css/experiment-highlight.css new file mode 100644 index 0000000..8da02c8 --- /dev/null +++ b/css/experiment-highlight.css @@ -0,0 +1,50 @@ +/** + * @file + * Styles for highlighting RL experiments in the UI. + * + * This CSS is only loaded for users with 'view rl reports' permission. + * Integrating modules can add the 'rl-experiment' class to their elements + * to apply the animated rainbow border branding. + */ + +:root { + --rl-color-1: #0077B6; + --rl-color-2: #0096C7; + --rl-color-3: #00B4D8; + --rl-color-4: #48CAE4; + --rl-color-5: #26828E; + --rl-color-6: #1F9E89; + --rl-color-7: #35B779; + --rl-color-8: #6CCE59; + --rl-color-9: #B4DE2C; + --rl-color-10: #DAE319; +} + +@keyframes rl-rainbow-border { + 0% { + border-image: linear-gradient(90deg, var(--rl-color-1), var(--rl-color-2), var(--rl-color-3), var(--rl-color-4), var(--rl-color-5), var(--rl-color-6), var(--rl-color-7), var(--rl-color-8), var(--rl-color-9), var(--rl-color-10)) 1; + } + 20% { + border-image: linear-gradient(90deg, var(--rl-color-3), var(--rl-color-4), var(--rl-color-5), var(--rl-color-6), var(--rl-color-7), var(--rl-color-8), var(--rl-color-9), var(--rl-color-10), var(--rl-color-1), var(--rl-color-2)) 1; + } + 40% { + border-image: linear-gradient(90deg, var(--rl-color-5), var(--rl-color-6), var(--rl-color-7), var(--rl-color-8), var(--rl-color-9), var(--rl-color-10), var(--rl-color-1), var(--rl-color-2), var(--rl-color-3), var(--rl-color-4)) 1; + } + 60% { + border-image: linear-gradient(90deg, var(--rl-color-7), var(--rl-color-8), var(--rl-color-9), var(--rl-color-10), var(--rl-color-1), var(--rl-color-2), var(--rl-color-3), var(--rl-color-4), var(--rl-color-5), var(--rl-color-6)) 1; + } + 80% { + border-image: linear-gradient(90deg, var(--rl-color-9), var(--rl-color-10), var(--rl-color-1), var(--rl-color-2), var(--rl-color-3), var(--rl-color-4), var(--rl-color-5), var(--rl-color-6), var(--rl-color-7), var(--rl-color-8)) 1; + } + 100% { + border-image: linear-gradient(90deg, var(--rl-color-1), var(--rl-color-2), var(--rl-color-3), var(--rl-color-4), var(--rl-color-5), var(--rl-color-6), var(--rl-color-7), var(--rl-color-8), var(--rl-color-9), var(--rl-color-10)) 1; + } +} + +.rl-experiment, +.contextual-links li a[href*="/admin/reports/rl/experiment/"], +.rl-admin-page .page-title { + animation: rl-rainbow-border 10s linear infinite !important; + border-width: 3px !important; + border-style: solid !important; +} diff --git a/rl.libraries.yml b/rl.libraries.yml index 2858b7d..c545d49 100644 --- a/rl.libraries.yml +++ b/rl.libraries.yml @@ -1,3 +1,9 @@ +experiment-highlight: + version: 1.0 + css: + theme: + css/experiment-highlight.css: {} + plotly: version: 2.35.2 license: diff --git a/rl.module b/rl.module index f21ebc2..97a350d 100644 --- a/rl.module +++ b/rl.module @@ -40,6 +40,27 @@ function rl_theme() { ]; } +/** + * Implements hook_page_attachments(). + */ +function rl_page_attachments(array &$attachments) { + // Attach experiment highlight styles for users who can view RL reports. + if (\Drupal::currentUser()->hasPermission('view rl reports')) { + $attachments['#attached']['library'][] = 'rl/experiment-highlight'; + } +} + +/** + * Implements hook_preprocess_html(). + */ +function rl_preprocess_html(&$variables) { + // Add body class on RL admin pages for styling. + $route_name = \Drupal::routeMatch()->getRouteName(); + if ($route_name && str_starts_with($route_name, 'rl.')) { + $variables['attributes']['class'][] = 'rl-admin-page'; + } +} + /** * Implements hook_help(). */ diff --git a/rl.routing.yml b/rl.routing.yml index 5c72913..e31a891 100644 --- a/rl.routing.yml +++ b/rl.routing.yml @@ -50,7 +50,7 @@ rl.reports.experiment_detail: path: '/admin/reports/rl/experiment/{experiment_id}' defaults: _controller: '\Drupal\rl\Controller\ReportsController::experimentDetail' - _title: 'Experiment Detail' + _title_callback: '\Drupal\rl\Controller\ReportsController::experimentDetailTitle' requirements: _permission: 'view rl reports' experiment_id: '.+' diff --git a/src/Controller/ReportsController.php b/src/Controller/ReportsController.php index 654b115..1977d2a 100644 --- a/src/Controller/ReportsController.php +++ b/src/Controller/ReportsController.php @@ -218,6 +218,21 @@ public function experimentsOverview() { return $build; } + /** + * Title callback for the experiment detail page. + * + * @param string $experiment_id + * The experiment ID. + * + * @return string + * The page title. + */ + public function experimentDetailTitle($experiment_id) { + $experiment_totals = $this->experimentStorage->getExperimentTotals($experiment_id); + $experiment_name = $experiment_totals->experiment_name ?? $experiment_id; + return $this->t('Experiment: @name', ['@name' => $experiment_name]); + } + /** * Detail page for a specific experiment showing all arms. *