- Add 4 separate I/O columns (D Read, D Write, N In, N Out) to guest table - Tighten column widths: fixed-width I/O columns, flexible progress bar columns - Remove sticky columns from NodeSummaryTable (not needed) - Shorten "Containers" to "CTs" in node summary for consistency - Always show full VM/LXC labels (no mobile abbreviation) - Increase name column minWidth to 100px for mobile readability - Add formatSpeed utility function for I/O display - Add responsive infrastructure: useBreakpoint hook, useGridTemplate hook
161 lines
4.6 KiB
TypeScript
161 lines
4.6 KiB
TypeScript
import { createSignal, onMount, onCleanup, createMemo, Accessor } from 'solid-js';
|
|
|
|
/**
|
|
* Tailwind CSS breakpoint values (in pixels)
|
|
* These match Tailwind's default breakpoints
|
|
*/
|
|
export const BREAKPOINTS = {
|
|
xs: 0,
|
|
sm: 640,
|
|
md: 768,
|
|
lg: 1024,
|
|
xl: 1280,
|
|
'2xl': 1536,
|
|
} as const;
|
|
|
|
export type Breakpoint = keyof typeof BREAKPOINTS;
|
|
|
|
/**
|
|
* Column priority tiers mapped to breakpoints
|
|
* - essential: Always visible (xs and up)
|
|
* - primary: Visible on small screens and up (sm: 640px+)
|
|
* - secondary: Visible on medium screens and up (md: 768px+)
|
|
* - supplementary: Visible on large screens and up (lg: 1024px+)
|
|
* - detailed: Visible on extra large screens and up (xl: 1280px+)
|
|
*/
|
|
export type ColumnPriority = 'essential' | 'primary' | 'secondary' | 'supplementary' | 'detailed';
|
|
|
|
export const PRIORITY_BREAKPOINTS: Record<ColumnPriority, Breakpoint> = {
|
|
essential: 'xs',
|
|
primary: 'sm',
|
|
secondary: 'md',
|
|
supplementary: 'lg',
|
|
detailed: 'xl',
|
|
};
|
|
|
|
/**
|
|
* Get the current breakpoint name based on window width
|
|
*/
|
|
function getBreakpointName(width: number): Breakpoint {
|
|
if (width >= BREAKPOINTS['2xl']) return '2xl';
|
|
if (width >= BREAKPOINTS.xl) return 'xl';
|
|
if (width >= BREAKPOINTS.lg) return 'lg';
|
|
if (width >= BREAKPOINTS.md) return 'md';
|
|
if (width >= BREAKPOINTS.sm) return 'sm';
|
|
return 'xs';
|
|
}
|
|
|
|
export interface UseBreakpointReturn {
|
|
/** Current window width in pixels */
|
|
width: Accessor<number>;
|
|
/** Current breakpoint name (xs, sm, md, lg, xl, 2xl) */
|
|
breakpoint: Accessor<Breakpoint>;
|
|
/** Check if current width is at least the given breakpoint */
|
|
isAtLeast: (bp: Breakpoint) => boolean;
|
|
/** Check if current width is below the given breakpoint */
|
|
isBelow: (bp: Breakpoint) => boolean;
|
|
/** Check if a column with the given priority should be visible */
|
|
isVisible: (priority: ColumnPriority) => boolean;
|
|
/** Convenience booleans for common checks */
|
|
isMobile: Accessor<boolean>;
|
|
isTablet: Accessor<boolean>;
|
|
isDesktop: Accessor<boolean>;
|
|
}
|
|
|
|
/**
|
|
* Reactive hook for tracking viewport width and breakpoints.
|
|
*
|
|
* Use this for conditional rendering based on screen size,
|
|
* which is more performant than rendering and hiding with CSS.
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* const { isAtLeast, isMobile, isVisible } = useBreakpoint();
|
|
*
|
|
* return (
|
|
* <Show when={isAtLeast('md')} fallback={<MobileView />}>
|
|
* <DesktopView />
|
|
* </Show>
|
|
* );
|
|
* ```
|
|
*/
|
|
export function useBreakpoint(): UseBreakpointReturn {
|
|
// Initialize with current window width (or 0 for SSR)
|
|
const initialWidth = typeof window !== 'undefined' ? window.innerWidth : 0;
|
|
const [width, setWidth] = createSignal(initialWidth);
|
|
|
|
onMount(() => {
|
|
// Debounce resize events to avoid excessive re-renders
|
|
let resizeTimeout: number | undefined;
|
|
|
|
const handleResize = () => {
|
|
if (resizeTimeout) {
|
|
window.cancelAnimationFrame(resizeTimeout);
|
|
}
|
|
resizeTimeout = window.requestAnimationFrame(() => {
|
|
setWidth(window.innerWidth);
|
|
});
|
|
};
|
|
|
|
// Set initial width in case it changed between SSR and mount
|
|
setWidth(window.innerWidth);
|
|
|
|
window.addEventListener('resize', handleResize, { passive: true });
|
|
|
|
onCleanup(() => {
|
|
window.removeEventListener('resize', handleResize);
|
|
if (resizeTimeout) {
|
|
window.cancelAnimationFrame(resizeTimeout);
|
|
}
|
|
});
|
|
});
|
|
|
|
const breakpoint = createMemo(() => getBreakpointName(width()));
|
|
|
|
const isAtLeast = (bp: Breakpoint): boolean => {
|
|
return width() >= BREAKPOINTS[bp];
|
|
};
|
|
|
|
const isBelow = (bp: Breakpoint): boolean => {
|
|
return width() < BREAKPOINTS[bp];
|
|
};
|
|
|
|
const isVisible = (priority: ColumnPriority): boolean => {
|
|
const minBreakpoint = PRIORITY_BREAKPOINTS[priority];
|
|
return width() >= BREAKPOINTS[minBreakpoint];
|
|
};
|
|
|
|
const isMobile = createMemo(() => width() < BREAKPOINTS.md);
|
|
const isTablet = createMemo(() => width() >= BREAKPOINTS.md && width() < BREAKPOINTS.xl);
|
|
const isDesktop = createMemo(() => width() >= BREAKPOINTS.xl);
|
|
|
|
return {
|
|
width,
|
|
breakpoint,
|
|
isAtLeast,
|
|
isBelow,
|
|
isVisible,
|
|
isMobile,
|
|
isTablet,
|
|
isDesktop,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get the Tailwind CSS class for hiding an element below a given breakpoint
|
|
*/
|
|
export function getVisibilityClass(priority: ColumnPriority, display: 'flex' | 'block' | 'table-cell' | 'grid' | 'inline' = 'flex'): string {
|
|
const bp = PRIORITY_BREAKPOINTS[priority];
|
|
if (bp === 'xs') {
|
|
// Always visible, no hiding class needed
|
|
return display;
|
|
}
|
|
return `hidden ${bp}:${display}`;
|
|
}
|
|
|
|
/**
|
|
* Get the minimum width for a priority level
|
|
*/
|
|
export function getPriorityMinWidth(priority: ColumnPriority): number {
|
|
return BREAKPOINTS[PRIORITY_BREAKPOINTS[priority]];
|
|
}
|