-
Notifications
You must be signed in to change notification settings - Fork 253
Fix: improve root directory modal behavior and UX #2453
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
c3fb402
45e9a40
5c31ce2
16f9f46
0181088
7f31579
aecdfd5
c849eed
9f13711
523f228
affe37f
0079071
5d4a44a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,245 @@ | ||
| <script lang="ts"> | ||
| import { getContext } from 'svelte'; | ||
| import type { createTreeView } from '@melt-ui/svelte'; | ||
| import { IconChevronRight } from '@appwrite.io/pink-icons-svelte'; | ||
| import { Icon, Layout, Selector, Spinner, Typography } from '@appwrite.io/pink-svelte'; | ||
|
|
||
| export let directories: Array<{ | ||
| title: string; | ||
| fileCount?: number; | ||
| fullPath: string; | ||
| thumbnailUrl?: string; | ||
| thumbnailIcon?: typeof Icon; | ||
| thumbnailHtml?: string; | ||
| children?: typeof directories; | ||
| hasChildren?: boolean; | ||
| showThumbnail?: boolean; | ||
| loading?: boolean; | ||
| }>; | ||
| export let level = 0; | ||
| export let containerWidth: number | undefined; | ||
| export let selectedPath: string | undefined; | ||
| export let onSelect: | ||
| | ((detail: { title: string; fullPath: string; hasChildren: boolean }) => void) | ||
| | undefined; | ||
|
|
||
| const Radio = Selector.Radio; | ||
|
|
||
| let radioInputs: Array<HTMLInputElement | undefined> = []; | ||
| let value: string | undefined; | ||
| let thumbnailStates: Array<{ loading: boolean; error: boolean }> = []; | ||
|
|
||
| $: if (directories) { | ||
| if (thumbnailStates.length < directories.length) { | ||
| thumbnailStates = [ | ||
| ...thumbnailStates, | ||
| ...Array.from({ length: directories.length - thumbnailStates.length }, () => ({ | ||
| loading: true, | ||
| error: false | ||
| })) | ||
| ]; | ||
| } else if (thumbnailStates.length > directories.length) { | ||
| thumbnailStates = thumbnailStates.slice(0, directories.length); | ||
| } | ||
| } | ||
|
|
||
| function handleThumbnailLoad(index: number) { | ||
| if (!thumbnailStates[index]) return; | ||
| thumbnailStates[index].loading = false; | ||
| thumbnailStates[index].error = false; | ||
| } | ||
|
|
||
| function handleThumbnailError(index: number) { | ||
| if (!thumbnailStates[index]) return; | ||
| thumbnailStates[index].loading = false; | ||
| thumbnailStates[index].error = true; | ||
| } | ||
|
|
||
| const { | ||
| elements: { item, group }, | ||
| helpers: { isExpanded } | ||
| } = getContext<ReturnType<typeof createTreeView>>('tree'); | ||
|
|
||
| const paddingLeftStyle = `padding-left: ${32 * level + 8}px`; | ||
|
|
||
| $: if (selectedPath && directories?.length) { | ||
| const idx = directories.findIndex((d) => d.fullPath === selectedPath); | ||
| if (idx !== -1 && radioInputs[idx]) { | ||
| radioInputs[idx].checked = true; | ||
| } | ||
| } | ||
| </script> | ||
|
|
||
| {#each directories as { title, fileCount, fullPath, thumbnailUrl, thumbnailIcon, thumbnailHtml, children, hasChildren: explicitHasChildren, showThumbnail = true, loading = false }, i} | ||
| {@const hasChildren = explicitHasChildren ?? !!children?.length} | ||
| {@const __MELTUI_BUILDER_1__ = $group({ id: fullPath })} | ||
| {@const __MELTUI_BUILDER_0__ = $item({ | ||
| id: fullPath, | ||
| hasChildren | ||
| })} | ||
|
|
||
| <div class="directory-item-container"> | ||
| <button | ||
| class="folder" | ||
| type="button" | ||
| style={paddingLeftStyle} | ||
| on:click={() => { | ||
| if (radioInputs[i]) radioInputs[i].checked = true; | ||
| onSelect?.({ title, fullPath, hasChildren }); | ||
| }} | ||
| {...__MELTUI_BUILDER_0__} use:__MELTUI_BUILDER_0__.action | ||
| > | ||
| <Layout.Stack direction="row" justifyContent="space-between"> | ||
| <Layout.Stack | ||
| direction="row" | ||
| justifyContent="flex-start" | ||
| gap="xxs" | ||
| alignItems="center" | ||
| > | ||
| <div> | ||
| <Layout.Stack direction="row" gap="xxs" alignItems="center"> | ||
| <Radio | ||
| group="directory" | ||
| name="directory" | ||
| size="s" | ||
| bind:value | ||
| bind:radioInput={radioInputs[i]} | ||
| /> | ||
| <div | ||
| class:folder-open={$isExpanded(fullPath)} | ||
| class:disabled={!hasChildren} | ||
| class="chevron-container" | ||
| > | ||
| <Icon | ||
| icon={IconChevronRight} | ||
| size="s" | ||
| color="--fgcolor-neutral-tertiary" | ||
| /> | ||
| </div> | ||
| </Layout.Stack> | ||
| </div> | ||
| <span | ||
| class="title" | ||
| style={containerWidth | ||
| ? `max-width: ${containerWidth - 100 - level * 40}px` | ||
| : ''}>{title}</span | ||
| > | ||
| {#if fileCount !== undefined} | ||
| <div class="fileCount"> | ||
| <Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary" | ||
| >({fileCount} files)</Typography.Text | ||
| > | ||
| </div> | ||
| {/if} | ||
| </Layout.Stack> | ||
| {#if showThumbnail} | ||
| {#if loading || (thumbnailStates[i]?.loading && !thumbnailIcon && !thumbnailHtml)} | ||
| <Spinner /> | ||
| {/if} | ||
|
|
||
| {#if thumbnailStates[i]?.error} | ||
| <div class="thumbnail-fallback" /> | ||
| {:else if thumbnailUrl} | ||
| <img | ||
| src={thumbnailUrl} | ||
| alt="Directory thumbnail" | ||
| class="thumbnail" | ||
| class:hidden={thumbnailStates[i]?.loading} | ||
| on:load={() => handleThumbnailLoad(i)} | ||
| on:error={() => handleThumbnailError(i)} | ||
| /> | ||
| {:else if thumbnailIcon} | ||
| <div class="thumbnail"> | ||
| <Icon icon={thumbnailIcon} size="l" /> | ||
| </div> | ||
| {:else if thumbnailHtml} | ||
| <div class="thumbnail"> | ||
| <!-- eslint-disable-next-line svelte/no-at-html-tags --> | ||
| {@html thumbnailHtml} | ||
| </div> | ||
|
HarshMN2345 marked this conversation as resolved.
|
||
| {/if} | ||
| {/if} | ||
| </Layout.Stack> | ||
| </button> | ||
|
|
||
| {#if children} | ||
| <div {...__MELTUI_BUILDER_1__} use:__MELTUI_BUILDER_1__.action> | ||
| <svelte:self | ||
| directories={children} | ||
| level={level + 1} | ||
| {containerWidth} | ||
| {selectedPath} | ||
| {onSelect} | ||
| /> | ||
| </div> | ||
| {/if} | ||
| </div> | ||
| {/each} | ||
|
|
||
| <style> | ||
| .directory-item-container { | ||
| width: 100%; | ||
| } | ||
| .folder { | ||
| display: flex; | ||
| width: 100%; | ||
| flex-direction: row; | ||
| padding: var(--space-3, 6px) var(--space-4, 8px); | ||
| justify-content: space-between; | ||
| align-items: center; | ||
| cursor: pointer; | ||
| -webkit-tap-highlight-color: rgba(0, 0, 0, 0); | ||
|
|
||
| &:hover, | ||
| &:focus { | ||
| border-radius: var(--border-radius-s, 8px); | ||
| background: var(--bgcolor-neutral-secondary, #f4f4f7); | ||
| } | ||
| } | ||
| .chevron-container { | ||
| width: var(--space-7); | ||
| height: var(--space-7); | ||
| transition: transform ease-in-out 0.1s; | ||
| } | ||
| .folder-open { | ||
| transform: rotate(90deg); | ||
| } | ||
| .disabled { | ||
| color: var(--fgcolor-neutral-tertiary); | ||
| } | ||
|
|
||
| .title { | ||
| white-space: nowrap; | ||
| overflow: hidden; | ||
| text-overflow: ellipsis; | ||
| flex-grow: 0; | ||
| } | ||
|
|
||
| .fileCount { | ||
| display: none; | ||
|
|
||
| @media (min-width: 1024px) { | ||
| display: block; | ||
| } | ||
| } | ||
|
|
||
| .hidden { | ||
| display: none; | ||
| } | ||
|
|
||
| .thumbnail { | ||
| width: var(--icon-size-l, 24px); | ||
| height: var(--icon-size-l, 24px); | ||
| flex-shrink: 0; | ||
| border-radius: var(--border-radius-circle, 99999px); | ||
| } | ||
|
|
||
| .thumbnail-fallback { | ||
| width: var(--icon-size-l, 24px); | ||
| height: var(--icon-size-l, 24px); | ||
| flex-shrink: 0; | ||
| border-radius: var(--border-radius-circle, 99999px); | ||
| border: var(--border-width-s, 1px) dashed var(--border-neutral-strong, #d8d8db); | ||
| background: var(--bgcolor-neutral-primary, #fff); | ||
| } | ||
| </style> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| <script lang="ts"> | ||
| import { createTreeView } from '@melt-ui/svelte'; | ||
| import { onMount, setContext } from 'svelte'; | ||
| import { writable, type Writable } from 'svelte/store'; | ||
| import DirectoryItem from '$lib/components/git/DirectoryItem.svelte'; | ||
| import { Spinner } from '@appwrite.io/pink-svelte'; | ||
|
|
||
| export let expanded: Writable<string[]> = writable(['lib-0', 'tree-0']); | ||
| export let selected: string | undefined; | ||
| export let openTo: string | undefined; | ||
| export let directories: Array<Record<string, unknown>>; | ||
| export let isLoading = true; | ||
| export let onSelect: | ||
| | ((detail: { fullPath: string; hasChildren: boolean; title: string }) => void | Promise<void>) | ||
| | undefined; | ||
| export let onChange: ((detail: { fullPath: string }) => void | Promise<void>) | undefined; | ||
|
|
||
| const ctx = createTreeView({ expanded }); | ||
| setContext('tree', ctx); | ||
|
|
||
| const { | ||
| elements: { tree } | ||
| } = ctx; | ||
|
|
||
| let rootContainer: HTMLDivElement | undefined; | ||
| let containerWidth: number | undefined; | ||
| let internalSelected: string | undefined; | ||
|
|
||
| $: internalSelected = selected; | ||
|
|
||
| onMount(() => { | ||
| updateWidth(); | ||
| if (openTo) { | ||
| const pathSegments = openTo.split('/').filter(Boolean); | ||
| const pathsToExpand: string[] = []; | ||
| let currentPath = ''; | ||
| for (const segment of pathSegments) { | ||
| currentPath += '/' + segment; | ||
| pathsToExpand.push(currentPath); | ||
| } | ||
| if (pathsToExpand.length > 0) { | ||
| expanded?.update((current) => { | ||
| const next = [...current]; | ||
| pathsToExpand.forEach((path) => { | ||
| if (!next.includes(path)) { | ||
| next.push(path); | ||
| } | ||
| }); | ||
| return next; | ||
| }); | ||
| } | ||
| } | ||
| }); | ||
|
Comment on lines
+46
to
+68
|
||
|
|
||
| function updateWidth() { | ||
| containerWidth = rootContainer ? rootContainer.getBoundingClientRect().width : undefined; | ||
| } | ||
|
|
||
| function handleSelect(detail: { fullPath: string; hasChildren: boolean; title: string }) { | ||
| internalSelected = detail.fullPath; | ||
| selected = internalSelected; | ||
| if (onChange) onChange({ fullPath: detail.fullPath }); | ||
| if (onSelect) onSelect(detail); | ||
| } | ||
|
|
||
| $: containerWidth = rootContainer ? rootContainer.getBoundingClientRect().width : undefined; | ||
| </script> | ||
|
|
||
| <svelte:window on:resize={updateWidth} /> | ||
|
HarshMN2345 marked this conversation as resolved.
Outdated
|
||
|
|
||
| <div class="directory-container" class:isLoading {...$tree} bind:this={rootContainer}> | ||
| {#if isLoading} | ||
| <div class="loading-container"> | ||
| <Spinner /><span>Loading directory data...</span> | ||
| </div> | ||
| {:else} | ||
| <DirectoryItem | ||
| {directories} | ||
| {containerWidth} | ||
| selectedPath={internalSelected} | ||
| onSelect={handleSelect} | ||
| /> | ||
| {/if} | ||
| </div> | ||
|
|
||
| <style> | ||
| .directory-container { | ||
| width: 560px; | ||
| max-width: 100%; | ||
| height: 316px; | ||
| overflow-y: auto; | ||
| flex-shrink: 0; | ||
| display: flex; | ||
| padding: var(--space-2, 4px); | ||
|
|
||
| border-radius: var(--border-radius-m, 12px); | ||
| border: var(--border-width-s, 1px) solid var(--border-neutral, #ededf0); | ||
| background: var(--bgcolor-neutral-primary, #fff); | ||
|
|
||
| &::-webkit-scrollbar { | ||
| display: none; | ||
| } | ||
| } | ||
|
|
||
| .isLoading { | ||
| justify-content: center; | ||
| align-items: center; | ||
| } | ||
|
|
||
| .loading-container { | ||
| display: flex; | ||
| flex-direction: column; | ||
| align-items: center; | ||
| gap: var(--gap-m); | ||
| } | ||
| </style> | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The mutation of thumbnailStates array elements on lines 56-57 and 62-63 may not trigger reactivity in Svelte 5. Consider replacing the entire array element instead of mutating properties. For example, use:
thumbnailStates[index] = { ...thumbnailStates[index], loading: false, error: false };or usethumbnailStates = thumbnailStates.with(index, { loading: false, error: false })to ensure reactivity.