fix issues with tabs
This commit is contained in:
parent
b27abe928d
commit
addc6bfb11
14 changed files with 210 additions and 137 deletions
|
@ -10,7 +10,6 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"start_url": ".",
|
"start_url": ".",
|
||||||
"display": "standalone",
|
|
||||||
"display_override": [
|
"display_override": [
|
||||||
"window-controls-overlay"
|
"window-controls-overlay"
|
||||||
],
|
],
|
||||||
|
|
|
@ -49,6 +49,7 @@ body {
|
||||||
grid: 100% / 100%;
|
grid: 100% / 100%;
|
||||||
|
|
||||||
background-color: var(--surface-1);
|
background-color: var(--surface-1);
|
||||||
|
color: var(--text-2);
|
||||||
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
|
@ -68,7 +69,7 @@ a {
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
color: var(--primary);
|
color: var(--text-1);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-size: 4rem;
|
font-size: 4rem;
|
||||||
font-weight: 100;
|
font-weight: 100;
|
||||||
|
|
|
@ -1,4 +1,22 @@
|
||||||
.picker {
|
.picker {
|
||||||
border: none;
|
display: flex;
|
||||||
background-color: var(--surface-3);
|
flex-flow: row;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: var(--radii-m);
|
||||||
|
padding: var(--padding-s);
|
||||||
|
|
||||||
|
& select {
|
||||||
|
border: none;
|
||||||
|
background-color: var(--surface-3);
|
||||||
|
border-radius: var(--radii-m);
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(:focus-visible) {
|
||||||
|
border-color: var(--info);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
import { Accessor, Component, createEffect, createSignal, For, Setter } from "solid-js";
|
import { Accessor, Component, createEffect, createSignal, For, Setter } from "solid-js";
|
||||||
import css from './colorschemepicker.module.css';
|
import css from './colorschemepicker.module.css';
|
||||||
|
import { CgDarkMode } from "solid-icons/cg";
|
||||||
|
|
||||||
export enum ColorScheme {
|
export enum ColorScheme {
|
||||||
Auto = 'light dark',
|
Auto = 'light dark',
|
||||||
|
@ -31,13 +32,17 @@ export const ColorSchemePicker: Component<ColorSchemePickerProps> = (props) => {
|
||||||
setter(currentValue);
|
setter(currentValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
return <select class={css.picker} name="color-scheme-picker" value={value()} onInput={(e) => {
|
return <label class={css.picker}>
|
||||||
if (e.target.value !== value()) {
|
<CgDarkMode />
|
||||||
setValue(e.target.value as any);
|
|
||||||
}
|
<select name="color-scheme-picker" value={value()} onInput={(e) => {
|
||||||
}}>
|
if (e.target.value !== value()) {
|
||||||
<For each={colorSchemeEntries}>{
|
setValue(e.target.value as any);
|
||||||
([value, label]) => <option value={value}>{label}</option>
|
}
|
||||||
}</For>
|
}}>
|
||||||
</select>;
|
<For each={colorSchemeEntries}>{
|
||||||
|
([value, label]) => <option value={value}>{label}</option>
|
||||||
|
}</For>
|
||||||
|
</select>
|
||||||
|
</label>;
|
||||||
};
|
};
|
|
@ -1,19 +1,14 @@
|
||||||
import { Accessor, children, createContext, createEffect, createMemo, createSignal, For, onCleanup, ParentComponent, Setter, Show, useContext } from "solid-js";
|
import { Accessor, children, createContext, createEffect, createMemo, createSignal, For, JSX, onCleanup, ParentComponent, Setter, Show, useContext } from "solid-js";
|
||||||
import { IoCloseCircleOutline } from "solid-icons/io";
|
import { IoCloseCircleOutline } from "solid-icons/io";
|
||||||
import css from "./tabs.module.css";
|
import css from "./tabs.module.css";
|
||||||
import { Command, CommandType, commandArguments, noop, useCommands } from "~/features/command";
|
import { Command, CommandType, noop, useCommands } from "~/features/command";
|
||||||
|
|
||||||
commandArguments;
|
|
||||||
|
|
||||||
interface TabsContextType {
|
interface TabsContextType {
|
||||||
register(id: string, label: string, options?: Partial<TabOptions>): Accessor<boolean>;
|
activate(id: string | undefined): void;
|
||||||
|
isActive(id: string): Accessor<boolean>;
|
||||||
readonly onClose: Accessor<CommandType<[string]> | undefined>
|
readonly onClose: Accessor<CommandType<[string]> | undefined>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TabOptions {
|
|
||||||
closable: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TabsContext = createContext<TabsContextType>();
|
const TabsContext = createContext<TabsContextType>();
|
||||||
|
|
||||||
const useTabs = () => {
|
const useTabs = () => {
|
||||||
|
@ -29,29 +24,39 @@ const useTabs = () => {
|
||||||
export const Tabs: ParentComponent<{ active?: Setter<string | undefined>, onClose?: CommandType<[string]> }> = (props) => {
|
export const Tabs: ParentComponent<{ active?: Setter<string | undefined>, onClose?: CommandType<[string]> }> = (props) => {
|
||||||
const commandsContext = useCommands();
|
const commandsContext = useCommands();
|
||||||
const [active, setActive] = createSignal<string | undefined>(undefined);
|
const [active, setActive] = createSignal<string | undefined>(undefined);
|
||||||
const [tabs, setTabs] = createSignal<Map<string, { label: string, options: Partial<TabOptions> }>>(new Map());
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
props.active?.(active());
|
props.active?.(active());
|
||||||
});
|
});
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
setActive(tabs().keys().toArray().at(-1));
|
|
||||||
});
|
|
||||||
|
|
||||||
const ctx = {
|
const ctx = {
|
||||||
register(id: string, label: string, options: Partial<TabOptions>) {
|
activate(id: string) {
|
||||||
setTabs(tabs => {
|
setActive(id);
|
||||||
tabs.set(id, { label, options });
|
},
|
||||||
|
|
||||||
return new Map(tabs);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
isActive(id: string) {
|
||||||
return createMemo(() => active() === id);
|
return createMemo(() => active() === id);
|
||||||
},
|
},
|
||||||
|
|
||||||
onClose: createMemo(() => props.onClose),
|
onClose: createMemo(() => props.onClose),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return <TabsContext.Provider value={ctx}>
|
||||||
|
<_Tabs active={active()} onClose={props.onClose}>{props.children}</_Tabs>
|
||||||
|
</TabsContext.Provider >;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _Tabs: ParentComponent<{ active: string | undefined, onClose?: CommandType<[string]> }> = (props) => {
|
||||||
|
const commandsContext = useCommands();
|
||||||
|
const tabsContext = useTabs();
|
||||||
|
|
||||||
|
const resolved = children(() => props.children);
|
||||||
|
const tabs = createMemo(() => resolved.toArray().filter(c => c instanceof HTMLElement).map(({ id, dataset }, i) => ({ id, label: dataset.tabLabel, options: { closable: dataset.tabClosable } })));
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
tabsContext.activate(tabs().at(-1)?.id);
|
||||||
|
});
|
||||||
|
|
||||||
const onClose = (e: Event) => {
|
const onClose = (e: Event) => {
|
||||||
if (!commandsContext || !props.onClose) {
|
if (!commandsContext || !props.onClose) {
|
||||||
return;
|
return;
|
||||||
|
@ -60,33 +65,44 @@ export const Tabs: ParentComponent<{ active?: Setter<string | undefined>, onClos
|
||||||
return commandsContext.execute(props.onClose, e);
|
return commandsContext.execute(props.onClose, e);
|
||||||
};
|
};
|
||||||
|
|
||||||
return <TabsContext.Provider value={ctx}>
|
return <div class={css.tabs}>
|
||||||
<div class={css.tabs}>
|
<header>
|
||||||
<header>
|
<For each={tabs()}>{
|
||||||
<For each={tabs().entries().toArray()}>{
|
({ id, label, options: { closable = false } }) => <Command.Context for={props.onClose} with={[id]}>
|
||||||
([id, { label, options: { closable = false } }]) => <Command.Context for={props.onClose} with={[id]}>
|
<span class={css.handle} classList={{ [css.active]: props.active === id }}>
|
||||||
<span class={css.handle} classList={{ [css.active]: active() === id }}>
|
<button onpointerdown={() => tabsContext.activate(id)}>{label}</button>
|
||||||
<button onpointerdown={() => setActive(id)}>{label}</button>
|
<Show when={closable}>
|
||||||
<Show when={closable}>
|
<button onPointerDown={onClose}> <IoCloseCircleOutline /></button>
|
||||||
<button onPointerDown={onClose}> <IoCloseCircleOutline /></button>
|
</Show>
|
||||||
</Show>
|
</span>
|
||||||
</span>
|
</Command.Context>
|
||||||
</Command.Context>
|
}</For>
|
||||||
}</For>
|
</header>
|
||||||
</header>
|
|
||||||
|
|
||||||
{props.children}
|
{resolved()}
|
||||||
</div>
|
</div>;
|
||||||
</TabsContext.Provider >;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export const Tab: ParentComponent<{ id: string, label: string, closable?: boolean }> = (props) => {
|
export const Tab: ParentComponent<{ id: string, label: string, closable?: boolean }> = (props) => {
|
||||||
const context = useTabs();
|
const context = useTabs();
|
||||||
|
|
||||||
const isActive = context.register(props.id, props.label, {
|
|
||||||
closable: props.closable ?? false
|
|
||||||
});
|
|
||||||
const resolved = children(() => props.children);
|
const resolved = children(() => props.children);
|
||||||
|
const isActive = context.isActive(props.id);
|
||||||
|
const [ref, setRef] = createSignal();
|
||||||
|
|
||||||
return <Show when={isActive()}><Command.Context for={context.onClose()} with={[props.id]}>{resolved()}</Command.Context></Show>;
|
// const isActive = context.register(props.id, props.label, {
|
||||||
|
// closable: props.closable ?? false,
|
||||||
|
// ref: ref,
|
||||||
|
// });
|
||||||
|
|
||||||
|
return <div
|
||||||
|
ref={setRef()}
|
||||||
|
id={props.id}
|
||||||
|
data-tab-label={props.label}
|
||||||
|
data-tab-closable={props.closable}
|
||||||
|
style="dispay: contents;"
|
||||||
|
>
|
||||||
|
<Show when={isActive()}>
|
||||||
|
<Command.Context for={context.onClose() ?? noop} with={[props.id]}>{resolved()}</Command.Context>
|
||||||
|
</Show>
|
||||||
|
</div>;
|
||||||
}
|
}
|
|
@ -31,24 +31,28 @@ const Root: ParentComponent<{ commands: CommandType[] }> = (props) => {
|
||||||
},
|
},
|
||||||
|
|
||||||
execute<T extends any[] = any[]>(command: CommandType<T>, event: Event): boolean | undefined {
|
execute<T extends any[] = any[]>(command: CommandType<T>, event: Event): boolean | undefined {
|
||||||
const contexts = contextualArguments.get(command);
|
const args = ((): T => {
|
||||||
|
|
||||||
if (contexts === undefined) {
|
const contexts = contextualArguments.get(command);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const element = event.composedPath().find(el => contexts.has(el));
|
if (contexts === undefined) {
|
||||||
|
return [] as any;
|
||||||
|
}
|
||||||
|
|
||||||
if (element === undefined) {
|
const element = event.composedPath().find(el => contexts.has(el));
|
||||||
return;
|
|
||||||
}
|
if (element === undefined) {
|
||||||
|
return [] as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = contexts.get(element)! as Accessor<T>;
|
||||||
|
return args();
|
||||||
|
})();
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
const args = contexts.get(element)! as Accessor<T>;
|
command(...args);
|
||||||
|
|
||||||
command(...args());
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
@ -159,17 +163,6 @@ export const createCommand = <TArgs extends any[] = []>(label: string, command:
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const commandArguments = <T extends any[] = any[]>(element: Element, commandAndArgs: Accessor<[CommandType<T>, 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', () => { }), {
|
export const noop = Object.defineProperties(createCommand('noop', () => { }), {
|
||||||
withLabel: {
|
withLabel: {
|
||||||
value(label: string) {
|
value(label: string) {
|
||||||
|
@ -180,12 +173,4 @@ export const noop = Object.defineProperties(createCommand('noop', () => { }), {
|
||||||
},
|
},
|
||||||
}) as CommandType & { withLabel(label: string): CommandType };
|
}) as CommandType & { withLabel(label: string): CommandType };
|
||||||
|
|
||||||
declare module "solid-js" {
|
|
||||||
namespace JSX {
|
|
||||||
interface Directives {
|
|
||||||
commandArguments<T extends any[] = any[]>(): [CommandType<T>, T];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Context } from './contextMenu';
|
export { Context } from './contextMenu';
|
|
@ -4,6 +4,8 @@ import { createStore } from "solid-js/store";
|
||||||
import { isServer } from "solid-js/web";
|
import { isServer } from "solid-js/web";
|
||||||
import * as json from './parser/json';
|
import * as json from './parser/json';
|
||||||
|
|
||||||
|
const ROOT = '__root__';
|
||||||
|
|
||||||
interface FileEntity {
|
interface FileEntity {
|
||||||
key: string;
|
key: string;
|
||||||
handle: FileSystemDirectoryHandle;
|
handle: FileSystemDirectoryHandle;
|
||||||
|
@ -25,7 +27,9 @@ interface InternalFilesContextType {
|
||||||
|
|
||||||
interface FilesContextType {
|
interface FilesContextType {
|
||||||
readonly files: Accessor<FileEntity[]>,
|
readonly files: Accessor<FileEntity[]>,
|
||||||
|
readonly root: Accessor<FileSystemDirectoryHandle | undefined>,
|
||||||
|
|
||||||
|
open(directory: FileSystemDirectoryHandle): void;
|
||||||
get(key: string): Accessor<FileSystemDirectoryHandle | undefined>
|
get(key: string): Accessor<FileSystemDirectoryHandle | undefined>
|
||||||
set(key: string, handle: FileSystemDirectoryHandle): Promise<void>;
|
set(key: string, handle: FileSystemDirectoryHandle): Promise<void>;
|
||||||
remove(key: string): Promise<void>;
|
remove(key: string): Promise<void>;
|
||||||
|
@ -42,10 +46,16 @@ const clientContext = (): InternalFilesContextType => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onChange(hook: (key: string, handle: FileSystemDirectoryHandle) => any) {
|
onChange(hook: (key: string, handle: FileSystemDirectoryHandle) => any) {
|
||||||
const callHook = (key: string, handle: FileSystemDirectoryHandle) => setTimeout(() => hook(key, handle), 1);
|
const callHook = (key: string, handle: FileSystemDirectoryHandle) => {
|
||||||
|
if (!key || key === ROOT) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => hook(key, handle), 1);
|
||||||
|
};
|
||||||
|
|
||||||
db.files.hook('creating', (_: string, { key, handle }: FileEntity) => { callHook(key, handle); });
|
db.files.hook('creating', (_: string, { key, handle }: FileEntity) => { callHook(key, handle); });
|
||||||
db.files.hook('deleting', (_: string, { key, handle }: FileEntity) => callHook(key, handle));
|
db.files.hook('deleting', (_: string, { key, handle }: FileEntity = { key: undefined!, handle: undefined! }) => callHook(key, handle));
|
||||||
db.files.hook('updating', (_1: Object, _2: string, { key, handle }: FileEntity) => callHook(key, handle));
|
db.files.hook('updating', (_1: Object, _2: string, { key, handle }: FileEntity) => callHook(key, handle));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -59,13 +69,13 @@ const clientContext = (): InternalFilesContextType => {
|
||||||
return (await db.files.delete(key));
|
return (await db.files.delete(key));
|
||||||
},
|
},
|
||||||
async keys() {
|
async keys() {
|
||||||
return (await db.files.toArray()).map(f => f.key);
|
return (await db.files.where('key').notEqual(ROOT).toArray()).map(f => f.key);
|
||||||
},
|
},
|
||||||
async entries() {
|
async entries() {
|
||||||
return await db.files.toArray();
|
return await db.files.where('key').notEqual(ROOT).toArray();
|
||||||
},
|
},
|
||||||
async list() {
|
async list() {
|
||||||
const files = await db.files.toArray();
|
const files = await db.files.where('key').notEqual(ROOT).toArray();
|
||||||
|
|
||||||
return files.map(f => f.handle)
|
return files.map(f => f.handle)
|
||||||
},
|
},
|
||||||
|
@ -99,27 +109,35 @@ const serverContext = (): InternalFilesContextType => ({
|
||||||
export const FilesProvider: ParentComponent = (props) => {
|
export const FilesProvider: ParentComponent = (props) => {
|
||||||
const internal = isServer ? serverContext() : clientContext();
|
const internal = isServer ? serverContext() : clientContext();
|
||||||
|
|
||||||
const [state, setState] = createStore<{ files: FileEntity[] }>({ files: [] });
|
const [state, setState] = createStore<{ openedFiles: FileEntity[], root: FileSystemDirectoryHandle | undefined }>({ openedFiles: [], root: undefined });
|
||||||
|
|
||||||
const updateFilesInState = async () => {
|
internal.onChange(async () => {
|
||||||
const entities = await internal.entries();
|
setState('openedFiles', await internal.entries());
|
||||||
|
|
||||||
setState('files', entities);
|
|
||||||
};
|
|
||||||
|
|
||||||
internal.onChange((key: string, handle: FileSystemDirectoryHandle) => {
|
|
||||||
updateFilesInState();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
updateFilesInState();
|
(async () => {
|
||||||
|
const [root, files] = await Promise.all([
|
||||||
|
internal.get(ROOT),
|
||||||
|
internal.entries(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
setState('root', root);
|
||||||
|
setState('openedFiles', files);
|
||||||
|
})();
|
||||||
});
|
});
|
||||||
|
|
||||||
const context: FilesContextType = {
|
const context: FilesContextType = {
|
||||||
files: createMemo(() => state.files),
|
files: createMemo(() => state.openedFiles),
|
||||||
|
root: createMemo(() => state.root),
|
||||||
|
|
||||||
|
open(directory: FileSystemDirectoryHandle) {
|
||||||
|
setState('root', directory);
|
||||||
|
internal.set(ROOT, directory);
|
||||||
|
},
|
||||||
|
|
||||||
get(key: string): Accessor<FileSystemDirectoryHandle | undefined> {
|
get(key: string): Accessor<FileSystemDirectoryHandle | undefined> {
|
||||||
return createMemo(() => state.files.find(entity => entity.key === key)?.handle);
|
return createMemo(() => state.openedFiles.find(entity => entity.key === key)?.handle);
|
||||||
},
|
},
|
||||||
|
|
||||||
async set(key: string, handle: FileSystemDirectoryHandle) {
|
async set(key: string, handle: FileSystemDirectoryHandle) {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Accessor, Component, For, JSX, Match, ParentComponent, Setter, Show, Switch, children, createContext, createEffect, createMemo, createSignal, createUniqueId, mergeProps, onCleanup, onMount, useContext } from "solid-js";
|
import { Accessor, Component, For, JSX, Match, ParentComponent, Setter, Show, Switch, children, createContext, createEffect, createMemo, createSignal, createUniqueId, mergeProps, onCleanup, onMount, useContext } from "solid-js";
|
||||||
import { Portal } from "solid-js/web";
|
import { Portal } from "solid-js/web";
|
||||||
import { createStore } from "solid-js/store";
|
import { createStore } from "solid-js/store";
|
||||||
import { CommandType, Command } from "../command";
|
import { CommandType, Command, useCommands } from "../command";
|
||||||
import css from "./index.module.css";
|
import css from "./index.module.css";
|
||||||
|
|
||||||
export interface MenuContextType {
|
export interface MenuContextType {
|
||||||
|
@ -35,7 +35,7 @@ const MenuContext = createContext<MenuContextType>();
|
||||||
|
|
||||||
export const MenuProvider: ParentComponent<{ commands?: CommandType[] }> = (props) => {
|
export const MenuProvider: ParentComponent<{ commands?: CommandType[] }> = (props) => {
|
||||||
const [ref, setRef] = createSignal<Node | undefined>();
|
const [ref, setRef] = createSignal<Node | undefined>();
|
||||||
const [store, setStore] = createStore<{ items: Record<string, Item | ItemWithChildren> }>({ items: {} });
|
const [store, setStore] = createStore<{ items: Map<string, Item | ItemWithChildren> }>({ items: new Map });
|
||||||
|
|
||||||
const ctx = {
|
const ctx = {
|
||||||
ref,
|
ref,
|
||||||
|
@ -43,19 +43,19 @@ export const MenuProvider: ParentComponent<{ commands?: CommandType[] }> = (prop
|
||||||
addItems(items: (Item | ItemWithChildren)[]) {
|
addItems(items: (Item | ItemWithChildren)[]) {
|
||||||
return setStore('items', values => {
|
return setStore('items', values => {
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
values[item.id] = item;
|
values.set(item.id, item);
|
||||||
}
|
}
|
||||||
|
|
||||||
return values;
|
return new Map(values.entries());
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
items() {
|
items() {
|
||||||
return Object.values(store.items);
|
return store.items.values();
|
||||||
},
|
},
|
||||||
commands() {
|
commands() {
|
||||||
return Object.values(store.items)
|
return store.items.values()
|
||||||
.map(item => item.kind === 'node' ? item.children.filter(c => c.kind === 'leaf').map(c => c.command) : item.command)
|
.flatMap(item => item.kind === 'node' ? item.children.filter(c => c.kind === 'leaf').map(c => c.command) : [item.command])
|
||||||
.flat()
|
.toArray()
|
||||||
.concat(props.commands ?? []);
|
.concat(props.commands ?? []);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -100,11 +100,12 @@ const Separator: Component = (props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const Root: ParentComponent<{}> = (props) => {
|
const Root: ParentComponent<{}> = (props) => {
|
||||||
const menu = useMenu();
|
const menuContext = useMenu();
|
||||||
|
const commandContext = useCommands();
|
||||||
const [current, setCurrent] = createSignal<HTMLElement>();
|
const [current, setCurrent] = createSignal<HTMLElement>();
|
||||||
const items = children(() => props.children).toArray() as unknown as (Item | ItemWithChildren)[];
|
const items = children(() => props.children).toArray() as unknown as (Item | ItemWithChildren)[];
|
||||||
|
|
||||||
menu.addItems(items)
|
menuContext.addItems(items)
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
const el = current();
|
const el = current();
|
||||||
|
@ -118,10 +119,10 @@ const Root: ParentComponent<{}> = (props) => {
|
||||||
|
|
||||||
const onExecute = (command?: CommandType) => {
|
const onExecute = (command?: CommandType) => {
|
||||||
return command
|
return command
|
||||||
? async () => {
|
? (e: Event) => {
|
||||||
await command?.();
|
|
||||||
|
|
||||||
close();
|
close();
|
||||||
|
|
||||||
|
return commandContext?.execute(command, e);
|
||||||
}
|
}
|
||||||
: () => { }
|
: () => { }
|
||||||
};
|
};
|
||||||
|
@ -132,7 +133,7 @@ const Root: ParentComponent<{}> = (props) => {
|
||||||
</button>
|
</button>
|
||||||
};
|
};
|
||||||
|
|
||||||
return <Portal mount={menu.ref()}>
|
return <Portal mount={menuContext.ref()}>
|
||||||
<For each={items}>{
|
<For each={items}>{
|
||||||
item => <Switch>
|
item => <Switch>
|
||||||
<Match when={item.kind === 'node' ? item as ItemWithChildren : undefined}>{
|
<Match when={item.kind === 'node' ? item as ItemWithChildren : undefined}>{
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import { Title } from "@solidjs/meta";
|
import { Title } from "@solidjs/meta";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return <>
|
||||||
<main>
|
<Title>About</Title>
|
||||||
<Title>About</Title>
|
<h1>About</h1>
|
||||||
<h1>About</h1>
|
</>;
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,3 +24,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.blank {
|
||||||
|
display: grid;
|
||||||
|
grid: 100% / 100%;
|
||||||
|
inline-size: 100%;
|
||||||
|
block-size: 100%;
|
||||||
|
|
||||||
|
place-items: center;
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ import { Sidebar } from "~/components/sidebar";
|
||||||
import { emptyFolder, FolderEntry, walk as fileTreeWalk, Tree } from "~/components/filetree";
|
import { emptyFolder, FolderEntry, walk as fileTreeWalk, Tree } from "~/components/filetree";
|
||||||
import { Menu } from "~/features/menu";
|
import { Menu } from "~/features/menu";
|
||||||
import { Grid, load, useFiles } from "~/features/file";
|
import { Grid, load, useFiles } from "~/features/file";
|
||||||
import { Command, Context, createCommand, Modifier, noop, useCommands } from "~/features/command";
|
import { Command, CommandType, Context, createCommand, Modifier, noop, useCommands } from "~/features/command";
|
||||||
import { GridApi } from "~/features/file/grid";
|
import { GridApi } from "~/features/file/grid";
|
||||||
import { Tab, Tabs } from "~/components/tabs";
|
import { Tab, Tabs } from "~/components/tabs";
|
||||||
import css from "./edit.module.css";
|
import css from "./edit.module.css";
|
||||||
|
@ -35,21 +35,20 @@ async function* walk(directory: FileSystemDirectoryHandle, path: string[] = []):
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const open = createCommand('open folder', async () => {
|
|
||||||
const directory = await window.showDirectoryPicker({ mode: 'readwrite' });
|
|
||||||
|
|
||||||
useFiles().set('__root__', directory);
|
|
||||||
}, { key: 'o', modifier: Modifier.Control });
|
|
||||||
|
|
||||||
interface Entries extends Map<string, Record<string, { value: string, handle: FileSystemFileHandle, id: string }>> { }
|
interface Entries extends Map<string, Record<string, { value: string, handle: FileSystemFileHandle, id: string }>> { }
|
||||||
|
|
||||||
export default function Edit(props: ParentProps) {
|
export default function Edit(props: ParentProps) {
|
||||||
const filesContext = useFiles();
|
const filesContext = useFiles();
|
||||||
|
|
||||||
const root = filesContext.get('__root__');
|
const open = createCommand('open folder', async () => {
|
||||||
|
const directory = await window.showDirectoryPicker({ mode: 'readwrite' });
|
||||||
|
|
||||||
|
filesContext.open(directory);
|
||||||
|
}, { key: 'o', modifier: Modifier.Control });
|
||||||
|
|
||||||
return <Context.Root commands={[open]}>
|
return <Context.Root commands={[open]}>
|
||||||
<Show when={root()} fallback={<button onpointerdown={() => open()}>open a folder</button>}>{
|
<Show when={filesContext.root()} fallback={<Blank open={open} />}>{
|
||||||
root => <Editor root={root()} />
|
root => <Editor root={root()} />
|
||||||
}</Show>
|
}</Show>
|
||||||
</Context.Root>;
|
</Context.Root>;
|
||||||
|
@ -58,11 +57,11 @@ export default function Edit(props: ParentProps) {
|
||||||
const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => {
|
const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => {
|
||||||
const filesContext = useFiles();
|
const filesContext = useFiles();
|
||||||
|
|
||||||
const tabs = createMemo(() => filesContext.files().map(({ handle }) => {
|
const tabs = createMemo(() => filesContext.files().map(({ key, handle }) => {
|
||||||
const [api, setApi] = createSignal<GridApi>();
|
const [api, setApi] = createSignal<GridApi>();
|
||||||
const [entries, setEntries] = createSignal<Entries>(new Map());
|
const [entries, setEntries] = createSignal<Entries>(new Map());
|
||||||
|
|
||||||
return ({ handle, api, setApi, entries, setEntries });
|
return ({ key, handle, api, setApi, entries, setEntries });
|
||||||
}));
|
}));
|
||||||
const [active, setActive] = createSignal<string>();
|
const [active, setActive] = createSignal<string>();
|
||||||
const [contents, setContents] = createSignal<Map<string, Map<string, string>>>(new Map());
|
const [contents, setContents] = createSignal<Map<string, Map<string, string>>>(new Map());
|
||||||
|
@ -247,8 +246,8 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => {
|
||||||
|
|
||||||
<Tabs active={setActive} onClose={commands.closeTab}>
|
<Tabs active={setActive} onClose={commands.closeTab}>
|
||||||
<For each={tabs()}>{
|
<For each={tabs()}>{
|
||||||
({ handle, setApi, setEntries }) => <Tab
|
({ key, handle, setApi, setEntries }) => <Tab
|
||||||
id={handle.name}
|
id={key}
|
||||||
label={handle.name}
|
label={handle.name}
|
||||||
closable
|
closable
|
||||||
>
|
>
|
||||||
|
@ -315,3 +314,9 @@ const Content: Component<{ directory: FileSystemDirectoryHandle, api?: Setter<Gr
|
||||||
|
|
||||||
return <Grid columns={columns()} rows={rows()} api={setApi} />;
|
return <Grid columns={columns()} rows={rows()} api={setApi} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const Blank: Component<{ open: CommandType }> = (props) => {
|
||||||
|
return <div class={css.blank}>
|
||||||
|
<button onpointerdown={() => props.open()}>open a folder</button>
|
||||||
|
</div>
|
||||||
|
};
|
|
@ -1,4 +1,13 @@
|
||||||
.main {
|
.main {
|
||||||
display: grid;
|
display: grid;
|
||||||
place-content: center;
|
place-content: center;
|
||||||
|
gap: var(--padding-m);
|
||||||
|
|
||||||
|
ul {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
gap: var(--padding-s);
|
||||||
|
padding-inline-start: var(--padding-l);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -46,7 +46,9 @@ export default function Index() {
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li><A href="/edit">Start editing</A></li>
|
<li><A href="/edit">Start editing</A></li>
|
||||||
<li><A href="/experimental">Try new features</A></li>
|
{/* <li><A href="/experimental">Try new features</A></li> */}
|
||||||
|
<li><A href="/instructions">Read the instructions</A></li>
|
||||||
|
<li><A href="/about">About this app</A></li>
|
||||||
</ul>
|
</ul>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|
7
src/routes/(editor)/instructions.tsx
Normal file
7
src/routes/(editor)/instructions.tsx
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
|
||||||
|
|
||||||
|
export default function Instructions() {
|
||||||
|
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
};
|
Loading…
Add table
Add a link
Reference in a new issue