new spinner

This commit is contained in:
Tickbase
2026-07-11 12:18:45 +02:00
parent f767d4d99e
commit 86061c72e7
7 changed files with 48 additions and 35 deletions
+2 -1
View File
@@ -1,4 +1,5 @@
import { FC, ButtonHTMLAttributes } from 'react' import { FC, ButtonHTMLAttributes } from 'react'
import Spinner from '@/components/common/Spinner'
export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'success' | 'warning' export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'success' | 'warning'
export type ButtonSize = 'small' | 'medium' | 'large' export type ButtonSize = 'small' | 'medium' | 'large'
@@ -58,7 +59,7 @@ const Button: FC<ButtonProps> = ({
> >
{isLoading && ( {isLoading && (
<span className="btn-spinner"> <span className="btn-spinner">
<span className="spinner"></span> <Spinner />
</span> </span>
)} )}
+6 -19
View File
@@ -1,25 +1,25 @@
import { ReactNode } from 'react' import { ReactNode } from 'react'
import Spinner from './Spinner'
export type LoadingType = 'spinner' | 'dots' | 'progress' export type LoadingType = 'spinner' | 'dots'
export type LoadingSize = 'small' | 'medium' | 'large' export type LoadingSize = 'small' | 'medium' | 'large'
interface LoadingIndicatorProps { interface LoadingIndicatorProps {
size?: LoadingSize size?: LoadingSize
type?: LoadingType type?: LoadingType
message?: string message?: string
progress?: number
className?: string className?: string
} }
/** /**
* Versatile loading indicator component * Versatile loading indicator component
* Supports multiple visual styles and sizes * Supports multiple visual styles and sizes.
* For a progress bar, use ProgressBar or ProgressDialog's progress bar instead.
*/ */
const LoadingIndicator = ({ const LoadingIndicator = ({
size = 'medium', size = 'medium',
type = 'spinner', type = 'spinner',
message, message,
progress = 0,
className = '', className = '',
}: LoadingIndicatorProps) => { }: LoadingIndicatorProps) => {
// Size class mapping // Size class mapping
@@ -33,7 +33,7 @@ const LoadingIndicator = ({
const renderLoadingIndicator = (): ReactNode => { const renderLoadingIndicator = (): ReactNode => {
switch (type) { switch (type) {
case 'spinner': case 'spinner':
return <div className="loading-spinner"></div> return <Spinner />
case 'dots': case 'dots':
return ( return (
@@ -44,21 +44,8 @@ const LoadingIndicator = ({
</div> </div>
) )
case 'progress':
return (
<div className="loading-progress">
<div className="progress-bar-container">
<div
className="progress-bar"
style={{ width: `${Math.min(Math.max(progress, 0), 100)}%` }}
></div>
</div>
{progress > 0 && <div className="progress-percentage">{Math.round(progress)}%</div>}
</div>
)
default: default:
return <div className="loading-spinner"></div> return <Spinner />
} }
} }
+17
View File
@@ -0,0 +1,17 @@
interface SpinnerProps {
className?: string
}
/**
* Windows/Fluent-style indeterminate ring spinner an SVG circle whose
* stroke-dasharray and rotation are animated (see .spinner-ring in
* _loading.scss), rather than a CSS conic-gradient mask. Size and color
* come from whatever wrapping className/context is passed in.
*/
const Spinner = ({ className = '' }: SpinnerProps) => (
<svg viewBox="0 0 16 16" className={`spinner-ring ${className}`}>
<circle cx="8" cy="8" r="7" />
</svg>
)
export default Spinner
+4 -1
View File
@@ -2,7 +2,10 @@ export { default as LoadingIndicator } from './LoadingIndicator'
export { default as ProgressBar } from './ProgressBar' export { default as ProgressBar } from './ProgressBar'
export { default as Dropdown } from './Dropdown' export { default as Dropdown } from './Dropdown'
export { default as VotesDisplay } from './VotesDisplay' export { default as VotesDisplay } from './VotesDisplay'
export { default as EntryList } from './EntryList'
export { default as Spinner } from './Spinner'
export type { LoadingSize, LoadingType } from './LoadingIndicator' export type { LoadingSize, LoadingType } from './LoadingIndicator'
export type { DropdownOption } from './Dropdown' export type { DropdownOption } from './Dropdown'
export type { GameVotes } from './VotesDisplay' export type { GameVotes } from './VotesDisplay'
export type { EntryListProps } from './EntryList'
@@ -6,6 +6,7 @@ import DialogFooter from './DialogFooter'
import DialogActions from './DialogActions' import DialogActions from './DialogActions'
import AddDlcDialog from './AddDlcDialog' import AddDlcDialog from './AddDlcDialog'
import { Button, AnimatedCheckbox } from '@/components/buttons' import { Button, AnimatedCheckbox } from '@/components/buttons'
import { Spinner } from '@/components/common'
import { DlcInfo } from '@/types' import { DlcInfo } from '@/types'
import { Icon, check, info } from '@/components/icons' import { Icon, check, info } from '@/components/icons'
@@ -223,7 +224,7 @@ const DlcSelectionDialog = ({
</ul> </ul>
) : ( ) : (
<div className="dlc-loading"> <div className="dlc-loading">
<div className="loading-spinner"></div> <Spinner />
<p>Loading DLC information...</p> <p>Loading DLC information...</p>
</div> </div>
)} )}
@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Spinner } from '@/components/common'
interface InitialLoadingScreenProps { interface InitialLoadingScreenProps {
message: string message: string
@@ -38,8 +39,7 @@ const InitialLoadingScreen = ({ message, progress }: InitialLoadingScreenProps)
<h1>CreamLinux</h1> <h1>CreamLinux</h1>
<div className="loading-animation"> <div className="loading-animation">
{/* Spinner animation */} <Spinner />
<div className="loading-spinner"></div>
</div> </div>
<p className="loading-message">{message}</p> <p className="loading-message">{message}</p>
+15 -11
View File
@@ -1,7 +1,7 @@
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback, useRef } from 'react'
import { check } from '@tauri-apps/plugin-updater' import { check } from '@tauri-apps/plugin-updater'
import { relaunch } from '@tauri-apps/plugin-process' import { relaunch } from '@tauri-apps/plugin-process'
import { ProgressBar } from '@/components/common' import { ProgressBar, Spinner } from '@/components/common'
interface UpdateScreenProps { interface UpdateScreenProps {
onComplete: () => void onComplete: () => void
@@ -16,6 +16,8 @@ const UpdateScreen = ({ onComplete }: UpdateScreenProps) => {
const [downloading, setDownloading] = useState(false) const [downloading, setDownloading] = useState(false)
const [progress, setProgress] = useState(0) const [progress, setProgress] = useState(0)
const [version, setVersion] = useState('') const [version, setVersion] = useState('')
const totalBytesRef = useRef(0)
const downloadedBytesRef = useRef(0)
const checkForUpdates = useCallback(async () => { const checkForUpdates = useCallback(async () => {
try { try {
@@ -29,20 +31,22 @@ const UpdateScreen = ({ onComplete }: UpdateScreenProps) => {
setVersion(update.version) setVersion(update.version)
// Download and install the update // Download and install the update
totalBytesRef.current = 0
downloadedBytesRef.current = 0
await update.downloadAndInstall((event) => { await update.downloadAndInstall((event) => {
switch (event.event) { switch (event.event) {
case 'Started': { case 'Started': {
const contentLength = event.data.contentLength totalBytesRef.current = event.data.contentLength ?? 0
console.log(`Started downloading ${contentLength} bytes`) console.log(`Started downloading ${totalBytesRef.current} bytes`)
break break
} }
case 'Progress': { case 'Progress': {
const { chunkLength } = event.data downloadedBytesRef.current += event.data.chunkLength
// Calculate cumulative progress // Percentage of total bytes downloaded so far (0 if the server
setProgress((prev) => { // didn't report a content length)
const newProgress = prev + chunkLength const total = totalBytesRef.current
return Math.min(newProgress, 100) setProgress(total > 0 ? Math.min((downloadedBytesRef.current / total) * 100, 100) : 0)
})
break break
} }
case 'Finished': case 'Finished':
@@ -77,7 +81,7 @@ const UpdateScreen = ({ onComplete }: UpdateScreenProps) => {
<h1>CreamLinux</h1> <h1>CreamLinux</h1>
<div className="loading-animation"> <div className="loading-animation">
{checking && <div className="loading-spinner"></div>} {checking && <Spinner />}
</div> </div>
<p className="loading-message"> <p className="loading-message">