@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,370 @@
1
+ 'use client';
2
+
3
+ import { Refresh as RefreshCw } from 'iconoir-react';
4
+ import type { ReactNode } from 'react';
5
+ import { cn } from '../lib/cn';
6
+ import { SuiteAppIcon } from '../icons/suite-app-icon';
7
+ import { SuiteIcon } from '../icons/suite-icon';
8
+ import type { SuiteIconName } from '../icons/glyphs';
9
+ type LucideIcon = import('react').ComponentType<import('react').SVGProps<SVGSVGElement>>;
10
+
11
+ export type SuiteAppSlug = 'shellstack' | 'tides' | 'turtletime' | 'kraken';
12
+
13
+ const APP_META: Record<SuiteAppSlug, { name: string }> = {
14
+ shellstack: { name: 'ShellStack' },
15
+ tides: { name: 'Tides' },
16
+ turtletime: { name: 'TurtleTime' },
17
+ kraken: { name: 'Kraken' },
18
+ };
19
+
20
+ const SUITE_APPS: SuiteAppSlug[] = [
21
+ 'shellstack',
22
+ 'tides',
23
+ 'turtletime',
24
+ 'kraken',
25
+ ];
26
+
27
+ export function sourceToSuiteApp(source: string): SuiteAppSlug {
28
+ if (source === 'tides' || source === 'turtletime' || source === 'kraken') {
29
+ return source;
30
+ }
31
+ return 'shellstack';
32
+ }
33
+
34
+ export function suiteAppLabel(source: string): string {
35
+ return APP_META[sourceToSuiteApp(source)].name;
36
+ }
37
+
38
+ const SOURCE_APP_GLYPH: Record<SuiteAppSlug, SuiteIconName> = {
39
+ shellstack: 'stack-hex',
40
+ tides: 'kanban-wave',
41
+ turtletime: 'timer-shell',
42
+ kraken: 'squid-doc',
43
+ };
44
+
45
+ /**
46
+ * Small line icon for list rows — the app's suite glyph with its brand
47
+ * accent on a muted base. Not the branded app tile (see SuiteAppIcon).
48
+ */
49
+ export function SourceAppGlyph({
50
+ source,
51
+ className,
52
+ }: {
53
+ source: string;
54
+ className?: string;
55
+ }) {
56
+ const app = sourceToSuiteApp(source);
57
+ return (
58
+ <SuiteIcon
59
+ name={SOURCE_APP_GLYPH[app]}
60
+ accent={app}
61
+ className={cn('h-4 w-4 shrink-0 text-ph-subtle', className)}
62
+ />
63
+ );
64
+ }
65
+
66
+ function formatLongDate(date = new Date()): string {
67
+ return new Intl.DateTimeFormat(undefined, {
68
+ month: 'long',
69
+ day: 'numeric',
70
+ }).format(date);
71
+ }
72
+
73
+ export function SuiteAppStrip({
74
+ className,
75
+ icons,
76
+ }: {
77
+ className?: string;
78
+ icons?: Partial<Record<SuiteAppSlug, ReactNode>>;
79
+ }) {
80
+ return (
81
+ <div
82
+ className={cn(
83
+ 'flex flex-wrap items-center justify-center gap-2.5',
84
+ className,
85
+ )}
86
+ aria-label="ShellStack suite apps"
87
+ >
88
+ {SUITE_APPS.map((app) => (
89
+ <div
90
+ key={app}
91
+ className="flex items-center gap-2 rounded-full border border-ph-border bg-ph-surface px-2.5 py-1.5 shadow-sm"
92
+ >
93
+ {icons?.[app] ?? <SuiteAppIcon app={app} size="sm" />}
94
+ <span className="text-xs font-medium text-ph-subtle">
95
+ {APP_META[app].name}
96
+ </span>
97
+ </div>
98
+ ))}
99
+ </div>
100
+ );
101
+ }
102
+
103
+ export function AnimatedSun({ className }: { className?: string }) {
104
+ return (
105
+ <svg
106
+ width="40"
107
+ height="40"
108
+ viewBox="0 0 40 40"
109
+ fill="none"
110
+ xmlns="http://www.w3.org/2000/svg"
111
+ className={cn(className)}
112
+ aria-hidden="true"
113
+ >
114
+ <defs>
115
+ <linearGradient id="animatedSunCenter" x1="0" y1="0" x2="40" y2="40">
116
+ <stop stopColor="#f59e0b" />
117
+ <stop offset="1" stopColor="#fb923c" />
118
+ </linearGradient>
119
+ </defs>
120
+ <g className="origin-center motion-safe:animate-[spin_20s_linear_infinite] motion-reduce:animate-none">
121
+ {[0, 45, 90, 135, 180, 225, 270, 315].map((angle) => (
122
+ <line
123
+ key={angle}
124
+ x1="20"
125
+ y1="4"
126
+ x2="20"
127
+ y2="9"
128
+ stroke="#fbbf24"
129
+ strokeWidth="2"
130
+ strokeLinecap="round"
131
+ transform={`rotate(${angle} 20 20)`}
132
+ />
133
+ ))}
134
+ </g>
135
+ <circle cx="20" cy="20" r="9" fill="url(#animatedSunCenter)" />
136
+ </svg>
137
+ );
138
+ }
139
+
140
+ export function AnimatedMoon({ className }: { className?: string }) {
141
+ return (
142
+ <svg
143
+ width="40"
144
+ height="40"
145
+ viewBox="0 0 40 40"
146
+ fill="none"
147
+ xmlns="http://www.w3.org/2000/svg"
148
+ className={cn(className)}
149
+ aria-hidden="true"
150
+ >
151
+ <defs>
152
+ <linearGradient id="animatedMoonBody" x1="0" y1="0" x2="40" y2="40">
153
+ <stop stopColor="#a5b4fc" />
154
+ <stop offset="1" stopColor="#818cf8" />
155
+ </linearGradient>
156
+ </defs>
157
+ <path
158
+ d="M27.5 25.5A11 11 0 1 1 14.5 9.6a9 9 0 1 0 13 15.9Z"
159
+ fill="url(#animatedMoonBody)"
160
+ />
161
+ <g className="motion-safe:animate-[pulse_4s_ease-in-out_infinite] motion-reduce:animate-none">
162
+ <circle cx="29" cy="10" r="1.4" fill="#c7d2fe" />
163
+ <circle cx="34" cy="16" r="1" fill="#c7d2fe" />
164
+ <circle cx="25" cy="5.5" r="0.9" fill="#c7d2fe" />
165
+ </g>
166
+ </svg>
167
+ );
168
+ }
169
+
170
+ /** Sun through the day, moon from 18:00 to 06:00. */
171
+ export function TodayTimeIcon({ className }: { className?: string }) {
172
+ const hour = new Date().getHours();
173
+ const isNight = hour >= 18 || hour < 6;
174
+ return isNight ? (
175
+ <AnimatedMoon className={className} />
176
+ ) : (
177
+ <AnimatedSun className={className} />
178
+ );
179
+ }
180
+ export function TodayTopBar({
181
+ refreshing,
182
+ loading,
183
+ onRefresh,
184
+ appIcon,
185
+ appName,
186
+ }: {
187
+ refreshing?: boolean;
188
+ loading?: boolean;
189
+ onRefresh: () => void;
190
+ appIcon?: ReactNode;
191
+ appName?: string;
192
+ }) {
193
+ const icon = appIcon ?? <TodayTimeIcon className="h-6 w-6 sm:h-7 sm:w-7 text-amber-500" />;
194
+ return (
195
+ <header className="flex items-center justify-between gap-4">
196
+ <div className="flex min-w-0 items-center gap-3">
197
+ <div className="hidden shrink-0 sm:block" aria-hidden="true">
198
+ {icon}
199
+ </div>
200
+ <div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
201
+ <h1 className="font-display text-[1.625rem] font-bold leading-tight tracking-tight text-ph-ink sm:text-[2.25rem]">
202
+ Today
203
+ </h1>
204
+ {appName ? (
205
+ <span className="rounded-md bg-ph-muted px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-ph-subtle">
206
+ {appName}
207
+ </span>
208
+ ) : null}
209
+ <span className="text-sm text-ph-subtle">{formatLongDate()}</span>
210
+ </div>
211
+ </div>
212
+ <div className="flex shrink-0 items-center">
213
+ <button
214
+ type="button"
215
+ onClick={onRefresh}
216
+ disabled={refreshing || loading}
217
+ className="press-subtle inline-flex h-10 w-10 items-center justify-center rounded-lg text-ph-subtle transition hover:bg-ph-surface hover:text-ph-ink disabled:opacity-50"
218
+ data-test="today-refresh"
219
+ aria-label="Refresh today"
220
+ >
221
+ <RefreshCw
222
+ className={cn('h-4 w-4', refreshing && 'animate-spin')}
223
+ aria-hidden
224
+ />
225
+ </button>
226
+ </div>
227
+ </header>
228
+ );
229
+ }
230
+
231
+ export function getTodayGreeting(
232
+ user?: { first_name?: string; name?: string; email?: string },
233
+ stats?: { taskCount?: number; overdueCount?: number; hours?: number },
234
+ ): string {
235
+ const hour = new Date().getHours();
236
+ let period = 'morning';
237
+ if (hour >= 12 && hour < 17) period = 'afternoon';
238
+ else if (hour >= 17 && hour < 21) period = 'evening';
239
+ else if (hour >= 21 || hour < 5) period = 'night';
240
+
241
+ const name =
242
+ user?.first_name ||
243
+ user?.name?.split(' ')[0] ||
244
+ user?.email?.split('@')[0] ||
245
+ null;
246
+
247
+ let greeting = `Good ${period}`;
248
+ if (name) greeting += `, ${name}`;
249
+
250
+ const taskCount = stats?.taskCount ?? 0;
251
+ const overdueCount = stats?.overdueCount ?? 0;
252
+ const hours = stats?.hours ?? 0;
253
+
254
+ if (overdueCount > 0) {
255
+ return `${greeting}. You have ${taskCount} task${taskCount === 1 ? '' : 's'} due today, ${overdueCount} overdue.`;
256
+ }
257
+ if (taskCount > 0) {
258
+ return `${greeting}. You have ${taskCount} task${taskCount === 1 ? '' : 's'} due today.`;
259
+ }
260
+ if (hours > 0) {
261
+ return `${greeting}. You logged ${hours} hour${hours === 1 ? '' : 's'} today.`;
262
+ }
263
+ if (period === 'evening' || period === 'night') {
264
+ return `${greeting}. Time to wrap up.`;
265
+ }
266
+ return `${greeting}. Nothing overdue — take a nice, deep breath.`;
267
+ }
268
+
269
+ export function TodayHero({
270
+ message = 'Take a nice, deep breath.',
271
+ }: {
272
+ message?: ReactNode;
273
+ }) {
274
+ return (
275
+ <div className="mt-8 text-center sm:mt-10">
276
+ <p
277
+ key={typeof message === 'string' ? message : 'hero'}
278
+ className="today-hero font-display text-2xl font-bold tracking-tight text-ph-ink sm:text-[1.75rem] sm:leading-tight"
279
+ >
280
+ {message}
281
+ </p>
282
+ </div>
283
+ );
284
+ }
285
+
286
+ export function TodaySection({
287
+ icon: Icon,
288
+ iconClassName,
289
+ title,
290
+ description,
291
+ action,
292
+ testId,
293
+ children,
294
+ }: {
295
+ icon: LucideIcon;
296
+ iconClassName?: string;
297
+ title: string;
298
+ description?: string;
299
+ action?: ReactNode;
300
+ testId?: string;
301
+ children: ReactNode;
302
+ }) {
303
+ return (
304
+ <section data-test={testId} className="space-y-4">
305
+ <div className="flex items-start justify-between gap-4">
306
+ <div className="flex min-w-0 items-start gap-3.5">
307
+ <span
308
+ className={cn(
309
+ 'inline-flex h-10 w-10 sm:h-11 sm:w-11 shrink-0 items-center justify-center rounded-2xl shadow-sm ring-1 ring-black/[0.06]',
310
+ iconClassName ?? 'bg-ph-muted text-ph-ink',
311
+ )}
312
+ >
313
+ <Icon className="h-5 w-5" aria-hidden />
314
+ </span>
315
+ <div className="min-w-0 pt-0.5">
316
+ <h2 className="font-display text-lg font-bold tracking-tight text-ph-ink">
317
+ {title}
318
+ </h2>
319
+ {description ? (
320
+ <p className="mt-1 max-w-xl text-sm leading-relaxed text-ph-subtle">
321
+ {description}
322
+ </p>
323
+ ) : null}
324
+ </div>
325
+ </div>
326
+ {action}
327
+ </div>
328
+ {children}
329
+ </section>
330
+ );
331
+ }
332
+
333
+ export function TodayPanel({
334
+ children,
335
+ className,
336
+ }: {
337
+ children: ReactNode;
338
+ className?: string;
339
+ }) {
340
+ return (
341
+ <div
342
+ className={cn(
343
+ 'border-ph-border/70 rounded-2xl border bg-ph-surface p-6 shadow-[0_2px_28px_-10px_rgba(15,23,42,0.14)] transition-all duration-200 ease-out sm:p-6',
344
+ 'hover:shadow-[0_8px_32px_-12px_rgba(15,23,42,0.18)] hover:-translate-y-0.5 motion-reduce:transition-none motion-reduce:hover:transform-none',
345
+ className,
346
+ )}
347
+ >
348
+ {children}
349
+ </div>
350
+ );
351
+ }
352
+
353
+ export function TodayEmptyState({
354
+ title,
355
+ description,
356
+ }: {
357
+ title: string;
358
+ description?: string;
359
+ }) {
360
+ return (
361
+ <div className="border-ph-border/80 rounded-xl border border-dashed px-5 py-8 text-center">
362
+ <p className="text-sm font-semibold text-ph-ink">{title}</p>
363
+ {description ? (
364
+ <p className="mt-1.5 text-sm leading-relaxed text-ph-subtle">
365
+ {description}
366
+ </p>
367
+ ) : null}
368
+ </div>
369
+ );
370
+ }
@@ -0,0 +1,75 @@
1
+ import type { SuiteIconName } from './glyphs';
2
+
3
+ /**
4
+ * Per-app brand colours for the suite icon system — the single place hexes
5
+ * live. `accent` tints a glyph's accent layer; `tile`/`onTile`/
6
+ * `onTileAccent` drive `<SuiteAppIcon>` (the programmatic favicon-style
7
+ * tile + glyph mark).
8
+ */
9
+
10
+ export type SuiteAccentSlug =
11
+ | 'turtletime'
12
+ | 'tides'
13
+ | 'kraken'
14
+ | 'shellstack'
15
+ | 'crew';
16
+
17
+ export interface SuiteAppAccent {
18
+ /** Display name (default aria label for app marks). */
19
+ label: string;
20
+ /** Brand accent — tints glyph accent layers via `--suite-icon-accent`. */
21
+ accent: string;
22
+ /** App tile background. */
23
+ tile: string;
24
+ /** Base glyph colour on the tile. */
25
+ onTile: string;
26
+ /** Accent-layer glyph colour on the tile. */
27
+ onTileAccent: string;
28
+ }
29
+
30
+ export const APP_ACCENTS: Record<SuiteAccentSlug, SuiteAppAccent> = {
31
+ turtletime: {
32
+ label: 'TurtleTime',
33
+ accent: '#3f8f57',
34
+ tile: '#173521',
35
+ onTile: '#f7ead0',
36
+ onTileAccent: '#f3c24e',
37
+ },
38
+ tides: {
39
+ label: 'Tides',
40
+ accent: '#a63bb5',
41
+ tile: '#8f2d96',
42
+ onTile: '#ffffff',
43
+ onTileAccent: '#bfe3ff',
44
+ },
45
+ kraken: {
46
+ label: 'Kraken',
47
+ accent: '#737373',
48
+ tile: '#191919',
49
+ onTile: '#ffffff',
50
+ onTileAccent: '#d4d4d4',
51
+ },
52
+ shellstack: {
53
+ label: 'ShellStack',
54
+ accent: '#5e5893',
55
+ tile: '#5e5893',
56
+ onTile: '#ffffff',
57
+ onTileAccent: '#c4a7e7',
58
+ },
59
+ crew: {
60
+ label: 'Crew',
61
+ accent: '#0891b2',
62
+ tile: '#155e75',
63
+ onTile: '#ffffff',
64
+ onTileAccent: '#a5f3fc',
65
+ },
66
+ };
67
+
68
+ /** Which glyph represents each app. */
69
+ export const APP_GLYPHS: Record<SuiteAccentSlug, SuiteIconName> = {
70
+ turtletime: 'timer-shell',
71
+ tides: 'kanban-wave',
72
+ kraken: 'squid-doc',
73
+ shellstack: 'stack-hex',
74
+ crew: 'crew-bot',
75
+ };
@@ -0,0 +1,67 @@
1
+ 'use client';
2
+
3
+ import type { SuiteGlyphElement } from './glyphs';
4
+
5
+ function GlyphElement({ element }: { element: SuiteGlyphElement }) {
6
+ const fill = element.filled ? 'currentColor' : 'none';
7
+ const stroke = element.filled ? 'none' : undefined;
8
+ switch (element.kind) {
9
+ case 'path':
10
+ return <path d={element.d} fill={fill} stroke={stroke} />;
11
+ case 'circle':
12
+ return (
13
+ <circle
14
+ cx={element.cx}
15
+ cy={element.cy}
16
+ r={element.r}
17
+ fill={fill}
18
+ stroke={stroke}
19
+ />
20
+ );
21
+ case 'rect':
22
+ return (
23
+ <rect
24
+ x={element.x}
25
+ y={element.y}
26
+ width={element.width}
27
+ height={element.height}
28
+ rx={element.rx}
29
+ fill={fill}
30
+ stroke={stroke}
31
+ />
32
+ );
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Renders a glyph's elements. Accent-flagged elements are wrapped in a group
38
+ * coloured by `var(--suite-icon-accent, currentColor)` — callers set the
39
+ * variable on an ancestor (inline style) to tint the accent layer.
40
+ */
41
+ export function GlyphParts({
42
+ elements,
43
+ accentClassName,
44
+ }: {
45
+ elements: SuiteGlyphElement[];
46
+ accentClassName?: string;
47
+ }) {
48
+ const base = elements.filter((el) => !el.accent);
49
+ const accented = elements.filter((el) => el.accent);
50
+ return (
51
+ <>
52
+ {base.map((el, i) => (
53
+ <GlyphElement key={i} element={el} />
54
+ ))}
55
+ {accented.length > 0 ? (
56
+ <g
57
+ className={accentClassName}
58
+ style={{ color: 'var(--suite-icon-accent, currentColor)' }}
59
+ >
60
+ {accented.map((el, i) => (
61
+ <GlyphElement key={i} element={el} />
62
+ ))}
63
+ </g>
64
+ ) : null}
65
+ </>
66
+ );
67
+ }