@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.
- package/dist/{chunk-54ZR4VL5.mjs → chunk-RZXHZWUB.mjs} +44 -17
- package/dist/{index-B5NOyoKS.d.mts → index-Od4pIP4F.d.mts} +7 -1
- package/dist/{index-B5NOyoKS.d.ts → index-Od4pIP4F.d.ts} +7 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +37 -10
- package/dist/index.mjs +1 -1
- package/dist/suite.d.mts +152 -41
- package/dist/suite.d.ts +152 -41
- package/dist/suite.js +1184 -661
- package/dist/suite.mjs +1123 -601
- package/dist/ui.d.mts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +37 -10
- package/dist/ui.mjs +1 -1
- package/package.json +7 -3
- package/src/components/ui/Modal.tsx +70 -26
- package/src/styles/suite-skin.css +284 -24
- package/src/suite/components/ai-panel.tsx +269 -103
- package/src/suite/components/suite-app-layout.tsx +48 -28
- package/src/suite/components/suite-layout.tsx +85 -41
- package/src/suite/components/suite-mobile-drawer.tsx +35 -22
- package/src/suite/components/suite-notification-bell.tsx +9 -3
- package/src/suite/components/suite-settings-layout.tsx +82 -0
- package/src/suite/components/suite-settings-mobile-nav.tsx +18 -17
- package/src/suite/components/suite-skeleton.tsx +3 -3
- package/src/suite/components/today-calibrating.tsx +11 -35
- package/src/suite/components/today-page-frame.tsx +19 -11
- package/src/suite/components/today-ui.tsx +276 -19
- package/src/suite/index.ts +41 -25
- package/src/suite/lib/apps.ts +32 -2
- package/src/suite/lib/use-sidebar-width.ts +114 -0
- package/src/components/sections/AccordionShowcase.tsx +0 -45
- package/src/components/sections/AlertShowcase.tsx +0 -40
- package/src/components/sections/AvatarShowcase.tsx +0 -80
- package/src/components/sections/BadgeShowcase.tsx +0 -65
- package/src/components/sections/ButtonShowcase.tsx +0 -308
- package/src/components/sections/CalendarShowcase.tsx +0 -35
- package/src/components/sections/CardShowcase.tsx +0 -159
- package/src/components/sections/CodeMockupShowcase.tsx +0 -56
- package/src/components/sections/ColorPalette.tsx +0 -117
- package/src/components/sections/CommandPaletteShowcase.tsx +0 -48
- package/src/components/sections/CountdownShowcase.tsx +0 -43
- package/src/components/sections/DiffShowcase.tsx +0 -70
- package/src/components/sections/DrawerShowcase.tsx +0 -97
- package/src/components/sections/DropdownShowcase.tsx +0 -98
- package/src/components/sections/EmptyStateShowcase.tsx +0 -50
- package/src/components/sections/FilterChipsShowcase.tsx +0 -98
- package/src/components/sections/FormShowcase.tsx +0 -127
- package/src/components/sections/HoverCardShowcase.tsx +0 -50
- package/src/components/sections/IconShowcase.tsx +0 -121
- package/src/components/sections/IndicatorShowcase.tsx +0 -52
- package/src/components/sections/KbdShowcase.tsx +0 -57
- package/src/components/sections/ModalShowcase.tsx +0 -211
- package/src/components/sections/NavigationShowcase.tsx +0 -199
- package/src/components/sections/NewComponentsShowcase.tsx +0 -1052
- package/src/components/sections/ProductLayoutsShowcase.tsx +0 -78
- package/src/components/sections/ProgressShowcase.tsx +0 -82
- package/src/components/sections/QuillChartShowcase.tsx +0 -92
- package/src/components/sections/QuillDashboardShowcase.tsx +0 -149
- package/src/components/sections/SegmentedControlShowcase.tsx +0 -49
- package/src/components/sections/SetupThemeShowcase.tsx +0 -262
- package/src/components/sections/SidebarShowcase.tsx +0 -152
- package/src/components/sections/StackShowcase.tsx +0 -33
- package/src/components/sections/StepperShowcase.tsx +0 -37
- package/src/components/sections/SuiteShowcase.tsx +0 -669
- package/src/components/sections/SwapShowcase.tsx +0 -99
- package/src/components/sections/TabShowcase.tsx +0 -84
- package/src/components/sections/TableShowcase.tsx +0 -142
- package/src/components/sections/TimelineShowcase.tsx +0 -83
- package/src/components/sections/ToastShowcase.tsx +0 -108
- package/src/components/sections/TooltipShowcase.tsx +0 -38
- package/src/components/sections/UtilityShowcase.tsx +0 -81
- package/src/styles/excel-themes.css +0 -110
- 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<
|
|
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
|
-
|
|
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="
|
|
128
|
+
y1="5"
|
|
126
129
|
x2="20"
|
|
127
130
|
y2="9"
|
|
128
131
|
stroke="#fbbf24"
|
|
129
|
-
strokeWidth="
|
|
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-[
|
|
162
|
-
<circle cx="29" cy="10" r="1.
|
|
163
|
-
<circle cx="34" cy="16" r="
|
|
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 ??
|
|
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}.
|
|
259
|
+
return `${greeting}. ${taskCount} task${taskCount === 1 ? '' : 's'} due today, ${overdueCount} overdue.`;
|
|
256
260
|
}
|
|
257
261
|
if (taskCount > 0) {
|
|
258
|
-
return `${greeting}.
|
|
262
|
+
return `${greeting}. ${taskCount} task${taskCount === 1 ? '' : 's'} due today.`;
|
|
259
263
|
}
|
|
260
264
|
if (hours > 0) {
|
|
261
|
-
return `${greeting}.
|
|
265
|
+
return `${greeting}. ${hours} hour${hours === 1 ? '' : 's'} logged today.`;
|
|
262
266
|
}
|
|
263
267
|
if (period === 'evening' || period === 'night') {
|
|
264
|
-
return `${greeting}.
|
|
268
|
+
return `${greeting}. Nice work — time to wrap up.`;
|
|
265
269
|
}
|
|
266
|
-
return `${greeting}.
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
>
|
package/src/suite/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { TodayPageFrame } from
|
|
2
|
-
export * from
|
|
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
|
|
11
|
+
} from "./components/app-switcher";
|
|
12
12
|
export {
|
|
13
13
|
SuiteNotificationBell,
|
|
14
14
|
type SuiteNotification,
|
|
15
15
|
type SuiteNotificationsResponse,
|
|
16
|
-
} from
|
|
17
|
-
export { CommandPaletteHost } from
|
|
18
|
-
export { DeferredChrome } from
|
|
19
|
-
export { SuiteMobileDrawer } from
|
|
20
|
-
export { SuiteMobileHeader } from
|
|
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
|
|
28
|
+
} from "./components/suite-user-menu";
|
|
25
29
|
export {
|
|
26
30
|
SuiteBottomNav,
|
|
27
31
|
type SuiteBottomNavItem,
|
|
28
|
-
} from
|
|
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
|
|
36
|
-
export { TodayCalibrating } from
|
|
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
|
|
49
|
+
} from "./components/theme-provider";
|
|
45
50
|
export {
|
|
46
51
|
SuiteAiPanel,
|
|
52
|
+
openSuiteAskAi,
|
|
53
|
+
SUITE_OPEN_ASK_AI_EVENT,
|
|
47
54
|
type SuiteAiPreset,
|
|
48
|
-
|
|
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
|
|
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
|
|
72
|
-
export { useIdleMount } from
|
|
73
|
-
export {
|
|
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
|
|
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
|
|
106
|
+
} from "./components/suite-layout";
|
|
90
107
|
export {
|
|
91
108
|
SuiteSkeleton,
|
|
92
109
|
SuiteSkeletonCard,
|
|
93
110
|
SuiteSkeletonList,
|
|
94
111
|
SuiteEmptyState,
|
|
95
|
-
} from
|
|
112
|
+
} from "./components/suite-skeleton";
|
|
96
113
|
export {
|
|
97
114
|
SuiteSettingsMobileNav,
|
|
98
115
|
type SuiteSettingsNavItem,
|
|
99
|
-
} from
|
|
116
|
+
} from "./components/suite-settings-mobile-nav";
|
|
100
117
|
export {
|
|
101
118
|
SuiteSidebar,
|
|
102
119
|
type SuiteSidebarNavItem,
|
|
103
|
-
} from
|
|
120
|
+
} from "./components/suite-sidebar";
|
|
104
121
|
export {
|
|
105
122
|
SuiteAppLayout,
|
|
106
123
|
type SuiteAppLayoutProps,
|
|
107
|
-
} from
|
|
108
|
-
|
|
124
|
+
} from "./components/suite-app-layout";
|
package/src/suite/lib/apps.ts
CHANGED
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
* suffixes (`switch-app-${slug}`, `suite-nav-${slug}`).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
export type SuiteAppSlug =
|
|
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: '/
|
|
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
|
+
}
|