1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-04 21:35:23 +02:00

feat(sidebar): add dark theme colors [EE-3666] (#7414)

This commit is contained in:
Chaim Lev-Ari 2022-08-10 07:12:20 +03:00 committed by GitHub
parent fb3a31a4fd
commit c3ce4d8b53
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
83 changed files with 1738 additions and 1200 deletions

View file

@ -0,0 +1,65 @@
import clsx from 'clsx';
import { PropsWithChildren } from 'react';
import ReactTooltip from 'react-tooltip';
import './BoxSelectorItem.css';
import { BoxSelectorOption } from './types';
interface Props<T extends number | string> {
radioName: string;
option: BoxSelectorOption<T>;
onChange?(value: T): void;
selectedValue: T;
disabled?: boolean;
tooltip?: string;
className?: string;
type?: 'radio' | 'checkbox';
}
export function BoxOption<T extends number | string>({
radioName,
option,
onChange = () => {},
selectedValue,
disabled,
tooltip,
className,
type = 'radio',
children,
}: PropsWithChildren<Props<T>>) {
const tooltipId = `box-option-${radioName}-${option.id}`;
return (
<div
className={clsx('box-selector-item', className)}
data-tip
data-for={tooltipId}
tooltip-append-to-body="true"
tooltip-placement="bottom"
tooltip-class="portainer-tooltip"
>
<input
type={type}
name={radioName}
id={option.id}
checked={option.value === selectedValue}
value={option.value}
disabled={disabled}
onChange={() => onChange(option.value)}
/>
<label htmlFor={option.id} data-cy={`${radioName}_${option.value}`}>
{children}
</label>
{tooltip && (
<ReactTooltip
place="bottom"
className="portainer-tooltip"
id={tooltipId}
>
{tooltip}
</ReactTooltip>
)}
</div>
);
}

View file

@ -13,26 +13,6 @@
}
}
.boxselector_wrapper input[type='radio']:checked + label,
.box-selector-item input[type='radio']:checked + label {
background-color: var(--bg-blocklist-hover-color) !important;
color: black !important;
border-radius: 8px;
border-color: var(--ui-blue-7);
padding: 15px;
box-shadow: none;
}
.boxselector_wrapper input[type='radio']:not(:disabled) + label,
.box-selector-item input[type='radio']:not(:disabled) + label {
background: var(--ui-gray-2);
color: var(--black-color) !important;
border-radius: 8px;
border-color: var(--ui-gray-5);
padding: 15px;
box-shadow: none;
}
.row.header {
background-color: var(--bg-body-color) !important;
margin-bottom: 5px !important;

View file

@ -1,6 +1,5 @@
.boxselector_wrapper > div,
.box-selector-item {
--selected-item-color: var(--ui-blue-6);
flex: 1;
}
@ -22,38 +21,40 @@
display: none;
}
.boxselector_wrapper input[type='radio']:not(:disabled) ~ label,
.box-selector-item input[type='radio']:not(:disabled) ~ label {
cursor: pointer;
background-color: var(--bg-boxselector-wrapper-disabled-color);
.boxselector_wrapper label,
.box-selector-item label {
@apply border border-solid;
@apply bg-gray-2 border-gray-5 text-black;
@apply th-dark:bg-gray-iron-10 th-dark:border-gray-neutral-8 th-dark:text-white;
font-weight: normal;
font-size: 12px;
display: block;
border-radius: 8px;
padding: 15px;
text-align: left;
box-shadow: var(--shadow-boxselector-color);
position: relative;
text-align: left;
height: 100%;
}
.boxselector_wrapper input[type='radio']:not(:disabled):hover ~ label:hover,
.box-selector-item input[type='radio']:not(:disabled):hover ~ label:hover {
/* not disabled */
.boxselector_wrapper input[type='radio']:not(:disabled) ~ label,
.box-selector-item input[type='radio']:not(:disabled) ~ label {
box-shadow: none;
cursor: pointer;
}
.boxselector_wrapper label,
.box-selector-item label {
font-weight: normal;
font-size: 12px;
display: block;
background: var(--bg-boxselector-color);
border: 1px solid var(--border-boxselector-color);
border-radius: 2px;
padding: 10px 10px 0 10px;
text-align: left;
box-shadow: var(--shadow-boxselector-color);
position: relative;
}
/* disabled */
.box-selector-item input:disabled + label,
.boxselector_wrapper label.boxselector_disabled {
background: var(--bg-boxselector-disabled-color) !important;
border-color: #787878;
color: #787878;
@apply !bg-white;
@apply th-dark:!bg-gray-7;
@apply th-highcontrast:!bg-black;
filter: opacity(0.3) grayscale(1);
cursor: not-allowed;
pointer-events: none;
}
@ -63,30 +64,19 @@
pointer-events: auto;
}
/* checked */
.boxselector_wrapper input[type='radio']:checked + label,
.box-selector-item input[type='radio']:checked + label {
color: white;
@apply bg-blue-3 border-blue-6;
@apply th-dark:bg-blue-10 th-dark:border-blue-7;
background-image: url(../../../assets/ico/checked.svg);
background-repeat: no-repeat;
background-position: right 15px top 15px;
border-color: var(--selected-item-color);
}
.box-selector-item input[type='radio']:checked:disabled + label {
color: #787878;
}
.boxselector_wrapper input[type='radio']:checked + label .box_selector_mask_icon {
color: var(--selected-item-color);
}
:root[theme='highcontrast'] .box_selector_mask_icon,
:root[theme='dark'] .box_selector_mask_icon {
color: var(--bg-boxselector-wrapper-disabled-color);
}
.box_selector_mask_icon {
color: var(--bg-boxselector-color);
border-radius: 8px;
padding: 15px;
box-shadow: none;
}
@media only screen and (max-width: 700px) {
@ -95,48 +85,23 @@
}
}
.box-selector-item.limited.business {
--selected-item-color: var(--BE-only);
}
.box-selector-item.limited.business label {
border-color: var(--BE-only);
border-width: 2px;
}
.box-selector-item .limited-icon {
position: absolute;
left: 1em;
top: calc(50% - 0.5em);
height: 1em;
}
@media (min-width: 992px) {
.box-selector-item .limited-icon {
left: 2em;
}
}
.box-selector-item.limited.business :checked + label {
background-color: initial;
color: initial;
.box-selector-item.limited.business label,
.box-selector-item.limited.business input[type='radio']:checked + label {
@apply border-warning-7 bg-warning-1 text-black;
@apply th-dark:bg-warning-3;
}
.boxselector_img_container {
width: 100%;
margin-bottom: 20px;
text-align: left;
}
.boxselector_img {
height: 48px;
width: 48px;
left: 5px;
line-height: 90px;
margin-bottom: 0;
}
.boxselector_icon,
.boxselector_icon img {
color: var(--ui-blue-8);
font-size: 90px;
display: block;
}
@ -149,16 +114,6 @@
padding-left: 20px;
}
.boxselector_wrapper input[type='radio']:not(:disabled) ~ label,
.box-selector-item input[type='radio']:not(:disabled) ~ label {
background-color: var(--ui-gray-2);
}
.boxselector_img_container {
line-height: 90px;
margin-bottom: 0;
}
.box-selector-item p {
margin-bottom: 0;
}

View file

@ -1,5 +1,4 @@
import clsx from 'clsx';
import ReactTooltip from 'react-tooltip';
import { isLimitedToBE } from '@/portainer/feature-flags/feature-flags.service';
import { Icon } from '@/react/components/Icon';
@ -7,6 +6,8 @@ import { Icon } from '@/react/components/Icon';
import './BoxSelectorItem.css';
import { BoxSelectorOption } from './types';
import { LimitedToBeIndicator } from './LimitedToBeIndicator';
import { BoxOption } from './BoxOption';
interface Props<T extends number | string> {
radioName: string;
@ -27,53 +28,38 @@ export function BoxSelectorItem<T extends number | string>({
}: Props<T>) {
const limitedToBE = isLimitedToBE(option.feature);
const tooltipId = `box-selector-item-${radioName}-${option.id}`;
const beIndicatorTooltipId = `box-selector-item-${radioName}-${option.id}-limited`;
return (
<div
className={clsx('box-selector-item', {
<BoxOption
className={clsx({
business: limitedToBE,
limited: limitedToBE,
})}
data-tip
data-for={tooltipId}
tooltip-append-to-body="true"
tooltip-placement="bottom"
tooltip-class="portainer-tooltip"
radioName={radioName}
option={option}
selectedValue={selectedValue}
disabled={disabled}
onChange={(value) => onChange(value, limitedToBE)}
tooltip={tooltip}
>
<input
type="radio"
name={radioName}
id={option.id}
checked={option.value === selectedValue}
value={option.value}
disabled={disabled}
onChange={() => onChange(option.value, limitedToBE)}
/>
<label htmlFor={option.id} data-cy={`${radioName}_${option.value}`}>
{limitedToBE && <i className="fas fa-briefcase limited-icon" />}
<div className="boxselector_img_container">
{!!option.icon && (
<Icon
icon={option.icon}
feather={option.featherIcon}
className="boxselector_icon space-right"
/>
)}
<>
{limitedToBE && (
<LimitedToBeIndicator tooltipId={beIndicatorTooltipId} />
)}
<div className={clsx({ 'opacity-30': limitedToBE })}>
<div className="boxselector_img_container">
{!!option.icon && (
<Icon
icon={option.icon}
feather={option.featherIcon}
className="boxselector_icon space-right"
/>
)}
</div>
<div className="boxselector_header">{option.label}</div>
<p className="box-selector-item-description">{option.description}</p>
</div>
<div className="boxselector_header">{option.label}</div>
<p className="box-selector-item-description">{option.description}</p>
</label>
{tooltip && (
<ReactTooltip
place="bottom"
className="portainer-tooltip"
id={tooltipId}
>
{tooltip}
</ReactTooltip>
)}
</div>
</>
</BoxOption>
);
}

View file

@ -0,0 +1,35 @@
import { HelpCircle } from 'react-feather';
import ReactTooltip from 'react-tooltip';
interface Props {
tooltipId: string;
}
export function LimitedToBeIndicator({ tooltipId }: Props) {
return (
<>
<div className="absolute left-0 top-0 w-full">
<div className="mx-auto max-w-fit bg-warning-4 rounded-b-lg py-1 px-3 flex gap-1 text-sm items-center">
<span className="text-warning-9">Pro Feature</span>
<HelpCircle
className="feather !text-warning-7"
data-tip
data-for={tooltipId}
tooltip-append-to-body="true"
tooltip-placement="top"
tooltip-class="portainer-tooltip"
/>
</div>
</div>
<ReactTooltip
className="portainer-tooltip"
id={tooltipId}
place="top"
delayHide={1000}
>
Business Edition feature. <br />
This feature is currently limited to Business Edition users only.
</ReactTooltip>
</>
);
}

View file

@ -53,7 +53,12 @@ export function Icon({ icon, feather, className, mode, size }: Props) {
if (icon.indexOf('svg-') === 0) {
const svgIcon = icon.replace('svg-', '');
return <Svg icon={svgIcon as keyof typeof SvgIcons} className={classes} />;
return (
<Svg
icon={svgIcon as keyof typeof SvgIcons}
className={clsx(classes, '!flex')}
/>
);
}
if (feather) {
@ -66,6 +71,10 @@ export function Icon({ icon, feather, className, mode, size }: Props) {
}
return (
<i className={clsx('fa', icon, classes)} aria-hidden="true" role="img" />
<i
className={clsx(icon.startsWith('fa-') ? `fa ${icon}` : icon, classes)}
aria-hidden="true"
role="img"
/>
);
}

View file

@ -36,7 +36,15 @@ export function HeaderTitle({ title, children }: PropsWithChildren<Props>) {
data-cy="userMenu-button"
aria-label="User menu toggle"
>
<User className="icon-nested-gray" />
<div
className={clsx(
'icon-badge text-lg !p-2 mr-1',
'bg-gray-4 text-gray-8',
'th-dark:bg-gray-warm-10 th-dark:text-gray-warm-7'
)}
>
<User className="feather" />
</div>
{user && <span>{user.Username}</span>}
<ChevronDown className={styles.arrowDown} />
</MenuButton>

View file

@ -25,11 +25,13 @@ export function WidgetTitle({
<div className="widget-header">
<div className="row">
<span className={clsx('pull-left', className)}>
<Icon
icon={icon}
feather={featherIcon}
className="icon-nested-blue icon-primary space-right"
/>
<div className="widget-icon">
<Icon
icon={icon}
feather={featherIcon}
className="space-right feather"
/>
</div>
<span>{title}</span>
</span>
<span className={clsx('pull-right', className)}>{children}</span>

View file

@ -9,6 +9,63 @@ export default {
title: 'Components/Buttons/Button',
} as Meta;
export function DifferentTheme() {
const colors = [
'primary',
'secondary',
'success',
'danger',
'dangerlight',
'warning',
'light',
'link',
] as const;
const themes = ['light', 'dark', 'highcontrast'] as const;
const states = ['', 'disabled'] as const;
return (
<table>
<thead>
<tr>
<th>Color/Theme</th>
{themes.map((theme) => (
<th key={theme} className="text-center">
{theme}
</th>
))}
</tr>
</thead>
<tbody>
{colors.map((color) => (
<tr key={color}>
<td>{color}</td>
{themes.map((theme) => (
<td
key={theme}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
theme={theme}
className="p-5"
style={{ background: 'var(--bg-body-color)' }}
>
{states.map((state) => (
<Button
color={color}
key={state}
disabled={state === 'disabled'}
>
{state} {color} button
</Button>
))}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}
function Template({
onClick,
color,

View file

@ -58,14 +58,7 @@ export function Button({
/* eslint-disable-next-line react/button-has-type */
type={type}
disabled={disabled}
className={clsx(
{
'opacity-60': disabled,
},
`btn btn-${color}`,
sizeClass(size),
className
)}
className={clsx(`btn btn-${color}`, sizeClass(size), className)}
onClick={onClick}
title={title}
// eslint-disable-next-line react/jsx-props-no-spreading

View file

@ -19,11 +19,13 @@ export function TableTitle({
return (
<div className="toolBar">
<div className="toolBarTitle">
<Icon
icon={icon}
feather={featherIcon}
className="icon-nested-blue icon-primary space-right"
/>
<div className="widget-icon">
<Icon
icon={icon}
feather={featherIcon}
className="space-right feather"
/>
</div>
{label}
</div>