From affcfe4614727f7d4d44532ab7516a3a6e6c9382 Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Fri, 13 Feb 2026 22:42:11 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(calendar):=20integrate=20TagSt?= =?UTF-8?q?rip=20into=20PillNavigation=20dropdown?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add PillTagSelector component for tag selection in navigation - Remove separate TagStrip bar (saves 70px vertical space) - Add tag-selector support to PillNavigation element rendering - Remove hasTagStrip prop from DateStrip/DateStripFab components - Export PillTagSelector and types from shared-ui Co-Authored-By: Claude Opus 4.5 --- .../lib/components/calendar/DateStrip.svelte | 13 +- .../components/calendar/DateStripFab.svelte | 21 +- .../apps/web/src/routes/(app)/+layout.svelte | 134 +---- packages/shared-ui/src/index.ts | 3 + .../src/navigation/PillNavigation.svelte | 34 +- .../src/navigation/PillTagSelector.svelte | 516 ++++++++++++++++++ packages/shared-ui/src/navigation/index.ts | 3 + packages/shared-ui/src/navigation/types.ts | 30 +- 8 files changed, 615 insertions(+), 139 deletions(-) create mode 100644 packages/shared-ui/src/navigation/PillTagSelector.svelte diff --git a/apps/calendar/apps/web/src/lib/components/calendar/DateStrip.svelte b/apps/calendar/apps/web/src/lib/components/calendar/DateStrip.svelte index c52232dd9..e8fff9c34 100644 --- a/apps/calendar/apps/web/src/lib/components/calendar/DateStrip.svelte +++ b/apps/calendar/apps/web/src/lib/components/calendar/DateStrip.svelte @@ -28,10 +28,9 @@ interface Props { isToolbarExpanded?: boolean; - hasTagStrip?: boolean; // Whether TagStrip is visible below } - let { isToolbarExpanded = false, hasTagStrip = false }: Props = $props(); + let { isToolbarExpanded = false }: Props = $props(); // Get event count for a day (max 5 dots displayed) function getEventCount(date: Date): number { @@ -246,7 +245,6 @@ class="date-strip-wrapper" class:toolbar-expanded={isToolbarExpanded} class:compact={settingsStore.dateStripCompact} - class:has-tag-strip={hasTagStrip} >
@@ -338,15 +336,6 @@ bottom: calc(210px + env(safe-area-inset-bottom, 0px)); /* Extra space for toolbar */ } - /* When TagStrip is visible below, add extra offset */ - .date-strip-wrapper.has-tag-strip { - bottom: calc(210px + env(safe-area-inset-bottom, 0px)); /* +70px for TagStrip */ - } - - .date-strip-wrapper.has-tag-strip.toolbar-expanded { - bottom: calc(280px + env(safe-area-inset-bottom, 0px)); - } - .today-button { position: absolute; right: 100%; diff --git a/apps/calendar/apps/web/src/lib/components/calendar/DateStripFab.svelte b/apps/calendar/apps/web/src/lib/components/calendar/DateStripFab.svelte index 928cdce0a..858f28c56 100644 --- a/apps/calendar/apps/web/src/lib/components/calendar/DateStripFab.svelte +++ b/apps/calendar/apps/web/src/lib/components/calendar/DateStripFab.svelte @@ -7,10 +7,9 @@ interface Props { isToolbarExpanded?: boolean; isMobile?: boolean; - hasTagStrip?: boolean; } - let { isToolbarExpanded = false, isMobile = false, hasTagStrip = false }: Props = $props(); + let { isToolbarExpanded = false, isMobile = false }: Props = $props(); let contextMenu: DateStripContextMenu; @@ -31,7 +30,6 @@ class="datestrip-fab-container" class:toolbar-expanded={isToolbarExpanded} class:mobile={isMobile} - class:has-tag-strip={hasTagStrip} >
@@ -911,55 +888,4 @@ padding-right: calc(54px + 1rem + 8px); /* FAB width + margin + gap */ } } - - /* Voice Button Wrapper */ - .input-bar-row { - position: relative; - } - - .voice-button-wrapper { - position: fixed; - bottom: calc(var(--bottom-offset, 70px) + env(safe-area-inset-bottom, 0px) + 7px); - left: 50%; - transform: translateX(calc(-50% + 260px)); /* Position to the right of centered InputBar */ - z-index: 91; - background: hsl(var(--color-surface) / 0.85); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border: 1px solid hsl(var(--color-border)); - border-radius: 50%; - padding: 0.25rem; - box-shadow: - 0 4px 6px -1px hsl(var(--color-foreground) / 0.1), - 0 2px 4px -1px hsl(var(--color-foreground) / 0.06); - transition: - bottom 0.3s ease, - transform 0.15s ease; - } - - .voice-button-wrapper:hover { - transform: translateX(calc(-50% + 260px)) scale(1.05); - } - - /* Adjust voice button position on smaller screens */ - @media (max-width: 900px) { - .voice-button-wrapper { - right: calc(1rem + 54px + 8px); /* FAB width + margin + gap from right FAB */ - left: auto; - transform: none; - } - - .voice-button-wrapper:hover { - transform: scale(1.05); - } - } - - /* Mobile: Hide voice button (use modal instead) */ - @media (max-width: 640px) { - .voice-button-wrapper { - right: calc(54px + 1rem + 54px + 8px); /* Right FAB + margin + voice btn + gap */ - left: auto; - transform: none; - } - } diff --git a/packages/shared-ui/src/index.ts b/packages/shared-ui/src/index.ts index aa2e30730..b6823779d 100644 --- a/packages/shared-ui/src/index.ts +++ b/packages/shared-ui/src/index.ts @@ -85,6 +85,7 @@ export { PillNavigation, PillDropdown, PillTabGroup, + PillTagSelector, PillTimeRangeSelector, PillViewSwitcher, PillToolbar, @@ -104,6 +105,8 @@ export type { PillNavigationProps, PillTabOption, PillTabGroupConfig, + PillTagItem, + PillTagSelectorConfig, ExpandableToolbarProps, } from './navigation'; diff --git a/packages/shared-ui/src/navigation/PillNavigation.svelte b/packages/shared-ui/src/navigation/PillNavigation.svelte index 90efdbb85..99e2068a4 100644 --- a/packages/shared-ui/src/navigation/PillNavigation.svelte +++ b/packages/shared-ui/src/navigation/PillNavigation.svelte @@ -5,10 +5,12 @@ PillDropdownItem, PillNavElement, PillTabGroupConfig, + PillTagSelectorConfig, PillAppItem, } from './types'; import PillDropdown from './PillDropdown.svelte'; import PillTabGroup from './PillTabGroup.svelte'; + import PillTagSelector from './PillTagSelector.svelte'; // Phosphor Icons (via shared-icons) import { House, @@ -311,6 +313,10 @@ return 'type' in element && element.type === 'divider'; } + function isTagSelector(element: PillNavElement): element is PillTagSelectorConfig { + return 'type' in element && element.type === 'tag-selector'; + } + function isNavItem(element: PillNavElement): element is PillNavItem { return 'href' in element; } @@ -468,6 +474,8 @@ 'M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z', 'share-2': 'M18 8a3 3 0 100-6 3 3 0 000 6zM6 15a3 3 0 100-6 3 3 0 000 6zM18 22a3 3 0 100-6 3 3 0 000 6zM8.59 13.51l6.83 3.98M15.41 6.51l-6.82 3.98', + filter: + 'M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z', }; function getIconPath(name: string): string { @@ -502,7 +510,7 @@ {/if} - + {#each prependElements as element} {#if isTabGroup(element)} {:else if isDivider(element)}
+ {:else if isTagSelector(element)} + {:else if isNavItem(element)} {#if element.icon} @@ -597,7 +616,7 @@ {/if} {/each} - + {#each elements as element} {#if isTabGroup(element)} {:else if isDivider(element)}
+ {:else if isTagSelector(element)} + {:else if isNavItem(element)}
{#if element.icon} diff --git a/packages/shared-ui/src/navigation/PillTagSelector.svelte b/packages/shared-ui/src/navigation/PillTagSelector.svelte new file mode 100644 index 000000000..a1efdefbb --- /dev/null +++ b/packages/shared-ui/src/navigation/PillTagSelector.svelte @@ -0,0 +1,516 @@ + + +
+ + + + {#if isOpen} + + + + + + {/if} +
+ + diff --git a/packages/shared-ui/src/navigation/index.ts b/packages/shared-ui/src/navigation/index.ts index e5a8947b8..5cc287faf 100644 --- a/packages/shared-ui/src/navigation/index.ts +++ b/packages/shared-ui/src/navigation/index.ts @@ -5,6 +5,7 @@ export { default as SidebarSection } from './SidebarSection.svelte'; export { default as PillNavigation } from './PillNavigation.svelte'; export { default as PillDropdown } from './PillDropdown.svelte'; export { default as PillTabGroup } from './PillTabGroup.svelte'; +export { default as PillTagSelector } from './PillTagSelector.svelte'; export { default as PillTimeRangeSelector } from './PillTimeRangeSelector.svelte'; export { default as PillViewSwitcher } from './PillViewSwitcher.svelte'; export { default as PillToolbar } from './PillToolbar.svelte'; @@ -24,6 +25,8 @@ export type { PillNavigationProps, PillTabOption, PillTabGroupConfig, + PillTagItem, + PillTagSelectorConfig, PillDivider, PillNavElement, } from './types'; diff --git a/packages/shared-ui/src/navigation/types.ts b/packages/shared-ui/src/navigation/types.ts index 7128c02b1..538e57a47 100644 --- a/packages/shared-ui/src/navigation/types.ts +++ b/packages/shared-ui/src/navigation/types.ts @@ -105,8 +105,36 @@ export interface PillDivider { type: 'divider'; } +export interface PillTagItem { + /** Unique tag identifier */ + id: string; + /** Tag display name */ + name: string; + /** Tag color (hex) */ + color: string; +} + +export interface PillTagSelectorConfig { + /** Discriminator for type checking */ + type: 'tag-selector'; + /** Available tags */ + tags: PillTagItem[]; + /** Currently selected tag IDs */ + selectedIds: string[]; + /** Called when a tag is toggled */ + onToggle: (tagId: string) => void; + /** Called when selection is cleared */ + onClear: () => void; + /** Called when "New Tag" is clicked (optional) */ + onCreate?: () => void; + /** Loading state */ + loading?: boolean; + /** Label for the selector button */ + label?: string; +} + /** Union type for all elements that can appear in PillNavigation */ -export type PillNavElement = PillNavItem | PillTabGroupConfig | PillDivider; +export type PillNavElement = PillNavItem | PillTabGroupConfig | PillDivider | PillTagSelectorConfig; export interface PillNavigationProps { /** Navigation items */