TRIVIAL: Add xray-core-subnet-mimicry article (#4)

This commit is contained in:
deytenit authored and GitHub committed 2026-03-08 02:29:09 +03:00
1 parent 49b545f1b4
commit ea35a3d9f5
21 files changed
+785 -79

No files matched your search

@@ -54,7 +54,7 @@ export const Article = memo(function Article({
className,
}: ArticleProps) {
return (
<Container className={className} href={href} padding={4} onPress={onPress}>
<Container border className={className} href={href} padding={4} rounded="md" onPress={onPress}>
<VStack align="stretch" gap={4}>
<VStack align="stretch" gap={2}>
<Header level={4}>{headline}</Header>
@@ -33,6 +33,18 @@ export const Default: Story = {
),
};
export const Interactive: Story = {
render: () => (
<Breadcrumbs>
<Breadcrumbs.Item href="/">Home</Breadcrumbs.Item>
<Breadcrumbs.Separator variant="arrow" />
<Breadcrumbs.Item onClick={() => alert('Clicked!')}>Click Me</Breadcrumbs.Item>
<Breadcrumbs.Separator variant="arrow" />
<Breadcrumbs.Item isCurrent>Current Page</Breadcrumbs.Item>
</Breadcrumbs>
),
};
export const WithArrows: Story = {
render: () => (
<Breadcrumbs>
@@ -1,7 +1,7 @@
import React from 'react';
import { Breadcrumbs } from './Breadcrumbs';
import { render, screen } from '../../lib/testing';
import { render, screen, fireEvent } from '../../lib/testing';
import { Link } from '../Link/Link';
describe('components/Breadcrumbs', () => {
@@ -12,14 +12,58 @@ describe('components/Breadcrumbs', () => {
<Link href="/">Home</Link>
</Breadcrumbs.Item>
<Breadcrumbs.Separator variant="slash" />
<Breadcrumbs.Item>Current</Breadcrumbs.Item>
<Breadcrumbs.Item isCurrent>Current</Breadcrumbs.Item>
</Breadcrumbs>
);
expect(screen.getByRole('navigation')).toHaveAttribute('aria-label', 'Breadcrumb');
expect(screen.getByRole('link', { name: 'Home' })).toBeInTheDocument();
expect(screen.getByText('/')).toBeInTheDocument();
expect(screen.getByText('Current')).toBeInTheDocument();
const currentItem = screen.getByText('Current').parentElement;
expect(currentItem).toHaveAttribute('aria-current', 'page');
});
it('supports isCurrent state', () => {
render(
<Breadcrumbs>
<Breadcrumbs.Item isCurrent>Current Page</Breadcrumbs.Item>
</Breadcrumbs>
);
const item = screen.getByText('Current Page').parentElement;
expect(item).toHaveAttribute('aria-current', 'page');
});
it('supports href directly on Item', () => {
render(
<Breadcrumbs>
<Breadcrumbs.Item href="/test-url">Test Link</Breadcrumbs.Item>
</Breadcrumbs>
);
const link = screen.getByRole('link', { name: 'Test Link' });
expect(link).toHaveAttribute('href', '/test-url');
});
it('supports onClick on Item', () => {
const handleClick = vi.fn();
render(
<Breadcrumbs>
<Breadcrumbs.Item onClick={handleClick}>Clickable</Breadcrumbs.Item>
</Breadcrumbs>
);
const button = screen.getByRole('button', { name: 'Clickable' });
fireEvent.click(button);
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('renders as a non-submit button when onClick is provided', () => {
render(
<Breadcrumbs>
<Breadcrumbs.Item onClick={vi.fn()}>Clickable</Breadcrumbs.Item>
</Breadcrumbs>
);
const button = screen.getByRole('button', { name: 'Clickable' });
expect(button).toHaveAttribute('type', 'button');
});
it('supports different separator variants', () => {
@@ -29,20 +29,57 @@ const BreadcrumbsRoot = memo(function Breadcrumbs({ children, ...props }: Breadc
);
});
/**
* Props for the individual Breadcrumbs item.
*/
export interface BreadcrumbsItemProps extends TestIdProps {
/** Content to be rendered inside the item */
children: React.ReactNode;
/** Optional className for the list item */
className?: string;
/** Whether this is the current page */
isCurrent?: boolean;
/** Optional href for the item (renders as a link) */
href?: string;
/** Optional click handler for the item */
onClick?: React.MouseEventHandler<HTMLElement>;
}
/**
* Individual step in the breadcrumb trail.
*/
const BreadcrumbsItem = memo(function BreadcrumbsItem({
children,
className,
isCurrent,
href,
onClick,
'data-testid': testId,
}: {
children: React.ReactNode;
className?: string;
} & TestIdProps) {
}: BreadcrumbsItemProps) {
const isClickable = !!(href ?? onClick);
return (
<li className={className} data-testid={testId}>
<Text color="muted" size="m" type="label">
<li
aria-current={isCurrent ? 'page' : undefined}
className={cn(
'group flex items-center transition-all duration-200',
!isCurrent && isClickable && 'cursor-pointer',
className
)}
data-testid={testId}
>
<Text
as={href ? 'a' : onClick ? 'button' : 'span'}
className={cn(
'transition-all duration-200',
!isCurrent && 'group-hover:text-[var(--rb-text)] group-hover:underline underline-offset-4'
)}
color={isCurrent ? 'default' : 'muted'}
href={href}
size="m"
type="label"
onClick={onClick}
>
{children}
</Text>
</li>
@@ -1,9 +1,23 @@
import React from 'react';
import { vi } from 'vitest';
import { Link } from './Link';
import { render, screen } from '../../lib/testing';
describe('components/Link', () => {
beforeEach(() => {
// Mock IntersectionObserver to avoid errors in Text component
class MockIntersectionObserver {
observe = vi.fn();
disconnect = vi.fn();
}
vi.stubGlobal('IntersectionObserver', MockIntersectionObserver);
});
afterEach(() => {
vi.restoreAllMocks();
});
it('renders correctly as a link when href is provided', () => {
render(<Link href="https://example.com">Click me</Link>);
const link = screen.getByRole('link', { name: 'Click me' });
@@ -44,4 +58,14 @@ describe('components/Link', () => {
render(<Link onPress={() => {}}>Click me</Link>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('passes delay prop to the underlying Text component', () => {
render(
<Link delay={500} href="/test">
Delayed Link
</Link>
);
const link = screen.getByRole('link');
expect(link).toHaveClass('opacity-0');
});
});
+5 -1
View File
@@ -24,6 +24,8 @@ export interface LinkProps extends AriaLinkOptions, ClassNameProps {
size?: TextSize;
/** Text color variant */
color?: TextColor;
/** Delay in milliseconds before the link appears after entering the viewport. */
delay?: number;
}
/**
@@ -33,7 +35,7 @@ export interface LinkProps extends AriaLinkOptions, ClassNameProps {
* It uses standard design system colors (--rb-ring) and provides responsive hover/focus states.
*/
export const Link = memo(function Link(props: LinkProps) {
const { children, className, href, isExternal, type, size, color, ...otherProps } = props;
const { children, className, href, isExternal, type, size, color, delay, ...otherProps } = props;
// We use a specific ref type that useLink expects.
// Since Link can be an 'a' or a 'span', we use HTMLElement as common denominator.
@@ -59,6 +61,7 @@ export const Link = memo(function Link(props: LinkProps) {
as="a"
className={sharedClasses}
color={color}
delay={delay}
href={href}
size={size}
type={type}
@@ -75,6 +78,7 @@ export const Link = memo(function Link(props: LinkProps) {
as="span"
className={sharedClasses}
color={color}
delay={delay}
size={size}
type={type}
>
@@ -29,7 +29,7 @@ describe('Markdown Components', () => {
);
expect(screen.getByText('Heading 1')).toBeInTheDocument();
expect(screen.getByText('This is a paragraph.')).toBeInTheDocument();
expect(screen.getByText('This is a paragraph.')).toHaveClass('mb-4');
expect(screen.getByRole('link')).toHaveAttribute('href', 'https://example.com');
expect(screen.getByRole('list')).toBeInTheDocument();
expect(screen.getByRole('table')).toBeInTheDocument();
@@ -1,12 +1,17 @@
import React, { memo } from 'react';
import { List } from '../../List/List';
import { MarkdownP } from '../MarkdownP/MarkdownP';
import type { ListItemProps } from '../../List/List';
/**
* A Markdown list item component.
*/
export const MarkdownLI = memo(function MarkdownLI(props: ListItemProps) {
return <List.Item {...props} />;
export const MarkdownLI = memo(function MarkdownLI({ children, ...props }: ListItemProps) {
return (
<List.Item {...props}>
<MarkdownP>{children}</MarkdownP>
</List.Item>
);
});
+91 -1
View File
@@ -1,14 +1,90 @@
import React from 'react';
import { vi } from 'vitest';
import { Text } from './Text';
import { render, screen } from '../../lib/testing';
import { render, screen, act } from '../../lib/testing';
describe('components/Text', () => {
let intersectionCallback: (entries: any[]) => void;
beforeEach(() => {
vi.useFakeTimers();
// Mock IntersectionObserver
class MockIntersectionObserver {
constructor(public callback: (entries: any[]) => void) {
intersectionCallback = callback;
}
observe = vi.fn();
disconnect = vi.fn();
}
vi.stubGlobal('IntersectionObserver', MockIntersectionObserver);
});
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});
it('renders children correctly', () => {
render(<Text>Test content</Text>);
expect(screen.getByText('Test content')).toBeInTheDocument();
});
it('is immediately visible when no delay is provided', () => {
render(<Text>No delay</Text>);
const el = screen.getByText('No delay');
expect(el).not.toHaveClass('opacity-0');
expect(el).not.toHaveClass('animate-in');
});
it('is initially hidden when delay is provided', () => {
render(<Text delay={500}>With delay</Text>);
const el = screen.getByText('With delay');
expect(el).toHaveClass('opacity-0');
});
it('becomes visible after intersection and specified delay', () => {
render(<Text delay={500}>Delayed visible</Text>);
const el = screen.getByText('Delayed visible');
expect(el).toHaveClass('opacity-0');
// Simulate intersection
act(() => {
intersectionCallback([{ isIntersecting: true }]);
});
// Should still be hidden before delay
act(() => {
vi.advanceTimersByTime(250);
});
expect(el).toHaveClass('opacity-0');
// Should be visible after delay
act(() => {
vi.advanceTimersByTime(250);
});
expect(el).not.toHaveClass('opacity-0');
expect(el).toHaveClass('animate-in');
expect(el).toHaveClass('fade-in');
});
it('becomes visible immediately after intersection if delay is 0', () => {
render(<Text delay={0}>Zero delay</Text>);
const el = screen.getByText('Zero delay');
expect(el).toHaveClass('opacity-0');
// Simulate intersection
act(() => {
intersectionCallback([{ isIntersecting: true }]);
});
expect(el).not.toHaveClass('opacity-0');
expect(el).toHaveClass('animate-in');
});
it('applies alignment classes', () => {
render(<Text align="center">Centered</Text>);
expect(screen.getByText('Centered')).toHaveClass('text-center');
@@ -39,4 +115,18 @@ describe('components/Text', () => {
expect(el).toHaveClass('line-clamp-2');
expect(el).not.toHaveClass('truncate');
});
it('applies type="button" by default when as="button"', () => {
render(<Text as="button">Button Text</Text>);
expect(screen.getByRole('button')).toHaveAttribute('type', 'button');
});
it('allows overriding type attribute with htmlType', () => {
render(
<Text as="button" htmlType="submit">
Submit Text
</Text>
);
expect(screen.getByRole('button')).toHaveAttribute('type', 'submit');
});
});
+44 -3
View File
@@ -1,5 +1,5 @@
import { cn, castRef, genericMemo } from '@ermnvldmr/stl';
import React, { forwardRef } from 'react';
import { cn, genericMemo, useIntersectionObserver, castMutableRef } from '@ermnvldmr/stl';
import React, { forwardRef, useState, useEffect, useRef } from 'react';
import type { ClassNameProps, TestIdProps } from '@ermnvldmr/stl';
@@ -60,6 +60,10 @@ export interface TextProps
overflow?: 'ellipsis' | 'clip';
/** Maximum number of lines to show (truncates with ellipsis) */
maxLines?: number;
/** Delay in milliseconds before the text appears after entering the viewport. */
delay?: number;
/** Underlying element's HTML type attribute (e.g. "button", "submit") */
htmlType?: string;
}
/**
@@ -82,13 +86,45 @@ const TextComponent = forwardRef<HTMLElement, TextProps>(function Text(
wrap,
overflow,
maxLines,
delay,
as: Component = 'span',
htmlType,
className,
'data-testid': testId,
...props
},
ref
) {
const [isVisible, setIsVisible] = useState(delay === undefined);
const internalRef = useRef<HTMLElement>(null);
// Sync internalRef with forwarded ref
useEffect(() => {
if (!ref) return;
if (typeof ref === 'function') {
ref(internalRef.current);
} else {
castMutableRef<HTMLElement>(ref).current = internalRef.current;
}
}, [ref]);
const isIntersecting = useIntersectionObserver(internalRef, {
threshold: 0.1,
once: true,
});
useEffect(() => {
if (delay !== undefined && isIntersecting) {
if (delay > 0) {
const timer = setTimeout(() => setIsVisible(true), delay);
return () => clearTimeout(timer);
} else {
setIsVisible(true);
}
}
return undefined;
}, [delay, isIntersecting]);
// Base typography styles mapping
const typeStyles: Record<TextType, Record<TextSize, string>> = {
display: {
@@ -152,7 +188,7 @@ const TextComponent = forwardRef<HTMLElement, TextProps>(function Text(
return (
<Component
{...props}
ref={castRef<HTMLElement>(ref)}
ref={internalRef}
className={cn(
typeStyles[type][size],
colorClasses[color],
@@ -163,9 +199,14 @@ const TextComponent = forwardRef<HTMLElement, TextProps>(function Text(
align && alignClasses[align],
wrap && wrapClasses[wrap],
truncationClass,
delay !== undefined &&
(isVisible
? 'animate-in fade-in slide-in-from-bottom-2 duration-1000 ease-out'
: 'opacity-0'),
className
)}
data-testid={testId}
type={htmlType ?? (Component === 'button' ? 'button' : undefined)}
>
{children}
</Component>