mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-03-28 03:29:25 +01:00
feat: alert and button styles redesign
This commit is contained in:
parent
12a6f2230d
commit
f4a69ea401
|
|
@ -125,13 +125,15 @@ export class MapService {
|
|||
throw new Error('Base styles file not found in storage/maps')
|
||||
}
|
||||
|
||||
const localUrl = env.get('URL')
|
||||
const rawStyles = JSON.parse(baseStyle.toString()) as BaseStylesFile
|
||||
|
||||
const regions = (await this.listRegions()).files
|
||||
const sources = this.generateSourcesArray(regions)
|
||||
|
||||
const baseUrl = urlJoin(localUrl, this.mapStoragePath, this.basemapsAssetsDir)
|
||||
const localUrl = env.get('URL')
|
||||
const withProtocol = localUrl.startsWith('http') ? localUrl : `http://${localUrl}`
|
||||
const baseUrlPath = urlJoin(this.mapStoragePath, this.basemapsAssetsDir)
|
||||
const baseUrl = new URL(baseUrlPath, withProtocol).toString()
|
||||
|
||||
const styles = await this.generateStylesFile(
|
||||
rawStyles,
|
||||
|
|
@ -173,10 +175,15 @@ export class MapService {
|
|||
if (region.type === 'file' && region.name.endsWith('.pmtiles')) {
|
||||
const regionName = region.name.replace('.pmtiles', '')
|
||||
const source: BaseStylesFile['sources'] = {}
|
||||
const sourceUrl = new URL(
|
||||
urlJoin(this.mapStoragePath, 'pmtiles', region.name),
|
||||
localUrl.startsWith('http') ? localUrl : `http://${localUrl}`
|
||||
).toString()
|
||||
|
||||
source[regionName] = {
|
||||
type: 'vector',
|
||||
attribution: PMTILES_ATTRIBUTION,
|
||||
url: `pmtiles://http://${urlJoin(localUrl, this.mapStoragePath, 'pmtiles', region.name)}`,
|
||||
url: `pmtiles://${sourceUrl}`,
|
||||
}
|
||||
sources.push(source)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,62 +1,200 @@
|
|||
import { ExclamationTriangleIcon, XCircleIcon } from '@heroicons/react/24/solid'
|
||||
import { IconCircleCheck } from '@tabler/icons-react'
|
||||
import * as Icons from '@heroicons/react/24/solid'
|
||||
import classNames from '~/lib/classNames'
|
||||
|
||||
export type AlertProps = React.HTMLAttributes<HTMLDivElement> & {
|
||||
title: string
|
||||
message?: string
|
||||
type: 'warning' | 'error' | 'success'
|
||||
type: 'warning' | 'error' | 'success' | 'info'
|
||||
children?: React.ReactNode
|
||||
dismissible?: boolean
|
||||
onDismiss?: () => void
|
||||
icon?: keyof typeof Icons
|
||||
variant?: 'standard' | 'bordered' | 'solid'
|
||||
}
|
||||
|
||||
export default function Alert({ title, message, type, children, ...props }: AlertProps) {
|
||||
const getIcon = () => {
|
||||
const Icon =
|
||||
type === 'warning'
|
||||
? ExclamationTriangleIcon
|
||||
: type === 'error'
|
||||
? XCircleIcon
|
||||
: IconCircleCheck
|
||||
const color =
|
||||
type === 'warning' ? 'text-yellow-400' : type === 'error' ? 'text-red-400' : 'text-green-400'
|
||||
|
||||
return <Icon aria-hidden="true" className={`size-5 ${color}`} />
|
||||
export default function Alert({
|
||||
title,
|
||||
message,
|
||||
type,
|
||||
children,
|
||||
dismissible = false,
|
||||
onDismiss,
|
||||
icon,
|
||||
variant = 'standard',
|
||||
...props
|
||||
}: AlertProps) {
|
||||
const getDefaultIcon = (): keyof typeof Icons => {
|
||||
switch (type) {
|
||||
case 'warning':
|
||||
return 'ExclamationTriangleIcon'
|
||||
case 'error':
|
||||
return 'XCircleIcon'
|
||||
case 'success':
|
||||
return 'CheckCircleIcon'
|
||||
case 'info':
|
||||
return 'InformationCircleIcon'
|
||||
default:
|
||||
return 'InformationCircleIcon'
|
||||
}
|
||||
}
|
||||
|
||||
const getBackground = () => {
|
||||
return type === 'warning' ? 'bg-yellow-100' : type === 'error' ? 'bg-red-50' : 'bg-green-50'
|
||||
const IconComponent = () => {
|
||||
const iconName = icon || getDefaultIcon()
|
||||
const Icon = Icons[iconName]
|
||||
if (!Icon) return null
|
||||
|
||||
return <Icon aria-hidden="true" className={classNames('size-5 shrink-0', getIconColor())} />
|
||||
}
|
||||
|
||||
const getTextColor = () => {
|
||||
return type === 'warning'
|
||||
? 'text-yellow-800'
|
||||
: type === 'error'
|
||||
? 'text-red-800'
|
||||
: 'text-green-800'
|
||||
const getIconColor = () => {
|
||||
if (variant === 'solid') return 'text-desert-white'
|
||||
switch (type) {
|
||||
case 'warning':
|
||||
return 'text-desert-orange'
|
||||
case 'error':
|
||||
return 'text-desert-red'
|
||||
case 'success':
|
||||
return 'text-desert-olive'
|
||||
case 'info':
|
||||
return 'text-desert-stone'
|
||||
default:
|
||||
return 'text-desert-stone'
|
||||
}
|
||||
}
|
||||
|
||||
const getVariantStyles = () => {
|
||||
const baseStyles = 'rounded-md transition-all duration-200'
|
||||
const variantStyles: string[] = []
|
||||
|
||||
switch (variant) {
|
||||
case 'bordered':
|
||||
variantStyles.push(
|
||||
type === 'warning'
|
||||
? 'border-desert-orange'
|
||||
: type === 'error'
|
||||
? 'border-desert-red'
|
||||
: type === 'success'
|
||||
? 'border-desert-olive'
|
||||
: type === 'info'
|
||||
? 'border-desert-stone'
|
||||
: ''
|
||||
)
|
||||
return classNames(baseStyles, 'border-2 bg-desert-white', ...variantStyles)
|
||||
case 'solid':
|
||||
variantStyles.push(
|
||||
type === 'warning'
|
||||
? 'bg-desert-orange text-desert-white border-desert-orange-dark'
|
||||
: type === 'error'
|
||||
? 'bg-desert-red text-desert-white border-desert-red-dark'
|
||||
: type === 'success'
|
||||
? 'bg-desert-olive text-desert-white border-desert-olive-dark'
|
||||
: type === 'info'
|
||||
? 'bg-desert-stone text-desert-white border-desert-stone-dark'
|
||||
: ''
|
||||
)
|
||||
return classNames(baseStyles, 'shadow-sm', ...variantStyles)
|
||||
default:
|
||||
variantStyles.push(
|
||||
type === 'warning'
|
||||
? 'bg-desert-orange-lighter bg-opacity-20 border-desert-orange-light'
|
||||
: type === 'error'
|
||||
? 'bg-desert-red-lighter bg-opacity-20 border-desert-red-light'
|
||||
: type === 'success'
|
||||
? 'bg-desert-olive-lighter bg-opacity-20 border-desert-olive-light'
|
||||
: type === 'info'
|
||||
? 'bg-desert-stone-lighter bg-opacity-20 border-desert-stone-light'
|
||||
: ''
|
||||
)
|
||||
return classNames(baseStyles, 'border shadow-sm', ...variantStyles)
|
||||
}
|
||||
}
|
||||
|
||||
const getTitleColor = () => {
|
||||
if (variant === 'solid') return 'text-desert-white'
|
||||
|
||||
switch (type) {
|
||||
case 'warning':
|
||||
return 'text-desert-orange-dark'
|
||||
case 'error':
|
||||
return 'text-desert-red-dark'
|
||||
case 'success':
|
||||
return 'text-desert-olive-dark'
|
||||
case 'info':
|
||||
return 'text-desert-stone-dark'
|
||||
default:
|
||||
return 'text-desert-stone-dark'
|
||||
}
|
||||
}
|
||||
|
||||
const getMessageColor = () => {
|
||||
if (variant === 'solid') return 'text-desert-white text-opacity-90'
|
||||
|
||||
switch (type) {
|
||||
case 'warning':
|
||||
return 'text-desert-orange-dark text-opacity-80'
|
||||
case 'error':
|
||||
return 'text-desert-red-dark text-opacity-80'
|
||||
case 'success':
|
||||
return 'text-desert-olive-dark text-opacity-80'
|
||||
case 'info':
|
||||
return 'text-desert-stone-dark text-opacity-80'
|
||||
default:
|
||||
return 'text-desert-stone-dark text-opacity-80'
|
||||
}
|
||||
}
|
||||
|
||||
const getCloseButtonStyles = () => {
|
||||
if (variant === 'solid') {
|
||||
return 'text-desert-white hover:text-desert-white hover:bg-black hover:bg-opacity-20'
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'warning':
|
||||
return 'text-desert-orange hover:text-desert-orange-dark hover:bg-desert-orange-lighter hover:bg-opacity-30'
|
||||
case 'error':
|
||||
return 'text-desert-red hover:text-desert-red-dark hover:bg-desert-red-lighter hover:bg-opacity-30'
|
||||
case 'success':
|
||||
return 'text-desert-olive hover:text-desert-olive-dark hover:bg-desert-olive-lighter hover:bg-opacity-30'
|
||||
case 'info':
|
||||
return 'text-desert-stone hover:text-desert-stone-dark hover:bg-desert-stone-lighter hover:bg-opacity-30'
|
||||
default:
|
||||
return 'text-desert-stone hover:text-desert-stone-dark hover:bg-desert-stone-lighter hover:bg-opacity-30'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={classNames(
|
||||
getBackground(),
|
||||
props.className,
|
||||
'border border-gray-200 rounded-md p-3 shadow-xs'
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-row justify-between items-center">
|
||||
<div className="flex flex-row">
|
||||
<div className="shrink-0">{getIcon()}</div>
|
||||
<div className="ml-3">
|
||||
<h3 className={`text-sm font-medium ${getTextColor()}`}>{title}</h3>
|
||||
{message && (
|
||||
<div className={`mt-2 text-sm ${getTextColor()}`}>
|
||||
<p>{message}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div {...props} className={classNames(getVariantStyles(), 'p-4', props.className)} role="alert">
|
||||
<div className="flex gap-3">
|
||||
<IconComponent />
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className={classNames('text-sm font-semibold', getTitleColor())}>{title}</h3>
|
||||
{message && (
|
||||
<div className={classNames('mt-1 text-sm', getMessageColor())}>
|
||||
<p>{message}</p>
|
||||
</div>
|
||||
)}
|
||||
{children && <div className="mt-3">{children}</div>}
|
||||
</div>
|
||||
{children}
|
||||
|
||||
{dismissible && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onDismiss}
|
||||
className={classNames(
|
||||
'shrink-0 rounded-md p-1.5 transition-colors duration-150',
|
||||
getCloseButtonStyles(),
|
||||
'focus:outline-none focus:ring-2 focus:ring-offset-2',
|
||||
type === 'warning' ? 'focus:ring-desert-orange' : '',
|
||||
type === 'error' ? 'focus:ring-desert-red' : '',
|
||||
type === 'success' ? 'focus:ring-desert-olive' : '',
|
||||
type === 'info' ? 'focus:ring-desert-stone' : ''
|
||||
)}
|
||||
aria-label="Dismiss alert"
|
||||
>
|
||||
<Icons.XMarkIcon className="size-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,15 +6,19 @@ export interface StyledButtonProps extends React.HTMLAttributes<HTMLButtonElemen
|
|||
// icon should be one of the HeroIcon names, e.g. ArrowTopRightOnSquareIcon
|
||||
icon?: keyof typeof Icons
|
||||
disabled?: boolean
|
||||
variant?: 'primary' | 'secondary' | 'danger' | 'action'
|
||||
variant?: 'primary' | 'secondary' | 'danger' | 'action' | 'success' | 'ghost' | 'outline'
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
loading?: boolean
|
||||
fullWidth?: boolean
|
||||
}
|
||||
|
||||
const StyledButton: React.FC<StyledButtonProps> = ({
|
||||
children,
|
||||
icon,
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
loading = false,
|
||||
fullWidth = false,
|
||||
...props
|
||||
}) => {
|
||||
const isDisabled = useMemo(() => {
|
||||
|
|
@ -24,30 +28,113 @@ const StyledButton: React.FC<StyledButtonProps> = ({
|
|||
const IconComponent = () => {
|
||||
if (!icon) return null
|
||||
const Icon = Icons[icon]
|
||||
return Icon ? <Icon className="h-4 w-4 mr-2" /> : null
|
||||
return Icon ? <Icon className={getIconSize()} /> : null
|
||||
}
|
||||
|
||||
const getBgColors = () => {
|
||||
// if primary, use desert-green
|
||||
if (variant === 'primary') {
|
||||
return 'bg-desert-green hover:bg-desert-green-light text-white hover:shadow-lg transition-all duration-200'
|
||||
}
|
||||
// if secondary, use outlined styles
|
||||
if (variant === 'secondary') {
|
||||
return 'bg-transparent border border-desert-green text-desert-green hover:bg-desert-green-light'
|
||||
const getIconSize = () => {
|
||||
switch (size) {
|
||||
case 'sm':
|
||||
return 'h-3.5 w-3.5 mr-1.5'
|
||||
case 'lg':
|
||||
return 'h-5 w-5 mr-2.5'
|
||||
default:
|
||||
return 'h-4 w-4 mr-2'
|
||||
}
|
||||
}
|
||||
|
||||
// if danger, use red styles
|
||||
if (variant === 'danger') {
|
||||
return 'bg-red-600 hover:bg-red-700 text-white'
|
||||
const getSizeClasses = () => {
|
||||
switch (size) {
|
||||
case 'sm':
|
||||
return 'px-2.5 py-1.5 text-xs'
|
||||
case 'lg':
|
||||
return 'px-4 py-3 text-base'
|
||||
default:
|
||||
return 'px-3 py-2 text-sm'
|
||||
}
|
||||
}
|
||||
|
||||
// if action, use orange styles
|
||||
if (variant === 'action') {
|
||||
return 'bg-desert-orange hover:bg-desert-orange-light text-white'
|
||||
const getVariantClasses = () => {
|
||||
const baseTransition = 'transition-all duration-200 ease-in-out'
|
||||
const baseHover = 'hover:shadow-md active:scale-[0.98]'
|
||||
|
||||
switch (variant) {
|
||||
case 'primary':
|
||||
return `
|
||||
bg-desert-green text-desert-white
|
||||
hover:bg-desert-green-dark hover:shadow-lg
|
||||
active:bg-desert-green-darker
|
||||
disabled:bg-desert-green-light disabled:text-desert-stone-light
|
||||
${baseTransition} ${baseHover}
|
||||
`
|
||||
|
||||
case 'secondary':
|
||||
return `
|
||||
bg-desert-tan text-desert-white
|
||||
hover:bg-desert-tan-dark hover:shadow-lg
|
||||
active:bg-desert-tan-dark
|
||||
disabled:bg-desert-tan-lighter disabled:text-desert-stone-light
|
||||
${baseTransition} ${baseHover}
|
||||
`
|
||||
|
||||
case 'danger':
|
||||
return `
|
||||
bg-desert-red text-desert-white
|
||||
hover:bg-desert-red-dark hover:shadow-lg
|
||||
active:bg-desert-red-dark
|
||||
disabled:bg-desert-red-lighter disabled:text-desert-stone-light
|
||||
${baseTransition} ${baseHover}
|
||||
`
|
||||
|
||||
case 'action':
|
||||
return `
|
||||
bg-desert-orange text-desert-white
|
||||
hover:bg-desert-orange-light hover:shadow-lg
|
||||
active:bg-desert-orange-dark
|
||||
disabled:bg-desert-orange-lighter disabled:text-desert-stone-light
|
||||
${baseTransition} ${baseHover}
|
||||
`
|
||||
|
||||
case 'success':
|
||||
return `
|
||||
bg-desert-olive text-desert-white
|
||||
hover:bg-desert-olive-dark hover:shadow-lg
|
||||
active:bg-desert-olive-dark
|
||||
disabled:bg-desert-olive-lighter disabled:text-desert-stone-light
|
||||
${baseTransition} ${baseHover}
|
||||
`
|
||||
|
||||
case 'ghost':
|
||||
return `
|
||||
bg-transparent text-desert-green
|
||||
hover:bg-desert-sand hover:text-desert-green-dark
|
||||
active:bg-desert-green-lighter
|
||||
disabled:text-desert-stone-light
|
||||
${baseTransition}
|
||||
`
|
||||
|
||||
case 'outline':
|
||||
return `
|
||||
bg-transparent border-2 border-desert-green text-desert-green
|
||||
hover:bg-desert-green hover:text-desert-white hover:border-desert-green-dark
|
||||
active:bg-desert-green-dark active:border-desert-green-darker
|
||||
disabled:border-desert-green-lighter disabled:text-desert-stone-light
|
||||
${baseTransition} ${baseHover}
|
||||
`
|
||||
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
const getLoadingSpinner = () => {
|
||||
const spinnerSize = size === 'sm' ? 'h-3.5 w-3.5' : size === 'lg' ? 'h-5 w-5' : 'h-4 w-4'
|
||||
return (
|
||||
<Icons.ArrowPathIcon
|
||||
className={`${spinnerSize} animate-spin ${fullWidth ? 'mx-auto' : ''}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const onClickHandler = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (isDisabled) {
|
||||
e.preventDefault()
|
||||
|
|
@ -59,23 +146,30 @@ const StyledButton: React.FC<StyledButtonProps> = ({
|
|||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`block rounded-md ${getBgColors()} px-3 py-2 text-center text-sm font-semibold shadow-sm cursor-pointer disabled:opacity-50 disabled:pointer-events-none`}
|
||||
className={`
|
||||
${fullWidth ? 'w-full' : 'inline-flex'}
|
||||
items-center justify-center
|
||||
rounded-md font-semibold
|
||||
${getSizeClasses()}
|
||||
${getVariantClasses()}
|
||||
focus:outline-none focus:ring-2 focus:ring-desert-green-light focus:ring-offset-2 focus:ring-offset-desert-sand
|
||||
disabled:cursor-not-allowed disabled:shadow-none
|
||||
${isDisabled ? 'pointer-events-none opacity-60' : 'cursor-pointer'}
|
||||
`}
|
||||
{...props}
|
||||
disabled={isDisabled}
|
||||
onClick={onClickHandler}
|
||||
>
|
||||
{loading ? (
|
||||
<Icons.EllipsisHorizontalCircleIcon className="h-5 w-5 animate-spin text-white" />
|
||||
) : icon ? (
|
||||
<div className="flex flex-row items-center justify-center">
|
||||
<IconComponent />
|
||||
{children}
|
||||
</div>
|
||||
getLoadingSpinner()
|
||||
) : (
|
||||
children
|
||||
<>
|
||||
{icon && <IconComponent />}
|
||||
<span className={fullWidth ? 'block text-center' : ''}>{children}</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default StyledButton
|
||||
export default StyledButton
|
||||
|
|
@ -2,13 +2,40 @@
|
|||
|
||||
@theme {
|
||||
--color-desert-white: #f6f6f4;
|
||||
--color-desert-green-light: #babaaa;
|
||||
--color-desert-green: #424420;
|
||||
--color-desert-orange: #a84a12;
|
||||
--color-desert-sand: #f7eedc;
|
||||
/* --color-desert-sand: #E2DAC2; */
|
||||
|
||||
--color-desert-green-darker: #2a2a15;
|
||||
--color-desert-green-dark: #353518;
|
||||
--color-desert-green: #424420;
|
||||
--color-desert-green-light: #babaaa;
|
||||
--color-desert-green-lighter: #d4d4c8;
|
||||
|
||||
--color-desert-orange-dark: #8a3d0f;
|
||||
--color-desert-orange: #a84a12;
|
||||
--color-desert-orange-light: #c85815;
|
||||
--color-desert-orange-lighter: #e69556;
|
||||
|
||||
--color-desert-tan-dark: #6b5d4f;
|
||||
--color-desert-tan: #8b7355;
|
||||
--color-desert-tan-light: #a8927a;
|
||||
--color-desert-tan-lighter: #c9b99f;
|
||||
|
||||
--color-desert-red-dark: #7a2e2e;
|
||||
--color-desert-red: #994444;
|
||||
--color-desert-red-light: #b05555;
|
||||
--color-desert-red-lighter: #d88989;
|
||||
|
||||
--color-desert-olive-dark: #5a5c3a;
|
||||
--color-desert-olive: #6d7042;
|
||||
--color-desert-olive-light: #858a55;
|
||||
--color-desert-olive-lighter: #a5ab7d;
|
||||
|
||||
--color-desert-stone-dark: #5c5c54;
|
||||
--color-desert-stone: #75756a;
|
||||
--color-desert-stone-light: #8f8f82;
|
||||
--color-desert-stone-lighter: #afafa5;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--color-desert-sand);
|
||||
}
|
||||
}
|
||||
|
|
@ -70,6 +70,7 @@ export default function ZimPage() {
|
|||
<Alert
|
||||
title="The Kiwix application is not installed. Please install it to view downloaded ZIM files"
|
||||
type="warning"
|
||||
variant='solid'
|
||||
className="!mt-6"
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -154,6 +154,7 @@ export default function ZimRemoteExplorer() {
|
|||
title="No internet connection. You may not be able to download files."
|
||||
message=""
|
||||
type="warning"
|
||||
variant="solid"
|
||||
className="!mt-6"
|
||||
/>
|
||||
)}
|
||||
|
|
@ -161,6 +162,7 @@ export default function ZimRemoteExplorer() {
|
|||
<Alert
|
||||
title="The Kiwix application is not installed. Please install it to view downloaded ZIM files"
|
||||
type="warning"
|
||||
variant="solid"
|
||||
className="!mt-6"
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user