@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,97 @@
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
+ * DataRow — LP section-level key-value / data display component.
9
+ *
10
+ * Extracted from khal-landing section components:
11
+ * - Architecture.tsx: font-mono code rules (IF/THEN/ELSE patterns)
12
+ * - Architecture.tsx: connection rows with status dots
13
+ * - Omnichannel-spotlight.tsx: channel count rows (font-mono tabular-nums)
14
+ * - Omnichannel-spotlight.tsx: observability inline data
15
+ *
16
+ * Renders a horizontal key-value pair with monospace font and optional accent.
17
+ */
18
+ const dataRowVariants = cva('flex items-center gap-3 rounded-lg border border-[#FFFFFF14] font-mono', {
19
+ variants: {
20
+ variant: {
21
+ /** Standard data row — subtle bg */
22
+ default: 'bg-[#FFFFFF08] py-2.5 px-3',
23
+ /** Inline/compact — for observability-style data */
24
+ inline: 'bg-[#FFFFFF06] py-1.5 px-2.5 text-[11px]',
25
+ /** Nested rule row — for indented rule displays */
26
+ rule: 'bg-[#FFFFFF05] py-2.5 px-3',
27
+ },
28
+ },
29
+ defaultVariants: {
30
+ variant: 'default',
31
+ },
32
+ });
33
+
34
+ interface DataRowProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof dataRowVariants> {
35
+ /** The label/key portion (left side) */
36
+ label: string;
37
+ /** The value portion (right side, pushed to end) */
38
+ value?: string;
39
+ /** Accent color for the value text */
40
+ accentColor?: string;
41
+ /** Show a status dot before the label */
42
+ statusDot?: boolean;
43
+ /** Custom status dot color */
44
+ dotColor?: string;
45
+ /** Optional tag/badge to show before the label (e.g. "IF", "THEN") */
46
+ tag?: string;
47
+ /** Tag color — defaults to accent */
48
+ tagColor?: string;
49
+ }
50
+
51
+ const DataRow = React.forwardRef<HTMLDivElement, DataRowProps>(
52
+ ({ className, variant, label, value, accentColor, statusDot, dotColor, tag, tagColor, children, ...props }, ref) => {
53
+ return (
54
+ <div ref={ref} className={cn(dataRowVariants({ variant }), className)} {...props}>
55
+ {/* Status dot */}
56
+ {statusDot && (
57
+ <span className="size-[6px] shrink-0 rounded-full" style={{ backgroundColor: dotColor || '#FFFFFF40' }} />
58
+ )}
59
+
60
+ {/* Tag badge (IF/THEN/ELSE style) */}
61
+ {tag && (
62
+ <span
63
+ className="shrink-0 rounded py-0.5 px-2 text-[10px] font-bold uppercase tracking-wide leading-3.5"
64
+ style={{
65
+ color: tagColor || 'var(--color-accent, #D49355)',
66
+ backgroundColor: tagColor
67
+ ? `color-mix(in srgb, ${tagColor} 10%, transparent)`
68
+ : 'rgba(var(--color-accent-rgb, 212,147,85), 0.1)',
69
+ }}
70
+ >
71
+ {tag}
72
+ </span>
73
+ )}
74
+
75
+ {/* Label */}
76
+ <span className="text-[12px] text-[#FFFFFFCC] leading-4 min-w-0 truncate">{label}</span>
77
+
78
+ {/* Spacer if value present */}
79
+ {(value || children) && <span className="grow" />}
80
+
81
+ {/* Value */}
82
+ {value && (
83
+ <span className="text-[12px] leading-4 tabular-nums shrink-0" style={{ color: accentColor || '#FFFFFF99' }}>
84
+ {value}
85
+ </span>
86
+ )}
87
+
88
+ {/* Custom children (for complex right-side content) */}
89
+ {children}
90
+ </div>
91
+ );
92
+ }
93
+ );
94
+ DataRow.displayName = 'DataRow';
95
+
96
+ export type { DataRowProps };
97
+ export { DataRow, dataRowVariants };
@@ -0,0 +1,233 @@
1
+ 'use client';
2
+
3
+ import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
4
+ import * as React from 'react';
5
+ import { cn } from '../utils';
6
+
7
+ const DropdownMenu = DropdownMenuPrimitive.Root;
8
+ const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
9
+ const DropdownMenuGroup = DropdownMenuPrimitive.Group;
10
+ const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
11
+ const DropdownMenuSub = DropdownMenuPrimitive.Sub;
12
+ const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
13
+
14
+ const DropdownMenuSubTrigger = React.forwardRef<
15
+ React.ComponentRef<typeof DropdownMenuPrimitive.SubTrigger>,
16
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
17
+ inset?: boolean;
18
+ }
19
+ >(({ className, inset, children, ...props }, ref) => (
20
+ <DropdownMenuPrimitive.SubTrigger
21
+ ref={ref}
22
+ className={cn(
23
+ 'flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-copy-13 outline-none select-none',
24
+ 'focus:bg-[var(--khal-menu-hover)]',
25
+ 'data-[state=open]:bg-[var(--khal-menu-hover)]',
26
+ inset && 'pl-8',
27
+ className
28
+ )}
29
+ style={{ color: 'var(--khal-text-primary)' }}
30
+ {...props}
31
+ >
32
+ {children}
33
+ </DropdownMenuPrimitive.SubTrigger>
34
+ ));
35
+ DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
36
+
37
+ const DropdownMenuSubContent = React.forwardRef<
38
+ React.ComponentRef<typeof DropdownMenuPrimitive.SubContent>,
39
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
40
+ >(({ className, ...props }, ref) => (
41
+ <DropdownMenuPrimitive.SubContent
42
+ ref={ref}
43
+ className={cn(
44
+ 'z-[9999] min-w-[8rem] overflow-hidden rounded-xl p-1',
45
+ 'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
46
+ 'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
47
+ 'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
48
+ className
49
+ )}
50
+ style={{
51
+ background: 'var(--khal-menu-bg)',
52
+ border: '1px solid var(--khal-menu-border)',
53
+ boxShadow: 'var(--khal-menu-shadow)',
54
+ color: 'var(--khal-text-primary)',
55
+ }}
56
+ {...props}
57
+ />
58
+ ));
59
+ DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
60
+
61
+ const DropdownMenuContent = React.forwardRef<
62
+ React.ComponentRef<typeof DropdownMenuPrimitive.Content>,
63
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
64
+ >(({ className, sideOffset = 4, ...props }, ref) => (
65
+ <DropdownMenuPrimitive.Portal>
66
+ <DropdownMenuPrimitive.Content
67
+ ref={ref}
68
+ sideOffset={sideOffset}
69
+ className={cn(
70
+ 'z-[9999] min-w-[8rem] overflow-hidden rounded-xl p-1',
71
+ 'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
72
+ 'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
73
+ 'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
74
+ className
75
+ )}
76
+ style={{
77
+ background: 'var(--khal-menu-bg)',
78
+ border: '1px solid var(--khal-menu-border)',
79
+ boxShadow: 'var(--khal-menu-shadow)',
80
+ color: 'var(--khal-text-primary)',
81
+ }}
82
+ {...props}
83
+ />
84
+ </DropdownMenuPrimitive.Portal>
85
+ ));
86
+ DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
87
+
88
+ const DropdownMenuItem = React.forwardRef<
89
+ React.ComponentRef<typeof DropdownMenuPrimitive.Item>,
90
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
91
+ inset?: boolean;
92
+ prefix?: React.ReactNode;
93
+ suffix?: React.ReactNode;
94
+ }
95
+ >(({ className, inset, prefix, suffix, children, ...props }, ref) => (
96
+ <DropdownMenuPrimitive.Item
97
+ ref={ref}
98
+ className={cn(
99
+ 'relative flex cursor-default items-center gap-2 rounded-lg px-2 py-1.5 text-copy-13 outline-none select-none transition-colors',
100
+ 'focus:bg-[var(--khal-menu-hover)]',
101
+ 'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
102
+ inset && 'pl-8',
103
+ className
104
+ )}
105
+ style={{ color: 'var(--khal-text-primary)' }}
106
+ {...props}
107
+ >
108
+ {prefix && <span className="inline-flex shrink-0">{prefix}</span>}
109
+ {children}
110
+ {suffix && (
111
+ <span className="ml-auto inline-flex shrink-0" style={{ color: 'var(--khal-text-muted)' }}>
112
+ {suffix}
113
+ </span>
114
+ )}
115
+ </DropdownMenuPrimitive.Item>
116
+ ));
117
+ DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
118
+
119
+ const DropdownMenuCheckboxItem = React.forwardRef<
120
+ React.ComponentRef<typeof DropdownMenuPrimitive.CheckboxItem>,
121
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
122
+ >(({ className, children, checked, ...props }, ref) => (
123
+ <DropdownMenuPrimitive.CheckboxItem
124
+ ref={ref}
125
+ className={cn(
126
+ 'relative flex cursor-default items-center rounded-lg py-1.5 pl-8 pr-2 text-copy-13 outline-none select-none transition-colors',
127
+ 'focus:bg-[var(--khal-menu-hover)]',
128
+ 'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
129
+ className
130
+ )}
131
+ style={{ color: 'var(--khal-text-primary)' }}
132
+ checked={checked}
133
+ {...props}
134
+ >
135
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
136
+ <DropdownMenuPrimitive.ItemIndicator>
137
+ <svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
138
+ <path
139
+ d="M11.4669 3.72684C11.7558 3.91574 11.8369 4.30308 11.648 4.59198L7.39799 11.092C7.29783 11.2452 7.13556 11.3467 6.95402 11.3699C6.77247 11.3931 6.58989 11.3354 6.45446 11.2124L3.70446 8.71241C3.44905 8.48022 3.43023 8.08494 3.66242 7.82953C3.89461 7.57412 4.28989 7.5553 4.5453 7.78749L6.75292 9.79441L10.6018 3.90792C10.7907 3.61902 11.178 3.53795 11.4669 3.72684Z"
140
+ fill="currentColor"
141
+ fillRule="evenodd"
142
+ clipRule="evenodd"
143
+ ></path>
144
+ </svg>
145
+ </DropdownMenuPrimitive.ItemIndicator>
146
+ </span>
147
+ {children}
148
+ </DropdownMenuPrimitive.CheckboxItem>
149
+ ));
150
+ DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
151
+
152
+ const DropdownMenuRadioItem = React.forwardRef<
153
+ React.ComponentRef<typeof DropdownMenuPrimitive.RadioItem>,
154
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
155
+ >(({ className, children, ...props }, ref) => (
156
+ <DropdownMenuPrimitive.RadioItem
157
+ ref={ref}
158
+ className={cn(
159
+ 'relative flex cursor-default items-center rounded-lg py-1.5 pl-8 pr-2 text-copy-13 outline-none select-none transition-colors',
160
+ 'focus:bg-[var(--khal-menu-hover)]',
161
+ 'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
162
+ className
163
+ )}
164
+ style={{ color: 'var(--khal-text-primary)' }}
165
+ {...props}
166
+ >
167
+ <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
168
+ <DropdownMenuPrimitive.ItemIndicator>
169
+ <svg width="8" height="8" viewBox="0 0 8 8" fill="none">
170
+ <circle cx="4" cy="4" r="4" fill="currentColor" />
171
+ </svg>
172
+ </DropdownMenuPrimitive.ItemIndicator>
173
+ </span>
174
+ {children}
175
+ </DropdownMenuPrimitive.RadioItem>
176
+ ));
177
+ DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
178
+
179
+ const DropdownMenuLabel = React.forwardRef<
180
+ React.ComponentRef<typeof DropdownMenuPrimitive.Label>,
181
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
182
+ inset?: boolean;
183
+ }
184
+ >(({ className, inset, ...props }, ref) => (
185
+ <DropdownMenuPrimitive.Label
186
+ ref={ref}
187
+ className={cn('px-2 py-1.5 text-label-12 font-semibold', inset && 'pl-8', className)}
188
+ style={{ color: 'var(--khal-text-secondary)' }}
189
+ {...props}
190
+ />
191
+ ));
192
+ DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
193
+
194
+ const DropdownMenuSeparator = React.forwardRef<
195
+ React.ComponentRef<typeof DropdownMenuPrimitive.Separator>,
196
+ React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
197
+ >(({ className, ...props }, ref) => (
198
+ <DropdownMenuPrimitive.Separator
199
+ ref={ref}
200
+ className={cn('-mx-1 my-1 h-px', className)}
201
+ style={{ background: 'var(--khal-border-default)' }}
202
+ {...props}
203
+ />
204
+ ));
205
+ DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
206
+
207
+ const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
208
+ return (
209
+ <span
210
+ className={cn('ml-auto text-label-12 tracking-widest', className)}
211
+ style={{ color: 'var(--khal-text-muted)' }}
212
+ {...props}
213
+ />
214
+ );
215
+ };
216
+
217
+ export {
218
+ DropdownMenu,
219
+ DropdownMenuCheckboxItem,
220
+ DropdownMenuContent,
221
+ DropdownMenuGroup,
222
+ DropdownMenuItem,
223
+ DropdownMenuLabel,
224
+ DropdownMenuPortal,
225
+ DropdownMenuRadioGroup,
226
+ DropdownMenuRadioItem,
227
+ DropdownMenuSeparator,
228
+ DropdownMenuShortcut,
229
+ DropdownMenuSub,
230
+ DropdownMenuSubContent,
231
+ DropdownMenuSubTrigger,
232
+ DropdownMenuTrigger,
233
+ };
@@ -0,0 +1,74 @@
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 glassCardVariants = cva(
8
+ 'relative overflow-hidden border transition-all [background:var(--khal-glass-tint)] [border-color:var(--khal-glass-border)]',
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: '[box-shadow:var(--khal-shadow-sm)]',
13
+ raised: '[box-shadow:var(--khal-shadow-md)]',
14
+ },
15
+ padding: {
16
+ sm: 'p-3',
17
+ md: 'p-4',
18
+ lg: 'p-6',
19
+ },
20
+ },
21
+ defaultVariants: {
22
+ variant: 'default',
23
+ padding: 'md',
24
+ },
25
+ }
26
+ );
27
+
28
+ interface GlassCardProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof glassCardVariants> {
29
+ hover?: boolean;
30
+ glow?: string;
31
+ }
32
+
33
+ const GlassCard = React.forwardRef<HTMLDivElement, GlassCardProps>(
34
+ ({ className, variant, padding, hover = false, glow, style, children, ...props }, ref) => {
35
+ return (
36
+ <div
37
+ ref={ref}
38
+ className={cn(
39
+ glassCardVariants({ variant, padding }),
40
+ hover && [
41
+ 'cursor-pointer',
42
+ 'hover:-translate-y-0.5',
43
+ 'hover:[border-color:var(--khal-border-strong)]',
44
+ 'hover:[box-shadow:var(--khal-shadow-lg)]',
45
+ ],
46
+ className
47
+ )}
48
+ style={{
49
+ backdropFilter: 'var(--khal-glass-filter)',
50
+ WebkitBackdropFilter: 'var(--khal-glass-filter)',
51
+ borderRadius: 'var(--khal-radius-xl)',
52
+ transitionTimingFunction: 'var(--khal-ease-spring)',
53
+ transitionDuration: 'var(--khal-duration-normal)',
54
+ ...style,
55
+ }}
56
+ {...props}
57
+ >
58
+ {glow && (
59
+ <div
60
+ className="pointer-events-none absolute inset-0 rounded-[inherit]"
61
+ style={{
62
+ background: `radial-gradient(ellipse at 50% 0%, color-mix(in srgb, ${glow} 20%, transparent), transparent 70%)`,
63
+ }}
64
+ />
65
+ )}
66
+ <div className="relative">{children}</div>
67
+ </div>
68
+ );
69
+ }
70
+ );
71
+ GlassCard.displayName = 'GlassCard';
72
+
73
+ export type { GlassCardProps };
74
+ export { GlassCard, glassCardVariants };
@@ -0,0 +1,48 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { cn } from '../utils';
5
+
6
+ interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
7
+ /** Label above the input (geistcn compat) */
8
+ label?: string;
9
+ /** Size variant (geistcn compat) */
10
+ size?: 'small' | 'medium' | 'large';
11
+ /** HTML input type (named typeName for geistcn compat, also accepts type) */
12
+ typeName?: string;
13
+ }
14
+
15
+ const Input = React.forwardRef<HTMLInputElement, InputProps>(
16
+ ({ className, label, size, typeName, type, ...props }, ref) => {
17
+ const input = (
18
+ <input
19
+ type={typeName ?? type}
20
+ className={cn(
21
+ 'flex w-full rounded-md border border-gray-alpha-400 bg-background-100 px-3 text-copy-13 text-gray-1000 transition-colors',
22
+ 'file:border-0 file:bg-transparent file:text-sm file:font-medium',
23
+ 'placeholder:text-gray-700',
24
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-700 focus-visible:ring-offset-1',
25
+ 'disabled:cursor-not-allowed disabled:opacity-50',
26
+ size === 'small' ? 'h-8' : size === 'large' ? 'h-11' : 'h-9',
27
+ className
28
+ )}
29
+ ref={ref}
30
+ {...props}
31
+ />
32
+ );
33
+
34
+ if (label) {
35
+ return (
36
+ <div className="flex flex-col gap-1.5">
37
+ <label className="text-label-13 text-gray-900">{label}</label>
38
+ {input}
39
+ </div>
40
+ );
41
+ }
42
+
43
+ return input;
44
+ }
45
+ );
46
+ Input.displayName = 'Input';
47
+
48
+ export { Input };
@@ -0,0 +1,73 @@
1
+ interface KhalLogoProps {
2
+ size?: number;
3
+ variant?: 'light' | 'dark';
4
+ className?: string;
5
+ }
6
+
7
+ /**
8
+ * KhalLogo — SVG wordmark for large sizes (>=20), single "K" letterform for small sizes.
9
+ * Uses currentColor so color is inherited from parent.
10
+ */
11
+ export function KhalLogo({ size = 20, variant = 'light', className }: KhalLogoProps) {
12
+ const color = variant === 'light' ? '#FFFFFF' : '#0A0A0A';
13
+ const showFull = size >= 20;
14
+
15
+ if (!showFull) {
16
+ // Small sizes: render just the K letterform from the SVG
17
+ return (
18
+ <svg
19
+ viewBox="0 0 156 155"
20
+ fill="none"
21
+ xmlns="http://www.w3.org/2000/svg"
22
+ width={size}
23
+ height={size}
24
+ className={className}
25
+ aria-label="K"
26
+ style={{ color }}
27
+ >
28
+ <path
29
+ d="M0 0H27.4425V65.9519H71.7054L122.829 0H155.362L95.3869 76.1317L164.657 154.92H128.805L72.5913 92.2878H27.4425V154.92H0V0Z"
30
+ fill="currentColor"
31
+ />
32
+ </svg>
33
+ );
34
+ }
35
+
36
+ // Full wordmark SVG
37
+ return (
38
+ <svg
39
+ viewBox="0 0 756 155"
40
+ fill="none"
41
+ xmlns="http://www.w3.org/2000/svg"
42
+ width={size * (756 / 155)}
43
+ height={size}
44
+ className={className}
45
+ aria-label="khal"
46
+ style={{ color }}
47
+ >
48
+ <g clipPath="url(#khal-logo-clip)">
49
+ <path
50
+ d="M616.81 0H644.252V128.584H765.533V154.92H638.499C635.4 154.92 632.524 154.33 629.867 153.149C627.211 151.969 624.924 150.42 623.007 148.502C621.088 146.584 619.539 144.371 618.359 141.863C617.326 139.206 616.81 136.403 616.81 133.453V0Z"
51
+ fill="currentColor"
52
+ />
53
+ <path
54
+ d="M443.058 21.2467C445.123 14.4594 448.295 9.22105 452.573 5.53287C456.853 1.8447 462.533 0 469.616 0H519.632C527.009 0 532.911 1.91744 537.337 5.75467C541.763 9.44285 545.009 14.6072 547.076 21.2467L589.125 154.92H560.133L546.411 110.657H461.87L468.73 84.3212H538.665L521.181 26.3359H468.951L430 154.92H400.786L443.058 21.2467Z"
55
+ fill="currentColor"
56
+ />
57
+ <path
58
+ d="M190.123 0H217.565V62.6322H344.6V0H372.043V154.92H344.6V88.9681H217.565V154.92H190.123V0Z"
59
+ fill="currentColor"
60
+ />
61
+ <path
62
+ d="M0 0H27.4425V65.9519H71.7054L122.829 0H155.362L95.3869 76.1317L164.657 154.92H128.805L72.5913 92.2878H27.4425V154.92H0V0Z"
63
+ fill="currentColor"
64
+ />
65
+ </g>
66
+ <defs>
67
+ <clipPath id="khal-logo-clip">
68
+ <rect width="756" height="155" fill="white" />
69
+ </clipPath>
70
+ </defs>
71
+ </svg>
72
+ );
73
+ }
@@ -0,0 +1,109 @@
1
+ 'use client';
2
+
3
+ import { AnimatePresence, motion } from 'motion/react';
4
+ import { useCallback, useEffect, useRef, useState } from 'react';
5
+ import { cn } from '../utils';
6
+
7
+ type FeedEventType = 'info' | 'success' | 'warning' | 'error' | 'agent' | 'system';
8
+
9
+ interface FeedEvent {
10
+ id: string;
11
+ type: FeedEventType;
12
+ message: string;
13
+ timestamp?: Date;
14
+ }
15
+
16
+ const typeColors: Record<FeedEventType, string> = {
17
+ info: 'var(--ds-blue-600)',
18
+ success: 'var(--ds-green-600)',
19
+ warning: 'var(--ds-amber-600)',
20
+ error: 'var(--ds-red-600)',
21
+ agent: 'var(--ds-purple-600)',
22
+ system: 'var(--ds-gray-600)',
23
+ };
24
+
25
+ interface LiveFeedProps {
26
+ /** Initial events to render */
27
+ events?: FeedEvent[];
28
+ /** Maximum visible events before oldest are removed */
29
+ maxVisible?: number;
30
+ /** Show timestamps next to events */
31
+ showTimestamps?: boolean;
32
+ /** Height of the feed container */
33
+ height?: number | string;
34
+ className?: string;
35
+ }
36
+
37
+ function LiveFeed({
38
+ events: externalEvents,
39
+ maxVisible = 50,
40
+ showTimestamps = true,
41
+ height = 300,
42
+ className,
43
+ }: LiveFeedProps) {
44
+ const [events, setEvents] = useState<FeedEvent[]>(externalEvents ?? []);
45
+ const scrollRef = useRef<HTMLDivElement>(null);
46
+ const isAtBottom = useRef(true);
47
+
48
+ // Sync external events
49
+ useEffect(() => {
50
+ if (externalEvents) {
51
+ setEvents((prev) => {
52
+ const combined = [...prev, ...externalEvents.filter((e) => !prev.some((p) => p.id === e.id))];
53
+ return combined.slice(-maxVisible);
54
+ });
55
+ }
56
+ }, [externalEvents, maxVisible]);
57
+
58
+ // Auto-scroll to bottom when new events arrive and user is at bottom
59
+ useEffect(() => {
60
+ const el = scrollRef.current;
61
+ if (el && isAtBottom.current) {
62
+ el.scrollTop = el.scrollHeight;
63
+ }
64
+ }, [events]);
65
+
66
+ const handleScroll = useCallback(() => {
67
+ const el = scrollRef.current;
68
+ if (el) {
69
+ isAtBottom.current = el.scrollHeight - el.scrollTop - el.clientHeight < 24;
70
+ }
71
+ }, []);
72
+
73
+ const formatTime = (d: Date) =>
74
+ d.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
75
+
76
+ return (
77
+ <div
78
+ ref={scrollRef}
79
+ onScroll={handleScroll}
80
+ className={cn('overflow-y-auto overflow-x-hidden font-mono text-xs leading-5 scrollbar-thin', className)}
81
+ style={{ height }}
82
+ >
83
+ <AnimatePresence initial={false}>
84
+ {events.map((event) => (
85
+ <motion.div
86
+ key={event.id}
87
+ initial={{ opacity: 0, height: 0 }}
88
+ animate={{ opacity: 1, height: 'auto' }}
89
+ exit={{ opacity: 0, height: 0 }}
90
+ transition={{ duration: 0.25, ease: [0.22, 1, 0.36, 1] }}
91
+ className="flex gap-2 px-2 py-0.5 hover:bg-[var(--ds-gray-alpha-100)]"
92
+ >
93
+ <span
94
+ className="inline-block w-1.5 h-1.5 rounded-full mt-1.5 shrink-0"
95
+ style={{ backgroundColor: typeColors[event.type] }}
96
+ />
97
+ {showTimestamps && event.timestamp && (
98
+ <span className="shrink-0 opacity-40 tabular-nums">{formatTime(event.timestamp)}</span>
99
+ )}
100
+ <span className="opacity-80 break-words min-w-0">{event.message}</span>
101
+ </motion.div>
102
+ ))}
103
+ </AnimatePresence>
104
+ </div>
105
+ );
106
+ }
107
+
108
+ export type { LiveFeedProps, FeedEvent, FeedEventType };
109
+ export { LiveFeed };
@@ -0,0 +1,57 @@
1
+ 'use client';
2
+
3
+ import { lazy, Suspense, useEffect, useRef, useState } from 'react';
4
+ import { useReducedMotion } from '../hooks/useReducedMotion';
5
+
6
+ const MeshGradientShader = lazy(() => import('@paper-design/shaders-react').then((m) => ({ default: m.MeshGradient })));
7
+
8
+ interface MeshGradientProps {
9
+ /** Array of CSS color strings (typically 4-8) */
10
+ colors: string[];
11
+ /** Animation speed multiplier (default 0.02) */
12
+ speed?: number;
13
+ className?: string;
14
+ style?: React.CSSProperties;
15
+ }
16
+
17
+ function StaticFallback({ colors }: { colors: string[] }) {
18
+ const bg =
19
+ colors.length >= 2
20
+ ? `linear-gradient(135deg, ${colors[0]} 0%, ${colors[Math.floor(colors.length / 2)]} 50%, ${colors[colors.length - 1]} 100%)`
21
+ : (colors[0] ?? '#0A0A0A');
22
+
23
+ return <div style={{ width: '100%', height: '100%', background: bg }} />;
24
+ }
25
+
26
+ function MeshGradientInner({ colors, speed = 0.02, className, style }: MeshGradientProps) {
27
+ const ref = useRef<HTMLDivElement>(null);
28
+ const [visible, setVisible] = useState(false);
29
+ const reducedMotion = useReducedMotion();
30
+
31
+ useEffect(() => {
32
+ const el = ref.current;
33
+ if (!el) return;
34
+
35
+ const observer = new IntersectionObserver(([entry]) => setVisible(entry.isIntersecting), {
36
+ rootMargin: '100px',
37
+ });
38
+
39
+ observer.observe(el);
40
+ return () => observer.disconnect();
41
+ }, []);
42
+
43
+ return (
44
+ <div ref={ref} className={className} style={{ position: 'absolute', inset: 0, ...style }}>
45
+ {visible && !reducedMotion ? (
46
+ <Suspense fallback={<StaticFallback colors={colors} />}>
47
+ <MeshGradientShader colors={colors} speed={speed} style={{ width: '100%', height: '100%' }} />
48
+ </Suspense>
49
+ ) : (
50
+ <StaticFallback colors={colors} />
51
+ )}
52
+ </div>
53
+ );
54
+ }
55
+
56
+ export type { MeshGradientProps };
57
+ export { MeshGradientInner as MeshGradient };