From c0102df54fea990bebfe6ca5ed9f838d092d3ec0 Mon Sep 17 00:00:00 2001 From: Alois Date: Sat, 14 Mar 2026 19:36:23 +0100 Subject: [PATCH] Updated a lot of stuff --- apps/web/package.json | 4 +- apps/web/src/components/modals/basic.tsx | 12 +- apps/web/src/components/navbar.tsx | 2 +- .../web/src/components/screens/login/form.tsx | 8 +- .../conversation/modal/conversation.tsx | 2 +- apps/web/src/features/legal/screen.tsx | 43 +-- apps/web/src/index.css | 175 ++++++------ apps/web/src/index.tsx | 37 +-- apps/web/src/routes/layout.tsx | 2 +- apps/web/src/routes/screens/login.tsx | 2 +- bun.lock | 18 +- packages/chat/src/components/input.tsx | 4 +- packages/chat/src/context.tsx | 31 +- packages/shared/src/data.ts | 20 +- packages/ttp/src/context.tsx | 268 ++++++++++++++++- packages/ttp/src/core.ts | 96 ++++++- packages/ui/components.json | 25 ++ packages/ui/package.json | 7 +- packages/ui/src/cmp/avatar.tsx | 107 +++++++ packages/ui/src/cmp/button.tsx | 56 ++++ packages/ui/src/cmp/card.tsx | 100 +++++++ packages/ui/src/cmp/checkbox.tsx | 27 ++ packages/ui/src/cmp/context-menu.tsx | 269 ++++++++++++++++++ packages/ui/src/cmp/input.tsx | 20 ++ packages/ui/src/cmp/label.tsx | 18 ++ packages/ui/src/cmp/sonner.tsx | 47 +++ packages/ui/src/index.css | 129 +++++++++ packages/ui/src/lib/utils.ts | 6 + packages/ui/src/libs/cn.ts | 5 - packages/ui/src/screens/error.tsx | 4 +- packages/ui/src/screens/loading.tsx | 27 +- packages/ui/src/theme.tsx | 229 +++++++++++++++ packages/ui/src/ui/avatar.tsx | 20 -- packages/ui/src/ui/button.tsx | 53 ---- packages/ui/src/ui/card.tsx | 35 --- packages/ui/src/ui/checkbox.tsx | 104 ------- packages/ui/src/ui/context-menu.tsx | 182 ------------ packages/ui/src/ui/input.tsx | 30 -- packages/ui/src/ui/label.tsx | 23 -- packages/ui/src/ui/sonner.tsx | 5 - packages/ui/tsconfig.json | 15 +- packages/user/src/context.tsx | 20 +- packages/user/src/values.ts | 13 - packages/user/src/wrapper.tsx | 17 +- 44 files changed, 1610 insertions(+), 707 deletions(-) create mode 100644 packages/ui/components.json create mode 100644 packages/ui/src/cmp/avatar.tsx create mode 100644 packages/ui/src/cmp/button.tsx create mode 100644 packages/ui/src/cmp/card.tsx create mode 100644 packages/ui/src/cmp/checkbox.tsx create mode 100644 packages/ui/src/cmp/context-menu.tsx create mode 100644 packages/ui/src/cmp/input.tsx create mode 100644 packages/ui/src/cmp/label.tsx create mode 100644 packages/ui/src/cmp/sonner.tsx create mode 100644 packages/ui/src/index.css create mode 100644 packages/ui/src/lib/utils.ts delete mode 100644 packages/ui/src/libs/cn.ts create mode 100644 packages/ui/src/theme.tsx delete mode 100644 packages/ui/src/ui/avatar.tsx delete mode 100644 packages/ui/src/ui/button.tsx delete mode 100644 packages/ui/src/ui/card.tsx delete mode 100644 packages/ui/src/ui/checkbox.tsx delete mode 100644 packages/ui/src/ui/context-menu.tsx delete mode 100644 packages/ui/src/ui/input.tsx delete mode 100644 packages/ui/src/ui/label.tsx delete mode 100644 packages/ui/src/ui/sonner.tsx delete mode 100644 packages/user/src/values.ts diff --git a/apps/web/package.json b/apps/web/package.json index 0091256..294e69b 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -34,7 +34,9 @@ "tailwindcss": "^4.2.1", "tw-animate-css": "^1.4.0", "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": { "@types/react": "^19.2.2", diff --git a/apps/web/src/components/modals/basic.tsx b/apps/web/src/components/modals/basic.tsx index df0e2e8..5aff126 100644 --- a/apps/web/src/components/modals/basic.tsx +++ b/apps/web/src/components/modals/basic.tsx @@ -1,16 +1,16 @@ 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 { Card, CardHeader } from "@tensamin/ui/card"; +import { Card, CardHeader } from "@tensamin/ui/cmp/card"; export default function Basic(props: { user: User }) { return ( - + + + {reduceDisplay(props.user.display)} +

{props.user.display}

diff --git a/apps/web/src/components/navbar.tsx b/apps/web/src/components/navbar.tsx index c23a880..006a570 100644 --- a/apps/web/src/components/navbar.tsx +++ b/apps/web/src/components/navbar.tsx @@ -1,4 +1,4 @@ -import { Button } from "@tensamin/ui/button"; +import { Button } from "@tensamin/ui/cmp/button"; import { House } from "lucide-react"; import { useNavigate, useRouterState } from "@tanstack/react-router"; import * as React from "react"; diff --git a/apps/web/src/components/screens/login/form.tsx b/apps/web/src/components/screens/login/form.tsx index 930eab1..082107c 100644 --- a/apps/web/src/components/screens/login/form.tsx +++ b/apps/web/src/components/screens/login/form.tsx @@ -1,7 +1,7 @@ -import { Button } from "@tensamin/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@tensamin/ui/card"; -import { Input } from "@tensamin/ui/input"; -import { Label } from "@tensamin/ui/label"; +import { Button } from "@tensamin/ui/cmp/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@tensamin/ui/cmp/card"; +import { Input } from "@tensamin/ui/cmp/input"; +import { Label } from "@tensamin/ui/cmp/label"; import { useStorage } from "@tensamin/storage/context"; import { log, toast } from "@tensamin/shared/log"; import { useNavigate } from "@tanstack/react-router"; diff --git a/apps/web/src/features/conversation/modal/conversation.tsx b/apps/web/src/features/conversation/modal/conversation.tsx index 6ec7477..2ce8251 100644 --- a/apps/web/src/features/conversation/modal/conversation.tsx +++ b/apps/web/src/features/conversation/modal/conversation.tsx @@ -6,7 +6,7 @@ import { ContextMenuGroup, ContextMenuItem, ContextMenuTrigger, -} from "@tensamin/ui/context-menu"; +} from "@tensamin/ui/cmp/context-menu"; import { useNavigate } from "@tanstack/react-router"; export default function ConversationModal(props: { userId: number }) { diff --git a/apps/web/src/features/legal/screen.tsx b/apps/web/src/features/legal/screen.tsx index b8e86ae..d351fc1 100644 --- a/apps/web/src/features/legal/screen.tsx +++ b/apps/web/src/features/legal/screen.tsx @@ -1,11 +1,7 @@ import * as React from "react"; import { useStorage } from "@tensamin/storage/context"; -import { Button } from "@tensamin/ui/button"; -import { - Checkbox, - CheckboxLabel, - CheckboxControl, -} from "@tensamin/ui/checkbox"; +import { Button } from "@tensamin/ui/cmp/button"; +import { Checkbox } from "@tensamin/ui/cmp/checkbox"; import { z } from "zod"; 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 { log } from "@tensamin/shared/log"; import Link from "@tensamin/ui/link"; +import { Label } from "@tensamin/ui/cmp/label"; export default function Screen(props: { children: React.ReactNode }) { const { load, save } = useStorage(); @@ -74,7 +71,13 @@ export default function Screen(props: { children: React.ReactNode }) { setErrorDescription( "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; } @@ -213,9 +216,11 @@ export default function Screen(props: { children: React.ReactNode }) { const currentCrashReports = crashReports; const currentUsageData = usageData; - void save("analytics_crash_reports", currentCrashReports).then(() => { - setCrashReports(currentCrashReports); - }); + void save("analytics_crash_reports", currentCrashReports).then( + () => { + setCrashReports(currentCrashReports); + }, + ); void save("analytics_usage_data", currentUsageData).then(() => { setUsageData(currentUsageData); }); @@ -252,13 +257,13 @@ export function BigCheckbox(props: { onChange: (checked: boolean) => void; }) { return ( - - - {props.label} - +
+ + +
); -} \ No newline at end of file +} diff --git a/apps/web/src/index.css b/apps/web/src/index.css index aa81232..05872f2 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -1,29 +1,83 @@ @import "tailwindcss"; @import "tailwind-scrollbar-hide/v4"; @import "tw-animate-css"; +@import "shadcn/tailwind.css"; +@import "@fontsource-variable/inter"; @source "./**/*.{ts,tsx}"; -@source "../../../packages/ui/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 { - --breakpoint-sm: 640px; - --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); + --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); @@ -53,81 +107,15 @@ --color-popover: var(--popover); --color-card-foreground: var(--card-foreground); --color-card: var(--card); - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); + --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) + 4px); -} - -:root { - --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; + --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 { @@ -136,10 +124,9 @@ } body { @apply bg-background text-foreground; - font-family: "Inter", system-ui, sans-serif; } - div { - @apply text-foreground; + html { + @apply font-sans; } ::selection { background: var(--color-primary, var(--primary)); diff --git a/apps/web/src/index.tsx b/apps/web/src/index.tsx index e2b249a..a15b308 100644 --- a/apps/web/src/index.tsx +++ b/apps/web/src/index.tsx @@ -1,4 +1,3 @@ -import * as React from "react"; import { createRoot } from "react-dom/client"; import { Outlet, @@ -20,6 +19,8 @@ import ChatContext from "@tensamin/chat/context"; import Login from "@/routes/screens/login"; import Signup from "@/routes/screens/signup"; +import { ThemeProvider } from "@tensamin/ui/theme"; + const wrapper = document.getElementById("root"); if (!wrapper) { @@ -33,32 +34,14 @@ window.setLogLevelToMax = () => { }; 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 ( -
- - - -
+ +
+ + + +
+
); } @@ -129,4 +112,4 @@ declare module "@tanstack/react-router" { } } -createRoot(wrapper).render(); \ No newline at end of file +createRoot(wrapper).render(); diff --git a/apps/web/src/routes/layout.tsx b/apps/web/src/routes/layout.tsx index 5ffab38..60de442 100644 --- a/apps/web/src/routes/layout.tsx +++ b/apps/web/src/routes/layout.tsx @@ -5,7 +5,7 @@ import Crypto from "@tensamin/crypto/context"; 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 }) { return ( diff --git a/apps/web/src/routes/screens/login.tsx b/apps/web/src/routes/screens/login.tsx index 437956f..95773ba 100644 --- a/apps/web/src/routes/screens/login.tsx +++ b/apps/web/src/routes/screens/login.tsx @@ -1,5 +1,5 @@ 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"; export default function Page() { diff --git a/bun.lock b/bun.lock index 573fa05..2409098 100644 --- a/bun.lock +++ b/bun.lock @@ -166,13 +166,15 @@ "name": "@tensamin/ui", "version": "0.0.0", "dependencies": { + "@base-ui/react": "^1.3.0", "@radix-ui/react-context-menu": "^2.2.16", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.564.0", + "next-themes": "^0.4.6", "react": "^19.2.0", "react-dom": "^19.2.0", - "sonner": "^1.0.0", + "sonner": "^2.0.7", "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/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/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=="], + "@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/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=="], + "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=="], "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=="], + "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=="], "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=="], + "tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="], + "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=="], @@ -907,6 +921,8 @@ "@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=="], "bun-types/@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="], diff --git a/packages/chat/src/components/input.tsx b/packages/chat/src/components/input.tsx index 84b7b4e..cdb6879 100644 --- a/packages/chat/src/components/input.tsx +++ b/packages/chat/src/components/input.tsx @@ -1,8 +1,8 @@ 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 * 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 { useChat } from "../context"; diff --git a/packages/chat/src/context.tsx b/packages/chat/src/context.tsx index e3ab4ac..0f81021 100644 --- a/packages/chat/src/context.tsx +++ b/packages/chat/src/context.tsx @@ -41,20 +41,27 @@ export default function Provider( let active = true; - void get(recipientId).then(async (recipientData) => { - const ownId = await load("user_id"); - const privateKey = await load("private_key"); - const ownData = await get(ownId); - const sharedSecret = await get_shared_secret( - privateKey, - ownData.public_key, - recipientData.public_key, - ); + void (async () => { + try { + const recipientData = await get(recipientId); + const ownId = await load("user_id"); + const privateKey = await load("private_key"); + const ownData = await get(ownId); + const sharedSecret = await get_shared_secret( + privateKey, + ownData.public_key, + recipientData.public_key, + ); - if (active) { - setCurrentSharedSecret(sharedSecret); + if (active) { + setCurrentSharedSecret(sharedSecret); + } + } catch { + if (active) { + setCurrentSharedSecret(""); + } } - }); + })(); return () => { active = false; diff --git a/packages/shared/src/data.ts b/packages/shared/src/data.ts index 1d4ffd9..eb578a5 100644 --- a/packages/shared/src/data.ts +++ b/packages/shared/src/data.ts @@ -24,9 +24,19 @@ const message = z.object({ // Socket export const socket = { identification: { - request: z.object({ - user_id: z.number(), - }), + request: z + .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({ challenge: z.string(), public_key: z.base64(), @@ -66,7 +76,9 @@ export const socket = { request: z.object({ challenge: z.base64(), }), - response: z.object({}), + response: z.object({ + accepted: z.boolean(), + }), }, ping: { request: z.object({ diff --git a/packages/ttp/src/context.tsx b/packages/ttp/src/context.tsx index 52e56c2..97526c1 100644 --- a/packages/ttp/src/context.tsx +++ b/packages/ttp/src/context.tsx @@ -1,5 +1,7 @@ import * as React from "react"; +import { useCrypto } from "@tensamin/crypto/context"; import { log } from "@tensamin/shared/log"; +import { useStorage } from "@tensamin/storage/context"; import { createTransportClient, READY_STATE, type BoundSendFn } from "./core"; import { PING_INTERVAL, @@ -14,18 +16,90 @@ import { import Loading from "@tensamin/ui/screens/loading"; 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 = { send: BoundSendFn; readyState: () => number; ownPing: () => number; iotaPing: () => number; + identified: () => boolean; }; const socketContext = React.createContext(undefined); export default function Provider(props: { children: React.ReactNode }) { - const [readyState, setReadyState] = React.useState(READY_STATE.CLOSED); + const { load } = useStorage(); + const { decrypt, get_shared_secret } = useCrypto(); + + const [readyState, setReadyState] = React.useState( + READY_STATE.CLOSED, + ); const [connected, setConnected] = React.useState(false); + const [identified, setIdentified] = React.useState(false); + const [identifying, setIdentifying] = React.useState(false); const [ownPing, setOwnPing] = React.useState(0); const [iotaPing, setIotaPing] = React.useState(0); @@ -36,6 +110,7 @@ export default function Provider(props: { children: React.ReactNode }) { const clientRef = React.useRef > | null>(null); + const identificationStartedRef = React.useRef(false); const send = React.useCallback>( (( @@ -65,7 +140,7 @@ export default function Provider(props: { children: React.ReactNode }) { ); React.useEffect(() => { - if (!connected) { + if (!connected || !identified) { return; } @@ -92,7 +167,7 @@ export default function Provider(props: { children: React.ReactNode }) { return () => { clearInterval(interval); }; - }, [connected, send]); + }, [connected, identified, send]); React.useEffect(() => { let attempts = 0; @@ -141,17 +216,34 @@ export default function Provider(props: { children: React.ReactNode }) { if (state === READY_STATE.OPEN) { attempts = 0; clearReconnectTimer(); + identificationStartedRef.current = false; setConnected(true); + setIdentified(false); setError(""); 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; } setConnected(false); - }, - onClose: ({ error: closeError, intentional }) => { - setConnected(false); + setIdentified(false); if (disposed || intentional) { return; @@ -176,6 +268,13 @@ export default function Provider(props: { children: React.ReactNode }) { return; } + if (isStopSendingError(connectError)) { + clearReconnectTimer(); + setError("Connection closed"); + setErrorDescription("Connection closed"); + return; + } + log(0, "Socket", "red", "Connection attempt failed", connectError); scheduleReconnect(connectError); } @@ -194,14 +293,147 @@ export default function Provider(props: { children: React.ReactNode }) { void transportClient.close("context-dispose"); setReadyState(READY_STATE.CLOSED); 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(() => { - if (readyState === READY_STATE.CONNECTING) return 70; - if (!connected) return 90; + if (readyState === READY_STATE.CONNECTING) return 30; + if (!connected) return 45; + if (identifying) return 75; + if (!identified) return 90; 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( () => ({ @@ -209,16 +441,24 @@ export default function Provider(props: { children: React.ReactNode }) { readyState: () => readyState, ownPing: () => ownPing, iotaPing: () => iotaPing, + identified: () => identified, }), - [iotaPing, ownPing, readyState, send], + [identified, iotaPing, ownPing, readyState, send], ); if (error !== "" && errorDescription !== "") { return ; } - if (!connected) { - return ; + if (!connected || !identified) { + return ( + + ); } return ( @@ -234,4 +474,4 @@ export function useSocket(): ContextType { throw new Error("useSocket must be used within a SocketProvider"); } return context; -} \ No newline at end of file +} diff --git a/packages/ttp/src/core.ts b/packages/ttp/src/core.ts index 0ca3cc6..df31b4c 100644 --- a/packages/ttp/src/core.ts +++ b/packages/ttp/src/core.ts @@ -269,6 +269,7 @@ registerDataKinds("number", [ "omikron_id", "send_time", "sub_level", + "sub_end", ]); registerDataKinds("string", [ @@ -301,6 +302,7 @@ registerDataKinds("string", [ "new_token", "call_token", "challenge", + "online_status", ]); registerDataKinds({ array: "container" }, [ @@ -351,10 +353,8 @@ registerDataKinds("null", [ "watcher", "created_at", "status", - "sub_end", "community_address", "community_title", - "online_status", "call_invited", "call_members", "calls", @@ -476,9 +476,31 @@ export function createTransportClient( 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) => { 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) { @@ -728,13 +750,20 @@ export function createTransportClient( 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") { - log(2, "Socket", "purple", type, payload); + log(2, "Socket", "purple", "Sent:", type, payload, { id: options.id }); } const expectsResponse = !options?.noResponse; const requestId = resolveRequestId( - options?.id, + options.id, expectsResponse, pending, () => { @@ -751,7 +780,10 @@ export function createTransportClient( }); if (!expectsResponse) { - return writeMessage(connection.transport, messageBytes); + return writeMessage(connection.transport, messageBytes).catch((error) => { + closeFromStopSending(connection, error); + throw error; + }); } return new Promise((resolve, reject) => { @@ -772,6 +804,7 @@ export function createTransportClient( }); void writeMessage(connection.transport, messageBytes).catch((error) => { + closeFromStopSending(connection, error); clearTimeout(timeoutId); pending.delete(requestId); reject(error); @@ -829,6 +862,34 @@ function normalizeName(value: string) { 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 { if (!isPlainObject(value)) { throw new Error("Protocol payload must be a plain object"); @@ -1175,7 +1236,9 @@ function encodeArrayPayload( if (!isBoolKindMarker(encodedItem.kind)) { 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; @@ -1222,10 +1285,13 @@ function encodeContainerPayload(value: Record, path: string) { ); 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; for (const [name, entry] of entries) { @@ -1242,7 +1308,9 @@ function encodeContainerPayload(value: Record, path: string) { totalLength += 2; } else { 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; @@ -1365,7 +1433,10 @@ function decodeContainerPayload(reader: ByteReader) { ); } - value[key] = normalizeIncomingValue(key, decodeValuePayload(marker, payload)); + value[key] = normalizeIncomingValue( + key, + decodeValuePayload(marker, payload), + ); } return value; @@ -1382,7 +1453,8 @@ function getDataTypeNameByIndex(index: number) { function isBoolKindMarker(marker: number) { 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 ); } diff --git a/packages/ui/components.json b/packages/ui/components.json new file mode 100644 index 0000000..0c1a401 --- /dev/null +++ b/packages/ui/components.json @@ -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": {} +} diff --git a/packages/ui/package.json b/packages/ui/package.json index c9970d5..c8f16e4 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -4,7 +4,8 @@ "version": "0.0.0", "type": "module", "exports": { - "./*": "./src/ui/*.tsx", + "./cmp/*": "./src/cmp/*.tsx", + "./theme": "./src/theme.tsx", "./screens/*": "./src/screens/*.tsx", "./link": "./src/link.tsx" }, @@ -14,13 +15,15 @@ "build": "tsc -p tsconfig.json --noEmit" }, "dependencies": { + "@base-ui/react": "^1.3.0", "@radix-ui/react-context-menu": "^2.2.16", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.564.0", + "next-themes": "^0.4.6", "react": "^19.2.0", "react-dom": "^19.2.0", - "sonner": "^1.0.0", + "sonner": "^2.0.7", "tailwind-merge": "^3.5.0" } } diff --git a/packages/ui/src/cmp/avatar.tsx b/packages/ui/src/cmp/avatar.tsx new file mode 100644 index 0000000..66b83f2 --- /dev/null +++ b/packages/ui/src/cmp/avatar.tsx @@ -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 ( + + ) +} + +function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: AvatarPrimitive.Fallback.Props) { + return ( + + ) +} + +function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) { + return ( + 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 ( +
+ ) +} + +function AvatarGroupCount({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
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, +} diff --git a/packages/ui/src/cmp/button.tsx b/packages/ui/src/cmp/button.tsx new file mode 100644 index 0000000..981f0df --- /dev/null +++ b/packages/ui/src/cmp/button.tsx @@ -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) { + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/packages/ui/src/cmp/card.tsx b/packages/ui/src/cmp/card.tsx new file mode 100644 index 0000000..19a7561 --- /dev/null +++ b/packages/ui/src/cmp/card.tsx @@ -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 ( +
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 ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/packages/ui/src/cmp/checkbox.tsx b/packages/ui/src/cmp/checkbox.tsx new file mode 100644 index 0000000..3158680 --- /dev/null +++ b/packages/ui/src/cmp/checkbox.tsx @@ -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 ( + + + + + + ) +} + +export { Checkbox } diff --git a/packages/ui/src/cmp/context-menu.tsx b/packages/ui/src/cmp/context-menu.tsx new file mode 100644 index 0000000..68c9be4 --- /dev/null +++ b/packages/ui/src/cmp/context-menu.tsx @@ -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 +} + +function ContextMenuPortal({ ...props }: ContextMenuPrimitive.Portal.Props) { + return ( + + ) +} + +function ContextMenuTrigger({ + className, + ...props +}: ContextMenuPrimitive.Trigger.Props) { + return ( + + ) +} + +function ContextMenuContent({ + className, + align = "start", + alignOffset = 4, + side = "right", + sideOffset = 0, + ...props +}: ContextMenuPrimitive.Popup.Props & + Pick< + ContextMenuPrimitive.Positioner.Props, + "align" | "alignOffset" | "side" | "sideOffset" + >) { + return ( + + + + + + ) +} + +function ContextMenuGroup({ ...props }: ContextMenuPrimitive.Group.Props) { + return ( + + ) +} + +function ContextMenuLabel({ + className, + inset, + ...props +}: ContextMenuPrimitive.GroupLabel.Props & { + inset?: boolean +}) { + return ( + + ) +} + +function ContextMenuItem({ + className, + inset, + variant = "default", + ...props +}: ContextMenuPrimitive.Item.Props & { + inset?: boolean + variant?: "default" | "destructive" +}) { + return ( + + ) +} + +function ContextMenuSub({ ...props }: ContextMenuPrimitive.SubmenuRoot.Props) { + return ( + + ) +} + +function ContextMenuSubTrigger({ + className, + inset, + children, + ...props +}: ContextMenuPrimitive.SubmenuTrigger.Props & { + inset?: boolean +}) { + return ( + + {children} + + + ) +} + +function ContextMenuSubContent({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function ContextMenuCheckboxItem({ + className, + children, + checked, + inset, + ...props +}: ContextMenuPrimitive.CheckboxItem.Props & { + inset?: boolean +}) { + return ( + + + + + + + {children} + + ) +} + +function ContextMenuRadioGroup({ + ...props +}: ContextMenuPrimitive.RadioGroup.Props) { + return ( + + ) +} + +function ContextMenuRadioItem({ + className, + children, + inset, + ...props +}: ContextMenuPrimitive.RadioItem.Props & { + inset?: boolean +}) { + return ( + + + + + + + {children} + + ) +} + +function ContextMenuSeparator({ + className, + ...props +}: ContextMenuPrimitive.Separator.Props) { + return ( + + ) +} + +function ContextMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +export { + ContextMenu, + ContextMenuTrigger, + ContextMenuContent, + ContextMenuItem, + ContextMenuCheckboxItem, + ContextMenuRadioItem, + ContextMenuLabel, + ContextMenuSeparator, + ContextMenuShortcut, + ContextMenuGroup, + ContextMenuPortal, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuRadioGroup, +} diff --git a/packages/ui/src/cmp/input.tsx b/packages/ui/src/cmp/input.tsx new file mode 100644 index 0000000..301157f --- /dev/null +++ b/packages/ui/src/cmp/input.tsx @@ -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 ( + + ) +} + +export { Input } diff --git a/packages/ui/src/cmp/label.tsx b/packages/ui/src/cmp/label.tsx new file mode 100644 index 0000000..1b102a6 --- /dev/null +++ b/packages/ui/src/cmp/label.tsx @@ -0,0 +1,18 @@ +import * as React from "react" + +import { cn } from "../lib/utils" + +function Label({ className, ...props }: React.ComponentProps<"label">) { + return ( +