@xenide-io/the-old-ui-theme 0.4.3 → 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 +1123 -601
  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 +85 -41
  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
@@ -6,7 +6,9 @@ import { cn } from '../lib/cn';
6
6
  import { SuiteAppIcon } from '../icons/suite-app-icon';
7
7
  import { SuiteIcon } from '../icons/suite-icon';
8
8
  import type { SuiteIconName } from '../icons/glyphs';
9
- type LucideIcon = import('react').ComponentType<import('react').SVGProps<SVGSVGElement>>;
9
+ type LucideIcon = import('react').ComponentType<
10
+ import('react').SVGProps<SVGSVGElement>
11
+ >;
10
12
 
11
13
  export type SuiteAppSlug = 'shellstack' | 'tides' | 'turtletime' | 'kraken';
12
14
 
@@ -117,16 +119,17 @@ export function AnimatedSun({ className }: { className?: string }) {
117
119
  <stop offset="1" stopColor="#fb923c" />
118
120
  </linearGradient>
119
121
  </defs>
120
- <g className="origin-center motion-safe:animate-[spin_20s_linear_infinite] motion-reduce:animate-none">
122
+ {/* ponytail: static rays + soft opacity pulse; spin was too busy for a sticky header */}
123
+ <g className="origin-center opacity-80 motion-safe:animate-[pulse_8s_ease-in-out_infinite] motion-reduce:animate-none">
121
124
  {[0, 45, 90, 135, 180, 225, 270, 315].map((angle) => (
122
125
  <line
123
126
  key={angle}
124
127
  x1="20"
125
- y1="4"
128
+ y1="5"
126
129
  x2="20"
127
130
  y2="9"
128
131
  stroke="#fbbf24"
129
- strokeWidth="2"
132
+ strokeWidth="1.75"
130
133
  strokeLinecap="round"
131
134
  transform={`rotate(${angle} 20 20)`}
132
135
  />
@@ -158,10 +161,9 @@ export function AnimatedMoon({ className }: { className?: string }) {
158
161
  d="M27.5 25.5A11 11 0 1 1 14.5 9.6a9 9 0 1 0 13 15.9Z"
159
162
  fill="url(#animatedMoonBody)"
160
163
  />
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" />
164
+ <g className="opacity-70 motion-safe:animate-[pulse_10s_ease-in-out_infinite] motion-reduce:animate-none">
165
+ <circle cx="29" cy="10" r="1.2" fill="#c7d2fe" />
166
+ <circle cx="34" cy="16" r="0.9" fill="#c7d2fe" />
165
167
  </g>
166
168
  </svg>
167
169
  );
@@ -190,7 +192,9 @@ export function TodayTopBar({
190
192
  appIcon?: ReactNode;
191
193
  appName?: string;
192
194
  }) {
193
- const icon = appIcon ?? <TodayTimeIcon className="h-6 w-6 sm:h-7 sm:w-7 text-amber-500" />;
195
+ const icon = appIcon ?? (
196
+ <TodayTimeIcon className="h-6 w-6 sm:h-7 sm:w-7 text-amber-500" />
197
+ );
194
198
  return (
195
199
  <header className="flex items-center justify-between gap-4">
196
200
  <div className="flex min-w-0 items-center gap-3">
@@ -252,33 +256,287 @@ export function getTodayGreeting(
252
256
  const hours = stats?.hours ?? 0;
253
257
 
254
258
  if (overdueCount > 0) {
255
- return `${greeting}. You have ${taskCount} task${taskCount === 1 ? '' : 's'} due today, ${overdueCount} overdue.`;
259
+ return `${greeting}. ${taskCount} task${taskCount === 1 ? '' : 's'} due today, ${overdueCount} overdue.`;
256
260
  }
257
261
  if (taskCount > 0) {
258
- return `${greeting}. You have ${taskCount} task${taskCount === 1 ? '' : 's'} due today.`;
262
+ return `${greeting}. ${taskCount} task${taskCount === 1 ? '' : 's'} due today.`;
259
263
  }
260
264
  if (hours > 0) {
261
- return `${greeting}. You logged ${hours} hour${hours === 1 ? '' : 's'} today.`;
265
+ return `${greeting}. ${hours} hour${hours === 1 ? '' : 's'} logged today.`;
262
266
  }
263
267
  if (period === 'evening' || period === 'night') {
264
- return `${greeting}. Time to wrap up.`;
268
+ return `${greeting}. Nice work — time to wrap up.`;
265
269
  }
266
- return `${greeting}. Nothing overdue — take a nice, deep breath.`;
270
+ return `${greeting}. Clear queue — take a nice, deep breath.`;
267
271
  }
268
272
 
269
273
  export function TodayHero({
270
274
  message = 'Take a nice, deep breath.',
275
+ brief,
276
+ briefMeta,
271
277
  }: {
272
278
  message?: ReactNode;
279
+ /** AI / local focus brief under the greeting. */
280
+ brief?: ReactNode;
281
+ briefMeta?: ReactNode;
273
282
  }) {
274
283
  return (
275
- <div className="mt-8 text-center sm:mt-10">
284
+ <div className="mt-4 sm:mt-6">
276
285
  <p
277
286
  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"
287
+ className="today-hero max-w-2xl font-display text-lg font-semibold tracking-tight text-ph-ink sm:text-xl sm:leading-snug"
279
288
  >
280
289
  {message}
281
290
  </p>
291
+ {brief ? (
292
+ <div className="mt-4 max-w-2xl" data-test="today-focus-brief">
293
+ <p className="text-[15px] leading-[1.65] text-ph-ink">{brief}</p>
294
+ {briefMeta ? (
295
+ <p className="mt-1.5 text-xs text-ph-mutedtext">{briefMeta}</p>
296
+ ) : null}
297
+ </div>
298
+ ) : null}
299
+ </div>
300
+ );
301
+ }
302
+
303
+ export type TodayPrimaryAccent =
304
+ 'tides' | 'turtletime' | 'kraken' | 'shellstack' | 'neutral';
305
+
306
+ /**
307
+ * Compact primary CTA for Today — one action per app.
308
+ * Colours live in suite-skin (.today-primary-action); no idle animation.
309
+ */
310
+ export function TodayPrimaryAction({
311
+ label,
312
+ description,
313
+ icon,
314
+ accent = 'neutral',
315
+ onClick,
316
+ href,
317
+ loading = false,
318
+ active = false,
319
+ className,
320
+ dataTest = 'today-primary-action',
321
+ }: {
322
+ label: string;
323
+ description?: string;
324
+ icon?: ReactNode;
325
+ accent?: TodayPrimaryAccent;
326
+ onClick?: () => void;
327
+ href?: string;
328
+ loading?: boolean;
329
+ /** e.g. timer already running */
330
+ active?: boolean;
331
+ className?: string;
332
+ dataTest?: string;
333
+ }) {
334
+ const classes = cn(
335
+ 'today-primary-action group relative flex w-full max-w-md items-center gap-3 overflow-hidden rounded-xl px-3.5 py-2.5 text-left transition sm:px-4 sm:py-3',
336
+ `today-primary-action--${accent}`,
337
+ active && 'today-primary-action--active',
338
+ loading && 'pointer-events-none opacity-70',
339
+ className,
340
+ );
341
+
342
+ const body = (
343
+ <>
344
+ <span className="today-primary-action__icon inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-lg sm:h-10 sm:w-10">
345
+ {icon}
346
+ </span>
347
+ <span className="min-w-0 flex-1">
348
+ <span className="block text-sm font-semibold tracking-tight sm:text-base">
349
+ {label}
350
+ </span>
351
+ {description ? (
352
+ <span className="mt-0.5 block text-xs opacity-80 sm:text-sm">
353
+ {description}
354
+ </span>
355
+ ) : null}
356
+ </span>
357
+ <span
358
+ className="today-primary-action__chevron shrink-0 text-base opacity-60 transition group-hover:translate-x-0.5 group-hover:opacity-100"
359
+ aria-hidden
360
+ >
361
+
362
+ </span>
363
+ </>
364
+ );
365
+
366
+ if (href && !onClick) {
367
+ return (
368
+ <a
369
+ href={href}
370
+ className={classes}
371
+ data-test={dataTest}
372
+ aria-busy={loading}
373
+ >
374
+ {body}
375
+ </a>
376
+ );
377
+ }
378
+
379
+ return (
380
+ <button
381
+ type="button"
382
+ onClick={onClick}
383
+ disabled={loading}
384
+ className={classes}
385
+ data-test={dataTest}
386
+ aria-busy={loading}
387
+ >
388
+ {body}
389
+ </button>
390
+ );
391
+ }
392
+
393
+ export function TodayEmptyAction({
394
+ title,
395
+ description,
396
+ actionLabel,
397
+ href,
398
+ onClick,
399
+ dataTest,
400
+ }: {
401
+ title: string;
402
+ description?: string;
403
+ actionLabel?: string;
404
+ href?: string;
405
+ onClick?: () => void;
406
+ dataTest?: string;
407
+ }) {
408
+ return (
409
+ <div
410
+ className="rounded-lg border border-dashed border-ph-border/80 bg-ph-muted/30 px-4 py-4 text-center"
411
+ data-test={dataTest}
412
+ >
413
+ <p className="text-sm font-semibold text-ph-ink">{title}</p>
414
+ {description ? (
415
+ <p className="mx-auto mt-1 max-w-sm text-xs leading-relaxed text-ph-subtle sm:text-sm">
416
+ {description}
417
+ </p>
418
+ ) : null}
419
+ {actionLabel && (href || onClick) ? (
420
+ href ? (
421
+ <a
422
+ href={href}
423
+ className="mt-2.5 inline-flex text-sm font-semibold text-ph-brand hover:underline"
424
+ >
425
+ {actionLabel}
426
+ </a>
427
+ ) : (
428
+ <button
429
+ type="button"
430
+ onClick={onClick}
431
+ className="mt-2.5 inline-flex text-sm font-semibold text-ph-brand hover:underline"
432
+ >
433
+ {actionLabel}
434
+ </button>
435
+ )
436
+ ) : null}
437
+ </div>
438
+ );
439
+ }
440
+
441
+ /** Desktop Today shell: main column + sticky Ask AI / brief aside. */
442
+ export function TodayLayout({
443
+ primary,
444
+ main,
445
+ aside,
446
+ className,
447
+ }: {
448
+ primary?: ReactNode;
449
+ main: ReactNode;
450
+ aside?: ReactNode;
451
+ className?: string;
452
+ }) {
453
+ return (
454
+ <div
455
+ className={cn('mt-5 space-y-6 sm:mt-6', className)}
456
+ data-test="today-layout"
457
+ >
458
+ {primary ? <div className="max-w-md">{primary}</div> : null}
459
+ <div className="grid gap-6 lg:grid-cols-[minmax(0,1fr)_minmax(16rem,20rem)] lg:items-start lg:gap-8">
460
+ <div className="min-w-0 space-y-8">{main}</div>
461
+ {aside ? (
462
+ <aside className="space-y-3 lg:sticky lg:top-6 lg:self-start">
463
+ {aside}
464
+ </aside>
465
+ ) : null}
466
+ </div>
467
+ </div>
468
+ );
469
+ }
470
+
471
+ /**
472
+ * Today Ask AI CTA — opens the suite assistant (not Kraken Deep Research).
473
+ * Prefer onClick → openSuiteAskAi(); href is a fallback only.
474
+ */
475
+ export function TodayAskAiCard({
476
+ title = 'Ask AI',
477
+ description = 'Ask about your workspace, draft something, or look a fact up online.',
478
+ cta = 'Ask',
479
+ className,
480
+ onClick,
481
+ href,
482
+ }: {
483
+ title?: string;
484
+ description?: string;
485
+ cta?: string;
486
+ className?: string;
487
+ onClick?: () => void;
488
+ href?: string;
489
+ }) {
490
+ const classes = cn(
491
+ 'group flex w-full items-start gap-3.5 rounded-2xl border border-ph-border/70 bg-ph-surface px-4 py-3.5 text-left shadow-[0_1px_12px_-8px_rgba(15,23,42,0.1)] transition hover:border-ph-brand/35 hover:bg-[color-mix(in_oklab,var(--ph-accent)_6%,var(--ph-surface))] sm:px-5',
492
+ className,
493
+ );
494
+
495
+ const body = (
496
+ <>
497
+ <span className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl bg-ph-brand/15 text-ph-brand shadow-sm ring-1 ring-black/[0.06]">
498
+ <SuiteIcon name="stack-hex" accent="shellstack" className="h-5 w-5" />
499
+ </span>
500
+ <span className="min-w-0 flex-1">
501
+ <span className="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1">
502
+ <span className="font-display text-sm font-bold tracking-tight text-ph-ink">
503
+ {title}
504
+ </span>
505
+ <span className="text-sm font-medium text-ph-brand transition group-hover:underline">
506
+ {cta}
507
+ </span>
508
+ </span>
509
+ <span className="mt-1 block text-sm leading-relaxed text-ph-subtle">
510
+ {description}
511
+ </span>
512
+ </span>
513
+ </>
514
+ );
515
+
516
+ if (onClick) {
517
+ return (
518
+ <button
519
+ type="button"
520
+ onClick={onClick}
521
+ className={classes}
522
+ data-test="today-ask-ai"
523
+ >
524
+ {body}
525
+ </button>
526
+ );
527
+ }
528
+
529
+ if (href) {
530
+ return (
531
+ <a href={href} className={classes} data-test="today-ask-ai">
532
+ {body}
533
+ </a>
534
+ );
535
+ }
536
+
537
+ return (
538
+ <div className={classes} data-test="today-ask-ai">
539
+ {body}
282
540
  </div>
283
541
  );
284
542
  }
@@ -301,7 +559,7 @@ export function TodaySection({
301
559
  children: ReactNode;
302
560
  }) {
303
561
  return (
304
- <section data-test={testId} className="space-y-4">
562
+ <section data-test={testId} className="space-y-5">
305
563
  <div className="flex items-start justify-between gap-4">
306
564
  <div className="flex min-w-0 items-start gap-3.5">
307
565
  <span
@@ -340,8 +598,7 @@ export function TodayPanel({
340
598
  return (
341
599
  <div
342
600
  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',
601
+ 'border-ph-border/70 rounded-2xl border bg-ph-surface p-5 shadow-[0_1px_16px_-8px_rgba(15,23,42,0.12)] sm:p-5',
345
602
  className,
346
603
  )}
347
604
  >
@@ -1,5 +1,5 @@
1
- export { TodayPageFrame } from './components/today-page-frame';
2
- export * from './components/today-ui';
1
+ export { TodayPageFrame } from "./components/today-page-frame";
2
+ export * from "./components/today-ui";
3
3
  export {
4
4
  AppSwitcher,
5
5
  AppSwitcherChevron,
@@ -8,32 +8,37 @@ export {
8
8
  appSwitcherMenuItemClass,
9
9
  appSwitcherTriggerClass,
10
10
  type SuiteAppEntry,
11
- } from './components/app-switcher';
11
+ } from "./components/app-switcher";
12
12
  export {
13
13
  SuiteNotificationBell,
14
14
  type SuiteNotification,
15
15
  type SuiteNotificationsResponse,
16
- } from './components/suite-notification-bell';
17
- export { CommandPaletteHost } from './components/command-palette-host';
18
- export { DeferredChrome } from './components/deferred-chrome';
19
- export { SuiteMobileDrawer } from './components/suite-mobile-drawer';
20
- export { SuiteMobileHeader } from './components/suite-mobile-header';
16
+ } from "./components/suite-notification-bell";
17
+ export { CommandPaletteHost } from "./components/command-palette-host";
18
+ export { DeferredChrome } from "./components/deferred-chrome";
19
+ export { SuiteMobileDrawer } from "./components/suite-mobile-drawer";
20
+ export { SuiteMobileHeader } from "./components/suite-mobile-header";
21
+ export {
22
+ SuiteSettingsLayout,
23
+ type SuiteSettingsLayoutProps,
24
+ } from "./components/suite-settings-layout";
21
25
  export {
22
26
  SuiteUserMenu,
23
27
  type SuiteUserMenuProps,
24
- } from './components/suite-user-menu';
28
+ } from "./components/suite-user-menu";
25
29
  export {
26
30
  SuiteBottomNav,
27
31
  type SuiteBottomNavItem,
28
- } from './components/suite-bottom-nav';
32
+ } from "./components/suite-bottom-nav";
29
33
  export {
30
34
  SUITE_APPS,
31
35
  SUITE_APP_MAP,
32
36
  suiteAppBaseUrl,
37
+ resolveSuiteNotificationHref,
33
38
  type SuiteAppDefinition,
34
39
  type SuiteAppSlug,
35
- } from './lib/apps';
36
- export { TodayCalibrating } from './components/today-calibrating';
40
+ } from "./lib/apps";
41
+ export { TodayCalibrating } from "./components/today-calibrating";
37
42
  export {
38
43
  SuiteThemeProvider,
39
44
  useSuiteTheme,
@@ -41,11 +46,15 @@ export {
41
46
  type SuiteTheme,
42
47
  type SuiteThemeConfig,
43
48
  type SuiteThemeContextValue,
44
- } from './components/theme-provider';
49
+ } from "./components/theme-provider";
45
50
  export {
46
51
  SuiteAiPanel,
52
+ openSuiteAskAi,
53
+ SUITE_OPEN_ASK_AI_EVENT,
47
54
  type SuiteAiPreset,
48
- } from './components/ai-panel';
55
+ type SuiteAiChatMessage,
56
+ type SuiteAskAiOpenDetail,
57
+ } from "./components/ai-panel";
49
58
  export {
50
59
  APP_ACCENTS,
51
60
  APP_GLYPHS,
@@ -60,7 +69,7 @@ export {
60
69
  type SuiteGlyphElement,
61
70
  type SuiteIconName,
62
71
  type SuiteIconProps,
63
- } from './icons';
72
+ } from "./icons";
64
73
  export type {
65
74
  SuiteCommandItem,
66
75
  SuiteCommandPaletteComponent,
@@ -68,15 +77,23 @@ export type {
68
77
  SuiteDropdownMenuComponent,
69
78
  SuiteDropdownMenuProps,
70
79
  SuiteSpinnerComponent,
71
- } from './lib/injected';
72
- export { useIdleMount } from './lib/use-idle-mount';
73
- export { cn } from './lib/cn';
80
+ } from "./lib/injected";
81
+ export { useIdleMount } from "./lib/use-idle-mount";
82
+ export {
83
+ SIDEBAR_COLLAPSE_THRESHOLD,
84
+ SIDEBAR_DEFAULT_WIDTH,
85
+ SIDEBAR_MAX_WIDTH,
86
+ SIDEBAR_MIN_EXPANDED_WIDTH,
87
+ SIDEBAR_RAIL_WIDTH,
88
+ useSidebarWidth,
89
+ } from "./lib/use-sidebar-width";
90
+ export { cn } from "./lib/cn";
74
91
  export {
75
92
  SuiteMotionProvider,
76
93
  m,
77
94
  SUITE_SPRINGS,
78
95
  type SuiteSpringName,
79
- } from './lib/motion';
96
+ } from "./lib/motion";
80
97
  export {
81
98
  SuitePage,
82
99
  SuitePageHeader,
@@ -86,23 +103,22 @@ export {
86
103
  SuiteSectionHeader,
87
104
  type SuitePageWidth,
88
105
  type SuiteBreadcrumbsProps,
89
- } from './components/suite-layout';
106
+ } from "./components/suite-layout";
90
107
  export {
91
108
  SuiteSkeleton,
92
109
  SuiteSkeletonCard,
93
110
  SuiteSkeletonList,
94
111
  SuiteEmptyState,
95
- } from './components/suite-skeleton';
112
+ } from "./components/suite-skeleton";
96
113
  export {
97
114
  SuiteSettingsMobileNav,
98
115
  type SuiteSettingsNavItem,
99
- } from './components/suite-settings-mobile-nav';
116
+ } from "./components/suite-settings-mobile-nav";
100
117
  export {
101
118
  SuiteSidebar,
102
119
  type SuiteSidebarNavItem,
103
- } from './components/suite-sidebar';
120
+ } from "./components/suite-sidebar";
104
121
  export {
105
122
  SuiteAppLayout,
106
123
  type SuiteAppLayoutProps,
107
- } from './components/suite-app-layout';
108
-
124
+ } from "./components/suite-app-layout";
@@ -5,7 +5,11 @@
5
5
  * suffixes (`switch-app-${slug}`, `suite-nav-${slug}`).
6
6
  */
7
7
 
8
- export type SuiteAppSlug = 'shellstack' | 'tides' | 'turtletime' | 'kraken';
8
+ export type SuiteAppSlug =
9
+ | 'shellstack'
10
+ | 'tides'
11
+ | 'turtletime'
12
+ | 'kraken';
9
13
 
10
14
  export interface SuiteAppDefinition {
11
15
  slug: SuiteAppSlug;
@@ -45,7 +49,7 @@ export const SUITE_APPS: readonly SuiteAppDefinition[] = [
45
49
  name: 'TurtleTime',
46
50
  description: 'Time tracking',
47
51
  icon: '/turtletime-icon.svg',
48
- landing: '/tracker',
52
+ landing: '/today',
49
53
  baseUrlEnv: 'NEXT_PUBLIC_TURTLETIME_URL',
50
54
  baseUrlFallback: 'http://localhost:3000',
51
55
  },
@@ -73,3 +77,29 @@ export function suiteAppBaseUrl(slug: SuiteAppSlug): string {
73
77
  typeof process !== 'undefined' ? process.env[def.baseUrlEnv] : undefined;
74
78
  return (fromEnv || def.baseUrlFallback).replace(/\/$/, '');
75
79
  }
80
+
81
+ const SOURCE_APP_SLUG: Record<string, SuiteAppSlug> = {
82
+ turtletime: 'turtletime',
83
+ tides: 'tides',
84
+ kraken: 'kraken',
85
+ portal: 'shellstack',
86
+ shellstack: 'shellstack',
87
+ };
88
+
89
+ /**
90
+ * Suite notification links are often stored as `/dashboard/...` relative to the
91
+ * *source* app. Resolving them with the current product origin opens the wrong
92
+ * app — prefix with that app's base URL instead.
93
+ */
94
+ export function resolveSuiteNotificationHref(
95
+ href: string | null | undefined,
96
+ sourceApp: string | null | undefined,
97
+ ): string {
98
+ const value = (href || '').trim();
99
+ if (!value) return '';
100
+ if (/^(https?:|mailto:|\/\/)/i.test(value)) return value;
101
+ const path = value.startsWith('/') ? value : `/${value}`;
102
+ const slug = SOURCE_APP_SLUG[(sourceApp || '').toLowerCase()];
103
+ if (!slug) return path;
104
+ return `${suiteAppBaseUrl(slug)}${path}`;
105
+ }
@@ -0,0 +1,114 @@
1
+ "use client";
2
+
3
+ import {
4
+ useCallback,
5
+ useEffect,
6
+ useState,
7
+ type PointerEvent as ReactPointerEvent,
8
+ } from "react";
9
+
10
+ export const SIDEBAR_RAIL_WIDTH = 56;
11
+ export const SIDEBAR_COLLAPSE_THRESHOLD = 80;
12
+ export const SIDEBAR_MIN_EXPANDED_WIDTH = 180;
13
+ export const SIDEBAR_MAX_WIDTH = 320;
14
+ export const SIDEBAR_DEFAULT_WIDTH = 320;
15
+
16
+ function clampWidth(width: number): number {
17
+ return Math.min(SIDEBAR_MAX_WIDTH, Math.max(SIDEBAR_RAIL_WIDTH, width));
18
+ }
19
+
20
+ function snapWidth(width: number): number {
21
+ if (width <= SIDEBAR_COLLAPSE_THRESHOLD) return SIDEBAR_RAIL_WIDTH;
22
+ if (width < SIDEBAR_MIN_EXPANDED_WIDTH) return SIDEBAR_MIN_EXPANDED_WIDTH;
23
+ return Math.min(SIDEBAR_MAX_WIDTH, width);
24
+ }
25
+
26
+ /**
27
+ * Shared suite sidebar resizing: drag below the threshold to collapse,
28
+ * drag the rail edge out to expand, and double-click to reset.
29
+ */
30
+ export function useSidebarWidth(storageKey: string) {
31
+ const [width, setWidth] = useState(SIDEBAR_DEFAULT_WIDTH);
32
+ const [narrowViewport, setNarrowViewport] = useState(false);
33
+ const collapsed = narrowViewport || width <= SIDEBAR_COLLAPSE_THRESHOLD;
34
+
35
+ useEffect(() => {
36
+ let stored: string | null = null;
37
+ try {
38
+ stored = localStorage.getItem(storageKey);
39
+ } catch {
40
+ // Storage can be unavailable in private or restricted browser contexts.
41
+ }
42
+ if (!stored) return;
43
+ const parsed = Number.parseInt(stored, 10);
44
+ if (!Number.isNaN(parsed)) {
45
+ queueMicrotask(() => setWidth(snapWidth(clampWidth(parsed))));
46
+ }
47
+ }, [storageKey]);
48
+
49
+ useEffect(() => {
50
+ const media = window.matchMedia("(max-width: 639px)");
51
+ const sync = () => setNarrowViewport(media.matches);
52
+ sync();
53
+ media.addEventListener("change", sync);
54
+ return () => media.removeEventListener("change", sync);
55
+ }, []);
56
+
57
+ const persist = useCallback(
58
+ (value: number) => {
59
+ try {
60
+ localStorage.setItem(storageKey, String(value));
61
+ } catch {
62
+ // A persisted preference is optional; resizing should still work.
63
+ }
64
+ },
65
+ [storageKey],
66
+ );
67
+
68
+ const setCollapsed = useCallback(
69
+ (value: boolean) => {
70
+ setWidth((current) => {
71
+ const next = value
72
+ ? SIDEBAR_RAIL_WIDTH
73
+ : current <= SIDEBAR_COLLAPSE_THRESHOLD
74
+ ? SIDEBAR_DEFAULT_WIDTH
75
+ : snapWidth(current);
76
+ persist(next);
77
+ return next;
78
+ });
79
+ },
80
+ [persist],
81
+ );
82
+
83
+ const resetWidth = useCallback(() => {
84
+ setWidth(SIDEBAR_DEFAULT_WIDTH);
85
+ persist(SIDEBAR_DEFAULT_WIDTH);
86
+ }, [persist]);
87
+
88
+ const startResize = useCallback(
89
+ (event: ReactPointerEvent<HTMLDivElement>) => {
90
+ event.preventDefault();
91
+ const startX = event.clientX;
92
+ const startWidth = width;
93
+
94
+ const onMove = (moveEvent: PointerEvent) => {
95
+ setWidth(clampWidth(startWidth + moveEvent.clientX - startX));
96
+ };
97
+ const onUp = () => {
98
+ document.removeEventListener("pointermove", onMove);
99
+ document.removeEventListener("pointerup", onUp);
100
+ setWidth((current) => {
101
+ const snapped = snapWidth(current);
102
+ persist(snapped);
103
+ return snapped;
104
+ });
105
+ };
106
+
107
+ document.addEventListener("pointermove", onMove);
108
+ document.addEventListener("pointerup", onUp);
109
+ },
110
+ [persist, width],
111
+ );
112
+
113
+ return { width, collapsed, setCollapsed, resetWidth, startResize };
114
+ }