@khal-os/ui 1.0.0

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 (51) hide show
  1. package/package.json +41 -0
  2. package/src/components/ContextMenu.tsx +130 -0
  3. package/src/components/avatar.tsx +71 -0
  4. package/src/components/badge.tsx +39 -0
  5. package/src/components/button.tsx +102 -0
  6. package/src/components/command.tsx +165 -0
  7. package/src/components/cost-counter.tsx +75 -0
  8. package/src/components/data-row.tsx +97 -0
  9. package/src/components/dropdown-menu.tsx +233 -0
  10. package/src/components/glass-card.tsx +74 -0
  11. package/src/components/input.tsx +48 -0
  12. package/src/components/khal-logo.tsx +73 -0
  13. package/src/components/live-feed.tsx +109 -0
  14. package/src/components/mesh-gradient.tsx +57 -0
  15. package/src/components/metric-display.tsx +93 -0
  16. package/src/components/note.tsx +55 -0
  17. package/src/components/number-flow.tsx +25 -0
  18. package/src/components/pill-badge.tsx +65 -0
  19. package/src/components/progress-bar.tsx +70 -0
  20. package/src/components/section-card.tsx +76 -0
  21. package/src/components/separator.tsx +25 -0
  22. package/src/components/spinner.tsx +42 -0
  23. package/src/components/status-dot.tsx +90 -0
  24. package/src/components/switch.tsx +36 -0
  25. package/src/components/theme-provider.tsx +58 -0
  26. package/src/components/theme-switcher.tsx +59 -0
  27. package/src/components/ticker-bar.tsx +41 -0
  28. package/src/components/tooltip.tsx +62 -0
  29. package/src/components/window-minimized-context.tsx +29 -0
  30. package/src/hooks/useReducedMotion.ts +21 -0
  31. package/src/index.ts +58 -0
  32. package/src/lib/animations.ts +50 -0
  33. package/src/primitives/collapsible-sidebar.tsx +226 -0
  34. package/src/primitives/dialog.tsx +76 -0
  35. package/src/primitives/empty-state.tsx +43 -0
  36. package/src/primitives/index.ts +22 -0
  37. package/src/primitives/list-view.tsx +155 -0
  38. package/src/primitives/property-panel.tsx +108 -0
  39. package/src/primitives/section-header.tsx +19 -0
  40. package/src/primitives/sidebar-nav.tsx +110 -0
  41. package/src/primitives/split-pane.tsx +146 -0
  42. package/src/primitives/status-badge.tsx +10 -0
  43. package/src/primitives/status-bar.tsx +100 -0
  44. package/src/primitives/toolbar.tsx +152 -0
  45. package/src/server.ts +4 -0
  46. package/src/stores/notification-store.ts +271 -0
  47. package/src/stores/theme-store.ts +33 -0
  48. package/src/tokens/lp-tokens.ts +36 -0
  49. package/src/utils.ts +6 -0
  50. package/tokens.css +295 -0
  51. package/tsconfig.json +17 -0
@@ -0,0 +1,93 @@
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
+ * MetricDisplay — LP section-level metric/stat component.
9
+ *
10
+ * Extracted from khal-landing section components:
11
+ * - Stat cards in metrics.tsx (large numbers with suffix/prefix)
12
+ * - Metric tiles in architecture.tsx AppBuilderMockup
13
+ * - ROI projected savings in metrics.tsx
14
+ *
15
+ * Renders a large highlighted value with a label and optional description.
16
+ */
17
+ const metricDisplayVariants = cva('flex flex-col', {
18
+ variants: {
19
+ size: {
20
+ /** Compact — for inline/tile use (architecture mockup metric tiles) */
21
+ sm: 'gap-1',
22
+ /** Standard — for dashboard displays */
23
+ md: 'gap-1.5',
24
+ /** Large — hero stat cards (metrics.tsx) */
25
+ lg: 'gap-2',
26
+ },
27
+ },
28
+ defaultVariants: {
29
+ size: 'md',
30
+ },
31
+ });
32
+
33
+ const valueSizeMap = {
34
+ sm: 'text-[22px] font-semibold leading-7 tracking-tight',
35
+ md: 'text-[28px] font-semibold leading-8 tracking-[-0.02em]',
36
+ lg: 'text-[36px] sm:text-[44px] font-semibold leading-none tracking-[-0.04em]',
37
+ } as const;
38
+
39
+ const labelSizeMap = {
40
+ sm: 'text-[11px] uppercase tracking-[0.06em] text-[#FFFFFF80] font-medium leading-3.5',
41
+ md: 'text-[13px] text-[#FFFFFFCC] font-medium leading-4',
42
+ lg: 'text-[15px] text-[#FFFFFFCC] font-medium leading-5',
43
+ } as const;
44
+
45
+ interface MetricDisplayProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof metricDisplayVariants> {
46
+ /** The primary value to display */
47
+ value: string | number;
48
+ /** Label describing the metric */
49
+ label: string;
50
+ /** Optional description/subtext below the label */
51
+ description?: string;
52
+ /** Optional prefix before the value (e.g. "+", "$") */
53
+ prefix?: string;
54
+ /** Optional suffix after the value (e.g. "%", "pts", "ms") */
55
+ suffix?: string;
56
+ /** Accent color for the value. Defaults to current text color. */
57
+ accentColor?: string;
58
+ }
59
+
60
+ const MetricDisplay = React.forwardRef<HTMLDivElement, MetricDisplayProps>(
61
+ ({ className, size = 'md', value, label, description, prefix, suffix, accentColor, ...props }, ref) => {
62
+ const resolvedSize = (size ?? 'md') as 'sm' | 'md' | 'lg';
63
+
64
+ return (
65
+ <div ref={ref} className={cn(metricDisplayVariants({ size }), className)} {...props}>
66
+ {/* Label above value for sm size (matches LP tile pattern) */}
67
+ {resolvedSize === 'sm' && <span className={labelSizeMap[resolvedSize]}>{label}</span>}
68
+
69
+ {/* Value */}
70
+ <div
71
+ className={cn(valueSizeMap[resolvedSize], 'tabular-nums')}
72
+ style={accentColor ? { color: accentColor } : undefined}
73
+ >
74
+ {prefix && <span>{prefix}</span>}
75
+ <span>{value}</span>
76
+ {suffix && (
77
+ <span className={resolvedSize === 'lg' ? 'text-[32px] tracking-[-0.02em] ml-0.5' : 'ml-0.5'}>{suffix}</span>
78
+ )}
79
+ </div>
80
+
81
+ {/* Label below value for md/lg sizes */}
82
+ {resolvedSize !== 'sm' && <span className={labelSizeMap[resolvedSize]}>{label}</span>}
83
+
84
+ {/* Description */}
85
+ {description && <span className="text-[13px] text-[#FFFFFF80] leading-4">{description}</span>}
86
+ </div>
87
+ );
88
+ }
89
+ );
90
+ MetricDisplay.displayName = 'MetricDisplay';
91
+
92
+ export type { MetricDisplayProps };
93
+ export { MetricDisplay, metricDisplayVariants };
@@ -0,0 +1,55 @@
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 };
@@ -0,0 +1,25 @@
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 };
@@ -0,0 +1,65 @@
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 };
@@ -0,0 +1,70 @@
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 };
@@ -0,0 +1,76 @@
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 };
@@ -0,0 +1,25 @@
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 };
@@ -0,0 +1,42 @@
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 };
@@ -0,0 +1,90 @@
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 };
@@ -0,0 +1,36 @@
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 };
@@ -0,0 +1,58 @@
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
+ }