mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-10 01:20:47 +03:00
Modal and select component fixes (#4253)
* feat: fix clickoutside for multi modal case, fix select list options display. * Move select to a separate folder * Serach field CSS tweak * Replace the old `Select` with the new one * Remove old `Select` component and switch `SelectNew` component name * Added `$effect` type to eslint config * Added the `searchable` property to `Select` * disable text selection and added an animation * dropzone hover animation update * Remove focus state outline * Move `Listitem` to `ProjectPopup` as a snippet * rename `useResize` to `resizeObserver` * Rename `useAutoHeight` to `autoHeight` * remove unused component `PopupMenuItem` * Update `Unupply` modal design * Handle `Select` position on resize * added an animation to the new context menu * Right click context menu added * revert right click context menu * Context menu: add `item` arg for the `open` method * updated context menu for `Filter`, `FileItem`, branch `kebab` menu * Updated Dropdown context menus * Temporary remove `copy PR link` * Rename: `BranchLanePopupMenu ` to `BranchLaneContextMenu` * FileListItem uncomment commented code * Hunk context menu updated * Remove `PopupMenu` * Replace the old `ContextMenu` file with the new one * Added blocking overlay for dropdown context menus * animation timing update * `Select` folder renamed to `select` * import fix * import fix * remove commented code and `console.log` * add arrow navigation to the `Select` component * case sensitivity added * Update HunkViewer.svelte * remove duplicated folder
This commit is contained in:
parent
8a77a4b179
commit
2cdbc5e205
@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import ActiveBranchStatus from './ActiveBranchStatus.svelte';
|
||||
import BranchLabel from './BranchLabel.svelte';
|
||||
import BranchLanePopupMenu from './BranchLanePopupMenu.svelte';
|
||||
import BranchLaneContextMenu from './BranchLaneContextMenu.svelte';
|
||||
import PullRequestButton from '../pr/PullRequestButton.svelte';
|
||||
import { Project } from '$lib/backend/projects';
|
||||
import { BranchService } from '$lib/branches/service';
|
||||
import { clickOutside } from '$lib/clickOutside';
|
||||
import ContextMenu from '$lib/components/contextmenu/ContextMenu.svelte';
|
||||
import { GitHubService } from '$lib/github/service';
|
||||
import { showError } from '$lib/notifications/toasts';
|
||||
import Button from '$lib/shared/Button.svelte';
|
||||
@ -33,8 +33,8 @@
|
||||
$: pr$ = githubService.getPr$(branch.upstream?.sha || branch.head);
|
||||
$: hasPullRequest = branch.upstreamName && $pr$;
|
||||
|
||||
let meatballButton: HTMLDivElement;
|
||||
let visible = false;
|
||||
let contextMenu: ContextMenu;
|
||||
let meatballButtonEl: HTMLDivElement;
|
||||
let isApplying = false;
|
||||
let isDeleting = false;
|
||||
let isLoading: boolean;
|
||||
@ -263,23 +263,19 @@
|
||||
/>
|
||||
{/if}
|
||||
<Button
|
||||
element={meatballButton}
|
||||
bind:el={meatballButtonEl}
|
||||
style="ghost"
|
||||
outline
|
||||
icon="kebab"
|
||||
on:mousedown={() => {
|
||||
visible = !visible;
|
||||
on:click={() => {
|
||||
contextMenu.toggle();
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
class="branch-popup-menu"
|
||||
use:clickOutside={{
|
||||
trigger: meatballButton,
|
||||
handler: () => (visible = false)
|
||||
}}
|
||||
>
|
||||
<BranchLanePopupMenu {isUnapplied} bind:visible on:action />
|
||||
</div>
|
||||
<BranchLaneContextMenu
|
||||
bind:contextMenuEl={contextMenu}
|
||||
{isUnapplied}
|
||||
target={meatballButtonEl}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@ -383,13 +379,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.branch-popup-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
right: 0;
|
||||
z-index: var(--z-floating);
|
||||
}
|
||||
|
||||
.header__remote-branch {
|
||||
color: var(--clr-scale-ntrl-50);
|
||||
padding-left: 2px;
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { useResize } from '$lib/utils/useResize';
|
||||
import { resizeObserver } from '$lib/utils/resizeObserver';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let name: string;
|
||||
@ -17,7 +17,7 @@
|
||||
</script>
|
||||
|
||||
<span
|
||||
use:useResize={(e) => {
|
||||
use:resizeObserver={(e) => {
|
||||
inputWidth = `${Math.round(e.frame.width)}px`;
|
||||
}}
|
||||
class="branch-name-mesure-el text-base-14 text-bold"
|
||||
|
@ -5,10 +5,10 @@
|
||||
import ContextMenuItem from '$lib/components/contextmenu/ContextMenuItem.svelte';
|
||||
import ContextMenuSection from '$lib/components/contextmenu/ContextMenuSection.svelte';
|
||||
import { projectAiGenEnabled } from '$lib/config/config';
|
||||
import Select from '$lib/select/Select.svelte';
|
||||
import SelectItem from '$lib/select/SelectItem.svelte';
|
||||
import Button from '$lib/shared/Button.svelte';
|
||||
import Modal from '$lib/shared/Modal.svelte';
|
||||
import Select from '$lib/shared/Select.svelte';
|
||||
import SelectItem from '$lib/shared/SelectItem.svelte';
|
||||
import TextBox from '$lib/shared/TextBox.svelte';
|
||||
import Toggle from '$lib/shared/Toggle.svelte';
|
||||
import { User } from '$lib/stores/user';
|
||||
@ -18,7 +18,8 @@
|
||||
import { Branch, type NameConflictResolution } from '$lib/vbranches/types';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let visible: boolean;
|
||||
export let contextMenuEl: ContextMenu;
|
||||
export let target: HTMLElement;
|
||||
export let isUnapplied = false;
|
||||
|
||||
const user = getContextStore(User);
|
||||
@ -50,10 +51,6 @@
|
||||
aiConfigurationValid = await aiService.validateConfiguration(user?.access_token);
|
||||
}
|
||||
|
||||
function close() {
|
||||
visible = false;
|
||||
}
|
||||
|
||||
let unapplyBranchModal: Modal;
|
||||
|
||||
type ResolutionVariants = NameConflictResolution['type'];
|
||||
@ -105,27 +102,47 @@
|
||||
branchController.convertToRealBranch(branch.id);
|
||||
}
|
||||
}
|
||||
|
||||
function setButtonCoppy() {
|
||||
switch (selectedResolution) {
|
||||
case 'overwrite':
|
||||
return 'Overwrite and unapply';
|
||||
case 'suffix':
|
||||
return 'Suffix and unapply';
|
||||
case 'rename':
|
||||
return 'Rename and unapply';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={unapplyBranchModal}>
|
||||
<Modal width="small" bind:this={unapplyBranchModal}>
|
||||
<div class="flow">
|
||||
<div class="modal-copy">
|
||||
<p class="text-base-15">There is already branch with the name</p>
|
||||
<Button size="tag" clickable={false}>{normalizeBranchName(branch.name)}</Button>
|
||||
<p class="text-base-15">.</p>
|
||||
<p class="text-base-15">Please choose how you want to resolve this:</p>
|
||||
<p class="text-base-14 text-semibold">
|
||||
"{normalizeBranchName(branch.name)}" branch already exists
|
||||
</p>
|
||||
|
||||
<p class="text-base-body-13 modal-copy-caption">
|
||||
A branch with the same name already exists.
|
||||
<br />
|
||||
Please select a resolution:
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Select
|
||||
items={resolutions}
|
||||
itemId={'value'}
|
||||
labelId={'label'}
|
||||
bind:selectedItemId={selectedResolution}
|
||||
value={selectedResolution}
|
||||
options={resolutions}
|
||||
onselect={(value) => {
|
||||
selectedResolution = value as ResolutionVariants;
|
||||
}}
|
||||
>
|
||||
<SelectItem slot="template" let:item let:selected {selected}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
{#snippet itemSnippet({ item, highlighted })}
|
||||
<SelectItem selected={item.value === selectedResolution} {highlighted}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
{/snippet}
|
||||
</Select>
|
||||
|
||||
{#if selectedResolution === 'rename'}
|
||||
<TextBox
|
||||
label="New branch name"
|
||||
@ -137,107 +154,112 @@
|
||||
</div>
|
||||
{#snippet controls()}
|
||||
<Button style="ghost" outline on:click={() => unapplyBranchModal.close()}>Cancel</Button>
|
||||
<Button style="pop" kind="solid" grow on:click={unapplyBranchWithSelectedResolution}
|
||||
>Submit</Button
|
||||
<Button
|
||||
style="pop"
|
||||
kind="solid"
|
||||
on:click={unapplyBranchWithSelectedResolution}
|
||||
disabled={!newBranchName && selectedResolution === 'rename'}
|
||||
>
|
||||
{setButtonCoppy()}
|
||||
</Button>
|
||||
{/snippet}
|
||||
</Modal>
|
||||
|
||||
{#if visible}
|
||||
<ContextMenu>
|
||||
<ContextMenuSection>
|
||||
{#if !isUnapplied}
|
||||
<ContextMenuItem
|
||||
label="Collapse lane"
|
||||
on:click={() => {
|
||||
dispatch('action', 'collapse');
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</ContextMenuSection>
|
||||
<ContextMenuSection>
|
||||
{#if !isUnapplied}
|
||||
<ContextMenuItem
|
||||
label="Unapply"
|
||||
on:click={() => {
|
||||
tryUnapplyBranch();
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<ContextMenu bind:this={contextMenuEl} {target}>
|
||||
<ContextMenuSection>
|
||||
{#if !isUnapplied}
|
||||
<ContextMenuItem
|
||||
label="Delete"
|
||||
on:click={async () => {
|
||||
if (
|
||||
branch.name.toLowerCase().includes('virtual branch') &&
|
||||
commits.length === 0 &&
|
||||
branch.files?.length === 0
|
||||
) {
|
||||
await branchController.deleteBranch(branch.id);
|
||||
} else {
|
||||
deleteBranchModal.show(branch);
|
||||
}
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
|
||||
<ContextMenuItem
|
||||
label="Generate branch name"
|
||||
label="Collapse lane"
|
||||
on:click={() => {
|
||||
dispatch('action', 'generate-branch-name');
|
||||
close();
|
||||
dispatch('action', 'collapse');
|
||||
contextMenuEl.close();
|
||||
}}
|
||||
disabled={isUnapplied ||
|
||||
!($aiGenEnabled && aiConfigurationValid) ||
|
||||
branch.files?.length === 0}
|
||||
/>
|
||||
</ContextMenuSection>
|
||||
|
||||
<ContextMenuSection>
|
||||
{/if}
|
||||
</ContextMenuSection>
|
||||
<ContextMenuSection>
|
||||
{#if !isUnapplied}
|
||||
<ContextMenuItem
|
||||
label="Set remote branch name"
|
||||
disabled={isUnapplied}
|
||||
label="Unapply"
|
||||
on:click={() => {
|
||||
newRemoteName = branch.upstreamName || normalizeBranchName(branch.name) || '';
|
||||
close();
|
||||
renameRemoteModal.show(branch);
|
||||
tryUnapplyBranch();
|
||||
contextMenuEl.close();
|
||||
}}
|
||||
/>
|
||||
</ContextMenuSection>
|
||||
{/if}
|
||||
|
||||
<ContextMenuSection>
|
||||
<ContextMenuItem label="Allow rebasing" on:click={toggleAllowRebasing}>
|
||||
<Toggle
|
||||
small
|
||||
slot="control"
|
||||
bind:checked={allowRebasing}
|
||||
on:click={toggleAllowRebasing}
|
||||
help="Having this enabled permits commit amending and reordering after a branch has been pushed, which would subsequently require force pushing"
|
||||
/>
|
||||
</ContextMenuItem>
|
||||
</ContextMenuSection>
|
||||
<ContextMenuItem
|
||||
label="Delete"
|
||||
on:click={async () => {
|
||||
if (
|
||||
branch.name.toLowerCase().includes('virtual branch') &&
|
||||
commits.length === 0 &&
|
||||
branch.files?.length === 0
|
||||
) {
|
||||
await branchController.deleteBranch(branch.id);
|
||||
} else {
|
||||
deleteBranchModal.show(branch);
|
||||
}
|
||||
contextMenuEl.close();
|
||||
}}
|
||||
/>
|
||||
|
||||
<ContextMenuSection>
|
||||
<ContextMenuItem
|
||||
label="Create branch to the left"
|
||||
on:click={() => {
|
||||
branchController.createBranch({ order: branch.order });
|
||||
close();
|
||||
}}
|
||||
<ContextMenuItem
|
||||
label="Generate branch name"
|
||||
on:click={() => {
|
||||
dispatch('action', 'generate-branch-name');
|
||||
contextMenuEl.close();
|
||||
}}
|
||||
disabled={isUnapplied ||
|
||||
!($aiGenEnabled && aiConfigurationValid) ||
|
||||
branch.files?.length === 0}
|
||||
/>
|
||||
</ContextMenuSection>
|
||||
|
||||
<ContextMenuSection>
|
||||
<ContextMenuItem
|
||||
label="Set remote branch name"
|
||||
disabled={isUnapplied}
|
||||
on:click={() => {
|
||||
console.log('Set remote branch name');
|
||||
|
||||
newRemoteName = branch.upstreamName || normalizeBranchName(branch.name) || '';
|
||||
renameRemoteModal.show(branch);
|
||||
contextMenuEl.close();
|
||||
}}
|
||||
/>
|
||||
</ContextMenuSection>
|
||||
|
||||
<ContextMenuSection>
|
||||
<ContextMenuItem label="Allow rebasing" on:click={toggleAllowRebasing}>
|
||||
<Toggle
|
||||
small
|
||||
slot="control"
|
||||
bind:checked={allowRebasing}
|
||||
on:click={toggleAllowRebasing}
|
||||
help="Having this enabled permits commit amending and reordering after a branch has been pushed, which would subsequently require force pushing"
|
||||
/>
|
||||
</ContextMenuItem>
|
||||
</ContextMenuSection>
|
||||
|
||||
<ContextMenuItem
|
||||
label="Create branch to the right"
|
||||
on:click={() => {
|
||||
branchController.createBranch({ order: branch.order + 1 });
|
||||
close();
|
||||
}}
|
||||
/>
|
||||
</ContextMenuSection>
|
||||
</ContextMenu>
|
||||
{/if}
|
||||
<ContextMenuSection>
|
||||
<ContextMenuItem
|
||||
label="Create branch to the left"
|
||||
on:click={() => {
|
||||
branchController.createBranch({ order: branch.order });
|
||||
contextMenuEl.close();
|
||||
}}
|
||||
/>
|
||||
|
||||
<ContextMenuItem
|
||||
label="Create branch to the right"
|
||||
on:click={() => {
|
||||
branchController.createBranch({ order: branch.order + 1 });
|
||||
contextMenuEl.close();
|
||||
}}
|
||||
/>
|
||||
</ContextMenuSection>
|
||||
</ContextMenu>
|
||||
|
||||
<Modal width="small" bind:this={renameRemoteModal}>
|
||||
<TextBox label="Remote branch name" id="newRemoteName" bind:value={newRemoteName} focus />
|
||||
@ -282,9 +304,14 @@
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.modal-copy {
|
||||
& > * {
|
||||
display: inline;
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.modal-copy-caption {
|
||||
color: var(--clr-text-2);
|
||||
}
|
||||
</style>
|
@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import BranchLabel from './BranchLabel.svelte';
|
||||
import { Project } from '$lib/backend/projects';
|
||||
import ViewPrContextMenu from '$lib/components/ViewPrContextMenu.svelte';
|
||||
import Button from '$lib/shared/Button.svelte';
|
||||
import Icon from '$lib/shared/Icon.svelte';
|
||||
import { getContext } from '$lib/utils/context';
|
||||
@ -9,7 +8,6 @@
|
||||
import { tooltip } from '$lib/utils/tooltip';
|
||||
import { openExternalUrl } from '$lib/utils/url';
|
||||
import { BranchController } from '$lib/vbranches/branchController';
|
||||
import { mount, onDestroy, unmount } from 'svelte';
|
||||
import type { PullRequest } from '$lib/github/types';
|
||||
import type { BaseBranch, RemoteBranch } from '$lib/vbranches/types';
|
||||
import { goto } from '$app/navigation';
|
||||
@ -22,22 +20,6 @@
|
||||
const project = getContext(Project);
|
||||
|
||||
let isApplying = false;
|
||||
|
||||
function updateContextMenu(copyablePrUrl: string) {
|
||||
if (popupMenu) unmount(popupMenu);
|
||||
return mount(ViewPrContextMenu, {
|
||||
target: document.body,
|
||||
props: { prUrl: copyablePrUrl }
|
||||
});
|
||||
}
|
||||
|
||||
$: popupMenu = updateContextMenu(pr?.htmlUrl || '');
|
||||
|
||||
onDestroy(() => {
|
||||
if (popupMenu) {
|
||||
unmount(popupMenu);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="header__wrapper">
|
||||
|
21
app/src/lib/clickOutsideNew.ts
Normal file
21
app/src/lib/clickOutsideNew.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export type ClickOpts = { excludeElement?: HTMLElement; handler: () => void };
|
||||
|
||||
export function clickOutside(node: HTMLElement, params: ClickOpts) {
|
||||
function onClick(event: MouseEvent) {
|
||||
if (
|
||||
node &&
|
||||
!node.contains(event.target as HTMLElement) &&
|
||||
!params.excludeElement?.contains(event.target as HTMLElement)
|
||||
) {
|
||||
params.handler();
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', onClick, true);
|
||||
document.addEventListener('contextmenu', onClick, true);
|
||||
return {
|
||||
destroy() {
|
||||
document.removeEventListener('mousedown', onClick, true);
|
||||
document.removeEventListener('contextmenu', onClick, true);
|
||||
}
|
||||
};
|
||||
}
|
@ -2,7 +2,6 @@
|
||||
import { PromptService } from '$lib/ai/promptService';
|
||||
import { AIService } from '$lib/ai/service';
|
||||
import { Project } from '$lib/backend/projects';
|
||||
import ContextMenu from '$lib/components/contextmenu/ContextMenu.svelte';
|
||||
import ContextMenuItem from '$lib/components/contextmenu/ContextMenuItem.svelte';
|
||||
import ContextMenuSection from '$lib/components/contextmenu/ContextMenuSection.svelte';
|
||||
import {
|
||||
@ -16,11 +15,11 @@
|
||||
import DropDownButton from '$lib/shared/DropDownButton.svelte';
|
||||
import Icon from '$lib/shared/Icon.svelte';
|
||||
import { User } from '$lib/stores/user';
|
||||
import { autoHeight } from '$lib/utils/autoHeight';
|
||||
import { splitMessage } from '$lib/utils/commitMessage';
|
||||
import { getContext, getContextStore } from '$lib/utils/context';
|
||||
import { resizeObserver } from '$lib/utils/resizeObserver';
|
||||
import { tooltip } from '$lib/utils/tooltip';
|
||||
import { useAutoHeight } from '$lib/utils/useAutoHeight';
|
||||
import { useResize } from '$lib/utils/useResize';
|
||||
import { Ownership } from '$lib/vbranches/ownership';
|
||||
import { Branch, LocalFile } from '$lib/vbranches/types';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
@ -115,9 +114,9 @@
|
||||
{#if isExpanded}
|
||||
<div
|
||||
class="commit-box__textarea-wrapper text-input"
|
||||
use:useResize={() => {
|
||||
if (titleTextArea) useAutoHeight(titleTextArea);
|
||||
if (descriptionTextArea) useAutoHeight(descriptionTextArea);
|
||||
use:resizeObserver={() => {
|
||||
if (titleTextArea) autoHeight(titleTextArea);
|
||||
if (descriptionTextArea) autoHeight(descriptionTextArea);
|
||||
}}
|
||||
>
|
||||
<textarea
|
||||
@ -129,10 +128,10 @@
|
||||
rows="1"
|
||||
bind:this={titleTextArea}
|
||||
use:focusTextAreaOnMount
|
||||
on:focus={(e) => useAutoHeight(e.currentTarget)}
|
||||
on:focus={(e) => autoHeight(e.currentTarget)}
|
||||
on:input={(e) => {
|
||||
commitMessage = concatMessage(e.currentTarget.value, description);
|
||||
useAutoHeight(e.currentTarget);
|
||||
autoHeight(e.currentTarget);
|
||||
}}
|
||||
on:keydown={(e) => {
|
||||
if (commit && (e.ctrlKey || e.metaKey) && e.key === 'Enter') commit();
|
||||
@ -152,10 +151,10 @@
|
||||
spellcheck="false"
|
||||
rows="1"
|
||||
bind:this={descriptionTextArea}
|
||||
on:focus={(e) => useAutoHeight(e.currentTarget)}
|
||||
on:focus={(e) => autoHeight(e.currentTarget)}
|
||||
on:input={(e) => {
|
||||
commitMessage = concatMessage(title, e.currentTarget.value);
|
||||
useAutoHeight(e.currentTarget);
|
||||
autoHeight(e.currentTarget);
|
||||
}}
|
||||
on:keydown={(e) => {
|
||||
const value = e.currentTarget.value;
|
||||
@ -165,7 +164,7 @@
|
||||
titleTextArea?.focus();
|
||||
titleTextArea.selectionStart = titleTextArea.textLength;
|
||||
}
|
||||
useAutoHeight(e.currentTarget);
|
||||
autoHeight(e.currentTarget);
|
||||
} else if (e.key === 'a' && (e.metaKey || e.ctrlKey) && value.length === 0) {
|
||||
// select previous textarea on cmd+a if this textarea is empty
|
||||
e.preventDefault();
|
||||
@ -205,23 +204,22 @@
|
||||
on:click={async () => await generateCommitMessage($branch.files)}
|
||||
>
|
||||
Generate message
|
||||
<ContextMenu slot="context-menu">
|
||||
<ContextMenuSection>
|
||||
<ContextMenuItem
|
||||
label="Extra concise"
|
||||
on:click={() => ($commitGenerationExtraConcise = !$commitGenerationExtraConcise)}
|
||||
>
|
||||
<Checkbox small slot="control" bind:checked={$commitGenerationExtraConcise} />
|
||||
</ContextMenuItem>
|
||||
|
||||
<ContextMenuItem
|
||||
label="Use emojis 😎"
|
||||
on:click={() => ($commitGenerationUseEmojis = !$commitGenerationUseEmojis)}
|
||||
>
|
||||
<Checkbox small slot="control" bind:checked={$commitGenerationUseEmojis} />
|
||||
</ContextMenuItem>
|
||||
</ContextMenuSection>
|
||||
</ContextMenu>
|
||||
<ContextMenuSection slot="context-menu">
|
||||
<ContextMenuItem
|
||||
label="Extra concise"
|
||||
on:click={() => ($commitGenerationExtraConcise = !$commitGenerationExtraConcise)}
|
||||
>
|
||||
<Checkbox small slot="control" bind:checked={$commitGenerationExtraConcise} />
|
||||
</ContextMenuItem>
|
||||
|
||||
<ContextMenuItem
|
||||
label="Use emojis 😎"
|
||||
on:click={() => ($commitGenerationUseEmojis = !$commitGenerationUseEmojis)}
|
||||
>
|
||||
<Checkbox small slot="control" bind:checked={$commitGenerationUseEmojis} />
|
||||
</ContextMenuItem>
|
||||
</ContextMenuSection>
|
||||
</DropDownButton>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -2,7 +2,7 @@
|
||||
import { MessageRole } from '$lib/ai/types';
|
||||
import Button from '$lib/shared/Button.svelte';
|
||||
import Icon from '$lib/shared/Icon.svelte';
|
||||
import { useAutoHeight } from '$lib/utils/useAutoHeight';
|
||||
import { autoHeight } from '$lib/utils/autoHeight';
|
||||
import { marked } from 'marked';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
@ -33,7 +33,7 @@
|
||||
|
||||
$: focusTextareaOnMount(textareaElement, autofocus, editing);
|
||||
|
||||
$: if (textareaElement) useAutoHeight(textareaElement);
|
||||
$: if (textareaElement) autoHeight(textareaElement);
|
||||
</script>
|
||||
|
||||
<div
|
||||
@ -61,12 +61,12 @@
|
||||
class:is-error={isError}
|
||||
rows={1}
|
||||
on:input={(e) => {
|
||||
useAutoHeight(e.currentTarget);
|
||||
autoHeight(e.currentTarget);
|
||||
|
||||
dispatcher('input', e.currentTarget.value);
|
||||
}}
|
||||
on:change={(e) => {
|
||||
useAutoHeight(e.currentTarget);
|
||||
autoHeight(e.currentTarget);
|
||||
}}
|
||||
></textarea>
|
||||
{:else}
|
||||
|
@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import Select from '../shared/Select.svelte';
|
||||
import { PromptService } from '$lib/ai/promptService';
|
||||
import { Project } from '$lib/backend/projects';
|
||||
import SelectItem from '$lib/shared/SelectItem.svelte';
|
||||
import Select from '$lib/select/Select.svelte';
|
||||
import SelectItem from '$lib/select/SelectItem.svelte';
|
||||
import { getContext } from '$lib/utils/context';
|
||||
import type { Prompts, UserPrompt } from '$lib/ai/types';
|
||||
import type { Persisted } from '$lib/persisted/persisted';
|
||||
@ -24,7 +24,6 @@
|
||||
}
|
||||
|
||||
let userPrompts = prompts.userPrompts;
|
||||
|
||||
let allPrompts: UserPrompt[] = [];
|
||||
|
||||
const defaultId = crypto.randomUUID();
|
||||
@ -44,16 +43,19 @@
|
||||
</script>
|
||||
|
||||
<Select
|
||||
items={allPrompts}
|
||||
bind:selectedItemId={$selectedPromptId}
|
||||
itemId="id"
|
||||
labelId="name"
|
||||
disabled={allPrompts.length === 1}
|
||||
wide={true}
|
||||
value={$selectedPromptId}
|
||||
options={allPrompts.map((p) => ({ label: p.name, value: p.id }))}
|
||||
label={promptUse === 'commits' ? 'Commit message' : 'Branch name'}
|
||||
wide={true}
|
||||
searchable
|
||||
disabled={allPrompts.length === 1}
|
||||
onselect={(value) => {
|
||||
$selectedPromptId = value;
|
||||
}}
|
||||
>
|
||||
<SelectItem slot="template" let:item let:selected {selected} let:highlighted {highlighted}>
|
||||
{item.name}
|
||||
{highlighted}
|
||||
</SelectItem>
|
||||
{#snippet itemSnippet({ item, highlighted })}
|
||||
<SelectItem selected={item.value === $selectedPromptId} {highlighted}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
{/snippet}
|
||||
</Select>
|
||||
|
@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import InfoMessage from '../shared/InfoMessage.svelte';
|
||||
import Select from '../shared/Select.svelte';
|
||||
import { Project } from '$lib/backend/projects';
|
||||
import SectionCard from '$lib/components/SectionCard.svelte';
|
||||
import Select from '$lib/select/Select.svelte';
|
||||
import SelectItem from '$lib/select/SelectItem.svelte';
|
||||
import Section from '$lib/settings/Section.svelte';
|
||||
import Button from '$lib/shared/Button.svelte';
|
||||
import SelectItem from '$lib/shared/SelectItem.svelte';
|
||||
import { getContext, getContextStore } from '$lib/utils/context';
|
||||
import { getRemoteBranches } from '$lib/vbranches/baseBranch';
|
||||
import { BranchController } from '$lib/vbranches/branchController';
|
||||
@ -67,47 +67,37 @@
|
||||
</svelte:fragment>
|
||||
|
||||
<Select
|
||||
items={remoteBranches}
|
||||
bind:value={selectedBranch}
|
||||
itemId="name"
|
||||
labelId="name"
|
||||
selectedItemId={$baseBranch.branchName}
|
||||
value={selectedBranch.name}
|
||||
options={remoteBranches.map((b) => ({ label: b.name, value: b.name }))}
|
||||
onselect={(value) => {
|
||||
selectedBranch = { name: value };
|
||||
}}
|
||||
disabled={targetChangeDisabled}
|
||||
wide={true}
|
||||
label="Current target branch"
|
||||
searchable
|
||||
>
|
||||
<SelectItem
|
||||
slot="template"
|
||||
let:item
|
||||
let:selected
|
||||
{selected}
|
||||
let:highlighted
|
||||
{highlighted}
|
||||
>
|
||||
{item.name}
|
||||
</SelectItem>
|
||||
{#snippet itemSnippet({ item, highlighted })}
|
||||
<SelectItem selected={item.value === selectedBranch.name} {highlighted}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
{/snippet}
|
||||
</Select>
|
||||
|
||||
{#if uniqueRemotes(remoteBranches).length > 1}
|
||||
<Select
|
||||
items={uniqueRemotes(remoteBranches)}
|
||||
bind:value={selectedRemote}
|
||||
itemId="name"
|
||||
labelId="name"
|
||||
selectedItemId={$baseBranch.actualPushRemoteName()}
|
||||
value={selectedRemote.name}
|
||||
options={uniqueRemotes(remoteBranches).map((r) => ({ label: r.name, value: r.name }))}
|
||||
onselect={(value) => {
|
||||
selectedRemote = { name: value };
|
||||
}}
|
||||
disabled={targetChangeDisabled}
|
||||
label="Create branches on remote"
|
||||
>
|
||||
<SelectItem
|
||||
slot="template"
|
||||
let:item
|
||||
let:selected
|
||||
{selected}
|
||||
let:highlighted
|
||||
{highlighted}
|
||||
>
|
||||
{item.name}
|
||||
</SelectItem>
|
||||
{#snippet itemSnippet({ item, highlighted })}
|
||||
<SelectItem selected={item.value === selectedRemote.name} {highlighted}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
{/snippet}
|
||||
</Select>
|
||||
{/if}
|
||||
|
||||
|
@ -2,11 +2,12 @@
|
||||
import ContextMenu from '$lib/components/contextmenu/ContextMenu.svelte';
|
||||
import ContextMenuItem from '$lib/components/contextmenu/ContextMenuItem.svelte';
|
||||
import ContextMenuSection from '$lib/components/contextmenu/ContextMenuSection.svelte';
|
||||
import Button from '$lib/shared/Button.svelte';
|
||||
import Checkbox from '$lib/shared/Checkbox.svelte';
|
||||
import Toggle from '$lib/shared/Toggle.svelte';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { Writable, Readable } from 'svelte/store';
|
||||
|
||||
export let visible: boolean;
|
||||
export let filtersActive: Readable<boolean>;
|
||||
export let showPrCheckbox: boolean;
|
||||
|
||||
export let includePrs: Writable<boolean | undefined>;
|
||||
@ -14,10 +15,26 @@
|
||||
export let includeStashed: Writable<boolean | undefined>;
|
||||
export let hideBots: Writable<boolean | undefined>;
|
||||
export let hideInactive: Writable<boolean | undefined>;
|
||||
|
||||
let target: HTMLElement;
|
||||
let contextMenu: ContextMenu;
|
||||
|
||||
export function onFilterClick() {
|
||||
contextMenu.toggle();
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if visible}
|
||||
<ContextMenu>
|
||||
<div class="header__filter-btn">
|
||||
<Button
|
||||
bind:el={target}
|
||||
style="ghost"
|
||||
outline
|
||||
icon={$filtersActive ? 'filter-applied-small' : 'filter-small'}
|
||||
on:mousedown={onFilterClick}
|
||||
>
|
||||
Filter
|
||||
</Button>
|
||||
<ContextMenu bind:this={contextMenu} {target}>
|
||||
<ContextMenuSection>
|
||||
{#if showPrCheckbox}
|
||||
<ContextMenuItem label="Pull requests" on:click={() => ($includePrs = !$includePrs)}>
|
||||
@ -42,4 +59,4 @@
|
||||
</ContextMenuItem>
|
||||
</ContextMenuSection>
|
||||
</ContextMenu>
|
||||
{/if}
|
||||
</div>
|
@ -5,10 +5,10 @@
|
||||
import Login from '$lib/components/Login.svelte';
|
||||
import SetupFeature from '$lib/components/SetupFeature.svelte';
|
||||
import { projectAiGenEnabled } from '$lib/config/config';
|
||||
import Select from '$lib/select/Select.svelte';
|
||||
import SelectItem from '$lib/select/SelectItem.svelte';
|
||||
import GithubIntegration from '$lib/settings/GithubIntegration.svelte';
|
||||
import Button from '$lib/shared/Button.svelte';
|
||||
import Select from '$lib/shared/Select.svelte';
|
||||
import SelectItem from '$lib/shared/SelectItem.svelte';
|
||||
import Toggle from '$lib/shared/Toggle.svelte';
|
||||
import { UserService } from '$lib/stores/user';
|
||||
import { getContext } from '$lib/utils/context';
|
||||
@ -88,11 +88,22 @@
|
||||
|
||||
<div class="project-setup__fields">
|
||||
<div class="project-setup__field-wrap">
|
||||
<Select items={remoteBranches} bind:value={selectedBranch} itemId="name" labelId="name">
|
||||
<SelectItem slot="template" let:item let:selected {selected} let:highlighted {highlighted}>
|
||||
{item.name}
|
||||
</SelectItem>
|
||||
<Select
|
||||
value={selectedBranch.name}
|
||||
options={remoteBranches.map((b) => ({ label: b.name, value: b.name }))}
|
||||
onselect={(value) => {
|
||||
selectedBranch = { name: value };
|
||||
}}
|
||||
label="Current target branch"
|
||||
searchable
|
||||
>
|
||||
{#snippet itemSnippet({ item, highlighted })}
|
||||
<SelectItem selected={item.value === selectedBranch.name} {highlighted}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
{/snippet}
|
||||
</Select>
|
||||
|
||||
<p class="project-setup__description-text text-base-body-12">
|
||||
This is the branch that you consider "production", normally something like "origin/master"
|
||||
or "upstream/main".
|
||||
@ -101,18 +112,21 @@
|
||||
|
||||
{#if remotes.length > 1}
|
||||
<div class="project-setup__field-wrap">
|
||||
<Select items={remotes} bind:value={selectedRemote} itemId="name" labelId="name">
|
||||
<SelectItem
|
||||
slot="template"
|
||||
let:item
|
||||
let:selected
|
||||
{selected}
|
||||
let:highlighted
|
||||
{highlighted}
|
||||
>
|
||||
{item.name}
|
||||
</SelectItem>
|
||||
<Select
|
||||
value={selectedRemote.value}
|
||||
options={remotes.map((r) => ({ label: r.name, value: r.value }))}
|
||||
onselect={(value) => {
|
||||
const newSelectedRemote = remotes.find((r) => r.value === value);
|
||||
selectedRemote = newSelectedRemote || selectedRemote;
|
||||
}}
|
||||
>
|
||||
{#snippet itemSnippet({ item, highlighted })}
|
||||
<SelectItem selected={item.value === selectedRemote.name} {highlighted}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
{/snippet}
|
||||
</Select>
|
||||
|
||||
<p class="project-setup__description-text text-base-body-12">
|
||||
You have branches from multiple remotes. If you want to specify a remote for creating
|
||||
branches that is different from the remote that your target branch is on, change it here.
|
||||
|
@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import Select from '../shared/Select.svelte';
|
||||
import SelectItem from '../shared/SelectItem.svelte';
|
||||
import { ProjectService, Project } from '$lib/backend/projects';
|
||||
import OptionsGroup from '$lib/select/OptionsGroup.svelte';
|
||||
import Select from '$lib/select/Select.svelte';
|
||||
import SelectItem from '$lib/select/SelectItem.svelte';
|
||||
import Button from '$lib/shared/Button.svelte';
|
||||
import { getContext, maybeGetContext } from '$lib/utils/context';
|
||||
import { derived } from 'svelte/store';
|
||||
@ -10,61 +11,59 @@
|
||||
const projectService = getContext(ProjectService);
|
||||
const project = maybeGetContext(Project);
|
||||
|
||||
type ProjectRecord = {
|
||||
id: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
const mappedProjects = derived(projectService.projects, (projects) =>
|
||||
projects.map((project) => ({
|
||||
id: project.id,
|
||||
title: project.title
|
||||
value: project.id,
|
||||
label: project.title
|
||||
}))
|
||||
);
|
||||
|
||||
let loading = false;
|
||||
let select: Select<ProjectRecord>;
|
||||
let selectValue: ProjectRecord | undefined = project;
|
||||
let selectedProjectId: string | undefined = project ? project.id : undefined;
|
||||
</script>
|
||||
|
||||
<div class="project-switcher">
|
||||
<Select
|
||||
id="select-project"
|
||||
value={selectedProjectId}
|
||||
options={$mappedProjects}
|
||||
label="Switch to another project"
|
||||
itemId="id"
|
||||
labelId="title"
|
||||
items={$mappedProjects}
|
||||
placeholder="Select a project..."
|
||||
wide
|
||||
bind:value={selectValue}
|
||||
bind:this={select}
|
||||
onselect={(value) => {
|
||||
selectedProjectId = value;
|
||||
}}
|
||||
searchable
|
||||
>
|
||||
<SelectItem slot="template" let:item let:selected {selected} let:highlighted {highlighted}>
|
||||
{item.title}
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
slot="append"
|
||||
icon="plus"
|
||||
{loading}
|
||||
on:click={async () => {
|
||||
loading = true;
|
||||
try {
|
||||
await projectService.addProject();
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}}
|
||||
>
|
||||
Add new project
|
||||
</SelectItem>
|
||||
{#snippet itemSnippet({ item, highlighted })}
|
||||
<SelectItem selected={item.value === selectedProjectId} {highlighted}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
{/snippet}
|
||||
|
||||
<OptionsGroup>
|
||||
<SelectItem
|
||||
icon="plus"
|
||||
{loading}
|
||||
on:click={async () => {
|
||||
loading = true;
|
||||
try {
|
||||
await projectService.addProject();
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}}
|
||||
>
|
||||
Add new project
|
||||
</SelectItem>
|
||||
</OptionsGroup>
|
||||
</Select>
|
||||
|
||||
<Button
|
||||
style="pop"
|
||||
kind="solid"
|
||||
icon="chevron-right-small"
|
||||
disabled={selectValue === project}
|
||||
disabled={selectedProjectId === project}
|
||||
on:mousedown={() => {
|
||||
if (selectValue) goto(`/${selectValue.id}/`);
|
||||
if (selectedProjectId) goto(`/${selectedProjectId}/`);
|
||||
}}
|
||||
>
|
||||
Open project
|
||||
|
@ -6,7 +6,6 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import ContextMenu from '$lib/components/contextmenu/ContextMenu.svelte';
|
||||
import ContextMenuItem from '$lib/components/contextmenu/ContextMenuItem.svelte';
|
||||
import ContextMenuSection from '$lib/components/contextmenu/ContextMenuSection.svelte';
|
||||
import { persisted } from '$lib/persisted/persisted';
|
||||
@ -26,7 +25,6 @@
|
||||
'projectDefaultAction_' + projectId
|
||||
);
|
||||
|
||||
let contextMenu: ContextMenu;
|
||||
let dropDown: DropDownButton;
|
||||
let disabled = false;
|
||||
|
||||
@ -56,23 +54,21 @@
|
||||
}}
|
||||
>
|
||||
{labels[action]}
|
||||
<ContextMenu slot="context-menu" bind:this={contextMenu}>
|
||||
<ContextMenuSection>
|
||||
<ContextMenuItem
|
||||
label={labels[BranchAction.Push]}
|
||||
on:click={() => {
|
||||
$preferredAction = BranchAction.Push;
|
||||
dropDown.close();
|
||||
}}
|
||||
/>
|
||||
<ContextMenuItem
|
||||
label={labels[BranchAction.Integrate]}
|
||||
disabled={!integrate}
|
||||
on:click={() => {
|
||||
$preferredAction = BranchAction.Integrate;
|
||||
dropDown.close();
|
||||
}}
|
||||
/>
|
||||
</ContextMenuSection>
|
||||
</ContextMenu>
|
||||
<ContextMenuSection slot="context-menu">
|
||||
<ContextMenuItem
|
||||
label={labels[BranchAction.Push]}
|
||||
on:click={() => {
|
||||
$preferredAction = BranchAction.Push;
|
||||
dropDown.close();
|
||||
}}
|
||||
/>
|
||||
<ContextMenuItem
|
||||
label={labels[BranchAction.Integrate]}
|
||||
disabled={!integrate}
|
||||
on:click={() => {
|
||||
$preferredAction = BranchAction.Integrate;
|
||||
dropDown.close();
|
||||
}}
|
||||
/>
|
||||
</ContextMenuSection>
|
||||
</DropDownButton>
|
||||
|
@ -1,28 +0,0 @@
|
||||
<script lang="ts">
|
||||
import ContextMenu from '$lib/components/contextmenu/ContextMenu.svelte';
|
||||
import ContextMenuItem from '$lib/components/contextmenu/ContextMenuItem.svelte';
|
||||
import ContextMenuSection from '$lib/components/contextmenu/ContextMenuSection.svelte';
|
||||
import PopupMenu from '$lib/shared/PopupMenu.svelte';
|
||||
import { copyToClipboard } from '$lib/utils/clipboard';
|
||||
|
||||
export let prUrl: string;
|
||||
let popupMenu: PopupMenu;
|
||||
|
||||
export function openByMouse(e: MouseEvent, item: any) {
|
||||
popupMenu.openByMouse(e, item);
|
||||
}
|
||||
</script>
|
||||
|
||||
<PopupMenu bind:this={popupMenu} let:dismiss>
|
||||
<ContextMenu>
|
||||
<ContextMenuSection>
|
||||
<ContextMenuItem
|
||||
label="Copy to Clipbaord"
|
||||
on:click={() => {
|
||||
copyToClipboard(prUrl);
|
||||
dismiss();
|
||||
}}
|
||||
/>
|
||||
</ContextMenuSection>
|
||||
</ContextMenu>
|
||||
</PopupMenu>
|
@ -1,14 +1,196 @@
|
||||
<div class="context-menu">
|
||||
<slot />
|
||||
</div>
|
||||
<script lang="ts">
|
||||
import { clickOutside } from '$lib/clickOutsideNew';
|
||||
import { portal } from '$lib/utils/portal';
|
||||
import { pxToRem } from '$lib/utils/pxToRem';
|
||||
import { resizeObserver } from '$lib/utils/resizeObserver';
|
||||
import { type Snippet } from 'svelte';
|
||||
|
||||
// TYPES AND INTERFACES
|
||||
interface Props {
|
||||
target?: HTMLElement;
|
||||
openByMouse?: boolean;
|
||||
verticalAlign?: 'top' | 'bottom';
|
||||
horizontalAlign?: 'left' | 'right';
|
||||
children: Snippet<[item: any]>;
|
||||
onclose?: () => void;
|
||||
onopen?: () => void;
|
||||
}
|
||||
|
||||
const {
|
||||
target,
|
||||
openByMouse,
|
||||
verticalAlign = 'bottom',
|
||||
horizontalAlign = 'right',
|
||||
children,
|
||||
onclose,
|
||||
onopen
|
||||
}: Props = $props();
|
||||
|
||||
// LOCAL VARS
|
||||
let menuMargin = 4;
|
||||
|
||||
// STATES
|
||||
let item = $state<any>();
|
||||
let contextMenuHeight = $state(0);
|
||||
let contextMenuWidth = $state(0);
|
||||
let isVisibile = $state(false);
|
||||
let menuPosition = $state({ x: 0, y: 0 });
|
||||
|
||||
// METHODS
|
||||
export function close() {
|
||||
isVisibile = false;
|
||||
onclose && onclose();
|
||||
}
|
||||
|
||||
export function open(e?: MouseEvent, newItem?: any) {
|
||||
if (!target) return;
|
||||
|
||||
if (newItem) item = newItem;
|
||||
isVisibile = true;
|
||||
onopen && onopen();
|
||||
|
||||
if (!openByMouse) {
|
||||
setAlignByTarget();
|
||||
}
|
||||
|
||||
if (openByMouse && e) {
|
||||
menuPosition = {
|
||||
x: e.clientX,
|
||||
y: e.clientY
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function toggle(e?: MouseEvent, newItem?: any) {
|
||||
if (!isVisibile) {
|
||||
open(e, newItem);
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
function setVerticalAlign(targetBoundingRect: DOMRect) {
|
||||
if (verticalAlign === 'top') {
|
||||
return targetBoundingRect?.top ? targetBoundingRect.top - contextMenuHeight - menuMargin : 0;
|
||||
}
|
||||
|
||||
return targetBoundingRect?.top
|
||||
? targetBoundingRect.top + targetBoundingRect.height + menuMargin
|
||||
: 0;
|
||||
}
|
||||
|
||||
function setHorizontalAlign(targetBoundingRect: DOMRect) {
|
||||
if (horizontalAlign === 'left') {
|
||||
return targetBoundingRect?.left ? targetBoundingRect.left : 0;
|
||||
}
|
||||
|
||||
return targetBoundingRect?.left
|
||||
? targetBoundingRect.left + targetBoundingRect.width - contextMenuWidth
|
||||
: 0;
|
||||
}
|
||||
|
||||
function setAlignByTarget() {
|
||||
if (target) {
|
||||
const targetBoundingRect = target.getBoundingClientRect();
|
||||
menuPosition = {
|
||||
x: setHorizontalAlign(targetBoundingRect),
|
||||
y: setVerticalAlign(targetBoundingRect)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function setTransformOrigin() {
|
||||
if (!openByMouse) {
|
||||
if (verticalAlign === 'top' && horizontalAlign === 'left') {
|
||||
return 'bottom left';
|
||||
}
|
||||
if (verticalAlign === 'top' && horizontalAlign === 'right') {
|
||||
return 'bottom right';
|
||||
}
|
||||
if (verticalAlign === 'bottom' && horizontalAlign === 'left') {
|
||||
return 'top left';
|
||||
}
|
||||
if (verticalAlign === 'bottom' && horizontalAlign === 'right') {
|
||||
return 'top right';
|
||||
}
|
||||
} else {
|
||||
return 'top left';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#snippet contextMenu()}
|
||||
<div
|
||||
use:clickOutside={{
|
||||
excludeElement: target,
|
||||
handler: () => close()
|
||||
}}
|
||||
use:resizeObserver={() => {
|
||||
if (!openByMouse) setAlignByTarget();
|
||||
}}
|
||||
bind:offsetHeight={contextMenuHeight}
|
||||
bind:offsetWidth={contextMenuWidth}
|
||||
class="context-menu"
|
||||
style:top={pxToRem(menuPosition.y)}
|
||||
style:left={pxToRem(menuPosition.x)}
|
||||
style:transform-origin={setTransformOrigin()}
|
||||
style:--animation-transform-shift={verticalAlign === 'top' ? '6px' : '-6px'}
|
||||
>
|
||||
{@render children(item)}
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
{#if isVisibile}
|
||||
<div class="portal-wrap" use:portal={'body'}>
|
||||
{#if openByMouse}
|
||||
{@render contextMenu()}
|
||||
{:else}
|
||||
<div class="overlay-wrapper">
|
||||
{@render contextMenu()}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="postcss">
|
||||
.portal-wrap {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.overlay-wrapper {
|
||||
z-index: var(--z-blocker);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* background-color: rgba(0, 0, 0, 0.1); */
|
||||
}
|
||||
|
||||
.context-menu {
|
||||
z-index: var(--z-blocker);
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--clr-bg-2);
|
||||
border: 1px solid var(--clr-border-2);
|
||||
border-radius: var(--radius-m);
|
||||
box-shadow: var(--fx-shadow-s);
|
||||
|
||||
animation: fadeIn 0.1s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(var(--animation-transform-shift)) scale(0.9);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -15,10 +15,7 @@
|
||||
}
|
||||
|
||||
const { hovered, activated, label = 'Drop here', extraPaddings }: Props = $props();
|
||||
let defaultPadding = $derived.by(() => {
|
||||
if (hovered) return 2;
|
||||
return 4;
|
||||
});
|
||||
let defaultPadding = 4;
|
||||
|
||||
const extraPaddingTop = extraPaddings?.top ?? 0;
|
||||
const extraPaddingRight = extraPaddings?.right ?? 0;
|
||||
@ -94,7 +91,7 @@
|
||||
transform: scale(1.01);
|
||||
|
||||
.animated-rectangle rect {
|
||||
fill: oklch(from var(--clr-scale-pop-50) l c h / 0.14);
|
||||
fill: oklch(from var(--clr-scale-pop-50) l c h / 0.16);
|
||||
}
|
||||
|
||||
.dropzone-label {
|
||||
|
@ -5,7 +5,6 @@
|
||||
import ContextMenuSection from '$lib/components/contextmenu/ContextMenuSection.svelte';
|
||||
import Button from '$lib/shared/Button.svelte';
|
||||
import Modal from '$lib/shared/Modal.svelte';
|
||||
import PopupMenu from '$lib/shared/PopupMenu.svelte';
|
||||
import { getContext } from '$lib/utils/context';
|
||||
import { computeFileStatus } from '$lib/utils/fileStatus';
|
||||
import { editor } from '$lib/utils/systemEditor';
|
||||
@ -13,15 +12,16 @@
|
||||
import { BranchController } from '$lib/vbranches/branchController';
|
||||
import { LocalFile, type AnyFile } from '$lib/vbranches/types';
|
||||
import { join } from '@tauri-apps/api/path';
|
||||
import { open } from '@tauri-apps/api/shell';
|
||||
import { open as openFile } from '@tauri-apps/api/shell';
|
||||
|
||||
export let target: HTMLElement;
|
||||
export let isUnapplied;
|
||||
|
||||
const branchController = getContext(BranchController);
|
||||
const project = getContext(Project);
|
||||
|
||||
let confirmationModal: Modal;
|
||||
let popupMenu: PopupMenu;
|
||||
let contextMenu: ContextMenu;
|
||||
|
||||
function containsBinaryFiles(item: any) {
|
||||
return item.files.some((f: AnyFile) => f.binary);
|
||||
@ -31,17 +31,16 @@
|
||||
return item.files.some((f: AnyFile) => computeFileStatus(f) === 'D');
|
||||
}
|
||||
|
||||
export function openByMouse(e: MouseEvent, item: any) {
|
||||
popupMenu.openByMouse(e, item);
|
||||
export function open(e: MouseEvent, item: any) {
|
||||
contextMenu.open(e, item);
|
||||
}
|
||||
</script>
|
||||
|
||||
<PopupMenu bind:this={popupMenu} let:item let:dismiss>
|
||||
<ContextMenu>
|
||||
<ContextMenu bind:this={contextMenu} {target} openByMouse>
|
||||
{#snippet children(item)}
|
||||
<ContextMenuSection>
|
||||
{#if item.files && item.files.length > 0}
|
||||
{@const files = item.files}
|
||||
<!-- TODO: Refactor so we can have types -->
|
||||
{#if files[0] instanceof LocalFile && !isUnapplied}
|
||||
{#if containsBinaryFiles(item)}
|
||||
<ContextMenuItem label="Discard changes (Binary files not yet supported)" disabled />
|
||||
@ -50,7 +49,7 @@
|
||||
label="Discard changes"
|
||||
on:click={() => {
|
||||
confirmationModal.show(item);
|
||||
dismiss();
|
||||
contextMenu.close();
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
@ -63,7 +62,8 @@
|
||||
if (!project) return;
|
||||
const absPath = await join(project.path, item.files[0].path);
|
||||
navigator.clipboard.writeText(absPath);
|
||||
dismiss();
|
||||
contextMenu.close();
|
||||
// dismiss();
|
||||
} catch (err) {
|
||||
console.error('Failed to copy path', err);
|
||||
toasts.error('Failed to copy path');
|
||||
@ -76,7 +76,7 @@
|
||||
try {
|
||||
if (!project) return;
|
||||
navigator.clipboard.writeText(item.files[0].path);
|
||||
dismiss();
|
||||
contextMenu.close();
|
||||
} catch (err) {
|
||||
console.error('Failed to copy relative path', err);
|
||||
toasts.error('Failed to copy relative path');
|
||||
@ -92,9 +92,9 @@
|
||||
if (!project) return;
|
||||
for (let file of item.files) {
|
||||
const absPath = await join(project.vscodePath, file.path);
|
||||
open(`${editor.get()}://file${absPath}`);
|
||||
openFile(`${editor.get()}://file${absPath}`);
|
||||
}
|
||||
dismiss();
|
||||
contextMenu.close();
|
||||
} catch {
|
||||
console.error('Failed to open in VSCode');
|
||||
toasts.error('Failed to open in VSCode');
|
||||
@ -103,8 +103,8 @@
|
||||
/>
|
||||
{/if}
|
||||
</ContextMenuSection>
|
||||
</ContextMenu>
|
||||
</PopupMenu>
|
||||
{/snippet}
|
||||
</ContextMenu>
|
||||
|
||||
<Modal width="small" title="Discard changes" bind:this={confirmationModal}>
|
||||
{#snippet children(item)}
|
||||
|
@ -12,7 +12,6 @@
|
||||
import { FileIdSelection } from '$lib/vbranches/fileIdSelection';
|
||||
import { Ownership } from '$lib/vbranches/ownership';
|
||||
import { Branch, type AnyFile } from '$lib/vbranches/types';
|
||||
import { mount, onDestroy, unmount } from 'svelte';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
export let file: AnyFile;
|
||||
@ -29,6 +28,7 @@
|
||||
const selectedFiles = fileIdSelection.files;
|
||||
|
||||
let checked = false;
|
||||
let contextMenu: FileContextMenu;
|
||||
let draggableElt: HTMLDivElement;
|
||||
let lastCheckboxDetail = true;
|
||||
|
||||
@ -49,25 +49,11 @@
|
||||
$: if ($fileIdSelection && draggableElt && $fileIdSelection.length === 1)
|
||||
updateFocus(draggableElt, file, fileIdSelection, $commit?.id);
|
||||
|
||||
$: popupMenu = updateContextMenu();
|
||||
|
||||
function updateContextMenu() {
|
||||
if (popupMenu) unmount(popupMenu);
|
||||
return mount(FileContextMenu, {
|
||||
target: document.body,
|
||||
props: { isUnapplied }
|
||||
});
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
if (popupMenu) {
|
||||
unmount(popupMenu);
|
||||
}
|
||||
});
|
||||
|
||||
const isDraggable = !readonly && !isUnapplied;
|
||||
</script>
|
||||
|
||||
<FileContextMenu bind:this={contextMenu} target={draggableElt} {isUnapplied} />
|
||||
|
||||
<div
|
||||
bind:this={draggableElt}
|
||||
class="file-list-item"
|
||||
@ -107,15 +93,15 @@
|
||||
draggableElt.classList.remove('locked-file-animation');
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:contextmenu|preventDefault={async (e) => {
|
||||
if (fileIdSelection.has(file.id, $commit?.id)) {
|
||||
popupMenu.openByMouse(e, { files: await $selectedFiles });
|
||||
contextMenu.open(e, { files: await $selectedFiles });
|
||||
} else {
|
||||
popupMenu.openByMouse(e, { files: [file] });
|
||||
contextMenu.open(e, { files: [file] });
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
use:draggableChips={{
|
||||
label: `${file.filename}`,
|
||||
filePath: file.path,
|
||||
|
@ -6,7 +6,7 @@
|
||||
import ScrollableContainer from '../shared/ScrollableContainer.svelte';
|
||||
import emptyFolderSvg from '$lib/assets/empty-state/empty-folder.svg?raw';
|
||||
import { Project } from '$lib/backend/projects';
|
||||
import { clickOutside } from '$lib/clickOutside';
|
||||
import { clickOutside } from '$lib/clickOutsideNew';
|
||||
import FileCard from '$lib/file/FileCard.svelte';
|
||||
import SnapshotCard from '$lib/history/SnapshotCard.svelte';
|
||||
import { HistoryService, createdOnDay } from '$lib/history/history';
|
||||
|
@ -2,34 +2,38 @@
|
||||
import ContextMenu from '$lib/components/contextmenu/ContextMenu.svelte';
|
||||
import ContextMenuItem from '$lib/components/contextmenu/ContextMenuItem.svelte';
|
||||
import ContextMenuSection from '$lib/components/contextmenu/ContextMenuSection.svelte';
|
||||
import PopupMenu from '$lib/shared/PopupMenu.svelte';
|
||||
import { getContext } from '$lib/utils/context';
|
||||
import { editor } from '$lib/utils/systemEditor';
|
||||
import { BranchController } from '$lib/vbranches/branchController';
|
||||
import { open } from '@tauri-apps/api/shell';
|
||||
import { open as openFile } from '@tauri-apps/api/shell';
|
||||
|
||||
export let target: HTMLElement;
|
||||
export let filePath: string;
|
||||
export let projectPath: string | undefined;
|
||||
export let readonly: boolean;
|
||||
|
||||
const branchController = getContext(BranchController);
|
||||
|
||||
let popupMenu: PopupMenu;
|
||||
let contextMenu: ContextMenu;
|
||||
|
||||
export function openByMouse(e: MouseEvent, item: any) {
|
||||
popupMenu.openByMouse(e, item);
|
||||
export function open(e: MouseEvent, item: any) {
|
||||
contextMenu.open(e, item);
|
||||
}
|
||||
|
||||
export function close() {
|
||||
contextMenu.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<PopupMenu bind:this={popupMenu} let:item let:dismiss>
|
||||
<ContextMenu>
|
||||
<ContextMenu bind:this={contextMenu} {target} openByMouse>
|
||||
{#snippet children(item)}
|
||||
<ContextMenuSection>
|
||||
{#if item.hunk !== undefined && !readonly}
|
||||
<ContextMenuItem
|
||||
label="Discard"
|
||||
on:click={() => {
|
||||
branchController.unapplyHunk(item.hunk);
|
||||
dismiss();
|
||||
contextMenu.close();
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
@ -38,11 +42,11 @@
|
||||
label="Open in VS Code"
|
||||
on:mousedown={() => {
|
||||
projectPath &&
|
||||
open(`${editor.get()}://file${projectPath}/${filePath}:${item.lineNumber}`);
|
||||
dismiss();
|
||||
openFile(`${editor.get()}://file${projectPath}/${filePath}:${item.lineNumber}`);
|
||||
contextMenu.close();
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</ContextMenuSection>
|
||||
</ContextMenu>
|
||||
</PopupMenu>
|
||||
{/snippet}
|
||||
</ContextMenu>
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
lineContextMenu: { lineNumber: number | undefined; event: MouseEvent };
|
||||
click: void;
|
||||
selected: boolean;
|
||||
}>();
|
||||
|
||||
@ -49,8 +50,10 @@
|
||||
>
|
||||
{#each lines as line}
|
||||
<div
|
||||
tabindex="-1"
|
||||
role="none"
|
||||
class="code-line"
|
||||
role="group"
|
||||
on:click={() => dispatch('click')}
|
||||
on:contextmenu={(event) => {
|
||||
const lineNumber = line.afterLineNumber ? line.afterLineNumber : line.beforeLineNumber;
|
||||
dispatch('lineContextMenu', { event, lineNumber });
|
||||
|
@ -10,12 +10,9 @@
|
||||
import { getContext, getContextStoreBySymbol, maybeGetContextStore } from '$lib/utils/context';
|
||||
import { Ownership } from '$lib/vbranches/ownership';
|
||||
import { Branch, type Hunk } from '$lib/vbranches/types';
|
||||
import { mount, onDestroy, unmount } from 'svelte';
|
||||
import type { HunkSection } from '$lib/utils/fileSections';
|
||||
import type { Writable } from 'svelte/store';
|
||||
|
||||
export let viewport: HTMLDivElement | undefined = undefined;
|
||||
export let contents: HTMLDivElement | undefined = undefined;
|
||||
export let filePath: string;
|
||||
export let section: HunkSection;
|
||||
export let minWidth: number;
|
||||
@ -30,6 +27,11 @@
|
||||
const branch = maybeGetContextStore(Branch);
|
||||
const project = getContext(Project);
|
||||
|
||||
let viewport: HTMLDivElement;
|
||||
let contents: HTMLDivElement;
|
||||
let contextMenu: HunkContextMenu;
|
||||
let alwaysShow = false;
|
||||
|
||||
function onHunkSelected(hunk: Hunk, isSelected: boolean) {
|
||||
if (!selectedOwnership) return;
|
||||
if (isSelected) {
|
||||
@ -38,26 +40,17 @@
|
||||
selectedOwnership.update((ownership) => ownership.remove(hunk.filePath, hunk.id));
|
||||
}
|
||||
}
|
||||
function updateContextMenu(filePath: string) {
|
||||
if (popupMenu) unmount(popupMenu);
|
||||
return mount(HunkContextMenu, {
|
||||
target: document.body,
|
||||
props: { projectPath: project.vscodePath, filePath, readonly }
|
||||
});
|
||||
}
|
||||
$: popupMenu = updateContextMenu(filePath);
|
||||
|
||||
$: draggingDisabled = readonly || isUnapplied;
|
||||
|
||||
onDestroy(() => {
|
||||
if (popupMenu) {
|
||||
unmount(popupMenu);
|
||||
}
|
||||
});
|
||||
|
||||
let alwaysShow = false;
|
||||
</script>
|
||||
|
||||
<HunkContextMenu
|
||||
bind:this={contextMenu}
|
||||
target={viewport}
|
||||
projectPath={project.vscodePath}
|
||||
{filePath}
|
||||
{readonly}
|
||||
/>
|
||||
|
||||
<div class="scrollable">
|
||||
<div
|
||||
bind:this={viewport}
|
||||
@ -93,8 +86,11 @@
|
||||
selected={$selectedOwnership?.contains(hunk.filePath, hunk.id)}
|
||||
on:selected={(e) => onHunkSelected(hunk, e.detail)}
|
||||
sectionType={subsection.sectionType}
|
||||
on:click={() => {
|
||||
contextMenu.close();
|
||||
}}
|
||||
on:lineContextMenu={(e) => {
|
||||
popupMenu.openByMouse(e.detail.event, {
|
||||
contextMenu.open(e.detail.event, {
|
||||
hunk,
|
||||
section: subsection,
|
||||
lineNumber: e.detail.lineNumber
|
||||
|
@ -128,5 +128,6 @@
|
||||
"item-dashed": "M14 5.75H12.5V5C12.5 4.79385 12.4592 4.601 12.3868 4.42622L13.7724 3.85164C13.919 4.20536 14 4.59323 14 5V5.75Z M5.75 2V3.5H5C4.79385 3.5 4.601 3.54075 4.42622 3.61323L3.85164 2.22764C4.20536 2.08096 4.59323 2 5 2H5.75Z M2.22764 3.85164C2.08096 4.20536 2 4.59323 2 5V5.75H3.5V5C3.5 4.79385 3.54075 4.601 3.61323 4.42622L2.22764 3.85164Z M2 10.25H3.5V11C3.5 11.2062 3.54075 11.399 3.61323 11.5738L2.22764 12.1484C2.08096 11.7946 2 11.4068 2 11V10.25Z M10.25 14V12.5H11C11.2062 12.5 11.399 12.4592 11.5738 12.3868L12.1484 13.7724C11.7946 13.919 11.4068 14 11 14H10.25Z M14 7.25H12.5V8.75H14V7.25Z M14 10.25H12.5V11C12.5 11.2062 12.4592 11.399 12.3868 11.5738L13.7724 12.1484C13.919 11.7946 14 11.4068 14 11V10.25Z M8.75 14V12.5H7.25V14H8.75Z M5.75 14V12.5H5C4.79385 12.5 4.601 12.4592 4.42622 12.3868L3.85164 13.7724C4.20536 13.919 4.59323 14 5 14H5.75Z M2 8.75H3.5V7.25H2V8.75Z M7.25 2V3.5H8.75V2H7.25Z M10.25 2V3.5H11C11.2062 3.5 11.399 3.54075 11.5738 3.61323L12.1484 2.22764C11.7946 2.08096 11.4068 2 11 2H10.25Z",
|
||||
"item-move": "M11 12.5H5C4.17157 12.5 3.5 11.8284 3.5 11V8.75H8.11127L6.30367 10.4543L7.33269 11.5457L10.5145 8.54569L11.0933 8L10.5145 7.45431L7.33269 4.45431L6.30367 5.54569L8.11127 7.25H3.5V5C3.5 4.17157 4.17157 3.5 5 3.5H11C11.8284 3.5 12.5 4.17157 12.5 5V11C12.5 11.8284 11.8284 12.5 11 12.5ZM2 5C2 3.34315 3.34315 2 5 2H11C12.6569 2 14 3.34315 14 5V11C14 12.6569 12.6569 14 11 14H5C3.34315 14 2 12.6569 2 11V5Z",
|
||||
"robot": "M12.3829 3.33776C11.9899 3.1336 11.5223 3.05284 10.8366 3.0209C10.4307 1.8448 9.31402 1 8 1C6.68598 1 5.56928 1.8448 5.1634 3.0209C4.47768 3.05284 4.01011 3.1336 3.61708 3.33776C3.06915 3.62239 2.62239 4.06915 2.33776 4.61708C2.1456 4.987 2.06277 5.42295 2.02706 6.04467C0.872483 6.26574 0 7.28098 0 8.5C0 9.71002 0.859646 10.7193 2.00153 10.9503C2.00923 12.1554 2.05584 12.8402 2.33776 13.3829C2.62239 13.9309 3.06915 14.3776 3.61708 14.6622C4.26729 15 5.12153 15 6.83 15H9.17C10.8785 15 11.7327 15 12.3829 14.6622C12.9309 14.3776 13.3776 13.9309 13.6622 13.3829C13.9442 12.8402 13.9908 12.1554 13.9985 10.9503C15.1404 10.7193 16 9.71002 16 8.5C16 7.28098 15.1275 6.26574 13.9729 6.04467C13.9372 5.42295 13.8544 4.987 13.6622 4.61708C13.3776 4.06915 12.9309 3.62239 12.3829 3.33776ZM6.83 4.5C5.95059 4.5 5.38275 4.50121 4.95083 4.53707C4.53687 4.57145 4.38385 4.62976 4.30854 4.66888C4.03457 4.81119 3.81119 5.03457 3.66888 5.30854C3.62976 5.38385 3.57145 5.53687 3.53707 5.95083C3.50121 6.38275 3.5 6.95059 3.5 7.83V10.17C3.5 11.0494 3.50121 11.6173 3.53707 12.0492C3.57145 12.4631 3.62976 12.6161 3.66888 12.6915C3.81119 12.9654 4.03457 13.1888 4.30854 13.3311C4.38385 13.3702 4.53687 13.4285 4.95083 13.4629C5.38275 13.4988 5.95059 13.5 6.83 13.5H9.17C10.0494 13.5 10.6173 13.4988 11.0492 13.4629C11.4631 13.4285 11.6161 13.3702 11.6915 13.3311C11.9654 13.1888 12.1888 12.9654 12.3311 12.6915C12.3702 12.6161 12.4285 12.4631 12.4629 12.0492C12.4988 11.6173 12.5 11.0494 12.5 10.17V7.83C12.5 6.95059 12.4988 6.38275 12.4629 5.95083C12.4285 5.53687 12.3702 5.38385 12.3311 5.30854C12.1888 5.03457 11.9654 4.81119 11.6915 4.66888C11.6161 4.62976 11.4631 4.57145 11.0492 4.53707C10.6173 4.50121 10.0494 4.5 9.17 4.5H6.83ZM5.75 10C5.33579 10 5 10.3358 5 10.75C5 11.1642 5.33579 11.5 5.75 11.5H10.25C10.6642 11.5 11 11.1642 11 10.75C11 10.3358 10.6642 10 10.25 10H5.75ZM6 9C6.55228 9 7 8.55228 7 8C7 7.44772 6.55228 7 6 7C5.44772 7 5 7.44772 5 8C5 8.55228 5.44772 9 6 9ZM11 8C11 8.55228 10.5523 9 10 9C9.44771 9 9 8.55228 9 8C9 7.44772 9.44771 7 10 7C10.5523 7 11 7.44772 11 8Z",
|
||||
"doc": "M12 6.75H4V5.25H12V6.75Z M4 9.75H8V8.25H4V9.75Z M4 1C2.34315 1 1 2.34315 1 4V12C1 13.6569 2.34315 15 4 15H12C13.6569 15 15 13.6569 15 12V4C15 2.34315 13.6569 1 12 1H4ZM2.5 12C2.5 12.8284 3.17157 13.5 4 13.5H12C12.8284 13.5 13.5 12.8284 13.5 12V4C13.5 3.17157 12.8284 2.5 12 2.5H4C3.17157 2.5 2.5 3.17157 2.5 4V12Z"
|
||||
"doc": "M12 6.75H4V5.25H12V6.75Z M4 9.75H8V8.25H4V9.75Z M4 1C2.34315 1 1 2.34315 1 4V12C1 13.6569 2.34315 15 4 15H12C13.6569 15 15 13.6569 15 12V4C15 2.34315 13.6569 1 12 1H4ZM2.5 12C2.5 12.8284 3.17157 13.5 4 13.5H12C12.8284 13.5 13.5 12.8284 13.5 12V4C13.5 3.17157 12.8284 2.5 12 2.5H4C3.17157 2.5 2.5 3.17157 2.5 4V12Z",
|
||||
"clear-input": "M1 8C1 4.13401 4.13401 1 8 1C11.866 1 15 4.13401 15 8C15 11.866 11.866 15 8 15C4.13401 15 1 11.866 1 8ZM8 6.93934L10.4697 4.46967L11.5303 5.53033L9.06066 8L11.5303 10.4697L10.4697 11.5303L8 9.06066L5.53033 11.5303L4.46967 10.4697L6.93934 8L4.46967 5.53033L5.53033 4.46967L8 6.93934Z"
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
import BranchesHeader from './BranchesHeader.svelte';
|
||||
import noBranchesSvg from '$lib/assets/empty-state/no-branches.svg?raw';
|
||||
import { BranchService } from '$lib/branches/service';
|
||||
import FilterPopupMenu from '$lib/components/FilterPopupMenu.svelte';
|
||||
import FilterButton from '$lib/components/FilterBranchesButton.svelte';
|
||||
import { GitHubService } from '$lib/github/service';
|
||||
import { persisted } from '$lib/persisted/persisted';
|
||||
import { storeToObservable } from '$lib/rxjs/store';
|
||||
@ -18,12 +18,12 @@
|
||||
const dispatch = createEventDispatcher<{ scrollbarDragging: boolean }>();
|
||||
|
||||
export let projectId: string;
|
||||
|
||||
export const textFilter$ = new BehaviorSubject<string | undefined>(undefined);
|
||||
|
||||
const branchService = getContext(BranchService);
|
||||
const githubService = getContext(GitHubService);
|
||||
|
||||
// let contextMenu: ContextMenuActions;
|
||||
let includePrs = persisted(true, 'includePrs_' + projectId);
|
||||
let includeRemote = persisted(true, 'includeRemote_' + projectId);
|
||||
let includeStashed = persisted(true, 'includeStashed_' + projectId);
|
||||
@ -114,9 +114,9 @@
|
||||
filteredBranchCount={$filteredBranches$?.length}
|
||||
filtersActive={$filtersActive}
|
||||
>
|
||||
{#snippet contextMenu({ visible })}
|
||||
<FilterPopupMenu
|
||||
{visible}
|
||||
{#snippet filterButton()}
|
||||
<FilterButton
|
||||
{filtersActive}
|
||||
{includePrs}
|
||||
{includeRemote}
|
||||
{includeStashed}
|
||||
|
@ -1,26 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { clickOutside } from '$lib/clickOutside';
|
||||
import Badge from '$lib/shared/Badge.svelte';
|
||||
import Button from '$lib/shared/Button.svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
filteredBranchCount?: number;
|
||||
totalBranchCount: number;
|
||||
filtersActive: boolean;
|
||||
contextMenu: Snippet<[{ visible: boolean }]>;
|
||||
filterButton: Snippet<[filtersActive: boolean]>;
|
||||
}
|
||||
|
||||
const { filteredBranchCount, totalBranchCount, filtersActive, contextMenu }: Props = $props();
|
||||
|
||||
let visible = $state(false);
|
||||
let filterButton = $state<HTMLDivElement>();
|
||||
|
||||
function onFilterClick(e: Event) {
|
||||
visible = !visible;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
const { filteredBranchCount, totalBranchCount, filtersActive, filterButton }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="header">
|
||||
@ -32,22 +21,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
{#if totalBranchCount > 0}
|
||||
<div class="header__filter-btn" bind:this={filterButton}>
|
||||
<Button
|
||||
style="ghost"
|
||||
outline
|
||||
icon={filtersActive ? 'filter-applied-small' : 'filter-small'}
|
||||
on:mousedown={onFilterClick}
|
||||
>
|
||||
Filter
|
||||
</Button>
|
||||
<div
|
||||
class="filter-popup-menu"
|
||||
use:clickOutside={{ trigger: filterButton, handler: () => (visible = false) }}
|
||||
>
|
||||
{@render contextMenu({ visible })}
|
||||
</div>
|
||||
</div>
|
||||
{@render filterButton(filtersActive)}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@ -64,16 +38,6 @@
|
||||
transition: border-bottom var(--transition-fast);
|
||||
position: relative;
|
||||
}
|
||||
.header__filter-btn {
|
||||
position: relative;
|
||||
}
|
||||
.filter-popup-menu {
|
||||
position: absolute;
|
||||
top: calc(var(--size-button) + 4px);
|
||||
right: 0;
|
||||
z-index: var(--z-floating);
|
||||
min-width: 160px;
|
||||
}
|
||||
.branches-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -1,11 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { ProjectService } from '$lib/backend/projects';
|
||||
import ListItem from '$lib/shared/ListItem.svelte';
|
||||
import Icon from '$lib/shared/Icon.svelte';
|
||||
import ScrollableContainer from '$lib/shared/ScrollableContainer.svelte';
|
||||
import { getContext } from '$lib/utils/context';
|
||||
import type iconsJson from '$lib/icons/icons.json';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
interface ItemSnippetProps {
|
||||
label: string;
|
||||
selected?: boolean;
|
||||
icon?: string;
|
||||
onclick: () => void;
|
||||
}
|
||||
|
||||
export let isNavCollapsed: boolean;
|
||||
|
||||
const projectService = getContext(ProjectService);
|
||||
@ -24,42 +32,61 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#snippet itemSnippet(props: ItemSnippetProps)}
|
||||
<button
|
||||
disabled={props.selected}
|
||||
class="list-item"
|
||||
class:selected={props.selected}
|
||||
on:click={props.onclick}
|
||||
>
|
||||
<div class="label text-base-14 text-bold">
|
||||
{props.label}
|
||||
</div>
|
||||
{#if props.icon || props.selected}
|
||||
<div class="icon">
|
||||
{#if props.icon}
|
||||
<Icon name={loading ? 'spinner' : props.icon as keyof typeof iconsJson} />
|
||||
{:else}
|
||||
<Icon name="tick" />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
{/snippet}
|
||||
|
||||
{#if !hidden}
|
||||
<div class="popup" class:collapsed={isNavCollapsed}>
|
||||
{#if $projects.length > 0}
|
||||
<ScrollableContainer maxHeight="20rem">
|
||||
<div class="popup__projects">
|
||||
{#each $projects as project}
|
||||
<!-- eslint-disable-next-line svelte/valid-compile -->
|
||||
{@const selected = project.id === $page.params.projectId}
|
||||
<ListItem
|
||||
{selected}
|
||||
icon={selected ? 'tick' : undefined}
|
||||
on:click={() => {
|
||||
{@render itemSnippet({
|
||||
label: project.title,
|
||||
selected,
|
||||
icon: selected ? 'tick' : undefined,
|
||||
onclick: () => {
|
||||
goto(`/${project.id}/`);
|
||||
hide();
|
||||
projectService.setLastOpenedProject(project.id);
|
||||
goto(`/${project.id}/board`);
|
||||
}}
|
||||
>
|
||||
{project.title}
|
||||
</ListItem>
|
||||
}
|
||||
})}
|
||||
{/each}
|
||||
</div>
|
||||
</ScrollableContainer>
|
||||
{/if}
|
||||
<div class="popup__actions">
|
||||
<ListItem
|
||||
icon="plus"
|
||||
{loading}
|
||||
on:click={async () => {
|
||||
{@render itemSnippet({
|
||||
label: 'Add new project',
|
||||
icon: 'plus',
|
||||
onclick: async () => {
|
||||
loading = true;
|
||||
try {
|
||||
await projectService.addProject();
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}}>Add new project</ListItem
|
||||
>
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@ -76,6 +103,21 @@
|
||||
background: var(--clr-bg-1);
|
||||
/* shadow/s */
|
||||
box-shadow: 0px 7px 14px 0px rgba(0, 0, 0, 0.1);
|
||||
animation: fadeIn 0.2s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
.popup__actions {
|
||||
padding: 8px;
|
||||
@ -88,6 +130,40 @@
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* LIST ITEM */
|
||||
.list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--clr-scale-ntrl-10);
|
||||
font-weight: 700;
|
||||
padding: 10px 10px;
|
||||
justify-content: space-between;
|
||||
border-radius: var(--radius-m);
|
||||
width: 100%;
|
||||
transition: background-color var(--transition-fast);
|
||||
|
||||
&:hover:enabled,
|
||||
&:focus:enabled {
|
||||
background-color: var(--clr-bg-1-muted);
|
||||
& .icon {
|
||||
color: var(--clr-scale-ntrl-40);
|
||||
}
|
||||
}
|
||||
&:disabled {
|
||||
background-color: var(--clr-bg-2);
|
||||
color: var(--clr-text-2);
|
||||
}
|
||||
& .icon {
|
||||
display: flex;
|
||||
color: var(--clr-scale-ntrl-50);
|
||||
}
|
||||
& .label {
|
||||
height: 16px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/* MODIFIERS */
|
||||
.popup.collapsed {
|
||||
width: 240px;
|
||||
|
@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import ContextMenu from '$lib/components/contextmenu/ContextMenu.svelte';
|
||||
import ContextMenuItem from '$lib/components/contextmenu/ContextMenuItem.svelte';
|
||||
import ContextMenuSection from '$lib/components/contextmenu/ContextMenuSection.svelte';
|
||||
import { MergeMethod } from '$lib/github/types';
|
||||
@ -43,17 +42,15 @@
|
||||
}}
|
||||
>
|
||||
{labels[$action]}
|
||||
<ContextMenu slot="context-menu">
|
||||
<ContextMenuSection>
|
||||
{#each Object.values(MergeMethod) as method}
|
||||
<ContextMenuItem
|
||||
label={labels[method]}
|
||||
on:click={() => {
|
||||
$action = method;
|
||||
dropDown.close();
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</ContextMenuSection>
|
||||
</ContextMenu>
|
||||
<ContextMenuSection slot="context-menu">
|
||||
{#each Object.values(MergeMethod) as method}
|
||||
<ContextMenuItem
|
||||
label={labels[method]}
|
||||
on:click={() => {
|
||||
$action = method;
|
||||
dropDown.close();
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</ContextMenuSection>
|
||||
</DropDownButton>
|
||||
|
@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import ContextMenu from '$lib/components/contextmenu/ContextMenu.svelte';
|
||||
import ContextMenuItem from '$lib/components/contextmenu/ContextMenuItem.svelte';
|
||||
import ContextMenuSection from '$lib/components/contextmenu/ContextMenuSection.svelte';
|
||||
import { persisted, type Persisted } from '$lib/persisted/persisted';
|
||||
@ -40,17 +39,15 @@
|
||||
}}
|
||||
>
|
||||
{labels[$action]}
|
||||
<ContextMenu slot="context-menu">
|
||||
<ContextMenuSection>
|
||||
{#each Object.values(Action) as method}
|
||||
<ContextMenuItem
|
||||
label={labels[method]}
|
||||
on:click={() => {
|
||||
$action = method;
|
||||
dropDown.close();
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</ContextMenuSection>
|
||||
</ContextMenu>
|
||||
<ContextMenuSection slot="context-menu">
|
||||
{#each Object.values(Action) as method}
|
||||
<ContextMenuItem
|
||||
label={labels[method]}
|
||||
on:click={() => {
|
||||
$action = method;
|
||||
dropDown.close();
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</ContextMenuSection>
|
||||
</DropDownButton>
|
||||
|
@ -3,7 +3,6 @@
|
||||
import InfoMessage from '../shared/InfoMessage.svelte';
|
||||
import { Project } from '$lib/backend/projects';
|
||||
import { BranchService } from '$lib/branches/service';
|
||||
import ViewPrContextMenu from '$lib/components/ViewPrContextMenu.svelte';
|
||||
import { GitHubService } from '$lib/github/service';
|
||||
import Button from '$lib/shared/Button.svelte';
|
||||
import { getContext, getContextStore } from '$lib/utils/context';
|
||||
@ -13,7 +12,6 @@
|
||||
import { BaseBranchService } from '$lib/vbranches/baseBranch';
|
||||
import { Branch } from '$lib/vbranches/types';
|
||||
import { distinctUntilChanged } from 'rxjs';
|
||||
import { mount, onDestroy, unmount } from 'svelte';
|
||||
import { derived, type Readable } from 'svelte/store';
|
||||
import type { ChecksStatus, DetailedPullRequest } from '$lib/github/types';
|
||||
import type iconsJson from '$lib/icons/icons.json';
|
||||
@ -236,22 +234,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateContextMenu(copyablePrUrl: string) {
|
||||
if (popupMenu) unmount(popupMenu);
|
||||
return mount(ViewPrContextMenu, {
|
||||
target: document.body,
|
||||
props: { prUrl: copyablePrUrl }
|
||||
});
|
||||
}
|
||||
|
||||
$: popupMenu = updateContextMenu($pr$?.htmlUrl || '');
|
||||
|
||||
onDestroy(() => {
|
||||
if (popupMenu) {
|
||||
unmount(popupMenu);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if $pr$}
|
||||
@ -307,10 +289,6 @@
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
on:contextmenu={(e) => {
|
||||
e.preventDefault();
|
||||
popupMenu.openByMouse(e, undefined);
|
||||
}}
|
||||
>
|
||||
Open in browser
|
||||
</Button>
|
||||
|
26
app/src/lib/select/OptionsGroup.svelte
Normal file
26
app/src/lib/select/OptionsGroup.svelte
Normal file
@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { type Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
const { children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="options__group">
|
||||
{@render children()}
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.options__group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 6px;
|
||||
gap: 2px;
|
||||
|
||||
&:not(&:last-child) {
|
||||
border-bottom: 1px solid var(--clr-border-2);
|
||||
}
|
||||
}
|
||||
</style>
|
103
app/src/lib/select/SearchItem.svelte
Normal file
103
app/src/lib/select/SearchItem.svelte
Normal file
@ -0,0 +1,103 @@
|
||||
<script lang="ts">
|
||||
import { type SelectItem } from './Select.svelte';
|
||||
import Icon from '$lib/shared/Icon.svelte';
|
||||
|
||||
interface Props {
|
||||
placeholder?: string;
|
||||
items: SelectItem[];
|
||||
onSort?: (items: SelectItem[]) => void;
|
||||
}
|
||||
|
||||
const { placeholder = 'Search…', items, onSort }: Props = $props();
|
||||
|
||||
let value = $state('');
|
||||
let filteredItems = $state(items);
|
||||
|
||||
let inputEl: HTMLInputElement;
|
||||
|
||||
function handleFilter() {
|
||||
filteredItems = items.filter((item) => item.label.toLowerCase().includes(value.toLowerCase()));
|
||||
}
|
||||
|
||||
function resetFilter() {
|
||||
value = '';
|
||||
handleFilter();
|
||||
inputEl.focus();
|
||||
}
|
||||
|
||||
function handleInput(event: Event) {
|
||||
value = (event.target as HTMLInputElement).value;
|
||||
handleFilter();
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
onSort?.(filteredItems);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
{#if !value}
|
||||
<i class="icon search-icon">
|
||||
<Icon name="search" />
|
||||
</i>
|
||||
{:else}
|
||||
<button class="icon" onclick={resetFilter}>
|
||||
<Icon name="clear-input" />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<input
|
||||
bind:this={inputEl}
|
||||
class="text-base-13 search-input"
|
||||
type="text"
|
||||
{placeholder}
|
||||
bind:value
|
||||
oninput={handleInput}
|
||||
autocorrect="off"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.container {
|
||||
position: relative;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
padding: 12px 34px 12px 12px;
|
||||
width: 100%;
|
||||
background-color: var(--clr-bg-1);
|
||||
color: var(--clr-text-1);
|
||||
|
||||
border-bottom: 1px solid var(--clr-border-2);
|
||||
transition: border-color var(--transition-fast);
|
||||
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
background-color: var(--clr-bg-1-muted);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--clr-text-3);
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 12px;
|
||||
transform: translateY(-50%);
|
||||
color: var(--clr-scale-ntrl-50);
|
||||
transition: color var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
color: var(--clr-scale-ntrl-40);
|
||||
}
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
281
app/src/lib/select/Select.svelte
Normal file
281
app/src/lib/select/Select.svelte
Normal file
@ -0,0 +1,281 @@
|
||||
<script lang="ts" context="module">
|
||||
export type SelectItem = {
|
||||
label: string;
|
||||
value: string;
|
||||
selectable?: boolean;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import OptionsGroup from './OptionsGroup.svelte';
|
||||
import SearchItem from './SearchItem.svelte';
|
||||
import ScrollableContainer from '../shared/ScrollableContainer.svelte';
|
||||
import TextBox from '../shared/TextBox.svelte';
|
||||
import { KeyName } from '$lib/utils/hotkeys';
|
||||
import { portal } from '$lib/utils/portal';
|
||||
import { pxToRem } from '$lib/utils/pxToRem';
|
||||
import { resizeObserver } from '$lib/utils/resizeObserver';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface SelectProps {
|
||||
id?: string;
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
wide?: boolean;
|
||||
options: SelectItem[];
|
||||
value?: any;
|
||||
placeholder?: string;
|
||||
maxHeight?: number;
|
||||
searchable?: boolean;
|
||||
itemSnippet: Snippet<[{ item: any; highlighted: boolean }]>;
|
||||
children?: Snippet;
|
||||
onselect?: (value: string) => void;
|
||||
}
|
||||
|
||||
const {
|
||||
id,
|
||||
label,
|
||||
disabled,
|
||||
loading,
|
||||
wide,
|
||||
options = [],
|
||||
value,
|
||||
placeholder,
|
||||
maxHeight,
|
||||
searchable,
|
||||
itemSnippet,
|
||||
children,
|
||||
onselect
|
||||
}: SelectProps = $props();
|
||||
|
||||
let selectWrapperEl: HTMLElement;
|
||||
|
||||
// let hightlighted = $state(false);
|
||||
let highlightedIndex: number | undefined = $state(undefined);
|
||||
let filteredOptions = $state(options);
|
||||
let maxHeightState = $state(maxHeight);
|
||||
let listOpen = $state(false);
|
||||
let inputBoundingRect = $state<DOMRect>();
|
||||
let optionsEl = $state<HTMLDivElement>();
|
||||
|
||||
const maxBottomPadding = 20;
|
||||
|
||||
function setMaxHeight() {
|
||||
if (maxHeight) return;
|
||||
maxHeightState =
|
||||
window.innerHeight - selectWrapperEl.getBoundingClientRect().bottom - maxBottomPadding;
|
||||
}
|
||||
|
||||
function openList() {
|
||||
setMaxHeight();
|
||||
listOpen = true;
|
||||
}
|
||||
|
||||
function closeList() {
|
||||
listOpen = false;
|
||||
}
|
||||
|
||||
function clickOutside(e: MouseEvent) {
|
||||
if (e.target === e.currentTarget) closeList();
|
||||
}
|
||||
|
||||
function getInputBoundingRect() {
|
||||
if (selectWrapperEl) {
|
||||
inputBoundingRect = selectWrapperEl.getBoundingClientRect();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleList() {
|
||||
getInputBoundingRect();
|
||||
|
||||
if (listOpen) closeList();
|
||||
else openList();
|
||||
}
|
||||
|
||||
function handleSelect(item: { label: string; value: string }) {
|
||||
const value = item.value;
|
||||
onselect?.(value);
|
||||
closeList();
|
||||
}
|
||||
|
||||
function handleEnter() {
|
||||
if (highlightedIndex !== undefined) {
|
||||
handleSelect(filteredOptions[highlightedIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
function handleArrowUp() {
|
||||
if (filteredOptions.length === 0) return;
|
||||
if (highlightedIndex === undefined) {
|
||||
highlightedIndex = filteredOptions.length - 1;
|
||||
} else {
|
||||
highlightedIndex = highlightedIndex === 0 ? filteredOptions.length - 1 : highlightedIndex - 1;
|
||||
}
|
||||
}
|
||||
|
||||
function handleArrowDown() {
|
||||
if (filteredOptions.length === 0) return;
|
||||
if (highlightedIndex === undefined) {
|
||||
highlightedIndex = 0;
|
||||
} else {
|
||||
highlightedIndex = highlightedIndex === filteredOptions.length - 1 ? 0 : highlightedIndex + 1;
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDown(e: CustomEvent<KeyboardEvent>) {
|
||||
if (!listOpen) {
|
||||
return;
|
||||
}
|
||||
e.detail.stopPropagation();
|
||||
e.detail.preventDefault();
|
||||
|
||||
const { key } = e.detail;
|
||||
|
||||
switch (key) {
|
||||
case KeyName.Escape:
|
||||
closeList();
|
||||
break;
|
||||
case KeyName.Up:
|
||||
handleArrowUp();
|
||||
break;
|
||||
case KeyName.Down:
|
||||
handleArrowDown();
|
||||
break;
|
||||
case KeyName.Enter:
|
||||
handleEnter();
|
||||
break;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="select-wrapper" class:wide bind:this={selectWrapperEl}>
|
||||
{#if label}
|
||||
<label for={id} class="select__label text-base-body-13 text-semibold">{label}</label>
|
||||
{/if}
|
||||
<TextBox
|
||||
{id}
|
||||
{placeholder}
|
||||
noselect
|
||||
readonly
|
||||
type="select"
|
||||
reversedDirection
|
||||
icon="select-chevron"
|
||||
value={options.find((item) => item.value === value)?.label || 'Select an option...'}
|
||||
disabled={disabled || loading}
|
||||
on:mousedown={toggleList}
|
||||
on:keydown={(ev) => handleKeyDown(ev)}
|
||||
/>
|
||||
{#if listOpen}
|
||||
<div
|
||||
role="presentation"
|
||||
class="overlay-wrapper"
|
||||
onclick={clickOutside}
|
||||
use:portal={'body'}
|
||||
use:resizeObserver={() => {
|
||||
getInputBoundingRect();
|
||||
setMaxHeight();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
bind:this={optionsEl}
|
||||
class="options card"
|
||||
style:width="{inputBoundingRect?.width}px"
|
||||
style:top={inputBoundingRect?.top
|
||||
? pxToRem(inputBoundingRect.top + inputBoundingRect.height)
|
||||
: undefined}
|
||||
style:left={inputBoundingRect?.left ? pxToRem(inputBoundingRect.left) : undefined}
|
||||
style:max-height={maxHeightState && pxToRem(maxHeightState)}
|
||||
>
|
||||
<ScrollableContainer initiallyVisible>
|
||||
{#if searchable && options.length > 5}
|
||||
<SearchItem
|
||||
items={options}
|
||||
onSort={(filtered) => {
|
||||
filteredOptions = filtered;
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<OptionsGroup>
|
||||
{#if filteredOptions.length === 0}
|
||||
<div class="text-base-13 text-semibold option nothing-found">
|
||||
<span class=""> Nothing found ¯\_(ツ)_/¯ </span>
|
||||
</div>
|
||||
{/if}
|
||||
{#each filteredOptions as item, idx}
|
||||
<div class="option" tabindex="-1" role="none" onmousedown={() => handleSelect(item)}>
|
||||
{@render itemSnippet({ item, highlighted: idx === highlightedIndex })}
|
||||
</div>
|
||||
{/each}
|
||||
</OptionsGroup>
|
||||
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{/if}
|
||||
</ScrollableContainer>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.select-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.select__label {
|
||||
text-align: left;
|
||||
color: var(--clr-scale-ntrl-50);
|
||||
}
|
||||
|
||||
.overlay-wrapper {
|
||||
z-index: var(--z-blocker);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* background-color: rgba(0, 0, 0, 0.1); */
|
||||
}
|
||||
|
||||
.options {
|
||||
user-select: none;
|
||||
position: absolute;
|
||||
z-index: var(--z-floating);
|
||||
margin-top: 4px;
|
||||
border-radius: var(--radius-m);
|
||||
border: 1px solid var(--clr-border-2);
|
||||
background: var(--clr-bg-1);
|
||||
box-shadow: var(--fx-shadow-s);
|
||||
overflow: hidden;
|
||||
transform-origin: top;
|
||||
|
||||
animation: fadeIn 0.16s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.wide {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nothing-found {
|
||||
padding: 8px 8px;
|
||||
color: var(--clr-text-3);
|
||||
}
|
||||
</style>
|
@ -36,6 +36,7 @@
|
||||
|
||||
<style lang="postcss">
|
||||
.button {
|
||||
user-select: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--clr-scale-ntrl-10);
|
||||
@ -77,6 +78,6 @@
|
||||
}
|
||||
|
||||
.highlighted {
|
||||
background-color: var(--clr-bg-3);
|
||||
background-color: var(--clr-bg-1-muted);
|
||||
}
|
||||
</style>
|
@ -4,12 +4,12 @@
|
||||
import SectionCard from '$lib/components/SectionCard.svelte';
|
||||
import SectionCardDisclaimer from '$lib/components/SectionCardDisclaimer.svelte';
|
||||
import { projectRunCommitHooks } from '$lib/config/config';
|
||||
import Select from '$lib/select/Select.svelte';
|
||||
import SelectItem from '$lib/select/SelectItem.svelte';
|
||||
import Section from '$lib/settings/Section.svelte';
|
||||
import Button from '$lib/shared/Button.svelte';
|
||||
import InfoMessage from '$lib/shared/InfoMessage.svelte';
|
||||
import Link from '$lib/shared/Link.svelte';
|
||||
import Select from '$lib/shared/Select.svelte';
|
||||
import SelectItem from '$lib/shared/SelectItem.svelte';
|
||||
import TextBox from '$lib/shared/TextBox.svelte';
|
||||
import Toggle from '$lib/shared/Toggle.svelte';
|
||||
import { getContext } from '$lib/utils/context';
|
||||
@ -58,11 +58,11 @@
|
||||
|
||||
const signingFormatOptions = [
|
||||
{
|
||||
name: 'GPG',
|
||||
label: 'GPG',
|
||||
value: 'openpgp'
|
||||
},
|
||||
{
|
||||
name: 'SSH',
|
||||
label: 'SSH',
|
||||
value: 'ssh'
|
||||
}
|
||||
];
|
||||
@ -152,16 +152,19 @@
|
||||
{#if signCommits}
|
||||
<SectionCard orientation="column">
|
||||
<Select
|
||||
items={signingFormatOptions}
|
||||
bind:selectedItemId={signingFormat}
|
||||
itemId="value"
|
||||
labelId="name"
|
||||
on:select={updateSigningInfo}
|
||||
value={signingFormat}
|
||||
options={signingFormatOptions}
|
||||
label="Signing format"
|
||||
onselect={(value) => {
|
||||
signingFormat = value;
|
||||
updateSigningInfo();
|
||||
}}
|
||||
>
|
||||
<SelectItem slot="template" let:item let:selected {selected} let:highlighted {highlighted}>
|
||||
{item.name}
|
||||
</SelectItem>
|
||||
{#snippet itemSnippet({ item, highlighted })}
|
||||
<SelectItem selected={item.value === signingFormat} {highlighted}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
{/snippet}
|
||||
</Select>
|
||||
|
||||
<TextBox
|
||||
|
@ -6,7 +6,7 @@
|
||||
import type { ComponentColor, ComponentStyleKind } from '$lib/vbranches/types';
|
||||
|
||||
// Interaction props
|
||||
export let element: HTMLAnchorElement | HTMLButtonElement | HTMLElement | null = null;
|
||||
export let el: HTMLAnchorElement | HTMLButtonElement | HTMLElement | null = null;
|
||||
export let disabled = false;
|
||||
export let clickable = true;
|
||||
export let id: string | undefined = undefined;
|
||||
@ -52,7 +52,7 @@
|
||||
text: help,
|
||||
delay: helpShowDelay
|
||||
}}
|
||||
bind:this={element}
|
||||
bind:this={el}
|
||||
disabled={disabled || loading}
|
||||
on:click
|
||||
on:mousedown
|
||||
@ -84,6 +84,7 @@
|
||||
|
||||
<style lang="postcss">
|
||||
.btn {
|
||||
user-select: none;
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { clickOutside } from '$lib/clickOutside';
|
||||
import ContextMenu from '$lib/components/contextmenu/ContextMenu.svelte';
|
||||
import Button from '$lib/shared/Button.svelte';
|
||||
import type iconsJson from '$lib/icons/icons.json';
|
||||
import type { ComponentColor, ComponentStyleKind } from '$lib/vbranches/types';
|
||||
@ -13,23 +13,25 @@
|
||||
export let wide = false;
|
||||
export let help = '';
|
||||
export let menuPosition: 'top' | 'bottom' = 'bottom';
|
||||
|
||||
let contextMenu: ContextMenu;
|
||||
let iconEl: HTMLElement;
|
||||
|
||||
let visible = false;
|
||||
|
||||
export function show() {
|
||||
visible = true;
|
||||
contextMenu.open();
|
||||
}
|
||||
|
||||
export function close() {
|
||||
visible = false;
|
||||
contextMenu.close();
|
||||
}
|
||||
|
||||
let container: HTMLDivElement;
|
||||
let contextMenuContainer: HTMLDivElement;
|
||||
let iconEl: HTMLElement;
|
||||
</script>
|
||||
|
||||
<div class="dropdown-wrapper" class:wide>
|
||||
<div class="dropdown" bind:this={container}>
|
||||
<div class="dropdown">
|
||||
<Button
|
||||
{style}
|
||||
{icon}
|
||||
@ -44,7 +46,7 @@
|
||||
<slot />
|
||||
</Button>
|
||||
<Button
|
||||
bind:element={iconEl}
|
||||
bind:el={iconEl}
|
||||
{style}
|
||||
{kind}
|
||||
{help}
|
||||
@ -55,23 +57,20 @@
|
||||
isDropdownChild
|
||||
on:click={() => {
|
||||
visible = !visible;
|
||||
contextMenu.toggle();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="context-menu-container"
|
||||
use:clickOutside={{
|
||||
trigger: iconEl,
|
||||
handler: () => (visible = false),
|
||||
enabled: visible
|
||||
<ContextMenu
|
||||
bind:this={contextMenu}
|
||||
target={iconEl}
|
||||
verticalAlign={menuPosition}
|
||||
onclose={() => {
|
||||
visible = false;
|
||||
}}
|
||||
bind:this={contextMenuContainer}
|
||||
style:display={visible ? 'block' : 'none'}
|
||||
class:dropdown-top={menuPosition === 'top'}
|
||||
class:dropdown-bottom={menuPosition === 'bottom'}
|
||||
>
|
||||
<slot name="context-menu" />
|
||||
</div>
|
||||
</ContextMenu>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
@ -85,23 +84,6 @@
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.context-menu-container {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
z-index: var(--z-floating);
|
||||
}
|
||||
|
||||
.dropdown-top {
|
||||
bottom: 100%;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.dropdown-bottom {
|
||||
top: 100%;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.wide {
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { clickOutside } from '$lib/clickOutside';
|
||||
import Icon from '$lib/shared/Icon.svelte';
|
||||
import { portal } from '$lib/utils/portal';
|
||||
import type iconsJson from '$lib/icons/icons.json';
|
||||
@ -33,16 +32,23 @@
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<div class="modal-container" class:open bind:this={dialog} onclose={close} use:portal={'body'}>
|
||||
<div
|
||||
bind:this={dialog}
|
||||
use:portal={'body'}
|
||||
role="presentation"
|
||||
class="modal-container"
|
||||
class:open
|
||||
onclick={(e) => {
|
||||
// Close the modal if the user clicks outside of it
|
||||
e.target === e.currentTarget && close();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="modal-content"
|
||||
class:s-default={width === 'default'}
|
||||
class:s-small={width === 'small'}
|
||||
class:s-large={width === 'large'}
|
||||
use:clickOutside={{
|
||||
trigger: dialog,
|
||||
handler: () => close()
|
||||
}}
|
||||
class:round-top-corners={!title}
|
||||
>
|
||||
{#if title}
|
||||
<div class="modal__header">
|
||||
|
@ -1,59 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { clickOutside } from '$lib/clickOutside';
|
||||
|
||||
let pos = { x: 0, y: 0 };
|
||||
let menu = { h: 0, w: 0 };
|
||||
let browser = { h: 0, w: 0 };
|
||||
let showMenu = false;
|
||||
let item: any;
|
||||
|
||||
function onDismiss() {
|
||||
showMenu = false;
|
||||
}
|
||||
|
||||
export function openByMouse(e: MouseEvent, item: any) {
|
||||
show(e.clientX, e.clientY, item);
|
||||
}
|
||||
|
||||
export function openByElement(elt: HTMLElement, item: any) {
|
||||
const rect = elt.getBoundingClientRect();
|
||||
show(rect.left, rect.top + rect.height, item);
|
||||
}
|
||||
|
||||
function show(x: number, y: number, newItem: any) {
|
||||
item = newItem;
|
||||
showMenu = true;
|
||||
browser = {
|
||||
w: window.innerWidth,
|
||||
h: window.innerHeight
|
||||
};
|
||||
pos = {
|
||||
x: x,
|
||||
y: y
|
||||
};
|
||||
|
||||
if (browser.h - pos.y < menu.h) pos.y = pos.y - menu.h;
|
||||
if (browser.w - pos.x < menu.w) pos.x = pos.x - menu.w;
|
||||
}
|
||||
|
||||
export function recordDimensions(node: HTMLDivElement) {
|
||||
let height = node.offsetHeight;
|
||||
let width = node.offsetWidth;
|
||||
menu = {
|
||||
h: height,
|
||||
w: width
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if showMenu}
|
||||
<div
|
||||
role="menu"
|
||||
tabindex="0"
|
||||
use:recordDimensions
|
||||
use:clickOutside={{ handler: () => onDismiss() }}
|
||||
style="z-index: var(--z-floating); position: absolute; top:{pos.y}px; left:{pos.x}px"
|
||||
>
|
||||
<slot {item} dismiss={onDismiss} />
|
||||
</div>
|
||||
{/if}
|
@ -1,13 +0,0 @@
|
||||
<script lang="ts">
|
||||
export let disabled = false;
|
||||
export let title = '';
|
||||
</script>
|
||||
|
||||
<button
|
||||
on:click|preventDefault
|
||||
{disabled}
|
||||
{title}
|
||||
class="w-full px-3 py-0.5 text-left no-underline enabled:hover:bg-light-100 disabled:text-light-600 enabled:dark:hover:bg-dark-800"
|
||||
>
|
||||
<slot />
|
||||
</button>
|
@ -1,263 +0,0 @@
|
||||
<script lang="ts" context="module">
|
||||
export type Selectable<S extends string> = Record<S, unknown>;
|
||||
</script>
|
||||
|
||||
<script lang="ts" generics="Selectable extends Record<string, unknown>">
|
||||
import ScrollableContainer from './ScrollableContainer.svelte';
|
||||
import TextBox from './TextBox.svelte';
|
||||
import { clickOutside } from '$lib/clickOutside';
|
||||
import { KeyName } from '$lib/utils/hotkeys';
|
||||
import { throttle } from '$lib/utils/misc';
|
||||
import { pxToRem } from '$lib/utils/pxToRem';
|
||||
import { isChar, isStr } from '$lib/utils/string';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const INPUT_THROTTLE_TIME = 100;
|
||||
|
||||
type SelectableKey = keyof Selectable;
|
||||
|
||||
export let id: undefined | string = undefined;
|
||||
export let label = '';
|
||||
export let disabled = false;
|
||||
export let loading = false;
|
||||
export let wide = false;
|
||||
export let items: Selectable[];
|
||||
export let labelId: SelectableKey = 'label';
|
||||
export let itemId: SelectableKey = 'value';
|
||||
export let value: any = undefined;
|
||||
export let selectedItemId: any = undefined;
|
||||
export let placeholder = '';
|
||||
export let maxHeight: number | undefined = 260;
|
||||
|
||||
$: if (selectedItemId) value = items.find((item) => item[itemId] === selectedItemId);
|
||||
|
||||
const dispatch = createEventDispatcher<{ select: { value: any } }>();
|
||||
const maxPadding = 10;
|
||||
|
||||
let listOpen = false;
|
||||
let element: HTMLElement;
|
||||
let options: HTMLDivElement;
|
||||
let highlightIndex: number | undefined = undefined;
|
||||
let highlightedItem: Selectable | undefined = undefined;
|
||||
let filterText: string | undefined = undefined;
|
||||
let filteredItems: Selectable[] = items;
|
||||
|
||||
const filterItems = throttle((items: Selectable[], filterText: string | undefined) => {
|
||||
if (!filterText) {
|
||||
return items;
|
||||
}
|
||||
|
||||
return items.filter((it) => {
|
||||
const property = it[labelId];
|
||||
if (!isStr(property)) return false;
|
||||
return property.includes(filterText);
|
||||
});
|
||||
}, INPUT_THROTTLE_TIME);
|
||||
|
||||
$: filteredItems = filterItems(items, filterText);
|
||||
|
||||
$: highlightedItem = highlightIndex !== undefined ? filteredItems[highlightIndex] : undefined;
|
||||
|
||||
function handleItemClick(item: Selectable) {
|
||||
if (item?.selectable === false) return;
|
||||
if (value && value[itemId] === item[itemId]) return closeList();
|
||||
selectedItemId = item[itemId];
|
||||
dispatch('select', { value });
|
||||
closeList();
|
||||
}
|
||||
function setMaxHeight() {
|
||||
if (maxHeight) return;
|
||||
maxHeight = window.innerHeight - element.getBoundingClientRect().bottom - maxPadding;
|
||||
}
|
||||
|
||||
function toggleList() {
|
||||
if (listOpen) closeList();
|
||||
else openList();
|
||||
}
|
||||
|
||||
function openList() {
|
||||
setMaxHeight();
|
||||
listOpen = true;
|
||||
}
|
||||
|
||||
function closeList() {
|
||||
listOpen = false;
|
||||
highlightIndex = undefined;
|
||||
filterText = undefined;
|
||||
}
|
||||
|
||||
function handleEnter() {
|
||||
if (highlightIndex !== undefined) {
|
||||
handleItemClick(filteredItems[highlightIndex]);
|
||||
}
|
||||
closeList();
|
||||
}
|
||||
|
||||
function handleArrowUp() {
|
||||
if (filteredItems.length === 0) return;
|
||||
if (highlightIndex === undefined) {
|
||||
highlightIndex = filteredItems.length - 1;
|
||||
} else {
|
||||
highlightIndex = highlightIndex === 0 ? filteredItems.length - 1 : highlightIndex - 1;
|
||||
}
|
||||
}
|
||||
|
||||
function handleArrowDown() {
|
||||
if (filteredItems.length === 0) return;
|
||||
if (highlightIndex === undefined) {
|
||||
highlightIndex = 0;
|
||||
} else {
|
||||
highlightIndex = highlightIndex === filteredItems.length - 1 ? 0 : highlightIndex + 1;
|
||||
}
|
||||
}
|
||||
|
||||
function handleChar(char: string) {
|
||||
highlightIndex = undefined;
|
||||
filterText ??= '';
|
||||
filterText += char;
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
if (filterText === undefined) return;
|
||||
|
||||
if (filterText.length === 1) {
|
||||
filterText = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
filterText = filterText.slice(0, -1);
|
||||
}
|
||||
|
||||
function handleKeyDown(e: CustomEvent<KeyboardEvent>) {
|
||||
if (!listOpen) {
|
||||
return;
|
||||
}
|
||||
e.detail.stopPropagation();
|
||||
e.detail.preventDefault();
|
||||
|
||||
const { key } = e.detail;
|
||||
switch (key) {
|
||||
case KeyName.Escape:
|
||||
closeList();
|
||||
break;
|
||||
case KeyName.Up:
|
||||
handleArrowUp();
|
||||
break;
|
||||
case KeyName.Down:
|
||||
handleArrowDown();
|
||||
break;
|
||||
case KeyName.Enter:
|
||||
handleEnter();
|
||||
break;
|
||||
case KeyName.Delete:
|
||||
handleDelete();
|
||||
break;
|
||||
default:
|
||||
if (isChar(key)) handleChar(key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="select-wrapper" class:wide bind:this={element}>
|
||||
{#if label}
|
||||
<label for={id} class="select__label text-base-body-13 text-semibold">{label}</label>
|
||||
{/if}
|
||||
<TextBox
|
||||
{id}
|
||||
{placeholder}
|
||||
noselect
|
||||
readonly
|
||||
type="select"
|
||||
reversedDirection
|
||||
icon="select-chevron"
|
||||
value={filterText ?? value?.[labelId]}
|
||||
disabled={disabled || loading}
|
||||
on:mousedown={() => toggleList()}
|
||||
on:keydown={(ev) => handleKeyDown(ev)}
|
||||
/>
|
||||
<div
|
||||
class="options card"
|
||||
style:display={listOpen ? undefined : 'none'}
|
||||
bind:this={options}
|
||||
style:max-height={maxHeight && pxToRem(maxHeight)}
|
||||
use:clickOutside={{
|
||||
trigger: element,
|
||||
handler: closeList,
|
||||
enabled: listOpen
|
||||
}}
|
||||
>
|
||||
<ScrollableContainer initiallyVisible>
|
||||
{#if filteredItems}
|
||||
<div class="options__group">
|
||||
{#each filteredItems as item}
|
||||
<div
|
||||
class="option"
|
||||
class:selected={item === value}
|
||||
tabindex="-1"
|
||||
role="none"
|
||||
on:mousedown={() => handleItemClick(item)}
|
||||
on:keydown|preventDefault|stopPropagation
|
||||
>
|
||||
<slot
|
||||
name="template"
|
||||
{item}
|
||||
selected={item === value}
|
||||
highlighted={item === highlightedItem}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if $$slots?.append}
|
||||
<div class="options__group">
|
||||
<slot name="append" />
|
||||
</div>
|
||||
{/if}
|
||||
</ScrollableContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.select-wrapper {
|
||||
/* display set directly on element */
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.select__label {
|
||||
text-align: left;
|
||||
color: var(--clr-scale-ntrl-50);
|
||||
}
|
||||
|
||||
.options {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 100%;
|
||||
width: 100%;
|
||||
z-index: var(--z-floating);
|
||||
margin-top: 4px;
|
||||
border-radius: var(--radius-m);
|
||||
border: 1px solid var(--clr-border-2);
|
||||
background: var(--clr-bg-1);
|
||||
box-shadow: var(--fx-shadow-s);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.options__group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 6px;
|
||||
gap: 2px;
|
||||
|
||||
&:not(&:first-child):last-child {
|
||||
border-top: 1px solid var(--clr-border-2);
|
||||
}
|
||||
}
|
||||
|
||||
.wide {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { autoHeight } from '$lib/utils/autoHeight';
|
||||
import { pxToRem } from '$lib/utils/pxToRem';
|
||||
import { useAutoHeight } from '$lib/utils/useAutoHeight';
|
||||
import { useResize } from '$lib/utils/useResize';
|
||||
import { resizeObserver } from '$lib/utils/resizeObserver';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let value: string | undefined;
|
||||
@ -42,16 +42,16 @@
|
||||
{spellcheck}
|
||||
on:input={(e) => {
|
||||
dispatch('input', e.currentTarget.value);
|
||||
useAutoHeight(e.currentTarget);
|
||||
autoHeight(e.currentTarget);
|
||||
}}
|
||||
on:change={(e) => {
|
||||
dispatch('change', e.currentTarget.value);
|
||||
useAutoHeight(e.currentTarget);
|
||||
autoHeight(e.currentTarget);
|
||||
}}
|
||||
use:useResize={(e) => {
|
||||
useAutoHeight(e.currentTarget as HTMLTextAreaElement);
|
||||
use:resizeObserver={(e) => {
|
||||
autoHeight(e.currentTarget as HTMLTextAreaElement);
|
||||
}}
|
||||
on:focus={(e) => useAutoHeight(e.currentTarget)}
|
||||
on:focus={(e) => autoHeight(e.currentTarget)}
|
||||
style:max-height={maxHeight ? pxToRem(maxHeight) : undefined}
|
||||
></textarea>
|
||||
</div>
|
||||
|
@ -210,6 +210,7 @@
|
||||
/* select */
|
||||
.textbox__input[type='select']:not([disabled]),
|
||||
.textbox__input[type='select']:not([readonly]) {
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
export function useAutoHeight(element: HTMLTextAreaElement) {
|
||||
export function autoHeight(element: HTMLTextAreaElement) {
|
||||
if (!element) return;
|
||||
|
||||
const elementBorder =
|
@ -1,4 +1,4 @@
|
||||
export function useResize(
|
||||
export function resizeObserver(
|
||||
element: HTMLElement,
|
||||
callback: (data: { currentTarget: HTMLElement; frame: { width: number; height: number } }) => void
|
||||
) {
|
@ -6,12 +6,12 @@
|
||||
import SectionCard from '$lib/components/SectionCard.svelte';
|
||||
import WelcomeSigninAction from '$lib/components/WelcomeSigninAction.svelte';
|
||||
import { getSecretsService } from '$lib/secrets/secretsService';
|
||||
import Select from '$lib/select/Select.svelte';
|
||||
import SelectItem from '$lib/select/SelectItem.svelte';
|
||||
import ContentWrapper from '$lib/settings/ContentWrapper.svelte';
|
||||
import Section from '$lib/settings/Section.svelte';
|
||||
import InfoMessage from '$lib/shared/InfoMessage.svelte';
|
||||
import RadioButton from '$lib/shared/RadioButton.svelte';
|
||||
import Select from '$lib/shared/Select.svelte';
|
||||
import SelectItem from '$lib/shared/SelectItem.svelte';
|
||||
import Spacer from '$lib/shared/Spacer.svelte';
|
||||
import TextBox from '$lib/shared/TextBox.svelte';
|
||||
import { UserService } from '$lib/stores/user';
|
||||
@ -86,45 +86,45 @@
|
||||
|
||||
const keyOptions = [
|
||||
{
|
||||
name: 'Use GitButler API',
|
||||
label: 'Use GitButler API',
|
||||
value: KeyOption.ButlerAPI
|
||||
},
|
||||
{
|
||||
name: 'Your own key',
|
||||
label: 'Your own key',
|
||||
value: KeyOption.BringYourOwn
|
||||
}
|
||||
];
|
||||
|
||||
const openAIModelOptions = [
|
||||
{
|
||||
name: 'GPT 3.5 Turbo',
|
||||
label: 'GPT 3.5 Turbo',
|
||||
value: OpenAIModelName.GPT35Turbo
|
||||
},
|
||||
{
|
||||
name: 'GPT 4',
|
||||
label: 'GPT 4',
|
||||
value: OpenAIModelName.GPT4
|
||||
},
|
||||
{
|
||||
name: 'GPT 4 Turbo',
|
||||
label: 'GPT 4 Turbo',
|
||||
value: OpenAIModelName.GPT4Turbo
|
||||
},
|
||||
{
|
||||
name: 'GPT 4 Omni',
|
||||
label: 'GPT 4 Omni',
|
||||
value: OpenAIModelName.GPT4o
|
||||
}
|
||||
];
|
||||
|
||||
const anthropicModelOptions = [
|
||||
{
|
||||
name: 'Sonnet',
|
||||
label: 'Sonnet',
|
||||
value: AnthropicModelName.Sonnet
|
||||
},
|
||||
{
|
||||
name: 'Opus',
|
||||
label: 'Opus',
|
||||
value: AnthropicModelName.Opus
|
||||
},
|
||||
{
|
||||
name: 'Haiku',
|
||||
label: 'Haiku',
|
||||
value: AnthropicModelName.Haiku
|
||||
}
|
||||
];
|
||||
@ -167,22 +167,18 @@
|
||||
<SectionCard roundedTop={false} roundedBottom={false} orientation="row" topDivider>
|
||||
<div class="inputs-group">
|
||||
<Select
|
||||
items={keyOptions}
|
||||
bind:selectedItemId={openAIKeyOption}
|
||||
itemId="value"
|
||||
labelId="name"
|
||||
value={openAIKeyOption}
|
||||
options={keyOptions}
|
||||
label="Do you want to provide your own key?"
|
||||
onselect={(value) => {
|
||||
openAIKeyOption = value as KeyOption;
|
||||
}}
|
||||
>
|
||||
<SelectItem
|
||||
slot="template"
|
||||
let:item
|
||||
let:selected
|
||||
{selected}
|
||||
let:highlighted
|
||||
{highlighted}
|
||||
>
|
||||
{item.name}
|
||||
</SelectItem>
|
||||
{#snippet itemSnippet({ item, highlighted })}
|
||||
<SelectItem selected={item.value === openAIKeyOption} {highlighted}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
{/snippet}
|
||||
</Select>
|
||||
|
||||
{#if openAIKeyOption === KeyOption.ButlerAPI}
|
||||
@ -197,22 +193,18 @@
|
||||
<TextBox label="API key" bind:value={openAIKey} required placeholder="sk-..." />
|
||||
|
||||
<Select
|
||||
items={openAIModelOptions}
|
||||
bind:selectedItemId={openAIModelName}
|
||||
itemId="value"
|
||||
labelId="name"
|
||||
value={openAIModelName}
|
||||
options={openAIModelOptions}
|
||||
label="Model version"
|
||||
onselect={(value) => {
|
||||
openAIModelName = value as OpenAIModelName;
|
||||
}}
|
||||
>
|
||||
<SelectItem
|
||||
slot="template"
|
||||
let:item
|
||||
let:selected
|
||||
{selected}
|
||||
let:highlighted
|
||||
{highlighted}
|
||||
>
|
||||
{item.name}
|
||||
</SelectItem>
|
||||
{#snippet itemSnippet({ item, highlighted })}
|
||||
<SelectItem selected={item.value === openAIModelName} {highlighted}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
{/snippet}
|
||||
</Select>
|
||||
{:else if !$user}
|
||||
<WelcomeSigninAction prompt="A user is required to make use of the GitButler API" />
|
||||
@ -237,22 +229,18 @@
|
||||
<SectionCard roundedTop={false} roundedBottom={false} orientation="row" topDivider>
|
||||
<div class="inputs-group">
|
||||
<Select
|
||||
items={keyOptions}
|
||||
bind:selectedItemId={anthropicKeyOption}
|
||||
itemId="value"
|
||||
labelId="name"
|
||||
value={anthropicKeyOption}
|
||||
options={keyOptions}
|
||||
label="Do you want to provide your own key?"
|
||||
onselect={(value) => {
|
||||
anthropicKeyOption = value as KeyOption;
|
||||
}}
|
||||
>
|
||||
<SelectItem
|
||||
slot="template"
|
||||
let:item
|
||||
let:selected
|
||||
{selected}
|
||||
let:highlighted
|
||||
{highlighted}
|
||||
>
|
||||
{item.name}
|
||||
</SelectItem>
|
||||
{#snippet itemSnippet({ item, highlighted })}
|
||||
<SelectItem selected={item.value === anthropicKeyOption} {highlighted}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
{/snippet}
|
||||
</Select>
|
||||
|
||||
{#if anthropicKeyOption === KeyOption.ButlerAPI}
|
||||
@ -272,22 +260,18 @@
|
||||
/>
|
||||
|
||||
<Select
|
||||
items={anthropicModelOptions}
|
||||
bind:selectedItemId={anthropicModelName}
|
||||
itemId="value"
|
||||
labelId="name"
|
||||
value={anthropicModelName}
|
||||
options={anthropicModelOptions}
|
||||
label="Model version"
|
||||
onselect={(value) => {
|
||||
anthropicModelName = value as AnthropicModelName;
|
||||
}}
|
||||
>
|
||||
<SelectItem
|
||||
slot="template"
|
||||
let:item
|
||||
let:selected
|
||||
{selected}
|
||||
let:highlighted
|
||||
{highlighted}
|
||||
>
|
||||
{item.name}
|
||||
</SelectItem>
|
||||
{#snippet itemSnippet({ item, highlighted })}
|
||||
<SelectItem selected={item.value === anthropicModelName} {highlighted}>
|
||||
{item.label}
|
||||
</SelectItem>
|
||||
{/snippet}
|
||||
</Select>
|
||||
{:else if !$user}
|
||||
<WelcomeSigninAction prompt="A user is required to make use of the GitButler API" />
|
||||
|
@ -29,7 +29,8 @@ export default tsEslint.config(
|
||||
$props: 'readonly',
|
||||
$bindable: 'readonly',
|
||||
$inspect: 'readonly',
|
||||
$host: 'readonly'
|
||||
$host: 'readonly',
|
||||
$effect: 'readonly'
|
||||
},
|
||||
parser: svelteParser,
|
||||
parserOptions: {
|
||||
|
Loading…
Reference in New Issue
Block a user