mirror of
https://github.com/n8n-io/n8n.git
synced 2026-07-28 03:24:59 +02:00
feat(editor): Add N8nStatusDot component (#34963)
Some checks are pending
CI: Master (Build, Test, Lint) / Build for Github Cache (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (22.22.3) (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (24.18.0) (push) Waiting to run
CI: Master (Build, Test, Lint) / Lint (push) Waiting to run
CI: Master (Build, Test, Lint) / Performance (push) Waiting to run
CI: Master (Build, Test, Lint) / Notify Slack on failure (push) Blocked by required conditions
Some checks are pending
CI: Master (Build, Test, Lint) / Build for Github Cache (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (22.22.3) (push) Waiting to run
CI: Master (Build, Test, Lint) / Unit tests (24.18.0) (push) Waiting to run
CI: Master (Build, Test, Lint) / Lint (push) Waiting to run
CI: Master (Build, Test, Lint) / Performance (push) Waiting to run
CI: Master (Build, Test, Lint) / Notify Slack on failure (push) Blocked by required conditions
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
8171aa8261
commit
f758a4bd05
|
|
@ -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<typeof meta>;
|
||||
|
||||
export const Default = {
|
||||
render: (args) => ({
|
||||
components: { N8nStatusDot },
|
||||
setup: () => ({ args }),
|
||||
template: '<N8nStatusDot v-bind="args" />',
|
||||
}),
|
||||
args: {
|
||||
variant: 'success',
|
||||
pulse: false,
|
||||
},
|
||||
} satisfies Story;
|
||||
|
||||
export const Variants = {
|
||||
render: () => ({
|
||||
components: { N8nStatusDot },
|
||||
template: `
|
||||
<div style="display: flex; flex-direction: column; gap: 16px; padding: 40px;">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<N8nStatusDot variant="success" /> Success
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<N8nStatusDot variant="warning" /> Warning
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<N8nStatusDot variant="danger" /> Danger
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
} satisfies Story;
|
||||
|
||||
export const Pulsing = {
|
||||
render: () => ({
|
||||
components: { N8nStatusDot },
|
||||
template: `
|
||||
<div style="display: flex; flex-direction: column; gap: 16px; padding: 40px;">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<N8nStatusDot variant="success" pulse /> Enabled
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<N8nStatusDot variant="danger" /> Disabled
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
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;
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export type StatusDotVariant = 'success' | 'warning' | 'danger';
|
||||
|
||||
export interface StatusDotProps {
|
||||
variant?: StatusDotVariant;
|
||||
pulse?: boolean;
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
<script lang="ts" setup>
|
||||
import type { StatusDotProps } from './StatusDot.types';
|
||||
|
||||
defineOptions({ name: 'N8nStatusDot' });
|
||||
|
||||
withDefaults(defineProps<StatusDotProps>(), {
|
||||
variant: 'success',
|
||||
pulse: false,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span :class="[$style[variant], { [$style.pulse]: pulse }]" aria-hidden="true" />
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
// A single per-variant custom property drives both the fill and the pulse ring,
|
||||
// so the animation automatically picks up the variant color.
|
||||
.dot {
|
||||
--status-dot--color: var(--color--success);
|
||||
display: inline-block;
|
||||
flex: 0 0 auto;
|
||||
width: var(--spacing--2xs);
|
||||
height: var(--spacing--2xs);
|
||||
border-radius: var(--radius--full);
|
||||
background-color: var(--status-dot--color);
|
||||
}
|
||||
|
||||
.success {
|
||||
composes: dot;
|
||||
}
|
||||
|
||||
.warning {
|
||||
composes: dot;
|
||||
--status-dot--color: var(--color--warning);
|
||||
}
|
||||
|
||||
.danger {
|
||||
composes: dot;
|
||||
--status-dot--color: var(--color--danger);
|
||||
}
|
||||
|
||||
// Expanding and fading box-shadow ring, tinted with the dot's own color.
|
||||
.pulse {
|
||||
animation: statusDotPulse var(--duration--slowest) var(--easing--ease-out) infinite;
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes statusDotPulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 color-mix(in srgb, var(--status-dot--color) 55%, transparent);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 var(--spacing--3xs) transparent;
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 transparent;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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`] = `"<span class="success pulse" aria-hidden="true"></span>"`;
|
||||
|
||||
exports[`components/N8nStatusDot > should render success variant without pulse by default 1`] = `"<span class="success" aria-hidden="true"></span>"`;
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import N8nStatusDot from './StatusDot.vue';
|
||||
|
||||
export default N8nStatusDot;
|
||||
|
||||
export type * from './StatusDot.types';
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user