Updated a lot of stuff
This commit is contained in:
parent
4ee57ab459
commit
c0102df54f
44 changed files with 1610 additions and 707 deletions
|
|
@ -34,7 +34,9 @@
|
||||||
"tailwindcss": "^4.2.1",
|
"tailwindcss": "^4.2.1",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"vite-tsconfig-paths": "^6.1.1",
|
"vite-tsconfig-paths": "^6.1.1",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6",
|
||||||
|
"@fontsource-variable/inter": "^5.2.8",
|
||||||
|
"shadcn": "^4.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^19.2.2",
|
"@types/react": "^19.2.2",
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
import type { User } from "@tensamin/user/context";
|
import type { User } from "@tensamin/user/context";
|
||||||
import Avatar from "@tensamin/ui/avatar";
|
import { Avatar, AvatarImage, AvatarFallback } from "@tensamin/ui/cmp/avatar";
|
||||||
import { reduceDisplay } from "./utils";
|
import { reduceDisplay } from "./utils";
|
||||||
import { Card, CardHeader } from "@tensamin/ui/card";
|
import { Card, CardHeader } from "@tensamin/ui/cmp/card";
|
||||||
|
|
||||||
export default function Basic(props: { user: User }) {
|
export default function Basic(props: { user: User }) {
|
||||||
return (
|
return (
|
||||||
<Card className="animate-in fade-in duration-300 rounded-2xl">
|
<Card className="animate-in fade-in duration-300 rounded-2xl">
|
||||||
<CardHeader className="flex flex-row gap-2.5 items-center justify-start p-2">
|
<CardHeader className="flex flex-row gap-2.5 items-center justify-start p-2">
|
||||||
<Avatar
|
<Avatar>
|
||||||
img={props.user.avatar}
|
<AvatarImage src={props.user.avatar} />
|
||||||
fallback={reduceDisplay(props.user.display)}
|
<AvatarFallback>{reduceDisplay(props.user.display)}</AvatarFallback>
|
||||||
/>
|
</Avatar>
|
||||||
<div className="flex flex-col gap-1 w-full items-start justify-center text-[15px]">
|
<div className="flex flex-col gap-1 w-full items-start justify-center text-[15px]">
|
||||||
<p>{props.user.display}</p>
|
<p>{props.user.display}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Button } from "@tensamin/ui/button";
|
import { Button } from "@tensamin/ui/cmp/button";
|
||||||
import { House } from "lucide-react";
|
import { House } from "lucide-react";
|
||||||
import { useNavigate, useRouterState } from "@tanstack/react-router";
|
import { useNavigate, useRouterState } from "@tanstack/react-router";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Button } from "@tensamin/ui/button";
|
import { Button } from "@tensamin/ui/cmp/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@tensamin/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@tensamin/ui/cmp/card";
|
||||||
import { Input } from "@tensamin/ui/input";
|
import { Input } from "@tensamin/ui/cmp/input";
|
||||||
import { Label } from "@tensamin/ui/label";
|
import { Label } from "@tensamin/ui/cmp/label";
|
||||||
import { useStorage } from "@tensamin/storage/context";
|
import { useStorage } from "@tensamin/storage/context";
|
||||||
import { log, toast } from "@tensamin/shared/log";
|
import { log, toast } from "@tensamin/shared/log";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import {
|
||||||
ContextMenuGroup,
|
ContextMenuGroup,
|
||||||
ContextMenuItem,
|
ContextMenuItem,
|
||||||
ContextMenuTrigger,
|
ContextMenuTrigger,
|
||||||
} from "@tensamin/ui/context-menu";
|
} from "@tensamin/ui/cmp/context-menu";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
|
||||||
export default function ConversationModal(props: { userId: number }) {
|
export default function ConversationModal(props: { userId: number }) {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,7 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useStorage } from "@tensamin/storage/context";
|
import { useStorage } from "@tensamin/storage/context";
|
||||||
import { Button } from "@tensamin/ui/button";
|
import { Button } from "@tensamin/ui/cmp/button";
|
||||||
import {
|
import { Checkbox } from "@tensamin/ui/cmp/checkbox";
|
||||||
Checkbox,
|
|
||||||
CheckboxLabel,
|
|
||||||
CheckboxControl,
|
|
||||||
} from "@tensamin/ui/checkbox";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import ErrorScreen from "@tensamin/ui/screens/error";
|
import ErrorScreen from "@tensamin/ui/screens/error";
|
||||||
|
|
@ -13,6 +9,7 @@ import ErrorScreen from "@tensamin/ui/screens/error";
|
||||||
import { legalDocsSchema } from "@tensamin/shared/features/legal/schema";
|
import { legalDocsSchema } from "@tensamin/shared/features/legal/schema";
|
||||||
import { log } from "@tensamin/shared/log";
|
import { log } from "@tensamin/shared/log";
|
||||||
import Link from "@tensamin/ui/link";
|
import Link from "@tensamin/ui/link";
|
||||||
|
import { Label } from "@tensamin/ui/cmp/label";
|
||||||
|
|
||||||
export default function Screen(props: { children: React.ReactNode }) {
|
export default function Screen(props: { children: React.ReactNode }) {
|
||||||
const { load, save } = useStorage();
|
const { load, save } = useStorage();
|
||||||
|
|
@ -74,7 +71,13 @@ export default function Screen(props: { children: React.ReactNode }) {
|
||||||
setErrorDescription(
|
setErrorDescription(
|
||||||
"The legal documents data received from the server is invalid. Please try again later.",
|
"The legal documents data received from the server is invalid. Please try again later.",
|
||||||
);
|
);
|
||||||
log(0, "Legal", "red", "Invalid legal documents data", safeCurrent.error);
|
log(
|
||||||
|
0,
|
||||||
|
"Legal",
|
||||||
|
"red",
|
||||||
|
"Invalid legal documents data",
|
||||||
|
safeCurrent.error,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -213,9 +216,11 @@ export default function Screen(props: { children: React.ReactNode }) {
|
||||||
const currentCrashReports = crashReports;
|
const currentCrashReports = crashReports;
|
||||||
const currentUsageData = usageData;
|
const currentUsageData = usageData;
|
||||||
|
|
||||||
void save("analytics_crash_reports", currentCrashReports).then(() => {
|
void save("analytics_crash_reports", currentCrashReports).then(
|
||||||
setCrashReports(currentCrashReports);
|
() => {
|
||||||
});
|
setCrashReports(currentCrashReports);
|
||||||
|
},
|
||||||
|
);
|
||||||
void save("analytics_usage_data", currentUsageData).then(() => {
|
void save("analytics_usage_data", currentUsageData).then(() => {
|
||||||
setUsageData(currentUsageData);
|
setUsageData(currentUsageData);
|
||||||
});
|
});
|
||||||
|
|
@ -252,13 +257,13 @@ export function BigCheckbox(props: {
|
||||||
onChange: (checked: boolean) => void;
|
onChange: (checked: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Checkbox
|
<div className="flex items-center space-x-2">
|
||||||
checked={props.checked}
|
<Checkbox
|
||||||
onChange={props.onChange}
|
checked={props.checked}
|
||||||
className="flex items-center space-x-2"
|
onCheckedChange={props.onChange}
|
||||||
>
|
className="size-5.5 rounded-md flex items-center justify-center"
|
||||||
<CheckboxControl className="size-5.5 rounded-md flex items-center justify-center" />
|
/>
|
||||||
<CheckboxLabel className="text-lg">{props.label}</CheckboxLabel>
|
<Label className="text-lg">{props.label}</Label>
|
||||||
</Checkbox>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,83 @@
|
||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "tailwind-scrollbar-hide/v4";
|
@import "tailwind-scrollbar-hide/v4";
|
||||||
@import "tw-animate-css";
|
@import "tw-animate-css";
|
||||||
|
@import "shadcn/tailwind.css";
|
||||||
|
@import "@fontsource-variable/inter";
|
||||||
|
|
||||||
@source "./**/*.{ts,tsx}";
|
@source "./**/*.{ts,tsx}";
|
||||||
@source "../../../packages/ui/src/**/*.{ts,tsx}";
|
|
||||||
@source "../../../packages/**/src/**/*.{ts,tsx}";
|
@source "../../../packages/**/src/**/*.{ts,tsx}";
|
||||||
@source "../../../packages/chat/src/**/*.{ts,tsx}";
|
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
:root {
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.148 0.004 228.8);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.148 0.004 228.8);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.148 0.004 228.8);
|
||||||
|
--primary: oklch(0.511 0.096 186.391);
|
||||||
|
--primary-foreground: oklch(0.984 0.014 180.72);
|
||||||
|
--secondary: oklch(0.967 0.001 286.375);
|
||||||
|
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--muted: oklch(0.963 0.002 197.1);
|
||||||
|
--muted-foreground: oklch(0.56 0.021 213.5);
|
||||||
|
--accent: oklch(0.963 0.002 197.1);
|
||||||
|
--accent-foreground: oklch(0.218 0.008 223.9);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.925 0.005 214.3);
|
||||||
|
--input: oklch(0.925 0.005 214.3);
|
||||||
|
--ring: oklch(0.723 0.014 214.4);
|
||||||
|
--chart-1: oklch(0.855 0.138 181.071);
|
||||||
|
--chart-2: oklch(0.704 0.14 182.503);
|
||||||
|
--chart-3: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-4: oklch(0.511 0.096 186.391);
|
||||||
|
--chart-5: oklch(0.437 0.078 188.216);
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--sidebar: oklch(0.987 0.002 197.1);
|
||||||
|
--sidebar-foreground: oklch(0.148 0.004 228.8);
|
||||||
|
--sidebar-primary: oklch(0.6 0.118 184.704);
|
||||||
|
--sidebar-primary-foreground: oklch(0.984 0.014 180.72);
|
||||||
|
--sidebar-accent: oklch(0.963 0.002 197.1);
|
||||||
|
--sidebar-accent-foreground: oklch(0.218 0.008 223.9);
|
||||||
|
--sidebar-border: oklch(0.925 0.005 214.3);
|
||||||
|
--sidebar-ring: oklch(0.723 0.014 214.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.148 0.004 228.8);
|
||||||
|
--foreground: oklch(0.987 0.002 197.1);
|
||||||
|
--card: oklch(0.218 0.008 223.9);
|
||||||
|
--card-foreground: oklch(0.987 0.002 197.1);
|
||||||
|
--popover: oklch(0.218 0.008 223.9);
|
||||||
|
--popover-foreground: oklch(0.987 0.002 197.1);
|
||||||
|
--primary: oklch(0.437 0.078 188.216);
|
||||||
|
--primary-foreground: oklch(0.984 0.014 180.72);
|
||||||
|
--secondary: oklch(0.274 0.006 286.033);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.275 0.011 216.9);
|
||||||
|
--muted-foreground: oklch(0.723 0.014 214.4);
|
||||||
|
--accent: oklch(0.275 0.011 216.9);
|
||||||
|
--accent-foreground: oklch(0.987 0.002 197.1);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.56 0.021 213.5);
|
||||||
|
--chart-1: oklch(0.855 0.138 181.071);
|
||||||
|
--chart-2: oklch(0.704 0.14 182.503);
|
||||||
|
--chart-3: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-4: oklch(0.511 0.096 186.391);
|
||||||
|
--chart-5: oklch(0.437 0.078 188.216);
|
||||||
|
--sidebar: oklch(0.218 0.008 223.9);
|
||||||
|
--sidebar-foreground: oklch(0.987 0.002 197.1);
|
||||||
|
--sidebar-primary: oklch(0.704 0.14 182.503);
|
||||||
|
--sidebar-primary-foreground: oklch(0.277 0.046 192.524);
|
||||||
|
--sidebar-accent: oklch(0.275 0.011 216.9);
|
||||||
|
--sidebar-accent-foreground: oklch(0.987 0.002 197.1);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
|
--sidebar-ring: oklch(0.56 0.021 213.5);
|
||||||
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--breakpoint-sm: 640px;
|
--font-sans: "Inter Variable", sans-serif;
|
||||||
--breakpoint-md: 768px;
|
|
||||||
--breakpoint-lg: 1024px;
|
|
||||||
--breakpoint-xl: 1280px;
|
|
||||||
--breakpoint-2xl: 1536px;
|
|
||||||
|
|
||||||
--animate-first: moveVertical 30s ease infinite;
|
|
||||||
--animate-second: moveInCircle 20s reverse infinite;
|
|
||||||
--animate-third: moveInCircle 40s linear infinite;
|
|
||||||
--animate-fourth: moveHorizontal 40s ease infinite;
|
|
||||||
--animate-fifth: moveInCircle 20s ease infinite;
|
|
||||||
|
|
||||||
--color-background: var(--background);
|
|
||||||
--color-foreground: var(--foreground);
|
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
|
@ -53,81 +107,15 @@
|
||||||
--color-popover: var(--popover);
|
--color-popover: var(--popover);
|
||||||
--color-card-foreground: var(--card-foreground);
|
--color-card-foreground: var(--card-foreground);
|
||||||
--color-card: var(--card);
|
--color-card: var(--card);
|
||||||
--radius-sm: calc(var(--radius) - 4px);
|
--color-foreground: var(--foreground);
|
||||||
--radius-md: calc(var(--radius) - 2px);
|
--color-background: var(--background);
|
||||||
|
--radius-sm: calc(var(--radius) * 0.6);
|
||||||
|
--radius-md: calc(var(--radius) * 0.8);
|
||||||
--radius-lg: var(--radius);
|
--radius-lg: var(--radius);
|
||||||
--radius-xl: calc(var(--radius) + 4px);
|
--radius-xl: calc(var(--radius) * 1.4);
|
||||||
}
|
--radius-2xl: calc(var(--radius) * 1.8);
|
||||||
|
--radius-3xl: calc(var(--radius) * 2.2);
|
||||||
:root {
|
--radius-4xl: calc(var(--radius) * 2.6);
|
||||||
--radius: 0.625rem;
|
|
||||||
--background: #f7f7f7;
|
|
||||||
--foreground: #1f1f1f;
|
|
||||||
--card: #ffffff;
|
|
||||||
--card-foreground: #1f1f1f;
|
|
||||||
--popover: #ffffff;
|
|
||||||
--popover-foreground: #1f1f1f;
|
|
||||||
--primary: #babbf1;
|
|
||||||
--primary-foreground: #181a22;
|
|
||||||
--secondary: #efefef;
|
|
||||||
--secondary-foreground: #1f1f1f;
|
|
||||||
--muted: #ececec;
|
|
||||||
--muted-foreground: #666666;
|
|
||||||
--accent: var(--border);
|
|
||||||
--accent-foreground: #1f1f1f;
|
|
||||||
--destructive: #d8d8d8;
|
|
||||||
--destructive-foreground: #1f1f1f;
|
|
||||||
--border: #dadada;
|
|
||||||
--input: #e7e7e7;
|
|
||||||
--ring: #babbf1;
|
|
||||||
--chart-1: #babbf1;
|
|
||||||
--chart-2: #bcbcbc;
|
|
||||||
--chart-3: #969696;
|
|
||||||
--chart-4: #717171;
|
|
||||||
--chart-5: #4f4f4f;
|
|
||||||
--sidebar: #f1f1f1;
|
|
||||||
--sidebar-foreground: #1f1f1f;
|
|
||||||
--sidebar-primary: #babbf1;
|
|
||||||
--sidebar-primary-foreground: #181a22;
|
|
||||||
--sidebar-accent: #e7e7e7;
|
|
||||||
--sidebar-accent-foreground: #1f1f1f;
|
|
||||||
--sidebar-border: #dadada;
|
|
||||||
--sidebar-ring: #babbf1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark {
|
|
||||||
--background: #141414;
|
|
||||||
--foreground: #f5f5f5;
|
|
||||||
--card: #1d1d1d;
|
|
||||||
--card-foreground: #f5f5f5;
|
|
||||||
--popover: #1d1d1d;
|
|
||||||
--popover-foreground: #f5f5f5;
|
|
||||||
--primary: #babbf1;
|
|
||||||
--primary-foreground: #11131a;
|
|
||||||
--secondary: #232323;
|
|
||||||
--secondary-foreground: #f5f5f5;
|
|
||||||
--muted: #262626;
|
|
||||||
--muted-foreground: #c7c7c7;
|
|
||||||
--accent: var(--border);
|
|
||||||
--accent-foreground: #f5f5f5;
|
|
||||||
--destructive: #2c2c2c;
|
|
||||||
--destructive-foreground: #f5f5f5;
|
|
||||||
--border: #343434;
|
|
||||||
--input: #2a2a2a;
|
|
||||||
--ring: #babbf1;
|
|
||||||
--chart-1: #babbf1;
|
|
||||||
--chart-2: #bcbcbc;
|
|
||||||
--chart-3: #969696;
|
|
||||||
--chart-4: #707070;
|
|
||||||
--chart-5: #525252;
|
|
||||||
--sidebar: #171717;
|
|
||||||
--sidebar-foreground: #f5f5f5;
|
|
||||||
--sidebar-primary: #babbf1;
|
|
||||||
--sidebar-primary-foreground: #11131a;
|
|
||||||
--sidebar-accent: #262626;
|
|
||||||
--sidebar-accent-foreground: #f5f5f5;
|
|
||||||
--sidebar-border: #343434;
|
|
||||||
--sidebar-ring: #babbf1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
|
|
@ -136,10 +124,9 @@
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
font-family: "Inter", system-ui, sans-serif;
|
|
||||||
}
|
}
|
||||||
div {
|
html {
|
||||||
@apply text-foreground;
|
@apply font-sans;
|
||||||
}
|
}
|
||||||
::selection {
|
::selection {
|
||||||
background: var(--color-primary, var(--primary));
|
background: var(--color-primary, var(--primary));
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import * as React from "react";
|
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import {
|
import {
|
||||||
Outlet,
|
Outlet,
|
||||||
|
|
@ -20,6 +19,8 @@ import ChatContext from "@tensamin/chat/context";
|
||||||
import Login from "@/routes/screens/login";
|
import Login from "@/routes/screens/login";
|
||||||
import Signup from "@/routes/screens/signup";
|
import Signup from "@/routes/screens/signup";
|
||||||
|
|
||||||
|
import { ThemeProvider } from "@tensamin/ui/theme";
|
||||||
|
|
||||||
const wrapper = document.getElementById("root");
|
const wrapper = document.getElementById("root");
|
||||||
|
|
||||||
if (!wrapper) {
|
if (!wrapper) {
|
||||||
|
|
@ -33,32 +34,14 @@ window.setLogLevelToMax = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
function RootShell() {
|
function RootShell() {
|
||||||
React.useEffect(() => {
|
|
||||||
try {
|
|
||||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
|
||||||
|
|
||||||
document.documentElement.classList.toggle("dark", mediaQuery.matches);
|
|
||||||
|
|
||||||
const handleChange = (event: MediaQueryListEvent) => {
|
|
||||||
document.documentElement.classList.toggle("dark", event.matches);
|
|
||||||
};
|
|
||||||
|
|
||||||
mediaQuery.addEventListener("change", handleChange);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
mediaQuery.removeEventListener("change", handleChange);
|
|
||||||
};
|
|
||||||
} catch {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-screen h-screen overflow-hidden">
|
<ThemeProvider>
|
||||||
<RootLayout>
|
<div className="w-screen h-screen overflow-hidden">
|
||||||
<Outlet />
|
<RootLayout>
|
||||||
</RootLayout>
|
<Outlet />
|
||||||
</div>
|
</RootLayout>
|
||||||
|
</div>
|
||||||
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -129,4 +112,4 @@ declare module "@tanstack/react-router" {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createRoot(wrapper).render(<RouterProvider router={router} />);
|
createRoot(wrapper).render(<RouterProvider router={router} />);
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import Crypto from "@tensamin/crypto/context";
|
||||||
|
|
||||||
import LegalWrapper from "@/features/legal/screen";
|
import LegalWrapper from "@/features/legal/screen";
|
||||||
|
|
||||||
import { Toaster } from "@tensamin/ui/sonner";
|
import { Toaster } from "@tensamin/ui/cmp/sonner";
|
||||||
|
|
||||||
export default function Layout(props: { children: ReactNode }) {
|
export default function Layout(props: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import Form from "@/components/screens/login/form";
|
import Form from "@/components/screens/login/form";
|
||||||
import { Button } from "@tensamin/ui/button";
|
import { Button } from "@tensamin/ui/cmp/button";
|
||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
|
|
|
||||||
18
bun.lock
18
bun.lock
|
|
@ -166,13 +166,15 @@
|
||||||
"name": "@tensamin/ui",
|
"name": "@tensamin/ui",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@base-ui/react": "^1.3.0",
|
||||||
"@radix-ui/react-context-menu": "^2.2.16",
|
"@radix-ui/react-context-menu": "^2.2.16",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.564.0",
|
"lucide-react": "^0.564.0",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"sonner": "^1.0.0",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -221,6 +223,8 @@
|
||||||
|
|
||||||
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
|
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
|
||||||
|
|
||||||
|
"@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
|
||||||
|
|
||||||
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
|
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
|
||||||
|
|
||||||
"@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
|
"@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
|
||||||
|
|
@ -229,6 +233,10 @@
|
||||||
|
|
||||||
"@babylonjs/core": ["@babylonjs/core@7.54.3", "", {}, "sha512-P5ncXVd8GEUJLhwloP9V0oVwQYIrvZztguVeLlvd5Rx+9aQnenKjpV8auJ6SRsUlAmNZU4pFTKzwF6o2EUfhAw=="],
|
"@babylonjs/core": ["@babylonjs/core@7.54.3", "", {}, "sha512-P5ncXVd8GEUJLhwloP9V0oVwQYIrvZztguVeLlvd5Rx+9aQnenKjpV8auJ6SRsUlAmNZU4pFTKzwF6o2EUfhAw=="],
|
||||||
|
|
||||||
|
"@base-ui/react": ["@base-ui/react@1.3.0", "", { "dependencies": { "@babel/runtime": "^7.28.6", "@base-ui/utils": "0.2.6", "@floating-ui/react-dom": "^2.1.8", "@floating-ui/utils": "^0.2.11", "tabbable": "^6.4.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-FwpKqZbPz14AITp1CVgf4AjhKPe1OeeVKSBMdgD10zbFlj3QSWelmtCMLi2+/PFZZcIm3l87G7rwtCZJwHyXWA=="],
|
||||||
|
|
||||||
|
"@base-ui/utils": ["@base-ui/utils@0.2.6", "", { "dependencies": { "@babel/runtime": "^7.28.6", "@floating-ui/utils": "^0.2.11", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-yQ+qeuqohwhsNpoYDqqXaLllYAkPCP4vYdDrVo8FQXaAPfHWm1pG/Vm+jmGTA5JFS0BAIjookyapuJFY8F9PIw=="],
|
||||||
|
|
||||||
"@codemirror/autocomplete": ["@codemirror/autocomplete@6.20.0", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg=="],
|
"@codemirror/autocomplete": ["@codemirror/autocomplete@6.20.0", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg=="],
|
||||||
|
|
||||||
"@codemirror/commands": ["@codemirror/commands@6.10.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.4.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ=="],
|
"@codemirror/commands": ["@codemirror/commands@6.10.2", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.4.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ=="],
|
||||||
|
|
@ -765,6 +773,8 @@
|
||||||
|
|
||||||
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||||
|
|
||||||
|
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
|
||||||
|
|
||||||
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
|
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
|
||||||
|
|
||||||
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
||||||
|
|
@ -807,6 +817,8 @@
|
||||||
|
|
||||||
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
|
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
|
||||||
|
|
||||||
|
"reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="],
|
||||||
|
|
||||||
"rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="],
|
"rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="],
|
||||||
|
|
||||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||||
|
|
@ -827,6 +839,8 @@
|
||||||
|
|
||||||
"style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="],
|
"style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="],
|
||||||
|
|
||||||
|
"tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="],
|
||||||
|
|
||||||
"tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="],
|
"tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="],
|
||||||
|
|
||||||
"tailwind-scrollbar-hide": ["tailwind-scrollbar-hide@4.0.0", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || >= 4.0.0 || >= 4.0.0-beta.8 || >= 4.0.0-alpha.20" } }, "sha512-gobtvVcThB2Dxhy0EeYSS1RKQJ5baDFkamkhwBvzvevwX6L4XQfpZ3me9s25Ss1ecFVT5jPYJ50n+7xTBJG9WQ=="],
|
"tailwind-scrollbar-hide": ["tailwind-scrollbar-hide@4.0.0", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || >= 4.0.0 || >= 4.0.0-beta.8 || >= 4.0.0-alpha.20" } }, "sha512-gobtvVcThB2Dxhy0EeYSS1RKQJ5baDFkamkhwBvzvevwX6L4XQfpZ3me9s25Ss1ecFVT5jPYJ50n+7xTBJG9WQ=="],
|
||||||
|
|
@ -907,6 +921,8 @@
|
||||||
|
|
||||||
"@tensamin/notifications/@tensamin/ttp": ["TTP@github:Tensamin/TTP#d5edc69", {}, "Tensamin-TTP-d5edc69"],
|
"@tensamin/notifications/@tensamin/ttp": ["TTP@github:Tensamin/TTP#d5edc69", {}, "Tensamin-TTP-d5edc69"],
|
||||||
|
|
||||||
|
"@tensamin/ui/sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="],
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||||
|
|
||||||
"bun-types/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
|
"bun-types/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import Input from "@tensamin/markdown/input";
|
import Input from "@tensamin/markdown/input";
|
||||||
import { Card, CardHeader } from "@tensamin/ui/card";
|
import { Card, CardHeader } from "@tensamin/ui/cmp/card";
|
||||||
import { useStorage } from "@tensamin/storage/context";
|
import { useStorage } from "@tensamin/storage/context";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Button } from "@tensamin/ui/button";
|
import { Button } from "@tensamin/ui/cmp/button";
|
||||||
|
|
||||||
import { Plus, Laugh, Clapperboard } from "lucide-react";
|
import { Plus, Laugh, Clapperboard } from "lucide-react";
|
||||||
import { useChat } from "../context";
|
import { useChat } from "../context";
|
||||||
|
|
|
||||||
|
|
@ -41,20 +41,27 @@ export default function Provider(
|
||||||
|
|
||||||
let active = true;
|
let active = true;
|
||||||
|
|
||||||
void get(recipientId).then(async (recipientData) => {
|
void (async () => {
|
||||||
const ownId = await load("user_id");
|
try {
|
||||||
const privateKey = await load("private_key");
|
const recipientData = await get(recipientId);
|
||||||
const ownData = await get(ownId);
|
const ownId = await load("user_id");
|
||||||
const sharedSecret = await get_shared_secret(
|
const privateKey = await load("private_key");
|
||||||
privateKey,
|
const ownData = await get(ownId);
|
||||||
ownData.public_key,
|
const sharedSecret = await get_shared_secret(
|
||||||
recipientData.public_key,
|
privateKey,
|
||||||
);
|
ownData.public_key,
|
||||||
|
recipientData.public_key,
|
||||||
|
);
|
||||||
|
|
||||||
if (active) {
|
if (active) {
|
||||||
setCurrentSharedSecret(sharedSecret);
|
setCurrentSharedSecret(sharedSecret);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
if (active) {
|
||||||
|
setCurrentSharedSecret("");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
})();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
active = false;
|
active = false;
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,19 @@ const message = z.object({
|
||||||
// Socket
|
// Socket
|
||||||
export const socket = {
|
export const socket = {
|
||||||
identification: {
|
identification: {
|
||||||
request: z.object({
|
request: z
|
||||||
user_id: z.number(),
|
.object({
|
||||||
}),
|
user_id: z.number().optional(),
|
||||||
|
iota_id: z.number().optional(),
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(value) =>
|
||||||
|
(typeof value.user_id === "number") !==
|
||||||
|
(typeof value.iota_id === "number"),
|
||||||
|
{
|
||||||
|
message: "Either user_id or iota_id must be provided",
|
||||||
|
},
|
||||||
|
),
|
||||||
response: z.object({
|
response: z.object({
|
||||||
challenge: z.string(),
|
challenge: z.string(),
|
||||||
public_key: z.base64(),
|
public_key: z.base64(),
|
||||||
|
|
@ -66,7 +76,9 @@ export const socket = {
|
||||||
request: z.object({
|
request: z.object({
|
||||||
challenge: z.base64(),
|
challenge: z.base64(),
|
||||||
}),
|
}),
|
||||||
response: z.object({}),
|
response: z.object({
|
||||||
|
accepted: z.boolean(),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
ping: {
|
ping: {
|
||||||
request: z.object({
|
request: z.object({
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import { useCrypto } from "@tensamin/crypto/context";
|
||||||
import { log } from "@tensamin/shared/log";
|
import { log } from "@tensamin/shared/log";
|
||||||
|
import { useStorage } from "@tensamin/storage/context";
|
||||||
import { createTransportClient, READY_STATE, type BoundSendFn } from "./core";
|
import { createTransportClient, READY_STATE, type BoundSendFn } from "./core";
|
||||||
import {
|
import {
|
||||||
PING_INTERVAL,
|
PING_INTERVAL,
|
||||||
|
|
@ -14,18 +16,90 @@ import {
|
||||||
import Loading from "@tensamin/ui/screens/loading";
|
import Loading from "@tensamin/ui/screens/loading";
|
||||||
import ErrorScreen from "@tensamin/ui/screens/error";
|
import ErrorScreen from "@tensamin/ui/screens/error";
|
||||||
|
|
||||||
|
const FATAL_IDENTIFICATION_ERROR_TYPES = new Set([
|
||||||
|
"error_invalid_user_id",
|
||||||
|
"error_no_user_id",
|
||||||
|
"error_invalid_challenge",
|
||||||
|
"error_invalid_secret",
|
||||||
|
"error_invalid_private_key",
|
||||||
|
"error_invalid_public_key",
|
||||||
|
"error_not_authenticated",
|
||||||
|
]);
|
||||||
|
|
||||||
|
function isStopSendingError(error: unknown) {
|
||||||
|
if (typeof error === "string") {
|
||||||
|
return error.includes("STOP_SENDING");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
if (error.message.includes("STOP_SENDING")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorWithCause = error as Error & { cause?: unknown };
|
||||||
|
if (errorWithCause.cause !== undefined) {
|
||||||
|
return isStopSendingError(errorWithCause.cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof error === "object" && error !== null) {
|
||||||
|
const maybeMessage = (error as { message?: unknown }).message;
|
||||||
|
if (typeof maybeMessage === "string") {
|
||||||
|
return maybeMessage.includes("STOP_SENDING");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isFatalIdentificationError(error: unknown) {
|
||||||
|
if (typeof error === "object" && error !== null && "type" in error) {
|
||||||
|
const type = (error as { type?: unknown }).type;
|
||||||
|
if (
|
||||||
|
typeof type === "string" &&
|
||||||
|
FATAL_IDENTIFICATION_ERROR_TYPES.has(type)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(error instanceof Error)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
error.message.includes("Missing or invalid user id") ||
|
||||||
|
error.message.includes("Missing private key") ||
|
||||||
|
error.message.includes("Identification challenge was rejected")
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
type ContextType = {
|
type ContextType = {
|
||||||
send: BoundSendFn<Schemas>;
|
send: BoundSendFn<Schemas>;
|
||||||
readyState: () => number;
|
readyState: () => number;
|
||||||
ownPing: () => number;
|
ownPing: () => number;
|
||||||
iotaPing: () => number;
|
iotaPing: () => number;
|
||||||
|
identified: () => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const socketContext = React.createContext<ContextType | undefined>(undefined);
|
const socketContext = React.createContext<ContextType | undefined>(undefined);
|
||||||
|
|
||||||
export default function Provider(props: { children: React.ReactNode }) {
|
export default function Provider(props: { children: React.ReactNode }) {
|
||||||
const [readyState, setReadyState] = React.useState<number>(READY_STATE.CLOSED);
|
const { load } = useStorage();
|
||||||
|
const { decrypt, get_shared_secret } = useCrypto();
|
||||||
|
|
||||||
|
const [readyState, setReadyState] = React.useState<number>(
|
||||||
|
READY_STATE.CLOSED,
|
||||||
|
);
|
||||||
const [connected, setConnected] = React.useState<boolean>(false);
|
const [connected, setConnected] = React.useState<boolean>(false);
|
||||||
|
const [identified, setIdentified] = React.useState<boolean>(false);
|
||||||
|
const [identifying, setIdentifying] = React.useState<boolean>(false);
|
||||||
|
|
||||||
const [ownPing, setOwnPing] = React.useState<number>(0);
|
const [ownPing, setOwnPing] = React.useState<number>(0);
|
||||||
const [iotaPing, setIotaPing] = React.useState<number>(0);
|
const [iotaPing, setIotaPing] = React.useState<number>(0);
|
||||||
|
|
@ -36,6 +110,7 @@ export default function Provider(props: { children: React.ReactNode }) {
|
||||||
const clientRef = React.useRef<ReturnType<
|
const clientRef = React.useRef<ReturnType<
|
||||||
typeof createTransportClient<Schemas>
|
typeof createTransportClient<Schemas>
|
||||||
> | null>(null);
|
> | null>(null);
|
||||||
|
const identificationStartedRef = React.useRef(false);
|
||||||
|
|
||||||
const send = React.useCallback<BoundSendFn<Schemas>>(
|
const send = React.useCallback<BoundSendFn<Schemas>>(
|
||||||
((
|
((
|
||||||
|
|
@ -65,7 +140,7 @@ export default function Provider(props: { children: React.ReactNode }) {
|
||||||
);
|
);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!connected) {
|
if (!connected || !identified) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,7 +167,7 @@ export default function Provider(props: { children: React.ReactNode }) {
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
};
|
};
|
||||||
}, [connected, send]);
|
}, [connected, identified, send]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
let attempts = 0;
|
let attempts = 0;
|
||||||
|
|
@ -141,17 +216,34 @@ export default function Provider(props: { children: React.ReactNode }) {
|
||||||
if (state === READY_STATE.OPEN) {
|
if (state === READY_STATE.OPEN) {
|
||||||
attempts = 0;
|
attempts = 0;
|
||||||
clearReconnectTimer();
|
clearReconnectTimer();
|
||||||
|
identificationStartedRef.current = false;
|
||||||
setConnected(true);
|
setConnected(true);
|
||||||
|
setIdentified(false);
|
||||||
setError("");
|
setError("");
|
||||||
setErrorDescription("");
|
setErrorDescription("");
|
||||||
log(1, "Socket", "green", "Connected");
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
identificationStartedRef.current = false;
|
||||||
|
setConnected(false);
|
||||||
|
setIdentified(false);
|
||||||
|
},
|
||||||
|
onClose: ({ error: closeError, intentional }) => {
|
||||||
|
if (isStopSendingError(closeError)) {
|
||||||
|
clearReconnectTimer();
|
||||||
|
setConnected(false);
|
||||||
|
setIdentified(false);
|
||||||
|
setIdentifying(false);
|
||||||
|
setError("Connection closed");
|
||||||
|
setErrorDescription(
|
||||||
|
"The connection was forcefully closed by the Omikron.",
|
||||||
|
);
|
||||||
|
log(0, "Socket", "red", "Connection closed", closeError);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setConnected(false);
|
setConnected(false);
|
||||||
},
|
setIdentified(false);
|
||||||
onClose: ({ error: closeError, intentional }) => {
|
|
||||||
setConnected(false);
|
|
||||||
|
|
||||||
if (disposed || intentional) {
|
if (disposed || intentional) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -176,6 +268,13 @@ export default function Provider(props: { children: React.ReactNode }) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isStopSendingError(connectError)) {
|
||||||
|
clearReconnectTimer();
|
||||||
|
setError("Connection closed");
|
||||||
|
setErrorDescription("Connection closed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
log(0, "Socket", "red", "Connection attempt failed", connectError);
|
log(0, "Socket", "red", "Connection attempt failed", connectError);
|
||||||
scheduleReconnect(connectError);
|
scheduleReconnect(connectError);
|
||||||
}
|
}
|
||||||
|
|
@ -194,14 +293,147 @@ export default function Provider(props: { children: React.ReactNode }) {
|
||||||
void transportClient.close("context-dispose");
|
void transportClient.close("context-dispose");
|
||||||
setReadyState(READY_STATE.CLOSED);
|
setReadyState(READY_STATE.CLOSED);
|
||||||
setConnected(false);
|
setConnected(false);
|
||||||
|
setIdentified(false);
|
||||||
|
setIdentifying(false);
|
||||||
|
identificationStartedRef.current = false;
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!connected) {
|
||||||
|
setIdentifying(false);
|
||||||
|
setIdentified(false);
|
||||||
|
identificationStartedRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (identificationStartedRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
identificationStartedRef.current = true;
|
||||||
|
let cancelled = false;
|
||||||
|
setIdentifying(true);
|
||||||
|
setIdentified(false);
|
||||||
|
|
||||||
|
const identify = async () => {
|
||||||
|
try {
|
||||||
|
const userId = await load("user_id");
|
||||||
|
const privateKey = await load("private_key");
|
||||||
|
|
||||||
|
if (!Number.isSafeInteger(userId) || userId <= 0) {
|
||||||
|
throw new Error("Missing or invalid user id for identification");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (privateKey.trim() === "") {
|
||||||
|
throw new Error("Missing private key for identification");
|
||||||
|
}
|
||||||
|
|
||||||
|
const challengeEnvelope = await send("identification", {
|
||||||
|
user_id: userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sharedSecret = await get_shared_secret(
|
||||||
|
privateKey,
|
||||||
|
"",
|
||||||
|
challengeEnvelope.data.public_key,
|
||||||
|
);
|
||||||
|
|
||||||
|
const decryptedChallenge = await decrypt(
|
||||||
|
sharedSecret,
|
||||||
|
challengeEnvelope.data.challenge,
|
||||||
|
);
|
||||||
|
|
||||||
|
const verification = await send("challenge_response", {
|
||||||
|
challenge: decryptedChallenge,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (verification.data.accepted !== true) {
|
||||||
|
throw new Error("Identification challenge was rejected");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setError("");
|
||||||
|
setErrorDescription("");
|
||||||
|
setIdentified(true);
|
||||||
|
} catch (identificationError) {
|
||||||
|
if (cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isStopSendingError(identificationError)) {
|
||||||
|
setError("Connection closed");
|
||||||
|
setErrorDescription("Connection closed");
|
||||||
|
setIdentified(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFatal = isFatalIdentificationError(identificationError);
|
||||||
|
|
||||||
|
log(
|
||||||
|
isFatal ? 0 : 1,
|
||||||
|
"Socket",
|
||||||
|
isFatal ? "red" : "yellow",
|
||||||
|
"Identification handshake failed",
|
||||||
|
identificationError,
|
||||||
|
);
|
||||||
|
|
||||||
|
setIdentified(false);
|
||||||
|
|
||||||
|
setError("Identification Failed");
|
||||||
|
setErrorDescription(
|
||||||
|
isFatal
|
||||||
|
? "Unable to complete secure identification. Please verify your credentials and try again."
|
||||||
|
: "Unable to complete secure identification because the transport request failed.",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
if (!cancelled) {
|
||||||
|
setIdentifying(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void identify();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [connected, decrypt, get_shared_secret, load, send]);
|
||||||
|
|
||||||
const progress = React.useMemo(() => {
|
const progress = React.useMemo(() => {
|
||||||
if (readyState === READY_STATE.CONNECTING) return 70;
|
if (readyState === READY_STATE.CONNECTING) return 30;
|
||||||
if (!connected) return 90;
|
if (!connected) return 45;
|
||||||
|
if (identifying) return 75;
|
||||||
|
if (!identified) return 90;
|
||||||
return 100;
|
return 100;
|
||||||
}, [connected, readyState]);
|
}, [connected, identified, identifying, readyState]);
|
||||||
|
|
||||||
|
const loadingTitle = React.useMemo(() => {
|
||||||
|
if (readyState === READY_STATE.CONNECTING || !connected) {
|
||||||
|
return "Connecting to Tensamin";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (identifying || !identified) {
|
||||||
|
return "Identifying secure session";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Loading";
|
||||||
|
}, [connected, identified, identifying, readyState]);
|
||||||
|
|
||||||
|
const loadingDescription = React.useMemo(() => {
|
||||||
|
if (readyState === READY_STATE.CONNECTING || !connected) {
|
||||||
|
return "Establishing transport channel";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (identifying || !identified) {
|
||||||
|
return "Verifying challenge-response handshake";
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}, [connected, identified, identifying, readyState]);
|
||||||
|
|
||||||
const contextValue = React.useMemo<ContextType>(
|
const contextValue = React.useMemo<ContextType>(
|
||||||
() => ({
|
() => ({
|
||||||
|
|
@ -209,16 +441,24 @@ export default function Provider(props: { children: React.ReactNode }) {
|
||||||
readyState: () => readyState,
|
readyState: () => readyState,
|
||||||
ownPing: () => ownPing,
|
ownPing: () => ownPing,
|
||||||
iotaPing: () => iotaPing,
|
iotaPing: () => iotaPing,
|
||||||
|
identified: () => identified,
|
||||||
}),
|
}),
|
||||||
[iotaPing, ownPing, readyState, send],
|
[identified, iotaPing, ownPing, readyState, send],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (error !== "" && errorDescription !== "") {
|
if (error !== "" && errorDescription !== "") {
|
||||||
return <ErrorScreen error={error} description={errorDescription} />;
|
return <ErrorScreen error={error} description={errorDescription} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!connected) {
|
if (!connected || !identified) {
|
||||||
return <Loading progress={progress} />;
|
return (
|
||||||
|
<Loading
|
||||||
|
progress={progress}
|
||||||
|
title={loadingTitle}
|
||||||
|
description={loadingDescription}
|
||||||
|
fullscreen
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -234,4 +474,4 @@ export function useSocket(): ContextType {
|
||||||
throw new Error("useSocket must be used within a SocketProvider");
|
throw new Error("useSocket must be used within a SocketProvider");
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -269,6 +269,7 @@ registerDataKinds("number", [
|
||||||
"omikron_id",
|
"omikron_id",
|
||||||
"send_time",
|
"send_time",
|
||||||
"sub_level",
|
"sub_level",
|
||||||
|
"sub_end",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
registerDataKinds("string", [
|
registerDataKinds("string", [
|
||||||
|
|
@ -301,6 +302,7 @@ registerDataKinds("string", [
|
||||||
"new_token",
|
"new_token",
|
||||||
"call_token",
|
"call_token",
|
||||||
"challenge",
|
"challenge",
|
||||||
|
"online_status",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
registerDataKinds({ array: "container" }, [
|
registerDataKinds({ array: "container" }, [
|
||||||
|
|
@ -351,10 +353,8 @@ registerDataKinds("null", [
|
||||||
"watcher",
|
"watcher",
|
||||||
"created_at",
|
"created_at",
|
||||||
"status",
|
"status",
|
||||||
"sub_end",
|
|
||||||
"community_address",
|
"community_address",
|
||||||
"community_title",
|
"community_title",
|
||||||
"online_status",
|
|
||||||
"call_invited",
|
"call_invited",
|
||||||
"call_members",
|
"call_members",
|
||||||
"calls",
|
"calls",
|
||||||
|
|
@ -476,9 +476,31 @@ export function createTransportClient<T extends SchemaMap>(
|
||||||
notifyClosed(connection, error);
|
notifyClosed(connection, error);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const closeFromStopSending = (
|
||||||
|
connection: ActiveConnection,
|
||||||
|
error: unknown,
|
||||||
|
) => {
|
||||||
|
if (!isStopSendingError(error)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
connection.transport.close({
|
||||||
|
closeCode: APPLICATION_CLOSE_CODE,
|
||||||
|
reason: "stop-sending",
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Ignore close failures while handling STOP_SENDING.
|
||||||
|
}
|
||||||
|
|
||||||
|
handleConnectionFailure(connection, error);
|
||||||
|
};
|
||||||
|
|
||||||
const handleIncomingMessage = (message: TypedMessage) => {
|
const handleIncomingMessage = (message: TypedMessage) => {
|
||||||
if (message.type !== "pong") {
|
if (message.type !== "pong") {
|
||||||
log(2, "Socket", "blue", message.type, message.data);
|
log(2, "Socket", "blue", "Received:", message.type, message.data, {
|
||||||
|
id: message.id,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.id !== 0) {
|
if (message.id !== 0) {
|
||||||
|
|
@ -728,13 +750,20 @@ export function createTransportClient<T extends SchemaMap>(
|
||||||
payload = coercePayload(input ?? {});
|
payload = coercePayload(input ?? {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!options?.id && !options?.noResponse) {
|
||||||
|
const array = new Uint32Array(1);
|
||||||
|
crypto.getRandomValues(array);
|
||||||
|
options = options ?? {};
|
||||||
|
options.id = array[0];
|
||||||
|
}
|
||||||
|
|
||||||
if (type !== "ping") {
|
if (type !== "ping") {
|
||||||
log(2, "Socket", "purple", type, payload);
|
log(2, "Socket", "purple", "Sent:", type, payload, { id: options.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
const expectsResponse = !options?.noResponse;
|
const expectsResponse = !options?.noResponse;
|
||||||
const requestId = resolveRequestId(
|
const requestId = resolveRequestId(
|
||||||
options?.id,
|
options.id,
|
||||||
expectsResponse,
|
expectsResponse,
|
||||||
pending,
|
pending,
|
||||||
() => {
|
() => {
|
||||||
|
|
@ -751,7 +780,10 @@ export function createTransportClient<T extends SchemaMap>(
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!expectsResponse) {
|
if (!expectsResponse) {
|
||||||
return writeMessage(connection.transport, messageBytes);
|
return writeMessage(connection.transport, messageBytes).catch((error) => {
|
||||||
|
closeFromStopSending(connection, error);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise<TypedMessage>((resolve, reject) => {
|
return new Promise<TypedMessage>((resolve, reject) => {
|
||||||
|
|
@ -772,6 +804,7 @@ export function createTransportClient<T extends SchemaMap>(
|
||||||
});
|
});
|
||||||
|
|
||||||
void writeMessage(connection.transport, messageBytes).catch((error) => {
|
void writeMessage(connection.transport, messageBytes).catch((error) => {
|
||||||
|
closeFromStopSending(connection, error);
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
pending.delete(requestId);
|
pending.delete(requestId);
|
||||||
reject(error);
|
reject(error);
|
||||||
|
|
@ -829,6 +862,34 @@ function normalizeName(value: string) {
|
||||||
return value.toLowerCase().replaceAll("_", "");
|
return value.toLowerCase().replaceAll("_", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isStopSendingError(error: unknown) {
|
||||||
|
if (typeof error === "string") {
|
||||||
|
return error.includes("STOP_SENDING");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
if (error.message.includes("STOP_SENDING")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorWithCause = error as Error & { cause?: unknown };
|
||||||
|
if (errorWithCause.cause !== undefined) {
|
||||||
|
return isStopSendingError(errorWithCause.cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof error === "object" && error !== null) {
|
||||||
|
const maybeMessage = (error as { message?: unknown }).message;
|
||||||
|
if (typeof maybeMessage === "string") {
|
||||||
|
return maybeMessage.includes("STOP_SENDING");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function coercePayload(value: unknown): Record<string, unknown> {
|
function coercePayload(value: unknown): Record<string, unknown> {
|
||||||
if (!isPlainObject(value)) {
|
if (!isPlainObject(value)) {
|
||||||
throw new Error("Protocol payload must be a plain object");
|
throw new Error("Protocol payload must be a plain object");
|
||||||
|
|
@ -1175,7 +1236,9 @@ function encodeArrayPayload(
|
||||||
|
|
||||||
if (!isBoolKindMarker(encodedItem.kind)) {
|
if (!isBoolKindMarker(encodedItem.kind)) {
|
||||||
if (encodedItem.payload.byteLength > 0xffff) {
|
if (encodedItem.payload.byteLength > 0xffff) {
|
||||||
throw new Error(`Array item at "${path}" is too large for protocol encoding`);
|
throw new Error(
|
||||||
|
`Array item at "${path}" is too large for protocol encoding`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
totalLength += 2 + encodedItem.payload.byteLength;
|
totalLength += 2 + encodedItem.payload.byteLength;
|
||||||
|
|
@ -1222,10 +1285,13 @@ function encodeContainerPayload(value: Record<string, unknown>, path: string) {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (entries.length > 0xffff) {
|
if (entries.length > 0xffff) {
|
||||||
throw new Error(`Container at "${path}" has too many entries for protocol encoding`);
|
throw new Error(
|
||||||
|
`Container at "${path}" has too many entries for protocol encoding`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const encodedEntries: Array<{ keyIndex: number; value: EncodedDataValue }> = [];
|
const encodedEntries: Array<{ keyIndex: number; value: EncodedDataValue }> =
|
||||||
|
[];
|
||||||
let totalLength = 2;
|
let totalLength = 2;
|
||||||
|
|
||||||
for (const [name, entry] of entries) {
|
for (const [name, entry] of entries) {
|
||||||
|
|
@ -1242,7 +1308,9 @@ function encodeContainerPayload(value: Record<string, unknown>, path: string) {
|
||||||
totalLength += 2;
|
totalLength += 2;
|
||||||
} else {
|
} else {
|
||||||
if (encodedValue.payload.byteLength > 0xffff) {
|
if (encodedValue.payload.byteLength > 0xffff) {
|
||||||
throw new Error(`Container entry "${pathForEntry}" is too large for protocol encoding`);
|
throw new Error(
|
||||||
|
`Container entry "${pathForEntry}" is too large for protocol encoding`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
totalLength += 4 + encodedValue.payload.byteLength;
|
totalLength += 4 + encodedValue.payload.byteLength;
|
||||||
|
|
@ -1365,7 +1433,10 @@ function decodeContainerPayload(reader: ByteReader) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
value[key] = normalizeIncomingValue(key, decodeValuePayload(marker, payload));
|
value[key] = normalizeIncomingValue(
|
||||||
|
key,
|
||||||
|
decodeValuePayload(marker, payload),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
|
|
@ -1382,7 +1453,8 @@ function getDataTypeNameByIndex(index: number) {
|
||||||
|
|
||||||
function isBoolKindMarker(marker: number) {
|
function isBoolKindMarker(marker: number) {
|
||||||
return (
|
return (
|
||||||
marker === DATA_VALUE_KIND_BOOL_TRUE || marker === DATA_VALUE_KIND_BOOL_FALSE
|
marker === DATA_VALUE_KIND_BOOL_TRUE ||
|
||||||
|
marker === DATA_VALUE_KIND_BOOL_FALSE
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
25
packages/ui/components.json
Normal file
25
packages/ui/components.json
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "base-mira",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "src/index.css",
|
||||||
|
"baseColor": "mist",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"rtl": false,
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/cmp",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"menuColor": "default-translucent",
|
||||||
|
"menuAccent": "subtle",
|
||||||
|
"registries": {}
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,8 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
"./*": "./src/ui/*.tsx",
|
"./cmp/*": "./src/cmp/*.tsx",
|
||||||
|
"./theme": "./src/theme.tsx",
|
||||||
"./screens/*": "./src/screens/*.tsx",
|
"./screens/*": "./src/screens/*.tsx",
|
||||||
"./link": "./src/link.tsx"
|
"./link": "./src/link.tsx"
|
||||||
},
|
},
|
||||||
|
|
@ -14,13 +15,15 @@
|
||||||
"build": "tsc -p tsconfig.json --noEmit"
|
"build": "tsc -p tsconfig.json --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@base-ui/react": "^1.3.0",
|
||||||
"@radix-ui/react-context-menu": "^2.2.16",
|
"@radix-ui/react-context-menu": "^2.2.16",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.564.0",
|
"lucide-react": "^0.564.0",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"sonner": "^1.0.0",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.5.0"
|
"tailwind-merge": "^3.5.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
107
packages/ui/src/cmp/avatar.tsx
Normal file
107
packages/ui/src/cmp/avatar.tsx
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar"
|
||||||
|
|
||||||
|
import { cn } from "../lib/utils"
|
||||||
|
|
||||||
|
function Avatar({
|
||||||
|
className,
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}: AvatarPrimitive.Root.Props & {
|
||||||
|
size?: "default" | "sm" | "lg"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Root
|
||||||
|
data-slot="avatar"
|
||||||
|
data-size={size}
|
||||||
|
className={cn(
|
||||||
|
"group/avatar relative flex size-8 shrink-0 rounded-full select-none after:absolute after:inset-0 after:rounded-full after:border after:border-border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Image
|
||||||
|
data-slot="avatar-image"
|
||||||
|
className={cn(
|
||||||
|
"aspect-square size-full rounded-full object-cover",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarFallback({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: AvatarPrimitive.Fallback.Props) {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Fallback
|
||||||
|
data-slot="avatar-fallback"
|
||||||
|
className={cn(
|
||||||
|
"flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground group-data-[size=sm]/avatar:text-xs",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="avatar-badge"
|
||||||
|
className={cn(
|
||||||
|
"absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground bg-blend-color ring-2 ring-background select-none",
|
||||||
|
"group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
|
||||||
|
"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
|
||||||
|
"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="avatar-group"
|
||||||
|
className={cn(
|
||||||
|
"group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarGroupCount({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="avatar-group-count"
|
||||||
|
className={cn(
|
||||||
|
"relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-xs/relaxed text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Avatar,
|
||||||
|
AvatarImage,
|
||||||
|
AvatarFallback,
|
||||||
|
AvatarGroup,
|
||||||
|
AvatarGroupCount,
|
||||||
|
AvatarBadge,
|
||||||
|
}
|
||||||
56
packages/ui/src/cmp/button.tsx
Normal file
56
packages/ui/src/cmp/button.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { Button as ButtonPrimitive } from "@base-ui/react/button"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "../lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"group/button inline-flex shrink-0 items-center justify-center rounded-md border border-transparent bg-clip-padding text-xs/relaxed font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30 active:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/80",
|
||||||
|
outline:
|
||||||
|
"border-border hover:bg-input/50 hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:bg-input/30",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
||||||
|
ghost:
|
||||||
|
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default:
|
||||||
|
"h-7 gap-1 px-2 text-xs/relaxed has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
||||||
|
xs: "h-5 gap-1 rounded-sm px-2 text-[0.625rem] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-2.5",
|
||||||
|
sm: "h-6 gap-1 px-2 text-xs/relaxed has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||||
|
lg: "h-8 gap-1 px-2.5 text-xs/relaxed has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
icon: "size-7 [&_svg:not([class*='size-'])]:size-3.5",
|
||||||
|
"icon-xs": "size-5 rounded-sm [&_svg:not([class*='size-'])]:size-2.5",
|
||||||
|
"icon-sm": "size-6 [&_svg:not([class*='size-'])]:size-3",
|
||||||
|
"icon-lg": "size-8 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Button({
|
||||||
|
className,
|
||||||
|
variant = "default",
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
|
||||||
|
return (
|
||||||
|
<ButtonPrimitive
|
||||||
|
data-slot="button"
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
100
packages/ui/src/cmp/card.tsx
Normal file
100
packages/ui/src/cmp/card.tsx
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "../lib/utils"
|
||||||
|
|
||||||
|
function Card({
|
||||||
|
className,
|
||||||
|
size = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card"
|
||||||
|
data-size={size}
|
||||||
|
className={cn(
|
||||||
|
"group/card flex flex-col gap-4 overflow-hidden rounded-lg bg-card py-4 text-xs/relaxed text-card-foreground ring-1 ring-foreground/10 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 *:[img:first-child]:rounded-t-lg *:[img:last-child]:rounded-b-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-header"
|
||||||
|
className={cn(
|
||||||
|
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-lg px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-title"
|
||||||
|
className={cn("text-sm font-medium", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-description"
|
||||||
|
className={cn("text-xs/relaxed text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-action"
|
||||||
|
className={cn(
|
||||||
|
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-content"
|
||||||
|
className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-footer"
|
||||||
|
className={cn(
|
||||||
|
"flex items-center rounded-b-lg px-4 group-data-[size=sm]/card:px-3 [.border-t]:pt-4 group-data-[size=sm]/card:[.border-t]:pt-3",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardFooter,
|
||||||
|
CardTitle,
|
||||||
|
CardAction,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
}
|
||||||
27
packages/ui/src/cmp/checkbox.tsx
Normal file
27
packages/ui/src/cmp/checkbox.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"
|
||||||
|
|
||||||
|
import { cn } from "../lib/utils"
|
||||||
|
import { CheckIcon } from "lucide-react"
|
||||||
|
|
||||||
|
function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
|
||||||
|
return (
|
||||||
|
<CheckboxPrimitive.Root
|
||||||
|
data-slot="checkbox"
|
||||||
|
className={cn(
|
||||||
|
"peer relative flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input transition-shadow outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<CheckboxPrimitive.Indicator
|
||||||
|
data-slot="checkbox-indicator"
|
||||||
|
className="grid place-content-center text-current transition-none [&>svg]:size-3.5"
|
||||||
|
>
|
||||||
|
<CheckIcon
|
||||||
|
/>
|
||||||
|
</CheckboxPrimitive.Indicator>
|
||||||
|
</CheckboxPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Checkbox }
|
||||||
269
packages/ui/src/cmp/context-menu.tsx
Normal file
269
packages/ui/src/cmp/context-menu.tsx
Normal file
|
|
@ -0,0 +1,269 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import { ContextMenu as ContextMenuPrimitive } from "@base-ui/react/context-menu"
|
||||||
|
|
||||||
|
import { cn } from "../lib/utils"
|
||||||
|
import { ChevronRightIcon, CheckIcon } from "lucide-react"
|
||||||
|
|
||||||
|
function ContextMenu({ ...props }: ContextMenuPrimitive.Root.Props) {
|
||||||
|
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuPortal({ ...props }: ContextMenuPrimitive.Portal.Props) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuTrigger({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: ContextMenuPrimitive.Trigger.Props) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Trigger
|
||||||
|
data-slot="context-menu-trigger"
|
||||||
|
className={cn("select-none", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuContent({
|
||||||
|
className,
|
||||||
|
align = "start",
|
||||||
|
alignOffset = 4,
|
||||||
|
side = "right",
|
||||||
|
sideOffset = 0,
|
||||||
|
...props
|
||||||
|
}: ContextMenuPrimitive.Popup.Props &
|
||||||
|
Pick<
|
||||||
|
ContextMenuPrimitive.Positioner.Props,
|
||||||
|
"align" | "alignOffset" | "side" | "sideOffset"
|
||||||
|
>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Portal>
|
||||||
|
<ContextMenuPrimitive.Positioner
|
||||||
|
className="isolate z-50 outline-none"
|
||||||
|
align={align}
|
||||||
|
alignOffset={alignOffset}
|
||||||
|
side={side}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
>
|
||||||
|
<ContextMenuPrimitive.Popup
|
||||||
|
data-slot="context-menu-content"
|
||||||
|
className={cn("z-50 max-h-(--available-height) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 animate-none! relative bg-popover/70 before:pointer-events-none before:absolute before:inset-0 before:-z-1 before:rounded-[inherit] before:backdrop-blur-2xl before:backdrop-saturate-150 **:data-[slot$=-item]:focus:bg-foreground/10 **:data-[slot$=-item]:data-highlighted:bg-foreground/10 **:data-[slot$=-separator]:bg-foreground/5 **:data-[slot$=-trigger]:focus:bg-foreground/10 **:data-[slot$=-trigger]:aria-expanded:bg-foreground/10! **:data-[variant=destructive]:focus:bg-foreground/10! **:data-[variant=destructive]:text-accent-foreground! **:data-[variant=destructive]:**:text-accent-foreground!", className )}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</ContextMenuPrimitive.Positioner>
|
||||||
|
</ContextMenuPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuGroup({ ...props }: ContextMenuPrimitive.Group.Props) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuLabel({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
...props
|
||||||
|
}: ContextMenuPrimitive.GroupLabel.Props & {
|
||||||
|
inset?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.GroupLabel
|
||||||
|
data-slot="context-menu-label"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"px-2 py-1.5 text-xs text-muted-foreground data-inset:pl-7.5",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuItem({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
variant = "default",
|
||||||
|
...props
|
||||||
|
}: ContextMenuPrimitive.Item.Props & {
|
||||||
|
inset?: boolean
|
||||||
|
variant?: "default" | "destructive"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Item
|
||||||
|
data-slot="context-menu-item"
|
||||||
|
data-inset={inset}
|
||||||
|
data-variant={variant}
|
||||||
|
className={cn(
|
||||||
|
"group/context-menu-item relative flex min-h-7 cursor-default items-center gap-2 rounded-md px-2 py-1 text-xs/relaxed outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7.5 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5 data-[variant=destructive]:*:[svg]:text-destructive",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuSub({ ...props }: ContextMenuPrimitive.SubmenuRoot.Props) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.SubmenuRoot data-slot="context-menu-sub" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuSubTrigger({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: ContextMenuPrimitive.SubmenuTrigger.Props & {
|
||||||
|
inset?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.SubmenuTrigger
|
||||||
|
data-slot="context-menu-sub-trigger"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"flex min-h-7 cursor-default items-center gap-2 rounded-md px-2 py-1 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7.5 data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRightIcon className="ml-auto" />
|
||||||
|
</ContextMenuPrimitive.SubmenuTrigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuSubContent({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ContextMenuContent>) {
|
||||||
|
return (
|
||||||
|
<ContextMenuContent
|
||||||
|
data-slot="context-menu-sub-content"
|
||||||
|
className="shadow-lg animate-none! relative bg-popover/70 before:pointer-events-none before:absolute before:inset-0 before:-z-1 before:rounded-[inherit] before:backdrop-blur-2xl before:backdrop-saturate-150 **:data-[slot$=-item]:focus:bg-foreground/10 **:data-[slot$=-item]:data-highlighted:bg-foreground/10 **:data-[slot$=-separator]:bg-foreground/5 **:data-[slot$=-trigger]:focus:bg-foreground/10 **:data-[slot$=-trigger]:aria-expanded:bg-foreground/10! **:data-[variant=destructive]:focus:bg-foreground/10! **:data-[variant=destructive]:text-accent-foreground! **:data-[variant=destructive]:**:text-accent-foreground!"
|
||||||
|
side="right"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuCheckboxItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
checked,
|
||||||
|
inset,
|
||||||
|
...props
|
||||||
|
}: ContextMenuPrimitive.CheckboxItem.Props & {
|
||||||
|
inset?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.CheckboxItem
|
||||||
|
data-slot="context-menu-checkbox-item"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"relative flex min-h-7 cursor-default items-center gap-2 rounded-md py-1.5 pr-8 pl-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7.5 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="pointer-events-none absolute right-2 flex items-center justify-center">
|
||||||
|
<ContextMenuPrimitive.CheckboxItemIndicator>
|
||||||
|
<CheckIcon
|
||||||
|
/>
|
||||||
|
</ContextMenuPrimitive.CheckboxItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</ContextMenuPrimitive.CheckboxItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuRadioGroup({
|
||||||
|
...props
|
||||||
|
}: ContextMenuPrimitive.RadioGroup.Props) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.RadioGroup
|
||||||
|
data-slot="context-menu-radio-group"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuRadioItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
inset,
|
||||||
|
...props
|
||||||
|
}: ContextMenuPrimitive.RadioItem.Props & {
|
||||||
|
inset?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.RadioItem
|
||||||
|
data-slot="context-menu-radio-item"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"relative flex min-h-7 cursor-default items-center gap-2 rounded-md py-1.5 pr-8 pl-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7.5 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="pointer-events-none absolute right-2 flex items-center justify-center">
|
||||||
|
<ContextMenuPrimitive.RadioItemIndicator>
|
||||||
|
<CheckIcon
|
||||||
|
/>
|
||||||
|
</ContextMenuPrimitive.RadioItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</ContextMenuPrimitive.RadioItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: ContextMenuPrimitive.Separator.Props) {
|
||||||
|
return (
|
||||||
|
<ContextMenuPrimitive.Separator
|
||||||
|
data-slot="context-menu-separator"
|
||||||
|
className={cn("-mx-1 my-1 h-px bg-border/50", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuShortcut({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="context-menu-shortcut"
|
||||||
|
className={cn(
|
||||||
|
"ml-auto text-[0.625rem] tracking-widest text-muted-foreground group-focus/context-menu-item:text-accent-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuTrigger,
|
||||||
|
ContextMenuContent,
|
||||||
|
ContextMenuItem,
|
||||||
|
ContextMenuCheckboxItem,
|
||||||
|
ContextMenuRadioItem,
|
||||||
|
ContextMenuLabel,
|
||||||
|
ContextMenuSeparator,
|
||||||
|
ContextMenuShortcut,
|
||||||
|
ContextMenuGroup,
|
||||||
|
ContextMenuPortal,
|
||||||
|
ContextMenuSub,
|
||||||
|
ContextMenuSubContent,
|
||||||
|
ContextMenuSubTrigger,
|
||||||
|
ContextMenuRadioGroup,
|
||||||
|
}
|
||||||
20
packages/ui/src/cmp/input.tsx
Normal file
20
packages/ui/src/cmp/input.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import { Input as InputPrimitive } from "@base-ui/react/input"
|
||||||
|
|
||||||
|
import { cn } from "../lib/utils"
|
||||||
|
|
||||||
|
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||||
|
return (
|
||||||
|
<InputPrimitive
|
||||||
|
type={type}
|
||||||
|
data-slot="input"
|
||||||
|
className={cn(
|
||||||
|
"h-7 w-full min-w-0 rounded-md border border-input bg-input/20 px-2 py-0.5 text-sm transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-xs/relaxed file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 md:text-xs/relaxed dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Input }
|
||||||
18
packages/ui/src/cmp/label.tsx
Normal file
18
packages/ui/src/cmp/label.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "../lib/utils"
|
||||||
|
|
||||||
|
function Label({ className, ...props }: React.ComponentProps<"label">) {
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
data-slot="label"
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 text-xs/relaxed leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Label }
|
||||||
47
packages/ui/src/cmp/sonner.tsx
Normal file
47
packages/ui/src/cmp/sonner.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { useTheme } from "next-themes"
|
||||||
|
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
||||||
|
import { CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon } from "lucide-react"
|
||||||
|
|
||||||
|
const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
|
const { theme = "system" } = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sonner
|
||||||
|
theme={theme as ToasterProps["theme"]}
|
||||||
|
className="toaster group"
|
||||||
|
icons={{
|
||||||
|
success: (
|
||||||
|
<CircleCheckIcon className="size-4" />
|
||||||
|
),
|
||||||
|
info: (
|
||||||
|
<InfoIcon className="size-4" />
|
||||||
|
),
|
||||||
|
warning: (
|
||||||
|
<TriangleAlertIcon className="size-4" />
|
||||||
|
),
|
||||||
|
error: (
|
||||||
|
<OctagonXIcon className="size-4" />
|
||||||
|
),
|
||||||
|
loading: (
|
||||||
|
<Loader2Icon className="size-4 animate-spin" />
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--normal-bg": "var(--popover)",
|
||||||
|
"--normal-text": "var(--popover-foreground)",
|
||||||
|
"--normal-border": "var(--border)",
|
||||||
|
"--border-radius": "var(--radius)",
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
toastOptions={{
|
||||||
|
classNames: {
|
||||||
|
toast: "cn-toast",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Toaster }
|
||||||
129
packages/ui/src/index.css
Normal file
129
packages/ui/src/index.css
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
@import "tailwindcss";
|
||||||
|
@import "tw-animate-css";
|
||||||
|
@import "shadcn/tailwind.css";
|
||||||
|
@import "@fontsource-variable/inter";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.148 0.004 228.8);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.148 0.004 228.8);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.148 0.004 228.8);
|
||||||
|
--primary: oklch(0.511 0.096 186.391);
|
||||||
|
--primary-foreground: oklch(0.984 0.014 180.72);
|
||||||
|
--secondary: oklch(0.967 0.001 286.375);
|
||||||
|
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||||
|
--muted: oklch(0.963 0.002 197.1);
|
||||||
|
--muted-foreground: oklch(0.56 0.021 213.5);
|
||||||
|
--accent: oklch(0.963 0.002 197.1);
|
||||||
|
--accent-foreground: oklch(0.218 0.008 223.9);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.925 0.005 214.3);
|
||||||
|
--input: oklch(0.925 0.005 214.3);
|
||||||
|
--ring: oklch(0.723 0.014 214.4);
|
||||||
|
--chart-1: oklch(0.855 0.138 181.071);
|
||||||
|
--chart-2: oklch(0.704 0.14 182.503);
|
||||||
|
--chart-3: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-4: oklch(0.511 0.096 186.391);
|
||||||
|
--chart-5: oklch(0.437 0.078 188.216);
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--sidebar: oklch(0.987 0.002 197.1);
|
||||||
|
--sidebar-foreground: oklch(0.148 0.004 228.8);
|
||||||
|
--sidebar-primary: oklch(0.6 0.118 184.704);
|
||||||
|
--sidebar-primary-foreground: oklch(0.984 0.014 180.72);
|
||||||
|
--sidebar-accent: oklch(0.963 0.002 197.1);
|
||||||
|
--sidebar-accent-foreground: oklch(0.218 0.008 223.9);
|
||||||
|
--sidebar-border: oklch(0.925 0.005 214.3);
|
||||||
|
--sidebar-ring: oklch(0.723 0.014 214.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.148 0.004 228.8);
|
||||||
|
--foreground: oklch(0.987 0.002 197.1);
|
||||||
|
--card: oklch(0.218 0.008 223.9);
|
||||||
|
--card-foreground: oklch(0.987 0.002 197.1);
|
||||||
|
--popover: oklch(0.218 0.008 223.9);
|
||||||
|
--popover-foreground: oklch(0.987 0.002 197.1);
|
||||||
|
--primary: oklch(0.437 0.078 188.216);
|
||||||
|
--primary-foreground: oklch(0.984 0.014 180.72);
|
||||||
|
--secondary: oklch(0.274 0.006 286.033);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.275 0.011 216.9);
|
||||||
|
--muted-foreground: oklch(0.723 0.014 214.4);
|
||||||
|
--accent: oklch(0.275 0.011 216.9);
|
||||||
|
--accent-foreground: oklch(0.987 0.002 197.1);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.56 0.021 213.5);
|
||||||
|
--chart-1: oklch(0.855 0.138 181.071);
|
||||||
|
--chart-2: oklch(0.704 0.14 182.503);
|
||||||
|
--chart-3: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-4: oklch(0.511 0.096 186.391);
|
||||||
|
--chart-5: oklch(0.437 0.078 188.216);
|
||||||
|
--sidebar: oklch(0.218 0.008 223.9);
|
||||||
|
--sidebar-foreground: oklch(0.987 0.002 197.1);
|
||||||
|
--sidebar-primary: oklch(0.704 0.14 182.503);
|
||||||
|
--sidebar-primary-foreground: oklch(0.277 0.046 192.524);
|
||||||
|
--sidebar-accent: oklch(0.275 0.011 216.9);
|
||||||
|
--sidebar-accent-foreground: oklch(0.987 0.002 197.1);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
|
--sidebar-ring: oklch(0.56 0.021 213.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--font-sans: 'Inter Variable', sans-serif;
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-background: var(--background);
|
||||||
|
--radius-sm: calc(var(--radius) * 0.6);
|
||||||
|
--radius-md: calc(var(--radius) * 0.8);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) * 1.4);
|
||||||
|
--radius-2xl: calc(var(--radius) * 1.8);
|
||||||
|
--radius-3xl: calc(var(--radius) * 2.2);
|
||||||
|
--radius-4xl: calc(var(--radius) * 2.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
@apply font-sans;
|
||||||
|
}
|
||||||
|
}
|
||||||
6
packages/ui/src/lib/utils.ts
Normal file
6
packages/ui/src/lib/utils.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { clsx, type ClassValue } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
import type { ClassValue } from "clsx";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import { twMerge } from "tailwind-merge";
|
|
||||||
|
|
||||||
export const cn = (...classLists: ClassValue[]) => twMerge(clsx(classLists));
|
|
||||||
|
|
@ -3,8 +3,8 @@ import Link from "../link";
|
||||||
export default function Screen(props: { error: string; description: string }) {
|
export default function Screen(props: { error: string; description: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-background w-full h-screen flex flex-col justify-center items-center">
|
<div className="bg-background w-full h-screen flex flex-col justify-center items-center">
|
||||||
<p className="text-3xl font-bold">{props.error}</p>
|
<p className="text-base font-semibold">{props.error}</p>
|
||||||
<p className="pt-4 text-lg w-1/2 text-center text-muted-foreground pb-8">
|
<p className="pt-4 text-sm w-1/2 text-center text-muted-foreground pb-8">
|
||||||
{props.description}
|
{props.description}
|
||||||
</p>
|
</p>
|
||||||
<Link label="status.tensamin.net" link="https://status.tensamin.net" />
|
<Link label="status.tensamin.net" link="https://status.tensamin.net" />
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,14 @@ import * as React from "react";
|
||||||
|
|
||||||
const DELAY = 250;
|
const DELAY = 250;
|
||||||
|
|
||||||
export default function Screen(props: { progress: number }) {
|
type ScreenProps = {
|
||||||
|
progress: number;
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
fullscreen?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Screen(props: ScreenProps) {
|
||||||
const [displayProgress, setDisplayProgress] = React.useState(0);
|
const [displayProgress, setDisplayProgress] = React.useState(0);
|
||||||
const displayProgressRef = React.useRef(0);
|
const displayProgressRef = React.useRef(0);
|
||||||
|
|
||||||
|
|
@ -41,8 +48,24 @@ export default function Screen(props: { progress: number }) {
|
||||||
};
|
};
|
||||||
}, [props.progress]);
|
}, [props.progress]);
|
||||||
|
|
||||||
|
const wrapperClass = props.fullscreen
|
||||||
|
? "fixed inset-0 z-50 bg-background"
|
||||||
|
: "bg-background w-full h-screen";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-background w-full h-screen flex flex-col justify-center items-center">
|
<div
|
||||||
|
className={`${wrapperClass} flex flex-col justify-center items-center px-6`}
|
||||||
|
>
|
||||||
|
<h2 className="text-base font-semibold text-foreground">
|
||||||
|
{props.title ?? "Loading"}
|
||||||
|
</h2>
|
||||||
|
{props.description ? (
|
||||||
|
<p className="text-sm text-muted-foreground mt-1 mb-5">
|
||||||
|
{props.description}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div className="mb-5" />
|
||||||
|
)}
|
||||||
<div className="w-64 h-1.5 bg-secondary rounded-full overflow-hidden relative">
|
<div className="w-64 h-1.5 bg-secondary rounded-full overflow-hidden relative">
|
||||||
<div
|
<div
|
||||||
className="h-full bg-primary absolute left-0 top-0"
|
className="h-full bg-primary absolute left-0 top-0"
|
||||||
|
|
|
||||||
229
packages/ui/src/theme.tsx
Normal file
229
packages/ui/src/theme.tsx
Normal file
|
|
@ -0,0 +1,229 @@
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
type Theme = "dark" | "light" | "system"
|
||||||
|
type ResolvedTheme = "dark" | "light"
|
||||||
|
|
||||||
|
type ThemeProviderProps = {
|
||||||
|
children: React.ReactNode
|
||||||
|
defaultTheme?: Theme
|
||||||
|
storageKey?: string
|
||||||
|
disableTransitionOnChange?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type ThemeProviderState = {
|
||||||
|
theme: Theme
|
||||||
|
setTheme: (theme: Theme) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const COLOR_SCHEME_QUERY = "(prefers-color-scheme: dark)"
|
||||||
|
const THEME_VALUES: Theme[] = ["dark", "light", "system"]
|
||||||
|
|
||||||
|
const ThemeProviderContext = React.createContext<
|
||||||
|
ThemeProviderState | undefined
|
||||||
|
>(undefined)
|
||||||
|
|
||||||
|
function isTheme(value: string | null): value is Theme {
|
||||||
|
if (value === null) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return THEME_VALUES.includes(value as Theme)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSystemTheme(): ResolvedTheme {
|
||||||
|
if (window.matchMedia(COLOR_SCHEME_QUERY).matches) {
|
||||||
|
return "dark"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "light"
|
||||||
|
}
|
||||||
|
|
||||||
|
function disableTransitionsTemporarily() {
|
||||||
|
const style = document.createElement("style")
|
||||||
|
style.appendChild(
|
||||||
|
document.createTextNode(
|
||||||
|
"*,*::before,*::after{-webkit-transition:none!important;transition:none!important}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
document.head.appendChild(style)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.getComputedStyle(document.body)
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
style.remove()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEditableTarget(target: EventTarget | null) {
|
||||||
|
if (!(target instanceof HTMLElement)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target.isContentEditable) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const editableParent = target.closest(
|
||||||
|
"input, textarea, select, [contenteditable='true']"
|
||||||
|
)
|
||||||
|
if (editableParent) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ThemeProvider({
|
||||||
|
children,
|
||||||
|
defaultTheme = "system",
|
||||||
|
storageKey = "theme",
|
||||||
|
disableTransitionOnChange = true,
|
||||||
|
...props
|
||||||
|
}: ThemeProviderProps) {
|
||||||
|
const [theme, setThemeState] = React.useState<Theme>(() => {
|
||||||
|
const storedTheme = localStorage.getItem(storageKey)
|
||||||
|
if (isTheme(storedTheme)) {
|
||||||
|
return storedTheme
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultTheme
|
||||||
|
})
|
||||||
|
|
||||||
|
const setTheme = React.useCallback(
|
||||||
|
(nextTheme: Theme) => {
|
||||||
|
localStorage.setItem(storageKey, nextTheme)
|
||||||
|
setThemeState(nextTheme)
|
||||||
|
},
|
||||||
|
[storageKey]
|
||||||
|
)
|
||||||
|
|
||||||
|
const applyTheme = React.useCallback(
|
||||||
|
(nextTheme: Theme) => {
|
||||||
|
const root = document.documentElement
|
||||||
|
const resolvedTheme =
|
||||||
|
nextTheme === "system" ? getSystemTheme() : nextTheme
|
||||||
|
const restoreTransitions = disableTransitionOnChange
|
||||||
|
? disableTransitionsTemporarily()
|
||||||
|
: null
|
||||||
|
|
||||||
|
root.classList.remove("light", "dark")
|
||||||
|
root.classList.add(resolvedTheme)
|
||||||
|
|
||||||
|
if (restoreTransitions) {
|
||||||
|
restoreTransitions()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[disableTransitionOnChange]
|
||||||
|
)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
applyTheme(theme)
|
||||||
|
|
||||||
|
if (theme !== "system") {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const mediaQuery = window.matchMedia(COLOR_SCHEME_QUERY)
|
||||||
|
const handleChange = () => {
|
||||||
|
applyTheme("system")
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaQuery.addEventListener("change", handleChange)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mediaQuery.removeEventListener("change", handleChange)
|
||||||
|
}
|
||||||
|
}, [theme, applyTheme])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.repeat) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.metaKey || event.ctrlKey || event.altKey) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEditableTarget(event.target)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key.toLowerCase() !== "d") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setThemeState((currentTheme) => {
|
||||||
|
const nextTheme =
|
||||||
|
currentTheme === "dark"
|
||||||
|
? "light"
|
||||||
|
: currentTheme === "light"
|
||||||
|
? "dark"
|
||||||
|
: getSystemTheme() === "dark"
|
||||||
|
? "light"
|
||||||
|
: "dark"
|
||||||
|
|
||||||
|
localStorage.setItem(storageKey, nextTheme)
|
||||||
|
return nextTheme
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleKeyDown)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", handleKeyDown)
|
||||||
|
}
|
||||||
|
}, [storageKey])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const handleStorageChange = (event: StorageEvent) => {
|
||||||
|
if (event.storageArea !== localStorage) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key !== storageKey) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTheme(event.newValue)) {
|
||||||
|
setThemeState(event.newValue)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setThemeState(defaultTheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("storage", handleStorageChange)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("storage", handleStorageChange)
|
||||||
|
}
|
||||||
|
}, [defaultTheme, storageKey])
|
||||||
|
|
||||||
|
const value = React.useMemo(
|
||||||
|
() => ({
|
||||||
|
theme,
|
||||||
|
setTheme,
|
||||||
|
}),
|
||||||
|
[theme, setTheme]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeProviderContext.Provider {...props} value={value}>
|
||||||
|
{children}
|
||||||
|
</ThemeProviderContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useTheme = () => {
|
||||||
|
const context = React.useContext(ThemeProviderContext)
|
||||||
|
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error("useTheme must be used within a ThemeProvider")
|
||||||
|
}
|
||||||
|
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
export default function Avatar(props: { img?: string; fallback: string }) {
|
|
||||||
const [imageFailed, setImageFailed] = React.useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="m-0 bg-card rounded-full border border-input/75 size-9 aspect-square flex items-center justify-center select-none overflow-hidden">
|
|
||||||
{props.img && !imageFailed ? (
|
|
||||||
<img
|
|
||||||
className="rounded-full w-full h-full object-cover"
|
|
||||||
src={props.img}
|
|
||||||
alt={props.fallback}
|
|
||||||
onError={() => setImageFailed(true)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<span>{props.fallback}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
import { cn } from "../libs/cn";
|
|
||||||
import type { VariantProps } from "class-variance-authority";
|
|
||||||
import { cva } from "class-variance-authority";
|
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
export const buttonVariants = cva(
|
|
||||||
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-[color,background-color,box-shadow] focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
variant: {
|
|
||||||
default:
|
|
||||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
|
||||||
destructive:
|
|
||||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
|
||||||
outline:
|
|
||||||
"border bg-card shadow-sm hover:bg-accent hover:text-accent-foreground",
|
|
||||||
secondary:
|
|
||||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
|
||||||
},
|
|
||||||
size: {
|
|
||||||
default: "h-9 px-4 py-2",
|
|
||||||
sm: "h-8 rounded-md px-3 text-xs",
|
|
||||||
lg: "h-10 rounded-md px-8",
|
|
||||||
icon: "h-9 w-9",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
variant: "default",
|
|
||||||
size: "default",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
|
|
||||||
VariantProps<typeof buttonVariants> & {
|
|
||||||
className?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
||||||
({ className, variant, size, ...props }, ref) => {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
ref={ref}
|
|
||||||
className={cn(buttonVariants({ variant, size }), className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
Button.displayName = "Button";
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
import { cn } from "../libs/cn";
|
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
export const Card = ({ className, ...props }: React.ComponentProps<"div">) => (
|
|
||||||
<div
|
|
||||||
className={cn("rounded-xl border bg-card text-card-foreground shadow", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const CardHeader = ({ className, ...props }: React.ComponentProps<"div">) => (
|
|
||||||
<div className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
|
|
||||||
);
|
|
||||||
|
|
||||||
export const CardTitle = ({ className, ...props }: React.ComponentProps<"h1">) => (
|
|
||||||
<h1
|
|
||||||
className={cn("font-semibold leading-none tracking-tight", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const CardDescription = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"h3">) => (
|
|
||||||
<h3 className={cn("text-sm text-muted-foreground", className)} {...props} />
|
|
||||||
);
|
|
||||||
|
|
||||||
export const CardContent = ({ className, ...props }: React.ComponentProps<"div">) => (
|
|
||||||
<div className={cn("p-6 pt-0", className)} {...props} />
|
|
||||||
);
|
|
||||||
|
|
||||||
export const CardFooter = ({ className, ...props }: React.ComponentProps<"div">) => (
|
|
||||||
<div className={cn("flex items-center p-6 pt-0", className)} {...props} />
|
|
||||||
);
|
|
||||||
|
|
@ -1,104 +0,0 @@
|
||||||
import * as React from "react";
|
|
||||||
import { cn } from "../libs/cn";
|
|
||||||
|
|
||||||
type CheckboxContextValue = {
|
|
||||||
checked: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const CheckboxContext = React.createContext<CheckboxContextValue | null>(null);
|
|
||||||
|
|
||||||
export type CheckboxProps = {
|
|
||||||
checked: boolean;
|
|
||||||
onChange: (checked: boolean) => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
className?: string;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function Checkbox({
|
|
||||||
checked,
|
|
||||||
onChange,
|
|
||||||
disabled,
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
}: CheckboxProps) {
|
|
||||||
return (
|
|
||||||
<CheckboxContext.Provider value={{ checked, disabled }}>
|
|
||||||
<label className={cn("inline-flex items-center", className)}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="sr-only peer"
|
|
||||||
checked={checked}
|
|
||||||
disabled={disabled}
|
|
||||||
onChange={(event) => onChange(event.currentTarget.checked)}
|
|
||||||
/>
|
|
||||||
{children}
|
|
||||||
</label>
|
|
||||||
</CheckboxContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CheckboxControl({
|
|
||||||
className,
|
|
||||||
}: {
|
|
||||||
className?: string;
|
|
||||||
}): React.ReactElement {
|
|
||||||
const context = React.useContext(CheckboxContext);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"h-4 w-4 shrink-0 rounded-sm border border-primary shadow transition-shadow peer-focus-visible:ring-[1.5px] peer-focus-visible:ring-ring peer-disabled:cursor-not-allowed",
|
|
||||||
context?.checked && "bg-primary text-primary-foreground",
|
|
||||||
context?.disabled && "opacity-50",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{context?.checked ? (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
className="h-4 w-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="2"
|
|
||||||
d="m5 12l5 5L20 7"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
) : null}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CheckboxLabel({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"span">) {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={cn("text-sm leading-none font-medium", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CheckboxErrorMessage({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"p">) {
|
|
||||||
return <p className={cn("text-sm text-destructive", className)} {...props} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CheckboxDescription({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"p">) {
|
|
||||||
return (
|
|
||||||
<p className={cn("text-sm text-muted-foreground", className)} {...props} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,182 +0,0 @@
|
||||||
import * as React from "react";
|
|
||||||
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
|
|
||||||
import { cn } from "../libs/cn";
|
|
||||||
|
|
||||||
export const ContextMenu = ContextMenuPrimitive.Root;
|
|
||||||
export const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
|
|
||||||
export const ContextMenuGroup = ContextMenuPrimitive.Group;
|
|
||||||
export const ContextMenuSub = ContextMenuPrimitive.Sub;
|
|
||||||
export const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
|
|
||||||
|
|
||||||
export const ContextMenuSubTrigger = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
|
|
||||||
inset?: boolean;
|
|
||||||
}
|
|
||||||
>(({ className, inset, children, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.SubTrigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground",
|
|
||||||
inset && "pl-8",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</ContextMenuPrimitive.SubTrigger>
|
|
||||||
));
|
|
||||||
|
|
||||||
ContextMenuSubTrigger.displayName = "ContextMenuSubTrigger";
|
|
||||||
|
|
||||||
export const ContextMenuSubContent = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.Portal>
|
|
||||||
<ContextMenuPrimitive.SubContent
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</ContextMenuPrimitive.Portal>
|
|
||||||
));
|
|
||||||
|
|
||||||
ContextMenuSubContent.displayName = "ContextMenuSubContent";
|
|
||||||
|
|
||||||
export const ContextMenuContent = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.Portal>
|
|
||||||
<ContextMenuPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</ContextMenuPrimitive.Portal>
|
|
||||||
));
|
|
||||||
|
|
||||||
ContextMenuContent.displayName = "ContextMenuContent";
|
|
||||||
|
|
||||||
export const ContextMenuItem = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
|
|
||||||
inset?: boolean;
|
|
||||||
}
|
|
||||||
>(({ className, inset, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.Item
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground",
|
|
||||||
inset && "pl-8",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
|
|
||||||
ContextMenuItem.displayName = "ContextMenuItem";
|
|
||||||
|
|
||||||
export const ContextMenuCheckboxItem = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.CheckboxItem
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</ContextMenuPrimitive.CheckboxItem>
|
|
||||||
));
|
|
||||||
|
|
||||||
ContextMenuCheckboxItem.displayName = "ContextMenuCheckboxItem";
|
|
||||||
|
|
||||||
export const ContextMenuRadioItem = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.RadioItem
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</ContextMenuPrimitive.RadioItem>
|
|
||||||
));
|
|
||||||
|
|
||||||
ContextMenuRadioItem.displayName = "ContextMenuRadioItem";
|
|
||||||
|
|
||||||
export const ContextMenuItemLabel = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
|
|
||||||
inset?: boolean;
|
|
||||||
}
|
|
||||||
>(({ className, inset, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.Label
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"px-2 py-1.5 text-sm font-semibold text-foreground",
|
|
||||||
inset && "pl-8",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
|
|
||||||
ContextMenuItemLabel.displayName = "ContextMenuItemLabel";
|
|
||||||
|
|
||||||
export const ContextMenuGroupLabel = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
|
|
||||||
inset?: boolean;
|
|
||||||
}
|
|
||||||
>(({ className, inset, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.Label
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"px-2 py-1.5 text-sm font-semibold text-foreground",
|
|
||||||
inset && "pl-8",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
|
|
||||||
ContextMenuGroupLabel.displayName = "ContextMenuGroupLabel";
|
|
||||||
|
|
||||||
export const ContextMenuSeparator = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<ContextMenuPrimitive.Separator
|
|
||||||
ref={ref}
|
|
||||||
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
|
|
||||||
ContextMenuSeparator.displayName = "ContextMenuSeparator";
|
|
||||||
|
|
||||||
export const ContextMenuShortcut = ({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"span">) => (
|
|
||||||
<span
|
|
||||||
className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
import { cn } from "../libs/cn";
|
|
||||||
|
|
||||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
||||||
type?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
||||||
({ className, type, ...props }, ref) => {
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
ref={ref}
|
|
||||||
type={type}
|
|
||||||
data-slot="input"
|
|
||||||
className={cn(
|
|
||||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base text-foreground shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
||||||
"focus-visible:border-input focus-visible:dark:bg-input/50 transition-all duration-200 ease-in-out",
|
|
||||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
Input.displayName = "Input";
|
|
||||||
|
|
||||||
export { Input };
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
import { cn } from "../libs/cn";
|
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
const Label = React.forwardRef<
|
|
||||||
HTMLLabelElement,
|
|
||||||
React.LabelHTMLAttributes<HTMLLabelElement>
|
|
||||||
>(({ className, ...props }, ref) => {
|
|
||||||
return (
|
|
||||||
<label
|
|
||||||
ref={ref}
|
|
||||||
data-slot="label"
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50 transition-all duration-200 ease-in-out",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
Label.displayName = "Label";
|
|
||||||
|
|
||||||
export { Label };
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
import { Toaster as Sonner } from "sonner";
|
|
||||||
|
|
||||||
export const Toaster = (props: Parameters<typeof Sonner>[0]) => {
|
|
||||||
return <Sonner className="toaster group" {...props} />;
|
|
||||||
};
|
|
||||||
|
|
@ -12,18 +12,5 @@
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["src/**/*.ts", "src/**/*.tsx"]
|
||||||
"src/link.tsx",
|
|
||||||
"src/libs/cn.ts",
|
|
||||||
"src/screens/error.tsx",
|
|
||||||
"src/screens/loading.tsx",
|
|
||||||
"src/ui/avatar.tsx",
|
|
||||||
"src/ui/button.tsx",
|
|
||||||
"src/ui/card.tsx",
|
|
||||||
"src/ui/checkbox.tsx",
|
|
||||||
"src/ui/context-menu.tsx",
|
|
||||||
"src/ui/input.tsx",
|
|
||||||
"src/ui/label.tsx",
|
|
||||||
"src/ui/sonner.tsx"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import { useSocket } from "@tensamin/ttp/context";
|
||||||
|
|
||||||
import { socket as schemas } from "@tensamin/shared/data";
|
import { socket as schemas } from "@tensamin/shared/data";
|
||||||
import type z from "zod";
|
import type z from "zod";
|
||||||
import { failedUser } from "./values";
|
|
||||||
|
|
||||||
export type User = z.infer<typeof schemas.get_user_data.response>;
|
export type User = z.infer<typeof schemas.get_user_data.response>;
|
||||||
|
|
||||||
|
|
@ -20,20 +19,17 @@ export default function UserProvider(props: { children: React.ReactNode }) {
|
||||||
|
|
||||||
async function get(userId: number): Promise<User> {
|
async function get(userId: number): Promise<User> {
|
||||||
if (storageRef.current[userId] === undefined) {
|
if (storageRef.current[userId] === undefined) {
|
||||||
try {
|
const userData = await send("get_user_data", { user_id: userId });
|
||||||
const userData = await send("get_user_data", { user_id: userId });
|
|
||||||
|
|
||||||
// Temp, add base64 stuff
|
// Temp, add base64 stuff
|
||||||
userData.data.avatar = userData.data.avatar
|
userData.data.avatar = userData.data.avatar
|
||||||
? `data:image/png;base64,${userData.data.avatar}`
|
? `data:image/png;base64,${userData.data.avatar}`
|
||||||
: undefined;
|
: undefined;
|
||||||
// Temp end
|
// Temp end
|
||||||
|
|
||||||
storageRef.current[userId] = userData.data;
|
storageRef.current[userId] = userData.data;
|
||||||
} catch {
|
|
||||||
storageRef.current[userId] = failedUser;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return storageRef.current[userId];
|
return storageRef.current[userId];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import type { User } from "./context";
|
|
||||||
|
|
||||||
export const failedUser: User = {
|
|
||||||
user_id: 0,
|
|
||||||
display: "Failed",
|
|
||||||
iota_id: 0,
|
|
||||||
omikron_connections: [],
|
|
||||||
online_status: "user_offline",
|
|
||||||
public_key: "",
|
|
||||||
sub_end: 0,
|
|
||||||
sub_level: 0,
|
|
||||||
username: "failed",
|
|
||||||
};
|
|
||||||
|
|
@ -10,11 +10,18 @@ export default function Wrapper(props: {
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
let active = true;
|
let active = true;
|
||||||
get(props.userId).then((value) => {
|
|
||||||
if (active) {
|
void get(props.userId)
|
||||||
setUser(value);
|
.then((value) => {
|
||||||
}
|
if (active) {
|
||||||
});
|
setUser(value);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (active) {
|
||||||
|
setUser(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
active = false;
|
active = false;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue