solid wow

This commit is contained in:
Brendan Allan 2023-12-19 17:12:25 +08:00
parent 0543216eef
commit 292ae97b5f
41 changed files with 2210 additions and 1399 deletions

View file

@ -1,4 +1,5 @@
{ {
"name": "publish-artifacts",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "ncc build index.ts --minify" "build": "ncc build index.ts --minify"

View file

@ -12,8 +12,8 @@
}, },
"dependencies": { "dependencies": {
"@remix-run/router": "^1.14.0", "@remix-run/router": "^1.14.0",
"@rspc/client": "0.0.0-main-45466c86", "@rspc/client": "0.0.0-main-b8b35d28",
"@rspc/tauri": "0.0.0-main-45466c86", "@rspc/tauri": "0.0.0-main-b8b35d28",
"@sd/client": "workspace:*", "@sd/client": "workspace:*",
"@sd/interface": "workspace:*", "@sd/interface": "workspace:*",
"@sd/ui": "workspace:*", "@sd/ui": "workspace:*",

View file

@ -4,7 +4,7 @@
}, },
"build": { "build": {
"distDir": "../dist", "distDir": "../dist",
"devPath": "http://localhost:8001", "devPath": "http://localhost:4321",
"beforeDevCommand": "pnpm dev", "beforeDevCommand": "pnpm dev",
"beforeBuildCommand": "pnpm turbo run build --filter=@sd/desktop..." "beforeBuildCommand": "pnpm turbo run build --filter=@sd/desktop..."
}, },

View file

@ -24,8 +24,8 @@
"@react-navigation/drawer": "^6.6.6", "@react-navigation/drawer": "^6.6.6",
"@react-navigation/native": "^6.1.9", "@react-navigation/native": "^6.1.9",
"@react-navigation/stack": "^6.3.20", "@react-navigation/stack": "^6.3.20",
"@rspc/client": "0.0.0-main-45466c86", "@rspc/client": "0.0.0-main-b8b35d28",
"@rspc/react": "0.0.0-main-45466c86", "@rspc/react": "0.0.0-main-b8b35d28",
"@sd/assets": "workspace:*", "@sd/assets": "workspace:*",
"@sd/client": "workspace:*", "@sd/client": "workspace:*",
"@shopify/flash-list": "1.4.3", "@shopify/flash-list": "1.4.3",

View file

@ -14,9 +14,10 @@
"@astrojs/react": "^3.0.7", "@astrojs/react": "^3.0.7",
"@astrojs/solid-js": "^3.0.2", "@astrojs/solid-js": "^3.0.2",
"@astrojs/tailwind": "^5.0.3", "@astrojs/tailwind": "^5.0.3",
"@rspc/solid": "0.0.0-main-45466c86", "@rspc/solid": "0.0.0-main-b8b35d28",
"@sd/assets": "workspace:^", "@sd/assets": "workspace:^",
"@sd/client": "workspace:^", "@sd/client": "workspace:^",
"@sd/ui": "workspace:^",
"@solid-primitives/event-listener": "^2.3.0", "@solid-primitives/event-listener": "^2.3.0",
"@solid-primitives/intersection-observer": "^2.1.3", "@solid-primitives/intersection-observer": "^2.1.3",
"@solid-primitives/resize-observer": "^2.0.22", "@solid-primitives/resize-observer": "^2.0.22",

View file

@ -1,7 +1,7 @@
import { createContext, useContext, type ParentProps } from 'solid-js'; import { createContext, useContext, type ParentProps } from 'solid-js';
import { Ordering } from './store'; import { type CreateExplorer } from './createExplorer';
import { type CreateExplorer } from './useExplorer'; import { type Ordering } from './store';
/** /**
* Context that must wrap anything to do with the explorer. * Context that must wrap anything to do with the explorer.

View file

@ -1,6 +1,15 @@
import { createEventListener } from '@solid-primitives/event-listener'; import { createEventListener } from '@solid-primitives/event-listener';
import { createResizeObserver } from '@solid-primitives/resize-observer';
import clsx from 'clsx'; import clsx from 'clsx';
import { createEffect, createSignal, splitProps, type ComponentProps } from 'solid-js'; import {
createEffect,
createMemo,
createSignal,
JSX,
Show,
splitProps,
type ComponentProps
} from 'solid-js';
// import { Tooltip } from '@sd/ui'; // import { Tooltip } from '@sd/ui';
@ -159,8 +168,8 @@ export function RenameTextBox(props: RenameTextBoxProps) {
ref={ref!} ref={ref!}
role="textbox" role="textbox"
contentEditable={allowRename()} contentEditable={allowRename()}
className={clsx( class={clsx(
'cursor-default overflow-hidden rounded-md px-1.5 py-px text-xs text-ink outline-none', 'cursor-default overflow-hidden rounded-md px-1.5 py-px text-center text-xs text-ink outline-none',
allowRename() && 'whitespace-normal bg-app !text-ink ring-2 ring-accent-deep', allowRename() && 'whitespace-normal bg-app !text-ink ring-2 ring-accent-deep',
!allowRename && props.idleClassName, !allowRename && props.idleClassName,
props.class props.class
@ -183,33 +192,116 @@ export function RenameTextBox(props: RenameTextBoxProps) {
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
{...wrapperProps} {...wrapperProps}
> >
{props.name} {allowRename()
{/* {allowRename ? ( ? props.name
name : (() => {
) : ( const ellipsis = createMemo(() => {
<TruncatedText text={name} lines={lines} onTruncate={setIsTruncated} /> const extension = props.name.lastIndexOf('.');
)} */} if (extension !== -1)
return `...${props.name.slice(
-Math.min(props.name.length - extension + 2, 8)
)}`;
return `...${props.name.slice(-8)}`;
});
return (
<TruncatedText
lines={props.lines ?? 2}
postfix={ellipsis()}
onTruncate={setIsTruncated}
>
{props.name}
</TruncatedText>
);
})()}
</div> </div>
// </Tooltip> // </Tooltip>
); );
} }
interface TruncatedTextProps { const LINE_HEIGHT = 19;
text: string;
lines?: number;
onTruncate: (wasTruncated: boolean) => void;
}
function TruncatedText(props: TruncatedTextProps) { function TruncatedText(props: {
const ellipsis = () => { lines: number;
const extension = props.text.lastIndexOf('.'); prefix?: JSX.Element;
if (extension !== -1) return `...${props.text.slice(-(props.text.length - extension + 2))}`; postfix?: JSX.Element;
return `...${props.text.slice(-8)}`; children: string;
}; style?: JSX.CSSProperties;
onTruncate?: (wasTruncated: boolean) => void;
}) {
const [cutoff, setCutoff] = createSignal<Array<'left' | 'right'>>([]);
const cutoffChildren = createMemo(() => {
const length = props.children.length;
let cursor = length;
const cutoffsArray = cutoff();
for (let i = 1; i <= cutoffsArray.length; i++) {
const delta = Math.ceil(length * Math.pow(0.5, i));
const cutoff = cutoffsArray[i]!;
cursor += (cutoff === 'left' ? -1 : 1) * delta;
}
return props.children.slice(0, cursor);
});
let ref!: HTMLDivElement;
let currentlyTruncating = false;
const fits = createMemo(
() => ref?.getBoundingClientRect().height ?? 0 / LINE_HEIGHT <= props.lines
);
function truncate() {
if (fits()) {
setCutoff((c) => [...c, 'right' as const]);
return (currentlyTruncating = false);
}
setCutoff((c) => [...c, 'left' as const]);
if (fits()) {
return (currentlyTruncating = false);
}
currentlyTruncating = true;
truncate();
}
function reset() {
setCutoff([]);
if (fits()) return;
currentlyTruncating = true;
truncate();
}
createResizeObserver(
() => ref,
() => {
if (currentlyTruncating) return;
reset();
}
);
return ( return (
<TruncateMarkup lines={props.lines} ellipsis={ellipsis} onTruncate={props.onTruncate}> <div
<div>{props.text}</div> style={{
</TruncateMarkup> 'word-break': 'break-word',
...props.style
}}
ref={ref}
>
<Show when={props.prefix}>
<div style={{ display: 'inline-block' }}>{props.prefix}</div>
</Show>
{cutoffChildren()}
{/* <Show when={cutoff().length > 0}>{props.postfix}</Show> */}
</div>
); );
} }

View file

@ -1,14 +1,23 @@
import { getIcon, getIconByName } from '@sd/assets/util'; import { getIcon, getIconByName } from '@sd/assets/util';
import { createElementSize } from '@solid-primitives/resize-observer'; import { createElementSize } from '@solid-primitives/resize-observer';
import clsx from 'clsx'; import clsx from 'clsx';
import { createMemo, createSignal, Match, Show, Switch, type ComponentProps } from 'solid-js'; import {
createEffect,
createMemo,
createSignal,
Match,
Show,
Switch,
type ComponentProps
} from 'solid-js';
import { createStore } from 'solid-js/store'; import { createStore } from 'solid-js/store';
import { getExplorerItemData, type ExplorerItem } from '@sd/client'; import { getExplorerItemData, type ExplorerItem } from '@sd/client';
import { usePlatform } from '../../Platform';
import { LayeredFileIcon } from './LayeredFileIcon'; import { LayeredFileIcon } from './LayeredFileIcon';
import classes from './Thumb.module.scss'; import classes from './Thumb.module.scss';
interface FileThumbProps extends Pick<ComponentProps<'img'>, 'ref'> { interface FileThumbProps extends Pick<ComponentProps<'img'>, 'ref' | 'class'> {
data: ExplorerItem; data: ExplorerItem;
loadOriginal?: boolean; loadOriginal?: boolean;
size?: number; size?: number;
@ -21,7 +30,6 @@ interface FileThumbProps extends Pick<ComponentProps<'img'>, 'ref'> {
extension?: boolean; extension?: boolean;
mediaControls?: boolean; mediaControls?: boolean;
pauseVideo?: boolean; pauseVideo?: boolean;
className?: string;
frameClassName?: string; frameClassName?: string;
childClassName?: string | ((type: ThumbType) => string | undefined); childClassName?: string | ((type: ThumbType) => string | undefined);
isSidebarPreview?: boolean; isSidebarPreview?: boolean;
@ -51,11 +59,13 @@ export function FileThumb(props: FileThumbProps) {
return { variant: 'icon' }; return { variant: 'icon' };
}); });
const platform = usePlatform();
const src = createMemo<string | undefined>(() => { const src = createMemo<string | undefined>(() => {
switch (thumbType().variant) { switch (thumbType().variant) {
case 'thumbnail': case 'thumbnail':
// if (itemData().thumbnailKey.length > 0) if (itemData().thumbnailKey.length > 0)
// return platform.getThumbnailUrlByThumbKey(itemData.thumbnailKey); return platform.getThumbnailUrlByThumbKey(itemData().thumbnailKey);
break; break;
case 'icon': case 'icon':
@ -80,11 +90,12 @@ export function FileThumb(props: FileThumbProps) {
: props.childClassName; : props.childClassName;
const childClassName = 'max-h-full max-w-full object-contain'; const childClassName = 'max-h-full max-w-full object-contain';
const frameClassName = clsx( const frameClassName = () =>
'rounded-sm border-2 border-app-line bg-app-darkBox', clsx(
props.frameClassName, 'rounded-sm border-2 border-app-line bg-app-darkBox',
true ? classes.checkers : classes.checkersLight props.frameClassName,
); true ? classes.checkers : classes.checkersLight
);
const getClass = () => clsx(childClassName, _childClassName()); const getClass = () => clsx(childClassName, _childClassName());
@ -109,60 +120,79 @@ export function FileThumb(props: FileThumbProps) {
}; };
return ( return (
<Show when={src()}> <div
{(src) => ( style={{
<Switch> ...(props.size
<Match when={thumbType().variant === 'thumbnail'}> ? {
<Thumbnail maxWidth: props.size.toString(),
{...props.childProps} width: props.size.toString(),
ref={props.ref} height: props.size.toString()
src={src()} }
cover={props.cover} : {})
onLoad={() => onLoad('thumbnail')} }}
onError={(e) => onError('thumbnail', e)} class={clsx(
decoding={props.size ? 'async' : 'sync'} 'relative flex shrink-0 items-center justify-center',
class={clsx( // !loaded && 'invisible',
props.cover !props.size && 'h-full w-full',
? [ props.cover && 'overflow-hidden',
'min-h-full min-w-full object-cover object-center', props.class
_childClassName()
]
: getClass(),
props.frame &&
!(itemData().kind === 'Video' && props.blackBars) &&
frameClassName
)}
crossOrigin="anonymous" // Here it is ok, because it is not a react attr
blackBars={
props.blackBars && itemData().kind === 'Video' && !props.cover
}
blackBarsSize={props.blackBarsSize}
extension={
props.extension &&
itemData().extension &&
itemData().kind === 'Video'
? itemData().extension || undefined
: undefined
}
/>
</Match>
<Match when={thumbType().variant === 'icon'}>
<LayeredFileIcon
{...props.childProps}
ref={props.ref}
src={src()}
kind={itemData().kind}
extension={itemData().extension}
onLoad={() => onLoad('icon')}
onError={(e) => onError('icon', e)}
decoding={props.size ? 'async' : 'sync'}
class={getClass()}
draggable={false}
/>
</Match>
</Switch>
)} )}
</Show> >
<Show when={src()}>
{(src) => (
<Switch>
<Match when={thumbType().variant === 'thumbnail'}>
<Thumbnail
{...props.childProps}
ref={props.ref}
src={src()}
cover={props.cover}
onLoad={() => onLoad('thumbnail')}
onError={(e) => onError('thumbnail', e)}
decoding={props.size ? 'async' : 'sync'}
class={clsx(
props.cover
? [
'min-h-full min-w-full object-cover object-center',
_childClassName()
]
: getClass(),
props.frame &&
!(itemData().kind === 'Video' && props.blackBars) &&
frameClassName()
)}
crossOrigin="anonymous" // Here it is ok, because it is not a react attr
blackBars={
props.blackBars && itemData().kind === 'Video' && !props.cover
}
blackBarsSize={props.blackBarsSize}
extension={
props.extension &&
itemData().extension &&
itemData().kind === 'Video'
? itemData().extension || undefined
: undefined
}
/>
</Match>
<Match when={thumbType().variant === 'icon'}>
<LayeredFileIcon
{...props.childProps}
ref={props.ref}
src={src()}
kind={itemData().kind}
extension={itemData().extension}
onLoad={() => onLoad('icon')}
onError={(e) => onError('icon', e)}
decoding={props.size ? 'async' : 'sync'}
class={getClass()}
draggable={false}
/>
</Match>
</Switch>
)}
</Show>
</div>
); );
} }
@ -180,7 +210,14 @@ function Thumbnail(props: ThumbnailProps) {
return ( return (
<> <>
<img ref={setRef} draggable={false} /> <img
{...props}
class={props.class}
style={props.style}
src={props.src}
ref={setRef}
draggable={false}
/>
{(props.cover || (size.width && size.width > 80)) && props.extension && ( {(props.cover || (size.width && size.width > 80)) && props.extension && (
<div <div
style={{ style={{

View file

@ -1,84 +1,145 @@
// import getLineHeight from 'line-height'; import { createResizeObserver } from '@solid-primitives/resize-observer';
// import * as Solid from 'solid-js'; import getLineHeight from 'line-height';
import * as Solid from 'solid-js';
// export interface TruncateMarkupProps { export interface TruncateMarkupProps {
// lines: number; lines: number;
// ellipsis?: Solid.JSX.Element | ((element: Solid.JSX.Element) => Solid.JSX.Element); ellipsis?: Solid.JSX.Element | ((element: Solid.JSX.Element) => Solid.JSX.Element);
// children: (ref: HTMLDivElement, style: any) => Solid.JSX.Element; children: (props: { ref: HTMLDivElement; style: any }) => Solid.JSX.Element;
// lineHeight?: number | string; lineHeight?: number | string;
// tokenize?: string; tokenize?: string;
// onTruncate?: (wasTruncated: boolean) => any; onTruncate?: (wasTruncated: boolean) => any;
// } }
// export function TruncateMarkup(props: TruncateMarkupProps) { export function TruncateMarkup(props: Solid.ParentProps<TruncateMarkupProps>) {
// let splitDirectionSeq = []; const [text, setText] = Solid.createSignal('');
// let shouldTruncate = true;
// let wasLastCharTested = false;
// let endFound = false;
// let latestThatFits = null;
// let onTruncateCalled = false;
// let el: HTMLDivElement; const children = Solid.children(() => props.children);
// Solid.onMount(() => { const [ref, setRef] = Solid.createSignal<HTMLElement | null>(null);
// // if (!isValid) return;
// let splitDirectionSeq: Array<'left' | 'right'> = []; let shouldTruncate = false;
// let shouldTruncate = true; let latestThatFits = null;
// let wasLastCharTested = false; let onTruncateCalled = false;
// let endFound = false; let lineHeight: any = null;
// let latestThatFits = null; let endFound = false;
// let onTruncateCalled = false; let splitDirectionSeq = [];
let wasLastCharTested = false;
// const lineHeight = props.lineHeight || getLineHeight(el); type SplitDirection = 'left' | 'right';
// const fits = Solid.createMemo(() => { function splitString(string: string, splitDirections: Array<SplitDirection>, level: any) {
// const maxLines = props.lines; if (!splitDirections.length) return string;
// const { height } = el!.getBoundingClientRect();
// const computedLines = Math.round(height / parseFloat(lineHeight));
// return maxLines >= computedLines; if (splitDirections.length && policy.isAtomic(string)) {
// }); if (!wasLastCharTested) wasLastCharTested = true;
else endFound = true;
// function onTruncate(wasTruncated: boolean) { return string;
// if (!onTruncateCalled) { }
// onTruncateCalled = true;
// props.onTruncate?.(wasTruncated);
// }
// }
// function truncateOriginalText() { if (policy.tokenizeString) {
// endFound = false; const wordsArray = splitArray(policy.tokenizeString(string), splitDirections, level);
// splitDirectionSeq = ['left'];
// wasLastCharTested = false;
// tryToFit(origText, splitDirectionSeq); return wordsArray.joing('');
// } }
const [splitDirection, ...restSplitDirections] = splitDirections;
const pivotIndex = Math.ceil(string.length / 2);
const beforeString = string.substring(0, pivotIndex);
// function tryToFit() {} if (splitDirection === 'left') return splitString(beforeString, restSplitDirections, level);
// function truncate() { const afterString = string.substring(pivotIndex);
// if (fits()) {
// shouldTruncate = false;
// onTruncate(false);
// return; return beforeString + splitString(afterString, restSplitDirections, level);
// } }
// truncateOriginalText(); function splitArray(array: string[], splitDirections: Array<SplitDirection>, level) {
// } if (!splitDirections.length) {
// }); return array;
}
// function childrenElementWithRef() { if (array.length === 1) {
// const childrenArray = children.toArray(); return [split(array[0]!, splitDirections, /* isRoot */ false, level)];
// if (childrenArray.length > 1) { }
// throw new Error('TruncateMarkup must have only one child element');
// }
// const child = childrenArray[0]; const [splitDirection, ...restSplitDirections] = splitDirections;
// } const pivotIndex = Math.ceil(array.length / 2);
const beforeArray = array.slice(0, pivotIndex);
// const [text, setText] = Solid.createSignal(props.children); if (splitDirection === 'left') return splitArray(beforeArray, restSplitDirections, level);
// return <>{text()}</>; const afterArray = array.slice(pivotIndex);
// }
return beforeArray.concat(splitArray(afterArray, restSplitDirections, level));
}
function split(
node: HTMLElement | string | null,
splitDirections: Array<SplitDirection>,
isRoot = false,
level = 1
): HTMLElement | string | null {
if (!node) {
endFound = true;
return node;
} else if (typeof node === 'string') {
return splitString(node, splitDirections, level);
} else if (node.nodeType === node.TEXT_NODE) {
return splitString(node.textContent ?? '', splitDirections, level);
}
return node;
}
function tryToFit(rootEl: HTMLElement, splitDirections: Array<'left' | 'right'>) {
if (!rootEl.firstChild) {
// no markup in container
return;
}
const newRootEL = split(rootEl, splitDirections, true);
}
function fits() {
const refValue = ref();
if (!refValue) return false;
const { height } = refValue.getBoundingClientRect();
const computedLines = Math.round(height / parseFloat(lineHeight));
return props.lines >= computedLines;
}
function truncateOriginalText() {
endFound = false;
splitDirectionSeq = ['left'];
wasLastCharTested = false;
tryToFit(origText, splitDirectionSeq);
}
function truncate() {
if (fits()) {
shouldTruncate = false;
onTruncate(false);
return;
}
truncateOriginalText();
}
createResizeObserver(ref, () => {
shouldTruncate = false;
latestThatFits = null;
setText(origText);
shouldTruncate = true;
onTruncateCalled = false;
truncate();
});
return text;
}

View file

@ -1,479 +1,498 @@
// import getLineHeight from 'line-height'; import getLineHeight from 'line-height';
// import memoizeOne from 'memoize-one'; import memoizeOne from 'memoize-one';
// import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
// import React from 'react'; import React from 'react';
// import ResizeObserver from 'resize-observer-polyfill'; import ResizeObserver from 'resize-observer-polyfill';
// import { Atom, ATOM_STRING_ID, isAtomComponent } from './atom'; import { Atom, ATOM_STRING_ID, isAtomComponent } from './atom';
// import TOKENIZE_POLICY from './tokenize-rules';
const TOKENIZE_POLICY = {
// const SPLIT = { characters: {
// LEFT: true, tokenizeString: null,
// RIGHT: false isAtomic: (str: string) => str.length <= 1
// }; },
words: {
// const toString = (node, string = '') => { tokenizeString: (str: string) => str.match(/(\s*\S[\S\xA0]*)/g),
// if (!node) { isAtomic: (str: string) => /^\s*[\S\xA0]*\s*$/.test(str)
// return string; }
// } else if (typeof node === 'string') { };
// return string + node;
// } else if (isAtomComponent(node)) { const SPLIT = {
// return string + ATOM_STRING_ID; LEFT: true,
// } RIGHT: false
// const children = Array.isArray(node) ? node : node.props.children || ''; };
// return string + React.Children.map(children, (child) => toString(child)).join(''); const toString = (node, string = '') => {
// }; if (!node) {
return string;
// const cloneWithChildren = (node, children, isRootEl, level) => { } else if (typeof node === 'string') {
// const getDisplayStyle = () => { return string + node;
// if (isRootEl) { } else if (isAtomComponent(node)) {
// return { return string + ATOM_STRING_ID;
// // root element cannot be an inline element because of the line calculation }
// display: (node.props.style || {}).display || 'block' const children = Array.isArray(node) ? node : node.props.children || '';
// };
// } else if (level === 2) { return string + React.Children.map(children, (child) => toString(child)).join('');
// return { };
// // level 2 elements (direct children of the root element) need to be inline because of the ellipsis.
// // if level 2 element was a block element, ellipsis would get rendered on a new line, breaking the max number of lines const cloneWithChildren = (node, children, isRootEl, level) => {
// display: (node.props.style || {}).display || 'inline-block' const getDisplayStyle = () => {
// }; if (isRootEl) {
// } else return {}; return {
// }; // root element cannot be an inline element because of the line calculation
display: (node.props.style || {}).display || 'block'
// return { };
// ...node, } else if (level === 2) {
// props: { return {
// ...node.props, // level 2 elements (direct children of the root element) need to be inline because of the ellipsis.
// style: { // if level 2 element was a block element, ellipsis would get rendered on a new line, breaking the max number of lines
// ...node.props.style, display: (node.props.style || {}).display || 'inline-block'
// ...getDisplayStyle() };
// }, } else return {};
// children };
// }
// }; return {
// }; ...node,
props: {
// const validateTree = (node) => { ...node.props,
// if (node == null || ['string', 'number'].includes(typeof node) || isAtomComponent(node)) { style: {
// return true; ...node.props.style,
// } else if (typeof node.type === 'function') { ...getDisplayStyle()
// if (process.env.NODE_ENV !== 'production') { },
// /* eslint-disable no-console */ children
// console.error( }
// `ReactTruncateMarkup tried to render <${node.type.name} />, but truncating React components is not supported, the full content is rendered instead. Only DOM elements are supported. Alternatively, you can take advantage of the <TruncateMarkup.Atom /> component (see more in the docs https://github.com/patrik-piskay/react-truncate-markup/blob/master/README.md#truncatemarkupatom-).` };
// ); };
// /* eslint-enable */
// } const validateTree = (node) => {
if (node == null || ['string', 'number'].includes(typeof node) || isAtomComponent(node)) {
// return false; return true;
// } } else if (typeof node.type === 'function') {
if (process.env.NODE_ENV !== 'production') {
// if (node.props && node.props.children) { /* eslint-disable no-console */
// return React.Children.toArray(node.props.children).reduce( console.error(
// (isValid, child) => isValid && validateTree(child), `ReactTruncateMarkup tried to render <${node.type.name} />, but truncating React components is not supported, the full content is rendered instead. Only DOM elements are supported. Alternatively, you can take advantage of the <TruncateMarkup.Atom /> component (see more in the docs https://github.com/patrik-piskay/react-truncate-markup/blob/master/README.md#truncatemarkupatom-).`
// true );
// ); /* eslint-enable */
// } }
// return true; return false;
// }; }
// export default class TruncateMarkup extends React.Component { if (node.props && node.props.children) {
// static Atom = Atom; return React.Children.toArray(node.props.children).reduce(
(isValid, child) => isValid && validateTree(child),
// static propTypes = { true
// children: PropTypes.element.isRequired, );
// lines: PropTypes.number, }
// ellipsis: PropTypes.oneOfType([PropTypes.element, PropTypes.string, PropTypes.func]),
// lineHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), return true;
// onTruncate: PropTypes.func, };
// // eslint-disable-next-line
// onAfterTruncate: (props, propName, componentName) => { export interface TruncateMarkupProps {
// if (props[propName]) { lines: number;
// return new Error( ellipsis?: JSX.Element | ((element: JSX.Element) => JSX.Element);
// `${componentName}: Setting \`onAfterTruncate\` prop is deprecated, use \`onTruncate\` instead.` children: JSX.Element;
// ); lineHeight?: number | string;
// } tokenize?: string;
// }, onTruncate?: (wasTruncated: boolean) => any;
// tokenize: (props, propName, componentName) => { }
// const tokenizeValue = props[propName];
export default class TruncateMarkup extends React.Component<TruncateMarkupProps> {
// if (typeof tokenizeValue !== 'undefined') { static Atom = Atom;
// if (!TOKENIZE_POLICY[tokenizeValue]) {
// /* eslint-disable no-console */ static propTypes = {
// return new Error( children: PropTypes.element.isRequired,
// `${componentName}: Unknown option for prop 'tokenize': '${tokenizeValue}'. Option 'characters' will be used instead.` lines: PropTypes.number,
// ); ellipsis: PropTypes.oneOfType([PropTypes.element, PropTypes.string, PropTypes.func]),
// /* eslint-enable */ lineHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
// } onTruncate: PropTypes.func,
// } // eslint-disable-next-line
// } onAfterTruncate: (props, propName, componentName) => {
// }; if (props[propName]) {
return new Error(
// static defaultProps = { `${componentName}: Setting \`onAfterTruncate\` prop is deprecated, use \`onTruncate\` instead.`
// lines: 1, );
// ellipsis: '...', }
// lineHeight: '', },
// onTruncate: () => {}, tokenize: (props, propName, componentName) => {
// tokenize: 'characters' const tokenizeValue = props[propName];
// };
if (typeof tokenizeValue !== 'undefined') {
// constructor(props) { if (!TOKENIZE_POLICY[tokenizeValue]) {
// super(props); /* eslint-disable no-console */
return new Error(
// this.state = { `${componentName}: Unknown option for prop 'tokenize': '${tokenizeValue}'. Option 'characters' will be used instead.`
// text: this.childrenWithRefMemo(this.props.children) );
// }; /* eslint-enable */
// } }
}
// lineHeight = null; }
// splitDirectionSeq = []; };
// shouldTruncate = true;
// wasLastCharTested = false; static defaultProps = {
// endFound = false; lines: 1,
// latestThatFits = null; ellipsis: '...',
// onTruncateCalled = false; lineHeight: '',
onTruncate: () => {},
// toStringMemo = memoizeOne(toString); tokenize: 'characters'
// childrenWithRefMemo = memoizeOne(this.childrenElementWithRef); };
// validateTreeMemo = memoizeOne(validateTree);
constructor(props) {
// get isValid() { super(props);
// return this.validateTreeMemo(this.props.children);
// } this.state = {
// get origText() { text: this.childrenWithRefMemo(this.props.children)
// return this.childrenWithRefMemo(this.props.children); };
// } }
// get policy() {
// return TOKENIZE_POLICY[this.props.tokenize] || TOKENIZE_POLICY.characters; lineHeight = null;
// } splitDirectionSeq = [];
shouldTruncate = true;
// componentDidMount() { wasLastCharTested = false;
// if (!this.isValid) { endFound = false;
// return; latestThatFits = null;
// } onTruncateCalled = false;
// // get the computed line-height of the parent element toStringMemo = memoizeOne(toString);
// // it'll be used for determining whether the text fits the container or not childrenWithRefMemo = memoizeOne(this.childrenElementWithRef);
// this.lineHeight = this.props.lineHeight || getLineHeight(this.el); validateTreeMemo = memoizeOne(validateTree);
// this.truncate();
// } get isValid() {
return this.validateTreeMemo(this.props.children);
// UNSAFE_componentWillReceiveProps(nextProps) { }
// this.shouldTruncate = false; get origText() {
// this.latestThatFits = null; return this.childrenWithRefMemo(this.props.children);
}
// this.setState( get policy() {
// { return TOKENIZE_POLICY[this.props.tokenize] || TOKENIZE_POLICY.characters;
// text: this.childrenWithRefMemo(nextProps.children) }
// },
// () => { componentDidMount() {
// if (!this.isValid) { if (!this.isValid) {
// return; return;
// } }
// this.lineHeight = nextProps.lineHeight || getLineHeight(this.el); // get the computed line-height of the parent element
// this.shouldTruncate = true; // it'll be used for determining whether the text fits the container or not
// this.truncate(); this.lineHeight = this.props.lineHeight || getLineHeight(this.el);
// } this.truncate();
// ); }
// }
UNSAFE_componentWillReceiveProps(nextProps) {
// componentDidUpdate() { this.shouldTruncate = false;
// if (this.shouldTruncate === false || this.isValid === false) { this.latestThatFits = null;
// return;
// } this.setState(
{
// if (this.endFound) { text: this.childrenWithRefMemo(nextProps.children)
// // we've found the end where we cannot split the text further },
// // that means we've already found the max subtree that fits the container () => {
// // so we are rendering that if (!this.isValid) {
// if (this.latestThatFits !== null && this.state.text !== this.latestThatFits) { return;
// /* eslint-disable react/no-did-update-set-state */ }
// this.setState({
// text: this.latestThatFits this.lineHeight = nextProps.lineHeight || getLineHeight(this.el);
// }); this.shouldTruncate = true;
this.truncate();
// return; }
// /* eslint-enable */ );
// } }
// this.onTruncate(/* wasTruncated */ true); componentDidUpdate() {
if (this.shouldTruncate === false || this.isValid === false) {
// return; return;
// } }
// if (this.splitDirectionSeq.length) { if (this.endFound) {
// if (this.fits()) { // we've found the end where we cannot split the text further
// this.latestThatFits = this.state.text; // that means we've already found the max subtree that fits the container
// // we've found a subtree that fits the container // so we are rendering that
// // but we need to check if we didn't cut too much of it off if (this.latestThatFits !== null && this.state.text !== this.latestThatFits) {
// // so we are changing the last splitting decision from splitting and going left /* eslint-disable react/no-did-update-set-state */
// // to splitting and going right this.setState({
// this.splitDirectionSeq.splice( text: this.latestThatFits
// this.splitDirectionSeq.length - 1, });
// 1,
// SPLIT.RIGHT, return;
// SPLIT.LEFT /* eslint-enable */
// ); }
// } else {
// this.splitDirectionSeq.push(SPLIT.LEFT); this.onTruncate(/* wasTruncated */ true);
// }
return;
// this.tryToFit(this.origText, this.splitDirectionSeq); }
// }
// } if (this.splitDirectionSeq.length) {
if (this.fits()) {
// componentWillUnmount() { this.latestThatFits = this.state.text;
// this.lineHeight = null; // we've found a subtree that fits the container
// this.latestThatFits = null; // but we need to check if we didn't cut too much of it off
// this.splitDirectionSeq = []; // so we are changing the last splitting decision from splitting and going left
// } // to splitting and going right
this.splitDirectionSeq.splice(
// onTruncate = (wasTruncated) => { this.splitDirectionSeq.length - 1,
// if (!this.onTruncateCalled) { 1,
// this.onTruncateCalled = true; SPLIT.RIGHT,
// this.props.onTruncate(wasTruncated); SPLIT.LEFT
// } );
// }; } else {
this.splitDirectionSeq.push(SPLIT.LEFT);
// handleResize = (el, prevResizeObserver) => { }
// // clean up previous observer
// if (prevResizeObserver) { this.tryToFit(this.origText, this.splitDirectionSeq);
// prevResizeObserver.disconnect(); }
// } }
// // unmounting or just unsetting the element to be replaced with a new one later componentWillUnmount() {
// if (!el) return null; this.lineHeight = null;
this.latestThatFits = null;
// /* Wrapper element resize handing */ this.splitDirectionSeq = [];
// let initialRender = true; }
// const resizeCallback = () => {
// if (initialRender) { onTruncate = (wasTruncated) => {
// // ResizeObserer cb is called on initial render too so we are skipping here if (!this.onTruncateCalled) {
// initialRender = false; this.onTruncateCalled = true;
// } else { this.props.onTruncate(wasTruncated);
// // wrapper element has been resized, recalculating with the original text }
// this.shouldTruncate = false; };
// this.latestThatFits = null;
handleResize = (el, prevResizeObserver) => {
// this.setState( // clean up previous observer
// { if (prevResizeObserver) {
// text: this.origText prevResizeObserver.disconnect();
// }, }
// () => {
// this.shouldTruncate = true; // unmounting or just unsetting the element to be replaced with a new one later
// this.onTruncateCalled = false; if (!el) return null;
// this.truncate();
// } /* Wrapper element resize handing */
// ); let initialRender = true;
// } const resizeCallback = () => {
// }; if (initialRender) {
// ResizeObserer cb is called on initial render too so we are skipping here
// const resizeObserver = prevResizeObserver || new ResizeObserver(resizeCallback); initialRender = false;
} else {
// resizeObserver.observe(el); // wrapper element has been resized, recalculating with the original text
this.shouldTruncate = false;
// return resizeObserver; this.latestThatFits = null;
// };
this.setState(
// truncate() { {
// if (this.fits()) { text: this.origText
// // the whole text fits on the first try, no need to do anything else },
// this.shouldTruncate = false; () => {
// this.onTruncate(/* wasTruncated */ false); this.shouldTruncate = true;
this.onTruncateCalled = false;
// return; this.truncate();
// } }
);
// this.truncateOriginalText(); }
// } };
// setRef = (el) => { const resizeObserver = prevResizeObserver || new ResizeObserver(resizeCallback);
// const isNewEl = this.el !== el;
// this.el = el; resizeObserver.observe(el);
// // whenever we obtain a new element, attach resize handler return resizeObserver;
// if (isNewEl) { };
// this.resizeObserver = this.handleResize(el, this.resizeObserver);
// } truncate() {
// }; if (this.fits()) {
// the whole text fits on the first try, no need to do anything else
// childrenElementWithRef(children) { this.shouldTruncate = false;
// const child = React.Children.only(children); this.onTruncate(/* wasTruncated */ false);
// return React.cloneElement(child, { return;
// ref: this.setRef, }
// style: {
// wordWrap: 'break-word', this.truncateOriginalText();
// ...child.props.style }
// }
// }); setRef = (el) => {
// } const isNewEl = this.el !== el;
this.el = el;
// truncateOriginalText() {
// this.endFound = false; // whenever we obtain a new element, attach resize handler
// this.splitDirectionSeq = [SPLIT.LEFT]; if (isNewEl) {
// this.wasLastCharTested = false; this.resizeObserver = this.handleResize(el, this.resizeObserver);
}
// this.tryToFit(this.origText, this.splitDirectionSeq); };
// }
childrenElementWithRef(children) {
// /** const child = React.Children.only(children);
// * Splits rootEl based on instructions and updates React's state with the returned element
// * After React rerenders the new text, we'll check if the new text fits in componentDidUpdate return React.cloneElement(child, {
// * @param {ReactElement} rootEl - the original children element ref: this.setRef,
// * @param {Array} splitDirections - list of SPLIT.RIGHT/LEFT instructions style: {
// */ wordWrap: 'break-word',
// tryToFit(rootEl, splitDirections) { ...child.props.style
// if (!rootEl.props.children) { }
// // no markup in container });
// return; }
// }
truncateOriginalText() {
// const newRootEl = this.split(rootEl, splitDirections, /* isRootEl */ true); this.endFound = false;
this.splitDirectionSeq = [SPLIT.LEFT];
// let ellipsis = this.wasLastCharTested = false;
// typeof this.props.ellipsis === 'function'
// ? this.props.ellipsis(newRootEl) this.tryToFit(this.origText, this.splitDirectionSeq);
// : this.props.ellipsis; }
// ellipsis = /**
// typeof ellipsis === 'object' * Splits rootEl based on instructions and updates React's state with the returned element
// ? React.cloneElement(ellipsis, { key: 'ellipsis' }) * After React rerenders the new text, we'll check if the new text fits in componentDidUpdate
// : ellipsis; * @param {ReactElement} rootEl - the original children element
* @param {Array} splitDirections - list of SPLIT.RIGHT/LEFT instructions
// const newChildren = newRootEl.props.children; */
// const newChildrenWithEllipsis = [].concat(newChildren, ellipsis); tryToFit(rootEl, splitDirections) {
if (!rootEl.props.children) {
// // edge case tradeoff EC#1 - on initial render it doesn't fit in the requested number of lines (1) so it starts truncating // no markup in container
// // - because of truncating and the ellipsis position, div#lvl2 will have display set to 'inline-block', return;
// // causing the whole body to fit in 1 line again }
// // - if that happens, ellipsis is not needed anymore as the whole body is rendered
// // - NOTE this could be fixed by checking for this exact case and handling it separately so it renders <div>foo {ellipsis}</div> const newRootEl = this.split(rootEl, splitDirections, /* isRootEl */ true);
// //
// // Example: let ellipsis =
// // <TruncateMarkup lines={1}> typeof this.props.ellipsis === 'function'
// // <div> ? this.props.ellipsis(newRootEl)
// // foo : this.props.ellipsis;
// // <div id="lvl2">bar</div>
// // </div> ellipsis =
// // </TruncateMarkup> typeof ellipsis === 'object'
// const shouldRenderEllipsis = ? React.cloneElement(ellipsis, { key: 'ellipsis' })
// toString(newChildren) !== this.toStringMemo(this.props.children); : ellipsis;
// this.setState({ const newChildren = newRootEl.props.children;
// text: { const newChildrenWithEllipsis = [].concat(newChildren, ellipsis);
// ...newRootEl,
// props: { // edge case tradeoff EC#1 - on initial render it doesn't fit in the requested number of lines (1) so it starts truncating
// ...newRootEl.props, // - because of truncating and the ellipsis position, div#lvl2 will have display set to 'inline-block',
// children: shouldRenderEllipsis ? newChildrenWithEllipsis : newChildren // causing the whole body to fit in 1 line again
// } // - if that happens, ellipsis is not needed anymore as the whole body is rendered
// } // - NOTE this could be fixed by checking for this exact case and handling it separately so it renders <div>foo {ellipsis}</div>
// }); //
// } // Example:
// <TruncateMarkup lines={1}>
// /** // <div>
// * Splits JSX node based on its type // foo
// * @param {null|string|Array|Object} node - JSX node // <div id="lvl2">bar</div>
// * @param {Array} splitDirections - list of SPLIT.RIGHT/LEFT instructions // </div>
// * @return {null|string|Array|Object} - split JSX node // </TruncateMarkup>
// */ const shouldRenderEllipsis =
// split(node, splitDirections, isRoot = false, level = 1) { toString(newChildren) !== this.toStringMemo(this.props.children);
// if (!node || isAtomComponent(node)) {
// this.endFound = true; this.setState({
text: {
// return node; ...newRootEl,
// } else if (typeof node === 'string') { props: {
// return this.splitString(node, splitDirections, level); ...newRootEl.props,
// } else if (Array.isArray(node)) { children: shouldRenderEllipsis ? newChildrenWithEllipsis : newChildren
// return this.splitArray(node, splitDirections, level); }
// } }
});
// const newChildren = this.split( }
// node.props.children,
// splitDirections, /**
// /* isRoot */ false, * Splits JSX node based on its type
// level + 1 * @param {null|string|Array|Object} node - JSX node
// ); * @param {Array} splitDirections - list of SPLIT.RIGHT/LEFT instructions
* @return {null|string|Array|Object} - split JSX node
// return cloneWithChildren(node, newChildren, isRoot, level); */
// } split(node, splitDirections, isRoot = false, level = 1) {
if (!node || isAtomComponent(node)) {
// splitString(string, splitDirections = [], level) { this.endFound = true;
// if (!splitDirections.length) {
// return string; return node;
// } } else if (typeof node === 'string') {
return this.splitString(node, splitDirections, level);
// if (splitDirections.length && this.policy.isAtomic(string)) { } else if (Array.isArray(node)) {
// // allow for an extra render test with the current character included return this.splitArray(node, splitDirections, level);
// // in most cases this variation was already tested, but some edge cases require this check }
// // NOTE could be removed once EC#1 is taken care of
// if (!this.wasLastCharTested) { const newChildren = this.split(
// this.wasLastCharTested = true; node.props.children,
// } else { splitDirections,
// // we are trying to split further but we have nowhere to go now /* isRoot */ false,
// // that means we've already found the max subtree that fits the container level + 1
// this.endFound = true; );
// }
return cloneWithChildren(node, newChildren, isRoot, level);
// return string; }
// }
splitString(string, splitDirections = [], level) {
// if (this.policy.tokenizeString) { if (!splitDirections.length) {
// const wordsArray = this.splitArray( return string;
// this.policy.tokenizeString(string), }
// splitDirections,
// level if (splitDirections.length && this.policy.isAtomic(string)) {
// ); // allow for an extra render test with the current character included
// in most cases this variation was already tested, but some edge cases require this check
// // in order to preserve the input structure // NOTE could be removed once EC#1 is taken care of
// return wordsArray.join(''); if (!this.wasLastCharTested) {
// } this.wasLastCharTested = true;
} else {
// const [splitDirection, ...restSplitDirections] = splitDirections; // we are trying to split further but we have nowhere to go now
// const pivotIndex = Math.ceil(string.length / 2); // that means we've already found the max subtree that fits the container
// const beforeString = string.substring(0, pivotIndex); this.endFound = true;
}
// if (splitDirection === SPLIT.LEFT) {
// return this.splitString(beforeString, restSplitDirections, level); return string;
// } }
// const afterString = string.substring(pivotIndex);
if (this.policy.tokenizeString) {
// return beforeString + this.splitString(afterString, restSplitDirections, level); const wordsArray = this.splitArray(
// } this.policy.tokenizeString(string),
splitDirections,
// splitArray(array, splitDirections = [], level) { level
// if (!splitDirections.length) { );
// return array;
// } // in order to preserve the input structure
return wordsArray.join('');
// if (array.length === 1) { }
// return [this.split(array[0], splitDirections, /* isRoot */ false, level)];
// } const [splitDirection, ...restSplitDirections] = splitDirections;
const pivotIndex = Math.ceil(string.length / 2);
// const [splitDirection, ...restSplitDirections] = splitDirections; const beforeString = string.substring(0, pivotIndex);
// const pivotIndex = Math.ceil(array.length / 2);
// const beforeArray = array.slice(0, pivotIndex); if (splitDirection === SPLIT.LEFT) {
return this.splitString(beforeString, restSplitDirections, level);
// if (splitDirection === SPLIT.LEFT) { }
// return this.splitArray(beforeArray, restSplitDirections, level); const afterString = string.substring(pivotIndex);
// }
// const afterArray = array.slice(pivotIndex); return beforeString + this.splitString(afterString, restSplitDirections, level);
}
// return beforeArray.concat(this.splitArray(afterArray, restSplitDirections, level));
// } splitArray(array, splitDirections = [], level) {
if (!splitDirections.length) {
// fits() { return array;
// const { lines: maxLines } = this.props; }
// const { height } = this.el.getBoundingClientRect();
// const computedLines = Math.round(height / parseFloat(this.lineHeight)); if (array.length === 1) {
return [this.split(array[0], splitDirections, /* isRoot */ false, level)];
// return maxLines >= computedLines; }
// }
const [splitDirection, ...restSplitDirections] = splitDirections;
// render() { const pivotIndex = Math.ceil(array.length / 2);
// return this.state.text; const beforeArray = array.slice(0, pivotIndex);
// }
// } if (splitDirection === SPLIT.LEFT) {
return this.splitArray(beforeArray, restSplitDirections, level);
}
const afterArray = array.slice(pivotIndex);
return beforeArray.concat(this.splitArray(afterArray, restSplitDirections, level));
}
fits() {
const { lines: maxLines } = this.props;
const { height } = this.el.getBoundingClientRect();
const computedLines = Math.round(height / parseFloat(this.lineHeight));
return maxLines >= computedLines;
}
render() {
return this.state.text;
}
}

View file

@ -67,7 +67,7 @@ export const GridItem = (props: Props) => {
class="h-full w-full" class="h-full w-full"
data-selectable="" data-selectable=""
data-selectable-index={props.index} data-selectable-index={props.index}
data-selectable-id={itemId} data-selectable-id={itemId()}
onContextMenu={(e) => { onContextMenu={(e) => {
if (explorerView.selectable && !explorer.selectedItems().has(props.item)) { if (explorerView.selectable && !explorer.selectedItems().has(props.item)) {
explorer.resetSelectedItems([props.item]); explorer.resetSelectedItems([props.item]);

View file

@ -16,24 +16,35 @@ export function VirtualGrid({ grid, children, ...props }: GridProps) {
const [offset, setOffset] = Solid.createSignal(0); const [offset, setOffset] = Solid.createSignal(0);
const useVisibilityObserver = createVisibilityObserver(); // const useVisibilityObserver = createVisibilityObserver();
let loadMoreRef: HTMLDivElement | undefined; let loadMoreRef: HTMLDivElement | undefined;
const inView = useVisibilityObserver(() => loadMoreRef); // const inView = useVisibilityObserver(() => loadMoreRef);
const rowVirtualizer = createVirtualizer({ const rowVirtualizer = createVirtualizer({
...grid().virtualizer.rowVirtualizer(), get count() {
return grid().virtualizer.rowVirtualizer.count;
},
getScrollElement: () => grid().virtualizer.rowVirtualizer.getScrollElement(),
estimateSize: (index) => grid().virtualizer.rowVirtualizer.estimateSize(index),
get scrollMargin() { get scrollMargin() {
return offset(); return offset();
} }
}); });
const columnVirtualizer = createVirtualizer(grid().virtualizer.columnVirtualizer()); const columnVirtualizer = createVirtualizer({
horizontal: true,
get count() {
return grid().virtualizer.columnVirtualizer.count;
},
getScrollElement: () => grid().virtualizer.columnVirtualizer.getScrollElement(),
estimateSize: (index) => grid().virtualizer.columnVirtualizer.estimateSize(index)
});
const width = columnVirtualizer.getTotalSize(); const width = Solid.createMemo(() => columnVirtualizer.getTotalSize());
const height = rowVirtualizer.getTotalSize(); const height = Solid.createMemo(() => rowVirtualizer.getTotalSize());
const internalWidth = () => width - (grid().padding.left + grid().padding.right); const internalWidth = () => width() - (grid().padding.left + grid().padding.right);
const internalHeight = () => height - (grid().padding.top + grid().padding.bottom); const internalHeight = () => height() - (grid().padding.top + grid().padding.bottom);
const loadMoreTriggerHeight = Solid.createMemo(() => { const loadMoreTriggerHeight = Solid.createMemo(() => {
if (grid().horizontal || !grid().onLoadMore || !grid().rowCount || !grid().totalRowCount) if (grid().horizontal || !grid().onLoadMore || !grid().rowCount || !grid().totalRowCount)
@ -54,9 +65,9 @@ export function VirtualGrid({ grid, children, ...props }: GridProps) {
loadMoreHeight = Math.max(0, rowVirtualizer.scrollElement.clientHeight - offset); loadMoreHeight = Math.max(0, rowVirtualizer.scrollElement.clientHeight - offset);
} }
const triggerHeight = height - lastRowTop + loadMoreHeight; const triggerHeight = height() - lastRowTop + loadMoreHeight;
return Math.min(height, triggerHeight); return Math.min(height(), triggerHeight);
}); });
const loadMoreTriggerWidth = Solid.createMemo(() => { const loadMoreTriggerWidth = Solid.createMemo(() => {
@ -76,9 +87,9 @@ export function VirtualGrid({ grid, children, ...props }: GridProps) {
const loadMoreWidth = const loadMoreWidth =
grid().loadMoreSize ?? columnVirtualizer.scrollElement?.clientWidth ?? 0; grid().loadMoreSize ?? columnVirtualizer.scrollElement?.clientWidth ?? 0;
const triggerWidth = width - lastColumnLeft + loadMoreWidth; const triggerWidth = width() - lastColumnLeft + loadMoreWidth;
return Math.min(width, triggerWidth); return Math.min(width(), triggerWidth);
}); });
Solid.createEffect( Solid.createEffect(
@ -95,24 +106,32 @@ export function VirtualGrid({ grid, children, ...props }: GridProps) {
) )
); );
Solid.createEffect(() => { // Solid.createEffect(() => {
if (inView()) grid().onLoadMore?.(); // if (inView()) grid().onLoadMore?.();
}); // });
Solid.createEffect(() => { // Solid.createEffect(() => {
const element = grid().scrollRef(); // const element = grid().scrollRef();
if (!element) return; // if (!element) return;
const observer = new MutationObserver(() => setOffset(ref?.offsetTop ?? 0)); // const observer = new MutationObserver(() => setOffset(ref?.offsetTop ?? 0));
observer.observe(element, { // observer.observe(element, {
childList: true // childList: true
}); // });
return () => observer.disconnect(); // return () => observer.disconnect();
}); // });
Solid.createEffect(() => setOffset(ref?.offsetTop ?? 0), []); // Solid.createEffect(() => setOffset(ref?.offsetTop ?? 0), []);
Solid.createEffect(() =>
console.log({
row: rowVirtualizer.getVirtualItems(),
column: columnVirtualizer.getVirtualItems(),
virtualizer: grid().virtualizer
})
);
return ( return (
<div <div
@ -121,8 +140,8 @@ export function VirtualGrid({ grid, children, ...props }: GridProps) {
style={{ style={{
...props.style, ...props.style,
position: 'relative', position: 'relative',
width: width.toString(), width: `${width()}px`,
height: height.toString() height: `${height()}px`
}} }}
> >
<Solid.Show when={internalWidth() > 0 || internalHeight() > 0}> <Solid.Show when={internalWidth() > 0 || internalHeight() > 0}>
@ -142,47 +161,61 @@ export function VirtualGrid({ grid, children, ...props }: GridProps) {
{(virtualRow) => ( {(virtualRow) => (
<Solid.For each={columnVirtualizer.getVirtualItems()}> <Solid.For each={columnVirtualizer.getVirtualItems()}>
{(virtualColumn) => { {(virtualColumn) => {
let index = grid().horizontal const index = Solid.createMemo(() => {
? virtualColumn.index * grid().rowCount + virtualRow.index let index = grid().horizontal
: virtualRow.index * grid().columnCount + virtualColumn.index; ? virtualColumn.index * grid().rowCount + virtualRow.index
: virtualRow.index * grid().columnCount +
virtualColumn.index;
if (grid().invert) index = grid().count - 1 - index; if (grid().invert) index = grid().count - 1 - index;
if (index >= grid().count || index < 0) return null; if (index >= grid().count || index < 0) return null;
return { value: index };
});
return ( return (
<div <Solid.Show when={index()}>
data-index={index} {(index) => (
style={{ <div
'position': 'absolute', data-index={index().value}
'top': 0, style={{
'left': 0, 'position': 'absolute',
'width': `${virtualColumn.size}px`, 'top': 0,
'height': `${virtualRow.size}px`, 'left': 0,
'transform': `translateX(${ 'width': `${virtualColumn.size}px`,
virtualColumn.start 'height': `${virtualRow.size}px`,
}px) translateY(${ 'transform': `translateX(${
virtualRow.start - virtualColumn.start
rowVirtualizer.options.scrollMargin }px) translateY(${
}px)`, virtualRow.start -
'padding-left': rowVirtualizer.options.scrollMargin
virtualColumn.index !== 0 }px)`,
? grid().gap.x.toString() 'padding-left':
: 0, virtualColumn.index !== 0
'padding-top': ? grid().gap.x.toString()
virtualRow.index !== 0 ? grid().gap.y.toString() : 0 : 0,
}} 'padding-top':
> virtualRow.index !== 0
<div ? grid().gap.y.toString()
style={{ : 0
margin: 'auto', }}
width: grid().itemSize.width?.toString() ?? '100%', >
height: grid().itemSize.height?.toString() ?? '100%' <div
}} style={{
> margin: 'auto',
{children(index)} width:
</div> `${grid().itemSize.width?.toString()}px` ??
</div> '100%',
height:
`${grid().itemSize.height?.toString()}px` ??
'100%'
}}
>
{children(index().value)}
</div>
</div>
)}
</Solid.Show>
); );
}} }}
</Solid.For> </Solid.For>

View file

@ -1,7 +1,8 @@
import { createElementSize, createResizeObserver } from '@solid-primitives/resize-observer'; import { createElementSize, createResizeObserver } from '@solid-primitives/resize-observer';
import { type createVirtualizer } from '@tanstack/solid-virtual'; import { type createVirtualizer } from '@tanstack/solid-virtual';
import * as Core from '@virtual-grid/core'; import * as Core from '@virtual-grid/core';
import { createEffect, createMemo, createSignal, type Accessor } from 'solid-js'; import { createEffect, createMemo, createSignal, onMount, type Accessor } from 'solid-js';
import { createStore } from 'solid-js/store';
type VirtualizerOptions = Parameters<typeof createVirtualizer>[0]; type VirtualizerOptions = Parameters<typeof createVirtualizer>[0];
@ -37,59 +38,83 @@ export type CreateGridProps<IdT extends Core.GridItemId, DataT extends Core.Grid
overscan?: number; overscan?: number;
}; };
export function createGrid<IdT extends Core.GridItemId, DataT extends Core.GridItemData>({ export function createGrid<IdT extends Core.GridItemId, DataT extends Core.GridItemData>(
scrollRef, props: Accessor<CreateGridProps<IdT, DataT>>
overscan, ) {
...props
}: CreateGridProps<IdT, DataT>) {
const [width, setWidth] = createSignal(0); const [width, setWidth] = createSignal(0);
let staticWidth: number | null = null; let staticWidth: number | null = null;
const grid = createMemo(() => Core.grid({ width: width(), ...props })); const grid = createMemo(() => Core.grid({ width: width(), ...props() }));
const rowVirtualizer = createMemo<VirtualizerOptions>(() => ({ const [rowVirtualizer, setRowVirtualizer] = createStore<VirtualizerOptions>({
...props.rowVirtualizer, ...props().rowVirtualizer,
count: grid().totalRowCount, count: grid().totalRowCount,
getScrollElement: scrollRef, getScrollElement: props().scrollRef,
estimateSize: grid().getItemHeight, estimateSize: grid().getItemHeight,
paddingStart: grid().padding.top, paddingStart: grid().padding.top,
paddingEnd: grid().padding.bottom, paddingEnd: grid().padding.bottom,
overscan: overscan ?? props.rowVirtualizer?.overscan overscan: props().overscan ?? props().rowVirtualizer?.overscan
})); });
const columnVirtualizer = createMemo<VirtualizerOptions>(() => ({ createEffect(() => {
...props.columnVirtualizer, setRowVirtualizer({
...props().rowVirtualizer,
count: grid().totalRowCount,
getScrollElement: props().scrollRef,
estimateSize: grid().getItemHeight,
paddingStart: grid().padding.top,
paddingEnd: grid().padding.bottom,
overscan: props().overscan ?? props().rowVirtualizer?.overscan
});
});
const [columnVirtualizer, setColumnVirtualizer] = createStore<VirtualizerOptions>({
...props().columnVirtualizer,
horizontal: true, horizontal: true,
count: grid().totalColumnCount, count: grid().totalColumnCount,
getScrollElement: scrollRef, getScrollElement: props().scrollRef,
estimateSize: grid().getItemWidth, estimateSize: grid().getItemWidth,
paddingStart: grid().padding.left, paddingStart: grid().padding.left,
paddingEnd: grid().padding.right, paddingEnd: grid().padding.right,
overscan: overscan ?? props.columnVirtualizer?.overscan overscan: props().overscan ?? props().columnVirtualizer?.overscan
})); });
createEffect(() => {
setColumnVirtualizer({
...props().columnVirtualizer,
horizontal: true,
count: grid().totalColumnCount,
getScrollElement: props().scrollRef,
estimateSize: grid().getItemWidth,
paddingStart: grid().padding.left,
paddingEnd: grid().padding.right,
overscan: props().overscan ?? props().columnVirtualizer?.overscan
});
});
const isStatic = createMemo( const isStatic = createMemo(
() => () =>
props.width !== undefined || props().width !== undefined ||
props.horizontal || props().horizontal ||
props.columns === 0 || props().columns === 0 ||
(props.columns === 'auto' (props().columns === 'auto'
? !props.size || (typeof props.size === 'object' && !props.size.width) ? !props().size || (typeof props().size === 'object' && !props().size.width)
: (props.columns === undefined || props.columns) && : (props().columns === undefined || props().columns) &&
((typeof props.size === 'object' && props.size.width) || ((typeof props().size === 'object' && props().size.width) ||
typeof props.size === 'number')) typeof props().size === 'number'))
); );
createElementSize; createResizeObserver(
() => props().scrollRef(),
createResizeObserver(scrollRef, ({ width }) => { ({ width }) => {
if (width === undefined || isStatic()) { if (width === undefined || isStatic()) {
if (width !== undefined) staticWidth = width; if (width !== undefined) staticWidth = width;
return; return;
}
setWidth(width);
} }
setWidth(width); );
});
createEffect(() => { createEffect(() => {
if (staticWidth === null || width() === staticWidth || isStatic()) return; if (staticWidth === null || width() === staticWidth || isStatic()) return;
@ -99,12 +124,12 @@ export function createGrid<IdT extends Core.GridItemId, DataT extends Core.GridI
return createMemo(() => ({ return createMemo(() => ({
...grid(), ...grid(),
scrollRef: scrollRef, scrollRef: props().scrollRef,
onLoadMore: props.onLoadMore, onLoadMore: props().onLoadMore,
loadMoreSize: props.loadMoreSize, loadMoreSize: props().loadMoreSize,
virtualizer: { virtualizer: {
rowVirtualizer: rowVirtualizer, rowVirtualizer,
columnVirtualizer: columnVirtualizer columnVirtualizer
} }
})); }));
} }

View file

@ -1,5 +1,5 @@
// import { Grid, useGrid } from '@virtual-grid/react'; // import { Grid, useGrid } from '@virtual-grid/react';
import { createEffect, createSignal, type JSX } from 'solid-js'; import { createEffect, createMemo, createSignal, Show, type JSX } from 'solid-js';
import { type ExplorerItem } from '@sd/client'; import { type ExplorerItem } from '@sd/client';
import { useExplorerContext } from '../../Context'; import { useExplorerContext } from '../../Context';
@ -18,16 +18,15 @@ export type RenderItem = (item: {
}) => JSX.Element; }) => JSX.Element;
const CHROME_REGEX = /Chrome/; const CHROME_REGEX = /Chrome/;
const isChrome = CHROME_REGEX.test(navigator.userAgent);
export default ({ children }: { children: RenderItem }) => { export default (props: { children: RenderItem }) => {
// const os = useOperatingSystem(); // const os = useOperatingSystem();
// const realOS = useOperatingSystem(true); // const realOS = useOperatingSystem(true);
const isChrome = CHROME_REGEX.test(navigator.userAgent);
const explorer = useExplorerContext(); const explorer = useExplorerContext();
const explorerView = useExplorerViewContext(); const explorerView = useExplorerViewContext();
const explorerSettings = explorer.useSettingsSnapshot(); // const explorerSettings = explorer.useSettingsSnapshot();
// const quickPreviewStore = useQuickPreviewStore(); // const quickPreviewStore = useQuickPreviewStore();
// const selecto = useRef<Selecto>(null); // const selecto = useRef<Selecto>(null);
@ -43,34 +42,34 @@ export default ({ children }: { children: RenderItem }) => {
const [dragFromThumbnail, setDragFromThumbnail] = createSignal(false); const [dragFromThumbnail, setDragFromThumbnail] = createSignal(false);
const itemDetailsHeight = 44 + (explorerSettings.showBytesInGridView ? 20 : 0); const itemDetailsHeight = 44 + (false ? 20 : 0);
const itemHeight = explorerSettings.gridItemSize + itemDetailsHeight; // const itemHeight = explorerSettings.gridItemSize + itemDetailsHeight;
const padding = explorerSettings.layoutMode === 'grid' ? 12 : 0; const itemHeight = 100 + itemDetailsHeight;
// const padding = explorerSettings.layoutMode === 'grid' ? 12 : 0;
const grid = createGrid({ const grid = createGrid(() => ({
scrollRef: explorer.scrollRef, scrollRef: explorer.scrollRef,
count: explorer.items?.length ?? 0, count: explorer.items()?.length ?? 0,
totalCount: explorer.count, totalCount: explorer.count,
...(explorerSettings.layoutMode === 'grid' // ...(explorerSettings.layoutMode === 'grid'
? { // ?
columns: 'auto', columns: 'auto',
size: { width: explorerSettings.gridItemSize, height: itemHeight } size: { width: 100, height: itemHeight },
} // : { columns: explorerSettings.mediaColumns }),
: { columns: explorerSettings.mediaColumns }), rowVirtualizer: { overscan: explorer.overscan ?? 10 },
rowVirtualizer: { overscan: explorer.overscan ?? 5 },
onLoadMore: explorer.loadMore, onLoadMore: explorer.loadMore,
getItemId: (index: number) => { getItemId: (index) => {
const item = explorer.items()?.[index]; const item = explorer.items()?.[index];
return item ? uniqueId(item) : undefined; return item ? uniqueId(item) : undefined;
}, },
getItemData: (index: number) => explorer.items()?.[index], getItemData: (index) => explorer.items()?.[index]
padding: { // padding: {
bottom: explorerView.bottom ? padding + explorerView.bottom : undefined, // bottom: explorerView.bottom ? padding + explorerView.bottom : undefined,
x: padding, // x: padding,
y: padding // y: padding
}, // },
gap: explorerSettings.layoutMode === 'grid' ? explorerSettings.gridGap : 1 // gap: explorerSettings.layoutMode === 'grid' ? explorerSettings.gridGap : 1
}); }));
const getElementById = (id: string) => { const getElementById = (id: string) => {
if (!explorer.parent) return; if (!explorer.parent) return;
@ -115,57 +114,6 @@ export default ({ children }: { children: RenderItem }) => {
return activeItem; return activeItem;
} }
// function handleDragEnd() {
// getExplorerStore().isDragSelecting = false;
// selectoFirstColumn.current = undefined;
// selectoLastColumn.current = undefined;
// setDragFromThumbnail(false);
// const allSelected = selecto.current?.getSelectedTargets() ?? [];
// activeItem.current = getActiveItem(allSelected);
// }
// useEffect(
// () => {
// const element = explorer.scrollRef.current;
// if (!element) return;
// const handleScroll = () => {
// selecto.current?.checkScroll();
// selecto.current?.findSelectableTargets();
// };
// element.addEventListener('scroll', handleScroll);
// return () => element.removeEventListener('scroll', handleScroll);
// },
// // explorer.scrollRef is a stable reference so this only actually runs once
// [explorer.scrollRef]
// );
// useEffect(() => {
// if (!selecto.current) return;
// const set = new Set(explorer.selectedItemHashes.value);
// if (set.size === 0) return;
// const items = [...document.querySelectorAll('[data-selectable]')].filter((item) => {
// const id = getElementId(item);
// if (id === null) return;
// const selected = set.has(id);
// if (selected) set.delete(id);
// return selected;
// });
// selectoUnselected.current = set;
// selecto.current.setSelectedTargets(items as HTMLElement[]);
// activeItem.current = getActiveItem(items);
// // eslint-disable-next-line react-hooks/exhaustive-deps
// }, [grid.columnCount, explorer.items]);
createEffect(() => { createEffect(() => {
if (explorer.selectedItems().size !== 0) return; if (explorer.selectedItems().size !== 0) return;
@ -179,165 +127,6 @@ export default ({ children }: { children: RenderItem }) => {
// selecto.current?.setSelectedTargets([]); // selecto.current?.setSelectedTargets([]);
// }); // });
const keyboardHandler = (e: KeyboardEvent, newIndex: number) => {
if (!explorerView.selectable) return;
if (explorer.selectedItems().size > 0) {
e.preventDefault();
e.stopPropagation();
}
const newSelectedItem = grid().getItem(newIndex);
if (!newSelectedItem?.data) return;
if (!explorer.allowMultiSelect) explorer.resetSelectedItems([newSelectedItem.data]);
else {
const selectedItemElement = getElementById(uniqueId(newSelectedItem.data));
if (!selectedItemElement) return;
// if (e.shiftKey && !getQuickPreviewStore().open) {
// if (!explorer.selectedItems.has(newSelectedItem.data)) {
// explorer.addSelectedItem(newSelectedItem.data);
// // selecto.current?.setSelectedTargets([
// // ...(selecto.current?.getSelectedTargets() || []),
// // selectedItemElement as HTMLElement
// // ]);
// }
// } else {
explorer.resetSelectedItems([newSelectedItem.data]);
// selecto.current?.setSelectedTargets([selectedItemElement as HTMLElement]);
// if (selectoUnselected.current.size > 0) selectoUnselected.current = new Set();
// }
}
activeItem = newSelectedItem.data;
const scrollRef = explorer.scrollRef(),
viewRef = explorerView.ref();
if (!scrollRef || !viewRef) return;
const { top: viewTop } = viewRef.getBoundingClientRect();
const itemTop = newSelectedItem.rect.top + viewTop;
const itemBottom = newSelectedItem.rect.bottom + viewTop;
const { height: scrollHeight } = scrollRef.getBoundingClientRect();
const scrollTop =
(explorerView.top ?? parseInt(getComputedStyle(scrollRef).paddingTop)) + 1;
const scrollBottom = scrollHeight - 2; // (os !== 'windows' && os !== 'browser' ? 2 : 1);
if (itemTop < scrollTop) {
scrollRef.scrollBy({
top:
itemTop -
scrollTop -
(newSelectedItem.row === 0 ? grid().padding.top : grid().gap.y / 2)
});
} else if (itemBottom > scrollBottom - (explorerView.bottom ?? 0)) {
scrollRef.scrollBy({
top:
itemBottom -
scrollBottom +
(explorerView.bottom ?? 0) +
(newSelectedItem.row === grid().rowCount - 1
? grid().padding.bottom
: grid().gap.y / 2)
});
}
};
const getGridItemHandler = (key: 'ArrowUp' | 'ArrowDown' | 'ArrowLeft' | 'ArrowRight') => {
const lastItem = activeItem;
if (!lastItem) return;
const lastItemIndex = explorer.items()?.findIndex((item) => item === lastItem);
if (lastItemIndex === undefined || lastItemIndex === -1) return;
const gridItem = grid().getItem(lastItemIndex);
if (!gridItem) return;
let newIndex = gridItem.index;
switch (key) {
case 'ArrowUp':
newIndex -= grid().columnCount;
break;
case 'ArrowDown':
newIndex += grid().columnCount;
break;
case 'ArrowLeft':
newIndex -= 1;
break;
case 'ArrowRight':
newIndex += 1;
break;
}
return newIndex;
};
// useShortcut('explorerDown', (e) => {
// if (!explorerView.selectable) return;
// if (explorer.selectedItems.size === 0) {
// const item = grid.getItem(0);
// if (!item?.data) return;
// const selectedItemElement = getElementById(uniqueId(item.data));
// if (!selectedItemElement) return;
// explorer.resetSelectedItems([item.data]);
// selecto.current?.setSelectedTargets([selectedItemElement as HTMLElement]);
// activeItem.current = item.data;
// return;
// }
// const newIndex = getGridItemHandler('ArrowDown');
// if (newIndex === undefined) return;
// keyboardHandler(e, newIndex);
// });
// useShortcut('explorerUp', (e) => {
// if (!explorerView.selectable) return;
// const newIndex = getGridItemHandler('ArrowUp');
// if (newIndex === undefined) return;
// keyboardHandler(e, newIndex);
// });
// useShortcut('explorerLeft', (e) => {
// if (!explorerView.selectable) return;
// const newIndex = getGridItemHandler('ArrowLeft');
// if (newIndex === undefined) return;
// keyboardHandler(e, newIndex);
// });
// useShortcut('explorerRight', (e) => {
// if (!explorerView.selectable) return;
// const newIndex = getGridItemHandler('ArrowRight');
// if (newIndex === undefined) return;
// keyboardHandler(e, newIndex);
// });
//everytime selected items change within quick preview we need to update selecto
// useEffect(() => {
// if (!selecto.current || !quickPreviewStore.open) return;
// if (explorer.selectedItems.size !== 1) return;
// const [item] = Array.from(explorer.selectedItems);
// if (!item) return;
// const itemId = uniqueId(item);
// const element = getElementById(itemId);
// if (!element) selectoUnselected.current = new Set(itemId);
// else selecto.current.setSelectedTargets([element as HTMLElement]);
// activeItem.current = item;
// }, [explorer.items, explorer.selectedItems, quickPreviewStore.open, realOS, getElementById]);
return ( return (
<GridContext.Provider <GridContext.Provider
value={{ value={{
@ -346,278 +135,39 @@ export default ({ children }: { children: RenderItem }) => {
getElementById getElementById
}} }}
> >
{/* {explorer.allowMultiSelect && (
<Selecto
ref={selecto}
boundContainer={
explorerView.ref.current
? {
element: explorerView.ref.current,
top: false,
bottom: false
}
: undefined
}
selectableTargets={['[data-selectable]']}
toggleContinueSelect="shift"
hitRate={0}
onDrag={(e) => {
if (!getExplorerStore().drag) return;
e.stop();
handleDragEnd();
}}
onDragStart={({ inputEvent }) => {
getExplorerStore().isDragSelecting = true;
if ((inputEvent as MouseEvent).target instanceof HTMLImageElement) {
setDragFromThumbnail(true);
}
}}
onDragEnd={handleDragEnd}
onScroll={({ direction }) => {
selecto.current?.findSelectableTargets();
explorer.scrollRef.current?.scrollBy(
(direction[0] || 0) * 10,
(direction[1] || 0) * 10
);
}}
scrollOptions={{
container: { current: explorer.scrollRef.current },
throttleTime: isChrome || dragFromThumbnail ? 30 : 10000
}}
onSelect={(e) => {
const inputEvent = e.inputEvent as MouseEvent;
if (inputEvent.type === 'mousedown') {
const el = inputEvent.shiftKey
? e.added[0] || e.removed[0]
: e.selected[0];
if (!el) return;
const item = getElementItem(el);
if (!item?.data) return;
if (!inputEvent.shiftKey) {
if (explorer.selectedItems.has(item.data)) {
selecto.current?.setSelectedTargets(e.beforeSelected);
} else {
selectoUnselected.current = new Set();
explorer.resetSelectedItems([item.data]);
}
return;
}
if (e.added[0]) explorer.addSelectedItem(item.data);
else explorer.removeSelectedItem(item.data);
} else if (inputEvent.type === 'mousemove') {
const unselectedItems: string[] = [];
e.added.forEach((el) => {
const item = getElementItem(el);
if (!item?.data) return;
explorer.addSelectedItem(item.data);
});
e.removed.forEach((el) => {
const item = getElementItem(el);
if (!item?.data || typeof item.id === 'number') return;
if (document.contains(el)) explorer.removeSelectedItem(item.data);
else unselectedItems.push(item.id);
});
const dragDirection = {
x: inputEvent.x === e.rect.left ? 'left' : 'right',
y: inputEvent.y === e.rect.bottom ? 'down' : 'up'
} as const;
const dragStart = {
x: dragDirection.x === 'right' ? e.rect.left : e.rect.right,
y: dragDirection.y === 'down' ? e.rect.top : e.rect.bottom
};
const dragEnd = { x: inputEvent.x, y: inputEvent.y };
const columns = new Set<number>();
const elements = [...e.added, ...e.removed];
const items = elements.reduce(
(items, el) => {
const item = getElementItem(el);
if (!item) return items;
columns.add(item.column);
return [...items, item];
},
[] as NonNullable<ReturnType<typeof getElementItem>>[]
);
if (columns.size > 1) {
items.sort((a, b) => a.column - b.column);
const firstItem =
dragDirection.x === 'right'
? items[0]
: items[items.length - 1];
const lastItem =
dragDirection.x === 'right'
? items[items.length - 1]
: items[0];
if (firstItem && lastItem) {
selectoFirstColumn.current = firstItem.column;
selectoLastColumn.current = lastItem.column;
}
} else if (columns.size === 1) {
const column = [...columns.values()][0]!;
items.sort((a, b) => a.row - b.row);
const itemRect = elements[0]?.getBoundingClientRect();
const inDragArea =
itemRect &&
(dragDirection.x === 'right'
? dragEnd.x >= itemRect.left
: dragEnd.x <= itemRect.right);
if (
column !== selectoLastColumn.current ||
(column === selectoLastColumn.current && !inDragArea)
) {
const firstItem =
dragDirection.y === 'down'
? items[0]
: items[items.length - 1];
if (firstItem) {
const viewRectTop =
explorerView.ref.current?.getBoundingClientRect().top ??
0;
const itemTop = firstItem.rect.top + viewRectTop;
const itemBottom = firstItem.rect.bottom + viewRectTop;
if (
dragDirection.y === 'down'
? dragStart.y < itemTop
: dragStart.y > itemBottom
) {
const dragHeight = Math.abs(
dragStart.y -
(dragDirection.y === 'down'
? itemTop
: itemBottom)
);
let itemsInDragCount =
(dragHeight - grid.gap.y) /
(grid.virtualItemSize.height + grid.gap.y);
if (itemsInDragCount > 1) {
itemsInDragCount = Math.ceil(itemsInDragCount);
} else {
itemsInDragCount = Math.round(itemsInDragCount);
}
[...Array(itemsInDragCount)].forEach((_, i) => {
const index =
dragDirection.y === 'down'
? itemsInDragCount - i
: i + 1;
const itemIndex =
firstItem.index +
(dragDirection.y === 'down' ? -index : index) *
grid.columnCount;
const item = explorer.items?.[itemIndex];
if (item) {
if (inputEvent.shiftKey) {
if (explorer.selectedItems.has(item))
explorer.removeSelectedItem(item);
else {
explorer.addSelectedItem(item);
if (inDragArea)
unselectedItems.push(
uniqueId(item)
);
}
} else if (!inDragArea)
explorer.removeSelectedItem(item);
else {
explorer.addSelectedItem(item);
if (inDragArea)
unselectedItems.push(uniqueId(item));
}
}
});
}
}
if (!inDragArea && column === selectoFirstColumn.current) {
selectoFirstColumn.current = undefined;
selectoLastColumn.current = undefined;
} else {
selectoLastColumn.current = column;
if (selectoFirstColumn.current === undefined) {
selectoFirstColumn.current = column;
}
}
}
}
if (unselectedItems.length > 0) {
selectoUnselected.current = new Set([
...selectoUnselected.current,
...unselectedItems
]);
}
}
}}
/>
)} */}
<VirtualGrid grid={grid}> <VirtualGrid grid={grid}>
{(index) => { {(index) => {
const item = explorer.items()?.[index]; const item = createMemo(() => explorer.items()?.[index]);
if (!item) return null;
return ( return (
<GridItem <Show when={item()}>
index={index} {(item) => (
item={item} <GridItem
onMouseDown={(e) => { index={index}
if (e.button !== 0 || !explorerView.selectable) return; item={item()}
onMouseDown={(e) => {
if (e.button !== 0 || !explorerView.selectable) return;
e.stopPropagation(); e.stopPropagation();
const item = grid().getItem(index); const item = grid().getItem(index);
if (!item?.data) return; if (!item?.data) return;
if (!explorer.allowMultiSelect) { if (!explorer.allowMultiSelect) {
explorer.resetSelectedItems([item.data]); explorer.resetSelectedItems([item.data]);
} else { } else {
// selectoFirstColumn.current = item.column; // selectoFirstColumn.current = item.column;
// selectoLastColumn.current = item.column; // selectoLastColumn.current = item.column;
} }
activeItem = item.data; activeItem = item.data;
}} }}
> >
{children} {props.children}
</GridItem> </GridItem>
)}
</Show>
); );
}} }}
</VirtualGrid> </VirtualGrid>

View file

@ -4,8 +4,8 @@ import { GridViewItem } from './Item';
export const GridView = () => { export const GridView = () => {
return ( return (
<Grid> <Grid>
{({ item, selected, cut }) => ( {(props) => (
<GridViewItem data={item} selected={selected} cut={cut} /> <GridViewItem data={props.item} selected={props.selected} cut={props.cut} />
)} )}
</Grid> </Grid>
); );

View file

@ -1,7 +1,8 @@
import clsx from 'clsx'; import clsx from 'clsx';
import { JSX } from 'solid-js'; import { JSX } from 'solid-js';
import { getExplorerItemData, useRspcLibraryContext, type ExplorerItem } from '@sd/client'; import { getExplorerItemData, type ExplorerItem } from '@sd/client';
import { useRspcLibraryContext } from '../../rspc';
import { useExplorerContext } from '../Context'; import { useExplorerContext } from '../Context';
// import { toast } from '@sd/ui'; // import { toast } from '@sd/ui';
// import { useIsDark } from '~/hooks'; // import { useIsDark } from '~/hooks';

View file

@ -1,7 +1,6 @@
import { ReactiveSet } from '@solid-primitives/set'; import { ReactiveSet } from '@solid-primitives/set';
import { type InfiniteQueryObserverResult } from '@tanstack/solid-query'; import { type InfiniteQueryObserverResult } from '@tanstack/solid-query';
import { createMemo, createSignal, type Accessor, type ComponentProps } from 'solid-js'; import { createMemo, createSignal, type Accessor, type ComponentProps } from 'solid-js';
import { useDebouncedCallback } from 'use-debounce';
import { z } from 'zod'; import { z } from 'zod';
import type { import type {
ExplorerItem, ExplorerItem,
@ -36,7 +35,7 @@ export type ExplorerParent =
}; };
export interface UseExplorerProps<TOrder extends Ordering> { export interface UseExplorerProps<TOrder extends Ordering> {
items: () => ExplorerItem[] | null; items: Accessor<ExplorerItem[] | null>;
count?: number; count?: number;
parent?: ExplorerParent; parent?: ExplorerParent;
loadMore?: () => void; loadMore?: () => void;
@ -52,7 +51,7 @@ export interface UseExplorerProps<TOrder extends Ordering> {
* @defaultValue `true` * @defaultValue `true`
*/ */
selectable?: boolean; selectable?: boolean;
settings: ReturnType<typeof useExplorerSettings<TOrder>>; settings: ReturnType<typeof createExplorerSettings<TOrder>>;
/** /**
* @defaultValue `true` * @defaultValue `true`
*/ */
@ -64,38 +63,38 @@ export interface UseExplorerProps<TOrder extends Ordering> {
* Controls top-level config and state for the explorer. * Controls top-level config and state for the explorer.
* View- and inspector-specific state is not handled here. * View- and inspector-specific state is not handled here.
*/ */
export function createExplorer<TOrder extends Ordering>({ export function createExplorer<TOrder extends Ordering>(props: UseExplorerProps<TOrder>) {
settings,
layouts,
...props
}: UseExplorerProps<TOrder>) {
const [scrollRef, setScrollRef] = createSignal<HTMLDivElement | null>(null); const [scrollRef, setScrollRef] = createSignal<HTMLDivElement | null>(null);
return { return createMemo(() => ({
// Provided values
...props,
// Default values // Default values
allowMultiSelect: true, allowMultiSelect: true,
selectable: true, selectable: true,
scrollRef, scrollRef,
setScrollRef, setScrollRef,
count: props.items?.length, get count() {
return props.items()?.length;
},
showPathBar: true, showPathBar: true,
layouts: { layouts: {
grid: true, grid: true,
list: true, list: true,
media: true, media: true,
...layouts ...props.layouts
}, },
...settings, ...props.settings,
// Provided values
...props,
// Selected items // Selected items
...createSelectedItems(() => props.items) ...createSelectedItems(() => props.items() ?? [])
}; }));
} }
export type CreateExplorer<TOrder extends Ordering> = ReturnType<typeof createExplorer<TOrder>>; export type CreateExplorer<TOrder extends Ordering> = ReturnType<
ReturnType<typeof createExplorer<TOrder>>
>;
export function useExplorerSettings<TOrder extends Ordering>({ export function createExplorerSettings<TOrder extends Ordering>({
settings, settings,
onSettingsChanged, onSettingsChanged,
orderingKeys, orderingKeys,
@ -110,40 +109,40 @@ export function useExplorerSettings<TOrder extends Ordering>({
}) { }) {
// const [store] = useState(() => proxy(settings)); // const [store] = useState(() => proxy(settings));
const updateSettings = useDebouncedCallback( // const updateSettings = useDebouncedCallback(
(settings: ExplorerSettings<TOrder>, location: Location) => { // (settings: ExplorerSettings<TOrder>, location: Location) => {
onSettingsChanged?.(settings, location); // onSettingsChanged?.(settings, location);
}, // },
500 // 500
); // );
useEffect(() => updateSettings.flush(), [location, updateSettings]); // useEffect(() => updateSettings.flush(), [location, updateSettings]);
useEffect(() => { // useEffect(() => {
if (updateSettings.isPending()) return; // if (updateSettings.isPending()) return;
Object.assign(store, settings); // Object.assign(store, settings);
}, [settings, store, updateSettings]); // }, [settings, store, updateSettings]);
useEffect(() => { // (() => {
if (!onSettingsChanged || !location) return; // if (!onSettingsChanged || !location) return;
const unsubscribe = subscribe(store, () => { // const unsubscribe = subscribe(store, () => {
updateSettings(snapshot(store) as ExplorerSettings<TOrder>, location); // updateSettings(snapshot(store) as ExplorerSettings<TOrder>, location);
}); // });
return () => unsubscribe(); // return () => unsubscribe();
}, [store, updateSettings, location, onSettingsChanged]); // }, [store, updateSettings, location, onSettingsChanged]);
return { return {
useSettingsSnapshot: () => useSnapshot(store), // useSettingsSnapshot: () => useSnapshot(store),
settingsStore: store, // settingsStore: store,
orderingKeys orderingKeys
}; };
} }
export type UseExplorerSettings<TOrder extends Ordering> = ReturnType< export type CreateExplorerSettings<TOrder extends Ordering> = ReturnType<
typeof useExplorerSettings<TOrder> typeof createExplorerSettings<TOrder>
>; >;
function createSelectedItems(items: () => ExplorerItem[] | null) { function createSelectedItems(items: Accessor<ExplorerItem[]>) {
// Doing pointer lookups for hashes is a bit faster than assembling a bunch of strings // Doing pointer lookups for hashes is a bit faster than assembling a bunch of strings
// WeakMap ensures that ExplorerItems aren't held onto after they're evicted from cache // WeakMap ensures that ExplorerItems aren't held onto after they're evicted from cache
const itemHashesWeakMap = new WeakMap<ExplorerItem, string>(); const itemHashesWeakMap = new WeakMap<ExplorerItem, string>();
@ -153,7 +152,7 @@ function createSelectedItems(items: () => ExplorerItem[] | null) {
const selectedItemHashes = new ReactiveSet<string>(); const selectedItemHashes = new ReactiveSet<string>();
const itemsMap = createMemo(() => const itemsMap = createMemo(() =>
(items() ?? []).reduce((items, item) => { items().reduce((items, item) => {
const hash = itemHashesWeakMap.get(item) ?? uniqueId(item); const hash = itemHashesWeakMap.get(item) ?? uniqueId(item);
itemHashesWeakMap.set(item, hash); itemHashesWeakMap.set(item, hash);
items.set(hash, item); items.set(hash, item);

View file

@ -8,7 +8,7 @@ export function Explorer() {
return ( return (
<div <div
ref={explorer.scrollRef} ref={explorer.setScrollRef}
class="custom-scroll explorer-scroll flex flex-1 flex-col overflow-x-hidden" class="custom-scroll explorer-scroll flex flex-1 flex-col overflow-x-hidden"
style={ style={
{ {

View file

@ -0,0 +1,9 @@
import { type CreateInfiniteQueryOptions } from '@tanstack/solid-query';
import { type ExplorerItem, type SearchData } from '@sd/client';
import { type Ordering } from '../store';
export type CreateExplorerInfiniteQueryArgs<TArg> = {
arg: TArg;
// explorerSettings: CreateExplorerSettings<TOrder>;
} & Pick<CreateInfiniteQueryOptions<SearchData<ExplorerItem>>, 'enabled' | 'suspense'>;

View file

@ -0,0 +1,19 @@
import { type CreateInfiniteQueryResult, type CreateQueryResult } from '@tanstack/solid-query';
import { createMemo } from 'solid-js';
export function createExplorerQuery<Q>(
query: CreateInfiniteQueryResult<Q[]>,
count: CreateQueryResult<number>
) {
const items = createMemo(() => query.data?.pages.flatMap((d) => d) ?? null);
const loadMore = () => {
if (query.hasNextPage && !query.isFetchingNextPage) {
query.fetchNextPage.call(undefined);
}
};
return { query, items, loadMore, count: count.data };
}
export type CreateExplorerQuery<Q> = ReturnType<typeof createExplorerQuery<Q>>;

View file

@ -0,0 +1,18 @@
import { type FilePathSearchArgs } from '@sd/client';
import { createLibraryQuery } from '../../rspc';
import { createExplorerQuery } from './createExplorerQuery';
import { createPathsInfiniteQuery } from './createPathsInfiniteQuery';
export function createPathsExplorerQuery(props: {
arg: FilePathSearchArgs;
// explorerSettings: CreateExplorerSettings<FilePathOrder>;
}) {
const query = createPathsInfiniteQuery();
const count = createLibraryQuery(() => ['search.pathsCount', { filters: props.arg.filters }], {
enabled: query.isSuccess
});
return createExplorerQuery(query, count);
}

View file

@ -0,0 +1,33 @@
import { createInfiniteQuery } from '@tanstack/solid-query';
import type { FilePathSearchArgs } from '@sd/client';
import { useCache as useCacheContext } from '../../cache';
import { useRspcLibraryContext } from '../../rspc';
import { useLibraryContext } from '../../useLibraryContext';
export function createPathsInfiniteQuery() {
// props: CreateExplorerInfiniteQueryArgs<FilePathSearchArgs>
const ctx = useRspcLibraryContext();
const cache = useCacheContext();
const library = useLibraryContext();
return createInfiniteQuery({
queryKey: () =>
[
'search.paths',
{ library_id: library.library.uuid, arg: {} as FilePathSearchArgs }
] as const,
queryFn: async ({ queryKey: [_, { arg }] }) => {
const result = await ctx!.client.query(['search.paths', arg]);
cache.setNodes(result.nodes);
return cache.useCache(result.items);
}
// getNextPageParam: (lastPage) => {
// if (arg.take === null || arg.take === undefined) return undefined;
// if (lastPage.items.length < arg.take) return undefined;
// else return lastPage.nodes[arg.take - 1];
// }
});
}

View file

@ -1,32 +1,143 @@
import { createInfiniteQuery } from '@tanstack/solid-query'; import { QueryClient, QueryClientProvider } from '@tanstack/solid-query';
import { createComputed, createMemo, Show } from 'solid-js';
import { type FilePathOrder, type Location } from '@sd/client';
import { cacheCtx, createCache, useCache } from './cache';
import { Explorer } from './Explorer'; import { Explorer } from './Explorer';
import { createExplorer } from './Explorer/useExplorer'; import { ExplorerContextProvider } from './Explorer/Context';
import { createLibraryQuery, useRspcLibraryContext } from './rspc'; import { createExplorer } from './Explorer/createExplorer';
import { createPathsExplorerQuery } from './Explorer/queries/createPathsExplorerQuery';
import { filePathOrderingKeysSchema } from './Explorer/store';
import { PlatformProvider, type Platform } from './Platform';
import { createLibraryQuery, RspcProvider, useRspcLibraryContext } from './rspc';
import { ClientContextProvider, useClientContext } from './useClientContext';
import { LibraryContextProvider } from './useLibraryContext';
import { useTheme } from './useTheme';
import './style.scss';
export const LIBRARY_UUID = 'f47c74cb-119d-42bf-b63d-87e2f9a2e3ba';
const spacedriveURL = (() => {
const currentURL = new URL(window.location.href);
if (import.meta.env.VITE_SDSERVER_ORIGIN) {
currentURL.host = import.meta.env.VITE_SDSERVER_ORIGIN;
} else if (import.meta.env.DEV) {
currentURL.host = 'localhost:8080';
}
return `${currentURL.origin}/spacedrive`;
})();
const platform: Platform = {
platform: 'web',
getThumbnailUrlByThumbKey: (keyParts) =>
`${spacedriveURL}/thumbnail/${keyParts.map((i) => encodeURIComponent(i)).join('/')}.webp`,
getFileUrl: (libraryId, locationLocalId, filePathId) =>
`${spacedriveURL}/file/${encodeURIComponent(libraryId)}/${encodeURIComponent(
locationLocalId
)}/${encodeURIComponent(filePathId)}`,
getFileUrlByPath: (path) => `${spacedriveURL}/local-file-by-path/${encodeURIComponent(path)}`,
openLink: (url) => window.open(url, '_blank')?.focus(),
confirm: (message, cb) => cb(window.confirm(message)),
// auth: {
// start(url) {
// return window.open(url);
// },
// finish(win: Window | null) {
// win?.close();
// }
// },
landingApiOrigin: 'https://spacedrive.com'
};
const queryClient = new QueryClient({
defaultOptions: {
queries: {
...(import.meta.env.VITE_SD_DEMO_MODE && {
refetchOnWindowFocus: false,
staleTime: Infinity,
cacheTime: Infinity,
networkMode: 'offlineFirst',
enabled: false
}),
networkMode: 'always'
},
mutations: {
networkMode: 'always'
}
// TODO: Mutations can't be globally disable which is annoying!
}
});
const cache = createCache();
export function Page() { export function Page() {
const { library } = useLibraryContext(); return (
<RspcProvider queryClient={queryClient}>
<PlatformProvider platform={platform}>
<QueryClientProvider client={queryClient}>
<cacheCtx.Provider value={cache}>
<ClientContextProvider currentLibraryId={LIBRARY_UUID}>
<ClientInner />
</ClientContextProvider>
</cacheCtx.Provider>
</QueryClientProvider>
</PlatformProvider>
</RspcProvider>
);
}
function ClientInner() {
useTheme();
const clientCtx = useClientContext();
return (
<Show when={clientCtx.library()}>
{(library) => (
<div class="bg-app">
<LibraryContextProvider library={library()}>
<Wrapper />
</LibraryContextProvider>
</div>
)}
</Show>
);
}
function Wrapper() {
const cache = useCache();
const locationQuery = createLibraryQuery(() => ['locations.get', 1]);
createComputed(() => cache.setNodes(locationQuery.data?.nodes ?? []));
const location = createMemo(() => cache.useCache(locationQuery.data?.item));
return <Show when={location()}>{(location) => <Inner location={location()} />}</Show>;
}
function Inner(props: { location: Location }) {
// const { library } = useLibraryContext();
const ctx = useRspcLibraryContext(); const ctx = useRspcLibraryContext();
const query = createInfiniteQuery({ const paths = createPathsExplorerQuery({ arg: { take: 100 } });
queryKey: ['search.paths', { library_id: library.uuid, arg }] as const,
queryFn: async ({ queryKey: [_, { arg }] }) => { const explorer = createExplorer<FilePathOrder>({
const result = await ctx!.client.query(['search.paths', arg]); ...paths,
return result; settings: { orderingKeys: filePathOrderingKeysSchema },
}, isFetchingNextPage: paths.query.isFetchingNextPage,
getNextPageParam: (lastPage) => { parent: {
if (arg.take === null || arg.take === undefined) return undefined; type: 'Location',
if (lastPage.items.length < arg.take) return undefined; get location() {
else return lastPage.nodes[arg.take - 1]; return props.location;
}, }
...args }
}); });
const count = createLibraryQuery(['search.pathsCount', { filters: props.arg.filters }], { return (
enabled: query.isSuccess <div class="flex h-screen w-screen">
}); <ExplorerContextProvider explorer={explorer()}>
<Explorer />
const explorer = createExplorer(); </ExplorerContextProvider>
</div>
return <Explorer />; );
} }

View file

@ -0,0 +1,86 @@
import { createContext, useContext, type ParentProps } from 'solid-js';
// import { auth } from '@sd/client';
export type OperatingSystem = 'browser' | 'linux' | 'macOS' | 'windows' | 'unknown';
// Platform represents the underlying native layer the app is running on.
// This could be Tauri or web.
export type Platform = {
platform: 'web' | 'tauri'; // This represents the specific platform implementation
getThumbnailUrlByThumbKey: (thumbKey: string[]) => string;
getFileUrl: (libraryId: string, locationLocalId: number, filePathId: number) => string;
getFileUrlByPath: (path: string) => string;
openLink: (url: string) => void;
// Tauri patches `window.confirm` to return `Promise` not `bool`
confirm(msg: string, cb: (result: boolean) => void): void;
getOs?(): Promise<OperatingSystem>;
openDirectoryPickerDialog?(opts?: { title?: string; multiple: false }): Promise<null | string>;
openDirectoryPickerDialog?(opts?: {
title?: string;
multiple?: boolean;
}): Promise<null | string | string[]>;
openFilePickerDialog?(): Promise<null | string | string[]>;
saveFilePickerDialog?(opts?: { title?: string; defaultPath?: string }): Promise<string | null>;
showDevtools?(): void;
openPath?(path: string): void;
openLogsDir?(): void;
userHomeDir?(): Promise<string>;
// Opens a file path with a given ID
openFilePaths?(library: string, ids: number[]): any;
openEphemeralFiles?(paths: string[]): any;
revealItems?(
library: string,
items: (
| { Location: { id: number } }
| { FilePath: { id: number } }
| { Ephemeral: { path: string } }
)[]
): Promise<unknown>;
requestFdaMacos?(): void;
getFilePathOpenWithApps?(library: string, ids: number[]): Promise<unknown>;
reloadWebview?(): Promise<unknown>;
getEphemeralFilesOpenWithApps?(paths: string[]): Promise<unknown>;
openFilePathWith?(library: string, fileIdsAndAppUrls: [number, string][]): Promise<unknown>;
openEphemeralFileWith?(pathsAndUrls: [string, string][]): Promise<unknown>;
refreshMenuBar?(): Promise<unknown>;
setMenuBarItemState?(id: string, enabled: boolean): Promise<unknown>;
lockAppTheme?(themeType: 'Auto' | 'Light' | 'Dark'): any;
updater?: {
useSnapshot: () => UpdateStore;
checkForUpdate(): Promise<Update | null>;
installUpdate(): Promise<any>;
runJustUpdatedCheck(onViewChangelog: () => void): Promise<void>;
};
// auth: auth.ProviderConfig;
landingApiOrigin: string;
};
export type Update = { version: string; body: string | null };
export type UpdateStore =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'error' }
| { status: 'updateAvailable'; update: Update }
| { status: 'noUpdateAvailable' }
| { status: 'installing' };
// Keep this private and use through helpers below
const context = createContext<Platform>(undefined!);
// is a hook which allows you to fetch information about the current platform from the React context.
export function usePlatform(): Platform {
const ctx = useContext(context);
if (!ctx)
throw new Error(
"The 'PlatformProvider' has not been mounted above the current 'usePlatform' call."
);
return ctx;
}
// provides the platform context to the rest of the app through React context.
// Mount it near the top of your component tree.
export function PlatformProvider(props: ParentProps<{ platform: Platform }>) {
return <context.Provider value={props.platform}>{props.children}</context.Provider>;
}

View file

@ -0,0 +1,4 @@
import './patches';
import '@sd/ui/style/style.scss';
export * from './Page';

View file

@ -0,0 +1,86 @@
import { createContext, useContext, type ParentProps } from 'solid-js';
import { createStore, type SetStoreFunction, type Store } from 'solid-js/store';
export type CacheNode = {
__type: string;
__id: string;
} & Record<string, unknown>;
export const cacheCtx = createContext<Cache>();
export type UseCacheResult<T> = T extends (infer A)[]
? UseCacheResult<A>[]
: T extends object
? T extends { '__type': any; '__id': string; '#type': infer U }
? UseCacheResult<U>
: { [K in keyof T]: UseCacheResult<T[K]> }
: { [K in keyof T]: UseCacheResult<T[K]> };
function constructCache(nodes: Store<Nodes>, setNodes: SetStoreFunction<Nodes>) {
return {
getNodes: () => nodes,
getNode: (type: string, id: string) => nodes?.[type]?.[id] as unknown | undefined,
setNodes: (newNodes: CacheNode | CacheNode[]) => {
if (!Array.isArray(newNodes)) newNodes = [newNodes];
for (const node of newNodes) {
if (!(typeof node === 'object' || '__type' in node || '__id' in node))
throw new Error(
`Tried to 'setNodes' but encountered invalid node '${JSON.stringify(node)}'`
);
const { __type, __id, ...copy } = { ...node } as any;
if (!nodes[node.__type]) setNodes(node.__type, {});
setNodes(node.__type, node.__id, copy); // Be aware this is a merge, not a replace
}
},
useCache<T>(item: T) {
return restore(nodes, item) as UseCacheResult<T>;
}
};
}
export type Cache = ReturnType<typeof constructCache>;
export type Nodes = Record<string, Record<string, unknown>>;
export function createCache() {
const [nodes, setNodes] = createStore({} as Nodes);
const cache = constructCache(nodes, setNodes);
return {
...cache,
Provider: (props: ParentProps) => (
<cacheCtx.Provider value={cache}>{props.children}</cacheCtx.Provider>
)
};
}
export function useCache() {
const c = useContext(cacheCtx);
if (!c) throw new Error('Did you forget to mount `cache.Provider`?');
return c;
}
function restore(nodes: Store<Nodes>, item: unknown): unknown {
if (item === undefined || item === null) {
return item;
} else if (Array.isArray(item)) {
return item.map((v) => restore(nodes, v));
} else if (typeof item === 'object') {
if ('__type' in item && '__id' in item) {
if (typeof item.__type !== 'string') throw new Error('Invalid `__type`');
if (typeof item.__id !== 'string') throw new Error('Invalid `__id`');
const result = nodes?.[item.__type]?.[item.__id];
if (!result)
throw new Error(`Missing node for id '${item.__id}' of type '${item.__type}'`);
return result;
}
return Object.fromEntries(
Object.entries(item).map(([key, value]) => [key, restore(nodes, value)])
);
}
return item;
}

View file

View file

@ -1,5 +1,5 @@
--- ---
import { Page } from "../Page"; import { Page } from "../Wrapper";
--- ---
<!doctype html> <!doctype html>

View file

@ -0,0 +1,22 @@
import { AlphaRSPCError, initRspc, wsBatchLink } from '@rspc/client/v2';
globalThis.isDev = import.meta.env.DEV;
globalThis.rspcLinks = [
// TODO
// loggerLink({
// enabled: () => getDebugState().rspcLogger
// }),
wsBatchLink({
url: (() => {
const currentURL = new URL(window.location.href);
currentURL.protocol = currentURL.protocol === 'https:' ? 'wss:' : 'ws:';
if (import.meta.env.VITE_SDSERVER_ORIGIN) {
currentURL.host = import.meta.env.VITE_SDSERVER_ORIGIN;
} else if (import.meta.env.DEV) {
currentURL.host = 'localhost:8080';
}
currentURL.pathname = 'rspc/ws';
return currentURL.href;
})()
})
];

View file

@ -1,10 +1,12 @@
import { type ProcedureDef } from '@rspc/client'; import { type ProcedureDef } from '@rspc/client';
import { AlphaRSPCError, initRspc } from '@rspc/client/v2'; import { AlphaRSPCError, initRspc, wsBatchLink } from '@rspc/client/v2';
import { createReactQueryHooks, type Context } from '@rspc/solid'; import { createReactQueryHooks, type Context } from '@rspc/solid';
import { QueryClient } from '@tanstack/react-query'; import { QueryClient } from '@tanstack/react-query';
import { createContext, useContext, type ParentProps } from 'solid-js'; import { createContext, useContext, type ParentProps } from 'solid-js';
import { match, P } from 'ts-pattern'; import { match, P } from 'ts-pattern';
import { currentLibraryCache, type LibraryArgs, type Procedures } from '@sd/client'; import { type LibraryArgs, type Procedures } from '@sd/client';
import { currentLibraryCache } from './useClientContext';
type NonLibraryProcedure<T extends keyof Procedures> = type NonLibraryProcedure<T extends keyof Procedures> =
| Exclude<Procedures[T], { input: LibraryArgs<any> }> | Exclude<Procedures[T], { input: LibraryArgs<any> }>
@ -71,11 +73,14 @@ const libraryHooks = createReactQueryHooks<LibraryProceduresDef>(libraryClient,
}); });
// TODO: Allow both hooks to use a unified context -> Right now they override each others local state // TODO: Allow both hooks to use a unified context -> Right now they override each others local state
export function RspcProvider({ queryClient, children }: ParentProps<{ queryClient: QueryClient }>) { export function RspcProvider(props: ParentProps<{ queryClient: QueryClient }>) {
return ( return (
<libraryHooks.Provider client={libraryClient as any} queryClient={queryClient}> <libraryHooks.Provider client={libraryClient as any} queryClient={props.queryClient}>
<nonLibraryHooks.Provider client={nonLibraryClient as any} queryClient={queryClient}> <nonLibraryHooks.Provider
{children as any} client={nonLibraryClient as any}
queryClient={props.queryClient}
>
{props.children}
</nonLibraryHooks.Provider> </nonLibraryHooks.Provider>
</libraryHooks.Provider> </libraryHooks.Provider>
); );

View file

@ -0,0 +1,384 @@
a,
button {
cursor: default !important;
}
body {
-webkit-user-select: none;
// font-family: 'InterVariable', sans-serif;
}
.app-background {
@apply bg-app;
}
.frame::before {
@apply bg-app-frame;
content: '';
pointer-events: none;
user-select: none;
position: absolute;
inset: 0px;
border-radius: inherit;
padding: 1px;
mask:
linear-gradient(black, black) content-box content-box,
linear-gradient(black, black);
mask-composite: xor;
z-index: 9999;
}
.has-blur-effects {
.app-background {
// adjust macOS blur intensity here
// @apply bg-app/[0.88];
@apply bg-app;
}
}
.no-scrollbar {
-ms-overflow-style: none; /* for Internet Explorer, Edge */
scrollbar-width: none; /* for Firefox */
overflow-y: scroll;
}
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar::-webkit-scrollbar {
display: none;
}
/*Tooltip*/
.TooltipContent {
animation-duration: 0.6s;
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}
.TooltipContent[data-side='top'] {
animation-name: slideUp;
}
.TooltipContent[data-side='bottom'] {
animation-name: slideDown;
}
.TooltipContent[data-side='right'] {
animation-name: slideRight;
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes slideRight {
from {
opacity: 0;
transform: translateX(-10px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.custom-scroll {
-ms-overflow-style: none; /* for Internet Explorer, Edge */
scrollbar-width: none; /* for Firefox */
overflow-y: scroll;
}
::-webkit-scrollbar-corner {
background-color: transparent;
}
.explorer-scroll {
&::-webkit-scrollbar {
height: 6px;
width: 6px;
}
&::-webkit-scrollbar-track {
@apply rounded-[6px] bg-transparent;
margin-top: var(--scrollbar-margin-top);
margin-bottom: var(--scrollbar-margin-bottom);
}
&::-webkit-scrollbar-thumb {
@apply rounded-[6px] bg-app-explorerScrollbar;
}
}
.default-scroll {
&::-webkit-scrollbar {
height: 6px;
width: 8px;
}
&::-webkit-scrollbar-track {
@apply rounded-[6px] bg-transparent;
}
&::-webkit-scrollbar-thumb {
@apply rounded-[6px] bg-app-box;
}
}
.page-scroll {
&::-webkit-scrollbar {
height: 6px;
width: 8px;
}
&::-webkit-scrollbar-track {
@apply rounded-[6px] bg-transparent;
}
&::-webkit-scrollbar-thumb {
@apply rounded-[6px] bg-app-box;
}
}
.topbar-page-scroll {
&::-webkit-scrollbar {
height: 6px;
width: 8px;
}
&::-webkit-scrollbar-track {
@apply mt-[46px] rounded-[6px] bg-transparent;
}
&::-webkit-scrollbar-thumb {
@apply rounded-[6px] bg-app-box;
}
}
.quick-preview-images-scroll {
&::-webkit-scrollbar {
height: 6px;
width: 8px;
}
&::-webkit-scrollbar-track {
@apply rounded-[6px] bg-transparent;
}
&::-webkit-scrollbar-thumb {
@apply rounded-[6px] bg-white/20;
}
}
.job-manager-scroll {
&::-webkit-scrollbar {
height: 6px;
width: 8px;
}
&::-webkit-scrollbar-track {
@apply my-[2px] rounded-[6px] bg-transparent;
}
&::-webkit-scrollbar-thumb {
@apply rounded-[6px] bg-app-shade/20;
}
}
.inspector-scroll {
// overflow: overlay;
&::-webkit-scrollbar {
height: 6px;
width: 5px;
}
&::-webkit-scrollbar-track {
@apply my-[8px] bg-transparent;
}
&::-webkit-scrollbar-thumb {
@apply rounded-[6px] bg-app/70 opacity-0;
}
&:hover {
&::-webkit-scrollbar-thumb {
@apply opacity-100;
}
}
}
.overlay-scroll {
// overflow: overlay;
&::-webkit-scrollbar {
height: 6px;
width: 5px;
}
&::-webkit-scrollbar-track {
@apply my-[5px] bg-transparent;
}
&::-webkit-scrollbar-thumb {
@apply w-[5px] rounded-[6px] bg-black/70 opacity-0;
}
&:hover {
&::-webkit-scrollbar-thumb {
@apply opacity-100;
}
}
}
.textviewer-scroll {
&::-webkit-scrollbar {
height: 6px;
width: 8px;
}
&::-webkit-scrollbar-track {
@apply bg-transparent;
}
&::-webkit-scrollbar-thumb {
@apply rounded-md bg-app-box;
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@-webkit-keyframes slide-top {
0% {
-webkit-transform: translateY(0);
transform: translateY(0);
}
100% {
-webkit-transform: translateY(-50px);
transform: translateY(-50px);
}
}
@keyframes slide-top {
0% {
-webkit-transform: translateY(0);
transform: translateY(0);
}
100% {
-webkit-transform: translateY(-50px);
transform: translateY(-50px);
}
}
.dialog-overlay[data-state='open'] {
animation: fadeIn 200ms ease-out forwards;
}
.dialog-overlay[data-state='closed'] {
animation: fadeIn 200ms ease-out forwards;
}
.dialog-content[data-state='open'] {
-webkit-animation: slide-top 0.3s cubic-bezier(0.215, 0.61, 0.355, 1) both;
animation: slide-top 0.3s cubic-bezier(0.215, 0.61, 0.355, 1) both;
}
.dialog-content[data-state='closed'] {
animation: bounceDown 100ms ease-in forwards;
}
.picker {
position: relative;
}
.swatch {
width: 28px;
height: 28px;
border-radius: 8px;
border: 3px solid #fff;
box-shadow:
0 0 0 1px rgba(0, 0, 0, 0.1),
inset 0 0 0 1px rgba(0, 0, 0, 0.1);
cursor: pointer;
}
.popover {
position: absolute;
top: calc(100% + 2px);
left: 0;
border-radius: 9px;
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
}
.react-colorful__saturation {
border-radius: 4px !important;
}
.react-colorful__saturation-pointer {
width: 12px !important;
height: 12px !important;
}
.react-colorful__hue,
.react-colorful__alpha {
margin-top: 12px !important;
height: 8px !important;
border-radius: 4px !important;
}
.react-colorful__hue-pointer,
.react-colorful__alpha-pointer {
height: 18px !important;
width: 8px !important;
border-radius: 3px !important;
}
.selecto-selection {
@apply rounded;
border-color: hsla(var(--color-accent));
background-color: hsla(var(--color-accent), 0.2) !important;
z-index: 10 !important;
}
.indeterminate-progress-bar {
/* Rounded border */
border-radius: 9999px;
/* Size */
height: 4px;
position: relative;
overflow: hidden;
}
.indeterminate-progress-bar__progress {
/* Rounded border */
border-radius: 9999px;
/* Absolute position */
position: absolute;
bottom: 0;
top: 0;
width: 50%;
/* Move the bar infinitely */
animation-duration: 2s;
animation-iteration-count: infinite;
animation-name: indeterminate-progress-bar;
}
@keyframes indeterminate-progress-bar {
from {
left: -50%;
}
to {
left: 100%;
}
}
.react-slidedown.search-options-slide {
transition-duration: 300ms;
transition-timing-function: cubic-bezier(0.85, 0, 0.15, 1);
}

View file

@ -0,0 +1,121 @@
import {
createContext,
createEffect,
createMemo,
useContext,
type Accessor,
type ParentProps
} from 'solid-js';
import { valtioPersist, type LibraryConfigWrapped } from '@sd/client';
import { useCache } from './cache';
import { createBridgeQuery, nonLibraryClient } from './rspc';
// The name of the localStorage key for caching library data
const libraryCacheLocalStorageKey = 'sd-library-list2'; // `2` is because the format of this underwent a breaking change when introducing normalised caching
export const useCachedLibraries = () => {
const query = createBridgeQuery(() => ['library.list'] as const, {
keepPreviousData: true
// initialData: () => {
// const cachedData = localStorage.getItem(libraryCacheLocalStorageKey);
// if (cachedData) {
// // If we fail to load cached data, it's fine
// try {
// return JSON.parse(cachedData);
// } catch (e) {
// console.error("Error loading cached 'sd-library-list' data", e);
// }
// }
// return undefined;
// },
// onSuccess: (data) => localStorage.setItem(libraryCacheLocalStorageKey, JSON.stringify(data))
});
const cache = useCache();
createMemo(() => cache.setNodes(query.data?.nodes ?? []));
const libraries = createMemo(() => cache.useCache(query.data?.items ?? []));
return {
...query,
get data() {
return libraries();
}
};
};
// export async function getCachedLibraries(cache: NormalisedCache) {
// const cachedData = localStorage.getItem(libraryCacheLocalStorageKey);
// if (cachedData) {
// // If we fail to load cached data, it's fine
// try {
// const data = JSON.parse(cachedData);
// cache.withNodes(data.nodes);
// return cache.withCache(data.items) as LibraryConfigWrapped[];
// } catch (e) {
// console.error("Error loading cached 'sd-library-list' data", e);
// }
// }
// const result = await nonLibraryClient.query(['library.list']);
// cache.withNodes(result.nodes);
// const libraries = cache.withCache(result.items);
// localStorage.setItem(libraryCacheLocalStorageKey, JSON.stringify(result));
// return libraries;
// }
export interface ClientContext {
currentLibraryId: string | null;
libraries: ReturnType<typeof useCachedLibraries>;
library: Accessor<LibraryConfigWrapped | null | undefined>;
}
const ClientContext = createContext<ClientContext>(null!);
interface ClientContextProviderProps extends ParentProps {
currentLibraryId: string | null;
}
export const ClientContextProvider = (props: ClientContextProviderProps) => {
const libraries = useCachedLibraries();
createEffect(() => {
currentLibraryCache.id = props.currentLibraryId;
});
const library = createMemo(
() =>
(libraries.data && libraries.data.find((l) => l.uuid === props.currentLibraryId)) ||
null
);
return (
<ClientContext.Provider
value={{
currentLibraryId: props.currentLibraryId,
libraries,
library
}}
>
{props.children}
</ClientContext.Provider>
);
};
export const useClientContext = () => {
const ctx = useContext(ClientContext);
if (ctx === undefined) throw new Error("'ClientContextProvider' not mounted");
return ctx;
};
export const useCurrentLibraryId = () => useClientContext().currentLibraryId;
export const currentLibraryCache = valtioPersist('sd-current-library', {
id: null as string | null
});

View file

@ -0,0 +1,47 @@
import { createContext, useContext, type ParentProps } from 'solid-js';
import type { LibraryConfigWrapped } from '@sd/client';
import { createBridgeSubscription } from './rspc';
import { useClientContext, type ClientContext } from './useClientContext';
export interface LibraryContext {
library: LibraryConfigWrapped;
libraries: ClientContext['libraries'];
}
const LibraryContext = createContext<LibraryContext>(null!);
interface LibraryContextProviderProps extends ParentProps {
library: LibraryConfigWrapped;
}
export const LibraryContextProvider = (props: LibraryContextProviderProps) => {
const { libraries } = useClientContext();
// We put this into context because each hook creates a new subscription which means we get duplicate events from the backend if we don't do this
// TODO: This should probs be a library subscription - https://linear.app/spacedriveapp/issue/ENG-724/locationsonline-should-be-a-library-not-a-bridge-subscription
createBridgeSubscription(() => ['locations.online'] as const, {
onData: (d) => {
// getLibraryStore().onlineLocations = d;
}
});
return (
<LibraryContext.Provider value={{ library: props.library, libraries }}>
{props.children}
</LibraryContext.Provider>
);
};
export const useLibraryContext = () => {
const ctx = useContext(LibraryContext);
if (ctx === undefined) throw new Error("'LibraryContextProvider' not mounted");
return ctx;
};
// export function useOnlineLocations() {
// const { onlineLocations } = useLibraryStore();
// return onlineLocations;
// }

View file

@ -0,0 +1,64 @@
import { createEffect } from 'solid-js';
import { createMutable } from 'solid-js/store';
import { getThemeStore, useThemeStore } from '@sd/client';
import { usePlatform } from './Platform';
export const themeStore = createMutable({
theme: 'dark',
syncThemeWithSystem: false,
hueValue: 235
});
export function useTheme() {
// const themeStore = useThemeStore();
const platform = usePlatform();
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)');
createEffect(() => {
const handleThemeChange = () => {
if (themeStore.syncThemeWithSystem) {
platform.lockAppTheme?.('Auto');
if (systemTheme.matches) {
document.documentElement.classList.remove('vanilla-theme');
document.documentElement.style.setProperty(
'--dark-hue',
getThemeStore().hueValue.toString()
);
getThemeStore().theme = 'dark';
} else {
document.documentElement.classList.add('vanilla-theme');
document.documentElement.style.setProperty(
'--light-hue',
getThemeStore().hueValue.toString()
);
getThemeStore().theme = 'vanilla';
}
} else {
if (themeStore.theme === 'dark') {
document.documentElement.classList.remove('vanilla-theme');
document.documentElement.style.setProperty(
'--dark-hue',
getThemeStore().hueValue.toString()
);
platform.lockAppTheme?.('Dark');
} else if (themeStore.theme === 'vanilla') {
document.documentElement.classList.add('vanilla-theme');
document.documentElement.style.setProperty(
'--light-hue',
getThemeStore().hueValue.toString()
);
platform.lockAppTheme?.('Light');
}
}
};
handleThemeChange();
systemTheme.addEventListener('change', handleThemeChange);
return () => {
systemTheme.removeEventListener('change', handleThemeChange);
};
});
}

View file

@ -1,8 +1,3 @@
/** @type {import('tailwindcss').Config} */ import tailwindFactory from '@sd/ui/tailwind';
export default {
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'], export default tailwindFactory('web');
theme: {
extend: {},
},
plugins: [],
}

View file

@ -1,30 +0,0 @@
import react from '@astrojs/react';
// import solid from '@astrojs/solid-js';
import tailwind from '@astrojs/tailwind';
import { defineConfig } from 'astro/config';
import { mergeConfig } from 'vite';
import baseConfig from '../../packages/config/vite';
import relativeAliasResolver from '../../packages/config/vite/relativeAliasResolver';
// https://astro.build/config
export default defineConfig({
integrations: [
react({
// exclude: ['**/*.solid.*']
}),
// solid({
// include: ['**/*.solid.*']
// }),
tailwind()
],
// server: {
// port: 8002
// },
vite: mergeConfig(baseConfig, {
resolve: {
// BE REALLY DAMN CAREFUL MODIFYING THIS: https://github.com/spacedriveapp/spacedrive/pull/1353
alias: [relativeAliasResolver]
}
})
});

View file

@ -3,7 +3,7 @@
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"scripts": { "scripts": {
"dev": "astro dev", "dev": "vite dev",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"test": "VITE_SD_DEMO_MODE=true playwright test", "test": "VITE_SD_DEMO_MODE=true playwright test",
@ -14,7 +14,7 @@
"@astrojs/solid-js": "^3.0.2", "@astrojs/solid-js": "^3.0.2",
"@astrojs/tailwind": "^5.0.3", "@astrojs/tailwind": "^5.0.3",
"@fontsource/inter": "^4.5.15", "@fontsource/inter": "^4.5.15",
"@rspc/client": "0.0.0-main-45466c86", "@rspc/client": "0.0.0-main-b8b35d28",
"@sd/client": "workspace:*", "@sd/client": "workspace:*",
"@sd/interface": "workspace:*", "@sd/interface": "workspace:*",
"@tanstack/react-query": "^4.36.1", "@tanstack/react-query": "^4.36.1",

17
apps/web/src/index.tsx Normal file
View file

@ -0,0 +1,17 @@
// WARNING: BE CAREFUL SAVING THIS FILE WITH A FORMATTER ENABLED. The import order is important and goes against prettier's recommendations.
import React, { Suspense } from 'react';
import ReactDOM from 'react-dom/client';
import '@sd/ui/style/style.scss';
import '~/patches';
import { App } from './App';
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
<React.StrictMode>
<Suspense>
<App />
</Suspense>
</React.StrictMode>
);

View file

@ -11,8 +11,8 @@
"typecheck": "tsc -b" "typecheck": "tsc -b"
}, },
"dependencies": { "dependencies": {
"@rspc/client": "0.0.0-main-45466c86", "@rspc/client": "0.0.0-main-b8b35d28",
"@rspc/react": "0.0.0-main-45466c86", "@rspc/react": "0.0.0-main-b8b35d28",
"@tanstack/react-query": "^4.36.1", "@tanstack/react-query": "^4.36.1",
"@zxcvbn-ts/core": "^3.0.4", "@zxcvbn-ts/core": "^3.0.4",
"@zxcvbn-ts/language-common": "^3.0.4", "@zxcvbn-ts/language-common": "^3.0.4",

View file

@ -1,4 +1,4 @@
// import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import { createHtmlPlugin } from 'vite-plugin-html'; import { createHtmlPlugin } from 'vite-plugin-html';
// import solid from 'vite-plugin-solid'; // import solid from 'vite-plugin-solid';
@ -8,12 +8,9 @@ import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
tsconfigPaths(), tsconfigPaths(),
// react({ react({
// exclude: ['**/*.solid.*'] exclude: ['**/*.solid.*']
// }), }),
// solid({
// include: ['**/*.solid.*']
// }),
svg({ svgrOptions: { icon: true } }), svg({ svgrOptions: { icon: true } }),
createHtmlPlugin({ createHtmlPlugin({
minify: true minify: true

View file

@ -81,11 +81,11 @@ importers:
specifier: ^1.14.0 specifier: ^1.14.0
version: 1.14.0 version: 1.14.0
'@rspc/client': '@rspc/client':
specifier: 0.0.0-main-45466c86 specifier: 0.0.0-main-b8b35d28
version: 0.0.0-main-45466c86 version: 0.0.0-main-b8b35d28
'@rspc/tauri': '@rspc/tauri':
specifier: 0.0.0-main-45466c86 specifier: 0.0.0-main-b8b35d28
version: 0.0.0-main-45466c86(@tauri-apps/api@1.5.1) version: 0.0.0-main-b8b35d28(@tauri-apps/api@1.5.1)
'@sd/client': '@sd/client':
specifier: workspace:* specifier: workspace:*
version: link:../../packages/client version: link:../../packages/client
@ -350,11 +350,11 @@ importers:
specifier: ^6.3.20 specifier: ^6.3.20
version: 6.3.20(@react-navigation/native@6.1.9)(react-native-gesture-handler@2.12.1)(react-native-safe-area-context@4.6.3)(react-native-screens@3.22.1)(react-native@0.72.6)(react@18.2.0) version: 6.3.20(@react-navigation/native@6.1.9)(react-native-gesture-handler@2.12.1)(react-native-safe-area-context@4.6.3)(react-native-screens@3.22.1)(react-native@0.72.6)(react@18.2.0)
'@rspc/client': '@rspc/client':
specifier: 0.0.0-main-45466c86 specifier: 0.0.0-main-b8b35d28
version: 0.0.0-main-45466c86 version: 0.0.0-main-b8b35d28
'@rspc/react': '@rspc/react':
specifier: 0.0.0-main-45466c86 specifier: 0.0.0-main-b8b35d28
version: 0.0.0-main-45466c86(@rspc/client@0.0.0-main-45466c86)(@tanstack/react-query@4.36.1)(react@18.2.0) version: 0.0.0-main-b8b35d28(@rspc/client@0.0.0-main-b8b35d28)(@tanstack/react-query@4.36.1)(react@18.2.0)
'@sd/assets': '@sd/assets':
specifier: workspace:* specifier: workspace:*
version: link:../../packages/assets version: link:../../packages/assets
@ -576,8 +576,8 @@ importers:
specifier: ^4.5.15 specifier: ^4.5.15
version: 4.5.15 version: 4.5.15
'@rspc/client': '@rspc/client':
specifier: 0.0.0-main-45466c86 specifier: 0.0.0-main-b8b35d28
version: 0.0.0-main-45466c86 version: 0.0.0-main-b8b35d28
'@sd/client': '@sd/client':
specifier: workspace:* specifier: workspace:*
version: link:../../packages/client version: link:../../packages/client
@ -670,14 +670,17 @@ importers:
specifier: ^5.0.3 specifier: ^5.0.3
version: 5.0.3(astro@4.0.5)(tailwindcss@3.3.6) version: 5.0.3(astro@4.0.5)(tailwindcss@3.3.6)
'@rspc/solid': '@rspc/solid':
specifier: 0.0.0-main-45466c86 specifier: 0.0.0-main-b8b35d28
version: 0.0.0-main-45466c86(@rspc/client@0.0.0-main-45466c86)(@tanstack/solid-query@4.36.1)(solid-js@1.8.7) version: 0.0.0-main-b8b35d28(@rspc/client@0.0.0-main-b8b35d28)(@tanstack/solid-query@4.36.1)(solid-js@1.8.7)
'@sd/assets': '@sd/assets':
specifier: workspace:^ specifier: workspace:^
version: link:../../packages/assets version: link:../../packages/assets
'@sd/client': '@sd/client':
specifier: workspace:^ specifier: workspace:^
version: link:../../packages/client version: link:../../packages/client
'@sd/ui':
specifier: workspace:^
version: link:../../packages/ui
'@solid-primitives/event-listener': '@solid-primitives/event-listener':
specifier: ^2.3.0 specifier: ^2.3.0
version: 2.3.0(solid-js@1.8.7) version: 2.3.0(solid-js@1.8.7)
@ -940,11 +943,11 @@ importers:
packages/client: packages/client:
dependencies: dependencies:
'@rspc/client': '@rspc/client':
specifier: 0.0.0-main-45466c86 specifier: 0.0.0-main-b8b35d28
version: 0.0.0-main-45466c86 version: 0.0.0-main-b8b35d28
'@rspc/react': '@rspc/react':
specifier: 0.0.0-main-45466c86 specifier: 0.0.0-main-b8b35d28
version: 0.0.0-main-45466c86(@rspc/client@0.0.0-main-45466c86)(@tanstack/react-query@4.36.1)(react@18.2.0) version: 0.0.0-main-b8b35d28(@rspc/client@0.0.0-main-b8b35d28)(@tanstack/react-query@4.36.1)(react@18.2.0)
'@tanstack/react-query': '@tanstack/react-query':
specifier: ^4.36.1 specifier: ^4.36.1
version: 4.36.1(react-dom@18.2.0)(react-native@0.72.6)(react@18.2.0) version: 4.36.1(react-dom@18.2.0)(react-native@0.72.6)(react@18.2.0)
@ -4681,7 +4684,7 @@ packages:
magic-string: 0.27.0 magic-string: 0.27.0
react-docgen-typescript: 2.2.2(typescript@5.3.3) react-docgen-typescript: 2.2.2(typescript@5.3.3)
typescript: 5.3.3 typescript: 5.3.3
vite: 5.0.9(less@4.2.0) vite: 5.0.9(@types/node@18.17.19)
/@jridgewell/gen-mapping@0.3.3: /@jridgewell/gen-mapping@0.3.3:
resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==}
@ -7154,40 +7157,40 @@ packages:
requiresBuild: true requiresBuild: true
optional: true optional: true
/@rspc/client@0.0.0-main-45466c86: /@rspc/client@0.0.0-main-b8b35d28:
resolution: {integrity: sha512-1a3+jSJXXcHyoMYrqlb5DModVf5m7S4Y7a6BaUHmkhfXG4rttvthFHNAU1ODMpbI371egEePFhiVR8SnPsXe6Q==} resolution: {integrity: sha512-wXBZ+KDBzBfXXKr2GWAe/UF+D5jLl1vM7mBTuFJsEV4ihquu2hzxAQBPuBE3j6JC7SIWzhQ+hodzGFUCL04Rsg==}
dev: false dev: false
/@rspc/react@0.0.0-main-45466c86(@rspc/client@0.0.0-main-45466c86)(@tanstack/react-query@4.36.1)(react@18.2.0): /@rspc/react@0.0.0-main-b8b35d28(@rspc/client@0.0.0-main-b8b35d28)(@tanstack/react-query@4.36.1)(react@18.2.0):
resolution: {integrity: sha512-4b3SBm6KFS0fZ0JwdZPFZeb+ZG/FgXn3Qb4DJga5ioGr11YsDGMgeEyaxfC8dLSHtT5tj8vFqX/a6LoWk0Vqbw==} resolution: {integrity: sha512-yjKtZkziLvUI3AyKAdN8l32O3yS2FBqKda24MWDcN/YWjFVw15Enfr9C2bGtuqrZZ/LKabrG67EnKWqGvTWkCQ==}
peerDependencies: peerDependencies:
'@rspc/client': 0.0.0-main-45466c86 '@rspc/client': 0.0.0-main-b8b35d28
'@tanstack/react-query': ^4.26.0 '@tanstack/react-query': ^4.26.0
react: ^18.2.0 react: ^18.2.0
dependencies: dependencies:
'@rspc/client': 0.0.0-main-45466c86 '@rspc/client': 0.0.0-main-b8b35d28
'@tanstack/react-query': 4.36.1(react-dom@18.2.0)(react-native@0.72.6)(react@18.2.0) '@tanstack/react-query': 4.36.1(react-dom@18.2.0)(react-native@0.72.6)(react@18.2.0)
react: 18.2.0 react: 18.2.0
dev: false dev: false
/@rspc/solid@0.0.0-main-45466c86(@rspc/client@0.0.0-main-45466c86)(@tanstack/solid-query@4.36.1)(solid-js@1.8.7): /@rspc/solid@0.0.0-main-b8b35d28(@rspc/client@0.0.0-main-b8b35d28)(@tanstack/solid-query@4.36.1)(solid-js@1.8.7):
resolution: {integrity: sha512-5rfeivH4I2LrMSx5Igfx2FfLn7F6s5OzVy9VtaJ6C4K/Yh9Y2AufR0yQwEApoUrfexGWbecmmoW+yPbDqaJvpA==} resolution: {integrity: sha512-KjssarTWhoj8chAoESekTfucy/vg/eum+5Ghp1vMfUQWhGQ7f6SOXqeGQefOz0itIBAMCmQn+R03uFm59+GMCQ==}
peerDependencies: peerDependencies:
'@rspc/client': 0.0.0-main-45466c86 '@rspc/client': 0.0.0-main-b8b35d28
'@tanstack/solid-query': ^4.6.0 '@tanstack/solid-query': ^4.6.0
solid-js: ^1.6.11 solid-js: ^1.6.11
dependencies: dependencies:
'@rspc/client': 0.0.0-main-45466c86 '@rspc/client': 0.0.0-main-b8b35d28
'@tanstack/solid-query': 4.36.1(solid-js@1.8.7) '@tanstack/solid-query': 4.36.1(solid-js@1.8.7)
solid-js: 1.8.7 solid-js: 1.8.7
dev: false dev: false
/@rspc/tauri@0.0.0-main-45466c86(@tauri-apps/api@1.5.1): /@rspc/tauri@0.0.0-main-b8b35d28(@tauri-apps/api@1.5.1):
resolution: {integrity: sha512-B+/PcZjxuVTQEtEgBV7UXYwf1uGNbN34+6aqbc5oxxifLDDn0MxVGmEju3Y518sYCfuYaIF8rYDl+oPM9a7NSg==} resolution: {integrity: sha512-uzBBxsP8ENBUs16j5KXni72WKtVdi37uA/+GA1rf2M/qJvnc8W+slXrI4PXL9CBHnGRbwXk/gM1z9AdadqsFRw==}
peerDependencies: peerDependencies:
'@tauri-apps/api': ^1.2.0 '@tauri-apps/api': ^1.2.0
dependencies: dependencies:
'@rspc/client': 0.0.0-main-45466c86 '@rspc/client': 0.0.0-main-b8b35d28
'@tauri-apps/api': 1.5.1 '@tauri-apps/api': 1.5.1
dev: false dev: false
@ -7834,7 +7837,7 @@ packages:
magic-string: 0.30.5 magic-string: 0.30.5
rollup: 3.29.4 rollup: 3.29.4
typescript: 5.3.3 typescript: 5.3.3
vite: 5.0.9(less@4.2.0) vite: 5.0.9(@types/node@18.17.19)
transitivePeerDependencies: transitivePeerDependencies:
- encoding - encoding
- supports-color - supports-color
@ -8176,7 +8179,7 @@ packages:
react: 18.2.0 react: 18.2.0
react-docgen: 7.0.1 react-docgen: 7.0.1
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
vite: 5.0.9(less@4.2.0) vite: 5.0.9(@types/node@18.17.19)
transitivePeerDependencies: transitivePeerDependencies:
- '@preact/preset-vite' - '@preact/preset-vite'
- encoding - encoding
@ -9304,7 +9307,7 @@ packages:
'@babel/plugin-transform-react-jsx-source': 7.23.3(@babel/core@7.23.6) '@babel/plugin-transform-react-jsx-source': 7.23.3(@babel/core@7.23.6)
magic-string: 0.27.0 magic-string: 0.27.0
react-refresh: 0.14.0 react-refresh: 0.14.0
vite: 5.0.9(less@4.2.0) vite: 5.0.9(@types/node@18.17.19)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -22537,6 +22540,7 @@ packages:
rollup: 4.9.0 rollup: 4.9.0
optionalDependencies: optionalDependencies:
fsevents: 2.3.3 fsevents: 2.3.3
dev: true
/vite@5.0.9(sass@1.69.5): /vite@5.0.9(sass@1.69.5):
resolution: {integrity: sha512-wVqMd5kp28QWGgfYPDfrj771VyHTJ4UDlCteLH7bJDGDEamaz5hV8IX6h1brSGgnnyf9lI2RnzXq/JmD0c2wwg==} resolution: {integrity: sha512-wVqMd5kp28QWGgfYPDfrj771VyHTJ4UDlCteLH7bJDGDEamaz5hV8IX6h1brSGgnnyf9lI2RnzXq/JmD0c2wwg==}