From cbbd2c84cf84495e3743c30e7c591e48d51420b7 Mon Sep 17 00:00:00 2001 From: paanSinghCoder Date: Mon, 20 Apr 2026 16:03:43 +0530 Subject: [PATCH 01/18] feat(floating-actions): add FloatingActions component A floating bar for surfacing contextual actions (bulk-action toolbar, row hover actions, etc.). Position-agnostic visual primitive with a matching vertical separator; composes freely with existing Chip, Button, and IconButton. - Component source, styles (--rs-shadow-lifted), and tests - Docs page with preview, bulk-actions, and icon-only demos - Playground example and examples/page.tsx section Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/www/src/app/examples/page.tsx | 79 ++++++++++++++++++- .../playground/floating-actions-examples.tsx | 57 +++++++++++++ apps/www/src/components/playground/index.ts | 1 + .../docs/components/floating-actions/demo.ts | 48 +++++++++++ .../components/floating-actions/index.mdx | 59 ++++++++++++++ .../docs/components/floating-actions/props.ts | 18 +++++ .../__tests__/floating-actions.test.tsx | 77 ++++++++++++++++++ .../floating-actions.module.css | 18 +++++ .../floating-actions/floating-actions.tsx | 34 ++++++++ .../components/floating-actions/index.tsx | 1 + packages/raystack/index.tsx | 1 + 11 files changed, 392 insertions(+), 1 deletion(-) create mode 100644 apps/www/src/components/playground/floating-actions-examples.tsx create mode 100644 apps/www/src/content/docs/components/floating-actions/demo.ts create mode 100644 apps/www/src/content/docs/components/floating-actions/index.mdx create mode 100644 apps/www/src/content/docs/components/floating-actions/props.ts create mode 100644 packages/raystack/components/floating-actions/__tests__/floating-actions.test.tsx create mode 100644 packages/raystack/components/floating-actions/floating-actions.module.css create mode 100644 packages/raystack/components/floating-actions/floating-actions.tsx create mode 100644 packages/raystack/components/floating-actions/index.tsx diff --git a/apps/www/src/app/examples/page.tsx b/apps/www/src/app/examples/page.tsx index b65134762..e35feb78e 100644 --- a/apps/www/src/app/examples/page.tsx +++ b/apps/www/src/app/examples/page.tsx @@ -10,7 +10,8 @@ import { MixerHorizontalIcon, PersonIcon, QuestionMarkCircledIcon, - BellIcon as RadixBellIcon + BellIcon as RadixBellIcon, + TransformIcon } from '@radix-ui/react-icons'; import { Amount, @@ -20,12 +21,14 @@ import { Button, Calendar, Callout, + Chip, DataTable, DatePicker, Dialog, Drawer, EmptyState, Flex, + FloatingActions, IconButton, Indicator, InputField, @@ -2755,6 +2758,80 @@ const Page = () => { + + FloatingActions Examples + + + + + Selection with actions: + + } + isDismissible + > + 2 selected + + + + + + + + + Multiple action groups: + + } + isDismissible + > + 5 selected + + + + + + + + + + + Icon-only actions: + + + + + + + + + + + + + + + diff --git a/apps/www/src/components/playground/floating-actions-examples.tsx b/apps/www/src/components/playground/floating-actions-examples.tsx new file mode 100644 index 000000000..490f0a0df --- /dev/null +++ b/apps/www/src/components/playground/floating-actions-examples.tsx @@ -0,0 +1,57 @@ +'use client'; + +import { TransformIcon } from '@radix-ui/react-icons'; +import { Button, Chip, Flex, FloatingActions, Text } from '@raystack/apsara'; +import PlaygroundLayout from './playground-layout'; + +export function FloatingActionsExamples() { + return ( + + + Default: + + } + isDismissible + > + 2 selected + + + + + + + Multiple action groups: + + } + isDismissible + > + 5 selected + + + + + + + + + + ); +} diff --git a/apps/www/src/components/playground/index.ts b/apps/www/src/components/playground/index.ts index 9ec2f5237..22d903501 100644 --- a/apps/www/src/components/playground/index.ts +++ b/apps/www/src/components/playground/index.ts @@ -23,6 +23,7 @@ export * from './field-examples'; export * from './fieldset-examples'; export * from './filter-chip-examples'; export * from './flex-examples'; +export * from './floating-actions-examples'; export * from './form-examples'; export * from './headline-examples'; export * from './icon-button-examples'; diff --git a/apps/www/src/content/docs/components/floating-actions/demo.ts b/apps/www/src/content/docs/components/floating-actions/demo.ts new file mode 100644 index 000000000..0b6b2f81e --- /dev/null +++ b/apps/www/src/content/docs/components/floating-actions/demo.ts @@ -0,0 +1,48 @@ +'use client'; + +export const preview = { + type: 'code', + code: ` + } + isDismissible + > + 2 selected + + + + +` +}; + +export const bulkActionsDemo = { + type: 'code', + code: ` + } + isDismissible + > + 5 selected + + + + + +` +}; + +export const iconOnlyDemo = { + type: 'code', + code: ` + + + + +` +}; diff --git a/apps/www/src/content/docs/components/floating-actions/index.mdx b/apps/www/src/content/docs/components/floating-actions/index.mdx new file mode 100644 index 000000000..22925f021 --- /dev/null +++ b/apps/www/src/content/docs/components/floating-actions/index.mdx @@ -0,0 +1,59 @@ +--- +title: Floating Actions +description: A floating bar for surfacing contextual actions, typically shown when items are selected. +source: packages/raystack/components/floating-actions +tag: new +--- + +import { preview, bulkActionsDemo, iconOnlyDemo } from "./demo.ts"; + + + +## Anatomy + +Compose the bar from existing Apsara primitives. `FloatingActions` provides the container and a matching vertical separator; everything inside is freely composable. + +```tsx +import { FloatingActions, Chip, Button } from '@raystack/apsara' + + + 2 selected + + + + +``` + +## API Reference + +### Root + +The floating container. Renders as a `role="toolbar"` element by default. + + + +### Separator + +A vertical divider sized to the bar's content. + + + +## Examples + +### Bulk actions + +Pair a dismissible `Chip` with action buttons to build a selection toolbar. + + + +### Icon-only actions + +Use `IconButton` inside the bar for compact toolbars. + + + +## Accessibility + +- The root uses `role="toolbar"` so it is announced as a group of interactive controls. Override the `role` prop when a different grouping is more appropriate. +- The separator is marked `aria-hidden` because it has no semantic meaning. +- Provide an `aria-label` on the root when the toolbar's purpose is not obvious from its contents (e.g. `aria-label="Selection actions"`). diff --git a/apps/www/src/content/docs/components/floating-actions/props.ts b/apps/www/src/content/docs/components/floating-actions/props.ts new file mode 100644 index 000000000..c64a55f9e --- /dev/null +++ b/apps/www/src/content/docs/components/floating-actions/props.ts @@ -0,0 +1,18 @@ +export interface FloatingActionsProps { + /** + * The ARIA role of the container. + * @defaultValue "toolbar" + */ + role?: string; + + /** Additional CSS class names. */ + className?: string; + + /** The contents of the floating bar. */ + children?: React.ReactNode; +} + +export interface FloatingActionsSeparatorProps { + /** Additional CSS class names. */ + className?: string; +} diff --git a/packages/raystack/components/floating-actions/__tests__/floating-actions.test.tsx b/packages/raystack/components/floating-actions/__tests__/floating-actions.test.tsx new file mode 100644 index 000000000..9bac30d59 --- /dev/null +++ b/packages/raystack/components/floating-actions/__tests__/floating-actions.test.tsx @@ -0,0 +1,77 @@ +import { render, screen } from '@testing-library/react'; +import { createRef } from 'react'; +import { describe, expect, it } from 'vitest'; +import { FloatingActions } from '../floating-actions'; +import styles from '../floating-actions.module.css'; + +describe('FloatingActions', () => { + it('renders with children and a toolbar role', () => { + render( + + content + + ); + const root = screen.getByRole('toolbar'); + expect(root).toBeInTheDocument(); + expect(screen.getByText('content')).toBeInTheDocument(); + }); + + it('applies custom className to the root', () => { + render(content); + const root = screen.getByRole('toolbar'); + expect(root.className).toContain(styles.root); + expect(root.className).toContain('custom'); + }); + + it('forwards ref to the root element', () => { + const ref = createRef(); + render(content); + expect(ref.current).toBeInstanceOf(HTMLDivElement); + expect(ref.current).toBe(screen.getByRole('toolbar')); + }); + + it('allows overriding the root role', () => { + render( + + content + + ); + expect(screen.getByRole('group', { name: 'bulk' })).toBeInTheDocument(); + }); + + describe('Separator', () => { + it('renders a separator with the separator class', () => { + render( + + + + ); + const sep = screen.getByTestId('sep'); + expect(sep).toBeInTheDocument(); + expect(sep.className).toContain(styles.separator); + expect(sep).toHaveAttribute('aria-hidden', 'true'); + }); + + it('applies custom className', () => { + render( + + + + ); + const sep = screen.getByTestId('sep'); + expect(sep.className).toContain(styles.separator); + expect(sep.className).toContain('custom-sep'); + }); + + it('forwards ref', () => { + const ref = createRef(); + render( + + + + ); + expect(ref.current).toBeInstanceOf(HTMLDivElement); + expect(ref.current).toBe(screen.getByTestId('sep')); + }); + }); +}); diff --git a/packages/raystack/components/floating-actions/floating-actions.module.css b/packages/raystack/components/floating-actions/floating-actions.module.css new file mode 100644 index 000000000..6e890ac4a --- /dev/null +++ b/packages/raystack/components/floating-actions/floating-actions.module.css @@ -0,0 +1,18 @@ +.root { + display: inline-flex; + width: fit-content; + align-items: center; + justify-content: center; + gap: var(--rs-space-3); + padding: var(--rs-space-4) var(--rs-space-5); + background: var(--rs-color-background-base-primary); + border-radius: var(--rs-radius-2); + box-shadow: var(--rs-shadow-lifted); +} + +.separator { + flex-shrink: 0; + width: 1px; + height: var(--rs-space-5); + background-color: var(--rs-color-border-base-primary); +} diff --git a/packages/raystack/components/floating-actions/floating-actions.tsx b/packages/raystack/components/floating-actions/floating-actions.tsx new file mode 100644 index 000000000..284e3eb7d --- /dev/null +++ b/packages/raystack/components/floating-actions/floating-actions.tsx @@ -0,0 +1,34 @@ +'use client'; + +import { cx } from 'class-variance-authority'; +import type { ComponentProps } from 'react'; +import styles from './floating-actions.module.css'; + +export interface FloatingActionsProps extends ComponentProps<'div'> {} + +const FloatingActionsRoot = ({ + className, + role = 'toolbar', + ...props +}: FloatingActionsProps) => ( +
+); +FloatingActionsRoot.displayName = 'FloatingActions'; + +export interface FloatingActionsSeparatorProps extends ComponentProps<'div'> {} + +const FloatingActionsSeparator = ({ + className, + ...props +}: FloatingActionsSeparatorProps) => ( +