create-arsh-tauri 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +55 -2
  3. package/index.js +239 -0
  4. package/package.json +43 -4
  5. package/template/.eslintrc.cjs +18 -0
  6. package/template/.github/workflows/ci.yml +50 -0
  7. package/template/AGENTS.md +50 -0
  8. package/template/README.md +51 -0
  9. package/template/gitignore +37 -0
  10. package/template/index.html +12 -0
  11. package/template/package-lock.json +4454 -0
  12. package/template/package.json +42 -0
  13. package/template/src/App.tsx +35 -0
  14. package/template/src/index.css +8 -0
  15. package/template/src/lib/tauri.ts +21 -0
  16. package/template/src/lib/utils.test.ts +17 -0
  17. package/template/src/lib/utils.ts +6 -0
  18. package/template/src/main.tsx +10 -0
  19. package/template/src/ui/appLayout.tsx +35 -0
  20. package/template/src/ui/button.tsx +63 -0
  21. package/template/src/ui/dialog.tsx +71 -0
  22. package/template/src/ui/index.ts +8 -0
  23. package/template/src/ui/navItem.tsx +37 -0
  24. package/template/src/ui/spinner.tsx +33 -0
  25. package/template/src/ui/styles.css +96 -0
  26. package/template/src/ui/switch.tsx +77 -0
  27. package/template/src/ui/titleBar.tsx +157 -0
  28. package/template/src/ui/tooltip.tsx +141 -0
  29. package/template/src/vite-env.d.ts +1 -0
  30. package/template/src-tauri/Cargo.lock +4316 -0
  31. package/template/src-tauri/Cargo.toml +26 -0
  32. package/template/src-tauri/build.rs +3 -0
  33. package/template/src-tauri/capabilities/default.json +16 -0
  34. package/template/src-tauri/icons/128x128.png +0 -0
  35. package/template/src-tauri/icons/128x128@2x.png +0 -0
  36. package/template/src-tauri/icons/32x32.png +0 -0
  37. package/template/src-tauri/icons/icon.icns +0 -0
  38. package/template/src-tauri/icons/icon.ico +0 -0
  39. package/template/src-tauri/src/lib.rs +14 -0
  40. package/template/src-tauri/src/main.rs +6 -0
  41. package/template/src-tauri/tauri.conf.json +40 -0
  42. package/template/temp/.gitkeep +0 -0
  43. package/template/tsconfig.json +25 -0
  44. package/template/tsconfig.node.json +12 -0
  45. package/template/vite.config.ts +24 -0
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "arsh-tauri-app",
3
+ "description": "Tauri + React + TypeScript desktop app",
4
+ "author": "Your Name",
5
+ "private": true,
6
+ "version": "0.1.0",
7
+ "type": "module",
8
+ "scripts": {
9
+ "dev": "tauri dev",
10
+ "dev:web": "vite",
11
+ "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && npm run build:web && tauri build",
12
+ "build:web": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc && vite build",
13
+ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
14
+ "test": "vitest run",
15
+ "preview": "vite preview",
16
+ "tauri": "tauri"
17
+ },
18
+ "dependencies": {},
19
+ "devDependencies": {
20
+ "@tailwindcss/vite": "^4.3.3",
21
+ "@tauri-apps/api": "^2.0.0",
22
+ "@tauri-apps/cli": "^2.0.0",
23
+ "@types/node": "^25.8.0",
24
+ "@types/react": "^19.2.2",
25
+ "@types/react-dom": "^19.2.2",
26
+ "@typescript-eslint/eslint-plugin": "^8.59.3",
27
+ "@typescript-eslint/parser": "^8.59.3",
28
+ "@vitejs/plugin-react": "^6.1.1",
29
+ "clsx": "^2.1.1",
30
+ "eslint": "^8.57.0",
31
+ "eslint-plugin-react-hooks": "^7.1.1",
32
+ "eslint-plugin-react-refresh": "^0.4.5",
33
+ "lucide-react": "^1.48.0",
34
+ "react": "^19.2.6",
35
+ "react-dom": "^19.2.6",
36
+ "tailwind-merge": "^3.6.0",
37
+ "tailwindcss": "^4.3.3",
38
+ "typescript": "^6.0.3",
39
+ "vite": "^8.0.0",
40
+ "vitest": "^4.0.14"
41
+ }
42
+ }
@@ -0,0 +1,35 @@
1
+ import { useState } from 'react'
2
+ import { invoke } from '@tauri-apps/api/core'
3
+ import { AppLayout, TitleBar, Button } from './ui'
4
+ import { hasNativeBridge } from './lib/tauri'
5
+
6
+ export default function App() {
7
+ const [count, setCount] = useState(0)
8
+ const [greetMsg, setGreetMsg] = useState('')
9
+
10
+ async function greet() {
11
+ if (!hasNativeBridge) return
12
+ setGreetMsg(await invoke<string>('greet', { name: 'Arsh' }))
13
+ }
14
+
15
+ return (
16
+ <AppLayout titleBar={<TitleBar brand="Arsh Tauri" />}>
17
+ <div className="h-full flex flex-col items-center justify-center gap-4 text-center px-6">
18
+ <h1 className="text-3xl font-semibold text-main">Congratulations!</h1>
19
+ <p className="text-sm text-muted max-w-md leading-relaxed">
20
+ Your Tauri project has been initialized. Open this project with any AI
21
+ coding tool and build your own marvelous Tauri app.
22
+ </p>
23
+ <div className="flex items-center gap-2">
24
+ <Button onClick={() => setCount((c) => c + 1)}>count is {count}</Button>
25
+ <Button variant="secondary" onClick={greet}>greet from Rust</Button>
26
+ </div>
27
+ {greetMsg && <p className="text-xs text-dim">{greetMsg}</p>}
28
+ <p className="text-xs text-dim">
29
+ Edit src/App.tsx and save to see hot reload. The Rust backend lives in
30
+ src-tauri/.
31
+ </p>
32
+ </div>
33
+ </AppLayout>
34
+ )
35
+ }
@@ -0,0 +1,8 @@
1
+ @import "tailwindcss";
2
+ @import "./ui/styles.css";
3
+
4
+ html,
5
+ body,
6
+ #root {
7
+ height: 100%;
8
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Native (Tauri) bridge detection.
3
+ *
4
+ * UI primitives must render correctly in plain web apps too. Anything that
5
+ * needs the native window (title-bar controls, drag regions) checks this
6
+ * first and degrades gracefully when it is absent.
7
+ */
8
+ import { getCurrentWindow, type Window } from '@tauri-apps/api/window'
9
+
10
+ export const hasNativeBridge =
11
+ typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window
12
+
13
+ /** The focused app window, or null when running as a plain web page. */
14
+ export function getWindow(): Window | null {
15
+ if (!hasNativeBridge) return null
16
+ try {
17
+ return getCurrentWindow()
18
+ } catch {
19
+ return null
20
+ }
21
+ }
@@ -0,0 +1,17 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { cn } from './utils'
3
+
4
+ describe('cn', () => {
5
+ it('joins conditional classes', () => {
6
+ expect(cn('flex', false && 'hidden', 'items-center')).toBe('flex items-center')
7
+ })
8
+
9
+ it('lets later tailwind classes win conflicts', () => {
10
+ expect(cn('px-2', 'px-4')).toBe('px-4')
11
+ expect(cn('text-sm text-main', 'text-lg')).toBe('text-main text-lg')
12
+ })
13
+
14
+ it('resolves conflicting class groups, not just identical classes', () => {
15
+ expect(cn('p-2', 'py-4')).toBe('p-2 py-4')
16
+ })
17
+ })
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from 'clsx'
2
+ import { twMerge } from 'tailwind-merge'
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -0,0 +1,10 @@
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import App from './App'
4
+ import './index.css'
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ )
@@ -0,0 +1,35 @@
1
+ import type { ReactNode } from 'react'
2
+ import { cn } from '../lib/utils'
3
+
4
+ interface AppLayoutProps {
5
+ /** The TitleBar component */
6
+ titleBar: ReactNode
7
+ /** Main content area */
8
+ children: ReactNode
9
+ /** Optional sidebar rendered between title bar and content */
10
+ sidebar?: ReactNode
11
+ className?: string
12
+ }
13
+
14
+ /**
15
+ * AppLayout provides the top-level shell for an application.
16
+ * It stacks the title bar, optional sidebar, and main content vertically.
17
+ * Height follows the parent (h-full): use 100vh on the root container (e.g. #root).
18
+ */
19
+ export function AppLayout({ titleBar, children, sidebar, className }: AppLayoutProps) {
20
+ return (
21
+ <div className={cn('h-full flex flex-col overflow-hidden bg-page', className)}>
22
+ {titleBar}
23
+ <div className="flex-1 flex overflow-hidden">
24
+ {sidebar && (
25
+ <div className="shrink-0 border-r border-line">
26
+ {sidebar}
27
+ </div>
28
+ )}
29
+ <main className="flex-1 overflow-auto">
30
+ {children}
31
+ </main>
32
+ </div>
33
+ </div>
34
+ )
35
+ }
@@ -0,0 +1,63 @@
1
+ import { type ReactNode, type ButtonHTMLAttributes } from 'react'
2
+ import { cn } from '../lib/utils'
3
+ import { Loader2 } from 'lucide-react'
4
+
5
+ const variants = {
6
+ default: 'bg-accent text-fg hover:opacity-80',
7
+ secondary: 'bg-surface border border-line text-main hover:bg-hover',
8
+ ghost: 'text-muted hover:text-main hover:bg-hover',
9
+ destructive: 'bg-destructive text-white hover:opacity-80',
10
+ link: 'text-accent underline-offset-4 hover:underline',
11
+ } as const
12
+
13
+ const sizes = {
14
+ sm: 'px-2.5 py-1 text-xs rounded-md',
15
+ md: 'px-4 py-1.5 text-sm rounded-lg',
16
+ lg: 'px-5 py-2 text-sm rounded-lg',
17
+ } as const
18
+
19
+ interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'className'> {
20
+ /** Button visual style */
21
+ variant?: keyof typeof variants
22
+ /** Button size */
23
+ size?: keyof typeof sizes
24
+ /** Optional icon shown before the label */
25
+ icon?: ReactNode
26
+ /** Show loading spinner */
27
+ loading?: boolean
28
+ className?: string
29
+ children?: ReactNode
30
+ }
31
+
32
+ export function Button({
33
+ variant = 'default',
34
+ size = 'md',
35
+ icon,
36
+ loading = false,
37
+ disabled,
38
+ className,
39
+ children,
40
+ ...props
41
+ }: ButtonProps) {
42
+ return (
43
+ <button
44
+ disabled={disabled || loading}
45
+ className={cn(
46
+ 'inline-flex items-center justify-center gap-1.5 font-medium transition-all cursor-pointer select-none',
47
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/20',
48
+ 'disabled:opacity-40 disabled:cursor-not-allowed',
49
+ variants[variant],
50
+ sizes[size],
51
+ className,
52
+ )}
53
+ {...props}
54
+ >
55
+ {loading ? (
56
+ <Loader2 className={cn('size-4 animate-spin', children && 'shrink-0')} />
57
+ ) : icon ? (
58
+ <span className="size-4 shrink-0 flex items-center justify-center">{icon}</span>
59
+ ) : null}
60
+ {children}
61
+ </button>
62
+ )
63
+ }
@@ -0,0 +1,71 @@
1
+ import { useEffect, useCallback, type ReactNode } from 'react'
2
+ import { createPortal } from 'react-dom'
3
+ import { cn } from '../lib/utils'
4
+
5
+ interface DialogProps {
6
+ open: boolean
7
+ onClose: () => void
8
+ children: ReactNode
9
+ /** Whether clicking the overlay closes the dialog */
10
+ closeOnOverlay?: boolean
11
+ /** Whether to close on Escape key */
12
+ closeOnEscape?: boolean
13
+ className?: string
14
+ overlayClassName?: string
15
+ }
16
+
17
+ export function Dialog({
18
+ open,
19
+ onClose,
20
+ children,
21
+ closeOnOverlay = true,
22
+ closeOnEscape = true,
23
+ className,
24
+ overlayClassName,
25
+ }: DialogProps) {
26
+ const handleKeyDown = useCallback(
27
+ (e: KeyboardEvent) => {
28
+ if (closeOnEscape && e.key === 'Escape') onClose()
29
+ },
30
+ [onClose, closeOnEscape],
31
+ )
32
+
33
+ useEffect(() => {
34
+ if (!open) return
35
+ document.addEventListener('keydown', handleKeyDown)
36
+ document.body.style.overflow = 'hidden'
37
+ return () => {
38
+ document.removeEventListener('keydown', handleKeyDown)
39
+ document.body.style.overflow = ''
40
+ }
41
+ }, [open, handleKeyDown])
42
+
43
+ if (!open) return null
44
+
45
+ return createPortal(
46
+ <div
47
+ className={cn(
48
+ 'fixed inset-0 z-60 flex items-center justify-center',
49
+ 'animate-[fade-in_150ms_ease-out]',
50
+ overlayClassName,
51
+ )}
52
+ style={{ background: 'rgba(0,0,0,0.45)' }}
53
+ onClick={(e) => {
54
+ if (closeOnOverlay && e.target === e.currentTarget) onClose()
55
+ }}
56
+ >
57
+ <div
58
+ role="dialog"
59
+ aria-modal="true"
60
+ className={cn(
61
+ 'bg-surface rounded-lg shadow-lg max-w-md w-full mx-4',
62
+ 'animate-[scale-in_150ms_ease-out]',
63
+ className,
64
+ )}
65
+ >
66
+ {children}
67
+ </div>
68
+ </div>,
69
+ document.body,
70
+ )
71
+ }
@@ -0,0 +1,8 @@
1
+ export { AppLayout } from './appLayout'
2
+ export { TitleBar } from './titleBar'
3
+ export { NavItem } from './navItem'
4
+ export { Button } from './button'
5
+ export { Dialog } from './dialog'
6
+ export { Tooltip } from './tooltip'
7
+ export { Spinner } from './spinner'
8
+ export { Switch } from './switch'
@@ -0,0 +1,37 @@
1
+ import { type ReactNode, type MouseEvent } from 'react'
2
+ import { cn } from '../lib/utils'
3
+
4
+ interface NavItemProps {
5
+ children: ReactNode
6
+ className?: string
7
+ /** Optional icon shown before the label */
8
+ icon?: ReactNode
9
+ /** Active (highlighted) state */
10
+ active?: boolean
11
+ onClick?: (e: MouseEvent<HTMLButtonElement>) => void
12
+ title?: string
13
+ }
14
+
15
+ /**
16
+ * Top navigation item, typically used as a child of TitleBar.
17
+ * Not bound to any router: the caller computes `active` and handles navigation.
18
+ */
19
+ export function NavItem({ children, className, icon, active = false, onClick, title }: NavItemProps) {
20
+ return (
21
+ <button
22
+ onClick={onClick}
23
+ aria-current={active ? 'page' : undefined}
24
+ title={title}
25
+ className={cn(
26
+ 'flex items-center gap-1.5 px-3 text-xs font-medium border-r border-line/40 transition-colors cursor-pointer',
27
+ active
28
+ ? 'bg-hover text-main'
29
+ : 'text-muted hover:text-main hover:bg-hover/50',
30
+ className,
31
+ )}
32
+ >
33
+ {icon && <span className="size-3.5 flex items-center justify-center">{icon}</span>}
34
+ {children}
35
+ </button>
36
+ )
37
+ }
@@ -0,0 +1,33 @@
1
+ import { cn } from '../lib/utils'
2
+ import { Loader2 } from 'lucide-react'
3
+
4
+ interface SpinnerProps {
5
+ /** Spinner size */
6
+ size?: 'sm' | 'md' | 'lg'
7
+ /** Show label below the spinner */
8
+ label?: string
9
+ className?: string
10
+ }
11
+
12
+ const sizes = {
13
+ sm: { icon: 'size-4', label: 'text-xs' },
14
+ md: { icon: 'size-6', label: 'text-sm' },
15
+ lg: { icon: 'size-8', label: 'text-sm' },
16
+ }
17
+
18
+ export function Spinner({ size = 'md', label, className }: SpinnerProps) {
19
+ const s = sizes[size]
20
+
21
+ return (
22
+ <div className={cn('inline-flex flex-col items-center gap-2', className)}>
23
+ <Loader2
24
+ className={cn('animate-spin text-accent', s.icon)}
25
+ role="status"
26
+ aria-label={label || 'Loading'}
27
+ />
28
+ {label && (
29
+ <span className={cn('text-muted', s.label)}>{label}</span>
30
+ )}
31
+ </div>
32
+ )
33
+ }
@@ -0,0 +1,96 @@
1
+ /*
2
+ * Theme tokens.
3
+ *
4
+ * Every color maps to a plain CSS variable, so apps (and dark themes) can
5
+ * re-skin the whole kit by overriding the `:root` block below.
6
+ */
7
+
8
+ @theme inline {
9
+ --color-page: var(--page);
10
+ --color-surface: var(--surface);
11
+ --color-hover: var(--hover);
12
+ --color-accent: var(--accent);
13
+ --color-fg: var(--accent-foreground);
14
+ --color-main: var(--text-primary);
15
+ --color-muted: var(--text-secondary);
16
+ --color-dim: var(--text-tertiary);
17
+ --color-line: var(--border);
18
+ --color-destructive: var(--destructive);
19
+ --color-success: var(--success);
20
+ --color-warning: var(--warning);
21
+ --color-info: var(--info);
22
+ }
23
+
24
+ :root {
25
+ --page: #f1f5f9;
26
+ --surface: #ffffff;
27
+ --hover: #e2e8f0;
28
+ --accent: #1e293b;
29
+ --accent-foreground: #ffffff;
30
+ --text-primary: #1e293b;
31
+ --text-secondary: #94a3b8;
32
+ --text-tertiary: #475569;
33
+ --border: #e2e8f0;
34
+ --destructive: #dc2626;
35
+ --success: #16a34a;
36
+ --warning: #d97706;
37
+ --info: #2563eb;
38
+ }
39
+
40
+ * {
41
+ border-color: var(--border);
42
+ }
43
+
44
+ body {
45
+ margin: 0;
46
+ background: var(--page);
47
+ color: var(--text-primary);
48
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
49
+ -webkit-font-smoothing: antialiased;
50
+ -moz-osx-font-smoothing: grayscale;
51
+ user-select: none;
52
+ }
53
+
54
+ /*
55
+ * Frameless window dragging: put `data-tauri-drag-region` on the element that
56
+ * should drag the window (see src/ui/titleBar.tsx). Interactive children must
57
+ * not carry the attribute.
58
+ */
59
+
60
+ svg:focus { outline: none; }
61
+ svg *:focus { outline: none; }
62
+
63
+ /* Overlay scrim shared by Dialog / Drawer / Toast containers */
64
+ .arshui-scrim {
65
+ background: rgba(0, 0, 0, 0.45);
66
+ }
67
+
68
+ @keyframes fade-in {
69
+ from { opacity: 0; }
70
+ to { opacity: 1; }
71
+ }
72
+
73
+ @keyframes scale-in {
74
+ from { opacity: 0; transform: scale(0.95); }
75
+ to { opacity: 1; transform: scale(1); }
76
+ }
77
+
78
+ @keyframes slide-in-right {
79
+ from { transform: translateX(100%); }
80
+ to { transform: translateX(0); }
81
+ }
82
+
83
+ @keyframes slide-in-left {
84
+ from { transform: translateX(-100%); }
85
+ to { transform: translateX(0); }
86
+ }
87
+
88
+ @keyframes slide-up {
89
+ from { transform: translateY(100%); }
90
+ to { transform: translateY(0); }
91
+ }
92
+
93
+ @keyframes progress-indeterminate {
94
+ 0% { transform: translateX(-100%); }
95
+ 100% { transform: translateX(400%); }
96
+ }
@@ -0,0 +1,77 @@
1
+ import { cn } from '../lib/utils'
2
+
3
+ interface SwitchProps {
4
+ checked: boolean
5
+ onChange: (checked: boolean) => void
6
+ /** Label text shown next to the switch */
7
+ label?: string
8
+ /** Position of the label */
9
+ labelPosition?: 'left' | 'right'
10
+ disabled?: boolean
11
+ className?: string
12
+ }
13
+
14
+ export function Switch({
15
+ checked,
16
+ onChange,
17
+ label,
18
+ labelPosition = 'right',
19
+ disabled = false,
20
+ className,
21
+ }: SwitchProps) {
22
+ const handleClick = () => {
23
+ if (!disabled) onChange(!checked)
24
+ }
25
+
26
+ const handleKeyDown = (e: React.KeyboardEvent) => {
27
+ if (e.key === 'Enter' || e.key === ' ') {
28
+ e.preventDefault()
29
+ handleClick()
30
+ }
31
+ }
32
+
33
+ const switchEl = (
34
+ <button
35
+ onClick={handleClick}
36
+ disabled={disabled}
37
+ className={cn(
38
+ 'relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full',
39
+ 'border-2 border-transparent transition-colors duration-200 ease-in-out',
40
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/20',
41
+ 'disabled:opacity-40 disabled:cursor-not-allowed',
42
+ checked ? 'bg-accent' : 'bg-hover',
43
+ )}
44
+ role="switch"
45
+ aria-checked={checked}
46
+ onKeyDown={handleKeyDown}
47
+ >
48
+ <span
49
+ className={cn(
50
+ 'pointer-events-none block size-3.5 rounded-full bg-surface shadow-sm',
51
+ 'ring-0 transition-transform duration-200 ease-in-out',
52
+ checked ? 'translate-x-[18px]' : 'translate-x-0.5',
53
+ )}
54
+ />
55
+ </button>
56
+ )
57
+
58
+ if (!label) return switchEl
59
+
60
+ return (
61
+ <label
62
+ className={cn(
63
+ 'inline-flex items-center gap-2 select-none cursor-pointer',
64
+ disabled && 'opacity-40 cursor-not-allowed',
65
+ className,
66
+ )}
67
+ >
68
+ {labelPosition === 'left' && (
69
+ <span className="text-sm text-main">{label}</span>
70
+ )}
71
+ {switchEl}
72
+ {labelPosition === 'right' && (
73
+ <span className="text-sm text-main">{label}</span>
74
+ )}
75
+ </label>
76
+ )
77
+ }