mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-28 07:32:52 +00:00
Inline style objects became gui props (Box for a div, View or Text rendering the element for the rest, since Box styles only a div), raw colours and sizes became the design's tokens, the brand swatches read @hanzo/design's own values, and the project and creation hues moved to palette.json, which is data. The newsletter's subscribe button is the standard filled Action. connect-steps.tsx, which nothing imports, is deleted with the brand constants nobody read. Pages compared before and after at 1440x900 and 390x844: blockchain, brand, directory, projects, settings, unsubscribe identical; founder 0.03% of pixels; buying specialist's title snaps from 28 to the ramp's 26px and the newsletter's button is now filled. Claude-Session: https://claude.ai/code/session_017vrVus6PPBgP5YRzKV1iqW
260 lines
7.6 KiB
TypeScript
260 lines
7.6 KiB
TypeScript
'use client'
|
|
|
|
import { Box, View } from '@hanzo/ui'
|
|
import { useEffect, useState, type ReactNode } from 'react'
|
|
|
|
function getWindow() {
|
|
if (typeof window === 'undefined') return null
|
|
const w = window as any
|
|
if (w.__TAURI__?.window?.getCurrentWindow) {
|
|
try {
|
|
return w.__TAURI__.window.getCurrentWindow()
|
|
} catch {}
|
|
}
|
|
return null
|
|
}
|
|
|
|
function getWindowLabel(): string {
|
|
if (typeof window === 'undefined') return 'main'
|
|
const w = window as any
|
|
const win = getWindow()
|
|
return (
|
|
win?.label ||
|
|
w.__TAURI_INTERNALS__?.metadata?.currentWindow?.label ||
|
|
w.__TAURI_INTERNALS__?.metadata?.currentWebview?.windowLabel ||
|
|
'main'
|
|
)
|
|
}
|
|
|
|
function invokeTauri(cmd: string, args: Record<string, unknown> = {}) {
|
|
if (typeof window === 'undefined') return Promise.resolve()
|
|
const w = window as any
|
|
const label = getWindowLabel()
|
|
const payload = { label, ...args }
|
|
|
|
if (w.__TAURI_INTERNALS__?.invoke) {
|
|
return w.__TAURI_INTERNALS__.invoke(cmd, payload)
|
|
}
|
|
if (w.__TAURI__?.core?.invoke) {
|
|
return w.__TAURI__.core.invoke(cmd, payload)
|
|
}
|
|
if (w.__TAURI__?.invoke) {
|
|
return w.__TAURI__.invoke(cmd, payload)
|
|
}
|
|
return Promise.resolve()
|
|
}
|
|
|
|
const INTERACTIVE_SELECTOR = [
|
|
'button',
|
|
'input',
|
|
'select',
|
|
'textarea',
|
|
'label',
|
|
'summary',
|
|
'[role="button"]',
|
|
'[role="link"]',
|
|
'[role="menuitem"]',
|
|
'[role="menuitemcheckbox"]',
|
|
'[role="menuitemradio"]',
|
|
'[role="tab"]',
|
|
'[role="checkbox"]',
|
|
'[role="radio"]',
|
|
'[role="switch"]',
|
|
'[role="option"]',
|
|
'[role="combobox"]',
|
|
'[aria-haspopup]',
|
|
'[contenteditable="true"]',
|
|
'[tabindex]:not([tabindex="-1"])',
|
|
'[data-tauri-drag-region="false"]',
|
|
'[data-slot="window-controls"]',
|
|
].join(', ')
|
|
|
|
function isInteractiveTarget(target: EventTarget | null): boolean {
|
|
const el = target as HTMLElement | null
|
|
if (!el || typeof el.closest !== 'function') return false
|
|
if (el.closest(INTERACTIVE_SELECTOR)) return true
|
|
if (el.closest('a:not([data-tauri-drag-region])')) return true
|
|
return false
|
|
}
|
|
|
|
let lastReactMouseDown = 0
|
|
|
|
export function dragWindow(e: React.MouseEvent) {
|
|
if (e.button !== 0) return
|
|
if (isInteractiveTarget(e.target)) return
|
|
|
|
lastReactMouseDown = Date.now()
|
|
e.preventDefault?.()
|
|
e.stopPropagation?.()
|
|
|
|
if (e.detail && e.detail > 1) return
|
|
|
|
const win = getWindow()
|
|
if (win?.startDragging) {
|
|
try {
|
|
win.startDragging()
|
|
return
|
|
} catch {}
|
|
}
|
|
invokeTauri('plugin:window|start_dragging')
|
|
}
|
|
|
|
export function toggleWindowMaximize(e: React.MouseEvent) {
|
|
if (isInteractiveTarget(e.target)) return
|
|
e.preventDefault?.()
|
|
e.stopPropagation?.()
|
|
|
|
const el = e.target as HTMLElement | null
|
|
if (
|
|
e.type === 'dblclick' &&
|
|
Date.now() - lastReactMouseDown > 500 &&
|
|
typeof el?.closest === 'function' &&
|
|
el.closest('[data-tauri-drag-region]')
|
|
) {
|
|
return
|
|
}
|
|
|
|
const win = getWindow()
|
|
if (win?.toggleMaximize) {
|
|
try {
|
|
win.toggleMaximize()
|
|
return
|
|
} catch {}
|
|
}
|
|
invokeTauri('plugin:window|toggle_maximize')
|
|
}
|
|
|
|
/** One of the three controls: a quiet square that takes a wash, and the ink, on hover. */
|
|
function Control({ label, onPress, wash = 'var(--white-08)', children }: { label: string; onPress: (e: React.MouseEvent) => void | Promise<void>; wash?: 'var(--white-08)' | 'var(--state-error)'; children: ReactNode }) {
|
|
return (
|
|
<View
|
|
render={<button type="button" data-tauri-drag-region="false" aria-label={label} onClick={onPress} onMouseDown={(e) => e.stopPropagation()} />}
|
|
items="center"
|
|
justify="center"
|
|
width={28}
|
|
height={28}
|
|
rounded={6}
|
|
bg="transparent"
|
|
borderWidth={0}
|
|
cursor="pointer"
|
|
hoverStyle={{ bg: wash }}
|
|
$platform-web={{ display: 'inline-flex', flexDirection: 'row', color: 'var(--muted-foreground)', transition: 'all 0.15s ease' }}
|
|
>
|
|
{children}
|
|
</View>
|
|
)
|
|
}
|
|
|
|
export function WindowControls() {
|
|
const [isDesktop, setIsDesktop] = useState(false)
|
|
const [isMaximized, setIsMaximized] = useState(false)
|
|
|
|
useEffect(() => {
|
|
const desktop =
|
|
typeof window !== 'undefined' &&
|
|
Boolean(
|
|
(window as any).__TAURI__ ||
|
|
(window as any).__TAURI_INTERNALS__
|
|
)
|
|
setIsDesktop(desktop)
|
|
if (!desktop) return
|
|
|
|
document.documentElement.setAttribute('data-desktop', 'true')
|
|
|
|
const syncMaximized = () => {
|
|
const win = getWindow()
|
|
const checkMax = win?.isMaximized
|
|
? win.isMaximized()
|
|
: invokeTauri('plugin:window|is_maximized')
|
|
|
|
Promise.resolve(checkMax)
|
|
.then((val) => {
|
|
const max = Boolean(val)
|
|
setIsMaximized(max)
|
|
if (max) document.documentElement.setAttribute('data-maximized', 'true')
|
|
else document.documentElement.removeAttribute('data-maximized')
|
|
})
|
|
.catch(() => {})
|
|
}
|
|
|
|
syncMaximized()
|
|
window.addEventListener('resize', syncMaximized)
|
|
return () => window.removeEventListener('resize', syncMaximized)
|
|
}, [])
|
|
|
|
if (!isDesktop) return null
|
|
|
|
const handleMinimize = (e: React.MouseEvent) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
const win = getWindow()
|
|
if (win?.minimize) win.minimize().catch(() => invokeTauri('plugin:window|minimize'))
|
|
else invokeTauri('plugin:window|minimize')
|
|
}
|
|
|
|
const handleToggleMaximize = async (e: React.MouseEvent) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
const win = getWindow()
|
|
if (win?.toggleMaximize) {
|
|
await win.toggleMaximize().catch(() => invokeTauri('plugin:window|toggle_maximize'))
|
|
} else {
|
|
await invokeTauri('plugin:window|toggle_maximize')
|
|
}
|
|
const val = win?.isMaximized
|
|
? await win.isMaximized().catch(() => !isMaximized)
|
|
: await invokeTauri('plugin:window|is_maximized').catch(() => !isMaximized)
|
|
const max = Boolean(val)
|
|
setIsMaximized(max)
|
|
if (max) document.documentElement.setAttribute('data-maximized', 'true')
|
|
else document.documentElement.removeAttribute('data-maximized')
|
|
}
|
|
|
|
const handleClose = (e: React.MouseEvent) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
const win = getWindow()
|
|
if (win?.close) win.close().catch(() => invokeTauri('plugin:window|close'))
|
|
else invokeTauri('plugin:window|close')
|
|
}
|
|
|
|
return (
|
|
<Box
|
|
data-slot="window-controls"
|
|
data-tauri-drag-region="false"
|
|
items="center"
|
|
gap={4}
|
|
marginInlineStart="auto"
|
|
shrink={0}
|
|
z="var(--z-banner)"
|
|
$platform-web={{ display: 'inline-flex', userSelect: 'none', flexDirection: 'row' }}
|
|
>
|
|
<Control label="Minimize" onPress={handleMinimize}>
|
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
<line x1="2" y1="6" x2="10" y2="6" strokeLinecap="round" />
|
|
</svg>
|
|
</Control>
|
|
|
|
<Control label={isMaximized ? 'Restore' : 'Maximize'} onPress={handleToggleMaximize}>
|
|
{isMaximized ? (
|
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.2">
|
|
<rect x="3.5" y="1.5" width="6.5" height="6.5" rx="1" />
|
|
<path d="M1.5 4.5v5a1 1 0 0 0 1 1h5" strokeLinecap="round" />
|
|
</svg>
|
|
) : (
|
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.2">
|
|
<rect x="2" y="2" width="8" height="8" rx="1.5" />
|
|
</svg>
|
|
)}
|
|
</Control>
|
|
|
|
<Control label="Close" onPress={handleClose} wash="var(--state-error)">
|
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
<line x1="2.5" y1="2.5" x2="9.5" y2="9.5" strokeLinecap="round" />
|
|
<line x1="9.5" y1="2.5" x2="2.5" y2="9.5" strokeLinecap="round" />
|
|
</svg>
|
|
</Control>
|
|
</Box>
|
|
)
|
|
}
|