Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
557783a
move Welcome CSS to a CSS module + allow overriding
severo Mar 27, 2025
0d39fab
remove redundant declaration
severo Mar 27, 2025
90ffb0a
fix type
severo Mar 27, 2025
dbf56e2
extract SideBar to its component + CSS module
severo Mar 27, 2025
4eb2408
reverse the config keys to group all class names under customClass
severo Mar 27, 2025
7102033
style
severo Mar 27, 2025
ba03e51
move TextView styles to CSS module
severo Mar 27, 2025
3fe4514
move MarkdownView styles to CSS module
severo Mar 27, 2025
126198a
fix import paths
severo Mar 27, 2025
7e91ca2
move ImageView styles to CSS module
severo Mar 27, 2025
661247e
move ContentHeader styles to CSS module
severo Mar 27, 2025
45e4969
rename ContentHeader as ContentWrapper
severo Mar 27, 2025
b73d419
move Spinner styles to CSS module + move to a new component file
severo Mar 27, 2025
283bace
move ErrorBar styles to CSS module + new component
severo Mar 27, 2025
39cbe2d
split Layout.test.tsx in three files
severo Mar 28, 2025
0fde1db
fix the tests + use VisuallyHidden component to insert accessible text
severo Mar 28, 2025
76a1f29
extract SlideCloseButton to its own component + CSS module
severo Mar 28, 2025
131ca4f
fix ErrorBar: don't take space when hidden
severo Mar 28, 2025
7ec75af
fix class name
severo Mar 28, 2025
ed07ac9
Move styles for SlidePanel to a CSS module
severo Mar 28, 2025
e39d855
Fix SlidePanel tests, and change panel from article to aside
severo Mar 28, 2025
e7b71dd
Move the progress bar to its component + CSS module
severo Mar 28, 2025
55af56b
fix bug in the computation of path parts
severo Mar 28, 2025
65c1a83
fix again the source parts
severo Mar 28, 2025
2eb13be
Move breadcrumb styles to CSS module + move tests their own file
severo Mar 28, 2025
c501fa4
extract Layout styles to CSS Module
severo Mar 28, 2025
c68c4c5
extract Center as a new component, and move the loading spinner to Co…
severo Mar 28, 2025
a005bed
memoize the list of files
severo Mar 28, 2025
fa5882d
refactor the conditionals for safety
severo Mar 28, 2025
2c9d13d
fix the conditionals: we work on filtered, not files
severo Mar 28, 2025
49fed0d
use CSS module for Folder styles
severo Mar 28, 2025
e94cdfc
move search styles to Folder CSS module
severo Mar 28, 2025
4c98b3b
remove useless intermediate top-actions div
severo Mar 28, 2025
fc3c27a
export global CSS in the package
severo Mar 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"import": "./lib/index.es.min.js",
"require": "./lib/index.umd.min.js"
},
"./global.css": "./lib/global.css",
"./hyperparam.css": "./lib/hyperparam.css"
},
"bin": {
Expand All @@ -29,7 +30,7 @@
],
"scripts": {
"build:types": "tsc -b",
"build:lib": "vite build -c vite.lib.config.ts",
"build:lib": "vite build -c vite.lib.config.ts && cp src/styles/global.css lib/global.css",
"build:app": "vite build",
"build": "run-s build:lib build:types build:app",
"coverage": "vitest run --coverage --coverage.include=src --coverage.include=bin",
Expand Down
3 changes: 1 addition & 2 deletions src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import 'hightable/src/HighTable.css'
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './components/App.js'
import './styles/app.css'
import './styles/global.css'

const root = document.getElementById('app')
if (!root) throw new Error('missing root element')
Expand Down
8 changes: 5 additions & 3 deletions src/components/Breadcrumb.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { ReactNode } from 'react'
import { useConfig } from '../hooks/useConfig.js'
import type { Source } from '../lib/sources/types.js'
import { cn } from '../lib/utils.js'
import styles from '../styles/Breadcrumb.module.css'

interface BreadcrumbProps {
source: Source,
Expand All @@ -11,10 +13,10 @@ interface BreadcrumbProps {
* Breadcrumb navigation
*/
export default function Breadcrumb({ source, children }: BreadcrumbProps) {
const { routes } = useConfig()
const { routes, customClass } = useConfig()

return <nav className='top-header top-header-divided'>
<div className='path'>
return <nav className={cn(styles.breadcrumb, customClass?.breadcrumb)}>
<div className={cn(styles.path, customClass?.path)}>
{source.sourceParts.map((part, depth) =>
<a href={routes?.getSourceRouteUrl?.({ sourceId: part.sourceId }) ?? ''} key={depth}>{part.text}</a>
)}
Expand Down
9 changes: 9 additions & 0 deletions src/components/Center.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ReactNode } from 'react'
import { useConfig } from '../hooks/useConfig.js'
import { cn } from '../lib/utils.js'
import styles from '../styles/Center.module.css'

export default function Center({ children }: {children?: ReactNode}) {
const { customClass } = useConfig()
return <div className={cn(styles.center, customClass?.center)}>{children}</div>
}
35 changes: 35 additions & 0 deletions src/components/ErrorBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useState } from 'react'
import { useConfig } from '../hooks/useConfig.js'
import { cn } from '../lib/utils.js'
import styles from '../styles/ErrorBar.module.css'

interface ErrorBarProps {
error?: Error
}

export default function ErrorBar({ error }: ErrorBarProps) {
const [showError, setShowError] = useState(error !== undefined)
const [prevError, setPrevError] = useState(error)
const { customClass } = useConfig()

if (error) console.error(error)
/// Reset error visibility when error prop changes
if (error !== prevError) {
setPrevError(error)
setShowError(error !== undefined)
}

return <div
className={cn(styles.errorBar, customClass?.errorBar)}
data-visible={showError}
>
<div>
<span>{error?.toString()}</span>
<button
aria-label='Close error message'
onClick={() => { setShowError(false) }}>
&times;
</button>
</div>
</div>
}
64 changes: 35 additions & 29 deletions src/components/Folder.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { useEffect, useRef, useState } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useConfig } from '../hooks/useConfig.js'
import type { DirSource, FileMetadata } from '../lib/sources/types.js'
import { cn, formatFileSize, getFileDate, getFileDateShort } from '../lib/utils.js'
import styles from '../styles/Folder.module.css'
import Breadcrumb from './Breadcrumb.js'
import Layout, { Spinner } from './Layout.js'
import Center from './Center.js'
import Layout from './Layout.js'
import Spinner from './Spinner.js'

interface FolderProps {
source: DirSource
Expand All @@ -19,8 +22,7 @@ export default function Folder({ source }: FolderProps) {
const [searchQuery, setSearchQuery] = useState('')
const searchRef = useRef<HTMLInputElement>(null)
const listRef = useRef<HTMLUListElement>(null)

const { routes } = useConfig()
const { routes, customClass } = useConfig()

// Fetch files on component mount
useEffect(() => {
Expand All @@ -33,7 +35,10 @@ export default function Folder({ source }: FolderProps) {
}, [source])

// File search
const filtered = files?.filter(file => file.name.toLowerCase().includes(searchQuery.toLowerCase()))
const filtered = useMemo(() => {
return files?.filter(file => file.name.toLowerCase().includes(searchQuery.toLowerCase()))
}, [files, searchQuery])

useEffect(() => {
const searchElement = searchRef.current
function handleKeyup(e: KeyboardEvent) {
Expand Down Expand Up @@ -84,32 +89,33 @@ export default function Folder({ source }: FolderProps) {

return <Layout error={error} title={source.prefix}>
<Breadcrumb source={source}>
<div className='top-actions'>
<input autoFocus className='search' placeholder='Search...' ref={searchRef} />
</div>
<input autoFocus className={cn(styles.search, customClass?.search)} placeholder='Search...' ref={searchRef} />
</Breadcrumb>

{files && files.length > 0 && <ul className='file-list' ref={listRef}>
{filtered?.map((file, index) =>
<li key={index}>
<a href={routes?.getSourceRouteUrl?.({ sourceId: file.sourceId }) ?? location.href}>
<span className={cn('file-name', 'file', file.kind === 'directory' && 'folder')}>
{file.name}
</span>
{file.kind === 'file' && <>
{file.size !== undefined && <span className='file-size' title={file.size.toLocaleString() + ' bytes'}>
{formatFileSize(file.size)}
</span>}
<span className='file-date' title={getFileDate(file)}>
{getFileDateShort(file)}
</span>
</>}
</a>
</li>
)}
</ul>}
{files?.length === 0 && <div className='center'>No files</div>}
{files === undefined && <div className='center'><Spinner /></div>}
{filtered === undefined ?
<Center><Spinner /></Center> :
filtered.length === 0 ?
<Center>No files</Center> :
<ul className={cn(styles.fileList, customClass?.fileList)} ref={listRef}>
{filtered.map((file, index) =>
<li key={index}>
<a href={routes?.getSourceRouteUrl?.({ sourceId: file.sourceId }) ?? location.href}>
<span data-file-kind={file.kind}>
{file.name}
</span>
{file.kind === 'file' && <>
{file.size !== undefined && <span data-file-size title={file.size.toLocaleString() + ' bytes'}>
{formatFileSize(file.size)}
</span>}
<span data-file-date title={getFileDate(file)}>
{getFileDateShort(file)}
</span>
</>}
</a>
</li>
)}
</ul>
}
</Layout>
}

Expand Down
62 changes: 13 additions & 49 deletions src/components/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { ReactNode, useEffect, useState } from 'react'
import { useConfig } from '../hooks/useConfig.js'
import { cn } from '../lib/utils.js'
import styles from '../styles/Layout.module.css'
import ErrorBar from './ErrorBar.js'
import ProgressBar from './ProgressBar.js'
import SideBar from './SideBar.js'
import Welcome from './Welcome.js'

interface LayoutProps {
children: ReactNode
className?: string
progress?: number
error?: Error
title?: string
Expand All @@ -16,13 +20,13 @@ interface LayoutProps {
*
* @param props
* @param props.children - content to display inside the layout
* @param props.className - additional class names to apply to the content container
* @param props.progress - progress bar value
* @param props.error - error message to display
* @param props.title - page title
*/
export default function Layout({ children, className, progress, error, title }: LayoutProps) {
export default function Layout({ children, progress, error, title }: LayoutProps) {
const [showWelcome, setShowWelcome] = useState(false)
const { customClass } = useConfig()

// Check localStorage on mount to see if the user has seen the welcome popup
useEffect(() => {
Expand All @@ -41,55 +45,15 @@ export default function Layout({ children, className, progress, error, title }:
document.title = title ? `${title} - hyperparam` : 'hyperparam'
}, [title])

return <main className='main'>
<Sidebar />
<div className='content-container'>
<div className={cn('content', className)}>
return <div className={cn(styles.layout, customClass?.layout)}>
<SideBar />
<main>
<div>
{children}
</div>
<ErrorBar error={error}></ErrorBar>
</div>
{progress !== undefined && progress < 1 &&
<div className={'progress-bar'} role='progressbar'>
<div style={{ width: `${100 * progress}%` }} />
</div>
}
</main>
{progress !== undefined && progress < 1 && <ProgressBar value={progress} />}
{showWelcome && <Welcome onClose={handleCloseWelcome} />}
</main>
}

function Sidebar() {
return <nav className='nav'>
<div>
<a className="brand" href='/'>hyperparam</a>
</div>
</nav>
}

export function Spinner({ className }: { className?: string }) {
return <div className={cn('spinner', className)}></div>
}

export function ErrorBar({ error }: { error?: Error }) {
const [showError, setShowError] = useState(error !== undefined)
const [prevError, setPrevError] = useState(error)

if (error) console.error(error)
/// Reset error visibility when error prop changes
if (error !== prevError) {
setPrevError(error)
setShowError(error !== undefined)
}

return <div className={cn('error-bar', showError && 'show-error')}>
<div className='error-content'>
<span>{error?.toString()}</span>
<button
aria-label='Close error message'
className='close-button'
onClick={() => { setShowError(false) }}>
&times;
</button>
</div>
</div>
}
25 changes: 25 additions & 0 deletions src/components/ProgressBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useConfig } from '../hooks/useConfig'
import { cn } from '../lib/utils.js'
import styles from '../styles/ProgressBar.module.css'
import VisuallyHidden from './VisuallyHidden.js'

export default function ProgressBar({ value }: {value: number}) {
const { customClass } = useConfig()
if (value < 0 || value > 1) {
throw new Error('ProgressBar value must be between 0 and 1')
}
const roundedValue = Math.round(value * 100) / 100
const percentage = roundedValue.toLocaleString('en-US', { style: 'percent' })
return (
<div
className={cn(styles.progressBar, customClass?.progressBar)}
role='progressbar'
aria-valuenow={roundedValue}
aria-valuemin={0}
aria-valuemax={1}
>
<VisuallyHidden>{percentage}</VisuallyHidden>
<div style={{ width: percentage }} role="presentation" />
</div>
)
}
12 changes: 12 additions & 0 deletions src/components/SideBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useConfig } from '../hooks/useConfig.js'
import { cn } from '../lib/utils.js'
import styles from '../styles/SideBar.module.css'

export default function SideBar() {
const { customClass } = useConfig()
return <nav className={cn(styles.sideBar, customClass?.sideBar)}>
<div>
<a className={cn(styles.brand, customClass?.brand)} href='/'>hyperparam</a>
</div>
</nav>
}
11 changes: 11 additions & 0 deletions src/components/SlideCloseButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { MouseEventHandler } from 'react'
import { useConfig } from '../hooks/useConfig.js'
import { cn } from '../lib/utils.js'
import styles from '../styles/SlideCloseButton.module.css'

export default function SlideCloseButton({ onClick }: { onClick: MouseEventHandler<HTMLButtonElement> | undefined }) {
const { customClass } = useConfig()
return (
<button className={ cn( styles.slideClose, customClass?.slideCloseButton ) } onClick={onClick}>&nbsp;</button>
)
}
14 changes: 14 additions & 0 deletions src/components/Spinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useConfig } from '../hooks/useConfig.js'
import { cn } from '../lib/utils.js'
import styles from '../styles/Spinner.module.css'
import VisuallyHidden from './VisuallyHidden.js'

export default function Spinner({ text }: {text?: string}) {
const { customClass } = useConfig()
const spinnerText = text ?? 'Loading...'
return <div
className={cn(styles.spinner, customClass?.spinner)}
role='status'
aria-live='polite'
><VisuallyHidden>{spinnerText}</VisuallyHidden></div>
}
6 changes: 6 additions & 0 deletions src/components/VisuallyHidden.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { HTMLAttributes } from 'react'
import styles from '../styles/VisuallyHidden.module.css'

export default function VisuallyHidden({ children, ...delegated }: HTMLAttributes<HTMLElement>) {
return <div className={styles.wrapper} {...delegated}>{children}</div>
};
6 changes: 5 additions & 1 deletion src/components/Welcome.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { MouseEvent, useEffect } from 'react'
import { useConfig } from '../hooks/useConfig.js'
import { cn } from '../lib/utils.js'
import styles from '../styles/Welcome.module.css'

interface WelcomePopupProps {
onClose: () => void
Expand All @@ -9,6 +12,7 @@ interface WelcomePopupProps {
* Clicking outside the popup or pressing Escape will dismiss it.
*/
export default function Welcome({ onClose }: WelcomePopupProps) {
const { customClass } = useConfig()
// Close popup when clicking outside
function handleBackdropClick(e: MouseEvent) {
if (e.target === e.currentTarget) {
Expand All @@ -29,7 +33,7 @@ export default function Welcome({ onClose }: WelcomePopupProps) {
}, [onClose])

return (
<div className="welcome" onClick={handleBackdropClick}>
<div className={cn(styles.welcome, customClass?.welcome)} onClick={handleBackdropClick}>
<div>
<h2>npx hyperparam</h2>
<p>
Expand Down
4 changes: 3 additions & 1 deletion src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import Breadcrumb from './Breadcrumb.js'
import Cell from './Cell.js'
import ErrorBar from './ErrorBar.js'
import File from './File.js'
import Folder from './Folder.js'
import Layout, { ErrorBar, Spinner } from './Layout.js'
import Layout from './Layout.js'
import Markdown from './Markdown.js'
import Page from './Page.js'
import Spinner from './Spinner.js'
export * from './viewers/index.js'
export { Breadcrumb, Cell, ErrorBar, File, Folder, Layout, Markdown, Page, Spinner }
Loading