diff --git a/public/images/favicon.dark.svg b/public/images/favicon.dark.svg new file mode 100644 index 0000000..6ac344c --- /dev/null +++ b/public/images/favicon.dark.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/images/favicon.light.svg b/public/images/favicon.light.svg new file mode 100644 index 0000000..3f2ef26 --- /dev/null +++ b/public/images/favicon.light.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/images/screenshots/narrow.png b/public/images/screenshots/narrow.png new file mode 100644 index 0000000..286d3a6 Binary files /dev/null and b/public/images/screenshots/narrow.png differ diff --git a/public/images/screenshots/wide.png b/public/images/screenshots/wide.png new file mode 100644 index 0000000..a3338ef Binary files /dev/null and b/public/images/screenshots/wide.png differ diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..491b2ca --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,33 @@ +{ + "short_name": "T-Tool", + "name": "Translation Tool", + "description": "Simple tool to help with maitaining i18n files", + "icons": [ + { + "src": "/images/favicon.dark.svg", + "type": "image/svg+xml", + "sizes": "any" + } + ], + "start_url": ".", + "display": "standalone", + "display_override": [ + "window-controls-overlay" + ], + "theme_color": "#222", + "background_color": "#222", + "screenshots": [ + { + "src": "/images/screenshots/narrow.png", + "type": "image/png", + "sizes": "538x1133", + "form_factor": "narrow" + }, + { + "src": "/images/screenshots/wide.png", + "type": "image/png", + "sizes": "2092x1295", + "form_factor": "wide" + } + ] +} \ No newline at end of file diff --git a/src/components/sidebar.module.css b/src/components/sidebar.module.css index 903f28b..69eee98 100644 --- a/src/components/sidebar.module.css +++ b/src/components/sidebar.module.css @@ -3,6 +3,10 @@ grid: auto 1fr / 100%; & > button { + display: flex; + flex-flow: row; + gap: var(--padding-s); + align-items: center; inline-size: max-content; padding: 0; background-color: var(--surface-1); diff --git a/src/components/tabs.module.css b/src/components/tabs.module.css index 94ee47a..b36c17f 100644 --- a/src/components/tabs.module.css +++ b/src/components/tabs.module.css @@ -14,13 +14,32 @@ border-block-end: 1px solid var(--surface-5); - & > button { + & > .handle { + display: grid; + grid-auto-flow: column; + column-gap: var(--padding-m); + background-color: var(--surface-1); color: var(--text-2); - padding: var(--padding-m) var(--padding-l); - border: none; cursor: pointer; + & > button { + display: grid; + align-content: center; + background-color: inherit; + color: inherit; + padding: var(--padding-m) 0; + border: none; + + &:first-child { + padding-inline-start: var(--padding-l); + } + + &:last-child { + padding-inline-end: var(--padding-l); + } + } + &.active { background-color: var(--surface-3); color: var(--text-1); diff --git a/src/components/tabs.tsx b/src/components/tabs.tsx index 0a77f4e..f27601a 100644 --- a/src/components/tabs.tsx +++ b/src/components/tabs.tsx @@ -1,8 +1,17 @@ -import { Accessor, children, createContext, createEffect, createMemo, createRenderEffect, createSignal, createUniqueId, For, JSX, onMount, ParentComponent, Setter, Show, useContext } from "solid-js"; +import { Accessor, children, createContext, createEffect, createMemo, createSignal, For, onCleanup, ParentComponent, Setter, Show, useContext } from "solid-js"; +import { IoCloseCircleOutline } from "solid-icons/io"; import css from "./tabs.module.css"; +import { Command, CommandType, commandArguments, noop, useCommands } from "~/features/command"; + +commandArguments; interface TabsContextType { - register(id: string, label: string): Accessor; + register(id: string, label: string, options?: Partial): Accessor; + readonly onClose: Accessor | undefined> +} + +interface TabOptions { + closable: boolean; } const TabsContext = createContext(); @@ -17,9 +26,10 @@ const useTabs = () => { return context!; } -export const Tabs: ParentComponent<{ active?: Setter }> = (props) => { +export const Tabs: ParentComponent<{ active?: Setter, onClose?: CommandType<[string]> }> = (props) => { + const commandsContext = useCommands(); const [active, setActive] = createSignal(undefined); - const [tabs, setTabs] = createSignal>(new Map()); + const [tabs, setTabs] = createSignal }>>(new Map()); createEffect(() => { props.active?.(active()); @@ -30,35 +40,53 @@ export const Tabs: ParentComponent<{ active?: Setter }> = (p }); const ctx = { - register(id: string, label: string) { + register(id: string, label: string, options: Partial) { setTabs(tabs => { - tabs.set(id, label); + tabs.set(id, { label, options }); return new Map(tabs); }); return createMemo(() => active() === id); }, + onClose: createMemo(() => props.onClose), + }; + + const onClose = (e: Event) => { + if (!commandsContext || !props.onClose) { + return; + } + + return commandsContext.execute(props.onClose, e); }; return
{ - ([id, label]) => + ([id, { label, options: { closable = false } }]) => + + + + + + + }
{props.children}
-
; + ; } -export const Tab: ParentComponent<{ id: string, label: string }> = (props) => { +export const Tab: ParentComponent<{ id: string, label: string, closable?: boolean }> = (props) => { const context = useTabs(); - const isActive = context.register(props.id, props.label); + const isActive = context.register(props.id, props.label, { + closable: props.closable ?? false + }); const resolved = children(() => props.children); - return {resolved()}; + return {resolved()}; } \ No newline at end of file diff --git a/src/entry-server.tsx b/src/entry-server.tsx index f66a790..551cb37 100644 --- a/src/entry-server.tsx +++ b/src/entry-server.tsx @@ -11,7 +11,6 @@ export default createHandler(() => ( - {assets} diff --git a/src/features/command/contextMenu.tsx b/src/features/command/contextMenu.tsx index 63dfad5..ec38ac1 100644 --- a/src/features/command/contextMenu.tsx +++ b/src/features/command/contextMenu.tsx @@ -11,7 +11,7 @@ interface ContextMenuType { const ContextMenu = createContext() -const Root: ParentComponent<{ commands: CommandType[] }> = (props) => { +const Root: ParentComponent<{ commands: CommandType[] }> = (props) => { const [target, setTarget] = createSignal(); const context = { diff --git a/src/features/command/index.tsx b/src/features/command/index.tsx index d9b44aa..7b5446f 100644 --- a/src/features/command/index.tsx +++ b/src/features/command/index.tsx @@ -1,48 +1,115 @@ -import { Component, Show } from 'solid-js'; +import { Accessor, children, Component, createContext, createEffect, createMemo, JSX, ParentComponent, ParentProps, Show, useContext } from 'solid-js'; -export enum Modifier { - None = 0, - Shift = 1 << 0, - Control = 1 << 1, - Meta = 1 << 2, - Alt = 1 << 3, +interface CommandContextType { + set(commands: CommandType[]): void; + addContextualArguments(command: CommandType, target: EventTarget, args: Accessor): void; + execute(command: CommandType, event: Event): void; } -export interface CommandType { - (): any; - label: string; - shortcut?: { - key: string; - modifier: Modifier; - }; -} +const CommandContext = createContext(); -export const createCommand = (label: string, command: () => any, shortcut?: CommandType['shortcut']): CommandType => { - return Object.defineProperties(command as CommandType, { - label: { - value: label, - configurable: false, - writable: false, +export const useCommands = () => useContext(CommandContext); + +const Root: ParentComponent<{ commands: CommandType[] }> = (props) => { + // const commands = () => props.commands ?? []; + const contextualArguments = new Map>>(); + const commands = new Set>(); + + const context = { + set(c: CommandType[]): void { + for (const command of c) { + commands.add(command); + } }, - shortcut: { - value: shortcut ? { key: shortcut.key.toLowerCase(), modifier: shortcut.modifier } : undefined, - configurable: false, - writable: false, - } + + addContextualArguments(command: CommandType, target: EventTarget, args: Accessor): void { + if (contextualArguments.has(command) === false) { + contextualArguments.set(command, new WeakMap()); + } + + contextualArguments.get(command)?.set(target, args); + }, + + execute(command: CommandType, event: Event): boolean | undefined { + const contexts = contextualArguments.get(command); + + if (contexts === undefined) { + return; + } + + const element = event.composedPath().find(el => contexts.has(el)); + + if (element === undefined) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + + const args = contexts.get(element)! as Accessor; + + command(...args()); + + return false; + }, + }; + + createEffect(() => { + context.set(props.commands ?? []); }); + + const listener = (e: KeyboardEvent) => { + const key = e.key.toLowerCase(); + const modifiers = + (e.shiftKey ? 1 : 0) << 0 | + (e.ctrlKey ? 1 : 0) << 1 | + (e.metaKey ? 1 : 0) << 2 | + (e.altKey ? 1 : 0) << 3; + + const command = commands.values().find(c => c.shortcut?.key === key && (c.shortcut.modifier === undefined || c.shortcut.modifier === modifiers)); + + if (command === undefined) { + return; + } + + return context.execute(command, e); + }; + + return +
{props.children}
+
; }; -export const noop = Object.defineProperties(createCommand('noop', () => { }), { - withLabel: { - value(label: string) { - return createCommand(label, () => { }); - }, - configurable: false, - writable: false, - }, -}) as CommandType & { withLabel(label: string): CommandType }; +const Add: Component<{ command: CommandType } | { commands: CommandType[] }> = (props) => { + const context = useCommands(); + const commands = createMemo[]>(() => props.commands ?? [props.command]); -export const Command: Component<{ command: CommandType }> = (props) => { + createEffect(() => { + context?.set(commands()); + }); + + return undefined; +}; + +const Context = (props: ParentProps<{ for: CommandType, with: T }>): JSX.Element => { + const resolved = children(() => props.children); + const context = useCommands(); + const args = createMemo(() => props.with); + + createEffect(() => { + const children = resolved(); + + if (Array.isArray(children) || !(children instanceof Element)) { + return; + } + + context?.addContextualArguments(props.for, children, args); + }); + + return <>{resolved()}; +}; + +const Handle: Component<{ command: CommandType }> = (props) => { return <> {props.command.label} { @@ -58,4 +125,67 @@ export const Command: Component<{ command: CommandType }> = (props) => { ; }; +export const Command = { Root, Handle, Add, Context }; + +export enum Modifier { + None = 0, + Shift = 1 << 0, + Control = 1 << 1, + Meta = 1 << 2, + Alt = 1 << 3, +} + +export interface CommandType { + (...args: TArgs): any; + label: string; + shortcut?: { + key: string; + modifier: Modifier; + }; +} + +export const createCommand = (label: string, command: (...args: TArgs) => 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 commandArguments = (element: Element, commandAndArgs: Accessor<[CommandType, T]>) => { + const ctx = useContext(CommandContext); + const args = createMemo(() => commandAndArgs()[1]); + + if (!ctx) { + return; + } + + ctx.addContextualArguments(commandAndArgs()[0], element, args); +} + +export const noop = Object.defineProperties(createCommand('noop', () => { }), { + withLabel: { + value(label: string) { + return createCommand(label, () => { }); + }, + configurable: false, + writable: false, + }, +}) as CommandType & { withLabel(label: string): CommandType }; + +declare module "solid-js" { + namespace JSX { + interface Directives { + commandArguments(): [CommandType, T]; + } + } +} + export { Context } from './contextMenu'; \ No newline at end of file diff --git a/src/features/menu/index.tsx b/src/features/menu/index.tsx index 1c0fb47..2713998 100644 --- a/src/features/menu/index.tsx +++ b/src/features/menu/index.tsx @@ -3,7 +3,6 @@ import { Portal } from "solid-js/web"; import { createStore } from "solid-js/store"; import { CommandType, Command } from "../command"; import css from "./index.module.css"; -import { join } from "vinxi/dist/types/lib/path"; export interface MenuContextType { ref: Accessor; @@ -61,7 +60,9 @@ export const MenuProvider: ParentComponent<{ commands?: CommandType[] }> = (prop }, }; - return {props.children}; + return + {props.children} + ; } const useMenu = () => { @@ -127,7 +128,7 @@ const Root: ParentComponent<{}> = (props) => { const Child: Component<{ command: CommandType }> = (props) => { return }; @@ -177,43 +178,10 @@ const Root: ParentComponent<{}> = (props) => { }; -declare module "solid-js" { - namespace JSX { - interface HTMLAttributes { - anchor?: string | undefined; - } - - interface Directives { - asMenuRoot: true; - } - } -} - const Mount: Component = (props) => { const menu = useMenu(); - const listener = (e: KeyboardEvent) => { - const key = e.key.toLowerCase(); - const modifiers = - (e.shiftKey ? 1 : 0) << 0 | - (e.ctrlKey ? 1 : 0) << 1 | - (e.metaKey ? 1 : 0) << 2 | - (e.altKey ? 1 : 0) << 3; - - const commands = menu.commands(); - const command = commands.find(c => c.shortcut?.key === key && (c.shortcut.modifier === undefined || c.shortcut.modifier === modifiers)); - - if (command === undefined) { - return; - } - - command(); - - e.preventDefault(); - return false; - }; - - return
; + return
; }; export const Menu = { Mount, Root, Item, Separator } as const; @@ -374,3 +342,11 @@ function SearchableList(props: SearchableListProps): JSX.Element { ; }; + +declare module "solid-js" { + namespace JSX { + interface HTMLAttributes { + anchor?: string | undefined; + } + } +} \ No newline at end of file diff --git a/src/features/selectable/index.tsx b/src/features/selectable/index.tsx index 38da649..fffac6b 100644 --- a/src/features/selectable/index.tsx +++ b/src/features/selectable/index.tsx @@ -177,22 +177,7 @@ const Root: ParentComponent = (props) => { }); }; - onMount(() => { - document.addEventListener('keydown', onKeyboardEvent); - document.addEventListener('keyup', onKeyboardEvent); - }); - - onCleanup(() => { - if (isServer) { - return; - } - - document.removeEventListener('keydown', onKeyboardEvent); - document.removeEventListener('keyup', onKeyboardEvent); - }); - - return
{c()}
; - // return
{c()}
; + return
{c()}
; }; export const selectable = (element: HTMLElement, options: Accessor<{ value: object, key?: string }>) => { diff --git a/src/routes/(editor).tsx b/src/routes/(editor).tsx index 933965d..f4900a0 100644 --- a/src/routes/(editor).tsx +++ b/src/routes/(editor).tsx @@ -1,4 +1,4 @@ -import { Meta, Title } from "@solidjs/meta"; +import { Link, Meta, Title } from "@solidjs/meta"; import { createSignal, For, ParentProps, Show } from "solid-js"; import { BsTranslate } from "solid-icons/bs"; import { FilesProvider } from "~/features/file"; @@ -23,6 +23,9 @@ export default function Editor(props: ParentProps) { return Translation-Tool + + +
; +}; const Content: Component<{ directory: FileSystemDirectoryHandle, api?: Setter, entries?: Setter }> = (props) => { const [entries, setEntries] = createSignal(new Map());