@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.
- package/package.json +41 -0
- package/src/components/ContextMenu.tsx +130 -0
- package/src/components/avatar.tsx +71 -0
- package/src/components/badge.tsx +39 -0
- package/src/components/button.tsx +102 -0
- package/src/components/command.tsx +165 -0
- package/src/components/cost-counter.tsx +75 -0
- package/src/components/data-row.tsx +97 -0
- package/src/components/dropdown-menu.tsx +233 -0
- package/src/components/glass-card.tsx +74 -0
- package/src/components/input.tsx +48 -0
- package/src/components/khal-logo.tsx +73 -0
- package/src/components/live-feed.tsx +109 -0
- package/src/components/mesh-gradient.tsx +57 -0
- package/src/components/metric-display.tsx +93 -0
- package/src/components/note.tsx +55 -0
- package/src/components/number-flow.tsx +25 -0
- package/src/components/pill-badge.tsx +65 -0
- package/src/components/progress-bar.tsx +70 -0
- package/src/components/section-card.tsx +76 -0
- package/src/components/separator.tsx +25 -0
- package/src/components/spinner.tsx +42 -0
- package/src/components/status-dot.tsx +90 -0
- package/src/components/switch.tsx +36 -0
- package/src/components/theme-provider.tsx +58 -0
- package/src/components/theme-switcher.tsx +59 -0
- package/src/components/ticker-bar.tsx +41 -0
- package/src/components/tooltip.tsx +62 -0
- package/src/components/window-minimized-context.tsx +29 -0
- package/src/hooks/useReducedMotion.ts +21 -0
- package/src/index.ts +58 -0
- package/src/lib/animations.ts +50 -0
- package/src/primitives/collapsible-sidebar.tsx +226 -0
- package/src/primitives/dialog.tsx +76 -0
- package/src/primitives/empty-state.tsx +43 -0
- package/src/primitives/index.ts +22 -0
- package/src/primitives/list-view.tsx +155 -0
- package/src/primitives/property-panel.tsx +108 -0
- package/src/primitives/section-header.tsx +19 -0
- package/src/primitives/sidebar-nav.tsx +110 -0
- package/src/primitives/split-pane.tsx +146 -0
- package/src/primitives/status-badge.tsx +10 -0
- package/src/primitives/status-bar.tsx +100 -0
- package/src/primitives/toolbar.tsx +152 -0
- package/src/server.ts +4 -0
- package/src/stores/notification-store.ts +271 -0
- package/src/stores/theme-store.ts +33 -0
- package/src/tokens/lp-tokens.ts +36 -0
- package/src/utils.ts +6 -0
- package/tokens.css +295 -0
- 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 };
|