@xenide-io/the-old-ui-theme 0.3.2 → 0.4.3
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-54ZR4VL5.mjs +4641 -0
- package/dist/chunk-G53YLHVE.mjs +1023 -0
- 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 +86 -342
- package/dist/suite.d.mts +760 -0
- package/dist/suite.d.ts +760 -0
- package/dist/suite.js +2800 -0
- package/dist/suite.mjs +2441 -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 +13 -11
- 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 +285 -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 +198 -0
- package/src/suite/components/suite-skeleton.tsx +191 -0
- package/src/suite/components/suite-user-menu.tsx +109 -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/dist/chunk-5HSOBJ4F.mjs +0 -5968
- 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,285 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ComponentType, ElementType, ReactNode, SVGProps } from 'react';
|
|
4
|
+
import { cn } from '../lib/cn';
|
|
5
|
+
import { TodayTimeIcon } from './today-ui';
|
|
6
|
+
|
|
7
|
+
type LucideIcon = ComponentType<SVGProps<SVGSVGElement>>;
|
|
8
|
+
|
|
9
|
+
const WIDTHS = {
|
|
10
|
+
full: 'max-w-none',
|
|
11
|
+
wide: 'max-w-7xl',
|
|
12
|
+
content: 'max-w-6xl',
|
|
13
|
+
narrow: 'max-w-4xl',
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
16
|
+
export type SuitePageWidth = keyof typeof WIDTHS;
|
|
17
|
+
|
|
18
|
+
export interface SuiteBreadcrumbsProps {
|
|
19
|
+
items: { label: ReactNode; href?: string }[];
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Minimal slash-separated breadcrumb line. */
|
|
24
|
+
export function SuiteBreadcrumbs({ items, className }: SuiteBreadcrumbsProps) {
|
|
25
|
+
return (
|
|
26
|
+
<nav aria-label="Breadcrumb" className={className}>
|
|
27
|
+
<ol className="flex min-w-0 items-center gap-1.5">
|
|
28
|
+
{items.map((item, index) => {
|
|
29
|
+
const isLast = index === items.length - 1;
|
|
30
|
+
return (
|
|
31
|
+
<li key={index} className="flex min-w-0 items-center gap-1.5">
|
|
32
|
+
{index > 0 ? (
|
|
33
|
+
<span className="select-none text-ph-border" aria-hidden="true">
|
|
34
|
+
/
|
|
35
|
+
</span>
|
|
36
|
+
) : null}
|
|
37
|
+
{item.href && !isLast ? (
|
|
38
|
+
<a
|
|
39
|
+
href={item.href}
|
|
40
|
+
className="truncate transition hover:text-ph-ink"
|
|
41
|
+
>
|
|
42
|
+
{item.label}
|
|
43
|
+
</a>
|
|
44
|
+
) : (
|
|
45
|
+
<span
|
|
46
|
+
className={cn(
|
|
47
|
+
'truncate',
|
|
48
|
+
isLast ? 'font-medium text-ph-ink' : 'text-ph-subtle',
|
|
49
|
+
)}
|
|
50
|
+
aria-current={isLast ? 'page' : undefined}
|
|
51
|
+
>
|
|
52
|
+
{item.label}
|
|
53
|
+
</span>
|
|
54
|
+
)}
|
|
55
|
+
</li>
|
|
56
|
+
);
|
|
57
|
+
})}
|
|
58
|
+
</ol>
|
|
59
|
+
</nav>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function SuitePage({
|
|
64
|
+
children,
|
|
65
|
+
width = 'wide',
|
|
66
|
+
inset = true,
|
|
67
|
+
className,
|
|
68
|
+
as: Component = 'div',
|
|
69
|
+
dataTest,
|
|
70
|
+
id,
|
|
71
|
+
role,
|
|
72
|
+
'aria-live': ariaLive,
|
|
73
|
+
}: {
|
|
74
|
+
children: ReactNode;
|
|
75
|
+
width?: SuitePageWidth;
|
|
76
|
+
inset?: boolean;
|
|
77
|
+
className?: string;
|
|
78
|
+
as?: ElementType;
|
|
79
|
+
dataTest?: string;
|
|
80
|
+
id?: string;
|
|
81
|
+
role?: string;
|
|
82
|
+
'aria-live'?: React.AriaAttributes['aria-live'];
|
|
83
|
+
}) {
|
|
84
|
+
return (
|
|
85
|
+
<Component
|
|
86
|
+
id={id}
|
|
87
|
+
data-test={dataTest}
|
|
88
|
+
role={role}
|
|
89
|
+
aria-live={ariaLive}
|
|
90
|
+
className={cn(
|
|
91
|
+
'mx-auto flex w-full min-w-0 flex-col',
|
|
92
|
+
WIDTHS[width],
|
|
93
|
+
// Tight inset keeps page titles aligned without excessive whitespace.
|
|
94
|
+
inset && 'px-4 pb-6 pt-4 sm:px-6 sm:pb-7 sm:pt-5 lg:px-8',
|
|
95
|
+
className,
|
|
96
|
+
)}
|
|
97
|
+
>
|
|
98
|
+
{children}
|
|
99
|
+
</Component>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function SuitePageHeader({
|
|
104
|
+
title,
|
|
105
|
+
description,
|
|
106
|
+
eyebrow,
|
|
107
|
+
actions,
|
|
108
|
+
className,
|
|
109
|
+
titleClassName,
|
|
110
|
+
dataTest,
|
|
111
|
+
/** Optional icon rendered before the title. */
|
|
112
|
+
icon,
|
|
113
|
+
/** Use the animated sun/moon icon from the Today page. */
|
|
114
|
+
todayIcon,
|
|
115
|
+
todayIconClassName,
|
|
116
|
+
/** Small badge/pill next to the title (e.g. "Beta", org name). */
|
|
117
|
+
badge,
|
|
118
|
+
/** Breadcrumb line rendered above the title. */
|
|
119
|
+
breadcrumb,
|
|
120
|
+
}: {
|
|
121
|
+
title: ReactNode;
|
|
122
|
+
description?: ReactNode;
|
|
123
|
+
eyebrow?: ReactNode;
|
|
124
|
+
actions?: ReactNode;
|
|
125
|
+
className?: string;
|
|
126
|
+
titleClassName?: string;
|
|
127
|
+
dataTest?: string;
|
|
128
|
+
icon?: ReactNode;
|
|
129
|
+
todayIcon?: boolean;
|
|
130
|
+
todayIconClassName?: string;
|
|
131
|
+
badge?: ReactNode;
|
|
132
|
+
breadcrumb?: ReactNode;
|
|
133
|
+
}) {
|
|
134
|
+
const resolvedIcon = todayIcon ? (
|
|
135
|
+
<TodayTimeIcon
|
|
136
|
+
className={cn(
|
|
137
|
+
'h-6 w-6 sm:h-7 sm:w-7 text-amber-500',
|
|
138
|
+
todayIconClassName,
|
|
139
|
+
)}
|
|
140
|
+
/>
|
|
141
|
+
) : (
|
|
142
|
+
icon
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<header
|
|
147
|
+
data-test={dataTest}
|
|
148
|
+
className={cn(
|
|
149
|
+
'flex items-start justify-between gap-4 pb-4 sm:pb-5',
|
|
150
|
+
className,
|
|
151
|
+
)}
|
|
152
|
+
>
|
|
153
|
+
<div className="flex min-w-0 items-start gap-3">
|
|
154
|
+
{resolvedIcon ? (
|
|
155
|
+
<div
|
|
156
|
+
className="hidden shrink-0 pt-1 sm:block"
|
|
157
|
+
aria-hidden="true"
|
|
158
|
+
>
|
|
159
|
+
{resolvedIcon}
|
|
160
|
+
</div>
|
|
161
|
+
) : null}
|
|
162
|
+
<div className="flex min-w-0 flex-col gap-1">
|
|
163
|
+
{eyebrow ? (
|
|
164
|
+
<div className="mb-0.5 flex min-w-0 items-center gap-1.5 text-xs text-ph-subtle">
|
|
165
|
+
{eyebrow}
|
|
166
|
+
</div>
|
|
167
|
+
) : null}
|
|
168
|
+
{breadcrumb ? (
|
|
169
|
+
<div className="mb-0.5 flex min-w-0 items-center gap-1.5 text-xs text-ph-subtle">
|
|
170
|
+
{breadcrumb}
|
|
171
|
+
</div>
|
|
172
|
+
) : null}
|
|
173
|
+
<div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
|
|
174
|
+
<h1
|
|
175
|
+
className={cn(
|
|
176
|
+
'font-display text-[1.625rem] font-bold leading-tight tracking-tight text-ph-ink sm:text-[2.25rem]',
|
|
177
|
+
titleClassName,
|
|
178
|
+
)}
|
|
179
|
+
>
|
|
180
|
+
{title}
|
|
181
|
+
</h1>
|
|
182
|
+
{badge ? (
|
|
183
|
+
<span className="rounded-md bg-ph-muted px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-ph-subtle">
|
|
184
|
+
{badge}
|
|
185
|
+
</span>
|
|
186
|
+
) : null}
|
|
187
|
+
</div>
|
|
188
|
+
{description ? (
|
|
189
|
+
<div className="mt-1 max-w-2xl text-pretty text-sm leading-5 text-ph-subtle">
|
|
190
|
+
{description}
|
|
191
|
+
</div>
|
|
192
|
+
) : null}
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
{actions ? (
|
|
196
|
+
<div className="flex shrink-0 items-center gap-2 pt-1">
|
|
197
|
+
{actions}
|
|
198
|
+
</div>
|
|
199
|
+
) : null}
|
|
200
|
+
</header>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function SuiteToolbar({
|
|
205
|
+
children,
|
|
206
|
+
className,
|
|
207
|
+
label = 'Page tools',
|
|
208
|
+
dataTest,
|
|
209
|
+
}: {
|
|
210
|
+
children: ReactNode;
|
|
211
|
+
className?: string;
|
|
212
|
+
label?: string;
|
|
213
|
+
dataTest?: string;
|
|
214
|
+
}) {
|
|
215
|
+
return (
|
|
216
|
+
<div
|
|
217
|
+
role="toolbar"
|
|
218
|
+
aria-label={label}
|
|
219
|
+
data-test={dataTest}
|
|
220
|
+
className={cn('flex min-w-0 flex-wrap items-center gap-2', className)}
|
|
221
|
+
>
|
|
222
|
+
{children}
|
|
223
|
+
</div>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function SuiteTabList({
|
|
228
|
+
children,
|
|
229
|
+
className,
|
|
230
|
+
label,
|
|
231
|
+
dataTest,
|
|
232
|
+
}: {
|
|
233
|
+
children: ReactNode;
|
|
234
|
+
className?: string;
|
|
235
|
+
label: string;
|
|
236
|
+
dataTest?: string;
|
|
237
|
+
}) {
|
|
238
|
+
return (
|
|
239
|
+
<nav
|
|
240
|
+
aria-label={label}
|
|
241
|
+
data-test={dataTest}
|
|
242
|
+
className={cn(
|
|
243
|
+
'flex min-w-0 flex-wrap gap-2 [&_a]:min-h-[48px] [&_button]:min-h-[48px] sm:[&_a]:min-h-[44px] sm:[&_button]:min-h-[44px]',
|
|
244
|
+
className,
|
|
245
|
+
)}
|
|
246
|
+
>
|
|
247
|
+
{children}
|
|
248
|
+
</nav>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function SuiteSectionHeader({
|
|
253
|
+
title,
|
|
254
|
+
description,
|
|
255
|
+
action,
|
|
256
|
+
className,
|
|
257
|
+
}: {
|
|
258
|
+
title: ReactNode;
|
|
259
|
+
description?: ReactNode;
|
|
260
|
+
action?: ReactNode;
|
|
261
|
+
className?: string;
|
|
262
|
+
}) {
|
|
263
|
+
return (
|
|
264
|
+
<div
|
|
265
|
+
className={cn(
|
|
266
|
+
'flex min-w-0 flex-col gap-4 sm:flex-row sm:items-end sm:justify-between',
|
|
267
|
+
className,
|
|
268
|
+
)}
|
|
269
|
+
>
|
|
270
|
+
<div className="min-w-0">
|
|
271
|
+
<h2 className="font-display text-balance text-lg font-semibold tracking-tight text-ph-ink sm:text-xl">
|
|
272
|
+
{title}
|
|
273
|
+
</h2>
|
|
274
|
+
{description ? (
|
|
275
|
+
<div className="mt-1 max-w-2xl text-pretty text-sm leading-5 text-ph-subtle">
|
|
276
|
+
{description}
|
|
277
|
+
</div>
|
|
278
|
+
) : null}
|
|
279
|
+
</div>
|
|
280
|
+
{action ? (
|
|
281
|
+
<div className="flex min-h-[44px] shrink-0 items-center">{action}</div>
|
|
282
|
+
) : null}
|
|
283
|
+
</div>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState, type ReactNode } from 'react';
|
|
4
|
+
import { Xmark as X } from 'iconoir-react';
|
|
5
|
+
|
|
6
|
+
import { cn } from '../lib/cn';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Suite mobile navigation drawer. Backdrop fade + panel slide, Escape to
|
|
10
|
+
* close, body scroll-lock and focus hand-off while open. Nav content stays
|
|
11
|
+
* app-side via `children`.
|
|
12
|
+
*/
|
|
13
|
+
export function SuiteMobileDrawer({
|
|
14
|
+
open,
|
|
15
|
+
onClose,
|
|
16
|
+
children,
|
|
17
|
+
side = 'left',
|
|
18
|
+
showCloseButton = false,
|
|
19
|
+
closeLabel = 'Close navigation',
|
|
20
|
+
backdropLabel = 'Close navigation',
|
|
21
|
+
dialogLabel = 'Navigation',
|
|
22
|
+
durationMs = 200,
|
|
23
|
+
dataTest,
|
|
24
|
+
panelDataTest,
|
|
25
|
+
panelClassName,
|
|
26
|
+
}: {
|
|
27
|
+
open: boolean;
|
|
28
|
+
onClose: () => void;
|
|
29
|
+
children: ReactNode;
|
|
30
|
+
/** Edge the panel slides in from. */
|
|
31
|
+
side?: 'left' | 'right';
|
|
32
|
+
/** Standard close row (h-14, trailing X button) above the content. */
|
|
33
|
+
showCloseButton?: boolean;
|
|
34
|
+
closeLabel?: string;
|
|
35
|
+
backdropLabel?: string;
|
|
36
|
+
/** Accessible name for the dialog panel. */
|
|
37
|
+
dialogLabel?: string;
|
|
38
|
+
/** Slide/fade duration; also the close-transition unmount delay. */
|
|
39
|
+
durationMs?: number;
|
|
40
|
+
dataTest?: string;
|
|
41
|
+
panelDataTest?: string;
|
|
42
|
+
/** Panel surface overrides (e.g. `bg-ph-canvas`). */
|
|
43
|
+
panelClassName?: string;
|
|
44
|
+
}) {
|
|
45
|
+
// `rendered` keeps the drawer mounted through the close transition;
|
|
46
|
+
// `shown` drives the slide/fade end-state.
|
|
47
|
+
const [rendered, setRendered] = useState(open);
|
|
48
|
+
const [shown, setShown] = useState(open);
|
|
49
|
+
const panelRef = useRef<HTMLElement | null>(null);
|
|
50
|
+
const onCloseRef = useRef(onClose);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
onCloseRef.current = onClose;
|
|
54
|
+
}, [onClose]);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (open) {
|
|
58
|
+
queueMicrotask(() => setRendered(true));
|
|
59
|
+
// Double rAF: paint the off-canvas frame before transitioning in.
|
|
60
|
+
let inner = 0;
|
|
61
|
+
const outer = requestAnimationFrame(() => {
|
|
62
|
+
inner = requestAnimationFrame(() => setShown(true));
|
|
63
|
+
});
|
|
64
|
+
return () => {
|
|
65
|
+
cancelAnimationFrame(outer);
|
|
66
|
+
cancelAnimationFrame(inner);
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
queueMicrotask(() => setShown(false));
|
|
70
|
+
const timeout = window.setTimeout(() => setRendered(false), durationMs);
|
|
71
|
+
return () => window.clearTimeout(timeout);
|
|
72
|
+
}, [open, durationMs]);
|
|
73
|
+
|
|
74
|
+
// Escape to close, body scroll-lock, and focus inside while open.
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (!open) return;
|
|
77
|
+
|
|
78
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
79
|
+
if (event.key === 'Escape') onCloseRef.current();
|
|
80
|
+
};
|
|
81
|
+
window.addEventListener('keydown', onKeyDown);
|
|
82
|
+
|
|
83
|
+
const previousOverflow = document.body.style.overflow;
|
|
84
|
+
document.body.style.overflow = 'hidden';
|
|
85
|
+
|
|
86
|
+
const previouslyFocused =
|
|
87
|
+
document.activeElement instanceof HTMLElement
|
|
88
|
+
? document.activeElement
|
|
89
|
+
: null;
|
|
90
|
+
const frame = requestAnimationFrame(() => panelRef.current?.focus());
|
|
91
|
+
|
|
92
|
+
return () => {
|
|
93
|
+
window.removeEventListener('keydown', onKeyDown);
|
|
94
|
+
document.body.style.overflow = previousOverflow;
|
|
95
|
+
cancelAnimationFrame(frame);
|
|
96
|
+
if (previouslyFocused?.isConnected) previouslyFocused.focus();
|
|
97
|
+
};
|
|
98
|
+
}, [open]);
|
|
99
|
+
|
|
100
|
+
if (!rendered) return null;
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div data-test={dataTest} className="no-print fixed inset-0 z-50 lg:hidden">
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
tabIndex={-1}
|
|
107
|
+
className={cn(
|
|
108
|
+
'absolute inset-0 bg-black/25 backdrop-blur-sm motion-safe:transition-opacity motion-safe:ease-spring-subtle',
|
|
109
|
+
shown ? 'opacity-100' : 'opacity-0',
|
|
110
|
+
)}
|
|
111
|
+
style={{ transitionDuration: `${durationMs}ms` }}
|
|
112
|
+
onClick={onClose}
|
|
113
|
+
aria-label={backdropLabel}
|
|
114
|
+
/>
|
|
115
|
+
<aside
|
|
116
|
+
ref={panelRef}
|
|
117
|
+
role="dialog"
|
|
118
|
+
aria-modal="true"
|
|
119
|
+
aria-label={dialogLabel}
|
|
120
|
+
tabIndex={-1}
|
|
121
|
+
data-test={panelDataTest}
|
|
122
|
+
className={cn(
|
|
123
|
+
'suite-scroll-lock relative flex h-full w-[86%] max-w-64 flex-col overflow-hidden bg-ph-surface shadow-xl outline-none motion-safe:transition-transform motion-safe:ease-spring-fast',
|
|
124
|
+
side === 'right' && 'ml-auto',
|
|
125
|
+
shown
|
|
126
|
+
? 'translate-x-0'
|
|
127
|
+
: side === 'right'
|
|
128
|
+
? 'translate-x-full'
|
|
129
|
+
: '-translate-x-full',
|
|
130
|
+
panelClassName,
|
|
131
|
+
)}
|
|
132
|
+
style={{ transitionDuration: `${durationMs}ms` }}
|
|
133
|
+
>
|
|
134
|
+
{showCloseButton ? (
|
|
135
|
+
<div className="flex h-14 shrink-0 items-center justify-end border-b border-ph-border px-3">
|
|
136
|
+
<button
|
|
137
|
+
type="button"
|
|
138
|
+
onClick={onClose}
|
|
139
|
+
className="-mr-1.5 inline-flex h-11 w-11 items-center justify-center rounded-lg text-ph-mutedtext transition-colors hover:bg-ph-muted hover:text-ph-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-brand"
|
|
140
|
+
aria-label={closeLabel}
|
|
141
|
+
>
|
|
142
|
+
<X className="h-4 w-4" aria-hidden />
|
|
143
|
+
</button>
|
|
144
|
+
</div>
|
|
145
|
+
) : null}
|
|
146
|
+
{children}
|
|
147
|
+
</aside>
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ComponentType, ReactNode } from 'react';
|
|
4
|
+
import { Menu } from 'iconoir-react';
|
|
5
|
+
|
|
6
|
+
import { cn } from '../lib/cn';
|
|
7
|
+
|
|
8
|
+
type SuiteMenuIcon = ComponentType<{ className?: string; 'aria-hidden'?: boolean | 'true' | 'false' }>;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Suite mobile app bar — the standardized top of every app on phone/tablet.
|
|
12
|
+
* Fixed slot order across the suite: [menu] [brand/context] … [actions].
|
|
13
|
+
* Translucent + blurred (suite-skin.css `.suite-app-bar`), safe-area aware,
|
|
14
|
+
* 44px minimum touch targets. Desktop chrome (rail/top bar) stays app-side.
|
|
15
|
+
*/
|
|
16
|
+
export function SuiteMobileHeader({
|
|
17
|
+
onMenuClick,
|
|
18
|
+
menuIcon: MenuIcon = Menu,
|
|
19
|
+
title,
|
|
20
|
+
actions,
|
|
21
|
+
menuLabel = 'Open navigation',
|
|
22
|
+
menuDataTest,
|
|
23
|
+
dataTest,
|
|
24
|
+
className,
|
|
25
|
+
}: {
|
|
26
|
+
/** Optional — omit to drop the hamburger (bottom-nav apps don't need one). */
|
|
27
|
+
onMenuClick?: () => void;
|
|
28
|
+
/** Leading button glyph — defaults to Menu; browse-style drawers can swap it. */
|
|
29
|
+
menuIcon?: SuiteMenuIcon;
|
|
30
|
+
/** Brand/context slot — app mark + title or app switcher. */
|
|
31
|
+
title?: ReactNode;
|
|
32
|
+
/** Right-hand slot — search, bell, avatar (keep this order). */
|
|
33
|
+
actions?: ReactNode;
|
|
34
|
+
menuLabel?: string;
|
|
35
|
+
menuDataTest?: string;
|
|
36
|
+
dataTest?: string;
|
|
37
|
+
className?: string;
|
|
38
|
+
}) {
|
|
39
|
+
return (
|
|
40
|
+
<header
|
|
41
|
+
data-test={dataTest}
|
|
42
|
+
className={cn(
|
|
43
|
+
'suite-app-bar no-print sticky top-0 z-30 flex min-h-[56px] shrink-0 items-center gap-1 border-b border-ph-border px-2 lg:hidden',
|
|
44
|
+
className,
|
|
45
|
+
)}
|
|
46
|
+
>
|
|
47
|
+
{onMenuClick ? (
|
|
48
|
+
<button
|
|
49
|
+
type="button"
|
|
50
|
+
data-test={menuDataTest}
|
|
51
|
+
className="suite-press inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-xl text-ph-ink transition-colors hover:bg-ph-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-brand"
|
|
52
|
+
onClick={onMenuClick}
|
|
53
|
+
aria-label={menuLabel}
|
|
54
|
+
>
|
|
55
|
+
<MenuIcon className="h-5 w-5" aria-hidden />
|
|
56
|
+
</button>
|
|
57
|
+
) : null}
|
|
58
|
+
<div
|
|
59
|
+
className={cn(
|
|
60
|
+
'flex min-w-0 flex-1 items-center',
|
|
61
|
+
!onMenuClick && 'pl-2',
|
|
62
|
+
)}
|
|
63
|
+
>
|
|
64
|
+
{title}
|
|
65
|
+
</div>
|
|
66
|
+
{actions ? (
|
|
67
|
+
<div className="flex shrink-0 items-center gap-1">{actions}</div>
|
|
68
|
+
) : null}
|
|
69
|
+
</header>
|
|
70
|
+
);
|
|
71
|
+
}
|