feat(calendar): improve navigation layout controls and sidebar integration

- Add layout control icons (sidebar/bottom-bar) for better UX
- Position FABs on right side for consistent minimization direction
- Fix app switcher dropdown direction (opens up when nav at bottom)
- Add isToolbarCollapsed store for toolbar state management
- Create CalendarToolbarContent component for reusable toolbar elements
- Improve sidebar toolbar integration with proper vertical layout
- Hide PillViewSwitcher sliding indicator in vertical mode
- Add scrollbar to sidebar toolbar content
- Ensure all toolbar buttons are full-width and left-aligned in sidebar

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-12-12 13:13:45 +01:00
parent 287a62a3fb
commit 81f77c424c
5 changed files with 261 additions and 10 deletions

View file

@ -0,0 +1,195 @@
<script lang="ts">
import { viewStore } from '$lib/stores/view.svelte';
import { settingsStore } from '$lib/stores/settings.svelte';
import type { CalendarViewType } from '@calendar/shared';
import {
PillToolbarButton,
PillToolbarDivider,
PillTimeRangeSelector,
PillViewSwitcher,
} from '@manacore/shared-ui';
import PillCalendarSelector from './PillCalendarSelector.svelte';
interface Props {
vertical?: boolean;
}
let { vertical = false }: Props = $props();
// View type labels
const viewLabels: Record<CalendarViewType, string> = {
day: 'Tag',
'5day': '5 Tage',
week: 'Woche',
'10day': '10 Tage',
'14day': '14 Tage',
month: 'Monat',
year: 'Jahr',
agenda: 'Agenda',
};
// Views to show in selector
const visibleViews: CalendarViewType[] = [
'day',
'5day',
'week',
'10day',
'14day',
'month',
'year',
];
// Convert to ViewOptions for PillViewSwitcher
const viewOptions = visibleViews.map((type) => ({
id: type,
label: viewLabels[type],
title: viewLabels[type],
}));
// Hours change handlers
function handleStartHourChange(hour: number) {
settingsStore.set('dayStartHour', hour);
}
function handleEndHourChange(hour: number) {
settingsStore.set('dayEndHour', hour);
}
function handleViewChange(type: string) {
viewStore.setViewType(type as CalendarViewType);
}
</script>
<div class="toolbar-content" class:vertical>
<!-- Calendar selector -->
<PillCalendarSelector direction={vertical ? 'down' : 'up'} embedded={true} />
{#if !vertical}
<PillToolbarDivider />
{/if}
<!-- Weekdays filter -->
<PillToolbarButton
onclick={() => settingsStore.set('showOnlyWeekdays', !settingsStore.showOnlyWeekdays)}
active={settingsStore.showOnlyWeekdays}
title="Nur Wochentage anzeigen (Mo-Fr)"
>
Mo-Fr
</PillToolbarButton>
<!-- Hours filter with time range selector -->
<PillTimeRangeSelector
startHour={settingsStore.dayStartHour}
endHour={settingsStore.dayEndHour}
onStartHourChange={handleStartHourChange}
onEndHourChange={handleEndHourChange}
direction={vertical ? 'down' : 'up'}
embedded={true}
toggleMode={true}
active={settingsStore.filterHoursEnabled}
onToggle={() => settingsStore.set('filterHoursEnabled', !settingsStore.filterHoursEnabled)}
labelFormat="range"
/>
{#if !vertical}
<PillToolbarDivider />
{/if}
<!-- View selector -->
<PillViewSwitcher
options={viewOptions}
value={viewStore.viewType}
onChange={handleViewChange}
primaryColor="#3b82f6"
embedded={true}
/>
</div>
<style>
.toolbar-content {
display: flex;
align-items: center;
gap: 0.25rem;
}
.toolbar-content.vertical {
flex-direction: column;
align-items: stretch;
gap: 0.5rem;
width: 100%;
}
/* All elements in vertical mode - full width, left aligned */
.toolbar-content.vertical :global(.pill-toolbar-btn),
.toolbar-content.vertical :global(.pill-dropdown .trigger-button),
.toolbar-content.vertical :global(button) {
width: 100%;
justify-content: flex-start;
text-align: left;
}
/* PillViewSwitcher in vertical mode */
.toolbar-content.vertical :global(.pill-view-switcher) {
flex-direction: column;
gap: 0.25rem;
padding: 0;
background: transparent;
border: none;
box-shadow: none;
width: 100%;
}
/* Hide the sliding indicator in vertical mode */
.toolbar-content.vertical :global(.pill-view-switcher .sliding-indicator) {
display: none;
}
.toolbar-content.vertical :global(.pill-view-switcher .switcher-btn) {
width: 100%;
justify-content: flex-start;
padding: 0.5rem 0.875rem;
border-radius: 9999px;
background: transparent;
border: 1px solid transparent;
}
.toolbar-content.vertical :global(.pill-view-switcher .switcher-btn:hover) {
background: rgba(0, 0, 0, 0.05);
}
:global(.dark) .toolbar-content.vertical :global(.pill-view-switcher .switcher-btn:hover) {
background: rgba(255, 255, 255, 0.1);
}
.toolbar-content.vertical :global(.pill-view-switcher .switcher-btn.active) {
background: color-mix(in srgb, #3b82f6 15%, transparent 85%);
border-color: color-mix(in srgb, #3b82f6 25%, transparent 75%);
}
:global(.dark) .toolbar-content.vertical :global(.pill-view-switcher .switcher-btn.active) {
background: color-mix(in srgb, #3b82f6 25%, transparent 75%);
border-color: color-mix(in srgb, #3b82f6 35%, transparent 65%);
}
/* PillTimeRangeSelector in vertical mode */
.toolbar-content.vertical :global(.pill-time-range-selector),
.toolbar-content.vertical :global(.pill-dropdown) {
width: 100%;
}
.toolbar-content.vertical :global(.pill-time-range-selector .trigger-button),
.toolbar-content.vertical :global(.pill-dropdown .trigger-button) {
width: 100%;
justify-content: flex-start;
}
/* PillCalendarSelector in vertical mode */
.toolbar-content.vertical :global(.calendar-selector) {
width: 100%;
}
.toolbar-content.vertical :global(.calendar-selector .trigger-button) {
width: 100%;
justify-content: flex-start;
}
</style>

View file

@ -2,3 +2,4 @@ import { writable } from 'svelte/store';
export const isSidebarMode = writable(false);
export const isNavCollapsed = writable(false);
export const isToolbarCollapsed = writable(false);

View file

@ -1191,37 +1191,85 @@
flex-direction: column;
align-items: stretch;
gap: 0.5rem;
width: 100%;
}
/* All buttons in sidebar toolbar - full width, left aligned */
.sidebar-toolbar-content :global(.pill-toolbar-btn),
.sidebar-toolbar-content :global(.pill-dropdown .trigger-button) {
.sidebar-toolbar-content :global(.pill-dropdown .trigger-button),
.sidebar-toolbar-content :global(button) {
width: 100%;
justify-content: flex-start;
text-align: left;
background: transparent;
border: 1px solid transparent;
box-shadow: none;
}
.sidebar-toolbar-content :global(.pill-toolbar-btn:hover),
.sidebar-toolbar-content :global(.pill-dropdown .trigger-button:hover) {
.sidebar-toolbar-content :global(.pill-dropdown .trigger-button:hover),
.sidebar-toolbar-content :global(button:hover) {
background: rgba(0, 0, 0, 0.05);
}
:global(.dark) .sidebar-toolbar-content :global(.pill-toolbar-btn:hover),
:global(.dark) .sidebar-toolbar-content :global(.pill-dropdown .trigger-button:hover) {
:global(.dark) .sidebar-toolbar-content :global(.pill-dropdown .trigger-button:hover),
:global(.dark) .sidebar-toolbar-content :global(button:hover) {
background: rgba(255, 255, 255, 0.1);
}
/* Style for PillViewSwitcher in sidebar */
/* Style for PillViewSwitcher in sidebar - vertical layout */
.sidebar-toolbar-content :global(.pill-view-switcher) {
flex-direction: column;
gap: 0.25rem;
width: 100%;
padding: 0;
background: transparent;
border: none;
box-shadow: none;
}
.sidebar-toolbar-content :global(.pill-view-switcher .view-option) {
/* Hide the sliding indicator in vertical mode */
.sidebar-toolbar-content :global(.pill-view-switcher .sliding-indicator) {
display: none;
}
.sidebar-toolbar-content :global(.pill-view-switcher .switcher-btn) {
width: 100%;
justify-content: flex-start;
padding: 0.5rem 0.875rem;
border-radius: 9999px;
background: transparent;
border: 1px solid transparent;
}
.sidebar-toolbar-content :global(.pill-view-switcher .switcher-btn:hover) {
background: rgba(0, 0, 0, 0.05);
}
:global(.dark) .sidebar-toolbar-content :global(.pill-view-switcher .switcher-btn:hover) {
background: rgba(255, 255, 255, 0.1);
}
.sidebar-toolbar-content :global(.pill-view-switcher .switcher-btn.active) {
background: color-mix(in srgb, var(--pill-primary-color, #3b82f6) 15%, transparent 85%);
border-color: color-mix(in srgb, var(--pill-primary-color, #3b82f6) 25%, transparent 75%);
}
:global(.dark) .sidebar-toolbar-content :global(.pill-view-switcher .switcher-btn.active) {
background: color-mix(in srgb, var(--pill-primary-color, #3b82f6) 25%, transparent 75%);
border-color: color-mix(in srgb, var(--pill-primary-color, #3b82f6) 35%, transparent 65%);
}
/* PillTimeRangeSelector in sidebar */
.sidebar-toolbar-content :global(.pill-time-range-selector),
.sidebar-toolbar-content :global(.pill-dropdown) {
width: 100%;
}
/* PillCalendarSelector in sidebar */
.sidebar-toolbar-content :global(.calendar-selector) {
width: 100%;
}
/* Mobile: Sidebar container adjustments */

View file

@ -50,6 +50,7 @@
.pill-toolbar.position-bottom {
top: auto;
bottom: var(--toolbar-bottom-offset, 70px);
transition: bottom 0.3s ease;
}
/* Mobile: always position above bottom nav */

View file

@ -57,6 +57,8 @@
appIcon?: string;
primaryColor?: string;
autoFocus?: boolean;
/** Bottom offset from viewport bottom (default: '70px') */
bottomOffset?: string;
}
let {
@ -72,6 +74,7 @@
appIcon = 'search',
primaryColor = '#8b5cf6',
autoFocus = true,
bottomOffset = '70px',
}: Props = $props();
let searchQuery = $state('');
@ -239,7 +242,10 @@
}
</script>
<div class="quick-input-bar" style="--primary-color: {primaryColor}">
<div
class="quick-input-bar"
style="--primary-color: {primaryColor}; --bottom-offset: {bottomOffset}"
>
<!-- Results Panel (above input) -->
{#if showPanel}
<div class="results-panel" transition:slide={{ duration: 150 }}>
@ -411,15 +417,15 @@
<style>
.quick-input-bar {
position: fixed;
top: 0;
bottom: calc(var(--bottom-offset, 70px) + env(safe-area-inset-bottom, 0px));
left: 0;
right: 0;
z-index: 90;
padding: 0.75rem 1rem;
padding-top: calc(0.75rem + env(safe-area-inset-top));
pointer-events: none;
/* Fixed height to prevent layout shift when results appear */
height: 72px;
transition: bottom 0.3s ease;
}
.input-container,
@ -588,11 +594,11 @@
/* Results Panel */
.results-panel {
position: absolute;
top: 100%;
bottom: 100%;
left: 1rem;
right: 1rem;
max-width: 700px;
margin: 0.5rem auto 0;
margin: 0 auto 0.5rem;
max-height: 320px;
overflow-y: auto;
background: rgba(255, 255, 255, 0.95);