feat: add warm sand theme colors and light / dark mode toggle #302
+17
-1
@@ -1,8 +1,24 @@
|
||||
<!doctype html>
|
||||
<html class="dark">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>llink</title>
|
||||
<script>
|
||||
// Apply the stored theme before first paint to avoid a flash. Mirrors
|
||||
// the logic in stores/theme-store.ts; defaults to dark.
|
||||
try {
|
||||
var m = localStorage.getItem('llink:theme');
|
||||
var dark =
|
||||
m === 'light'
|
||||
? false
|
||||
: m === 'system'
|
||||
? matchMedia('(prefers-color-scheme: dark)').matches
|
||||
: true;
|
||||
if (dark) document.documentElement.classList.add('dark');
|
||||
} catch (e) {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -29,14 +29,14 @@ export function ComposingIndicator({
|
||||
return (
|
||||
<div
|
||||
key={u.humanId}
|
||||
className="flex rotate-180 items-center gap-1.5 rounded-full bg-white/10 px-2 py-1 backdrop-blur-sm"
|
||||
className="bg-card/70 border-border flex rotate-180 items-center gap-1.5 rounded-full border px-2 py-1 backdrop-blur-sm"
|
||||
>
|
||||
<span className="flex gap-0.5">
|
||||
<span className="size-1 rounded-full bg-white/70 animate-bounce [animation-delay:0ms]" />
|
||||
<span className="size-1 rounded-full bg-white/70 animate-bounce [animation-delay:150ms]" />
|
||||
<span className="size-1 rounded-full bg-white/70 animate-bounce [animation-delay:300ms]" />
|
||||
<span className="bg-muted-foreground size-1 animate-bounce rounded-full [animation-delay:0ms]" />
|
||||
<span className="bg-muted-foreground size-1 animate-bounce rounded-full [animation-delay:150ms]" />
|
||||
<span className="bg-muted-foreground size-1 animate-bounce rounded-full [animation-delay:300ms]" />
|
||||
</span>
|
||||
<span className="whitespace-nowrap text-[10px] text-white/50">
|
||||
<span className="text-muted-foreground whitespace-nowrap text-[10px]">
|
||||
{displayName} {modeLabel}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -38,23 +38,23 @@ export function ConfirmDestructiveOverlay({
|
||||
return createPortal(
|
||||
<div className="fixed inset-0 z-[100]">
|
||||
<div
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
||||
className="absolute inset-0 bg-scrim/60 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
/>
|
||||
<div className="absolute left-1/2 top-1/2 w-full max-w-sm -translate-x-1/2 -translate-y-1/2 rounded-2xl border border-white/10 bg-white/5 p-6 shadow-2xl backdrop-blur-xl">
|
||||
<div className="bg-popover/95 text-popover-foreground border-border absolute left-1/2 top-1/2 w-full max-w-sm -translate-x-1/2 -translate-y-1/2 rounded-2xl border p-6 shadow-2xl backdrop-blur-xl">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h2 className="text-sm font-semibold text-white/70">{title}</h2>
|
||||
<h2 className="text-sm font-semibold">{title}</h2>
|
||||
<KeyHint
|
||||
keys="Esc"
|
||||
onClick={onClose}
|
||||
title="Close (or press Esc)"
|
||||
className="text-xs text-white/30"
|
||||
className="text-muted-foreground text-xs"
|
||||
>
|
||||
to close
|
||||
</KeyHint>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-white/60">{description}</div>
|
||||
<div className="text-muted-foreground text-sm">{description}</div>
|
||||
|
||||
<div className="mt-5 flex items-center justify-end gap-2">
|
||||
<Button
|
||||
|
||||
@@ -2,9 +2,10 @@ import { Fragment } from 'react';
|
||||
import { Kbd } from '@/components/ui/kbd';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
/** Dark-overlay chip restyle of the design-system Kbd, kept in one place. */
|
||||
/** Chip restyle of the design-system Kbd, kept in one place. Tints to the
|
||||
surrounding text color so it reads on themed panels and over media alike. */
|
||||
const chipClass =
|
||||
'rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs font-normal text-current';
|
||||
'rounded bg-current/10 px-1.5 py-0.5 font-mono text-xs font-normal text-current';
|
||||
|
||||
interface KeyHintProps {
|
||||
/** Key chip(s): "Esc" or ["Esc", "Q"]. */
|
||||
@@ -68,7 +69,7 @@ export function KeyHint({
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
title={title}
|
||||
className={cn(
|
||||
'cursor-pointer rounded transition-colors hover:text-white/80',
|
||||
'cursor-pointer rounded transition-opacity hover:opacity-80',
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
|
||||
@@ -48,19 +48,19 @@ export function KeybindingsOverlay({
|
||||
<div className="fixed inset-0 z-[100]">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
||||
className="absolute inset-0 bg-scrim/60 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
/>
|
||||
{/* Panel */}
|
||||
<div className="absolute left-1/2 top-1/2 w-full max-w-sm -translate-x-1/2 -translate-y-1/2 rounded-2xl border border-white/10 bg-white/5 p-6 shadow-2xl backdrop-blur-xl">
|
||||
<div className="bg-popover/95 text-popover-foreground border-border absolute left-1/2 top-1/2 w-full max-w-sm -translate-x-1/2 -translate-y-1/2 rounded-2xl border p-6 shadow-2xl backdrop-blur-xl">
|
||||
<div className="mb-5 flex items-center justify-between">
|
||||
<h2 className="text-sm font-semibold text-white/70">{title}</h2>
|
||||
<h2 className="text-sm font-semibold">{title}</h2>
|
||||
<KeyHint
|
||||
keys={['Esc', '?']}
|
||||
separator="or"
|
||||
onClick={onClose}
|
||||
title="Close (or press Esc / ?)"
|
||||
className="text-xs text-white/30"
|
||||
className="text-muted-foreground text-xs"
|
||||
>
|
||||
to close
|
||||
</KeyHint>
|
||||
@@ -68,21 +68,19 @@ export function KeybindingsOverlay({
|
||||
<div className="flex flex-col gap-5">
|
||||
{groups.map((group) => (
|
||||
<section key={group.label}>
|
||||
<h3 className="mb-2 text-[10px] font-medium uppercase tracking-widest text-white/30">
|
||||
<h3 className="text-muted-foreground mb-2 text-[10px] font-medium uppercase tracking-widest">
|
||||
{group.label}
|
||||
</h3>
|
||||
<ul className="flex flex-col gap-1">
|
||||
{group.bindings.map((binding) => (
|
||||
<li
|
||||
key={binding.description}
|
||||
className="flex items-center justify-between border-b border-white/5 pb-1 last:border-0 last:pb-0"
|
||||
className="border-border flex items-center justify-between border-b pb-1 last:border-0 last:pb-0"
|
||||
>
|
||||
<span className="text-sm text-white/70">
|
||||
{binding.description}
|
||||
</span>
|
||||
<span className="text-sm">{binding.description}</span>
|
||||
<KeyHint
|
||||
keys={binding.keys}
|
||||
className="flex items-center gap-1 text-white/60"
|
||||
className="text-muted-foreground flex items-center gap-1"
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -22,7 +22,7 @@ export function LinkPreviewCard({ metadata, compact }: LinkPreviewCardProps) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="max-w-sm overflow-hidden rounded-2xl bg-white/10 backdrop-blur-md"
|
||||
className="bg-card text-card-foreground border-border max-w-sm overflow-hidden rounded-2xl border"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleOpen(e);
|
||||
@@ -39,7 +39,7 @@ export function LinkPreviewCard({ metadata, compact }: LinkPreviewCardProps) {
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col gap-1 p-3">
|
||||
<div className="flex items-center gap-1.5 text-xs text-white/50">
|
||||
<div className="text-muted-foreground flex items-center gap-1.5 text-xs">
|
||||
{metadata.favicon ? (
|
||||
<img
|
||||
src={metadata.favicon}
|
||||
@@ -57,12 +57,12 @@ export function LinkPreviewCard({ metadata, compact }: LinkPreviewCardProps) {
|
||||
<span className="truncate">{metadata.domain}</span>
|
||||
</div>
|
||||
{metadata.title && (
|
||||
<p className="truncate text-sm font-semibold leading-snug text-white">
|
||||
<p className="truncate text-sm font-semibold leading-snug">
|
||||
{metadata.title}
|
||||
</p>
|
||||
)}
|
||||
{metadata.description && (
|
||||
<p className="line-clamp-2 text-xs leading-relaxed text-white/70">
|
||||
<p className="text-muted-foreground line-clamp-2 text-xs leading-relaxed">
|
||||
{metadata.description}
|
||||
</p>
|
||||
)}
|
||||
@@ -71,7 +71,7 @@ export function LinkPreviewCard({ metadata, compact }: LinkPreviewCardProps) {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="text-white/70 hover:bg-white/10 hover:text-white"
|
||||
className="text-muted-foreground"
|
||||
onClick={handleOpen}
|
||||
>
|
||||
<ExternalLink data-icon="inline-start" />
|
||||
@@ -80,7 +80,7 @@ export function LinkPreviewCard({ metadata, compact }: LinkPreviewCardProps) {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="text-white/70 hover:bg-white/10 hover:text-white"
|
||||
className="text-muted-foreground"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
<Copy data-icon="inline-start" />
|
||||
@@ -111,12 +111,12 @@ export function LinkPreviewCardFallback({ url }: { url: string }) {
|
||||
|
||||
export function LinkPreviewCardSkeleton() {
|
||||
return (
|
||||
<div className="max-w-sm overflow-hidden rounded-2xl bg-white/10 backdrop-blur-md">
|
||||
<Skeleton className="h-32 w-full rounded-none bg-white/5" />
|
||||
<div className="bg-card border-border max-w-sm overflow-hidden rounded-2xl border">
|
||||
<Skeleton className="h-32 w-full rounded-none" />
|
||||
<div className="flex flex-col gap-2 p-3">
|
||||
<Skeleton className="h-3 w-24 bg-white/10" />
|
||||
<Skeleton className="h-4 w-48 bg-white/10" />
|
||||
<Skeleton className="h-3 w-full bg-white/10" />
|
||||
<Skeleton className="h-3 w-24" />
|
||||
<Skeleton className="h-4 w-48" />
|
||||
<Skeleton className="h-3 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -34,13 +34,13 @@ export function ScreenSourcePicker({
|
||||
const windows = sources.filter((s) => s.id.startsWith('window:'));
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 z-50 flex items-center justify-center bg-black/90">
|
||||
<div className="mx-4 flex max-h-[80vh] w-full max-w-2xl flex-col rounded-lg bg-zinc-900 shadow-xl">
|
||||
<div className="flex items-center justify-between border-b border-zinc-700 px-5 py-4">
|
||||
<h2 className="text-base font-medium text-zinc-100">{title}</h2>
|
||||
<div className="bg-scrim/80 absolute inset-0 z-50 flex items-center justify-center">
|
||||
<div className="bg-popover text-popover-foreground mx-4 flex max-h-[80vh] w-full max-w-2xl flex-col rounded-lg shadow-xl">
|
||||
<div className="border-border flex items-center justify-between border-b px-5 py-4">
|
||||
<h2 className="text-base font-medium">{title}</h2>
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="text-zinc-400 hover:text-zinc-200"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
@@ -48,7 +48,7 @@ export function ScreenSourcePicker({
|
||||
|
||||
<div className="flex-1 overflow-y-auto px-5 py-4">
|
||||
{loading ? (
|
||||
<p className="text-center text-sm text-zinc-400">
|
||||
<p className="text-muted-foreground text-center text-sm">
|
||||
Loading sources...
|
||||
</p>
|
||||
) : (
|
||||
@@ -73,17 +73,17 @@ export function ScreenSourcePicker({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 border-t border-zinc-700 px-5 py-3">
|
||||
<div className="border-border flex justify-end gap-2 border-t px-5 py-3">
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="rounded-md px-4 py-2 text-sm text-zinc-300 hover:bg-zinc-800"
|
||||
className="hover:bg-accent text-muted-foreground rounded-md px-4 py-2 text-sm"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
disabled={!selectedId}
|
||||
onClick={() => selectedId && onSelect(selectedId)}
|
||||
className="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-500 disabled:opacity-40 disabled:hover:bg-blue-600"
|
||||
className="bg-primary text-primary-foreground rounded-md px-4 py-2 text-sm font-medium hover:bg-primary/90 disabled:opacity-40"
|
||||
>
|
||||
{confirmLabel}
|
||||
</button>
|
||||
@@ -106,7 +106,7 @@ function SourceSection({
|
||||
}) {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<h3 className="mb-2 text-xs font-medium uppercase tracking-wide text-zinc-400">
|
||||
<h3 className="text-muted-foreground mb-2 text-xs font-medium uppercase tracking-wide">
|
||||
{title}
|
||||
</h3>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
@@ -116,8 +116,8 @@ function SourceSection({
|
||||
onClick={() => onSelect(source.id)}
|
||||
className={`overflow-hidden rounded-lg border-2 text-left transition-colors ${
|
||||
selectedId === source.id
|
||||
? 'border-blue-500 bg-zinc-800'
|
||||
: 'border-transparent bg-zinc-800/50 hover:border-zinc-600'
|
||||
? 'border-primary bg-accent'
|
||||
: 'border-transparent bg-muted hover:border-border'
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
@@ -125,7 +125,7 @@ function SourceSection({
|
||||
alt={source.name}
|
||||
className="aspect-video w-full object-cover"
|
||||
/>
|
||||
<p className="truncate px-2 py-1.5 text-xs text-zinc-300">
|
||||
<p className="text-muted-foreground truncate px-2 py-1.5 text-xs">
|
||||
{source.name}
|
||||
</p>
|
||||
</button>
|
||||
|
||||
@@ -39,7 +39,7 @@ function DialogOverlay({
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50',
|
||||
'data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-scrim/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -47,7 +47,7 @@ function Slider({
|
||||
<SliderPrimitive.Thumb
|
||||
data-slot="slider-thumb"
|
||||
key={index}
|
||||
className="relative block size-3 shrink-0 rounded-full border border-ring bg-white ring-ring/50 transition-[color,box-shadow] select-none after:absolute after:-inset-2 hover:ring-3 focus-visible:ring-3 focus-visible:outline-hidden active:ring-3 disabled:pointer-events-none disabled:opacity-50"
|
||||
className="border-ring bg-background ring-ring/50 relative block size-3 shrink-0 select-none rounded-full border transition-[color,box-shadow] after:absolute after:-inset-2 hover:ring-3 focus-visible:outline-hidden focus-visible:ring-3 active:ring-3 disabled:pointer-events-none disabled:opacity-50"
|
||||
/>
|
||||
))}
|
||||
</SliderPrimitive.Root>
|
||||
|
||||
@@ -6,11 +6,13 @@ import {
|
||||
OctagonXIcon,
|
||||
Loader2Icon,
|
||||
} from 'lucide-react';
|
||||
import { useThemeStore } from '@/stores/theme-store';
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const isDark = useThemeStore((s) => s.isDark);
|
||||
return (
|
||||
<Sonner
|
||||
theme="dark"
|
||||
theme={isDark ? 'dark' : 'light'}
|
||||
className="toaster group"
|
||||
icons={{
|
||||
success: <CircleCheckIcon className="size-4" />,
|
||||
|
||||
@@ -21,7 +21,7 @@ export function WindowControls() {
|
||||
size="icon-sm"
|
||||
onClick={() => platform.window.minimize()}
|
||||
aria-label="Minimize"
|
||||
className="dark:hover:bg-white/10 rounded text-white/50"
|
||||
className="text-muted-foreground rounded"
|
||||
>
|
||||
<Minus />
|
||||
</Button>
|
||||
@@ -34,7 +34,7 @@ export function WindowControls() {
|
||||
size="icon-sm"
|
||||
onClick={() => platform.window.maximize()}
|
||||
aria-label={isMaximized ? 'Restore' : 'Maximize'}
|
||||
className="dark:hover:bg-white/10 rounded text-white/50"
|
||||
className="text-muted-foreground rounded"
|
||||
>
|
||||
{isMaximized ? <Copy /> : <Square />}
|
||||
</Button>
|
||||
@@ -47,7 +47,7 @@ export function WindowControls() {
|
||||
size="icon-sm"
|
||||
onClick={() => platform.window.close()}
|
||||
aria-label="Close"
|
||||
className="hover:text-destructive dark:hover:bg-white/10 rounded text-white/50"
|
||||
className="hover:text-destructive text-muted-foreground rounded"
|
||||
>
|
||||
<X />
|
||||
</Button>
|
||||
|
||||
@@ -172,7 +172,7 @@ export function AttachmentLightbox({
|
||||
}}
|
||||
>
|
||||
<DialogPrimitive.Portal>
|
||||
<DialogPrimitive.Overlay className="data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 fixed inset-0 z-50 bg-black/90 backdrop-blur-sm duration-100" />
|
||||
<DialogPrimitive.Overlay className="data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-scrim/90 fixed inset-0 z-50 backdrop-blur-sm duration-100" />
|
||||
<DialogPrimitive.Content
|
||||
aria-describedby={undefined}
|
||||
className="data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 fixed inset-0 z-50 flex items-center justify-center p-16 outline-none duration-100"
|
||||
|
||||
@@ -62,7 +62,7 @@ function AttachmentThumbnail({
|
||||
tabIndex={previewable ? 0 : undefined}
|
||||
onClick={previewable && onPreview ? onPreview : undefined}
|
||||
className={cn(
|
||||
'group relative flex h-16 w-16 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-white/10',
|
||||
'bg-muted group relative flex h-16 w-16 shrink-0 items-center justify-center overflow-hidden rounded-lg',
|
||||
previewable && 'cursor-pointer',
|
||||
isError && 'ring-1 ring-red-400/50',
|
||||
)}
|
||||
@@ -75,19 +75,19 @@ function AttachmentThumbnail({
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-0.5 px-1">
|
||||
<FileIcon className="size-5 text-white/60" />
|
||||
<span className="max-w-full truncate text-[9px] text-white/50">
|
||||
<FileIcon className="text-muted-foreground size-5" />
|
||||
<span className="text-muted-foreground max-w-full truncate text-[9px]">
|
||||
{attachment.file.name}
|
||||
</span>
|
||||
<span className="text-[9px] text-white/40">
|
||||
<span className="text-muted-foreground text-[9px]">
|
||||
{formatFileSize(attachment.file.size)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isUploading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/50">
|
||||
<Loader2 className="size-4 animate-spin text-white/70" />
|
||||
<div className="bg-scrim/50 absolute inset-0 flex items-center justify-center">
|
||||
<Loader2 className="size-4 animate-spin text-white" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -97,7 +97,7 @@ function AttachmentThumbnail({
|
||||
e.stopPropagation();
|
||||
onRemove();
|
||||
}}
|
||||
className="absolute right-0.5 top-0.5 hidden rounded-full bg-black/70 p-0.5 text-white/70 hover:text-white group-hover:block"
|
||||
className="bg-scrim/70 absolute right-0.5 top-0.5 hidden rounded-full p-0.5 text-white/80 hover:text-white group-hover:block"
|
||||
>
|
||||
<X className="size-3" />
|
||||
</button>
|
||||
@@ -108,9 +108,9 @@ function AttachmentThumbnail({
|
||||
function LinkPreviewThumbnail({ entry }: { entry: LinkPreviewEntry }) {
|
||||
if (entry.isLoading) {
|
||||
return (
|
||||
<div className="flex h-16 w-28 shrink-0 flex-col gap-1.5 rounded-lg bg-white/10 p-2">
|
||||
<Skeleton className="h-2 w-16 bg-white/10" />
|
||||
<Skeleton className="h-3 w-24 bg-white/10" />
|
||||
<div className="bg-muted flex h-16 w-28 shrink-0 flex-col gap-1.5 rounded-lg p-2">
|
||||
<Skeleton className="h-2 w-16" />
|
||||
<Skeleton className="h-3 w-24" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -124,9 +124,9 @@ function LinkPreviewThumbnail({ entry }: { entry: LinkPreviewEntry }) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => platform.link.openExternal(metadata?.url ?? entry.url)}
|
||||
className="flex h-16 w-28 shrink-0 flex-col justify-center gap-1 overflow-hidden rounded-lg bg-white/10 px-2 py-1.5 text-left transition-colors hover:bg-white/15"
|
||||
className="bg-muted hover:bg-accent flex h-16 w-28 shrink-0 flex-col justify-center gap-1 overflow-hidden rounded-lg px-2 py-1.5 text-left transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-1 text-[10px] text-white/40">
|
||||
<div className="text-muted-foreground flex items-center gap-1 text-[10px]">
|
||||
{metadata?.favicon ? (
|
||||
<img
|
||||
src={metadata.favicon}
|
||||
@@ -142,7 +142,7 @@ function LinkPreviewThumbnail({ entry }: { entry: LinkPreviewEntry }) {
|
||||
<span className="truncate">{domain}</span>
|
||||
</div>
|
||||
{title && (
|
||||
<p className="line-clamp-2 text-[11px] font-medium leading-tight text-white/80">
|
||||
<p className="text-foreground line-clamp-2 text-[11px] font-medium leading-tight">
|
||||
{title}
|
||||
</p>
|
||||
)}
|
||||
@@ -197,7 +197,7 @@ export function AttachmentStrip({
|
||||
e.stopPropagation();
|
||||
onAddClick();
|
||||
}}
|
||||
className="flex h-16 w-16 shrink-0 items-center justify-center rounded-lg border border-dashed border-white/20 text-white/40 transition-colors hover:border-white/40 hover:text-white/60"
|
||||
className="border-border text-muted-foreground hover:border-foreground/40 hover:text-foreground flex h-16 w-16 shrink-0 items-center justify-center rounded-lg border border-dashed transition-colors"
|
||||
>
|
||||
<Plus className="size-5" />
|
||||
</button>
|
||||
|
||||
@@ -768,7 +768,7 @@ export function ComposeOverlay({
|
||||
/>
|
||||
)}
|
||||
{step === 'recording' && recordingSource === 'screen' && (
|
||||
<div className="absolute inset-0 z-50 flex flex-col items-center justify-center bg-black/90">
|
||||
<div className="bg-scrim/90 absolute inset-0 z-50 flex flex-col items-center justify-center">
|
||||
<div className="absolute top-8 z-10">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="h-2.5 w-2.5 animate-pulse rounded-full bg-red-500" />
|
||||
@@ -840,8 +840,8 @@ export function ComposeOverlay({
|
||||
/>
|
||||
)}
|
||||
{step === 'submitting' && (
|
||||
<div className="absolute inset-0 z-50 flex flex-col items-center justify-center bg-black/90">
|
||||
<span className="animate-pulse text-sm text-white/60">
|
||||
<div className="bg-background/95 absolute inset-0 z-50 flex flex-col items-center justify-center backdrop-blur-sm">
|
||||
<span className="text-muted-foreground animate-pulse text-sm">
|
||||
Sending...
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -77,13 +77,13 @@ export function ConfigureContainerStep({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute inset-0 z-50 flex flex-col bg-black/90 pt-16"
|
||||
className="bg-background/95 absolute inset-0 z-50 flex flex-col pt-16 backdrop-blur-sm"
|
||||
onKeyDown={handleKeyDown}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<div className="mx-auto w-full max-w-sm space-y-4 px-6">
|
||||
<div>
|
||||
<Label className="mb-1 text-xs text-white/50">
|
||||
<Label className="text-muted-foreground mb-1 text-xs">
|
||||
{kind === 'folder' ? 'Folder name' : 'Stream name'}
|
||||
</Label>
|
||||
<Input
|
||||
@@ -94,21 +94,22 @@ export function ConfigureContainerStep({
|
||||
setName(e.target.value);
|
||||
}}
|
||||
placeholder="Give it a name..."
|
||||
className="border-white/10 bg-white/5 text-white placeholder-white/30 focus-visible:border-white/30 focus-visible:ring-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Visibility */}
|
||||
<div>
|
||||
<Label className="mb-1 text-xs text-white/50">Visible to</Label>
|
||||
<div className="rounded-md border border-white/10">
|
||||
<Label className="text-muted-foreground mb-1 text-xs">
|
||||
Visible to
|
||||
</Label>
|
||||
<div className="border-border rounded-md border">
|
||||
{/* Everyone in network */}
|
||||
<div
|
||||
role="button"
|
||||
onClick={() => setEveryone((prev) => !prev)}
|
||||
className={cn(
|
||||
'flex w-full cursor-pointer items-center gap-2.5 px-3 py-2 text-left text-sm transition-colors',
|
||||
'text-white/70 hover:bg-white/5',
|
||||
'hover:bg-accent',
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
@@ -137,7 +138,7 @@ export function ConfigureContainerStep({
|
||||
onClick={() => toggleMember(member.id)}
|
||||
className={cn(
|
||||
'flex w-full cursor-pointer items-center gap-2.5 rounded px-3 py-1.5 text-left text-sm transition-colors',
|
||||
'text-white/70 hover:bg-white/5',
|
||||
'hover:bg-accent',
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
@@ -145,7 +146,7 @@ export function ConfigureContainerStep({
|
||||
tabIndex={-1}
|
||||
className="pointer-events-none"
|
||||
/>
|
||||
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-white/10 text-[10px] font-medium">
|
||||
<span className="bg-muted flex h-6 w-6 items-center justify-center rounded-full text-[10px] font-medium">
|
||||
{initials}
|
||||
</span>
|
||||
<span className="flex-1 truncate">
|
||||
@@ -162,7 +163,7 @@ export function ConfigureContainerStep({
|
||||
</div>
|
||||
|
||||
{/* Keyboard hints */}
|
||||
<div className="absolute bottom-8 left-0 right-0 flex items-center justify-center gap-4 text-sm text-white/50">
|
||||
<div className="text-muted-foreground absolute bottom-8 left-0 right-0 flex items-center justify-center gap-4 text-sm">
|
||||
<KeyHint keys="Esc" onClick={onCancel} title="Cancel (or press Esc)">
|
||||
cancel
|
||||
</KeyHint>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
/* Theme Milkdown Crepe to blend into the app's translucent "glass" surfaces.
|
||||
* Crepe's frame-dark theme is a grayscale palette driven by --crepe-* custom
|
||||
* properties; we override those to white-on-transparent with a blue accent so
|
||||
* the editor (and the read-only display, which uses the same engine) sits on
|
||||
/* Theme Milkdown Crepe to the app's design tokens so it follows light/dark.
|
||||
* Crepe's theme is a palette driven by --crepe-* custom properties; we map
|
||||
* those onto our CSS variables (globals.css) so the editor (and the read-only
|
||||
* display, which uses the same engine) inherits the active theme and sits on
|
||||
* top of the existing card instead of painting its own opaque background.
|
||||
*
|
||||
* The floating menus (slash menu, selection toolbar, link tooltip) are portaled
|
||||
* to <body>, OUTSIDE .milkdown — so the variables must be declared on those
|
||||
* selectors too, otherwise they fall back to transparent and the menu is
|
||||
* unreadable over the content behind it. */
|
||||
* unreadable over the content behind it. Our tokens resolve against the
|
||||
* :root/.dark cascade on <html>, so the portaled menus stay themed too. */
|
||||
.llink-crepe .milkdown,
|
||||
.milkdown-slash-menu,
|
||||
.milkdown-toolbar,
|
||||
@@ -15,24 +16,30 @@
|
||||
.milkdown-link-preview,
|
||||
.milkdown-block-handle {
|
||||
--crepe-color-background: transparent;
|
||||
--crepe-color-on-background: rgb(255 255 255 / 0.92);
|
||||
/* Surface backs code blocks, tables, and the floating menus — keep it
|
||||
* (near-)opaque so menus read clearly over content. */
|
||||
--crepe-color-surface: rgb(24 24 28 / 0.96);
|
||||
--crepe-color-surface-low: rgb(42 42 50 / 0.96);
|
||||
--crepe-color-on-surface: #ffffff;
|
||||
--crepe-color-on-surface-variant: rgb(255 255 255 / 0.65);
|
||||
--crepe-color-outline: rgb(255 255 255 / 0.2);
|
||||
--crepe-color-primary: #60a5fa;
|
||||
--crepe-color-secondary: rgb(96 165 250 / 0.25);
|
||||
--crepe-color-on-secondary: #ffffff;
|
||||
--crepe-color-inverse: #ffffff;
|
||||
--crepe-color-on-inverse: #0b0b0b;
|
||||
--crepe-color-inline-code: #fca5a5;
|
||||
--crepe-color-inline-area: rgb(255 255 255 / 0.1);
|
||||
--crepe-color-error: #f87171;
|
||||
--crepe-color-hover: rgb(255 255 255 / 0.08);
|
||||
--crepe-color-selected: rgb(96 165 250 / 0.25);
|
||||
--crepe-color-on-background: var(--card-foreground);
|
||||
/* Surface backs code blocks, tables, and the floating menus — use an opaque
|
||||
* themed surface so menus read clearly over content. */
|
||||
--crepe-color-surface: var(--popover);
|
||||
--crepe-color-surface-low: var(--muted);
|
||||
--crepe-color-on-surface: var(--popover-foreground);
|
||||
--crepe-color-on-surface-variant: var(--muted-foreground);
|
||||
--crepe-color-outline: var(--border);
|
||||
--crepe-color-primary: var(--primary);
|
||||
--crepe-color-secondary: color-mix(in oklch, var(--primary) 22%, transparent);
|
||||
--crepe-color-on-secondary: var(--foreground);
|
||||
--crepe-color-inverse: var(--foreground);
|
||||
--crepe-color-on-inverse: var(--background);
|
||||
--crepe-color-inline-code: var(--primary);
|
||||
--crepe-color-inline-area: var(--muted);
|
||||
--crepe-color-error: var(--destructive);
|
||||
--crepe-color-hover: var(--accent);
|
||||
--crepe-color-selected: color-mix(in oklch, var(--primary) 22%, transparent);
|
||||
/* frame-dark ships white "glow" shadows that vanish on light surfaces;
|
||||
* use a conventional dark drop shadow so floating menus lift in both themes. */
|
||||
--crepe-shadow-1:
|
||||
0px 1px 2px 0px rgb(0 0 0 / 0.12), 0px 1px 3px 1px rgb(0 0 0 / 0.08);
|
||||
--crepe-shadow-2:
|
||||
0px 1px 2px 0px rgb(0 0 0 / 0.16), 0px 2px 6px 2px rgb(0 0 0 / 0.12);
|
||||
/* Use the application font, not Crepe's bundled Noto Sans / Noto Serif. */
|
||||
--crepe-font-default: inherit;
|
||||
--crepe-font-title: inherit;
|
||||
@@ -57,7 +64,7 @@
|
||||
.llink-crepe .milkdown .ProseMirror {
|
||||
padding: 0;
|
||||
outline: none;
|
||||
caret-color: white;
|
||||
caret-color: var(--foreground);
|
||||
}
|
||||
|
||||
/* Crepe's default heading scale (42px h1) is sized for a document editor;
|
||||
@@ -103,7 +110,7 @@
|
||||
line-height: 1.375rem;
|
||||
font-weight: 700;
|
||||
margin-top: 12px;
|
||||
color: rgb(255 255 255 / 0.7);
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.llink-crepe .milkdown .ProseMirror > :first-child {
|
||||
|
||||
@@ -178,7 +178,7 @@ export function RecordingOverlay({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute inset-0 z-50 flex flex-col items-center justify-center bg-black/90',
|
||||
'bg-scrim/90 absolute inset-0 z-50 flex flex-col items-center justify-center',
|
||||
isReviewing && isDragging && 'ring-2 ring-inset ring-white/30',
|
||||
isRecording && isWarning && 'record-warning-glow',
|
||||
)}
|
||||
@@ -230,7 +230,7 @@ export function RecordingOverlay({
|
||||
|
||||
{/* Bottom gradient scrim for keyboard hint readability */}
|
||||
{(isRecording || isReviewing) && !isLoading && (
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 z-[5] h-1/4 bg-gradient-to-t from-black/60 to-transparent" />
|
||||
<div className="from-scrim/60 pointer-events-none absolute inset-x-0 bottom-0 z-[5] h-1/4 bg-gradient-to-t to-transparent" />
|
||||
)}
|
||||
|
||||
{/* Top center: recording indicator */}
|
||||
|
||||
@@ -63,37 +63,38 @@ export function TaskComposeStep({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute inset-0 z-50 flex flex-col bg-black/90 pt-16"
|
||||
className="bg-background/95 absolute inset-0 z-50 flex flex-col pt-16 backdrop-blur-sm"
|
||||
onKeyDown={handleKeyDown}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<div className="mx-auto w-full max-w-sm space-y-4 px-6">
|
||||
<div>
|
||||
<Label className="mb-1 text-xs text-white/50">Task</Label>
|
||||
<Label className="text-muted-foreground mb-1 text-xs">Task</Label>
|
||||
<Input
|
||||
type="text"
|
||||
autoFocus
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="What needs to get done?"
|
||||
className="border-white/10 bg-white/5 text-white placeholder-white/30 focus-visible:border-white/30 focus-visible:ring-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="mb-1 text-xs text-white/50">Notes</Label>
|
||||
<Label className="text-muted-foreground mb-1 text-xs">Notes</Label>
|
||||
<Textarea
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
placeholder="Optional details…"
|
||||
className="min-h-20 border-white/10 bg-white/5 text-white placeholder:text-white/30 focus-visible:border-white/30 focus-visible:ring-0 dark:bg-white/5"
|
||||
className="min-h-20"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="mb-1 text-xs text-white/50">Assign to</Label>
|
||||
<Label className="text-muted-foreground mb-1 text-xs">
|
||||
Assign to
|
||||
</Label>
|
||||
<Select value={assignedTo} onValueChange={setAssignedTo}>
|
||||
<SelectTrigger className="w-full border-white/10 bg-white/5 text-white">
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Unassigned" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -108,7 +109,7 @@ export function TaskComposeStep({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-8 left-0 right-0 flex items-center justify-center gap-4 text-sm text-white/50">
|
||||
<div className="text-muted-foreground absolute bottom-8 left-0 right-0 flex items-center justify-center gap-4 text-sm">
|
||||
<KeyHint keys="Esc" onClick={onCancel} title="Cancel (or press Esc)">
|
||||
cancel
|
||||
</KeyHint>
|
||||
|
||||
@@ -112,7 +112,7 @@ export function TextEditor({
|
||||
);
|
||||
|
||||
const keyboardHints = (
|
||||
<div className="absolute bottom-4 flex items-center gap-4 text-sm text-white/50">
|
||||
<div className="text-muted-foreground absolute bottom-4 flex items-center gap-4 text-sm">
|
||||
<KeyHint keys="Esc" onClick={onCancel} title="Cancel (or press Esc)">
|
||||
cancel
|
||||
</KeyHint>
|
||||
@@ -161,8 +161,8 @@ export function TextEditor({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute inset-0 z-50 flex items-center justify-center bg-black/90',
|
||||
isDragging && 'ring-2 ring-inset ring-white/30',
|
||||
'bg-background/95 absolute inset-0 z-50 flex items-center justify-center backdrop-blur-sm',
|
||||
isDragging && 'ring-primary/40 ring-2 ring-inset',
|
||||
)}
|
||||
{...dropZoneProps}
|
||||
>
|
||||
@@ -174,7 +174,7 @@ export function TextEditor({
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Type a message..."
|
||||
className={cn(
|
||||
'w-full resize-none border-none bg-transparent p-8 text-center text-white placeholder-white/40 outline-none',
|
||||
'text-foreground placeholder-muted-foreground w-full resize-none border-none bg-transparent p-8 text-center outline-none',
|
||||
style.size,
|
||||
style.weight,
|
||||
)}
|
||||
@@ -189,13 +189,13 @@ export function TextEditor({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute inset-0 z-50 flex items-center justify-center bg-black/90',
|
||||
isDragging && 'ring-2 ring-inset ring-white/30',
|
||||
'bg-background/95 absolute inset-0 z-50 flex items-center justify-center backdrop-blur-sm',
|
||||
isDragging && 'ring-primary/40 ring-2 ring-inset',
|
||||
)}
|
||||
{...dropZoneProps}
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
>
|
||||
<div className="mx-8 flex h-[calc(100%-8rem)] w-full min-w-0 max-w-[calc(var(--message-content-width)_+_var(--message-editor-gutter)*2)] flex-col overflow-hidden rounded border border-white/10 bg-white/5 backdrop-blur-xl animate-in fade-in zoom-in-95 duration-200">
|
||||
<div className="bg-card text-card-foreground border-border animate-in fade-in zoom-in-95 mx-8 flex h-[calc(100%-8rem)] w-full min-w-0 max-w-[calc(var(--message-content-width)_+_var(--message-editor-gutter)*2)] flex-col overflow-hidden rounded border duration-200">
|
||||
{/* No padding here: the editor's own scroll box hosts the slash menu,
|
||||
so we pad inside the editor (ProseMirror) instead. That keeps the
|
||||
menu's clipping bounds the full card rather than the inset box. */}
|
||||
@@ -208,7 +208,7 @@ export function TextEditor({
|
||||
</div>
|
||||
|
||||
{hasEnrichments && strip && (
|
||||
<div className="shrink-0 border-t border-white/10 px-5 py-3">
|
||||
<div className="border-border shrink-0 border-t px-5 py-3">
|
||||
{strip}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -230,7 +230,7 @@ function ContainerControls({
|
||||
}) {
|
||||
const requestIntent = useComposeIntentStore((s) => s.request);
|
||||
return (
|
||||
<div className="flex items-center gap-4 text-sm text-white/50">
|
||||
<div className="text-muted-foreground flex items-center gap-4 text-sm">
|
||||
<KeyHint keys={['↑↓', 'Enter']}>navigate</KeyHint>
|
||||
<KeyHint keys="1–9">jump</KeyHint>
|
||||
<VideoAudioToggle />
|
||||
|
||||
@@ -38,14 +38,16 @@ export function DeletedParticleView({
|
||||
}, [paused, onEnded, particle.id]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center bg-gradient-to-b from-black/40 via-black/20 to-black/40 px-8">
|
||||
<div className="flex h-full w-full items-center justify-center px-8">
|
||||
<div className="flex flex-col items-center gap-3 text-center">
|
||||
<Trash2 className="text-white/40 size-6" />
|
||||
<p className="text-white/70 text-base font-medium">
|
||||
<Trash2 className="text-muted-foreground size-6" />
|
||||
<p className="text-foreground text-base font-medium">
|
||||
This particle was deleted
|
||||
</p>
|
||||
{deleter && (
|
||||
<p className="text-white/40 text-xs">by {deleter.displayName}</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
by {deleter.displayName}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -141,7 +141,7 @@ export const MediaParticleView = forwardRef<
|
||||
|
||||
if (isAudio) {
|
||||
return (
|
||||
<div className="relative flex h-full w-full items-center justify-center bg-black/90">
|
||||
<div className="bg-scrim/90 relative flex h-full w-full items-center justify-center">
|
||||
<audio
|
||||
ref={(el) => {
|
||||
audioRef.current = el;
|
||||
@@ -186,7 +186,7 @@ export const MediaParticleView = forwardRef<
|
||||
playsInline
|
||||
onEnded={onEnded}
|
||||
onTimeUpdate={handleTimeUpdate}
|
||||
className="h-full w-full object-contain bg-black"
|
||||
className="bg-black h-full w-full object-contain"
|
||||
/>
|
||||
|
||||
{transcript && (
|
||||
|
||||
@@ -63,7 +63,7 @@ function ImageAttachment({
|
||||
);
|
||||
|
||||
if (isLoading || !url) {
|
||||
return <Skeleton className="h-20 w-20 shrink-0 rounded-lg bg-white/10" />;
|
||||
return <Skeleton className="h-20 w-20 shrink-0 rounded-lg" />;
|
||||
}
|
||||
|
||||
const handleDownload = (e: React.MouseEvent) => {
|
||||
@@ -78,7 +78,7 @@ function ImageAttachment({
|
||||
e.stopPropagation();
|
||||
onPreview();
|
||||
}}
|
||||
className="group relative block h-30 w-40 shrink-0 overflow-hidden rounded-lg bg-white/10"
|
||||
className="bg-muted group relative block h-30 w-40 shrink-0 overflow-hidden rounded-lg"
|
||||
>
|
||||
<img
|
||||
src={url}
|
||||
@@ -89,7 +89,7 @@ function ImageAttachment({
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={handleDownload}
|
||||
className="absolute bottom-1 right-1 rounded-full bg-black/60 p-1 text-white/70 opacity-0 transition-opacity hover:text-white group-hover:opacity-100"
|
||||
className="bg-scrim/60 absolute bottom-1 right-1 rounded-full p-1 text-white/80 opacity-0 transition-opacity hover:text-white group-hover:opacity-100"
|
||||
>
|
||||
<Download className="size-3.5" />
|
||||
</span>
|
||||
@@ -123,14 +123,14 @@ function FileAttachment({
|
||||
<div
|
||||
role="button"
|
||||
onClick={handleOpen}
|
||||
className="flex shrink-0 cursor-pointer flex-col gap-1.5 rounded-lg bg-white/10 px-3 py-2 transition-colors hover:bg-white/15"
|
||||
className="bg-muted hover:bg-accent flex shrink-0 cursor-pointer flex-col gap-1.5 rounded-lg px-3 py-2 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<FileIcon className="size-4 shrink-0 text-white/60" />
|
||||
<span className="max-w-[10rem] truncate text-xs font-medium text-white/90">
|
||||
<FileIcon className="text-muted-foreground size-4 shrink-0" />
|
||||
<span className="text-foreground max-w-[10rem] truncate text-xs font-medium">
|
||||
{particle.properties.filename}
|
||||
</span>
|
||||
<span className="text-[10px] text-white/40">
|
||||
<span className="text-muted-foreground text-[10px]">
|
||||
{formatFileSize(particle.properties.size_bytes)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -138,7 +138,7 @@ function FileAttachment({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="text-white/70 hover:bg-white/10 hover:text-white"
|
||||
className="text-muted-foreground"
|
||||
onClick={handleOpen}
|
||||
>
|
||||
<ExternalLink data-icon="inline-start" />
|
||||
@@ -147,7 +147,7 @@ function FileAttachment({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="text-white/70 hover:bg-white/10 hover:text-white"
|
||||
className="text-muted-foreground"
|
||||
onClick={handleDownload}
|
||||
>
|
||||
<Download data-icon="inline-start" />
|
||||
@@ -177,7 +177,7 @@ function CompactAttachmentItem({
|
||||
e.stopPropagation();
|
||||
openParticle(particle, index, url, onPreview);
|
||||
}}
|
||||
className="flex w-full items-center gap-2 rounded-md bg-white/10 px-2.5 py-1.5 text-left transition-colors hover:bg-white/15"
|
||||
className="bg-muted hover:bg-accent flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left transition-colors"
|
||||
>
|
||||
{isImage ? (
|
||||
url ? (
|
||||
@@ -187,15 +187,15 @@ function CompactAttachmentItem({
|
||||
className="size-5 shrink-0 rounded object-cover"
|
||||
/>
|
||||
) : (
|
||||
<ImageIcon className="size-4 shrink-0 text-white/50" />
|
||||
<ImageIcon className="text-muted-foreground size-4 shrink-0" />
|
||||
)
|
||||
) : (
|
||||
<FileIcon className="size-4 shrink-0 text-white/50" />
|
||||
<FileIcon className="text-muted-foreground size-4 shrink-0" />
|
||||
)}
|
||||
<span className="min-w-0 truncate text-xs text-white/80">
|
||||
<span className="text-foreground min-w-0 truncate text-xs">
|
||||
{particle.properties.filename}
|
||||
</span>
|
||||
<span className="shrink-0 text-[10px] text-white/40">
|
||||
<span className="text-muted-foreground shrink-0 text-[10px]">
|
||||
{formatFileSize(particle.properties.size_bytes)}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -191,7 +191,7 @@ const StreamRow = memo(function StreamRow({
|
||||
)}
|
||||
>
|
||||
{shortcutKey && (
|
||||
<kbd className="flex size-5 shrink-0 items-center justify-center rounded bg-white/10 font-mono text-xs text-muted-foreground">
|
||||
<kbd className="bg-muted text-muted-foreground flex size-5 shrink-0 items-center justify-center rounded font-mono text-xs">
|
||||
{shortcutKey}
|
||||
</kbd>
|
||||
)}
|
||||
@@ -220,8 +220,8 @@ const StreamRow = memo(function StreamRow({
|
||||
<div className="flex shrink-0 items-center gap-1.5">
|
||||
{hasActiveHuddle && (
|
||||
<span className="flex items-center gap-1 rounded-full bg-red-500/15 px-1.5 py-0.5">
|
||||
<Headphones className="size-3 text-red-400" />
|
||||
<span className="text-[10px] font-medium text-red-400">
|
||||
<Headphones className="size-3 text-red-600 dark:text-red-400" />
|
||||
<span className="text-[10px] font-medium text-red-600 dark:text-red-400">
|
||||
{huddleCount}
|
||||
</span>
|
||||
</span>
|
||||
@@ -298,7 +298,7 @@ const FolderRow = memo(function FolderRow({
|
||||
)}
|
||||
>
|
||||
{shortcutKey && (
|
||||
<kbd className="flex size-5 shrink-0 items-center justify-center rounded bg-white/10 font-mono text-xs text-muted-foreground">
|
||||
<kbd className="bg-muted text-muted-foreground flex size-5 shrink-0 items-center justify-center rounded font-mono text-xs">
|
||||
{shortcutKey}
|
||||
</kbd>
|
||||
)}
|
||||
@@ -356,7 +356,7 @@ const LeafRow = memo(function LeafRow({
|
||||
)}
|
||||
>
|
||||
{shortcutKey && (
|
||||
<kbd className="flex size-5 shrink-0 items-center justify-center rounded bg-white/10 font-mono text-xs text-muted-foreground">
|
||||
<kbd className="bg-muted text-muted-foreground flex size-5 shrink-0 items-center justify-center rounded font-mono text-xs">
|
||||
{shortcutKey}
|
||||
</kbd>
|
||||
)}
|
||||
|
||||
@@ -69,9 +69,9 @@ function MediaPreview({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center gap-2 bg-black/90">
|
||||
<Mic className="h-8 w-8 text-white/60" />
|
||||
<span className="font-mono text-xs text-white/50">{durationLabel}</span>
|
||||
<div className="bg-muted text-muted-foreground flex h-full w-full flex-col items-center justify-center gap-2">
|
||||
<Mic className="h-8 w-8" />
|
||||
<span className="font-mono text-xs">{durationLabel}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -103,8 +103,8 @@ function VideoThumbnail({
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center bg-black/80">
|
||||
<Video className="h-8 w-8 text-white/40" />
|
||||
<div className="bg-muted flex h-full w-full items-center justify-center">
|
||||
<Video className="text-muted-foreground h-8 w-8" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -114,7 +114,7 @@ function VideoThumbnail({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full bg-black">
|
||||
<div className="bg-muted relative h-full w-full">
|
||||
<video
|
||||
src={`${url}#t=2`}
|
||||
preload="metadata"
|
||||
@@ -122,7 +122,7 @@ function VideoThumbnail({
|
||||
playsInline
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
<span className="absolute right-1.5 bottom-1.5 rounded bg-black/70 px-1.5 py-0.5 font-mono text-[10px] text-white/80">
|
||||
<span className="bg-scrim/70 absolute bottom-1.5 right-1.5 rounded px-1.5 py-0.5 font-mono text-[10px] text-white">
|
||||
{duration}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -138,7 +138,7 @@ function LeafParticleView({
|
||||
})();
|
||||
|
||||
return (
|
||||
<div className="min-h-0 flex-1 bg-black text-white [--stream-safe-top:2rem] [--stream-safe-bottom:2rem]">
|
||||
<div className="bg-background min-h-0 flex-1 [--stream-safe-top:2rem] [--stream-safe-bottom:2rem]">
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -77,10 +77,10 @@ export function PlaybackPageIndicator({
|
||||
className="group relative block h-3 w-full"
|
||||
>
|
||||
{/* Dim track */}
|
||||
<div className="absolute inset-x-0 bottom-0 h-[3px] bg-white/30 transition-all group-hover:h-1.5" />
|
||||
<div className="bg-muted-foreground/30 absolute inset-x-0 bottom-0 h-[3px] transition-all group-hover:h-1.5" />
|
||||
{/* Fill */}
|
||||
<div
|
||||
className="absolute left-0 bottom-0 h-[3px] bg-white/90 transition-all group-hover:h-1.5"
|
||||
className="bg-foreground absolute bottom-0 left-0 h-[3px] transition-all group-hover:h-1.5"
|
||||
style={{
|
||||
width:
|
||||
i < current
|
||||
@@ -107,7 +107,7 @@ export function PlaybackPageIndicator({
|
||||
)}
|
||||
</div>
|
||||
{paginated && showTracks && current >= 0 && (
|
||||
<div className="pointer-events-none pt-1 text-center text-[10px] font-medium tabular-nums tracking-wide text-white/40">
|
||||
<div className="text-muted-foreground pointer-events-none pt-1 text-center text-[10px] font-medium tabular-nums tracking-wide">
|
||||
{current + 1} / {total}
|
||||
</div>
|
||||
)}
|
||||
@@ -141,7 +141,7 @@ function GhostStub({
|
||||
className="group relative block h-3 w-2 shrink-0"
|
||||
aria-label="Jump to adjacent page"
|
||||
>
|
||||
<div className="absolute inset-x-0 bottom-0 h-[3px] bg-white/15 transition-all group-hover:h-1.5 group-hover:bg-white/40" />
|
||||
<div className="bg-muted-foreground/20 group-hover:bg-muted-foreground/40 absolute inset-x-0 bottom-0 h-[3px] transition-all group-hover:h-1.5" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -166,7 +166,7 @@ function SegmentPresenceAvatars({
|
||||
className={
|
||||
onlineHumanIds?.has(human.humanId)
|
||||
? 'ring-2 ring-green-500'
|
||||
: 'ring-1 ring-black/50'
|
||||
: 'ring-1 ring-background'
|
||||
}
|
||||
avatarObjectId={human.avatarObjectId}
|
||||
initials={human.emailPrefix.slice(0, 2).toUpperCase()}
|
||||
@@ -178,7 +178,9 @@ function SegmentPresenceAvatars({
|
||||
</Tooltip>
|
||||
))}
|
||||
{overflow > 0 && (
|
||||
<span className="text-[10px] text-white/70 pl-1">+{overflow}</span>
|
||||
<span className="text-muted-foreground pl-1 text-[10px]">
|
||||
+{overflow}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -77,12 +77,12 @@ export function ReactionBar({
|
||||
className={cn(
|
||||
'flex items-center gap-1 rounded-full px-2 py-0.5 text-xs backdrop-blur-sm transition-colors',
|
||||
isMine
|
||||
? 'bg-white/20 ring-1 ring-white/40'
|
||||
: 'bg-black/40 hover:bg-black/50',
|
||||
? 'bg-primary/15 text-foreground ring-1 ring-primary/40'
|
||||
: 'bg-card/70 text-foreground border border-border hover:bg-accent',
|
||||
)}
|
||||
>
|
||||
<span className="text-sm">{emoji}</span>
|
||||
<span className="text-white/80">{reactors.length}</span>
|
||||
<span className="text-muted-foreground">{reactors.length}</span>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left" className="text-xs">
|
||||
@@ -109,8 +109,8 @@ export function ReactionBar({
|
||||
className={cn(
|
||||
'flex max-w-[200px] items-center gap-1.5 rounded-full py-0.5 pl-0.5 pr-2.5 text-xs backdrop-blur-sm transition-colors',
|
||||
isMine
|
||||
? 'bg-white/20 ring-1 ring-white/40'
|
||||
: 'bg-black/40 hover:bg-black/50',
|
||||
? 'bg-primary/15 text-foreground ring-1 ring-primary/40'
|
||||
: 'bg-card/70 text-foreground border border-border hover:bg-accent',
|
||||
)}
|
||||
>
|
||||
<HumanAvatar
|
||||
@@ -118,11 +118,11 @@ export function ReactionBar({
|
||||
className="shrink-0"
|
||||
avatarObjectId={firstReactor.avatarObjectId}
|
||||
initials={firstReactor.initials}
|
||||
fallbackClassName="bg-white/15 text-[9px] font-medium text-white"
|
||||
fallbackClassName="bg-muted text-muted-foreground text-[9px] font-medium"
|
||||
/>
|
||||
<span className="truncate text-white/90">{text}</span>
|
||||
<span className="truncate text-foreground">{text}</span>
|
||||
{reactors.length > 1 && (
|
||||
<span className="shrink-0 text-white/60">
|
||||
<span className="text-muted-foreground shrink-0">
|
||||
{reactors.length}
|
||||
</span>
|
||||
)}
|
||||
@@ -154,7 +154,7 @@ export function ReactionBar({
|
||||
|
||||
{/* Picker / actions */}
|
||||
{expanded ? (
|
||||
<div className="flex flex-col items-center gap-0.5 rounded-full bg-black/40 px-0.5 py-1.5 backdrop-blur-sm">
|
||||
<div className="bg-card/80 border-border flex flex-col items-center gap-0.5 rounded-full border px-0.5 py-1.5 backdrop-blur-sm">
|
||||
{REACTION_EMOJIS.map((emoji) => {
|
||||
if (activeEmojis.includes(emoji)) return null;
|
||||
return (
|
||||
@@ -164,7 +164,7 @@ export function ReactionBar({
|
||||
e.stopPropagation();
|
||||
handleToggle(emoji);
|
||||
}}
|
||||
className="rounded-full px-0.5 py-1 text-sm transition-colors hover:bg-white/15"
|
||||
className="hover:bg-accent rounded-full px-0.5 py-1 text-sm transition-colors"
|
||||
>
|
||||
{emoji}
|
||||
</button>
|
||||
@@ -175,9 +175,9 @@ export function ReactionBar({
|
||||
e.stopPropagation();
|
||||
setExpanded(false);
|
||||
}}
|
||||
className="flex size-5 items-center justify-center rounded-full transition-colors hover:bg-white/15"
|
||||
className="hover:bg-accent flex size-5 items-center justify-center rounded-full transition-colors"
|
||||
>
|
||||
<X className="size-3 text-white/60" />
|
||||
<X className="text-muted-foreground size-3" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
@@ -189,14 +189,14 @@ export function ReactionBar({
|
||||
e.stopPropagation();
|
||||
onOpenTextReaction();
|
||||
}}
|
||||
className="flex size-6 items-center justify-center rounded-full bg-black/40 backdrop-blur-sm transition-colors hover:bg-black/50"
|
||||
className="bg-card/70 border-border hover:bg-accent flex size-6 items-center justify-center rounded-full border backdrop-blur-sm transition-colors"
|
||||
>
|
||||
<Type className="size-3 text-white/60" />
|
||||
<Type className="text-muted-foreground size-3" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left" className="text-xs">
|
||||
Quick reply{' '}
|
||||
<kbd className="ml-1 rounded bg-white/10 px-1 font-mono text-[10px]">
|
||||
<kbd className="bg-muted ml-1 rounded px-1 font-mono text-[10px]">
|
||||
R
|
||||
</kbd>
|
||||
</TooltipContent>
|
||||
@@ -206,9 +206,9 @@ export function ReactionBar({
|
||||
e.stopPropagation();
|
||||
setExpanded(true);
|
||||
}}
|
||||
className="flex size-6 items-center justify-center rounded-full bg-black/40 backdrop-blur-sm transition-colors hover:bg-black/50"
|
||||
className="bg-card/70 border-border hover:bg-accent flex size-6 items-center justify-center rounded-full border backdrop-blur-sm transition-colors"
|
||||
>
|
||||
<Plus className="size-3 text-white/60" />
|
||||
<Plus className="text-muted-foreground size-3" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -56,17 +56,17 @@ export function RenameStreamOverlay({
|
||||
return createPortal(
|
||||
<div className="fixed inset-0 z-[100]">
|
||||
<div
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
||||
className="absolute inset-0 bg-scrim/60 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
/>
|
||||
<div className="absolute left-1/2 top-1/2 w-full max-w-sm -translate-x-1/2 -translate-y-1/2 rounded-2xl border border-white/10 bg-white/5 p-6 shadow-2xl backdrop-blur-xl">
|
||||
<div className="bg-popover/95 text-popover-foreground border-border absolute left-1/2 top-1/2 w-full max-w-sm -translate-x-1/2 -translate-y-1/2 rounded-2xl border p-6 shadow-2xl backdrop-blur-xl">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2 className="text-sm font-semibold text-white/70">Rename stream</h2>
|
||||
<h2 className="text-sm font-semibold">Rename stream</h2>
|
||||
<KeyHint
|
||||
keys="Esc"
|
||||
onClick={onClose}
|
||||
title="Close (or press Esc)"
|
||||
className="text-xs text-white/30"
|
||||
className="text-muted-foreground text-xs"
|
||||
>
|
||||
to close
|
||||
</KeyHint>
|
||||
@@ -85,7 +85,6 @@ export function RenameStreamOverlay({
|
||||
}
|
||||
}}
|
||||
placeholder="Stream name"
|
||||
className="border-white/10 bg-white/5 text-white placeholder-white/30 focus-visible:border-white/30 focus-visible:ring-0"
|
||||
/>
|
||||
|
||||
<div className="mt-4 flex items-center justify-end gap-2">
|
||||
|
||||
@@ -61,7 +61,7 @@ export function BottomBar({
|
||||
<div className="flex items-center justify-center px-3 pt-2 gap-2">
|
||||
{exitRemainingMs !== null && (
|
||||
<div className="flex justify-center">
|
||||
<span className="rounded-full bg-black/30 px-2 text-xs text-white/70 backdrop-blur-sm">
|
||||
<span className="bg-card/70 text-muted-foreground border-border rounded-full border px-2 text-xs backdrop-blur-sm">
|
||||
Closing in {Math.ceil(exitRemainingMs / 1000)}s
|
||||
</span>
|
||||
</div>
|
||||
@@ -91,7 +91,7 @@ export function StreamViewControls({
|
||||
}) {
|
||||
const requestIntent = useComposeIntentStore((s) => s.request);
|
||||
return (
|
||||
<div className="flex items-center gap-4 text-sm text-white/50">
|
||||
<div className="text-muted-foreground flex items-center gap-4 text-sm">
|
||||
{showEscape && (
|
||||
<KeyHint
|
||||
keys="Esc"
|
||||
|
||||
@@ -39,17 +39,17 @@ export function StreamListSidebar({
|
||||
}, [currentIndex]);
|
||||
|
||||
return (
|
||||
<aside className="dark flex w-60 shrink-0 flex-col overflow-hidden border-l border-white/10 bg-zinc-950">
|
||||
<div className="flex shrink-0 items-center gap-2 border-b border-white/10 px-4 py-3">
|
||||
<List className="size-3.5 text-white/40" />
|
||||
<span className="truncate text-sm font-medium text-white/90 mr-auto">
|
||||
<aside className="bg-sidebar text-sidebar-foreground border-border flex w-60 shrink-0 flex-col overflow-hidden border-l">
|
||||
<div className="border-border flex shrink-0 items-center gap-2 border-b px-4 py-3">
|
||||
<List className="text-muted-foreground size-3.5" />
|
||||
<span className="mr-auto truncate text-sm font-medium">
|
||||
{items.length} messages
|
||||
</span>
|
||||
<KeyHint
|
||||
keys="L"
|
||||
onClick={onToggle}
|
||||
title="Hide list (or press L)"
|
||||
className="text-xs text-white/40"
|
||||
className="text-muted-foreground text-xs"
|
||||
>
|
||||
to close
|
||||
</KeyHint>
|
||||
@@ -72,7 +72,7 @@ export function StreamListSidebar({
|
||||
</div>
|
||||
))}
|
||||
{items.length === 0 && (
|
||||
<p className="px-2 py-8 text-center text-sm text-white/40">
|
||||
<p className="text-muted-foreground px-2 py-8 text-center text-sm">
|
||||
No particles in this stream yet.
|
||||
</p>
|
||||
)}
|
||||
@@ -106,7 +106,7 @@ function ChatRow({
|
||||
}}
|
||||
className={cn(
|
||||
'flex cursor-pointer items-start gap-2.5 rounded-md px-2 py-2 text-left transition-colors',
|
||||
isSelected ? 'bg-white/10' : 'hover:bg-white/5',
|
||||
isSelected ? 'bg-accent' : 'hover:bg-accent/50',
|
||||
)}
|
||||
>
|
||||
<HumanAvatar
|
||||
@@ -114,14 +114,14 @@ function ChatRow({
|
||||
className="mt-0.5 shrink-0"
|
||||
initials={sender.initials}
|
||||
avatarObjectId={sender.avatarObjectId}
|
||||
fallbackClassName="bg-white/10 text-white/80 text-[10px] font-medium"
|
||||
fallbackClassName="bg-muted text-muted-foreground text-[10px] font-medium"
|
||||
/>
|
||||
<div className="min-w-0 flex-1 overflow-hidden">
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
<span className="truncate text-xs font-semibold text-white/90">
|
||||
<span className="truncate text-xs font-semibold">
|
||||
{sender.displayName}
|
||||
</span>
|
||||
<span className="shrink-0 text-[10px] text-white/35">
|
||||
<span className="text-muted-foreground shrink-0 text-[10px]">
|
||||
<RelativeTimestamp date={particle.created_at} />
|
||||
</span>
|
||||
</div>
|
||||
@@ -134,14 +134,16 @@ function ChatRow({
|
||||
function ChatRowContent({ particle }: { particle: Particle }) {
|
||||
if (isParticleDeleted(particle)) {
|
||||
return (
|
||||
<p className="text-xs italic text-white/35">This particle was deleted</p>
|
||||
<p className="text-muted-foreground text-xs italic">
|
||||
This particle was deleted
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
switch (particle.type) {
|
||||
case 'text':
|
||||
return (
|
||||
<p className="line-clamp-2 text-xs leading-relaxed [overflow-wrap:anywhere] whitespace-pre-line text-white/70">
|
||||
<p className="text-muted-foreground line-clamp-2 whitespace-pre-line text-xs leading-relaxed [overflow-wrap:anywhere]">
|
||||
{particle.properties.content}
|
||||
</p>
|
||||
);
|
||||
@@ -166,13 +168,13 @@ function ChatRowContent({ particle }: { particle: Particle }) {
|
||||
: '';
|
||||
return (
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="flex items-center gap-1.5 text-xs text-white/70">
|
||||
<Icon className="size-3.5 shrink-0 text-white/50" />
|
||||
<span className="text-muted-foreground flex items-center gap-1.5 text-xs">
|
||||
<Icon className="text-muted-foreground size-3.5 shrink-0" />
|
||||
{label}
|
||||
{duration}
|
||||
</span>
|
||||
{transcript && (
|
||||
<p className="line-clamp-2 text-xs leading-relaxed text-white/45">
|
||||
<p className="text-muted-foreground line-clamp-2 text-xs leading-relaxed">
|
||||
{transcript}
|
||||
</p>
|
||||
)}
|
||||
@@ -181,8 +183,8 @@ function ChatRowContent({ particle }: { particle: Particle }) {
|
||||
}
|
||||
case 'file':
|
||||
return (
|
||||
<span className="flex items-center gap-1.5 text-xs text-white/70">
|
||||
<FileText className="size-3.5 shrink-0 text-white/50" />
|
||||
<span className="text-muted-foreground flex items-center gap-1.5 text-xs">
|
||||
<FileText className="text-muted-foreground size-3.5 shrink-0" />
|
||||
<span className="truncate">{particle.properties.filename}</span>
|
||||
</span>
|
||||
);
|
||||
@@ -191,21 +193,26 @@ function ChatRowContent({ particle }: { particle: Particle }) {
|
||||
const doneCount = checklist.filter((item) => item.done).length;
|
||||
return (
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="flex items-center gap-1.5 text-xs text-white/70">
|
||||
<span className="text-muted-foreground flex items-center gap-1.5 text-xs">
|
||||
<CircleCheck
|
||||
className={cn(
|
||||
'size-3.5 shrink-0',
|
||||
done ? 'text-emerald-400' : 'text-white/50',
|
||||
done
|
||||
? 'text-emerald-600 dark:text-emerald-400'
|
||||
: 'text-muted-foreground',
|
||||
)}
|
||||
/>
|
||||
<span
|
||||
className={cn('truncate', done && 'text-white/40 line-through')}
|
||||
className={cn(
|
||||
'truncate',
|
||||
done && 'text-muted-foreground line-through',
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
</span>
|
||||
{checklist.length > 0 && (
|
||||
<span className="pl-5 text-[10px] text-white/40">
|
||||
<span className="text-muted-foreground pl-5 text-[10px]">
|
||||
{doneCount} / {checklist.length} subtasks
|
||||
</span>
|
||||
)}
|
||||
@@ -214,11 +221,11 @@ function ChatRowContent({ particle }: { particle: Particle }) {
|
||||
}
|
||||
case 'paper':
|
||||
return (
|
||||
<p className="truncate text-xs text-white/70">
|
||||
<p className="text-muted-foreground truncate text-xs">
|
||||
{particle.properties.title}
|
||||
</p>
|
||||
);
|
||||
default:
|
||||
return <p className="text-xs text-white/45">{particle.type}</p>;
|
||||
return <p className="text-muted-foreground text-xs">{particle.type}</p>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,18 +97,18 @@ export function StreamMembersOverlay({
|
||||
return createPortal(
|
||||
<div className="fixed inset-0 z-[100]">
|
||||
<div
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
||||
className="bg-scrim/60 absolute inset-0 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
/>
|
||||
<div className="absolute left-1/2 top-1/2 flex max-h-[80vh] w-full max-w-sm -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-2xl border border-white/10 bg-white/5 p-6 shadow-2xl backdrop-blur-xl">
|
||||
<div className="bg-popover/95 text-popover-foreground border-border absolute left-1/2 top-1/2 flex max-h-[80vh] w-full max-w-sm -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-2xl border p-6 shadow-2xl backdrop-blur-xl">
|
||||
{/* Header */}
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2 className="text-sm font-semibold text-white/70">Members</h2>
|
||||
<h2 className="text-sm font-semibold">Members</h2>
|
||||
<KeyHint
|
||||
keys="Esc"
|
||||
onClick={onClose}
|
||||
title="Close (or press Esc)"
|
||||
className="text-xs text-white/30"
|
||||
className="text-muted-foreground text-xs"
|
||||
>
|
||||
to close
|
||||
</KeyHint>
|
||||
@@ -116,11 +116,11 @@ export function StreamMembersOverlay({
|
||||
|
||||
{/* Visibility */}
|
||||
<section className="mb-4">
|
||||
<h3 className="mb-2 text-[10px] font-medium uppercase tracking-widest text-white/30">
|
||||
<h3 className="text-muted-foreground mb-2 text-[10px] font-medium uppercase tracking-widest">
|
||||
Visibility
|
||||
</h3>
|
||||
{isCreator ? (
|
||||
<div className="grid grid-cols-2 gap-1 rounded-lg bg-white/5 p-1">
|
||||
<div className="bg-muted grid grid-cols-2 gap-1 rounded-lg p-1">
|
||||
<VisibilityPill
|
||||
active={visibility.mode === 'network'}
|
||||
icon={<Globe className="size-3.5" />}
|
||||
@@ -135,15 +135,15 @@ export function StreamMembersOverlay({
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 text-sm text-white/70">
|
||||
<div className="text-foreground flex items-center gap-2 text-sm">
|
||||
{visibility.mode === 'network' ? (
|
||||
<>
|
||||
<Globe className="size-3.5 text-white/40" />
|
||||
<Globe className="text-muted-foreground size-3.5" />
|
||||
<span>Everyone in {network?.name ?? 'network'}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Lock className="size-3.5 text-white/40" />
|
||||
<Lock className="text-muted-foreground size-3.5" />
|
||||
<span>{memberIds.length} specific people</span>
|
||||
</>
|
||||
)}
|
||||
@@ -153,9 +153,11 @@ export function StreamMembersOverlay({
|
||||
|
||||
{/* Member list */}
|
||||
<section className="flex min-h-0 flex-1 flex-col">
|
||||
<h3 className="mb-2 text-[10px] font-medium uppercase tracking-widest text-white/30">
|
||||
<h3 className="text-muted-foreground mb-2 text-[10px] font-medium uppercase tracking-widest">
|
||||
{visibility.mode === 'network' ? 'Has access' : 'People'}{' '}
|
||||
<span className="ml-1 text-white/20">{memberIds.length}</span>
|
||||
<span className="text-muted-foreground ml-1">
|
||||
{memberIds.length}
|
||||
</span>
|
||||
</h3>
|
||||
<ScrollArea className="min-h-0 flex-1">
|
||||
<ul className="flex flex-col gap-0.5 pr-2">
|
||||
@@ -167,7 +169,7 @@ export function StreamMembersOverlay({
|
||||
return (
|
||||
<li
|
||||
key={id}
|
||||
className="group flex items-center gap-2.5 rounded px-2 py-1.5 text-sm text-white/70"
|
||||
className="text-foreground group flex items-center gap-2.5 rounded px-2 py-1.5 text-sm"
|
||||
>
|
||||
<HumanAvatar
|
||||
size="sm"
|
||||
@@ -178,13 +180,13 @@ export function StreamMembersOverlay({
|
||||
<span
|
||||
className={cn(
|
||||
'flex-1 truncate',
|
||||
!display.exists && 'italic text-white/40',
|
||||
!display.exists && 'text-muted-foreground italic',
|
||||
)}
|
||||
>
|
||||
{display.displayName}
|
||||
</span>
|
||||
{isCreatorRow && (
|
||||
<span className="text-[10px] uppercase tracking-wider text-white/30">
|
||||
<span className="text-muted-foreground text-[10px] uppercase tracking-wider">
|
||||
Creator
|
||||
</span>
|
||||
)}
|
||||
@@ -192,7 +194,7 @@ export function StreamMembersOverlay({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeMember(id)}
|
||||
className="rounded p-1 text-white/30 opacity-0 transition-opacity hover:bg-white/10 hover:text-white/70 group-hover:opacity-100"
|
||||
className="text-muted-foreground hover:bg-accent hover:text-foreground rounded p-1 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
aria-label={`Remove ${display.displayName}`}
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
@@ -209,8 +211,8 @@ export function StreamMembersOverlay({
|
||||
{isCreator &&
|
||||
visibility.mode === 'custom' &&
|
||||
availableToAdd.length > 0 && (
|
||||
<section className="mt-4 border-t border-white/5 pt-4">
|
||||
<h3 className="mb-2 flex items-center gap-1.5 text-[10px] font-medium uppercase tracking-widest text-white/30">
|
||||
<section className="border-border mt-4 border-t pt-4">
|
||||
<h3 className="text-muted-foreground mb-2 flex items-center gap-1.5 text-[10px] font-medium uppercase tracking-widest">
|
||||
<UserPlus className="size-3" />
|
||||
Add people
|
||||
</h3>
|
||||
@@ -222,7 +224,7 @@ export function StreamMembersOverlay({
|
||||
type="button"
|
||||
onClick={() => addMember(human.id)}
|
||||
className={cn(
|
||||
'flex w-full items-center gap-2.5 rounded px-2 py-1.5 text-left text-sm text-white/70 transition-colors hover:bg-white/5',
|
||||
'text-foreground hover:bg-accent flex w-full items-center gap-2.5 rounded px-2 py-1.5 text-left text-sm transition-colors',
|
||||
)}
|
||||
>
|
||||
<HumanAvatar
|
||||
@@ -234,7 +236,7 @@ export function StreamMembersOverlay({
|
||||
<span className="flex-1 truncate">
|
||||
{human.email_prefix}
|
||||
</span>
|
||||
<UserPlus className="size-3.5 text-white/30" />
|
||||
<UserPlus className="text-muted-foreground size-3.5" />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
@@ -246,7 +248,7 @@ export function StreamMembersOverlay({
|
||||
{isCreator &&
|
||||
visibility.mode === 'custom' &&
|
||||
availableToAdd.length === 0 && (
|
||||
<p className="mt-4 text-center text-xs text-white/30">
|
||||
<p className="text-muted-foreground mt-4 text-center text-xs">
|
||||
<Users className="mr-1 inline size-3" />
|
||||
Everyone in the network is already a member
|
||||
</p>
|
||||
@@ -275,8 +277,8 @@ function VisibilityPill({
|
||||
className={cn(
|
||||
'flex items-center justify-center gap-1.5 rounded-md px-2 py-1.5 text-xs transition-colors',
|
||||
active
|
||||
? 'bg-white/10 text-white/90'
|
||||
: 'text-white/50 hover:text-white/80',
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:text-foreground',
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
|
||||
@@ -92,7 +92,7 @@ export function TopBar({
|
||||
<div className="drag-region flex flex-row px-2 gap-1 items-center">
|
||||
<WindowControls />
|
||||
|
||||
<Breadcrumb className="no-drag rounded-full bg-black/30 backdrop-blur-sm px-3 py-1 mx-auto">
|
||||
<Breadcrumb className="no-drag bg-card/70 border-border mx-auto rounded-full border px-3 py-1 backdrop-blur-sm">
|
||||
<BreadcrumbList>
|
||||
{streamParticle && (
|
||||
<>
|
||||
@@ -139,7 +139,7 @@ export function TopBar({
|
||||
size="sm"
|
||||
avatarObjectId={display.avatarObjectId}
|
||||
initials={display.initials}
|
||||
fallbackClassName="bg-red-500/30 text-[8px] text-red-200"
|
||||
fallbackClassName="bg-red-500/30 text-[8px] text-red-700 dark:text-red-200"
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{display.email}</TooltipContent>
|
||||
@@ -147,12 +147,14 @@ export function TopBar({
|
||||
);
|
||||
})}
|
||||
</AvatarGroup>
|
||||
<span className="text-xs font-medium text-red-200">Join</span>
|
||||
<span className="text-xs font-medium text-red-600 dark:text-red-200">
|
||||
Join
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{!isStreamOpen(streamParticle) && (
|
||||
<span className="no-drag text-muted-foreground flex items-center gap-1 rounded-full bg-white/10 px-2 py-0.5 text-xs backdrop-blur-sm">
|
||||
<span className="no-drag text-muted-foreground bg-muted border-border flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs backdrop-blur-sm">
|
||||
<CircleCheckBig className="size-3" />
|
||||
Closed
|
||||
</span>
|
||||
@@ -280,16 +282,16 @@ function MembersIndicator({
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="no-drag flex items-center gap-1.5 rounded-full bg-white/5 px-2 py-1 text-xs text-white/70 backdrop-blur-sm transition-colors hover:bg-white/10"
|
||||
className="no-drag bg-card/70 text-muted-foreground border-border hover:bg-accent flex items-center gap-1.5 rounded-full border px-2 py-1 text-xs backdrop-blur-sm transition-colors"
|
||||
>
|
||||
{isNetworkWide ? (
|
||||
<>
|
||||
<Globe className="size-3 text-white/50" />
|
||||
<Globe className="text-muted-foreground size-3" />
|
||||
<span>Everyone</span>
|
||||
</>
|
||||
) : isPrivate ? (
|
||||
<>
|
||||
<Lock className="size-3 text-white/50" />
|
||||
<Lock className="text-muted-foreground size-3" />
|
||||
<span>Private</span>
|
||||
</>
|
||||
) : (
|
||||
@@ -306,7 +308,7 @@ function MembersIndicator({
|
||||
))}
|
||||
</AvatarGroup>
|
||||
{overflow > 0 && (
|
||||
<span className="text-white/50">+{overflow}</span>
|
||||
<span className="text-muted-foreground">+{overflow}</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -381,7 +381,7 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) {
|
||||
|
||||
if (children.length === 0) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-4 bg-black text-white">
|
||||
<div className="bg-background flex h-full flex-col items-center justify-center gap-4">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
No particles in this stream yet
|
||||
</p>
|
||||
@@ -460,15 +460,15 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen overflow-hidden bg-black">
|
||||
<div className="bg-background flex h-screen overflow-hidden">
|
||||
{/* Stream chrome — immersive playback column */}
|
||||
<div
|
||||
className="relative flex min-w-0 flex-1 flex-col overflow-hidden bg-black text-white [--stream-safe-top:4rem] [--stream-safe-bottom:5rem]"
|
||||
className="bg-background relative flex min-w-0 flex-1 flex-col overflow-hidden [--stream-safe-top:4rem] [--stream-safe-bottom:5rem]"
|
||||
onMouseMove={handleMouseActivity}
|
||||
onMouseLeave={() => setShowControls(false)}
|
||||
>
|
||||
{/* Top gradient safe zone */}
|
||||
<div className="pointer-events-none absolute inset-x-0 top-0 z-[5] h-32 bg-gradient-to-b from-black/60 to-transparent" />
|
||||
<div className="from-background/80 pointer-events-none absolute inset-x-0 top-0 z-[5] h-32 bg-gradient-to-b to-transparent" />
|
||||
|
||||
{/* TopBar — always visible */}
|
||||
<div className="z-10 absolute left-0 right-0 pt-2">
|
||||
@@ -488,7 +488,7 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) {
|
||||
|
||||
<div className="pointer-events-none absolute right-4 top-14 z-20 flex flex-col items-end gap-1.5">
|
||||
{fastPlayback && (
|
||||
<div className="rounded-full bg-black/50 px-2.5 py-1 text-xs font-medium text-white backdrop-blur-sm">
|
||||
<div className="bg-card/70 text-foreground border-border rounded-full border px-2.5 py-1 text-xs font-medium backdrop-blur-sm">
|
||||
1.5x
|
||||
</div>
|
||||
)}
|
||||
@@ -497,7 +497,7 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) {
|
||||
type="button"
|
||||
onClick={resume}
|
||||
title="Resume (or press Space)"
|
||||
className="pointer-events-auto flex items-center gap-1.5 rounded-full bg-black/40 px-2.5 py-1 text-xs font-medium text-white/70 backdrop-blur-sm transition-colors hover:bg-black/60 hover:text-white"
|
||||
className="bg-card/70 text-muted-foreground border-border hover:bg-accent hover:text-foreground pointer-events-auto flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium backdrop-blur-sm transition-colors"
|
||||
>
|
||||
<Play className="size-3 fill-current" />
|
||||
Paused
|
||||
@@ -541,7 +541,7 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) {
|
||||
/>
|
||||
|
||||
{/* Bottom gradient safe zone for keyboard hints */}
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 z-[5] h-1/4 bg-gradient-to-t from-black/60 to-transparent" />
|
||||
<div className="from-background/80 pointer-events-none absolute inset-x-0 bottom-0 z-[5] h-1/4 bg-gradient-to-t to-transparent" />
|
||||
|
||||
{/* BottomBar — pinned visible while browsing, mouse-activity in player */}
|
||||
<BottomBar
|
||||
|
||||
@@ -212,20 +212,20 @@ export function TaskParticleView({
|
||||
const doneCount = checklist.filter((item) => item.done).length;
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center bg-gradient-to-b from-black/40 via-black/20 to-black/40 px-8 pt-[var(--stream-safe-top,2rem)] pb-[var(--stream-safe-bottom,2rem)]">
|
||||
<div className="flex h-full w-full items-center justify-center px-8 pt-[var(--stream-safe-top,2rem)] pb-[var(--stream-safe-bottom,2rem)]">
|
||||
{immersive && editing && (
|
||||
<div
|
||||
className="animate-in fade-in-0 fixed inset-0 z-30 bg-black/50 backdrop-blur-md duration-200"
|
||||
className="animate-in fade-in-0 fixed inset-0 z-30 bg-scrim/50 backdrop-blur-md duration-200"
|
||||
onMouseDown={exitFocus}
|
||||
aria-hidden
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
'scrollbar-card flex max-h-full w-full max-w-[calc(var(--message-content-width)_+_var(--message-card-padding)*2)] flex-col gap-5 overflow-y-auto overscroll-contain rounded bg-white/10 p-[var(--message-card-padding)] backdrop-blur-md transition-shadow',
|
||||
'scrollbar-card bg-card text-card-foreground border-border flex max-h-full w-full max-w-[calc(var(--message-content-width)_+_var(--message-card-padding)*2)] flex-col gap-5 overflow-y-auto overscroll-contain rounded border p-[var(--message-card-padding)] transition-shadow',
|
||||
immersive &&
|
||||
editing &&
|
||||
'relative z-40 shadow-2xl shadow-black/50 ring-1 ring-white/15',
|
||||
'relative z-40 shadow-2xl shadow-black/50 ring-1 ring-border',
|
||||
)}
|
||||
onFocusCapture={() => setEditing(true)}
|
||||
onBlurCapture={(e) => {
|
||||
@@ -236,7 +236,7 @@ export function TaskParticleView({
|
||||
<Checkbox
|
||||
checked={done}
|
||||
onCheckedChange={(checked) => handleToggleDone(checked === true)}
|
||||
className="mt-1.5 size-5 rounded-full border-white/40 data-[state=checked]:border-emerald-500 data-[state=checked]:bg-emerald-500"
|
||||
className="mt-1.5 size-5 rounded-full border-muted-foreground/40 data-[state=checked]:border-emerald-500 data-[state=checked]:bg-emerald-500"
|
||||
aria-label={done ? 'Mark task as not done' : 'Mark task as done'}
|
||||
/>
|
||||
<input
|
||||
@@ -246,8 +246,8 @@ export function TaskParticleView({
|
||||
onBlur={titleField.onBlur}
|
||||
placeholder="Task title"
|
||||
className={cn(
|
||||
'w-full bg-transparent text-2xl font-semibold text-white outline-none placeholder:text-white/30',
|
||||
done && 'text-white/50 line-through',
|
||||
'text-foreground placeholder:text-muted-foreground w-full bg-transparent text-2xl font-semibold outline-none',
|
||||
done && 'text-muted-foreground line-through',
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -258,12 +258,12 @@ export function TaskParticleView({
|
||||
onFocus={notesField.onFocus}
|
||||
onBlur={notesField.onBlur}
|
||||
placeholder="Add notes…"
|
||||
className="min-h-16 resize-none border-none bg-transparent p-0 text-sm text-white/80 shadow-none placeholder:text-white/30 focus-visible:ring-0 dark:bg-transparent"
|
||||
className="text-foreground placeholder:text-muted-foreground min-h-16 resize-none border-none bg-transparent p-0 text-sm shadow-none focus-visible:ring-0 dark:bg-transparent"
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{checklist.length > 0 && (
|
||||
<span className="text-xs text-white/40">
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{doneCount} / {checklist.length} done
|
||||
</span>
|
||||
)}
|
||||
@@ -286,10 +286,7 @@ export function TaskParticleView({
|
||||
value={assigned_to ?? UNASSIGNED}
|
||||
onValueChange={handleAssign}
|
||||
>
|
||||
<SelectTrigger
|
||||
size="sm"
|
||||
className="w-fit gap-2 border-white/15 bg-white/5 text-white/80"
|
||||
>
|
||||
<SelectTrigger size="sm" className="w-fit gap-2">
|
||||
{assigned_to && assignee.exists && (
|
||||
<HumanAvatar
|
||||
size="sm"
|
||||
@@ -315,7 +312,7 @@ export function TaskParticleView({
|
||||
keys="Esc"
|
||||
onClick={exitFocus}
|
||||
title="Finish editing (or press Esc)"
|
||||
className="fixed bottom-[calc(var(--stream-safe-bottom,2rem)+0.5rem)] left-1/2 z-40 -translate-x-1/2 text-xs text-white/40"
|
||||
className="fixed bottom-[calc(var(--stream-safe-bottom,2rem)+0.5rem)] left-1/2 z-40 -translate-x-1/2 text-xs text-white/60"
|
||||
>
|
||||
to finish
|
||||
</KeyHint>
|
||||
@@ -345,7 +342,7 @@ function ChecklistItemRow({
|
||||
<Checkbox
|
||||
checked={item.done}
|
||||
onCheckedChange={(checked) => onToggle(checked === true)}
|
||||
className="border-white/30 data-[state=checked]:border-emerald-500 data-[state=checked]:bg-emerald-500"
|
||||
className="border-muted-foreground/40 data-[state=checked]:border-emerald-500 data-[state=checked]:bg-emerald-500"
|
||||
aria-label={
|
||||
item.done ? 'Mark subtask as not done' : 'Mark subtask as done'
|
||||
}
|
||||
@@ -357,14 +354,14 @@ function ChecklistItemRow({
|
||||
onBlur={textField.onBlur}
|
||||
placeholder="Subtask"
|
||||
className={cn(
|
||||
'w-full bg-transparent text-sm text-white/90 outline-none placeholder:text-white/30',
|
||||
item.done && 'text-white/40 line-through',
|
||||
'text-foreground placeholder:text-muted-foreground w-full bg-transparent text-sm outline-none',
|
||||
item.done && 'text-muted-foreground line-through',
|
||||
)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRemove}
|
||||
className="text-white/30 opacity-0 transition-opacity hover:text-white/70 group-hover/item:opacity-100"
|
||||
className="text-muted-foreground hover:text-foreground opacity-0 transition-opacity group-hover/item:opacity-100"
|
||||
aria-label="Remove subtask"
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
@@ -385,7 +382,7 @@ function AddChecklistItemRow({ onAdd }: { onAdd: (text: string) => void }) {
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Plus className="size-4 text-white/30" />
|
||||
<Plus className="text-muted-foreground size-4" />
|
||||
<input
|
||||
value={draft}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
@@ -397,7 +394,7 @@ function AddChecklistItemRow({ onAdd }: { onAdd: (text: string) => void }) {
|
||||
}}
|
||||
onBlur={submit}
|
||||
placeholder="Add subtask…"
|
||||
className="w-full bg-transparent text-sm text-white/70 outline-none placeholder:text-white/30"
|
||||
className="text-foreground placeholder:text-muted-foreground w-full bg-transparent text-sm outline-none"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -122,7 +122,7 @@ export function TextParticleView({
|
||||
setIsEditing(true);
|
||||
}}
|
||||
title="Edit"
|
||||
className="absolute bottom-[calc(var(--stream-safe-bottom,2rem)+0.5rem)] left-1/2 -translate-x-1/2 z-30 flex items-center gap-1.5 rounded-full bg-black/40 px-3 py-1.5 text-xs text-white/70 backdrop-blur-sm transition-colors hover:bg-black/60 hover:text-white"
|
||||
className="bg-card/70 text-muted-foreground border-border hover:bg-accent hover:text-foreground absolute bottom-[calc(var(--stream-safe-bottom,2rem)+0.5rem)] left-1/2 z-30 flex -translate-x-1/2 items-center gap-1.5 rounded-full border px-3 py-1.5 text-xs backdrop-blur-sm transition-colors"
|
||||
>
|
||||
<Pencil className="size-3.5" />
|
||||
Edit
|
||||
@@ -130,7 +130,7 @@ export function TextParticleView({
|
||||
);
|
||||
|
||||
const editedLabel = particle.properties.edited_at && (
|
||||
<span className="text-xs text-white/40">
|
||||
<span className="text-muted-foreground text-xs">
|
||||
edited <RelativeTimestamp date={particle.properties.edited_at} />
|
||||
</span>
|
||||
);
|
||||
@@ -146,7 +146,7 @@ export function TextParticleView({
|
||||
// Mode 1: bare URLs only — show link cards centered
|
||||
if (linksOnly && !hasAttachments) {
|
||||
return (
|
||||
<div className="group relative flex h-full w-full items-center justify-center bg-gradient-to-b from-black/40 via-black/20 to-black/40 px-8 pt-[var(--stream-safe-top,2rem)] pb-[var(--stream-safe-bottom,2rem)]">
|
||||
<div className="group relative flex h-full w-full items-center justify-center px-8 pt-[var(--stream-safe-top,2rem)] pb-[var(--stream-safe-bottom,2rem)]">
|
||||
<LinkPreviews entries={linkPreviews} />
|
||||
{editedLabel && (
|
||||
<div className="absolute bottom-6 left-1/2 -translate-x-1/2">
|
||||
@@ -167,10 +167,10 @@ export function TextParticleView({
|
||||
) {
|
||||
const style = getImmersiveTextStyle(content.length);
|
||||
return (
|
||||
<div className="group relative flex h-full w-full flex-col items-center justify-center gap-4 bg-gradient-to-b from-black/40 via-black/20 to-black/40 px-8 pt-[var(--stream-safe-top,2rem)] pb-[var(--stream-safe-bottom,2rem)]">
|
||||
<div className="group relative flex h-full w-full flex-col items-center justify-center gap-4 px-8 pt-[var(--stream-safe-top,2rem)] pb-[var(--stream-safe-bottom,2rem)]">
|
||||
<p
|
||||
className={cn(
|
||||
'max-w-2xl break-words text-center leading-relaxed text-white select-text cursor-text',
|
||||
'text-foreground max-w-2xl cursor-text select-text break-words text-center leading-relaxed',
|
||||
style.size,
|
||||
style.weight,
|
||||
)}
|
||||
@@ -186,8 +186,8 @@ export function TextParticleView({
|
||||
|
||||
// Mode 3: card layout
|
||||
return (
|
||||
<div className="group relative flex h-full w-full items-center justify-center bg-gradient-to-b from-black/40 via-black/20 to-black/40 px-8 pt-[var(--stream-safe-top,2rem)] pb-[var(--stream-safe-bottom,2rem)]">
|
||||
<div className="scrollbar-card flex max-h-full w-full max-w-[calc(var(--message-content-width)_+_var(--message-card-padding)*2)] flex-col gap-4 overflow-y-auto overscroll-contain rounded bg-white/10 p-[var(--message-card-padding)] backdrop-blur-md">
|
||||
<div className="group relative flex h-full w-full items-center justify-center px-8 pt-[var(--stream-safe-top,2rem)] pb-[var(--stream-safe-bottom,2rem)]">
|
||||
<div className="scrollbar-card bg-card text-card-foreground border-border flex max-h-full w-full max-w-[calc(var(--message-content-width)_+_var(--message-card-padding)*2)] flex-col gap-4 overflow-y-auto overscroll-contain rounded border p-[var(--message-card-padding)]">
|
||||
<MarkdownEditor
|
||||
key={content}
|
||||
value={content}
|
||||
|
||||
@@ -54,7 +54,7 @@ export function TextReactionInput({
|
||||
return (
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="flex items-center gap-1 rounded-full bg-black/60 py-1 pl-3 pr-1 shadow-lg ring-1 ring-white/15 backdrop-blur-md"
|
||||
className="bg-popover/90 text-popover-foreground border-border flex items-center gap-1 rounded-full border py-1 pl-3 pr-1 shadow-lg backdrop-blur-md"
|
||||
>
|
||||
<input
|
||||
ref={inputRef}
|
||||
@@ -74,12 +74,14 @@ export function TextReactionInput({
|
||||
}}
|
||||
placeholder="Quick reply…"
|
||||
maxLength={MAX_LENGTH}
|
||||
className="w-24 bg-transparent text-sm text-white outline-none placeholder:text-white/40"
|
||||
className="text-foreground placeholder:text-muted-foreground w-24 bg-transparent text-sm outline-none"
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
'min-w-[1.5ch] text-right text-[10px] tabular-nums',
|
||||
remaining <= 8 ? 'text-amber-300/80' : 'text-white/30',
|
||||
remaining <= 8
|
||||
? 'text-amber-600 dark:text-amber-400'
|
||||
: 'text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
{remaining}
|
||||
@@ -91,8 +93,8 @@ export function TextReactionInput({
|
||||
className={cn(
|
||||
'ml-1 flex size-6 items-center justify-center rounded-full transition-colors',
|
||||
canSubmit
|
||||
? 'bg-white/20 text-white hover:bg-white/30'
|
||||
: 'text-white/30',
|
||||
? 'bg-primary text-primary-foreground hover:bg-primary/90'
|
||||
: 'text-muted-foreground',
|
||||
)}
|
||||
aria-label="Send reaction"
|
||||
>
|
||||
|
||||
@@ -12,11 +12,16 @@ import {
|
||||
Volume2,
|
||||
ArrowLeft,
|
||||
Camera,
|
||||
Sun,
|
||||
Moon,
|
||||
Monitor,
|
||||
Palette,
|
||||
} from 'lucide-react';
|
||||
import { HumanAvatar } from '@/components/human-avatar';
|
||||
import { AvatarEditDialog } from '@/features/settings/avatar-edit-dialog';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
||||
import { WindowControls } from '@/components/window-controls';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Muted } from '@/components/ui/typography';
|
||||
@@ -24,6 +29,7 @@ import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { CopyableEmail } from '@/components/copyable-email';
|
||||
import { useAuthStore } from '@/stores/auth-store';
|
||||
import { useSoundEffectsStore } from '@/stores/sound-effects-store';
|
||||
import { useThemeStore, type ThemeMode } from '@/stores/theme-store';
|
||||
import { apiClient } from '@/api/client';
|
||||
import { logError, toUserMessage } from '@/lib/errors';
|
||||
import { toast } from 'sonner';
|
||||
@@ -80,6 +86,50 @@ function SettingsGroup({
|
||||
);
|
||||
}
|
||||
|
||||
const THEME_OPTIONS: {
|
||||
value: ThemeMode;
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
}[] = [
|
||||
{ value: 'system', label: 'System', icon: <Monitor className="size-3.5" /> },
|
||||
{ value: 'light', label: 'Light', icon: <Sun className="size-3.5" /> },
|
||||
{ value: 'dark', label: 'Dark', icon: <Moon className="size-3.5" /> },
|
||||
];
|
||||
|
||||
function ThemeSetting() {
|
||||
const mode = useThemeStore((s) => s.mode);
|
||||
const setMode = useThemeStore((s) => s.setMode);
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center gap-3 px-4 py-3">
|
||||
<span className="text-muted-foreground flex size-5 items-center justify-center">
|
||||
<Palette className="size-4" />
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 text-sm font-medium">Theme</span>
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
size="sm"
|
||||
value={mode}
|
||||
onValueChange={(value) => value && setMode(value as ThemeMode)}
|
||||
className="border-border bg-muted/50 border p-0.5"
|
||||
spacing={2}
|
||||
>
|
||||
{THEME_OPTIONS.map((option) => (
|
||||
<ToggleGroupItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
aria-label={option.label}
|
||||
className="data-[state=on]:bg-background data-[state=on]:text-foreground data-[state=on]:shadow-sm gap-1.5 rounded-md px-2.5 text-xs"
|
||||
>
|
||||
{option.icon}
|
||||
{option.label}
|
||||
</ToggleGroupItem>
|
||||
))}
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SettingsPage() {
|
||||
const navigate = useNavigate();
|
||||
const user = useAuthStore((s) => s.user);
|
||||
@@ -150,7 +200,7 @@ export default function SettingsPage() {
|
||||
initials={initials}
|
||||
fallbackClassName="bg-primary/10 text-primary text-xl font-medium"
|
||||
/>
|
||||
<span className="absolute inset-0 flex items-center justify-center rounded-full bg-black/40 opacity-0 transition-opacity group-hover:opacity-100" />
|
||||
<span className="bg-scrim/40 absolute inset-0 flex items-center justify-center rounded-full opacity-0 transition-opacity group-hover:opacity-100" />
|
||||
<span className="bg-primary text-primary-foreground ring-background absolute bottom-0 right-0 flex size-5 items-center justify-center rounded-full ring-2">
|
||||
<Camera className="size-2.5" />
|
||||
</span>
|
||||
@@ -165,6 +215,12 @@ export default function SettingsPage() {
|
||||
|
||||
<Separator />
|
||||
|
||||
<SettingsGroup title="Appearance">
|
||||
<ThemeSetting />
|
||||
</SettingsGroup>
|
||||
|
||||
<Separator className="mt-4" />
|
||||
|
||||
<SettingsGroup title="Notifications">
|
||||
<div className="flex w-full items-center gap-3 px-4 py-3">
|
||||
<span className="text-muted-foreground flex size-5 items-center justify-center">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App';
|
||||
import { initSentryRenderer } from '@/lib/sentry';
|
||||
import '@/stores/theme-store';
|
||||
import './styles/globals.css';
|
||||
|
||||
initSentryRenderer();
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
export type ThemeMode = 'light' | 'dark' | 'system';
|
||||
|
||||
const STORAGE_KEY = 'llink:theme';
|
||||
const DEFAULT_MODE: ThemeMode = 'dark';
|
||||
|
||||
function loadMode(): ThemeMode {
|
||||
try {
|
||||
const v = localStorage.getItem(STORAGE_KEY);
|
||||
if (v === 'light' || v === 'dark' || v === 'system') return v;
|
||||
} catch {
|
||||
// storage unavailable
|
||||
}
|
||||
return DEFAULT_MODE;
|
||||
}
|
||||
|
||||
function systemPrefersDark(): boolean {
|
||||
return window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? true;
|
||||
}
|
||||
|
||||
function resolveDark(mode: ThemeMode): boolean {
|
||||
return mode === 'system' ? systemPrefersDark() : mode === 'dark';
|
||||
}
|
||||
|
||||
function applyMode(mode: ThemeMode) {
|
||||
document.documentElement.classList.toggle('dark', resolveDark(mode));
|
||||
}
|
||||
|
||||
interface ThemeState {
|
||||
mode: ThemeMode;
|
||||
isDark: boolean;
|
||||
setMode: (mode: ThemeMode) => void;
|
||||
}
|
||||
|
||||
export const useThemeStore = create<ThemeState>((set, get) => {
|
||||
const initialMode = loadMode();
|
||||
applyMode(initialMode);
|
||||
|
||||
// Track the OS preference so "system" mode stays in sync live.
|
||||
window
|
||||
.matchMedia?.('(prefers-color-scheme: dark)')
|
||||
.addEventListener('change', () => {
|
||||
if (get().mode === 'system') {
|
||||
applyMode('system');
|
||||
set({ isDark: systemPrefersDark() });
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
mode: initialMode,
|
||||
isDark: resolveDark(initialMode),
|
||||
setMode: (mode) => {
|
||||
applyMode(mode);
|
||||
set({ mode, isDark: resolveDark(mode) });
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, mode);
|
||||
} catch {
|
||||
// storage unavailable
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -47,75 +47,92 @@
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-scrim: var(--scrim);
|
||||
}
|
||||
|
||||
/* Warm Sand / Amber palette. Neutrals carry a low-chroma warm hue (~60-85°)
|
||||
so surfaces read as paper/espresso rather than flat gray, with a burnt-amber
|
||||
primary as the brand accent. */
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--background: oklch(0.985 0.006 83);
|
||||
--foreground: oklch(0.255 0.012 64);
|
||||
--card: oklch(0.998 0.003 90);
|
||||
--card-foreground: oklch(0.255 0.012 64);
|
||||
--popover: oklch(0.998 0.003 90);
|
||||
--popover-foreground: oklch(0.255 0.012 64);
|
||||
--primary: oklch(0.62 0.128 62);
|
||||
--primary-foreground: oklch(0.99 0.006 90);
|
||||
--secondary: oklch(0.945 0.014 78);
|
||||
--secondary-foreground: oklch(0.32 0.014 60);
|
||||
--muted: oklch(0.945 0.012 78);
|
||||
--muted-foreground: oklch(0.535 0.02 62);
|
||||
--accent: oklch(0.92 0.03 70);
|
||||
--accent-foreground: oklch(0.3 0.035 55);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
--border: oklch(0.9 0.014 75);
|
||||
--input: oklch(0.9 0.014 75);
|
||||
--ring: oklch(0.62 0.128 62);
|
||||
--chart-1: oklch(0.66 0.14 60);
|
||||
--chart-2: oklch(0.7 0.12 95);
|
||||
--chart-3: oklch(0.58 0.09 45);
|
||||
--chart-4: oklch(0.75 0.13 80);
|
||||
--chart-5: oklch(0.6 0.11 30);
|
||||
--sidebar: oklch(0.965 0.01 80);
|
||||
--sidebar-foreground: oklch(0.255 0.012 64);
|
||||
--sidebar-primary: oklch(0.62 0.128 62);
|
||||
--sidebar-primary-foreground: oklch(0.99 0.006 90);
|
||||
--sidebar-accent: oklch(0.92 0.03 70);
|
||||
--sidebar-accent-foreground: oklch(0.3 0.035 55);
|
||||
--sidebar-border: oklch(0.9 0.014 75);
|
||||
--sidebar-ring: oklch(0.62 0.128 62);
|
||||
--scrollbar-thumb: oklch(0.45 0.03 60 / 25%);
|
||||
--scrollbar-thumb-hover: oklch(0.45 0.03 60 / 40%);
|
||||
--scrollbar-thumb-strong: oklch(0.45 0.03 60 / 35%);
|
||||
--scrollbar-thumb-strong-hover: oklch(0.45 0.03 60 / 55%);
|
||||
|
||||
/* Modal/overlay backdrop. A dim scrim behind dialogs reads the same in both
|
||||
themes (matching shadcn's default overlay), so it is intentionally
|
||||
theme-independent. Use with an alpha modifier, e.g. bg-scrim/60. */
|
||||
--scrim: oklch(0 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--background: oklch(0.19 0.012 58);
|
||||
--foreground: oklch(0.955 0.008 85);
|
||||
--card: oklch(0.232 0.014 56);
|
||||
--card-foreground: oklch(0.955 0.008 85);
|
||||
--popover: oklch(0.232 0.014 56);
|
||||
--popover-foreground: oklch(0.955 0.008 85);
|
||||
--primary: oklch(0.78 0.13 78);
|
||||
--primary-foreground: oklch(0.24 0.03 60);
|
||||
--secondary: oklch(0.285 0.014 54);
|
||||
--secondary-foreground: oklch(0.955 0.008 85);
|
||||
--muted: oklch(0.285 0.014 54);
|
||||
--muted-foreground: oklch(0.72 0.018 72);
|
||||
--accent: oklch(0.33 0.032 56);
|
||||
--accent-foreground: oklch(0.955 0.008 85);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
--border: oklch(0.92 0.03 85 / 10%);
|
||||
--input: oklch(0.92 0.03 85 / 15%);
|
||||
--ring: oklch(0.78 0.13 78);
|
||||
--chart-1: oklch(0.78 0.13 78);
|
||||
--chart-2: oklch(0.7 0.12 50);
|
||||
--chart-3: oklch(0.72 0.11 95);
|
||||
--chart-4: oklch(0.65 0.13 35);
|
||||
--chart-5: oklch(0.6 0.1 60);
|
||||
--sidebar: oklch(0.165 0.012 58);
|
||||
--sidebar-foreground: oklch(0.955 0.008 85);
|
||||
--sidebar-primary: oklch(0.78 0.13 78);
|
||||
--sidebar-primary-foreground: oklch(0.24 0.03 60);
|
||||
--sidebar-accent: oklch(0.33 0.032 56);
|
||||
--sidebar-accent-foreground: oklch(0.955 0.008 85);
|
||||
--sidebar-border: oklch(0.92 0.03 85 / 10%);
|
||||
--sidebar-ring: oklch(0.78 0.13 78);
|
||||
--scrollbar-thumb: oklch(0.92 0.03 85 / 18%);
|
||||
--scrollbar-thumb-hover: oklch(0.92 0.03 85 / 32%);
|
||||
--scrollbar-thumb-strong: oklch(0.92 0.03 85 / 30%);
|
||||
--scrollbar-thumb-strong-hover: oklch(0.92 0.03 85 / 50%);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
@@ -130,7 +147,7 @@
|
||||
/* Scrollbar styling */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: oklch(1 0 0 / 20%) transparent;
|
||||
scrollbar-color: var(--scrollbar-thumb) transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
@@ -143,12 +160,12 @@
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: oklch(1 0 0 / 20%);
|
||||
background: var(--scrollbar-thumb);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: oklch(1 0 0 / 35%);
|
||||
background: var(--scrollbar-thumb-hover);
|
||||
}
|
||||
|
||||
/* Message cards: more prominent scrollbar than the subtle global default.
|
||||
@@ -160,13 +177,13 @@
|
||||
|
||||
.scrollbar-card::-webkit-scrollbar-thumb,
|
||||
.scrollbar-card ::-webkit-scrollbar-thumb {
|
||||
background: oklch(1 0 0 / 30%);
|
||||
background: var(--scrollbar-thumb-strong);
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.scrollbar-card::-webkit-scrollbar-thumb:hover,
|
||||
.scrollbar-card ::-webkit-scrollbar-thumb:hover {
|
||||
background: oklch(1 0 0 / 50%);
|
||||
background: var(--scrollbar-thumb-strong-hover);
|
||||
}
|
||||
|
||||
/* Message layout: the compose editor and the posted card derive their widths
|
||||
|
||||
@@ -1,10 +1,26 @@
|
||||
<!doctype html>
|
||||
<html class="dark">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/png" href="/icon.png" />
|
||||
<title>llink</title>
|
||||
<script>
|
||||
// Apply the stored theme before first paint to avoid a flash. Mirrors
|
||||
// the logic in stores/theme-store.ts; defaults to dark.
|
||||
try {
|
||||
var m = localStorage.getItem('llink:theme');
|
||||
var dark =
|
||||
m === 'light'
|
||||
? false
|
||||
: m === 'system'
|
||||
? matchMedia('(prefers-color-scheme: dark)').matches
|
||||
: true;
|
||||
if (dark) document.documentElement.classList.add('dark');
|
||||
} catch (e) {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from '@/App';
|
||||
import { initSentryRenderer } from '@/lib/sentry';
|
||||
import '@/stores/theme-store';
|
||||
import '@/styles/globals.css';
|
||||
|
||||
initSentryRenderer();
|
||||
|
||||
Reference in New Issue
Block a user