Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion apps/electron-client/src/renderer/components/Highlight.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const Highlight = ({ children, className }: HighlightProps) => {
if (ref.current) {
hljs.highlightElement(ref.current);
}
}, [children]);
}, []);

return (
<code ref={ref} className={className}>
Expand Down
145 changes: 116 additions & 29 deletions apps/electron-client/src/renderer/components/List/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { Keyboard } from "@raycast/api";
import * as Popover from "@radix-ui/react-popover";
import type { Image, Keyboard, List as List_1 } from "@raycast/api";
import { Command } from "cmdk";
import React from "react";
import React, { useMemo, type ComponentProps } from "react";
import type { Client } from "rpc-websockets";
import { useShallow } from "zustand/react/shallow";

import type { ObjectFromList } from "../../lib/typeUtils";
import { useBlastUIStore, useRemoteBlastTree } from "../../store";
Expand All @@ -12,21 +14,42 @@ import { useNavigationContext } from "../Navigation/context";
import { EmptyView } from "./EmptyView";
import { ListFooter } from "./ListFooter";

const getIconComponent = (icon: string) => {
const Icon = Icons[icon as keyof typeof Icons] as () => JSX.Element;
const IconComp = ({ icon }: { icon: List_1.Item.Props["icon"] }) => {
if (typeof icon === "string") {
const Icon = Icons[icon as keyof typeof Icons] as () => JSX.Element;

if (!Icon) {
console.warn(`Icon ${icon} not found`);
return null;
if (!Icon) {
console.warn(`Icon ${JSON.stringify(icon)} not found`);
return null;
}

return <Icon />;
}

if ((icon as Image)?.source) {
const source = (icon as Image)?.source;
if (typeof source === "string" && source.startsWith("data:image/svg+xml,")) {
// Split the source into prefix and SVG markup.
const [prefix, svg] = source.split(/,(.+)/); // split into two parts; the regex keeps the rest intact
// Encode the SVG markup.
const encodedSvg = encodeURIComponent(svg);
// Reassemble the data URL.
const encodedSource = `${prefix},${encodedSvg}`;

return (
<div className="w-5 h-5 text-center align-middle">
<img src={encodedSource} alt="" />
</div>
);
}
}

return Icon;
return null;
};

const serializedKeys = [
// navigation props
"navigationTitle",
"isLoading",

// search bar props
"filtering",
Expand Down Expand Up @@ -71,8 +94,6 @@ const Action = ({ action, ws, close }: { action: BlastComponent; ws: Client; clo
props: { shortcut, actionEventName, title, icon },
} = action;

const Icon = getIconComponent(icon);

return (
<SubItem
shortcut={shortcut ? renderShortcutToString(shortcut) : keyToSymbol.enter}
Expand All @@ -81,7 +102,7 @@ const Action = ({ action, ws, close }: { action: BlastComponent; ws: Client; clo
ws.call(actionEventName);
close();
}}
icon={Icon && <Icon />}
icon={<IconComp icon={icon} />}
>
{title}
</SubItem>
Expand Down Expand Up @@ -165,6 +186,66 @@ export function getListIndexFromValue(value: string) {
return Number.parseInt(value.replace("listitem-", ""), 10);
}

function ListDropdown(props: {
tooltip: string;
isLoading: boolean;
throttle: boolean;
value: string;
placeholder: string;
searchTextValue: string;
onChangeEventName: string;
onSearchTextChangeEventName: string;
children: BlastComponent[];
}) {
const { ws } = useRemoteBlastTree();

const uiStore = useBlastUIStore(
useShallow((state) => ({
open: state.dropdownOpen,
setOpen: state.setDropdownOpen,
}))
);
const items = props?.children?.filter((child) => child.elementType === "DropdownItem");
const onSelect = (v: string) => {
ws.call(props.onChangeEventName, {
value: v,
});
uiStore.setOpen(false);
};

const selectedTitle = useMemo(() => {
return items.find(item => item.props.value === props.value)?.props.title
}, [items, props.value])

return (
<Popover.Root open={uiStore.open} onOpenChange={uiStore.setOpen} modal>
<Popover.Trigger
cmdk-raycast-subcommand-trigger=""
onClick={() => uiStore.setOpen(true)}
aria-expanded={uiStore.open}
className="border rounded border-gray-500 w-[250px] h-full mt-4"
>
<span className="text-white">
{selectedTitle}
</span>
</Popover.Trigger>
<Popover.Content side="bottom" align="end" className="raycast-submenu z-10" sideOffset={16} alignOffset={0}>
<Command>
<Command.Input placeholder={props.placeholder} />

<Command.List className="max-h-[270px]">
{items.map((item) => (
<Command.Item key={item.props.value} onSelect={onSelect} value={item.props.value}>
{item.props.title}
</Command.Item>
))}
</Command.List>
</Command>
</Popover.Content>
</Popover.Root>
);
}

export const List = ({ children, props }: { children: BlastComponent[]; props: ListProps }): JSX.Element => {
const listItems = children.filter((child) => child.elementType === "ListItem");
const emptyView = children.find((child) => child.elementType === "EmptyView");
Expand All @@ -174,10 +255,12 @@ export const List = ({ children, props }: { children: BlastComponent[]; props: L
? emptyView.children.find((child) => child.elementType === "ActionPanel")
: null;

const dropdownElement = children.find((child) => child.elementType === "Dropdown");

const listRef = React.useRef(null);
const [value, setValue] = React.useState(getListItemValue(0));
const inputRef = React.useRef<HTMLInputElement | null>(null);
const isSubCommandOpen = useBlastUIStore((state) => state.subcommandOpen);
const isSomethingOpen = useBlastUIStore((state) => state.subcommandOpen || state.dropdownOpen);
const { ws } = useRemoteBlastTree();

const handleKeyDown = (e: React.KeyboardEvent) => {
Expand All @@ -188,7 +271,7 @@ export const List = ({ children, props }: { children: BlastComponent[]; props: L
e.preventDefault();
if (inputRef.current.value) {
inputRef.current.value = "";
} else if (!isSubCommandOpen) {
} else if (!isSomethingOpen) {
pop();
}
} else if (e.key === "Backspace" && !inputRef.current.value) {
Expand All @@ -209,8 +292,7 @@ export const List = ({ children, props }: { children: BlastComponent[]; props: L
matchedAction = actionData.children.find((action) => {
if (action.props?.shortcut) {
const shortcut = action.props.shortcut;
const keyMatches =
e.key.toLowerCase() === shortcut.key.toLowerCase();
const keyMatches = e.key.toLowerCase() === shortcut.key.toLowerCase();
const requiredModifiers = shortcut.modifiers || [];
const modifiersMatch = requiredModifiers.every((mod: string) => {
if (mod === "ctrl") return e.ctrlKey;
Expand Down Expand Up @@ -242,20 +324,26 @@ export const List = ({ children, props }: { children: BlastComponent[]; props: L

return (
<div className="h-full raycast drag-area">
<Command
value={value}
onValueChange={(v) => setValue(v)}
onKeyDown={handleKeyDown}
>
<Command value={value} onValueChange={(v) => setValue(v)} onKeyDown={handleKeyDown}>
<div className="absolute top-0 left-0 w-full h-2 drag-area" />

<div cmdk-raycast-top-shine="" />
<Command.Input
autoFocus
ref={inputRef}
style={{ paddingTop: 16 }}
placeholder={props.searchBarPlaceholder || "Search..."}
/>

<div className="flex pr-4">
<Command.Input
autoFocus
ref={inputRef}
style={{ paddingTop: 16 }}
placeholder={props.searchBarPlaceholder || "Search..."}
/>

{dropdownElement && (
<ListDropdown {...(dropdownElement.props as ComponentProps<typeof ListDropdown>)}>
{dropdownElement.children}
</ListDropdown>
)}
</div>

<hr cmdk-raycast-loader="" />

<Command.List ref={listRef}>
Expand All @@ -276,11 +364,10 @@ export const List = ({ children, props }: { children: BlastComponent[]; props: L
} = listItem;

const value = getListItemValue(index);
const Icon = getIconComponent(icon);

return (
<Command.Item key={value} value={value}>
{icon && <Icon />}
<IconComp icon={icon} />

{title}
</Command.Item>
Expand Down
8 changes: 6 additions & 2 deletions apps/electron-client/src/renderer/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@ export const useRemoteBlastTree = () => useStore(remoteBlastTree);

type BlastUIState = {
subcommandOpen: boolean,
setSubcommandOpen: (open: boolean) => void
setSubcommandOpen: (open: boolean) => void,
dropdownOpen: boolean,
setDropdownOpen: (open: boolean) => void,
}

export const useBlastUIStore = create<BlastUIState>((set) => ({
subcommandOpen: false,
setSubcommandOpen: (open: boolean) => set({ subcommandOpen: open })
setSubcommandOpen: (open: boolean) => set({ subcommandOpen: open }),
dropdownOpen: false,
setDropdownOpen: (open: boolean) => set({ dropdownOpen: open }),
}))
2 changes: 2 additions & 0 deletions packages/blast-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
"dependencies": {
"@blastlauncher/renderer": "workspace:*",
"@blastlauncher/utils": "workspace:*",
"clipboardy": "^4.0.0",
"fs-extra": "^11.1.0",
"open": "^10.1.0",
"react": "^18.2.0"
},
"devDependencies": {
Expand Down
6 changes: 5 additions & 1 deletion packages/blast-api/src/@types/reactAst.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
declare namespace JSX {
import type { ActionPanel, Action, List, Detail, Form } from "raycast-original";
import type { ActionPanel, Action, List, Detail, Form, Grid } from "raycast-original";

type BlastNodeProps = {
serializedKeys?: string[];
Expand Down Expand Up @@ -32,5 +32,9 @@ declare namespace JSX {
children?: React.ReactNode;
stacksLength?: number;
} & BlastNodeProps;

Dropdown: List.Dropdown.Props & Grid.Dropdown.Props & Form.Dropdown.Props & BlastNodeProps;
DropdownSection: List.Dropdown.Section.Props & Grid.Dropdown.Section.Props & Form.Dropdown.Section.Props & BlastNodeProps;
DropdownItem: List.Dropdown.Item.Props & Grid.Dropdown.Item.Props & Form.Dropdown.Item.Props & BlastNodeProps;
}
}
Loading
Loading