@xenide-io/the-old-ui-theme 0.4.4 → 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.
Files changed (75) hide show
  1. package/dist/{chunk-54ZR4VL5.mjs → chunk-RZXHZWUB.mjs} +44 -17
  2. package/dist/{index-B5NOyoKS.d.mts → index-Od4pIP4F.d.mts} +7 -1
  3. package/dist/{index-B5NOyoKS.d.ts → index-Od4pIP4F.d.ts} +7 -1
  4. package/dist/index.d.mts +2 -2
  5. package/dist/index.d.ts +2 -2
  6. package/dist/index.js +37 -10
  7. package/dist/index.mjs +1 -1
  8. package/dist/suite.d.mts +152 -41
  9. package/dist/suite.d.ts +152 -41
  10. package/dist/suite.js +1184 -661
  11. package/dist/suite.mjs +1122 -600
  12. package/dist/ui.d.mts +1 -1
  13. package/dist/ui.d.ts +1 -1
  14. package/dist/ui.js +37 -10
  15. package/dist/ui.mjs +1 -1
  16. package/package.json +7 -3
  17. package/src/components/ui/Modal.tsx +70 -26
  18. package/src/styles/suite-skin.css +284 -24
  19. package/src/suite/components/ai-panel.tsx +269 -103
  20. package/src/suite/components/suite-app-layout.tsx +48 -28
  21. package/src/suite/components/suite-layout.tsx +84 -40
  22. package/src/suite/components/suite-mobile-drawer.tsx +35 -22
  23. package/src/suite/components/suite-notification-bell.tsx +9 -3
  24. package/src/suite/components/suite-settings-layout.tsx +82 -0
  25. package/src/suite/components/suite-settings-mobile-nav.tsx +18 -17
  26. package/src/suite/components/suite-skeleton.tsx +3 -3
  27. package/src/suite/components/today-calibrating.tsx +11 -35
  28. package/src/suite/components/today-page-frame.tsx +19 -11
  29. package/src/suite/components/today-ui.tsx +276 -19
  30. package/src/suite/index.ts +41 -25
  31. package/src/suite/lib/apps.ts +32 -2
  32. package/src/suite/lib/use-sidebar-width.ts +114 -0
  33. package/src/components/sections/AccordionShowcase.tsx +0 -45
  34. package/src/components/sections/AlertShowcase.tsx +0 -40
  35. package/src/components/sections/AvatarShowcase.tsx +0 -80
  36. package/src/components/sections/BadgeShowcase.tsx +0 -65
  37. package/src/components/sections/ButtonShowcase.tsx +0 -308
  38. package/src/components/sections/CalendarShowcase.tsx +0 -35
  39. package/src/components/sections/CardShowcase.tsx +0 -159
  40. package/src/components/sections/CodeMockupShowcase.tsx +0 -56
  41. package/src/components/sections/ColorPalette.tsx +0 -117
  42. package/src/components/sections/CommandPaletteShowcase.tsx +0 -48
  43. package/src/components/sections/CountdownShowcase.tsx +0 -43
  44. package/src/components/sections/DiffShowcase.tsx +0 -70
  45. package/src/components/sections/DrawerShowcase.tsx +0 -97
  46. package/src/components/sections/DropdownShowcase.tsx +0 -98
  47. package/src/components/sections/EmptyStateShowcase.tsx +0 -50
  48. package/src/components/sections/FilterChipsShowcase.tsx +0 -98
  49. package/src/components/sections/FormShowcase.tsx +0 -127
  50. package/src/components/sections/HoverCardShowcase.tsx +0 -50
  51. package/src/components/sections/IconShowcase.tsx +0 -121
  52. package/src/components/sections/IndicatorShowcase.tsx +0 -52
  53. package/src/components/sections/KbdShowcase.tsx +0 -57
  54. package/src/components/sections/ModalShowcase.tsx +0 -211
  55. package/src/components/sections/NavigationShowcase.tsx +0 -199
  56. package/src/components/sections/NewComponentsShowcase.tsx +0 -1052
  57. package/src/components/sections/ProductLayoutsShowcase.tsx +0 -78
  58. package/src/components/sections/ProgressShowcase.tsx +0 -82
  59. package/src/components/sections/QuillChartShowcase.tsx +0 -92
  60. package/src/components/sections/QuillDashboardShowcase.tsx +0 -149
  61. package/src/components/sections/SegmentedControlShowcase.tsx +0 -49
  62. package/src/components/sections/SetupThemeShowcase.tsx +0 -262
  63. package/src/components/sections/SidebarShowcase.tsx +0 -152
  64. package/src/components/sections/StackShowcase.tsx +0 -33
  65. package/src/components/sections/StepperShowcase.tsx +0 -37
  66. package/src/components/sections/SuiteShowcase.tsx +0 -669
  67. package/src/components/sections/SwapShowcase.tsx +0 -99
  68. package/src/components/sections/TabShowcase.tsx +0 -84
  69. package/src/components/sections/TableShowcase.tsx +0 -142
  70. package/src/components/sections/TimelineShowcase.tsx +0 -83
  71. package/src/components/sections/ToastShowcase.tsx +0 -108
  72. package/src/components/sections/TooltipShowcase.tsx +0 -38
  73. package/src/components/sections/UtilityShowcase.tsx +0 -81
  74. package/src/styles/excel-themes.css +0 -110
  75. package/src/styles/lemon-primitives.css +0 -550
@@ -1,19 +1,22 @@
1
1
  'use client';
2
2
 
3
- import type { ComponentType, ElementType, ReactNode, SVGProps } from 'react';
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
- 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;
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 = 'wide',
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
- '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-5 pt-2 sm:px-6 sm:pb-6 sm:pt-3 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-6 w-6 sm:h-7 sm:w-7 text-amber-500',
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
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="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}
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
- <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}
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-tight tracking-tight text-ph-ink sm:text-[2.25rem]',
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 pt-1">
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
- 'use client';
1
+ "use client";
2
2
 
3
- import { useEffect, useRef, useState, type ReactNode } from 'react';
4
- import { Xmark as X } from 'iconoir-react';
3
+ import { useEffect, useRef, useState, type ReactNode } from "react";
4
+ import { Xmark as X } from "iconoir-react";
5
5
 
6
- import { cn } from '../lib/cn';
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 = 'left',
17
+ side = "left",
18
18
  showCloseButton = false,
19
- closeLabel = 'Close navigation',
20
- backdropLabel = 'Close navigation',
21
- dialogLabel = 'Navigation',
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?: 'left' | 'right';
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 === 'Escape') onCloseRef.current();
82
+ if (event.key === "Escape") onCloseRef.current();
80
83
  };
81
- window.addEventListener('keydown', onKeyDown);
84
+ window.addEventListener("keydown", onKeyDown);
82
85
 
83
86
  const previousOverflow = document.body.style.overflow;
84
- document.body.style.overflow = 'hidden';
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('keydown', onKeyDown);
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
- 'absolute inset-0 bg-black/25 backdrop-blur-sm motion-safe:transition-opacity motion-safe:ease-spring-subtle',
109
- shown ? 'opacity-100' : 'opacity-0',
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
- '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',
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
- ? 'translate-x-0'
127
- : side === 'right'
128
- ? 'translate-x-full'
129
- : '-translate-x-full',
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 className="flex h-14 shrink-0 items-center justify-end border-b border-ph-border px-3">
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
- if (!n.href) return;
138
- if (/^https?:\/\//i.test(n.href)) window.location.assign(n.href);
139
- else router.push(n.href);
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
- 'use client';
1
+ "use client";
2
2
 
3
- import { isValidElement, type ComponentType, type ReactNode } from 'react';
4
- import { cn } from '../lib/cn';
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: ComponentType<{ className?: string; 'aria-hidden'?: boolean }> | ReactNode;
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="Settings sections"
36
- className="-mx-4 px-4 lg:hidden"
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 ComponentType<{
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
- 'h-4 w-4 shrink-0 transition',
52
- active ? 'text-ph-surface' : 'text-ph-mutedtext',
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 ? 'page' : undefined}
64
+ aria-current={active ? "page" : undefined}
64
65
  className={cn(
65
- 'group relative flex shrink-0 snap-start items-center gap-2 rounded-full px-3.5 py-2.5 text-sm font-medium transition',
66
- 'min-h-[44px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-brand',
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
- ? 'bg-ph-ink text-ph-surface shadow-sm'
69
- : 'bg-ph-surface text-ph-subtle ring-1 ring-ph-border hover:bg-ph-muted hover:text-ph-ink',
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/70 bg-ph-surface p-6 shadow-sm',
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/60 bg-ph-surface p-4">
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/80 bg-ph-surface text-center',
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; `variant` picks the ornamentation style:
11
- * - `ring` large spinner ring + ping behind the sun (TurtleTime/ShellStack)
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.12),transparent_55%)]"
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-6 flex h-24 w-24 items-center justify-center">
42
- {variant === 'ring' ? (
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 gap-1.5" aria-hidden>
70
- {[0, 1, 2, 3, 4].map((i) => (
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
- 'use client';
1
+ "use client";
2
2
 
3
- import type { ReactNode } from 'react';
3
+ import type { ReactNode } from "react";
4
4
 
5
- /** Full-bleed theme wash + soft clouds; uses ph-* tokens from each app's theme. */
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-y-auto overflow-x-hidden"
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-x-0 top-0 h-[34rem] overflow-hidden"
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-frame__sun absolute -right-16 -top-16 h-72 w-72 rounded-full bg-gradient-to-br from-amber-300/30 via-rose-300/20 to-rose-400/10 blur-3xl opacity-60 motion-safe:animate-[pulse_8s_ease-in-out_infinite] motion-reduce:opacity-40" />
18
- <div className="today-page-frame__top-fade absolute inset-x-0 top-0 h-64" />
19
- <div className="today-page-frame__cloud today-page-frame__cloud--1 absolute -left-24 top-28 h-80 w-[26rem] rounded-[45%]" />
20
- <div className="today-page-frame__cloud today-page-frame__cloud--2 absolute -right-20 top-40 h-72 w-80 rounded-[40%]" />
21
- <div className="today-page-frame__cloud today-page-frame__cloud--3 absolute left-[28%] top-[58%] h-64 w-[22rem] rounded-[50%]" />
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 className="relative flex-1 px-4 pb-5 pt-2 sm:px-6 sm:pb-6 sm:pt-3 lg:px-8 [&_.list-row]:min-h-[52px] [&_.list-row]:sm:min-h-[44px]">
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>