ica web responsive theme

This commit is contained in:
Pranay 2025-12-24 17:50:22 +05:30
parent 53f90a7834
commit ad5967d420
64 changed files with 2388 additions and 0 deletions

View File

@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'ICA Web Responsive',
'author':"Agga, IdeaCode Academy",
'version': '18.0.1.0',
'depends': ['web', 'base_setup'],
# 'auto_install': ['web'],
'auto_install': False,
'data': [
'views/webclient_templates.xml',
],
'assets': {
'web._assets_primary_variables': [
('after', 'web/static/src/scss/primary_variables.scss', 'ica_web_responsive/static/src/**/*.variables.scss'),
('before', 'web/static/src/scss/primary_variables.scss', 'ica_web_responsive/static/src/scss/primary_variables.scss'),
],
'web._assets_secondary_variables': [
('before', 'web/static/src/scss/secondary_variables.scss', 'ica_web_responsive/static/src/scss/secondary_variables.scss'),
],
'web._assets_backend_helpers': [
('before', 'web/static/src/scss/bootstrap_overridden.scss', 'ica_web_responsive/static/src/scss/bootstrap_overridden.scss'),
],
'web.assets_frontend': [
'ica_web_responsive/static/src/webclient/home_menu/home_menu_background.scss', # used by login page
'ica_web_responsive/static/src/webclient/navbar/navbar.scss',
],
'web.assets_backend': [
'ica_web_responsive/static/src/webclient/**/*.scss',
'ica_web_responsive/static/src/views/**/*.scss',
'ica_web_responsive/static/src/core/**/*',
'ica_web_responsive/static/src/webclient/**/*.js',
('after', 'web/static/src/views/list/list_renderer.xml', 'ica_web_responsive/static/src/views/list/list_renderer_desktop.xml'),
'ica_web_responsive/static/src/webclient/**/*.xml',
'ica_web_responsive/static/src/views/**/*.js',
'ica_web_responsive/static/src/views/**/*.xml',
('remove', 'ica_web_responsive/static/src/views/pivot/**'),
# Don't include dark mode files in light mode
('remove', 'ica_web_responsive/static/src/**/*.dark.scss'),
],
'web.assets_backend_lazy': [
'ica_web_responsive/static/src/views/pivot/**',
],
'web.assets_backend_lazy_dark': [
('include', 'web.dark_mode_variables'),
# web._assets_backend_helpers
('before', 'ica_web_responsive/static/src/scss/bootstrap_overridden.scss', 'ica_web_responsive/static/src/scss/bootstrap_overridden.dark.scss'),
('after', 'web/static/lib/bootstrap/scss/_functions.scss', 'ica_web_responsive/static/src/scss/bs_functions_overridden.dark.scss'),
],
'web.assets_web': [
('replace', 'web/static/src/main.js', 'ica_web_responsive/static/src/main.js'),
],
# ========= Dark Mode =========
"web.dark_mode_variables": [
# web._assets_primary_variables
('before', 'ica_web_responsive/static/src/scss/primary_variables.scss', 'ica_web_responsive/static/src/scss/primary_variables.dark.scss'),
('before', 'ica_web_responsive/static/src/**/*.variables.scss', 'ica_web_responsive/static/src/**/*.variables.dark.scss'),
# web._assets_secondary_variables
('before', 'ica_web_responsive/static/src/scss/secondary_variables.scss', 'ica_web_responsive/static/src/scss/secondary_variables.dark.scss'),
],
"web.assets_web_dark": [
('include', 'web.dark_mode_variables'),
# web._assets_backend_helpers
('before', 'ica_web_responsive/static/src/scss/bootstrap_overridden.scss', 'ica_web_responsive/static/src/scss/bootstrap_overridden.dark.scss'),
('after', 'web/static/lib/bootstrap/scss/_functions.scss', 'ica_web_responsive/static/src/scss/bs_functions_overridden.dark.scss'),
# assets_backend
'ica_web_responsive/static/src/**/*.dark.scss',
],
},
'license': 'LGPL-3',
"images":["static/description/img.png"],
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 419 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@ -0,0 +1,35 @@
<svg width="1920" height="1080" viewBox="0 0 1920 1080" xmlns="http://www.w3.org/2000/svg">
<path d="M3.51001 1080H76.35L1153.55 0H3.51001V1080Z" fill="url(#o_app_switcher_gradient_01)"/>
<path d="M76.35 1080H842.98L1920 0.18V0H1153.55L76.35 1080Z" fill="url(#o_app_switcher_gradient_02)"/>
<path d="M1920 0.180176L842.98 1080H1063.11L1920 220.88V0.180176Z" fill="url(#o_app_switcher_gradient_03)"/>
<path d="M1920 1080V220.88L1063.11 1080H1920Z" fill="url(#o_app_switcher_gradient_04)"/>
<rect width="1920" height="1080" fill="url(#o_app_switcher_gradient_05)" fill-opacity="0.25"/>
<rect width="1920" height="1080" fill="#E9E6F9" fill-opacity="0.25"/>
<defs>
<linearGradient id="o_app_switcher_gradient_01" x1="-222.43" y1="727.19" x2="904.26" y2="-76.67" gradientUnits="userSpaceOnUse">
<stop offset="0.1" stop-color="white"/>
<stop offset="0.36" stop-color="#FEFEFE"/>
<stop offset="0.68" stop-color="#EAE7F9"/>
<stop offset="1" stop-color="#E4E9F7"/>
</linearGradient>
<linearGradient id="o_app_switcher_gradient_02" x1="407.23" y1="1021.82" x2="1848.47" y2="-153.08" gradientUnits="userSpaceOnUse">
<stop offset="0.32" stop-color="#FEFEFE"/>
<stop offset="0.66" stop-color="#EAE7F9"/>
<stop offset="1" stop-color="#E5E2F6"/>
</linearGradient>
<linearGradient id="o_app_switcher_gradient_03" x1="1142.33" y1="846.57" x2="1951.83" y2="136.16" gradientUnits="userSpaceOnUse">
<stop offset="0.15" stop-color="white"/>
<stop offset="0.51" stop-color="#F7F0FD"/>
<stop offset="0.85" stop-color="#F0E7F9"/>
</linearGradient>
<linearGradient id="o_app_switcher_gradient_04" x1="1409.74" y1="1071" x2="2070.98" y2="526.01" gradientUnits="userSpaceOnUse">
<stop offset="0.45" stop-color="white"/>
<stop offset="0.88" stop-color="#F7F0FD"/>
<stop offset="1" stop-color="#ECE5F8"/>
</linearGradient>
<radialGradient id="o_app_switcher_gradient_05" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(960 540) rotate(90) scale(540 960)">
<stop stop-color="#9996A9" stop-opacity="0.53"/>
<stop offset="1" stop-color="#7A768F"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,3 @@
declare module "@odoo/owl" {
export * from "@odoo/owl/dist/types/owl"
}

View File

@ -0,0 +1,8 @@
// Overrides the existing classes to fit the text-color of
// tag_list.dark.scss
@for $size from 2 through length($o-colors) {
.o_colorlist_item_color_#{$size - 1} {
--background-color: #{adjust-color(nth($o-colors, $size), $lightness: -5%, $saturation: -15%)};
--color: #{mix(nth($o-colors, $size), $o-view-background-color, 15%)};
}
}

View File

@ -0,0 +1,8 @@
// = Dropdowns
// ============================================================================
// No CSS hacks, variables overrides only
.o-dropdown {
--border-color: #{$dropdown-border-color};
--o-input-border-color: #{$dropdown-border-color};
}

View File

@ -0,0 +1,12 @@
// Custom SCSS for enterprise version of notebook tabs
.o_notebook {
--notebook-link-border-color: #{$border-color};
--notebook-link-border-color-hover: #{$border-color};
--notebook-link-border-color-active-accent: #{$o-brand-odoo};
.modal & {
--notebook-padding-x: #{$modal-inner-padding};
--notebook-margin-x: -#{$modal-inner-padding};
}
}

View File

@ -0,0 +1,15 @@
// = Popovers
// ============================================================================
// No CSS hacks, variables overrides only
.o_popover {
--border-color: #{$popover-border-color};
.table {
--table-bg: #{$popover-bg};
}
.o_input {
--o-input-border-color: #{$popover-border-color};
}
}

View File

@ -0,0 +1,13 @@
.o_tag {
@for $size from 1 through length($o-colors) {
&.o_tag_color_#{$size - 1} {
--background-color: #{mix(nth($o-colors, $size), $o-view-background-color, 15%)};
--color: #{adjust-color(nth($o-colors, $size), $lightness: 5%, $saturation: -15%)};
&::after {
--background-color: var(--background-color);
--color: var(--color);
}
}
}
}

View File

@ -0,0 +1,11 @@
/** @odoo-module **/
import { startWebClient } from "@web/start";
import { WebClientEnterprise } from "./webclient/webclient";
/**
* This file starts the enterprise webclient. In the manifest, it replaces
* the community main.js to load a different webclient class
* (WebClientEnterprise instead of WebClient)
*/
startWebClient(WebClientEnterprise);

View File

@ -0,0 +1,180 @@
///
/// This file is a copy of the bootstrap _variables.scss file where all the
/// left-untouched variables definition have been removed.
///
// == Color system
$danger: $o-danger !default;
$min-contrast-ratio: 4.5 !default;
$info-text-emphasis: shift-color($o-info, 90%) !default;
$info-bg-subtle: shift-color($o-info, -65%) !default;
$info-border-subtle: shift-color($o-info, 0%) !default;
// == Characters which are escaped by the escape-svg function
// == Options
// == Prefix for :root CSS variables
// == Gradient
// == Spacing
// == Position
// == Body
// == Links
$link-shade-percentage: 15% !default;
$link-hover-color: shift-color($o-action, 30%) !default;
// == Paragraphs
// == Grid breakpoints
// == Grid containers
// == Grid columns
// == Components
$box-shadow: 0 .5rem 1rem rgba($o-white, .3) !default;
$box-shadow-sm: 0 .125rem .25rem rgba($o-white, .15) !default;
$box-shadow-lg: 0 1rem 3rem rgba($o-white, .3) !default;
$box-shadow-inset: inset 0 1px 2px rgba($o-white, .15) !default;
$component-active-bg: $o-gray-300 !default;
// == Typography
$mark-bg: #ffdebc !default;
// == Tables
$table-bg: $o-view-background-color !default;
$table-border-color: $o-gray-300 !default;
$table-group-separator-color: $o-gray-300 !default;
$table-bg-scale: -70% !default;
$table-striped-bg-factor: .02 !default;
$table-hover-bg-factor: .1 !default;
$table-active-bg-factor: .1 !default;
// == Buttons + Forms
// == Buttons
// == Forms
$input-border-color: $o-gray-300 !default;
$input-placeholder-color: mix($o-gray-500, $o-gray-600) !default;
$input-focus-bg: inherit !default;
$form-range-thumb-active-bg: lighten($o-brand-primary, 10%);
$form-range-track-bg: $o-gray-300 !default;
$form-switch-color: rgba($o-black, .5) !default;
$form-switch-focus-color: $o-black !default;
$form-switch-checked-color: $o-view-background-color !default;
// == Form validation
// == Z-index master list
// == Navs
// == Navbar
// == Dropdowns
$dropdown-bg: $o-gray-300 !default;
$dropdown-border-color: $o-gray-400 !default;
$dropdown-header-color: $o-gray-700 !default;
// == Pagination
// == Placeholders
// == Cards
$card-cap-bg: $o-view-background-color !default;
// == Accordion
// == Tooltips
$tooltip-color: $o-gray-800 !default;
$tooltip-bg: $o-gray-300 !default;
// == Form tooltips must come after regular tooltips
// == Popovers
$popover-bg: $o-gray-300 !default;
$popover-border-color: $o-gray-400 !default;
// == Toasts
// == Badges
// == Modals
// == Alerts
$alert-bg-scale: -65% !default;
$alert-border-scale: 0% !default;
$alert-color-scale: 90% !default;
// == Progress bars
// == List group
$list-group-bg: $o-view-background-color !default;
// == Image thumbnails
// == Figures
// == Breadcrumbs
// == Carousel
// == Spinners
// == Close
// == Offcanvas
// == Code
// == Keyboard Input
$kbd-color: $o-gray-200 !default;
$kbd-bg: $o-gray-900 !default;
$kbd-box-shadow: 0px 1px 1px rgba($o-white, 0.2), inset 0px -1px 1px 1px rgba($o-gray-800, 0.8), inset 0px 2px 0px 0px rgba($o-black, 0.8) !default;

View File

@ -0,0 +1,136 @@
///
/// This file is a copy of the bootstrap _variables.scss file where all the
/// left-untouched variables definition have been removed.
///
//
// Color system
//
$light: $o-white !default;
$dark: $o-gray-900 !default;
$warning: #e99d00 !default;
$danger: #d44c59 !default;
// Options
// Enable predefined decorative box-shadow styles on various components.
// Does not affect box-shadows used for focus states.
$enable-shadows: true !default;
// Components
//
// Define common padding and border radius sizes and more.
$component-active-color: unset !default;
$component-active-bg: mix($o-enterprise-action-color, $o-white, 10%) !default;
$nav-tabs-border-radius: 0 !default;
$nav-pills-border-radius: 0 !default;
$card-border-radius: 0 !default;
$accordion-border-radius: 0 !default;
$toast-border-radius: 0 !default;
$badge-border-radius: 0 !default;
$progress-border-radius: 0 !default;
$list-group-border-radius: 0 !default;
$thumbnail-border-radius: 0 !default;
$form-check-input-border-radius: 0 !default;
// Typography
//
// Font, line-height, and color for body text, headings, and more.
$h1-font-size: $o-font-size-base * 2.4 !default;
$h2-font-size: $o-font-size-base * 1.5 !default;
$h3-font-size: $o-font-size-base * 1.3 !default;
$h4-font-size: $o-font-size-base * 1.2 !default;
$h5-font-size: $o-font-size-base * 1.1 !default;
// Buttons
//
// For each of Bootstrap's buttons, define text, background, and border color.
$btn-transition: none !default;
$btn-box-shadow: 0 !default;
$btn-active-box-shadow: 0 !default;
$btn-focus-box-shadow: 0 !default;
// Dropdowns
//
// Dropdown menu container and contents.
$dropdown-box-shadow: 0 .3rem 1rem rgba(#000, .1) !default;
// Forms
//
$input-border-color: $o-gray-200 !default;
$input-box-shadow: 0 !default;
$input-focus-bg: $o-white !default;
$input-focus-box-shadow: 0 !default;
$input-focus-border-color: mix($o-enterprise-action-color, $o-gray-200) !default;
$form-check-input-checked-color: $o-white !default;
$form-check-input-checked-border-color: $o-enterprise-action-color !default;
$form-check-input-checked-bg-color: $o-enterprise-action-color !default;
$form-select-focus-box-shadow: 0 !default;
$form-range-track-box-shadow: 0 !default;
// Z-index master list
//
// Change the z-index of the modal-backdrop elements to be equal to the
// modal elements' ones. Bootstrap does not support multi-modals, and without
// this rule all the modal-backdrops are below all the opened modals.
// Indeed, bootstrap forces them to a lower z-index as the modal-backdrop
// element (unique in their supported cases) might be put after the modal
// element (if the modal is already in the DOM, hidden, then opened). This
// cannot happen in odoo though as modals are not hidden but removed from
// the DOM and are always put at the end of the body when opened.
//
// TODO the following code was disabled because it is saas-incompatible
//
// $zindex-modal-backdrop: $zindex-modal;
// Navs
$nav-link-color: $o-main-text-color !default;
$nav-tabs-link-active-color: $o-main-headings-color !default;
$nav-tabs-link-active-bg: transparent !default;
// Badges
$badge-border-radius: $o-border-radius !default;
$badge-font-weight: normal !default;
// Alerts
//
// Define alert colors, border radius, and padding.
$alert-border-width: 0 !default;
// Progress bars
$progress-box-shadow: 0 !default;
// List group
$list-group-active-color: $o-enterprise-action-color !default;
$list-group-active-bg: $component-active-bg !default;
$list-group-active-border-color: $o-enterprise-action-color !default;
// Image thumbnails
$thumbnail-box-shadow: 0 !default;
// Breadcrumbs
$breadcrumb-active-color: $o-main-text-color !default;
$breadcrumb-divider-color: $o-main-color-muted !default;

View File

@ -0,0 +1,14 @@
///
/// This file is a copy of the bootstrap _functions.scss file where all the
/// left-untouched function definition have been removed.
///
// Tint a color: mix a color with black
@function tint-color($color, $weight) {
@return mix(#000, $color, $weight);
}
// Shade a color: mix a color with white
@function shade-color($color, $weight) {
@return mix(#FFF, $color, $weight);
}

View File

@ -0,0 +1,193 @@
///
/// Odoo Dark-Mode
///
///
// = Colors
// ============================================================================
$o-white: #000 !default;
$o-black: #FFF !default;
$o-gray-100: #1B1D26 !default;
$o-gray-200: #262A36 !default;
$o-gray-300: #3C3E4B !default;
$o-gray-400: #5A5E6B !default;
$o-gray-500: #6B707F !default;
$o-gray-600: #7E8392 !default;
$o-gray-700: #B1B3BC !default;
$o-gray-800: #D1D1D1 !default;
$o-gray-900: #E4E4E4 !default;
$o-enterprise-color: #6b3e66 !default;
$o-brand-primary: $o-enterprise-color !default;
$o-enterprise-action-color: #02c7b5 !default;
$o-success: #1dc959 !default;
$o-info: #6AB5FB !default;
$o-warning: #FBB56A !default;
$o-danger: #b83232 !default;
$o-action: $o-enterprise-action-color !default;
$light: $o-gray-300 !default;
$dark: $o-gray-700 !default;
// = Text
// ============================================================================
$o-main-text-color: $o-gray-800 !default;
$o-main-link-color: $o-action !default;
$o-enterprise-color: $o-brand-odoo !default;
$o-main-favorite-color: #ffd532 !default;
$o-main-code-color: #c58bc8 !default;
// = Fine-tune contextual text colors.
$o-theme-text-colors: (
"primary": #b972a6,
"success": #1dc959,
"info": #6AB5FB,
"warning": #FBB56A,
"danger": #ff5757,
) !default;
// = Webclient
// ============================================================================
$o-webclient-color-scheme: dark !default;
$o-webclient-background-color: $o-gray-100 !default;
$o-view-background-color: $o-gray-200 !default;
// = Inputs
$o-input-border-required: $o-black !default;
// = Components
// ============================================================================
$o-component-active-bg: mix($o-action, $o-gray-300, 10%) !default;
$o-form-lightsecondary: $o-gray-300 !default;
// = List-group
$o-list-group-active-color: $o-gray-900 !default;
$o-list-group-active-bg: rgba(saturate(adjust-hue($o-info, 15), 1.8), .5) !default;
// = Modal
$modal-backdrop-bg: $o-white !default;
// = Buttons
$o-btns-bs-override: () !default;
$o-btns-bs-override: map-merge((
"primary": (
background: $o-brand-primary,
border: $o-brand-primary,
color: $o-black,
hover-background: lighten($o-brand-primary, 5%),
hover-border: lighten($o-brand-primary, 5%),
hover-color: $o-black,
active-background: lighten($o-brand-primary, 10%),
active-border: lighten($o-brand-primary, 10%),
active-color: $o-black,
),
"secondary": (
background: $o-gray-300,
border: $o-gray-300,
color: $o-gray-900,
hover-background: $o-gray-400,
hover-border: $o-gray-400,
hover-color: $o-gray-900,
active-background: mix($o-action, $o-gray-100, 15%),
active-border: lighten($o-action, 10%),
active-color: $o-black,
),
"light": (
background: $o-gray-200,
border: $o-gray-200,
color: $o-gray-800,
hover-background: $o-gray-300,
hover-border: $o-gray-300,
hover-color: $o-gray-900,
active-background: mix($o-action, $o-gray-100, 15%),
active-border: darken($o-action, 10%),
active-color: $o-black,
),
"danger": (
background: $o-danger,
border: $o-danger,
color: $o-black,
hover-background: lighten($o-danger, 5%),
hover-border: lighten($o-danger, 5%),
hover-color: $o-black,
active-background: lighten($o-danger, 10%),
active-border: lighten($o-danger, 10%),
active-color: $o-black,
),
), $o-btns-bs-override);
$o-btns-bs-outline-override: () !default;
$o-btns-bs-outline-override: map-merge((
"primary": (
background: transparent,
border: map-get($o-theme-text-colors, 'primary'),
color: map-get($o-theme-text-colors, 'primary'),
hover-background: lighten($o-brand-primary, 5%),
hover-border: lighten($o-brand-primary, 5%),
hover-color: $o-black,
active-background: lighten($o-brand-primary, 10%),
active-border: lighten($o-brand-primary, 10%),
active-color: $o-black,
),
"secondary": (
background: transparent,
border: $o-gray-300,
color: $o-gray-700,
hover-background: $o-gray-300,
hover-border: $o-gray-300,
hover-color: $o-black,
active-background: mix($o-action, $o-gray-100, 15%),
active-border: lighten($o-action, 10%),
active-color: $o-black,
),
"light": (
background: transparent,
border: $o-gray-300,
color: $o-black,
hover-background: $o-gray-300,
hover-border: $o-gray-300,
hover-color: $o-gray-900,
active-background: mix($o-action, $o-gray-100, 15%),
active-border: lighten($o-action, 10%),
active-color: $o-black,
),
"danger": (
background: transparent,
border: $o-danger,
color: $o-danger,
hover-background: lighten($o-danger, 5%),
hover-border: lighten($o-danger, 5%),
hover-color: $o-black,
active-background: lighten($o-danger, 10%),
active-border: lighten($o-danger, 10%),
active-color: $o-black,
),
), $o-btns-bs-outline-override);

View File

@ -0,0 +1,107 @@
///
/// This file regroups the variables that style odoo components.
/// They are available in every asset bundle.
///
// Colors
$o-white: #FFF !default;
$o-black: #000 !default;
$o-gray-100: #F9FAFB !default;
$o-gray-200: #e7e9ed !default;
$o-gray-300: #d8dadd !default;
$o-gray-400: #9a9ca5 !default;
$o-gray-500: #7c7f89 !default;
$o-gray-600: #5f636f !default;
$o-gray-700: #374151 !default;
$o-gray-800: #1F2937 !default;
$o-gray-900: #111827 !default;
$o-enterprise-color: #714B67 !default;
$o-enterprise-action-color: #017e84 !default;
$o-opacity-disabled: .5 !default;
$o-opacity-muted: .76 !default;
$o-brand-odoo: $o-enterprise-color !default;
$o-brand-primary: $o-brand-odoo !default;
$o-brand-secondary: #8f8f8f !default;
$o-brand-lightsecondary: $o-gray-100 !default;
$o-action: $o-enterprise-action-color !default;
$o-main-text-color: $o-gray-700 !default;
$o-main-link-color: $o-enterprise-action-color !default;
$o-main-color-muted: rgba($o-main-text-color, $o-opacity-muted) !default;
// Components
$o-component-active-color: $o-gray-900 !default;
$o-component-active-bg: mix($o-action, $o-white, 10%) !default;
$o-component-active-border: $o-action !default;
$o-list-group-header-color: $o-gray-900 !default;
$o-list-footer-color: $o-gray-900 !default;
$o-list-footer-bg-color: transparent !default;
$o-list-footer-font-weight: 500 !default;
$o-form-lightsecondary: $o-gray-200 !default;
// o-inputs
$o-input-padding-y: 1px !default;
$o-input-padding-x: 0 !default;
$o-input-border-required: $o-gray-900 !default;
// Badges
$o-badge-min-width: 2.7ch !default !default;
// Buttons
// Map of customized values for each button. If a button's design is defined
// here, the relative values will take priority over default BS ones.
// Notice: each map's entry is passed directly to the Bootstrap mixin, meaning
// that all states must be defined, there can't be omissions.
$o-btns-bs-override: () !default;
$o-btns-bs-override: map-merge((
"primary": (
background: $o-brand-primary,
border: $o-brand-primary,
color: $o-white,
hover-background: darken($o-brand-primary, 10%),
hover-border: darken($o-brand-primary, 10%),
hover-color: $o-white,
active-background: mix($o-brand-primary, $o-white, 10%),
active-border: $o-brand-primary,
active-color:$o-brand-primary,
),
"secondary": (
background: $o-gray-200,
border: $o-gray-200,
color: $o-gray-700,
hover-background: $o-gray-300,
hover-border: $o-gray-300,
hover-color: $o-gray-800,
active-background: $o-component-active-bg,
active-border: $o-component-active-border,
active-color: $o-component-active-color,
),
), $o-btns-bs-override);
$o-btns-bs-outline-override: () !default;
$o-btns-bs-outline-override: map-merge((
"secondary": (
background: transparent,
border: $o-gray-300,
color: $o-gray-700,
hover-background: $o-gray-200,
hover-border: $o-gray-300,
hover-color: $o-gray-800,
active-background: mix($o-enterprise-action-color, $o-white, 10%),
active-border: $o-enterprise-action-color,
active-color: $o-gray-900,
),
), $o-btns-bs-outline-override);

View File

@ -0,0 +1,19 @@
$o-colors-original: lighten(#000, 46.7%), #f07b50, #f4b660, #F7CD1F, #6cedeb, #8d5482,
#e74e4e, #2C8397, #475577, #dc0457, #30C381, #9365B8 !default;
$o-colors-secondary-original: #aa4b6b, #30C381, #97743a, #F7CD1F, #4285F4, #8E24AA,
#D6145F, #173e43, #348F50, #AA3A38, #795548, #5e0231,
#6be585, #999966, #e9d362, #b56969, #bdc3c7, #649173 !default;
$o-colors: ()!default;
$o-colors-secondary: ()!default;
@each $-color in $o-colors-original {
$-adjusted: saturate(mix($-color, $o-black, 50%), 60%);
$o-colors: append($o-colors, $-adjusted);
}
@each $-color in $o-colors-secondary-original {
$-adjusted: saturate(mix($-color, $o-black, 50%), 80%);
$o-colors-secondary: append($o-colors-secondary, $-adjusted);
}

View File

@ -0,0 +1,4 @@
// Needed for having no spacing between sheet and mail body in mass_mailing:
// Different required cancel paddings between web and ica_web_responsive
$o-sheet-cancel-tpadding: $o-horizontal-padding !default;
$o-sheet-cancel-bpadding: $o-horizontal-padding + $o-sheet-vpadding !default;

View File

@ -0,0 +1,7 @@
// = Search Bar
// ============================================================================
// No CSS hacks, variables overrides only
.o_searchview_facet {
--SearchBar-facet-background: #{$o-black};
}

View File

@ -0,0 +1,12 @@
// = Mobile Search
// ============================================================================
// No CSS hacks, variables overrides only
.o_mobile_search {
--mobileSearch-bg: #{$o-gray-200};
--mobileSearch__header-bg: #{$o-gray-100};
}
.o_searchview {
--SearchBar-background-color: #{$o-gray-100};
}

View File

@ -0,0 +1,10 @@
// = Dashboard View
// ============================================================================
// No CSS hacks, variables overrides only
.o_dashboard_view {
--DashboardView-background-color: #{$o-gray-100};
--DashboardView__controlPanel-background-color: transparent;
--DashboardView__pieChart-background-color: transparent;
}

View File

@ -0,0 +1,7 @@
// = Image Field
// ============================================================================
// No CSS hacks, variables overrides only
.o_field_image {
--ImageField-background-color: #{$o-gray-900};
}

View File

@ -0,0 +1,19 @@
.o_field_property_definition_type, .o_field_property_definition_type_menu {
.o_field_property_dropdown > img {
-webkit-filter: invert(100%);
filter: invert(100%);
}
}
.o_property_field_value {
select {
option {
background-color: $border-color;
}
}
}
.o_field_property_definition_type_popover.popover {
.dropdown-item img {
-webkit-filter: invert(100%);
filter: invert(100%);
}
}

View File

@ -0,0 +1,4 @@
.o-form-buttonbox {
--o-stat-button-color: currentColor;
--o-stat-text-color: #{o-text-color('primary')};
}

View File

@ -0,0 +1,9 @@
// = Gantt View Variables
// ============================================================================
// No CSS hacks, variables overrides only
$gantt-highlight-today-border: rgba($o-warning, 0.5) !default;
$gantt-highlight-today-bg: rgba($o-warning, 0.15)!default;
$gantt-highlight-hover-row: rgba($o-brand-primary, .1) !default;
$gantt-row-open-bg: $o-gray-100 !default;
$gantt-unavailability-bg: $o-gray-200 !default;

View File

@ -0,0 +1,11 @@
// = Kanban Rendered
// ============================================================================
// No CSS hacks, variables overrides only
.o_kanban_renderer {
--KanbanGroup-grouped-bg: #{$o-view-background-color};
--KanbanRecord__image-bg-color: #{$o-gray-900};
--KanbanColumn__highlight-background: #{mix($o-action, $o-gray-100, 15%)};
--KanbanColumn__highlight-border: #{$o-component-active-border};
--Kanban-background: #{$gray-100};
}

View File

@ -0,0 +1,44 @@
/* @odoo-module */
import { registry } from "@web/core/registry";
import { patch } from "@web/core/utils/patch";
import { KanbanHeader } from "@web/views/kanban/kanban_header";
import { PromoteStudioAutomationDialog } from "@ica_web_responsive/webclient/promote_studio_dialog/promote_studio_dialog";
import { _t } from "@web/core/l10n/translation";
import { user } from "@web/core/user";
patch(KanbanHeader.prototype, {
/**
* @override
*/
get permissions() {
const permissions = super.permissions;
Object.defineProperty(permissions, "canEditAutomations", {
get: () => user.isAdmin,
configurable: true,
});
return permissions;
},
async openAutomations() {
if (typeof this._openAutomations === "function") {
// this is the case if base_automation is installed
return this._openAutomations();
} else {
this.env.services.dialog.add(PromoteStudioAutomationDialog, {
title: _t("Odoo Studio - Customize workflows in minutes"),
});
}
},
});
registry.category("kanban_header_config_items").add(
"open_automations",
{
label: _t("Automations"),
method: "openAutomations",
isVisible: ({ permissions }) => permissions.canEditAutomations,
class: "o_column_automations",
},
{ sequence: 25, force: true }
);

View File

@ -0,0 +1,14 @@
.o_kanban_view {
.o_column_quick_create .o_kanban_quick_create {
input {
&, &:focus, &:hover {
background: transparent;
border-bottom: 1px solid map-get($grays, '600');
}
}
.input-group-append, .input-group-prepend {
border-left: 10px solid map-get($grays, '200');
}
}
}

View File

@ -0,0 +1,4 @@
// = ListRenderer
// ============================================================================
// No CSS hacks, variables overrides only

View File

@ -0,0 +1,3 @@
.o_list_renderer {
--ListRenderer-thead-border-end-color: transparent;
}

View File

@ -0,0 +1,100 @@
/** @odoo-module */
import { isMobileOS } from "@web/core/browser/feature_detection";
import { user } from "@web/core/user";
import { useService } from "@web/core/utils/hooks";
import { patch } from "@web/core/utils/patch";
import { ListRenderer } from "@web/views/list/list_renderer";
import { PromoteStudioDialog } from "@ica_web_responsive/webclient/promote_studio_dialog/promote_studio_dialog";
import { _t } from "@web/core/l10n/translation";
import { onWillDestroy, useState } from "@odoo/owl";
export const patchListRendererDesktop = () => ({
setup() {
super.setup(...arguments);
this.actionService = useService("action");
const list = this.props.list;
const { actionId, actionType } = this.env.config || {};
// Start by determining if the current ListRenderer is in a context that would
// allow the edition of the arch by studio.
// It needs to be a full list view, in an action
// (not a X2Many list, and not an "embedded" list in another component)
// Also, there is not enough information when an action is in target new,
// and this use case is fairly outside of the feature's scope
const isPotentiallyEditable =
!isMobileOS() &&
!this.env.inDialog &&
user.isSystem &&
list === list.model.root &&
actionId &&
actionType === "ir.actions.act_window";
this.studioEditable = useState({ value: isPotentiallyEditable });
if (isPotentiallyEditable) {
const computeStudioEditable = (action) => {
// Finalize the computation when the actionService is ready.
// The following code is copied from studioService.
if (!action.xml_id) {
return false;
}
if (
action.res_model.indexOf("settings") > -1 &&
action.res_model.indexOf("x_") !== 0
) {
return false; // settings views aren't editable; but x_settings is
}
if (action.res_model === "board.board") {
return false; // dashboard isn't editable
}
if (action.view_mode === "qweb") {
// Apparently there is a QWebView that allows to
// implement ActWindow actions that are completely custom
// but not editable by studio
return false;
}
if (action.res_model === "knowledge.article") {
// The knowledge form view is very specific and custom, it doesn't make sense
// to edit it. Editing the list and kanban is more debatable, but for simplicity's sake
// we set them to not editable too.
return false;
}
return Boolean(action.res_model);
};
const onUiUpdated = () => {
const action = this.actionService.currentController.action;
if (action.id === actionId) {
this.studioEditable.value = computeStudioEditable(action);
}
stopListening();
};
const stopListening = () =>
this.env.bus.removeEventListener("ACTION_MANAGER:UI-UPDATED", onUiUpdated);
this.env.bus.addEventListener("ACTION_MANAGER:UI-UPDATED", onUiUpdated);
onWillDestroy(stopListening);
}
},
isStudioEditable() {
return this.studioEditable.value;
},
get displayOptionalFields() {
return this.isStudioEditable() || super.displayOptionalFields;
},
/**
* This function opens promote studio dialog
*
* @private
*/
onSelectedAddCustomField() {
this.env.services.dialog.add(PromoteStudioDialog, {
title: _t("Odoo Studio - Add new fields to any view"),
});
},
});
export const unpatchListRendererDesktop = patch(ListRenderer.prototype, patchListRendererDesktop());

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-inherit="web.ListRenderer" t-inherit-mode="extension">
<xpath expr="//Dropdown/t[@t-set-slot='content']" position="inside">
<t t-if="this.isStudioEditable ? this.isStudioEditable() : false">
<div t-if="hasOptionalFields" class="dropdown-divider"/>
<DropdownItem closingMode="'none'" onSelected="() => this.onSelectedAddCustomField()" class="'dropdown-item-studio'">
<i class="fa fa-plus fa-fw me-2"/>
<span>Add Custom Field</span>
</DropdownItem>
</t>
</xpath>
</t>
</templates>

View File

@ -0,0 +1,31 @@
/** @odoo-module */
import { patch } from "@web/core/utils/patch";
import { PivotRenderer } from "@web/views/pivot/pivot_renderer";
import { useEffect, useRef } from "@odoo/owl";
patch(PivotRenderer.prototype, {
setup() {
super.setup();
this.root = useRef("root");
if (this.env.isSmall) {
useEffect(() => {
if (this.root.el) {
const tooltipElems = this.root.el.querySelectorAll("*[data-tooltip]");
for (const el of tooltipElems) {
el.removeAttribute("data-tooltip");
el.removeAttribute("data-tooltip-position");
}
}
});
}
},
getPadding(cell) {
if (this.env.isSmall) {
return 5 + cell.indent * 5;
}
return super.getPadding(...arguments);
},
});

View File

@ -0,0 +1,27 @@
@include media-breakpoint-down(md) {
.o_pivot {
height: 100%;
.dropdown.show {
> .dropdown-toggle::after {
@include o-caret-down;
}
}
th > .o_group_by_menu > .dropdown-menu {
.dropdown-item {
// caret centered vertically
.dropdown-toggle::after{
top: 12px;
}
// nested dropdown should be *under* the parent, not on its side
.dropdown-menu {
top: initial !important;
left: 5% !important;
width: 95%;
}
}
}
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-inherit="web.PivotRenderer" t-inherit-mode="extension">
<xpath expr="//div[hasclass('o_pivot')]" position="attributes">
<attribute name="t-ref">root</attribute>
</xpath>
</t>
</templates>

View File

@ -0,0 +1,21 @@
/** @odoo-module **/
import { BurgerMenu } from "@web/webclient/burger_menu/burger_menu";
import { useService } from "@web/core/utils/hooks";
import { registry } from "@web/core/registry";
export class EnterpriseBurgerMenu extends BurgerMenu {
setup() {
super.setup();
this.hm = useService("home_menu");
}
get currentApp() {
return !this.hm.hasHomeMenu && super.currentApp;
}
}
const systrayItem = {
Component: EnterpriseBurgerMenu,
};
registry.category("systray").add("burger_menu", systrayItem, { sequence: 0, force: true });

View File

@ -0,0 +1,6 @@
// = Burger Menu Variables
// ============================================================================
// No CSS hacks, variables overrides only
$o-burger-topbar-bg: $o-gray-100 !default;
$o-burger-topbar-color: $o-gray-900 !default;

View File

@ -0,0 +1,19 @@
/** @odoo-module **/
import { _t } from "@web/core/l10n/translation";
import { cookie as cookieManager } from "@web/core/browser/cookie";
export function switchColorSchemeItem(env) {
return {
type: "switch",
id: "color_scheme.switch_theme",
description: _t("Dark Mode"),
callback: () => {
const cookie = cookieManager.get("color_scheme");
const scheme = cookie === "dark" ? "light" : "dark";
env.services.color_scheme.switchToColorScheme(scheme);
},
isChecked: cookieManager.get("color_scheme") === "dark",
sequence: 30,
};
}

View File

@ -0,0 +1,27 @@
import { registry } from "@web/core/registry";
import { browser } from "@web/core/browser/browser";
import { cookie } from "@web/core/browser/cookie";
import { switchColorSchemeItem } from "./color_scheme_menu_items";
const serviceRegistry = registry.category("services");
const userMenuRegistry = registry.category("user_menuitems");
export const colorSchemeService = {
dependencies: ["ui"],
start(env, { ui }) {
userMenuRegistry.add("color_scheme.switch", switchColorSchemeItem);
return {
switchToColorScheme: (scheme) => {
cookie.set("color_scheme", scheme);
ui.block();
this.reload();
},
};
},
reload() {
browser.location.reload();
},
};
serviceRegistry.add("color_scheme", colorSchemeService);

View File

@ -0,0 +1,16 @@
// = Home Menu
// ============================================================================
// No CSS hacks, variables overrides only
.o_home_menu_background {
.o_app_icon {
--AppSwitcherIcon-background: #{rgba(#fff, .05)};
--AppSwitcherIcon-inset-shadow: #{inset 0 0 0 1px rgba(#fff, .1)};
--AppSwitcherIcon-border-color: transparent;
}
.o_app:hover .o_app_icon {
--AppSwitcherIcon-inset-shadow: #{inset 0 0 0 1px rgba(#fff, .2)};
}
}

View File

@ -0,0 +1,375 @@
/** @odoo-module **/
import { hasTouch, isIosApp, isMacOS } from "@web/core/browser/feature_detection";
import { useHotkey } from "@web/core/hotkeys/hotkey_hook";
import { user } from "@web/core/user";
import { useService } from "@web/core/utils/hooks";
import { useSortable } from "@web/core/utils/sortable_owl";
import {
Component,
useExternalListener,
onMounted,
onPatched,
onWillUpdateProps,
useState,
useRef,
} from "@odoo/owl";
class FooterComponent extends Component {
static template = "ica_web_responsive.HomeMenu.CommandPalette.Footer";
static props = {
//prop added by the command palette
switchNamespace: { type: Function, optional: true },
};
setup() {
this.controlKey = isMacOS() ? "COMMAND" : "CONTROL";
}
}
/**
* Home menu
*
* This component handles the display and navigation between the different
* available applications and menus.
* @extends Component
*/
export class HomeMenu extends Component {
static template = "ica_web_responsive.HomeMenu";
static components = { };
static props = {
apps: {
type: Array,
element: {
type: Object,
shape: {
actionID: Number,
href: String,
appID: Number,
id: Number,
label: String,
parents: String,
webIcon: {
type: [
Boolean,
String,
{
type: Object,
optional: 1,
shape: {
iconClass: String,
color: String,
backgroundColor: String,
},
},
],
optional: true,
},
webIconData: { type: String, optional: 1 },
xmlid: String,
},
},
},
reorderApps: { type: Function },
};
/**
* @param {Object} props
* @param {Object[]} props.apps application icons
* @param {number} props.apps[].actionID
* @param {number} props.apps[].id
* @param {string} props.apps[].label
* @param {string} props.apps[].parents
* @param {(boolean|string|Object)} props.apps[].webIcon either:
* - boolean: false (no webIcon)
* - string: path to Odoo icon file
* - Object: customized icon (background, class and color)
* @param {string} [props.apps[].webIconData]
* @param {string} props.apps[].xmlid
* @param {function} props.reorderApps
*/
setup() {
this.command = useService("command");
this.menus = useService("menu");
this.homeMenuService = useService("home_menu");
this.ui = useService("ui");
this.state = useState({
focusedIndex: null,
isIosApp: isIosApp(),
});
this.inputRef = useRef("input");
this.rootRef = useRef("root");
this.pressTimer;
if (!this.env.isSmall) {
this._registerHotkeys();
}
useSortable({
enable: this._enableAppsSorting,
// Params
ref: this.rootRef,
elements: ".o_draggable",
cursor: "move",
delay: 500,
tolerance: 10,
// Hooks
onWillStartDrag: (params) => this._sortStart(params),
onDrop: (params) => this._sortAppDrop(params),
});
onWillUpdateProps(() => {
// State is reset on each remount
this.state.focusedIndex = null;
});
onMounted(() => {
if (!hasTouch()) {
this._focusInput();
}
});
onPatched(() => {
if (this.state.focusedIndex !== null && !this.env.isSmall) {
const selectedItem = document.querySelector(".o_home_menu .o_menuitem.o_focused");
// When TAB is managed externally the class o_focused disappears.
if (selectedItem) {
// Center window on the focused item
selectedItem.scrollIntoView({ block: "center" });
}
}
});
}
//--------------------------------------------------------------------------
// Getters
//--------------------------------------------------------------------------
/**
* @returns {Object[]}
*/
get displayedApps() {
return this.props.apps;
}
/**
* @returns {number}
*/
get maxIconNumber() {
const w = window.innerWidth;
if (w < 576) {
return 3;
} else if (w < 768) {
return 4;
} else {
return 6;
}
}
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @private
* @param {Object} menu
* @returns {Promise}
*/
_openMenu(menu) {
return this.menus.selectMenu(menu);
}
/**
* Update this.state.focusedIndex if not null.
* @private
* @param {string} cmd
*/
_updateFocusedIndex(cmd) {
const nbrApps = this.displayedApps.length;
const lastIndex = nbrApps - 1;
const focusedIndex = this.state.focusedIndex;
if (lastIndex < 0) {
return;
}
if (focusedIndex === null) {
this.state.focusedIndex = 0;
return;
}
const lineNumber = Math.ceil(nbrApps / this.maxIconNumber);
const currentLine = Math.ceil((focusedIndex + 1) / this.maxIconNumber);
let newIndex;
switch (cmd) {
case "previousElem":
newIndex = focusedIndex - 1;
break;
case "nextElem":
newIndex = focusedIndex + 1;
break;
case "previousColumn":
if (focusedIndex % this.maxIconNumber) {
// app is not the first one on its line
newIndex = focusedIndex - 1;
} else {
newIndex =
focusedIndex + Math.min(lastIndex - focusedIndex, this.maxIconNumber - 1);
}
break;
case "nextColumn":
if (focusedIndex === lastIndex || (focusedIndex + 1) % this.maxIconNumber === 0) {
// app is the last one on its line
newIndex = (currentLine - 1) * this.maxIconNumber;
} else {
newIndex = focusedIndex + 1;
}
break;
case "previousLine":
if (currentLine === 1) {
newIndex = focusedIndex + (lineNumber - 1) * this.maxIconNumber;
if (newIndex > lastIndex) {
newIndex = lastIndex;
}
} else {
// we go to the previous line on same column
newIndex = focusedIndex - this.maxIconNumber;
}
break;
case "nextLine":
if (currentLine === lineNumber) {
newIndex = focusedIndex % this.maxIconNumber;
} else {
// we go to the next line on the closest column
newIndex =
focusedIndex + Math.min(this.maxIconNumber, lastIndex - focusedIndex);
}
break;
}
// if newIndex is out of bounds -> normalize it
if (newIndex < 0) {
newIndex = lastIndex;
} else if (newIndex > lastIndex) {
newIndex = 0;
}
this.state.focusedIndex = newIndex;
}
_focusInput() {
if (!this.env.isSmall && this.inputRef.el) {
this.inputRef.el.focus({ preventScroll: true });
}
}
_enableAppsSorting() {
return true;
}
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @param {Object} params
* @param {HTMLElement} params.element
* @param {HTMLElement} params.previous
*/
_sortAppDrop({ element, previous }) {
const order = this.props.apps.map((app) => app.xmlid);
const elementId = element.children[0].dataset.menuXmlid;
const elementIndex = order.indexOf(elementId);
// first remove dragged element
order.splice(elementIndex, 1);
if (previous) {
const prevIndex = order.indexOf(previous.children[0].dataset.menuXmlid);
// insert dragged element after previous element
order.splice(prevIndex + 1, 0, elementId);
} else {
// insert dragged element at beginning if no previous element
order.splice(0, 0, elementId);
}
// apply new order
this.props.reorderApps(order);
user.setUserSettings("homemenu_config", JSON.stringify(order));
}
/**
* @param {Object} params
* @param {HTMLElement} params.element
*/
_sortStart({ element, addClass }) {
addClass(element.children[0], "o_dragged_app");
}
/**
* @private
* @param {Object} app
*/
_onAppClick(app) {
this._openMenu(app);
}
/**
* @private
*/
_registerHotkeys() {
const hotkeys = [
["ArrowDown", () => this._updateFocusedIndex("nextLine")],
["ArrowRight", () => this._updateFocusedIndex("nextColumn")],
["ArrowUp", () => this._updateFocusedIndex("previousLine")],
["ArrowLeft", () => this._updateFocusedIndex("previousColumn")],
["Tab", () => this._updateFocusedIndex("nextElem")],
["shift+Tab", () => this._updateFocusedIndex("previousElem")],
[
"Enter",
() => {
const menu = this.displayedApps[this.state.focusedIndex];
if (menu) {
this._openMenu(menu);
}
},
],
["Escape", () => this.homeMenuService.toggle(false)],
];
hotkeys.forEach((hotkey) => {
useHotkey(...hotkey, {
allowRepeat: true,
});
});
useExternalListener(window, "keydown", this._onKeydownFocusInput);
}
_onKeydownFocusInput() {
if (
document.activeElement !== this.inputRef.el &&
this.ui.activeElement === document &&
!["TEXTAREA", "INPUT"].includes(document.activeElement.tagName)
) {
this._focusInput();
}
}
_onInputSearch() {
const onClose = () => {
this._focusInput();
this.inputRef.el.value = "";
};
const searchValue = this.compositionStart ? "/" : `/${this.inputRef.el.value.trim()}`;
this.compositionStart = false;
this.command.openMainPalette({ searchValue, FooterComponent }, onClose);
}
_onInputBlur() {
if (hasTouch()) {
return;
}
// if we blur search input to focus on body (eg. click on any
// non-interactive element) restore focus to avoid IME input issue
setTimeout(() => {
if (document.activeElement === document.body && this.ui.activeElement === document) {
this._focusInput();
}
}, 0);
}
_onCompositionStart() {
this.compositionStart = true;
}
}

View File

@ -0,0 +1,140 @@
.o_home_menu_background {
// 'Home menu background' design is shared with enterprise login
// screens and it's located in './home_menu_background.scss'
// When applied on webclient (note: we do not specify the webclient class
// here to avoid breaking studio custom style)
&:not(.o_home_menu_background_custom):not(.o_in_studio) .o_main_navbar {
background: transparent;
border-bottom-color: transparent;
.o_dropdown_active,
> ul > li.show > a {
outline: none;
}
}
&.o_home_menu_background_custom .o_home_menu {
background: {
size: cover;
repeat: no-repeat;
position: center;
}
}
.o_menu_systray {
@include print-variable(o-navbar-badge-bg, $o-navbar-badge-bg);
}
}
.o_home_menu {
font-size: $font-size-base;
.container {
@include media-breakpoint-up(md) {
max-width: $o-home-menu-container-size !important;
}
}
.o_app {
.o_app_icon {
width: $o-home-menu-app-icon-max-width;
aspect-ratio: 1;
padding: 10px;
background-color: var(--AppSwitcherIcon-background, rgba(#fff, 1));
object-fit: cover;
transform-origin: center bottom;
transition: box-shadow ease-in 0.1s, transform ease-in 0.1s;
box-shadow: var(--AppSwitcherIcon-inset-shadow, inset 0 0 0 1px rgba(0,0,0, .2)),
0 1px 1px rgba(#000, .02),
0 2px 2px rgba(#000, .02),
0 4px 4px rgba(#000, .02),
0 8px 8px rgba(#000, .02),
0 16px 16px rgba(#000, .02);
.fa {
font-size: $o-home-menu-app-icon-max-width * 0.5;
}
}
&:hover .o_app_icon {
box-shadow: var(--AppSwitcherIcon-inset-shadow, inset 0 0 0 1px rgba(0,0,0, .2)),
0 2px 2px rgba(#000, .03),
0 4px 4px rgba(#000, .03),
0 8px 8px rgba(#000, .03),
0 12px 12px rgba(#000, .03),
0 24px 24px rgba(#000, .03);
transform: translateY(-2px);
}
&:active .o_app_icon {
transform: translateY(-2px) scale(.98);
transition: none;
}
.o_caption {
color: var(--homeMenuCaption-color, #{$o-home-menu-caption-color});
text-shadow: $o-home-menu-caption-shadow;
}
&.o_focused {
background: $component-active-bg;
outline: 1px solid $o-action;
border-radius: $border-radius;
}
}
.o_dragged_app {
transition: transform 0.5s;
transform: rotate(6deg);
.o_app_icon {
box-shadow: 0 8px 15px -10px black;
transform: translateY(-1px);
}
}
// iOS iPhone list layout due to Apple AppStore review
@include media-breakpoint-down(md) {
&.o_ios_app {
.o_apps {
flex-direction: column;
font-size: $o-home-menu-font-size-base * 1.25;
margin-top: map-get($spacers, 1);
padding: 0 map-get($spacers, 2);
> *, .o_app {
width: 100%;
}
}
.o_app {
flex-direction: row !important;
justify-content: initial !important;
background-color: rgba(255, 255, 255, 0.1);
padding: map-get($spacers, 3) map-get($spacers, 4) !important;
}
.o_app_icon {
width: $o-home-menu-app-icon-max-width * 0.75;
height: $o-home-menu-app-icon-max-width * 0.75;
margin-right: map-get($spacers, 4);
}
.o_caption {
text-align: start !important;
}
}
&:not(.o_ios_app) .o_caption {
font-size: $font-size-sm;
font-weight: $font-weight-bold;
}
}
}
.o_home_menu_background_custom {
.o_home_menu .o_app .o_caption {
color: $o-home-menu-custom-caption-color;
text-shadow: $o-home-menu-custom-caption-shadow;
}
}

View File

@ -0,0 +1,6 @@
// = Home Menu Variables
// ============================================================================
// No CSS hacks, variables overrides only
$o-home-menu-caption-color: $o-black !default;
$o-home-menu-caption-shadow: 0 1px 2px rgba(0, 0, 0, .75), 0 2px 5px rgba(0, 0, 0, .05), 0 0 5px rgba(0, 0, 0, .05) !default;

View File

@ -0,0 +1,9 @@
$o-home-menu-font-size-base: 1rem;
$o-home-menu-container-size: 850px;
$o-home-menu-app-icon-max-width: 70px;
$o-home-menu-caption-color: $o-gray-700 !default;
$o-home-menu-caption-shadow: none !default;
$o-home-menu-custom-caption-color: #fff !default;
$o-home-menu-custom-caption-shadow: 0 1px 2px rgba(0, 0, 0, .75), 0 2px 5px rgba(0, 0, 0, .05), 0 0 5px rgba(0, 0, 0, .05) !default;

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="ica_web_responsive.HomeMenu">
<div t-ref="root" class="o_home_menu h-100 overflow-auto" t-att-class="{ o_ios_app: state.isIosApp }">
<div class="container">
<input t-ref="input" type="text" class="o_search_hidden visually-hidden w-auto" data-allow-hotkeys="true" t-on-input="_onInputSearch" t-on-blur="_onInputBlur" t-on-compositionstart="_onCompositionStart"
role="combobox"
t-att-aria-activedescendant="'result_app_' + state.focusedIndex"
t-att-aria-expanded="displayedApps.length ? 'true' : 'false'"
aria-autocomplete="list"
aria-haspopup="listbox"
/>
<!-- When the subscription has expired, the expiration panel is show over the whole UI instead of here -->
<div t-if="displayedApps.length" role="listbox" class="o_apps row user-select-none mt-5 mx-0">
<div t-foreach="displayedApps" t-as="app" t-key="app.id" class="col-3 col-md-2 o_draggable mb-3 px-0">
<a t-att-id="'result_app_' + app_index"
role="option"
t-att-aria-selected="state.focusedIndex === app_index ? 'true' : 'false'"
class="o_app o_menuitem d-flex flex-column rounded-3 justify-content-start align-items-center w-100 p-1 p-md-2"
t-att-class="{o_focused: state.focusedIndex === app_index}"
t-att-data-menu-xmlid="app.xmlid"
t-att-href="app.href"
t-on-click.prevent="() => this._onAppClick(app)"
>
<img t-if="app.webIconData" class="o_app_icon rounded-3"
t-attf-src="{{app.webIconData}}"
/>
<div t-else="" class="o_app_icon position-relative d-flex justify-content-center align-items-center p-2 rounded-3 ratio ratio-1x1"
t-attf-style="background-color: {{app.webIcon.backgroundColor}};"
>
<i t-attf-class="{{app.webIcon.iconClass}} position-relative w-auto h-auto" t-attf-style="color: {{app.webIcon.color}};"/>
</div>
<div class="o_caption w-100 text-center text-truncate mt-2" t-esc="app.label or app.name"/>
</a>
</div>
</div>
<div t-elif="!displayedApps.length" id="result_menu_0" role="option" aria-selected="true" class="o_no_result">
No result
</div>
</div>
</div>
</t>
<t t-name="ica_web_responsive.HomeMenu.CommandPalette.Footer">
<span>
<span class='fw-bolder text-primary'>TIP</span> — open me anywhere with <span t-esc="controlKey" class='fw-bolder text-primary'/> + <span class='fw-bolder text-primary'>K</span>
</span>
</t>
</templates>

View File

@ -0,0 +1,8 @@
// = Home Menu Background
// ============================================================================
// No CSS hacks, variables overrides only
.o_home_menu_background {
--homeMenu-bg-color: #000511;
--homeMenu-bg-image: url("/ica_web_responsive/static/img/background-dark.jpg");
}

View File

@ -0,0 +1,9 @@
// Shared with web client and login screen
.o_home_menu_background, .o_web_client.o_home_menu_background {
background: {
size: cover;
attachment: fixed;
color: var(--homeMenu-bg-color, #{$o-gray-200});
image: var(--homeMenu-bg-image, url("/ica_web_responsive/static/img/background-light.svg"));
}
}

View File

@ -0,0 +1,96 @@
/** @odoo-module **/
import { _t } from "@web/core/l10n/translation";
import { registry } from "@web/core/registry";
import { user } from "@web/core/user";
import { Mutex } from "@web/core/utils/concurrency";
import { useService } from "@web/core/utils/hooks";
import { computeAppsAndMenuItems, reorderApps } from "@web/webclient/menus/menu_helpers";
import {
ControllerNotFoundError,
standardActionServiceProps,
} from "@web/webclient/actions/action_service";
import { HomeMenu } from "./home_menu";
import { Component, onMounted, onWillUnmount, useState, reactive, xml } from "@odoo/owl";
export const homeMenuService = {
dependencies: ["action"],
start(env) {
const state = reactive({
hasHomeMenu: false, // true iff the HomeMenu is currently displayed
hasBackgroundAction: false, // true iff there is an action behind the HomeMenu
toggle,
});
const mutex = new Mutex(); // used to protect against concurrent toggling requests
class HomeMenuAction extends Component {
static components = { HomeMenu };
static target = "current";
static props = { ...standardActionServiceProps };
static template = xml`<HomeMenu t-props="homeMenuProps"/>`;
static displayName = _t("Home");
setup() {
this.menus = useService("menu");
const homemenuConfig = JSON.parse(user.settings?.homemenu_config || "null");
const apps = useState(
computeAppsAndMenuItems(this.menus.getMenuAsTree("root")).apps
);
if (homemenuConfig) {
reorderApps(apps, homemenuConfig);
}
this.homeMenuProps = {
apps: apps,
reorderApps: (order) => {
reorderApps(apps, order);
},
};
onMounted(() => this.onMounted());
onWillUnmount(this.onWillUnmount);
}
async onMounted() {
const { breadcrumbs } = this.env.config;
state.hasHomeMenu = true;
state.hasBackgroundAction = breadcrumbs.length > 0;
this.env.bus.trigger("HOME-MENU:TOGGLED");
}
onWillUnmount() {
state.hasHomeMenu = false;
state.hasBackgroundAction = false;
this.env.bus.trigger("HOME-MENU:TOGGLED");
}
}
registry.category("actions").add("menu", HomeMenuAction);
env.bus.addEventListener("HOME-MENU:TOGGLED", () => {
document.body.classList.toggle("o_home_menu_background", state.hasHomeMenu);
});
async function toggle(show) {
return mutex.exec(async () => {
show = show === undefined ? !state.hasHomeMenu : Boolean(show);
if (show !== state.hasHomeMenu) {
if (show) {
await env.services.action.doAction("menu");
} else {
try {
await env.services.action.restore();
} catch (err) {
if (!(err instanceof ControllerNotFoundError)) {
throw err;
}
}
}
}
// hack: wait for a tick to ensure that the url has been updated before
// switching again
return new Promise((r) => setTimeout(r));
});
}
return state;
},
};
registry.category("services").add("home_menu", homeMenuService);

View File

@ -0,0 +1,14 @@
// = Navbar
// ============================================================================
// No CSS hacks, variables overrides only
.o_main_navbar {
--o-navbar-badge-color: #{$black};
--o-navbar-badge-text-shadow: none;
--NavBar-menuToggle-color: #{$o-black};
--NavBar-brand-color: #{$o-gray-800};
--NavBar-entry-borderColor-active: #{darken($o-action, 10%)};
--NavBar-entry-backgroundColor--active: #{mix($o-action, $o-gray-100, 15%)};
--NavBar-entry-backgroundColor--hover: #{$o-gray-300};
--NavBar-entry-backgroundColor--focus: #{$o-gray-300};
}

View File

@ -0,0 +1,77 @@
/** @odoo-module **/
import { NavBar } from "@web/webclient/navbar/navbar";
import { useService, useBus } from "@web/core/utils/hooks";
import { _t } from "@web/core/l10n/translation";
import { useState, useEffect, useRef } from "@odoo/owl";
export class EnterpriseNavBar extends NavBar {
static template = "ica_web_responsive.EnterpriseNavBar";
setup() {
super.setup();
this.hm = useState(useService("home_menu"));
this.pwa = useService("pwa");
this.menuAppsRef = useRef("menuApps");
this.navRef = useRef("nav");
this._busToggledCallback = () => this._updateMenuAppsIcon();
useBus(this.env.bus, "HOME-MENU:TOGGLED", this._busToggledCallback);
useEffect(() => this._updateMenuAppsIcon());
}
get hasBackgroundAction() {
return this.hm.hasBackgroundAction;
}
get isInApp() {
return !this.hm.hasHomeMenu;
}
_openAppMenuSidebar() {
if (this.hm.hasHomeMenu) {
this.hm.toggle(false);
} else {
this.state.isAppMenuSidebarOpened = true;
}
}
_updateMenuAppsIcon() {
const menuAppsEl = this.menuAppsRef.el;
menuAppsEl.classList.toggle("o_hidden", !this.isInApp && !this.hasBackgroundAction);
menuAppsEl.classList.toggle(
"o_menu_toggle_back",
!this.isInApp && this.hasBackgroundAction
);
if (!this.isScopedApp) {
const title =
!this.isInApp && this.hasBackgroundAction ? _t("Previous view") : _t("Home menu");
menuAppsEl.title = title;
menuAppsEl.ariaLabel = title;
}
const menuBrand = this.navRef.el.querySelector(".o_menu_brand");
if (menuBrand) {
menuBrand.classList.toggle("o_hidden", !this.isInApp);
}
const menuBrandIcon = this.navRef.el.querySelector(".o_menu_brand_icon");
if (menuBrandIcon) {
menuBrandIcon.classList.toggle("o_hidden", !this.isInApp);
}
const appSubMenus = this.appSubMenus.el;
if (appSubMenus) {
appSubMenus.classList.toggle("o_hidden", !this.isInApp);
}
const breadcrumb = this.navRef.el.querySelector(".o_breadcrumb");
if (breadcrumb) {
breadcrumb.classList.toggle("o_hidden", !this.isInApp);
}
}
/**
* @override
*/
onAllAppsBtnClick() {
super.onAllAppsBtnClick();
this.hm.toggle(true);
this._closeAppMenuSidebar();
}
}

View File

@ -0,0 +1,22 @@
// = Main Navbar
// ============================================================================
.o_main_navbar {
--NavBar-entry-color--active: #{$o-component-active-color};
--NavBar-entry-borderColor-active: #{$o-component-active-border};
--NavBar-entry-backgroundColor--active: #{$o-component-active-bg};
--NavBar-entry-backgroundColor--hover: #{$o-gray-200};
--NavBar-entry-backgroundColor--focus: #{$o-gray-200};
--Dropdown_menu-margin-y: #{map-get($spacers, 1)};
.o_menu_toggle {
color: var(--NavBar-menuToggle-color, #{$o-brand-odoo});
}
}
// Ensuring SuperUser Design menu is not compressed in Enterprise
// ============================================================================
body.o_is_superuser .o_menu_systray {
border-image-outset: map-get($border-widths, 5);
}

View File

@ -0,0 +1,6 @@
// = Navbar Variables
// ============================================================================
// No CSS hacks, variables overrides only
$o-navbar-background: $o-view-background-color !default;
$o-navbar-entry-color: $o-gray-900 !default;

View File

@ -0,0 +1,25 @@
// = Enterprise Main Navbar Variables
// ============================================================================
$o-navbar-background: $o-white !default;
$o-navbar-padding-v: 10px !default;
$o-navbar-border-bottom: 0 !default;
$o-navbar-font-size: $o-font-size-base !default;
$o-navbar-entry-margin-h: 1px !default;
$o-navbar-entry-border-radius: $o-border-radius !default;
$o-navbar-entry-color: $o-gray-800 !default;
$o-navbar-entry-padding-h: .63em !default;
$o-navbar-entry-bg--hover: $o-gray-200 !default;
$o-navbar-entry-color--hover: $o-gray-900 !default;
$o-navbar-entry-bg--active: unset !default;
$o-navbar-entry-color--active: unset !default;
$o-navbar-brand-color: $o-gray-700 !default;
$o-navbar-badge-size: .7em !default;
$o-navbar-badge-padding: 6px !default;
$o-navbar-badge-bg: $o-danger !default;
$o-navbar-badge-color: $o-white !default;
$o-navbar-badge-text-shadow: none !default;

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="ica_web_responsive.EnterpriseNavBar" t-inherit="web.NavBar" t-inherit-mode="primary">
<xpath expr="//nav" position="attributes">
<attribute name="t-ref">nav</attribute>
</xpath>
<xpath expr="//t[@t-call='web.NavBar.AppsMenu']" position="replace">
<a t-if="!isScopedApp" href="/odoo" class="o_menu_toggle border-0" t-att-class="{'hasImage': currentApp?.webIconData}" accesskey="h" t-ref="menuApps" t-on-click.prevent="() => { env.isSmall ? this._openAppMenuSidebar() : this.hm.toggle() }">
<t t-if="env.isSmall and !hm.hasHomeMenu">
<t t-call="web.NavBar.AppsMenu.Sidebar"/>
</t>
<t t-else="">
<svg class="o_menu_toggle_icon pe-none" width="14px" height="14px" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg">
<g t-foreach="[0, 5, 10]" t-as="Y" t-att-id="'o_menu_toggle_row_' + Y_index" fill="currentColor" t-key="'o_menu_toggle_row_' + Y_index">
<rect t-foreach="[0, 5, 10]" t-as="X" width="3" height="3" t-att-x="X" t-att-y="Y" t-key="'o_menu_toggle_cell_' + X_index"/>
</g>
</svg>
<t t-if="!env.isSmall and currentApp">
<img
t-if="currentApp.webIconData"
t-att-src="currentApp.webIconData"
class="o_menu_brand_icon d-inline position-absolute start-0 h-100 ps-1 ms-2"
t-att-alt="currentApp.name"
t-ref="appIcon"/>
<span t-esc="currentApp.name" class="o_menu_brand d-flex ms-3 pe-0"/>
</t>
</t>
</a>
<a t-else="" t-att-href="pwa.startUrl" class="o_menu_toggle" t-ref="menuApps">
<img
t-if="currentApp &amp;&amp; currentApp.webIconData"
t-att-src="currentApp.webIconData"
class="o_menu_brand_icon d-none d-lg-inline position-absolute start-0 h-100 ps-1 ms-2"
t-att-alt="currentApp.name"
t-ref="appIcon"/>
<span
t-if="currentApp"
t-esc="currentApp.name"
class="o_menu_brand d-none d-md-flex ps-4 pe-0"/>
</a>
</xpath>
<xpath expr="//DropdownItem[@t-esc='currentApp.name']" position="replace"/>
</t>
<t t-name="ica_web_responsive.EnterpriseNavBar.SectionsMenu" t-inherit="web.NavBar.SectionsMenu" t-inherit-mode="extension">
<xpath expr="//Dropdown/button" position="attributes">
<attribute name="class" add="fw-normal" separator=" "/>
</xpath>
</t>
</templates>

View File

@ -0,0 +1,56 @@
/** @odoo-module */
import { browser } from "@web/core/browser/browser";
import { Dialog } from "@web/core/dialog/dialog";
import { useChildRef, useService } from "@web/core/utils/hooks";
import { Component, useExternalListener } from "@odoo/owl";
export class PromoteStudioDialog extends Component {
static template = "ica_web_responsive.PromoteStudioDialog";
static components = { Dialog };
static props = {
title: String,
close: Function,
};
setup() {
this.ormService = useService("orm");
this.uiService = useService("ui");
this.modalRef = useChildRef();
useExternalListener(window, "mousedown", this.onWindowMouseDown);
}
async onClickInstallStudio() {
this.disableClick = true;
this.uiService.block();
const modules = await this.ormService.searchRead(
"ir.module.module",
[["name", "=", "web_studio"]],
["id"]
);
await this.ormService.call("ir.module.module", "button_immediate_install", [
[modules[0].id],
]);
// on rpc call return, the framework unblocks the page
// make sure to keep the page blocked until the reload ends.
this.uiService.unblock();
browser.localStorage.setItem("openStudioOnReload", "main");
browser.location.reload();
}
/**
* Close the dialog on outside click.
*/
onWindowMouseDown(ev) {
const dialogContent = this.modalRef.el.querySelector(".modal-content");
if (!this.disableClick && !dialogContent.contains(ev.target)) {
this.props.close();
}
}
}
export class PromoteStudioAutomationDialog extends PromoteStudioDialog {
static template = "ica_web_responsive.PromoteStudioAutomationDialog";
}

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<template>
<t t-name="ica_web_responsive.PromoteStudioDialog">
<Dialog title="props.title" modalRef="modalRef">
<div class="modal-studio">
<div id="studio_install_dialog"
class="d-flex flex-row align-items-center flex-wrap">
<div id="studio_dialog_pitch"
class="w-100 w-md-50">
<h4>Want to tailor-make your Odoo?</h4>
<p>Unleash the power of Odoo Studio:</p>
<ul>
<li>Create automation rules</li>
<li>Customize any screen</li>
<li>Customize Reports</li>
<li>Build new reports</li>
<li>Build new apps from scratch</li>
<li>Define webhooks</li>
<li>and more!</li>
</ul>
<a role="button" class="btn btn-secondary btn-block"
href="https://www.odoo.com/app/studio" target="_blank"> Learn More <i
class="fa fa-external-link" />
</a>
</div>
<div id="studio_video" class="o_video_embed w-100 w-md-50 ratio ratio-16x9">
<iframe class="embed-responsive-item"
t-attf-src="https://www.youtube.com/embed/xCvFZrrQq7k?autoplay=1"
frameborder="0" allowfullscreen="true" />
</div>
</div>
<t t-set-slot="footer">
<button class="btn btn-primary btn-block o_install_studio"
t-on-click.stop="onClickInstallStudio"
data-tooltip="Install Odoo Studio and its dependencies">
Start using Odoo Studio
</button>
<button class="btn btn-secondary btn-block"
t-on-click="props.close">
Discard
</button>
</t>
</div>
</Dialog>
</t>
<t t-name="ica_web_responsive.PromoteStudioAutomationDialog"
t-inherit="ica_web_responsive.PromoteStudioDialog" t-inherit-mode="primary">
<xpath expr="//div[@id='studio_video']" position="replace">
<img class="w-100 w-md-50"
src="/ica_web_responsive/static/img/automation.svg" />
</xpath>
</t>
</template>

View File

@ -0,0 +1,16 @@
/** @odoo-module **/
import { isDisplayStandalone } from "@web/core/browser/feature_detection";
import { patch } from "@web/core/utils/patch";
import { BurgerMenu } from "@web/webclient/burger_menu/burger_menu";
import { shareUrl } from "./share_url";
if (navigator.share && isDisplayStandalone()) {
patch(BurgerMenu.prototype, {
shareUrl,
});
patch(BurgerMenu, {
template: "ica_web_responsive.BurgerMenu",
});
}

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="ica_web_responsive.BurgerMenu" t-inherit="web.BurgerMenu" t-inherit-mode="primary">
<xpath expr="//button[hasclass('o_sidebar_close')]" position="replace">
<div class="d-flex align-items-center h-100 bg-transparent">
<button class="o_burger_menu_share fa fa-share-alt btn border-0 fs-5 text-reset" aria-label="Share URL" title="Share URL" t-on-click.stop="shareUrl"/>
<button class="o_sidebar_close oi oi-close btn border-0 fs-2 text-reset" aria-label="Close menu" title="Close menu" t-on-click.stop="_closeBurger"/>
</div>
</xpath>
</t>
</templates>

View File

@ -0,0 +1,42 @@
/** @odoo-module **/
import { _t } from "@web/core/l10n/translation";
import { markup } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { browser } from "@web/core/browser/browser";
import { isDisplayStandalone } from "@web/core/browser/feature_detection";
import { escape } from "@web/core/utils/strings";
export async function shareUrl() {
await navigator
.share({
url: browser.location.href,
title: document.title,
})
.catch((e) => {
if (!(e instanceof DOMException && e.name === "AbortError")) {
throw e;
}
});
}
export function shareUrlMenuItem(env) {
const translatedText = _t("Share");
return {
type: "item",
hide: env.isSmall || !isDisplayStandalone(),
id: "share_url",
description: markup(
`<div class="d-flex align-items-center justify-content-between">
<span>${escape(translatedText)}</span>
<span class="fa fa-share-alt"></span>
</div>`
),
callback: shareUrl,
sequence: 25,
};
}
if (navigator.share) {
registry.category("user_menuitems").add("share_url", shareUrlMenuItem);
}

View File

@ -0,0 +1,19 @@
/** @odoo-module **/
import { WebClient } from "@web/webclient/webclient";
import { useService } from "@web/core/utils/hooks";
import { EnterpriseNavBar } from "./navbar/navbar";
export class WebClientEnterprise extends WebClient {
static components = {
...WebClient.components,
NavBar: EnterpriseNavBar,
};
setup() {
super.setup();
this.hm = useService("home_menu");
}
_loadDefaultApp() {
return this.hm.toggle(true);
}
}

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="webclient_login" inherit_id="web.login_layout">
<xpath expr="//t[@t-call='web.frontend_layout']/t[last()]" position="after">
<t t-set="body_classname" t-value="'o_home_menu_background'"/>
<t t-set="login_card_classes" t-value="'rounded-0 shadow-sm bg-white'"/>
</xpath>
</template>
<template id="webclient_bootstrap" inherit_id="web.webclient_bootstrap">
<xpath expr="//meta[@name='theme-color']" position="replace">
<meta name="theme-color" t-att-content="'#242733' if request.cookies.get('color_scheme') == 'dark' else '#714B67'"/>
</xpath>
</template>
</odoo>