Skip to content

Commit dcb467a

Browse files
authored
Use CSS modules (#188)
* move Welcome CSS to a CSS module + allow overriding * remove redundant declaration * fix type * extract SideBar to its component + CSS module * reverse the config keys to group all class names under customClass * style * move TextView styles to CSS module * move MarkdownView styles to CSS module * fix import paths * move ImageView styles to CSS module * move ContentHeader styles to CSS module * rename ContentHeader as ContentWrapper * move Spinner styles to CSS module + move to a new component file * move ErrorBar styles to CSS module + new component * split Layout.test.tsx in three files * fix the tests + use VisuallyHidden component to insert accessible text * extract SlideCloseButton to its own component + CSS module * fix ErrorBar: don't take space when hidden * fix class name * Move styles for SlidePanel to a CSS module * Fix SlidePanel tests, and change panel from article to aside * Move the progress bar to its component + CSS module * fix bug in the computation of path parts * fix again the source parts * Move breadcrumb styles to CSS module + move tests their own file * extract Layout styles to CSS Module * extract Center as a new component, and move the loading spinner to ContentWrapper * memoize the list of files * refactor the conditionals for safety * fix the conditionals: we work on filtered, not files * use CSS module for Folder styles * move search styles to Folder CSS module * remove useless intermediate top-actions div * export global CSS in the package
1 parent e4ff6e8 commit dcb467a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+991
-838
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"import": "./lib/index.es.min.js",
1717
"require": "./lib/index.umd.min.js"
1818
},
19+
"./global.css": "./lib/global.css",
1920
"./hyperparam.css": "./lib/hyperparam.css"
2021
},
2122
"bin": {
@@ -29,7 +30,7 @@
2930
],
3031
"scripts": {
3132
"build:types": "tsc -b",
32-
"build:lib": "vite build -c vite.lib.config.ts",
33+
"build:lib": "vite build -c vite.lib.config.ts && cp src/styles/global.css lib/global.css",
3334
"build:app": "vite build",
3435
"build": "run-s build:lib build:types build:app",
3536
"coverage": "vitest run --coverage --coverage.include=src --coverage.include=bin",

src/app.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import 'hightable/src/HighTable.css'
21
import { StrictMode } from 'react'
32
import { createRoot } from 'react-dom/client'
43
import App from './components/App.js'
5-
import './styles/app.css'
4+
import './styles/global.css'
65

76
const root = document.getElementById('app')
87
if (!root) throw new Error('missing root element')

src/components/Breadcrumb.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type { ReactNode } from 'react'
22
import { useConfig } from '../hooks/useConfig.js'
33
import type { Source } from '../lib/sources/types.js'
4+
import { cn } from '../lib/utils.js'
5+
import styles from '../styles/Breadcrumb.module.css'
46

57
interface BreadcrumbProps {
68
source: Source,
@@ -11,10 +13,10 @@ interface BreadcrumbProps {
1113
* Breadcrumb navigation
1214
*/
1315
export default function Breadcrumb({ source, children }: BreadcrumbProps) {
14-
const { routes } = useConfig()
16+
const { routes, customClass } = useConfig()
1517

16-
return <nav className='top-header top-header-divided'>
17-
<div className='path'>
18+
return <nav className={cn(styles.breadcrumb, customClass?.breadcrumb)}>
19+
<div className={cn(styles.path, customClass?.path)}>
1820
{source.sourceParts.map((part, depth) =>
1921
<a href={routes?.getSourceRouteUrl?.({ sourceId: part.sourceId }) ?? ''} key={depth}>{part.text}</a>
2022
)}

src/components/Center.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { ReactNode } from 'react'
2+
import { useConfig } from '../hooks/useConfig.js'
3+
import { cn } from '../lib/utils.js'
4+
import styles from '../styles/Center.module.css'
5+
6+
export default function Center({ children }: {children?: ReactNode}) {
7+
const { customClass } = useConfig()
8+
return <div className={cn(styles.center, customClass?.center)}>{children}</div>
9+
}

src/components/ErrorBar.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { useState } from 'react'
2+
import { useConfig } from '../hooks/useConfig.js'
3+
import { cn } from '../lib/utils.js'
4+
import styles from '../styles/ErrorBar.module.css'
5+
6+
interface ErrorBarProps {
7+
error?: Error
8+
}
9+
10+
export default function ErrorBar({ error }: ErrorBarProps) {
11+
const [showError, setShowError] = useState(error !== undefined)
12+
const [prevError, setPrevError] = useState(error)
13+
const { customClass } = useConfig()
14+
15+
if (error) console.error(error)
16+
/// Reset error visibility when error prop changes
17+
if (error !== prevError) {
18+
setPrevError(error)
19+
setShowError(error !== undefined)
20+
}
21+
22+
return <div
23+
className={cn(styles.errorBar, customClass?.errorBar)}
24+
data-visible={showError}
25+
>
26+
<div>
27+
<span>{error?.toString()}</span>
28+
<button
29+
aria-label='Close error message'
30+
onClick={() => { setShowError(false) }}>
31+
&times;
32+
</button>
33+
</div>
34+
</div>
35+
}

src/components/Folder.tsx

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
import { useEffect, useRef, useState } from 'react'
1+
import { useEffect, useMemo, useRef, useState } from 'react'
22
import { useConfig } from '../hooks/useConfig.js'
33
import type { DirSource, FileMetadata } from '../lib/sources/types.js'
44
import { cn, formatFileSize, getFileDate, getFileDateShort } from '../lib/utils.js'
5+
import styles from '../styles/Folder.module.css'
56
import Breadcrumb from './Breadcrumb.js'
6-
import Layout, { Spinner } from './Layout.js'
7+
import Center from './Center.js'
8+
import Layout from './Layout.js'
9+
import Spinner from './Spinner.js'
710

811
interface FolderProps {
912
source: DirSource
@@ -19,8 +22,7 @@ export default function Folder({ source }: FolderProps) {
1922
const [searchQuery, setSearchQuery] = useState('')
2023
const searchRef = useRef<HTMLInputElement>(null)
2124
const listRef = useRef<HTMLUListElement>(null)
22-
23-
const { routes } = useConfig()
25+
const { routes, customClass } = useConfig()
2426

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

3537
// File search
36-
const filtered = files?.filter(file => file.name.toLowerCase().includes(searchQuery.toLowerCase()))
38+
const filtered = useMemo(() => {
39+
return files?.filter(file => file.name.toLowerCase().includes(searchQuery.toLowerCase()))
40+
}, [files, searchQuery])
41+
3742
useEffect(() => {
3843
const searchElement = searchRef.current
3944
function handleKeyup(e: KeyboardEvent) {
@@ -84,32 +89,33 @@ export default function Folder({ source }: FolderProps) {
8489

8590
return <Layout error={error} title={source.prefix}>
8691
<Breadcrumb source={source}>
87-
<div className='top-actions'>
88-
<input autoFocus className='search' placeholder='Search...' ref={searchRef} />
89-
</div>
92+
<input autoFocus className={cn(styles.search, customClass?.search)} placeholder='Search...' ref={searchRef} />
9093
</Breadcrumb>
9194

92-
{files && files.length > 0 && <ul className='file-list' ref={listRef}>
93-
{filtered?.map((file, index) =>
94-
<li key={index}>
95-
<a href={routes?.getSourceRouteUrl?.({ sourceId: file.sourceId }) ?? location.href}>
96-
<span className={cn('file-name', 'file', file.kind === 'directory' && 'folder')}>
97-
{file.name}
98-
</span>
99-
{file.kind === 'file' && <>
100-
{file.size !== undefined && <span className='file-size' title={file.size.toLocaleString() + ' bytes'}>
101-
{formatFileSize(file.size)}
102-
</span>}
103-
<span className='file-date' title={getFileDate(file)}>
104-
{getFileDateShort(file)}
105-
</span>
106-
</>}
107-
</a>
108-
</li>
109-
)}
110-
</ul>}
111-
{files?.length === 0 && <div className='center'>No files</div>}
112-
{files === undefined && <div className='center'><Spinner /></div>}
95+
{filtered === undefined ?
96+
<Center><Spinner /></Center> :
97+
filtered.length === 0 ?
98+
<Center>No files</Center> :
99+
<ul className={cn(styles.fileList, customClass?.fileList)} ref={listRef}>
100+
{filtered.map((file, index) =>
101+
<li key={index}>
102+
<a href={routes?.getSourceRouteUrl?.({ sourceId: file.sourceId }) ?? location.href}>
103+
<span data-file-kind={file.kind}>
104+
{file.name}
105+
</span>
106+
{file.kind === 'file' && <>
107+
{file.size !== undefined && <span data-file-size title={file.size.toLocaleString() + ' bytes'}>
108+
{formatFileSize(file.size)}
109+
</span>}
110+
<span data-file-date title={getFileDate(file)}>
111+
{getFileDateShort(file)}
112+
</span>
113+
</>}
114+
</a>
115+
</li>
116+
)}
117+
</ul>
118+
}
113119
</Layout>
114120
}
115121

src/components/Layout.tsx

Lines changed: 13 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import { ReactNode, useEffect, useState } from 'react'
2+
import { useConfig } from '../hooks/useConfig.js'
23
import { cn } from '../lib/utils.js'
4+
import styles from '../styles/Layout.module.css'
5+
import ErrorBar from './ErrorBar.js'
6+
import ProgressBar from './ProgressBar.js'
7+
import SideBar from './SideBar.js'
38
import Welcome from './Welcome.js'
49

510
interface LayoutProps {
611
children: ReactNode
7-
className?: string
812
progress?: number
913
error?: Error
1014
title?: string
@@ -16,13 +20,13 @@ interface LayoutProps {
1620
*
1721
* @param props
1822
* @param props.children - content to display inside the layout
19-
* @param props.className - additional class names to apply to the content container
2023
* @param props.progress - progress bar value
2124
* @param props.error - error message to display
2225
* @param props.title - page title
2326
*/
24-
export default function Layout({ children, className, progress, error, title }: LayoutProps) {
27+
export default function Layout({ children, progress, error, title }: LayoutProps) {
2528
const [showWelcome, setShowWelcome] = useState(false)
29+
const { customClass } = useConfig()
2630

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

44-
return <main className='main'>
45-
<Sidebar />
46-
<div className='content-container'>
47-
<div className={cn('content', className)}>
48+
return <div className={cn(styles.layout, customClass?.layout)}>
49+
<SideBar />
50+
<main>
51+
<div>
4852
{children}
4953
</div>
5054
<ErrorBar error={error}></ErrorBar>
51-
</div>
52-
{progress !== undefined && progress < 1 &&
53-
<div className={'progress-bar'} role='progressbar'>
54-
<div style={{ width: `${100 * progress}%` }} />
55-
</div>
56-
}
55+
</main>
56+
{progress !== undefined && progress < 1 && <ProgressBar value={progress} />}
5757
{showWelcome && <Welcome onClose={handleCloseWelcome} />}
58-
</main>
59-
}
60-
61-
function Sidebar() {
62-
return <nav className='nav'>
63-
<div>
64-
<a className="brand" href='/'>hyperparam</a>
65-
</div>
66-
</nav>
67-
}
68-
69-
export function Spinner({ className }: { className?: string }) {
70-
return <div className={cn('spinner', className)}></div>
71-
}
72-
73-
export function ErrorBar({ error }: { error?: Error }) {
74-
const [showError, setShowError] = useState(error !== undefined)
75-
const [prevError, setPrevError] = useState(error)
76-
77-
if (error) console.error(error)
78-
/// Reset error visibility when error prop changes
79-
if (error !== prevError) {
80-
setPrevError(error)
81-
setShowError(error !== undefined)
82-
}
83-
84-
return <div className={cn('error-bar', showError && 'show-error')}>
85-
<div className='error-content'>
86-
<span>{error?.toString()}</span>
87-
<button
88-
aria-label='Close error message'
89-
className='close-button'
90-
onClick={() => { setShowError(false) }}>
91-
&times;
92-
</button>
93-
</div>
9458
</div>
9559
}

src/components/ProgressBar.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { useConfig } from '../hooks/useConfig'
2+
import { cn } from '../lib/utils.js'
3+
import styles from '../styles/ProgressBar.module.css'
4+
import VisuallyHidden from './VisuallyHidden.js'
5+
6+
export default function ProgressBar({ value }: {value: number}) {
7+
const { customClass } = useConfig()
8+
if (value < 0 || value > 1) {
9+
throw new Error('ProgressBar value must be between 0 and 1')
10+
}
11+
const roundedValue = Math.round(value * 100) / 100
12+
const percentage = roundedValue.toLocaleString('en-US', { style: 'percent' })
13+
return (
14+
<div
15+
className={cn(styles.progressBar, customClass?.progressBar)}
16+
role='progressbar'
17+
aria-valuenow={roundedValue}
18+
aria-valuemin={0}
19+
aria-valuemax={1}
20+
>
21+
<VisuallyHidden>{percentage}</VisuallyHidden>
22+
<div style={{ width: percentage }} role="presentation" />
23+
</div>
24+
)
25+
}

src/components/SideBar.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { useConfig } from '../hooks/useConfig.js'
2+
import { cn } from '../lib/utils.js'
3+
import styles from '../styles/SideBar.module.css'
4+
5+
export default function SideBar() {
6+
const { customClass } = useConfig()
7+
return <nav className={cn(styles.sideBar, customClass?.sideBar)}>
8+
<div>
9+
<a className={cn(styles.brand, customClass?.brand)} href='/'>hyperparam</a>
10+
</div>
11+
</nav>
12+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { MouseEventHandler } from 'react'
2+
import { useConfig } from '../hooks/useConfig.js'
3+
import { cn } from '../lib/utils.js'
4+
import styles from '../styles/SlideCloseButton.module.css'
5+
6+
export default function SlideCloseButton({ onClick }: { onClick: MouseEventHandler<HTMLButtonElement> | undefined }) {
7+
const { customClass } = useConfig()
8+
return (
9+
<button className={ cn( styles.slideClose, customClass?.slideCloseButton ) } onClick={onClick}>&nbsp;</button>
10+
)
11+
}

0 commit comments

Comments
 (0)