diff --git a/playground/src/app/app.html b/playground/src/app/app.html index 401503d..a0a574c 100644 --- a/playground/src/app/app.html +++ b/playground/src/app/app.html @@ -40,6 +40,36 @@ + + + + + + - @switch (group.label) { - @case ('General') { - - } - @case ('Contribute') { - - } - @case ('Inputs') { - - } - @case ('Selection') { - - } - @case ('Form Structure') { - - } - @case ('Actions') { - - } - @case ('Navigation') { - - } - @case ('Feedback') { - - } - @case ('Data Display') { - - } - @case ('Layout') { - - } - @case ('Utilities') { - - } - } + {{ group.heading ?? group.label }} diff --git a/playground/src/app/app.navigation.ts b/playground/src/app/app.navigation.ts index 7104a2e..a0e0f35 100644 --- a/playground/src/app/app.navigation.ts +++ b/playground/src/app/app.navigation.ts @@ -1,3 +1,18 @@ +import { + LucideGitPullRequest, + LucideHouse, + LucideLayers, + LucideListChecks, + LucideMessageCircle, + LucideMousePointerClick, + LucideNavigation, + LucidePanelsTopLeft, + LucideRows3, + LucideTableProperties, + LucideTextCursorInput, + LucideWrench, +} from '@lucide/angular'; +import type { LucideIcon } from '@lucide/angular'; import type { NavbarItem, SidebarGroup } from '@sushi-kit/angular'; export interface NavigationItem { @@ -5,15 +20,17 @@ export interface NavigationItem { readonly path: string; } -export interface NavigationGroup { +interface NavigationGroup { readonly dividerBefore?: boolean; + readonly icon: LucideIcon; readonly label: string; readonly items: readonly NavigationItem[]; } -export const navigation: readonly NavigationGroup[] = [ +const navigation: readonly NavigationGroup[] = [ { dividerBefore: true, + icon: LucideTextCursorInput, label: 'Inputs', items: [ { @@ -51,6 +68,7 @@ export const navigation: readonly NavigationGroup[] = [ ], }, { + icon: LucideListChecks, label: 'Selection', items: [ { @@ -96,6 +114,7 @@ export const navigation: readonly NavigationGroup[] = [ ], }, { + icon: LucideRows3, label: 'Form Structure', items: [ { @@ -121,6 +140,7 @@ export const navigation: readonly NavigationGroup[] = [ ], }, { + icon: LucideMousePointerClick, label: 'Actions', items: [ { @@ -134,6 +154,7 @@ export const navigation: readonly NavigationGroup[] = [ ], }, { + icon: LucideNavigation, label: 'Navigation', items: [ { @@ -163,6 +184,7 @@ export const navigation: readonly NavigationGroup[] = [ ], }, { + icon: LucideLayers, label: 'Overlays', items: [ { @@ -184,6 +206,7 @@ export const navigation: readonly NavigationGroup[] = [ ], }, { + icon: LucideMessageCircle, label: 'Feedback', items: [ { @@ -217,6 +240,7 @@ export const navigation: readonly NavigationGroup[] = [ ], }, { + icon: LucideTableProperties, label: 'Data Display', items: [ { @@ -254,6 +278,7 @@ export const navigation: readonly NavigationGroup[] = [ ], }, { + icon: LucidePanelsTopLeft, label: 'Layout', items: [ { @@ -271,6 +296,7 @@ export const navigation: readonly NavigationGroup[] = [ ], }, { + icon: LucideWrench, label: 'Utilities', items: [ { @@ -297,18 +323,20 @@ export const apiNavigation: readonly NavigationItem[] = navigation .flatMap((group: NavigationGroup): readonly NavigationItem[] => group.items) .filter((item: NavigationItem): boolean => item.path !== '/icon'); -export type SidebarNavigationGroup = SidebarGroup>; +export interface SidebarNavigationGroup extends SidebarGroup> { + readonly icon: LucideIcon; +} -export const overviewNavigation: readonly NavbarItem[] = [{ label: 'Overview', value: '/', routerLink: '/' }]; +const overviewNavigation: readonly NavbarItem[] = [{ label: 'Overview', value: '/', routerLink: '/' }]; -export const generalNavigation: readonly NavbarItem[] = [ +const generalNavigation: readonly NavbarItem[] = [ ...overviewNavigation, { label: 'Getting started', value: '/getting-started', routerLink: '/getting-started' }, { label: 'Styling & themes', value: '/styling-and-themes', routerLink: '/styling-and-themes' }, { label: 'Theme tokens', value: '/theme-tokens', routerLink: '/theme-tokens' }, ]; -export const contributorNavigation: readonly NavbarItem[] = [ +const contributorNavigation: readonly NavbarItem[] = [ { label: 'Contribution guide', value: '/contribute', routerLink: '/contribute' }, { label: 'Component workflow', value: '/contribute/components', routerLink: '/contribute/components' }, { label: 'Component styling', value: '/contribute/styles', routerLink: '/contribute/styles' }, @@ -317,30 +345,19 @@ export const contributorNavigation: readonly NavbarItem[] = [ { label: 'Responsible AI', value: '/contribute/ai', routerLink: '/contribute/ai' }, ]; -export const navbarNavigation: readonly SidebarNavigationGroup[] = navigation.map( - (group: NavigationGroup): SidebarNavigationGroup => ({ - dividerBefore: group.dividerBefore, - label: group.label, - items: group.items.map((item: NavigationItem): NavbarItem => ({ - label: item.label, - value: item.path, - routerLink: item.path, - })), - }), -); +const navbarNavigation: readonly SidebarNavigationGroup[] = navigation.map((group: NavigationGroup): SidebarNavigationGroup => ({ + dividerBefore: group.dividerBefore, + icon: group.icon, + label: group.label, + items: group.items.map((item: NavigationItem): NavbarItem => ({ + label: item.label, + value: item.path, + routerLink: item.path, + })), +})); export const sidebarNavigation: readonly SidebarNavigationGroup[] = [ - { label: 'General', items: generalNavigation }, - { label: 'Contribute', items: contributorNavigation }, + { icon: LucideHouse, label: 'General', items: generalNavigation }, + { icon: LucideGitPullRequest, label: 'Contribute', items: contributorNavigation }, ...navbarNavigation, ]; - -export const mobileNavigation: readonly NavbarItem[] = [ - ...generalNavigation, - { label: 'Contribute', value: 'group:Contribute', items: contributorNavigation }, - ...navbarNavigation.map((group: SidebarNavigationGroup): NavbarItem => ({ - label: group.label, - value: `group:${group.label}`, - items: group.items, - })), -]; diff --git a/playground/src/app/app.ts b/playground/src/app/app.ts index 463d02d..65835ed 100644 --- a/playground/src/app/app.ts +++ b/playground/src/app/app.ts @@ -14,23 +14,7 @@ import { } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { NavigationEnd, Router, RouterLink, RouterOutlet } from '@angular/router'; -import { - LucideHouse, - LucideGitPullRequest, - LucideListChecks, - LucideMenu, - LucideMessageCircle, - LucideMoon, - LucideMousePointerClick, - LucideNavigation, - LucidePanelsTopLeft, - LucideRows3, - LucideSun, - LucideTableProperties, - LucideTextCursorInput, - LucideWrench, - LucideX, -} from '@lucide/angular'; +import { LucideDynamicIcon, LucideGitFork, LucideMenu, LucideMoon, LucidePackage, LucideSun, LucideX } from '@lucide/angular'; import { Button, Drawer, @@ -60,20 +44,12 @@ type PlaygroundTheme = 'sushi' | 'sushi-dark'; DrawerClose, DrawerContent, DrawerTrigger, - LucideHouse, - LucideGitPullRequest, - LucideListChecks, + LucideDynamicIcon, + LucideGitFork, LucideMenu, - LucideMessageCircle, LucideMoon, - LucideMousePointerClick, - LucideNavigation, - LucidePanelsTopLeft, - LucideRows3, + LucidePackage, LucideSun, - LucideTableProperties, - LucideTextCursorInput, - LucideWrench, LucideX, Navbar, NavbarAction, diff --git a/playground/src/app/generated/api-reference.generated.ts b/playground/src/app/generated/api-reference.generated.ts index 0199f2a..83631b3 100644 --- a/playground/src/app/generated/api-reference.generated.ts +++ b/playground/src/app/generated/api-reference.generated.ts @@ -11496,13 +11496,13 @@ export const apiReference: Readonly<{ templates: [ { name: 'suiSidebarGroup', - context: 'SidebarGroupContext', + context: 'SidebarGroupContext', description: 'Customizes visible Sidebar group headings without changing navigation items.', members: [ { name: 'groups', kind: 'input', - type: 'readonly SidebarGroup[] | undefined', + type: 'readonly G[] | undefined', defaultValue: 'undefined', description: 'Group source used to infer custom fields inside the template.', }, @@ -11680,18 +11680,18 @@ export const apiReference: Readonly<{ kind: 'interface', source: 'library', declaration: - 'interface SidebarGroupContext {\n readonly $implicit: SidebarGroup;\n readonly group: SidebarGroup;\n readonly collapsed: boolean;\n}', + 'interface SidebarGroupContext = SidebarGroup> {\n readonly $implicit: G;\n readonly group: G;\n readonly collapsed: boolean;\n}', description: 'Context exposed to a custom Sidebar group-heading template.', members: [ { name: '$implicit', - type: 'SidebarGroup', + type: 'G', optional: false, description: 'Sidebar group available as the implicit template value.', }, { name: 'group', - type: 'SidebarGroup', + type: 'G', optional: false, description: 'Sidebar group available by its explicit context name.', }, @@ -11709,6 +11709,11 @@ export const apiReference: Readonly<{ constraint: 'NavbarItem', defaultValue: 'NavbarItem', }, + { + name: 'G', + constraint: 'SidebarGroup', + defaultValue: 'SidebarGroup', + }, ], }, { diff --git a/playground/src/styles.css b/playground/src/styles.css index 077f3d5..3777590 100644 --- a/playground/src/styles.css +++ b/playground/src/styles.css @@ -17,7 +17,16 @@ } .pg-documentation-navbar .sui-navbar { - padding-inline: 0; + padding-inline: 1rem; +} + +@media (width < 40rem) { + .pg-documentation-navbar .sui-navbar > sui-navbar-menu { + position: absolute; + left: 50%; + margin-inline-start: 0; + transform: translateX(-50%); + } } .pg-page { @@ -125,7 +134,11 @@ pg-example-code { border-radius: 0; } -@media (min-width: 40rem) { +@media (width >= 40rem) { + .pg-documentation-navbar .sui-navbar { + padding-inline: 0; + } + .pg-page { padding: 3.5rem 2rem 6rem; } diff --git a/sushi/src/lib/sidebar/sidebar-group-template.directive.ts b/sushi/src/lib/sidebar/sidebar-group-template.directive.ts index 60b38e0..9110d07 100644 --- a/sushi/src/lib/sidebar/sidebar-group-template.directive.ts +++ b/sushi/src/lib/sidebar/sidebar-group-template.directive.ts @@ -4,17 +4,16 @@ import { SidebarGroup, SidebarGroupContext } from './sidebar.interfaces'; /** Customizes visible Sidebar group headings without changing navigation items. */ @Directive({ selector: 'ng-template[suiSidebarGroup]' }) -export class SidebarGroupTemplate { +export class SidebarGroupTemplate = SidebarGroup> { /** Group source used to infer custom fields inside the template. */ - public readonly groups: InputSignal[] | undefined> = input[] | undefined>( - undefined, - { alias: 'suiSidebarGroup' }, - ); + public readonly groups: InputSignal = input(undefined, { + alias: 'suiSidebarGroup', + }); - public static ngTemplateContextGuard( - _directive: SidebarGroupTemplate, + public static ngTemplateContextGuard>( + _directive: SidebarGroupTemplate, _context: unknown, - ): _context is SidebarGroupContext { + ): _context is SidebarGroupContext { return true; } } diff --git a/sushi/src/lib/sidebar/sidebar.interfaces.ts b/sushi/src/lib/sidebar/sidebar.interfaces.ts index 36b09b4..6db1630 100644 --- a/sushi/src/lib/sidebar/sidebar.interfaces.ts +++ b/sushi/src/lib/sidebar/sidebar.interfaces.ts @@ -24,11 +24,11 @@ export interface SidebarItemContext extends N } /** Context exposed to a custom Sidebar group-heading template. */ -export interface SidebarGroupContext { +export interface SidebarGroupContext = SidebarGroup> { /** Sidebar group available as the implicit template value. */ - readonly $implicit: SidebarGroup; + readonly $implicit: G; /** Sidebar group available by its explicit context name. */ - readonly group: SidebarGroup; + readonly group: G; /** Whether the Sidebar currently renders as an icon rail. Use it to hide text or show a meaningful group icon. */ readonly collapsed: boolean; } diff --git a/sushi/src/lib/sidebar/testing/sidebar.component.spec.ts b/sushi/src/lib/sidebar/testing/sidebar.component.spec.ts index bb82eed..7acad63 100644 --- a/sushi/src/lib/sidebar/testing/sidebar.component.spec.ts +++ b/sushi/src/lib/sidebar/testing/sidebar.component.spec.ts @@ -15,6 +15,10 @@ interface AppItem extends NavbarItem { readonly badge?: string; } +interface AppGroup extends SidebarGroup { + readonly icon: string; +} + @Component({ imports: [Sidebar, SidebarFooter, SidebarGroupTemplate, SidebarHeader, SidebarItemTemplate], template: ` @@ -28,7 +32,9 @@ interface AppItem extends NavbarItem { > Orbit {{ group.label }}{{ + group.label + }} = signal('overview'); public readonly collapsed: WritableSignal = signal(false); public readonly selected: (Page | null)[] = []; - public readonly groups: readonly SidebarGroup[] = [ + public readonly groups: readonly AppGroup[] = [ { + icon: 'workspace', label: 'Workspace', items: [ { label: 'Overview', value: 'overview' }, @@ -57,8 +64,13 @@ class SidebarHost { { label: 'Reports', value: 'reports', items: [{ label: 'Activity reports', value: 'activity-reports' }] }, ], }, - { label: 'Administration', dividerBefore: true, items: [{ label: 'Settings', value: 'settings', disabled: true }] }, - { label: 'Empty', items: [] }, + { + icon: 'administration', + label: 'Administration', + dividerBefore: true, + items: [{ label: 'Settings', value: 'settings', disabled: true }], + }, + { icon: 'empty', label: 'Empty', items: [] }, ]; } @@ -76,6 +88,7 @@ describe('Sidebar', (): void => { expect(query(fixture, '[data-footer]').textContent).toContain('Signed in'); expect(queryAll(fixture, 'nav')).toHaveLength(2); expect(query(fixture, '[data-drawer-group="Workspace"]')).toBeDefined(); + expect(query(fixture, '[data-drawer-group="Workspace"]').getAttribute('data-group-icon')).toBe('workspace'); expect(query(fixture, '[data-drawer-group="Administration"]').closest('section')?.hasAttribute('data-divider-before')).toBe( true, ); diff --git a/sushi/src/styles/features/button.styles.css b/sushi/src/styles/features/button.styles.css index cf6bfae..e940a0b 100644 --- a/sushi/src/styles/features/button.styles.css +++ b/sushi/src/styles/features/button.styles.css @@ -15,8 +15,22 @@ transition-duration: 0s; } +@media (hover: none) and (pointer: coarse) { + .sui-button.btn { + /** Hold and return timing for the pressed state on a touch device. */ + transition-delay: var(--sui-button-touch-release-delay, 150ms); + transition-duration: var(--sui-button-touch-release-duration, 300ms); + transition-property: opacity, background-color, border-color, color, transform; + } + + .sui-button.btn:not(.sui-button--disabled):active { + transition-delay: 0s; + } +} + @media (prefers-reduced-motion: reduce) { .sui-button.btn { + transition-delay: 0s; transition-duration: 0s; }