quick and dirty key copy feature

This commit is contained in:
Chris Kruining 2025-01-08 10:20:50 +01:00
parent 9f0aa56361
commit 7b044c6050
No known key found for this signature in database
GPG key ID: EB894A3560CCCAD2
11 changed files with 43 additions and 32 deletions

BIN
bun.lockb

Binary file not shown.

View file

@ -1,6 +1,7 @@
{ {
"name": "calque", "name": "calque",
"dependencies": { "dependencies": {
"@solid-primitives/clipboard": "^1.5.10",
"@solid-primitives/i18n": "^2.1.1", "@solid-primitives/i18n": "^2.1.1",
"@solid-primitives/storage": "^4.2.1", "@solid-primitives/storage": "^4.2.1",
"@solidjs/meta": "^0.29.4", "@solidjs/meta": "^0.29.4",

View file

@ -29,7 +29,7 @@
white-space: nowrap; white-space: nowrap;
} }
& *:is(.cell:first-child, .checkbox + .cell) { & :is(.cell:first-child, .checkbox + .cell) {
position: sticky; position: sticky;
inset-inline-start: 1px; inset-inline-start: 1px;
padding-inline-start: calc(var(--depth, 0) * (1em + var(--padding-s)) + var(--padding-m)); padding-inline-start: calc(var(--depth, 0) * (1em + var(--padding-s)) + var(--padding-m));
@ -212,11 +212,6 @@
& :is(.cell:first-child, .checkbox + .cell) { & :is(.cell:first-child, .checkbox + .cell) {
inset-inline-start: 2em; inset-inline-start: 2em;
} }
& details > summary {
inset-inline-start: 2em;
grid-column: 2;
}
} }
} }

View file

@ -9,7 +9,7 @@ export enum Modifier {
} }
export interface CommandType<T extends (...args: any[]) => any = (...args: any[]) => any> { export interface CommandType<T extends (...args: any[]) => any = (...args: any[]) => any> {
(...args: Parameters<T>): Promise<ReturnType<T>>; (...args: Parameters<T>): (ReturnType<T> extends Promise<any> ? ReturnType<T> : Promise<ReturnType<T>>);
label: DictionaryKey; label: DictionaryKey;
shortcut?: { shortcut?: {
key: string; key: string;

View file

@ -106,7 +106,7 @@ const Add: Component<{ command: CommandType, commands: undefined } | { commands:
return undefined; return undefined;
}; };
const Context = <T extends (...args: any[]) => any = any>(props: ParentProps<{ for: CommandType<T>, with: Parameters<T> }>): JSX.Element => { const Context = <T extends (...args: any[]) => any = (...args: any[]) => any>(props: ParentProps<{ for: CommandType<T>, with: Parameters<T> }>): JSX.Element => {
const resolved = children(() => props.children); const resolved = children(() => props.children);
const context = useCommands(); const context = useCommands();
const args = createMemo(() => props.with); const args = createMemo(() => props.with);

View file

@ -1,27 +1,28 @@
import { Accessor, Component, createContext, createEffect, createMemo, createSignal, For, JSX, ParentComponent, splitProps, useContext } from "solid-js"; import { Accessor, Component, createContext, createEffect, createMemo, createSignal, For, JSX, ParentComponent, splitProps, useContext } from "solid-js";
import { CommandType } from "./command"; import { CommandType } from "./command";
import css from "./contextMenu.module.css"; import css from "./contextMenu.module.css";
import { useCommands } from "./context";
interface ContextMenuType { interface ContextMenuType {
readonly commands: Accessor<CommandType[]>; readonly commands: Accessor<CommandType[]>;
readonly target: Accessor<HTMLElement | undefined>; readonly event: Accessor<Event | undefined>;
show(element: HTMLElement): void; show(event: Event): void;
hide(): void; hide(): void;
} }
const ContextMenu = createContext<ContextMenuType>() const ContextMenu = createContext<ContextMenuType>()
const Root: ParentComponent<{ commands: CommandType[] }> = (props) => { const Root: ParentComponent<{ commands: CommandType[] }> = (props) => {
const [target, setTarget] = createSignal<HTMLElement>(); const [event, setEvent] = createSignal<Event>();
const context = { const context: ContextMenuType = {
commands: createMemo(() => props.commands), commands: createMemo(() => props.commands),
target, event,
show(element: HTMLElement) { show(event) {
setTarget(element); setEvent(event);
}, },
hide() { hide() {
setTarget(undefined); setEvent(undefined);
}, },
}; };
@ -32,17 +33,18 @@ const Root: ParentComponent<{ commands: CommandType[] }> = (props) => {
const Menu: Component<{ children: (command: CommandType) => JSX.Element }> = (props) => { const Menu: Component<{ children: (command: CommandType) => JSX.Element }> = (props) => {
const context = useContext(ContextMenu)!; const context = useContext(ContextMenu)!;
const commandContext = useCommands();
const [root, setRoot] = createSignal<HTMLElement>(); const [root, setRoot] = createSignal<HTMLElement>();
createEffect(() => { createEffect(() => {
const target = context.target(); const event = context.event();
const menu = root(); const menu = root();
if (!menu) { if (!menu) {
return; return;
} }
if (target) { if (event) {
menu.showPopover(); menu.showPopover();
} }
else { else {
@ -57,12 +59,11 @@ const Menu: Component<{ children: (command: CommandType) => JSX.Element }> = (pr
}; };
const onCommand = (command: CommandType) => (e: PointerEvent) => { const onCommand = (command: CommandType) => (e: PointerEvent) => {
commandContext?.execute(command, context.event()!);
context.hide(); context.hide();
command();
}; };
return <menu ref={setRoot} class={css.menu} style={`position-anchor: ${context.target()?.style.getPropertyValue('anchor-name')};`} popover ontoggle={onToggle}> return <menu ref={setRoot} class={css.menu} style={`position-anchor: ${context.event()?.target?.style.getPropertyValue('anchor-name')};`} popover ontoggle={onToggle}>
<For each={context.commands()}>{ <For each={context.commands()}>{
command => <li onpointerdown={onCommand(command)}>{props.children(command)}</li> command => <li onpointerdown={onCommand(command)}>{props.children(command)}</li>
}</For> }</For>
@ -73,12 +74,11 @@ const Handle: ParentComponent<Record<string, any>> = (props) => {
const [local, rest] = splitProps(props, ['children']); const [local, rest] = splitProps(props, ['children']);
const context = useContext(ContextMenu)!; const context = useContext(ContextMenu)!;
const [handle, setHandle] = createSignal<HTMLElement>();
return <span {...rest} ref={setHandle} style={`anchor-name: --context-menu-${createUniqueId()};`} oncontextmenu={(e) => { return <span {...rest} style={`anchor-name: --context-menu-${createUniqueId()};`} oncontextmenu={(e) => {
e.preventDefault(); e.preventDefault();
context.show(handle()!); context.show(e);
return false; return false;
}}>{local.children}</span>; }}>{local.children}</span>;

View file

@ -1,4 +1,4 @@
import { Accessor, Component, createEffect, createMemo, createSignal } from "solid-js"; import { Accessor, Component, createEffect, createMemo, createSignal, JSX } from "solid-js";
import { debounce, decode, Mutation } from "~/utilities"; import { debounce, decode, Mutation } from "~/utilities";
import { Column, GridApi as GridCompApi, Grid as GridComp } from "~/components/grid"; import { Column, GridApi as GridCompApi, Grid as GridComp } from "~/components/grid";
import { createDataSet, DataSetNode, DataSetRowNode } from "~/components/table"; import { createDataSet, DataSetNode, DataSetRowNode } from "~/components/table";
@ -29,7 +29,7 @@ const groupBy = (rows: DataSetRowNode<number, Entry>[]) => {
return group(rows.map<R>(r => ({ ...r, _key: r.value.key }))) as any; return group(rows.map<R>(r => ({ ...r, _key: r.value.key }))) as any;
} }
export function Grid(props: { class?: string, rows: Entry[], locales: string[], api?: (api: GridApi) => any }) { export function Grid(props: { class?: string, rows: Entry[], locales: string[], api?: (api: GridApi) => any, children?: (key: string) => JSX.Element }) {
const { t } = useI18n(); const { t } = useI18n();
const rows = createMemo(() => createDataSet<Entry>(props.rows, { group: { by: 'key', with: groupBy } })); const rows = createMemo(() => createDataSet<Entry>(props.rows, { group: { by: 'key', with: groupBy } }));
@ -38,7 +38,7 @@ export function Grid(props: { class?: string, rows: Entry[], locales: string[],
{ {
id: 'key', id: 'key',
label: t('feature.file.grid.key'), label: t('feature.file.grid.key'),
renderer: ({ value }) => value.split('.').at(-1), renderer: ({ value }) => props.children?.(value) ?? value.split('.').at(-1),
}, },
...locales().map<Column<Entry>>(lang => ({ ...locales().map<Column<Entry>>(lang => ({
id: lang, id: lang,

View file

@ -1,8 +1,8 @@
import { Accessor, createContext, createMemo, createSignal, ParentComponent, Setter, useContext } from 'solid-js'; import { Accessor, createContext, createMemo, createSignal, ParentComponent, Setter, useContext } from 'solid-js';
import { translator, flatten, Translator, Flatten } from "@solid-primitives/i18n"; import { translator, flatten, Translator, Flatten } from "@solid-primitives/i18n";
import { makePersisted } from '@solid-primitives/storage';
import en from '~/i18n/en-GB.json'; import en from '~/i18n/en-GB.json';
import nl from '~/i18n/nl-NL.json'; import nl from '~/i18n/nl-NL.json';
import { makePersisted } from '@solid-primitives/storage';
type RawDictionary = typeof en; type RawDictionary = typeof en;
export type Dictionary = Flatten<RawDictionary>; export type Dictionary = Flatten<RawDictionary>;

View file

@ -28,7 +28,8 @@
"clearSelection": "Clear selection", "clearSelection": "Clear selection",
"insertKey": "Insert new key", "insertKey": "Insert new key",
"insertLanguage": "Insert new language", "insertLanguage": "Insert new language",
"delete": "Delete selected items" "delete": "Delete selected items",
"copyKey": "Copy key"
}, },
"prompt": { "prompt": {
"newKey": { "newKey": {

View file

@ -28,7 +28,8 @@
"clearSelection": "Selectie leeg maken", "clearSelection": "Selectie leeg maken",
"insertKey": "Voeg nieuwe sleutel toe", "insertKey": "Voeg nieuwe sleutel toe",
"insertLanguage": "Voeg nieuwe taal toe", "insertLanguage": "Voeg nieuwe taal toe",
"delete": "Verwijder geselecteerde items" "delete": "Verwijder geselecteerde items",
"copyKey": "Kopieer sleutel"
}, },
"prompt": { "prompt": {
"newKey": { "newKey": {
@ -50,4 +51,4 @@
} }
} }
} }
} }

View file

@ -12,6 +12,7 @@ import { Prompt, PromptApi } from "~/components/prompt";
import EditBlankImage from '~/assets/edit-blank.svg' import EditBlankImage from '~/assets/edit-blank.svg'
import { useI18n } from "~/features/i18n"; import { useI18n } from "~/features/i18n";
import { makePersisted } from "@solid-primitives/storage"; import { makePersisted } from "@solid-primitives/storage";
import { writeClipboard } from "@solid-primitives/clipboard";
import css from "./edit.module.css"; import css from "./edit.module.css";
const isInstalledPWA = !isServer && window.matchMedia('(display-mode: standalone)').matches; const isInstalledPWA = !isServer && window.matchMedia('(display-mode: standalone)').matches;
@ -449,7 +450,19 @@ const Content: Component<{ directory: FileSystemDirectoryHandle, api?: Setter<(G
})(); })();
}); });
return <Grid rows={rows()} locales={locales()} api={setApi} />; const copyKey = createCommand('page.edit.command.copyKey', (key: string) => writeClipboard(key));
return <Grid rows={rows()} locales={locales()} api={setApi}>{
key => {
return <Context.Root commands={[copyKey.with(key)]}>
<Context.Menu>{
command => <Command.Handle command={command} />
}</Context.Menu>
<Context.Handle>{key.split('.').at(-1)!}</Context.Handle>
</Context.Root>;
}
}</Grid>;
}; };
const Blank: Component<{ open: CommandType }> = (props) => { const Blank: Component<{ open: CommandType }> = (props) => {