diff --git a/__tests__/Callout.test.tsx b/__tests__/Callout.test.tsx
index c395eebc..ed34d085 100644
--- a/__tests__/Callout.test.tsx
+++ b/__tests__/Callout.test.tsx
@@ -68,3 +68,246 @@ test('Renders with container effect', () => {
expect(document.querySelectorAll('.reqore-callout').length).toBe(1);
expect(document.querySelector('.reqore-callout')!.textContent).toBe('Gradient surface');
});
+
+test('Renders with label and description', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelector('.reqore-callout-label')!.textContent).toBe('Heads up');
+ expect(document.querySelector('.reqore-callout-description')!.textContent).toBe(
+ 'Something to look at'
+ );
+});
+
+test('Falls back to children when label/description are not provided', () => {
+ render(
+
+
+
+ Plain children content
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-callout-content').length).toBe(1);
+ expect(document.querySelectorAll('.reqore-callout-label').length).toBe(0);
+});
+
+test('Renders with icon', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-callout-icon').length).toBe(1);
+});
+
+test('Renders with badge (string)', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelector('.reqore-button-badge')!.textContent).toContain('New');
+});
+
+test('Renders with badge array', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-button-badge').length).toBe(2);
+});
+
+test('Renders with onClose and fires it', () => {
+ const handleClose = jest.fn();
+
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-callout-close').length).toBe(1);
+
+ fireEvent.click(document.querySelector('.reqore-callout-close')!);
+ expect(handleClose).toHaveBeenCalledTimes(1);
+});
+
+test('Close button click does not bubble to onClick', () => {
+ const handleClick = jest.fn();
+ const handleClose = jest.fn();
+
+ render(
+
+
+
+
+
+
+
+ );
+
+ fireEvent.click(document.querySelector('.reqore-callout-close')!);
+ expect(handleClose).toHaveBeenCalledTimes(1);
+ expect(handleClick).toHaveBeenCalledTimes(0);
+});
+
+test('Renders with intents', () => {
+ render(
+
+
+
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-callout').length).toBe(4);
+});
+
+test('Renders with different sizes', () => {
+ render(
+
+
+
+ Tiny
+ Small
+ Normal
+ Big
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-callout').length).toBe(4);
+});
+
+test('Renders bordered with flat={false}', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-callout').length).toBe(1);
+});
+
+test('Renders with rounded={false}', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-callout').length).toBe(1);
+});
+
+test('Renders with transparent background', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-callout').length).toBe(1);
+});
+
+test('Renders with labelEffect / descriptionEffect', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-callout-label').length).toBe(1);
+ expect(document.querySelectorAll('.reqore-callout-description').length).toBe(1);
+});
+
+test('Renders disabled', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-callout').length).toBe(1);
+});
+
+test('Renders with raised effect', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-callout').length).toBe(1);
+});
diff --git a/__tests__/EmptyState.test.tsx b/__tests__/EmptyState.test.tsx
index b4aa77b6..2c89f21f 100644
--- a/__tests__/EmptyState.test.tsx
+++ b/__tests__/EmptyState.test.tsx
@@ -279,3 +279,17 @@ test('Renders with all features', () => {
);
expect(document.querySelectorAll('.reqore-empty-state-actions').length).toBe(1);
});
+
+test('Renders with raised effect', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-empty-state').length).toBe(1);
+});
diff --git a/__tests__/EntityRow.test.tsx b/__tests__/EntityRow.test.tsx
new file mode 100644
index 00000000..ec92a1e0
--- /dev/null
+++ b/__tests__/EntityRow.test.tsx
@@ -0,0 +1,324 @@
+import { fireEvent, render } from '@testing-library/react';
+import {
+ ReqoreContent,
+ ReqoreEntityRow,
+ ReqoreLayoutContent,
+ ReqoreUIProvider,
+} from '../src';
+
+test('Renders with label', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-entity-row').length).toBe(1);
+ expect(document.querySelector('.reqore-entity-row-label')!.textContent).toContain(
+ 'Process Incoming Order'
+ );
+});
+
+test('Renders with description and metadata', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelector('.reqore-entity-row-description')!.textContent).toBe(
+ 'Routes Shopify orders'
+ );
+ expect(document.querySelector('.reqore-entity-row-metadata')!.textContent).toBe(
+ 'Last run: success · just now'
+ );
+});
+
+test('Renders with icon tile', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-entity-row-icon-tile').length).toBe(1);
+});
+
+test('Does not render icon tile without icon or image', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-entity-row-icon-tile').length).toBe(0);
+});
+
+test('Renders with badge (object)', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelector('.reqore-entity-row-label')!.textContent).toContain('Failed');
+});
+
+test('Renders with badge array', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-button-badge').length).toBe(2);
+});
+
+test('Renders with actions', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-entity-row-actions').length).toBe(1);
+ expect(document.querySelector('.reqore-entity-row-actions')!.textContent).toContain('Run');
+});
+
+test('Calls onClick when row is clicked', () => {
+ const handleClick = jest.fn();
+
+ render(
+
+
+
+
+
+
+
+ );
+
+ fireEvent.click(document.querySelector('.reqore-entity-row')!);
+ expect(handleClick).toHaveBeenCalledTimes(1);
+});
+
+test('Renders with intents', () => {
+ render(
+
+
+
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-entity-row').length).toBe(4);
+});
+
+test('Renders with different sizes', () => {
+ render(
+
+
+
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-entity-row').length).toBe(4);
+});
+
+test('Renders bordered with flat={false}', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-entity-row').length).toBe(1);
+});
+
+test('Renders with rounded={false}', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-entity-row').length).toBe(1);
+});
+
+test('Renders with transparent background', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-entity-row').length).toBe(1);
+});
+
+test('Renders with effect / labelEffect / descriptionEffect / metadataEffect', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-entity-row').length).toBe(1);
+ expect(document.querySelectorAll('.reqore-entity-row-description').length).toBe(1);
+ expect(document.querySelectorAll('.reqore-entity-row-metadata').length).toBe(1);
+});
+
+test('Renders with iconImage', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-entity-row-icon-tile').length).toBe(1);
+});
+
+test('Does not render description/metadata when not provided', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-entity-row-description').length).toBe(0);
+ expect(document.querySelectorAll('.reqore-entity-row-metadata').length).toBe(0);
+});
+
+test('Renders with wrap=false (single-line ellipsis)', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-entity-row-description').length).toBe(1);
+ expect(document.querySelectorAll('.reqore-entity-row-metadata').length).toBe(1);
+});
+
+test('Renders with wrap=true by default', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-entity-row-description').length).toBe(1);
+});
+
+test('Renders with raised effect', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-entity-row').length).toBe(1);
+});
diff --git a/__tests__/FeatureCard.test.tsx b/__tests__/FeatureCard.test.tsx
index d5ded4fd..803adde2 100644
--- a/__tests__/FeatureCard.test.tsx
+++ b/__tests__/FeatureCard.test.tsx
@@ -68,3 +68,194 @@ test('Calls onClick handler', () => {
fireEvent.click(document.querySelector('.reqore-feature-card')!);
expect(handleClick).toHaveBeenCalledTimes(1);
});
+
+test('Renders with badge (string)', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelector('.reqore-button-badge')!.textContent).toContain('New');
+});
+
+test('Renders with badge array', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-button-badge').length).toBe(2);
+});
+
+test('Renders with intents', () => {
+ render(
+
+
+
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-feature-card').length).toBe(4);
+});
+
+test('Renders with different sizes', () => {
+ render(
+
+
+
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-feature-card').length).toBe(4);
+});
+
+test('Renders bordered with flat={false}', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-feature-card').length).toBe(1);
+});
+
+test('Renders with rounded={false}', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-feature-card').length).toBe(1);
+});
+
+test('Renders with transparent background', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-feature-card').length).toBe(1);
+});
+
+test('Renders disabled', () => {
+ render(
+
+
+
+ {}} />
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-feature-card').length).toBe(1);
+});
+
+test('Renders with effect / labelEffect / descriptionEffect', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-feature-card').length).toBe(1);
+ expect(document.querySelectorAll('.reqore-feature-card-description').length).toBe(1);
+});
+
+test('Renders with wrap=false (single-line ellipsis)', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-feature-card-label').length).toBe(1);
+ expect(document.querySelectorAll('.reqore-feature-card-description').length).toBe(1);
+});
+
+test('Auto-detects interactive when onClick is provided', () => {
+ const handleClick = jest.fn();
+
+ render(
+
+
+
+
+
+
+
+ );
+
+ fireEvent.click(document.querySelector('.reqore-feature-card')!);
+ expect(handleClick).toHaveBeenCalledTimes(1);
+});
+
+test('Renders with raised effect', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-feature-card').length).toBe(1);
+});
diff --git a/__tests__/Message.test.tsx b/__tests__/Message.test.tsx
new file mode 100644
index 00000000..ca66c512
--- /dev/null
+++ b/__tests__/Message.test.tsx
@@ -0,0 +1,37 @@
+import { render } from '@testing-library/react';
+import {
+ ReqoreContent,
+ ReqoreLayoutContent,
+ ReqoreMessage,
+ ReqoreUIProvider,
+} from '../src';
+
+test('Renders properly', () => {
+ render(
+
+
+
+ Heads up
+
+
+
+ );
+
+ expect(document.body.textContent).toContain('Heads up');
+});
+
+test('Renders with raised effect', () => {
+ render(
+
+
+
+
+ Raised
+
+
+
+
+ );
+
+ expect(document.body.textContent).toContain('Raised');
+});
diff --git a/__tests__/SeverityRow.test.tsx b/__tests__/SeverityRow.test.tsx
new file mode 100644
index 00000000..6a3a8748
--- /dev/null
+++ b/__tests__/SeverityRow.test.tsx
@@ -0,0 +1,346 @@
+import { fireEvent, render } from '@testing-library/react';
+import {
+ ReqoreContent,
+ ReqoreLayoutContent,
+ ReqoreSeverityRow,
+ ReqoreTag,
+ ReqoreUIProvider,
+} from '../src';
+
+test('Renders with label', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-severity-row').length).toBe(1);
+ expect(document.querySelector('.reqore-severity-row-label')!.textContent).toContain(
+ 'Critical issue'
+ );
+});
+
+test('Renders with description', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelector('.reqore-severity-row-description')!.textContent).toBe(
+ 'Threshold exceeded'
+ );
+});
+
+test('Renders with strip by default', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-severity-row-strip').length).toBe(1);
+});
+
+test('Hides strip when showStrip is false', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-severity-row-strip').length).toBe(0);
+});
+
+test('Renders with leading content', () => {
+ render(
+
+
+
+ }
+ />
+
+
+
+ );
+
+ expect(document.querySelector('.reqore-severity-row-label')!.textContent).toContain('Critical');
+});
+
+test('Renders with badge (string)', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelector('.reqore-button-badge')!.textContent).toContain('3 open');
+});
+
+test('Renders with badge array', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-button-badge').length).toBe(2);
+});
+
+test('Renders with actions', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-severity-row-actions').length).toBe(1);
+ expect(document.querySelector('.reqore-severity-row-actions')!.textContent).toContain(
+ 'Investigate'
+ );
+});
+
+test('Calls onClick when row is clicked', () => {
+ const handleClick = jest.fn();
+
+ render(
+
+
+
+
+
+
+
+ );
+
+ fireEvent.click(document.querySelector('.reqore-severity-row')!);
+ expect(handleClick).toHaveBeenCalledTimes(1);
+});
+
+test('Renders with intents', () => {
+ render(
+
+
+
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-severity-row').length).toBe(4);
+});
+
+test('Renders with different sizes', () => {
+ render(
+
+
+
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-severity-row').length).toBe(4);
+});
+
+test('Renders bordered with flat={false}', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-severity-row').length).toBe(1);
+});
+
+test('Renders with rounded={false}', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-severity-row').length).toBe(1);
+});
+
+test('Renders with transparent background', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-severity-row').length).toBe(1);
+});
+
+test('Renders with effect/labelEffect/descriptionEffect', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-severity-row').length).toBe(1);
+ expect(document.querySelectorAll('.reqore-severity-row-description').length).toBe(1);
+});
+
+test('Renders disabled', () => {
+ const handleClick = jest.fn();
+
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-severity-row').length).toBe(1);
+});
+
+test('Does not render description when not provided', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-severity-row-description').length).toBe(0);
+});
+
+test('Does not render actions when not provided', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-severity-row-actions').length).toBe(0);
+});
+
+test('Renders with wrap=false (single-line ellipsis)', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-severity-row-description').length).toBe(1);
+});
+
+test('Renders with wrap=true by default', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-severity-row-description').length).toBe(1);
+});
+
+test('Renders with raised effect', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-severity-row').length).toBe(1);
+});
diff --git a/__tests__/Statistic.test.tsx b/__tests__/Statistic.test.tsx
index 9ae9ef61..6e038d2b 100644
--- a/__tests__/Statistic.test.tsx
+++ b/__tests__/Statistic.test.tsx
@@ -316,3 +316,17 @@ test('Renders as interactive when onClick is provided', () => {
expect(handleClick).toHaveBeenCalledTimes(1);
});
+
+test('Renders with raised effect', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-statistic').length).toBe(1);
+});
diff --git a/__tests__/button.test.tsx b/__tests__/button.test.tsx
index 1e114776..e79166ad 100644
--- a/__tests__/button.test.tsx
+++ b/__tests__/button.test.tsx
@@ -215,3 +215,17 @@ test('Tooltip on works', () => {
expect(document.querySelectorAll('.reqore-popover-content').length).toBe(1);
});
+
+test('Renders with raised effect', () => {
+ render(
+
+
+
+ Raised
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-button').length).toBe(1);
+});
diff --git a/__tests__/icon.test.tsx b/__tests__/icon.test.tsx
index 22501a20..e8131c1d 100644
--- a/__tests__/icon.test.tsx
+++ b/__tests__/icon.test.tsx
@@ -51,3 +51,114 @@ test('Tooltip on works', () => {
expect(document.querySelectorAll('.reqore-popover-content').length).toBe(1);
});
+
+test('Renders with glow=true (drop-shadow filter)', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ const wrapper = document.querySelector('.reqore-icon') as HTMLElement;
+ expect(wrapper).toBeTruthy();
+ expect(wrapper.style.filter).toContain('drop-shadow');
+});
+
+test('Renders with glow as a color string', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ const wrapper = document.querySelector('.reqore-icon') as HTMLElement;
+ expect(wrapper.style.filter).toContain('drop-shadow');
+});
+
+test('Renders with glow object (custom blur and opacity)', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ const wrapper = document.querySelector('.reqore-icon') as HTMLElement;
+ expect(wrapper.style.filter).toContain('drop-shadow');
+ expect(wrapper.style.filter).toContain('16px');
+});
+
+test('Does not apply glow filter when glow is not set', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ const wrapper = document.querySelector('.reqore-icon') as HTMLElement;
+ expect(wrapper.style.filter).toBe('');
+});
+
+test('Applies glow when global glowingIcons option is enabled', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ const wrapper = document.querySelector('.reqore-icon') as HTMLElement;
+ expect(wrapper.style.filter).toContain('drop-shadow');
+});
+
+test('Local glow=false overrides global glowingIcons', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ const wrapper = document.querySelector('.reqore-icon') as HTMLElement;
+ expect(wrapper.style.filter).toBe('');
+});
+
+test('Does not apply glow when glowingIcons option is disabled', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ const wrapper = document.querySelector('.reqore-icon') as HTMLElement;
+ expect(wrapper.style.filter).toBe('');
+});
diff --git a/__tests__/panel.test.tsx b/__tests__/panel.test.tsx
index 5416d51d..d786c0b0 100644
--- a/__tests__/panel.test.tsx
+++ b/__tests__/panel.test.tsx
@@ -222,3 +222,15 @@ test('Custom control props on ', () => {
expect(screen.getAllByText('Close me')).toBeTruthy();
expect(screen.getAllByText('Collapse me')).toBeTruthy();
});
+
+test('Renders with raised effect', () => {
+ render(
+
+
+ Raised
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-panel').length).toBe(1);
+});
diff --git a/__tests__/progress.test.tsx b/__tests__/progress.test.tsx
index e8ac0eb2..e7952559 100644
--- a/__tests__/progress.test.tsx
+++ b/__tests__/progress.test.tsx
@@ -253,3 +253,90 @@ test('Does not show labels section when no label props provided', () => {
expect(document.querySelectorAll('.reqore-progress-labels').length).toBe(0);
});
+
+test('Renders with target marker', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-progress-target').length).toBe(1);
+ expect(document.querySelectorAll('.reqore-progress-target-label').length).toBe(1);
+ expect(document.querySelector('.reqore-progress-target-label')!.textContent).toBe('Target 80%');
+});
+
+test('Renders with custom target label', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelector('.reqore-progress-target-label')!.textContent).toBe('Goal');
+});
+
+test('Hides target label when showTargetLabel is false', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-progress-target').length).toBe(1);
+ expect(document.querySelectorAll('.reqore-progress-target-label').length).toBe(0);
+});
+
+test('Does not render target marker when target is not set', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-progress-target').length).toBe(0);
+});
+
+test('Does not render target marker when indeterminate', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelectorAll('.reqore-progress-target').length).toBe(0);
+});
+
+test('Clamps target value to max', () => {
+ render(
+
+
+
+
+
+
+
+ );
+
+ expect(document.querySelector('.reqore-progress-target-label')!.textContent).toBe('Target 100%');
+});
diff --git a/package.json b/package.json
index 6e211970..d1acc2b0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@qoretechnologies/reqore",
- "version": "0.65.0",
+ "version": "0.66.0",
"description": "ReQore is a highly theme-able and modular UI library for React",
"main": "dist/index.js",
"types": "dist/index.d.ts",
diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx
index 59410e0f..3128f9fc 100644
--- a/src/components/Button/index.tsx
+++ b/src/components/Button/index.tsx
@@ -31,6 +31,7 @@ import {
ActiveIconScale,
DisabledElement,
InactiveIconScale,
+ RaisedElement,
ReadOnlyElement,
ScaleIconOnHover,
StyledActiveContent,
@@ -111,6 +112,13 @@ export interface IReqoreButtonProps
pill?: boolean;
circle?: boolean;
+ /**
+ * Subtle 3D "raised" effect — inset top highlight + inset bottom shadow.
+ * Best paired with `flat={true}` (no border) and a non-`minimal`/non-
+ * `transparent` button so the surface is visible. Suppressed when a border
+ * is rendered (`flat={false}`) since the border already provides definition.
+ */
+ raised?: boolean;
}
export interface IReqoreButtonStyle extends IReqoreButtonProps {
@@ -221,6 +229,9 @@ export const StyledButton = styled(StyledEffect)`
${InactiveIconScale};
+ ${({ raised, flat, minimal, transparent }) =>
+ raised && flat && !minimal && !transparent && RaisedElement}
+
${({ readOnly, animate, active, theme, color, effect }) =>
!readOnly && !active
? css`
diff --git a/src/components/COMPONENTS.md b/src/components/COMPONENTS.md
index c9d1b04d..cf328245 100644
--- a/src/components/COMPONENTS.md
+++ b/src/components/COMPONENTS.md
@@ -4,7 +4,8 @@
| --- | --- |
| **ReqoreAccordion** | Expandable/collapsible panels that can display multiple items with optional icons, badges, and callbacks for toggling. |
| **ReqoreBreadcrumbs** | Navigation component displaying hierarchical breadcrumb trails with responsive collapse and optional tab support. |
-| **ReqoreButton** | Primary action button with support for icons, badges, loading states, effects, and multiple style variants (minimal, flat, transparent). |
+| **ReqoreButton** | Primary action button with support for icons, badges, loading states, effects, multiple style variants (minimal, flat, transparent), and an optional `raised` prop that adds a subtle 3D inset highlight when paired with `flat`. |
+| **ReqoreCallout** | Inline message surface for inline notices, warnings, and confirmations. Supports a leading icon, optional `label` + `description` (or freeform `children`), badge (TReqoreBadge), accent strip (left/top with configurable size), close button (`onClose` + `closeButtonProps`), and the standard prop set: intent (info/success/warning/danger; controls accent + border + icon colour), size, flat (border on `false`), rounded, fluid, fixed, transparent, raised (subtle 3D inset highlight when paired with `flat`), customTheme, tooltip, disabled, effect / labelEffect / descriptionEffect / contentEffect, and click handlers. |
| **ReqoreCheckbox** | Toggle control that can render as a standard checkbox or switch component with customizable icons and text labels. |
| **ReqoreCollection** | Grid/list view component for displaying arrays of items with filtering, sorting, zoom, and pagination capabilities. |
| **ReqoreColumns** | Layout component using CSS Grid to arrange content into responsive columns with customizable gaps and alignment. |
@@ -16,12 +17,14 @@
| **ReqoreDrawer** | Sliding panel that can be positioned on any edge of the screen, with support for resizing, hiding, and optional backdrop. |
| **ReqoreDropdown** | Button-triggered dropdown menu supporting filtering, nested items, multi-select, and keyboard navigation. |
| **ReqoreEffect** | Styled component providing visual effects like gradients, glows, filters, and text decorations. |
-| **ReqoreEmptyState** | Placeholder component displayed when content is unavailable, with optional icon, title, description, and action buttons. |
+| **ReqoreEmptyState** | Placeholder component displayed when content is unavailable, with optional icon, title, description, and action buttons. Optional `raised` adds a subtle 3D inset highlight to the placeholder surface. |
+| **ReqoreEntityRow** | Compact row primitive for lists of items (e.g. automations, drafts, integrations) with a leading icon tile, label/description/metadata stack, badge support (TReqoreBadge), and right-side action buttons. Supports the standard prop set: intent (with tinted backgrounds), size, flat (border on `false`), rounded, fluid, transparent, raised (subtle 3D inset highlight when paired with `flat`), customTheme, tooltip, disabled, effect / labelEffect / descriptionEffect / metadataEffect, wrap (single-line ellipsis on `false`), and click handlers. |
+| **ReqoreFeatureCard** | Highlight card for onboarding flows, feature spotlights, and product steps. Renders an optional marker (line/number/none), label, description, and badge. Supports the standard prop set: intent (controls marker + border colour), size, flat (border on `false`), rounded, fluid, fixed, transparent, raised (subtle 3D inset highlight when paired with `flat`), customTheme, tooltip, disabled, interactive (auto-detected from `onClick`), effect / labelEffect / descriptionEffect / markerEffect, and wrap (single-line ellipsis on `false`). |
| **ReqoreErrorBoundary** | React error boundary that catches errors and displays them with optional fallback UI and reset functionality. |
| **ReqoreExportModal** | Modal dialog for exporting data in multiple formats (CSV, JSON, YAML) with copy-to-clipboard functionality. |
| **ReqoreModalsWrapper** | Portal wrapper that renders all queued modals and dialogs from the global context. |
| **ReqoreHeading** | Typography component for headings (H1-H6) with theme support and text effects. |
-| **ReqoreIcon** | Icon rendering component using RemixIcon set with size, color, animation, and tooltip support. |
+| **ReqoreIcon** | Icon rendering component using RemixIcon set with size, color, animation, tooltip, and an optional contour-following `glow` (boolean / colour / `{ color, blur, opacity }`) rendered via `filter: drop-shadow`. The global `ReqoreUIProvider` option `glowingIcons: true` opts every icon into glow mode by default; individual icons opt out with `glow={false}`. |
| **ReqoreInput** | Text input field with optional icons, clear button, loading states, and comprehensive styling options. |
| **ReqoreInputClearButton** | Animated clear button component for input fields that appears on focus. |
| **ReqoreInternalPopover** | Internal popover positioning and rendering component using Popper.js for precise placement. |
@@ -29,26 +32,27 @@
| **ReqoreLabel** | Label element wrapper that extends ReqoreTag for form field labels. |
| **ReqoreLayoutWrapper** | Root layout container providing flex layout structure and theme context for the entire application. |
| **ReqoreMenu** | Vertical menu container with support for resizing, customizable item gaps, and menu styling. |
-| **ReqoreMessage** | Notification/alert message component with optional icon, title, auto-dismiss, and click handlers. |
+| **ReqoreMessage** | Notification/alert message component with optional icon, title, auto-dismiss, and click handlers. Optional `raised` adds a subtle 3D inset highlight when paired with `flat` (suppressed for `minimal` messages). |
| **ReqoreModal** | Modal dialog that extends Drawer with centered positioning and ESC key handling. |
| **ReqoreMultiSelect** | Multi-select input allowing users to add/remove items from a dropdown with customizable tagging. |
| **ReqoreNavbar** | Header or footer navigation bar with customizable styling and position options. |
| **ReqoreNotificationsWrapper** | Container for managing toast notifications at fixed screen positions. |
| **ReqorePaging** | Pagination controls component with page buttons, load more, and scroll-based infinite loading options. |
-| **ReqorePanel** | Container component with optional header, footer, actions, and resizable panels with breadcrumbs support. |
+| **ReqorePanel** | Container component with optional header, footer, actions, and resizable panels with breadcrumbs support. Optional `raised` adds a subtle 3D inset highlight when paired with `flat` (suppressed when an `intent` is set since the intent border already provides definition). |
| **ReqoreP** | Text paragraph component with theme support, effects, and size customization. |
| **ReqorePopover** | Popover/tooltip component with multiple trigger handlers (hover, click, focus) and smart positioning. |
-| **ReqoreProgress** | Progress bar component with optional animations, indeterminate state, and customizable labels and icons. |
+| **ReqoreProgress** | Progress bar component with optional animations, indeterminate state, customizable labels/icons, and an optional target marker for visualising goals (e.g. "90% target coverage"). |
| **ReqoreRadioGroup** | Radio button group component allowing single selection from multiple options with optional dividers. |
| **ReqoreRating** | Star rating component supporting half-steps, keyboard navigation, and optional clear functionality. |
| **ReqoreRichTextEditor** | Rich text editor using Slate framework with support for inline tags and text formatting options. |
| **ReqoreSegmentedControl** | Compact button-bar toggle for selecting one of 2-4 exclusive options with a sliding indicator animation. |
+| **ReqoreSeverityRow** | Row primitive for issue/alert/anomaly lists with a left severity strip, leading slot for a severity tag, label/description body, badge support (TReqoreBadge), and right-side action buttons. Supports the standard prop set: intent (info/success/warning/danger with tinted backgrounds), size, flat (border on `false`), rounded, fluid, transparent, raised (subtle 3D inset highlight when paired with `flat`), customTheme, tooltip, disabled, effect / labelEffect / descriptionEffect, wrap (single-line ellipsis on `false`), optional strip-hide, and click handlers. |
| **ReqoreSkeleton** | Animated skeleton/placeholder component for loading states with customizable dimensions. |
| **ReqoreSlider** | Range slider component supporting single values or ranges with optional labels and customizable styling. |
| **ReqoreSpacer** | Flexible spacing component that creates horizontal or vertical gaps with optional dividing lines. |
| **ReqoreSpan** | Inline text component with theme support and text effects. |
| **ReqoreSpinner** | Animated loading indicator with optional text label and multiple icon variants. |
-| **ReqoreStatistic** | Metric display component showing values with optional prefixes, suffixes, trends, and labels. |
+| **ReqoreStatistic** | Metric display component showing values with optional prefixes, suffixes, trends, and labels. Optional `raised` adds a subtle 3D inset highlight when the surface is also `rounded` and `flat`. |
| **ReqoreTable** | Advanced data table with sorting, filtering, column pinning, resizing, paging, and export capabilities. |
| **ReqoreTabs** | Tab interface component supporting closeable tabs, vertical orientation, and uncontrolled/controlled states. |
| **ReqoreTag** | Compact labeled component with optional icons, actions, colors, and badge styling. |
diff --git a/src/components/Callout/index.tsx b/src/components/Callout/index.tsx
index d5b28948..db53257b 100644
--- a/src/components/Callout/index.tsx
+++ b/src/components/Callout/index.tsx
@@ -1,7 +1,7 @@
-import { forwardRef, memo } from 'react';
import { rgba } from 'polished';
+import { forwardRef, memo, useMemo } from 'react';
import styled, { css } from 'styled-components';
-import { PADDING_FROM_SIZE, TEXT_FROM_SIZE } from '../../constants/sizes';
+import { PADDING_FROM_SIZE, RADIUS_FROM_SIZE, TEXT_FROM_SIZE } from '../../constants/sizes';
import { IReqoreTheme } from '../../constants/theme';
import {
changeDarkness,
@@ -9,8 +9,9 @@ import {
getMainBackgroundColor,
getReadableColor,
} from '../../helpers/colors';
+import { getOneLessSize } from '../../helpers/utils';
import { useReqoreTheme } from '../../hooks/useTheme';
-import { DisabledElement } from '../../styles';
+import { DisabledElement, RaisedElement } from '../../styles';
import {
IReqoreDisabled,
IReqoreIntent,
@@ -22,7 +23,13 @@ import {
IWithReqoreSize,
IWithReqoreTooltip,
} from '../../types/global';
-import { IReqoreEffect, StyledEffect, StyledTextEffect } from '../Effect';
+import { IReqoreIconName } from '../../types/icons';
+import ReqoreButton, { ButtonBadge, IReqoreButtonProps, TReqoreBadge } from '../Button';
+import ReqoreControlGroup from '../ControlGroup';
+import { IReqoreEffect, StyledEffect, StyledTextEffect, TReqoreEffectColor } from '../Effect';
+import ReqoreIcon, { IReqoreIconProps } from '../Icon';
+import { ReqoreP } from '../Paragraph';
+import { ReqoreSpan } from '../Span';
import { ReqoreTooltipComponent } from '../TooltipComponent';
export interface IReqoreCalloutProps
@@ -36,21 +43,58 @@ export interface IReqoreCalloutProps
IWithReqoreFluid,
IWithReqoreSize,
IWithReqoreTooltip {
+ /** Strong primary heading rendered above the body. */
+ label?: React.ReactNode;
+ /** Effect applied to the label. */
+ labelEffect?: IReqoreEffect;
+ /** Body copy rendered under the label. Falls back to `children`. */
+ description?: React.ReactNode;
+ /** Effect applied to the description. */
+ descriptionEffect?: IReqoreEffect;
+ /** Leading icon (typically an info / alert / error glyph). */
+ icon?: IReqoreIconName;
+ /** Color of the leading icon. Defaults to the intent color. */
+ iconColor?: TReqoreEffectColor;
+ /** Additional props passed to the leading ReqoreIcon. */
+ iconProps?: Partial;
+ /** Badge(s) shown next to the label, identical to other Reqore components. */
+ badge?: TReqoreBadge | TReqoreBadge[];
+ /** Renders a close button on the right edge and fires when clicked. */
+ onClose?: () => void;
+ /** Override props for the close button. */
+ closeButtonProps?: Partial;
+ /** Where the accent strip is rendered. */
accentPosition?: 'left' | 'top';
+ /** Thickness of the accent strip in pixels. */
accentSize?: number;
+ /** Effect applied to the legacy `children` slot. */
contentEffect?: IReqoreEffect;
+ /** Round the corners. Default `true`. */
rounded?: boolean;
+ /** Marks the callout as clickable; auto-detected from `onClick`. */
interactive?: boolean;
+ /** Hide the tinted surface background. */
+ transparent?: boolean;
+ /**
+ * Subtle 3D "raised" effect — inset top highlight + inset bottom shadow.
+ * Best paired with `flat={true}` (no border); the highlight is suppressed
+ * when `flat={false}` because the border already provides surface definition.
+ */
+ raised?: boolean;
}
-interface IStyledCalloutProps extends IReqoreCalloutProps {
+interface IStyledCalloutProps extends Omit {
theme: IReqoreTheme;
+ $transparent?: boolean;
+ $raised?: boolean;
+ $hasContent?: boolean;
}
const StyledCallout = styled(StyledEffect)`
position: relative;
display: flex;
- align-items: center;
+ align-items: flex-start;
+ gap: ${({ size = 'normal' }) => PADDING_FROM_SIZE[size] * 2}px;
width: ${({ fluid, fixed }) => (fluid && !fixed ? '100%' : undefined)};
max-width: 100%;
padding: ${({ size = 'normal', accentPosition = 'left', accentSize = 5 }) =>
@@ -61,10 +105,17 @@ const StyledCallout = styled(StyledEffect)`
: `${PADDING_FROM_SIZE[size] * 3 + accentSize}px ${PADDING_FROM_SIZE[size] * 3}px ${
PADDING_FROM_SIZE[size] * 3
}px`};
- background-color: ${({ theme }) => changeDarkness(getMainBackgroundColor(theme), 0.03)};
- border: ${({ theme, flat }) =>
- flat ? 0 : `1px solid ${changeLightness(getMainBackgroundColor(theme), 0.08)}`};
- border-radius: ${({ rounded }) => (rounded ? '8px' : 0)};
+ background-color: ${({ theme, $transparent }) =>
+ $transparent ? 'transparent' : changeDarkness(getMainBackgroundColor(theme), 0.03)};
+ border: ${({ theme, flat, intent }) =>
+ flat
+ ? 0
+ : `1px solid ${changeLightness(
+ intent ? theme.intents[intent] : getMainBackgroundColor(theme),
+ 0.08
+ )}`};
+ border-radius: ${({ rounded, size = 'normal' }) =>
+ rounded === false ? 0 : `${RADIUS_FROM_SIZE[size]}px`};
overflow: hidden;
flex: ${({ fluid }) => (fluid ? '1 auto' : '0 0 auto')};
transition:
@@ -92,6 +143,8 @@ const StyledCallout = styled(StyledEffect)`
intent ? theme.intents[intent] : changeLightness(getMainBackgroundColor(theme), 0.22)};
}
+ ${({ $raised, flat }) => $raised && flat !== false && RaisedElement}
+
${({ disabled }) => disabled && DisabledElement}
${({ interactive, theme, intent }) =>
@@ -110,6 +163,22 @@ const StyledCallout = styled(StyledEffect)`
: undefined}
`;
+const StyledCalloutBody = styled.div`
+ display: flex;
+ flex-flow: column;
+ gap: 6px;
+ flex: 1 1 auto;
+ min-width: 0;
+`;
+
+const StyledCalloutLabelRow = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
+ min-width: 0;
+`;
+
const StyledCalloutContent = styled(StyledTextEffect)<{
size: IReqoreCalloutProps['size'];
theme: IReqoreTheme;
@@ -125,6 +194,16 @@ export const ReqoreCallout = memo(
(
{
children,
+ label,
+ labelEffect,
+ description,
+ descriptionEffect,
+ icon,
+ iconColor,
+ iconProps,
+ badge,
+ onClose,
+ closeButtonProps,
size = 'normal',
customTheme,
inheritCustomTheme,
@@ -135,7 +214,9 @@ export const ReqoreCallout = memo(
fixed,
disabled,
tooltip,
- rounded,
+ rounded = true,
+ transparent = false,
+ raised,
effect,
contentEffect,
interactive,
@@ -148,6 +229,16 @@ export const ReqoreCallout = memo(
) => {
const theme = useReqoreTheme('main', customTheme, undefined, undefined, inheritCustomTheme);
const isInteractive = interactive || !!onClick;
+ const descriptionSize = useMemo(() => getOneLessSize(size), [size]);
+ const hasBadge = badge !== undefined && badge !== null;
+ const hasIcon = !!icon || !!iconProps?.image;
+ const hasStructuredContent = !!label || !!description;
+
+ const resolvedIconColor: TReqoreEffectColor = useMemo(() => {
+ if (iconColor) return iconColor;
+ if (intent) return theme.intents[intent] as TReqoreEffectColor;
+ return getReadableColor(theme, undefined, undefined, true) as TReqoreEffectColor;
+ }, [iconColor, intent, theme]);
return (
-
- {children}
-
+ {hasIcon && (
+
+ )}
+ {hasStructuredContent ? (
+
+ {label && (
+
+
+ {label}
+
+ {hasBadge && }
+
+ )}
+ {description && (
+
+ {description}
+
+ )}
+ {!label && hasBadge && }
+
+ ) : (
+
+ {children}
+ {hasBadge && (
+
+ )}
+
+ )}
+ {onClose && (
+
+ {
+ event.stopPropagation();
+ closeButtonProps?.onClick?.(event);
+ onClose();
+ }}
+ className={`reqore-callout-close ${closeButtonProps?.className ?? ''}`.trim()}
+ />
+
+ )}
);
}
diff --git a/src/components/EmptyState/index.tsx b/src/components/EmptyState/index.tsx
index 2037a8af..c85f8c19 100644
--- a/src/components/EmptyState/index.tsx
+++ b/src/components/EmptyState/index.tsx
@@ -49,6 +49,12 @@ export interface IReqoreEmptyStateProps
/** Background opacity */
opacity?: number;
gapSize?: TSizes;
+ /**
+ * Subtle 3D "raised" effect — inset top highlight + inset bottom shadow.
+ * Best paired with `flat={true}` (default); the highlight is suppressed
+ * when a border is rendered.
+ */
+ raised?: boolean;
}
export const ReqoreEmptyState = memo(
@@ -75,6 +81,7 @@ export const ReqoreEmptyState = memo(
effect,
className,
gapSize,
+ raised,
...rest
},
ref
@@ -107,6 +114,7 @@ export const ReqoreEmptyState = memo(
minimal
flat={flat}
padded='massive'
+ raised={raised}
{...rest}
tooltip={tooltip}
ref={ref}
diff --git a/src/components/EntityRow/index.tsx b/src/components/EntityRow/index.tsx
new file mode 100644
index 00000000..9918ed0d
--- /dev/null
+++ b/src/components/EntityRow/index.tsx
@@ -0,0 +1,355 @@
+import { rgba } from 'polished';
+import { forwardRef, memo, useMemo } from 'react';
+import styled, { css } from 'styled-components';
+import { PADDING_FROM_SIZE, RADIUS_FROM_SIZE, TSizes } from '../../constants/sizes';
+import { IReqoreTheme, TReqoreIntent } from '../../constants/theme';
+import { changeLightness, getMainBackgroundColor, getReadableColor } from '../../helpers/colors';
+import { getOneLessSize } from '../../helpers/utils';
+import { useReqoreTheme } from '../../hooks/useTheme';
+import { DisabledElement, RaisedElement } from '../../styles';
+import {
+ IReqoreDisabled,
+ IReqoreIntent,
+ IWithReqoreCustomTheme,
+ IWithReqoreEffect,
+ IWithReqoreFlat,
+ IWithReqoreFluid,
+ IWithReqoreSize,
+ IWithReqoreTooltip,
+} from '../../types/global';
+import { IReqoreIconName } from '../../types/icons';
+import ReqoreButton, { ButtonBadge, IReqoreButtonProps, TReqoreBadge } from '../Button';
+import ReqoreControlGroup from '../ControlGroup';
+import { IReqoreEffect, StyledEffect, TReqoreEffectColor } from '../Effect';
+import ReqoreIcon, { IReqoreIconProps } from '../Icon';
+import { ReqoreP } from '../Paragraph';
+import { ReqoreSpan } from '../Span';
+import { ReqoreTooltipComponent } from '../TooltipComponent';
+
+export interface IReqoreEntityRowAction extends Omit {
+ /** Visible button label. */
+ label?: string;
+}
+
+export interface IReqoreEntityRowProps
+ extends Omit, 'title'>,
+ IReqoreDisabled,
+ IReqoreIntent,
+ IWithReqoreCustomTheme,
+ IWithReqoreEffect,
+ IWithReqoreFlat,
+ IWithReqoreFluid,
+ IWithReqoreSize,
+ IWithReqoreTooltip {
+ /** Primary text — e.g. the Qog name. */
+ label: React.ReactNode;
+ /** Secondary text — typically a one-line description. */
+ description?: React.ReactNode;
+ /** Tertiary text — e.g. "Last run: success · 3 hours ago · 384ms". */
+ metadata?: React.ReactNode;
+ /** Badge(s) shown next to the label, identical to other Reqore components. */
+ badge?: TReqoreBadge | TReqoreBadge[];
+ /** Icon shown in the leading icon tile. */
+ icon?: IReqoreIconName;
+ /** Image rendered inside the leading icon tile (overrides `icon` if both set). */
+ iconImage?: string;
+ /** Custom color for the leading icon (defaults to intent or readable color). */
+ iconColor?: TReqoreEffectColor;
+ /** Additional props passed to the leading ReqoreIcon. */
+ iconProps?: Partial;
+ /** Right-side action button(s). */
+ actions?: IReqoreEntityRowAction[];
+ /** Hide the intent-tinted background. Mirrors `transparent` on other components. */
+ transparent?: boolean;
+ /** Round the row corners. Default `true`. */
+ rounded?: boolean;
+ /** Show the leading icon tile. Default `true` when icon/iconImage is provided. */
+ showIcon?: boolean;
+ /**
+ * Subtle 3D "raised" effect — inset top highlight + inset bottom shadow.
+ * Best paired with `flat={true}` (no border); the highlight is suppressed
+ * when `flat={false}` because the border already provides surface definition.
+ */
+ raised?: boolean;
+ /** Effect applied to the label text. */
+ labelEffect?: IReqoreEffect;
+ /** Effect applied to the description text. */
+ descriptionEffect?: IReqoreEffect;
+ /** Effect applied to the metadata text. */
+ metadataEffect?: IReqoreEffect;
+ /**
+ * Whether description and metadata wrap when they overflow.
+ * - `true` (default): wrap to multiple lines
+ * - `false`: single line with ellipsis
+ */
+ wrap?: boolean;
+}
+
+interface IStyledRowProps {
+ theme: IReqoreTheme;
+ $intent?: TReqoreIntent;
+ $transparent?: boolean;
+ size: TSizes;
+ $fluid?: boolean;
+ flat?: boolean;
+ $clickable?: boolean;
+ rounded?: boolean;
+ disabled?: boolean;
+ $hasIcon?: boolean;
+ $raised?: boolean;
+}
+
+const ICON_TILE_SIZE_FROM_SIZE: Record = {
+ micro: 18,
+ tiny: 22,
+ small: 26,
+ normal: 32,
+ big: 40,
+ huge: 48,
+ massive: 56,
+};
+
+const tintedBgFor = (theme: IReqoreTheme, intent?: TReqoreIntent) =>
+ intent
+ ? rgba(theme.intents[intent], 0.06)
+ : rgba(changeLightness(getMainBackgroundColor(theme), 0.04), 1);
+
+const StyledRow = styled(StyledEffect)`
+ display: grid;
+ grid-template-columns: ${({ $hasIcon, size }) =>
+ $hasIcon ? `${ICON_TILE_SIZE_FROM_SIZE[size]}px 1fr auto` : '1fr auto'};
+ gap: ${({ size }) => PADDING_FROM_SIZE[size] * 2}px;
+ padding: ${({ size }) => PADDING_FROM_SIZE[size] * 2}px
+ ${({ size }) => PADDING_FROM_SIZE[size] * 3}px;
+ border-radius: ${({ rounded, size }) => (rounded ? `${RADIUS_FROM_SIZE[size]}px` : '0')};
+ background-color: ${({ theme, $intent, $transparent }) =>
+ $transparent ? 'transparent' : tintedBgFor(theme, $intent)};
+ border: ${({ flat, theme, $intent }) =>
+ flat
+ ? 'none'
+ : `1px solid ${changeLightness(
+ $intent ? theme.intents[$intent] : getMainBackgroundColor(theme),
+ 0.08
+ )}`};
+ align-items: center;
+ width: ${({ $fluid }) => ($fluid ? '100%' : 'auto')};
+ color: ${({ theme }) => getReadableColor(theme, undefined, undefined, true)};
+ transition: background-color 0.15s ease-out;
+
+ ${({ $clickable, theme, $intent }) =>
+ $clickable &&
+ css`
+ cursor: pointer;
+ &:hover {
+ background-color: ${$intent
+ ? rgba(theme.intents[$intent], 0.1)
+ : rgba(changeLightness(getMainBackgroundColor(theme), 0.08), 1)};
+ }
+ `}
+
+ ${({ $raised, flat }) => $raised && flat !== false && RaisedElement}
+
+ ${({ disabled }) => disabled && DisabledElement}
+`;
+
+interface IStyledIconTileProps {
+ $size: TSizes;
+ $intent?: TReqoreIntent;
+ theme: IReqoreTheme;
+}
+
+const StyledIconTile = styled.div`
+ width: ${({ $size }) => ICON_TILE_SIZE_FROM_SIZE[$size]}px;
+ height: ${({ $size }) => ICON_TILE_SIZE_FROM_SIZE[$size]}px;
+ border-radius: ${({ $size }) => RADIUS_FROM_SIZE[$size]}px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ background-color: ${({ theme, $intent }) =>
+ $intent
+ ? rgba(theme.intents[$intent], 0.15)
+ : rgba(changeLightness(getMainBackgroundColor(theme), 0.06), 1)};
+`;
+
+const StyledBody = styled.div`
+ display: flex;
+ flex-flow: column;
+ gap: 2px;
+ min-width: 0;
+`;
+
+const StyledLabelLine = styled.div<{ $wrap: boolean }>`
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: ${({ $wrap }) => ($wrap ? 'wrap' : 'nowrap')};
+ min-width: 0;
+ font-weight: 500;
+`;
+
+// `text-overflow: ellipsis` only works on the actual text-bearing element,
+// not its wrapper. Cascade into the inner / via `& > *`
+// so the `…` glyph appears. The wrapper itself stays a flex item that can
+// shrink below its content (`min-width: 0; flex: 1 1 auto`).
+const StyledTextSlot = styled.div<{ $wrap: boolean }>`
+ min-width: 0;
+ ${({ $wrap }) =>
+ !$wrap &&
+ css`
+ flex: 1 1 auto;
+ overflow: hidden;
+
+ & > * {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ display: block;
+ max-width: 100%;
+ }
+ `}
+`;
+
+const ReqoreEntityRow = memo(
+ forwardRef(
+ (
+ {
+ label,
+ description,
+ metadata,
+ badge,
+ icon,
+ iconImage,
+ iconColor,
+ iconProps,
+ actions,
+ intent,
+ transparent = false,
+ showIcon,
+ size = 'normal',
+ flat = true,
+ fluid = true,
+ rounded = true,
+ raised,
+ customTheme,
+ inheritCustomTheme,
+ disabled,
+ tooltip,
+ effect,
+ labelEffect,
+ descriptionEffect,
+ metadataEffect,
+ wrap = true,
+ className,
+ ...rest
+ },
+ ref
+ ) => {
+ const theme = useReqoreTheme('main', customTheme, undefined, undefined, inheritCustomTheme);
+ const secondarySize = useMemo(() => getOneLessSize(size), [size]);
+
+ const hasIcon = (showIcon ?? (!!icon || !!iconImage)) && (!!icon || !!iconImage);
+ const interactive = !!(rest.onClick || rest.onDoubleClick);
+ const hasBadge = badge !== undefined && badge !== null;
+
+ const resolvedIconColor: TReqoreEffectColor = useMemo(() => {
+ if (iconColor) return iconColor;
+ if (intent) return theme.intents[intent] as TReqoreEffectColor;
+ return getReadableColor(theme, undefined, undefined, true) as TReqoreEffectColor;
+ }, [iconColor, intent, theme]);
+
+ return (
+
+ {hasIcon && (
+
+
+
+ )}
+
+
+
+
+ {label}
+
+
+ {hasBadge && (
+
+ )}
+
+ {description && (
+
+
+ {description}
+
+
+ )}
+ {metadata && (
+
+
+ {metadata}
+
+
+ )}
+
+ {actions && actions.length > 0 && (
+
+ {actions.map((action, idx) => (
+
+ {action.label}
+
+ ))}
+
+ )}
+
+ );
+ }
+ )
+);
+
+export default ReqoreEntityRow;
diff --git a/src/components/FeatureCard/index.tsx b/src/components/FeatureCard/index.tsx
index c1139e3d..71be0f4b 100644
--- a/src/components/FeatureCard/index.tsx
+++ b/src/components/FeatureCard/index.tsx
@@ -1,9 +1,10 @@
-import { forwardRef, memo, useMemo } from 'react';
import { rgba } from 'polished';
+import { forwardRef, memo, useMemo } from 'react';
import styled, { css } from 'styled-components';
import {
HEADER_SIZE_TO_NUMBER,
PADDING_FROM_SIZE,
+ RADIUS_FROM_SIZE,
TEXT_FROM_SIZE,
TSizes,
} from '../../constants/sizes';
@@ -16,7 +17,7 @@ import {
} from '../../helpers/colors';
import { getOneLessSize } from '../../helpers/utils';
import { useReqoreTheme } from '../../hooks/useTheme';
-import { DisabledElement } from '../../styles';
+import { DisabledElement, RaisedElement } from '../../styles';
import {
IReqoreDisabled,
IReqoreIntent,
@@ -28,6 +29,7 @@ import {
IWithReqoreSize,
IWithReqoreTooltip,
} from '../../types/global';
+import { ButtonBadge, TReqoreBadge } from '../Button';
import { IReqoreEffect, StyledEffect } from '../Effect';
import { ReqoreHeading } from '../Header';
import { ReqoreP } from '../Paragraph';
@@ -46,19 +48,46 @@ export interface IReqoreFeatureCardProps
IWithReqoreFluid,
IWithReqoreSize,
IWithReqoreTooltip {
+ /** Card heading. */
label: React.ReactNode;
+ /** Effect applied to the label heading. */
labelEffect?: IReqoreEffect;
+ /** Body copy under the label. */
description?: React.ReactNode;
+ /** Effect applied to the description paragraph. */
descriptionEffect?: IReqoreEffect;
+ /** Visual marker rendered above the label. */
marker?: TReqoreFeatureCardMarker;
+ /** Used when `marker === 'number'`. */
markerLabel?: string | number;
+ /** Effect applied to the marker label. */
markerEffect?: IReqoreEffect;
+ /** Badge(s) shown next to the label, identical to other Reqore components. */
+ badge?: TReqoreBadge | TReqoreBadge[];
+ /** Round the card corners. Default `true`. */
rounded?: boolean;
+ /** Marks the card as clickable; auto-detected from `onClick`. */
interactive?: boolean;
+ /** Hide the card's tinted background. */
+ transparent?: boolean;
+ /**
+ * Subtle 3D "raised" effect — inset top highlight + inset bottom shadow.
+ * Best paired with `flat={true}` (no border); the highlight is suppressed
+ * when `flat={false}` because the border already provides surface definition.
+ */
+ raised?: boolean;
+ /**
+ * Whether the description wraps when it overflows.
+ * - `true` (default): wrap to multiple lines
+ * - `false`: single line with ellipsis
+ */
+ wrap?: boolean;
}
-interface IStyledFeatureCardProps extends IReqoreFeatureCardProps {
+interface IStyledFeatureCardProps extends Omit {
theme: IReqoreTheme;
+ $transparent?: boolean;
+ $raised?: boolean;
}
const StyledFeatureCard = styled(StyledEffect)`
@@ -68,7 +97,8 @@ const StyledFeatureCard = styled(StyledEffect)`
width: ${({ fluid, fixed }) => (fluid && !fixed ? '100%' : undefined)};
max-width: 100%;
padding: ${({ size = 'normal' }) => PADDING_FROM_SIZE[size] * 3}px;
- background-color: ${({ theme }) => changeDarkness(getMainBackgroundColor(theme), 0.03)};
+ background-color: ${({ theme, $transparent }) =>
+ $transparent ? 'transparent' : changeDarkness(getMainBackgroundColor(theme), 0.03)};
border: ${({ theme, intent, flat }) =>
flat
? 0
@@ -76,15 +106,15 @@ const StyledFeatureCard = styled(StyledEffect)`
intent ? theme.intents[intent] : getMainBackgroundColor(theme),
0.08
)}`};
- border-radius: ${({ rounded }) => (rounded ? '8px' : 0)};
+ border-radius: ${({ rounded, size = 'normal' }) =>
+ rounded === false ? 0 : `${RADIUS_FROM_SIZE[size]}px`};
color: ${({ theme }) => getReadableColor(theme, undefined, undefined, true)};
overflow: hidden;
position: relative;
flex: ${({ fluid }) => (fluid ? '1 auto' : '0 0 auto')};
- transition:
- border-color 0.16s ease,
- background-color 0.16s ease,
- transform 0.16s ease;
+ transition: border-color 0.16s ease, background-color 0.16s ease, transform 0.16s ease;
+
+ ${({ $raised, flat }) => $raised && flat !== false && RaisedElement}
${({ disabled }) => disabled && DisabledElement}
@@ -130,8 +160,10 @@ const StyledFeatureCardMarker = styled.div<{
: changeLightness(getMainBackgroundColor(theme), 0.22)};
box-shadow: 0 0 22px
${rgba(
- intent ? theme.intents[intent] : changeLightness(getMainBackgroundColor(theme), 0.22),
- 0.3
+ intent
+ ? theme.intents[intent]
+ : changeLightness(getMainBackgroundColor(theme), 0.22),
+ 0.8
)};
}
`
@@ -144,6 +176,34 @@ const StyledFeatureCardContent = styled.div`
gap: 12px;
`;
+const StyledLabelRow = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
+ min-width: 0;
+`;
+
+// Cascade ellipsis CSS into the inner ReqoreP — `text-overflow: ellipsis`
+// only takes effect on the actual text-bearing element.
+const StyledTextSlot = styled.div<{ $wrap: boolean }>`
+ min-width: 0;
+ ${({ $wrap }) =>
+ !$wrap &&
+ css`
+ flex: 1 1 auto;
+ overflow: hidden;
+
+ & > * {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ display: block;
+ max-width: 100%;
+ }
+ `}
+`;
+
export const ReqoreFeatureCard = memo(
forwardRef(
(
@@ -155,6 +215,7 @@ export const ReqoreFeatureCard = memo(
marker = 'line',
markerLabel,
markerEffect,
+ badge,
size = 'normal',
customTheme,
inheritCustomTheme,
@@ -165,7 +226,10 @@ export const ReqoreFeatureCard = memo(
fixed,
disabled,
tooltip,
- rounded,
+ rounded = true,
+ transparent = false,
+ raised,
+ wrap = true,
effect,
interactive,
onClick,
@@ -174,12 +238,10 @@ export const ReqoreFeatureCard = memo(
ref
) => {
const theme = useReqoreTheme('main', customTheme, undefined, undefined, inheritCustomTheme);
- const labelSize = useMemo(
- () => HEADER_SIZE_TO_NUMBER[size] as 1 | 2 | 3 | 4 | 5 | 6,
- [size]
- );
+ const labelSize = useMemo(() => HEADER_SIZE_TO_NUMBER[size] as 1 | 2 | 3 | 4 | 5 | 6, [size]);
const descriptionSize = useMemo(() => getOneLessSize(size), [size]);
const isInteractive = interactive || !!onClick;
+ const hasBadge = badge !== undefined && badge !== null;
return (
)}
-
- {label}
-
+
+
+
+ {label}
+
+
+ {hasBadge && }
+
{description && (
-
- {description}
-
+
+
+ {description}
+
+
)}
diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx
index fbafc939..666b2228 100644
--- a/src/components/Icon/index.tsx
+++ b/src/components/Icon/index.tsx
@@ -1,3 +1,4 @@
+import { rgba } from 'polished';
import React, { forwardRef, memo, useMemo } from 'react';
import { IconContext } from 'react-icons';
import { IconBaseProps, IconType } from 'react-icons/lib';
@@ -5,8 +6,9 @@ import * as RemixIcons from 'react-icons/ri';
import styled, { css, keyframes } from 'styled-components';
import { useReqoreTheme } from '../..';
import { ICON_FROM_SIZE, PADDING_FROM_SIZE, TSizes } from '../../constants/sizes';
-import { getColorFromMaybeString } from '../../helpers/colors';
+import { getColorFromMaybeString, getReadableColor } from '../../helpers/colors';
import { isStringSize } from '../../helpers/utils';
+import { useReqoreProperty } from '../../hooks/useReqoreContext';
import { IReqoreIntent, IWithReqoreEffect, IWithReqoreTooltip } from '../../types/global';
import { IReqoreIconName } from '../../types/icons';
import { StyledEffect, TReqoreEffectColor } from '../Effect';
@@ -33,6 +35,21 @@ export interface IReqoreIconProps
animation?: 'spin' | 'heartbeat';
interactive?: boolean;
compact?: boolean;
+ /**
+ * Subtle glow rendered behind the icon shape via `filter: drop-shadow(...)`
+ * so it follows the icon's actual contour rather than its bounding box.
+ *
+ * - `true` — glow with the icon's resolved colour (intent / `color` prop / readable default)
+ * - `TReqoreEffectColor` — glow with that specific colour
+ * - object — `{ color?, blur?, opacity? }` for full control
+ */
+ glow?: boolean | TReqoreEffectColor | IReqoreGlowConfig;
+}
+
+export interface IReqoreGlowConfig {
+ color?: TReqoreEffectColor;
+ blur?: number;
+ opacity?: number;
}
const SpinKeyframes = keyframes`
@@ -109,11 +126,13 @@ const ReqoreIcon = memo(
iconProps,
intent,
image,
+ glow,
...rest
}: IReqoreIconProps,
ref
) => {
const theme = useReqoreTheme();
+ const glowingIconsDefault = useReqoreProperty('glowingIcons');
const Icon: IconType = RemixIcons[`Ri${icon}`];
const finalColor: string | undefined = useMemo(
() => (intent ? theme.intents[intent] : getColorFromMaybeString(theme, color)),
@@ -128,9 +147,45 @@ const ReqoreIcon = memo(
const finalMarginSize: number = isStringSize(marginSize)
? PADDING_FROM_SIZE[marginSize]
: marginSize;
+
+ // Resolve the glow shorthand into a `filter: drop-shadow(...)` value so
+ // the glow follows the SVG outline rather than the wrapper's bounding box.
+ // When the local `glow` prop is undefined, fall back to the global
+ // `glowingIcons` UI option so a single switch can turn glows on app-wide.
+ // Pass `glow={false}` explicitly to opt a single icon out.
+ const effectiveGlow = glow === undefined ? glowingIconsDefault : glow;
+ const glowFilter = useMemo(() => {
+ if (!effectiveGlow) return undefined;
+ const config: IReqoreGlowConfig =
+ typeof effectiveGlow === 'boolean'
+ ? {}
+ : typeof effectiveGlow === 'string'
+ ? { color: effectiveGlow }
+ : effectiveGlow;
+ // Resolution order: explicit glow.color → icon's intent/color prop.
+ // When the prop was set explicitly (not via the global `glowingIcons`
+ // option), fall back to a theme-readable colour so a plain icon still
+ // glows. The global flag intentionally does NOT force-glow uncoloured
+ // icons — it only enhances icons that already have an intent/colour.
+ const explicit = glow !== undefined;
+ const resolved =
+ (config.color ? (getColorFromMaybeString(theme, config.color) as string) : undefined) ??
+ finalColor ??
+ (explicit ? getReadableColor(theme, undefined, undefined, true) : undefined);
+ if (!resolved) return undefined;
+ const blur = config.blur ?? 5;
+ const opacity = config.opacity ?? 0.8;
+ return `drop-shadow(0 0 ${blur}px ${rgba(resolved, opacity)})`;
+ }, [effectiveGlow, glow, finalColor, theme]);
+
const finalStyle = useMemo(
- () => ({ width: finalWrapperSize, height: finalWrapperSize, ...style }),
- [finalWrapperSize, style]
+ () => ({
+ width: finalWrapperSize,
+ height: finalWrapperSize,
+ ...(glowFilter ? { filter: glowFilter } : {}),
+ ...style,
+ }),
+ [finalWrapperSize, glowFilter, style]
);
if (image) {
diff --git a/src/components/Message/index.tsx b/src/components/Message/index.tsx
index 1dfa4447..e1caf884 100644
--- a/src/components/Message/index.tsx
+++ b/src/components/Message/index.tsx
@@ -55,6 +55,12 @@ export interface IReqoreMessageProps
hasShadow?: boolean;
margin?: 'top' | 'bottom' | 'both' | 'none';
backgroundBlur?: number;
+ /**
+ * Subtle 3D "raised" effect — inset top highlight + inset bottom shadow.
+ * Best paired with `flat={true}` and a non-`minimal` message; suppressed
+ * when `flat={false}` (border) or `minimal={true}` (no surface).
+ */
+ raised?: boolean;
}
export interface IReqoreNotificationStyle extends IReqoreMessageProps {
diff --git a/src/components/Notifications/notification.tsx b/src/components/Notifications/notification.tsx
index 208e2b81..b3ab0cfd 100644
--- a/src/components/Notifications/notification.tsx
+++ b/src/components/Notifications/notification.tsx
@@ -22,6 +22,7 @@ import {
IWithReqoreOpaque,
} from '../../types/global';
import { IReqoreIconName } from '../../types/icons';
+import { RaisedElement } from '../../styles';
import { StyledEffect } from '../Effect';
import { ReqoreHeading } from '../Header';
import ReqoreIcon from '../Icon';
@@ -63,6 +64,7 @@ export interface IReqoreNotificationStyle extends IWithReqoreOpaque {
asMessage?: boolean;
margin?: 'top' | 'bottom' | 'both' | 'none';
backgroundBlur?: number;
+ raised?: boolean;
}
const timeoutAnimation = keyframes`
@@ -120,6 +122,7 @@ export const StyledReqoreNotification = styled(StyledEffect) css`
background-color: ${minimal
? 'transparent'
@@ -139,6 +142,8 @@ export const StyledReqoreNotification = styled(StyledEffect)`
`
: undefined}
+ ${({ raised, flat, intent }) => raised && flat && !intent && RaisedElement}
+
${({ fill, isCollapsed }) =>
!isCollapsed && fill
? css`
diff --git a/src/components/Progress/index.tsx b/src/components/Progress/index.tsx
index 4dec54c9..ff549046 100644
--- a/src/components/Progress/index.tsx
+++ b/src/components/Progress/index.tsx
@@ -63,6 +63,17 @@ export interface IReqoreProgressProps
rightIconColor?: TReqoreEffectColor;
/** Right icon props */
rightIconProps?: IReqoreIconProps;
+ /**
+ * Target marker drawn on top of the track at this value (must be between 0 and `max`).
+ * Useful for visualising goals (e.g. "90% target coverage"). Hidden when not set.
+ */
+ target?: number;
+ /** Optional label rendered above the target marker. Defaults to the target percentage. */
+ targetLabel?: string;
+ /** Whether to render the marker label above the marker line. Defaults to `true`. */
+ showTargetLabel?: boolean;
+ /** Custom color for the target marker line/label. Falls back to a theme-readable colour. */
+ targetColor?: TReqoreEffectColor;
}
export interface IReqoreProgressStyle extends IReqoreProgressProps {
@@ -156,6 +167,38 @@ const StyledProgressTrack = styled.div`
flat === false ? `1px solid ${rgba(getProgressColor(theme), 0.6)}` : 'none'};
`;
+interface IStyledProgressTargetProps {
+ $left: number;
+ $color: string;
+}
+
+const StyledProgressTargetMarker = styled.div`
+ position: absolute;
+ top: -2px;
+ bottom: -2px;
+ left: ${({ $left }) => $left}%;
+ width: 2px;
+ background-color: ${({ $color }) => $color};
+ border-radius: 1px;
+ pointer-events: none;
+ z-index: 2;
+`;
+
+const StyledProgressTargetLabel = styled.div<{ $left: number; $color: string }>`
+ position: absolute;
+ bottom: calc(100% + 4px);
+ left: ${({ $left }) => $left}%;
+ transform: translateX(-50%);
+ font-size: 9px;
+ font-weight: 600;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ color: ${({ $color }) => $color};
+ white-space: nowrap;
+ pointer-events: none;
+ z-index: 2;
+`;
+
const StyledProgressBar = styled.div`
position: absolute;
top: 0;
@@ -218,6 +261,10 @@ const ReqoreProgress = memo(
rightIcon,
rightIconColor,
rightIconProps,
+ target,
+ targetLabel,
+ showTargetLabel = true,
+ targetColor,
...rest
},
ref
@@ -238,6 +285,23 @@ const ReqoreProgress = memo(
return `${Math.round(percentage)}%`;
}, [showValue, percentage, indeterminate]);
+ const targetPercent = useMemo(() => {
+ if (target === undefined || indeterminate || max <= 0) return null;
+ const clamped = Math.max(0, Math.min(target, max));
+ return (clamped / max) * 100;
+ }, [target, max, indeterminate]);
+
+ const resolvedTargetColor = useMemo(() => {
+ if (targetColor) return targetColor as string;
+ return getReadableColor(baseTheme, undefined, undefined, true);
+ }, [targetColor, baseTheme]);
+
+ const resolvedTargetLabel = useMemo(() => {
+ if (targetPercent === null) return null;
+ if (targetLabel !== undefined) return targetLabel;
+ return `Target ${Math.round(targetPercent)}%`;
+ }, [targetPercent, targetLabel]);
+
const hasLabels = !!(
label ||
icon ||
@@ -304,6 +368,25 @@ const ReqoreProgress = memo(
animated={animated}
className='reqore-progress-bar'
/>
+ {targetPercent !== null && (
+ <>
+ {showTargetLabel && resolvedTargetLabel && (
+
+ {resolvedTargetLabel}
+
+ )}
+
+ >
+ )}
);
diff --git a/src/components/SeverityRow/index.tsx b/src/components/SeverityRow/index.tsx
new file mode 100644
index 00000000..2fc27eec
--- /dev/null
+++ b/src/components/SeverityRow/index.tsx
@@ -0,0 +1,295 @@
+import { rgba } from 'polished';
+import { forwardRef, memo, useMemo } from 'react';
+import styled, { css } from 'styled-components';
+import { PADDING_FROM_SIZE, RADIUS_FROM_SIZE, TSizes } from '../../constants/sizes';
+import { IReqoreTheme, TReqoreIntent } from '../../constants/theme';
+import { changeLightness, getMainBackgroundColor, getReadableColor } from '../../helpers/colors';
+import { getOneLessSize } from '../../helpers/utils';
+import { useReqoreTheme } from '../../hooks/useTheme';
+import { DisabledElement, RaisedElement } from '../../styles';
+import {
+ IReqoreDisabled,
+ IReqoreIntent,
+ IWithReqoreCustomTheme,
+ IWithReqoreEffect,
+ IWithReqoreFlat,
+ IWithReqoreFluid,
+ IWithReqoreSize,
+ IWithReqoreTooltip,
+} from '../../types/global';
+import ReqoreButton, { ButtonBadge, IReqoreButtonProps, TReqoreBadge } from '../Button';
+import ReqoreControlGroup from '../ControlGroup';
+import { IReqoreEffect, StyledEffect } from '../Effect';
+import { ReqoreP } from '../Paragraph';
+import { ReqoreSpan } from '../Span';
+import { ReqoreTooltipComponent } from '../TooltipComponent';
+
+export interface IReqoreSeverityRowAction extends Omit {
+ /** Visible button label. */
+ label?: string;
+}
+
+export interface IReqoreSeverityRowProps
+ extends Omit, 'title'>,
+ IReqoreDisabled,
+ IReqoreIntent,
+ IWithReqoreCustomTheme,
+ IWithReqoreEffect,
+ IWithReqoreFlat,
+ IWithReqoreFluid,
+ IWithReqoreSize,
+ IWithReqoreTooltip {
+ /** Primary line — e.g. "Payment Processing · stripe-webhook-receiver". */
+ label: React.ReactNode;
+ /** Secondary line — e.g. "Avg duration 4.7s exceeded 3.5s threshold · just now". */
+ description?: React.ReactNode;
+ /** Optional inline content rendered before the label (e.g. severity Tag). */
+ leading?: React.ReactNode;
+ /** Badge(s) shown next to the label, identical to other Reqore components. */
+ badge?: TReqoreBadge | TReqoreBadge[];
+ /** Right-side action buttons (Investigate / Dismiss). */
+ actions?: IReqoreSeverityRowAction[];
+ /** Hide the intent-tinted background. Mirrors `transparent` on other components. */
+ transparent?: boolean;
+ /** Show the colored severity strip on the left edge. Default `true`. */
+ showStrip?: boolean;
+ /** Rounded corners on the row. Default `true`. */
+ rounded?: boolean;
+ /**
+ * Subtle 3D "raised" effect — inset top highlight + inset bottom shadow.
+ * Best paired with `flat={true}` (no border) since a border already provides
+ * surface definition; the highlight is suppressed when `flat={false}`.
+ */
+ raised?: boolean;
+ /** Effect applied to the label text. */
+ labelEffect?: IReqoreEffect;
+ /** Effect applied to the description text. */
+ descriptionEffect?: IReqoreEffect;
+ /**
+ * Whether the description wraps when it overflows.
+ * - `true` (default): wrap to multiple lines
+ * - `false`: single line with ellipsis
+ */
+ wrap?: boolean;
+}
+
+interface IStyledRowProps {
+ theme: IReqoreTheme;
+ $intent?: TReqoreIntent;
+ $transparent?: boolean;
+ size: TSizes;
+ $fluid?: boolean;
+ flat?: boolean;
+ $clickable?: boolean;
+ rounded?: boolean;
+ disabled?: boolean;
+ $raised?: boolean;
+}
+
+const stripColorFor = (theme: IReqoreTheme, intent?: TReqoreIntent) =>
+ intent ? theme.intents[intent] : changeLightness(getMainBackgroundColor(theme), 0.16);
+
+const tintedBgFor = (theme: IReqoreTheme, intent?: TReqoreIntent) =>
+ intent
+ ? rgba(theme.intents[intent], 0.06)
+ : rgba(changeLightness(getMainBackgroundColor(theme), 0.04), 1);
+
+const StyledRow = styled(StyledEffect)`
+ display: grid;
+ grid-template-columns: 4px 1fr auto;
+ gap: ${({ size }) => PADDING_FROM_SIZE[size] * 2}px;
+ padding: ${({ size }) => PADDING_FROM_SIZE[size] * 2}px
+ ${({ size }) => PADDING_FROM_SIZE[size] * 3}px;
+ border-radius: ${({ rounded, size }) => (rounded ? `${RADIUS_FROM_SIZE[size]}px` : '0')};
+ background-color: ${({ theme, $intent, $transparent }) =>
+ $transparent ? 'transparent' : tintedBgFor(theme, $intent)};
+ border: ${({ flat, theme, $intent }) =>
+ flat
+ ? 'none'
+ : `1px solid ${changeLightness(
+ $intent ? theme.intents[$intent] : getMainBackgroundColor(theme),
+ 0.08
+ )}`};
+ align-items: center;
+ width: ${({ $fluid }) => ($fluid ? '100%' : 'auto')};
+ color: ${({ theme }) => getReadableColor(theme, undefined, undefined, true)};
+ transition: background-color 0.15s ease-out;
+
+ ${({ $clickable, theme, $intent }) =>
+ $clickable &&
+ css`
+ cursor: pointer;
+ &:hover {
+ background-color: ${$intent
+ ? rgba(theme.intents[$intent], 0.1)
+ : rgba(changeLightness(getMainBackgroundColor(theme), 0.08), 1)};
+ }
+ `}
+
+ ${({ $raised, flat }) => $raised && flat !== false && RaisedElement}
+
+ ${({ disabled }) => disabled && DisabledElement}
+`;
+
+const StyledStrip = styled.div<{ $intent?: TReqoreIntent; theme: IReqoreTheme }>`
+ align-self: stretch;
+ border-radius: 2px;
+ background-color: ${({ theme, $intent }) => stripColorFor(theme, $intent)};
+ // Subtle glow matching the strip colour — same pattern as FeatureCard's
+ // line marker. Pulls focus to the row without using a heavier border.
+ box-shadow: 0 0 22px ${({ theme, $intent }) => rgba(stripColorFor(theme, $intent), 0.3)};
+`;
+
+const StyledBody = styled.div`
+ display: flex;
+ flex-flow: column;
+ gap: 4px;
+ min-width: 0;
+`;
+
+const StyledLabelLine = styled.div<{ $wrap: boolean }>`
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: ${({ $wrap }) => ($wrap ? 'wrap' : 'nowrap')};
+ min-width: 0;
+ font-weight: 500;
+`;
+
+// `text-overflow: ellipsis` only works on the actual text-bearing element,
+// not its wrapper. Cascade the ellipsis CSS into the child /
+// so the `…` glyph actually appears, while keeping the wrapper itself a flex
+// item that can shrink (`min-width: 0; flex: 1 1 auto`) inside its parent.
+const StyledTextSlot = styled.div<{ $wrap: boolean }>`
+ min-width: 0;
+ ${({ $wrap }) =>
+ !$wrap &&
+ css`
+ flex: 1 1 auto;
+ overflow: hidden;
+
+ & > * {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ display: block;
+ max-width: 100%;
+ }
+ `}
+`;
+
+const ReqoreSeverityRow = memo(
+ forwardRef(
+ (
+ {
+ label,
+ description,
+ leading,
+ badge,
+ actions,
+ intent,
+ transparent = false,
+ showStrip = true,
+ size = 'normal',
+ flat = true,
+ fluid = true,
+ rounded = true,
+ raised,
+ customTheme,
+ inheritCustomTheme,
+ disabled,
+ tooltip,
+ effect,
+ labelEffect,
+ descriptionEffect,
+ wrap = true,
+ className,
+ ...rest
+ },
+ ref
+ ) => {
+ const theme = useReqoreTheme('main', customTheme, undefined, undefined, inheritCustomTheme);
+ const secondarySize = useMemo(() => getOneLessSize(size), [size]);
+
+ const interactive = !!(rest.onClick || rest.onDoubleClick);
+
+ const hasBadge = badge !== undefined && badge !== null;
+
+ return (
+
+ {showStrip ? (
+
+ ) : (
+
+ )}
+
+
+ {leading}
+
+
+ {label}
+
+
+ {hasBadge && (
+
+ )}
+
+ {description && (
+
+
+ {description}
+
+
+ )}
+
+ {actions && actions.length > 0 && (
+
+ {actions.map((action, idx) => (
+
+ {action.label}
+
+ ))}
+
+ )}
+
+ );
+ }
+ )
+);
+
+export default ReqoreSeverityRow;
diff --git a/src/components/Statistic/index.tsx b/src/components/Statistic/index.tsx
index e2c063b5..30eb922d 100644
--- a/src/components/Statistic/index.tsx
+++ b/src/components/Statistic/index.tsx
@@ -11,7 +11,7 @@ import {
} from '../../helpers/colors';
import { alignToFlexAlign, getOneHigherSize, getOneLessSize } from '../../helpers/utils';
import { useReqoreTheme } from '../../hooks/useTheme';
-import { DisabledElement, InactiveIconScale, ScaleIconOnHover } from '../../styles';
+import { DisabledElement, InactiveIconScale, RaisedElement, ScaleIconOnHover } from '../../styles';
import {
IReqoreDisabled,
IReqoreIntent,
@@ -81,6 +81,12 @@ export interface IReqoreStatisticProps
transparent?: boolean;
/** Background opacity */
opacity?: number;
+ /**
+ * Subtle 3D "raised" effect — inset top highlight + inset bottom shadow.
+ * Best paired with `flat={true}` (no border) and `rounded` so the surface
+ * reads as a tactile card; the highlight is suppressed when `flat={false}`.
+ */
+ raised?: boolean;
}
interface IStyledStatisticWrapper {
@@ -95,6 +101,7 @@ interface IStyledStatisticWrapper {
flat?: boolean;
intent?: string;
opacity?: number;
+ $raised?: boolean;
}
const TREND_ICONS: Record = {
@@ -129,6 +136,9 @@ const StyledStatisticWrapper = styled(StyledEffect)`
padding: ${PADDING_FROM_SIZE[size] * 3}px ${PADDING_FROM_SIZE[size] * 5}px;
`}
+ ${({ $raised, $hasBackground, flat }) =>
+ $raised && $hasBackground && flat !== false && RaisedElement}
+
${({ $interactive, disabled }) =>
$interactive && !disabled
? css`
@@ -184,6 +194,7 @@ const ReqoreStatistic = memo(
rounded,
transparent,
opacity,
+ raised,
className,
...rest
},
@@ -200,8 +211,9 @@ const ReqoreStatistic = memo(
);
const hasBackground = useMemo(
- () => !!(effect || rounded || flat !== undefined || transparent || opacity !== undefined),
- [effect, rounded, flat, transparent, opacity]
+ () =>
+ !!(effect || rounded || flat !== undefined || transparent || opacity !== undefined || raised),
+ [effect, rounded, flat, transparent, opacity, raised]
);
const trendIntent = useMemo(
@@ -243,6 +255,7 @@ const ReqoreStatistic = memo(
flat={flat}
intent={intent}
opacity={transparent ? 0 : opacity}
+ $raised={raised}
effect={transformedEffect}
className={`${className || ''} reqore-statistic`}
>
diff --git a/src/containers/ReqoreProvider.tsx b/src/containers/ReqoreProvider.tsx
index b7085431..be910f8b 100644
--- a/src/containers/ReqoreProvider.tsx
+++ b/src/containers/ReqoreProvider.tsx
@@ -262,6 +262,7 @@ const ReqoreProvider: React.FC = memo(({ children, options
options && 'closeModalsOnEscPress' in options ? options.closeModalsOnEscPress : true,
customPortalId: options?.customPortalId,
uiScale: options?.uiScale,
+ glowingIcons: options?.glowingIcons ?? false,
errorBoundaryOptions: options?.errorBoundaryOptions || DEFAULT_ERROR_BOUNDARY_OPTIONS,
}),
[options]
@@ -284,6 +285,7 @@ const ReqoreProvider: React.FC = memo(({ children, options
getAndIncreaseZIndex,
animations: resolvedOptions.animations,
tooltips: resolvedOptions.tooltips,
+ glowingIcons: resolvedOptions.glowingIcons,
closePopoversOnEscPress: resolvedOptions.closePopoversOnEscPress,
// ESC Closable modals management
closeModalsOnEscPress: resolvedOptions.closeModalsOnEscPress,
diff --git a/src/containers/UIProvider.tsx b/src/containers/UIProvider.tsx
index de1816b7..5ac0c54d 100644
--- a/src/containers/UIProvider.tsx
+++ b/src/containers/UIProvider.tsx
@@ -20,6 +20,7 @@ export interface IReqoreOptions
| 'tooltips'
| 'customPortalId'
| 'errorBoundaryOptions'
+ | 'glowingIcons'
> {
withSidebar?: boolean;
uiScale?: number;
diff --git a/src/context/ReqoreContext.tsx b/src/context/ReqoreContext.tsx
index f64c486e..9cb06de1 100644
--- a/src/context/ReqoreContext.tsx
+++ b/src/context/ReqoreContext.tsx
@@ -38,6 +38,13 @@ export interface IReqoreContext {
* */
delay?: number;
};
+ /**
+ * When `true`, every `` rendered without an explicit `glow` prop
+ * receives a subtle drop-shadow glow using its resolved colour. Individual icons
+ * can opt out with `glow={false}`.
+ * @default false
+ */
+ readonly glowingIcons?: boolean;
readonly customPortalId?: string;
readonly closePopoversOnEscPress?: boolean;
readonly closeModalsOnEscPress?: boolean;
diff --git a/src/index.tsx b/src/index.tsx
index 45383c0c..3b996dd5 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -15,6 +15,7 @@ export * from './components/DatePicker';
export { ReqoreDrawer } from './components/Drawer';
export { ReqoreBackdrop } from './components/Drawer/backdrop';
export { default as ReqoreDropdown } from './components/Dropdown';
+export { default as ReqoreEntityRow } from './components/EntityRow';
export { ReqoreDropdownDivider, ReqoreDropdownItem } from './components/Dropdown/item';
export { ReqoreEffect, ReqoreTextEffect } from './components/Effect';
export { ReqoreEmptyState } from './components/EmptyState';
@@ -56,6 +57,7 @@ export { default as ReqoreProgress } from './components/Progress';
export { default as ReqoreRadioGroup } from './components/RadioGroup';
export { default as ReqoreRating } from './components/Rating';
export { default as ReqoreSegmentedControl } from './components/SegmentedControl';
+export { default as ReqoreSeverityRow } from './components/SeverityRow';
export { ReqoreRichTextEditor } from './components/RichTextEditor';
export { ReqoreSkeleton } from './components/Skeleton';
export { ReqoreSlider } from './components/Slider';
diff --git a/src/stories/Button/Button.stories.tsx b/src/stories/Button/Button.stories.tsx
index c2dfabe3..406b945b 100644
--- a/src/stories/Button/Button.stories.tsx
+++ b/src/stories/Button/Button.stories.tsx
@@ -485,3 +485,11 @@ export const Loading: Story = {
loading: true,
},
};
+
+export const Raised: Story = {
+ args: {
+ label: 'Raised button',
+ flat: true,
+ raised: true,
+ },
+};
diff --git a/src/stories/Callout/Callout.stories.tsx b/src/stories/Callout/Callout.stories.tsx
index 35e3ca99..d2716d88 100644
--- a/src/stories/Callout/Callout.stories.tsx
+++ b/src/stories/Callout/Callout.stories.tsx
@@ -146,3 +146,162 @@ export const ContentEffect: Story = {
},
},
};
+
+export const WithLabel: Story = {
+ render: Template,
+ args: {
+ label: 'Configuration required',
+ description:
+ 'Set up your AI provider before publishing this Qog. The build will succeed but runs will fail at execution time.',
+ intent: 'warning',
+ },
+};
+
+export const WithIcon: Story = {
+ render: Template,
+ args: {
+ label: 'Heads up',
+ description: 'Icons inherit the intent colour automatically.',
+ icon: 'InformationLine',
+ intent: 'info',
+ },
+};
+
+export const WithBadge: Story = {
+ render: Template,
+ args: {
+ label: 'New release available',
+ description: 'A new version of the platform is ready to install.',
+ icon: 'AlertLine',
+ intent: 'info',
+ badge: { label: 'v7.2', intent: 'info' },
+ },
+};
+
+export const WithMultipleBadges: Story = {
+ render: Template,
+ args: {
+ label: 'Pending approvals',
+ description: 'Three workflows are waiting for review across two business areas.',
+ icon: 'TimeLine',
+ intent: 'pending',
+ badge: [3, { label: 'high priority', intent: 'danger' }],
+ },
+};
+
+export const Closable: Story = {
+ render: Template,
+ args: {
+ label: 'Cookie notice',
+ description: 'We use cookies to improve your experience. Dismiss to acknowledge.',
+ icon: 'InformationLine',
+ intent: 'info',
+ onClose: () => alert('Closed'),
+ },
+};
+
+export const Bordered: Story = {
+ render: Template,
+ args: {
+ label: 'Bordered callout',
+ description: 'flat={false} renders an intent-coloured border in addition to the accent.',
+ icon: 'InformationLine',
+ intent: 'info',
+ flat: false,
+ },
+};
+
+export const Square: Story = {
+ render: Template,
+ args: {
+ label: 'Square corners',
+ description: 'rounded={false} removes the corner radius.',
+ rounded: false,
+ },
+};
+
+export const Transparent: Story = {
+ render: Template,
+ args: {
+ label: 'Transparent surface',
+ description: 'transparent={true} drops the surface colour, leaving the accent strip visible.',
+ icon: 'InformationLine',
+ intent: 'info',
+ transparent: true,
+ },
+};
+
+export const Clickable: Story = {
+ render: Template,
+ args: {
+ label: 'Whole-callout click',
+ description: 'Provide onClick (or interactive) to enable hover/lift behaviour.',
+ icon: 'CursorLine',
+ intent: 'info',
+ onClick: () => alert('Callout clicked'),
+ },
+};
+
+export const Disabled: Story = {
+ render: Template,
+ args: {
+ label: 'Disabled callout',
+ description: 'Disabled callouts dim and do not respond to hover or click.',
+ icon: 'CloseCircleLine',
+ disabled: true,
+ },
+};
+
+export const Tooltip: Story = {
+ render: Template,
+ args: {
+ children: 'Hover me to see a tooltip.',
+ tooltip: 'Callouts expose the same tooltip prop as every other Reqore component.',
+ },
+};
+
+export const Fixed: Story = {
+ render: (args) => ,
+ args: {
+ label: 'Fixed width callout',
+ description: 'fixed={true} prevents the callout from stretching.',
+ icon: 'InformationLine',
+ intent: 'info',
+ },
+};
+
+export const CustomTheme: Story = {
+ render: Template,
+ args: {
+ label: 'Branded callout',
+ description: 'customTheme overrides the surface colour while keeping the accent strip.',
+ icon: 'InformationLine',
+ customTheme: { main: '#1a1142' },
+ },
+};
+
+export const FullyComposed: Story = {
+ render: Template,
+ args: {
+ label: 'New deploy ready · staging',
+ description:
+ 'Promotion to production is gated on the staging smoke tests; pass them then click Promote.',
+ icon: 'RocketLine',
+ intent: 'success',
+ badge: { label: 'v7.2', intent: 'success' },
+ onClose: () => alert('Dismissed'),
+ accentSize: 4,
+ },
+};
+
+export const Raised: Story = {
+ render: Template,
+ args: {
+ label: 'Raised callout',
+ description: 'Pairs the accent strip with a subtle inset highlight for a tactile surface.',
+ icon: 'InformationLine',
+ intent: 'info',
+ flat: true,
+ raised: true,
+ },
+};
diff --git a/src/stories/EmptyState/EmptyState.stories.tsx b/src/stories/EmptyState/EmptyState.stories.tsx
index f957c6ee..192f4266 100644
--- a/src/stories/EmptyState/EmptyState.stories.tsx
+++ b/src/stories/EmptyState/EmptyState.stories.tsx
@@ -4,7 +4,7 @@ import { ReqoreButton, ReqoreControlGroup } from '../../index';
import { StoryMeta } from '../utils';
const meta = {
- title: 'Data Display/EmptyState/Stories',
+ title: 'Display/Empty State/Stories',
component: ReqoreEmptyState,
} as StoryMeta;
@@ -229,3 +229,13 @@ export const SearchNoResults: Story = {
/>
),
};
+
+export const Raised: Story = {
+ args: {
+ icon: 'InboxLine',
+ title: 'Raised empty state',
+ description:
+ 'Subtle inset highlight gives the placeholder surface a tactile, slightly elevated feel.',
+ raised: true,
+ },
+};
diff --git a/src/stories/EntityRow/EntityRow.stories.tsx b/src/stories/EntityRow/EntityRow.stories.tsx
new file mode 100644
index 00000000..c9dad2d7
--- /dev/null
+++ b/src/stories/EntityRow/EntityRow.stories.tsx
@@ -0,0 +1,226 @@
+import { StoryObj } from '@storybook/react';
+import ReqoreControlGroup from '../../components/ControlGroup';
+import ReqoreEntityRow from '../../components/EntityRow';
+import { StoryMeta } from '../utils';
+
+const meta = {
+ title: 'Display/Entity Row/Stories',
+ component: ReqoreEntityRow,
+} as StoryMeta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Basic: Story = {
+ args: {
+ label: 'Process Incoming Order',
+ description: 'Routes incoming Shopify orders into the warehouse pipeline',
+ metadata: 'Last run: success · just now · 384ms',
+ icon: 'PlayCircleLine',
+ actions: [{ label: 'Run', icon: 'PlayLine' }],
+ },
+};
+
+export const WithBadge: Story = {
+ args: {
+ label: 'Reconcile Payments',
+ description: 'Daily reconciliation between Stripe and the ledger',
+ metadata: 'Last run: failed · 1.5s',
+ icon: 'ErrorWarningLine',
+ intent: 'danger',
+ badge: { label: 'Failed', intent: 'danger' },
+ actions: [{ label: 'Investigate', intent: 'danger' }],
+ },
+};
+
+export const WithMultipleBadges: Story = {
+ args: {
+ label: 'Process Incoming Order',
+ description: 'Routes incoming Shopify orders into the warehouse pipeline',
+ metadata: 'Last run: success · just now',
+ icon: 'PlayCircleLine',
+ intent: 'success',
+ badge: [
+ { label: 'v2', minimal: true },
+ { label: 'on-demand', intent: 'info', minimal: true },
+ ],
+ actions: [{ label: 'Run', icon: 'PlayLine' }],
+ },
+};
+
+export const WithIntents: Story = {
+ render: (args) => (
+
+
+
+
+
+ ),
+};
+
+export const WithImage: Story = {
+ args: {
+ label: 'Stripe payment integration',
+ description: 'OAuth2 connected · Last sync 5 minutes ago',
+ iconImage: 'https://stripe.com/img/v3/home/social.png',
+ actions: [{ label: 'Manage' }],
+ },
+};
+
+export const WithoutIcon: Story = {
+ args: {
+ label: 'Plain row, no icon tile',
+ description: 'Useful for very compact lists where the entity speaks for itself',
+ metadata: 'just now',
+ actions: [{ icon: 'ArrowRightSLine', minimal: true, flat: true }],
+ },
+};
+
+export const Sizes: Story = {
+ render: () => (
+
+ {(['tiny', 'small', 'normal', 'big'] as const).map((size) => (
+
+ ))}
+
+ ),
+};
+
+export const Bordered: Story = {
+ args: {
+ label: 'Bordered entity row',
+ description: 'flat={false} renders an intent-coloured border',
+ metadata: 'just now',
+ icon: 'SettingsLine',
+ intent: 'info',
+ flat: false,
+ },
+};
+
+export const Square: Story = {
+ args: {
+ label: 'Square entity row',
+ description: 'rounded={false} removes the corner radius',
+ metadata: 'just now',
+ icon: 'SettingsLine',
+ intent: 'success',
+ rounded: false,
+ },
+};
+
+export const WithEffects: Story = {
+ args: {
+ label: 'Effects on label, description, metadata',
+ description: 'Description with custom italic effect',
+ metadata: 'Metadata with custom uppercase effect',
+ icon: 'SparklingLine',
+ intent: 'info',
+ labelEffect: { weight: 'bold', uppercase: true, spaced: 1 },
+ descriptionEffect: { italic: true, opacity: 0.8 },
+ metadataEffect: { uppercase: true, spaced: 2, opacity: 0.6 },
+ effect: {
+ gradient: {
+ colors: { 0: 'info:darken:5', 100: 'transparent' },
+ direction: 'to right',
+ },
+ },
+ },
+};
+
+export const Clickable: Story = {
+ args: {
+ label: 'Inventory Reorder Trigger',
+ description: 'Watches stock thresholds and fires reorder workflows',
+ metadata: 'Last run: success · 16 hours ago',
+ icon: 'StackLine',
+ onClick: () => alert('Row clicked'),
+ actions: [{ icon: 'ArrowRightSLine', minimal: true, flat: true }],
+ },
+};
+
+export const Disabled: Story = {
+ args: {
+ label: 'Archived integration',
+ description: 'This automation has been archived',
+ icon: 'ArchiveLine',
+ disabled: true,
+ actions: [{ label: 'Restore' }],
+ },
+};
+
+export const NoWrap: Story = {
+ args: {
+ label: 'Process Incoming Order',
+ description:
+ 'Routes incoming Shopify orders into the warehouse pipeline with full validation against the SKU registry',
+ metadata: 'Last run: success · just now · 384ms · attempt 1 of 3',
+ icon: 'PlayCircleLine',
+ intent: 'success',
+ wrap: false,
+ actions: [{ label: 'Run', icon: 'PlayLine' }],
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+};
+
+export const Transparent: Story = {
+ args: {
+ label: 'Transparent entity row',
+ description: 'Even with intent set, the background stays transparent',
+ icon: 'SettingsLine',
+ intent: 'info',
+ transparent: true,
+ actions: [{ label: 'Open' }],
+ },
+};
+
+export const Raised: Story = {
+ args: {
+ label: 'Raised entity row',
+ description: 'Inset top highlight + bottom shadow give the surface tactile depth.',
+ metadata: 'Last run: success · just now',
+ icon: 'PlayCircleLine',
+ intent: 'success',
+ raised: true,
+ actions: [{ label: 'Run', icon: 'PlayLine' }],
+ },
+};
diff --git a/src/stories/FeatureCard/FeatureCard.stories.tsx b/src/stories/FeatureCard/FeatureCard.stories.tsx
index b7c3ed85..b0a1c11e 100644
--- a/src/stories/FeatureCard/FeatureCard.stories.tsx
+++ b/src/stories/FeatureCard/FeatureCard.stories.tsx
@@ -1,6 +1,6 @@
import { StoryFn, StoryObj } from '@storybook/react';
-import { ReqoreFeatureCard, IReqoreFeatureCardProps } from '../../components/FeatureCard';
import ReqoreControlGroup from '../../components/ControlGroup';
+import { IReqoreFeatureCardProps, ReqoreFeatureCard } from '../../components/FeatureCard';
import { DEFAULT_INTENTS } from '../../constants/theme';
import { StoryMeta } from '../utils';
import { FlatArg, IntentArg, SizeArg, argManager } from '../utils/args';
@@ -8,7 +8,7 @@ import { FlatArg, IntentArg, SizeArg, argManager } from '../utils/args';
const { createArg } = argManager();
const meta = {
- title: 'Display/FeatureCard/Stories',
+ title: 'Display/Feature Card/Stories',
component: ReqoreFeatureCard,
parameters: {
chromatic: {
@@ -136,3 +136,144 @@ export const FrostedLabel: Story = {
},
},
};
+
+export const Bordered: Story = {
+ render: Template,
+ args: {
+ label: 'Bordered card',
+ description: 'flat={false} renders an intent-coloured border around the card.',
+ intent: 'info',
+ flat: false,
+ },
+};
+
+export const Square: Story = {
+ render: Template,
+ args: {
+ label: 'Square corners',
+ description: 'rounded={false} removes the corner radius.',
+ rounded: false,
+ },
+};
+
+export const Transparent: Story = {
+ render: Template,
+ args: {
+ label: 'Transparent surface',
+ description: 'transparent={true} drops the tinted background, leaving the border + marker.',
+ intent: 'warning',
+ transparent: true,
+ },
+};
+
+export const Clickable: Story = {
+ render: Template,
+ args: {
+ label: 'Clickable card',
+ description: 'Provide onClick (or interactive) to enable hover/lift behaviour.',
+ onClick: () => alert('Card clicked'),
+ },
+};
+
+export const Disabled: Story = {
+ render: Template,
+ args: {
+ label: 'Disabled card',
+ description: 'Disabled cards dim and do not respond to hover or click.',
+ disabled: true,
+ onClick: () => {
+ // no-op while disabled
+ },
+ },
+};
+
+export const Tooltip: Story = {
+ render: Template,
+ args: {
+ label: 'With tooltip',
+ description: 'Hover the card to see a contextual tooltip.',
+ tooltip: 'Cards expose the same tooltip prop as every other Reqore component.',
+ },
+};
+
+export const Fixed: Story = {
+ render: (args) => ,
+ args: {
+ label: 'Fixed width card',
+ description: 'fixed={true} prevents the card from stretching.',
+ },
+};
+
+export const WithBadge: Story = {
+ render: Template,
+ args: {
+ label: 'Just shipped',
+ description: 'Badges render to the right of the label using the standard TReqoreBadge type.',
+ badge: { label: 'New', intent: 'success' },
+ intent: 'success',
+ },
+};
+
+export const WithMultipleBadges: Story = {
+ render: Template,
+ args: {
+ label: 'Pricing tier',
+ description: 'Pass an array of badges to surface several pieces of metadata at once.',
+ badge: [
+ { label: 'v2', minimal: true },
+ { label: 'beta', intent: 'warning', minimal: true },
+ ],
+ },
+};
+
+export const NoWrap: Story = {
+ render: (args) => (
+
+
+
+ ),
+ args: {
+ label: 'Process incoming order with strict SKU validation',
+ description:
+ 'Routes Shopify orders into the warehouse pipeline and validates each line against the master catalog before fulfilment.',
+ wrap: false,
+ },
+};
+
+export const WithEffects: Story = {
+ render: Template,
+ args: {
+ label: 'Effects everywhere',
+ description: 'Background gradient, label uppercase, italic description.',
+ intent: 'info',
+ effect: {
+ gradient: {
+ colors: { 0: 'info:darken:5', 100: 'transparent' },
+ direction: 'to right',
+ },
+ },
+ labelEffect: { weight: 'bold', uppercase: true, spaced: 1 },
+ descriptionEffect: { italic: true },
+ },
+};
+
+export const CustomTheme: Story = {
+ render: Template,
+ args: {
+ label: 'Branded card',
+ description: 'customTheme overrides the surface colour while keeping the same primitives.',
+ customTheme: { main: '#1a1142' },
+ },
+};
+
+export const Raised: Story = {
+ render: Template,
+ args: {
+ label: 'Raised card',
+ description:
+ 'Inset top highlight + bottom shadow. Pairs with `flat={true}` to add depth without a hard border.',
+ intent: 'info',
+ flat: true,
+ raised: true,
+ },
+};
diff --git a/src/stories/Icon/Icon.stories.tsx b/src/stories/Icon/Icon.stories.tsx
index f65584fc..1ce7e43a 100644
--- a/src/stories/Icon/Icon.stories.tsx
+++ b/src/stories/Icon/Icon.stories.tsx
@@ -1,5 +1,5 @@
import { StoryObj } from '@storybook/react';
-import { ReqoreIcon, ReqorePanel } from '../../index';
+import { ReqoreControlGroup, ReqoreIcon, ReqorePanel, ReqoreUIProvider } from '../../index';
import { StoryMeta } from '../utils';
const meta = {
@@ -170,3 +170,48 @@ export const Basic: Story = {
);
},
};
+
+export const Glow: Story = {
+ render: () => (
+
+
+
+
+
+
+
+
+
+
+ ),
+};
+
+export const GlobalGlowingIcons: Story = {
+ render: () => (
+
+
+
+
+
+
+
+ {/* Opt-out: pass glow={false} explicitly */}
+
+
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'Setting `glowingIcons: true` on the UI provider applies the glow to every ReqoreIcon by default. Individual icons can opt out with `glow={false}`.',
+ },
+ },
+ },
+};
diff --git a/src/stories/Message/Message.stories.tsx b/src/stories/Message/Message.stories.tsx
index 2c376a6a..ee29c636 100644
--- a/src/stories/Message/Message.stories.tsx
+++ b/src/stories/Message/Message.stories.tsx
@@ -183,3 +183,13 @@ export const WithBackgroundBlur: Story = {
opaque: false,
},
};
+
+export const Raised: Story = {
+ args: {
+ title: 'Raised message',
+ children: 'Subtle inset highlight on top + inset shadow on bottom for a tactile surface.',
+ intent: 'info',
+ flat: true,
+ raised: true,
+ },
+};
diff --git a/src/stories/Panel/Panel.stories.tsx b/src/stories/Panel/Panel.stories.tsx
index 157771e4..1315eed7 100644
--- a/src/stories/Panel/Panel.stories.tsx
+++ b/src/stories/Panel/Panel.stories.tsx
@@ -951,3 +951,13 @@ export const StickyHeaderOutsideScroll: Story = {
await new Promise((resolve) => setTimeout(resolve, 200));
},
};
+
+export const Raised: Story = {
+ args: {
+ label: 'Raised panel',
+ children: 'Subtle inset highlight on top + inset shadow on bottom — best paired with `flat`.',
+ flat: true,
+ raised: true,
+ padded: true,
+ },
+};
diff --git a/src/stories/Progress/Progress.stories.tsx b/src/stories/Progress/Progress.stories.tsx
index d1a63604..f07b4444 100644
--- a/src/stories/Progress/Progress.stories.tsx
+++ b/src/stories/Progress/Progress.stories.tsx
@@ -291,6 +291,54 @@ export const Disabled: Story = {
),
};
+export const WithTargetMarker: Story = {
+ render: (args) => (
+
+
+
+
+
+
+
+ ),
+};
+
export const NotRounded: Story = {
render: (args) => (
diff --git a/src/stories/SeverityRow/SeverityRow.stories.tsx b/src/stories/SeverityRow/SeverityRow.stories.tsx
new file mode 100644
index 00000000..47edc341
--- /dev/null
+++ b/src/stories/SeverityRow/SeverityRow.stories.tsx
@@ -0,0 +1,213 @@
+import { StoryObj } from '@storybook/react';
+import ReqoreControlGroup from '../../components/ControlGroup';
+import ReqoreSeverityRow from '../../components/SeverityRow';
+import ReqoreTag from '../../components/Tag';
+import { StoryMeta } from '../utils';
+
+const meta = {
+ title: 'Display/Severity Row/Stories',
+ component: ReqoreSeverityRow,
+} as StoryMeta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Basic: Story = {
+ args: {
+ label: 'Payment Processing · stripe-webhook-receiver',
+ description: 'Avg duration 4.7s exceeded 3.5s threshold · just now',
+ intent: 'danger',
+ leading: ,
+ actions: [
+ { label: 'Investigate', intent: 'danger' },
+ { icon: 'CloseLine', tooltip: 'Dismiss', minimal: true, flat: true },
+ ],
+ },
+};
+
+export const WithBadge: Story = {
+ args: {
+ label: 'Payment Processing',
+ description: 'Three open detector flags from the last summary cycle',
+ intent: 'danger',
+ badge: [3, { label: 'mad', intent: 'danger', minimal: true }],
+ actions: [{ label: 'Investigate', intent: 'danger' }],
+ },
+};
+
+export const Intents: Story = {
+ render: (args) => (
+
+ }
+ actions={[{ label: 'Investigate', intent: 'danger' }]}
+ />
+ }
+ actions={[{ label: 'Investigate', intent: 'warning' }]}
+ />
+ }
+ />
+ }
+ />
+
+ ),
+};
+
+export const Sizes: Story = {
+ render: () => (
+
+
+
+
+
+
+ ),
+};
+
+export const Bordered: Story = {
+ args: {
+ label: 'Bordered row',
+ description: 'flat={false} renders an intent-coloured border',
+ intent: 'warning',
+ flat: false,
+ leading: ,
+ },
+};
+
+export const Square: Story = {
+ args: {
+ label: 'Square row',
+ description: 'rounded={false} removes the corner radius',
+ intent: 'info',
+ rounded: false,
+ leading: ,
+ },
+};
+
+export const WithEffects: Story = {
+ args: {
+ label: 'Effects on label and description',
+ description: 'Description with custom effect',
+ intent: 'info',
+ labelEffect: { weight: 'bold', uppercase: true, spaced: 1 },
+ descriptionEffect: { italic: true, opacity: 0.8 },
+ effect: {
+ gradient: {
+ colors: { 0: 'info:darken:5', 100: 'transparent' },
+ direction: 'to right',
+ },
+ },
+ leading: ,
+ },
+};
+
+export const Clickable: Story = {
+ args: {
+ label: 'Customer Support · zendesk-ticket-sync',
+ description: 'Avg duration scored 3.2 (threshold 3.0)',
+ intent: 'warning',
+ onClick: () => alert('Row clicked'),
+ leading: ,
+ },
+};
+
+export const NoStrip: Story = {
+ args: {
+ label: 'Customer Onboarding · onboard-new-customer',
+ description: 'No issues — operating within baseline',
+ intent: 'success',
+ showStrip: false,
+ leading: ,
+ },
+};
+
+export const Transparent: Story = {
+ args: {
+ label: 'Inventory Management · stock-sync',
+ description: 'Operating normally — strip only, no tinted background',
+ intent: 'info',
+ transparent: true,
+ leading: ,
+ },
+};
+
+export const NoWrap: Story = {
+ args: {
+ label: 'Payment Processing · stripe-webhook-receiver-fallback',
+ description:
+ 'Avg duration 4.7s exceeded 3.5s threshold over the 14-period rolling baseline measured by the mad detector · just now',
+ intent: 'danger',
+ wrap: false,
+ leading: ,
+ actions: [{ label: 'Investigate', intent: 'danger' }],
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+};
+
+export const Disabled: Story = {
+ args: {
+ label: 'Disabled row',
+ description: 'No actions allowed',
+ intent: 'warning',
+ disabled: true,
+ leading: ,
+ actions: [{ label: 'Investigate' }],
+ },
+};
+
+export const Raised: Story = {
+ args: {
+ label: 'Raised severity row',
+ description:
+ 'Subtle inset highlight on top + inset shadow on bottom — best paired with `flat={true}` (default).',
+ intent: 'danger',
+ raised: true,
+ leading: ,
+ actions: [{ label: 'Investigate', intent: 'danger' }],
+ },
+};
diff --git a/src/stories/Statistic/Statistic.stories.tsx b/src/stories/Statistic/Statistic.stories.tsx
index ad0c4b16..f0649291 100644
--- a/src/stories/Statistic/Statistic.stories.tsx
+++ b/src/stories/Statistic/Statistic.stories.tsx
@@ -5,7 +5,7 @@ import { ReqoreControlGroup } from '../../index';
import { StoryMeta } from '../utils';
const meta = {
- title: 'Data Display/Statistic/Stories',
+ title: 'Display/Statistic/Stories',
component: ReqoreStatistic,
} as StoryMeta;
@@ -438,3 +438,27 @@ export const StackedStatistics: Story = {
),
};
+
+export const Raised: Story = {
+ render: () => (
+
+
+
+
+
+ ),
+};
diff --git a/src/styles.ts b/src/styles.ts
index a12faefc..9868fac9 100644
--- a/src/styles.ts
+++ b/src/styles.ts
@@ -121,3 +121,18 @@ export const DisabledElement = css`
export const ReadOnlyElement = css`
cursor: not-allowed;
`;
+
+/**
+ * Subtle "raised" effect — adds a 1px inset highlight on the top edge and a
+ * 1px inset shadow on the bottom edge so a borderless surface reads as a
+ * tactile, slightly elevated card. Designed to be paired with `flat={true}`
+ * (no border); pass `raised={true}` on supporting components.
+ *
+ * The colours are theme-neutral additive overlays (white-on-dark, black-on-
+ * light) so the same recipe lights up correctly across every Reqore theme.
+ */
+export const RaisedElement = css`
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, 0.06),
+ inset 0 -1px 0 rgba(0, 0, 0, 0.22);
+`;