@demicodes/web-ui 0.3.1
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/package.json +55 -0
- package/src/agent/AgentMessageInput.vue +164 -0
- package/src/agent/AgentMessageList.vue +124 -0
- package/src/agent/AgentPanel.vue +18 -0
- package/src/agent/AgentRoot.vue +17 -0
- package/src/agent/AgentTabBar.vue +422 -0
- package/src/agent/AgentTabItem.vue +150 -0
- package/src/agent/ContextUsageIndicator.vue +136 -0
- package/src/agent/ConversationListDropdown.vue +71 -0
- package/src/agent/ConversationStatusDot.vue +26 -0
- package/src/agent/ConversationView.vue +82 -0
- package/src/agent/MessageQueueBar.vue +67 -0
- package/src/agent/ModelSelector.vue +125 -0
- package/src/agent/ReasoningSelector.vue +106 -0
- package/src/agent/SelectorTrigger.vue +17 -0
- package/src/agent/__tests__/block-helpers.test.ts +73 -0
- package/src/agent/__tests__/input-actions.test.ts +130 -0
- package/src/agent/__tests__/input-editor.test.ts +15 -0
- package/src/agent/__tests__/pending-steers.test.ts +99 -0
- package/src/agent/__tests__/queue-submit.test.ts +14 -0
- package/src/agent/__tests__/reasoning.test.ts +32 -0
- package/src/agent/__tests__/tail-loading.test.ts +131 -0
- package/src/agent/__tests__/tool-rendering.test.ts +35 -0
- package/src/agent/__tests__/visible-blocks.test.ts +125 -0
- package/src/agent/block-helpers.ts +59 -0
- package/src/agent/block-types.ts +12 -0
- package/src/agent/blocks/AbortedBlock.vue +26 -0
- package/src/agent/blocks/AgentMessageVirtualBlock.vue +88 -0
- package/src/agent/blocks/AnsiText.vue +75 -0
- package/src/agent/blocks/AssistantTextBlock.vue +34 -0
- package/src/agent/blocks/CompactionBlock.vue +36 -0
- package/src/agent/blocks/ErrorBlock.vue +79 -0
- package/src/agent/blocks/FunctionalBlock.vue +118 -0
- package/src/agent/blocks/LoadingBlock.vue +28 -0
- package/src/agent/blocks/ThinkingBlock.vue +86 -0
- package/src/agent/blocks/ToolCallBlock.vue +40 -0
- package/src/agent/blocks/ToolGenericBlock.vue +43 -0
- package/src/agent/blocks/ToolShellAbortBlock.vue +13 -0
- package/src/agent/blocks/ToolShellBlock.vue +47 -0
- package/src/agent/blocks/ToolShellControlBlock.vue +51 -0
- package/src/agent/blocks/ToolShellStatusBlock.vue +13 -0
- package/src/agent/blocks/ToolShellWriteBlock.vue +13 -0
- package/src/agent/blocks/ToolStatusBadge.vue +13 -0
- package/src/agent/blocks/ToolYieldBlock.vue +13 -0
- package/src/agent/blocks/UserBlock.vue +112 -0
- package/src/agent/conversation-runtime.ts +205 -0
- package/src/agent/conversation-status.ts +12 -0
- package/src/agent/message-input/input-model.ts +33 -0
- package/src/agent/message-input/useAgentInputActions.ts +96 -0
- package/src/agent/message-input/useAgentInputEditor.ts +90 -0
- package/src/agent/message-input/useAgentInputSessionState.ts +48 -0
- package/src/agent/pending-steers.ts +134 -0
- package/src/agent/providers/ProviderIcon.vue +32 -0
- package/src/agent/providers/icons/claude.svg +1 -0
- package/src/agent/providers/icons/openai.svg +3 -0
- package/src/agent/queue-submit.ts +4 -0
- package/src/agent/reasoning.ts +45 -0
- package/src/agent/tail-loading.ts +31 -0
- package/src/agent/thinking-streaming.ts +7 -0
- package/src/agent/tool-rendering.ts +59 -0
- package/src/agent/types.ts +39 -0
- package/src/agent/ui-options.ts +21 -0
- package/src/agent/visible-blocks.ts +25 -0
- package/src/agent/workspace.ts +355 -0
- package/src/composables/useBlockVirtualizer.ts +290 -0
- package/src/composables/useContextMenuOwner.ts +44 -0
- package/src/composables/useOverlay.ts +18 -0
- package/src/composables/useTerminalTheme.ts +75 -0
- package/src/env.d.ts +10 -0
- package/src/infra/errors.ts +29 -0
- package/src/infra/i18n.ts +41 -0
- package/src/markdown/filePath.ts +54 -0
- package/src/markdown/highlight.ts +139 -0
- package/src/markdown/md.ts +58 -0
- package/src/markdown/render.ts +117 -0
- package/src/markdown/types.ts +20 -0
- package/src/overlay/appOverlay.ts +3 -0
- package/src/overlay/overlayStore.ts +54 -0
- package/src/store/createStore.ts +33 -0
- package/src/store/useStore.ts +7 -0
- package/src/styles/base.css +342 -0
- package/src/theme/appTheme.ts +56 -0
- package/src/theme/codeThemes.ts +454 -0
- package/src/theme/themeStore.ts +33 -0
- package/src/transport/agent-socket.ts +20 -0
- package/src/transport/control-client.ts +64 -0
- package/src/transport/protocol.ts +57 -0
- package/src/ui/Button.vue +30 -0
- package/src/ui/Checkbox.vue +25 -0
- package/src/ui/ContextMenu.vue +33 -0
- package/src/ui/Dialog.vue +48 -0
- package/src/ui/DropdownMenu.vue +77 -0
- package/src/ui/ErrorBox.vue +43 -0
- package/src/ui/HighlightText.vue +39 -0
- package/src/ui/IconButton.vue +30 -0
- package/src/ui/IndeterminateSpinner.vue +72 -0
- package/src/ui/InlineError.vue +25 -0
- package/src/ui/LoadMore.vue +32 -0
- package/src/ui/MarkdownPreview.vue +31 -0
- package/src/ui/Menu.vue +8 -0
- package/src/ui/MenuDivider.vue +6 -0
- package/src/ui/MenuItem.vue +48 -0
- package/src/ui/OptionMenu.vue +8 -0
- package/src/ui/OptionMenuGroup.vue +16 -0
- package/src/ui/OptionMenuItem.vue +28 -0
- package/src/ui/Popover.vue +90 -0
- package/src/ui/ScrollToBottomButton.vue +32 -0
- package/src/ui/SelectDropdown.vue +231 -0
- package/src/ui/Switch.vue +41 -0
- package/src/ui/TextInput.vue +44 -0
- package/src/ui/ThemeToggle.vue +49 -0
- package/src/ui/ToggleSwitch.vue +34 -0
- package/src/ui/Tooltip.vue +143 -0
- package/src/ui/blankClickGuard.ts +12 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { OverlayStore } from '../overlay/overlayStore'
|
|
3
|
+
import { useContextMenuOwner } from '../composables/useContextMenuOwner'
|
|
4
|
+
import Popover from './Popover.vue'
|
|
5
|
+
import Menu from './Menu.vue'
|
|
6
|
+
|
|
7
|
+
defineProps<{
|
|
8
|
+
overlayStore: OverlayStore
|
|
9
|
+
}>()
|
|
10
|
+
|
|
11
|
+
const emit = defineEmits<{
|
|
12
|
+
open: [event: MouseEvent]
|
|
13
|
+
close: []
|
|
14
|
+
}>()
|
|
15
|
+
|
|
16
|
+
const { isOpen, anchorX, anchorY, menuKey, open, close } = useContextMenuOwner(() => emit('close'))
|
|
17
|
+
|
|
18
|
+
function handleContextMenu(event: MouseEvent) {
|
|
19
|
+
open(event)
|
|
20
|
+
emit('open', event)
|
|
21
|
+
}
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<div @contextmenu="handleContextMenu">
|
|
26
|
+
<slot name="trigger" />
|
|
27
|
+
<Popover :key="menuKey" :overlay-store="overlayStore" :is-open="isOpen" :anchor-x="anchorX" :anchor-y="anchorY" :offset="0" @close="close">
|
|
28
|
+
<Menu @click="close">
|
|
29
|
+
<slot name="menu" />
|
|
30
|
+
</Menu>
|
|
31
|
+
</Popover>
|
|
32
|
+
</div>
|
|
33
|
+
</template>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { OverlayStore } from '../overlay/overlayStore'
|
|
3
|
+
import { useOverlay } from '../composables/useOverlay'
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{
|
|
6
|
+
isOpen: boolean
|
|
7
|
+
overlayStore: OverlayStore
|
|
8
|
+
}>()
|
|
9
|
+
|
|
10
|
+
const emit = defineEmits<{
|
|
11
|
+
close: []
|
|
12
|
+
}>()
|
|
13
|
+
|
|
14
|
+
useOverlay(props.overlayStore, () => props.isOpen, () => {
|
|
15
|
+
if (props.isOpen) emit('close')
|
|
16
|
+
})
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<template>
|
|
20
|
+
<Teleport to="body">
|
|
21
|
+
<Transition
|
|
22
|
+
enter-active-class="transition-opacity duration-150 ease-out"
|
|
23
|
+
leave-active-class="transition-opacity duration-150 ease-out"
|
|
24
|
+
enter-from-class="opacity-0"
|
|
25
|
+
leave-to-class="opacity-0"
|
|
26
|
+
>
|
|
27
|
+
<div
|
|
28
|
+
v-if="isOpen"
|
|
29
|
+
class="fixed inset-0 z-50 grid place-items-center bg-black/60"
|
|
30
|
+
@click.self="emit('close')"
|
|
31
|
+
>
|
|
32
|
+
<Transition
|
|
33
|
+
appear
|
|
34
|
+
enter-active-class="transition-[opacity,transform] duration-150 ease-out"
|
|
35
|
+
leave-active-class="transition-[opacity,transform] duration-150 ease-out"
|
|
36
|
+
enter-from-class="opacity-0 scale-95"
|
|
37
|
+
leave-to-class="opacity-0 scale-95"
|
|
38
|
+
>
|
|
39
|
+
<div
|
|
40
|
+
class="w-full max-w-md rounded-xl border border-line bg-surface shadow-2xl"
|
|
41
|
+
>
|
|
42
|
+
<slot />
|
|
43
|
+
</div>
|
|
44
|
+
</Transition>
|
|
45
|
+
</div>
|
|
46
|
+
</Transition>
|
|
47
|
+
</Teleport>
|
|
48
|
+
</template>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
import type { OverlayStore } from '../overlay/overlayStore'
|
|
4
|
+
import Popover from './Popover.vue'
|
|
5
|
+
|
|
6
|
+
const props = withDefaults(defineProps<{
|
|
7
|
+
overlayStore: OverlayStore
|
|
8
|
+
placement?: 'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end'
|
|
9
|
+
offset?: number
|
|
10
|
+
anchorInset?: number
|
|
11
|
+
shiftPadding?: number
|
|
12
|
+
}>(), {
|
|
13
|
+
placement: 'top',
|
|
14
|
+
offset: 8,
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const emit = defineEmits<{
|
|
18
|
+
close: []
|
|
19
|
+
}>()
|
|
20
|
+
|
|
21
|
+
const isOpen = ref(false)
|
|
22
|
+
const anchorX = ref(0)
|
|
23
|
+
const anchorY = ref(0)
|
|
24
|
+
const anchorWidth = ref(0)
|
|
25
|
+
const anchorHeight = ref(0)
|
|
26
|
+
const triggerRef = ref<HTMLDivElement>()
|
|
27
|
+
const ignoreEls = computed(() => triggerRef.value ? [triggerRef.value] : [])
|
|
28
|
+
|
|
29
|
+
function updateAnchor() {
|
|
30
|
+
if (!triggerRef.value) return
|
|
31
|
+
const rect = triggerRef.value.getBoundingClientRect()
|
|
32
|
+
const inset = props.anchorInset ?? 0
|
|
33
|
+
anchorX.value = rect.left + inset
|
|
34
|
+
anchorY.value = rect.top
|
|
35
|
+
anchorWidth.value = rect.width - inset * 2
|
|
36
|
+
anchorHeight.value = rect.height
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function handleClick() {
|
|
40
|
+
updateAnchor()
|
|
41
|
+
isOpen.value = !isOpen.value
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function open() {
|
|
45
|
+
updateAnchor()
|
|
46
|
+
isOpen.value = true
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function close() {
|
|
50
|
+
if (!isOpen.value) return
|
|
51
|
+
isOpen.value = false
|
|
52
|
+
emit('close')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
defineExpose({ open, close })
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<template>
|
|
59
|
+
<div ref="triggerRef" @click="handleClick">
|
|
60
|
+
<slot name="trigger" :is-open="isOpen" />
|
|
61
|
+
</div>
|
|
62
|
+
<Popover
|
|
63
|
+
:overlay-store="props.overlayStore"
|
|
64
|
+
:is-open="isOpen"
|
|
65
|
+
:anchor-x="anchorX"
|
|
66
|
+
:anchor-y="anchorY"
|
|
67
|
+
:anchor-width="anchorWidth"
|
|
68
|
+
:anchor-height="anchorHeight"
|
|
69
|
+
:placement="props.placement"
|
|
70
|
+
:offset="props.offset"
|
|
71
|
+
:shift-padding="props.shiftPadding"
|
|
72
|
+
:ignore-els="ignoreEls"
|
|
73
|
+
@close="close"
|
|
74
|
+
>
|
|
75
|
+
<slot name="content" :close="close" :trigger-width="anchorWidth" />
|
|
76
|
+
</Popover>
|
|
77
|
+
</template>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useClipboard } from '@vueuse/core'
|
|
3
|
+
import { CheckLine } from '@mingcute/vue/check'
|
|
4
|
+
import { CopyLine } from '@mingcute/vue/copy'
|
|
5
|
+
import { CloseLine } from '@mingcute/vue/close'
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
message: string
|
|
9
|
+
copyable?: boolean
|
|
10
|
+
dismissible?: boolean
|
|
11
|
+
}>()
|
|
12
|
+
|
|
13
|
+
const emit = defineEmits<{
|
|
14
|
+
dismiss: []
|
|
15
|
+
}>()
|
|
16
|
+
|
|
17
|
+
const { copy, copied } = useClipboard({ copiedDuring: 1500 })
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<div class="rounded-lg border border-on-danger-muted bg-tint-danger px-3 py-2">
|
|
22
|
+
<div class="flex items-start gap-2">
|
|
23
|
+
<pre class="m-0 min-w-0 flex-1 whitespace-pre-wrap break-words font-mono text-[12px] leading-5 text-on-danger">{{ message }}</pre>
|
|
24
|
+
<div v-if="copyable || dismissible" class="flex shrink-0 items-center gap-0.5">
|
|
25
|
+
<span
|
|
26
|
+
v-if="copyable"
|
|
27
|
+
class="flex cursor-pointer items-center rounded-md p-1 text-on-danger-muted transition-colors hover:bg-tint-danger-strong hover:text-on-danger"
|
|
28
|
+
@click="copy(props.message)"
|
|
29
|
+
>
|
|
30
|
+
<CheckLine v-if="copied" :size="12" class="text-on-success" />
|
|
31
|
+
<CopyLine v-else :size="12" />
|
|
32
|
+
</span>
|
|
33
|
+
<div
|
|
34
|
+
v-if="dismissible"
|
|
35
|
+
class="flex cursor-pointer items-center justify-center rounded p-1 text-on-danger-muted transition-colors hover:bg-tint-danger-strong hover:text-on-danger"
|
|
36
|
+
@click="emit('dismiss')"
|
|
37
|
+
>
|
|
38
|
+
<CloseLine :size="12" />
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</template>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{
|
|
5
|
+
text: string
|
|
6
|
+
query: string
|
|
7
|
+
}>()
|
|
8
|
+
|
|
9
|
+
const segments = computed(() => {
|
|
10
|
+
const q = props.query.trim().toLowerCase()
|
|
11
|
+
if (!q) return [{ text: props.text, isMatch: false }]
|
|
12
|
+
|
|
13
|
+
const result: { text: string; isMatch: boolean }[] = []
|
|
14
|
+
const lower = props.text.toLowerCase()
|
|
15
|
+
let cursor = 0
|
|
16
|
+
|
|
17
|
+
while (cursor < props.text.length) {
|
|
18
|
+
const matchIdx = lower.indexOf(q, cursor)
|
|
19
|
+
if (matchIdx === -1) {
|
|
20
|
+
result.push({ text: props.text.slice(cursor), isMatch: false })
|
|
21
|
+
break
|
|
22
|
+
}
|
|
23
|
+
if (matchIdx > cursor) {
|
|
24
|
+
result.push({ text: props.text.slice(cursor, matchIdx), isMatch: false })
|
|
25
|
+
}
|
|
26
|
+
result.push({ text: props.text.slice(matchIdx, matchIdx + q.length), isMatch: true })
|
|
27
|
+
cursor = matchIdx + q.length
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return result
|
|
31
|
+
})
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<template>
|
|
35
|
+
<template v-for="(seg, i) in segments" :key="i">
|
|
36
|
+
<mark v-if="seg.isMatch" class="bg-tint-highlight text-on-highlight">{{ seg.text }}</mark>
|
|
37
|
+
<template v-else>{{ seg.text }}</template>
|
|
38
|
+
</template>
|
|
39
|
+
</template>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Component } from 'vue'
|
|
3
|
+
|
|
4
|
+
withDefaults(defineProps<{
|
|
5
|
+
icon: Component
|
|
6
|
+
iconSize?: number
|
|
7
|
+
size?: 'xs' | 'sm' | 'md' | 'lg'
|
|
8
|
+
variant?: 'default' | 'danger'
|
|
9
|
+
disabled?: boolean
|
|
10
|
+
}>(), {
|
|
11
|
+
size: 'md',
|
|
12
|
+
variant: 'default',
|
|
13
|
+
})
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<span
|
|
18
|
+
class="flex shrink-0 cursor-pointer items-center justify-center transition-colors"
|
|
19
|
+
:class="[
|
|
20
|
+
size === 'xs' ? 'size-4 rounded-sm' : size === 'sm' ? 'size-5 rounded' : size === 'lg' ? 'size-8 rounded-lg' : 'size-6 rounded-md',
|
|
21
|
+
disabled
|
|
22
|
+
? 'pointer-events-none text-fg-ghost'
|
|
23
|
+
: variant === 'danger'
|
|
24
|
+
? 'text-fg-subtle hover:bg-hover hover:text-on-danger'
|
|
25
|
+
: 'text-fg-subtle hover:bg-hover hover:text-fg-body',
|
|
26
|
+
]"
|
|
27
|
+
>
|
|
28
|
+
<component :is="icon" :size="iconSize ?? (size === 'xs' ? 10 : size === 'sm' ? 14 : size === 'lg' ? 18 : 16)" />
|
|
29
|
+
</span>
|
|
30
|
+
</template>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { onMounted, ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
const props = defineProps<{
|
|
5
|
+
size?: number
|
|
6
|
+
strokeWidth?: number
|
|
7
|
+
trackClass?: string
|
|
8
|
+
arcClass?: string
|
|
9
|
+
}>()
|
|
10
|
+
|
|
11
|
+
const svgSize = props.size ?? 14
|
|
12
|
+
const stroke = props.strokeWidth ?? 2.5
|
|
13
|
+
const radius = (svgSize - stroke) / 2
|
|
14
|
+
const circumference = 2 * Math.PI * radius
|
|
15
|
+
const cx = svgSize / 2
|
|
16
|
+
const cy = svgSize / 2
|
|
17
|
+
|
|
18
|
+
const arcRef = ref<SVGCircleElement>()
|
|
19
|
+
|
|
20
|
+
const dashMin = circumference * 0.1
|
|
21
|
+
const dashMax = circumference * 0.45
|
|
22
|
+
|
|
23
|
+
onMounted(() => {
|
|
24
|
+
arcRef.value?.animate([
|
|
25
|
+
{ strokeDasharray: `${dashMin} ${circumference - dashMin}`, strokeDashoffset: '0' },
|
|
26
|
+
{ strokeDasharray: `${dashMax} ${circumference - dashMax}`, strokeDashoffset: `${circumference * -0.2}` },
|
|
27
|
+
{ strokeDasharray: `${dashMin} ${circumference - dashMin}`, strokeDashoffset: `${-circumference}` },
|
|
28
|
+
], {
|
|
29
|
+
duration: 1500,
|
|
30
|
+
easing: 'ease-in-out',
|
|
31
|
+
iterations: Infinity,
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<template>
|
|
37
|
+
<svg
|
|
38
|
+
:width="svgSize" :height="svgSize"
|
|
39
|
+
:viewBox="`0 0 ${svgSize} ${svgSize}`"
|
|
40
|
+
class="spinner-rotate"
|
|
41
|
+
>
|
|
42
|
+
<circle
|
|
43
|
+
:cx="cx" :cy="cy" :r="radius"
|
|
44
|
+
fill="none"
|
|
45
|
+
stroke="currentColor"
|
|
46
|
+
:stroke-width="stroke"
|
|
47
|
+
:class="trackClass ?? 'text-overlay/8'"
|
|
48
|
+
/>
|
|
49
|
+
<circle
|
|
50
|
+
ref="arcRef"
|
|
51
|
+
:cx="cx" :cy="cy" :r="radius"
|
|
52
|
+
fill="none"
|
|
53
|
+
stroke="currentColor"
|
|
54
|
+
:stroke-width="stroke"
|
|
55
|
+
stroke-linecap="round"
|
|
56
|
+
:class="arcClass ?? 'text-fg-muted'"
|
|
57
|
+
:stroke-dasharray="`${dashMin} ${circumference - dashMin}`"
|
|
58
|
+
:style="{ transformOrigin: `${cx}px ${cy}px` }"
|
|
59
|
+
/>
|
|
60
|
+
</svg>
|
|
61
|
+
</template>
|
|
62
|
+
|
|
63
|
+
<style scoped>
|
|
64
|
+
.spinner-rotate {
|
|
65
|
+
animation: spinner-rotate 2s linear infinite;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@keyframes spinner-rotate {
|
|
69
|
+
0% { transform: rotate(0deg); }
|
|
70
|
+
100% { transform: rotate(360deg); }
|
|
71
|
+
}
|
|
72
|
+
</style>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { CloseLine } from '@mingcute/vue/close'
|
|
3
|
+
|
|
4
|
+
defineProps<{
|
|
5
|
+
message: string
|
|
6
|
+
dismissible?: boolean
|
|
7
|
+
}>()
|
|
8
|
+
|
|
9
|
+
const emit = defineEmits<{
|
|
10
|
+
dismiss: []
|
|
11
|
+
}>()
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<div class="flex items-start gap-1.5">
|
|
16
|
+
<p class="min-w-0 flex-1 text-[12px] leading-4 text-on-danger">{{ message }}</p>
|
|
17
|
+
<div
|
|
18
|
+
v-if="dismissible"
|
|
19
|
+
class="flex shrink-0 cursor-pointer items-center justify-center rounded p-0.5 text-on-danger-muted transition-colors hover:bg-hover hover:text-on-danger"
|
|
20
|
+
@click="emit('dismiss')"
|
|
21
|
+
>
|
|
22
|
+
<CloseLine :size="12" />
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
</template>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import { useIntersectionObserver } from '@vueuse/core'
|
|
4
|
+
|
|
5
|
+
const props = withDefaults(defineProps<{
|
|
6
|
+
hasMore: boolean
|
|
7
|
+
isLoading: boolean
|
|
8
|
+
loadingLabel?: string
|
|
9
|
+
}>(), {
|
|
10
|
+
loadingLabel: 'Loading...',
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const emit = defineEmits<{
|
|
14
|
+
load: []
|
|
15
|
+
}>()
|
|
16
|
+
|
|
17
|
+
const sentinel = ref<HTMLElement | null>(null)
|
|
18
|
+
|
|
19
|
+
useIntersectionObserver(sentinel, ([entry]) => {
|
|
20
|
+
if (entry?.isIntersecting && props.hasMore && !props.isLoading) {
|
|
21
|
+
emit('load')
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<template>
|
|
27
|
+
<div ref="sentinel">
|
|
28
|
+
<slot v-if="isLoading" name="loading">
|
|
29
|
+
<div class="py-2 text-center text-[12px] text-fg-subtle">{{ loadingLabel }}</div>
|
|
30
|
+
</slot>
|
|
31
|
+
</div>
|
|
32
|
+
</template>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import type { MarkdownLinkHandler, MarkdownRenderer } from '../markdown/types'
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{
|
|
6
|
+
content: string
|
|
7
|
+
basePath?: string
|
|
8
|
+
renderMarkdown: MarkdownRenderer
|
|
9
|
+
onLinkClick?: MarkdownLinkHandler
|
|
10
|
+
}>()
|
|
11
|
+
|
|
12
|
+
const renderedHtml = computed(() =>
|
|
13
|
+
props.renderMarkdown(props.content, props.basePath ? { basePath: props.basePath } : undefined),
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
function handleClick(event: MouseEvent) {
|
|
17
|
+
const target = (event.target as HTMLElement).closest('a')
|
|
18
|
+
if (!target) return
|
|
19
|
+
const href = target.getAttribute('href')
|
|
20
|
+
if (!href) return
|
|
21
|
+
props.onLinkClick?.({ href, event, basePath: props.basePath })
|
|
22
|
+
}
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<template>
|
|
26
|
+
<div
|
|
27
|
+
class="markdown-body h-full overflow-y-auto bg-surface-editor p-4 text-sm leading-relaxed text-fg-body select-text"
|
|
28
|
+
v-html="renderedHtml"
|
|
29
|
+
@click="handleClick"
|
|
30
|
+
/>
|
|
31
|
+
</template>
|
package/src/ui/Menu.vue
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import type { Component } from 'vue'
|
|
4
|
+
import Tooltip from './Tooltip.vue'
|
|
5
|
+
|
|
6
|
+
const props = defineProps<{
|
|
7
|
+
icon?: Component
|
|
8
|
+
label: string
|
|
9
|
+
isDanger?: boolean
|
|
10
|
+
disabled?: boolean
|
|
11
|
+
disabledReason?: string
|
|
12
|
+
shortcut?: string
|
|
13
|
+
}>()
|
|
14
|
+
|
|
15
|
+
const emit = defineEmits<{
|
|
16
|
+
select: []
|
|
17
|
+
}>()
|
|
18
|
+
|
|
19
|
+
const displayShortcut = computed(() => props.shortcut ?? '')
|
|
20
|
+
const isDisabled = computed(() => props.disabled || !!props.disabledReason)
|
|
21
|
+
const tooltipContent = computed(() => props.disabledReason?.trim() || undefined)
|
|
22
|
+
|
|
23
|
+
function handleClick(event: MouseEvent) {
|
|
24
|
+
if (isDisabled.value) {
|
|
25
|
+
event.stopPropagation()
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
emit('select')
|
|
29
|
+
}
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<template>
|
|
33
|
+
<Tooltip :content="tooltipContent" :disabled="!tooltipContent" placement="bottom" :open-delay-ms="80" tag="div">
|
|
34
|
+
<div
|
|
35
|
+
class="flex select-none items-center gap-2 rounded-md px-2.5 py-1.5 text-[13px] transition-colors"
|
|
36
|
+
:class="isDisabled
|
|
37
|
+
? 'cursor-not-allowed text-fg-faint'
|
|
38
|
+
: isDanger
|
|
39
|
+
? 'cursor-pointer text-on-danger hover:bg-tint-danger-strong hover:text-on-danger'
|
|
40
|
+
: 'cursor-pointer text-fg-body hover:bg-active hover:text-fg-emphasis'"
|
|
41
|
+
@click="handleClick"
|
|
42
|
+
>
|
|
43
|
+
<component :is="icon" v-if="icon" :size="15" class="shrink-0" />
|
|
44
|
+
<span class="flex-1">{{ label }}</span>
|
|
45
|
+
<span class="w-8 shrink-0 text-right text-[11px] text-fg-faint">{{ displayShortcut }}</span>
|
|
46
|
+
</div>
|
|
47
|
+
</Tooltip>
|
|
48
|
+
</template>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
defineProps<{
|
|
3
|
+
label: string
|
|
4
|
+
}>()
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<template>
|
|
8
|
+
<div class="border-t border-line pt-0.5 first:border-t-0 first:pt-0">
|
|
9
|
+
<div class="px-2.5 pt-1.5 pb-1 text-[11px] font-medium text-fg-subtle">
|
|
10
|
+
{{ label }}
|
|
11
|
+
</div>
|
|
12
|
+
<div class="flex flex-col gap-0.5">
|
|
13
|
+
<slot />
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
</template>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Component } from 'vue'
|
|
3
|
+
import { CheckLine } from '@mingcute/vue/check'
|
|
4
|
+
|
|
5
|
+
defineProps<{
|
|
6
|
+
icon?: Component
|
|
7
|
+
label: string
|
|
8
|
+
isSelected?: boolean
|
|
9
|
+
}>()
|
|
10
|
+
|
|
11
|
+
const emit = defineEmits<{
|
|
12
|
+
select: []
|
|
13
|
+
}>()
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<div
|
|
18
|
+
class="flex cursor-pointer items-center gap-2 rounded-md px-2.5 py-1.5 text-[13px] transition-colors"
|
|
19
|
+
:class="isSelected
|
|
20
|
+
? 'bg-active text-fg-emphasis'
|
|
21
|
+
: 'text-fg-muted hover:bg-hover hover:text-fg'"
|
|
22
|
+
@click="emit('select')"
|
|
23
|
+
>
|
|
24
|
+
<component :is="icon" v-if="icon" :size="15" class="shrink-0" />
|
|
25
|
+
<span class="flex-1">{{ label }}</span>
|
|
26
|
+
<CheckLine v-if="isSelected" :size="14" class="shrink-0 text-fg-body" />
|
|
27
|
+
</div>
|
|
28
|
+
</template>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
import { useFloating, offset as offsetMiddleware, flip, shift, autoUpdate } from '@floating-ui/vue'
|
|
4
|
+
import type { Placement } from '@floating-ui/vue'
|
|
5
|
+
import { onClickOutside } from '@vueuse/core'
|
|
6
|
+
import type { OverlayStore } from '../overlay/overlayStore'
|
|
7
|
+
import { useOverlay } from '../composables/useOverlay'
|
|
8
|
+
|
|
9
|
+
const props = withDefaults(defineProps<{
|
|
10
|
+
isOpen: boolean
|
|
11
|
+
overlayStore: OverlayStore
|
|
12
|
+
anchorX: number
|
|
13
|
+
anchorY: number
|
|
14
|
+
anchorWidth?: number
|
|
15
|
+
anchorHeight?: number
|
|
16
|
+
placement?: Placement
|
|
17
|
+
offset?: number
|
|
18
|
+
shiftPadding?: number
|
|
19
|
+
ignoreEls?: HTMLElement[]
|
|
20
|
+
}>(), {
|
|
21
|
+
anchorWidth: 0,
|
|
22
|
+
anchorHeight: 0,
|
|
23
|
+
placement: 'bottom-start',
|
|
24
|
+
offset: 6,
|
|
25
|
+
shiftPadding: 8,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const emit = defineEmits<{
|
|
29
|
+
close: []
|
|
30
|
+
}>()
|
|
31
|
+
|
|
32
|
+
const floatingRef = ref<HTMLElement | null>(null)
|
|
33
|
+
|
|
34
|
+
const virtualRef = computed(() => ({
|
|
35
|
+
getBoundingClientRect: () => ({
|
|
36
|
+
x: props.anchorX,
|
|
37
|
+
y: props.anchorY,
|
|
38
|
+
width: props.anchorWidth,
|
|
39
|
+
height: props.anchorHeight,
|
|
40
|
+
top: props.anchorY,
|
|
41
|
+
left: props.anchorX,
|
|
42
|
+
right: props.anchorX + props.anchorWidth,
|
|
43
|
+
bottom: props.anchorY + props.anchorHeight,
|
|
44
|
+
}),
|
|
45
|
+
}))
|
|
46
|
+
|
|
47
|
+
const { floatingStyles, placement: resolvedPlacement } = useFloating(virtualRef, floatingRef, {
|
|
48
|
+
placement: computed(() => props.placement),
|
|
49
|
+
middleware: computed(() => [
|
|
50
|
+
offsetMiddleware(props.offset),
|
|
51
|
+
flip(),
|
|
52
|
+
shift({ padding: props.shiftPadding }),
|
|
53
|
+
]),
|
|
54
|
+
whileElementsMounted: autoUpdate,
|
|
55
|
+
transform: false,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const transformOrigin = computed(() => {
|
|
59
|
+
const p = resolvedPlacement.value
|
|
60
|
+
const y = p.startsWith('top') ? 'bottom' : 'top'
|
|
61
|
+
const x = p.endsWith('start') ? 'left' : p.endsWith('end') ? 'right' : 'center'
|
|
62
|
+
return `${y} ${x}`
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
onClickOutside(floatingRef, () => {
|
|
66
|
+
if (props.isOpen) emit('close')
|
|
67
|
+
}, { ignore: computed(() => props.ignoreEls ?? []) })
|
|
68
|
+
|
|
69
|
+
useOverlay(props.overlayStore, () => props.isOpen, () => emit('close'))
|
|
70
|
+
</script>
|
|
71
|
+
|
|
72
|
+
<template>
|
|
73
|
+
<Teleport to="body">
|
|
74
|
+
<Transition
|
|
75
|
+
enter-active-class="transition-[opacity,transform] duration-150 ease-out"
|
|
76
|
+
leave-active-class="transition-[opacity,transform] duration-150 ease-out"
|
|
77
|
+
enter-from-class="opacity-0 scale-95"
|
|
78
|
+
leave-to-class="opacity-0 scale-95"
|
|
79
|
+
>
|
|
80
|
+
<div
|
|
81
|
+
v-if="isOpen"
|
|
82
|
+
ref="floatingRef"
|
|
83
|
+
class="popover-floating z-50"
|
|
84
|
+
:style="{ ...floatingStyles, transformOrigin: transformOrigin }"
|
|
85
|
+
>
|
|
86
|
+
<slot />
|
|
87
|
+
</div>
|
|
88
|
+
</Transition>
|
|
89
|
+
</Teleport>
|
|
90
|
+
</template>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { DownLine } from '@mingcute/vue/down'
|
|
3
|
+
|
|
4
|
+
const props = withDefaults(defineProps<{
|
|
5
|
+
visible: boolean
|
|
6
|
+
bottomOffset?: number
|
|
7
|
+
}>(), {
|
|
8
|
+
bottomOffset: 12,
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
const emit = defineEmits<{
|
|
12
|
+
click: []
|
|
13
|
+
}>()
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<Transition
|
|
18
|
+
enter-active-class="transition duration-200 ease-out"
|
|
19
|
+
leave-active-class="transition duration-200 ease-in"
|
|
20
|
+
enter-from-class="translate-y-2 opacity-0"
|
|
21
|
+
leave-to-class="translate-y-2 opacity-0"
|
|
22
|
+
>
|
|
23
|
+
<div
|
|
24
|
+
v-if="visible"
|
|
25
|
+
class="absolute right-5 z-20 cursor-pointer rounded-full border border-fg-ghost bg-surface-raised p-1.5 text-fg-muted shadow-lg transition-colors hover:border-fg-faint hover:bg-fg-ghost hover:text-fg"
|
|
26
|
+
:style="{ bottom: `${props.bottomOffset}px` }"
|
|
27
|
+
@click="emit('click')"
|
|
28
|
+
>
|
|
29
|
+
<DownLine :size="16" />
|
|
30
|
+
</div>
|
|
31
|
+
</Transition>
|
|
32
|
+
</template>
|