Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
3390cc8
fix: give h1-h4 their own paired line-height token
ravisuhag Jul 12, 2026
6adbf18
fix: stop sidebar width transition from clobbering interactive states
ravisuhag Jul 12, 2026
562bcfc
fix: compose motion tokens into --rs-transition-interactive, add pres…
ravisuhag Jul 12, 2026
5eb7050
fix: re-step type ramp tracking and leading to be monotonic
ravisuhag Jul 12, 2026
a7b2dda
fix: add distinct pressed (:active) feedback across interactive compo…
ravisuhag Jul 12, 2026
77d00c0
fix: add missing :focus-visible indicators across the library
ravisuhag Jul 12, 2026
b3ab3bc
fix: expand sub-24px hit targets with transparent pseudo-elements
ravisuhag Jul 12, 2026
e6a4265
fix: stop CopyButton timer leak, cross-fade icon swap, announce state
ravisuhag Jul 12, 2026
5cde232
fix: give data-table its missing sort-icon class, rotate icon on sort
ravisuhag Jul 12, 2026
e485859
fix: invalid toolbar background value, misnamed text decoration class
ravisuhag Jul 12, 2026
639dd88
fix: animate indeterminate Progress, move fills to transform: scaleX
ravisuhag Jul 12, 2026
b1f8963
fix: give number-field input real hover/focus/invalid states
ravisuhag Jul 12, 2026
fba0233
fix: drawer tracks swipes 1:1, tokenize exit duration, fade under red…
ravisuhag Jul 12, 2026
77009c8
fix: reduced-motion and reduced-transparency fallbacks
ravisuhag Jul 12, 2026
1af66f0
fix: rewrite tooltip motion as interruptible, symmetric transitions
ravisuhag Jul 12, 2026
202bf82
fix: animate callout enter and dismiss (fade + height collapse)
ravisuhag Jul 12, 2026
868ab8a
fix: let sidebar collapse fade labels instead of popping them
ravisuhag Jul 12, 2026
908e365
fix: color-picker area thumb uses transform, blocks touch scroll, fad…
ravisuhag Jul 12, 2026
5de9c38
fix: animate Field error in, keep description, surface DatePicker error
ravisuhag Jul 12, 2026
6d75d7d
fix: render clickable chips as real buttons
ravisuhag Jul 12, 2026
0fa9850
fix: select popup background, nested multi-select checkbox, chevron r…
ravisuhag Jul 12, 2026
9fd301e
fix: distinguish toggle hover from pressed, animate content, fix disa…
ravisuhag Jul 12, 2026
36f2c1b
fix: style selected/pressed rows, add row-state hooks to Table primitive
ravisuhag Jul 12, 2026
f44b7d2
fix: fade the code-block copy button reveal at the fast tier
ravisuhag Jul 12, 2026
08c564f
fix: default Amount to tabular numerals
ravisuhag Jul 12, 2026
f4163e1
fix: link hover reveals an underline instead of dimming the text
ravisuhag Jul 12, 2026
fae6527
fix: unify calendar day-cell radius, cross-fade the loading skeleton
ravisuhag Jul 12, 2026
10b843c
fix: show sticky-header hairlines only once content scrolls beneath them
ravisuhag Jul 12, 2026
58030ff
fix: scope preview-card inset/size transitions to viewport-morph mode
ravisuhag Jul 12, 2026
014742b
fix: scroll-area thumb grows via transform, gains hover/pressed color
ravisuhag Jul 12, 2026
89064ad
fix: side-panel overflow:auto, document as persistent layout
ravisuhag Jul 12, 2026
c6e2a0d
fix: align popover shadow depth with its surface size
ravisuhag Jul 12, 2026
4e81eac
fix: breadcrumb hover eases, active item weight actually differentiates
ravisuhag Jul 12, 2026
7c861bd
fix: tabs standalone/plain variants glide their indicator instead of …
ravisuhag Jul 12, 2026
7f7e11f
feat: announcement-bar action-button states and opt-in dismissible mode
ravisuhag Jul 12, 2026
5a9863d
fix: scope Headline's width:100% to the truncate variant
ravisuhag Jul 12, 2026
a79c1c7
fix: avatar fallback tracking scales per size, drop the !important ra…
ravisuhag Jul 12, 2026
7fd0062
fix: guard Image's fallback re-entry, fade in loaded images/avatars
ravisuhag Jul 12, 2026
d071a95
fix(tour): fade out overlay in place on close, hold last frame
ravisuhag Jul 12, 2026
c2a88c4
fix(floating-actions): margin-based centering, fade+rise entrance
ravisuhag Jul 12, 2026
0a68b0f
fix(spinner): speed the loop up from 1.2s to 0.9s
ravisuhag Jul 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions packages/raystack/components/accordion/accordion.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,19 @@
border-top: none;
}

.accordion-trigger:hover,
.accordion-trigger:focus-visible {
.accordion-trigger:hover {
background-color: var(--rs-color-background-base-primary-hover);
}

.accordion-trigger:focus-visible {
outline: 2px solid var(--rs-color-border-accent-emphasis);
outline-offset: -2px;
}

.accordion-trigger:active:not(:disabled) {
background-color: var(--rs-color-background-neutral-secondary);
}

.accordion-trigger:disabled {
pointer-events: none;
opacity: 0.5;
Expand Down Expand Up @@ -80,10 +88,10 @@

@media (prefers-reduced-motion: no-preference) {
.accordion-icon {
transition: transform 150ms ease-in-out;
transition: transform var(--rs-duration-fast) var(--rs-ease-in-out);
}

.accordion-content {
transition: height 150ms ease-out;
transition: height var(--rs-duration-fast) var(--rs-ease-out);
}
}
3 changes: 3 additions & 0 deletions packages/raystack/components/amount/amount.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.amount {
font-variant-numeric: tabular-nums;
}
15 changes: 13 additions & 2 deletions packages/raystack/components/amount/amount.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { cx } from 'class-variance-authority';
import { type ComponentProps } from 'react';
import styles from './amount.module.css';

export interface AmountProps extends ComponentProps<'span'> {
/**
Expand Down Expand Up @@ -167,6 +169,7 @@ export const Amount = ({
groupDigits = true,
valueInMinorUnits = true,
hideCurrency = false,
className,
...props
}: AmountProps) => {
try {
Expand Down Expand Up @@ -265,10 +268,18 @@ export const Amount = ({
finalBaseValue
);

return <span {...props}>{formattedValue}</span>;
return (
<span {...props} className={cx(styles.amount, className)}>
{formattedValue}
</span>
);
} catch (error) {
console.error('Error formatting amount:', error);
return <span {...props}>{String(value)}</span>;
return (
<span {...props} className={cx(styles.amount, className)}>
{String(value)}
</span>
);
}
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { act, fireEvent, render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { AnnouncementBar } from '../announcement-bar';
import styles from '../announcement-bar.module.css';
Expand Down Expand Up @@ -119,4 +119,65 @@ describe('AnnouncementBar', () => {
expect(actionBtn).toHaveClass(styles['action-btn']);
});
});

describe('Dismissible', () => {
it('does not render a dismiss button by default', () => {
render(<AnnouncementBar text='Test' />);
expect(
screen.queryByRole('button', { name: 'Dismiss announcement' })
).not.toBeInTheDocument();
});

it('renders a dismiss button when dismissible', () => {
render(<AnnouncementBar text='Test' dismissible />);
expect(
screen.getByRole('button', { name: 'Dismiss announcement' })
).toBeInTheDocument();
});

it('calls onDismiss after the exit animation when controlled', () => {
vi.useFakeTimers();
try {
const onDismiss = vi.fn();
render(
<AnnouncementBar text='Test' dismissible onDismiss={onDismiss} />
);

fireEvent.click(
screen.getByRole('button', { name: 'Dismiss announcement' })
);
expect(onDismiss).not.toHaveBeenCalled();

act(() => {
vi.advanceTimersByTime(200);
});

expect(onDismiss).toHaveBeenCalledTimes(1);
expect(screen.getByText('Test')).toBeInTheDocument();
} finally {
vi.useRealTimers();
}
});

it('removes itself after the exit animation when uncontrolled', () => {
vi.useFakeTimers();
try {
render(<AnnouncementBar text='Uncontrolled dismiss' dismissible />);

fireEvent.click(
screen.getByRole('button', { name: 'Dismiss announcement' })
);

act(() => {
vi.advanceTimersByTime(200);
});

expect(
screen.queryByText('Uncontrolled dismiss')
).not.toBeInTheDocument();
} finally {
vi.useRealTimers();
}
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
width: 100%;
min-height: var(--rs-space-10);
padding: var(--rs-space-3) var(--rs-space-7);
position: relative; /* anchors .dismiss-btn */
}

.announcement-bar-gradient {
Expand All @@ -25,7 +26,9 @@
.announcement-bar-normal > .icon,
.announcement-bar-error > .icon,
.announcement-bar-normal > .action-btn,
.announcement-bar-error > .action-btn {
.announcement-bar-error > .action-btn,
.announcement-bar-normal > .dismiss-btn,
.announcement-bar-error > .dismiss-btn {
color: var(--rs-color-foreground-base-emphasis);
}

Expand All @@ -50,4 +53,63 @@
padding: 0;
font: inherit;
color: inherit;
border-radius: var(--rs-radius-1);
transition: var(--rs-transition-interactive);
}

.action-btn:hover {
opacity: 0.8;
}

.action-btn:active {
opacity: 0.64;
}

.action-btn:focus-visible {
outline: 1px solid currentColor;
outline-offset: 2px;
}

.dismiss-btn {
position: absolute;
right: var(--rs-space-3);
top: 50%;
translate: 0 -50%;
color: inherit;
}

.dismiss-btn:hover:not(:disabled),
.dismiss-btn:active:not(:disabled) {
background-color: transparent;
color: inherit;
opacity: 0.8;
}

.dismiss-btn:focus-visible {
outline: 1px solid currentColor;
outline-offset: 2px;
}

/* Grid-row collapse: fade + height-to-zero without measuring. */
.collapse {
display: grid;
grid-template-rows: 1fr;
}

.collapse-inner {
min-height: 0;
overflow: hidden;
}

.collapse-closing {
grid-template-rows: 0fr;
opacity: 0;
}

@media (prefers-reduced-motion: no-preference) {
.collapse {
transition:
grid-template-rows var(--rs-duration-normal) var(--rs-ease-out),
opacity var(--rs-duration-normal) var(--rs-ease-out);
}
}
59 changes: 56 additions & 3 deletions packages/raystack/components/announcement-bar/announcement-bar.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
'use client';

import { cva, type VariantProps } from 'class-variance-authority';
import { ReactNode } from 'react';
import { Cross1Icon } from '@radix-ui/react-icons';
import { cva, cx, type VariantProps } from 'class-variance-authority';
import { ReactNode, useEffect, useState } from 'react';

import { Flex } from '../flex';
import { IconButton } from '../icon-button';
import { Text } from '../text';
import styles from './announcement-bar.module.css';

// Must match --rs-duration-normal used by .collapse in announcement-bar.module.css.
const DISMISS_EXIT_MS = 200;

const announcementBar = cva(styles['announcement-bar'], {
variants: {
variant: {
Expand All @@ -27,6 +32,14 @@ type AnnouncementBarProps = VariantProps<typeof announcementBar> & {
actionLabel?: string;
actionIcon?: ReactNode;
onActionClick?: () => void;
/** Show a dismiss (close) button. */
dismissible?: boolean;
/**
* Called after the dismiss animation finishes. When provided, the consumer
* owns removal (the bar stays mounted, collapsed). When omitted, the bar
* removes itself.
*/
onDismiss?: () => void;
};

export const AnnouncementBar = ({
Expand All @@ -37,9 +50,26 @@ export const AnnouncementBar = ({
actionLabel,
actionIcon,
onActionClick = () => {},
dismissible,
onDismiss,
...props
}: AnnouncementBarProps) => {
return (
const [dismissState, setDismissState] = useState<
'open' | 'closing' | 'closed'
>('open');

useEffect(() => {
if (dismissState !== 'closing') return;
const timer = setTimeout(() => {
setDismissState('closed');
onDismiss?.();
}, DISMISS_EXIT_MS);
return () => clearTimeout(timer);
}, [dismissState, onDismiss]);

if (dismissState === 'closed' && !onDismiss) return null;

const bar = (
<Flex
className={announcementBar({ className, variant })}
justify='center'
Expand Down Expand Up @@ -67,8 +97,31 @@ export const AnnouncementBar = ({
{actionIcon && <span aria-hidden='true'>{actionIcon}</span>}
</button>
) : null}
{dismissible && (
<IconButton
size={2}
className={styles['dismiss-btn']}
aria-label='Dismiss announcement'
onClick={() => setDismissState('closing')}
>
<Cross1Icon />
</IconButton>
)}
</Flex>
);

if (!dismissible) return bar;

return (
<div
className={cx(
styles.collapse,
dismissState !== 'open' && styles['collapse-closing']
)}
>
<div className={styles['collapse-inner']}>{bar}</div>
</div>
);
};

AnnouncementBar.displayName = 'AnnouncementBar';
Loading