Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,5 @@
},
"resolutions": {
"@types/scheduler": "< 0.23.0"
},
"packageManager": "[email protected]+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
}
1 change: 0 additions & 1 deletion packages/storybook/.storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ const config: StorybookConfig = {
`,
staticDirs: [
'../assets',
{ from: '../../themes/dist', to: '/themes' },
...getCodeEditorStaticDirs(__filename)
],
stories: [
Expand Down
1 change: 1 addition & 0 deletions packages/storybook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"start": "yarn generate:versions && storybook dev -p 6006 --no-open"
},
"dependencies": {
"@ant-design/colors": "7.1.0",
"@ark-ui/react": "5.25.1",
"@ovhcloud/ods-react": "19.2.1",
"@ovhcloud/ods-themes": "19.2.1",
Expand Down
12 changes: 5 additions & 7 deletions packages/storybook/src/components/storyGrid/StoryGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,22 +38,20 @@ function StoryGrid({ components, themeClass, themeVariables }: Props): JSX.Eleme
const iframeRefs = useRef<(HTMLIFrameElement | null)[]>([]);

const injectThemeIntoIframe = (iframe: HTMLIFrameElement) => {
if (!iframe?.contentDocument || !themeVariables || !themeClass) return;
if (!iframe?.contentDocument || !iframe?.contentDocument?.head || !themeVariables || !themeClass) return;

const styleId = 'theme-generator-variables';
let styleElement = iframe.contentDocument.getElementById(styleId) as HTMLStyleElement;

if (!styleElement) {
styleElement = iframe.contentDocument.createElement('style');
styleElement.id = styleId;
iframe.contentDocument.head.appendChild(styleElement);
}

const cssText = `.${themeClass} {\n${Object.entries(themeVariables)
.map(([key, value]) => ` ${key}: ${value};`)
.join('\n')}\n}`;

styleElement.textContent = cssText;
styleElement.textContent = `.${ themeClass } {\n${ Object.entries(themeVariables)
.map(([key, value]) => ` ${ key }: ${ value };`)
.join('\n') }\n}`;
iframe.contentDocument.body.classList.add(themeClass);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ThemeGeneratorSwitchThemeModal } from './themeGeneratorSwitchThemeModal
import { ThemeGeneratorJSONModal } from './themeGeneratorJSONModal/ThemeGeneratorJSONModal';
import defaultTokens from '@ovhcloud/ods-themes/default/tokens';
import developerTokens from '@ovhcloud/ods-themes/developer/tokens';
import { ThemeGeneratorPaletteModal } from './themeGeneratorPaletteModal/ThemeGeneratorPaletteModal';

const ThemeGenerator = (): JSX.Element => {
const [isFullscreen, setIsFullscreen] = useState(false);
Expand All @@ -20,6 +21,7 @@ const ThemeGenerator = (): JSX.Element => {
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
const [pendingTheme, setPendingTheme] = useState<string | null>(null);
const [isJsonOpen, setIsJsonOpen] = useState(false);
const [isPaletteOpen, setIsPaletteOpen] = useState(false);

useEffect(() => {
if (selectedTheme === 'custom') {
Expand Down Expand Up @@ -86,6 +88,12 @@ const ThemeGenerator = (): JSX.Element => {
Custom
</SwitchItem>
</Switch>
<Button
onClick={ () => setIsPaletteOpen(true) }
variant={ BUTTON_VARIANT.ghost }>
<Icon name={ ICON_NAME.magicWand }/>
Generate palette
</Button>
</div>
<div className={styles['theme-generator__menu__right']}>
<OrientationSwitch
Expand Down Expand Up @@ -145,6 +153,20 @@ const ThemeGenerator = (): JSX.Element => {
}}
/>

<ThemeGeneratorPaletteModal
open={ isPaletteOpen }
onClose={ () => setIsPaletteOpen(false) }
currentVariables={ editedVariables }
onApply={(variables) => {
setEditedVariables((prev) => ({
...prev,
...variables,
}));
setSelectedTheme('custom');
setIsCustomTheme(true);
}}
/>

<ThemeGeneratorJSONModal
open={ isJsonOpen }
variables={ editedVariables }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { ColorPicker, parseColor } from '@ark-ui/react/color-picker';
import React from 'react';
import { FormField, FormFieldLabel, Input, Quantity, QuantityControl, QuantityInput } from '@ovhcloud/ods-react';
import styles from './themeGeneratorColorPicker.module.css';

interface ThemeGeneratorColorPickerProps {
label?: string;
value: string;
onChange: (value: string) => void;
className?: string;
showLabel?: boolean;
disabled?: boolean;
}

const formatColorValue = (colorValue: any): string => {
const hexColor = colorValue.toString('hex');
const alpha = colorValue.getChannelValue('alpha');

const roundedAlpha = Math.round(alpha * 100) / 100;

if (roundedAlpha === 1) {
return hexColor;
}

const alphaHex = Math.round(roundedAlpha * 255).toString(16).padStart(2, '0');

return `${hexColor}${alphaHex}`;
};

const parseColorWithRoundedAlpha = (colorString: string) => {
try {
const color = parseColor(colorString.startsWith('var(') ? '#000000' : colorString);
const alpha = color.getChannelValue('alpha');
const roundedAlpha = Math.round(alpha * 100) / 100;
return color.withChannelValue('alpha', roundedAlpha);
} catch {
return parseColor('#000000');
}
};

const ThemeGeneratorColorPicker = ({
label,
value,
onChange,
className = '',
showLabel = true,
disabled = false,
}: ThemeGeneratorColorPickerProps) => {
const handleValueChange = (details: any) => {
onChange(formatColorValue(details.value));
};

const roundAlphaValue = (inputValue: string) => {
const parsedValue = parseFloat(inputValue);
if (!isNaN(parsedValue)) {
const rounded = Math.round(parsedValue * 100) / 100;
const currentColor = parseColorWithRoundedAlpha(value);
const newColor = currentColor.withChannelValue('alpha', rounded);
onChange(formatColorValue(newColor));
}
};

const handleAlphaBlur = (e: React.FocusEvent<HTMLInputElement>) => {
roundAlphaValue(e.target.value);
};

const handleAlphaKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
e.stopPropagation();
if (e.key === 'Enter') {
roundAlphaValue(e.currentTarget.value);
e.currentTarget.blur();
}
};

const handleClick = (e: React.MouseEvent) => {
e.stopPropagation();
};

return (
<ColorPicker.Root
className={`${styles['theme-generator-color-picker']} ${className}`}
disabled={ disabled }
onClick={ handleClick }
onValueChange={ handleValueChange }
value={ parseColorWithRoundedAlpha(value) }
>
{showLabel && label && (
<ColorPicker.Label className={styles['theme-generator-color-picker__label']}>
{label}
</ColorPicker.Label>
)}
<ColorPicker.Control className={styles['theme-generator-color-picker__control']}>
<ColorPicker.ChannelInput
asChild
channel="hex"
onKeyDown={(e) => e.stopPropagation()}
>
<Input type="text" />
</ColorPicker.ChannelInput>
<ColorPicker.Trigger className={styles['theme-generator-color-picker__control__trigger']}>
<ColorPicker.ValueSwatch className={styles['theme-generator-color-picker__control__trigger__swatch']} />
</ColorPicker.Trigger>
</ColorPicker.Control>

<ColorPicker.Positioner>
<ColorPicker.Content className={styles['theme-generator-color-picker__popover']}>
<ColorPicker.Area className={styles['theme-generator-color-picker__popover__area']}>
<ColorPicker.AreaBackground className={styles['theme-generator-color-picker__popover__area__background']} />
<ColorPicker.AreaThumb className={styles['theme-generator-color-picker__popover__area__thumb']} />
</ColorPicker.Area>

<ColorPicker.ChannelSlider channel="hue" className={styles['theme-generator-color-picker__popover__slider']}>
<ColorPicker.ChannelSliderTrack className={styles['theme-generator-color-picker__popover__slider__track']} />
<ColorPicker.ChannelSliderThumb className={styles['theme-generator-color-picker__popover__slider__thumb']} />
</ColorPicker.ChannelSlider>

<ColorPicker.ChannelSlider channel="alpha" className={styles['theme-generator-color-picker__popover__slider']}>
<ColorPicker.TransparencyGrid className={styles['theme-generator-color-picker__popover__slider__transparency-grid']} />
<ColorPicker.ChannelSliderTrack className={styles['theme-generator-color-picker__popover__slider__track']} />
<ColorPicker.ChannelSliderThumb className={styles['theme-generator-color-picker__popover__slider__thumb']} />
</ColorPicker.ChannelSlider>

<div className={styles['theme-generator-color-picker__popover__inputs']}>
<FormField>
<FormFieldLabel>
Hex
</FormFieldLabel>
<ColorPicker.ChannelInput
asChild
channel="hex"
onKeyDown={(e) => e.stopPropagation()}
>
<Input type="text" />
</ColorPicker.ChannelInput>
</FormField>
<FormField>
<FormFieldLabel>
Alpha
</FormFieldLabel>
<ColorPicker.ChannelInput
asChild
channel="alpha"
onKeyDown={handleAlphaKeyDown}
onBlur={handleAlphaBlur}
>
<Quantity min={0} max={1} step={0.01}>
<QuantityControl>
<QuantityInput />
</QuantityControl>
</Quantity>
</ColorPicker.ChannelInput>
</FormField>
</div>
</ColorPicker.Content>
</ColorPicker.Positioner>

<ColorPicker.HiddenInput />
</ColorPicker.Root>
);
};

export { ThemeGeneratorColorPicker };
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
.theme-generator-color-picker {
display: flex;
gap: 8px;
align-items: center;
justify-content: space-between;
width: 100%;
}

.theme-generator-color-picker:not(:has(.theme-generator-color-picker__label)) {
justify-content: flex-end;
width: auto;
}

.theme-generator-color-picker__label {
flex: 1;
color: var(--ods-color-text);
}

.theme-generator-color-picker__control {
display: flex;
border: 0;
}

.theme-generator-color-picker__control__trigger {
border: 0;
background-color: transparent;
cursor: pointer;
}

.theme-generator-color-picker__control__trigger:disabled {
opacity: 1;
cursor: not-allowed;
}

.theme-generator-color-picker__control__trigger__swatch {
border: 1px solid var(--ods-color-neutral-300);
cursor: pointer;
width: 16px;
height: 16px;
}

.theme-generator-color-picker__popover {
flex-direction: column;
gap: 16px;
z-index: 1000;
border: 1px solid var(--ods-color-neutral-300);
border-radius: var(--ods-border-radius-md);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
background-color: var(--ods-color-neutral-000);
padding: 16px;
}

.theme-generator-color-picker__popover[data-state="open"] {
display: flex;
}

.theme-generator-color-picker__popover__area {
position: relative;
min-width: 200px;
min-height: 200px;
overflow: hidden;
}

.theme-generator-color-picker__popover__area__background {
width: 100%;
min-width: 200px;
height: 100%;
min-height: 200px;
}

.theme-generator-color-picker__popover__area__thumb {
position: absolute;
transform: translate(-50%, -50%);
border: 2px solid var(--ods-color-neutral-000);
border-radius: 50%;
box-shadow: 0 0 0 1px var(--ods-color-neutral-500);
cursor: pointer;
width: 24px;
height: 24px;
}

.theme-generator-color-picker__popover__slider {
position: relative;
border-radius: var(--ods-border-radius-sm);
cursor: pointer;
width: 100%;
min-width: 200px;
height: 12px;
min-height: 12px;
}

.theme-generator-color-picker__popover__slider__track {
border-radius: var(--ods-border-radius-sm);
width: 100%;
height: 100%;
}

.theme-generator-color-picker__popover__slider__thumb {
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
border: 2px solid var(--ods-color-neutral-000);
border-radius: 50%;
box-shadow: 0 0 0 1px var(--ods-color-neutral-500);
cursor: pointer;
width: 10px;
min-width: 10px;
height: 10px;
min-height: 10px;
}

.theme-generator-color-picker__popover__slider__transparency-grid {
position: absolute;
border-radius: var(--ods-border-radius-sm);
background-image:
linear-gradient(45deg, var(--ods-color-neutral-200) 25%, transparent 25%),
linear-gradient(-45deg, var(--ods-color-neutral-200) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, var(--ods-color-neutral-200) 75%),
linear-gradient(-45deg, transparent 75%, var(--ods-color-neutral-200) 75%);
background-position: 0 0, 0 4px, 4px -4px, -4px 0;
background-size: 8px 8px;
width: 100%;
height: 100%;
}

.theme-generator-color-picker__popover__inputs {
display: flex;
gap: 8px;
min-width: 200px;
}
Loading