@khal-os/ui 1.0.0 → 1.0.2

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 (59) hide show
  1. package/LICENSE +94 -0
  2. package/README.md +25 -0
  3. package/dist/index.cjs +2661 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +926 -0
  6. package/dist/index.d.ts +926 -0
  7. package/dist/index.js +2510 -0
  8. package/dist/index.js.map +1 -0
  9. package/package.json +59 -40
  10. package/tokens.css +260 -238
  11. package/src/components/ContextMenu.tsx +0 -130
  12. package/src/components/avatar.tsx +0 -71
  13. package/src/components/badge.tsx +0 -39
  14. package/src/components/button.tsx +0 -102
  15. package/src/components/command.tsx +0 -165
  16. package/src/components/cost-counter.tsx +0 -75
  17. package/src/components/data-row.tsx +0 -97
  18. package/src/components/dropdown-menu.tsx +0 -233
  19. package/src/components/glass-card.tsx +0 -74
  20. package/src/components/input.tsx +0 -48
  21. package/src/components/khal-logo.tsx +0 -73
  22. package/src/components/live-feed.tsx +0 -109
  23. package/src/components/mesh-gradient.tsx +0 -57
  24. package/src/components/metric-display.tsx +0 -93
  25. package/src/components/note.tsx +0 -55
  26. package/src/components/number-flow.tsx +0 -25
  27. package/src/components/pill-badge.tsx +0 -65
  28. package/src/components/progress-bar.tsx +0 -70
  29. package/src/components/section-card.tsx +0 -76
  30. package/src/components/separator.tsx +0 -25
  31. package/src/components/spinner.tsx +0 -42
  32. package/src/components/status-dot.tsx +0 -90
  33. package/src/components/switch.tsx +0 -36
  34. package/src/components/theme-provider.tsx +0 -58
  35. package/src/components/theme-switcher.tsx +0 -59
  36. package/src/components/ticker-bar.tsx +0 -41
  37. package/src/components/tooltip.tsx +0 -62
  38. package/src/components/window-minimized-context.tsx +0 -29
  39. package/src/hooks/useReducedMotion.ts +0 -21
  40. package/src/index.ts +0 -58
  41. package/src/lib/animations.ts +0 -50
  42. package/src/primitives/collapsible-sidebar.tsx +0 -226
  43. package/src/primitives/dialog.tsx +0 -76
  44. package/src/primitives/empty-state.tsx +0 -43
  45. package/src/primitives/index.ts +0 -22
  46. package/src/primitives/list-view.tsx +0 -155
  47. package/src/primitives/property-panel.tsx +0 -108
  48. package/src/primitives/section-header.tsx +0 -19
  49. package/src/primitives/sidebar-nav.tsx +0 -110
  50. package/src/primitives/split-pane.tsx +0 -146
  51. package/src/primitives/status-badge.tsx +0 -10
  52. package/src/primitives/status-bar.tsx +0 -100
  53. package/src/primitives/toolbar.tsx +0 -152
  54. package/src/server.ts +0 -4
  55. package/src/stores/notification-store.ts +0 -271
  56. package/src/stores/theme-store.ts +0 -33
  57. package/src/tokens/lp-tokens.ts +0 -36
  58. package/src/utils.ts +0 -6
  59. package/tsconfig.json +0 -17
@@ -1,55 +0,0 @@
1
- 'use client';
2
-
3
- import { cva, type VariantProps } from 'class-variance-authority';
4
- import { AlertCircle, AlertTriangle, CheckCircle, Info } from 'lucide-react';
5
- import * as React from 'react';
6
- import { cn } from '../utils';
7
-
8
- const noteVariants = cva('flex items-start gap-2 rounded-md border p-3 text-copy-13', {
9
- variants: {
10
- type: {
11
- default: 'border-gray-alpha-400 bg-gray-alpha-100 text-gray-900',
12
- error: 'border-red-300 bg-red-100 text-red-900',
13
- warning: 'border-amber-300 bg-amber-100 text-amber-900',
14
- success: 'border-green-300 bg-green-100 text-green-900',
15
- },
16
- size: {
17
- default: 'p-3',
18
- small: 'p-2 text-label-12',
19
- },
20
- },
21
- defaultVariants: {
22
- type: 'default',
23
- size: 'default',
24
- },
25
- });
26
-
27
- const iconMap = {
28
- default: Info,
29
- error: AlertCircle,
30
- warning: AlertTriangle,
31
- success: CheckCircle,
32
- } as const;
33
-
34
- interface NoteProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof noteVariants> {
35
- type?: 'default' | 'error' | 'warning' | 'success';
36
- label?: boolean | string;
37
- action?: React.ReactNode;
38
- disabled?: boolean;
39
- fill?: boolean;
40
- }
41
-
42
- function Note({ className, type = 'default', size, label, action, children, ...props }: NoteProps) {
43
- const Icon = iconMap[type];
44
- const showLabel = label !== false;
45
-
46
- return (
47
- <div className={cn(noteVariants({ type, size }), className)} role="alert" {...props}>
48
- {showLabel && <Icon className="h-4 w-4 shrink-0 mt-0.5" />}
49
- <div className="flex-1 min-w-0">{children}</div>
50
- {action && <div className="shrink-0">{action}</div>}
51
- </div>
52
- );
53
- }
54
-
55
- export { Note };
@@ -1,25 +0,0 @@
1
- 'use client';
2
-
3
- import NumberFlowBase from '@number-flow/react';
4
- import type { ComponentProps } from 'react';
5
-
6
- type NumberFlowProps = ComponentProps<typeof NumberFlowBase>;
7
-
8
- /**
9
- * NumberFlow — animated number transitions via @number-flow/react.
10
- * Thin wrapper that sets KhalOS-branded timing defaults.
11
- */
12
- function NumberFlow({ transformTiming, spinTiming, opacityTiming, ...props }: NumberFlowProps) {
13
- return (
14
- <NumberFlowBase
15
- transformTiming={transformTiming ?? { duration: 800, easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)' }}
16
- spinTiming={spinTiming ?? { duration: 800, easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)' }}
17
- opacityTiming={opacityTiming ?? { duration: 350, easing: 'ease-out' }}
18
- willChange
19
- {...props}
20
- />
21
- );
22
- }
23
-
24
- export type { NumberFlowProps };
25
- export { NumberFlow };
@@ -1,65 +0,0 @@
1
- 'use client';
2
-
3
- import { cva, type VariantProps } from 'class-variance-authority';
4
- import * as React from 'react';
5
- import { cn } from '../utils';
6
-
7
- /**
8
- * PillBadge — LP section-level badge/tag component.
9
- *
10
- * Extracted from khal-landing section components:
11
- * - Capability tags (omnichannel-spotlight.tsx)
12
- * - Section label pills (fast-secure.tsx header)
13
- * - Compliance badges (fast-secure.tsx badge strip)
14
- * - Accent badges (metrics.tsx ROI calculator)
15
- *
16
- * Always uppercase with wide tracking — the LP's signature badge style.
17
- */
18
- const pillBadgeVariants = cva(
19
- 'inline-flex items-center gap-1.5 rounded-full font-medium uppercase leading-none whitespace-nowrap',
20
- {
21
- variants: {
22
- variant: {
23
- /** Subtle border badge — compliance tags, capability tags */
24
- default: 'border border-[#FFFFFF26] bg-[#FFFFFF08] text-[#FFFFFFCC]',
25
- /** Muted badge — less prominent */
26
- muted: 'border border-[#FFFFFF14] bg-[#FFFFFF05] text-[#FFFFFFCC]',
27
- /** Accent-filled badge — ROI labels, active state badges */
28
- accent: 'text-[var(--color-accent,#D49355)] bg-[rgba(var(--color-accent-rgb,212,147,85),0.12)]',
29
- },
30
- size: {
31
- sm: 'py-1 px-2.5 text-[10px] tracking-[0.08em]',
32
- md: 'py-1.5 px-3.5 text-[11px] tracking-widest',
33
- lg: 'py-2 px-4 text-[11px] tracking-widest',
34
- },
35
- },
36
- defaultVariants: {
37
- variant: 'default',
38
- size: 'md',
39
- },
40
- }
41
- );
42
-
43
- interface PillBadgeProps extends React.HTMLAttributes<HTMLSpanElement>, VariantProps<typeof pillBadgeVariants> {
44
- /** Show a small dot indicator before the text */
45
- dot?: boolean;
46
- /** Custom dot color (defaults to current text color at 40% opacity) */
47
- dotColor?: string;
48
- }
49
-
50
- const PillBadge = React.forwardRef<HTMLSpanElement, PillBadgeProps>(
51
- ({ className, variant, size, dot, dotColor, children, ...props }, ref) => {
52
- return (
53
- <span ref={ref} className={cn(pillBadgeVariants({ variant, size }), className)} {...props}>
54
- {dot && (
55
- <span className="size-1.5 shrink-0 rounded-full" style={{ backgroundColor: dotColor || '#FFFFFF40' }} />
56
- )}
57
- {children}
58
- </span>
59
- );
60
- }
61
- );
62
- PillBadge.displayName = 'PillBadge';
63
-
64
- export type { PillBadgeProps };
65
- export { PillBadge, pillBadgeVariants };
@@ -1,70 +0,0 @@
1
- 'use client';
2
-
3
- import { cva, type VariantProps } from 'class-variance-authority';
4
- import * as React from 'react';
5
- import { cn } from '../utils';
6
-
7
- const progressBarVariants = cva(
8
- 'relative w-full overflow-hidden rounded-full [background:var(--khal-border-default)]',
9
- {
10
- variants: {
11
- size: {
12
- sm: 'h-1.5',
13
- md: 'h-2.5',
14
- },
15
- },
16
- defaultVariants: {
17
- size: 'md',
18
- },
19
- }
20
- );
21
-
22
- interface ProgressBarProps
23
- extends Omit<React.HTMLAttributes<HTMLDivElement>, 'color'>,
24
- VariantProps<typeof progressBarVariants> {
25
- value: number;
26
- max?: number;
27
- color?: string;
28
- showLabel?: boolean;
29
- }
30
-
31
- function ProgressBar({
32
- value,
33
- max = 100,
34
- color = 'var(--khal-stage-build)',
35
- size,
36
- showLabel = false,
37
- className,
38
- ...props
39
- }: ProgressBarProps) {
40
- const percentage = Math.min(100, Math.max(0, (value / max) * 100));
41
-
42
- return (
43
- <div className={cn('flex items-center gap-2', className)} {...props}>
44
- <div
45
- role="progressbar"
46
- aria-valuenow={value}
47
- aria-valuemin={0}
48
- aria-valuemax={max}
49
- className={progressBarVariants({ size })}
50
- >
51
- <div
52
- className="h-full rounded-full"
53
- style={{
54
- width: `${percentage}%`,
55
- background: `linear-gradient(90deg, color-mix(in srgb, ${color} 85%, black), ${color})`,
56
- transition: 'width var(--khal-duration-normal) var(--khal-ease-spring)',
57
- }}
58
- />
59
- </div>
60
- {showLabel && (
61
- <span className="shrink-0 text-xs tabular-nums opacity-60">
62
- {value}/{max}
63
- </span>
64
- )}
65
- </div>
66
- );
67
- }
68
-
69
- export type { ProgressBarProps };
70
- export { ProgressBar, progressBarVariants };
@@ -1,76 +0,0 @@
1
- 'use client';
2
-
3
- import { cva, type VariantProps } from 'class-variance-authority';
4
- import * as React from 'react';
5
- import { cn } from '../utils';
6
-
7
- /**
8
- * SectionCard — LP section-level card component.
9
- *
10
- * Extracted from khal-landing section components (architecture.tsx, fast-secure.tsx).
11
- * Two variants matching the LP's two card styles:
12
- * - `default`: full-border card (architecture section outer cards)
13
- * - `inset`: top-left border card (architecture mockup panels)
14
- */
15
- const sectionCardVariants = cva('relative flex flex-col overflow-hidden', {
16
- variants: {
17
- variant: {
18
- default: 'rounded-2xl border border-[#FFFFFF1A] bg-[#FFFFFF0A]',
19
- inset: 'rounded-tl-xl border-t border-l border-[#FFFFFF26] bg-[#111111]',
20
- solid: 'rounded-2xl border border-[#FFFFFF1A] bg-[#0D0D0D]',
21
- },
22
- padding: {
23
- none: '',
24
- sm: 'p-4',
25
- md: 'p-5 sm:p-6',
26
- lg: 'p-5 sm:p-6 md:p-8',
27
- },
28
- },
29
- defaultVariants: {
30
- variant: 'default',
31
- padding: 'md',
32
- },
33
- });
34
-
35
- interface SectionCardProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof sectionCardVariants> {
36
- /** Optional gradient overlay color (hex). Renders a subtle top-down gradient. */
37
- glow?: string;
38
- }
39
-
40
- const SectionCard = React.forwardRef<HTMLDivElement, SectionCardProps>(
41
- ({ className, variant, padding, glow, children, ...props }, ref) => {
42
- return (
43
- <div ref={ref} className={cn(sectionCardVariants({ variant, padding }), className)} {...props}>
44
- {glow && (
45
- <div
46
- className="pointer-events-none absolute inset-0 z-0"
47
- style={{
48
- background: `linear-gradient(180deg, ${glow}22 0%, transparent 60%)`,
49
- }}
50
- />
51
- )}
52
- <div className="relative z-10 flex flex-col h-full">{children}</div>
53
- </div>
54
- );
55
- }
56
- );
57
- SectionCard.displayName = 'SectionCard';
58
-
59
- /**
60
- * SectionCardHeader — optional header row with bottom border separator.
61
- * Matches the LP mockup panel headers: `py-3 px-4 border-b border-[#FFFFFF1A]`.
62
- */
63
- function SectionCardHeader({ className, children, ...props }: React.HTMLAttributes<HTMLDivElement>) {
64
- return (
65
- <div
66
- className={cn('flex items-center justify-between py-3 px-4 border-b border-[#FFFFFF1A]', className)}
67
- {...props}
68
- >
69
- {children}
70
- </div>
71
- );
72
- }
73
- SectionCardHeader.displayName = 'SectionCardHeader';
74
-
75
- export type { SectionCardProps };
76
- export { SectionCard, SectionCardHeader, sectionCardVariants };
@@ -1,25 +0,0 @@
1
- 'use client';
2
-
3
- import * as SeparatorPrimitive from '@radix-ui/react-separator';
4
- import * as React from 'react';
5
- import { cn } from '../utils';
6
-
7
- const Separator = React.forwardRef<
8
- React.ComponentRef<typeof SeparatorPrimitive.Root>,
9
- React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
10
- >(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => (
11
- <SeparatorPrimitive.Root
12
- ref={ref}
13
- decorative={decorative}
14
- orientation={orientation}
15
- className={cn(
16
- 'shrink-0 bg-gray-alpha-400',
17
- orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
18
- className
19
- )}
20
- {...props}
21
- />
22
- ));
23
- Separator.displayName = SeparatorPrimitive.Root.displayName;
24
-
25
- export { Separator };
@@ -1,42 +0,0 @@
1
- 'use client';
2
-
3
- import * as React from 'react';
4
- import { cn } from '../utils';
5
-
6
- const sizeMap = {
7
- sm: 16,
8
- md: 20,
9
- lg: 24,
10
- } as const;
11
-
12
- interface SpinnerProps extends React.HTMLAttributes<HTMLDivElement> {
13
- size?: keyof typeof sizeMap;
14
- }
15
-
16
- function Spinner({ size = 'md', className, ...props }: SpinnerProps) {
17
- const px = sizeMap[size];
18
- return (
19
- <div
20
- role="status"
21
- aria-label="Loading"
22
- className={cn('inline-flex items-center justify-center', className)}
23
- {...props}
24
- >
25
- <svg width={px} height={px} viewBox="0 0 20 20" fill="none" className="animate-spin">
26
- <circle cx="10" cy="10" r="8" stroke="currentColor" strokeWidth="2" strokeLinecap="round" opacity="0.25" />
27
- <circle
28
- cx="10"
29
- cy="10"
30
- r="8"
31
- stroke="currentColor"
32
- strokeWidth="2"
33
- strokeLinecap="round"
34
- strokeDasharray="50.26"
35
- strokeDashoffset="37.7"
36
- />
37
- </svg>
38
- </div>
39
- );
40
- }
41
-
42
- export { Spinner };
@@ -1,90 +0,0 @@
1
- 'use client';
2
-
3
- import * as React from 'react';
4
- import { cn } from '../utils';
5
-
6
- const sizeMap = { sm: 8, md: 10, lg: 12 } as const;
7
-
8
- /** All recognized status states */
9
- type StatusState = 'live' | 'online' | 'active' | 'working' | 'idle' | 'away' | 'queued' | 'error';
10
-
11
- const stateConfig: Record<StatusState, { color: string; label: string; pulse: boolean }> = {
12
- live: { color: '#22c55e', label: 'Live', pulse: true },
13
- online: { color: '#22c55e', label: 'Online', pulse: false },
14
- active: { color: '#22c55e', label: 'Active', pulse: true },
15
- working: { color: '#f59e0b', label: 'Working', pulse: true },
16
- idle: { color: '#64748b', label: 'Idle', pulse: false },
17
- away: { color: '#64748b', label: 'Away', pulse: false },
18
- queued: { color: '#f59e0b', label: 'Queued', pulse: false },
19
- error: { color: '#ef4444', label: 'Error', pulse: true },
20
- };
21
-
22
- interface StatusDotProps extends React.HTMLAttributes<HTMLSpanElement> {
23
- /** Typed state — determines color, pulse, and label automatically */
24
- state?: StatusState;
25
- /** Manual color override (legacy support) */
26
- color?: string;
27
- /** Manual pulse override */
28
- pulse?: boolean;
29
- size?: keyof typeof sizeMap;
30
- /** Manual label override */
31
- label?: string;
32
- /** Show text label next to dot */
33
- showLabel?: boolean;
34
- }
35
-
36
- function StatusDot({
37
- state,
38
- color: colorProp,
39
- pulse: pulseProp,
40
- size = 'md',
41
- label: labelProp,
42
- showLabel = false,
43
- className,
44
- style,
45
- ...props
46
- }: StatusDotProps) {
47
- const config = state ? stateConfig[state] : null;
48
- const color = colorProp ?? config?.color ?? '#64748b';
49
- const pulse = pulseProp ?? config?.pulse ?? false;
50
- const label = labelProp ?? config?.label;
51
- const px = sizeMap[size];
52
-
53
- return (
54
- <span
55
- role="status"
56
- aria-label={label}
57
- className={cn('relative inline-flex shrink-0 items-center gap-1.5', className)}
58
- style={style}
59
- {...props}
60
- >
61
- <span className="relative inline-flex shrink-0" style={{ width: px, height: px }}>
62
- {pulse && (
63
- <span
64
- className="absolute -inset-0.5 rounded-full"
65
- style={{
66
- backgroundColor: color,
67
- opacity: 0.35,
68
- animation: 'khal-pulse 2s ease-in-out infinite',
69
- }}
70
- />
71
- )}
72
- <span
73
- className="absolute inset-0 rounded-full"
74
- style={{
75
- backgroundColor: color,
76
- boxShadow: pulse ? `0 0 ${px}px ${color}80` : undefined,
77
- }}
78
- />
79
- </span>
80
- {showLabel && label && (
81
- <span className="text-[11px] leading-none" style={{ color }}>
82
- {label}
83
- </span>
84
- )}
85
- </span>
86
- );
87
- }
88
-
89
- export type { StatusDotProps, StatusState };
90
- export { StatusDot, stateConfig };
@@ -1,36 +0,0 @@
1
- 'use client';
2
-
3
- import * as SwitchPrimitive from '@radix-ui/react-switch';
4
- import * as React from 'react';
5
- import { cn } from '../utils';
6
-
7
- interface ToggleProps extends Omit<React.ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>, 'onChange'> {
8
- onChange?: (checked: boolean) => void;
9
- }
10
-
11
- const Toggle = React.forwardRef<React.ComponentRef<typeof SwitchPrimitive.Root>, ToggleProps>(
12
- ({ className, onChange, onCheckedChange, ...props }, ref) => (
13
- <SwitchPrimitive.Root
14
- className={cn(
15
- 'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors',
16
- 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-700 focus-visible:ring-offset-2 focus-visible:ring-offset-background-100',
17
- 'disabled:cursor-not-allowed disabled:opacity-50',
18
- 'data-[state=checked]:bg-gray-1000 data-[state=unchecked]:bg-gray-alpha-400',
19
- className
20
- )}
21
- onCheckedChange={onCheckedChange ?? onChange}
22
- {...props}
23
- ref={ref}
24
- >
25
- <SwitchPrimitive.Thumb
26
- className={cn(
27
- 'pointer-events-none block h-4 w-4 rounded-full bg-white shadow-lg ring-0 transition-transform',
28
- 'data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0'
29
- )}
30
- />
31
- </SwitchPrimitive.Root>
32
- )
33
- );
34
- Toggle.displayName = 'Toggle';
35
-
36
- export { Toggle };
@@ -1,58 +0,0 @@
1
- 'use client';
2
-
3
- import { ThemeProvider as NextThemesProvider } from 'next-themes';
4
- import { useEffect } from 'react';
5
- import { useThemeStore } from '../stores/theme-store';
6
-
7
- function ReduceMotionSync() {
8
- const reduceMotion = useThemeStore((s) => s.reduceMotion);
9
-
10
- useEffect(() => {
11
- document.documentElement.setAttribute('data-reduce-motion', String(reduceMotion));
12
- }, [reduceMotion]);
13
-
14
- return null;
15
- }
16
-
17
- function GlassSync() {
18
- const glassEnabled = useThemeStore((s) => s.glassEnabled);
19
-
20
- useEffect(() => {
21
- const el = document.documentElement;
22
- if (glassEnabled) {
23
- el.setAttribute('data-glass', '');
24
- el.style.setProperty('--khal-glass-enabled', '1');
25
- } else {
26
- el.removeAttribute('data-glass');
27
- el.style.setProperty('--khal-glass-enabled', '0');
28
- }
29
- }, [glassEnabled]);
30
-
31
- return null;
32
- }
33
-
34
- function GpuTerminalsSync() {
35
- const gpuTerminals = useThemeStore((s) => s.gpuTerminals);
36
-
37
- useEffect(() => {
38
- // Sync to the localStorage key the terminal reads on mount
39
- if (gpuTerminals) {
40
- localStorage.setItem('khal-gpu-terminals', 'true');
41
- } else {
42
- localStorage.removeItem('khal-gpu-terminals');
43
- }
44
- }, [gpuTerminals]);
45
-
46
- return null;
47
- }
48
-
49
- export function ThemeProvider({ children, ...props }: React.ComponentProps<typeof NextThemesProvider>) {
50
- return (
51
- <NextThemesProvider {...props}>
52
- <ReduceMotionSync />
53
- <GlassSync />
54
- <GpuTerminalsSync />
55
- {children}
56
- </NextThemesProvider>
57
- );
58
- }
@@ -1,59 +0,0 @@
1
- 'use client';
2
-
3
- import { Monitor, Moon, Sun } from 'lucide-react';
4
- import { useTheme } from 'next-themes';
5
- import { useEffect, useState } from 'react';
6
- import { cn } from '../utils';
7
-
8
- const themes = [
9
- { value: 'system', label: 'System', icon: Monitor },
10
- { value: 'light', label: 'Light', icon: Sun },
11
- { value: 'dark', label: 'Dark', icon: Moon },
12
- ] as const;
13
-
14
- interface ThemeSwitcherProps {
15
- small?: boolean;
16
- className?: string;
17
- onThemeSwitch?: (theme: string) => void;
18
- disabled?: boolean;
19
- }
20
-
21
- function ThemeSwitcher({ small, className, onThemeSwitch, disabled }: ThemeSwitcherProps) {
22
- const { theme, setTheme } = useTheme();
23
- const [mounted, setMounted] = useState(false);
24
- useEffect(() => setMounted(true), []);
25
-
26
- // Avoid hydration mismatch: useTheme() returns different values on server vs client
27
- const resolvedTheme = mounted ? theme : undefined;
28
-
29
- return (
30
- <fieldset
31
- className={cn('inline-flex items-center gap-1 rounded-lg bg-gray-alpha-100 p-1', className)}
32
- disabled={disabled}
33
- >
34
- {themes.map(({ value, label, icon: Icon }) => (
35
- <button
36
- key={value}
37
- type="button"
38
- role="radio"
39
- aria-checked={resolvedTheme === value}
40
- onClick={() => {
41
- setTheme(value);
42
- onThemeSwitch?.(value);
43
- }}
44
- className={cn(
45
- 'inline-flex items-center gap-1.5 rounded-md px-2.5 py-1 text-label-12 transition-colors cursor-pointer',
46
- resolvedTheme === value
47
- ? 'bg-background-100 text-gray-1000 shadow-sm'
48
- : 'text-gray-700 hover:text-gray-1000'
49
- )}
50
- >
51
- <Icon className={small ? 'h-3.5 w-3.5' : 'h-4 w-4'} />
52
- {!small && label}
53
- </button>
54
- ))}
55
- </fieldset>
56
- );
57
- }
58
-
59
- export { ThemeSwitcher };
@@ -1,41 +0,0 @@
1
- 'use client';
2
-
3
- import type { ReactNode } from 'react';
4
- import { cn } from '../utils';
5
-
6
- interface TickerBarProps {
7
- /** Items to scroll — rendered twice for seamless looping */
8
- children: ReactNode;
9
- /** Animation duration in seconds (default 30) */
10
- duration?: number;
11
- /** Pause on hover */
12
- pauseOnHover?: boolean;
13
- className?: string;
14
- }
15
-
16
- function TickerBar({ children, duration = 30, pauseOnHover = true, className }: TickerBarProps) {
17
- return (
18
- <div
19
- className={cn('relative w-full overflow-hidden', className)}
20
- style={{
21
- maskImage: 'linear-gradient(to right, transparent, black 10%, black 90%, transparent)',
22
- WebkitMaskImage: 'linear-gradient(to right, transparent, black 10%, black 90%, transparent)',
23
- }}
24
- >
25
- <div
26
- className={cn('flex w-max', pauseOnHover && 'hover:[animation-play-state:paused]')}
27
- style={{
28
- animation: `khal-ticker ${duration}s linear infinite`,
29
- }}
30
- >
31
- <div className="flex shrink-0 items-center">{children}</div>
32
- <div className="flex shrink-0 items-center" aria-hidden>
33
- {children}
34
- </div>
35
- </div>
36
- </div>
37
- );
38
- }
39
-
40
- export type { TickerBarProps };
41
- export { TickerBar };