From 7db65413be555167aa42f682243a737bfd784653 Mon Sep 17 00:00:00 2001 From: Chris Kruining Date: Wed, 16 Oct 2024 16:13:12 +0200 Subject: [PATCH] polished the UI a bit. next up: saving changes (and maybe a diff ui as confirmation dialog) --- src/features/command/contextMenu.module.css | 15 ++++ src/features/command/contextMenu.tsx | 84 +++++++++++++++++++++ src/features/command/index.ts | 33 -------- src/features/command/index.tsx | 53 +++++++++++++ src/features/file/grid.tsx | 54 +++++++++---- src/features/menu/index.module.css | 58 ++++++++++++++ src/features/menu/index.tsx | 68 ++++++++--------- src/features/selectable/index.tsx | 2 - src/routes/(editor)/edit.tsx | 73 +++++++++++------- 9 files changed, 323 insertions(+), 117 deletions(-) create mode 100644 src/features/command/contextMenu.module.css create mode 100644 src/features/command/contextMenu.tsx delete mode 100644 src/features/command/index.ts create mode 100644 src/features/command/index.tsx create mode 100644 src/features/menu/index.module.css diff --git a/src/features/command/contextMenu.module.css b/src/features/command/contextMenu.module.css new file mode 100644 index 0000000..f2c38f4 --- /dev/null +++ b/src/features/command/contextMenu.module.css @@ -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); +} \ No newline at end of file diff --git a/src/features/command/contextMenu.tsx b/src/features/command/contextMenu.tsx new file mode 100644 index 0000000..752173e --- /dev/null +++ b/src/features/command/contextMenu.tsx @@ -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; + readonly target: Accessor; + show(element: HTMLElement): void; + hide(): void; +} + +const ContextMenu = createContext() + +const Root: ParentComponent<{ commands: CommandType[] }> = (props) => { + const [target, setTarget] = createSignal(); + + const context = { + commands: createMemo(() => props.commands), + target, + show(element: HTMLElement) { + setTarget(element); + }, + hide() { + setTarget(undefined); + }, + }; + + return + {props.children} + +}; + +const Menu: Component<{ children: (command: CommandType) => JSX.Element }> = (props) => { + const context = useContext(ContextMenu)!; + const [root, setRoot] = createSignal(); + + 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
    + { + command =>
  • {props.children(command)}
  • + }
    +
; +}; + +const Handle: ParentComponent = (props) => { + const context = useContext(ContextMenu)!; + + return { + e.preventDefault(); + + context.show(e.target as HTMLElement); + + return false; + }}>{props.children}; +}; + +export const Context = { Root, Menu, Handle }; \ No newline at end of file diff --git a/src/features/command/index.ts b/src/features/command/index.ts deleted file mode 100644 index affc14d..0000000 --- a/src/features/command/index.ts +++ /dev/null @@ -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', () => { }); \ No newline at end of file diff --git a/src/features/command/index.tsx b/src/features/command/index.tsx new file mode 100644 index 0000000..587ca88 --- /dev/null +++ b/src/features/command/index.tsx @@ -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} + { + 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 {ctrl}{shift}{meta}{alt}{shortcut().key}; + } + } + ; +}; + +export { Context } from './contextMenu'; \ No newline at end of file diff --git a/src/features/file/grid.tsx b/src/features/file/grid.tsx index 910ebc7..bdddc3a 100644 --- a/src/features/file/grid.tsx +++ b/src/features/file/grid.tsx @@ -20,20 +20,28 @@ const debounce = void>(callback: T, delay: number interface Leaf extends Record { } export interface Entry extends Record { } +type Rows = Record; + export interface GridContextType { - rows: Record; - selection: Accessor; + readonly rows: Accessor; + readonly selection: Accessor; mutate(prop: string, lang: string, value: string): void; } +export interface GridApi { + readonly rows: Accessor; + selectAll(): void; + clear(): void; +} + const GridContext = createContext(); const isLeaf = (entry: Entry | Leaf): entry is Leaf => Object.values(entry).some(v => typeof v === 'string'); const useGrid = () => useContext(GridContext)!; -const GridProvider: ParentComponent<{ rows: Map, context?: (ctx: GridContextType) => any }> = (props) => { +const GridProvider: ParentComponent<{ rows: Map }> = (props) => { const [selection, setSelection] = createSignal([]); - const [state, setState] = createStore<{ rows: GridContextType['rows'], numberOfRows: number }>({ + const [state, setState] = createStore<{ rows: Rows, numberOfRows: number }>({ rows: {}, numberOfRows: 0, }); @@ -46,13 +54,12 @@ const GridProvider: ParentComponent<{ rows: Map { setState('numberOfRows', Object.keys(state.rows).length); }); const ctx: GridContextType = { - rows: state.rows, + rows: createMemo(() => state.rows), selection, mutate(prop: string, lang: string, value: string) { @@ -62,16 +69,8 @@ const GridProvider: ParentComponent<{ rows: Map { - props.context?.(ctx); - }); - const mutated = createMemo(() => Object.values(state.rows).filter(entry => Object.values(entry).some(lang => lang.original !== lang.value))); - createEffect(() => { - console.log('tap', mutated()); - }); - return {props.children} @@ -79,7 +78,7 @@ const GridProvider: ParentComponent<{ rows: Map; }; -export const Grid: Component<{ class?: string, columns: string[], rows: Map, context?: (ctx: GridContextType) => any }> = (props) => { +export const Grid: Component<{ class?: string, columns: string[], rows: Map, api?: (api: GridApi) => any }> = (props) => { const columnCount = createMemo(() => props.columns.length - 1); const root = createMemo(() => { return props.rows @@ -107,7 +106,9 @@ export const Grid: Component<{ class?: string, columns: string[], rows: Map - + + +
@@ -117,6 +118,27 @@ export const Grid: Component<{ class?: string, columns: string[], rows: Map }; +const Api: Component<{ api: undefined | ((api: GridApi) => any) }> = (props) => { + const gridContext = useGrid(); + const selectionContext = useSelection(); + + const api: GridApi = { + rows: gridContext.rows, + selectAll() { + selectionContext.selectAll(); + }, + clear() { + selectionContext.clear(); + }, + }; + + createEffect(() => { + props.api?.(api); + }); + + return null; +}; + const Head: Component<{ headers: string[] }> = (props) => { const context = useSelection(); diff --git a/src/features/menu/index.module.css b/src/features/menu/index.module.css new file mode 100644 index 0000000..6638b08 --- /dev/null +++ b/src/features/menu/index.module.css @@ -0,0 +1,58 @@ +.item { + padding: .5em 1em; + + background-color: inherit; + color: var(--text-1); + border: none; + cursor: pointer; + + text-align: start; + + &:hover { + background-color: var(--surface-2); + } +} + +.child { + position: fixed; + inset-inline-start: anchor(self-start); + inset-block-start: anchor(end); + + grid-template-columns: auto auto; + place-content: start; + + gap: .5em; + padding: .5em 0; + inline-size: max-content; + + background-color: var(--surface-2); + border: 1px solid var(--surface-3); + border-block-start-width: 0; + margin: unset; + + &:popover-open { + display: grid; + } + + & > .item { + grid-column: span 2; + display: grid; + grid-template-columns: subgrid; + align-items: center; + + background-color: var(--surface-2); + + &:hover { + background-color: var(--surface-3); + } + + & > sub { + color: var(--text-2); + text-align: end; + } + } +} + +:popover-open + .item { + background-color: var(--surface-2); +} \ No newline at end of file diff --git a/src/features/menu/index.tsx b/src/features/menu/index.tsx index 55a784b..ae6c474 100644 --- a/src/features/menu/index.tsx +++ b/src/features/menu/index.tsx @@ -1,7 +1,8 @@ -import { Accessor, Component, For, JSX, ParentComponent, Setter, Show, children, createContext, createEffect, createMemo, createSignal, createUniqueId, mergeProps, onCleanup, onMount, splitProps, useContext } from "solid-js"; -import { Portal, isServer } from "solid-js/web"; +import { Accessor, Component, For, JSX, Match, ParentComponent, Setter, Show, Switch, children, createContext, createEffect, createMemo, createSignal, createUniqueId, mergeProps, onCleanup, onMount, splitProps, useContext } from "solid-js"; +import { Portal } from "solid-js/web"; import { createStore } from "solid-js/store"; -import { Command, Modifier } from "../command"; +import { CommandType, Command } from "../command"; +import css from "./index.module.css"; export interface MenuContextType { ref: Accessor; @@ -9,13 +10,13 @@ export interface MenuContextType { addItems(items: (Item | ItemWithChildren)[]): void; items: Accessor<(Item | ItemWithChildren)[]>; - commands(): Command[]; + commands(): CommandType[]; }; export interface Item { id: string; label: string; - command: Command; + command: CommandType; } export interface ItemWithChildren { @@ -55,7 +56,7 @@ const useMenu = () => { return context; } -type ItemProps = { label: string, children: JSX.Element } | { label: string, command: Command }; +type ItemProps = { label: string, children: JSX.Element } | { command: CommandType }; const Item: Component = (props) => { const id = createUniqueId(); @@ -77,7 +78,7 @@ const Item: Component = (props) => { const Root: ParentComponent<{}> = (props) => { const menu = useMenu(); const [current, setCurrent] = createSignal(); - const items = children(() => props.children).toArray() as unknown as (Item & ItemWithChildren)[]; + const items = children(() => props.children).toArray() as unknown as (Item | ItemWithChildren)[]; menu.addItems(items) @@ -91,7 +92,7 @@ const Root: ParentComponent<{}> = (props) => { } }; - const onExecute = (command?: Command) => { + const onExecute = (command?: CommandType) => { return command ? async () => { await command?.(); @@ -101,31 +102,20 @@ const Root: ParentComponent<{}> = (props) => { : () => { } }; - const Button: Component<{ label: string, command?: Command } & { [key: string]: any }> = (props) => { - const [local, rest] = splitProps(props, ['label', 'command']); - return ; + const Child: Component<{ command: CommandType }> = (props) => { + return }; return { - item => <> - { - children =>