diff --git a/examples/emmer/en.json b/examples/emmer/en-GB.json similarity index 100% rename from examples/emmer/en.json rename to examples/emmer/en-GB.json diff --git a/examples/emmer/namespace/en.json b/examples/emmer/namespace/en-GB.json similarity index 100% rename from examples/emmer/namespace/en.json rename to examples/emmer/namespace/en-GB.json diff --git a/examples/emmer/nl.json b/examples/emmer/nl-NL.json similarity index 100% rename from examples/emmer/nl.json rename to examples/emmer/nl-NL.json diff --git a/src/components/grid/grid.tsx b/src/components/grid/grid.tsx index 26b6360..38b5c85 100644 --- a/src/components/grid/grid.tsx +++ b/src/components/grid/grid.tsx @@ -1,14 +1,14 @@ import { Accessor, createContext, createEffect, createMemo, createSignal, JSX, useContext } from "solid-js"; import { Mutation } from "~/utilities"; -import { SelectionMode, Table, Column as TableColumn, TableApi, DataSet, CellRenderer } from "~/components/table"; +import { SelectionMode, Table, Column as TableColumn, TableApi, DataSet, CellRenderer as TableCellRenderer } from "~/components/table"; import css from './grid.module.css'; -export interface CellEditor, K extends keyof T> { - (cell: Parameters>[0] & { mutate: (next: T[K]) => any }): JSX.Element; +export interface CellRenderer, K extends keyof T> { + (cell: Parameters>[0] & { mutate: (next: T[K]) => any }): JSX.Element; } -export interface Column> extends TableColumn { - editor?: CellEditor; +export interface Column> extends Omit, 'renderer'> { + renderer?: CellRenderer; } export interface GridApi> extends TableApi { @@ -63,16 +63,16 @@ export function Grid>(props: GridProps) { }, }; - const cellEditors = createMemo(() => Object.fromEntries( + const cellRenderers = createMemo(() => Object.fromEntries( props.columns - .filter(c => c.editor !== undefined) + .filter(c => c.renderer !== undefined) .map(c => { const Editor: CellRenderer = ({ row, column, value }) => { const mutate = (next: T[keyof T]) => { ctx.mutate(row, column, next); }; - return c.editor!({ row, column, value, mutate }); + return c.renderer!({ row, column, value, mutate }); }; return [c.id, Editor] as const; @@ -83,7 +83,7 @@ export function Grid>(props: GridProps) { { - cellEditors() + cellRenderers() }
; }; diff --git a/src/components/grid/index.tsx b/src/components/grid/index.tsx index 6ce5a66..1783f2c 100644 --- a/src/components/grid/index.tsx +++ b/src/components/grid/index.tsx @@ -1,4 +1,4 @@ export type { DataSetRowNode, DataSetGroupNode, DataSetNode, SelectionMode, SortingFunction, SortOptions, GroupingFunction, GroupOptions } from '../table'; -export type { GridApi, Column, CellEditor } from './grid'; +export type { GridApi, Column, CellRenderer as CellEditor } from './grid'; export { Grid } from './grid'; \ No newline at end of file diff --git a/src/components/table/dataset.ts b/src/components/table/dataset.ts index d48dd89..10dbba0 100644 --- a/src/components/table/dataset.ts +++ b/src/components/table/dataset.ts @@ -1,5 +1,5 @@ import { Accessor, createEffect, createMemo } from "solid-js"; -import { createStore, NotWrappable, StoreSetter, unwrap } from "solid-js/store"; +import { createStore, NotWrappable, produce, StoreSetter, unwrap } from "solid-js/store"; import { CustomPartial } from "solid-js/store/types/store.js"; import { deepCopy, deepDiff, Mutation } from "~/utilities"; @@ -38,13 +38,14 @@ export type Setter = export interface DataSet> { data: T[]; - value: Accessor[]>; + nodes: Accessor[]>; + value: Accessor<(T | undefined)[]>; mutations: Accessor; sorting: Accessor | undefined>; grouping: Accessor | undefined>; - // mutate(index: number, value: T): void; mutate(index: number, prop: K, value: T[K]): void; + mutateEach(setter: (value: T) => T): void; remove(indices: number[]): void; insert(item: T, at?: number): void; @@ -59,15 +60,14 @@ function defaultGroupingFunction(groupBy: keyof T): GroupingFunction>(data: T[], initialOptions?: { sort?: SortOptions, group?: GroupOptions }): DataSet => { - const nodes = data; const [state, setState] = createStore>({ - value: deepCopy(nodes), - snapshot: nodes, + value: deepCopy(data), + snapshot: data, sorting: initialOptions?.sort, grouping: initialOptions?.group, }); - const value = createMemo(() => { + const nodes = createMemo(() => { const sorting = state.sorting; const grouping = state.grouping; @@ -106,7 +106,8 @@ export const createDataSet = >(data: T[], initialO const set: DataSet = { data, - value, + nodes, + value: createMemo(() => state.value), mutations, sorting, grouping, @@ -115,6 +116,10 @@ export const createDataSet = >(data: T[], initialO setState('value', index, prop as any, value); }, + mutateEach(setter) { + setState('value', value => value.map(i => i === undefined ? undefined : setter(i))); + }, + remove(indices) { setState('value', value => value.map((item, i) => indices.includes(i) ? undefined : item)); }, diff --git a/src/components/table/table.module.css b/src/components/table/table.module.css index 3cc9732..16b2019 100644 --- a/src/components/table/table.module.css +++ b/src/components/table/table.module.css @@ -163,6 +163,7 @@ & > .header { border-block-end-color: transparent; + animation: none; & .cell { justify-content: start; @@ -171,6 +172,7 @@ & > label { --state: 0; display: contents; + cursor: pointer; & input[type="checkbox"] { display: none; @@ -181,6 +183,7 @@ transition: rotate .3s ease-in-out; inline-size: 1em; aspect-ratio: 1; + opacity: 1 !important; } &:has(input:not(:checked)) { diff --git a/src/components/table/table.tsx b/src/components/table/table.tsx index 0ba68bc..70178f1 100644 --- a/src/components/table/table.tsx +++ b/src/components/table/table.tsx @@ -6,17 +6,18 @@ import css from './table.module.css'; selectable; -export type Column = { +export type CellRenderer, K extends keyof T> = (cell: { row: number, column: K, value: T[K] }) => JSX.Element; +export type CellRenderers> = { [K in keyof T]?: CellRenderer }; + +export interface Column> { id: keyof T, label: string, sortable?: boolean, group?: string, + renderer?: CellRenderer, readonly groupBy?: (rows: DataSetRowNode[]) => DataSetNode[], }; -export type CellRenderer, K extends keyof T> = (cell: { row: number, column: K, value: T[K] }) => JSX.Element; -export type CellRenderers> = { [K in keyof T]?: CellRenderer }; - export interface TableApi> { readonly selection: Accessor[]>; readonly rows: Accessor>; @@ -96,7 +97,7 @@ function InnerTable>(props: InnerTableProps) { - { + { node => } diff --git a/src/features/file/grid.module.css b/src/features/file/grid.module.css index e69de29..67bf54e 100644 --- a/src/features/file/grid.module.css +++ b/src/features/file/grid.module.css @@ -0,0 +1,19 @@ +.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-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 58a2b66..e72de38 100644 --- a/src/features/file/grid.tsx +++ b/src/features/file/grid.tsx @@ -1,185 +1,70 @@ -import { Accessor, Component, createContext, createEffect, createMemo, createSignal, For, ParentComponent, Show, useContext } from "solid-js"; -import { createStore, produce, unwrap } from "solid-js/store"; -import { debounce, deepCopy, deepDiff, Mutation, splitAt } from "~/utilities"; -import { DataSetRowNode, DataSetNode, SelectionMode, Table } from "~/components/table"; -import css from './grid.module.css'; - -type Rows = Map>; - -export interface GridContextType { - readonly rows: Accessor; - readonly mutations: Accessor; - // readonly selection: Accessor; - mutate(prop: string, value: string): void; - remove(props: string[]): void; - insert(prop: string): void; - addColumn(name: string): void; -} +import { Accessor, Component, createEffect, createMemo, createSignal } from "solid-js"; +import { debounce, Mutation } from "~/utilities"; +import { Column, GridApi as GridCompApi, Grid as GridComp } from "~/components/grid"; +import { createDataSet, DataSetNode, DataSetRowNode } from "~/components/table"; +import css from "./grid.module.css" +export type Entry = { key: string } & { [lang: string]: string }; export interface GridApi { - readonly selection: Accessor>>; - readonly rows: Accessor>>; readonly mutations: Accessor; - selectAll(): void; - clear(): void; - remove(keys: string[]): void; - insert(prop: string): void; - addColumn(name: string): void; + remove(indices: number[]): void; + addKey(key: string): void; + addLocale(locale: string): void; +}; + +const groupBy = (rows: DataSetRowNode[]) => { + type R = DataSetRowNode & { _key: string }; + + const group = (nodes: R[]): DataSetNode[] => Object + .entries(Object.groupBy(nodes, r => r._key.split('.').at(0)!) as Record) + .map(([key, nodes]) => nodes.at(0)?._key === key + ? nodes[0] + : ({ kind: 'group', key, groupedBy: 'key', nodes: group(nodes.map(n => ({ ...n, _key: n._key.slice(key.length + 1) }))) }) + ); + + return group(rows.map(r => ({ ...r, _key: r.value.key }))) as any; } -const GridContext = createContext(); +export function Grid(props: { class?: string, rows: Entry[], api?: (api: GridApi) => any }) { + const rows = createMemo(() => createDataSet(props.rows, { group: { by: 'key', with: groupBy } })); + const locales = createMemo(() => Object.keys(rows().value().at(0) ?? {}).filter(k => k !== 'key')); + const columns = createMemo[]>(() => [ + { + id: 'key', + label: 'Key', + renderer: ({ value }) => value.split('.').at(-1), + }, + ...locales().map>(lang => ({ + id: lang, + label: lang, + renderer: ({ row, column, value, mutate }) => { + const entry = rows().value()[row]!; -const useGrid = () => useContext(GridContext)!; - -export const Grid: Component<{ class?: string, columns: string[], rows: Rows, api?: (api: GridApi) => any }> = (props) => { - const [table, setTable] = createSignal(); - const [state, setState] = createStore<{ rows: Record>, columns: string[], snapshot: Rows, numberOfRows: number }>({ - rows: {}, - columns: [], - snapshot: new Map, - numberOfRows: 0, - }); - - const mutations = createMemo(() => { - // enumerate all values to make sure the memo is recalculated on any change - Object.values(state.rows).map(entry => Object.values(entry)); - - return deepDiff(state.snapshot, state.rows).toArray(); - }); - - type Entry = { key: string, [lang: string]: string }; - - const groupBy = (rows: DataSetRowNode[]) => { - const group = (nodes: DataSetRowNode[]): DataSetNode[] => Object - .entries(Object.groupBy(nodes, r => r.key.split('.').at(0)!) as Record[]>) - .map>(([key, nodes]) => nodes.at(0)?.key === key - ? { ...nodes[0], key: nodes[0].value.key, value: { ...nodes[0].value, key: nodes[0].key } } - : ({ kind: 'group', key, groupedBy: 'key', nodes: group(nodes.map(n => ({ ...n, key: n.key.slice(key.length + 1) }))) }) - ); - - return group(rows.map(r => ({ ...r, key: r.value.key }))); - } - - const rows = createMemo(() => Object.entries(state.rows).map(([key, values]) => ({ key, ...values }))); - const columns = createMemo(() => [ - { id: 'key', label: 'Key', groupBy }, - ...state.columns.map(c => ({ id: c, label: c })), + return