@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,109 @@
1
+ 'use client';
2
+
3
+ import Image from 'next/image';
4
+ import { useRouter } from 'next/navigation';
5
+ import { LogOut, Settings } from 'iconoir-react';
6
+
7
+ import { cn } from '../lib/cn';
8
+ import { DropdownMenu, DropdownItem } from '../../components/ui/DropdownMenu';
9
+
10
+ export interface SuiteUserMenuProps {
11
+ name?: string | null;
12
+ email?: string | null;
13
+ image?: string | null;
14
+ /** Destination for the "Profile settings" item. */
15
+ settingsHref: string;
16
+ onSignOut: () => void;
17
+ /** Fallback letter(s) when there is no avatar image. */
18
+ fallbackInitials?: string;
19
+ dataTest?: string;
20
+ className?: string;
21
+ }
22
+
23
+ function computeInitials(name: string | null | undefined, email: string | null | undefined): string {
24
+ const source = (name || email || '?').trim();
25
+ return source
26
+ .split(/[\s@]+/)
27
+ .filter(Boolean)
28
+ .slice(0, 2)
29
+ .map((part) => part[0])
30
+ .join('')
31
+ .toUpperCase();
32
+ }
33
+
34
+ /**
35
+ * Suite account context menu — one tap on the avatar opens a small menu
36
+ * with profile settings and sign out. Used in the mobile app bar of every
37
+ * app so account actions behave identically across the suite (no dedicated
38
+ * logout icon cluttering the mobile chrome).
39
+ */
40
+ export function SuiteUserMenu({
41
+ name,
42
+ email,
43
+ image,
44
+ settingsHref,
45
+ onSignOut,
46
+ fallbackInitials,
47
+ dataTest = 'suite-user-menu',
48
+ className,
49
+ }: SuiteUserMenuProps) {
50
+ const router = useRouter();
51
+ const initials = fallbackInitials ?? computeInitials(name, email);
52
+
53
+ const avatar = (
54
+ <span className="inline-flex h-11 w-11 items-center justify-center rounded-full transition-colors hover:bg-ph-muted">
55
+ {image ? (
56
+ <Image
57
+ src={image}
58
+ alt=""
59
+ width={32}
60
+ height={32}
61
+ className="h-8 w-8 rounded-full object-cover"
62
+ unoptimized
63
+ />
64
+ ) : (
65
+ <span className="flex h-8 w-8 items-center justify-center rounded-full bg-ph-muted text-sm font-semibold text-ph-brand">
66
+ {initials}
67
+ </span>
68
+ )}
69
+ </span>
70
+ );
71
+
72
+ return (
73
+ <DropdownMenu
74
+ trigger={avatar}
75
+ aria-label="Account menu"
76
+ align="end"
77
+ side="bottom"
78
+ sideOffset={6}
79
+ collisionPadding={16}
80
+ modal={false}
81
+ className={className}
82
+ data-test={dataTest}
83
+ >
84
+ <div className="px-2 py-1.5">
85
+ <p className="truncate text-sm font-medium text-ph-ink">
86
+ {name || email || 'Account'}
87
+ </p>
88
+ {email ? (
89
+ <p className="truncate text-xs text-ph-mutedtext">{email}</p>
90
+ ) : null}
91
+ </div>
92
+ <div className="my-1 border-t border-ph-border" />
93
+ <DropdownItem
94
+ data-test={`${dataTest}-settings`}
95
+ onClick={() => router.push(settingsHref)}
96
+ >
97
+ <Settings className="h-4 w-4 text-ph-mutedtext" aria-hidden />
98
+ Profile settings
99
+ </DropdownItem>
100
+ <DropdownItem
101
+ data-test={`${dataTest}-sign-out`}
102
+ onClick={onSignOut}
103
+ >
104
+ <LogOut className="h-4 w-4 text-ph-mutedtext" aria-hidden />
105
+ Sign out
106
+ </DropdownItem>
107
+ </DropdownMenu>
108
+ );
109
+ }
@@ -0,0 +1,174 @@
1
+ 'use client';
2
+
3
+ import {
4
+ createContext,
5
+ useCallback,
6
+ useContext,
7
+ useEffect,
8
+ useMemo,
9
+ useState,
10
+ type ReactNode,
11
+ } from 'react';
12
+
13
+ export type SuiteTheme = 'system' | 'light' | 'dark';
14
+ export type SuiteResolvedTheme = 'light' | 'dark';
15
+
16
+ export interface SuiteThemeConfig {
17
+ /** localStorage key holding the stored preference. */
18
+ storageKey: string;
19
+ /** `data-theme` attribute value for the light theme. */
20
+ lightThemeId: string;
21
+ /** `data-theme` attribute value for the dark theme. */
22
+ darkThemeId: string;
23
+ /**
24
+ * Resolved theme to assume when `matchMedia` is unavailable
25
+ * (server render / pre-hydration).
26
+ */
27
+ fallbackTheme: SuiteResolvedTheme;
28
+ /**
29
+ * Optional `<meta id="theme-color-meta">` colours, synced whenever the
30
+ * theme is (re)applied after mount. Omit both to leave the meta tag
31
+ * untouched.
32
+ */
33
+ themeColorLight?: string;
34
+ themeColorDark?: string;
35
+ }
36
+
37
+ export interface SuiteThemeContextValue {
38
+ theme: SuiteTheme;
39
+ resolvedTheme: SuiteResolvedTheme;
40
+ setTheme: (theme: SuiteTheme) => void;
41
+ toggleTheme: () => void;
42
+ }
43
+
44
+ const SuiteThemeContext = createContext<SuiteThemeContextValue | null>(null);
45
+
46
+ function systemTheme(fallback: SuiteResolvedTheme): SuiteResolvedTheme {
47
+ if (typeof window === 'undefined') return fallback;
48
+ return window.matchMedia('(prefers-color-scheme: dark)').matches
49
+ ? 'dark'
50
+ : 'light';
51
+ }
52
+
53
+ function resolveTheme(
54
+ theme: SuiteTheme,
55
+ fallback: SuiteResolvedTheme,
56
+ ): SuiteResolvedTheme {
57
+ return theme === 'system' ? systemTheme(fallback) : theme;
58
+ }
59
+
60
+ function applyTheme(
61
+ theme: SuiteTheme,
62
+ config: SuiteThemeConfig,
63
+ ): SuiteResolvedTheme {
64
+ const resolved = resolveTheme(theme, config.fallbackTheme);
65
+ if (typeof document === 'undefined') return resolved;
66
+ document.documentElement.setAttribute(
67
+ 'data-theme',
68
+ resolved === 'dark' ? config.darkThemeId : config.lightThemeId,
69
+ );
70
+ document.documentElement.classList.toggle('dark', resolved === 'dark');
71
+ if (config.themeColorLight && config.themeColorDark) {
72
+ const meta = document.getElementById(
73
+ 'theme-color-meta',
74
+ ) as HTMLMetaElement | null;
75
+ if (meta) {
76
+ meta.content =
77
+ resolved === 'light' ? config.themeColorLight : config.themeColorDark;
78
+ }
79
+ }
80
+ return resolved;
81
+ }
82
+
83
+ function readStoredTheme(storageKey: string): SuiteTheme {
84
+ if (typeof window === 'undefined') return 'system';
85
+ const stored = localStorage.getItem(storageKey);
86
+ if (stored === 'light' || stored === 'dark' || stored === 'system')
87
+ return stored;
88
+ return 'system';
89
+ }
90
+
91
+ /**
92
+ * Theme provider shared by suite apps. Each app's root layout runs a small
93
+ * boot script that applies `data-theme` before hydration; this provider
94
+ * adopts that DOM state on mount and owns every post-hydration change (user
95
+ * picks a theme, or the OS preference flips while set to `system`).
96
+ *
97
+ * Apps wrap this with their own `ThemeProvider`/`useTheme` module that pins
98
+ * the storage key, theme ids and meta colours.
99
+ */
100
+ export function SuiteThemeProvider({
101
+ config,
102
+ children,
103
+ }: {
104
+ config: SuiteThemeConfig;
105
+ children: ReactNode;
106
+ }) {
107
+ const [state, setState] = useState<{
108
+ theme: SuiteTheme;
109
+ resolvedTheme: SuiteResolvedTheme;
110
+ }>(() => {
111
+ const theme = readStoredTheme(config.storageKey);
112
+ if (typeof document === 'undefined') {
113
+ return { theme, resolvedTheme: config.fallbackTheme };
114
+ }
115
+ const domTheme = document.documentElement.getAttribute('data-theme');
116
+ const resolvedTheme =
117
+ domTheme === config.darkThemeId
118
+ ? 'dark'
119
+ : domTheme === config.lightThemeId
120
+ ? 'light'
121
+ : resolveTheme(theme, config.fallbackTheme);
122
+ return { theme, resolvedTheme };
123
+ });
124
+ const { theme, resolvedTheme } = state;
125
+
126
+ useEffect(() => {
127
+ if (theme !== 'system') return;
128
+ const media = window.matchMedia('(prefers-color-scheme: dark)');
129
+ const update = () => {
130
+ const resolved = applyTheme('system', config);
131
+ setState((current) => ({ ...current, resolvedTheme: resolved }));
132
+ };
133
+ media.addEventListener('change', update);
134
+ return () => media.removeEventListener('change', update);
135
+ }, [theme, config]);
136
+
137
+ const setTheme = useCallback(
138
+ (next: SuiteTheme) => {
139
+ const resolved = applyTheme(next, config);
140
+ setState({ theme: next, resolvedTheme: resolved });
141
+ localStorage.setItem(config.storageKey, next);
142
+ },
143
+ [config],
144
+ );
145
+
146
+ const toggleTheme = useCallback(() => {
147
+ setState((prev) => {
148
+ const next: SuiteTheme =
149
+ resolveTheme(prev.theme, config.fallbackTheme) === 'dark'
150
+ ? 'light'
151
+ : 'dark';
152
+ const resolved = applyTheme(next, config);
153
+ localStorage.setItem(config.storageKey, next);
154
+ return { theme: next, resolvedTheme: resolved };
155
+ });
156
+ }, [config]);
157
+
158
+ const value = useMemo(
159
+ () => ({ theme, resolvedTheme, setTheme, toggleTheme }),
160
+ [theme, resolvedTheme, setTheme, toggleTheme],
161
+ );
162
+
163
+ return (
164
+ <SuiteThemeContext.Provider value={value}>
165
+ {children}
166
+ </SuiteThemeContext.Provider>
167
+ );
168
+ }
169
+
170
+ export function useSuiteTheme(): SuiteThemeContextValue {
171
+ const ctx = useContext(SuiteThemeContext);
172
+ if (!ctx) throw new Error('useTheme must be used within ThemeProvider');
173
+ return ctx;
174
+ }
@@ -0,0 +1,95 @@
1
+ 'use client';
2
+
3
+ import { Refresh as RefreshCw, SunLight as Sun } from 'iconoir-react';
4
+
5
+ import { cn } from '../lib/cn';
6
+ import type { SuiteSpinnerComponent } from '../lib/injected';
7
+
8
+ /**
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)
13
+ */
14
+ export function TodayCalibrating({
15
+ onRetry,
16
+ retrying = false,
17
+ className,
18
+ spinner: Spinner,
19
+ variant = 'ring',
20
+ retrySpinnerClassName = 'text-ph-brand',
21
+ }: {
22
+ onRetry?: () => void;
23
+ retrying?: boolean;
24
+ className?: string;
25
+ spinner: SuiteSpinnerComponent;
26
+ variant?: 'ring' | 'pulse';
27
+ retrySpinnerClassName?: string;
28
+ }) {
29
+ return (
30
+ <div
31
+ 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
+ className,
34
+ )}
35
+ data-test="today-calibrating"
36
+ >
37
+ <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
+ aria-hidden
40
+ />
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>
62
+ </div>
63
+ <h2 className="relative text-base font-semibold tracking-tight text-ph-ink sm:text-lg md:text-xl">
64
+ Calibrating your day
65
+ </h2>
66
+ <p className="relative mt-1.5 max-w-sm text-sm sm:text-[15px] leading-relaxed text-ph-subtle">
67
+ Pulling together tasks, time and updates from your workspace…
68
+ </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
+ ))}
77
+ </div>
78
+ {onRetry ? (
79
+ <button
80
+ type="button"
81
+ onClick={onRetry}
82
+ disabled={retrying}
83
+ className="relative mt-6 inline-flex items-center gap-2 rounded-xl border border-ph-border bg-ph-canvas px-4 py-2 text-sm font-medium text-ph-ink transition hover:bg-ph-muted disabled:opacity-60"
84
+ >
85
+ {retrying ? (
86
+ <Spinner size="sm" className={retrySpinnerClassName} />
87
+ ) : (
88
+ <RefreshCw className="h-4 w-4" aria-hidden />
89
+ )}
90
+ Try again
91
+ </button>
92
+ ) : null}
93
+ </div>
94
+ );
95
+ }
@@ -0,0 +1,28 @@
1
+ 'use client';
2
+
3
+ import type { ReactNode } from 'react';
4
+
5
+ /** Full-bleed theme wash + soft clouds; uses ph-* tokens from each app's theme. */
6
+ export function TodayPageFrame({ children }: { children: ReactNode }) {
7
+ return (
8
+ <div
9
+ data-test="today-page"
10
+ className="today-page-frame relative flex min-h-0 flex-1 flex-col overflow-y-auto overflow-x-hidden"
11
+ >
12
+ <div
13
+ className="pointer-events-none absolute inset-x-0 top-0 h-[34rem] overflow-hidden"
14
+ aria-hidden
15
+ >
16
+ <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%]" />
22
+ </div>
23
+ <div className="relative flex-1 px-4 pb-10 pt-5 sm:px-6 sm:pt-6 lg:px-8 [&_.list-row]:min-h-[52px] [&_.list-row]:sm:min-h-[44px]">
24
+ {children}
25
+ </div>
26
+ </div>
27
+ );
28
+ }