polished the UI a bit. next up: saving changes (and maybe a diff ui as confirmation dialog)
This commit is contained in:
parent
1a963a665e
commit
7db65413be
9 changed files with 323 additions and 117 deletions
15
src/features/command/contextMenu.module.css
Normal file
15
src/features/command/contextMenu.module.css
Normal file
|
@ -0,0 +1,15 @@
|
|||
.menu {
|
||||
position: fixed;
|
||||
display: grid;
|
||||
grid-template-columns: max-content;
|
||||
|
||||
inset-inline-start: anchor(start);
|
||||
inset-block-start: anchor(end);
|
||||
margin: 0;
|
||||
|
||||
background-color: var(--surface-1);
|
||||
color: var(--text-1);
|
||||
border: none;
|
||||
|
||||
padding: var(--padding-m);
|
||||
}
|
84
src/features/command/contextMenu.tsx
Normal file
84
src/features/command/contextMenu.tsx
Normal file
|
@ -0,0 +1,84 @@
|
|||
import { Accessor, Component, createContext, createEffect, createMemo, createSignal, createUniqueId, For, JSX, ParentComponent, useContext } from "solid-js";
|
||||
import { CommandType } from "./index";
|
||||
import css from "./contextMenu.module.css";
|
||||
|
||||
interface ContextMenuType {
|
||||
readonly commands: Accessor<CommandType[]>;
|
||||
readonly target: Accessor<HTMLElement | undefined>;
|
||||
show(element: HTMLElement): void;
|
||||
hide(): void;
|
||||
}
|
||||
|
||||
const ContextMenu = createContext<ContextMenuType>()
|
||||
|
||||
const Root: ParentComponent<{ commands: CommandType[] }> = (props) => {
|
||||
const [target, setTarget] = createSignal<HTMLElement>();
|
||||
|
||||
const context = {
|
||||
commands: createMemo(() => props.commands),
|
||||
target,
|
||||
show(element: HTMLElement) {
|
||||
setTarget(element);
|
||||
},
|
||||
hide() {
|
||||
setTarget(undefined);
|
||||
},
|
||||
};
|
||||
|
||||
return <ContextMenu.Provider value={context}>
|
||||
{props.children}
|
||||
</ContextMenu.Provider>
|
||||
};
|
||||
|
||||
const Menu: Component<{ children: (command: CommandType) => JSX.Element }> = (props) => {
|
||||
const context = useContext(ContextMenu)!;
|
||||
const [root, setRoot] = createSignal<HTMLElement>();
|
||||
|
||||
createEffect(() => {
|
||||
const target = context.target();
|
||||
const menu = root();
|
||||
|
||||
if (!menu) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (target) {
|
||||
menu.showPopover();
|
||||
}
|
||||
else {
|
||||
menu.hidePopover();
|
||||
}
|
||||
});
|
||||
|
||||
const onToggle = (e: ToggleEvent) => {
|
||||
if (e.newState === 'closed') {
|
||||
context.hide();
|
||||
}
|
||||
};
|
||||
|
||||
const onCommand = (command: CommandType) => (e: PointerEvent) => {
|
||||
context.hide();
|
||||
|
||||
command();
|
||||
};
|
||||
|
||||
return <ul ref={setRoot} class={css.menu} style={`position-anchor: ${context.target()?.style.getPropertyValue('anchor-name')};`} popover ontoggle={onToggle}>
|
||||
<For each={context.commands()}>{
|
||||
command => <li onpointerdown={onCommand(command)}>{props.children(command)}</li>
|
||||
}</For>
|
||||
</ul>;
|
||||
};
|
||||
|
||||
const Handle: ParentComponent = (props) => {
|
||||
const context = useContext(ContextMenu)!;
|
||||
|
||||
return <span style={`anchor-name: --context-menu-handle-${createUniqueId()};`} oncontextmenu={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
context.show(e.target as HTMLElement);
|
||||
|
||||
return false;
|
||||
}}>{props.children}</span>;
|
||||
};
|
||||
|
||||
export const Context = { Root, Menu, Handle };
|
|
@ -1,33 +0,0 @@
|
|||
export enum Modifier {
|
||||
None = 0,
|
||||
Shift = 1 << 0,
|
||||
Control = 1 << 1,
|
||||
Meta = 1 << 2,
|
||||
Alt = 1 << 3,
|
||||
}
|
||||
|
||||
export interface Command {
|
||||
(): any;
|
||||
label: string;
|
||||
shortcut?: {
|
||||
key: string;
|
||||
modifier: Modifier;
|
||||
};
|
||||
}
|
||||
|
||||
export const createCommand = (label: string, command: () => any, shortcut?: Command['shortcut']): Command => {
|
||||
return Object.defineProperties(command as Command, {
|
||||
label: {
|
||||
value: label,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
},
|
||||
shortcut: {
|
||||
value: shortcut ? { key: shortcut.key.toLowerCase(), modifier: shortcut.modifier } : undefined,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const noop = createCommand('noop', () => { });
|
53
src/features/command/index.tsx
Normal file
53
src/features/command/index.tsx
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { Component, Show } from 'solid-js';
|
||||
|
||||
export enum Modifier {
|
||||
None = 0,
|
||||
Shift = 1 << 0,
|
||||
Control = 1 << 1,
|
||||
Meta = 1 << 2,
|
||||
Alt = 1 << 3,
|
||||
}
|
||||
|
||||
export interface CommandType {
|
||||
(): any;
|
||||
label: string;
|
||||
shortcut?: {
|
||||
key: string;
|
||||
modifier: Modifier;
|
||||
};
|
||||
}
|
||||
|
||||
export const createCommand = (label: string, command: () => any, shortcut?: CommandType['shortcut']): CommandType => {
|
||||
return Object.defineProperties(command as CommandType, {
|
||||
label: {
|
||||
value: label,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
},
|
||||
shortcut: {
|
||||
value: shortcut ? { key: shortcut.key.toLowerCase(), modifier: shortcut.modifier } : undefined,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const noop = createCommand('noop', () => { });
|
||||
|
||||
export const Command: Component<{ command: CommandType }> = (props) => {
|
||||
return <>
|
||||
{props.command.label}
|
||||
<Show when={props.command.shortcut}>{
|
||||
shortcut => {
|
||||
const shift = shortcut().modifier & Modifier.Shift ? 'Shft+' : '';
|
||||
const ctrl = shortcut().modifier & Modifier.Control ? 'Ctrl+' : '';
|
||||
const meta = shortcut().modifier & Modifier.Meta ? 'Meta+' : '';
|
||||
const alt = shortcut().modifier & Modifier.Alt ? 'Alt+' : '';
|
||||
|
||||
return <sub>{ctrl}{shift}{meta}{alt}{shortcut().key}</sub>;
|
||||
}
|
||||
}</Show>
|
||||
</>;
|
||||
};
|
||||
|
||||
export { Context } from './contextMenu';
|
Loading…
Add table
Add a link
Reference in a new issue