mirror of https://github.com/buster-so/buster.git
pass cookies to layout components
This commit is contained in:
parent
2d48de17ee
commit
23472b86c3
|
@ -45,6 +45,9 @@
|
|||
"noExcessiveCognitiveComplexity": "off",
|
||||
"noForEach": "off"
|
||||
},
|
||||
"nursery": {
|
||||
"useSortedClasses": "off"
|
||||
},
|
||||
"performance": {
|
||||
"noDelete": "error"
|
||||
},
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import type React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { LayoutSize } from './AppSplitter';
|
||||
import { AppSplitter } from './AppSplitter/AppSplitter';
|
||||
|
||||
const DEFAULT_LAYOUT: LayoutSize = ['230px', 'auto'];
|
||||
import type { LayoutSize } from './AppSplitter/AppSplitter.types';
|
||||
import { createAutoSaveId } from './AppSplitter/create-auto-save-id';
|
||||
|
||||
/**
|
||||
* @param floating - Applies floating styles with padding and border (default: true)
|
||||
|
@ -21,7 +20,7 @@ export const AppLayout: React.FC<
|
|||
defaultLayout: LayoutSize;
|
||||
initialLayout: LayoutSize | null;
|
||||
leftHidden?: boolean;
|
||||
autoSaveId?: string;
|
||||
autoSaveId: string;
|
||||
}>
|
||||
> = ({
|
||||
children,
|
||||
|
@ -44,7 +43,7 @@ export const AppLayout: React.FC<
|
|||
|
||||
return (
|
||||
<AppSplitter
|
||||
defaultLayout={defaultLayout ?? DEFAULT_LAYOUT}
|
||||
defaultLayout={defaultLayout}
|
||||
className="max-h-screen min-h-screen overflow-hidden"
|
||||
autoSaveId={autoSaveId}
|
||||
preserveSide="left"
|
||||
|
@ -81,3 +80,6 @@ const PageLayout: React.FC<
|
|||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export type { LayoutSize };
|
||||
export { createAutoSaveId };
|
||||
|
|
|
@ -39,7 +39,7 @@ type Story = StoryObj<typeof AppSplitter>;
|
|||
|
||||
// Helper components for demo content
|
||||
const LeftContent = ({ title = 'Left Panel' }: { title?: string }) => (
|
||||
<div className="bg-muted/20 h-full bg-blue-100/10 p-6">
|
||||
<div className="bg-muted/20 h-full p-6">
|
||||
<Title as="h3">{title}</Title>
|
||||
<Text className="text-muted-foreground mt-2">
|
||||
This is the left panel content. Try resizing the panels by dragging the splitter.
|
||||
|
@ -836,7 +836,7 @@ export const CollapsedLeftPanel: Story = {
|
|||
</div>
|
||||
),
|
||||
autoSaveId: 'collapsed-left-panel',
|
||||
defaultLayout: ['0%', '100%'],
|
||||
defaultLayout: ['auto', '100%'],
|
||||
leftPanelMinSize: '200px',
|
||||
preserveSide: 'right',
|
||||
},
|
||||
|
|
|
@ -17,7 +17,8 @@ import { useMount } from '@/hooks/useMount';
|
|||
import { cn } from '@/lib/classMerge';
|
||||
import type { AppSplitterRef, IAppSplitterProps, SplitterState } from './AppSplitter.types';
|
||||
import { AppSplitterProvider } from './AppSplitterProvider';
|
||||
import { createAutoSaveId, easeInOutCubic, sizeToPixels } from './helpers';
|
||||
import { createAutoSaveId } from './create-auto-save-id';
|
||||
import { easeInOutCubic, sizeToPixels } from './helpers';
|
||||
import { Panel } from './Panel';
|
||||
import { Splitter } from './Splitter';
|
||||
import { useDefaultValue } from './useDefaultValue';
|
||||
|
@ -109,17 +110,15 @@ const AppSplitterWrapper = forwardRef<AppSplitterRef, IAppSplitterProps>(
|
|||
className={cn('flex h-full w-full', isVertical ? 'flex-row' : 'flex-col', className)}
|
||||
style={style}
|
||||
>
|
||||
{mounted && (
|
||||
<AppSplitterBase
|
||||
{...props}
|
||||
ref={componentRef}
|
||||
isVertical={isVertical}
|
||||
containerRef={containerRef}
|
||||
splitterAutoSaveId={splitterAutoSaveId}
|
||||
split={split}
|
||||
calculatedInitialValue={initialValue}
|
||||
/>
|
||||
)}
|
||||
<AppSplitterBase
|
||||
{...props}
|
||||
ref={componentRef}
|
||||
isVertical={isVertical}
|
||||
containerRef={containerRef}
|
||||
splitterAutoSaveId={splitterAutoSaveId}
|
||||
split={split}
|
||||
calculatedInitialValue={initialValue}
|
||||
/>
|
||||
</div>
|
||||
</AppSplitterContext.Provider>
|
||||
);
|
||||
|
@ -285,33 +284,19 @@ const AppSplitterBase = forwardRef<
|
|||
return constrainedSize;
|
||||
});
|
||||
|
||||
// Calculate panel sizes with simplified logic
|
||||
const { leftSize, rightSize } = useMemo(() => {
|
||||
// Calculate preserved panel size - non-preserved panel will use flex-1
|
||||
const preservedPanelSize = useMemo(() => {
|
||||
const { containerSize, isAnimating, sizeSetByAnimation, isDragging, hasUserInteracted } =
|
||||
state;
|
||||
|
||||
if (!containerSize) {
|
||||
return { leftSize: 0, rightSize: 0 };
|
||||
}
|
||||
|
||||
// Handle hidden panels
|
||||
if (leftHidden && !rightHidden) return { leftSize: 0, rightSize: containerSize };
|
||||
if (rightHidden && !leftHidden) return { leftSize: containerSize, rightSize: 0 };
|
||||
if (leftHidden && rightHidden) return { leftSize: 0, rightSize: 0 };
|
||||
if (leftHidden || rightHidden) return 0;
|
||||
|
||||
const currentSize = savedLayout ?? 0;
|
||||
|
||||
// Check if a panel is at 0px and should remain at 0px
|
||||
const isLeftPanelZero = currentSize === 0 && preserveSide === 'left';
|
||||
const isRightPanelZero = currentSize === 0 && preserveSide === 'right';
|
||||
|
||||
// If a panel is at 0px, keep it at 0px and give all space to the other panel
|
||||
if (isLeftPanelZero) {
|
||||
return { leftSize: 0, rightSize: containerSize };
|
||||
}
|
||||
if (isRightPanelZero) {
|
||||
return { leftSize: containerSize, rightSize: 0 };
|
||||
}
|
||||
// Check if the preserved panel is at 0px
|
||||
const isPanelZero = currentSize === 0;
|
||||
if (isPanelZero) return 0;
|
||||
|
||||
// During animation or when size was set by animation (and not currently dragging),
|
||||
// don't apply constraints to allow smooth animations
|
||||
|
@ -319,17 +304,17 @@ const AppSplitterBase = forwardRef<
|
|||
!isAnimating && !sizeSetByAnimation && hasUserInteracted && !isDragging;
|
||||
|
||||
const finalSize = shouldApplyConstraints ? applyConstraints(currentSize) : currentSize;
|
||||
return Math.max(0, finalSize);
|
||||
}, [state, savedLayout, leftHidden, rightHidden, applyConstraints]);
|
||||
|
||||
// Determine panel sizes based on preserve side
|
||||
const { leftSize, rightSize } = useMemo(() => {
|
||||
if (preserveSide === 'left') {
|
||||
const left = Math.max(0, finalSize);
|
||||
const right = Math.max(0, containerSize - left);
|
||||
return { leftSize: left, rightSize: right };
|
||||
return { leftSize: preservedPanelSize, rightSize: 'auto' as const };
|
||||
} else {
|
||||
const right = Math.max(0, finalSize);
|
||||
const left = Math.max(0, containerSize - right);
|
||||
return { leftSize: left, rightSize: right };
|
||||
return { leftSize: 'auto' as const, rightSize: preservedPanelSize };
|
||||
}
|
||||
}, [state, savedLayout, leftHidden, rightHidden, preserveSide, applyConstraints]);
|
||||
}, [preservedPanelSize, preserveSide]);
|
||||
|
||||
// ================================
|
||||
// CONTAINER RESIZE HANDLING
|
||||
|
@ -507,18 +492,44 @@ const AppSplitterBase = forwardRef<
|
|||
const isSideClosed = useCallback(
|
||||
(side: 'left' | 'right') => {
|
||||
if (side === 'left') {
|
||||
return leftHidden || leftSize === 0;
|
||||
return (
|
||||
leftHidden ||
|
||||
leftSize === 0 ||
|
||||
(preserveSide === 'right' && preservedPanelSize === state.containerSize)
|
||||
);
|
||||
} else {
|
||||
return rightHidden || rightSize === 0;
|
||||
return (
|
||||
rightHidden ||
|
||||
rightSize === 0 ||
|
||||
(preserveSide === 'left' && preservedPanelSize === state.containerSize)
|
||||
);
|
||||
}
|
||||
},
|
||||
[leftHidden, rightHidden, leftSize, rightSize]
|
||||
[
|
||||
leftHidden,
|
||||
rightHidden,
|
||||
leftSize,
|
||||
rightSize,
|
||||
preserveSide,
|
||||
preservedPanelSize,
|
||||
state.containerSize,
|
||||
]
|
||||
);
|
||||
|
||||
// Get sizes in pixels
|
||||
const getSizesInPixels = useCallback((): [number, number] => {
|
||||
return [leftSize, rightSize];
|
||||
}, [leftSize, rightSize]);
|
||||
const containerSize = state.containerSize;
|
||||
|
||||
if (preserveSide === 'left') {
|
||||
const left = typeof leftSize === 'number' ? leftSize : 0;
|
||||
const right = containerSize - left;
|
||||
return [left, right];
|
||||
} else {
|
||||
const right = typeof rightSize === 'number' ? rightSize : 0;
|
||||
const left = containerSize - right;
|
||||
return [left, right];
|
||||
}
|
||||
}, [leftSize, rightSize, preserveSide, state.containerSize]);
|
||||
|
||||
// ================================
|
||||
// MOUSE EVENT HANDLERS
|
||||
|
@ -622,11 +633,14 @@ const AppSplitterBase = forwardRef<
|
|||
|
||||
// Determine if splitter should be hidden
|
||||
const shouldHideSplitter =
|
||||
hideSplitterProp || (leftHidden && rightHidden) || leftSize === 0 || rightSize === 0;
|
||||
hideSplitterProp || (leftHidden && rightHidden) || preservedPanelSize === 0;
|
||||
|
||||
const showSplitter = !leftHidden && !rightHidden;
|
||||
|
||||
const sizes: [string | number, string | number] = [`${leftSize}px`, `${rightSize}px`];
|
||||
const sizes: [string | number, string | number] =
|
||||
preserveSide === 'left'
|
||||
? [`${preservedPanelSize}px`, 'auto']
|
||||
: ['auto', `${preservedPanelSize}px`];
|
||||
|
||||
const content = (
|
||||
<>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
export type PanelSize = `${number}px` | `${number}%` | 'auto' | number;
|
||||
export type LayoutSize = [PanelSize, PanelSize];
|
||||
type PanelSizeWithAuto = Exclude<PanelSize, 'auto'>;
|
||||
export type LayoutSize = ['auto', PanelSizeWithAuto] | [PanelSizeWithAuto, 'auto'];
|
||||
|
||||
export interface IAppSplitterProps {
|
||||
/** Content to display in the left panel */
|
||||
|
@ -101,12 +102,6 @@ export interface IAppSplitterProps {
|
|||
|
||||
/** Additional CSS classes for the right panel */
|
||||
rightPanelClassName?: string;
|
||||
|
||||
/**
|
||||
* Whether to clear saved layout from cookies on initialization
|
||||
* Can be a boolean or a function that returns a boolean based on preserved side value and container width
|
||||
*/
|
||||
bustStorageOnInit?: boolean | ((preservedSideValue: number | null, refSize: number) => boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
const PREFIX = 'app-splitter';
|
||||
|
||||
export const createAutoSaveId = (id: string) => `${PREFIX}-${id}`;
|
|
@ -1,5 +1,3 @@
|
|||
export const createAutoSaveId = (id: string) => `app-splitter-${id}`;
|
||||
|
||||
// Helper function to convert size values to pixels
|
||||
export const sizeToPixels = (size: string | number, containerSize: number): number => {
|
||||
if (typeof size === 'number') {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
export { AppSplitter } from './AppSplitter';
|
||||
export type { AppSplitterRef, IAppSplitterProps, LayoutSize, PanelSize } from './AppSplitter.types';
|
||||
export { AppSplitterProvider, useAppSplitterContext } from './AppSplitterProvider';
|
||||
export { createAutoSaveId } from './helpers';
|
||||
export { createAutoSaveId } from './create-auto-save-id';
|
||||
export { Panel } from './Panel';
|
||||
export { Splitter } from './Splitter';
|
||||
|
|
|
@ -152,7 +152,6 @@ export function useCookieState<T>(
|
|||
value: JSON.parse(serializer(newState)),
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
console.log(key, storageData);
|
||||
setCookie(key, JSON.stringify(storageData), expirationTime, cookieOptions);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
import type React from 'react';
|
||||
import { AppLayout, type LayoutSize } from '@/components/ui/layouts/AppLayout';
|
||||
|
||||
export const PRIMARY_APP_LAYOUT_ID = 'app-layout';
|
||||
|
||||
const DEFAULT_LAYOUT: LayoutSize = ['230px', 'auto'];
|
||||
|
||||
interface IPrimaryAppLayoutProps {
|
||||
children: React.ReactNode;
|
||||
initialLayout: LayoutSize | null;
|
||||
}
|
||||
|
||||
export const PrimaryAppLayout: React.FC<IPrimaryAppLayoutProps> = ({ children, initialLayout }) => {
|
||||
return (
|
||||
<AppLayout
|
||||
autoSaveId={PRIMARY_APP_LAYOUT_ID}
|
||||
defaultLayout={DEFAULT_LAYOUT}
|
||||
initialLayout={initialLayout}
|
||||
sidebar={<div>Sidebar</div>}
|
||||
>
|
||||
{children}
|
||||
</AppLayout>
|
||||
);
|
||||
};
|
|
@ -1,11 +1,31 @@
|
|||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { AppSplitter } from '@/components/ui/layouts/AppSplitter/AppSplitter';
|
||||
import { createAutoSaveId } from '../components/ui/layouts/AppLayout';
|
||||
import { getAppLayout } from '../serverFns/getAppLayout';
|
||||
|
||||
export const Route = createFileRoute('/app/home')({
|
||||
component: RouteComponent,
|
||||
loader: async () => {
|
||||
const id = 'test0';
|
||||
const initialLayout = await getAppLayout({ data: { id } });
|
||||
return {
|
||||
initialLayout,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const { initialLayout } = Route.useLoaderData();
|
||||
return (
|
||||
<div className="bg-red-500 p-10 border border-purple-500 border-4">Hello "/app/home"!</div>
|
||||
<div className=" h-full">
|
||||
<AppSplitter
|
||||
preserveSide="left"
|
||||
defaultLayout={['230px', 'auto']}
|
||||
autoSaveId="test0"
|
||||
leftChildren={<div>Left</div>}
|
||||
rightChildren={<div>Right!!!!</div>}
|
||||
initialLayout={initialLayout}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router';
|
||||
import { AppProviders } from '../context/Providers';
|
||||
import { PRIMARY_APP_LAYOUT_ID, PrimaryAppLayout } from '../layouts/PrimaryAppLayout';
|
||||
import { getAppLayout } from '../serverFns/getAppLayout';
|
||||
|
||||
export const Route = createFileRoute('/app')({
|
||||
beforeLoad: async ({ context, location }) => {
|
||||
|
@ -14,10 +16,20 @@ export const Route = createFileRoute('/app')({
|
|||
throw redirect({ to: '/app/home' });
|
||||
}
|
||||
},
|
||||
|
||||
loader: async () => {
|
||||
const initialLayout = await getAppLayout({ data: { id: PRIMARY_APP_LAYOUT_ID } });
|
||||
return {
|
||||
initialLayout,
|
||||
};
|
||||
},
|
||||
component: () => {
|
||||
const { initialLayout } = Route.useLoaderData();
|
||||
return (
|
||||
<AppProviders>
|
||||
<Outlet />
|
||||
<PrimaryAppLayout initialLayout={initialLayout}>
|
||||
<Outlet />
|
||||
</PrimaryAppLayout>
|
||||
</AppProviders>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import { createServerFn } from '@tanstack/react-start';
|
||||
import { getCookie } from '@tanstack/react-start/server';
|
||||
import { z } from 'zod';
|
||||
import type { LayoutSize } from '../components/ui/layouts/AppLayout';
|
||||
import { createAutoSaveId } from '../components/ui/layouts/AppSplitter/create-auto-save-id';
|
||||
|
||||
export const getAppLayout = createServerFn({ method: 'GET' })
|
||||
.validator(
|
||||
z.object({
|
||||
// The id must not be prefixed with "app-splitter"
|
||||
id: z
|
||||
.string()
|
||||
.min(1, 'id is required')
|
||||
.refine((val) => !val.startsWith('app-splitter'), {
|
||||
message: 'id cannot be prefixed with "app-splitter"',
|
||||
}),
|
||||
preservedSide: z.enum(['left', 'right']).optional(),
|
||||
})
|
||||
)
|
||||
.handler<LayoutSize | null>(async ({ data: { id, preservedSide } }) => {
|
||||
const cookieName = createAutoSaveId(id);
|
||||
const cookieValue = getCookie(cookieName);
|
||||
|
||||
if (!cookieValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const { value } = JSON.parse(cookieValue) as { value: number };
|
||||
const isLeft = preservedSide !== 'right';
|
||||
const layout: LayoutSize = isLeft ? [`${value}px`, 'auto'] : ['auto', `${value}px`];
|
||||
|
||||
return layout;
|
||||
} catch (error) {
|
||||
console.error('Error parsing cookie value', error);
|
||||
return null;
|
||||
}
|
||||
});
|
|
@ -13,18 +13,7 @@ const config = defineConfig({
|
|||
tailwindcss(),
|
||||
tanstackStart({ customViteReactPlugin: true }),
|
||||
viteReact(),
|
||||
// Custom plugin to exclude test and stories files in dev mode
|
||||
{
|
||||
name: 'exclude-test-stories',
|
||||
resolveId(id) {
|
||||
// Exclude .test and .stories files from being resolved
|
||||
if (/\.(test|stories)\.(js|ts|jsx|tsx)$/.test(id)) {
|
||||
return { id, external: true };
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
!process.env.VITEST
|
||||
!process.env.VITEST && process.env.NODE_ENV !== 'development'
|
||||
? checker({
|
||||
typescript: true,
|
||||
biome: true,
|
||||
|
@ -41,11 +30,6 @@ const config = defineConfig({
|
|||
output: {
|
||||
// Force lodash and lodash-es into a dedicated vendor chunk
|
||||
manualChunks(id) {
|
||||
// Skip chunking for test and stories files (they should be excluded anyway)
|
||||
if (/\.(test|stories)\.(js|ts|jsx|tsx)$/.test(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (id.includes('node_modules/lodash')) {
|
||||
return 'vendor-lodash';
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue