Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
277 changes: 277 additions & 0 deletions packages/panel/public/build/assets/app-9cG5AHLo.js

Large diffs are not rendered by default.

272 changes: 0 additions & 272 deletions packages/panel/public/build/assets/app-C3gs_Or0.js

This file was deleted.

1 change: 0 additions & 1 deletion packages/panel/public/build/assets/app-DrcNYi_n.css

This file was deleted.

1 change: 1 addition & 0 deletions packages/panel/public/build/assets/app-XBH6bAy4.css

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions packages/panel/public/build/manifest.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 49 additions & 0 deletions packages/panel/resources/js/components/StickySidebar.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import StickySidebar from './StickySidebar.vue';

const mountAt = (sidebarHeight: number, viewportHeight: number) => {
Object.defineProperty(window, 'innerHeight', { value: viewportHeight, configurable: true });
vi.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockReturnValue(sidebarHeight);

return mount(StickySidebar, { slots: { default: '<p>card</p>' } });
};

describe('StickySidebar', () => {
beforeEach(() => {
vi.stubGlobal('ResizeObserver', undefined);
});

afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
});

it('pins below the breadcrumb bar when it fits in the viewport', async () => {
const wrapper = mountAt(400, 900);
await nextTick();

expect(wrapper.attributes('style')).toContain('top: 60px');
expect(wrapper.classes()).toContain('lg:sticky');
});

it('sticks by its bottom edge when taller than the viewport', async () => {
const wrapper = mountAt(1500, 900);
await nextTick();

// 900 - 1500 - 16: the sidebar scrolls with the page until its end is in view.
expect(wrapper.attributes('style')).toContain('top: -616px');
});

it('re-measures when the window resizes', async () => {
const wrapper = mountAt(1500, 900);
await nextTick();

Object.defineProperty(window, 'innerHeight', { value: 2000, configurable: true });
window.dispatchEvent(new Event('resize'));
await nextTick();

expect(wrapper.attributes('style')).toContain('top: 60px');
});
});
48 changes: 48 additions & 0 deletions packages/panel/resources/js/components/StickySidebar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';

// A sidebar column that scrolls with the page and pins when it can. Shorter
// than the viewport, it sticks below the breadcrumb bar like before. Taller,
// it scrolls with the page straight away and sticks when its own end reaches
// the bottom of the viewport, so cards added by add-ons are reachable without
// scrolling the main column to its end. A negative sticky `top` is what makes
// the second case work; the value tracks the sidebar's own height.
const props = withDefaults(defineProps<{ offset?: number; gap?: number }>(), { offset: 60, gap: 16 });

const element = ref<HTMLElement | null>(null);
const height = ref(0);
const viewport = ref(0);
let observer: ResizeObserver | undefined;

const measure = (): void => {
height.value = element.value?.offsetHeight ?? 0;
viewport.value = window.innerHeight;
};

onMounted(() => {
measure();
window.addEventListener('resize', measure);

if (typeof ResizeObserver !== 'undefined' && element.value) {
observer = new ResizeObserver(measure);
observer.observe(element.value);
}
});

onBeforeUnmount(() => {
window.removeEventListener('resize', measure);
observer?.disconnect();
});

const top = computed(() => {
const fits = height.value + props.offset + props.gap <= viewport.value;

return `${fits ? props.offset : viewport.value - height.value - props.gap}px`;
});
</script>

<template>
<div ref="element" class="lg:sticky" :style="{ top }" data-testid="sticky-sidebar">
<slot />
</div>
</template>
5 changes: 3 additions & 2 deletions packages/panel/resources/js/pages/brands/Edit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import PageHeader from '../../components/PageHeader.vue';
import PageZone from '../../components/PageZone.vue';
import Section from '../../components/Section.vue';
import SideCard from '../../components/SideCard.vue';
import StickySidebar from '../../components/StickySidebar.vue';
import StatusBadge from '../../components/StatusBadge.vue';
import StatusSegmentedControl from '../../components/StatusSegmentedControl.vue';
import TextInput from '../../components/TextInput.vue';
Expand Down Expand Up @@ -259,7 +260,7 @@ const timelineEvents = computed(() =>

<!-- Sidebar -->
<aside>
<div class="lg:sticky lg:top-[60px] flex flex-col gap-4">
<StickySidebar class="flex flex-col gap-4">
<SideCard :title="t('brands.side_status')">
<StatusSegmentedControl v-model="details.status" :options="statusOptions" />
<div class="text-[11.5px] text-ink-500 mt-2.5">
Expand Down Expand Up @@ -302,7 +303,7 @@ const timelineEvents = computed(() =>
</SideCard>

<PageZone region="sidebar" position="after" :brand="brand" />
</div>
</StickySidebar>
</aside>
</div>
</div>
Expand Down
5 changes: 3 additions & 2 deletions packages/panel/resources/js/pages/collections/Edit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import ProductPickerDialog from '../../components/ProductPickerDialog.vue';
import Section from '../../components/Section.vue';
import Select from '../../components/Select.vue';
import SideCard from '../../components/SideCard.vue';
import StickySidebar from '../../components/StickySidebar.vue';
import StatusBadge from '../../components/StatusBadge.vue';
import StatusSegmentedControl from '../../components/StatusSegmentedControl.vue';
import TextInput from '../../components/TextInput.vue';
Expand Down Expand Up @@ -517,7 +518,7 @@ const timelineEvents = computed(() =>

<!-- Sidebar -->
<aside>
<div class="lg:sticky lg:top-[60px] flex flex-col gap-4">
<StickySidebar class="flex flex-col gap-4">
<SideCard :title="t('collections.side_status')">
<StatusSegmentedControl v-model="details.status" :options="statusOptions" />
<div class="text-[11.5px] text-ink-500 mt-2.5">{{ statusHelp }}</div>
Expand Down Expand Up @@ -555,7 +556,7 @@ const timelineEvents = computed(() =>
</SideCard>

<PageZone region="sidebar" position="after" :collection="collection" />
</div>
</StickySidebar>
</aside>
</div>
</div>
Expand Down
5 changes: 3 additions & 2 deletions packages/panel/resources/js/pages/customers/Edit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import PageZone from '../../components/PageZone.vue';
import Section from '../../components/Section.vue';
import Select from '../../components/Select.vue';
import SideCard from '../../components/SideCard.vue';
import StickySidebar from '../../components/StickySidebar.vue';
import StatusBadge from '../../components/StatusBadge.vue';
import Tabs from '../../components/Tabs.vue';
import Textarea from '../../components/Textarea.vue';
Expand Down Expand Up @@ -795,7 +796,7 @@ const tabDefs = computed(() => [
</div>

<aside>
<div class="lg:sticky lg:top-[60px] flex flex-col gap-0">
<StickySidebar class="flex flex-col gap-0">
<SideCard :title="t('customers.at_a_glance')">
<div class="flex flex-col gap-2 text-[12px]">
<div class="flex items-center justify-between gap-2">
Expand Down Expand Up @@ -897,7 +898,7 @@ const tabDefs = computed(() => [
<p v-else class="m-0 text-[12px] text-ink-500 italic">{{ t('customers.notes_empty') }}</p>
</template>
</SideCard>
</div>
</StickySidebar>
</aside>
</div>
</div>
Expand Down
5 changes: 3 additions & 2 deletions packages/panel/resources/js/pages/product-types/Edit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import PageZone from '../../components/PageZone.vue';
import Section from '../../components/Section.vue';
import Select from '../../components/Select.vue';
import SideCard from '../../components/SideCard.vue';
import StickySidebar from '../../components/StickySidebar.vue';
import StatusBadge from '../../components/StatusBadge.vue';
import StatusSegmentedControl from '../../components/StatusSegmentedControl.vue';
import Textarea from '../../components/Textarea.vue';
Expand Down Expand Up @@ -251,7 +252,7 @@ const timelineEvents = computed(() =>

<!-- Sidebar -->
<aside>
<div class="lg:sticky lg:top-[60px] flex flex-col gap-4">
<StickySidebar class="flex flex-col gap-4">
<SideCard :title="t('product-types.side_status')">
<StatusSegmentedControl v-model="details.status" :options="statusOptions" />
<div class="text-[11.5px] text-ink-500 mt-2.5">
Expand Down Expand Up @@ -291,7 +292,7 @@ const timelineEvents = computed(() =>
</SideCard>

<PageZone region="sidebar" position="after" :product-type="productType" />
</div>
</StickySidebar>
</aside>
</div>
</div>
Expand Down
5 changes: 3 additions & 2 deletions packages/panel/resources/js/pages/products/Edit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import ProductPickerDialog, { type ProductOption } from '../../components/Produc
import Section from '../../components/Section.vue';
import Select from '../../components/Select.vue';
import SideCard from '../../components/SideCard.vue';
import StickySidebar from '../../components/StickySidebar.vue';
import StatusBadge from '../../components/StatusBadge.vue';
import StatusSegmentedControl from '../../components/StatusSegmentedControl.vue';
import TagsInput from '../../components/TagsInput.vue';
Expand Down Expand Up @@ -688,7 +689,7 @@ const timelineEvents = computed(() =>

<!-- Sidebar -->
<aside>
<div class="lg:sticky lg:top-[60px] flex flex-col gap-4">
<StickySidebar class="flex flex-col gap-4">
<PageZone region="sidebar" position="before" :product="product" />

<SideCard :title="t('products.side_status')">
Expand Down Expand Up @@ -744,7 +745,7 @@ const timelineEvents = computed(() =>
</SideCard>

<PageZone region="sidebar" position="after" :product="product" />
</div>
</StickySidebar>
</aside>
</div>
</div>
Expand Down
5 changes: 3 additions & 2 deletions packages/panel/resources/js/pages/products/VariantEdit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import PageZone from '../../components/PageZone.vue';
import PricingEditor, { type CurrencyOption, type PriceRow } from '../../components/PricingEditor.vue';
import ShippingCard from '../../components/ShippingCard.vue';
import SideCard from '../../components/SideCard.vue';
import StickySidebar from '../../components/StickySidebar.vue';
import StatusBadge from '../../components/StatusBadge.vue';
import TaxCard from '../../components/TaxCard.vue';
import Toggle from '../../components/Toggle.vue';
Expand Down Expand Up @@ -264,7 +265,7 @@ const timelineEvents = computed(() =>

<!-- Sidebar -->
<aside>
<div class="lg:sticky lg:top-[60px] flex flex-col gap-4">
<StickySidebar class="flex flex-col gap-4">
<SideCard :title="t('products.side_variant_status')">
<div class="flex items-center gap-2.5">
<Toggle :on="enabled" @toggle="toggleEnabled" />
Expand Down Expand Up @@ -304,7 +305,7 @@ const timelineEvents = computed(() =>
</SideCard>

<PageZone region="sidebar" position="after" :variant="variant" />
</div>
</StickySidebar>
</aside>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import FieldLabel from '../../../components/FieldLabel.vue';
import Icon from '../../../components/Icon.vue';
import Section from '../../../components/Section.vue';
import SideCard from '../../../components/SideCard.vue';
import StickySidebar from '../../../components/StickySidebar.vue';
import StatusBadge from '../../../components/StatusBadge.vue';
import SwatchInput from '../../../components/SwatchInput.vue';
import TextInput from '../../../components/TextInput.vue';
Expand Down Expand Up @@ -352,7 +353,7 @@ const formatDate = (iso: string): string => (iso ? new Date(iso).toLocaleDateStr
</div>

<aside>
<div class="lg:sticky lg:top-[60px] flex flex-col gap-4">
<StickySidebar class="flex flex-col gap-4">
<SideCard :title="t('product_options.side_usage')">
<div class="flex items-baseline gap-1.5">
<span class="text-2xl font-semibold tracking-[-0.02em] text-ink-900 [font-variant-numeric:tabular-nums]">{{ productOption.products_count }}</span>
Expand Down Expand Up @@ -384,7 +385,7 @@ const formatDate = (iso: string): string => (iso ? new Date(iso).toLocaleDateStr
<ActivityTimeline v-if="activities.length" :events="timelineEvents" :reverse="false" />
<div v-else class="text-[11.5px] text-ink-500">{{ t('product_options.side_activity_empty') }}</div>
</SideCard>
</div>
</StickySidebar>
</aside>
</div>
</SettingsShell>
Expand Down
1 change: 1 addition & 0 deletions packages/panel/resources/js/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export { default as Slideout } from './components/Slideout.vue';
export { default as ConfirmDialog } from './components/ConfirmDialog.vue';
export { default as Tooltip } from './components/Tooltip.vue';
export { default as SideCard } from './components/SideCard.vue';
export { default as StickySidebar } from './components/StickySidebar.vue';
export { default as Tabs } from './components/Tabs.vue';

// Edit drafts
Expand Down
1 change: 1 addition & 0 deletions packages/panel/resources/panel-package/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export const Slideout = ui().Slideout;
export const ConfirmDialog = ui().ConfirmDialog;
export const Tooltip = ui().Tooltip;
export const SideCard = ui().SideCard;
export const StickySidebar = ui().StickySidebar;
export const Tabs = ui().Tabs;
export const ValuePreviewChip = ui().ValuePreviewChip;

Expand Down
Loading