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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@ All notable changes to this project will be documented in this file.

This projects adheres to [Semantic Versioning](https://semver.org/) and [Keep a CHANGELOG](https://keepachangelog.com/).

## [6.0.2] - 2025-11-21
- `Container` updates
- Added `centered` prop to vertically center content (you might need to make inner content flex grow or fill width).
- Added `lessSpaceStart` and `lessSpaceEnd` props for more control over horizontal padding.
- Tweaked accent shade.
- `ContainerGroup` now supports a `label` prop to label the group.
- Fixed `DraggableList` item z-index while dragging.
- `Menu` now supports the `hidden` prop to programmatically hide the component.
- `OptionsPanelHeader` has set title margins to prevent overrides.
- `RichLabel` doesn't have the `noColor` prop anymore - it's now the default behavior.
- Improved `SmartImage` analysis reliability.
- Updated dependencies.

## [6.0.1] - 2025-11-14
- Fix output issue with `ContainerGroup` passed components.

Expand Down Expand Up @@ -501,6 +514,7 @@ Co-authored with @piqusy
- Initial release

[Unreleased]: https://github.com/infinum/eightshift-ui-components/compare/master...HEAD
[6.0.2]: https://github.com/infinum/eightshift-ui-components/compare/6.0.1...6.0.2
[6.0.1]: https://github.com/infinum/eightshift-ui-components/compare/6.0.0...6.0.1
[6.0.0]: https://github.com/infinum/eightshift-ui-components/compare/5.6.1...6.0.0
[5.6.1]: https://github.com/infinum/eightshift-ui-components/compare/5.6.0...5.6.1
Expand Down
98 changes: 29 additions & 69 deletions bun.lock

Large diffs are not rendered by default.

52 changes: 45 additions & 7 deletions lib/components/base-control/container.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import { cloneElement, forwardRef } from 'react';
* @property {boolean} [props.isChild] - If `true`, applies child-specific styling for nested containers.
* @property {boolean} [props.compact] - If `true`, the vertical padding is reduced for a more compact appearance.
* @property {boolean} [props.standalone] - If `true`, the border radius is not adjusted automatically, based on neightboring containers.
* @property {boolean} [props.centered] - If `true`, the content is centered vertically.
* @property {boolean} [props.lessSpaceStart] - If `true`, space on the start (left) is reduced. Useful for symmetric components.
* @property {boolean} [props.lessSpaceEnd] - If `true`, space on the end (right) is reduced. For example, use with text fields, or taller items.
* @property {string|JSX.Element} [props.as] - The HTML element or React component to render as the container.
*
* @preserve
Expand All @@ -36,15 +39,15 @@ import { cloneElement, forwardRef } from 'react';
* @preserve
*/
export const Container = forwardRef((props, ref) => {
const { className, children, as, hidden, accent, elevated, primary, isChild, compact, standalone, horizontal, ...rest } = props;
const { className, children, as, hidden, accent, elevated, primary, isChild, compact, standalone, horizontal, centered, lessSpaceStart, lessSpaceEnd, ...rest } = props;

const ComponentToRender = as || 'div';

if (hidden) {
return null;
}

const containerClasses = cva([' es:inset-ring es:px-2.5', className], {
const containerClasses = cva(['es:inset-ring', className], {
variants: {
elevated: {
true: 'es:inset-shadow-sm es:shadow-sm es:shadow-black/5',
Expand All @@ -56,6 +59,17 @@ export const Container = forwardRef((props, ref) => {
false: 'es:py-2 es:min-h-13',
true: 'es:py-1 es:min-h-9',
},
centered: {
true: 'es:flex es:items-center',
},
lessSpaceStart: {
true: 'es:pl-2',
false: 'es:pl-3',
},
lessSpaceEnd: {
true: 'es:pr-2',
false: 'es:pr-3',
},
},
compoundVariants: [
{
Expand Down Expand Up @@ -111,7 +125,7 @@ export const Container = forwardRef((props, ref) => {
{
accent: true,
elevated: false,
class: 'es:bg-surface-50 es:inset-ring-surface-100',
class: 'es:bg-surface-100/80 es:inset-ring-surface-200 es:text-accent-900',
},
{
accent: false,
Expand All @@ -132,14 +146,17 @@ export const Container = forwardRef((props, ref) => {
compact: false,
standalone: false,
horizontal: false,
centered: false,
lessSpaceStart: false,
lessSpaceEnd: false,
},
});

return (
<ComponentToRender
{...rest}
ref={ref}
className={containerClasses({ accent, elevated, primary, isChild, compact, horizontal, standalone })}
className={containerClasses({ accent, elevated, primary, isChild, compact, horizontal, standalone, centered, lessSpaceStart, lessSpaceEnd })}
>
{children}
</ComponentToRender>
Expand All @@ -151,6 +168,8 @@ Container.displayName = 'Container';
/**
* @typedef {Object} ContainerGroupProps
* @property {string} [className] - Classes to pass to the container group.
* @property {string} [wrapClassName] - Classes to pass to the control wrapper - only if label is set.
* @property {string|JSX.Element} [label] - Label to show above the container group.
* @property {boolean} [hidden] - If `true`, the component is not rendered.
* @property {boolean} [horizontal] - If `true`, the component uses a horizontal orientation.
* @property {string|JSX.Element} [as] - The HTML element or React component to render as the container group.
Expand Down Expand Up @@ -178,7 +197,7 @@ Container.displayName = 'Container';
* @preserve
*/
export const ContainerGroup = forwardRef((props, ref) => {
const { className, children, as, hidden, horizontal, ...rest } = props;
const { className, children, as, hidden, horizontal, label, wrapClassName, ...rest } = props;

const ComponentToRender = as || 'div';

Expand All @@ -188,7 +207,7 @@ export const ContainerGroup = forwardRef((props, ref) => {

const processedChildren = Array.isArray(children)
? children.reduce((acc, child, index) => {
if (child.type.displayName === 'Container') {
if (child?.type?.displayName === 'Container') {
return [
...acc,
cloneElement(child, {
Expand All @@ -202,7 +221,11 @@ export const ContainerGroup = forwardRef((props, ref) => {
}, [])
: children;

return (
if (!processedChildren || processedChildren?.length < 1) {
return null;
}

const inner = (
<ComponentToRender
{...rest}
ref={ref}
Expand All @@ -211,6 +234,21 @@ export const ContainerGroup = forwardRef((props, ref) => {
{processedChildren}
</ComponentToRender>
);

if (!label) {
return inner;
}

if (Array.isArray(inner?.props?.children) && !inner?.props?.children?.filter(Boolean)?.length) {
return null;
}

return (
<div className={wrapClassName}>
<span className='es:ml-1 es:mb-1 es:inline-block es:text-12 es:font-variation-["wdth"_125,"wght"_400] es:text-surface-500'>{label}</span>
{inner}
</div>
);
});

ContainerGroup.displayName = 'ContainerGroup';
2 changes: 1 addition & 1 deletion lib/components/draggable-list/draggable-list.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export const DraggableList = (props) => {
key={key}
accent={isDragged || isSelected}
elevated={isDragged || isSelected}
className={clsx('es:list-none es:m-0!', itemClassName)}
className={clsx('es:list-none es:m-0!', isDragged && 'es:z-99999', itemClassName)}
data-selected={isDragged || isSelected || props?.style?.position === 'fixed'}
{...rest}
>
Expand Down
7 changes: 7 additions & 0 deletions lib/components/menu/menu.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ export const MenuSeparator = ({ className }) => {
* @param {boolean} [props.danger] - If `true`, the item appearance is tweaked to indicate a dangerous action.
* @param {boolean} [props.primary] - If `true`, the item appearance is tweaked to indicate a primary action.
* @param {string} [props.className] - Classes to pass to the menu item.
* @param {string} [props.aria-label] - Aria label for the menu item. Defaults to the children text or 'Menu item' if children is not a string.
* @param {boolean} [props.hidden] - If `true`, the component is not rendered.
*
* @returns {JSX.Element} The MenuItem component.
*
Expand All @@ -236,8 +238,13 @@ export const MenuItem = (props) => {
primary,
className,
'aria-label': ariaLabel = typeof children === 'string' ? children : __('Menu item', 'eightshift-ui-components'),
hidden,
} = props;

if (hidden) {
return null;
}

let itemIcon = icon;

if (checked === true) {
Expand Down
5 changes: 3 additions & 2 deletions lib/components/options-panel/options-panel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,16 +122,17 @@ export const OptionsPanelHeader = ({ children, sticky, title, className, actions

return (
<div className={clsx('es:space-y-2.5', limitWidth && 'es:max-w-2xl', sticky && 'es:sticky es:top-0 es:z-10 es:bg-white', className)}>
<div className='es:flex es:flex-wrap es:items-center es:justify-between es:gap-x-8 es:gap-y-4'>
<div className='es:flex es:flex-wrap es:items-center es:justify-between es:gap-x-8 es:gap-y-4 es:mb-10'>
<Heading
className='es:text-3xl es:text-surface-800 es:font-variation-["wdth"_180,"YTLC"_540,"wght"_300]'
className='es:text-3xl es:text-surface-800 es:font-variation-["wdth"_180,"YTLC"_540,"wght"_300] es:m-0!'
level={level}
>
{title}
</Heading>

<div className='es:flex es:items-center es:gap-2'>{actions}</div>
</div>

{children}
</div>
);
Expand Down
14 changes: 5 additions & 9 deletions lib/components/rich-label/rich-label.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import { clsx } from 'clsx/lite';
* @param {boolean} [props.fullWidth=false] - If `true`, the component will take up as much space as it can.
* @param {boolean} [props.contentsOnly] - If `true`, only the label (/icon/subtitle) will be rendered, without any wrapping elements. Useful if you want to provide your own layout.
* @param {boolean} [props.hidden] - If `true`, the component is not rendered.
* @param {boolean} [props.noColor] - If `true`, colors on text won't be set, opacity will be used instead.
* @param {boolean} [props.fullSizeSubtitle] - If `true`, the subtitle is the same size as the label.
* @param {boolean} [props.inlineSubtitle] - If `true`, the subtitle is shown after the label instead of below it.
*
Expand Down Expand Up @@ -45,7 +44,6 @@ export const RichLabel = (props) => {
fullWidth = false,
contentsOnly,
hidden,
noColor,
fullSizeSubtitle,
inlineSubtitle,
} = props;
Expand All @@ -59,18 +57,16 @@ export const RichLabel = (props) => {
if (contentsOnly) {
return (
<>
{icon && <span className={clsx('es:icon:size-5', !noColor && 'es:text-secondary-500', iconClassName)}>{icon}</span>}
{label && <span className={clsx('es:text-balance', !noColor && 'es:text-secondary-800', labelClassName)}>{label}</span>}
{subtitle && <span className={clsx('es:text-balance es:text-xs es:not-contrast-more:opacity-65', !noColor && 'es:text-secondary-700', subtitleClassName)}>{subtitle}</span>}
{icon && <span className={clsx('es:icon:size-5 es:not-contrast-more:opacity-85', iconClassName)}>{icon}</span>}
{label && <span className={clsx('es:text-balance', labelClassName)}>{label}</span>}
{subtitle && <span className={clsx('es:text-balance es:text-xs es:not-contrast-more:opacity-65', subtitleClassName)}>{subtitle}</span>}
</>
);
}

return (
<ComponentToRender
className={clsx('es:flex es:items-center es:gap-1.75 es:text-sm', !noColor && 'es:text-secondary-700 es:any-icon:text-secondary-500', fullWidth && 'es:grow', className)}
>
{icon && <span className={clsx('es:icon:size-5 es:shrink-0', noColor && 'es:not-contrast-more:opacity-80', iconClassName)}>{icon}</span>}
<ComponentToRender className={clsx('es:flex es:items-center es:gap-1.75 es:text-sm', fullWidth && 'es:grow', className)}>
{icon && <span className={clsx('es:icon:size-5 es:shrink-0', 'es:not-contrast-more:opacity-85', iconClassName)}>{icon}</span>}

{(label || subtitle) && (
<div className={clsx('es:flex es:items-start es:text-balance es:text-start', inlineSubtitle ? 'es:gap-1.5' : 'es:flex-col', labelSubtitleWrapClassName)}>
Expand Down
18 changes: 8 additions & 10 deletions lib/components/smart-image/smart-image.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,6 @@ export const SmartImage = (props) => {

const classFetchProps = { isLoaded, hasAnalysed, isTransparent, dominantColors, isDark, transparencyInfo };

if (analysisData) {
delete imageProps.analysisData;
}

const imageElement = (
<img
decoding='async'
Expand Down Expand Up @@ -122,13 +118,15 @@ export const SmartImage = (props) => {
if (analysisData) {
const { isDark: dark, dominantColors: colors, isTransparent: transparent, transparencyInfo } = analysisData;

setIsDark(dark);
setDominantColors(colors);
setIsTransparent(transparent);
setTransparencyInfo(transparencyInfo);
setHasAnalysed(true);
if (dark !== undefined && colors !== undefined && transparent !== undefined && transparencyInfo !== undefined) {
setIsDark(dark);
setDominantColors(colors);
setIsTransparent(transparent);
setTransparencyInfo(transparencyInfo);
setHasAnalysed(true);

return;
return;
}
}

// Cache results in localstorage.
Expand Down
2 changes: 1 addition & 1 deletion lib/components/smart-image/worker-inline.js

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions lib/utilities/general.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,12 @@ export const analyzeImage = (image, rawSettings) => {
skipTransparencyCheck = !['png', 'webp', 'gif', 'tiff', 'svg', 'avif'].includes(fileExtension);
}

const imageWidth = image.naturalWidth;
const imageHeight = image.naturalHeight;
let imageWidth = image.naturalWidth || image.width;
let imageHeight = image.naturalHeight || image.height;

if (!image.complete) {
throw new Error('Image not fully loaded');
}

if (!imageWidth || !imageHeight) {
return false;
Expand Down
18 changes: 9 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@eightshift/ui-components",
"version": "6.0.1",
"version": "6.0.2",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
Expand Down Expand Up @@ -58,15 +58,15 @@
"@dnd-kit/react": "^0.1.21",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@eslint/compat": "^1.4.1",
"@eslint/compat": "^2.0.0",
"@react-stately/collections": "^3.12.8",
"@stylistic/eslint-plugin-js": "^4.4.1",
"@tailwindcss/vite": "^4.1.17",
"@thi.ng/color": "^5.8.2",
"@thi.ng/pixel": "^7.5.15",
"@thi.ng/pixel-analysis": "^2.0.16",
"@thi.ng/pixel-dominant-colors": "^2.0.20",
"@types/react": "^18.3.26",
"@thi.ng/pixel": "^7.5.16",
"@thi.ng/pixel-analysis": "^2.0.17",
"@thi.ng/pixel-dominant-colors": "^2.0.21",
"@types/react": "^18.3.27",
"@types/react-dom": "^18.3.7",
"@vitejs/plugin-react-swc": "^4.2.2",
"@wordpress/i18n": "^6.8.0",
Expand All @@ -76,9 +76,9 @@
"css-gradient-parser": "^0.0.18",
"eslint": "^9.39.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-jsdoc": "^61.2.1",
"eslint-plugin-jsdoc": "^61.4.0",
"eslint-plugin-prettier": "^5.5.4",
"glob": "^11.0.3",
"glob": "^13.0.0",
"globals": "^16.5.0",
"just-camel-case": "^6.2.0",
"just-debounce-it": "^3.2.0",
Expand All @@ -101,7 +101,7 @@
"tailwindcss": "^4.1.17",
"tailwindcss-motion": "^1.1.1",
"tailwindcss-react-aria-components": "^2.0.1",
"vite": "^7.2.2",
"vite": "^7.2.4",
"vite-plugin-lib-inject-css": "^2.2.2"
},
"dependencies": {
Expand Down