chore(frontend): introduce prettier + eslint and prune unused code
- Add Prettier 3 with prettier-plugin-organize-imports (sorts/removes unused imports) - Add ESLint 10 flat config with typescript-eslint + eslint-plugin-vue + eslint-config-prettier - Add root scripts: format, format:check, lint, lint:fix - Reformat 257 files across admin-web, ops-web, desktop-client, packages - Remove unused locals/exports flagged by --noUnusedLocals/--noUnusedParameters - Fix duplicate localTabLabel key, surrogate-pair regex u-flag, NBSP literal in regex - Skip server/ (Go) and apps/browser-extension/ (deprecated per ADR) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,96 +1,105 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch, type Component, type CSSProperties } from "vue";
|
||||
import { DownOutlined } from "@ant-design/icons-vue";
|
||||
import { DownOutlined } from '@ant-design/icons-vue'
|
||||
import {
|
||||
computed,
|
||||
nextTick,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
ref,
|
||||
watch,
|
||||
type Component,
|
||||
type CSSProperties,
|
||||
} from 'vue'
|
||||
|
||||
type EditorActionMenuItem = {
|
||||
key: string;
|
||||
label: string;
|
||||
icon?: Component;
|
||||
danger?: boolean;
|
||||
disabled?: boolean;
|
||||
active?: boolean;
|
||||
children?: EditorActionMenuItem[];
|
||||
};
|
||||
key: string
|
||||
label: string
|
||||
icon?: Component
|
||||
danger?: boolean
|
||||
disabled?: boolean
|
||||
active?: boolean
|
||||
children?: EditorActionMenuItem[]
|
||||
}
|
||||
|
||||
type EditorActionMenuGroup = {
|
||||
key: string;
|
||||
columns?: number;
|
||||
items: EditorActionMenuItem[];
|
||||
};
|
||||
key: string
|
||||
columns?: number
|
||||
items: EditorActionMenuItem[]
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
x: number;
|
||||
y: number;
|
||||
groups: EditorActionMenuGroup[];
|
||||
variant?: "tile" | "inline";
|
||||
minWidth?: string;
|
||||
maxWidth?: string;
|
||||
}>();
|
||||
x: number
|
||||
y: number
|
||||
groups: EditorActionMenuGroup[]
|
||||
variant?: 'tile' | 'inline'
|
||||
minWidth?: string
|
||||
maxWidth?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [key: string];
|
||||
}>();
|
||||
select: [key: string]
|
||||
}>()
|
||||
|
||||
const menuRef = ref<HTMLElement | null>(null);
|
||||
const menuRef = ref<HTMLElement | null>(null)
|
||||
const frame = ref({
|
||||
x: props.x,
|
||||
y: props.y,
|
||||
});
|
||||
})
|
||||
|
||||
const resolvedVariant = computed(() => props.variant ?? "inline");
|
||||
const resolvedVariant = computed(() => props.variant ?? 'inline')
|
||||
|
||||
const menuStyle = computed<CSSProperties>(() => {
|
||||
const style: CSSProperties = {
|
||||
left: `${frame.value.x}px`,
|
||||
top: `${frame.value.y}px`,
|
||||
};
|
||||
}
|
||||
|
||||
if (props.minWidth) {
|
||||
style.minWidth = props.minWidth;
|
||||
style.minWidth = props.minWidth
|
||||
}
|
||||
|
||||
if (props.maxWidth) {
|
||||
style.maxWidth = props.maxWidth;
|
||||
style.maxWidth = props.maxWidth
|
||||
}
|
||||
|
||||
return style;
|
||||
});
|
||||
return style
|
||||
})
|
||||
|
||||
function clamp(value: number, min: number, max: number): number {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
return Math.min(Math.max(value, min), max)
|
||||
}
|
||||
|
||||
function syncMenuFrame(): void {
|
||||
void nextTick(() => {
|
||||
const menu = menuRef.value;
|
||||
const menu = menuRef.value
|
||||
if (!menu) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const offset = 16;
|
||||
const offset = 16
|
||||
frame.value = {
|
||||
x: clamp(props.x, offset, Math.max(offset, window.innerWidth - menu.offsetWidth - offset)),
|
||||
y: clamp(props.y, offset, Math.max(offset, window.innerHeight - menu.offsetHeight - offset)),
|
||||
};
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleWindowResize(): void {
|
||||
syncMenuFrame();
|
||||
syncMenuFrame()
|
||||
}
|
||||
|
||||
function handleSelect(item: EditorActionMenuItem): void {
|
||||
if (item.disabled) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
emit("select", item.key);
|
||||
emit('select', item.key)
|
||||
}
|
||||
|
||||
function resolveGroupStyle(group: EditorActionMenuGroup): CSSProperties {
|
||||
return {
|
||||
gridTemplateColumns: `repeat(${Math.max(group.columns ?? group.items.length, 1)}, minmax(0, 1fr))`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
@@ -103,23 +112,23 @@ watch(
|
||||
variant: resolvedVariant.value,
|
||||
}),
|
||||
() => {
|
||||
syncMenuFrame();
|
||||
syncMenuFrame()
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
flush: "post",
|
||||
flush: 'post',
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener("resize", handleWindowResize);
|
||||
syncMenuFrame();
|
||||
});
|
||||
window.addEventListener('resize', handleWindowResize)
|
||||
syncMenuFrame()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("resize", handleWindowResize);
|
||||
});
|
||||
window.removeEventListener('resize', handleWindowResize)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,192 +1,200 @@
|
||||
<script setup lang="ts">
|
||||
import { CheckOutlined, CloseOutlined, DeleteOutlined, LoadingOutlined, RedoOutlined } from "@ant-design/icons-vue";
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch, type CSSProperties } from "vue";
|
||||
import {
|
||||
CheckOutlined,
|
||||
CloseOutlined,
|
||||
DeleteOutlined,
|
||||
LoadingOutlined,
|
||||
RedoOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch, type CSSProperties } from 'vue'
|
||||
|
||||
type EditorAiAssistStatus = "idle" | "generating" | "completed" | "error";
|
||||
type EditorAiAssistStatus = 'idle' | 'generating' | 'completed' | 'error'
|
||||
type EditorAiAssistBoundary = {
|
||||
left: number;
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
};
|
||||
type EditorAiAssistPlacement = "bottom" | "top";
|
||||
left: number
|
||||
top: number
|
||||
right: number
|
||||
bottom: number
|
||||
}
|
||||
type EditorAiAssistPlacement = 'bottom' | 'top'
|
||||
|
||||
const props = defineProps<{
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
boundary?: EditorAiAssistBoundary | null;
|
||||
boundaryElement?: HTMLElement | null;
|
||||
placement?: EditorAiAssistPlacement;
|
||||
prompt: string;
|
||||
status: EditorAiAssistStatus;
|
||||
streaming: boolean;
|
||||
hasPreview: boolean;
|
||||
error: string;
|
||||
title: string;
|
||||
promptPlaceholder: string;
|
||||
continuePlaceholder?: string;
|
||||
loadingLabel: string;
|
||||
errorLabel: string;
|
||||
disclaimer: string;
|
||||
historyLabel?: string;
|
||||
regenerateLabel: string;
|
||||
discardLabel: string;
|
||||
applyLabel: string;
|
||||
}>();
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
boundary?: EditorAiAssistBoundary | null
|
||||
boundaryElement?: HTMLElement | null
|
||||
placement?: EditorAiAssistPlacement
|
||||
prompt: string
|
||||
status: EditorAiAssistStatus
|
||||
streaming: boolean
|
||||
hasPreview: boolean
|
||||
error: string
|
||||
title: string
|
||||
promptPlaceholder: string
|
||||
continuePlaceholder?: string
|
||||
loadingLabel: string
|
||||
errorLabel: string
|
||||
disclaimer: string
|
||||
historyLabel?: string
|
||||
regenerateLabel: string
|
||||
discardLabel: string
|
||||
applyLabel: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:prompt": [value: string];
|
||||
submit: [];
|
||||
close: [];
|
||||
reset: [];
|
||||
apply: [];
|
||||
}>();
|
||||
'update:prompt': [value: string]
|
||||
submit: []
|
||||
close: []
|
||||
reset: []
|
||||
apply: []
|
||||
}>()
|
||||
|
||||
const IDLE_MAX_WIDTH = 760;
|
||||
const PANEL_OFFSET = 16;
|
||||
const MIN_PANEL_WIDTH = 240;
|
||||
const RESULT_CHROME_HEIGHT = 176;
|
||||
const IDLE_MAX_WIDTH = 760
|
||||
const PANEL_OFFSET = 16
|
||||
const MIN_PANEL_WIDTH = 240
|
||||
const RESULT_CHROME_HEIGHT = 176
|
||||
|
||||
const panelRef = ref<HTMLElement | null>(null);
|
||||
const resultContentRef = ref<HTMLElement | null>(null);
|
||||
const promptInputRef = ref<HTMLInputElement | null>(null);
|
||||
const panelRef = ref<HTMLElement | null>(null)
|
||||
const resultContentRef = ref<HTMLElement | null>(null)
|
||||
const promptInputRef = ref<HTMLInputElement | null>(null)
|
||||
const frame = ref({
|
||||
x: props.x,
|
||||
y: props.y,
|
||||
width: props.width,
|
||||
maxHeight: 480,
|
||||
resultMaxHeight: 300,
|
||||
});
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
let mutationObserver: MutationObserver | null = null;
|
||||
let syncFrameId = 0;
|
||||
let scrollFrameId = 0;
|
||||
})
|
||||
let resizeObserver: ResizeObserver | null = null
|
||||
let mutationObserver: MutationObserver | null = null
|
||||
let syncFrameId = 0
|
||||
let scrollFrameId = 0
|
||||
|
||||
const promptValue = computed({
|
||||
get: () => props.prompt,
|
||||
set: (value: string) => {
|
||||
emit("update:prompt", value);
|
||||
emit('update:prompt', value)
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const resolvedContinuePlaceholder = computed(
|
||||
() => props.continuePlaceholder?.trim() || props.promptPlaceholder,
|
||||
);
|
||||
)
|
||||
|
||||
const panelStyle = computed<CSSProperties>(() => ({
|
||||
left: `${frame.value.x}px`,
|
||||
top: `${frame.value.y}px`,
|
||||
width: `${frame.value.width}px`,
|
||||
maxHeight: `${frame.value.maxHeight}px`,
|
||||
"--editor-ai-assist-result-max-height": `${frame.value.resultMaxHeight}px`,
|
||||
} as CSSProperties));
|
||||
const panelStyle = computed<CSSProperties>(
|
||||
() =>
|
||||
({
|
||||
left: `${frame.value.x}px`,
|
||||
top: `${frame.value.y}px`,
|
||||
width: `${frame.value.width}px`,
|
||||
maxHeight: `${frame.value.maxHeight}px`,
|
||||
'--editor-ai-assist-result-max-height': `${frame.value.resultMaxHeight}px`,
|
||||
}) as CSSProperties,
|
||||
)
|
||||
|
||||
function clamp(value: number, min: number, max: number): number {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
return Math.min(Math.max(value, min), max)
|
||||
}
|
||||
|
||||
function normalizeBoundary(rawBoundary: EditorAiAssistBoundary | null | undefined): EditorAiAssistBoundary {
|
||||
function normalizeBoundary(
|
||||
rawBoundary: EditorAiAssistBoundary | null | undefined,
|
||||
): EditorAiAssistBoundary {
|
||||
const viewportBoundary = {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: window.innerWidth,
|
||||
bottom: window.innerHeight,
|
||||
};
|
||||
}
|
||||
if (!rawBoundary) {
|
||||
return viewportBoundary;
|
||||
return viewportBoundary
|
||||
}
|
||||
|
||||
const left = clamp(rawBoundary.left, 0, window.innerWidth);
|
||||
const top = clamp(rawBoundary.top, 0, window.innerHeight);
|
||||
const right = clamp(rawBoundary.right, left, window.innerWidth);
|
||||
const bottom = clamp(rawBoundary.bottom, top, window.innerHeight);
|
||||
const left = clamp(rawBoundary.left, 0, window.innerWidth)
|
||||
const top = clamp(rawBoundary.top, 0, window.innerHeight)
|
||||
const right = clamp(rawBoundary.right, left, window.innerWidth)
|
||||
const bottom = clamp(rawBoundary.bottom, top, window.innerHeight)
|
||||
|
||||
if (right - left < 1 || bottom - top < 1) {
|
||||
return viewportBoundary;
|
||||
return viewportBoundary
|
||||
}
|
||||
|
||||
return { left, top, right, bottom };
|
||||
return { left, top, right, bottom }
|
||||
}
|
||||
|
||||
function resolveBoundary(): EditorAiAssistBoundary {
|
||||
if (props.boundaryElement) {
|
||||
const rect = props.boundaryElement.getBoundingClientRect();
|
||||
const rect = props.boundaryElement.getBoundingClientRect()
|
||||
return normalizeBoundary({
|
||||
left: rect.left,
|
||||
top: rect.top,
|
||||
right: rect.right,
|
||||
bottom: rect.bottom,
|
||||
});
|
||||
})
|
||||
}
|
||||
return normalizeBoundary(props.boundary);
|
||||
return normalizeBoundary(props.boundary)
|
||||
}
|
||||
|
||||
function updateFrame(nextFrame: typeof frame.value): void {
|
||||
const current = frame.value;
|
||||
const current = frame.value
|
||||
const hasChanged =
|
||||
Math.abs(current.x - nextFrame.x) > 0.5 ||
|
||||
Math.abs(current.y - nextFrame.y) > 0.5 ||
|
||||
Math.abs(current.width - nextFrame.width) > 0.5 ||
|
||||
Math.abs(current.maxHeight - nextFrame.maxHeight) > 0.5 ||
|
||||
Math.abs(current.resultMaxHeight - nextFrame.resultMaxHeight) > 0.5;
|
||||
Math.abs(current.resultMaxHeight - nextFrame.resultMaxHeight) > 0.5
|
||||
|
||||
if (hasChanged) {
|
||||
frame.value = nextFrame;
|
||||
frame.value = nextFrame
|
||||
}
|
||||
}
|
||||
|
||||
function scrollResultContentToLatest(): void {
|
||||
if (!(props.streaming || props.status === "generating")) {
|
||||
return;
|
||||
if (!(props.streaming || props.status === 'generating')) {
|
||||
return
|
||||
}
|
||||
|
||||
if (scrollFrameId) {
|
||||
cancelAnimationFrame(scrollFrameId);
|
||||
cancelAnimationFrame(scrollFrameId)
|
||||
}
|
||||
scrollFrameId = requestAnimationFrame(() => {
|
||||
scrollFrameId = 0;
|
||||
const resultContent = resultContentRef.value;
|
||||
scrollFrameId = 0
|
||||
const resultContent = resultContentRef.value
|
||||
if (resultContent) {
|
||||
resultContent.scrollTop = resultContent.scrollHeight;
|
||||
resultContent.scrollTop = resultContent.scrollHeight
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function handleResultContentChanged(): void {
|
||||
schedulePanelFrameSync();
|
||||
scrollResultContentToLatest();
|
||||
schedulePanelFrameSync()
|
||||
scrollResultContentToLatest()
|
||||
}
|
||||
|
||||
function syncPanelFrame(): void {
|
||||
void nextTick(() => {
|
||||
const panel = panelRef.value;
|
||||
const panel = panelRef.value
|
||||
if (!panel) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const boundary = resolveBoundary();
|
||||
const availableWidth = Math.max(0, boundary.right - boundary.left - PANEL_OFFSET * 2);
|
||||
const maxWidth = Math.max(160, availableWidth);
|
||||
const minWidth = Math.min(MIN_PANEL_WIDTH, maxWidth);
|
||||
const boundary = resolveBoundary()
|
||||
const availableWidth = Math.max(0, boundary.right - boundary.left - PANEL_OFFSET * 2)
|
||||
const maxWidth = Math.max(160, availableWidth)
|
||||
const minWidth = Math.min(MIN_PANEL_WIDTH, maxWidth)
|
||||
const width =
|
||||
props.status === "idle"
|
||||
props.status === 'idle'
|
||||
? clamp(Math.min(props.width, IDLE_MAX_WIDTH), minWidth, maxWidth)
|
||||
: clamp(props.width, minWidth, maxWidth);
|
||||
const placement = props.placement ?? "bottom";
|
||||
const anchorY = clamp(props.y, boundary.top + PANEL_OFFSET, boundary.bottom - PANEL_OFFSET);
|
||||
: clamp(props.width, minWidth, maxWidth)
|
||||
const placement = props.placement ?? 'bottom'
|
||||
const anchorY = clamp(props.y, boundary.top + PANEL_OFFSET, boundary.bottom - PANEL_OFFSET)
|
||||
const availableHeight = Math.max(
|
||||
120,
|
||||
placement === "top"
|
||||
placement === 'top'
|
||||
? anchorY - boundary.top - PANEL_OFFSET
|
||||
: boundary.bottom - anchorY - PANEL_OFFSET,
|
||||
);
|
||||
const panelHeight = Math.min(panel.offsetHeight || availableHeight, availableHeight);
|
||||
const resultMaxHeight = Math.max(
|
||||
48,
|
||||
availableHeight - RESULT_CHROME_HEIGHT,
|
||||
);
|
||||
const panelTop = placement === "top" ? anchorY - panelHeight : anchorY;
|
||||
)
|
||||
const panelHeight = Math.min(panel.offsetHeight || availableHeight, availableHeight)
|
||||
const resultMaxHeight = Math.max(48, availableHeight - RESULT_CHROME_HEIGHT)
|
||||
const panelTop = placement === 'top' ? anchorY - panelHeight : anchorY
|
||||
|
||||
updateFrame({
|
||||
x: clamp(
|
||||
@@ -202,51 +210,51 @@ function syncPanelFrame(): void {
|
||||
width,
|
||||
maxHeight: availableHeight,
|
||||
resultMaxHeight,
|
||||
});
|
||||
scrollResultContentToLatest();
|
||||
});
|
||||
})
|
||||
scrollResultContentToLatest()
|
||||
})
|
||||
}
|
||||
|
||||
function schedulePanelFrameSync(): void {
|
||||
if (syncFrameId) {
|
||||
cancelAnimationFrame(syncFrameId);
|
||||
cancelAnimationFrame(syncFrameId)
|
||||
}
|
||||
syncFrameId = requestAnimationFrame(() => {
|
||||
syncFrameId = 0;
|
||||
syncPanelFrame();
|
||||
});
|
||||
syncFrameId = 0
|
||||
syncPanelFrame()
|
||||
})
|
||||
}
|
||||
|
||||
function focusPromptInput(): void {
|
||||
if (props.status !== "idle") {
|
||||
return;
|
||||
if (props.status !== 'idle') {
|
||||
return
|
||||
}
|
||||
|
||||
void nextTick(() => {
|
||||
promptInputRef.value?.focus();
|
||||
promptInputRef.value?.setSelectionRange(props.prompt.length, props.prompt.length);
|
||||
});
|
||||
promptInputRef.value?.focus()
|
||||
promptInputRef.value?.setSelectionRange(props.prompt.length, props.prompt.length)
|
||||
})
|
||||
}
|
||||
|
||||
function handleWindowResize(): void {
|
||||
syncPanelFrame();
|
||||
syncPanelFrame()
|
||||
}
|
||||
|
||||
function refreshResultContentObserver(): void {
|
||||
void nextTick(() => {
|
||||
mutationObserver?.disconnect();
|
||||
const resultContent = resultContentRef.value;
|
||||
mutationObserver?.disconnect()
|
||||
const resultContent = resultContentRef.value
|
||||
if (!resultContent) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
mutationObserver?.observe(resultContent, {
|
||||
childList: true,
|
||||
characterData: true,
|
||||
subtree: true,
|
||||
});
|
||||
scrollResultContentToLatest();
|
||||
});
|
||||
})
|
||||
scrollResultContentToLatest()
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
@@ -262,45 +270,45 @@ watch(
|
||||
placement: props.placement,
|
||||
}),
|
||||
() => {
|
||||
syncPanelFrame();
|
||||
refreshResultContentObserver();
|
||||
focusPromptInput();
|
||||
syncPanelFrame()
|
||||
refreshResultContentObserver()
|
||||
focusPromptInput()
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
flush: "post",
|
||||
flush: 'post',
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener("resize", handleWindowResize);
|
||||
mutationObserver = new MutationObserver(handleResultContentChanged);
|
||||
if ("ResizeObserver" in window) {
|
||||
resizeObserver = new ResizeObserver(schedulePanelFrameSync);
|
||||
window.addEventListener('resize', handleWindowResize)
|
||||
mutationObserver = new MutationObserver(handleResultContentChanged)
|
||||
if ('ResizeObserver' in window) {
|
||||
resizeObserver = new ResizeObserver(schedulePanelFrameSync)
|
||||
if (panelRef.value) {
|
||||
resizeObserver.observe(panelRef.value);
|
||||
resizeObserver.observe(panelRef.value)
|
||||
}
|
||||
}
|
||||
refreshResultContentObserver();
|
||||
syncPanelFrame();
|
||||
focusPromptInput();
|
||||
});
|
||||
refreshResultContentObserver()
|
||||
syncPanelFrame()
|
||||
focusPromptInput()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("resize", handleWindowResize);
|
||||
mutationObserver?.disconnect();
|
||||
mutationObserver = null;
|
||||
resizeObserver?.disconnect();
|
||||
resizeObserver = null;
|
||||
window.removeEventListener('resize', handleWindowResize)
|
||||
mutationObserver?.disconnect()
|
||||
mutationObserver = null
|
||||
resizeObserver?.disconnect()
|
||||
resizeObserver = null
|
||||
if (syncFrameId) {
|
||||
cancelAnimationFrame(syncFrameId);
|
||||
syncFrameId = 0;
|
||||
cancelAnimationFrame(syncFrameId)
|
||||
syncFrameId = 0
|
||||
}
|
||||
if (scrollFrameId) {
|
||||
cancelAnimationFrame(scrollFrameId);
|
||||
scrollFrameId = 0;
|
||||
cancelAnimationFrame(scrollFrameId)
|
||||
scrollFrameId = 0
|
||||
}
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -315,12 +323,36 @@ onBeforeUnmount(() => {
|
||||
<div class="editor-ai-assist__shell">
|
||||
<div v-if="status === 'idle'" class="editor-ai-assist__bar">
|
||||
<div class="editor-ai-assist__bar-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M6.75 15.25L8.78 9.78L14.25 7.75L12.22 13.22L6.75 15.25Z" stroke="url(#editor-ai-assist-gradient)" stroke-width="2" stroke-linejoin="round" />
|
||||
<path d="M14.5 4.5L15.07 6.43L17 7L15.07 7.57L14.5 9.5L13.93 7.57L12 7L13.93 6.43L14.5 4.5Z" fill="url(#editor-ai-assist-gradient)" />
|
||||
<path d="M18 13L18.58 14.92L20.5 15.5L18.58 16.08L18 18L17.42 16.08L15.5 15.5L17.42 14.92L18 13Z" fill="url(#editor-ai-assist-gradient)" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M6.75 15.25L8.78 9.78L14.25 7.75L12.22 13.22L6.75 15.25Z"
|
||||
stroke="url(#editor-ai-assist-gradient)"
|
||||
stroke-width="2"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M14.5 4.5L15.07 6.43L17 7L15.07 7.57L14.5 9.5L13.93 7.57L12 7L13.93 6.43L14.5 4.5Z"
|
||||
fill="url(#editor-ai-assist-gradient)"
|
||||
/>
|
||||
<path
|
||||
d="M18 13L18.58 14.92L20.5 15.5L18.58 16.08L18 18L17.42 16.08L15.5 15.5L17.42 14.92L18 13Z"
|
||||
fill="url(#editor-ai-assist-gradient)"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient id="editor-ai-assist-gradient" x1="4.5" y1="4.5" x2="20.5" y2="18.5" gradientUnits="userSpaceOnUse">
|
||||
<linearGradient
|
||||
id="editor-ai-assist-gradient"
|
||||
x1="4.5"
|
||||
y1="4.5"
|
||||
x2="20.5"
|
||||
y2="18.5"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#245bdb" />
|
||||
<stop offset="1" stop-color="#ef5da8" />
|
||||
</linearGradient>
|
||||
@@ -342,7 +374,17 @@ onBeforeUnmount(() => {
|
||||
</button>
|
||||
<span class="editor-ai-assist__divider"></span>
|
||||
<button type="button" class="editor-ai-assist__send-button" @click="emit('submit')">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<line x1="22" y1="2" x2="11" y2="13"></line>
|
||||
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
|
||||
</svg>
|
||||
@@ -358,7 +400,10 @@ onBeforeUnmount(() => {
|
||||
<LoadingOutlined />
|
||||
<span>{{ loadingLabel }}</span>
|
||||
</div>
|
||||
<div v-else class="editor-ai-assist__placeholder editor-ai-assist__placeholder--error">
|
||||
<div
|
||||
v-else
|
||||
class="editor-ai-assist__placeholder editor-ai-assist__placeholder--error"
|
||||
>
|
||||
{{ error || errorLabel }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -375,18 +420,38 @@ onBeforeUnmount(() => {
|
||||
<div class="editor-ai-assist__toolbar">
|
||||
<div class="editor-ai-assist__toolbar-head">
|
||||
<button type="button" class="editor-ai-assist__back-button" @click="emit('reset')">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<line x1="19" y1="12" x2="5" y2="12"></line>
|
||||
<polyline points="12 19 5 12 12 5"></polyline>
|
||||
</svg>
|
||||
<span>{{ title }}</span>
|
||||
</button>
|
||||
<div class="editor-ai-assist__history">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<polyline points="12 6 12 12 16 14"></polyline>
|
||||
</svg>
|
||||
<span>{{ historyLabel || "1/1" }}</span>
|
||||
<span>{{ historyLabel || '1/1' }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="editor-ai-assist__icon-button editor-ai-assist__icon-button--close"
|
||||
@@ -405,19 +470,41 @@ onBeforeUnmount(() => {
|
||||
:placeholder="resolvedContinuePlaceholder"
|
||||
@keydown.enter.prevent="emit('submit')"
|
||||
/>
|
||||
<button type="button" class="editor-ai-assist__continue-send" @click="emit('submit')">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<button
|
||||
type="button"
|
||||
class="editor-ai-assist__continue-send"
|
||||
@click="emit('submit')"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<line x1="22" y1="2" x2="11" y2="13"></line>
|
||||
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="editor-ai-assist__toolbar-actions">
|
||||
<button type="button" class="editor-ai-assist__toolbar-button" @click="emit('submit')">
|
||||
<button
|
||||
type="button"
|
||||
class="editor-ai-assist__toolbar-button"
|
||||
@click="emit('submit')"
|
||||
>
|
||||
<RedoOutlined />
|
||||
<span>{{ regenerateLabel }}</span>
|
||||
</button>
|
||||
<button type="button" class="editor-ai-assist__toolbar-button" @click="emit('close')">
|
||||
<button
|
||||
type="button"
|
||||
class="editor-ai-assist__toolbar-button"
|
||||
@click="emit('close')"
|
||||
>
|
||||
<DeleteOutlined />
|
||||
<span>{{ discardLabel }}</span>
|
||||
</button>
|
||||
@@ -463,7 +550,7 @@ onBeforeUnmount(() => {
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
border: 1px solid rgba(17, 24, 39, 0.08);
|
||||
box-shadow:
|
||||
0 16px 48px -4px rgba(17, 24, 39, 0.12),
|
||||
0 16px 48px -4px rgba(17, 24, 39, 0.12),
|
||||
0 6px 20px -4px rgba(17, 24, 39, 0.08);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
@@ -710,7 +797,9 @@ onBeforeUnmount(() => {
|
||||
|
||||
.editor-ai-assist__continue:focus-within {
|
||||
border-color: rgba(37, 99, 235, 0.4);
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.02), 0 0 0 2px rgba(37, 99, 235, 0.1);
|
||||
box-shadow:
|
||||
inset 0 1px 2px rgba(0, 0, 0, 0.02),
|
||||
0 0 0 2px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
.editor-ai-assist__continue input {
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
emptyLabel: string;
|
||||
hint: string;
|
||||
initialRows?: number;
|
||||
initialCols?: number;
|
||||
maxRows?: number;
|
||||
maxCols?: number;
|
||||
expandStep?: number;
|
||||
cellSize?: number;
|
||||
gap?: number;
|
||||
emptyLabel: string
|
||||
hint: string
|
||||
initialRows?: number
|
||||
initialCols?: number
|
||||
maxRows?: number
|
||||
maxCols?: number
|
||||
expandStep?: number
|
||||
cellSize?: number
|
||||
gap?: number
|
||||
}>(),
|
||||
{
|
||||
initialRows: 4,
|
||||
@@ -22,138 +22,141 @@ const props = withDefaults(
|
||||
cellSize: 22,
|
||||
gap: 6,
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [row: number, col: number];
|
||||
}>();
|
||||
select: [row: number, col: number]
|
||||
}>()
|
||||
|
||||
const gridRef = ref<HTMLElement | null>(null);
|
||||
const rows = ref(props.initialRows);
|
||||
const cols = ref(props.initialCols);
|
||||
const previewRow = ref(0);
|
||||
const previewCol = ref(0);
|
||||
const pointerActive = ref(false);
|
||||
const gridRef = ref<HTMLElement | null>(null)
|
||||
const rows = ref(props.initialRows)
|
||||
const cols = ref(props.initialCols)
|
||||
const previewRow = ref(0)
|
||||
const previewCol = ref(0)
|
||||
const pointerActive = ref(false)
|
||||
|
||||
const gridStyle = computed(() => ({
|
||||
gridTemplateColumns: `repeat(${cols.value}, ${props.cellSize}px)`,
|
||||
gap: `${props.gap}px`,
|
||||
}));
|
||||
}))
|
||||
|
||||
const cellStyle = computed(() => ({
|
||||
width: `${props.cellSize}px`,
|
||||
height: `${props.cellSize}px`,
|
||||
}));
|
||||
}))
|
||||
|
||||
const cells = computed(() => {
|
||||
const entries: Array<{ key: string; row: number; col: number }> = [];
|
||||
const entries: Array<{ key: string; row: number; col: number }> = []
|
||||
|
||||
for (let row = 1; row <= rows.value; row += 1) {
|
||||
for (let col = 1; col <= cols.value; col += 1) {
|
||||
entries.push({ key: `${row}-${col}`, row, col });
|
||||
entries.push({ key: `${row}-${col}`, row, col })
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
});
|
||||
return entries
|
||||
})
|
||||
|
||||
const selectionLabel = computed(() => {
|
||||
if (!previewRow.value || !previewCol.value) {
|
||||
return props.emptyLabel;
|
||||
return props.emptyLabel
|
||||
}
|
||||
|
||||
return `${previewRow.value} × ${previewCol.value}`;
|
||||
});
|
||||
return `${previewRow.value} × ${previewCol.value}`
|
||||
})
|
||||
|
||||
function clamp(value: number, min: number, max: number): number {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
return Math.min(Math.max(value, min), max)
|
||||
}
|
||||
|
||||
function ensureCapacity(row: number, col: number): void {
|
||||
while (row > rows.value && rows.value < props.maxRows) {
|
||||
rows.value = Math.min(props.maxRows, rows.value + props.expandStep);
|
||||
rows.value = Math.min(props.maxRows, rows.value + props.expandStep)
|
||||
}
|
||||
|
||||
while (col > cols.value && cols.value < props.maxCols) {
|
||||
cols.value = Math.min(props.maxCols, cols.value + props.expandStep);
|
||||
cols.value = Math.min(props.maxCols, cols.value + props.expandStep)
|
||||
}
|
||||
|
||||
if (row === rows.value && rows.value < props.maxRows) {
|
||||
rows.value = Math.min(props.maxRows, rows.value + props.expandStep);
|
||||
rows.value = Math.min(props.maxRows, rows.value + props.expandStep)
|
||||
}
|
||||
|
||||
if (col === cols.value && cols.value < props.maxCols) {
|
||||
cols.value = Math.min(props.maxCols, cols.value + props.expandStep);
|
||||
cols.value = Math.min(props.maxCols, cols.value + props.expandStep)
|
||||
}
|
||||
}
|
||||
|
||||
function updatePreview(row: number, col: number): void {
|
||||
const nextRow = clamp(row, 1, props.maxRows);
|
||||
const nextCol = clamp(col, 1, props.maxCols);
|
||||
const nextRow = clamp(row, 1, props.maxRows)
|
||||
const nextCol = clamp(col, 1, props.maxCols)
|
||||
|
||||
ensureCapacity(nextRow, nextCol);
|
||||
previewRow.value = nextRow;
|
||||
previewCol.value = nextCol;
|
||||
ensureCapacity(nextRow, nextCol)
|
||||
previewRow.value = nextRow
|
||||
previewCol.value = nextCol
|
||||
}
|
||||
|
||||
function resolveSizeFromPointer(clientX: number, clientY: number): { row: number; col: number } | null {
|
||||
const grid = gridRef.value;
|
||||
function resolveSizeFromPointer(
|
||||
clientX: number,
|
||||
clientY: number,
|
||||
): { row: number; col: number } | null {
|
||||
const grid = gridRef.value
|
||||
if (!grid) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
const rect = grid.getBoundingClientRect();
|
||||
const stepX = props.cellSize + props.gap;
|
||||
const stepY = props.cellSize + props.gap;
|
||||
const relativeX = clientX - rect.left;
|
||||
const relativeY = clientY - rect.top;
|
||||
const rect = grid.getBoundingClientRect()
|
||||
const stepX = props.cellSize + props.gap
|
||||
const stepY = props.cellSize + props.gap
|
||||
const relativeX = clientX - rect.left
|
||||
const relativeY = clientY - rect.top
|
||||
|
||||
return {
|
||||
row: clamp(Math.ceil((relativeY + props.gap) / stepY), 1, props.maxRows),
|
||||
col: clamp(Math.ceil((relativeX + props.gap) / stepX), 1, props.maxCols),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function beginSelection(row: number, col: number): void {
|
||||
pointerActive.value = true;
|
||||
updatePreview(row, col);
|
||||
pointerActive.value = true
|
||||
updatePreview(row, col)
|
||||
}
|
||||
|
||||
function handleWindowPointerMove(event: PointerEvent): void {
|
||||
if (!pointerActive.value) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const nextSize = resolveSizeFromPointer(event.clientX, event.clientY);
|
||||
const nextSize = resolveSizeFromPointer(event.clientX, event.clientY)
|
||||
if (!nextSize) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
updatePreview(nextSize.row, nextSize.col);
|
||||
updatePreview(nextSize.row, nextSize.col)
|
||||
}
|
||||
|
||||
function handleWindowPointerUp(): void {
|
||||
if (!pointerActive.value) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
pointerActive.value = false;
|
||||
pointerActive.value = false
|
||||
if (!previewRow.value || !previewCol.value) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
emit("select", previewRow.value, previewCol.value);
|
||||
emit('select', previewRow.value, previewCol.value)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener("pointermove", handleWindowPointerMove);
|
||||
window.addEventListener("pointerup", handleWindowPointerUp);
|
||||
});
|
||||
window.addEventListener('pointermove', handleWindowPointerMove)
|
||||
window.addEventListener('pointerup', handleWindowPointerUp)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("pointermove", handleWindowPointerMove);
|
||||
window.removeEventListener("pointerup", handleWindowPointerUp);
|
||||
});
|
||||
window.removeEventListener('pointermove', handleWindowPointerMove)
|
||||
window.removeEventListener('pointerup', handleWindowPointerUp)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { computed } from 'vue'
|
||||
|
||||
type SelectionHandle = "nw" | "ne" | "sw" | "se";
|
||||
type SelectionHandle = 'nw' | 'ne' | 'sw' | 'se'
|
||||
|
||||
const props = defineProps<{
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}>();
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
"resize-start": [event: PointerEvent, handle: SelectionHandle];
|
||||
}>();
|
||||
'resize-start': [event: PointerEvent, handle: SelectionHandle]
|
||||
}>()
|
||||
|
||||
const frameStyle = computed(() => ({
|
||||
left: `${props.x}px`,
|
||||
top: `${props.y}px`,
|
||||
width: `${props.width}px`,
|
||||
height: `${props.height}px`,
|
||||
}));
|
||||
}))
|
||||
|
||||
function handleResizeStart(event: PointerEvent, handle: SelectionHandle): void {
|
||||
emit("resize-start", event, handle);
|
||||
emit('resize-start', event, handle)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user