Skip to content
Closed
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
63 changes: 57 additions & 6 deletions src/apps/Files.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import {
import {
ArrowLeft,
ArrowDown,
ArrowRight,
ArrowUp,
CheckCheck,
ChevronRight,
Eye,
FileCode2,
Expand All @@ -31,6 +33,9 @@ import {
Scissors,
ClipboardPaste,
Copy,
ListFilter,
Trash2,
type LucideIcon,
} from "lucide-react";
import type {
AppContext,
Expand Down Expand Up @@ -70,6 +75,52 @@ import { fileSourceKey } from "../workspace-bindings";
import { capabilityOperationReason } from "../sdk";
import { scanDirectory } from "../directory-scan";
import { useVirtualRows } from "../components/useVirtualRows";

const fileMenuIcons: Record<string, LucideIcon> = {
upload: Upload,
"attach-drive": HardDrive,
"upload-folder": FolderPlus,
download: Download,
"download-files": Download,
"copy-clipboard": Copy,
"copy-files": Copy,
"copy-file": Copy,
mkdir: FolderPlus,
"new-file": FileText,
edit: FileText,
open: Eye,
"new-window": Folder,
"pin-to-desktop": Home,
"copy-name": Copy,
"copy-names": Copy,
"copy-path": Copy,
"copy-paths": Copy,
"copy-folder": Copy,
cut: Scissors,
rename: FileText,
move: ArrowRight,
delete: Trash2,
"delete-selection": Trash2,
"paste-move": ClipboardPaste,
back: ArrowLeft,
parent: ArrowUp,
refresh: RefreshCw,
"clipboard-path": ClipboardPaste,
"sort-view": ListFilter,
"select-all": CheckCheck,
"clear-selection": X,
"hidden-files": Eye,
"sort-name": ListFilter,
"sort-modified": ListFilter,
"sort-size": ListFilter,
ascending: ArrowUp,
descending: ArrowDown,
filesFoldersFirst: Folder,
filesShowHidden: Eye,
filesCompact: ListFilter,
};
const withFileIcons = (actions: MenuAction[]): MenuAction[] =>
actions.map((action) => ({ ...action, icon: fileMenuIcons[action.id] }));
function size(bytes: number) {
return bytes >= 1024 * 1024
? `${(bytes / 1048576).toFixed(1)} MB`
Expand Down Expand Up @@ -1034,7 +1085,7 @@ export function Files({
}, [selected, directory, query, preferences.filesShowHidden, loading]);
function menuActions(entry?: FileEntry): MenuAction[] {
if (selectedEntries.length > 1)
return [
return withFileIcons([
{
id: "copy-files",
label: `Copy ${selectedEntries.length} ${selectedEntries.some((item) => item.kind === "directory") ? "items" : "files"}`,
Expand Down Expand Up @@ -1101,8 +1152,8 @@ export function Files({
if (menu) setMenu({ x: menu.x, y: menu.y, sort: true });
},
},
];
return [
]);
return withFileIcons([
{
id: "upload",
label: "Upload files…",
Expand Down Expand Up @@ -1343,15 +1394,15 @@ export function Files({
run: () =>
setPreference("filesShowHidden", !preferences.filesShowHidden),
},
];
]);
}
function sortBy(key: typeof preferences.filesSort) {
if (key === preferences.filesSort)
setPreference("filesDescending", !preferences.filesDescending);
else setPreference("filesSort", key);
}
function sortActions(): MenuAction[] {
return [
return withFileIcons([
...(["name", "modified", "size"] as const).map((key) => ({
id: `sort-${key}`,
label: { name: "Name", modified: "Modified", size: "Size" }[key],
Expand Down Expand Up @@ -1385,7 +1436,7 @@ export function Files({
disabled: preferencesBlocked,
run: () => setPreference(key, !preferences[key]),
})),
];
]);
}
return (
<div
Expand Down
34 changes: 33 additions & 1 deletion src/apps/HostDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,36 @@
import type { AppContext, Capability } from "../sdk";
import { capabilityLabels, capabilityStatus } from "../sdk";
import { useState } from "react";
import {
ArrowDownToLine,
ArrowUpFromLine,
FilePenLine,
FilePlus2,
Files,
Folder,
FolderOpen,
FolderPlus,
Move,
Settings2,
SquareTerminal,
type LucideIcon,
} from "lucide-react";
import { RemoteSettings } from "../components/RemoteSettings";

const toolIcons: Record<Capability, LucideIcon> = {
terminal: SquareTerminal,
"files.read": FolderOpen,
"files.edit": FilePenLine,
"files.create": FilePlus2,
"files.manage": Folder,
"files.move": Move,
"files.copy": Files,
"files.folders": FolderPlus,
"files.upload": ArrowUpFromLine,
"files.download": ArrowDownToLine,
"host.settings": Settings2,
};

export function HostDetails(context: AppContext) {
const [tab, setTab] = useState<"overview" | "settings">("overview");
const [settingsOpened, setSettingsOpened] = useState(false);
Expand Down Expand Up @@ -76,13 +104,17 @@ function HostOverview({ session, preview, connected = true }: AppContext) {
aria-label="Workspace service availability"
>
{(Object.keys(capabilityLabels) as Capability[]).map((capability) => {
const Icon = toolIcons[capability];
const status = capabilityStatus(session, capability);
const state =
!connected && status.state === "available"
? "disconnected"
: status.state;
return (
<li key={capability} data-state={state}>
<span className="host-tool-icon" aria-hidden="true">
<Icon size={17} />
</span>
<div>
<strong>{capabilityLabels[capability]}</strong>
<small>
Expand All @@ -95,7 +127,7 @@ function HostOverview({ session, preview, connected = true }: AppContext) {
: state)}
</small>
</div>
<span>{state}</span>
<span className="host-tool-state">{state}</span>
</li>
);
})}
Expand Down
1 change: 1 addition & 0 deletions src/components/AppWindow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ export function AppWindow({
} as CSSProperties
}
className={`app-window window-${app.window?.layout ?? "standard"} ${focused ? "focused" : ""} ${maximized ? "maximized" : ""} ${tiled ? `tiled tiled-${tiled}` : ""} ${!visible ? "hidden-window" : ""}`}
data-app-id={app.id}
onPointerDownCapture={focus}
onFocusCapture={focus}
aria-label={`${title} window`}
Expand Down
75 changes: 43 additions & 32 deletions src/components/ContextMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
// SPDX-License-Identifier: MPL-2.0
import { useLayoutEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { Check } from "lucide-react";
import { Check, type LucideIcon } from "lucide-react";

export interface MenuAction {
id: string;
label: string;
icon?: LucideIcon;
shortcut?: string;
disabled?: boolean;
separatorBefore?: boolean;
Expand Down Expand Up @@ -103,37 +104,47 @@ export function ContextMenu({
role={group.name ? "group" : undefined}
aria-label={group.name}
>
{group.actions.map((action) => (
<button
role={
action.checked === undefined
? "menuitem"
: action.checkType === "radio"
? "menuitemradio"
: "menuitemcheckbox"
}
aria-checked={action.checked}
key={action.id}
disabled={action.disabled}
className={
action.separatorBefore ? "menu-group-start" : undefined
}
onClick={() => {
close();
action.run();
}}
>
<span className="context-menu-label">
{action.checked !== undefined && (
<span className="menu-check" aria-hidden="true">
{action.checked && <Check size={13} />}
</span>
)}
{action.label}
</span>
<kbd>{action.shortcut}</kbd>
</button>
))}
{group.actions.map((action) => {
const Icon = action.icon;
return (
<button
role={
action.checked === undefined
? "menuitem"
: action.checkType === "radio"
? "menuitemradio"
: "menuitemcheckbox"
}
aria-checked={action.checked}
key={action.id}
disabled={action.disabled}
className={
action.separatorBefore ? "menu-group-start" : undefined
}
onClick={() => {
close();
action.run();
}}
>
<span className="context-menu-label">
{action.checked !== undefined && (
<span className="menu-check" aria-hidden="true">
{action.checked && <Check size={13} />}
</span>
)}
{Icon && (
<Icon
className="menu-action-icon"
size={16}
aria-hidden="true"
/>
)}
{action.label}
</span>
<kbd>{action.shortcut}</kbd>
</button>
);
})}
</div>
))}
</div>,
Expand Down
42 changes: 28 additions & 14 deletions src/components/HostProfilePicker.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,15 @@
font-size: 0.769231rem;
margin-bottom: 0.538462rem;
}
.host-picker-row {
display: flex;
gap: 0.615385rem;
align-items: stretch;
}
.host-picker-trigger {
display: flex;
width: 100%;
flex: 1;
min-width: 0;
align-items: center;
gap: 0.846154rem;
padding: 0.846154rem 0.923077rem;
Expand All @@ -20,6 +26,25 @@
color: var(--sc-accent, #a2cbbc);
text-align: left;
}
.host-picker-new {
display: grid;
place-items: center;
width: 4rem;
flex: 0 0 4rem;
color: var(--sc-accent, #a2cbbc);
background: var(--sc-hover, #101f2b55);
border: 1px solid var(--sc-border, #aac5cd35);
border-radius: 0.615385rem;
}
.host-picker-new:hover,
.host-picker-trigger:hover {
background: var(--sc-selection, #a2cbbc16);
}
.host-picker-new:focus-visible,
.host-picker-trigger:focus-visible {
outline: 0.153846rem solid var(--sc-accent, #a2cbbc);
outline-offset: 0.153846rem;
}
.host-picker-trigger > span,
.host-picker-list button > span {
flex: 1;
Expand Down Expand Up @@ -54,7 +79,7 @@
border: 1px solid var(--sc-border, #9fbec644);
border-radius: 0.769231rem;
background: var(--sc-surface, #1a2d38);
box-shadow: 0 1.076923rem 2.923077rem rgba(var(--sc-shadow-rgb), 0.400);
box-shadow: 0 1.076923rem 2.923077rem rgba(var(--sc-shadow-rgb), 0.4);
overflow: hidden;
}
.host-picker-search {
Expand All @@ -76,7 +101,7 @@
outline: none;
}
.host-picker-search:focus-within {
box-shadow: inset 0 -0.153846rem rgba(var(--sc-shadow-rgb), 0.400);
box-shadow: inset 0 -0.153846rem rgba(var(--sc-shadow-rgb), 0.4);
}
.host-picker-list {
max-height: 16rem;
Expand Down Expand Up @@ -113,22 +138,11 @@
color: var(--sc-accent, #a2cbbc);
}
.host-picker-bottom {
display: flex;
align-items: center;
justify-content: space-between;
border-top: 1px solid var(--sc-border, #94b4bf22);
padding: 0.692308rem 0.923077rem;
color: var(--sc-muted, #8eaab8);
font-size: 0.692308rem;
}
.host-picker-bottom button {
display: flex;
gap: 0.384615rem;
align-items: center;
color: var(--sc-accent, #bbdecf);
font-size: 0.769231rem;
padding: 0.307692rem;
}
.host-picker-empty {
color: var(--sc-muted, #9fb9c5);
font-size: 0.846154rem;
Expand Down
Loading