mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-22 05:36:15 +01:00
ui: conditional title attribute and better CJK truncation
- Adds CJK support for system name truncation - Change tooltip to title attribute and show only if system name is truncated
This commit is contained in:
@@ -128,28 +128,36 @@ export function SystemsTableColumns(viewMode: "table" | "grid"): ColumnDef<Syste
|
|||||||
cell: (info) => {
|
cell: (info) => {
|
||||||
const { name, id } = info.row.original
|
const { name, id } = info.row.original
|
||||||
const longestName = useStore($longestSystemNameLen)
|
const longestName = useStore($longestSystemNameLen)
|
||||||
|
const linkUrl = getPagePath($router, "system", { id })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip>
|
<>
|
||||||
<TooltipTrigger asChild>
|
<span className="flex gap-2 items-center font-medium text-sm text-nowrap md:ps-1">
|
||||||
<div className="relative">
|
<IndicatorDot system={info.row.original} />
|
||||||
<span className="flex gap-2 items-center font-medium text-sm text-nowrap md:ps-1">
|
<Link
|
||||||
<IndicatorDot system={info.row.original} />
|
href={linkUrl}
|
||||||
{/* NOTE: change to 1 ch if switching to monospace font */}
|
tabIndex={-1}
|
||||||
<span className="truncate" style={{ width: `${longestName / 1.1}ch` }}>
|
className="truncate z-10 relative"
|
||||||
{name}
|
style={{ width: `${longestName / 1.05}ch` }}
|
||||||
</span>
|
onMouseEnter={(e) => {
|
||||||
</span>
|
// set title on hover if text is truncated to show full name
|
||||||
<Link
|
const a = e.currentTarget
|
||||||
href={getPagePath($router, "system", { id })}
|
if (a.scrollWidth > a.clientWidth) {
|
||||||
className="inset-0 absolute size-full"
|
a.title = name
|
||||||
aria-label={name}
|
} else {
|
||||||
></Link>
|
a.removeAttribute("title")
|
||||||
</div>
|
}
|
||||||
</TooltipTrigger>
|
}}
|
||||||
<TooltipContent>
|
>
|
||||||
<p>{name}</p>
|
{name}
|
||||||
</TooltipContent>
|
</Link>
|
||||||
</Tooltip>
|
</span>
|
||||||
|
<Link
|
||||||
|
href={linkUrl}
|
||||||
|
className="inset-0 absolute size-full"
|
||||||
|
aria-label={name}
|
||||||
|
></Link>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
header: sortableHeader,
|
header: sortableHeader,
|
||||||
@@ -446,9 +454,9 @@ function TableCellWithMeter(info: CellContext<SystemRecord, unknown>) {
|
|||||||
const meterClass = cn(
|
const meterClass = cn(
|
||||||
"h-full",
|
"h-full",
|
||||||
(info.row.original.status !== SystemStatus.Up && STATUS_COLORS.paused) ||
|
(info.row.original.status !== SystemStatus.Up && STATUS_COLORS.paused) ||
|
||||||
(threshold === MeterState.Good && STATUS_COLORS.up) ||
|
(threshold === MeterState.Good && STATUS_COLORS.up) ||
|
||||||
(threshold === MeterState.Warn && STATUS_COLORS.pending) ||
|
(threshold === MeterState.Warn && STATUS_COLORS.pending) ||
|
||||||
STATUS_COLORS.down
|
STATUS_COLORS.down
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2 items-center tabular-nums tracking-tight w-full">
|
<div className="flex gap-2 items-center tabular-nums tracking-tight w-full">
|
||||||
@@ -560,7 +568,7 @@ export function IndicatorDot({ system, className }: { system: SystemRecord; clas
|
|||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn("shrink-0 size-2 rounded-full", className)}
|
className={cn("shrink-0 size-2 rounded-full", className)}
|
||||||
// style={{ marginBottom: "-1px" }}
|
// style={{ marginBottom: "-1px" }}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
$pausedSystems,
|
$pausedSystems,
|
||||||
$upSystems,
|
$upSystems,
|
||||||
} from "@/lib/stores"
|
} from "@/lib/stores"
|
||||||
import { updateFavicon } from "@/lib/utils"
|
import { getVisualStringWidth, updateFavicon } from "@/lib/utils"
|
||||||
import type { SystemRecord } from "@/types"
|
import type { SystemRecord } from "@/types"
|
||||||
import { SystemStatus } from "./enums"
|
import { SystemStatus } from "./enums"
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ function onSystemsChanged(_: Record<string, SystemRecord>, changedSystem: System
|
|||||||
|
|
||||||
// Update longest system name length
|
// Update longest system name length
|
||||||
const longestName = $longestSystemNameLen.get()
|
const longestName = $longestSystemNameLen.get()
|
||||||
const nameLen = Math.min(MAX_SYSTEM_NAME_LENGTH, changedSystem?.name.length || 0)
|
const nameLen = Math.min(MAX_SYSTEM_NAME_LENGTH, getVisualStringWidth(changedSystem?.name || ""))
|
||||||
if (nameLen > longestName) {
|
if (nameLen > longestName) {
|
||||||
$longestSystemNameLen.set(nameLen)
|
$longestSystemNameLen.set(nameLen)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export async function copyToClipboard(content: string) {
|
|||||||
duration,
|
duration,
|
||||||
description: t`Copied to clipboard`,
|
description: t`Copied to clipboard`,
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (_e) {
|
||||||
$copyContent.set(content)
|
$copyContent.set(content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -316,7 +316,7 @@ export const getHostDisplayValue = (system: SystemRecord): string => system.host
|
|||||||
export const generateToken = () => {
|
export const generateToken = () => {
|
||||||
try {
|
try {
|
||||||
return crypto?.randomUUID()
|
return crypto?.randomUUID()
|
||||||
} catch (e) {
|
} catch (_e) {
|
||||||
return Array.from({ length: 2 }, () => (performance.now() * Math.random()).toString(16).replace(".", "-")).join("-")
|
return Array.from({ length: 2 }, () => (performance.now() * Math.random()).toString(16).replace(".", "-")).join("-")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -429,6 +429,30 @@ export function runOnce<T extends (...args: any[]) => any>(fn: T): T {
|
|||||||
}) as T
|
}) as T
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get the visual width of a string, accounting for full-width characters */
|
||||||
|
export function getVisualStringWidth(str: string): number {
|
||||||
|
let width = 0
|
||||||
|
for (const char of str) {
|
||||||
|
const code = char.codePointAt(0) || 0
|
||||||
|
// Hangul Jamo and Syllables are often slightly thinner than Hanzi/Kanji
|
||||||
|
if ((code >= 0x1100 && code <= 0x115f) || (code >= 0xac00 && code <= 0xd7af)) {
|
||||||
|
width += 1.8
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Count CJK and other full-width characters as 2 units, others as 1
|
||||||
|
// Arabic and Cyrillic are counted as 1
|
||||||
|
const isFullWidth =
|
||||||
|
(code >= 0x2e80 && code <= 0x9fff) || // CJK Radicals, Symbols, and Ideographs
|
||||||
|
(code >= 0xf900 && code <= 0xfaff) || // CJK Compatibility Ideographs
|
||||||
|
(code >= 0xfe30 && code <= 0xfe6f) || // CJK Compatibility Forms
|
||||||
|
(code >= 0xff00 && code <= 0xff60) || // Fullwidth Forms
|
||||||
|
(code >= 0xffe0 && code <= 0xffe6) || // Fullwidth Symbols
|
||||||
|
code > 0xffff // Emojis and other supplementary plane characters
|
||||||
|
width += isFullWidth ? 2 : 1
|
||||||
|
}
|
||||||
|
return width
|
||||||
|
}
|
||||||
|
|
||||||
/** Format seconds to hours, minutes, or seconds */
|
/** Format seconds to hours, minutes, or seconds */
|
||||||
export function secondsToString(seconds: number, unit: "hour" | "minute" | "day"): string {
|
export function secondsToString(seconds: number, unit: "hour" | "minute" | "day"): string {
|
||||||
const count = Math.floor(seconds / (unit === "hour" ? 3600 : unit === "minute" ? 60 : 86400))
|
const count = Math.floor(seconds / (unit === "hour" ? 3600 : unit === "minute" ? 60 : 86400))
|
||||||
|
|||||||
Reference in New Issue
Block a user