@xenide-io/the-old-ui-theme 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-5HSOBJ4F.mjs → chunk-KORWJCIO.mjs} +600 -932
- package/dist/{index-BgG8Homu.d.mts → index-B5NOyoKS.d.mts} +100 -100
- package/dist/{index-BgG8Homu.d.ts → index-B5NOyoKS.d.ts} +100 -100
- package/dist/index.d.mts +9 -145
- package/dist/index.d.ts +9 -145
- package/dist/index.js +653 -1111
- package/dist/index.mjs +9 -267
- package/dist/suite.d.mts +760 -0
- package/dist/suite.d.ts +760 -0
- package/dist/suite.js +2564 -0
- package/dist/suite.mjs +2465 -0
- package/dist/ui.d.mts +1 -2
- package/dist/ui.d.ts +1 -2
- package/dist/ui.js +14 -10
- package/dist/ui.mjs +1 -1
- package/package.json +43 -24
- package/src/components/icons/icons.tsx +0 -2
- package/src/components/icons/index.ts +0 -3
- package/src/components/sections/IconShowcase.tsx +1 -110
- package/src/components/sections/NewComponentsShowcase.tsx +4 -4
- package/src/components/sections/SidebarShowcase.tsx +3 -3
- package/src/components/sections/SuiteShowcase.tsx +669 -0
- package/src/components/ui/ComponentDocs.tsx +2 -2
- package/src/components/ui/ContextMenu.tsx +1 -1
- package/src/components/ui/Menubar.tsx +1 -1
- package/src/components/ui/Sidebar.tsx +10 -8
- package/src/styles/suite-skin.css +255 -0
- package/src/suite/components/ai-panel.tsx +202 -0
- package/src/suite/components/app-switcher.tsx +196 -0
- package/src/suite/components/command-palette-host.tsx +62 -0
- package/src/suite/components/deferred-chrome.tsx +12 -0
- package/src/suite/components/suite-app-layout.tsx +69 -0
- package/src/suite/components/suite-bottom-nav.tsx +113 -0
- package/src/suite/components/suite-layout.tsx +287 -0
- package/src/suite/components/suite-mobile-drawer.tsx +150 -0
- package/src/suite/components/suite-mobile-header.tsx +71 -0
- package/src/suite/components/suite-notification-bell.tsx +227 -0
- package/src/suite/components/suite-settings-mobile-nav.tsx +83 -0
- package/src/suite/components/suite-sidebar.tsx +191 -0
- package/src/suite/components/suite-skeleton.tsx +191 -0
- package/src/suite/components/suite-user-menu.tsx +128 -0
- package/src/suite/components/theme-provider.tsx +174 -0
- package/src/suite/components/today-calibrating.tsx +95 -0
- package/src/suite/components/today-page-frame.tsx +28 -0
- package/src/suite/components/today-ui.tsx +370 -0
- package/src/suite/icons/app-accents.ts +75 -0
- package/src/suite/icons/glyph-parts.tsx +67 -0
- package/src/suite/icons/glyphs.ts +203 -0
- package/src/suite/icons/index.ts +12 -0
- package/src/suite/icons/suite-app-icon.tsx +68 -0
- package/src/suite/icons/suite-icon.tsx +93 -0
- package/src/suite/index.ts +108 -0
- package/src/suite/lib/apps.ts +75 -0
- package/src/suite/lib/cn.ts +6 -0
- package/src/suite/lib/injected.ts +54 -0
- package/src/suite/lib/motion.tsx +48 -0
- package/src/suite/lib/use-idle-mount.ts +25 -0
- package/src/components/icons/lemon-brand-icons.tsx +0 -16
- package/src/components/icons/lemon-category-icons.tsx +0 -72
- package/src/components/icons/lemon-icons.tsx +0 -57
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Image from 'next/image';
|
|
4
|
+
import { useState, type ReactNode } from 'react';
|
|
5
|
+
import { Check, NavArrowDown as ChevronDown } from 'iconoir-react';
|
|
6
|
+
|
|
7
|
+
import { cn } from '../lib/cn';
|
|
8
|
+
import type {
|
|
9
|
+
SuiteDropdownItemComponent,
|
|
10
|
+
SuiteDropdownMenuComponent,
|
|
11
|
+
} from '../lib/injected';
|
|
12
|
+
|
|
13
|
+
export function appSwitcherTriggerClass(open: boolean, collapsed = false) {
|
|
14
|
+
return cn(
|
|
15
|
+
'group flex min-w-0 items-center gap-2 rounded-lg border px-2 py-1.5 text-left transition-all duration-200',
|
|
16
|
+
collapsed ? 'h-11 w-11 justify-center px-0 py-0' : 'w-full',
|
|
17
|
+
open
|
|
18
|
+
? 'border-ph-brand/30 bg-ph-muted shadow-[0_1px_2px_rgba(0,0,0,0.06)] ring-1 ring-ph-brand/15'
|
|
19
|
+
: 'border-transparent hover:border-ph-border hover:bg-ph-muted/80 hover:shadow-sm',
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function appSwitcherMarkClass() {
|
|
24
|
+
return 'h-8 w-8 shrink-0 rounded-lg shadow-[0_1px_2px_rgba(0,0,0,0.1),0_4px_12px_rgba(0,0,0,0.08)] ring-1 ring-black/[0.06]';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** No sticky Radix highlight while waiting for cross-app redirect. */
|
|
28
|
+
export function appSwitcherMenuItemClass() {
|
|
29
|
+
return 'rounded-lg px-2 py-2 data-[highlighted]:bg-transparent hover:bg-ph-muted/80';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function AppSwitcherMark({
|
|
33
|
+
children,
|
|
34
|
+
collapsed,
|
|
35
|
+
}: {
|
|
36
|
+
children: ReactNode;
|
|
37
|
+
collapsed?: boolean;
|
|
38
|
+
}) {
|
|
39
|
+
return (
|
|
40
|
+
<span
|
|
41
|
+
className={cn(
|
|
42
|
+
'relative shrink-0',
|
|
43
|
+
collapsed && 'group-hover:scale-[1.02]',
|
|
44
|
+
)}
|
|
45
|
+
>
|
|
46
|
+
{children}
|
|
47
|
+
</span>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function AppSwitcherChevron({
|
|
52
|
+
open,
|
|
53
|
+
collapsed = false,
|
|
54
|
+
}: {
|
|
55
|
+
open: boolean;
|
|
56
|
+
collapsed?: boolean;
|
|
57
|
+
}) {
|
|
58
|
+
if (collapsed) {
|
|
59
|
+
return (
|
|
60
|
+
<span
|
|
61
|
+
className={cn(
|
|
62
|
+
'absolute bottom-0 right-0 flex h-3 w-3 items-center justify-center rounded-full border border-ph-border bg-ph-surface shadow-sm transition-colors',
|
|
63
|
+
open && 'border-ph-brand/40 bg-ph-brand/10',
|
|
64
|
+
)}
|
|
65
|
+
aria-hidden
|
|
66
|
+
>
|
|
67
|
+
<ChevronDown
|
|
68
|
+
className={cn(
|
|
69
|
+
'h-2 w-2 text-ph-mutedtext transition-transform duration-200',
|
|
70
|
+
open && 'rotate-180 text-ph-brand',
|
|
71
|
+
)}
|
|
72
|
+
/>
|
|
73
|
+
</span>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<span
|
|
79
|
+
className={cn(
|
|
80
|
+
'border-ph-border/80 flex h-5 w-5 shrink-0 items-center justify-center rounded-lg border bg-ph-canvas shadow-sm transition-all duration-200',
|
|
81
|
+
open
|
|
82
|
+
? 'border-ph-brand/35 bg-ph-brand/10 text-ph-brand'
|
|
83
|
+
: 'text-ph-mutedtext group-hover:border-ph-border group-hover:bg-ph-surface',
|
|
84
|
+
)}
|
|
85
|
+
aria-hidden
|
|
86
|
+
>
|
|
87
|
+
<ChevronDown
|
|
88
|
+
className={cn(
|
|
89
|
+
'h-3 w-3 transition-transform duration-200',
|
|
90
|
+
open && 'rotate-180',
|
|
91
|
+
)}
|
|
92
|
+
/>
|
|
93
|
+
</span>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface SuiteAppEntry {
|
|
98
|
+
slug: string;
|
|
99
|
+
name: string;
|
|
100
|
+
description: string;
|
|
101
|
+
icon: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Suite app switcher dropdown. Apps inject their brand mark/title, the
|
|
106
|
+
* visible app list, the cross-app navigation handler, and the theme
|
|
107
|
+
* dropdown primitives.
|
|
108
|
+
*/
|
|
109
|
+
export function AppSwitcher({
|
|
110
|
+
apps,
|
|
111
|
+
currentApp,
|
|
112
|
+
onSelect,
|
|
113
|
+
mark,
|
|
114
|
+
title,
|
|
115
|
+
collapsed = false,
|
|
116
|
+
dropdownMenu: DropdownMenu,
|
|
117
|
+
dropdownItem: DropdownItem,
|
|
118
|
+
}: {
|
|
119
|
+
apps: SuiteAppEntry[];
|
|
120
|
+
currentApp: string;
|
|
121
|
+
onSelect: (app: SuiteAppEntry) => void;
|
|
122
|
+
mark: ReactNode;
|
|
123
|
+
title: ReactNode;
|
|
124
|
+
collapsed?: boolean;
|
|
125
|
+
dropdownMenu: SuiteDropdownMenuComponent;
|
|
126
|
+
dropdownItem: SuiteDropdownItemComponent;
|
|
127
|
+
}) {
|
|
128
|
+
const [open, setOpen] = useState(false);
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<DropdownMenu
|
|
132
|
+
aria-label="Switch application"
|
|
133
|
+
className={collapsed ? 'w-11 shrink-0' : 'min-w-0 flex-1'}
|
|
134
|
+
panelClassName="w-64"
|
|
135
|
+
align="start"
|
|
136
|
+
open={open}
|
|
137
|
+
onOpenChange={setOpen}
|
|
138
|
+
trigger={
|
|
139
|
+
<span
|
|
140
|
+
className={appSwitcherTriggerClass(open, collapsed)}
|
|
141
|
+
data-test="app-switcher-trigger"
|
|
142
|
+
aria-expanded={open}
|
|
143
|
+
>
|
|
144
|
+
<AppSwitcherMark collapsed={collapsed}>
|
|
145
|
+
{mark}
|
|
146
|
+
{collapsed ? <AppSwitcherChevron open={open} collapsed /> : null}
|
|
147
|
+
</AppSwitcherMark>
|
|
148
|
+
{!collapsed ? title : null}
|
|
149
|
+
{!collapsed ? <AppSwitcherChevron open={open} /> : null}
|
|
150
|
+
</span>
|
|
151
|
+
}
|
|
152
|
+
>
|
|
153
|
+
<div className="p-1" data-test="app-switcher-menu">
|
|
154
|
+
<p className="px-3 py-2 text-[10px] font-semibold uppercase tracking-[0.14em] text-ph-mutedtext">
|
|
155
|
+
Switch application
|
|
156
|
+
</p>
|
|
157
|
+
{apps.map((app) => (
|
|
158
|
+
<DropdownItem
|
|
159
|
+
key={app.slug}
|
|
160
|
+
data-test={`switch-app-${app.slug}`}
|
|
161
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
162
|
+
onClick={() => {
|
|
163
|
+
setOpen(false);
|
|
164
|
+
onSelect(app);
|
|
165
|
+
}}
|
|
166
|
+
className={appSwitcherMenuItemClass()}
|
|
167
|
+
>
|
|
168
|
+
<span className="flex min-w-0 items-center gap-3">
|
|
169
|
+
<Image
|
|
170
|
+
src={app.icon}
|
|
171
|
+
alt=""
|
|
172
|
+
width={28}
|
|
173
|
+
height={28}
|
|
174
|
+
className="h-7 w-7 shrink-0 rounded-md object-contain"
|
|
175
|
+
/>
|
|
176
|
+
<span className="min-w-0 flex-1">
|
|
177
|
+
<span className="block truncate text-sm font-medium text-ph-ink">
|
|
178
|
+
{app.name}
|
|
179
|
+
</span>
|
|
180
|
+
<span className="block truncate text-xs text-ph-mutedtext">
|
|
181
|
+
{app.description}
|
|
182
|
+
</span>
|
|
183
|
+
</span>
|
|
184
|
+
{app.slug === currentApp ? (
|
|
185
|
+
<Check
|
|
186
|
+
className="h-4 w-4 shrink-0 text-ph-brand"
|
|
187
|
+
aria-hidden="true"
|
|
188
|
+
/>
|
|
189
|
+
) : null}
|
|
190
|
+
</span>
|
|
191
|
+
</DropdownItem>
|
|
192
|
+
))}
|
|
193
|
+
</div>
|
|
194
|
+
</DropdownMenu>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
SuiteCommandItem,
|
|
7
|
+
SuiteCommandPaletteComponent,
|
|
8
|
+
} from '../lib/injected';
|
|
9
|
+
|
|
10
|
+
function isEditableTarget(): boolean {
|
|
11
|
+
if (typeof document === 'undefined') return false;
|
|
12
|
+
const el = document.activeElement;
|
|
13
|
+
return (
|
|
14
|
+
el instanceof HTMLElement &&
|
|
15
|
+
Boolean(
|
|
16
|
+
el.closest(
|
|
17
|
+
"input, textarea, select, [contenteditable='true'], [role='textbox']",
|
|
18
|
+
),
|
|
19
|
+
)
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Cmd/Ctrl+K command palette host. Apps build the item list and inject the
|
|
25
|
+
* theme CommandPalette primitive.
|
|
26
|
+
*/
|
|
27
|
+
export function CommandPaletteHost({
|
|
28
|
+
items,
|
|
29
|
+
isAuthenticated = true,
|
|
30
|
+
placeholder = 'Type a command or search…',
|
|
31
|
+
commandPalette: CommandPalette,
|
|
32
|
+
}: {
|
|
33
|
+
items: SuiteCommandItem[];
|
|
34
|
+
isAuthenticated?: boolean;
|
|
35
|
+
placeholder?: string;
|
|
36
|
+
commandPalette: SuiteCommandPaletteComponent;
|
|
37
|
+
}) {
|
|
38
|
+
const [open, setOpen] = useState(false);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const onKey = (e: KeyboardEvent) => {
|
|
42
|
+
if (!((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k')) return;
|
|
43
|
+
// Leave editors (BlockNote uses Cmd/Ctrl+K for links) alone unless the palette is open.
|
|
44
|
+
if (!open && isEditableTarget()) return;
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
setOpen((o) => !o);
|
|
47
|
+
};
|
|
48
|
+
window.addEventListener('keydown', onKey);
|
|
49
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
50
|
+
}, [open]);
|
|
51
|
+
|
|
52
|
+
if (!isAuthenticated) return null;
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<CommandPalette
|
|
56
|
+
items={items}
|
|
57
|
+
isOpen={open}
|
|
58
|
+
onClose={() => setOpen(false)}
|
|
59
|
+
placeholder={placeholder}
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
import { useIdleMount } from '../lib/use-idle-mount';
|
|
6
|
+
|
|
7
|
+
/** Command K — mount after idle so first paint stays light. */
|
|
8
|
+
export function DeferredChrome({ children }: { children: ReactNode }) {
|
|
9
|
+
const ready = useIdleMount();
|
|
10
|
+
if (!ready) return null;
|
|
11
|
+
return <>{children}</>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { type ReactNode, type PointerEvent } from 'react';
|
|
4
|
+
|
|
5
|
+
import { cn } from '../lib/cn';
|
|
6
|
+
|
|
7
|
+
export interface SuiteAppLayoutProps {
|
|
8
|
+
sidebar: ReactNode;
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
mobileHeader?: ReactNode;
|
|
11
|
+
bottomNav?: ReactNode;
|
|
12
|
+
/** Desktop sidebar width in px. */
|
|
13
|
+
sidebarWidth?: number;
|
|
14
|
+
/** True when the sidebar is collapsed to the icon rail. */
|
|
15
|
+
collapsed?: boolean;
|
|
16
|
+
/** Drag this to resize the sidebar. */
|
|
17
|
+
onStartResize?: (e: PointerEvent<HTMLDivElement>) => void;
|
|
18
|
+
/** Double-click to reset sidebar width. */
|
|
19
|
+
onResetWidth?: () => void;
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Responsive ShellStack app shell. Desktop: resizable fixed sidebar +
|
|
25
|
+
* scrollable content with an optional drag handle. Mobile/tablet: mobile
|
|
26
|
+
* header + content + bottom nav. This is the single layout primitive every
|
|
27
|
+
* app should use.
|
|
28
|
+
*/
|
|
29
|
+
export function SuiteAppLayout({
|
|
30
|
+
sidebar,
|
|
31
|
+
children,
|
|
32
|
+
mobileHeader,
|
|
33
|
+
bottomNav,
|
|
34
|
+
sidebarWidth = 240,
|
|
35
|
+
collapsed = false,
|
|
36
|
+
onStartResize,
|
|
37
|
+
onResetWidth,
|
|
38
|
+
className,
|
|
39
|
+
}: SuiteAppLayoutProps) {
|
|
40
|
+
return (
|
|
41
|
+
<div data-test="app-shell" className={cn('min-h-screen', className)}>
|
|
42
|
+
<div className="hidden lg:flex h-screen overflow-hidden">
|
|
43
|
+
<aside
|
|
44
|
+
className="relative flex shrink-0 flex-col border-r border-ph-border transition-[width] duration-200 ease-out"
|
|
45
|
+
style={{ width: `${sidebarWidth}px` }}
|
|
46
|
+
>
|
|
47
|
+
{sidebar}
|
|
48
|
+
{onStartResize ? (
|
|
49
|
+
<div
|
|
50
|
+
role="separator"
|
|
51
|
+
aria-orientation="vertical"
|
|
52
|
+
aria-label="Resize sidebar"
|
|
53
|
+
onPointerDown={onStartResize}
|
|
54
|
+
onDoubleClick={onResetWidth}
|
|
55
|
+
className="absolute inset-y-0 right-0 z-10 w-1 cursor-col-resize transition-colors hover:bg-[color-mix(in_oklab,var(--ph-accent)_30%,transparent)]"
|
|
56
|
+
/>
|
|
57
|
+
) : null}
|
|
58
|
+
</aside>
|
|
59
|
+
<main className="min-w-0 flex-1 overflow-y-auto bg-ph-canvas">{children}</main>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div className="flex min-h-screen flex-col lg:hidden">
|
|
63
|
+
{mobileHeader ? <div className="shrink-0">{mobileHeader}</div> : null}
|
|
64
|
+
<main className="min-w-0 flex-1 overflow-y-auto bg-ph-canvas">{children}</main>
|
|
65
|
+
{bottomNav ? <div className="shrink-0">{bottomNav}</div> : null}
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import type { ComponentType } from 'react';
|
|
5
|
+
|
|
6
|
+
import { cn } from '../lib/cn';
|
|
7
|
+
|
|
8
|
+
/** Any icon component that accepts sizing/stroke props (Lucide, theme icons). */
|
|
9
|
+
export type SuiteNavIcon = ComponentType<{
|
|
10
|
+
className?: string;
|
|
11
|
+
strokeWidth?: number | string;
|
|
12
|
+
'aria-hidden'?: boolean | 'true' | 'false';
|
|
13
|
+
}>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Suite bottom navigation — the mobile/tablet primary nav, identical in
|
|
17
|
+
* every app. Hick's Law: 5 items maximum (dev warning beyond that).
|
|
18
|
+
* Visuals come from suite-skin.css (`suite-bottom-nav`, `__pill`) so the
|
|
19
|
+
* translucent blur, safe-area padding, and press physics stay in sync
|
|
20
|
+
* across the suite; per-item content stays app-side via `items`.
|
|
21
|
+
*/
|
|
22
|
+
export interface SuiteBottomNavItem {
|
|
23
|
+
href: string;
|
|
24
|
+
label: string;
|
|
25
|
+
icon: SuiteNavIcon;
|
|
26
|
+
active?: boolean;
|
|
27
|
+
dataTest?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function SuiteBottomNav({
|
|
31
|
+
items,
|
|
32
|
+
ariaLabel = 'Primary navigation',
|
|
33
|
+
dataTest = 'suite-bottom-nav',
|
|
34
|
+
className,
|
|
35
|
+
}: {
|
|
36
|
+
items: readonly SuiteBottomNavItem[];
|
|
37
|
+
ariaLabel?: string;
|
|
38
|
+
dataTest?: string;
|
|
39
|
+
className?: string;
|
|
40
|
+
}) {
|
|
41
|
+
if (process.env.NODE_ENV !== 'production' && items.length > 5) {
|
|
42
|
+
// eslint-disable-next-line no-console
|
|
43
|
+
console.warn(
|
|
44
|
+
`SuiteBottomNav: ${items.length} items — bottom nav should have at most 5 (Hick's Law).`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const activeIndex = items.findIndex((item) => item.active);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<>
|
|
52
|
+
{/* In-flow spacer reserves the fixed bar's height so page content
|
|
53
|
+
never slides underneath it. Height = item height + safe area. */}
|
|
54
|
+
<div
|
|
55
|
+
aria-hidden
|
|
56
|
+
className="shrink-0 lg:hidden"
|
|
57
|
+
style={{ height: 'calc(60px + env(safe-area-inset-bottom))' }}
|
|
58
|
+
/>
|
|
59
|
+
<nav
|
|
60
|
+
aria-label={ariaLabel}
|
|
61
|
+
data-test={dataTest}
|
|
62
|
+
className={cn(
|
|
63
|
+
'suite-bottom-nav no-print z-40 grid auto-cols-fr grid-flow-col lg:hidden',
|
|
64
|
+
className,
|
|
65
|
+
)}
|
|
66
|
+
style={
|
|
67
|
+
{
|
|
68
|
+
gridTemplateColumns: `repeat(${items.length}, minmax(0, 1fr))`,
|
|
69
|
+
'--suite-nav-items': items.length,
|
|
70
|
+
'--suite-nav-active': activeIndex >= 0 ? activeIndex : 0,
|
|
71
|
+
} as React.CSSProperties
|
|
72
|
+
}
|
|
73
|
+
>
|
|
74
|
+
{activeIndex >= 0 ? (
|
|
75
|
+
<span aria-hidden className="suite-bottom-nav__indicator" />
|
|
76
|
+
) : null}
|
|
77
|
+
{items.map((item) => {
|
|
78
|
+
const Icon = item.icon;
|
|
79
|
+
const active = Boolean(item.active);
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<Link
|
|
83
|
+
key={item.href}
|
|
84
|
+
href={item.href}
|
|
85
|
+
data-test={item.dataTest ?? `suite-nav-${item.label.toLowerCase().replace(/\s+/g, '-')}`}
|
|
86
|
+
aria-current={active ? 'page' : undefined}
|
|
87
|
+
className={cn(
|
|
88
|
+
'suite-bottom-nav__item relative flex min-h-[60px] min-w-0 flex-col items-center justify-center gap-1 px-1 pb-1.5 pt-2 text-[10px] font-medium leading-none text-ph-mutedtext transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ph-brand motion-reduce:transition-none',
|
|
89
|
+
active && 'font-semibold text-ph-brand',
|
|
90
|
+
)}
|
|
91
|
+
>
|
|
92
|
+
<span
|
|
93
|
+
className={cn(
|
|
94
|
+
'suite-bottom-nav__icon relative z-10 inline-flex items-center justify-center',
|
|
95
|
+
active && 'suite-bottom-nav__icon--active',
|
|
96
|
+
)}
|
|
97
|
+
>
|
|
98
|
+
<Icon
|
|
99
|
+
className="h-6 w-6"
|
|
100
|
+
strokeWidth={active ? 2.25 : 1.75}
|
|
101
|
+
aria-hidden
|
|
102
|
+
/>
|
|
103
|
+
</span>
|
|
104
|
+
<span className="relative z-10 max-w-full truncate">
|
|
105
|
+
{item.label}
|
|
106
|
+
</span>
|
|
107
|
+
</Link>
|
|
108
|
+
);
|
|
109
|
+
})}
|
|
110
|
+
</nav>
|
|
111
|
+
</>
|
|
112
|
+
);
|
|
113
|
+
}
|