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,157 @@
1
+ import { useState, useEffect, useCallback, type ReactNode } from 'react'
2
+ import { Minus, Pin } from 'lucide-react'
3
+ import { cn } from '../lib/utils'
4
+ import { getWindow } from '../lib/tauri'
5
+
6
+ interface TitleBarProps {
7
+ /** App brand name shown on the left */
8
+ brand?: string
9
+ /** Navigation items rendered next to the brand */
10
+ children?: ReactNode
11
+ /** Extra action buttons rendered before window controls */
12
+ actions?: ReactNode
13
+ /** Whether to show the always-on-top pin button */
14
+ enablePin?: boolean
15
+ /** Current pin state (controlled) */
16
+ pinned?: boolean
17
+ /** Called when pin is toggled */
18
+ onPinToggle?: (pinned: boolean) => void
19
+ /** Called when window close is requested */
20
+ onCloseRequest?: () => void
21
+ }
22
+
23
+ export function TitleBar({
24
+ brand = 'Arsh Tauri',
25
+ children,
26
+ actions,
27
+ enablePin = false,
28
+ pinned = false,
29
+ onPinToggle,
30
+ onCloseRequest,
31
+ }: TitleBarProps) {
32
+ const win = getWindow()
33
+ const [isMaximized, setIsMaximized] = useState(false)
34
+
35
+ useEffect(() => {
36
+ if (!win) return
37
+ let disposed = false
38
+ let unlisten: (() => void) | undefined
39
+ win.isMaximized().then((v) => !disposed && setIsMaximized(v))
40
+ win.onResized(async () => setIsMaximized(await win.isMaximized())).then((un) => {
41
+ if (disposed) un()
42
+ else unlisten = un
43
+ })
44
+ return () => {
45
+ disposed = true
46
+ unlisten?.()
47
+ }
48
+ }, [win])
49
+
50
+ const handleDoubleClick = useCallback(() => {
51
+ getWindow()?.toggleMaximize()
52
+ }, [])
53
+
54
+ // Plain web environment (no Tauri runtime): hide window controls, keep the bar
55
+ if (!win) {
56
+ return (
57
+ <div className="flex items-stretch shrink-0 h-7 bg-page select-none relative z-50 border-b border-line">
58
+ <span className="flex items-center px-3 text-sm font-medium text-main/60 shrink-0 select-none">
59
+ {brand}
60
+ </span>
61
+ {children}
62
+ <div className="flex-1 min-w-0" />
63
+ {actions && <div className="flex items-center gap-1 px-1.5">{actions}</div>}
64
+ </div>
65
+ )
66
+ }
67
+
68
+ const handleMinimize = () => void win.minimize()
69
+ const handleMaximize = () => void win.toggleMaximize()
70
+ const handleClose = () => {
71
+ if (onCloseRequest) {
72
+ onCloseRequest()
73
+ } else {
74
+ void win.close()
75
+ }
76
+ }
77
+
78
+ return (
79
+ <div
80
+ className="flex items-stretch shrink-0 h-7 bg-page select-none relative z-50 border-b border-line"
81
+ onDoubleClick={handleDoubleClick}
82
+ >
83
+ {/* Brand */}
84
+ <span
85
+ data-tauri-drag-region
86
+ className="flex items-center px-3 text-sm font-medium text-main/60 shrink-0 select-none"
87
+ >
88
+ {brand}
89
+ </span>
90
+
91
+ {/* Navigation items */}
92
+ {children}
93
+
94
+ {/* Spacer */}
95
+ <div data-tauri-drag-region className="flex-1 min-w-0" />
96
+
97
+ {/* Actions */}
98
+ {actions && (
99
+ <div className="flex items-center gap-1 px-1.5">
100
+ {actions}
101
+ </div>
102
+ )}
103
+
104
+ {/* Window controls */}
105
+ <div className="flex items-center gap-0.5 px-1.5">
106
+ {enablePin && (
107
+ <button
108
+ onClick={() => onPinToggle?.(!pinned)}
109
+ className={cn(
110
+ 'size-5 flex items-center justify-center transition-all duration-200 cursor-pointer',
111
+ pinned ? 'text-main' : 'text-muted hover:text-main',
112
+ )}
113
+ title={pinned ? 'Unpin window' : 'Pin window'}
114
+ >
115
+ <Pin className={cn('size-4', pinned && 'fill-main')} />
116
+ </button>
117
+ )}
118
+ {enablePin && <div className="w-px h-3.5 bg-line/60 mx-0.5" />}
119
+ <button
120
+ onClick={handleMinimize}
121
+ className="size-5 flex items-center justify-center text-muted hover:text-main transition-all duration-200 cursor-pointer"
122
+ title="Minimize"
123
+ aria-label="Minimize"
124
+ >
125
+ <Minus className="size-4" />
126
+ </button>
127
+ <button
128
+ onClick={handleMaximize}
129
+ className="size-5 flex items-center justify-center text-muted hover:text-main transition-all duration-200 cursor-pointer"
130
+ title={isMaximized ? 'Restore' : 'Maximize'}
131
+ aria-label={isMaximized ? 'Restore' : 'Maximize'}
132
+ >
133
+ {isMaximized ? (
134
+ <svg width="14" height="14" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.3">
135
+ <rect x="3" y="3" width="7" height="7" rx="1" />
136
+ <path d="M3 5H2V2a1 1 0 0 1 1-1h5v1" />
137
+ </svg>
138
+ ) : (
139
+ <svg width="14" height="14" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.3">
140
+ <rect x="1.5" y="1.5" width="9" height="9" rx="1.2" />
141
+ </svg>
142
+ )}
143
+ </button>
144
+ <button
145
+ onClick={handleClose}
146
+ className="size-5 flex items-center justify-center text-muted hover:text-main transition-all duration-200 cursor-pointer"
147
+ title="Close"
148
+ aria-label="Close"
149
+ >
150
+ <svg width="14" height="14" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
151
+ <path d="M2 2l8 8M10 2l-8 8" />
152
+ </svg>
153
+ </button>
154
+ </div>
155
+ </div>
156
+ )
157
+ }
@@ -0,0 +1,141 @@
1
+ import { useState, useRef, useCallback, useLayoutEffect, useEffect, type ReactNode } from 'react'
2
+ import { createPortal } from 'react-dom'
3
+ import { cn } from '../lib/utils'
4
+
5
+ type TooltipPosition = 'top' | 'bottom' | 'left' | 'right'
6
+
7
+ interface TooltipProps {
8
+ children: ReactNode
9
+ content: string
10
+ position?: TooltipPosition
11
+ /** Delay in ms before showing (default: 400) */
12
+ showDelay?: number
13
+ /** Make the trigger element fill full width */
14
+ fullWidth?: boolean
15
+ /** Temporarily disable tooltip */
16
+ disabled?: boolean
17
+ }
18
+
19
+ const GAP = 6
20
+
21
+ export function Tooltip({ children, content, position = 'bottom', showDelay = 400, fullWidth = false, disabled = false }: TooltipProps) {
22
+ const [open, setOpen] = useState(false)
23
+ const triggerRef = useRef<HTMLDivElement>(null)
24
+ const tooltipRef = useRef<HTMLDivElement>(null)
25
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
26
+
27
+ const clearTimer = () => {
28
+ if (timerRef.current !== null) {
29
+ clearTimeout(timerRef.current)
30
+ timerRef.current = null
31
+ }
32
+ }
33
+
34
+ // Hiding when disabled is derived from `visible`; no setState inside the effect
35
+ useEffect(() => {
36
+ if (disabled) clearTimer()
37
+ }, [disabled])
38
+ const visible = open && !disabled
39
+
40
+ const calcPosition = useCallback(() => {
41
+ const trigger = triggerRef.current
42
+ const tooltip = tooltipRef.current
43
+ if (!trigger || !tooltip) return null
44
+
45
+ const triggerRect = trigger.getBoundingClientRect()
46
+ const { width: tw, height: th } = tooltip.getBoundingClientRect()
47
+ const viewW = window.innerWidth
48
+ const viewH = window.innerHeight
49
+
50
+ let top = 0, left = 0, useTranslateX = true
51
+
52
+ switch (position) {
53
+ case 'top': {
54
+ top = triggerRect.top - GAP - th
55
+ left = triggerRect.left + triggerRect.width / 2
56
+ if (top < 0) { top = triggerRect.bottom + GAP }
57
+ break
58
+ }
59
+ case 'bottom': {
60
+ top = triggerRect.bottom + GAP
61
+ left = triggerRect.left + triggerRect.width / 2
62
+ if (top + th > viewH) { top = triggerRect.top - GAP - th }
63
+ break
64
+ }
65
+ case 'left': {
66
+ top = triggerRect.top + triggerRect.height / 2
67
+ left = triggerRect.left - GAP - tw
68
+ useTranslateX = false
69
+ if (left < 0) { left = triggerRect.right + GAP }
70
+ break
71
+ }
72
+ case 'right': {
73
+ top = triggerRect.top + triggerRect.height / 2
74
+ left = triggerRect.right + GAP
75
+ useTranslateX = false
76
+ if (left + tw > viewW) { left = triggerRect.left - GAP - tw }
77
+ break
78
+ }
79
+ }
80
+
81
+ // Clamp
82
+ if (useTranslateX) {
83
+ const halfW = tw / 2
84
+ if (left < halfW) left = halfW
85
+ if (left > viewW - halfW) left = viewW - halfW
86
+ return { position: 'fixed' as const, top, left, transform: 'translate(-50%, 0)' }
87
+ } else {
88
+ const halfH = th / 2
89
+ if (top < halfH) top = halfH
90
+ if (top > viewH - halfH) top = viewH - halfH
91
+ return { position: 'fixed' as const, top, left, transform: 'translate(0, -50%)' }
92
+ }
93
+ }, [position])
94
+
95
+ // After opening, use the measured dimensions for positioning before the browser paints: write directly to the DOM, avoiding setState cascades
96
+ useLayoutEffect(() => {
97
+ if (!visible) return
98
+ const pos = calcPosition()
99
+ if (pos && tooltipRef.current) Object.assign(tooltipRef.current.style, pos)
100
+ }, [visible, calcPosition])
101
+
102
+ const handleMouseEnter = () => {
103
+ if (disabled) return
104
+ clearTimer()
105
+ timerRef.current = setTimeout(() => setOpen(true), showDelay)
106
+ }
107
+
108
+ const handleMouseLeave = () => {
109
+ clearTimer()
110
+ setOpen(false)
111
+ }
112
+
113
+ return (
114
+ <>
115
+ <div
116
+ ref={triggerRef}
117
+ className={cn('inline-flex', fullWidth && 'w-full')}
118
+ onMouseEnter={handleMouseEnter}
119
+ onMouseLeave={handleMouseLeave}
120
+ onFocus={handleMouseEnter}
121
+ onBlur={handleMouseLeave}
122
+ >
123
+ {children}
124
+ </div>
125
+ {visible && createPortal(
126
+ <div
127
+ ref={tooltipRef}
128
+ className={cn(
129
+ 'px-2 py-0.5 text-xs rounded-md',
130
+ 'bg-surface text-main text-center',
131
+ 'shadow-md border border-line whitespace-nowrap',
132
+ 'pointer-events-none select-none z-[100]',
133
+ )}
134
+ >
135
+ {content}
136
+ </div>,
137
+ document.body,
138
+ )}
139
+ </>
140
+ )
141
+ }
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />