forked from boos/BoosAPI
feat(desktop): embed BoosAPI Desktop client as a subproject
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as a monorepo subdirectory under desktop/. Source only — node_modules, dist, Cargo target and the upstream .git history are excluded. desktop/ keeps its own independent Cargo workspace (desktop/src-tauri with crates/*); it is not part of the backend boosapi crate, so a `cargo check` at the metapi root still compiles only the backend. Build the desktop app from desktop/src-tauri per desktop/README.md. Note: app icons are still placeholder assets pending the final BoosAPI logo. The updater signing keys live outside the repo and are not included.
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
initial: number
|
||||
}>()
|
||||
const { count, inc, dec } = useCounter(props.initial)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
{{ count }}
|
||||
<button class="inc" @click="inc()">
|
||||
+
|
||||
</button>
|
||||
<button class="dec" @click="dec()">
|
||||
-
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import Icon from '@iconify/vue'
|
||||
import { isDark, toggleDark } from '~/composables'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav text-xl mt-6 inline-flex gap-2>
|
||||
<button class="icon-btn !outline-none" @click="toggleDark()">
|
||||
<Icon v-if="isDark" class="i-carbon-moon" />
|
||||
<Icon v-else class="i-carbon-sun" />
|
||||
</button>
|
||||
|
||||
<a
|
||||
class="icon-btn"
|
||||
i-carbon-logo-github
|
||||
rel="noreferrer"
|
||||
href="https://github.com/Simon-He95/my-vitesse-app"
|
||||
target="_blank"
|
||||
title="GitHub"
|
||||
/>
|
||||
</nav>
|
||||
</template>
|
||||
@@ -0,0 +1,472 @@
|
||||
<script setup lang="ts">
|
||||
import { onClickOutside, useEventListener } from '@vueuse/core'
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
|
||||
type LabSelectValue = string | number
|
||||
|
||||
interface LabSelectOption {
|
||||
value: LabSelectValue
|
||||
label: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: LabSelectValue
|
||||
label: string
|
||||
options: readonly LabSelectOption[]
|
||||
placeholder?: string
|
||||
}>(), {
|
||||
placeholder: '请选择',
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: LabSelectValue]
|
||||
}>()
|
||||
|
||||
const uid = `lab-select-${Math.random().toString(36).slice(2, 9)}`
|
||||
const rootRef = ref<HTMLElement | null>(null)
|
||||
const triggerRef = ref<HTMLButtonElement | null>(null)
|
||||
const listRef = ref<HTMLElement | null>(null)
|
||||
const isOpen = ref(false)
|
||||
const activeIndex = ref(0)
|
||||
const placement = ref<'top' | 'bottom'>('bottom')
|
||||
const labelId = `${uid}-label`
|
||||
const listId = `${uid}-list`
|
||||
|
||||
const selectedIndex = computed(() => props.options.findIndex(option => option.value === props.modelValue))
|
||||
const selectedOption = computed(() => props.options[selectedIndex.value] ?? null)
|
||||
const activeDescendantId = computed(() => `${uid}-option-${activeIndex.value}`)
|
||||
|
||||
function syncActiveIndex() {
|
||||
activeIndex.value = selectedIndex.value >= 0 ? selectedIndex.value : 0
|
||||
}
|
||||
|
||||
function updatePlacement() {
|
||||
const element = rootRef.value
|
||||
if (!element)
|
||||
return
|
||||
|
||||
const rect = element.getBoundingClientRect()
|
||||
const estimatedHeight = Math.min(props.options.length * 50 + 18, 280)
|
||||
const spaceBelow = window.innerHeight - rect.bottom
|
||||
const spaceAbove = rect.top
|
||||
|
||||
placement.value = spaceBelow >= estimatedHeight || spaceBelow >= spaceAbove ? 'bottom' : 'top'
|
||||
}
|
||||
|
||||
function focusListbox() {
|
||||
nextTick(() => {
|
||||
updatePlacement()
|
||||
listRef.value?.focus()
|
||||
})
|
||||
}
|
||||
|
||||
function openMenu() {
|
||||
if (isOpen.value || !props.options.length)
|
||||
return
|
||||
|
||||
syncActiveIndex()
|
||||
isOpen.value = true
|
||||
focusListbox()
|
||||
}
|
||||
|
||||
function closeMenu({ restoreFocus = false }: { restoreFocus?: boolean } = {}) {
|
||||
if (!isOpen.value)
|
||||
return
|
||||
|
||||
isOpen.value = false
|
||||
|
||||
if (restoreFocus) {
|
||||
nextTick(() => {
|
||||
triggerRef.value?.focus()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function toggleMenu() {
|
||||
if (isOpen.value)
|
||||
closeMenu()
|
||||
else
|
||||
openMenu()
|
||||
}
|
||||
|
||||
function moveActive(step: number) {
|
||||
if (!props.options.length)
|
||||
return
|
||||
|
||||
if (!isOpen.value) {
|
||||
openMenu()
|
||||
return
|
||||
}
|
||||
|
||||
const next = activeIndex.value + step
|
||||
const lastIndex = props.options.length - 1
|
||||
|
||||
if (next < 0)
|
||||
activeIndex.value = lastIndex
|
||||
else if (next > lastIndex)
|
||||
activeIndex.value = 0
|
||||
else
|
||||
activeIndex.value = next
|
||||
}
|
||||
|
||||
function selectActiveOption() {
|
||||
const option = props.options[activeIndex.value]
|
||||
if (option)
|
||||
selectOption(option.value)
|
||||
}
|
||||
|
||||
function selectOption(value: LabSelectValue) {
|
||||
emit('update:modelValue', value)
|
||||
closeMenu({ restoreFocus: true })
|
||||
}
|
||||
|
||||
function handleTriggerKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'ArrowDown' || event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault()
|
||||
openMenu()
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowUp') {
|
||||
event.preventDefault()
|
||||
syncActiveIndex()
|
||||
moveActive(-1)
|
||||
}
|
||||
}
|
||||
|
||||
function handleListKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'ArrowDown') {
|
||||
event.preventDefault()
|
||||
moveActive(1)
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowUp') {
|
||||
event.preventDefault()
|
||||
moveActive(-1)
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === 'Home') {
|
||||
event.preventDefault()
|
||||
activeIndex.value = 0
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === 'End') {
|
||||
event.preventDefault()
|
||||
activeIndex.value = Math.max(props.options.length - 1, 0)
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault()
|
||||
selectActiveOption()
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault()
|
||||
closeMenu({ restoreFocus: true })
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === 'Tab')
|
||||
closeMenu()
|
||||
}
|
||||
|
||||
watch(() => props.modelValue, () => {
|
||||
if (!isOpen.value)
|
||||
syncActiveIndex()
|
||||
})
|
||||
|
||||
watch(() => props.options, () => {
|
||||
syncActiveIndex()
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
onClickOutside(rootRef, () => {
|
||||
closeMenu()
|
||||
})
|
||||
|
||||
useEventListener(window, 'resize', () => {
|
||||
if (isOpen.value)
|
||||
updatePlacement()
|
||||
})
|
||||
|
||||
useEventListener(window, 'scroll', () => {
|
||||
if (isOpen.value)
|
||||
updatePlacement()
|
||||
}, { capture: true, passive: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="rootRef"
|
||||
class="lab-select select-control"
|
||||
:class="{
|
||||
'lab-select--open': isOpen,
|
||||
'lab-select--top': placement === 'top',
|
||||
}"
|
||||
>
|
||||
<span :id="labelId" class="lab-select__label">{{ label }}</span>
|
||||
|
||||
<button
|
||||
ref="triggerRef"
|
||||
type="button"
|
||||
class="lab-select__trigger"
|
||||
:aria-expanded="isOpen"
|
||||
aria-haspopup="listbox"
|
||||
:aria-labelledby="labelId"
|
||||
@click="toggleMenu"
|
||||
@keydown="handleTriggerKeydown"
|
||||
>
|
||||
<span class="lab-select__value">
|
||||
{{ selectedOption?.label ?? placeholder }}
|
||||
</span>
|
||||
<span class="lab-select__chevron" aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
<transition name="lab-select-menu">
|
||||
<div
|
||||
v-if="isOpen"
|
||||
:id="listId"
|
||||
ref="listRef"
|
||||
class="lab-select__menu"
|
||||
role="listbox"
|
||||
tabindex="-1"
|
||||
:aria-activedescendant="activeDescendantId"
|
||||
:aria-labelledby="labelId"
|
||||
@keydown="handleListKeydown"
|
||||
>
|
||||
<div
|
||||
v-for="(option, index) in options"
|
||||
:id="`${uid}-option-${index}`"
|
||||
:key="option.value"
|
||||
class="lab-select__option"
|
||||
:class="{
|
||||
'lab-select__option--active': index === activeIndex,
|
||||
'lab-select__option--selected': option.value === modelValue,
|
||||
}"
|
||||
role="option"
|
||||
:aria-selected="option.value === modelValue"
|
||||
@click="selectOption(option.value)"
|
||||
@mouseenter="activeIndex = index"
|
||||
>
|
||||
<span class="lab-select__option-label">{{ option.label }}</span>
|
||||
<span
|
||||
v-if="option.value === modelValue"
|
||||
class="lab-select__option-indicator"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.lab-select {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.lab-select__label {
|
||||
color: var(--lab-muted);
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
.lab-select__trigger {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
min-height: 52px;
|
||||
padding: 12px 14px 12px 16px;
|
||||
border: 1px solid rgba(15, 23, 42, 0.08);
|
||||
border-radius: 16px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 250, 252, 0.92));
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.82),
|
||||
0 10px 24px rgba(15, 23, 42, 0.06);
|
||||
color: var(--lab-text);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition:
|
||||
border-color 0.18s ease,
|
||||
box-shadow 0.18s ease,
|
||||
transform 0.18s ease,
|
||||
background 0.18s ease;
|
||||
}
|
||||
|
||||
.lab-select__trigger:hover {
|
||||
border-color: rgba(15, 118, 110, 0.18);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.88),
|
||||
0 14px 28px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.lab-select__trigger:focus-visible {
|
||||
outline: none;
|
||||
border-color: var(--lab-accent);
|
||||
box-shadow:
|
||||
0 0 0 3px var(--lab-accent-soft),
|
||||
0 16px 32px rgba(15, 23, 42, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.lab-select--open .lab-select__trigger {
|
||||
border-color: rgba(15, 118, 110, 0.24);
|
||||
box-shadow:
|
||||
0 0 0 3px var(--lab-accent-soft),
|
||||
0 18px 36px rgba(15, 23, 42, 0.12);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.lab-select__value {
|
||||
min-width: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.lab-select__chevron {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-right: 4px;
|
||||
border-right: 1.6px solid currentColor;
|
||||
border-bottom: 1.6px solid currentColor;
|
||||
color: var(--lab-muted);
|
||||
transform: rotate(45deg);
|
||||
transition: transform 0.18s ease, color 0.18s ease;
|
||||
}
|
||||
|
||||
.lab-select--open .lab-select__chevron {
|
||||
color: var(--lab-accent);
|
||||
transform: rotate(-135deg) translate(-1px, -1px);
|
||||
}
|
||||
|
||||
.lab-select__menu {
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
right: 14px;
|
||||
top: calc(100% - 2px);
|
||||
z-index: 40;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
max-height: 280px;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
border-radius: 18px;
|
||||
border: 1px solid rgba(15, 23, 42, 0.08);
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
box-shadow: 0 24px 56px rgba(15, 23, 42, 0.16);
|
||||
backdrop-filter: blur(20px) saturate(1.2);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.lab-select--top .lab-select__menu {
|
||||
top: auto;
|
||||
bottom: calc(100% - 2px);
|
||||
}
|
||||
|
||||
.lab-select__option {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-height: 44px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
color: var(--lab-text);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 0.14s ease,
|
||||
color 0.14s ease,
|
||||
transform 0.14s ease;
|
||||
}
|
||||
|
||||
.lab-select__option--active {
|
||||
background: rgba(15, 23, 42, 0.04);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.lab-select__option--selected {
|
||||
background: var(--lab-accent-soft);
|
||||
color: var(--lab-accent);
|
||||
}
|
||||
|
||||
.lab-select__option-label {
|
||||
min-width: 0;
|
||||
font-size: 0.94rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.lab-select__option-indicator {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 999px;
|
||||
background: currentColor;
|
||||
box-shadow: 0 0 0 4px color-mix(in srgb, currentColor 14%, transparent);
|
||||
}
|
||||
|
||||
.lab-select-menu-enter-active,
|
||||
.lab-select-menu-leave-active {
|
||||
transition: opacity 0.14s ease, transform 0.14s ease;
|
||||
}
|
||||
|
||||
.lab-select-menu-enter-from,
|
||||
.lab-select-menu-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
}
|
||||
|
||||
.lab-select--top .lab-select-menu-enter-from,
|
||||
.lab-select--top .lab-select-menu-leave-to {
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
|
||||
:global(.test-lab--dark) .lab-select__trigger {
|
||||
border-color: rgba(148, 163, 184, 0.16);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(15, 23, 42, 0.92), rgba(15, 23, 42, 0.76));
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.04),
|
||||
0 12px 28px rgba(2, 6, 23, 0.26);
|
||||
}
|
||||
|
||||
:global(.test-lab--dark) .lab-select__trigger:hover {
|
||||
border-color: rgba(34, 211, 238, 0.22);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.05),
|
||||
0 16px 32px rgba(2, 6, 23, 0.3);
|
||||
}
|
||||
|
||||
:global(.test-lab--dark) .lab-select--open .lab-select__trigger,
|
||||
:global(.test-lab--dark) .lab-select__trigger:focus-visible {
|
||||
border-color: rgba(34, 211, 238, 0.28);
|
||||
box-shadow:
|
||||
0 0 0 3px rgba(34, 211, 238, 0.16),
|
||||
0 18px 36px rgba(2, 6, 23, 0.34);
|
||||
}
|
||||
|
||||
:global(.test-lab--dark) .lab-select__menu {
|
||||
border-color: rgba(148, 163, 184, 0.12);
|
||||
background: rgba(15, 23, 42, 0.92);
|
||||
box-shadow: 0 28px 64px rgba(2, 6, 23, 0.44);
|
||||
}
|
||||
|
||||
:global(.test-lab--dark) .lab-select__option--active {
|
||||
background: rgba(148, 163, 184, 0.08);
|
||||
}
|
||||
|
||||
:global(.test-lab--dark) .lab-select__option--selected {
|
||||
background: rgba(34, 211, 238, 0.14);
|
||||
color: #67e8f9;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
node: {
|
||||
type: 'thinking'
|
||||
content: string
|
||||
children: any[]
|
||||
loading?: boolean
|
||||
}
|
||||
isDark?: boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="thinking-node">
|
||||
{{ node.content }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.thinking-node {
|
||||
margin: 1em 0;
|
||||
padding: 0.75em 1em;
|
||||
font-size: 0.8125rem;
|
||||
color: hsl(var(--ms-muted-foreground));
|
||||
border-left: 2px solid hsl(var(--ms-border));
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user