diff --git a/README.md b/README.md
index 7bd32ba7..69556690 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
[](https://www.npmjs.com/package/hyperparam)
[](https://github.com/hyparam/hyperparam-cli/actions)
[](https://opensource.org/licenses/MIT)
-
+
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()
+ })
+})