feat(desktop): add redis gui foundation baseline
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
12
apps/desktop/index.html
Normal file
12
apps/desktop/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Redis GUI Foundation</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
28
apps/desktop/package.json
Normal file
28
apps/desktop/package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "@redis-gui/desktop",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host 0.0.0.0 --port 1420",
|
||||
"build": "vite build",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@tauri-apps/api": "^2.1.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.544.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"tailwind-merge": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.1.12",
|
||||
"@tauri-apps/cli": "^2.1.0",
|
||||
"@vitejs/plugin-react": "^5.0.2",
|
||||
"tailwindcss": "^4.1.12",
|
||||
"vite": "^7.1.0"
|
||||
}
|
||||
}
|
||||
2
apps/desktop/src-tauri/.gitignore
vendored
Normal file
2
apps/desktop/src-tauri/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
|
||||
15
apps/desktop/src-tauri/Cargo.toml
Normal file
15
apps/desktop/src-tauri/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "desktop-shell"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0", features = [] }
|
||||
|
||||
[dependencies]
|
||||
redis-core = { path = "../../../crates/redis-core" }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tauri = { version = "2.0", features = [] }
|
||||
|
||||
3
apps/desktop/src-tauri/build.rs
Normal file
3
apps/desktop/src-tauri/build.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
12
apps/desktop/src-tauri/capabilities/default.json
Normal file
12
apps/desktop/src-tauri/capabilities/default.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Default capability for the primary desktop window.",
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default"
|
||||
]
|
||||
}
|
||||
|
||||
1
apps/desktop/src-tauri/gen/schemas/acl-manifests.json
Normal file
1
apps/desktop/src-tauri/gen/schemas/acl-manifests.json
Normal file
File diff suppressed because one or more lines are too long
1
apps/desktop/src-tauri/gen/schemas/capabilities.json
Normal file
1
apps/desktop/src-tauri/gen/schemas/capabilities.json
Normal file
@@ -0,0 +1 @@
|
||||
{"default":{"identifier":"default","description":"Default capability for the primary desktop window.","local":true,"windows":["main"],"permissions":["core:default"]}}
|
||||
2244
apps/desktop/src-tauri/gen/schemas/desktop-schema.json
Normal file
2244
apps/desktop/src-tauri/gen/schemas/desktop-schema.json
Normal file
File diff suppressed because it is too large
Load Diff
2244
apps/desktop/src-tauri/gen/schemas/linux-schema.json
Normal file
2244
apps/desktop/src-tauri/gen/schemas/linux-schema.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
apps/desktop/src-tauri/icons/icon.png
Normal file
BIN
apps/desktop/src-tauri/icons/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 B |
69
apps/desktop/src-tauri/src/main.rs
Normal file
69
apps/desktop/src-tauri/src/main.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use redis_core::{
|
||||
BackendBootstrap, BackendError, CommandExecutionResult, ConnectionTestResult,
|
||||
RedisCommandRequest, RedisConnectionRequest, RedisKeyBrowseRequest, RedisKeyBrowseResult,
|
||||
RedisKeyMetadata, RedisKeyMetadataRequest, RedisKeyTtlUpdateRequest, RedisKeyTtlUpdateResult,
|
||||
RedisValueReadRequest, RedisValueRecord, RedisValueWriteRequest, browse_keys, execute_command,
|
||||
inspect_key, read_value, test_connection, update_key_ttl, write_value,
|
||||
};
|
||||
|
||||
#[tauri::command]
|
||||
fn backend_bootstrap() -> BackendBootstrap {
|
||||
BackendBootstrap::for_desktop()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn test_redis_connection(
|
||||
request: RedisConnectionRequest,
|
||||
) -> Result<ConnectionTestResult, BackendError> {
|
||||
test_connection(request)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn execute_redis_command(
|
||||
request: RedisCommandRequest,
|
||||
) -> Result<CommandExecutionResult, BackendError> {
|
||||
execute_command(request)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn browse_redis_keys(request: RedisKeyBrowseRequest) -> Result<RedisKeyBrowseResult, BackendError> {
|
||||
browse_keys(request)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn inspect_redis_key(request: RedisKeyMetadataRequest) -> Result<RedisKeyMetadata, BackendError> {
|
||||
inspect_key(request)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn read_redis_value(request: RedisValueReadRequest) -> Result<RedisValueRecord, BackendError> {
|
||||
read_value(request)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn write_redis_value(request: RedisValueWriteRequest) -> Result<RedisValueRecord, BackendError> {
|
||||
write_value(request)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn update_redis_key_ttl(
|
||||
request: RedisKeyTtlUpdateRequest,
|
||||
) -> Result<RedisKeyTtlUpdateResult, BackendError> {
|
||||
update_key_ttl(request)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
backend_bootstrap,
|
||||
test_redis_connection,
|
||||
execute_redis_command,
|
||||
browse_redis_keys,
|
||||
inspect_redis_key,
|
||||
read_redis_value,
|
||||
write_redis_value,
|
||||
update_redis_key_ttl
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("failed to run desktop shell");
|
||||
}
|
||||
30
apps/desktop/src-tauri/tauri.conf.json
Normal file
30
apps/desktop/src-tauri/tauri.conf.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Redis GUI Foundation",
|
||||
"version": "0.1.0",
|
||||
"identifier": "ing.paperclip.redis-gui-foundation",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
"beforeBuildCommand": "pnpm build",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"label": "main",
|
||||
"title": "Redis GUI Foundation",
|
||||
"width": 1280,
|
||||
"height": 820,
|
||||
"resizable": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all"
|
||||
}
|
||||
}
|
||||
2326
apps/desktop/src/App.tsx
Normal file
2326
apps/desktop/src/App.tsx
Normal file
File diff suppressed because it is too large
Load Diff
32
apps/desktop/src/components/ui/badge.tsx
Normal file
32
apps/desktop/src/components/ui/badge.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-full border px-2.5 py-1 font-mono text-[11px] uppercase tracking-[0.18em]",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
neutral:
|
||||
"border-[var(--line-soft)] bg-[var(--surface-base)] text-[var(--ink-muted)]",
|
||||
accent:
|
||||
"border-[var(--accent-strong)] bg-[var(--accent-faint)] text-[var(--accent-strong)]",
|
||||
danger:
|
||||
"border-[var(--danger-soft)] bg-[var(--danger-faint)] text-[var(--danger-strong)]",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "neutral",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
|
||||
}
|
||||
|
||||
export { Badge };
|
||||
51
apps/desktop/src/components/ui/button.tsx
Normal file
51
apps/desktop/src/components/ui/button.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 rounded-full border text-sm font-medium transition outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent-strong)] focus-visible:ring-offset-2 focus-visible:ring-offset-transparent disabled:pointer-events-none disabled:opacity-45",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-[var(--accent-strong)] bg-[var(--accent-strong)] text-white hover:opacity-92",
|
||||
outline:
|
||||
"border-[var(--line-soft)] bg-[var(--surface-base)] text-[var(--ink-0)] hover:border-[var(--line-strong)]",
|
||||
subtle:
|
||||
"border-[var(--line-soft)] bg-[var(--surface-muted)] text-[var(--ink-0)] hover:border-[var(--line-strong)]",
|
||||
danger:
|
||||
"border-[var(--danger-soft)] bg-[var(--danger-strong)] text-white hover:opacity-92",
|
||||
ghost:
|
||||
"border-transparent bg-transparent text-[var(--ink-soft)] hover:border-[var(--line-soft)] hover:bg-[var(--surface-base)] hover:text-[var(--ink-0)]",
|
||||
},
|
||||
size: {
|
||||
default: "px-4 py-2.5",
|
||||
sm: "px-3 py-2",
|
||||
lg: "px-5 py-3",
|
||||
icon: "h-10 w-10 rounded-2xl p-0",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, type = "button", ...props }, ref) => (
|
||||
<button
|
||||
ref={ref}
|
||||
type={type}
|
||||
className={cn(buttonVariants({ variant, size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants };
|
||||
102
apps/desktop/src/components/ui/dialog.tsx
Normal file
102
apps/desktop/src/components/ui/dialog.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import * as React from "react";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { X } from "lucide-react";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
const DialogPortal = DialogPrimitive.Portal;
|
||||
const DialogClose = DialogPrimitive.Close;
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-[color:color-mix(in_srgb,var(--ink-0)_24%,transparent)] backdrop-blur-[2px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-1/2 top-1/2 z-50 grid w-[min(100%-2rem,44rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-[28px] border border-[var(--line-soft)] bg-[var(--surface-raised)] p-6 shadow-[var(--shadow-panel)] outline-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-2xl border border-[var(--line-soft)] bg-[var(--surface-base)] p-2 text-[var(--ink-muted)] transition hover:border-[var(--line-strong)] hover:text-[var(--ink-0)]">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
function DialogHeader({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return <div className={cn("flex flex-col gap-2", className)} {...props} />;
|
||||
}
|
||||
|
||||
function DialogFooter({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn("flex flex-wrap justify-end gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-2xl font-semibold tracking-[-0.03em] text-[var(--ink-0)]", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm leading-6 text-[var(--ink-soft)]", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
};
|
||||
18
apps/desktop/src/components/ui/input.tsx
Normal file
18
apps/desktop/src/components/ui/input.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||
({ className, ...props }, ref) => (
|
||||
<input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-11 w-full rounded-2xl border border-[var(--line-soft)] bg-[var(--surface-base)] px-3 py-2 text-sm text-[var(--ink-0)] outline-none transition placeholder:text-[var(--ink-muted)] focus-visible:border-[var(--accent-strong)] focus-visible:ring-2 focus-visible:ring-[color:color-mix(in_srgb,var(--accent-strong)_18%,transparent)] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input };
|
||||
19
apps/desktop/src/components/ui/textarea.tsx
Normal file
19
apps/desktop/src/components/ui/textarea.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
const Textarea = React.forwardRef<
|
||||
HTMLTextAreaElement,
|
||||
React.ComponentProps<"textarea">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<textarea
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex min-h-28 w-full rounded-[22px] border border-[var(--line-soft)] bg-[var(--surface-base)] px-4 py-3 text-sm text-[var(--ink-0)] outline-none transition placeholder:text-[var(--ink-muted)] focus-visible:border-[var(--accent-strong)] focus-visible:ring-2 focus-visible:ring-[color:color-mix(in_srgb,var(--accent-strong)_18%,transparent)] disabled:cursor-not-allowed disabled:opacity-60",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Textarea.displayName = "Textarea";
|
||||
|
||||
export { Textarea };
|
||||
79
apps/desktop/src/index.css
Normal file
79
apps/desktop/src/index.css
Normal file
@@ -0,0 +1,79 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
color-scheme: light;
|
||||
font-family: "IBM Plex Sans", "Aptos", "Segoe UI", sans-serif;
|
||||
--surface-base: #f3f0ea;
|
||||
--surface-muted: #ebe6de;
|
||||
--surface-raised: #fbfaf7;
|
||||
--surface-strong: #f1ede7;
|
||||
--line-soft: #d9d0c4;
|
||||
--line-strong: #b5a897;
|
||||
--ink-0: #1f1d1a;
|
||||
--ink-soft: #4f4a44;
|
||||
--ink-muted: #7f766d;
|
||||
--accent-strong: #1f5c52;
|
||||
--accent-faint: #dcece7;
|
||||
--danger-strong: #9b3b2e;
|
||||
--danger-soft: #d7a39a;
|
||||
--danger-faint: #f6e5e1;
|
||||
--shadow-panel: 0 18px 48px rgba(34, 29, 25, 0.09);
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(247, 243, 236, 0.96) 0%, rgba(238, 232, 222, 0.92) 48%, rgba(232, 226, 214, 0.9) 100%);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--surface-base: #181715;
|
||||
--surface-muted: #201e1a;
|
||||
--surface-raised: #24221e;
|
||||
--surface-strong: #2d2a25;
|
||||
--line-soft: #433f38;
|
||||
--line-strong: #5f584d;
|
||||
--ink-0: #f1ece4;
|
||||
--ink-soft: #d2c8bc;
|
||||
--ink-muted: #9e9487;
|
||||
--accent-strong: #7ec2b5;
|
||||
--accent-faint: rgba(71, 124, 114, 0.22);
|
||||
--danger-strong: #f08c7d;
|
||||
--danger-soft: rgba(210, 110, 92, 0.42);
|
||||
--danger-faint: rgba(151, 63, 49, 0.18);
|
||||
--shadow-panel: 0 20px 56px rgba(0, 0, 0, 0.32);
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(35, 32, 29, 0.98) 0%, rgba(21, 20, 18, 0.96) 52%, rgba(16, 15, 13, 0.98) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
background: transparent;
|
||||
color: var(--ink-0);
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
textarea {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
color: var(--ink-muted);
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: color-mix(in srgb, var(--accent-strong) 24%, transparent);
|
||||
}
|
||||
6
apps/desktop/src/lib/utils.ts
Normal file
6
apps/desktop/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));
|
||||
}
|
||||
10
apps/desktop/src/main.tsx
Normal file
10
apps/desktop/src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import "./index.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
11
apps/desktop/vite.config.ts
Normal file
11
apps/desktop/vite.config.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import desktopPackage from "./package.json";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
define: {
|
||||
__APP_VERSION__: JSON.stringify(desktopPackage.version),
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user