@xenide-io/the-old-ui-theme 0.4.3 → 0.4.6
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 → chunk-RZXHZWUB.mjs} +44 -17
- package/dist/{index-B5NOyoKS.d.mts → index-Od4pIP4F.d.mts} +7 -1
- package/dist/{index-B5NOyoKS.d.ts → index-Od4pIP4F.d.ts} +7 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +37 -10
- package/dist/index.mjs +1 -1
- package/dist/suite.d.mts +152 -41
- package/dist/suite.d.ts +152 -41
- package/dist/suite.js +1184 -661
- package/dist/suite.mjs +1123 -601
- package/dist/ui.d.mts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +37 -10
- package/dist/ui.mjs +1 -1
- package/package.json +7 -3
- package/src/components/ui/Modal.tsx +70 -26
- package/src/styles/suite-skin.css +284 -24
- package/src/suite/components/ai-panel.tsx +269 -103
- package/src/suite/components/suite-app-layout.tsx +48 -28
- package/src/suite/components/suite-layout.tsx +85 -41
- package/src/suite/components/suite-mobile-drawer.tsx +35 -22
- package/src/suite/components/suite-notification-bell.tsx +9 -3
- package/src/suite/components/suite-settings-layout.tsx +82 -0
- package/src/suite/components/suite-settings-mobile-nav.tsx +18 -17
- package/src/suite/components/suite-skeleton.tsx +3 -3
- package/src/suite/components/today-calibrating.tsx +11 -35
- package/src/suite/components/today-page-frame.tsx +19 -11
- package/src/suite/components/today-ui.tsx +276 -19
- package/src/suite/index.ts +41 -25
- package/src/suite/lib/apps.ts +32 -2
- package/src/suite/lib/use-sidebar-width.ts +114 -0
- package/src/components/sections/AccordionShowcase.tsx +0 -45
- package/src/components/sections/AlertShowcase.tsx +0 -40
- package/src/components/sections/AvatarShowcase.tsx +0 -80
- package/src/components/sections/BadgeShowcase.tsx +0 -65
- package/src/components/sections/ButtonShowcase.tsx +0 -308
- package/src/components/sections/CalendarShowcase.tsx +0 -35
- package/src/components/sections/CardShowcase.tsx +0 -159
- package/src/components/sections/CodeMockupShowcase.tsx +0 -56
- package/src/components/sections/ColorPalette.tsx +0 -117
- package/src/components/sections/CommandPaletteShowcase.tsx +0 -48
- package/src/components/sections/CountdownShowcase.tsx +0 -43
- package/src/components/sections/DiffShowcase.tsx +0 -70
- package/src/components/sections/DrawerShowcase.tsx +0 -97
- package/src/components/sections/DropdownShowcase.tsx +0 -98
- package/src/components/sections/EmptyStateShowcase.tsx +0 -50
- package/src/components/sections/FilterChipsShowcase.tsx +0 -98
- package/src/components/sections/FormShowcase.tsx +0 -127
- package/src/components/sections/HoverCardShowcase.tsx +0 -50
- package/src/components/sections/IconShowcase.tsx +0 -121
- package/src/components/sections/IndicatorShowcase.tsx +0 -52
- package/src/components/sections/KbdShowcase.tsx +0 -57
- package/src/components/sections/ModalShowcase.tsx +0 -211
- package/src/components/sections/NavigationShowcase.tsx +0 -199
- package/src/components/sections/NewComponentsShowcase.tsx +0 -1052
- package/src/components/sections/ProductLayoutsShowcase.tsx +0 -78
- package/src/components/sections/ProgressShowcase.tsx +0 -82
- package/src/components/sections/QuillChartShowcase.tsx +0 -92
- package/src/components/sections/QuillDashboardShowcase.tsx +0 -149
- package/src/components/sections/SegmentedControlShowcase.tsx +0 -49
- package/src/components/sections/SetupThemeShowcase.tsx +0 -262
- package/src/components/sections/SidebarShowcase.tsx +0 -152
- package/src/components/sections/StackShowcase.tsx +0 -33
- package/src/components/sections/StepperShowcase.tsx +0 -37
- package/src/components/sections/SuiteShowcase.tsx +0 -669
- package/src/components/sections/SwapShowcase.tsx +0 -99
- package/src/components/sections/TabShowcase.tsx +0 -84
- package/src/components/sections/TableShowcase.tsx +0 -142
- package/src/components/sections/TimelineShowcase.tsx +0 -83
- package/src/components/sections/ToastShowcase.tsx +0 -108
- package/src/components/sections/TooltipShowcase.tsx +0 -38
- package/src/components/sections/UtilityShowcase.tsx +0 -81
- package/src/styles/excel-themes.css +0 -110
- package/src/styles/lemon-primitives.css +0 -550
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import {
|
|
4
|
+
useEffect,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
type ElementType,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
} from 'react';
|
|
4
10
|
import { cn } from '../lib/cn';
|
|
5
11
|
import { TodayTimeIcon } from './today-ui';
|
|
6
12
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
} as const;
|
|
15
|
-
|
|
16
|
-
export type SuitePageWidth = keyof typeof WIDTHS;
|
|
13
|
+
/** Width tokens — applied as data-width; CSS in suite-skin.css owns max-width. */
|
|
14
|
+
export type SuitePageWidth =
|
|
15
|
+
| 'full'
|
|
16
|
+
| 'wide'
|
|
17
|
+
| 'content'
|
|
18
|
+
| 'narrow'
|
|
19
|
+
| 'reading';
|
|
17
20
|
|
|
18
21
|
export interface SuiteBreadcrumbsProps {
|
|
19
22
|
items: { label: ReactNode; href?: string }[];
|
|
@@ -62,7 +65,7 @@ export function SuiteBreadcrumbs({ items, className }: SuiteBreadcrumbsProps) {
|
|
|
62
65
|
|
|
63
66
|
export function SuitePage({
|
|
64
67
|
children,
|
|
65
|
-
width = '
|
|
68
|
+
width = 'full',
|
|
66
69
|
inset = true,
|
|
67
70
|
className,
|
|
68
71
|
as: Component = 'div',
|
|
@@ -85,13 +88,12 @@ export function SuitePage({
|
|
|
85
88
|
<Component
|
|
86
89
|
id={id}
|
|
87
90
|
data-test={dataTest}
|
|
91
|
+
data-width={width}
|
|
88
92
|
role={role}
|
|
89
93
|
aria-live={ariaLive}
|
|
90
94
|
className={cn(
|
|
91
|
-
'
|
|
92
|
-
|
|
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
|
+
'suite-page-stage flex w-full min-w-0 flex-col',
|
|
96
|
+
inset && 'suite-page-inset',
|
|
95
97
|
className,
|
|
96
98
|
)}
|
|
97
99
|
>
|
|
@@ -117,6 +119,8 @@ export function SuitePageHeader({
|
|
|
117
119
|
badge,
|
|
118
120
|
/** Breadcrumb line rendered above the title. */
|
|
119
121
|
breadcrumb,
|
|
122
|
+
/** Stick to the top of the scrollport (desktop page titles). */
|
|
123
|
+
sticky = false,
|
|
120
124
|
}: {
|
|
121
125
|
title: ReactNode;
|
|
122
126
|
description?: ReactNode;
|
|
@@ -130,11 +134,40 @@ export function SuitePageHeader({
|
|
|
130
134
|
todayIconClassName?: string;
|
|
131
135
|
badge?: ReactNode;
|
|
132
136
|
breadcrumb?: ReactNode;
|
|
137
|
+
sticky?: boolean;
|
|
133
138
|
}) {
|
|
139
|
+
const headerRef = useRef<HTMLElement>(null);
|
|
140
|
+
const [stuck, setStuck] = useState(false);
|
|
141
|
+
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
if (!sticky) return;
|
|
144
|
+
const el = headerRef.current;
|
|
145
|
+
if (!el) return;
|
|
146
|
+
|
|
147
|
+
// Sentinel just above the header — when it leaves the scrollport, we're stuck.
|
|
148
|
+
const sentinel = document.createElement('div');
|
|
149
|
+
sentinel.setAttribute('aria-hidden', 'true');
|
|
150
|
+
sentinel.style.cssText =
|
|
151
|
+
'position:relative;height:1px;width:100%;margin:0;padding:0;pointer-events:none';
|
|
152
|
+
el.parentElement?.insertBefore(sentinel, el);
|
|
153
|
+
|
|
154
|
+
const observer = new IntersectionObserver(
|
|
155
|
+
([entry]) => {
|
|
156
|
+
setStuck(!entry.isIntersecting);
|
|
157
|
+
},
|
|
158
|
+
{ threshold: [1], root: null },
|
|
159
|
+
);
|
|
160
|
+
observer.observe(sentinel);
|
|
161
|
+
return () => {
|
|
162
|
+
observer.disconnect();
|
|
163
|
+
sentinel.remove();
|
|
164
|
+
};
|
|
165
|
+
}, [sticky]);
|
|
166
|
+
|
|
134
167
|
const resolvedIcon = todayIcon ? (
|
|
135
168
|
<TodayTimeIcon
|
|
136
169
|
className={cn(
|
|
137
|
-
'h-
|
|
170
|
+
'h-7 w-7 text-amber-500 sm:h-8 sm:w-8',
|
|
138
171
|
todayIconClassName,
|
|
139
172
|
)}
|
|
140
173
|
/>
|
|
@@ -144,36 +177,42 @@ export function SuitePageHeader({
|
|
|
144
177
|
|
|
145
178
|
return (
|
|
146
179
|
<header
|
|
180
|
+
ref={headerRef}
|
|
147
181
|
data-test={dataTest}
|
|
182
|
+
data-stuck={sticky ? (stuck ? 'true' : 'false') : undefined}
|
|
148
183
|
className={cn(
|
|
149
|
-
'flex items-start justify-between gap-4 pb-
|
|
184
|
+
'flex items-start justify-between gap-4 pb-2 sm:pb-3',
|
|
185
|
+
// No extra pt here — page inset sets the shared title baseline.
|
|
186
|
+
// Stuck state adds pad via .suite-page-header-sticky[data-stuck].
|
|
187
|
+
sticky && 'suite-page-header-sticky',
|
|
150
188
|
className,
|
|
151
189
|
)}
|
|
152
190
|
>
|
|
153
|
-
<div className="
|
|
154
|
-
{
|
|
155
|
-
<div
|
|
156
|
-
|
|
157
|
-
aria-hidden="true"
|
|
158
|
-
>
|
|
159
|
-
{resolvedIcon}
|
|
191
|
+
<div className="min-w-0 flex-1">
|
|
192
|
+
{eyebrow ? (
|
|
193
|
+
<div className="mb-0.5 flex min-w-0 items-center gap-1.5 text-xs text-ph-subtle">
|
|
194
|
+
{eyebrow}
|
|
160
195
|
</div>
|
|
161
196
|
) : null}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
197
|
+
{breadcrumb ? (
|
|
198
|
+
<div className="mb-0.5 flex min-w-0 items-center gap-1.5 text-xs text-ph-subtle">
|
|
199
|
+
{breadcrumb}
|
|
200
|
+
</div>
|
|
201
|
+
) : null}
|
|
202
|
+
{/* Icon aligns with the title line only — not the description. */}
|
|
203
|
+
<div className="flex min-w-0 items-center gap-3">
|
|
204
|
+
{resolvedIcon ? (
|
|
205
|
+
<div
|
|
206
|
+
className="hidden shrink-0 sm:flex sm:items-center [&_svg]:h-7 [&_svg]:w-7 sm:[&_svg]:h-8 sm:[&_svg]:w-8"
|
|
207
|
+
aria-hidden="true"
|
|
208
|
+
>
|
|
209
|
+
{resolvedIcon}
|
|
171
210
|
</div>
|
|
172
211
|
) : null}
|
|
173
212
|
<div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
|
|
174
213
|
<h1
|
|
175
214
|
className={cn(
|
|
176
|
-
'font-display text-[1.625rem] font-bold leading-
|
|
215
|
+
'font-display text-[1.625rem] font-bold leading-none tracking-tight text-ph-ink sm:text-[2.25rem]',
|
|
177
216
|
titleClassName,
|
|
178
217
|
)}
|
|
179
218
|
>
|
|
@@ -185,15 +224,20 @@ export function SuitePageHeader({
|
|
|
185
224
|
</span>
|
|
186
225
|
) : null}
|
|
187
226
|
</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
227
|
</div>
|
|
228
|
+
{description ? (
|
|
229
|
+
<div
|
|
230
|
+
className={cn(
|
|
231
|
+
'mt-1.5 max-w-2xl text-pretty text-sm leading-5 text-ph-subtle',
|
|
232
|
+
resolvedIcon && 'sm:pl-11',
|
|
233
|
+
)}
|
|
234
|
+
>
|
|
235
|
+
{description}
|
|
236
|
+
</div>
|
|
237
|
+
) : null}
|
|
194
238
|
</div>
|
|
195
239
|
{actions ? (
|
|
196
|
-
<div className="flex shrink-0 items-center gap-2
|
|
240
|
+
<div className="flex shrink-0 items-center gap-2 self-center">
|
|
197
241
|
{actions}
|
|
198
242
|
</div>
|
|
199
243
|
) : null}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
"use client";
|
|
2
2
|
|
|
3
|
-
import { useEffect, useRef, useState, type ReactNode } from
|
|
4
|
-
import { Xmark as X } from
|
|
3
|
+
import { useEffect, useRef, useState, type ReactNode } from "react";
|
|
4
|
+
import { Xmark as X } from "iconoir-react";
|
|
5
5
|
|
|
6
|
-
import { cn } from
|
|
6
|
+
import { cn } from "../lib/cn";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Suite mobile navigation drawer. Backdrop fade + panel slide, Escape to
|
|
@@ -14,11 +14,12 @@ export function SuiteMobileDrawer({
|
|
|
14
14
|
open,
|
|
15
15
|
onClose,
|
|
16
16
|
children,
|
|
17
|
-
side =
|
|
17
|
+
side = "left",
|
|
18
18
|
showCloseButton = false,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
title,
|
|
20
|
+
closeLabel = "Close navigation",
|
|
21
|
+
backdropLabel = "Close navigation",
|
|
22
|
+
dialogLabel = "Navigation",
|
|
22
23
|
durationMs = 200,
|
|
23
24
|
dataTest,
|
|
24
25
|
panelDataTest,
|
|
@@ -28,9 +29,11 @@ export function SuiteMobileDrawer({
|
|
|
28
29
|
onClose: () => void;
|
|
29
30
|
children: ReactNode;
|
|
30
31
|
/** Edge the panel slides in from. */
|
|
31
|
-
side?:
|
|
32
|
+
side?: "left" | "right";
|
|
32
33
|
/** Standard close row (h-14, trailing X button) above the content. */
|
|
33
34
|
showCloseButton?: boolean;
|
|
35
|
+
/** Optional label in the standard close row. */
|
|
36
|
+
title?: ReactNode;
|
|
34
37
|
closeLabel?: string;
|
|
35
38
|
backdropLabel?: string;
|
|
36
39
|
/** Accessible name for the dialog panel. */
|
|
@@ -76,12 +79,12 @@ export function SuiteMobileDrawer({
|
|
|
76
79
|
if (!open) return;
|
|
77
80
|
|
|
78
81
|
const onKeyDown = (event: KeyboardEvent) => {
|
|
79
|
-
if (event.key ===
|
|
82
|
+
if (event.key === "Escape") onCloseRef.current();
|
|
80
83
|
};
|
|
81
|
-
window.addEventListener(
|
|
84
|
+
window.addEventListener("keydown", onKeyDown);
|
|
82
85
|
|
|
83
86
|
const previousOverflow = document.body.style.overflow;
|
|
84
|
-
document.body.style.overflow =
|
|
87
|
+
document.body.style.overflow = "hidden";
|
|
85
88
|
|
|
86
89
|
const previouslyFocused =
|
|
87
90
|
document.activeElement instanceof HTMLElement
|
|
@@ -90,7 +93,7 @@ export function SuiteMobileDrawer({
|
|
|
90
93
|
const frame = requestAnimationFrame(() => panelRef.current?.focus());
|
|
91
94
|
|
|
92
95
|
return () => {
|
|
93
|
-
window.removeEventListener(
|
|
96
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
94
97
|
document.body.style.overflow = previousOverflow;
|
|
95
98
|
cancelAnimationFrame(frame);
|
|
96
99
|
if (previouslyFocused?.isConnected) previouslyFocused.focus();
|
|
@@ -105,8 +108,8 @@ export function SuiteMobileDrawer({
|
|
|
105
108
|
type="button"
|
|
106
109
|
tabIndex={-1}
|
|
107
110
|
className={cn(
|
|
108
|
-
|
|
109
|
-
shown ?
|
|
111
|
+
"absolute inset-0 bg-black/25 backdrop-blur-sm motion-safe:transition-opacity motion-safe:ease-spring-subtle",
|
|
112
|
+
shown ? "opacity-100" : "opacity-0",
|
|
110
113
|
)}
|
|
111
114
|
style={{ transitionDuration: `${durationMs}ms` }}
|
|
112
115
|
onClick={onClose}
|
|
@@ -120,19 +123,29 @@ export function SuiteMobileDrawer({
|
|
|
120
123
|
tabIndex={-1}
|
|
121
124
|
data-test={panelDataTest}
|
|
122
125
|
className={cn(
|
|
123
|
-
|
|
124
|
-
side ===
|
|
126
|
+
"suite-scroll-lock relative flex h-full w-[86%] max-w-72 flex-col overflow-hidden bg-ph-surface shadow-xl outline-none motion-safe:transition-transform motion-safe:ease-spring-fast",
|
|
127
|
+
side === "right" && "ml-auto",
|
|
125
128
|
shown
|
|
126
|
-
?
|
|
127
|
-
: side ===
|
|
128
|
-
?
|
|
129
|
-
:
|
|
129
|
+
? "translate-x-0"
|
|
130
|
+
: side === "right"
|
|
131
|
+
? "translate-x-full"
|
|
132
|
+
: "-translate-x-full",
|
|
130
133
|
panelClassName,
|
|
131
134
|
)}
|
|
132
135
|
style={{ transitionDuration: `${durationMs}ms` }}
|
|
133
136
|
>
|
|
134
137
|
{showCloseButton ? (
|
|
135
|
-
<div
|
|
138
|
+
<div
|
|
139
|
+
className={cn(
|
|
140
|
+
"flex h-14 shrink-0 items-center border-b border-ph-border px-3",
|
|
141
|
+
title ? "justify-between" : "justify-end",
|
|
142
|
+
)}
|
|
143
|
+
>
|
|
144
|
+
{title ? (
|
|
145
|
+
<span className="truncate text-sm font-semibold text-ph-ink">
|
|
146
|
+
{title}
|
|
147
|
+
</span>
|
|
148
|
+
) : null}
|
|
136
149
|
<button
|
|
137
150
|
type="button"
|
|
138
151
|
onClick={onClose}
|
|
@@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation';
|
|
|
5
5
|
import { Bell } from 'iconoir-react';
|
|
6
6
|
|
|
7
7
|
import type { SuiteDropdownMenuComponent } from '../lib/injected';
|
|
8
|
+
import { resolveSuiteNotificationHref } from '../lib/apps';
|
|
8
9
|
|
|
9
10
|
const POLL_MS = 60_000;
|
|
10
11
|
|
|
@@ -134,9 +135,14 @@ export function SuiteNotificationBell({
|
|
|
134
135
|
// ignore
|
|
135
136
|
}
|
|
136
137
|
}
|
|
137
|
-
|
|
138
|
-
if (
|
|
139
|
-
|
|
138
|
+
const target = resolveSuiteNotificationHref(n.href, n.source_app);
|
|
139
|
+
if (!target) return;
|
|
140
|
+
// Cross-app notifications must leave this product origin.
|
|
141
|
+
if (/^https?:\/\//i.test(target)) {
|
|
142
|
+
window.location.assign(target);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
router.push(target);
|
|
140
146
|
},
|
|
141
147
|
[router, markRead],
|
|
142
148
|
);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../lib/cn";
|
|
6
|
+
import { SuitePage, SuitePageHeader } from "./suite-layout";
|
|
7
|
+
|
|
8
|
+
export interface SuiteSettingsLayoutProps {
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
sidebar: ReactNode;
|
|
11
|
+
mobileNav?: ReactNode;
|
|
12
|
+
loading?: ReactNode;
|
|
13
|
+
title?: ReactNode;
|
|
14
|
+
description?: ReactNode;
|
|
15
|
+
header?: ReactNode;
|
|
16
|
+
dataTest?: string;
|
|
17
|
+
headerDataTest?: string;
|
|
18
|
+
className?: string;
|
|
19
|
+
contentClassName?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Canonical settings geometry for every suite app.
|
|
24
|
+
* Apps own nav data and content; the suite owns spacing, scrolling and breakpoints.
|
|
25
|
+
*/
|
|
26
|
+
export function SuiteSettingsLayout({
|
|
27
|
+
children,
|
|
28
|
+
sidebar,
|
|
29
|
+
mobileNav,
|
|
30
|
+
loading,
|
|
31
|
+
title = "Settings",
|
|
32
|
+
description,
|
|
33
|
+
header,
|
|
34
|
+
dataTest = "settings-page",
|
|
35
|
+
headerDataTest = "settings-page-header",
|
|
36
|
+
className,
|
|
37
|
+
contentClassName,
|
|
38
|
+
}: SuiteSettingsLayoutProps) {
|
|
39
|
+
return (
|
|
40
|
+
<SuitePage
|
|
41
|
+
dataTest={dataTest}
|
|
42
|
+
className={cn("flex h-full flex-col overflow-hidden", className)}
|
|
43
|
+
>
|
|
44
|
+
{header ?? (
|
|
45
|
+
<SuitePageHeader
|
|
46
|
+
sticky
|
|
47
|
+
dataTest={headerDataTest}
|
|
48
|
+
title={title}
|
|
49
|
+
description={description}
|
|
50
|
+
/>
|
|
51
|
+
)}
|
|
52
|
+
|
|
53
|
+
{loading ?? (
|
|
54
|
+
<div className="suite-page-body mt-6 flex min-h-0 flex-1 flex-col">
|
|
55
|
+
{mobileNav ? (
|
|
56
|
+
<div className="sticky top-0 z-20 mb-2 shrink-0 bg-ph-canvas/95 backdrop-blur lg:hidden">
|
|
57
|
+
{mobileNav}
|
|
58
|
+
</div>
|
|
59
|
+
) : null}
|
|
60
|
+
|
|
61
|
+
<div className="flex min-h-0 flex-1 flex-col gap-6 lg:flex-row lg:gap-8">
|
|
62
|
+
<aside
|
|
63
|
+
aria-label="Settings"
|
|
64
|
+
className="hidden w-56 shrink-0 bg-transparent lg:block lg:self-stretch lg:overflow-y-auto lg:overscroll-contain"
|
|
65
|
+
>
|
|
66
|
+
{sidebar}
|
|
67
|
+
</aside>
|
|
68
|
+
|
|
69
|
+
<div
|
|
70
|
+
className={cn(
|
|
71
|
+
"min-h-0 min-w-0 flex-1 overflow-y-auto overscroll-contain pb-6",
|
|
72
|
+
contentClassName,
|
|
73
|
+
)}
|
|
74
|
+
>
|
|
75
|
+
{children}
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
)}
|
|
80
|
+
</SuitePage>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
"use client";
|
|
2
2
|
|
|
3
|
-
import { isValidElement, type
|
|
4
|
-
import { cn } from
|
|
3
|
+
import { isValidElement, type ElementType, type ReactNode } from "react";
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
5
|
|
|
6
6
|
export interface SuiteSettingsNavItem {
|
|
7
7
|
label: string;
|
|
8
8
|
href: string;
|
|
9
|
-
icon:
|
|
9
|
+
icon: ElementType | ReactNode;
|
|
10
10
|
testId?: string;
|
|
11
11
|
}
|
|
12
12
|
|
|
@@ -15,6 +15,8 @@ interface SuiteSettingsMobileNavProps {
|
|
|
15
15
|
activeHref: string;
|
|
16
16
|
onSelect: (href: string) => void;
|
|
17
17
|
dataTest?: string;
|
|
18
|
+
ariaLabel?: string;
|
|
19
|
+
className?: string;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
/**
|
|
@@ -28,19 +30,18 @@ export function SuiteSettingsMobileNav({
|
|
|
28
30
|
activeHref,
|
|
29
31
|
onSelect,
|
|
30
32
|
dataTest,
|
|
33
|
+
ariaLabel = "Settings sections",
|
|
34
|
+
className,
|
|
31
35
|
}: SuiteSettingsMobileNavProps) {
|
|
32
36
|
return (
|
|
33
37
|
<nav
|
|
34
38
|
data-test={dataTest}
|
|
35
|
-
aria-label=
|
|
36
|
-
className="-
|
|
39
|
+
aria-label={ariaLabel}
|
|
40
|
+
className={cn("suite-settings-mobile-nav lg:hidden", className)}
|
|
37
41
|
>
|
|
38
42
|
<div className="flex snap-x gap-2 overflow-x-auto pb-3 pt-1 scrollbar-hide [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
|
39
43
|
{items.map((item) => {
|
|
40
|
-
const Icon = item.icon as
|
|
41
|
-
className?: string;
|
|
42
|
-
'aria-hidden'?: boolean;
|
|
43
|
-
}>;
|
|
44
|
+
const Icon = item.icon as ElementType;
|
|
44
45
|
const active =
|
|
45
46
|
activeHref === item.href || activeHref.startsWith(`${item.href}/`);
|
|
46
47
|
const iconNode = isValidElement(item.icon) ? (
|
|
@@ -48,8 +49,8 @@ export function SuiteSettingsMobileNav({
|
|
|
48
49
|
) : (
|
|
49
50
|
<Icon
|
|
50
51
|
className={cn(
|
|
51
|
-
|
|
52
|
-
active ?
|
|
52
|
+
"h-4 w-4 shrink-0 transition",
|
|
53
|
+
active ? "text-ph-surface" : "text-ph-mutedtext",
|
|
53
54
|
)}
|
|
54
55
|
aria-hidden
|
|
55
56
|
/>
|
|
@@ -60,13 +61,13 @@ export function SuiteSettingsMobileNav({
|
|
|
60
61
|
type="button"
|
|
61
62
|
data-test={item.testId}
|
|
62
63
|
onClick={() => onSelect(item.href)}
|
|
63
|
-
aria-current={active ?
|
|
64
|
+
aria-current={active ? "page" : undefined}
|
|
64
65
|
className={cn(
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
"group relative flex shrink-0 snap-start items-center gap-2 rounded-full px-3.5 py-2.5 text-sm font-medium transition-colors",
|
|
67
|
+
"min-h-[44px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-brand",
|
|
67
68
|
active
|
|
68
|
-
?
|
|
69
|
-
:
|
|
69
|
+
? "bg-ph-ink text-ph-surface shadow-sm"
|
|
70
|
+
: "bg-ph-surface text-ph-subtle ring-1 ring-ph-border hover:bg-ph-muted hover:text-ph-ink",
|
|
70
71
|
)}
|
|
71
72
|
>
|
|
72
73
|
{iconNode}
|
|
@@ -65,7 +65,7 @@ export function SuiteSkeletonCard({
|
|
|
65
65
|
return (
|
|
66
66
|
<div
|
|
67
67
|
className={cn(
|
|
68
|
-
'ph-skeleton-card animate-[skeleton-stagger_0.5s_ease-out_both] rounded-2xl border border-ph-border
|
|
68
|
+
'ph-skeleton-card animate-[skeleton-stagger_0.5s_ease-out_both] rounded-2xl border border-ph-border bg-ph-surface p-6 shadow-sm',
|
|
69
69
|
className,
|
|
70
70
|
)}
|
|
71
71
|
aria-hidden
|
|
@@ -115,7 +115,7 @@ export function SuiteSkeletonList({
|
|
|
115
115
|
className="animate-[skeleton-stagger_0.6s_ease-out_both]"
|
|
116
116
|
style={{ animationDelay: `${index * 100}ms` }}
|
|
117
117
|
>
|
|
118
|
-
<div className="flex items-center gap-3 rounded-xl border border-ph-border
|
|
118
|
+
<div className="flex items-center gap-3 rounded-xl border border-ph-border bg-ph-surface p-4">
|
|
119
119
|
<div className="ph-skeleton h-10 w-10 rounded-lg" />
|
|
120
120
|
<div className="min-w-0 flex-1 space-y-2">
|
|
121
121
|
<div className="ph-skeleton h-3.5 w-3/4" />
|
|
@@ -152,7 +152,7 @@ export function SuiteEmptyState({
|
|
|
152
152
|
return (
|
|
153
153
|
<div
|
|
154
154
|
className={cn(
|
|
155
|
-
'relative overflow-hidden rounded-2xl border border-ph-border
|
|
155
|
+
'relative overflow-hidden rounded-2xl border border-ph-border bg-ph-surface text-center',
|
|
156
156
|
size === 'sm' ? 'px-5 py-8' : 'px-6 py-12 sm:px-10 sm:py-16',
|
|
157
157
|
className,
|
|
158
158
|
)}
|
|
@@ -7,58 +7,40 @@ import type { SuiteSpinnerComponent } from '../lib/injected';
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Today-page loading/empty calibrating state. Apps inject their branded
|
|
10
|
-
* AppSpinner
|
|
11
|
-
*
|
|
12
|
-
* - `pulse` — pulsing sun only (Tides/Kraken)
|
|
10
|
+
* AppSpinner. Kept deliberately quiet — single soft fade + one spinner,
|
|
11
|
+
* no ping/pulse stacks that compete with OAuth/launch handoffs.
|
|
13
12
|
*/
|
|
14
13
|
export function TodayCalibrating({
|
|
15
14
|
onRetry,
|
|
16
15
|
retrying = false,
|
|
17
16
|
className,
|
|
18
17
|
spinner: Spinner,
|
|
19
|
-
variant = 'ring',
|
|
18
|
+
variant: _variant = 'ring',
|
|
20
19
|
retrySpinnerClassName = 'text-ph-brand',
|
|
21
20
|
}: {
|
|
22
21
|
onRetry?: () => void;
|
|
23
22
|
retrying?: boolean;
|
|
24
23
|
className?: string;
|
|
25
24
|
spinner: SuiteSpinnerComponent;
|
|
25
|
+
/** @deprecated Kept for call-site compat; ornamentation is always calm. */
|
|
26
26
|
variant?: 'ring' | 'pulse';
|
|
27
27
|
retrySpinnerClassName?: string;
|
|
28
28
|
}) {
|
|
29
|
+
void _variant;
|
|
29
30
|
return (
|
|
30
31
|
<div
|
|
31
32
|
className={cn(
|
|
32
|
-
'relative flex min-h-[14rem] flex-col items-center justify-center overflow-hidden rounded-2xl border border-ph-border bg-ph-surface px-5 py-8 text-center shadow-sm',
|
|
33
|
+
'suite-auth-handoff relative flex min-h-[14rem] flex-col items-center justify-center overflow-hidden rounded-2xl border border-ph-border bg-ph-surface px-5 py-8 text-center shadow-sm',
|
|
33
34
|
className,
|
|
34
35
|
)}
|
|
35
36
|
data-test="today-calibrating"
|
|
36
37
|
>
|
|
37
38
|
<div
|
|
38
|
-
className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_50%_35%,rgba(251,191,36,0.
|
|
39
|
+
className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_50%_35%,rgba(251,191,36,0.1),transparent_55%)]"
|
|
39
40
|
aria-hidden
|
|
40
41
|
/>
|
|
41
|
-
<div className="relative mb-
|
|
42
|
-
|
|
43
|
-
<>
|
|
44
|
-
<Spinner
|
|
45
|
-
size="lg"
|
|
46
|
-
className="absolute inset-0 m-auto opacity-40"
|
|
47
|
-
label="Loading today"
|
|
48
|
-
/>
|
|
49
|
-
<span
|
|
50
|
-
className="bg-ph-muted/30 absolute inset-2 animate-ping rounded-full"
|
|
51
|
-
aria-hidden
|
|
52
|
-
/>
|
|
53
|
-
</>
|
|
54
|
-
) : null}
|
|
55
|
-
<span className="relative flex h-14 w-14 items-center justify-center rounded-2xl border border-ph-border bg-ph-surface text-ph-ink shadow-sm">
|
|
56
|
-
<Sun
|
|
57
|
-
className={cn('h-9 w-9', variant === 'pulse' && 'animate-pulse')}
|
|
58
|
-
strokeWidth={2}
|
|
59
|
-
aria-hidden
|
|
60
|
-
/>
|
|
61
|
-
</span>
|
|
42
|
+
<div className="relative mb-5 flex h-14 w-14 items-center justify-center rounded-2xl border border-ph-border bg-ph-surface text-ph-ink shadow-sm">
|
|
43
|
+
<Sun className="h-8 w-8" strokeWidth={2} aria-hidden />
|
|
62
44
|
</div>
|
|
63
45
|
<h2 className="relative text-base font-semibold tracking-tight text-ph-ink sm:text-lg md:text-xl">
|
|
64
46
|
Calibrating your day
|
|
@@ -66,14 +48,8 @@ export function TodayCalibrating({
|
|
|
66
48
|
<p className="relative mt-1.5 max-w-sm text-sm sm:text-[15px] leading-relaxed text-ph-subtle">
|
|
67
49
|
Pulling together tasks, time and updates from your workspace…
|
|
68
50
|
</p>
|
|
69
|
-
<div className="relative mt-5 flex
|
|
70
|
-
|
|
71
|
-
<span
|
|
72
|
-
key={i}
|
|
73
|
-
className="h-1.5 w-5 animate-pulse rounded-full bg-ph-muted"
|
|
74
|
-
style={{ animationDelay: `${i * 120}ms` }}
|
|
75
|
-
/>
|
|
76
|
-
))}
|
|
51
|
+
<div className="relative mt-5 flex justify-center">
|
|
52
|
+
<Spinner size="sm" className="opacity-70" label="Loading today" />
|
|
77
53
|
</div>
|
|
78
54
|
{onRetry ? (
|
|
79
55
|
<button
|
|
@@ -1,26 +1,34 @@
|
|
|
1
|
-
|
|
1
|
+
"use client";
|
|
2
2
|
|
|
3
|
-
import type { ReactNode } from
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
4
|
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Today page content wrapper with soft atmosphere.
|
|
7
|
+
* Does not own scrolling — `#main-content` is the scrollport so sticky headers work.
|
|
8
|
+
* Glow/clouds are absolute and do not create a nested scrollport.
|
|
9
|
+
*/
|
|
6
10
|
export function TodayPageFrame({ children }: { children: ReactNode }) {
|
|
7
11
|
return (
|
|
8
12
|
<div
|
|
9
13
|
data-test="today-page"
|
|
10
|
-
className="today-page-frame relative flex min-h-0 flex-1 flex-col overflow-
|
|
14
|
+
className="today-page-frame relative flex min-h-0 w-full flex-1 flex-col overflow-x-hidden"
|
|
11
15
|
>
|
|
12
16
|
<div
|
|
13
|
-
className="pointer-events-none absolute inset-
|
|
17
|
+
className="pointer-events-none absolute inset-0 overflow-hidden"
|
|
14
18
|
aria-hidden
|
|
15
19
|
>
|
|
16
20
|
<div className="today-page-frame__glow absolute inset-0" />
|
|
17
|
-
<div className="today-page-
|
|
18
|
-
<div className="today-page-
|
|
19
|
-
<div className="today-page-frame__cloud today-page-frame__cloud--
|
|
20
|
-
<div className="today-page-frame__cloud today-page-frame__cloud--
|
|
21
|
-
<div className="today-page-frame__cloud today-page-frame__cloud--
|
|
21
|
+
<div className="today-page-frame__top-fade absolute inset-x-0 top-0 h-[28rem]" />
|
|
22
|
+
<div className="today-page-frame__cloud today-page-frame__cloud--1 absolute -left-28 top-16 h-96 w-[30rem] rounded-[45%]" />
|
|
23
|
+
<div className="today-page-frame__cloud today-page-frame__cloud--2 absolute -right-24 top-24 h-80 w-[22rem] rounded-[40%]" />
|
|
24
|
+
<div className="today-page-frame__cloud today-page-frame__cloud--3 absolute left-[22%] top-48 h-72 w-[24rem] rounded-[50%]" />
|
|
25
|
+
<div className="today-page-frame__cloud today-page-frame__cloud--4 absolute right-[18%] top-[42%] h-64 w-[20rem] rounded-[48%]" />
|
|
26
|
+
<div className="today-page-frame__cloud today-page-frame__cloud--5 absolute left-[8%] top-[68%] h-56 w-[18rem] rounded-[42%]" />
|
|
22
27
|
</div>
|
|
23
|
-
<div
|
|
28
|
+
<div
|
|
29
|
+
data-width="full"
|
|
30
|
+
className="suite-page-stage suite-page-inset relative z-[1] flex w-full min-w-0 flex-1 flex-col [&_.list-row]:min-h-[52px] [&_.list-row]:sm:min-h-[44px]"
|
|
31
|
+
>
|
|
24
32
|
{children}
|
|
25
33
|
</div>
|
|
26
34
|
</div>
|