feat(dashboard): improve student course sidebar navigation (#814)
* docs(prototypes): add course sidebar navigation HTML prototypes Co-authored-by: Rotimi Best <[email protected]> * docs(prototypes): add downloadable zip for course sidebar mockups Co-authored-by: Rotimi Best <[email protected]> * feat(dashboard): improve student course sidebar navigation Collapse all sections except the active one for students, show section completion as counts (e.g. 3/4), and replace the footer progress card with prev/next controls plus a thin progress bar. Co-authored-by: Rotimi Best <[email protected]> * fix(dashboard): address PR review and remove sidebar prototypes Remove HTML prototype files from the PR. Match content IDs as exact path segments for active-item resolution, and use content-neutral prev/next labels. Co-authored-by: Rotimi Best <[email protected]> * fix(dashboard): keep ClassroomIO brand untranslated in powered-by footer Co-authored-by: Rotimi Best <[email protected]> * fix(dashboard): restore progress summary in course sidebar footer Co-authored-by: Rotimi Best <[email protected]> * fix(dashboard): move progress bar below summary in sidebar footer Co-authored-by: Rotimi Best <[email protected]> * feat(course): add mobile student bottom nav with outline sheet Implement prototype 3 mobile navigation for course learners: a fixed bottom bar with progress ring, prev/next controls, and a sheet showing the progress card plus collapsible course outline with scroll-to-active. Extends public-course UI primitives (ring bottom nav, collapsible sidebar sections, mobile sheet progress slot) and wires them into the authenticated course layout for isCourseLearnerView on lesson/exercise routes. Hides duplicate header prev/next and progress popover on mobile. Co-authored-by: Rotimi Best <[email protected]> * fix(course): address review feedback and exclude prototypes - Add .coderabbit.yaml and .greptile/config.json to skip prototypes/** - Document prototypes/ as out-of-scope in AGENTS.md and Greptile rules - Render locked mobile outline rows without href navigation targets - Replace nested IconButton in collapsible section trigger with chevron icon - Reset public-course sidebar section collapse on every mobile sheet reopen Co-authored-by: Rotimi Best <[email protected]> ---------
This commit is contained in:
co-authored by
Rotimi Best
parent
bf3a7e7057
commit
55ae6dd4fe
@@ -93,6 +93,27 @@ export const SIDEBAR_FIXTURE: PublicCourseSidebarSection[] = [
|
||||
}
|
||||
];
|
||||
|
||||
export const LONG_SIDEBAR_FIXTURE: PublicCourseSidebarSection[] = Array.from({ length: 10 }, (_, sectionIndex) => {
|
||||
const sectionNumber = sectionIndex + 1;
|
||||
|
||||
return {
|
||||
id: `section-${sectionNumber}`,
|
||||
title: `Module ${sectionNumber}`,
|
||||
items: Array.from({ length: 4 }, (_, itemIndex) => {
|
||||
const itemNumber = itemIndex + 1;
|
||||
const slug = `module-${sectionNumber}-lesson-${itemNumber}`;
|
||||
|
||||
return {
|
||||
kind: 'lesson' as const,
|
||||
id: `lesson-${sectionNumber}-${itemNumber}`,
|
||||
slug,
|
||||
title: `Lesson ${itemNumber}: Topic ${sectionNumber}.${itemNumber}`,
|
||||
isUnlocked: true
|
||||
};
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
export const LESSON_FIXTURE: PublicLessonViewData = {
|
||||
kind: 'lesson',
|
||||
title: 'Hallucination & limitations',
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
EXERCISE_FIXTURE,
|
||||
LESSON_FIXTURE,
|
||||
LESSON_LOCKED_FIXTURE,
|
||||
LONG_SIDEBAR_FIXTURE,
|
||||
ORG_FIXTURE,
|
||||
SIDEBAR_FIXTURE
|
||||
} from './fixtures';
|
||||
@@ -105,6 +106,40 @@
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Bottom nav · ring variant">
|
||||
{#snippet template()}
|
||||
<div class="ui:relative ui:min-h-[200px] ui:bg-background">
|
||||
<PublicCourse.PublicCourseBottomNav
|
||||
positionLabel="3 / 12"
|
||||
sublineLabel="Hallucination & limitations"
|
||||
hasPrev
|
||||
hasNext
|
||||
centerVariant="ring"
|
||||
progressPercent={42}
|
||||
onPrev={() => console.log('prev')}
|
||||
onNext={() => console.log('next')}
|
||||
onOpenSheet={() => console.log('open sheet')}
|
||||
/>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Mobile sheet · collapse + long course">
|
||||
{#snippet template()}
|
||||
{@const activeSlug = 'module-10-lesson-4'}
|
||||
<div class="ui:mx-auto ui:max-w-sm ui:border ui:border-border">
|
||||
<PublicCourse.PublicCourseMobileSheet
|
||||
open
|
||||
sections={LONG_SIDEBAR_FIXTURE}
|
||||
{activeSlug}
|
||||
collapseToSectionId="section-10"
|
||||
title="Course outline"
|
||||
onItemClick={(item: PublicCourseSidebarItem) => console.log('navigate', item.slug)}
|
||||
/>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Story>
|
||||
|
||||
<Story name="Callout · inline + full variants">
|
||||
{#snippet template()}
|
||||
<div class="ui:mx-auto ui:max-w-2xl ui:space-y-8 ui:p-8">
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
openSheetLabel?: string;
|
||||
prevLabel?: string;
|
||||
nextLabel?: string;
|
||||
/** Center button style: list icon (public default) or progress ring (student mobile). */
|
||||
centerVariant?: 'list' | 'ring';
|
||||
/** 0–100 progress for the ring variant. */
|
||||
progressPercent?: number;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
@@ -31,8 +35,22 @@
|
||||
openSheetLabel = 'Open course outline',
|
||||
prevLabel = 'Previous lesson',
|
||||
nextLabel = 'Next lesson',
|
||||
centerVariant = 'list',
|
||||
progressPercent = 0,
|
||||
class: className
|
||||
}: Props = $props();
|
||||
|
||||
const navButtonClass =
|
||||
'ui:inline-flex ui:items-center ui:justify-center ui:rounded-md ui:px-3 ui:py-3 ui:text-foreground ui:transition-[background,transform] ui:duration-100 ui:hover:bg-muted ui:active:scale-[0.94] ui:active:bg-muted ui:disabled:opacity-30 ui:disabled:active:scale-100';
|
||||
|
||||
const centerButtonClass =
|
||||
'ui:flex ui:flex-1 ui:items-center ui:justify-center ui:gap-2 ui:rounded-md ui:px-3 ui:py-2 ui:text-center ui:transition-[background,transform] ui:duration-100 ui:hover:bg-muted ui:active:scale-[0.98] ui:active:bg-muted';
|
||||
|
||||
const ringCircumference = 94.2;
|
||||
const ringOffset = $derived(
|
||||
ringCircumference - (ringCircumference * Math.min(100, Math.max(0, progressPercent))) / 100
|
||||
);
|
||||
const ringLabel = $derived(`${Math.round(progressPercent)}%`);
|
||||
</script>
|
||||
|
||||
<nav
|
||||
@@ -42,23 +60,39 @@
|
||||
)}
|
||||
aria-label="Lesson navigation"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="ui:inline-flex ui:items-center ui:justify-center ui:rounded-md ui:px-3 ui:py-3 ui:text-foreground ui:transition-colors ui:hover:bg-muted ui:disabled:opacity-30"
|
||||
aria-label={prevLabel}
|
||||
disabled={!hasPrev}
|
||||
onclick={onPrev}
|
||||
>
|
||||
<button type="button" class={navButtonClass} aria-label={prevLabel} disabled={!hasPrev} onclick={onPrev}>
|
||||
<ChevronLeftIcon class="ui:size-5" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="ui:flex ui:flex-1 ui:items-center ui:justify-center ui:gap-2 ui:rounded-md ui:px-3 ui:py-2 ui:text-center ui:transition-colors ui:hover:bg-muted"
|
||||
aria-label={openSheetLabel}
|
||||
onclick={onOpenSheet}
|
||||
>
|
||||
<ListIcon class="ui:size-4 ui:text-primary" />
|
||||
<button type="button" class={centerButtonClass} aria-label={openSheetLabel} onclick={onOpenSheet}>
|
||||
{#if centerVariant === 'ring'}
|
||||
<svg class="ui:size-7 ui:shrink-0" viewBox="0 0 36 36" aria-hidden="true">
|
||||
<circle cx="18" cy="18" r="15" fill="none" class="ui:stroke-border" stroke-width="3"></circle>
|
||||
<circle
|
||||
cx="18"
|
||||
cy="18"
|
||||
r="15"
|
||||
fill="none"
|
||||
class="ui:stroke-primary"
|
||||
stroke-width="3"
|
||||
stroke-linecap="round"
|
||||
stroke-dasharray={ringCircumference}
|
||||
stroke-dashoffset={ringOffset}
|
||||
transform="rotate(-90 18 18)"
|
||||
></circle>
|
||||
<text
|
||||
x="18"
|
||||
y="19"
|
||||
text-anchor="middle"
|
||||
dominant-baseline="middle"
|
||||
class="ui:fill-primary ui:text-[9px] ui:font-bold"
|
||||
>
|
||||
{ringLabel}
|
||||
</text>
|
||||
</svg>
|
||||
{:else}
|
||||
<ListIcon class="ui:size-4 ui:text-primary" />
|
||||
{/if}
|
||||
<span class="ui:flex ui:min-w-0 ui:flex-col ui:items-start ui:text-left ui:leading-tight">
|
||||
<span class="ui:text-[0.7rem] ui:font-medium ui:uppercase ui:tracking-wide ui:text-muted-foreground">
|
||||
{positionLabel}
|
||||
@@ -69,13 +103,7 @@
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="ui:inline-flex ui:items-center ui:justify-center ui:rounded-md ui:px-3 ui:py-3 ui:text-foreground ui:transition-colors ui:hover:bg-muted ui:disabled:opacity-30"
|
||||
aria-label={nextLabel}
|
||||
disabled={!hasNext}
|
||||
onclick={onNext}
|
||||
>
|
||||
<button type="button" class={navButtonClass} aria-label={nextLabel} disabled={!hasNext} onclick={onNext}>
|
||||
<ChevronRightIcon class="ui:size-5" />
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import * as Drawer from '../../base/drawer';
|
||||
import Sidebar from './sidebar.svelte';
|
||||
import type { PublicCourseSidebarItem, PublicCourseSidebarSection } from './types';
|
||||
@@ -11,7 +12,10 @@
|
||||
onItemClick?: (item: PublicCourseSidebarItem) => void;
|
||||
hrefFor?: (item: PublicCourseSidebarItem) => string;
|
||||
title?: string;
|
||||
/** Forwarded to Sidebar's powered-by footer. */
|
||||
/** Optional progress card rendered between header and scrollable outline. */
|
||||
progress?: Snippet;
|
||||
/** When set, collapses all sections except this one when the sheet opens. */
|
||||
collapseToSectionId?: string | null;
|
||||
showPoweredBy?: boolean;
|
||||
courseSlug?: string | null;
|
||||
orgSlug?: string | null;
|
||||
@@ -27,6 +31,8 @@
|
||||
onItemClick,
|
||||
hrefFor,
|
||||
title = 'Course outline',
|
||||
progress,
|
||||
collapseToSectionId = null,
|
||||
showPoweredBy = true,
|
||||
courseSlug = null,
|
||||
orgSlug = null,
|
||||
@@ -34,9 +40,30 @@
|
||||
poweredByBrand = 'ClassroomIO'
|
||||
}: Props = $props();
|
||||
|
||||
let scrollContainer = $state<HTMLDivElement | null>(null);
|
||||
let collapseResetKey = $state(0);
|
||||
|
||||
function scrollToActiveItem() {
|
||||
if (!scrollContainer || !activeSlug) {
|
||||
return;
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const activeRow = scrollContainer?.querySelector(`[data-item-slug="${activeSlug}"]`);
|
||||
if (activeRow instanceof HTMLElement) {
|
||||
activeRow.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleOpenChange(next: boolean) {
|
||||
open = next;
|
||||
onOpenChange?.(next);
|
||||
|
||||
if (next) {
|
||||
collapseResetKey += 1;
|
||||
scrollToActiveItem();
|
||||
}
|
||||
}
|
||||
|
||||
function handleItemClick(item: PublicCourseSidebarItem) {
|
||||
@@ -55,12 +82,23 @@
|
||||
<Drawer.Header class="ui:px-5 ui:pt-3 ui:pb-2">
|
||||
<Drawer.Title class="ui:text-sm ui:font-semibold ui:text-foreground">{title}</Drawer.Title>
|
||||
</Drawer.Header>
|
||||
<div class="ui:flex-1 ui:overflow-y-auto ui:px-2 ui:pb-[max(env(safe-area-inset-bottom),1rem)]">
|
||||
{#if progress}
|
||||
<div class="ui:shrink-0 ui:px-3 ui:pb-2">
|
||||
{@render progress()}
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
bind:this={scrollContainer}
|
||||
class="ui:flex-1 ui:overflow-y-auto ui:px-2 ui:pb-[max(env(safe-area-inset-bottom),1rem)]"
|
||||
>
|
||||
<Sidebar
|
||||
{sections}
|
||||
{activeSlug}
|
||||
onItemClick={handleItemClick}
|
||||
{hrefFor}
|
||||
collapsibleSections={Boolean(collapseToSectionId)}
|
||||
{collapseToSectionId}
|
||||
{collapseResetKey}
|
||||
{showPoweredBy}
|
||||
{courseSlug}
|
||||
{orgSlug}
|
||||
|
||||
@@ -99,6 +99,20 @@
|
||||
activeFlatIndex !== null && totalItems > 0 ? `${activeFlatIndex + 1} / ${totalItems}` : ''
|
||||
);
|
||||
|
||||
const activeSectionId = $derived.by(() => {
|
||||
if (!activeSlug) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const section of sections) {
|
||||
if (section.items.some((item) => item.slug === activeSlug)) {
|
||||
return section.id;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
const orgInitial = $derived((org?.name?.trim()?.charAt(0) ?? 'C').toUpperCase());
|
||||
|
||||
/** Left/right arrow keys navigate between items when focus is not in an editable element. */
|
||||
@@ -237,6 +251,7 @@
|
||||
orgSlug={org?.siteName ?? org?.name ?? null}
|
||||
{poweredByLabel}
|
||||
{poweredByBrand}
|
||||
collapseToSectionId={activeSectionId}
|
||||
onItemClick={(item) => {
|
||||
if (!hrefFor) {
|
||||
onItemClick?.(item);
|
||||
|
||||
@@ -10,13 +10,15 @@
|
||||
active?: boolean;
|
||||
/** Locked rows show a lock icon and a muted treatment. */
|
||||
locked?: boolean;
|
||||
/** Slug used for scroll-into-view targeting in mobile sheets. */
|
||||
itemSlug?: string;
|
||||
/** Link href for the row. If omitted, the row renders as a button (story/testing). */
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let { number, title, active = false, locked = false, href, onClick, class: className }: Props = $props();
|
||||
let { number, title, active = false, locked = false, itemSlug, href, onClick, class: className }: Props = $props();
|
||||
|
||||
const rowClass = $derived(
|
||||
cn(
|
||||
@@ -60,7 +62,7 @@
|
||||
{/snippet}
|
||||
|
||||
{#if href}
|
||||
<a {href} class={rowClass} aria-current={active ? 'page' : undefined}>
|
||||
<a {href} class={rowClass} aria-current={active ? 'page' : undefined} data-item-slug={itemSlug}>
|
||||
{@render body()}
|
||||
</a>
|
||||
{:else}
|
||||
@@ -69,6 +71,7 @@
|
||||
class={rowClass}
|
||||
aria-current={active ? 'page' : undefined}
|
||||
aria-disabled={locked || undefined}
|
||||
data-item-slug={itemSlug}
|
||||
onclick={onClick}
|
||||
>
|
||||
{@render body()}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import { mergeProps } from 'bits-ui';
|
||||
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
||||
import * as Collapsible from '../../base/collapsible';
|
||||
import * as Tooltip from '../../base/tooltip';
|
||||
import { cn } from '../../tools';
|
||||
import PoweredBy from './powered-by.svelte';
|
||||
@@ -7,24 +10,20 @@
|
||||
import type { PublicCourseSidebarItem, PublicCourseSidebarSection } from './types';
|
||||
|
||||
interface Props {
|
||||
// Sections
|
||||
sections: PublicCourseSidebarSection[];
|
||||
/** Slug of the active item. Matches against `item.slug`. */
|
||||
activeSlug?: string | null;
|
||||
/** Receives the target item on click; components using `hrefFor` should also provide this. */
|
||||
onItemClick?: (item: PublicCourseSidebarItem) => void;
|
||||
/** Builds the `href` for a given item. Omit to render rows as buttons. */
|
||||
hrefFor?: (item: PublicCourseSidebarItem) => string;
|
||||
/**
|
||||
* Show the "Powered by ClassroomIO" footer pinned to the bottom of the sidebar.
|
||||
* Defaults to `true` when paired with `courseSlug` / `orgSlug`.
|
||||
*/
|
||||
/** When true, sections collapse/expand; only `collapseToSectionId` stays open on reset. */
|
||||
collapsibleSections?: boolean;
|
||||
/** When set, collapses all sections except this one (e.g. on mobile sheet open). */
|
||||
collapseToSectionId?: string | null;
|
||||
/** Increment to force collapse reset even when `collapseToSectionId` is unchanged. */
|
||||
collapseResetKey?: number;
|
||||
showPoweredBy?: boolean;
|
||||
/** Course slug used in the powered-by attribution link. */
|
||||
courseSlug?: string | null;
|
||||
/** Org slug used in the powered-by attribution link. */
|
||||
orgSlug?: string | null;
|
||||
/** Localized labels for the powered-by footer. */
|
||||
poweredByLabel?: string;
|
||||
poweredByBrand?: string;
|
||||
class?: string;
|
||||
@@ -35,6 +34,9 @@
|
||||
activeSlug = null,
|
||||
onItemClick,
|
||||
hrefFor,
|
||||
collapsibleSections = false,
|
||||
collapseToSectionId = null,
|
||||
collapseResetKey = 0,
|
||||
showPoweredBy = true,
|
||||
courseSlug = null,
|
||||
orgSlug = null,
|
||||
@@ -42,49 +44,117 @@
|
||||
poweredByBrand = 'ClassroomIO',
|
||||
class: className
|
||||
}: Props = $props();
|
||||
|
||||
let expandedSectionIds = $state<Set<string>>(new Set());
|
||||
let trackedCollapseSignature = $state<string | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
if (!collapsibleSections || !collapseToSectionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const collapseSignature = `${collapseToSectionId}:${collapseResetKey}`;
|
||||
if (collapseSignature === trackedCollapseSignature) {
|
||||
return;
|
||||
}
|
||||
|
||||
trackedCollapseSignature = collapseSignature;
|
||||
expandedSectionIds = new Set([collapseToSectionId]);
|
||||
});
|
||||
|
||||
function isSectionOpen(sectionId: string): boolean {
|
||||
if (!collapsibleSections) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return expandedSectionIds.has(sectionId);
|
||||
}
|
||||
|
||||
function handleSectionOpenChange(sectionId: string, open: boolean) {
|
||||
if (!collapsibleSections) {
|
||||
return;
|
||||
}
|
||||
|
||||
const next = new Set(expandedSectionIds);
|
||||
if (open) {
|
||||
next.add(sectionId);
|
||||
} else {
|
||||
next.delete(sectionId);
|
||||
}
|
||||
expandedSectionIds = next;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#snippet sectionItems(section: PublicCourseSidebarSection)}
|
||||
{#each section.items as item, index (item.id)}
|
||||
<SidebarRow
|
||||
number={index + 1}
|
||||
title={item.title}
|
||||
active={item.slug === activeSlug}
|
||||
locked={!item.isUnlocked}
|
||||
itemSlug={item.slug}
|
||||
href={hrefFor?.(item)}
|
||||
onClick={() => onItemClick?.(item)}
|
||||
/>
|
||||
{/each}
|
||||
{/snippet}
|
||||
|
||||
<div class={cn('ui:flex ui:h-full ui:flex-col ui:bg-sidebar', className)}>
|
||||
<nav class="ui:flex ui:flex-1 ui:flex-col ui:gap-6 ui:py-6 ui:px-2" aria-label="Course outline">
|
||||
<Tooltip.Provider delayDuration={300}>
|
||||
{#each sections as section (section.id)}
|
||||
<div class="ui:flex ui:min-w-0 ui:flex-col ui:gap-2">
|
||||
<h2 class="ui:m-0 ui:min-w-0 ui:w-full">
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<span
|
||||
{...mergeProps(
|
||||
{
|
||||
class: cn(
|
||||
'ui:block ui:w-full ui:min-w-0 ui:truncate ui:px-3 ui:font-medium ui:uppercase ui:text-[11px] ui:text-muted-foreground ui:transition-colors'
|
||||
)
|
||||
},
|
||||
props
|
||||
)}
|
||||
>
|
||||
{section.title}
|
||||
</span>
|
||||
{/snippet}
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side="top" sideOffset={6} class="ui:max-w-xs">
|
||||
{section.title}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</h2>
|
||||
<div class="ui:flex ui:flex-col">
|
||||
{#each section.items as item, index (item.id)}
|
||||
<SidebarRow
|
||||
number={index + 1}
|
||||
title={item.title}
|
||||
active={item.slug === activeSlug}
|
||||
locked={!item.isUnlocked}
|
||||
href={hrefFor?.(item)}
|
||||
onClick={() => onItemClick?.(item)}
|
||||
/>
|
||||
{/each}
|
||||
{#if collapsibleSections}
|
||||
<Collapsible.Root
|
||||
open={isSectionOpen(section.id)}
|
||||
onOpenChange={(open) => handleSectionOpenChange(section.id, open)}
|
||||
class="ui:group/section ui:flex ui:min-w-0 ui:flex-col ui:gap-1"
|
||||
>
|
||||
<Collapsible.Trigger
|
||||
class="ui:flex ui:w-full ui:items-center ui:justify-between ui:gap-2 ui:rounded-sm ui:px-3 ui:py-1.5 ui:text-left"
|
||||
>
|
||||
<span class="ui:flex ui:min-w-0 ui:items-center ui:gap-1.5">
|
||||
<ChevronRightIcon
|
||||
class="ui:size-3.5 ui:shrink-0 ui:text-muted-foreground ui:transition-transform ui:group-data-[state=open]/section:rotate-90"
|
||||
/>
|
||||
<span class="ui:truncate ui:font-medium ui:uppercase ui:text-[11px] ui:text-muted-foreground">
|
||||
{section.title}
|
||||
</span>
|
||||
</span>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content class="ui:flex ui:flex-col">
|
||||
{@render sectionItems(section)}
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
{:else}
|
||||
<div class="ui:flex ui:min-w-0 ui:flex-col ui:gap-2">
|
||||
<h2 class="ui:m-0 ui:min-w-0 ui:w-full">
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<span
|
||||
{...mergeProps(
|
||||
{
|
||||
class: cn(
|
||||
'ui:block ui:w-full ui:min-w-0 ui:truncate ui:px-3 ui:font-medium ui:uppercase ui:text-[11px] ui:text-muted-foreground ui:transition-colors'
|
||||
)
|
||||
},
|
||||
props
|
||||
)}
|
||||
>
|
||||
{section.title}
|
||||
</span>
|
||||
{/snippet}
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side="top" sideOffset={6} class="ui:max-w-xs">
|
||||
{section.title}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</h2>
|
||||
<div class="ui:flex ui:flex-col">
|
||||
{@render sectionItems(section)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</Tooltip.Provider>
|
||||
</nav>
|
||||
|
||||
Reference in New Issue
Block a user