diff --git a/README.md b/README.md index 7bd32ba7..69556690 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![npm](https://img.shields.io/npm/v/hyperparam)](https://www.npmjs.com/package/hyperparam) [![workflow status](https://github.com/hyparam/hyperparam-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/hyparam/hyperparam-cli/actions) [![mit license](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) -![coverage](https://img.shields.io/badge/Coverage-35-darkred) +![coverage](https://img.shields.io/badge/Coverage-51-darkred) This is the hyperparam cli tool. diff --git a/package.json b/package.json index a8cf8851..2afd16b1 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,8 @@ "build": "run-s build:lib build:types build:app", "coverage": "vitest run -c vite.lib.config.js --coverage --coverage.include=src --coverage.include=bin", "dev": "run-p -l watch:ts watch:vite watch:serve", - "lint": "eslint .", + "lint": "eslint", + "lint:fix": "eslint --fix", "prepublishOnly": "npm run build", "serve": "node bin/cli.js", "preserve": "npm run build", @@ -47,7 +48,7 @@ }, "dependencies": { "hightable": "0.12.1", - "hyparquet": "1.8.6", + "hyparquet": "1.8.7", "hyparquet-compressors": "1.0.0", "react": "18.3.1", "react-dom": "18.3.1" diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 7b2b64ba..a5d93d9b 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -1,5 +1,6 @@ import { ReactNode, useEffect, useState } from 'react' import { cn } from '../lib/utils.js' +import Welcome from './Welcome.js' interface LayoutProps { children: ReactNode @@ -21,10 +22,25 @@ interface LayoutProps { * @param props.title - page title */ export default function Layout({ children, className, progress, error, title }: LayoutProps) { + const [showWelcome, setShowWelcome] = useState(false) + + // Check localStorage on mount to see if the user has seen the welcome popup + useEffect(() => { + const dismissed = localStorage.getItem('welcome:dismissed') === 'true' + setShowWelcome(!dismissed) + }, []) + + // Handle closing the welcome popup + function handleCloseWelcome() { + setShowWelcome(false) + localStorage.setItem('welcome:dismissed', 'true') + } + // Update title useEffect(() => { document.title = title ? `${title} - hyperparam` : 'hyperparam' }, [title]) + return
@@ -38,6 +54,7 @@ export default function Layout({ children, className, progress, error, title }:
} + {showWelcome && }
} diff --git a/src/components/Welcome.tsx b/src/components/Welcome.tsx new file mode 100644 index 00000000..178ce0a8 --- /dev/null +++ b/src/components/Welcome.tsx @@ -0,0 +1,50 @@ +import { MouseEvent, useEffect } from 'react' + +interface WelcomePopupProps { + onClose: () => void +} + +/** + * Welcome popup component shown to first-time users. + * Clicking outside the popup or pressing Escape will dismiss it. + */ +export default function Welcome({ onClose }: WelcomePopupProps) { + // Close popup when clicking outside + function handleBackdropClick(e: MouseEvent) { + if (e.target === e.currentTarget) { + onClose() + } + } + + // Close popup when pressing Escape key + useEffect(() => { + function handleEscKey(e: KeyboardEvent) { + if (e.key === 'Escape') { + onClose() + } + } + + window.addEventListener('keydown', handleEscKey) + return () => { window.removeEventListener('keydown', handleEscKey) } + }, [onClose]) + + return ( +
+
+

npx hyperparam

+

+ This is the Hyperparam cli for local data viewing. +

+

+ This tool lets you browse and explore large datasets particularly in parquet format. +

+

+ Supported file types include Parquet, CSV, JSON, Markdown, and Text. +

+ +
+
+ ) +} diff --git a/src/styles/app.css b/src/styles/app.css index 0708e60e..a54bc87d 100644 --- a/src/styles/app.css +++ b/src/styles/app.css @@ -5,6 +5,26 @@ padding: 0; } +button { + background-color: #111; + color: #fff; + border: none; + border-radius: 8px; + padding: 8px 16px; + cursor: pointer; + outline: none; + transition: background-color 0.2s; +} +button:active, +button:focus, +button:hover { + background-color: #333; +} + +code { + font-family: monospace; +} + #app { display: flex; flex-direction: column; @@ -499,4 +519,45 @@ button.close-button:hover { width: 100%; overflow-x: auto; white-space: pre-wrap; -} \ No newline at end of file +} + +/* Welcome popup */ +.welcome { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + animation: fadeIn 0.3s ease-in; +} + +.welcome > div { + background-color: white; + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + display: flex; + flex-direction: column; + padding: 30px; + width: 450px; + max-width: 90%; +} + +.welcome h2 { + color: #342267; + margin-bottom: 16px; + font-size: 24px; +} + +.welcome p { + color: #444; + line-height: 1.5; + margin-bottom: 24px; +} + +.welcome button { + margin-left: auto; +} diff --git a/test/components/Welcome.test.tsx b/test/components/Welcome.test.tsx new file mode 100644 index 00000000..bd5e147a --- /dev/null +++ b/test/components/Welcome.test.tsx @@ -0,0 +1,68 @@ +import { fireEvent, render } from '@testing-library/react' +import React from 'react' +import { describe, expect, it, vi } from 'vitest' +import Welcome from '../../src/components/Welcome.js' + +describe('Welcome Component', () => { + it('renders welcome content', () => { + const onClose = vi.fn() + const { getByRole, getByText } = render() + + expect(getByText('npx hyperparam')).toBeDefined() + expect(getByText('Got it')).toBeDefined() + const button = getByRole('button') + expect(button).toBeDefined() + expect(button.textContent).toBe('Got it') + }) + + it('calls onClose when button is clicked', () => { + const onClose = vi.fn() + const { getByRole } = render() + + fireEvent.click(getByRole('button')) + expect(onClose).toHaveBeenCalledTimes(1) + }) + + it('calls onClose when clicking outside the popup', () => { + const onClose = vi.fn() + const { container } = render() + + // Find the backdrop element + const backdropElement = container.querySelector('.welcome') + expect(backdropElement).toBeDefined() + + if (backdropElement) { + fireEvent.click(backdropElement) + expect(onClose).toHaveBeenCalledTimes(1) + } + }) + + it('does not call onClose when clicking inside the popup', () => { + const onClose = vi.fn() + const { getByText } = render() + + // Find and click on an element inside the popup content + const paragraphElement = getByText('Supported file types include Parquet, CSV, JSON, Markdown, and Text.') + fireEvent.click(paragraphElement) + + expect(onClose).not.toHaveBeenCalled() + }) + + it('calls onClose when pressing Escape key', () => { + const onClose = vi.fn() + render() + + // Simulate pressing the Escape key + fireEvent.keyDown(window, { key: 'Escape' }) + expect(onClose).toHaveBeenCalledTimes(1) + }) + + it('does not call onClose when pressing other keys', () => { + const onClose = vi.fn() + render() + + // Simulate pressing a different key + fireEvent.keyDown(window, { key: 'Enter' }) + expect(onClose).not.toHaveBeenCalled() + }) +})