From 75bd06cac3029df5c48a368f7647c7523b716d65 Mon Sep 17 00:00:00 2001 From: Chris Kruining Date: Mon, 14 Oct 2024 17:09:54 +0200 Subject: [PATCH] lots of work --- src/app.css | 10 +++ src/components/Counter.css | 20 ----- src/components/Counter.tsx | 11 --- src/components/filetree.module.css | 25 ++++++ src/components/filetree.tsx | 64 +++++++++++++ src/components/sidebar.module.css | 21 +++++ src/components/sidebar.tsx | 29 ++++++ src/components/tabs.module.css | 20 +++++ src/components/tabs.tsx | 97 ++++++++++++++++++++ src/features/command/index.ts | 33 +++++++ src/features/file/grid.css | 11 +++ src/features/file/grid.tsx | 123 ++++++++++++++++++++++--- src/features/menu/index.tsx | 37 ++------ src/routes/(editor)/edit.css | 0 src/routes/(editor)/edit.module.css | 13 +++ src/routes/(editor)/edit.tsx | 71 +++++++++------ src/routes/(editor)/experimental.css | 23 ----- src/routes/(editor)/experimental.tsx | 129 +++++++++------------------ src/routes/(editor)/index.tsx | 100 +-------------------- 19 files changed, 523 insertions(+), 314 deletions(-) delete mode 100644 src/components/Counter.css delete mode 100644 src/components/Counter.tsx create mode 100644 src/components/filetree.module.css create mode 100644 src/components/filetree.tsx create mode 100644 src/components/sidebar.module.css create mode 100644 src/components/sidebar.tsx create mode 100644 src/components/tabs.module.css create mode 100644 src/components/tabs.tsx create mode 100644 src/features/command/index.ts delete mode 100644 src/routes/(editor)/edit.css create mode 100644 src/routes/(editor)/edit.module.css diff --git a/src/app.css b/src/app.css index 097d704..61a752b 100644 --- a/src/app.css +++ b/src/app.css @@ -15,6 +15,16 @@ --fail: oklch(.64 .21 25.3); --warn: oklch(.82 .18 78.9); --succ: oklch(.86 .28 150); + + --radii-s: .125em; + --radii-m: .25em; + --radii-l: .5em; + + --text-s: .8rem; + --text-m: 1rem; + --text-l: 1.25rem; + --text-xl: 1.6rem; + --text-xxl: 2rem; } @media (prefers-color-scheme: dark) { diff --git a/src/components/Counter.css b/src/components/Counter.css deleted file mode 100644 index 8bd0eb3..0000000 --- a/src/components/Counter.css +++ /dev/null @@ -1,20 +0,0 @@ -.increment { - font-family: inherit; - font-size: inherit; - padding: 1em 2em; - color: #335d92; - background-color: rgba(68, 107, 158, 0.1); - border-radius: 2em; - border: 2px solid rgba(68, 107, 158, 0); - outline: none; - width: 200px; - font-variant-numeric: tabular-nums; -} - -.increment:focus { - border: 2px solid #335d92; -} - -.increment:active { - background-color: rgba(68, 107, 158, 0.2); -} \ No newline at end of file diff --git a/src/components/Counter.tsx b/src/components/Counter.tsx deleted file mode 100644 index 091fc5d..0000000 --- a/src/components/Counter.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { createSignal } from "solid-js"; -import "./Counter.css"; - -export default function Counter() { - const [count, setCount] = createSignal(0); - return ( - - ); -} diff --git a/src/components/filetree.module.css b/src/components/filetree.module.css new file mode 100644 index 0000000..933a142 --- /dev/null +++ b/src/components/filetree.module.css @@ -0,0 +1,25 @@ +.root { + display: flex; + flex-direction: column; + list-style: none; + padding-inline-start: 0; + + & details { + & > summary::marker { + content: none; + color: var(--text-1) !important; + } + + & span { + cursor: pointer; + } + } + + & ul { + padding-inline-start: 1.25em; + } + + & span { + white-space: nowrap; + } +} \ No newline at end of file diff --git a/src/components/filetree.tsx b/src/components/filetree.tsx new file mode 100644 index 0000000..00d0a11 --- /dev/null +++ b/src/components/filetree.tsx @@ -0,0 +1,64 @@ +import { Accessor, Component, createSignal, For, JSX, Show } from "solid-js"; +import css from "./filetree.module.css"; +import { AiFillFile, AiFillFolder, AiFillFolderOpen } from "solid-icons/ai"; + +export interface FileEntry { + name: string; + kind: 'file'; + meta: File; +} + +export interface FolderEntry { + name: string; + kind: 'folder'; + entries: Entry[]; +} + +export type Entry = FileEntry | FolderEntry; + +export const emptyFolder: FolderEntry = { name: '', kind: 'folder', entries: [] } as const; + +export async function* walk(directory: FileSystemDirectoryHandle, filters: RegExp[] = [], depth = 0): AsyncGenerator { + if (depth === 10) { + return; + } + + for await (const handle of directory.values()) { + + if (filters.some(f => f.test(handle.name))) { + continue; + } + + if (handle.kind === 'file') { + yield { name: handle.name, kind: 'file', meta: await handle.getFile() }; + } + else { + yield { name: handle.name, kind: 'folder', entries: await Array.fromAsync(walk(handle, filters, depth + 1)) }; + } + } +} + +export const Tree: Component<{ entries: Entry[], children: (file: Accessor) => JSX.Element }> = (props) => { + return
    + { + (entry, index) =>
  • + { + folder => + } + + { + file => <>{props.children(file)} + } +
  • + }
    +
+} + +const Folder: Component<{ folder: FolderEntry, children: (file: Accessor) => JSX.Element }> = (props) => { + const [open, setOpen] = createSignal(false); + + return
setOpen(o => !o)}> + }> {props.folder.name} + +
; +}; \ No newline at end of file diff --git a/src/components/sidebar.module.css b/src/components/sidebar.module.css new file mode 100644 index 0000000..2b91599 --- /dev/null +++ b/src/components/sidebar.module.css @@ -0,0 +1,21 @@ +.root { + display: grid; + grid: auto 1fr / 100%; + + & > button { + inline-size: max-content; + padding: 0; + background-color: var(--surface-1); + color: var(--text-1); + border: none; + font-size: var(--text-l); + } + + & > .content { + overflow: auto clip; + } + + &.closed > .content { + inline-size: 0; + } +} \ No newline at end of file diff --git a/src/components/sidebar.tsx b/src/components/sidebar.tsx new file mode 100644 index 0000000..e5cfaf4 --- /dev/null +++ b/src/components/sidebar.tsx @@ -0,0 +1,29 @@ +import { TbLayoutSidebarLeftCollapse, TbLayoutSidebarLeftExpand } from "solid-icons/tb"; +import { createMemo, createSignal, onMount, ParentComponent, Show } from "solid-js"; +import { Dynamic, Portal, render } from "solid-js/web"; +import css from "./sidebar.module.css"; + +export const Sidebar: ParentComponent<{ as?: string, open?: boolean, name?: string }> = (props) => { + const [open, setOpen] = createSignal(props.open ?? true) + const cssClass = createMemo(() => open() ? css.open : css.closed); + const name = createMemo(() => props.name ?? 'sidebar'); + + const toggle = () => setOpen(o => !o); + + let ref: Element; + return + + + +
+ +
+
+ + {props.children} +
+}; \ No newline at end of file diff --git a/src/components/tabs.module.css b/src/components/tabs.module.css new file mode 100644 index 0000000..38d0765 --- /dev/null +++ b/src/components/tabs.module.css @@ -0,0 +1,20 @@ +.root { + display: grid; + grid-template-rows: auto 1fr; + + .tab { + display: contents; + + & > summary { + grid-row: 1 / 1; + + &::marker { + content: none; + } + } + + &::details-content { + grid-area: 2 / 1; + } + } +} \ No newline at end of file diff --git a/src/components/tabs.tsx b/src/components/tabs.tsx new file mode 100644 index 0000000..35dd2e2 --- /dev/null +++ b/src/components/tabs.tsx @@ -0,0 +1,97 @@ +import { Accessor, children, Component, createContext, createEffect, createMemo, createSignal, createUniqueId, For, JSX, ParentComponent, useContext } from "solid-js"; +import { createStore } from "solid-js/store"; +import css from "./tabs.module.css"; +import { Portal } from "solid-js/web"; + +interface TabsContextType { + isActive(): boolean; +} + +interface TabsState { + tabs: TabType[]; +} + +interface TabType { + id: string; + label: string; +} + +const TabsContext = createContext(); + +export const Tabs: Component<{ children?: JSX.Element }> = (props) => { + const [state, setState] = createStore({ tabs: [] }); + + createEffect(() => { + const tabs = children(() => props.children).toArray(); + + console.log(tabs); + + setState('tabs', tabs.map(t => ({ id: t.id, label: t.getAttribute('data-label') }))) + }); + + const ctx: TabsContextType = { + isActive() { + return false; + } + }; + + return +
+ { + tab => + } +
+ + {props.children} +
+}; + +export const Tab: ParentComponent<{ label: string }> = (props) => { + const context = useContext(TabsContext); + + return
{props.children}
+} + +interface TabsSimpleContextType { + activate(id: string): void; + active: Accessor; + isActive(id: string): Accessor; +} + +const TabsSimpleContext = createContext(); + +export const TabsSimple: ParentComponent = (props) => { + const [active, setActive] = createSignal(undefined); + + return active() === id); + }, + }}> +
+ {props.children} +
+
; +} + +export const TabSimple: ParentComponent<{ label: string }> = (props) => { + const id = `tab-${createUniqueId()}`; + const context = useContext(TabsSimpleContext); + + if (!context) { + return undefined; + } + + return
e.newState === 'open' && context.activate(id)}> + {props.label} + + {props.children} +
+} \ No newline at end of file diff --git a/src/features/command/index.ts b/src/features/command/index.ts new file mode 100644 index 0000000..affc14d --- /dev/null +++ b/src/features/command/index.ts @@ -0,0 +1,33 @@ +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/file/grid.css b/src/features/file/grid.css index 4b17fd0..f312d29 100644 --- a/src/features/file/grid.css +++ b/src/features/file/grid.css @@ -19,12 +19,23 @@ resize: vertical; min-block-size: 2em; max-block-size: 50em; + + background-color: var(--surface-1); + color: var(--text-1); + border-color: var(--text-2); + border-radius: var(--radii-s); } & .cell { display: grid; place-content: center stretch; padding: .5em; + border: 1px solid transparent; + border-radius: var(--radii-m); + + &:focus-within { + border-color: var(--info); + } } & > :is(header, main, footer) { diff --git a/src/features/file/grid.tsx b/src/features/file/grid.tsx index fe90151..81a15e1 100644 --- a/src/features/file/grid.tsx +++ b/src/features/file/grid.tsx @@ -1,22 +1,41 @@ -import { Accessor, Component, createContext, createEffect, createMemo, createSignal, For, ParentComponent, Show, useContext } from "solid-js"; +import { Component, createContext, createEffect, createMemo, For, ParentComponent, Show, useContext } from "solid-js"; +import { createStore, produce } from "solid-js/store"; import './grid.css'; -import { createStore } from "solid-js/store"; + +const debounce = void>(callback: T, delay: number): T => { + let handle: ReturnType | undefined; + + return (...args: any[]) => { + if (handle) { + clearTimeout(handle); + } + + handle = setTimeout(() => callback(...args), delay); + } +}; interface Leaf extends Record { } export interface Entry extends Record { } -interface SelectionContextType { +export interface SelectionContextType { rowCount(): number; selection(): string[]; isSelected(key: string): boolean, selectAll(select: boolean): void; - select(key: string, select: true): void; + select(key: string, select: boolean): void; +} +export interface GridContextType { + mutate(prop: string, lang: string, value: string): void; + add(prop: string): void; + selection: SelectionContextType; } const SelectionContext = createContext(); +const GridContext = createContext(); const isLeaf = (entry: Entry | Leaf): entry is Leaf => Object.values(entry).some(v => typeof v === 'string'); const useSelection = () => useContext(SelectionContext)!; +const useGrid = () => useContext(GridContext)!; const SelectionProvider: ParentComponent<{ rows: Map, context?: (ctx: SelectionContextType) => any }> = (props) => { const [state, setState] = createStore<{ selection: string[] }>({ selection: [] }); @@ -57,8 +76,58 @@ const SelectionProvider: ParentComponent<{ rows: Map; }; +const GridProvider: ParentComponent<{ rows: Map, context?: (ctx: GridContextType) => any }> = (props) => { + type Entry = { [lang: string]: { original: string, value: string } }; -export const Grid: Component<{ columns: string[], rows: Map, context?: (ctx: SelectionContextType) => any }> = (props) => { + const [state, setState] = createStore<{ rows: { [prop: string]: Entry }, numberOfRows: number }>({ + rows: {}, + numberOfRows: 0, + }); + + createEffect(() => { + const rows = props.rows + .entries() + .map(([prop, entry]) => [prop, Object.fromEntries(Object.entries(entry).map(([lang, { value }]) => [lang, { original: value, value }]))]); + + setState('rows', Object.fromEntries(rows)); + }); + + createEffect(() => { + setState('numberOfRows', Object.keys(state.rows).length); + }); + + createEffect(() => { + console.log(state.rows.toplevel?.nl.value); + }); + + const ctx: GridContextType = { + mutate(prop: string, lang: string, value: string) { + // setState('rows', prop, lang, ({ original }) => ({ original, value })); + setState('rows', produce(rows => { + rows[prop][lang].value = value; + })); + }, + + add(prop: string) { + + }, + + selection: undefined!, + }; + + createEffect(() => { + console.log(ctx); + props.context?.(ctx); + }); + + return + ctx.selection = selction}> + {props.children} + + ; +}; + +export const Grid: Component<{ columns: string[], rows: Map, context?: (ctx: GridContextType) => any }> = (props) => { const columnCount = createMemo(() => props.columns.length - 1); const root = createMemo(() => { return props.rows @@ -86,13 +155,13 @@ export const Grid: Component<{ columns: string[], rows: Map - +
-
+ }; @@ -116,17 +185,37 @@ const Head: Component<{ headers: string[] }> = (props) => { }; const Row: Component<{ entry: Entry, path?: string[] }> = (props) => { + const grid = useGrid(); + return { ([key, value]) => { - const values = Object.values(value); + const values = Object.entries(value); const path = [...(props.path ?? []), key]; const k = path.join('.'); const context = useSelection(); + const resize = (element: HTMLElement) => { + element.style.blockSize = `1px`; + element.style.blockSize = `${11 + element.scrollHeight}px`; + }; + + const mutate = debounce((element: HTMLTextAreaElement) => { + const [prop, lang] = element.name.split(':'); + + grid.mutate(prop, lang, element.value.trim()) + }, 300); + + const onKeyUp = (e: KeyboardEvent) => { + const element = e.target as HTMLTextAreaElement; + + resize(element); + mutate(element); + }; + return }>