diff --git a/CLAUDE.md b/CLAUDE.md index 8b71b1c1..884eb602 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,6 +15,7 @@ Datalayer Core - Python SDK and CLI for the Datalayer AI Platform. Hybrid Python **Python**: `pip install -e .[test]` | `pytest datalayer_core/tests/` | `mypy datalayer_core/` **TypeScript Library**: `npm install` | `npm run build:lib` | `npm run lint` | `npm run test` **Examples**: `npm run example` (starts dev server at http://localhost:3000/) +**Storybook**: `npm run storybook` (starts on http://localhost:6006/) | `npm run build-storybook` **Code Quality**: `npm run check` | `npm run check:fix` | `npm run lint` | `npm run format` | `npm run type-check` **Docs**: `cd docs && make build` | `npm run typedoc` (generates TypeScript API docs) **Make**: `make build` | `make start` | `make docs` @@ -64,6 +65,36 @@ Datalayer Core - Python SDK and CLI for the Datalayer AI Platform. Hybrid Python - Ensure things always build after changes - Run also npm run format/lint/type-check to ensure all is working properly +## Storybook Component Testing + +**Component Story Structure:** +- All UI components have `.stories.tsx` files colocated with their source code +- Stories follow the pattern: `ComponentName.tsx` → `ComponentName.stories.tsx` +- Each story file includes multiple variants showcasing different states +- Interactive controls allow testing component props in real-time + +**Running Storybook:** +```bash +npm run storybook # Start dev server (http://localhost:6006) +npm run build-storybook # Build static Storybook +``` + +**Component Coverage:** +All 50+ UI components have comprehensive stories including: +- Avatars, Banners, Buttons, Checkout, Confetti +- Context, Display, ECharts, Flashes, IAM +- Icons, Labels, Landings, NavBar, NBGrader +- Notebooks, Primer, Progress, Runtimes, Screenshot +- Snapshots, Snippets, Storage, Students, SubNav +- Tables, TextReveal, Tokens, Toolbars, Users + +**Story Best Practices:** +- Mock external dependencies (API calls, services) +- Provide realistic test data +- Include edge cases (loading, error, empty states) +- Use TypeScript for type safety +- Follow existing patterns in the codebase + ## Running Examples **Start the examples server:** diff --git a/README.md b/README.md index b681a990..0f478fdf 100644 --- a/README.md +++ b/README.md @@ -225,6 +225,35 @@ pip install -e .[test] npm install ``` +### Storybook Component Development + +Datalayer Core includes comprehensive Storybook coverage for all UI components. Each component has its own `.stories.tsx` file located next to the component source code. + +```bash +# Start Storybook development server +npm run storybook # Runs on http://localhost:6006 + +# Build static Storybook +npm run build-storybook + +# Run Storybook on a different port +npm run storybook -- --port 6007 +``` + +**Component Story Structure:** +- Stories are colocated with components (e.g., `Button.tsx` → `Button.stories.tsx`) +- All UI components have comprehensive test coverage +- Multiple story variants showcase different component states +- Interactive controls for testing component props + +**Available Component Categories:** +- Avatars, Banners, Buttons, Checkout, Confetti +- Context, Display, ECharts, Flashes, IAM +- Icons, Labels, Landings, NavBar, NBGrader +- Notebooks, Primer, Progress, Runtimes, Screenshot +- Snapshots, Snippets, Storage, Students, SubNav +- Tables, TextReveal, Tokens, Toolbars, Users + ### Code Quality This project maintains high code quality standards with automated linting, formatting, and type checking: diff --git a/examples/nextjs-notebook/src/app/environments/page.tsx b/examples/nextjs-notebook/src/app/environments/page.tsx index 04d03a36..8c8a1920 100644 --- a/examples/nextjs-notebook/src/app/environments/page.tsx +++ b/examples/nextjs-notebook/src/app/environments/page.tsx @@ -36,7 +36,6 @@ export default function EnvironmentsPage() { // Redirect to welcome page if no token router.push('/welcome'); } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [token, router]); const fetchEnvironments = async () => { @@ -142,7 +141,6 @@ export default function EnvironmentsPage() { }} > {imageUrl ? ( - // eslint-disable-next-line {displayName} - {/* eslint-disable-next-line */} + {} Datalayer Logo - {/* eslint-disable-next-line */} + {} Datalayer; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + user: { + id: '1', + username: 'johndoe', + email: 'john@example.com', + avatarUrl: 'https://github.com/octocat.png', + firstName: 'John', + lastName: 'Doe', + }, + size: 100, + }, +}; + +export const WithClick: Story = { + args: { + user: { + id: '1', + username: 'johndoe', + email: 'john@example.com', + avatarUrl: 'https://github.com/octocat.png', + firstName: 'John', + lastName: 'Doe', + }, + size: 100, + onClick: () => alert('Avatar clicked!'), + }, +}; + +export const SmallSize: Story = { + args: { + user: { + id: '1', + username: 'johndoe', + email: 'john@example.com', + avatarUrl: 'https://github.com/octocat.png', + firstName: 'John', + lastName: 'Doe', + }, + size: 40, + }, +}; + +export const Loading: Story = { + args: { + user: undefined, + size: 100, + }, +}; diff --git a/src/components/banners/NoAutomationBanner.stories.tsx b/src/components/banners/NoAutomationBanner.stories.tsx new file mode 100644 index 00000000..086026c0 --- /dev/null +++ b/src/components/banners/NoAutomationBanner.stories.tsx @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { NoAutomationBanner } from './NoAutomationBanner'; + +const meta = { + title: 'Datalayer/Banners/NoAutomationBanner', + component: NoAutomationBanner, + tags: ['autodocs'], + parameters: { + layout: 'padded', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/src/components/buttons/DownloadCSVButton.stories.tsx b/src/components/buttons/DownloadCSVButton.stories.tsx new file mode 100644 index 00000000..72b3bb19 --- /dev/null +++ b/src/components/buttons/DownloadCSVButton.stories.tsx @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { DownloadCSVButton } from './DownloadCSVButton'; + +const meta = { + title: 'Datalayer/Buttons/DownloadCSVButton', + component: DownloadCSVButton, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, + argTypes: { + variant: { + control: 'select', + options: ['default', 'primary', 'invisible', 'danger', 'link'], + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const sampleData = { + users: [ + { name: 'Alice', age: 30, email: 'alice@example.com' }, + { name: 'Bob', age: 25, email: 'bob@example.com' }, + { name: 'Charlie', age: 35, email: 'charlie@example.com' }, + ], +}; + +export const Default: Story = { + args: { + data: sampleData, + fileName: 'sample-data', + variant: 'default', + }, +}; + +export const Primary: Story = { + args: { + data: sampleData, + fileName: 'sample-data', + variant: 'primary', + }, +}; + +export const Danger: Story = { + args: { + data: sampleData, + fileName: 'sample-data', + variant: 'danger', + }, +}; + +export const NoData: Story = { + args: { + fileName: 'empty-data', + variant: 'default', + }, +}; diff --git a/src/components/buttons/DownloadJsonButton.stories.tsx b/src/components/buttons/DownloadJsonButton.stories.tsx new file mode 100644 index 00000000..9f2148d1 --- /dev/null +++ b/src/components/buttons/DownloadJsonButton.stories.tsx @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { DownloadJsonButton } from './DownloadJsonButton'; + +const meta = { + title: 'Datalayer/Buttons/DownloadJsonButton', + component: DownloadJsonButton, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, + argTypes: { + variant: { + control: 'select', + options: ['default', 'primary', 'invisible', 'danger', 'link'], + }, + extension: { + control: 'select', + options: ['json', 'yaml', 'xml'], + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const sampleData = { + project: 'Datalayer Core', + version: '0.0.3', + components: ['buttons', 'banners', 'notebooks'], + metadata: { + created: '2023-01-01', + updated: '2025-01-01', + }, +}; + +export const Default: Story = { + args: { + data: sampleData, + fileName: 'project-data', + extension: 'json', + variant: 'default', + }, +}; + +export const Primary: Story = { + args: { + data: sampleData, + fileName: 'project-data', + extension: 'json', + variant: 'primary', + }, +}; + +export const YamlExport: Story = { + args: { + data: sampleData, + fileName: 'project-data', + extension: 'yaml', + variant: 'default', + }, +}; + +export const NoData: Story = { + args: { + fileName: 'empty-data', + extension: 'json', + variant: 'default', + }, +}; diff --git a/src/components/buttons/LongActionButton.stories.tsx b/src/components/buttons/LongActionButton.stories.tsx new file mode 100644 index 00000000..63cd98a5 --- /dev/null +++ b/src/components/buttons/LongActionButton.stories.tsx @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { fn } from 'storybook/test'; +import { PlayIcon, StopIcon, SyncIcon } from '@primer/octicons-react'; + +import { LongActionButton } from './LongActionButton'; + +const meta = { + title: 'Datalayer/Buttons/LongActionButton', + component: LongActionButton, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, + argTypes: { + onClick: { action: 'clicked' }, + }, + args: { + onClick: fn(), + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + label: 'Run Action', + icon: PlayIcon, + onClick: async () => { + await new Promise(resolve => setTimeout(resolve, 1000)); + }, + }, +}; + +export const WithCustomIcon: Story = { + args: { + label: 'Sync Data', + icon: SyncIcon, + onClick: async () => { + await new Promise(resolve => setTimeout(resolve, 2000)); + }, + }, +}; + +export const Disabled: Story = { + args: { + label: 'Disabled Action', + icon: StopIcon, + disabled: true, + onClick: async () => { + await new Promise(resolve => setTimeout(resolve, 1000)); + }, + }, +}; + +export const ForcedProgress: Story = { + args: { + label: 'Processing', + icon: PlayIcon, + inProgress: true, + onClick: async () => { + await new Promise(resolve => setTimeout(resolve, 1000)); + }, + }, +}; + +export const LongRunning: Story = { + args: { + label: 'Long Process', + icon: PlayIcon, + onClick: async () => { + await new Promise(resolve => setTimeout(resolve, 5000)); + }, + }, +}; diff --git a/src/components/buttons/UploadButton.stories.tsx b/src/components/buttons/UploadButton.stories.tsx new file mode 100644 index 00000000..09ab245c --- /dev/null +++ b/src/components/buttons/UploadButton.stories.tsx @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { fn } from 'storybook/test'; + +import { UploadButton, UploadIconButton } from './UploadButton'; + +const meta = { + title: 'Datalayer/Buttons/UploadButton', + component: UploadButton, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, + argTypes: { + variant: { + control: 'select', + options: ['default', 'primary', 'invisible', 'danger', 'link'], + }, + upload: { action: 'file uploaded' }, + }, + args: { + upload: fn().mockResolvedValue(undefined), + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + label: 'Upload File', + variant: 'primary', + multiple: false, + }, +}; + +export const Multiple: Story = { + args: { + label: 'Upload Files', + variant: 'default', + multiple: true, + }, +}; + +export const Primary: Story = { + args: { + label: 'Upload Document', + variant: 'primary', + multiple: false, + }, +}; + +export const Invisible: Story = { + args: { + label: 'Upload', + variant: 'invisible', + multiple: false, + }, +}; + +// Icon button variant +const iconMeta = { + ...meta, + title: 'Datalayer/Buttons/UploadIconButton', + component: UploadIconButton, +} satisfies Meta; + +export const IconButton: StoryObj = { + args: { + label: 'Upload file', + multiple: false, + }, +}; + +export const IconButtonMultiple: StoryObj = { + args: { + label: 'Upload files', + multiple: true, + }, +}; diff --git a/src/components/checkout/StripeCheckout.stories.tsx b/src/components/checkout/StripeCheckout.stories.tsx new file mode 100644 index 00000000..26345e8f --- /dev/null +++ b/src/components/checkout/StripeCheckout.stories.tsx @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { StripeCheckout } from './StripeCheckout'; +import type { ICheckoutPortal } from '../../models'; + +const meta = { + title: 'Datalayer/Checkout/StripeCheckout', + component: StripeCheckout, + tags: ['autodocs'], + parameters: { + layout: 'padded', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const mockCheckoutPortal: ICheckoutPortal = { + route: '/checkout', + is_modal: false, + metadata: { + stripe_key: 'pk_test_mock_stripe_key', + }, +}; + +const mockCheckoutPortalModal: ICheckoutPortal = { + url: 'https://checkout.stripe.com/test', + is_modal: true, + metadata: { + stripe_key: 'pk_test_mock_stripe_key', + }, +}; + +export const Default: Story = { + args: { + checkoutPortal: mockCheckoutPortal, + }, +}; + +export const Modal: Story = { + args: { + checkoutPortal: mockCheckoutPortalModal, + }, +}; + +export const NoPortal: Story = { + args: { + checkoutPortal: null, + }, +}; diff --git a/src/components/confetti/ConfettiSuccess.stories.tsx b/src/components/confetti/ConfettiSuccess.stories.tsx new file mode 100644 index 00000000..0ec908ab --- /dev/null +++ b/src/components/confetti/ConfettiSuccess.stories.tsx @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { ConfettiSuccess } from './ConfettiSuccess'; + +const meta = { + title: 'Datalayer/Confetti/ConfettiSuccess', + component: ConfettiSuccess, + tags: ['autodocs'], + parameters: { + layout: 'fullscreen', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/src/components/context/OrganizationSelect.stories.tsx b/src/components/context/OrganizationSelect.stories.tsx new file mode 100644 index 00000000..e6091f15 --- /dev/null +++ b/src/components/context/OrganizationSelect.stories.tsx @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { OrganizationSelect } from './OrganizationSelect'; + +// Mock dependencies +const meta = { + title: 'Datalayer/Context/OrganizationSelect', + component: OrganizationSelect, + tags: ['autodocs'], + parameters: { + layout: 'centered', + mockAddonConfigs: { + globalMockData: [ + { + path: '../../hooks', + default: { + useUser: () => ({ + id: '1', + name: 'Test User', + email: 'test@example.com', + }), + useCache: () => ({ + refreshUserOrganizations: () => + Promise.resolve({ success: true }), + getUserOrganizations: () => [ + { id: '1', name: 'Organization 1' }, + { id: '2', name: 'Organization 2' }, + { id: '3', name: 'Organization 3' }, + ], + }), + }, + }, + { + path: '../../state', + default: { + useLayoutStore: () => ({ + organization: undefined, + updateLayoutOrganization: () => {}, + updateLayoutSpace: () => {}, + }), + }, + }, + ], + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const WithSelection: Story = { + parameters: { + mockAddonConfigs: { + globalMockData: [ + { + path: '../../hooks', + default: { + useUser: () => ({ + id: '1', + name: 'Test User', + email: 'test@example.com', + }), + useCache: () => ({ + refreshUserOrganizations: () => + Promise.resolve({ success: true }), + getUserOrganizations: () => [ + { id: '1', name: 'Organization 1' }, + { id: '2', name: 'Organization 2' }, + { id: '3', name: 'Organization 3' }, + ], + }), + }, + }, + { + path: '../../state', + default: { + useLayoutStore: () => ({ + organization: { id: '1', name: 'Organization 1' }, + updateLayoutOrganization: () => {}, + updateLayoutSpace: () => {}, + }), + }, + }, + ], + }, + }, +}; + +export const EmptyOrganizations: Story = { + parameters: { + mockAddonConfigs: { + globalMockData: [ + { + path: '../../hooks', + default: { + useUser: () => ({ + id: '1', + name: 'Test User', + email: 'test@example.com', + }), + useCache: () => ({ + refreshUserOrganizations: () => + Promise.resolve({ success: true }), + getUserOrganizations: () => [], + }), + }, + }, + { + path: '../../state', + default: { + useLayoutStore: () => ({ + organization: undefined, + updateLayoutOrganization: () => {}, + updateLayoutSpace: () => {}, + }), + }, + }, + ], + }, + }, +}; diff --git a/src/components/context/SpaceSelect.stories.tsx b/src/components/context/SpaceSelect.stories.tsx new file mode 100644 index 00000000..2b0c289c --- /dev/null +++ b/src/components/context/SpaceSelect.stories.tsx @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { SpaceSelect } from './SpaceSelect'; + +// Mock dependencies +const meta = { + title: 'Datalayer/Context/SpaceSelect', + component: SpaceSelect, + tags: ['autodocs'], + parameters: { + layout: 'centered', + mockAddonConfigs: { + globalMockData: [ + { + path: '../../hooks', + default: { + useUser: () => ({ + id: '1', + name: 'Test User', + email: 'test@example.com', + }), + useCache: () => ({ + refreshUserSpaces: () => Promise.resolve({ success: true }), + getUserSpaces: () => [ + { id: '1', name: 'Personal Space' }, + { id: '2', name: 'Team Space' }, + { id: '3', name: 'Project Space' }, + ], + refreshOrganizationSpaces: () => + Promise.resolve({ success: true }), + getOrganizationSpaces: () => [ + { id: '1', name: 'Org Space 1' }, + { id: '2', name: 'Org Space 2' }, + ], + }), + }, + }, + { + path: '../../state', + default: { + useLayoutStore: () => ({ + organization: undefined, + space: undefined, + updateLayoutSpace: () => {}, + }), + }, + }, + ], + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const WithOrganization: Story = { + parameters: { + mockAddonConfigs: { + globalMockData: [ + { + path: '../../hooks', + default: { + useUser: () => ({ + id: '1', + name: 'Test User', + email: 'test@example.com', + }), + useCache: () => ({ + refreshUserSpaces: () => Promise.resolve({ success: true }), + getUserSpaces: () => [ + { id: '1', name: 'Personal Space' }, + { id: '2', name: 'Team Space' }, + ], + refreshOrganizationSpaces: () => + Promise.resolve({ success: true }), + getOrganizationSpaces: () => [ + { id: '1', name: 'Org Space 1' }, + { id: '2', name: 'Org Space 2' }, + { id: '3', name: 'Org Space 3' }, + ], + }), + }, + }, + { + path: '../../state', + default: { + useLayoutStore: () => ({ + organization: { id: '1', name: 'Test Organization' }, + space: undefined, + updateLayoutSpace: () => {}, + }), + }, + }, + ], + }, + }, +}; + +export const WithSelectedSpace: Story = { + parameters: { + mockAddonConfigs: { + globalMockData: [ + { + path: '../../hooks', + default: { + useUser: () => ({ + id: '1', + name: 'Test User', + email: 'test@example.com', + }), + useCache: () => ({ + refreshUserSpaces: () => Promise.resolve({ success: true }), + getUserSpaces: () => [ + { id: '1', name: 'Personal Space' }, + { id: '2', name: 'Team Space' }, + { id: '3', name: 'Project Space' }, + ], + refreshOrganizationSpaces: () => + Promise.resolve({ success: true }), + getOrganizationSpaces: () => [ + { id: '1', name: 'Org Space 1' }, + { id: '2', name: 'Org Space 2' }, + ], + }), + }, + }, + { + path: '../../state', + default: { + useLayoutStore: () => ({ + organization: undefined, + space: { id: '1', name: 'Personal Space' }, + updateLayoutSpace: () => {}, + }), + }, + }, + ], + }, + }, +}; diff --git a/src/components/display/AvatarSkeleton.stories.tsx b/src/components/display/AvatarSkeleton.stories.tsx new file mode 100644 index 00000000..fda21163 --- /dev/null +++ b/src/components/display/AvatarSkeleton.stories.tsx @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { AvatarSkeleton } from './AvatarSkeleton'; + +const meta = { + title: 'Datalayer/Display/AvatarSkeleton', + component: AvatarSkeleton, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, + argTypes: { + size: { + control: 'number', + description: 'Size of the avatar skeleton', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const Small: Story = { + args: { + size: 24, + }, +}; + +export const Medium: Story = { + args: { + size: 48, + }, +}; + +export const Large: Story = { + args: { + size: 72, + }, +}; diff --git a/src/components/display/CenteredSpinner.stories.tsx b/src/components/display/CenteredSpinner.stories.tsx new file mode 100644 index 00000000..7a1e6987 --- /dev/null +++ b/src/components/display/CenteredSpinner.stories.tsx @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { CenteredSpinner } from './CenteredSpinner'; + +const meta = { + title: 'Datalayer/Display/CenteredSpinner', + component: CenteredSpinner, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, + argTypes: { + message: { + control: 'text', + description: 'Optional message to display next to spinner', + }, + size: { + control: 'select', + options: ['small', 'medium', 'large'], + description: 'Size of the spinner', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const WithMessage: Story = { + args: { + message: 'Loading data...', + }, +}; + +export const Small: Story = { + args: { + size: 'small', + message: 'Processing...', + }, +}; + +export const Large: Story = { + args: { + size: 'large', + message: 'Loading application...', + }, +}; diff --git a/src/components/display/CodePreview.stories.tsx b/src/components/display/CodePreview.stories.tsx new file mode 100644 index 00000000..b9d2a1df --- /dev/null +++ b/src/components/display/CodePreview.stories.tsx @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { CodePreview } from './CodePreview'; + +const meta = { + title: 'Datalayer/Display/CodePreview', + component: CodePreview, + tags: ['autodocs'], + parameters: { + layout: 'padded', + }, + argTypes: { + code: { + control: 'text', + description: 'Code to display', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + code: 'console.log("Hello, world!");', + }, +}; + +export const PythonCode: Story = { + args: { + code: `import pandas as pd +import numpy as np + +def analyze_data(data): + """Analyze the input data""" + result = pd.DataFrame(data) + return result.describe() + +# Example usage +data = {'values': np.random.randn(100)} +stats = analyze_data(data) +print(stats)`, + }, +}; + +export const JSONCode: Story = { + args: { + code: `{ + "name": "datalayer-core", + "version": "1.0.0", + "description": "Datalayer Core SDK", + "scripts": { + "build": "npm run build:lib", + "dev": "vite", + "test": "vitest" + } +}`, + }, +}; + +export const LongCode: Story = { + args: { + code: `function calculateComplexAnalysis(dataset, options = {}) { + const { threshold = 0.5, method = 'standard', includeMetrics = true } = options; + + // This is a very long line that will demonstrate word wrapping behavior in the code preview component when dealing with extensive content that exceeds normal display widths + const processedData = dataset.map(item => ({ ...item, processed: true, timestamp: Date.now(), metadata: { source: 'analysis', confidence: Math.random() } })); + + return { + results: processedData, + summary: includeMetrics ? generateSummaryMetrics(processedData) : null, + config: { threshold, method } + }; +}`, + }, +}; diff --git a/src/components/display/DatalayerBox.stories.tsx b/src/components/display/DatalayerBox.stories.tsx new file mode 100644 index 00000000..b944ba6a --- /dev/null +++ b/src/components/display/DatalayerBox.stories.tsx @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { DatalayerBox } from './DatalayerBox'; + +const meta = { + title: 'Datalayer/Display/DatalayerBox', + component: DatalayerBox, + tags: ['autodocs'], + parameters: { + layout: 'padded', + mockAddonConfigs: { + globalMockData: [ + { + path: '../../hooks', + default: { + useNavigate: () => (path: string) => { + console.log('Navigate to:', path); + }, + }, + }, + ], + }, + }, + argTypes: { + title: { + control: 'text', + description: 'Title of the box', + }, + linkLabel: { + control: 'text', + description: 'Label for the navigation link', + }, + linkRoute: { + control: 'text', + description: 'Route for the navigation link', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + title: 'Sample Box Title', + children: 'This is the content inside the Datalayer box component.', + }, +}; + +export const WithLink: Story = { + args: { + title: 'Notebooks', + linkLabel: 'View All', + linkRoute: '/notebooks', + children: 'Here you can see your recent notebooks and create new ones.', + }, +}; + +export const WithComplexContent: Story = { + args: { + title: 'Dashboard Statistics', + linkLabel: 'Details', + linkRoute: '/dashboard', + children: ( +
+

Recent activity summary:

+
    +
  • 5 notebooks created this week
  • +
  • 12 experiments completed
  • +
  • 3 models deployed
  • +
+
+ ), + }, +}; + +export const LongTitle: Story = { + args: { + title: 'This is a Very Long Title That Might Wrap to Multiple Lines', + linkLabel: 'See More', + linkRoute: '/details', + children: 'Content for the box with a long title.', + }, +}; diff --git a/src/components/display/HorizontalCenter.stories.tsx b/src/components/display/HorizontalCenter.stories.tsx new file mode 100644 index 00000000..118f948b --- /dev/null +++ b/src/components/display/HorizontalCenter.stories.tsx @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { HorizontalCenter } from './HorizontalCenter'; + +const meta = { + title: 'Datalayer/Display/HorizontalCenter', + component: HorizontalCenter, + tags: ['autodocs'], + parameters: { + layout: 'padded', + }, + argTypes: { + margin: { + control: 'text', + description: 'CSS margin value', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + children: ( +
+ Centered Content +
+ ), + }, +}; + +export const WithMargin: Story = { + args: { + margin: '20px', + children: ( +
+ Content with margin +
+ ), + }, +}; + +export const MultipleElements: Story = { + args: { + margin: '10px', + children: ( + <> +
+ Element 1 +
+
+ Element 2 +
+
Element 3
+ + ), + }, +}; diff --git a/src/components/display/JupyterDialog.stories.tsx b/src/components/display/JupyterDialog.stories.tsx new file mode 100644 index 00000000..cd4f80df --- /dev/null +++ b/src/components/display/JupyterDialog.stories.tsx @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { createElement } from 'react'; +import { Dialog } from '@jupyterlab/apputils'; + +import { JupyterDialog } from './JupyterDialog'; + +const meta = { + title: 'Datalayer/Display/JupyterDialog', + component: JupyterDialog, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, + argTypes: { + title: { + control: 'text', + description: 'Dialog title', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Simple body component for stories +const SimpleBody = ({ + setValue, +}: { + setValue: (v: string | Error) => void; +}) => { + return createElement('div', {}, 'This is a simple dialog body'); +}; + +// Form body component for stories +const FormBody = ({ setValue }: { setValue: (v: string | Error) => void }) => { + return createElement('div', { + children: [ + createElement('p', { key: 'text' }, 'Please enter some text:'), + createElement('input', { + key: 'input', + type: 'text', + placeholder: 'Enter text here...', + onChange: (e: any) => setValue(e.target.value), + style: { width: '100%', padding: '8px', marginTop: '8px' }, + }), + ], + }); +}; + +export const Default: Story = { + render: () => { + const dialog = new JupyterDialog({ + title: 'Sample Dialog', + body: SimpleBody, + buttons: [Dialog.cancelButton(), Dialog.okButton()], + checkbox: null, + host: document.body, + }); + + // For Storybook, we'll render the component directly + return dialog.render(); + }, +}; + +export const WithCheckbox: Story = { + render: () => { + const dialog = new JupyterDialog({ + title: 'Dialog with Checkbox', + body: SimpleBody, + buttons: [Dialog.cancelButton(), Dialog.okButton()], + checkbox: { + label: 'Remember my choice', + caption: 'This will save your preference', + checked: false, + }, + host: document.body, + }); + + return dialog.render(); + }, +}; + +export const WithForm: Story = { + render: () => { + const dialog = new JupyterDialog({ + title: 'Input Dialog', + body: FormBody, + buttons: [Dialog.cancelButton(), Dialog.okButton()], + checkbox: null, + host: document.body, + }); + + return dialog.render(); + }, +}; + +export const DangerButton: Story = { + render: () => { + const dialog = new JupyterDialog({ + title: 'Confirm Action', + body: ({ setValue }) => + createElement('div', {}, 'This action cannot be undone. Are you sure?'), + buttons: [ + Dialog.cancelButton(), + Dialog.warnButton({ label: 'Delete', accept: true }), + ], + checkbox: null, + host: document.body, + }); + + return dialog.render(); + }, +}; diff --git a/src/components/display/Markdown.stories.tsx b/src/components/display/Markdown.stories.tsx new file mode 100644 index 00000000..4bf6d094 --- /dev/null +++ b/src/components/display/Markdown.stories.tsx @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { Markdown } from './Markdown'; + +const meta = { + title: 'Datalayer/Display/Markdown', + component: Markdown, + tags: ['autodocs'], + parameters: { + layout: 'padded', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const mockMarkdownParser = { + render: async (text: string) => { + // Simple markdown to HTML conversion for demo + return text + .replace(/^# (.+)$/gm, '

$1

') + .replace(/^## (.+)$/gm, '

$1

') + .replace(/\*\*(.+?)\*\*/g, '$1') + .replace(/\*(.+?)\*/g, '$1') + .replace(/\n/g, '
'); + }, +}; + +export const SimpleMarkdown: Story = { + args: { + text: '# Hello World\nThis is **bold** and this is *italic*', + markdownParser: mockMarkdownParser as any, + }, +}; + +export const ComplexMarkdown: Story = { + args: { + text: `# Main Title +## Subtitle +This is a paragraph with **bold** and *italic* text. + +Another paragraph here.`, + markdownParser: mockMarkdownParser as any, + }, +}; + +export const WithSanitizer: Story = { + args: { + text: '# Hello World\n', + markdownParser: mockMarkdownParser as any, + sanitizer: { + sanitize: (html: string) => + html.replace(/)<[^<]*)*<\/script>/gi, ''), + }, + }, +}; diff --git a/src/components/display/NavLink.stories.tsx b/src/components/display/NavLink.stories.tsx new file mode 100644 index 00000000..0ebfb0ca --- /dev/null +++ b/src/components/display/NavLink.stories.tsx @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { NavLink } from './NavLink'; + +const meta = { + title: 'Datalayer/Display/NavLink', + component: NavLink, + tags: ['autodocs'], + parameters: { + layout: 'centered', + mockAddonConfigs: { + globalMockData: [ + { + path: '../../hooks', + default: { + useNavigate: () => (path: string) => { + console.log('Navigate to:', path); + }, + }, + }, + ], + }, + }, + argTypes: { + to: { + control: 'text', + description: 'Navigation target path', + }, + children: { + control: 'text', + description: 'Link content', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + to: '/dashboard', + children: 'Dashboard', + }, +}; + +export const WithIcon: Story = { + args: { + to: '/settings', + children: '⚙️ Settings', + }, +}; + +export const LongText: Story = { + args: { + to: '/very-long-path-name', + children: 'Navigate to a page with a very long name', + }, +}; diff --git a/src/components/display/NotebookSkeleton.stories.tsx b/src/components/display/NotebookSkeleton.stories.tsx new file mode 100644 index 00000000..78dc80c9 --- /dev/null +++ b/src/components/display/NotebookSkeleton.stories.tsx @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { NotebookSkeleton } from './NotebookSkeleton'; + +const meta = { + title: 'Datalayer/Display/NotebookSkeleton', + component: NotebookSkeleton, + tags: ['autodocs'], + parameters: { + layout: 'padded', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/src/components/display/Placeholder.stories.tsx b/src/components/display/Placeholder.stories.tsx new file mode 100644 index 00000000..8c564f17 --- /dev/null +++ b/src/components/display/Placeholder.stories.tsx @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { Placeholder } from './Placeholder'; + +const meta = { + title: 'Datalayer/Display/Placeholder', + component: Placeholder, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/src/components/display/ToTopBranded.stories.tsx b/src/components/display/ToTopBranded.stories.tsx new file mode 100644 index 00000000..6f570872 --- /dev/null +++ b/src/components/display/ToTopBranded.stories.tsx @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { ToTopBranded } from './ToTopBranded'; + +const meta = { + title: 'Datalayer/Display/ToTopBranded', + component: ToTopBranded, + tags: ['autodocs'], + parameters: { + layout: 'fullscreen', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const WithScrollableContent: Story = { + decorators: [ + Story => ( +
+
+

Scroll down to see the "Go Top" button in action

+

This is some content that makes the page scrollable.

+

The button is positioned fixed at the bottom left.

+
+

Middle of content

+

More content here...

+
+
+

End of content

+

Try clicking the "Go Top" button to scroll back to top.

+
+
+ +
+ ), + ], +}; diff --git a/src/components/display/VisuallyHidden.stories.tsx b/src/components/display/VisuallyHidden.stories.tsx new file mode 100644 index 00000000..df9bec90 --- /dev/null +++ b/src/components/display/VisuallyHidden.stories.tsx @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { VisuallyHidden } from './VisuallyHidden'; + +const meta = { + title: 'Datalayer/Display/VisuallyHidden', + component: VisuallyHidden, + tags: ['autodocs'], + parameters: { + layout: 'padded', + }, + argTypes: { + isVisible: { + control: 'boolean', + description: 'Whether the content should be visible', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + children: 'This text is visually hidden but accessible to screen readers', + }, +}; + +export const Visible: Story = { + args: { + isVisible: true, + children: 'This text is now visible', + }, +}; + +export const WithContext: Story = { + render: args => ( +
+

Here is some visible content.

+ + This is hidden text that provides additional context for screen readers. + +

And here is more visible content.

+
+ ), + args: { + children: + 'Screen reader only: This section contains accessibility information', + }, +}; diff --git a/src/components/echarts/EChartsReact.stories.tsx b/src/components/echarts/EChartsReact.stories.tsx new file mode 100644 index 00000000..6c97a1df --- /dev/null +++ b/src/components/echarts/EChartsReact.stories.tsx @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; +import type { EChartsOption } from 'echarts'; + +import { EChartsReact } from './EChartsReact'; + +const meta = { + title: 'Datalayer/ECharts/EChartsReact', + component: EChartsReact, + tags: ['autodocs'], + parameters: { + layout: 'padded', + }, + argTypes: { + loading: { + control: 'boolean', + description: 'Show loading state', + }, + theme: { + control: 'select', + options: ['light', 'dark'], + description: 'Chart theme', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const basicLineChartOptions: EChartsOption = { + title: { + text: 'Basic Line Chart', + }, + tooltip: { + trigger: 'axis', + }, + legend: { + data: ['Sales', 'Revenue'], + }, + grid: { + left: '3%', + right: '4%', + bottom: '3%', + containLabel: true, + }, + xAxis: { + type: 'category', + boundaryGap: false, + data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], + }, + yAxis: { + type: 'value', + }, + series: [ + { + name: 'Sales', + type: 'line', + stack: 'Total', + data: [120, 132, 101, 134, 90, 230, 210], + }, + { + name: 'Revenue', + type: 'line', + stack: 'Total', + data: [220, 182, 191, 234, 290, 330, 310], + }, + ], +}; + +const barChartOptions: EChartsOption = { + title: { + text: 'Monthly Usage', + }, + tooltip: {}, + legend: { + data: ['Usage'], + }, + xAxis: { + data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'], + }, + yAxis: {}, + series: [ + { + name: 'Usage', + type: 'bar', + data: [5, 20, 36, 10, 10, 20], + }, + ], +}; + +const pieChartOptions: EChartsOption = { + title: { + text: 'Resource Distribution', + left: 'center', + }, + tooltip: { + trigger: 'item', + }, + legend: { + orient: 'vertical', + left: 'left', + }, + series: [ + { + name: 'Resources', + type: 'pie', + radius: '50%', + data: [ + { value: 1048, name: 'CPU' }, + { value: 735, name: 'Memory' }, + { value: 580, name: 'Storage' }, + { value: 484, name: 'GPU' }, + { value: 300, name: 'Network' }, + ], + emphasis: { + itemStyle: { + shadowBlur: 10, + shadowOffsetX: 0, + shadowColor: 'rgba(0, 0, 0, 0.5)', + }, + }, + }, + ], +}; + +export const LineChart: Story = { + args: { + options: basicLineChartOptions, + style: { height: '400px' }, + }, +}; + +export const BarChart: Story = { + args: { + options: barChartOptions, + style: { height: '400px' }, + }, +}; + +export const PieChart: Story = { + args: { + options: pieChartOptions, + style: { height: '400px' }, + }, +}; + +export const LoadingState: Story = { + args: { + options: basicLineChartOptions, + loading: true, + style: { height: '400px' }, + }, +}; + +export const DarkTheme: Story = { + args: { + options: basicLineChartOptions, + theme: 'dark', + style: { height: '400px' }, + }, +}; + +export const SmallChart: Story = { + args: { + options: barChartOptions, + style: { height: '200px' }, + }, +}; diff --git a/src/components/flashes/FlashClosable.stories.tsx b/src/components/flashes/FlashClosable.stories.tsx new file mode 100644 index 00000000..47acce52 --- /dev/null +++ b/src/components/flashes/FlashClosable.stories.tsx @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { FlashClosable } from './FlashClosable'; +import { Button } from '@primer/react'; + +const meta = { + title: 'Datalayer/Flashes/FlashClosable', + component: FlashClosable, + tags: ['autodocs'], + parameters: { + layout: 'padded', + }, + argTypes: { + variant: { + control: 'select', + options: ['default', 'success', 'warning', 'danger'], + description: 'Flash variant/theme', + }, + closable: { + control: 'boolean', + description: 'Whether the flash can be closed', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + children: 'This is a default flash message that can be closed.', + closable: true, + }, +}; + +export const Success: Story = { + args: { + variant: 'success', + children: 'Operation completed successfully!', + closable: true, + }, +}; + +export const Warning: Story = { + args: { + variant: 'warning', + children: 'Please be aware of this important warning message.', + closable: true, + }, +}; + +export const Danger: Story = { + args: { + variant: 'danger', + children: 'An error has occurred. Please check your input and try again.', + closable: true, + }, +}; + +export const NotClosable: Story = { + args: { + variant: 'warning', + children: 'This flash message cannot be closed by the user.', + closable: false, + }, +}; + +export const WithActions: Story = { + args: { + variant: 'warning', + children: 'Would you like to save your changes before continuing?', + closable: true, + actions: ( + <> + + + + ), + }, +}; + +export const LongMessage: Story = { + args: { + variant: 'default', + children: + 'This is a very long flash message that demonstrates how the component handles extended content. It might wrap to multiple lines depending on the available width, and the close button should remain properly positioned.', + closable: true, + }, +}; diff --git a/src/components/flashes/FlashDisclaimer.stories.tsx b/src/components/flashes/FlashDisclaimer.stories.tsx new file mode 100644 index 00000000..b3e25c6d --- /dev/null +++ b/src/components/flashes/FlashDisclaimer.stories.tsx @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { FlashDisclaimer } from './FlashDisclaimer'; + +const meta = { + title: 'Datalayer/Flashes/FlashDisclaimer', + component: FlashDisclaimer, + tags: ['autodocs'], + parameters: { + layout: 'padded', + mockAddonConfigs: { + globalMockData: [ + { + path: '../../state', + default: { + useCoreStore: () => ({ + configuration: { whiteLabel: false }, + }), + useRuntimesStore: () => ({ + showDisclaimer: true, + setShowDisclaimer: (show: boolean) => { + console.log('Setting disclaimer visibility:', show); + }, + }), + }, + }, + { + path: '../../hooks', + default: { + useNavigate: () => (path: string, e?: Event) => { + console.log('Navigate to:', path); + }, + }, + }, + ], + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const WhiteLabelHidden: Story = { + parameters: { + mockAddonConfigs: { + globalMockData: [ + { + path: '../../state', + default: { + useCoreStore: () => ({ + configuration: { whiteLabel: true }, + }), + useRuntimesStore: () => ({ + showDisclaimer: true, + setShowDisclaimer: (show: boolean) => { + console.log('Setting disclaimer visibility:', show); + }, + }), + }, + }, + { + path: '../../hooks', + default: { + useNavigate: () => (path: string, e?: Event) => { + console.log('Navigate to:', path); + }, + }, + }, + ], + }, + }, +}; + +export const DisclaimerHidden: Story = { + parameters: { + mockAddonConfigs: { + globalMockData: [ + { + path: '../../state', + default: { + useCoreStore: () => ({ + configuration: { whiteLabel: false }, + }), + useRuntimesStore: () => ({ + showDisclaimer: false, + setShowDisclaimer: (show: boolean) => { + console.log('Setting disclaimer visibility:', show); + }, + }), + }, + }, + { + path: '../../hooks', + default: { + useNavigate: () => (path: string, e?: Event) => { + console.log('Navigate to:', path); + }, + }, + }, + ], + }, + }, +}; diff --git a/src/components/flashes/FlashGuest.stories.tsx b/src/components/flashes/FlashGuest.stories.tsx new file mode 100644 index 00000000..38ff6371 --- /dev/null +++ b/src/components/flashes/FlashGuest.stories.tsx @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { FlashGuest } from './FlashGuest'; + +const meta = { + title: 'Datalayer/Flashes/FlashGuest', + component: FlashGuest, + tags: ['autodocs'], + parameters: { + layout: 'padded', + mockAddonConfigs: { + globalMockData: [ + { + path: '../../state', + default: { + useCoreStore: () => ({ + configuration: { whiteLabel: false }, + }), + useIAMStore: () => ({ + user: { id: '1', name: 'Guest User', roles: ['guest'] }, + logout: () => { + console.log('Logging out user'); + }, + }), + }, + }, + { + path: '../../hooks', + default: { + useNavigate: () => (path: string) => { + console.log('Navigate to:', path); + }, + useAuthorization: () => ({ + checkIsPlatformMember: (user: any) => false, + }), + }, + }, + { + path: '../../routes', + default: { + CONTACT_ROUTE: '/support/contact', + }, + }, + ], + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const WhiteLabel: Story = { + parameters: { + mockAddonConfigs: { + globalMockData: [ + { + path: '../../state', + default: { + useCoreStore: () => ({ + configuration: { whiteLabel: true }, + }), + useIAMStore: () => ({ + user: { id: '1', name: 'Guest User', roles: ['guest'] }, + logout: () => { + console.log('Logging out user'); + }, + }), + }, + }, + { + path: '../../hooks', + default: { + useNavigate: () => (path: string) => { + console.log('Navigate to:', path); + }, + useAuthorization: () => ({ + checkIsPlatformMember: (user: any) => false, + }), + }, + }, + { + path: '../../routes', + default: { + CONTACT_ROUTE: '/support/contact', + }, + }, + ], + }, + }, +}; + +export const PlatformMemberHidden: Story = { + parameters: { + mockAddonConfigs: { + globalMockData: [ + { + path: '../../state', + default: { + useCoreStore: () => ({ + configuration: { whiteLabel: false }, + }), + useIAMStore: () => ({ + user: { id: '1', name: 'Platform User', roles: ['member'] }, + logout: () => { + console.log('Logging out user'); + }, + }), + }, + }, + { + path: '../../hooks', + default: { + useNavigate: () => (path: string) => { + console.log('Navigate to:', path); + }, + useAuthorization: () => ({ + checkIsPlatformMember: (user: any) => true, + }), + }, + }, + { + path: '../../routes', + default: { + CONTACT_ROUTE: '/support/contact', + }, + }, + ], + }, + }, +}; diff --git a/src/components/flashes/FlashSurveys.stories.tsx b/src/components/flashes/FlashSurveys.stories.tsx new file mode 100644 index 00000000..34a47437 --- /dev/null +++ b/src/components/flashes/FlashSurveys.stories.tsx @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { FlashSurveys, SURVEY_2025_1_NAME } from './FlashSurveys'; + +const meta = { + title: 'Datalayer/Flashes/FlashSurveys', + component: FlashSurveys, + tags: ['autodocs'], + parameters: { + layout: 'padded', + mockAddonConfigs: { + globalMockData: [ + { + path: '../../hooks', + default: { + useToast: () => ({ + enqueueToast: (message: string, options?: any) => { + console.log('Toast:', message, options); + }, + }), + }, + }, + { + path: '../../state', + default: { + useSurveysStore: () => ({ + surveys: new Map(), + createSurvey: (name: string, data: any) => { + console.log('Creating survey:', name, data); + }, + }), + useCoreStore: () => ({ + configuration: { whiteLabel: false }, + }), + }, + }, + { + path: './surveys', + default: { + Survey2025_1: ({ formData, onSubmit }: any) => ( +
+

Mock Survey Component

+ +
+ ), + }, + }, + ], + }, + }, + argTypes: { + surveyName: { + control: 'text', + description: 'Specific survey name to show', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const SpecificSurvey: Story = { + args: { + surveyName: SURVEY_2025_1_NAME, + }, +}; + +export const WithExistingSurvey: Story = { + parameters: { + mockAddonConfigs: { + globalMockData: [ + { + path: '../../hooks', + default: { + useToast: () => ({ + enqueueToast: (message: string, options?: any) => { + console.log('Toast:', message, options); + }, + }), + }, + }, + { + path: '../../state', + default: { + useSurveysStore: () => ({ + surveys: new Map([ + [ + SURVEY_2025_1_NAME, + { form: { question1: 'previous answer' } }, + ], + ]), + createSurvey: (name: string, data: any) => { + console.log('Creating survey:', name, data); + }, + }), + useCoreStore: () => ({ + configuration: { whiteLabel: false }, + }), + }, + }, + { + path: './surveys', + default: { + Survey2025_1: ({ formData, onSubmit }: any) => ( +
+

Mock Survey Component (Already Completed)

+

Previous data: {JSON.stringify(formData)}

+ +
+ ), + }, + }, + ], + }, + }, +}; + +export const WhiteLabelHidden: Story = { + parameters: { + mockAddonConfigs: { + globalMockData: [ + { + path: '../../hooks', + default: { + useToast: () => ({ + enqueueToast: (message: string, options?: any) => { + console.log('Toast:', message, options); + }, + }), + }, + }, + { + path: '../../state', + default: { + useSurveysStore: () => ({ + surveys: new Map(), + createSurvey: (name: string, data: any) => { + console.log('Creating survey:', name, data); + }, + }), + useCoreStore: () => ({ + configuration: { whiteLabel: true }, + }), + }, + }, + { + path: './surveys', + default: { + Survey2025_1: ({ formData, onSubmit }: any) => ( +
Survey Component (should be hidden)
+ ), + }, + }, + ], + }, + }, +}; diff --git a/src/components/flashes/FlashUnauthorized.stories.tsx b/src/components/flashes/FlashUnauthorized.stories.tsx new file mode 100644 index 00000000..bf4eea7d --- /dev/null +++ b/src/components/flashes/FlashUnauthorized.stories.tsx @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { FlashUnauthorized } from './FlashUnauthorized'; + +const meta = { + title: 'Datalayer/Flashes/FlashUnauthorized', + component: FlashUnauthorized, + tags: ['autodocs'], + parameters: { + layout: 'padded', + mockAddonConfigs: { + globalMockData: [ + { + path: '../../hooks', + default: { + useNavigate: () => (path: string, e?: Event) => { + console.log('Navigate to:', path); + }, + }, + }, + ], + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/src/components/flashes/surveys/Survey2025_1.stories.tsx b/src/components/flashes/surveys/Survey2025_1.stories.tsx new file mode 100644 index 00000000..35d1c6fb --- /dev/null +++ b/src/components/flashes/surveys/Survey2025_1.stories.tsx @@ -0,0 +1,309 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { Survey2025_1 } from './Survey2025_1'; + +const meta = { + title: 'Datalayer/Flashes/Surveys/Survey2025_1', + component: Survey2025_1, + tags: ['autodocs'], + parameters: { + layout: 'centered', + mockAddonConfigs: { + globalMockData: [ + { + path: '@datalayer/primer-rjsf', + default: ({ + schema, + uiSchema, + formData, + onSubmit, + readonly, + disabled, + validator, + }) => { + const handleSubmit = e => { + e.preventDefault(); + const formDataFromForm = new FormData(e.target); + const data = Object.fromEntries(formDataFromForm); + onSubmit?.({ formData: data }, e); + }; + + return ( +
+
+
+ + +
+ +
+ +
+ {[ + { + value: 'learn how to analyse data', + label: 'Learn how to analyse data', + }, + { + value: + 'scale my laptop jupyter notebook with stronger cpu gpu memory', + label: + 'Scale my laptop Jupyter Notebook with stronger CPU/GPU/Memory', + }, + { + value: + 'scale from jupyterhub with stronger cpu gpu memory', + label: + 'Scale from JupyterHub with stronger CPU/GPU/Memory', + }, + { + value: 'create ai models', + label: 'Create AI models', + }, + { + value: 'work in team on jupyter notebooks', + label: 'Work in team on Jupyter Notebooks', + }, + { + value: 'publis and share my data analysis', + label: 'Publish and share my data analysis', + }, + ].map(activity => ( + + ))} +
+
+ + {!readonly && ( +
+ +
+ )} +
+
+ ); + }, + }, + { + path: '@rjsf/validator-ajv8', + default: { + // Mock validator - in a real implementation this would validate the form + validate: () => ({ errors: [] }), + }, + }, + ], + }, + }, + argTypes: { + readonly: { + control: 'boolean', + description: 'Whether the form is readonly', + }, + formData: { + control: 'object', + description: 'Pre-populated form data', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + onSubmit: (data, e) => { + console.log('Survey submitted:', data); + }, + readonly: false, + }, +}; + +export const WithPrefilledData: Story = { + args: { + onSubmit: (data, e) => { + console.log('Survey submitted:', data); + }, + readonly: false, + formData: { + profile_s: 'data analyst scientist', + activities_txt: [ + 'scale my laptop jupyter notebook with stronger cpu gpu memory', + 'create ai models', + 'work in team on jupyter notebooks', + ], + }, + }, +}; + +export const ReadonlyMode: Story = { + args: { + onSubmit: (data, e) => { + console.log('Survey submitted (readonly):', data); + }, + readonly: true, + formData: { + profile_s: 'researcher phd', + activities_txt: [ + 'learn how to analyse data', + 'create ai models', + 'publis and share my data analysis', + ], + }, + }, +}; + +export const BeginnerStudent: Story = { + args: { + onSubmit: (data, e) => { + console.log('Survey submitted:', data); + }, + readonly: false, + formData: { + profile_s: 'beginner student', + activities_txt: [ + 'learn how to analyse data', + 'scale my laptop jupyter notebook with stronger cpu gpu memory', + ], + }, + }, +}; + +export const DataPlatformBuilder: Story = { + args: { + onSubmit: (data, e) => { + console.log('Survey submitted:', data); + }, + readonly: false, + formData: { + profile_s: 'data platform builder', + activities_txt: [ + 'scale from jupyterhub with stronger cpu gpu memory', + 'work in team on jupyter notebooks', + 'publis and share my data analysis', + ], + }, + }, +}; + +export const InteractiveDemo: Story = { + render: args => { + return ( +
+

2025 Data Survey

+

+ Help us understand your data needs and how we can better serve you. + This survey includes questions about your profile and data-related + activities. +

+ +
+

+ Survey Features: +

+
    +
  • Profile selection (required)
  • +
  • Multiple activity selection (required)
  • +
  • Form validation
  • +
  • Readonly mode available
  • +
+
+
+ ); + }, + args: { + onSubmit: (data, e) => { + console.log('Survey submitted:', data); + alert('Survey submitted! Check console for data.'); + }, + readonly: false, + }, +}; diff --git a/src/components/iam/ExternalTokenSilentLogin.stories.tsx b/src/components/iam/ExternalTokenSilentLogin.stories.tsx new file mode 100644 index 00000000..8cda6bbd --- /dev/null +++ b/src/components/iam/ExternalTokenSilentLogin.stories.tsx @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { ExternalTokenSilentLogin } from './ExternalTokenSilentLogin'; + +const meta = { + title: 'Datalayer/IAM/ExternalTokenSilentLogin', + component: ExternalTokenSilentLogin, + tags: ['autodocs'], + parameters: { + layout: 'centered', + mockAddonConfigs: { + globalMockData: [ + { + path: '../../state', + default: { + useIAMStore: () => ({ + logout: () => { + console.log('Logging out'); + }, + checkIAMToken: () => Promise.resolve({ valid: true }), + externalToken: 'mock-external-token', + }), + }, + }, + { + path: '../../hooks', + default: { + useToast: () => ({ + enqueueToast: (message: string, options?: any) => { + console.log('Toast:', message, options); + }, + }), + useIAM: () => ({ + loginAndNavigate: ( + token: string, + logout: Function, + checkToken: Function, + ) => { + console.log('Login and navigate with token:', token); + return Promise.resolve(); + }, + }), + }, + }, + ], + }, + }, + argTypes: { + message: { + control: 'text', + description: 'Message to display during login', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + message: 'Logging in with external token...', + }, +}; + +export const WithoutExternalToken: Story = { + args: { + message: 'Waiting for external token...', + }, + parameters: { + mockAddonConfigs: { + globalMockData: [ + { + path: '../../state', + default: { + useIAMStore: () => ({ + logout: () => { + console.log('Logging out'); + }, + checkIAMToken: () => Promise.resolve({ valid: true }), + externalToken: null, + }), + }, + }, + { + path: '../../hooks', + default: { + useToast: () => ({ + enqueueToast: (message: string, options?: any) => { + console.log('Toast:', message, options); + }, + }), + useIAM: () => ({ + loginAndNavigate: ( + token: string, + logout: Function, + checkToken: Function, + ) => { + console.log('Login and navigate with token:', token); + return Promise.resolve(); + }, + }), + }, + }, + ], + }, + }, +}; + +export const LoginError: Story = { + args: { + message: 'Logging in...', + }, + parameters: { + mockAddonConfigs: { + globalMockData: [ + { + path: '../../state', + default: { + useIAMStore: () => ({ + logout: () => { + console.log('Logging out'); + }, + checkIAMToken: () => Promise.resolve({ valid: true }), + externalToken: 'invalid-token', + }), + }, + }, + { + path: '../../hooks', + default: { + useToast: () => ({ + enqueueToast: (message: string, options?: any) => { + console.log('Toast:', message, options); + }, + }), + useIAM: () => ({ + loginAndNavigate: ( + token: string, + logout: Function, + checkToken: Function, + ) => { + console.log('Login failed with token:', token); + return Promise.reject(new Error('Invalid token')); + }, + }), + }, + }, + ], + }, + }, +}; diff --git a/src/components/icons/ArtifactIcon.stories.tsx b/src/components/icons/ArtifactIcon.stories.tsx new file mode 100644 index 00000000..732397bf --- /dev/null +++ b/src/components/icons/ArtifactIcon.stories.tsx @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { ArtifactIcon } from './ArtifactIcon'; + +const meta = { + title: 'Datalayer/Icons/ArtifactIcon', + component: ArtifactIcon, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, + argTypes: { + type: { + control: 'select', + options: [ + 'assignment', + 'authoring', + 'cell', + 'content', + 'credits', + 'dataset', + 'datasource', + 'document', + 'documentation', + 'environment', + 'exercise', + 'growth', + 'home', + 'invite', + 'runtime', + 'runtime-snapshot', + 'library', + 'lesson', + 'mail', + 'management', + 'notebook', + 'organization', + 'onboarding', + 'page', + 'settings', + 'share', + 'space', + 'success', + 'support', + 'storage', + 'tag', + 'team', + 'usage', + 'user', + 'undefined', + ], + description: 'Type of artifact to display icon for', + }, + size: { + control: { type: 'select' }, + options: [16, 24, 32, 'small', 'medium', 'large'], + description: 'Size of the icon', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + type: 'notebook', + }, +}; + +export const Assignment: Story = { + args: { + type: 'assignment', + size: 24, + }, +}; + +export const Runtime: Story = { + args: { + type: 'runtime', + size: 32, + }, +}; + +export const WithItem: Story = { + args: { + item: { + id: '1', + type: 'space', + name: 'My Space', + }, + size: 24, + }, +}; + +export const AllSizes: Story = { + render: () => ( +
+ + + + + + +
+ ), +}; + +export const AllTypes: Story = { + render: () => ( +
+ {[ + 'assignment', + 'authoring', + 'cell', + 'content', + 'credits', + 'dataset', + 'datasource', + 'document', + 'documentation', + 'environment', + 'exercise', + 'growth', + 'home', + 'invite', + 'runtime', + 'runtime-snapshot', + 'library', + 'lesson', + 'mail', + 'management', + 'notebook', + 'organization', + 'onboarding', + 'page', + 'settings', + 'share', + 'space', + 'success', + 'support', + 'storage', + 'tag', + 'team', + 'usage', + 'user', + 'undefined', + ].map(type => ( +
+ +
{type}
+
+ ))} +
+ ), +}; diff --git a/src/components/labels/VisibilityLabel.stories.tsx b/src/components/labels/VisibilityLabel.stories.tsx new file mode 100644 index 00000000..55b15c7e --- /dev/null +++ b/src/components/labels/VisibilityLabel.stories.tsx @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { VisibilityLabel } from './VisibilityLabel'; + +const meta = { + title: 'Datalayer/Labels/VisibilityLabel', + component: VisibilityLabel, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, + argTypes: { + isPublic: { + control: { type: 'select' }, + options: [true, false, undefined], + description: 'Whether the item is public, private, or undefined', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Public: Story = { + args: { + isPublic: true, + }, +}; + +export const Private: Story = { + args: { + isPublic: false, + }, +}; + +export const Undefined: Story = { + args: { + isPublic: undefined, + }, +}; diff --git a/src/components/landings/StepBlock.stories.tsx b/src/components/landings/StepBlock.stories.tsx new file mode 100644 index 00000000..5890531d --- /dev/null +++ b/src/components/landings/StepBlock.stories.tsx @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { StepBlock } from './StepBlock'; +import { RocketIcon, StarIcon, CheckIcon } from '@primer/octicons-react'; + +const meta = { + title: 'Datalayer/Landings/StepBlock', + component: StepBlock, + tags: ['autodocs'], + parameters: { + layout: 'padded', + }, + argTypes: { + date: { + control: 'text', + description: 'Date or step number', + }, + title: { + control: 'text', + description: 'Title of the step', + }, + description: { + control: 'text', + description: 'Description of the step (supports HTML)', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + date: 'Step 1', + title: 'Launch Your Project', + description: + 'Create a new project and start building amazing applications with our platform.', + StepIcon: RocketIcon, + }, +}; + +export const WithHTMLDescription: Story = { + args: { + date: 'Step 2', + title: 'Configure Settings', + description: + 'Set up your project configuration and customize it according to your needs. You can also add integrations and API keys.', + StepIcon: StarIcon, + }, +}; + +export const CompletedStep: Story = { + args: { + date: 'Step 3', + title: 'Deploy & Monitor', + description: + 'Deploy your application to production and monitor its performance using our comprehensive dashboard.', + StepIcon: CheckIcon, + }, +}; + +export const DateFormat: Story = { + args: { + date: 'Jan 2024', + title: 'Feature Launch', + description: + 'We launched this amazing feature that revolutionizes how you work with data.', + StepIcon: RocketIcon, + }, +}; diff --git a/src/components/navbar/NavigationVisbilityObserver.stories.tsx b/src/components/navbar/NavigationVisbilityObserver.stories.tsx new file mode 100644 index 00000000..d1616545 --- /dev/null +++ b/src/components/navbar/NavigationVisbilityObserver.stories.tsx @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; +import React from 'react'; + +import { NavigationVisbilityObserver } from './NavigationVisbilityObserver'; + +const meta = { + title: 'Components/Navbar/NavigationVisbilityObserver', + component: NavigationVisbilityObserver, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, + argTypes: { + className: { + control: 'text', + description: 'CSS class name to apply to the component', + }, + children: { + control: 'object', + description: 'Child navigation items to observe for visibility', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const mockNavItems = [ + React.createElement( + 'li', + { key: 1, 'data-navitemid': 'home', className: 'nav-item' }, + React.createElement('a', { href: '#home' }, 'Home'), + ), + React.createElement( + 'li', + { key: 2, 'data-navitemid': 'about', className: 'nav-item' }, + React.createElement('a', { href: '#about' }, 'About'), + ), + React.createElement( + 'li', + { key: 3, 'data-navitemid': 'services', className: 'nav-item' }, + React.createElement('a', { href: '#services' }, 'Services'), + ), + React.createElement( + 'li', + { key: 4, 'data-navitemid': 'contact', className: 'nav-item' }, + React.createElement('a', { href: '#contact' }, 'Contact'), + ), +]; + +export const Default: Story = { + args: { + children: mockNavItems, + className: '', + }, +}; + +export const WithCustomClass: Story = { + args: { + children: mockNavItems, + className: 'custom-navigation-observer', + }, +}; + +export const MinimalItems: Story = { + args: { + children: [ + React.createElement( + 'li', + { key: 1, 'data-navitemid': 'home', className: 'nav-item' }, + React.createElement('a', { href: '#home' }, 'Home'), + ), + React.createElement( + 'li', + { key: 2, 'data-navitemid': 'about', className: 'nav-item' }, + React.createElement('a', { href: '#about' }, 'About'), + ), + ], + className: '', + }, +}; + +export const ManyItems: Story = { + args: { + children: [ + ...mockNavItems, + React.createElement( + 'li', + { key: 5, 'data-navitemid': 'blog', className: 'nav-item' }, + React.createElement('a', { href: '#blog' }, 'Blog'), + ), + React.createElement( + 'li', + { key: 6, 'data-navitemid': 'portfolio', className: 'nav-item' }, + React.createElement('a', { href: '#portfolio' }, 'Portfolio'), + ), + React.createElement( + 'li', + { key: 7, 'data-navitemid': 'testimonials', className: 'nav-item' }, + React.createElement('a', { href: '#testimonials' }, 'Testimonials'), + ), + React.createElement( + 'li', + { key: 8, 'data-navitemid': 'careers', className: 'nav-item' }, + React.createElement('a', { href: '#careers' }, 'Careers'), + ), + ], + className: '', + }, +}; diff --git a/src/components/navbar/SubdomainNavBar.stories.tsx b/src/components/navbar/SubdomainNavBar.stories.tsx new file mode 100644 index 00000000..56691924 --- /dev/null +++ b/src/components/navbar/SubdomainNavBar.stories.tsx @@ -0,0 +1,268 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { useState, useRef } from 'react'; + +import { SubdomainNavBar } from './SubdomainNavBar'; + +const meta = { + title: 'Datalayer/NavBar/SubdomainNavBar', + component: SubdomainNavBar, + tags: ['autodocs'], + parameters: { + layout: 'fullscreen', + mockAddonConfigs: { + globalMockData: [ + { + path: '../../state', + default: { + useCoreStore: () => ({ + configuration: { + brand: { + logoUrl: 'https://datalayer.io/favicon.ico', + }, + }, + }), + useRunStore: () => ({ + isDev: false, + }), + }, + }, + { + path: '../../hooks', + default: { + useOnClickOutside: () => {}, + useFocusTrap: () => {}, + useKeyboardEscape: () => {}, + useWindowSize: () => ({ isMedium: true }), + useNavigate: () => (path: string, e?: Event) => { + if (e) e.preventDefault(); + console.log('Navigate to:', path); + }, + useId: (prefix: string) => + `${prefix}-${Math.random().toString(36).slice(2)}`, + }, + }, + ], + }, + }, + argTypes: { + title: { + control: 'text', + description: 'The title or name of the subdomain', + }, + fixed: { + control: 'boolean', + description: 'Fixes the navigation bar to the top of the viewport', + }, + fullWidth: { + control: 'boolean', + description: 'Fill the maximum width of the parent container', + }, + titleHref: { + control: 'text', + description: 'The URL for the site title', + }, + logoHref: { + control: 'text', + description: 'The URL for the logo', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + title: 'Datalayer Platform', + fixed: true, + fullWidth: false, + }, +}; + +export const WithLinks: Story = { + args: { + title: 'Datalayer Platform', + fixed: true, + fullWidth: false, + }, + render: args => ( + + Dashboard + Notebooks + + Environments + + + Documentation + + + ), +}; + +export const WithActions: Story = { + args: { + title: 'Datalayer Platform', + fixed: true, + fullWidth: false, + }, + render: args => ( + + Dashboard + Notebooks + console.log('Primary action clicked')} + > + Get Started + + console.log('Secondary action clicked')} + > + Learn More + + + ), +}; + +const SearchExample = args => { + const [searchTerm, setSearchTerm] = useState(''); + const [searchResults, setSearchResults] = useState([]); + const searchInputRef = useRef(null); + + const handleSearchSubmit = e => { + e.preventDefault(); + // Simulate search results + const mockResults = [ + { + title: 'Getting Started with Notebooks', + description: 'Learn how to create and run your first notebook', + url: '/docs/notebooks', + date: '2024-01-15', + category: 'Documentation', + }, + { + title: 'Environment Setup Guide', + description: 'Configure your development environment', + url: '/docs/environments', + date: '2024-01-10', + category: 'Guides', + }, + ]; + setSearchResults(searchTerm ? mockResults : []); + }; + + const handleSearchChange = e => { + const value = e.target.value; + setSearchTerm(value); + // Simulate real-time search + if (value.length > 2) { + setSearchResults([ + { + title: `Results for "${value}"`, + description: 'Sample search result description', + url: `/search?q=${value}`, + date: '2024-01-20', + category: 'Search', + }, + ]); + } else { + setSearchResults([]); + } + }; + + return ( + + Dashboard + Notebooks + + Sign In + + ); +}; + +export const WithSearch: Story = { + args: { + title: 'Datalayer Platform', + fixed: true, + fullWidth: false, + }, + render: SearchExample, +}; + +export const DevMode: Story = { + args: { + title: 'Datalayer Platform', + fixed: true, + fullWidth: false, + }, + parameters: { + mockAddonConfigs: { + globalMockData: [ + { + path: '../../state', + default: { + useCoreStore: () => ({ + configuration: { + brand: { + logoUrl: 'https://datalayer.io/favicon.ico', + }, + }, + }), + useRunStore: () => ({ + isDev: true, // Enable dev mode + }), + }, + }, + { + path: '../../hooks', + default: { + useOnClickOutside: () => {}, + useFocusTrap: () => {}, + useKeyboardEscape: () => {}, + useWindowSize: () => ({ isMedium: true }), + useNavigate: () => (path: string, e?: Event) => { + if (e) e.preventDefault(); + console.log('Navigate to:', path); + }, + useId: (prefix: string) => + `${prefix}-${Math.random().toString(36).slice(2)}`, + }, + }, + ], + }, + }, + render: args => ( + + Dashboard + Notebooks + + ), +}; + +export const FullWidth: Story = { + args: { + title: 'Datalayer Platform', + fixed: false, + fullWidth: true, + }, + render: args => ( + + Dashboard + Notebooks + + Environments + + Get Started + + ), +}; diff --git a/src/components/nbgrader/NbGradesDetails.stories.tsx b/src/components/nbgrader/NbGradesDetails.stories.tsx new file mode 100644 index 00000000..83048ee7 --- /dev/null +++ b/src/components/nbgrader/NbGradesDetails.stories.tsx @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { NbGradesDetails } from './NbGradesDetails'; +import { IStudentItem } from '../../models'; + +const meta = { + title: 'Datalayer/NBGrader/NbGradesDetails', + component: NbGradesDetails, + tags: ['autodocs'], + parameters: { + layout: 'centered', + mockAddonConfigs: { + globalMockData: [ + { + path: '@primer/react', + default: { + Heading: ({ children, sx, ...props }) => ( +

+ {children} +

+ ), + Text: ({ children, sx, ...props }) => ( + + {children} + + ), + }, + }, + { + path: '@datalayer/primer-addons', + default: { + Box: ({ children, mt, ...props }) => ( +
+ {children} +
+ ), + }, + }, + ], + }, + }, + argTypes: { + studentItem: { + description: 'Student item data containing grades information', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const mockStudentItemWithGrades: IStudentItem = { + id: '1', + type: 'student_item', + itemId: 'assignment-1', + itemType: 'assignment', + nbgrades: [ + { + grade_id_s: 'Problem 1', + score_f: 8.5, + }, + { + grade_id_s: 'Problem 2', + score_f: 6.0, + }, + { + grade_id_s: 'Problem 3', + score_f: 9.2, + }, + { + grade_id_s: 'Problem 4', + score_f: 7.8, + }, + ], + nbgradesTotalScore: 31.5, + nbgradesTotalPoints: 40, + completed: true, + pass: true, +}; + +const mockStudentItemWithoutGrades: IStudentItem = { + id: '2', + type: 'student_item', + itemId: 'assignment-2', + itemType: 'assignment', + completed: false, +}; + +export const WithGrades: Story = { + args: { + studentItem: mockStudentItemWithGrades, + }, +}; + +export const WithoutGrades: Story = { + args: { + studentItem: mockStudentItemWithoutGrades, + }, +}; + +export const NoStudentItem: Story = { + args: { + studentItem: undefined, + }, +}; + +export const PartialGrades: Story = { + args: { + studentItem: { + id: '3', + type: 'student_item', + itemId: 'assignment-3', + itemType: 'assignment', + nbgrades: [ + { + grade_id_s: 'Problem 1', + score_f: 10.0, + }, + { + grade_id_s: 'Problem 2', + score_f: 5.5, + }, + ], + nbgradesTotalScore: 15.5, + nbgradesTotalPoints: 25, + completed: true, + pass: false, + }, + }, +}; + +export const PerfectScore: Story = { + args: { + studentItem: { + id: '4', + type: 'student_item', + itemId: 'assignment-4', + itemType: 'assignment', + nbgrades: [ + { + grade_id_s: 'Problem 1', + score_f: 10.0, + }, + { + grade_id_s: 'Problem 2', + score_f: 10.0, + }, + { + grade_id_s: 'Problem 3', + score_f: 10.0, + }, + ], + nbgradesTotalScore: 30.0, + nbgradesTotalPoints: 30, + completed: true, + pass: true, + }, + }, +}; diff --git a/src/stories/Cell.stories.tsx b/src/components/notebooks/Cell.stories.tsx similarity index 96% rename from src/stories/Cell.stories.tsx rename to src/components/notebooks/Cell.stories.tsx index bf2ea2e5..b5a7b94d 100644 --- a/src/stories/Cell.stories.tsx +++ b/src/components/notebooks/Cell.stories.tsx @@ -1,3 +1,8 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + /* * Copyright (c) 2021-2023 Datalayer, Inc. * diff --git a/src/components/notebooks/JupyterNotebook.stories.tsx b/src/components/notebooks/JupyterNotebook.stories.tsx new file mode 100644 index 00000000..1e752391 --- /dev/null +++ b/src/components/notebooks/JupyterNotebook.stories.tsx @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { BoxPanel } from '@lumino/widgets'; + +import { JupyterNotebook } from './JupyterNotebook'; + +// Mock BoxPanel for Storybook +const createMockBoxPanel = () => { + const boxPanel = new BoxPanel(); + boxPanel.title.label = 'Mock Notebook'; + return boxPanel; +}; + +const meta = { + title: 'Datalayer/Notebooks/JupyterNotebook', + component: JupyterNotebook, + tags: ['autodocs'], + parameters: { + layout: 'fullscreen', + mockAddonConfigs: { + globalMockData: [ + { + path: '@datalayer/primer-addons', + default: { + Box: ({ children, ...props }) =>
{children}
, + }, + }, + { + path: '@datalayer/jupyter-react', + default: { + Lumino: ({ children }) => ( +
+ Mock Jupyter Notebook Panel:{' '} + {children?.title?.label || 'Untitled'} +
+ ), + }, + }, + ], + }, + }, + argTypes: { + height: { + control: 'text', + description: 'Height of the notebook component', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + boxPanel: createMockBoxPanel(), + height: '100%', + }, +}; + +export const FixedHeight: Story = { + args: { + boxPanel: createMockBoxPanel(), + height: '500px', + }, +}; + +export const CustomHeight: Story = { + args: { + boxPanel: createMockBoxPanel(), + height: '300px', + }, +}; + +export const TallNotebook: Story = { + args: { + boxPanel: createMockBoxPanel(), + height: '800px', + }, +}; + +export const CompactNotebook: Story = { + args: { + boxPanel: createMockBoxPanel(), + height: '200px', + }, +}; diff --git a/src/components/notebooks/JupyterNotebookToolbar.stories.tsx b/src/components/notebooks/JupyterNotebookToolbar.stories.tsx new file mode 100644 index 00000000..e06f3cd1 --- /dev/null +++ b/src/components/notebooks/JupyterNotebookToolbar.stories.tsx @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { JupyterNotebookToolbar } from './JupyterNotebookToolbar'; + +const meta = { + title: 'Datalayer/Notebooks/JupyterNotebookToolbar', + component: JupyterNotebookToolbar, + tags: ['autodocs'], + parameters: { + layout: 'centered', + mockAddonConfigs: { + globalMockData: [ + { + path: '@datalayer/icons-react', + default: { + CircleCurrentColorIcon: ({ style, ...props }) => ( +
+ ), + CircleGreenIcon: props => ( +
+ ), + CircleOrangeIcon: props => ( +
+ ), + }, + }, + { + path: '@primer/react', + default: { + Text: ({ children, ...props }) => ( + {children} + ), + }, + }, + { + path: '@datalayer/primer-addons', + default: { + Box: ({ children, sx, ...props }) => ( +
+ {children} +
+ ), + }, + }, + { + path: 'react-sparklines', + default: { + Sparklines: ({ children, data }) => ( + + + Sparkline ({data?.length || 0} points) + + {children} + + ), + SparklinesBars: () => null, + SparklinesLine: () => null, + SparklinesReferenceLine: () => null, + }, + }, + { + path: '../../theme', + default: { + DatalayerThemeProvider: ({ children }) => children, + }, + }, + ], + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const WithMockData: Story = { + parameters: { + docs: { + description: { + story: + 'Shows the notebook toolbar with mock sparkline data and cell status indicators.', + }, + }, + }, +}; diff --git a/src/components/primer/Helper.stories.tsx b/src/components/primer/Helper.stories.tsx new file mode 100644 index 00000000..4350d4b6 --- /dev/null +++ b/src/components/primer/Helper.stories.tsx @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { Container, RedlineBackground } from './Helper'; + +const meta = { + title: 'Datalayer/Primer/Helper', + component: Container, + tags: ['autodocs'], + parameters: { + layout: 'fullscreen', + docs: { + description: { + component: + 'Helper components for layout and debugging in Primer components.', + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const ContainerDefault: Story = { + args: { + children: ( +
+

Container Content

+

This content is centered within a max-width container of 1024px.

+
+ ), + }, +}; + +export const ContainerWithCustomStyle: Story = { + args: { + children: ( +
+

Custom Styled Container

+

Container with custom background and styling.

+
+ ), + style: { + background: '#ffffff', + padding: '1rem', + border: '2px solid #0366d6', + borderRadius: '8px', + }, + }, +}; + +export const ContainerWithMultipleChildren: Story = { + args: { + children: [ +
+

First Child

+

This is the first child element.

+
, +
+

Second Child

+

This is the second child element.

+
, + ], + }, +}; + +const RedlineBackgroundMeta = { + title: 'Datalayer/Primer/RedlineBackground', + component: RedlineBackground, + tags: ['autodocs'], + parameters: { + layout: 'centered', + docs: { + description: { + component: + 'A diagnostic component that shows a red checkerboard background pattern, useful for debugging layouts.', + }, + }, + }, + argTypes: { + height: { + control: 'number', + description: 'Height of the redline background in pixels', + }, + hasBorder: { + control: 'boolean', + description: 'Whether to show a border around the component', + }, + }, +} satisfies Meta; + +export const RedlineBackgroundDefault: StoryObj = + { + args: { + height: 200, + hasBorder: true, + children: ( +
+ Content over redline background +
+ ), + }, + }; + +export const RedlineBackgroundNoBorder: StoryObj = + { + args: { + height: 150, + hasBorder: false, + children: ( +
+ No border redline background +
+ ), + }, + }; + +export const RedlineBackgroundTall: StoryObj = { + args: { + height: 400, + hasBorder: true, + children: ( +
+

Tall Redline Background

+

+ This demonstrates a taller redline background for debugging larger + layouts. +

+
+ ), + }, +}; + +export const RedlineBackgroundEmpty: StoryObj = { + args: { + height: 100, + hasBorder: true, + }, +}; diff --git a/src/components/primer/Portals.stories.tsx b/src/components/primer/Portals.stories.tsx new file mode 100644 index 00000000..6e64b647 --- /dev/null +++ b/src/components/primer/Portals.stories.tsx @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { useEffect } from 'react'; + +import setupPrimerPortals from './Portals'; + +// This component doesn't render anything visual but sets up portals +const PortalsDemo = () => { + useEffect(() => { + setupPrimerPortals(); + }, []); + + return ( +
+

Primer Portals Setup

+

This component sets up the Primer portal root for React components.

+

+ Check the DOM to see the portal root has been configured on the body + element. +

+
+ Portal Configuration: +
    +
  • Portal root ID: __primerPortalRoot__
  • +
  • Color mode: light
  • +
  • Light theme: light
  • +
  • Dark theme: dark
  • +
+
+
+ ); +}; + +const meta = { + title: 'Datalayer/Primer/Portals', + component: PortalsDemo, + tags: ['autodocs'], + parameters: { + layout: 'centered', + docs: { + description: { + component: + 'Sets up the Primer portal root for React components. This is a utility function that configures the DOM for Primer portals.', + }, + }, + mockAddonConfigs: { + globalMockData: [ + { + path: '@primer/react', + default: { + registerPortalRoot: element => { + console.log('Registered portal root:', element); + }, + }, + }, + ], + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const SetupExample: Story = { + parameters: { + docs: { + description: { + story: + 'Demonstrates the setup of Primer portals. The function configures the DOM body element as the portal root.', + }, + }, + }, +}; diff --git a/src/components/primer/Styles.stories.tsx b/src/components/primer/Styles.stories.tsx new file mode 100644 index 00000000..26a4d6a9 --- /dev/null +++ b/src/components/primer/Styles.stories.tsx @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2023-2025 Datalayer, Inc. + * Distributed under the terms of the Modified BSD License. + */ + +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { Styles } from './Styles'; + +const meta = { + title: 'Datalayer/Primer/Styles', + component: Styles, + tags: ['autodocs'], + parameters: { + layout: 'centered', + docs: { + description: { + component: + 'Provides Primer theme and base styles using JupyterLab theme configuration.', + }, + }, + mockAddonConfigs: { + globalMockData: [ + { + path: '@primer/react', + default: { + ThemeProvider: ({ children, theme }) => ( +
+ Theme: {theme?.name || 'JupyterLab'} + {children} +
+ ), + BaseStyles: () => ( +