Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion src/apps/ohos/entry/src/main/ets/utils/CommonUtils.ets
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,21 @@ export class CommonUtils {
try {
hilog.info(0x0000, 'vnext', `open_file_dialog: multiple=${multiple}, mode=${selectMode}`);
const documentOptions = new filePicker.DocumentSelectOptions;
documentOptions.defaultFilePathUri = 'file://docs/storage/Users/currentUser';
// Mirror reveal_in_explorer path→URI conversion so the picker opens at the
// caller-supplied path (e.g. the parent directory shown in NewProjectDialog)
// instead of the hardcoded default. Fallback keeps legacy behavior.
const defaultPathRaw: string = (opts.defaultPath ?? '').trim();
let defaultUri: string = 'file://docs/storage/Users/currentUser';
if (defaultPathRaw.length > 0) {
if (defaultPathRaw.startsWith('file://')) {
defaultUri = defaultPathRaw;
} else if (defaultPathRaw.startsWith('/data/storage/')) {
defaultUri = fileUri.getUriFromPath(defaultPathRaw);
} else {
defaultUri = 'file://docs' + defaultPathRaw;
}
}
documentOptions.defaultFilePathUri = defaultUri;
documentOptions.selectMode = selectMode;
documentOptions.maxSelectNumber = multiple ? 500 : 1;

Expand Down Expand Up @@ -185,4 +199,5 @@ interface PickerOptions {
multiple?: boolean;
directory?: boolean;
filters?: PickerFilter[];
defaultPath?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ interface WorkspaceItemProps {
draggable?: boolean;
isDragging?: boolean;
onDragStart?: React.DragEventHandler<HTMLDivElement>;
onDrag?: React.DragEventHandler<HTMLDivElement>;
onDragEnd?: React.DragEventHandler<HTMLDivElement>;
}

Expand All @@ -85,6 +86,7 @@ const WorkspaceItem: React.FC<WorkspaceItemProps> = ({
draggable = false,
isDragging = false,
onDragStart,
onDrag,
onDragEnd,
}) => {
const { t } = useI18n('common');
Expand Down Expand Up @@ -142,7 +144,8 @@ const WorkspaceItem: React.FC<WorkspaceItemProps> = ({
const [acpClientsLoading, setAcpClientsLoading] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
const menuAnchorRef = useRef<HTMLDivElement>(null);
const menuPopoverRef = useRef<HTMLDivElement>(null);
const menuPopoverRef = useRef<HTMLDivElement | null>(null);
const popoverResizeObserverRef = useRef<ResizeObserver | null>(null);
const cardRef = useRef<HTMLDivElement>(null);
const [menuPosition, setMenuPosition] = useState<{ top: number; left: number } | null>(null);
const isNamedAssistantWorkspace =
Expand Down Expand Up @@ -384,6 +387,26 @@ const WorkspaceItem: React.FC<WorkspaceItemProps> = ({
requestAnimationFrame(apply);
}, []);

// Callback ref for the menu popover. The popover only mounts once menuPosition
// is set (chicken-and-egg: menuPosition needs the popover's size), so a
// ResizeObserver created in the menuOpen effect would attach to a null ref.
// Attaching here ties the observer to the element's actual mount/unmount:
// on mount it fires once with the real size (fixing the stale initial height)
// and again whenever async content (ACP client rows, loading toggle, remote
// /git conditional rows, locale label width) changes the popover height.
const setMenuPopoverRef = useCallback((node: HTMLDivElement | null) => {
if (popoverResizeObserverRef.current) {
popoverResizeObserverRef.current.disconnect();
popoverResizeObserverRef.current = null;
}
menuPopoverRef.current = node;
if (node && typeof ResizeObserver !== 'undefined') {
const ro = new ResizeObserver(() => updateMenuPosition());
ro.observe(node);
popoverResizeObserverRef.current = ro;
}
}, [updateMenuPosition]);

const handleMenuTriggerClick = useCallback(() => {
const nextOpen = !menuOpen;
setMenuOpen(nextOpen);
Expand Down Expand Up @@ -856,6 +879,7 @@ const WorkspaceItem: React.FC<WorkspaceItemProps> = ({
className="bitfun-nav-panel__assistant-item-card"
draggable={draggable}
onDragStart={onDragStart}
onDrag={onDrag}
onDragEnd={onDragEnd}
onClick={() => { void handleCardNameClick(); }}
style={{ cursor: 'pointer' }}
Expand Down Expand Up @@ -886,7 +910,7 @@ const WorkspaceItem: React.FC<WorkspaceItemProps> = ({
</span>
</span>
</button>
<Tooltip content={workspace.rootPath} placement="right" followCursor>
<Tooltip content={workspace.rootPath} placement="right" followCursor disabled={isDragging}>
<button
type="button"
className="bitfun-nav-panel__assistant-item-name-btn"
Expand Down Expand Up @@ -932,7 +956,7 @@ const WorkspaceItem: React.FC<WorkspaceItemProps> = ({

{menuOpen && menuPosition && createPortal(
<div
ref={menuPopoverRef}
ref={setMenuPopoverRef}
className="bitfun-nav-panel__workspace-item-menu-popover"
role="menu"
style={{ top: `${menuPosition.top}px`, left: `${menuPosition.left}px` }}
Expand Down Expand Up @@ -1110,6 +1134,7 @@ const WorkspaceItem: React.FC<WorkspaceItemProps> = ({
className="bitfun-nav-panel__workspace-item-card"
draggable={draggable}
onDragStart={onDragStart}
onDrag={onDrag}
onDragEnd={onDragEnd}
onClick={() => { void handleCardNameClick(); }}
style={{ cursor: 'pointer' }}
Expand Down Expand Up @@ -1143,7 +1168,7 @@ const WorkspaceItem: React.FC<WorkspaceItemProps> = ({
<div className="bitfun-nav-panel__workspace-item-name-cluster">
<div className="bitfun-nav-panel__workspace-item-name-stack">
<div className="bitfun-nav-panel__workspace-item-name-row">
<Tooltip content={workspace.rootPath} placement="right" followCursor>
<Tooltip content={workspace.rootPath} placement="right" followCursor disabled={isDragging}>
<button
type="button"
className="bitfun-nav-panel__workspace-item-name-btn"
Expand Down Expand Up @@ -1315,7 +1340,7 @@ const WorkspaceItem: React.FC<WorkspaceItemProps> = ({

{menuOpen && menuPosition && createPortal(
<div
ref={menuPopoverRef}
ref={setMenuPopoverRef}
className="bitfun-nav-panel__workspace-item-menu-popover"
role="menu"
style={{ top: `${menuPosition.top}px`, left: `${menuPosition.left}px` }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,15 @@
.bitfun-nav-panel__workspace-item-card {
cursor: default;
}

// Suppress focus outlines on children (icon/name/menu buttons) while
// dragging. The global :focus-visible ring around the icon, dimmed by the
// 0.42 opacity and baked into the native drag snapshot, reads as a thick
// whitish border around the icon. More specific than the global rule.
:focus,
:focus-visible {
outline: none;
}
}

&.is-active {
Expand Down Expand Up @@ -1037,6 +1046,14 @@
.bitfun-nav-panel__assistant-item-card {
cursor: default;
}

// See the pro-mode &.is-dragging rule: suppress the focus-visible ring
// around the icon that, dimmed and snapshotted into the drag image, looks
// like a thick whitish border.
:focus,
:focus-visible {
outline: none;
}
}

&.is-active {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useCallback, useRef, useState } from 'react';
import { useI18n } from '@/infrastructure/i18n';
import { useWorkspaceContext } from '@/infrastructure/contexts/WorkspaceContext';
import { notificationService } from '@/shared/notification-system';
import type { WorkspaceInfo } from '@/shared/types';
import WorkspaceItem from './WorkspaceItem';
import './WorkspaceListSection.scss';

Expand Down Expand Up @@ -37,6 +38,12 @@ const WorkspaceListSection: React.FC<WorkspaceListSectionProps> = ({ variant })
// Refs for values that must be read inside event handlers without stale closures
const draggedWorkspaceIdRef = useRef<string | null>(null);
const dropTargetRef = useRef<{ workspaceId: string; position: WorkspaceDragPosition } | null>(null);
// Custom drag preview (a regular DOM element following the cursor) + a 1x1
// transparent element used as the native drag image to hide the default ghost.
// The default ghost gets a white webview/OS halo that no CSS on the drag image
// can remove; rendering the preview as a normal DOM element avoids it.
const dragPreviewRef = useRef<HTMLDivElement | null>(null);
const dragHideRef = useRef<HTMLDivElement | null>(null);

const workspaces = variant === 'assistants'
? assistantWorkspacesList
Expand All @@ -45,17 +52,73 @@ const WorkspaceListSection: React.FC<WorkspaceListSectionProps> = ({ variant })
? t('nav.workspaces.emptyAssistants')
: t('nav.workspaces.emptyProjects');

const handleDragStart = useCallback((workspaceId: string) => (event: React.DragEvent<HTMLDivElement>) => {
const payload: WorkspaceDragPayload = { workspaceId, variant };
const handleDragStart = useCallback((workspace: WorkspaceInfo) => (event: React.DragEvent<HTMLDivElement>) => {
const payload: WorkspaceDragPayload = { workspaceId: workspace.id, variant };
const serializedPayload = JSON.stringify(payload);
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData(WORKSPACE_DRAG_MIME_TYPE, serializedPayload);
event.dataTransfer.setData('text/plain', serializedPayload);
draggedWorkspaceIdRef.current = workspaceId;
setDraggedWorkspaceId(workspaceId);
draggedWorkspaceIdRef.current = workspace.id;
setDraggedWorkspaceId(workspace.id);

// Hide the native drag ghost with a 1x1 fully-transparent element so the
// platform does not render the default item snapshot (which carries a white
// webview/OS halo no CSS on the drag image can remove).
const hideEl = document.createElement('div');
hideEl.style.width = '1px';
hideEl.style.height = '1px';
hideEl.style.position = 'absolute';
hideEl.style.top = '-1000px';
hideEl.style.background = 'transparent';
document.body.appendChild(hideEl);
dragHideRef.current = hideEl;
void hideEl.offsetWidth;
event.dataTransfer.setDragImage(hideEl, 0, 0);

// Render the visible drag preview as a regular DOM element following the
// cursor. Because it is NOT the native drag image, it has no platform halo,
// so rounded corners / borders are safe.
const label = variant === 'assistants'
? (workspace.identity?.name?.trim() || workspace.name)
: workspace.name;
const preview = document.createElement('div');
preview.textContent = label;
preview.style.position = 'fixed';
preview.style.left = `${event.clientX + 12}px`;
preview.style.top = `${event.clientY + 8}px`;
preview.style.padding = '6px 10px';
preview.style.background = 'var(--color-bg-elevated)';
preview.style.color = 'var(--color-text-primary)';
preview.style.border = '1px solid var(--border-subtle)';
preview.style.borderRadius = '6px';
preview.style.fontSize = '12px';
preview.style.maxWidth = '240px';
preview.style.whiteSpace = 'nowrap';
preview.style.overflow = 'hidden';
preview.style.textOverflow = 'ellipsis';
preview.style.pointerEvents = 'none';
preview.style.zIndex = '10000';
preview.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.3)';
document.body.appendChild(preview);
dragPreviewRef.current = preview;
}, [variant]);

const handleDrag = useCallback((event: React.DragEvent<HTMLDivElement>) => {
const preview = dragPreviewRef.current;
if (!preview) return;
preview.style.left = `${event.clientX + 12}px`;
preview.style.top = `${event.clientY + 8}px`;
}, []);

const handleDragEnd = useCallback(() => {
if (dragPreviewRef.current && document.body.contains(dragPreviewRef.current)) {
document.body.removeChild(dragPreviewRef.current);
}
dragPreviewRef.current = null;
if (dragHideRef.current && document.body.contains(dragHideRef.current)) {
document.body.removeChild(dragHideRef.current);
}
dragHideRef.current = null;
draggedWorkspaceIdRef.current = null;
dropTargetRef.current = null;
setDraggedWorkspaceId(null);
Expand Down Expand Up @@ -192,7 +255,8 @@ const WorkspaceListSection: React.FC<WorkspaceListSectionProps> = ({ variant })
isSingle={openedWorkspacesList.length === 1}
draggable={workspaces.length > 1}
isDragging={draggedWorkspaceId === workspace.id}
onDragStart={handleDragStart(workspace.id)}
onDragStart={handleDragStart(workspace)}
onDrag={handleDrag}
onDragEnd={handleDragEnd}
/>
{dropTarget?.workspaceId === workspace.id && dropTarget.position === 'after' ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,28 +125,39 @@
&__path-input {
flex: 1;
min-width: 0;

input {

// Style the container (not the <input>) so the overlay background and dashed
// border fill the full width including the container's padding area. The
// base Input.scss sets `border: none !important` on the <input>, so a border
// on the <input> never applied; putting it on the container restores the
// dashed look the design intended. Fills in all themes; only the dark theme
// made the unfilled ends visible because the white-04 overlay is invisible
// on light backgrounds.
.bitfun-input-wrapper .bitfun-input-container {
width: 100%;
cursor: default;
user-select: none;
font-size: 12px;
padding: 10px 12px;
background: var(--color-overlay-white-04);
border: 1px dashed var(--border-subtle);
border-radius: 8px;
padding: 10px 12px;
transition: border-color 0.2s ease;

&:focus-within {
border-style: solid;
border-color: var(--border-base);
background: var(--color-overlay-white-04);
}
}

input {
cursor: default;
user-select: none;
font-size: 12px;
color: var(--color-text-primary);

&::placeholder {
color: var(--color-text-muted);
font-size: 12px;
}

&:focus {
border-style: solid;
border-color: var(--border-base);
outline: none;
}
}
}

Expand Down Expand Up @@ -194,27 +205,32 @@
// ==================== Project Name Input ====================

&__name-input {
input {
// See &__path-input: style the container so the overlay background fills the
// full width and the border (here solid) renders as intended.
.bitfun-input-wrapper .bitfun-input-container {
width: 100%;
font-size: 12px;
padding: 10px 12px;
background: var(--color-overlay-white-04);
border: 1px solid var(--border-subtle);
border-radius: 8px;
padding: 10px 12px;
transition: border-color 0.2s ease, box-shadow 0.2s ease;

&:focus-within {
border-color: color-mix(in srgb, var(--color-accent-500) 45%, transparent);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-accent-500) 10%, transparent);
background: var(--color-overlay-white-04);
}
}

input {
font-size: 12px;
color: var(--color-text-primary);
transition: all 0.2s ease;


&::placeholder {
color: var(--color-text-muted);
font-size: 12px;
}

&:focus {
border-color: color-mix(in srgb, var(--color-accent-500) 45%, transparent);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-accent-500) 10%, transparent);
outline: none;
}


&:disabled {
opacity: 0.5;
cursor: not-allowed;
Expand Down
Loading
Loading