@xenide-io/the-old-ui-theme 0.3.2 → 0.4.0

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