-
Notifications
You must be signed in to change notification settings - Fork 215
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 all 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,247 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <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'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import DirectoryItemSelf from './DirectoryItem.svelte'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| directories, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| level = 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| containerWidth, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| selectedPath, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onSelect | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| directories: Array<{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| title: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fileCount?: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fullPath: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| thumbnailUrl?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| thumbnailIcon?: typeof Icon; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| thumbnailHtml?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| children?: typeof directories; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hasChildren?: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| showThumbnail?: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| loading?: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| level?: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| containerWidth?: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| selectedPath?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onSelect?: (detail: { title: string; fullPath: string; hasChildren: boolean }) => void; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } = $props(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const Radio = Selector.Radio; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let radioInputs = $state<Array<HTMLInputElement | undefined>>([]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let value = $state<string | undefined>(undefined); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let thumbnailStates = $state<Array<{ loading: boolean; error: boolean }>>([]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $effect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!directories) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+55
to
+63
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 current = thumbnailStates[index]; | |
| if (!current) return; | |
| thumbnailStates = thumbnailStates.with(index, { | |
| ...current, | |
| loading: false, | |
| error: false | |
| }); | |
| } | |
| function handleThumbnailError(index: number) { | |
| const current = thumbnailStates[index]; | |
| if (!current) return; | |
| thumbnailStates = thumbnailStates.with(index, { | |
| ...current, | |
| loading: false, | |
| error: true | |
| }); |
Copilot
AI
Feb 19, 2026
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.
Directly setting radioInputs[idx].checked = true mutates a DOM element property, which may not properly sync with Svelte's reactivity system. Consider using a controlled approach by managing the checked state through Svelte state and binding, or ensuring that the Radio component's value is properly synchronized instead of directly manipulating the DOM.
| if (idx !== -1 && radioInputs[idx]) { | |
| radioInputs[idx].checked = true; | |
| if (idx !== -1) { | |
| value = directories[idx].fullPath; |
Copilot
AI
Feb 19, 2026
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 button element on lines 92-166 lacks an accessible label (aria-label) that describes its purpose. While it contains visual elements like title and icons, screen reader users may not get a clear indication of what the button does. Consider adding an aria-label that includes the directory name and its state (e.g., aria-label="Select directory: {title}, {hasChildren ? 'expandable' : 'not expandable'}")
HarshMN2345 marked this conversation as resolved.
Show resolved
Hide resolved
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,132 @@ | ||||||||||
| <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 type { DirectoryEntry } from '$lib/components/git/types'; | ||||||||||
| import { Spinner } from '@appwrite.io/pink-svelte'; | ||||||||||
|
|
||||||||||
| let { | ||||||||||
| expanded = $bindable(writable(['lib-0', 'tree-0'])), | ||||||||||
| selected = $bindable(undefined), | ||||||||||
| openTo, | ||||||||||
| directories, | ||||||||||
| isLoading = true, | ||||||||||
| onSelect, | ||||||||||
| onChange | ||||||||||
| }: { | ||||||||||
| expanded?: Writable<string[]>; | ||||||||||
| selected?: string; | ||||||||||
| openTo?: string; | ||||||||||
| directories: DirectoryEntry[]; | ||||||||||
| isLoading?: boolean; | ||||||||||
| onSelect?: (detail: { | ||||||||||
| fullPath: string; | ||||||||||
| hasChildren: boolean; | ||||||||||
| title: string; | ||||||||||
| }) => void | Promise<void>; | ||||||||||
| onChange?: (detail: { fullPath: string }) => void | Promise<void>; | ||||||||||
| } = $props(); | ||||||||||
|
|
||||||||||
| const ctx = createTreeView({ expanded }); | ||||||||||
| setContext('tree', ctx); | ||||||||||
|
|
||||||||||
| const { | ||||||||||
| elements: { tree } | ||||||||||
| } = ctx; | ||||||||||
|
|
||||||||||
| let rootContainer = $state<HTMLDivElement | undefined>(undefined); | ||||||||||
| let containerWidth = $state<number | undefined>(undefined); | ||||||||||
| let internalSelected = $state<string | undefined>(undefined); | ||||||||||
|
|
||||||||||
| $effect(() => { | ||||||||||
| 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); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| $effect(() => { | ||||||||||
| containerWidth = rootContainer ? rootContainer.getBoundingClientRect().width : undefined; | ||||||||||
| }); | ||||||||||
|
Comment on lines
+80
to
+83
|
||||||||||
| $effect(() => { | |
| containerWidth = rootContainer ? rootContainer.getBoundingClientRect().width : undefined; | |
| }); |
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 self-import on line 6 uses the alias
DirectoryItemSelfwhich is the same component importing itself. While this is technically correct for recursive components in Svelte 5, the nameDirectoryItemSelfis not clear about its purpose. Consider using a more descriptive name likeDirectoryItemRecursiveor just referencing the component by its original name if that's clearer in context.