feat: photostream can have scrollbar, style options, standardize small/large layout sizes

- Add configurable header height props for responsive layouts
  (smallHeaderHeight/largeHeaderHeight)
  - Add style customization props: styleMarginContentHorizontal,
  styleMarginTop, alwaysShowScrollbar
  - Replace hardcoded layout values with configurable props
  - Change root element from <section> to custom <photostream> element for
  better semantic structure
  - Move viewport width binding to inner timeline element for more accurate
  measurements
  - Simplify HMR handler by removing file-specific checks
  - Add segment loading check to prevent rendering unloaded segments
  - Add spacing margin between month groups using layout options
  - Change scrollbar-width from 'auto' to 'thin' for consistency
  - Remove unused UpdatePayload type import
This commit is contained in:
midzelis 2025-09-25 14:26:23 +00:00
parent 1b60c9d32f
commit 79adb016e8

View file

@ -8,7 +8,6 @@
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { mobileDevice } from '$lib/stores/mobile-device.svelte'; import { mobileDevice } from '$lib/stores/mobile-device.svelte';
import { onMount, type Snippet } from 'svelte'; import { onMount, type Snippet } from 'svelte';
import type { UpdatePayload } from 'vite';
interface Props { interface Props {
segment: Snippet< segment: Snippet<
@ -35,6 +34,7 @@
enableRouting: boolean; enableRouting: boolean;
timelineManager: PhotostreamManager; timelineManager: PhotostreamManager;
alwaysShowScrollbar?: boolean;
showSkeleton?: boolean; showSkeleton?: boolean;
isShowDeleteConfirmation?: boolean; isShowDeleteConfirmation?: boolean;
styleMarginRightOverride?: string; styleMarginRightOverride?: string;
@ -43,6 +43,18 @@
children?: Snippet; children?: Snippet;
empty?: Snippet; empty?: Snippet;
handleTimelineScroll?: () => void; handleTimelineScroll?: () => void;
smallHeaderHeight?: {
rowHeight: number;
headerHeight: number;
};
largeHeaderHeight?: {
rowHeight: number;
headerHeight: number;
};
styleMarginContentHorizontal?: string;
styleMarginTop?: string;
} }
let { let {
@ -51,14 +63,27 @@
enableRouting, enableRouting,
timelineManager = $bindable(), timelineManager = $bindable(),
showSkeleton = $bindable(true), showSkeleton = $bindable(true),
styleMarginRightOverride,
isShowDeleteConfirmation = $bindable(false),
showScrollbar, showScrollbar,
styleMarginRightOverride,
styleMarginContentHorizontal = '0px',
styleMarginTop = '0px',
alwaysShowScrollbar,
isShowDeleteConfirmation = $bindable(false),
children, children,
skeleton, skeleton,
empty, empty,
header, header,
handleTimelineScroll = () => {}, handleTimelineScroll = () => {},
smallHeaderHeight = {
rowHeight: 100,
headerHeight: 32,
},
largeHeaderHeight = {
rowHeight: 235,
headerHeight: 48,
},
}: Props = $props(); }: Props = $props();
let { gridScrollTarget } = assetViewingStore; let { gridScrollTarget } = assetViewingStore;
@ -70,15 +95,7 @@
const isEmpty = $derived(timelineManager.isInitialized && timelineManager.months.length === 0); const isEmpty = $derived(timelineManager.isInitialized && timelineManager.months.length === 0);
$effect(() => { $effect(() => {
const layoutOptions = maxMd const layoutOptions = maxMd ? smallHeaderHeight : largeHeaderHeight;
? {
rowHeight: 100,
headerHeight: 32,
}
: {
rowHeight: 235,
headerHeight: 48,
};
timelineManager.setLayoutOptions(layoutOptions); timelineManager.setLayoutOptions(layoutOptions);
}); });
@ -173,7 +190,7 @@
</script> </script>
<HotModuleReload <HotModuleReload
onAfterUpdate={(payload: UpdatePayload) => { onAfterUpdate={() => {
// when hmr happens, skeleton is initialized to true by default // when hmr happens, skeleton is initialized to true by default
// normally, loading asset-grid is part of a navigation event, and the completion of // normally, loading asset-grid is part of a navigation event, and the completion of
// that event triggers a scroll-to-asset, if necessary, when then clears the skeleton. // that event triggers a scroll-to-asset, if necessary, when then clears the skeleton.
@ -186,38 +203,41 @@
} }
void completeAfterNavigate({ scrollToAssetQueryParam: true }); void completeAfterNavigate({ scrollToAssetQueryParam: true });
}; };
const assetGridUpdate = payload.updates.some((update) => update.path.endsWith('Photostream.svelte'));
if (assetGridUpdate) { // wait 500ms for the update to be fully swapped in
// wait 500ms for the update to be fully swapped in setTimeout(finishHmr, 500);
setTimeout(finishHmr, 500);
}
}} }}
/> />
{@render header?.(scrollTo)} {@render header?.(scrollTo)}
<!-- Right margin MUST be equal to the width of scrubber --> <!-- Right margin MUST be equal to the width of scrubber -->
<section <photostream
id="asset-grid" id="asset-grid"
class={[ class={[
'h-full overflow-y-auto outline-none', 'overflow-y-auto outline-none',
{ 'scrollbar-hidden': !showScrollbar }, { 'scrollbar-hidden': !showScrollbar },
{ 'overflow-y-scroll': alwaysShowScrollbar },
{ 'm-0': isEmpty }, { 'm-0': isEmpty },
{ 'ms-0': !isEmpty }, { 'ms-0': !isEmpty },
]} ]}
style:height={`calc(100% - ${styleMarginTop})`}
style:margin-top={styleMarginTop}
style:margin-right={styleMarginRightOverride} style:margin-right={styleMarginRightOverride}
style:scrollbar-width={showScrollbar ? 'auto' : 'none'} style:scrollbar-width={showScrollbar ? 'thin' : 'none'}
tabindex="-1" tabindex="-1"
bind:clientHeight={timelineManager.viewportHeight} bind:clientHeight={timelineManager.viewportHeight}
bind:clientWidth={null, (v: number) => ((timelineManager.viewportWidth = v), updateSlidingWindow())}
bind:this={element} bind:this={element}
onscroll={() => (handleTimelineScroll(), updateSlidingWindow(), updateIsScrolling())} onscroll={() => (handleTimelineScroll(), updateSlidingWindow(), updateIsScrolling())}
> >
<section <section
bind:this={timelineElement} bind:this={timelineElement}
id="virtual-timeline" id="virtual-timeline"
style:margin-left={styleMarginContentHorizontal}
style:margin-right={styleMarginContentHorizontal}
class:invisible={showSkeleton} class:invisible={showSkeleton}
style:height={timelineManager.timelineHeight + 'px'} style:height={timelineManager.timelineHeight + 'px'}
bind:clientWidth={null, (v: number) => ((timelineManager.viewportWidth = v), updateSlidingWindow())}
> >
<section <section
use:resizeObserver={topSectionResizeObserver} use:resizeObserver={topSectionResizeObserver}
@ -234,13 +254,15 @@
</section> </section>
{#each timelineManager.months as monthGroup (monthGroup.id)} {#each timelineManager.months as monthGroup (monthGroup.id)}
{@const shouldDisplay = monthGroup.intersecting} {@const shouldDisplay = monthGroup.intersecting && monthGroup.isLoaded}
{@const absoluteHeight = monthGroup.top} {@const absoluteHeight = monthGroup.top}
<div <div
class="month-group" class="month-group"
style:height={monthGroup.height + 'px'} style:margin-bottom={timelineManager.createLayoutOptions().spacing + 'px'}
style:position="absolute" style:position="absolute"
style:transform={`translate3d(0,${absoluteHeight}px,0)`} style:transform={`translate3d(0,${absoluteHeight}px,0)`}
style:height={`${monthGroup.height}px`}
style:width="100%" style:width="100%"
> >
{#if !shouldDisplay} {#if !shouldDisplay}
@ -263,9 +285,12 @@
style:transform={`translate3d(0,${timelineManager.timelineHeight}px,0)`} style:transform={`translate3d(0,${timelineManager.timelineHeight}px,0)`}
></div> ></div>
</section> </section>
</section> </photostream>
<style> <style>
photostream {
display: block;
}
#asset-grid { #asset-grid {
contain: strict; contain: strict;
scrollbar-width: none; scrollbar-width: none;