@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.
Files changed (62) hide show
  1. package/dist/chunk-54ZR4VL5.mjs +4641 -0
  2. package/dist/chunk-G53YLHVE.mjs +1023 -0
  3. package/dist/{index-BgG8Homu.d.mts → index-B5NOyoKS.d.mts} +100 -100
  4. package/dist/{index-BgG8Homu.d.ts → index-B5NOyoKS.d.ts} +100 -100
  5. package/dist/index.d.mts +9 -145
  6. package/dist/index.d.ts +9 -145
  7. package/dist/index.js +653 -1111
  8. package/dist/index.mjs +86 -342
  9. package/dist/suite.d.mts +760 -0
  10. package/dist/suite.d.ts +760 -0
  11. package/dist/suite.js +2800 -0
  12. package/dist/suite.mjs +2441 -0
  13. package/dist/ui.d.mts +1 -2
  14. package/dist/ui.d.ts +1 -2
  15. package/dist/ui.js +14 -10
  16. package/dist/ui.mjs +13 -11
  17. package/package.json +43 -24
  18. package/src/components/icons/icons.tsx +0 -2
  19. package/src/components/icons/index.ts +0 -3
  20. package/src/components/sections/IconShowcase.tsx +1 -110
  21. package/src/components/sections/NewComponentsShowcase.tsx +4 -4
  22. package/src/components/sections/SidebarShowcase.tsx +3 -3
  23. package/src/components/sections/SuiteShowcase.tsx +669 -0
  24. package/src/components/ui/ComponentDocs.tsx +2 -2
  25. package/src/components/ui/ContextMenu.tsx +1 -1
  26. package/src/components/ui/Menubar.tsx +1 -1
  27. package/src/components/ui/Sidebar.tsx +10 -8
  28. package/src/styles/suite-skin.css +255 -0
  29. package/src/suite/components/ai-panel.tsx +202 -0
  30. package/src/suite/components/app-switcher.tsx +196 -0
  31. package/src/suite/components/command-palette-host.tsx +62 -0
  32. package/src/suite/components/deferred-chrome.tsx +12 -0
  33. package/src/suite/components/suite-app-layout.tsx +69 -0
  34. package/src/suite/components/suite-bottom-nav.tsx +113 -0
  35. package/src/suite/components/suite-layout.tsx +285 -0
  36. package/src/suite/components/suite-mobile-drawer.tsx +150 -0
  37. package/src/suite/components/suite-mobile-header.tsx +71 -0
  38. package/src/suite/components/suite-notification-bell.tsx +227 -0
  39. package/src/suite/components/suite-settings-mobile-nav.tsx +83 -0
  40. package/src/suite/components/suite-sidebar.tsx +198 -0
  41. package/src/suite/components/suite-skeleton.tsx +191 -0
  42. package/src/suite/components/suite-user-menu.tsx +109 -0
  43. package/src/suite/components/theme-provider.tsx +174 -0
  44. package/src/suite/components/today-calibrating.tsx +95 -0
  45. package/src/suite/components/today-page-frame.tsx +28 -0
  46. package/src/suite/components/today-ui.tsx +370 -0
  47. package/src/suite/icons/app-accents.ts +75 -0
  48. package/src/suite/icons/glyph-parts.tsx +67 -0
  49. package/src/suite/icons/glyphs.ts +203 -0
  50. package/src/suite/icons/index.ts +12 -0
  51. package/src/suite/icons/suite-app-icon.tsx +68 -0
  52. package/src/suite/icons/suite-icon.tsx +93 -0
  53. package/src/suite/index.ts +108 -0
  54. package/src/suite/lib/apps.ts +75 -0
  55. package/src/suite/lib/cn.ts +6 -0
  56. package/src/suite/lib/injected.ts +54 -0
  57. package/src/suite/lib/motion.tsx +48 -0
  58. package/src/suite/lib/use-idle-mount.ts +25 -0
  59. package/dist/chunk-5HSOBJ4F.mjs +0 -5968
  60. package/src/components/icons/lemon-brand-icons.tsx +0 -16
  61. package/src/components/icons/lemon-category-icons.tsx +0 -72
  62. 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
+ }