kaas
This commit is contained in:
parent
75bd06cac3
commit
40f46eba1d
17 changed files with 426 additions and 150 deletions
|
@ -1,3 +1,10 @@
|
||||||
import { defineConfig } from "@solidjs/start/config";
|
import { defineConfig } from "@solidjs/start/config";
|
||||||
|
import { VitePWA } from 'vite-plugin-pwa'
|
||||||
|
|
||||||
export default defineConfig({});
|
export default defineConfig({
|
||||||
|
vite: {
|
||||||
|
plugins: [
|
||||||
|
VitePWA({ registerType: 'autoUpdate' }),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
@ -21,6 +21,8 @@
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/wicg-file-system-access": "^2023.10.5"
|
"@types/wicg-file-system-access": "^2023.10.5",
|
||||||
|
"vite-plugin-pwa": "^0.20.5",
|
||||||
|
"workbox-window": "^7.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,10 @@
|
||||||
--text-l: 1.25rem;
|
--text-l: 1.25rem;
|
||||||
--text-xl: 1.6rem;
|
--text-xl: 1.6rem;
|
||||||
--text-xxl: 2rem;
|
--text-xxl: 2rem;
|
||||||
|
|
||||||
|
--padding-s: .25em;
|
||||||
|
--padding-m: .5em;
|
||||||
|
--padding-l: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
|
|
|
@ -2,24 +2,29 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding-inline-start: 0;
|
|
||||||
|
|
||||||
& details {
|
& details {
|
||||||
& > summary::marker {
|
& > summary {
|
||||||
|
padding: var(--padding-s);
|
||||||
|
|
||||||
|
&::marker {
|
||||||
content: none;
|
content: none;
|
||||||
color: var(--text-1) !important;
|
color: var(--text-1) !important;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::details-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
list-style: none;
|
||||||
|
padding-inline-start: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
& span {
|
& span {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& ul {
|
|
||||||
padding-inline-start: 1.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
& span {
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
padding: var(--padding-s);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import { Accessor, Component, createSignal, For, JSX, Show } from "solid-js";
|
import { Accessor, Component, createContext, createSignal, For, JSX, Show, useContext } from "solid-js";
|
||||||
import css from "./filetree.module.css";
|
import css from "./filetree.module.css";
|
||||||
import { AiFillFile, AiFillFolder, AiFillFolderOpen } from "solid-icons/ai";
|
import { AiFillFile, AiFillFolder, AiFillFolderOpen } from "solid-icons/ai";
|
||||||
|
import { SelectionProvider, selectable } from "~/features/selectable";
|
||||||
|
|
||||||
export interface FileEntry {
|
export interface FileEntry {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -38,27 +39,61 @@ export async function* walk(directory: FileSystemDirectoryHandle, filters: RegEx
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Tree: Component<{ entries: Entry[], children: (file: Accessor<FileEntry>) => JSX.Element }> = (props) => {
|
interface TreeContextType {
|
||||||
return <ul class={css.root}>
|
open(file: File): void;
|
||||||
<For each={props.entries}>{
|
}
|
||||||
(entry, index) => <li style={`order: ${(entry.kind === 'file' ? 200 : 100) + index()}`}>
|
|
||||||
|
const TreeContext = createContext<TreeContextType>();
|
||||||
|
|
||||||
|
export const Tree: Component<{ entries: Entry[], children: (file: Accessor<FileEntry>) => JSX.Element, open: TreeContextType['open'] }> = (props) => {
|
||||||
|
const [selection, setSelection] = createSignal();
|
||||||
|
|
||||||
|
// createEffect(() => {
|
||||||
|
// console.log(selection());
|
||||||
|
// });
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
open: props.open,
|
||||||
|
// open(file: File) {
|
||||||
|
// console.log(`open ${file.name}`)
|
||||||
|
// },
|
||||||
|
};
|
||||||
|
|
||||||
|
return <SelectionProvider selection={setSelection}>
|
||||||
|
<TreeContext.Provider value={context}>
|
||||||
|
<div class={css.root}><_Tree entries={props.entries} children={props.children} /></div>
|
||||||
|
</TreeContext.Provider>
|
||||||
|
</SelectionProvider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _Tree: Component<{ entries: Entry[], children: (file: Accessor<FileEntry>) => JSX.Element }> = (props) => {
|
||||||
|
const context = useContext(TreeContext);
|
||||||
|
|
||||||
|
return <For each={props.entries.sort(sort_by('kind'))}>{
|
||||||
|
entry => <>
|
||||||
<Show when={entry.kind === 'folder' ? entry : undefined}>{
|
<Show when={entry.kind === 'folder' ? entry : undefined}>{
|
||||||
folder => <Folder folder={folder()} children={props.children} />
|
folder => <Folder folder={folder()} children={props.children} />
|
||||||
}</Show>
|
}</Show>
|
||||||
|
|
||||||
<Show when={entry.kind === 'file' ? entry : undefined}>{
|
<Show when={entry.kind === 'file' ? entry : undefined}>{
|
||||||
file => <><AiFillFile />{props.children(file)}</>
|
file => <span use:selectable={file()} ondblclick={() => context?.open(file().meta)}><AiFillFile /> {props.children(file)}</span>
|
||||||
}</Show>
|
}</Show>
|
||||||
</li>
|
</>
|
||||||
}</For>
|
}</For>
|
||||||
</ul>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Folder: Component<{ folder: FolderEntry, children: (file: Accessor<FileEntry>) => JSX.Element }> = (props) => {
|
const Folder: Component<{ folder: FolderEntry, children: (file: Accessor<FileEntry>) => JSX.Element }> = (props) => {
|
||||||
const [open, setOpen] = createSignal(false);
|
const [open, setOpen] = createSignal(false);
|
||||||
|
|
||||||
return <details open={open()} on:toggle={() => setOpen(o => !o)}>
|
return <details open={open()} ontoggle={() => setOpen(o => !o)}>
|
||||||
<summary><Show when={open()} fallback={<AiFillFolder />}><AiFillFolderOpen /></Show> {props.folder.name}</summary>
|
<summary><Show when={open()} fallback={<AiFillFolder />}><AiFillFolderOpen /></Show> {props.folder.name}</summary>
|
||||||
<Tree entries={props.folder.entries} children={props.children} />
|
<_Tree entries={props.folder.entries} children={props.children} />
|
||||||
</details>;
|
</details>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sort_by = (key: string) => (objA: Record<string, any>, objB: Record<string, any>) => {
|
||||||
|
const a = objA[key];
|
||||||
|
const b = objB[key];
|
||||||
|
|
||||||
|
return Number(a < b) - Number(b < a);
|
||||||
|
};
|
|
@ -1,29 +1,25 @@
|
||||||
import { TbLayoutSidebarLeftCollapse, TbLayoutSidebarLeftExpand } from "solid-icons/tb";
|
import { TbLayoutSidebarLeftCollapse, TbLayoutSidebarLeftExpand } from "solid-icons/tb";
|
||||||
import { createMemo, createSignal, onMount, ParentComponent, Show } from "solid-js";
|
import { createMemo, createSignal, ParentComponent, Show } from "solid-js";
|
||||||
import { Dynamic, Portal, render } from "solid-js/web";
|
import { Dynamic } from "solid-js/web";
|
||||||
import css from "./sidebar.module.css";
|
import css from "./sidebar.module.css";
|
||||||
|
|
||||||
export const Sidebar: ParentComponent<{ as?: string, open?: boolean, name?: string }> = (props) => {
|
export const Sidebar: ParentComponent<{ as?: string, open?: boolean, name?: string }> = (props) => {
|
||||||
const [open, setOpen] = createSignal(props.open ?? true)
|
const [open, setOpen] = createSignal(props.open ?? true);
|
||||||
const cssClass = createMemo(() => open() ? css.open : css.closed);
|
|
||||||
const name = createMemo(() => props.name ?? 'sidebar');
|
const name = createMemo(() => props.name ?? 'sidebar');
|
||||||
|
|
||||||
const toggle = () => setOpen(o => !o);
|
return <Dynamic component={props.as ?? 'div'} class={`${css.root} ${open() ? css.open : css.closed}`}>
|
||||||
|
<button
|
||||||
let ref: Element;
|
role="button"
|
||||||
return <Dynamic component={props.as ?? 'div'} class={`${css.root} ${cssClass()}`} ref={ref}>
|
onclick={() => setOpen(o => !o)}
|
||||||
<Portal mount={ref!} useShadow={true}>
|
title={`${open() ? 'close' : 'open'} ${name()}`}
|
||||||
<button onclick={() => toggle()} role="button" title={`${open() ? 'close' : 'open'} ${name()}`}>
|
>
|
||||||
<Show when={open()} fallback={<TbLayoutSidebarLeftExpand />}>
|
<Show when={open()} fallback={<TbLayoutSidebarLeftExpand />}>
|
||||||
<TbLayoutSidebarLeftCollapse />
|
<TbLayoutSidebarLeftCollapse />
|
||||||
</Show>
|
</Show>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class={css.content}>
|
<div class={css.content}>
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</Portal>
|
|
||||||
|
|
||||||
{props.children}
|
{props.children}
|
||||||
|
</div>
|
||||||
</Dynamic>
|
</Dynamic>
|
||||||
};
|
};
|
|
@ -1,6 +1,10 @@
|
||||||
.root {
|
.root {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto 1fr;
|
grid: auto minmax(0, 1fr) / repeat(var(--tab-count), auto);
|
||||||
|
justify-content: start;
|
||||||
|
|
||||||
|
inline-size: 100%;
|
||||||
|
block-size: 100%;
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
display: contents;
|
display: contents;
|
||||||
|
@ -8,13 +12,37 @@
|
||||||
& > summary {
|
& > summary {
|
||||||
grid-row: 1 / 1;
|
grid-row: 1 / 1;
|
||||||
|
|
||||||
|
padding: var(--padding-s) var(--padding-m);
|
||||||
|
|
||||||
&::marker {
|
&::marker {
|
||||||
content: none;
|
content: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&::details-content {
|
&::details-content {
|
||||||
grid-area: 2 / 1;
|
grid-area: 2 / 1 / span 1 / span var(--tab-count);
|
||||||
|
display: none;
|
||||||
|
grid: 100% / 100%;
|
||||||
|
inline-size: 100%;
|
||||||
|
block-size: 100%;
|
||||||
|
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[open] {
|
||||||
|
& > summary {
|
||||||
|
background-color: var(--surface-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::details-content {
|
||||||
|
display: grid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@property --tab-count {
|
||||||
|
syntax: '<integer>';
|
||||||
|
inherits: true;
|
||||||
|
initial-value: 0;
|
||||||
|
}
|
|
@ -1,72 +1,21 @@
|
||||||
import { Accessor, children, Component, createContext, createEffect, createMemo, createSignal, createUniqueId, For, JSX, ParentComponent, useContext } from "solid-js";
|
import { Accessor, children, createContext, createMemo, createSignal, createUniqueId, For, JSX, ParentComponent, useContext } from "solid-js";
|
||||||
import { createStore } from "solid-js/store";
|
|
||||||
import css from "./tabs.module.css";
|
import css from "./tabs.module.css";
|
||||||
import { Portal } from "solid-js/web";
|
|
||||||
|
|
||||||
interface TabsContextType {
|
interface TabsContextType {
|
||||||
isActive(): boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TabsState {
|
|
||||||
tabs: TabType[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TabType {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TabsContext = createContext<TabsContextType>();
|
|
||||||
|
|
||||||
export const Tabs: Component<{ children?: JSX.Element }> = (props) => {
|
|
||||||
const [state, setState] = createStore<TabsState>({ 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 <TabsContext.Provider value={ctx}>
|
|
||||||
<header>
|
|
||||||
<For each={state.tabs}>{
|
|
||||||
tab => <button type="button" onpointerdown={() => activate(tab.id)}>{tab.label}</button>
|
|
||||||
}</For>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{props.children}
|
|
||||||
</TabsContext.Provider>
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Tab: ParentComponent<{ label: string }> = (props) => {
|
|
||||||
const context = useContext(TabsContext);
|
|
||||||
|
|
||||||
return <div id={createUniqueId()} data-label={props.label}>{props.children}</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TabsSimpleContextType {
|
|
||||||
activate(id: string): void;
|
activate(id: string): void;
|
||||||
active: Accessor<string | undefined>;
|
active: Accessor<string | undefined>;
|
||||||
isActive(id: string): Accessor<boolean>;
|
isActive(id: string): Accessor<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TabsSimpleContext = createContext<TabsSimpleContextType>();
|
const TabsContext = createContext<TabsContextType>();
|
||||||
|
|
||||||
export const TabsSimple: ParentComponent = (props) => {
|
export const Tabs: ParentComponent = (props) => {
|
||||||
const [active, setActive] = createSignal<string | undefined>(undefined);
|
const [active, setActive] = createSignal<string | undefined>(undefined);
|
||||||
|
const numberOfTabs = createMemo(() => children(() => props.children).toArray().length);
|
||||||
|
|
||||||
return <TabsSimpleContext.Provider value={{
|
return <TabsContext.Provider value={{
|
||||||
activate(id: string) {
|
activate(id: string) {
|
||||||
setActive(id);
|
setActive(id);
|
||||||
// setState('active', id);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
active,
|
active,
|
||||||
|
@ -75,15 +24,15 @@ export const TabsSimple: ParentComponent = (props) => {
|
||||||
return createMemo(() => active() === id);
|
return createMemo(() => active() === id);
|
||||||
},
|
},
|
||||||
}}>
|
}}>
|
||||||
<div class={css.root}>
|
<div class={css.root} style={{ '--tab-count': numberOfTabs() }}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
</TabsSimpleContext.Provider>;
|
</TabsContext.Provider>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TabSimple: ParentComponent<{ label: string }> = (props) => {
|
export const Tab: ParentComponent<{ label: string }> = (props) => {
|
||||||
const id = `tab-${createUniqueId()}`;
|
const id = `tab-${createUniqueId()}`;
|
||||||
const context = useContext(TabsSimpleContext);
|
const context = useContext(TabsContext);
|
||||||
|
|
||||||
if (!context) {
|
if (!context) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: var(--radii-m);
|
border-radius: var(--radii-m);
|
||||||
|
|
||||||
&:focus-within {
|
&:has(textarea:focus) {
|
||||||
border-color: var(--info);
|
border-color: var(--info);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,9 +25,9 @@ export interface SelectionContextType {
|
||||||
select(key: string, select: boolean): void;
|
select(key: string, select: boolean): void;
|
||||||
}
|
}
|
||||||
export interface GridContextType {
|
export interface GridContextType {
|
||||||
mutate(prop: string, lang: string, value: string): void;
|
rows: Record<string, { [lang: string]: { original: string, value: string } }>;
|
||||||
add(prop: string): void;
|
|
||||||
selection: SelectionContextType;
|
selection: SelectionContextType;
|
||||||
|
mutate(prop: string, lang: string, value: string): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SelectionContext = createContext<SelectionContextType>();
|
const SelectionContext = createContext<SelectionContextType>();
|
||||||
|
@ -77,9 +77,7 @@ const SelectionProvider: ParentComponent<{ rows: Map<string, { [lang: string]: {
|
||||||
</SelectionContext.Provider>;
|
</SelectionContext.Provider>;
|
||||||
};
|
};
|
||||||
const GridProvider: ParentComponent<{ rows: Map<string, { [lang: string]: { value: string, handle: FileSystemFileHandle } }>, context?: (ctx: GridContextType) => any }> = (props) => {
|
const GridProvider: ParentComponent<{ rows: Map<string, { [lang: string]: { value: string, handle: FileSystemFileHandle } }>, context?: (ctx: GridContextType) => any }> = (props) => {
|
||||||
type Entry = { [lang: string]: { original: string, value: string } };
|
const [state, setState] = createStore<{ rows: GridContextType['rows'], numberOfRows: number }>({
|
||||||
|
|
||||||
const [state, setState] = createStore<{ rows: { [prop: string]: Entry }, numberOfRows: number }>({
|
|
||||||
rows: {},
|
rows: {},
|
||||||
numberOfRows: 0,
|
numberOfRows: 0,
|
||||||
});
|
});
|
||||||
|
@ -96,30 +94,27 @@ const GridProvider: ParentComponent<{ rows: Map<string, { [lang: string]: { valu
|
||||||
setState('numberOfRows', Object.keys(state.rows).length);
|
setState('numberOfRows', Object.keys(state.rows).length);
|
||||||
});
|
});
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
console.log(state.rows.toplevel?.nl.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
const ctx: GridContextType = {
|
const ctx: GridContextType = {
|
||||||
|
rows: state.rows,
|
||||||
|
selection: undefined!,
|
||||||
|
|
||||||
mutate(prop: string, lang: string, value: string) {
|
mutate(prop: string, lang: string, value: string) {
|
||||||
// setState('rows', prop, lang, ({ original }) => ({ original, value }));
|
|
||||||
setState('rows', produce(rows => {
|
setState('rows', produce(rows => {
|
||||||
rows[prop][lang].value = value;
|
rows[prop][lang].value = value;
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
add(prop: string) {
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
selection: undefined!,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
console.log(ctx);
|
|
||||||
props.context?.(ctx);
|
props.context?.(ctx);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mutated = createMemo(() => Object.values(state.rows).filter(entry => Object.values(entry).some(lang => lang.original !== lang.value)));
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
console.log('tap', mutated());
|
||||||
|
});
|
||||||
|
|
||||||
return <GridContext.Provider value={ctx}>
|
return <GridContext.Provider value={ctx}>
|
||||||
<SelectionProvider rows={props.rows} context={(selction) => ctx.selection = selction}>
|
<SelectionProvider rows={props.rows} context={(selction) => ctx.selection = selction}>
|
||||||
{props.children}
|
{props.children}
|
||||||
|
|
|
@ -62,7 +62,7 @@ export const FilesProvider = (props) => {
|
||||||
return <FilesContext.Provider value={ctx}>{props.children}</FilesContext.Provider>;
|
return <FilesContext.Provider value={ctx}>{props.children}</FilesContext.Provider>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useFiles = () => useContext(FilesContext)!;
|
export const useFiles = () => useContext(FilesContext);
|
||||||
|
|
||||||
export const load = (file: File): Promise<Map<string, string> | undefined> => {
|
export const load = (file: File): Promise<Map<string, string> | undefined> => {
|
||||||
switch (file.type) {
|
switch (file.type) {
|
||||||
|
|
5
src/features/selectable/index.module.css
Normal file
5
src/features/selectable/index.module.css
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
.selectable {
|
||||||
|
&[data-selected="true"] {
|
||||||
|
background-color: color(from var(--info) xyz x y z / .2);
|
||||||
|
}
|
||||||
|
}
|
236
src/features/selectable/index.tsx
Normal file
236
src/features/selectable/index.tsx
Normal file
|
@ -0,0 +1,236 @@
|
||||||
|
import { Accessor, children, createContext, createEffect, createMemo, createRenderEffect, createSignal, createUniqueId, onCleanup, onMount, ParentComponent, useContext } from "solid-js";
|
||||||
|
import { createStore, produce } from "solid-js/store";
|
||||||
|
import { isServer } from "solid-js/web";
|
||||||
|
import css from "./index.module.css";
|
||||||
|
|
||||||
|
export interface SelectionContextType {
|
||||||
|
selection(): object[];
|
||||||
|
select(selection: string[], options?: Partial<{ append: boolean }>): void;
|
||||||
|
selectAll(): void;
|
||||||
|
clear(): void;
|
||||||
|
isSelected(key: string): Accessor<boolean>;
|
||||||
|
add(key: string, value: object, element: HTMLElement): void;
|
||||||
|
}
|
||||||
|
export type SelectionHandler = (selection: object[]) => any;
|
||||||
|
|
||||||
|
enum Modifier {
|
||||||
|
None = 0,
|
||||||
|
Shift = 1 << 0,
|
||||||
|
Control = 1 << 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
const SelectionContext = createContext<SelectionContextType>();
|
||||||
|
|
||||||
|
const useSelection = () => {
|
||||||
|
const context = useContext(SelectionContext);
|
||||||
|
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('selection context is used outside of a provider');
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
selection: string[],
|
||||||
|
data: { key: string, value: Accessor<any>, element: HTMLElement }[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SelectionProvider: ParentComponent<{ selection?: SelectionHandler }> = (props) => {
|
||||||
|
const [state, setState] = createStore<State>({ selection: [], data: [] });
|
||||||
|
const selection = createMemo(() => state.data.filter(({ key }) => state.selection.includes(key)));
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
props.selection?.(selection().map(({ value }) => value()));
|
||||||
|
});
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
selection,
|
||||||
|
select(selection: string[]) {
|
||||||
|
setState('selection', selection);
|
||||||
|
},
|
||||||
|
selectAll() {
|
||||||
|
setState('selection', state.data.map(({ key }) => key));
|
||||||
|
},
|
||||||
|
clear() {
|
||||||
|
setState('selection', []);
|
||||||
|
},
|
||||||
|
isSelected(key: string) {
|
||||||
|
return createMemo(() => state.selection.includes(key));
|
||||||
|
},
|
||||||
|
add(key: string, value: Accessor<any>, element: HTMLElement) {
|
||||||
|
setState('data', data => [...data, { key, value, element }]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return <SelectionContext.Provider value={context}>
|
||||||
|
<Root>{props.children}</Root>
|
||||||
|
</SelectionContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Root: ParentComponent = (props) => {
|
||||||
|
const context = useSelection();
|
||||||
|
const c = children(() => props.children);
|
||||||
|
|
||||||
|
const [modifier, setModifier] = createSignal<Modifier>(Modifier.None);
|
||||||
|
const [latest, setLatest] = createSignal<HTMLElement>();
|
||||||
|
const [root, setRoot] = createSignal<HTMLElement>();
|
||||||
|
const selectables = createMemo(() => {
|
||||||
|
const r = root();
|
||||||
|
|
||||||
|
if (!r) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from((function* () {
|
||||||
|
const iterator = document.createTreeWalker(r, NodeFilter.SHOW_ELEMENT, {
|
||||||
|
acceptNode: (node: HTMLElement) => node.dataset.selectionKey ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP,
|
||||||
|
});
|
||||||
|
|
||||||
|
while (iterator.nextNode()) {
|
||||||
|
yield iterator.currentNode;
|
||||||
|
}
|
||||||
|
})());
|
||||||
|
});
|
||||||
|
|
||||||
|
createRenderEffect(() => {
|
||||||
|
const children = c.toArray();
|
||||||
|
const r = root();
|
||||||
|
|
||||||
|
if (!r) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log(r, children, Array.from((function* () {
|
||||||
|
const iterator = document.createTreeWalker(r, NodeFilter.SHOW_ELEMENT, {
|
||||||
|
acceptNode: (node: HTMLElement) => node.dataset.selectionKey ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP,
|
||||||
|
});
|
||||||
|
|
||||||
|
while (iterator.nextNode()) {
|
||||||
|
console.log(iterator.currentNode);
|
||||||
|
|
||||||
|
yield iterator.currentNode;
|
||||||
|
}
|
||||||
|
})()));
|
||||||
|
}, 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
const createRange = (a?: HTMLElement, b?: HTMLElement): string[] => {
|
||||||
|
if (!a && !b) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!a) {
|
||||||
|
return [b!.dataset.selecatableKey!];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!b) {
|
||||||
|
return [a!.dataset.selecatableKey!];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a === b) {
|
||||||
|
return [a!.dataset.selecatableKey!];
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodes = selectables();
|
||||||
|
const aIndex = nodes.indexOf(a);
|
||||||
|
const bIndex = nodes.indexOf(b);
|
||||||
|
const selection = nodes.slice(Math.min(aIndex, bIndex), Math.max(aIndex, bIndex) + 1);
|
||||||
|
|
||||||
|
console.log(aIndex, bIndex, nodes,);
|
||||||
|
|
||||||
|
return selection.map(n => n.dataset.selectionKey);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPointerDown = (e: PointerEvent) => {
|
||||||
|
const key = e.target?.dataset.selectionKey;
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shift = Boolean(modifier() & Modifier.Shift);
|
||||||
|
const append = Boolean(modifier() & Modifier.Control);
|
||||||
|
|
||||||
|
// Logic table
|
||||||
|
// shift | control | behavior |
|
||||||
|
// ------|---------|---------------------------------------------------|
|
||||||
|
// true | true | create range from latest to current and append |
|
||||||
|
// true | false | create range from latest to current and overwrite |
|
||||||
|
// false | true | append |
|
||||||
|
// false | false | overwrite / set |
|
||||||
|
|
||||||
|
context.select(shift ? createRange(latest(), e.target as HTMLElement) : [key], { append });
|
||||||
|
setLatest(e.target);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKeyboardEvent = (e: KeyboardEvent) => {
|
||||||
|
if (e.repeat || ['Control', 'Shift'].includes(e.key) === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setModifier(state => {
|
||||||
|
if (e.shiftKey) {
|
||||||
|
state |= Modifier.Shift;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
state &= ~Modifier.Shift;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.ctrlKey) {
|
||||||
|
state |= Modifier.Control;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
state &= ~Modifier.Control;
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
document.addEventListener('pointerdown', onPointerDown);
|
||||||
|
document.addEventListener('keydown', onKeyboardEvent);
|
||||||
|
document.addEventListener('keyup', onKeyboardEvent);
|
||||||
|
});
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
if (isServer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.removeEventListener('pointerdown', onPointerDown);
|
||||||
|
document.removeEventListener('keydown', onKeyboardEvent);
|
||||||
|
document.removeEventListener('keyup', onKeyboardEvent);
|
||||||
|
});
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
console.log(selectables());
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div ref={setRoot} style={{ 'display': 'contents' }}>{c()}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const selectable = (element: HTMLElement, value: Accessor<any>) => {
|
||||||
|
const context = useSelection();
|
||||||
|
const key = createUniqueId();
|
||||||
|
const isSelected = context.isSelected(key);
|
||||||
|
|
||||||
|
context.add(key, value, element);
|
||||||
|
|
||||||
|
createRenderEffect(() => {
|
||||||
|
element.dataset.selected = isSelected() ? 'true' : undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
element.classList.add(css.selectable);
|
||||||
|
element.dataset.selectionKey = key;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare module "solid-js" {
|
||||||
|
namespace JSX {
|
||||||
|
interface Directives {
|
||||||
|
selectable: any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { Menu } from "~/features/menu";
|
import { Menu } from "~/features/menu";
|
||||||
import { Sidebar } from "~/components/sidebar";
|
import { Sidebar } from "~/components/sidebar";
|
||||||
import { Component, createEffect, createResource, createSignal, For, onMount, Show } from "solid-js";
|
import { Component, createEffect, createMemo, createResource, createSignal, For, onMount, Show } from "solid-js";
|
||||||
import { Grid, load, useFiles } from "~/features/file";
|
import { Grid, load, useFiles } from "~/features/file";
|
||||||
import { createCommand, Modifier, noop } from "~/features/command";
|
import { createCommand, Modifier, noop } from "~/features/command";
|
||||||
import { GridContextType } from "~/features/file/grid";
|
import { GridContextType } from "~/features/file/grid";
|
||||||
|
@ -108,6 +108,12 @@ export default function Edit(props) {
|
||||||
}, { key: 'a', modifier: Modifier.Control }),
|
}, { key: 'a', modifier: Modifier.Control }),
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
const mutated = createMemo(() => Object.values(ctx()?.rows ?? {}).filter(row => Object.values(row).some(lang => lang.original !== lang.value)));
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
console.log('KAAS', mutated());
|
||||||
|
});
|
||||||
|
|
||||||
return <div class={css.root}>
|
return <div class={css.root}>
|
||||||
<Menu.Root>
|
<Menu.Root>
|
||||||
<Menu.Item label="file">
|
<Menu.Item label="file">
|
||||||
|
@ -131,7 +137,7 @@ export default function Edit(props) {
|
||||||
|
|
||||||
<Sidebar as="aside">
|
<Sidebar as="aside">
|
||||||
<Tree entries={tree().entries}>{
|
<Tree entries={tree().entries}>{
|
||||||
file => <span>{file().name}</span>
|
(file, icon) => <span>{icon} {file().name}</span>
|
||||||
}</Tree>
|
}</Tree>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
section.index {
|
section.index {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid: 100% / auto 1fr;
|
grid: 100% / auto minmax(0, 1fr);
|
||||||
inline-size: 100%;
|
inline-size: 100%;
|
||||||
block-size: 100%;
|
block-size: 100%;
|
||||||
|
|
||||||
|
@ -17,6 +17,11 @@ section.index {
|
||||||
}
|
}
|
||||||
|
|
||||||
& > section {
|
& > section {
|
||||||
|
display: grid;
|
||||||
|
grid: 100% / 100%;
|
||||||
|
inline-size: 100%;
|
||||||
|
block-size: 100%;
|
||||||
|
|
||||||
padding-inline: 1em;
|
padding-inline: 1em;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,11 +1,12 @@
|
||||||
import { createEffect, createMemo, createResource, createSignal, For, lazy, onMount, Suspense } from "solid-js";
|
import { Component, createEffect, createMemo, createResource, createSignal, For, lazy, onMount, Suspense } from "solid-js";
|
||||||
import { useFiles } from "~/features/file";
|
import { useFiles } from "~/features/file";
|
||||||
import { Menu } from "~/features/menu";
|
import { Menu } from "~/features/menu";
|
||||||
import "./experimental.css";
|
|
||||||
import { createCommand, Modifier } from "~/features/command";
|
import { createCommand, Modifier } from "~/features/command";
|
||||||
import { emptyFolder, FolderEntry, Tree, walk } from "~/components/filetree";
|
import { emptyFolder, FolderEntry, Tree, walk } from "~/components/filetree";
|
||||||
import { createStore, produce } from "solid-js/store";
|
import { createStore, produce } from "solid-js/store";
|
||||||
import { Tab, Tabs, TabSimple, TabsSimple } from "~/components/tabs";
|
import { Tab, Tabs } from "~/components/tabs";
|
||||||
|
import "./experimental.css";
|
||||||
|
import { selectable, SelectionProvider } from "~/features/selectable";
|
||||||
|
|
||||||
interface ExperimentalState {
|
interface ExperimentalState {
|
||||||
files: File[];
|
files: File[];
|
||||||
|
@ -21,7 +22,7 @@ export default function Experimental() {
|
||||||
});
|
});
|
||||||
const [showHiddenFiles, setShowHiddenFiles] = createSignal<boolean>(false);
|
const [showHiddenFiles, setShowHiddenFiles] = createSignal<boolean>(false);
|
||||||
const filters = createMemo<RegExp[]>(() => showHiddenFiles() ? [/^node_modules$/] : [/^node_modules$/, /^\..+$/]);
|
const filters = createMemo<RegExp[]>(() => showHiddenFiles() ? [/^node_modules$/] : [/^node_modules$/, /^\..+$/]);
|
||||||
const [root, { mutate, refetch }] = createResource(() => files.get('root'));
|
const [root, { mutate, refetch }] = createResource(() => files?.get('root'));
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
setState('numberOfFiles', state.files.length);
|
setState('numberOfFiles', state.files.length);
|
||||||
|
@ -84,12 +85,6 @@ export default function Experimental() {
|
||||||
}, { key: 's', modifier: Modifier.Control | Modifier.Shift }),
|
}, { key: 's', modifier: Modifier.Control | Modifier.Shift }),
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const Content = lazy(async () => {
|
|
||||||
const text = Promise.resolve('this is text');
|
|
||||||
|
|
||||||
return { default: () => <>{text}</> };
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Menu.Root>
|
<Menu.Root>
|
||||||
|
@ -107,23 +102,31 @@ export default function Experimental() {
|
||||||
<section class="index">
|
<section class="index">
|
||||||
<aside>
|
<aside>
|
||||||
<label><input type="checkbox" on:input={() => setShowHiddenFiles(v => !v)} />Show hidden files</label>
|
<label><input type="checkbox" on:input={() => setShowHiddenFiles(v => !v)} />Show hidden files</label>
|
||||||
<Tree entries={tree().entries}>{
|
<Tree entries={tree().entries} open={open}>{
|
||||||
file => <span on:dblclick={() => open(file().meta)}>{file().name}</span>
|
file => file().name
|
||||||
}</Tree>
|
}</Tree>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<TabsSimple>
|
<Tabs>
|
||||||
<For each={state.files}>{
|
<For each={state.files}>{
|
||||||
file => <TabSimple label={file.name}>
|
file => <Tab label={file.name}>
|
||||||
<pre>
|
<Content file={file} />
|
||||||
<Suspense><Content /></Suspense>
|
</Tab>
|
||||||
</pre>
|
|
||||||
</TabSimple>
|
|
||||||
}</For>
|
}</For>
|
||||||
</TabsSimple>
|
</Tabs>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Content: Component<{ file: File }> = (props) => {
|
||||||
|
const [content] = createResource(async () => {
|
||||||
|
return await props.file.text();
|
||||||
|
});
|
||||||
|
|
||||||
|
return <Suspense fallback={'loading'}>
|
||||||
|
<pre>{content()}</pre>
|
||||||
|
</Suspense>
|
||||||
|
};
|
Loading…
Add table
Add a link
Reference in a new issue