diff --git a/packages/frontend/@n8n/design-system/src/components/N8nStatusDot/StatusDot.stories.ts b/packages/frontend/@n8n/design-system/src/components/N8nStatusDot/StatusDot.stories.ts new file mode 100644 index 00000000000..82ea6211ae9 --- /dev/null +++ b/packages/frontend/@n8n/design-system/src/components/N8nStatusDot/StatusDot.stories.ts @@ -0,0 +1,83 @@ +import type { StoryObj } from '@storybook/vue3-vite'; + +import N8nStatusDot from './StatusDot.vue'; + +const meta = { + title: 'Core/StatusDot', + component: N8nStatusDot, + argTypes: { + variant: { + control: 'select', + options: ['success', 'warning', 'danger'], + }, + pulse: { + control: 'boolean', + }, + }, + parameters: { + docs: { + description: { + component: + 'A small decorative status indicator dot. Pair it with text that carries the meaning; the dot itself is hidden from assistive technology.', + }, + }, + }, +}; +export default meta; + +type Story = StoryObj; + +export const Default = { + render: (args) => ({ + components: { N8nStatusDot }, + setup: () => ({ args }), + template: '', + }), + args: { + variant: 'success', + pulse: false, + }, +} satisfies Story; + +export const Variants = { + render: () => ({ + components: { N8nStatusDot }, + template: ` +
+
+ Success +
+
+ Warning +
+
+ Danger +
+
+ `, + }), +} satisfies Story; + +export const Pulsing = { + render: () => ({ + components: { N8nStatusDot }, + template: ` +
+
+ Enabled +
+
+ Disabled +
+
+ `, + }), + parameters: { + docs: { + description: { + story: + 'The pulse draws attention to a live or active state. The animation is disabled when the user prefers reduced motion.', + }, + }, + }, +} satisfies Story; diff --git a/packages/frontend/@n8n/design-system/src/components/N8nStatusDot/StatusDot.test.ts b/packages/frontend/@n8n/design-system/src/components/N8nStatusDot/StatusDot.test.ts new file mode 100644 index 00000000000..1fbf19ad2c1 --- /dev/null +++ b/packages/frontend/@n8n/design-system/src/components/N8nStatusDot/StatusDot.test.ts @@ -0,0 +1,36 @@ +import { render } from '@testing-library/vue'; + +import N8nStatusDot from './StatusDot.vue'; + +describe('components/N8nStatusDot', () => { + it('should render success variant without pulse by default', () => { + const wrapper = render(N8nStatusDot); + const dot = wrapper.container.firstElementChild; + expect(dot?.className).toContain('success'); + expect(dot?.className).not.toContain('pulse'); + expect(wrapper.html()).toMatchSnapshot(); + }); + + it('should be hidden from assistive technology', () => { + const wrapper = render(N8nStatusDot); + expect(wrapper.container.firstElementChild).toHaveAttribute('aria-hidden', 'true'); + }); + + it.each(['success', 'warning', 'danger'] as const)( + 'should render %s variant class', + (variant) => { + const wrapper = render(N8nStatusDot, { + props: { variant }, + }); + expect(wrapper.container.firstElementChild?.className).toContain(variant); + }, + ); + + it('should render pulse class when pulse is enabled', () => { + const wrapper = render(N8nStatusDot, { + props: { pulse: true }, + }); + expect(wrapper.container.firstElementChild?.className).toContain('pulse'); + expect(wrapper.html()).toMatchSnapshot(); + }); +}); diff --git a/packages/frontend/@n8n/design-system/src/components/N8nStatusDot/StatusDot.types.ts b/packages/frontend/@n8n/design-system/src/components/N8nStatusDot/StatusDot.types.ts new file mode 100644 index 00000000000..fb860017573 --- /dev/null +++ b/packages/frontend/@n8n/design-system/src/components/N8nStatusDot/StatusDot.types.ts @@ -0,0 +1,6 @@ +export type StatusDotVariant = 'success' | 'warning' | 'danger'; + +export interface StatusDotProps { + variant?: StatusDotVariant; + pulse?: boolean; +} diff --git a/packages/frontend/@n8n/design-system/src/components/N8nStatusDot/StatusDot.vue b/packages/frontend/@n8n/design-system/src/components/N8nStatusDot/StatusDot.vue new file mode 100644 index 00000000000..e5c5befe681 --- /dev/null +++ b/packages/frontend/@n8n/design-system/src/components/N8nStatusDot/StatusDot.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/packages/frontend/@n8n/design-system/src/components/N8nStatusDot/__snapshots__/StatusDot.test.ts.snap b/packages/frontend/@n8n/design-system/src/components/N8nStatusDot/__snapshots__/StatusDot.test.ts.snap new file mode 100644 index 00000000000..39a9e3cb5f8 --- /dev/null +++ b/packages/frontend/@n8n/design-system/src/components/N8nStatusDot/__snapshots__/StatusDot.test.ts.snap @@ -0,0 +1,5 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`components/N8nStatusDot > should render pulse class when pulse is enabled 1`] = `""`; + +exports[`components/N8nStatusDot > should render success variant without pulse by default 1`] = `""`; diff --git a/packages/frontend/@n8n/design-system/src/components/N8nStatusDot/index.ts b/packages/frontend/@n8n/design-system/src/components/N8nStatusDot/index.ts new file mode 100644 index 00000000000..6f5933e4683 --- /dev/null +++ b/packages/frontend/@n8n/design-system/src/components/N8nStatusDot/index.ts @@ -0,0 +1,5 @@ +import N8nStatusDot from './StatusDot.vue'; + +export default N8nStatusDot; + +export type * from './StatusDot.types'; diff --git a/packages/frontend/@n8n/design-system/src/components/index.ts b/packages/frontend/@n8n/design-system/src/components/index.ts index 018015ad2c0..75f6b99f8e2 100644 --- a/packages/frontend/@n8n/design-system/src/components/index.ts +++ b/packages/frontend/@n8n/design-system/src/components/index.ts @@ -109,6 +109,7 @@ export { default as N8nRecycleScroller } from './N8nRecycleScroller'; export { default as N8nResizeWrapper } from './N8nResizeWrapper'; export { default as N8nSelect } from './N8nSelect'; export { default as N8nSpinner } from './N8nSpinner'; +export { default as N8nStatusDot } from './N8nStatusDot'; export { default as N8nSticky } from './N8nSticky'; export { default as N8nResizeableSticky } from './N8nResizeableSticky'; export { default as N8nSuggestedActions } from './N8nSuggestedActions';