From 487e41c2d70b529607784d1defd66175ae20f52a Mon Sep 17 00:00:00 2001 From: Chris Kruining Date: Tue, 11 Feb 2025 16:55:12 +1100 Subject: [PATCH] stash --- package.json | 21 +++--- src/components/textarea/grammarChecker.ts | 8 +++ src/components/textarea/index.ts | 3 + src/components/textarea/spellChecker.ts | 8 +++ src/components/textarea/textarea.module.css | 24 +++++++ src/components/textarea/textarea.tsx | 80 +++++++++++++++++++++ src/features/command/palette.tsx | 7 +- src/features/file/grid.module.css | 15 +--- src/features/file/grid.tsx | 49 +++---------- src/utilities.ts | 17 +++++ 10 files changed, 163 insertions(+), 69 deletions(-) create mode 100644 src/components/textarea/grammarChecker.ts create mode 100644 src/components/textarea/index.ts create mode 100644 src/components/textarea/spellChecker.ts create mode 100644 src/components/textarea/textarea.module.css create mode 100644 src/components/textarea/textarea.tsx diff --git a/package.json b/package.json index 7174ad2..232af0b 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,10 @@ { "name": "calque", + "type": "module", + "engines": { + "node": ">=18", + "bun": ">=1" + }, "dependencies": { "@solid-primitives/clipboard": "^1.5.10", "@solid-primitives/destructure": "^0.2.0", @@ -19,16 +24,6 @@ "ts-pattern": "^5.6.0", "vinxi": "^0.4.3" }, - "engines": { - "node": ">=18" - }, - "scripts": { - "dev": "vinxi dev", - "build": "vinxi build", - "start": "vinxi start", - "version": "vinxi version" - }, - "type": "module", "devDependencies": { "@happy-dom/global-registrator": "^15.11.7", "@sinonjs/fake-timers": "^14.0.0", @@ -41,5 +36,11 @@ "vite-plugin-pwa": "^0.21.1", "vite-plugin-solid-svg": "^0.8.1", "workbox-window": "^7.3.0" + }, + "scripts": { + "dev": "vinxi dev", + "build": "vinxi build", + "start": "vinxi start", + "version": "vinxi version" } } \ No newline at end of file diff --git a/src/components/textarea/grammarChecker.ts b/src/components/textarea/grammarChecker.ts new file mode 100644 index 0000000..b2059e3 --- /dev/null +++ b/src/components/textarea/grammarChecker.ts @@ -0,0 +1,8 @@ + + +const regex = /\w+\s+\w+/gi; +export function defaultChecker(subject: string, lang: string): [number, number][] { + return Array.from(subject.matchAll(regex)).filter(() => Math.random() >= .5).map(({ 0: match, index }) => { + return [index, index + match.length - 1]; + }); +} \ No newline at end of file diff --git a/src/components/textarea/index.ts b/src/components/textarea/index.ts new file mode 100644 index 0000000..6065c5f --- /dev/null +++ b/src/components/textarea/index.ts @@ -0,0 +1,3 @@ + + +export { Textarea } from './textarea'; \ No newline at end of file diff --git a/src/components/textarea/spellChecker.ts b/src/components/textarea/spellChecker.ts new file mode 100644 index 0000000..ed1126a --- /dev/null +++ b/src/components/textarea/spellChecker.ts @@ -0,0 +1,8 @@ + + +const regex = /\w+/gi; +export function defaultChecker(subject: string, lang: string): [number, number][] { + return Array.from(subject.matchAll(regex)).filter(() => Math.random() >= .5).map(({ 0: match, index }) => { + return [index, index + match.length - 1]; + }); +} \ No newline at end of file diff --git a/src/components/textarea/textarea.module.css b/src/components/textarea/textarea.module.css new file mode 100644 index 0000000..5b0439f --- /dev/null +++ b/src/components/textarea/textarea.module.css @@ -0,0 +1,24 @@ +.textarea { + /* Make sure resizing works as intended */ + display: block; + overflow: clip auto; + resize: block; + + white-space: wrap; + min-block-size: max(2em, 100%); + max-block-size: 50em; + + unicode-bidi: plaintext; + white-space-collapse: preserve; + text-wrap-mode: wrap; + overflow-wrap: break-word; + cursor: text; +} + +.spellingError { + text-decoration-line: spelling-error; +} + +.grammarError { + text-decoration-line: grammar-error; +} \ No newline at end of file diff --git a/src/components/textarea/textarea.tsx b/src/components/textarea/textarea.tsx new file mode 100644 index 0000000..3e76406 --- /dev/null +++ b/src/components/textarea/textarea.tsx @@ -0,0 +1,80 @@ +import { createEffect, createMemo, createSignal } from 'solid-js'; +import { decode } from '~/utilities'; +import { debounce } from '@solid-primitives/scheduled'; +import { defaultChecker as spellChecker } from './spellChecker'; +import { defaultChecker as grammarChecker } from './grammarChecker'; +import css from './textarea.module.css'; + +interface TextareaProps { + class?: string; + value: string; + lang: string; + oninput?: (event: InputEvent) => any; + spellChecker?: any; + grammarChecker?: any; +} + +export function Textarea(props: TextareaProps) { + const [value, setValue] = createSignal(decode(props.value)); + const [element, setElement] = createSignal(); + + createEffect(() => { + setValue(decode(props.value)); + }); + + const resize = () => { + const el = element(); + + if (!el) { + return; + } + + el.style.height = `1px`; + el.style.height = `${2 + element()!.scrollHeight}px`; + }; + + const mutate = debounce(() => { + props.oninput?.(new InputEvent('input', { + data: value(), + })) + }, 300); + + const onKeyUp = (e: KeyboardEvent) => { + e.stopPropagation(); + e.preventDefault(); + + setValue(element()!.innerText.trim()); + + resize(); + mutate(); + + return false; + }; + + const spellingErrors = createMemo(() => spellChecker(value(), props.lang)); + const grammarErrors = createMemo(() => grammarChecker(value(), props.lang)); + + const html = createMemo(() => { + return value().split('').map((letter, index) => { + const spellingOpen = spellingErrors().some(([start]) => start === index) ? `` : ''; + const spellingClose = spellingErrors().some(([, end]) => end === index) ? `` : ''; + + const grammarOpen = grammarErrors().some(([start]) => start === index) ? `` : ''; + const grammarClose = grammarErrors().some(([, end]) => end === index) ? `` : ''; + + return `${grammarOpen}${spellingOpen}${letter}${spellingClose}${grammarClose}`; + }).join(''); + }); + + return
e.stopPropagation()} + on:pointerdown={e => e.stopPropagation()} + contentEditable + innerHTML={html()} + />; +} \ No newline at end of file diff --git a/src/features/command/palette.tsx b/src/features/command/palette.tsx index 6959114..c7f8465 100644 --- a/src/features/command/palette.tsx +++ b/src/features/command/palette.tsx @@ -3,6 +3,7 @@ import { useI18n } from "../i18n"; import { CommandType } from "./command"; import { useCommands } from "./context"; import css from "./palette.module.css"; +import { split_by_filter } from "~/utilities"; export interface CommandPaletteApi { readonly open: Accessor; @@ -59,11 +60,9 @@ export const CommandPalette: Component<{ api?: (api: CommandPaletteApi) => any, (item, ctx) => { const label = t(item.label) as string; const filter = ctx.filter().toLowerCase(); - const length = filter.length; - const indices = [0, ...Array.from(label.matchAll(new RegExp(filter, 'gi')).flatMap(({ index }) => [index, index + length]))]; - return label.slice(current, indices[i + 1]))}>{ - (part) => {part} + return { + ([is_hit, part]) => {part} }; } } diff --git a/src/features/file/grid.module.css b/src/features/file/grid.module.css index 67bf54e..814072d 100644 --- a/src/features/file/grid.module.css +++ b/src/features/file/grid.module.css @@ -1,19 +1,6 @@ .textarea { - resize: vertical; - min-block-size: max(2em, 100%); - max-block-size: 50em; - background-color: var(--surface-600); color: var(--text-1); - border-color: var(--text-2); + border: 1px solid var(--text-2); border-radius: var(--radii-s); - - &:has(::spelling-error, ::grammar-error) { - border-color: var(--fail); - } - - & ::spelling-error { - outline: 1px solid var(--fail); - text-decoration: yellow underline; - } } \ No newline at end of file diff --git a/src/features/file/grid.tsx b/src/features/file/grid.tsx index 01859b4..58e2665 100644 --- a/src/features/file/grid.tsx +++ b/src/features/file/grid.tsx @@ -1,4 +1,4 @@ -import { Accessor, Component, createEffect, createMemo, createSignal, JSX, untrack } from "solid-js"; +import { Accessor, Component, createEffect, createMemo, createSignal, For, JSX, Show, untrack } from "solid-js"; import { decode, Mutation } from "~/utilities"; import { Column, GridApi as GridCompApi, Grid as GridComp } from "~/components/grid"; import { createDataSet, DataSetNode, DataSetRowNode } from "~/features/dataset"; @@ -6,6 +6,7 @@ import { SelectionItem } from "../selectable"; import { useI18n } from "../i18n"; import { debounce } from "@solid-primitives/scheduled"; import css from "./grid.module.css" +import { Textarea } from "~/components/textarea"; export type Entry = { key: string } & { [lang: string]: string }; export interface GridApi { @@ -43,7 +44,7 @@ export function Grid(props: { class?: string, rows: Entry[], locales: string[], label: t('feature.file.grid.key'), renderer: ({ value }) => props.children?.(value) ?? value.split('.').at(-1), }, - ...locales().map>(lang => ({ + ...locales().toSorted().map>(lang => ({ id: lang, label: lang, renderer: ({ row, column, value, mutate }) => { @@ -59,8 +60,8 @@ export function Grid(props: { class?: string, rows: Entry[], locales: string[], // Normalize dataset in order to make sure all the files have the correct structure createEffect(() => { // For tracking - props.rows - const value = untrack(() => rows.value); + props.rows; + // const value = untrack(() => rows.value); rows.mutateEach(({ key, ...locales }) => ({ key, ...Object.fromEntries(Object.entries(locales).map(([locale, value]) => [locale, value ?? ''])) })) }); @@ -95,44 +96,10 @@ export function Grid(props: { class?: string, rows: Entry[], locales: string[], }; const TextArea: Component<{ row: number, key: string, lang: string, value: string, oninput?: (event: InputEvent) => any }> = (props) => { - const [element, setElement] = createSignal(); - - const resize = () => { - const el = element(); - - if (!el) { - return; - } - - el.style.height = `1px`; - el.style.height = `${2 + element()!.scrollHeight}px`; - }; - - const mutate = debounce(() => { - props.oninput?.(new InputEvent('input', { - data: element()?.value.trim(), - })) - }, 300); - - const onKeyUp = (e: KeyboardEvent) => { - resize(); - mutate(); - }; - - const value = createMemo(() => decode(props.value)); - - return