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.
- package/LICENSE +21 -0
- package/README.md +55 -2
- package/index.js +239 -0
- package/package.json +43 -4
- package/template/.eslintrc.cjs +18 -0
- package/template/.github/workflows/ci.yml +50 -0
- package/template/AGENTS.md +50 -0
- package/template/README.md +51 -0
- package/template/gitignore +37 -0
- package/template/index.html +12 -0
- package/template/package-lock.json +4454 -0
- package/template/package.json +42 -0
- package/template/src/App.tsx +35 -0
- package/template/src/index.css +8 -0
- package/template/src/lib/tauri.ts +21 -0
- package/template/src/lib/utils.test.ts +17 -0
- package/template/src/lib/utils.ts +6 -0
- package/template/src/main.tsx +10 -0
- package/template/src/ui/appLayout.tsx +35 -0
- package/template/src/ui/button.tsx +63 -0
- package/template/src/ui/dialog.tsx +71 -0
- package/template/src/ui/index.ts +8 -0
- package/template/src/ui/navItem.tsx +37 -0
- package/template/src/ui/spinner.tsx +33 -0
- package/template/src/ui/styles.css +96 -0
- package/template/src/ui/switch.tsx +77 -0
- package/template/src/ui/titleBar.tsx +157 -0
- package/template/src/ui/tooltip.tsx +141 -0
- package/template/src/vite-env.d.ts +1 -0
- package/template/src-tauri/Cargo.lock +4316 -0
- package/template/src-tauri/Cargo.toml +26 -0
- package/template/src-tauri/build.rs +3 -0
- package/template/src-tauri/capabilities/default.json +16 -0
- package/template/src-tauri/icons/128x128.png +0 -0
- package/template/src-tauri/icons/128x128@2x.png +0 -0
- package/template/src-tauri/icons/32x32.png +0 -0
- package/template/src-tauri/icons/icon.icns +0 -0
- package/template/src-tauri/icons/icon.ico +0 -0
- package/template/src-tauri/src/lib.rs +14 -0
- package/template/src-tauri/src/main.rs +6 -0
- package/template/src-tauri/tauri.conf.json +40 -0
- package/template/temp/.gitkeep +0 -0
- package/template/tsconfig.json +25 -0
- package/template/tsconfig.node.json +12 -0
- 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" />
|