feat(web): expand slash command menu
This commit is contained in:
parent
39328cf6a9
commit
50310a1bf8
@ -54,30 +54,43 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="skillSlashMenuOpen"
|
||||
ref="skillSlashList"
|
||||
class="skill-slash-menu"
|
||||
role="listbox"
|
||||
aria-label="可用 Skills"
|
||||
<transition
|
||||
name="skill-slash-menu-motion"
|
||||
@before-enter="handleSlashMenuTransitionStart"
|
||||
@before-leave="handleSlashMenuTransitionStart"
|
||||
@after-enter="handleSlashMenuAfterEnter"
|
||||
@after-leave="handleSlashMenuAfterLeave"
|
||||
>
|
||||
<button
|
||||
v-for="(skill, index) in filteredSkills"
|
||||
:key="skill.path"
|
||||
type="button"
|
||||
class="skill-slash-item"
|
||||
:class="{ 'skill-slash-item--active': index === skillSlashActiveIndex }"
|
||||
role="option"
|
||||
:aria-selected="index === skillSlashActiveIndex"
|
||||
@mousedown.prevent="selectSkillSlashItem(index)"
|
||||
<div
|
||||
v-if="skillSlashMenuOpen"
|
||||
:key="slashMenuMode"
|
||||
ref="skillSlashList"
|
||||
class="skill-slash-menu"
|
||||
role="listbox"
|
||||
:aria-label="slashMenuAriaLabel"
|
||||
>
|
||||
<span class="skill-slash-item__name">{{ skill.name }}</span>
|
||||
<span class="skill-slash-item__description">{{ skill.description }}</span>
|
||||
</button>
|
||||
<div v-if="!filteredSkills.length" class="skill-slash-empty">
|
||||
没有匹配的 skill
|
||||
<button
|
||||
v-for="(item, index) in activeSlashMenuItems"
|
||||
:key="item.id"
|
||||
type="button"
|
||||
class="skill-slash-item"
|
||||
:class="{
|
||||
'skill-slash-item--active': index === skillSlashActiveIndex,
|
||||
'skill-slash-item--disabled': item.disabled
|
||||
}"
|
||||
role="option"
|
||||
:aria-selected="index === skillSlashActiveIndex"
|
||||
:disabled="item.disabled"
|
||||
@mousedown.prevent="selectSlashMenuItem(index)"
|
||||
>
|
||||
<span class="skill-slash-item__name">{{ item.label }}</span>
|
||||
<span class="skill-slash-item__description">{{ item.description }}</span>
|
||||
</button>
|
||||
<div v-if="!activeSlashMenuItems.length" class="skill-slash-empty">
|
||||
{{ slashMenuEmptyText }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<div
|
||||
class="stadium-shell"
|
||||
ref="compactInputShell"
|
||||
@ -428,6 +441,16 @@ const stadiumInput = ref<HTMLTextAreaElement | null>(null);
|
||||
const fileUploadInput = ref<HTMLInputElement | null>(null);
|
||||
const baselineComposerVisualHeight = ref<number>(0);
|
||||
type SkillItem = { name: string; description: string; path: string };
|
||||
type SlashMenuMode = 'root' | 'skills' | 'theme';
|
||||
type SlashMenuItem = {
|
||||
id: string;
|
||||
label: string;
|
||||
description: string;
|
||||
disabled?: boolean;
|
||||
action?: () => void;
|
||||
mode?: SlashMenuMode;
|
||||
skill?: SkillItem;
|
||||
};
|
||||
const availableSkills = ref<SkillItem[]>([]);
|
||||
const selectedSkillRefs = ref<SkillItem[]>([]);
|
||||
const skillsLoaded = ref(false);
|
||||
@ -436,6 +459,9 @@ const skillSlashOpen = ref(false);
|
||||
const skillSlashQuery = ref('');
|
||||
const skillSlashActiveIndex = ref(0);
|
||||
const skillSlashList = ref<HTMLElement | null>(null);
|
||||
const slashMenuMode = ref<SlashMenuMode>('root');
|
||||
const slashMenuParentIndex = ref(0);
|
||||
const slashMenuTransitioning = ref(false);
|
||||
let composerResizeObserver: ResizeObserver | null = null;
|
||||
let syncingEditorFromProps = false;
|
||||
|
||||
@ -476,7 +502,7 @@ const findSlashToken = () => {
|
||||
const { $from } = state.selection;
|
||||
const lineStart = $from.start();
|
||||
const beforeInLine = $from.parent.textBetween(0, $from.parentOffset, '\n', '\n');
|
||||
const match = /(^|[ \n])\/([A-Za-z0-9_-]*)$/.exec(beforeInLine);
|
||||
const match = /(^|[ \n])\/([^\s/]*)$/.exec(beforeInLine);
|
||||
if (!match) return null;
|
||||
const query = match[2] || '';
|
||||
const start = $from.pos - query.length - 1;
|
||||
@ -488,6 +514,30 @@ const findSlashToken = () => {
|
||||
};
|
||||
};
|
||||
|
||||
const closeSlashMenu = () => {
|
||||
skillSlashOpen.value = false;
|
||||
skillSlashQuery.value = '';
|
||||
skillSlashActiveIndex.value = 0;
|
||||
slashMenuMode.value = 'root';
|
||||
slashMenuParentIndex.value = 0;
|
||||
};
|
||||
|
||||
const deleteSlashToken = () => {
|
||||
const token = findSlashToken();
|
||||
const instance = editor.value;
|
||||
if (!token || !instance) {
|
||||
return;
|
||||
}
|
||||
const { state, view } = instance;
|
||||
let tr = state.tr.delete(token.start, token.end);
|
||||
tr = tr.setSelection(TextSelection.near(tr.doc.resolve(Math.max(0, token.start))));
|
||||
view.dispatch(tr);
|
||||
view.focus();
|
||||
nextTick(() => {
|
||||
adjustTextareaSize();
|
||||
});
|
||||
};
|
||||
|
||||
const scrollSkillSlashSelectionIntoMiddle = () => {
|
||||
nextTick(() => {
|
||||
const list = skillSlashList.value;
|
||||
@ -510,7 +560,186 @@ const filteredSkills = computed(() => {
|
||||
return filtered.slice(0, 100);
|
||||
});
|
||||
|
||||
const skillSlashMenuOpen = computed(() => skillSlashOpen.value && (skillsLoading.value || filteredSkills.value.length >= 0));
|
||||
const currentModelSupportsImage = computed(() => {
|
||||
const found = props.modelOptions?.find((m) => m.key === props.currentModelKey) as any;
|
||||
return !!found?.supportsImage;
|
||||
});
|
||||
|
||||
const currentModelSupportsVideo = computed(() => {
|
||||
const found = props.modelOptions?.find((m) => m.key === props.currentModelKey) as any;
|
||||
return !!found?.supportsVideo;
|
||||
});
|
||||
|
||||
const applySlashTheme = async (theme: 'classic' | 'light' | 'dark') => {
|
||||
personalizationStore.applyTheme(theme);
|
||||
personalizationStore.updateField({ key: 'theme', value: theme });
|
||||
try {
|
||||
const resp = await fetch('/api/personalization', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ theme })
|
||||
});
|
||||
const result = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok || !result.success) {
|
||||
console.warn('保存主题到后端失败:', result?.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('保存主题到后端失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const executeSlashAction = (action: () => void) => {
|
||||
deleteSlashToken();
|
||||
closeSlashMenu();
|
||||
action();
|
||||
};
|
||||
|
||||
const rootSlashMenuItems = computed<SlashMenuItem[]>(() => {
|
||||
const items: SlashMenuItem[] = [
|
||||
{
|
||||
id: 'upload',
|
||||
label: '上传文件',
|
||||
description: props.uploading ? '上传中...' : '选择本地文件上传',
|
||||
disabled: !props.isConnected || props.uploading || props.blockUpload,
|
||||
action: () => emit('quick-upload')
|
||||
},
|
||||
{
|
||||
id: 'image',
|
||||
label: '发送图片',
|
||||
description: currentModelSupportsImage.value ? '选择图片并附加到消息' : '当前模型不支持图片',
|
||||
disabled: !props.isConnected || props.streamingMessage || !currentModelSupportsImage.value,
|
||||
action: () => emit('pick-images')
|
||||
},
|
||||
{
|
||||
id: 'video',
|
||||
label: '发送视频',
|
||||
description: currentModelSupportsVideo.value ? '选择视频并附加到消息' : '当前模型不支持视频',
|
||||
disabled: !props.isConnected || props.streamingMessage || !currentModelSupportsVideo.value,
|
||||
action: () => emit('pick-video')
|
||||
},
|
||||
{
|
||||
id: 'compress',
|
||||
label: '压缩对话',
|
||||
description: props.compressing ? '压缩中...' : '手动压缩当前对话上下文',
|
||||
disabled:
|
||||
!props.isConnected ||
|
||||
props.compressing ||
|
||||
props.streamingMessage ||
|
||||
props.blockCompressConversation,
|
||||
action: () => emit('compress-conversation')
|
||||
},
|
||||
{
|
||||
id: 'skills',
|
||||
label: '选择 AgentSkill',
|
||||
description: '插入一个 AgentSkill 引用',
|
||||
mode: 'skills'
|
||||
},
|
||||
{
|
||||
id: 'theme',
|
||||
label: '切换主题',
|
||||
description: '经典 / 明亮 / 夜间',
|
||||
mode: 'theme'
|
||||
},
|
||||
{
|
||||
id: 'review',
|
||||
label: '对话回顾',
|
||||
description: '打开当前对话回顾',
|
||||
disabled: !props.isConnected || props.streamingMessage || props.blockConversationReview,
|
||||
action: () => emit('open-review')
|
||||
},
|
||||
{
|
||||
id: 'tokens',
|
||||
label: '用量统计',
|
||||
description: '打开上下文与 token 用量面板',
|
||||
disabled: !props.currentConversationId || props.blockTokenPanel,
|
||||
action: () => emit('toggle-token-panel', true)
|
||||
},
|
||||
{
|
||||
id: 'goal',
|
||||
label: '目标模式',
|
||||
description: props.goalRunning ? '目标模式运行中' : props.goalModeArmed ? '目标模式已就绪' : '切换目标模式',
|
||||
disabled: !props.isConnected,
|
||||
action: () => emit('toggle-goal-mode')
|
||||
},
|
||||
{
|
||||
id: 'personalization',
|
||||
label: '个人设置',
|
||||
description: '打开个人空间设置',
|
||||
action: () => {
|
||||
void personalizationStore.openDrawer();
|
||||
}
|
||||
}
|
||||
];
|
||||
const query = skillSlashQuery.value.trim().toLowerCase();
|
||||
if (!query) {
|
||||
return items;
|
||||
}
|
||||
return items.filter((item) => {
|
||||
const haystack = `${item.label} ${item.description} ${item.id}`.toLowerCase();
|
||||
return haystack.includes(query);
|
||||
});
|
||||
});
|
||||
|
||||
const themeSlashMenuItems = computed<SlashMenuItem[]>(() => {
|
||||
const themes: Array<SlashMenuItem & { theme: 'classic' | 'light' | 'dark' }> = [
|
||||
{
|
||||
id: 'theme-classic',
|
||||
label: '经典',
|
||||
description: '米色质感,柔和高对比',
|
||||
theme: 'classic'
|
||||
},
|
||||
{
|
||||
id: 'theme-light',
|
||||
label: '明亮',
|
||||
description: '纯白底色 + 优雅灰,简洁清爽',
|
||||
theme: 'light'
|
||||
},
|
||||
{
|
||||
id: 'theme-dark',
|
||||
label: '夜间',
|
||||
description: '深灰 + 黑,低亮度显示',
|
||||
theme: 'dark'
|
||||
}
|
||||
];
|
||||
return themes.map((item) => ({
|
||||
...item,
|
||||
action: () => {
|
||||
void applySlashTheme(item.theme);
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
const skillSlashMenuItems = computed<SlashMenuItem[]>(() =>
|
||||
filteredSkills.value.map((skill) => ({
|
||||
id: `skill:${skill.path}`,
|
||||
label: skill.name,
|
||||
description: skill.description,
|
||||
skill
|
||||
}))
|
||||
);
|
||||
|
||||
const activeSlashMenuItems = computed<SlashMenuItem[]>(() => {
|
||||
if (slashMenuMode.value === 'skills') {
|
||||
return skillSlashMenuItems.value;
|
||||
}
|
||||
if (slashMenuMode.value === 'theme') {
|
||||
return themeSlashMenuItems.value;
|
||||
}
|
||||
return rootSlashMenuItems.value;
|
||||
});
|
||||
|
||||
const slashMenuAriaLabel = computed(() => {
|
||||
if (slashMenuMode.value === 'skills') return '可用 AgentSkills';
|
||||
if (slashMenuMode.value === 'theme') return '主题选项';
|
||||
return '快捷指令';
|
||||
});
|
||||
|
||||
const slashMenuEmptyText = computed(() => {
|
||||
if (slashMenuMode.value === 'skills') return skillsLoading.value ? '正在加载 skills...' : '没有匹配的 skill';
|
||||
return '没有匹配的指令';
|
||||
});
|
||||
|
||||
const skillSlashMenuOpen = computed(() => skillSlashOpen.value && (skillsLoading.value || activeSlashMenuItems.value.length >= 0));
|
||||
|
||||
const loadSkills = async () => {
|
||||
if (skillsLoaded.value || skillsLoading.value) {
|
||||
@ -545,9 +774,7 @@ const loadSkills = async () => {
|
||||
const refreshSkillSlashState = () => {
|
||||
const token = findSlashToken();
|
||||
if (!token || !props.isConnected || props.inputLocked) {
|
||||
skillSlashOpen.value = false;
|
||||
skillSlashQuery.value = '';
|
||||
skillSlashActiveIndex.value = 0;
|
||||
closeSlashMenu();
|
||||
return;
|
||||
}
|
||||
skillSlashOpen.value = true;
|
||||
@ -555,9 +782,11 @@ const refreshSkillSlashState = () => {
|
||||
skillSlashActiveIndex.value = 0;
|
||||
}
|
||||
skillSlashQuery.value = token.query;
|
||||
skillSlashActiveIndex.value = Math.min(skillSlashActiveIndex.value, Math.max(0, filteredSkills.value.length - 1));
|
||||
if (slashMenuMode.value === 'skills') {
|
||||
void loadSkills();
|
||||
}
|
||||
skillSlashActiveIndex.value = Math.min(skillSlashActiveIndex.value, Math.max(0, activeSlashMenuItems.value.length - 1));
|
||||
scrollSkillSlashSelectionIntoMiddle();
|
||||
void loadSkills();
|
||||
};
|
||||
|
||||
const selectSkillSlashItem = (index = skillSlashActiveIndex.value) => {
|
||||
@ -590,14 +819,37 @@ const selectSkillSlashItem = (index = skillSlashActiveIndex.value) => {
|
||||
tr = tr.setSelection(TextSelection.near(tr.doc.resolve(afterMention + 1)));
|
||||
view.dispatch(tr);
|
||||
view.focus();
|
||||
skillSlashOpen.value = false;
|
||||
skillSlashQuery.value = '';
|
||||
skillSlashActiveIndex.value = 0;
|
||||
closeSlashMenu();
|
||||
nextTick(() => {
|
||||
adjustTextareaSize();
|
||||
});
|
||||
};
|
||||
|
||||
const selectSlashMenuItem = (index = skillSlashActiveIndex.value) => {
|
||||
const item = activeSlashMenuItems.value[index];
|
||||
if (!item || item.disabled) {
|
||||
return;
|
||||
}
|
||||
if (item.mode) {
|
||||
slashMenuParentIndex.value = index;
|
||||
slashMenuMode.value = item.mode;
|
||||
skillSlashActiveIndex.value = 0;
|
||||
if (item.mode === 'skills') {
|
||||
void loadSkills();
|
||||
}
|
||||
scrollSkillSlashSelectionIntoMiddle();
|
||||
return;
|
||||
}
|
||||
if (item.skill) {
|
||||
const skillIndex = filteredSkills.value.findIndex((skill) => skill.path === item.skill?.path);
|
||||
selectSkillSlashItem(skillIndex >= 0 ? skillIndex : index);
|
||||
return;
|
||||
}
|
||||
if (item.action) {
|
||||
executeSlashAction(item.action);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSlashMenuKeydown = (event: KeyboardEvent): boolean => {
|
||||
if (event.ctrlKey && event.key === 'Enter') {
|
||||
return false;
|
||||
@ -605,7 +857,7 @@ const handleSlashMenuKeydown = (event: KeyboardEvent): boolean => {
|
||||
if (skillSlashOpen.value) {
|
||||
if (event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
const count = filteredSkills.value.length;
|
||||
const count = activeSlashMenuItems.value.length;
|
||||
if (count > 0) {
|
||||
skillSlashActiveIndex.value = (skillSlashActiveIndex.value + 1) % count;
|
||||
scrollSkillSlashSelectionIntoMiddle();
|
||||
@ -614,7 +866,7 @@ const handleSlashMenuKeydown = (event: KeyboardEvent): boolean => {
|
||||
}
|
||||
if (event.key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
const count = filteredSkills.value.length;
|
||||
const count = activeSlashMenuItems.value.length;
|
||||
if (count > 0) {
|
||||
skillSlashActiveIndex.value = (skillSlashActiveIndex.value - 1 + count) % count;
|
||||
scrollSkillSlashSelectionIntoMiddle();
|
||||
@ -622,16 +874,25 @@ const handleSlashMenuKeydown = (event: KeyboardEvent): boolean => {
|
||||
return true;
|
||||
}
|
||||
if (event.key === 'Enter') {
|
||||
const count = filteredSkills.value.length;
|
||||
const count = activeSlashMenuItems.value.length;
|
||||
if (count > 0) {
|
||||
event.preventDefault();
|
||||
selectSkillSlashItem(skillSlashActiveIndex.value);
|
||||
selectSlashMenuItem(skillSlashActiveIndex.value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
skillSlashOpen.value = false;
|
||||
if (slashMenuMode.value !== 'root') {
|
||||
slashMenuMode.value = 'root';
|
||||
skillSlashActiveIndex.value = Math.min(
|
||||
slashMenuParentIndex.value,
|
||||
Math.max(0, activeSlashMenuItems.value.length - 1)
|
||||
);
|
||||
scrollSkillSlashSelectionIntoMiddle();
|
||||
} else {
|
||||
closeSlashMenu();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -649,7 +910,7 @@ const onKeydown = (event: KeyboardEvent): boolean => {
|
||||
const onInputBlur = () => {
|
||||
emit('input-blur');
|
||||
window.setTimeout(() => {
|
||||
skillSlashOpen.value = false;
|
||||
closeSlashMenu();
|
||||
}, 120);
|
||||
};
|
||||
|
||||
@ -772,7 +1033,9 @@ const runtimeQueuePrevElements = new Map<string, HTMLElement>();
|
||||
const runtimeQueueGhosts = new Map<string, HTMLElement>();
|
||||
const runtimeQueueActiveAnimations = new WeakMap<HTMLElement, Animation>();
|
||||
const runtimeQueueFallbackTimers = new WeakMap<HTMLElement, number>();
|
||||
const runtimeQueueTransitioning = ref(false);
|
||||
let runtimeQueueAnimationReady = false;
|
||||
let runtimeQueueTransitionTimer: number | null = null;
|
||||
|
||||
const bindRuntimeQueueItemRef = (id: string) => (el: Element | null) => {
|
||||
if (!id) {
|
||||
@ -805,6 +1068,18 @@ const stopRuntimeQueueAnimation = (el: HTMLElement) => {
|
||||
el.style.removeProperty('pointer-events');
|
||||
};
|
||||
|
||||
const keepRuntimeQueueTransitionCollapsed = () => {
|
||||
runtimeQueueTransitioning.value = true;
|
||||
if (runtimeQueueTransitionTimer !== null) {
|
||||
window.clearTimeout(runtimeQueueTransitionTimer);
|
||||
}
|
||||
runtimeQueueTransitionTimer = window.setTimeout(() => {
|
||||
runtimeQueueTransitioning.value = false;
|
||||
runtimeQueueTransitionTimer = null;
|
||||
emitComposerHeight();
|
||||
}, RUNTIME_QUEUE_ANIM_DURATION + 40);
|
||||
};
|
||||
|
||||
const playRuntimeQueueTransform = (
|
||||
el: HTMLElement,
|
||||
fromY: number,
|
||||
@ -818,6 +1093,7 @@ const playRuntimeQueueTransform = (
|
||||
done?.();
|
||||
return;
|
||||
}
|
||||
keepRuntimeQueueTransitionCollapsed();
|
||||
stopRuntimeQueueAnimation(el);
|
||||
const from = `translateY(${fromY}px)`;
|
||||
const to = `translateY(${toY}px)`;
|
||||
@ -1034,10 +1310,28 @@ const hasRuntimeLayoutExpansion = computed(() => {
|
||||
|
||||
const goalCompleted = computed(() => String(props.goalProgress?.status || '').toLowerCase() === 'done');
|
||||
|
||||
const handleSlashMenuTransitionStart = () => {
|
||||
slashMenuTransitioning.value = true;
|
||||
};
|
||||
|
||||
const handleSlashMenuAfterEnter = () => {
|
||||
slashMenuTransitioning.value = false;
|
||||
emitComposerHeight();
|
||||
};
|
||||
|
||||
const handleSlashMenuAfterLeave = () => {
|
||||
slashMenuTransitioning.value = false;
|
||||
emitComposerHeight();
|
||||
};
|
||||
|
||||
// 当消息队列非空或 skill 选择菜单展开时,把目标横幅收起为仅圆点,
|
||||
// 让队列/菜单紧贴输入框,不再被横幅顶高。队列清空且无 skill 选择时自动恢复。
|
||||
const goalBannerCollapsed = computed(
|
||||
() => runtimeQueuedMessagesForRender.value.length > 0 || skillSlashMenuOpen.value
|
||||
() =>
|
||||
runtimeQueuedMessagesForRender.value.length > 0 ||
|
||||
runtimeQueueTransitioning.value ||
|
||||
skillSlashMenuOpen.value ||
|
||||
slashMenuTransitioning.value
|
||||
);
|
||||
|
||||
const goalBannerTitle = computed(() => {
|
||||
@ -1342,6 +1636,11 @@ onBeforeUnmount(() => {
|
||||
runtimeQueuePrevRects.clear();
|
||||
runtimeQueuePrevElements.clear();
|
||||
runtimeQueueAnimationReady = false;
|
||||
if (runtimeQueueTransitionTimer !== null) {
|
||||
window.clearTimeout(runtimeQueueTransitionTimer);
|
||||
runtimeQueueTransitionTimer = null;
|
||||
}
|
||||
runtimeQueueTransitioning.value = false;
|
||||
if (composerResizeObserver) {
|
||||
composerResizeObserver.disconnect();
|
||||
composerResizeObserver = null;
|
||||
|
||||
@ -28,7 +28,7 @@
|
||||
--runtime-queue-bg: var(--theme-surface-soft);
|
||||
--runtime-queue-divider: rgba(15, 23, 42, 0.12);
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
left: calc(50% - 7px);
|
||||
bottom: calc(100% - 2px);
|
||||
transform: translateX(-50%);
|
||||
z-index: 1;
|
||||
@ -125,9 +125,11 @@
|
||||
.skill-slash-menu {
|
||||
--skill-slash-row-height: 38px;
|
||||
--skill-slash-gap: 3px;
|
||||
--skill-slash-motion-duration: 300ms;
|
||||
--skill-slash-motion-easing: cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
--skill-slash-visible-rows: 5;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
left: calc(50% - 7px);
|
||||
bottom: calc(100% - 2px);
|
||||
transform: translateX(-50%);
|
||||
z-index: 1;
|
||||
@ -152,6 +154,27 @@
|
||||
gap: var(--skill-slash-gap);
|
||||
}
|
||||
|
||||
.skill-slash-menu-motion-enter-active,
|
||||
.skill-slash-menu-motion-leave-active {
|
||||
transition:
|
||||
transform var(--skill-slash-motion-duration, 300ms) var(--skill-slash-motion-easing, cubic-bezier(0.25, 0.8, 0.25, 1)),
|
||||
clip-path var(--skill-slash-motion-duration, 300ms) var(--skill-slash-motion-easing, cubic-bezier(0.25, 0.8, 0.25, 1));
|
||||
transform-origin: bottom center;
|
||||
will-change: transform, clip-path;
|
||||
}
|
||||
|
||||
.skill-slash-menu-motion-enter-from,
|
||||
.skill-slash-menu-motion-leave-to {
|
||||
transform: translateX(-50%) translateY(100%);
|
||||
clip-path: inset(0 0 100% 0 round 12px 12px 0 0);
|
||||
}
|
||||
|
||||
.skill-slash-menu-motion-enter-to,
|
||||
.skill-slash-menu-motion-leave-from {
|
||||
transform: translateX(-50%) translateY(0);
|
||||
clip-path: inset(0 0 0 0 round 12px 12px 0 0);
|
||||
}
|
||||
|
||||
.skill-slash-menu::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
@ -180,6 +203,18 @@
|
||||
box-shadow: 0 1px 2px rgba(61, 57, 41, 0.08);
|
||||
}
|
||||
|
||||
.skill-slash-item--disabled,
|
||||
.skill-slash-item:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.skill-slash-item--disabled:hover,
|
||||
.skill-slash-item:disabled:hover {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.skill-slash-item__name {
|
||||
flex: 0 0 auto;
|
||||
max-width: 210px;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user