Merge pull request #5037 from gitbutlerapp/pr-details

PR details modal
This commit is contained in:
Esteban Vega 2024-10-07 14:37:20 +02:00 committed by GitHub
commit cb90ca5e7c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 906 additions and 414 deletions

View File

@ -0,0 +1,12 @@
<svg width="120" height="100" viewBox="0 0 120 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.51" d="M82.704 16.6898C83.1784 18.3664 82.6488 20.3379 81.079 22.5277C79.5136 24.7113 76.9709 27.0258 73.6152 29.3264C66.909 33.9242 57.0708 38.389 45.6906 41.6093C34.3105 44.8295 23.5914 46.1817 15.4705 45.7796C11.4068 45.5784 8.02838 44.9395 5.55096 43.8999C3.06645 42.8573 1.58231 41.4556 1.10789 39.779C0.633476 38.1025 1.16307 36.1309 2.73293 33.9411C4.2983 31.7575 6.841 29.4431 10.1967 27.1424C16.9029 22.5447 26.7411 18.0798 38.1213 14.8596C49.5014 11.6393 60.2205 10.2871 68.3414 10.6892C72.4051 10.8904 75.7835 11.5294 78.261 12.569C80.7455 13.6115 82.2296 15.0132 82.704 16.6898Z" stroke="var(--clr-core-ntrl-50)" stroke-width="1.2"/>
<path opacity="0.4" d="M23.7969 19.3298C21.5413 11.3585 26.1748 3.06793 34.146 0.812305C42.1173 -1.44333 50.4079 3.19012 52.6635 11.1614L54.5695 17.8969C54.5695 17.8969 53.0442 23.5249 41.4976 26.7922C29.9509 30.0595 25.7029 26.0653 25.7029 26.0653L23.7969 19.3298Z" fill="var(--clr-core-ntrl-50)"/>
<path opacity="0.7" d="M30.6594 21.544L29.5331 17.5639C28.2003 12.8536 30.997 7.93803 35.7797 6.58465" stroke="white" stroke-width="2" stroke-linecap="round"/>
<path opacity="0.15" d="M31 99L38 43.5L54.5 39L121 99H31Z" fill="var(--clr-core-pop-50)"/>
<path opacity="0.35" d="M102.567 82.3696C102.954 81.5209 103.273 80.353 103.773 78.5241C104.249 76.7826 104.487 75.9118 104.484 75.0794C104.481 73.7696 104.057 72.4955 103.277 71.4437C102.781 70.7753 102.058 70.2121 100.614 69.0856C99.5084 68.2229 98.9552 67.7914 98.5327 67.283C97.9115 66.5359 97.4768 65.6518 97.2645 64.7036C97.12 64.0585 97.1161 63.3667 97.1084 61.9831L97.0649 54.1634C97.0596 53.2118 97.0569 52.7359 96.9951 52.3123C96.6873 50.2045 95.3076 48.4074 93.3508 47.5656C92.9575 47.3964 92.4985 47.2709 91.5806 47.02L75.5818 42.6467C71.7657 41.6036 69.8576 41.082 68.2183 41.5447C67.2557 41.8164 66.3742 42.3194 65.6506 43.01C64.7504 43.8691 64.2295 45.1189 63.6074 47.2179L102.567 82.3696Z" fill="var(--clr-core-ntrl-60)"/>
<path opacity="0.4" d="M63.6075 47.2168L102.567 82.3685C102.419 82.6938 102.261 82.9722 102.079 83.223C101.044 84.6497 99.4454 85.5621 97.6906 85.7274C96.5775 85.8322 95.3125 85.4864 92.7825 84.7948L66.0572 77.4894C62.2411 76.4463 60.333 75.9247 59.1571 74.6924C58.4665 73.9689 57.9635 73.0873 57.6918 72.1247C57.2291 70.4855 57.7507 68.5774 58.7938 64.7613L62.8537 49.909C63.1348 48.8808 63.378 47.991 63.6075 47.2168Z" fill="var(--clr-core-pop-50)"/>
<rect opacity="0.4" x="44.8945" y="82.9434" width="11" height="10" rx="5" transform="rotate(-28.8031 44.8945 82.9434)" fill="var(--clr-core-pop-50)"/>
<rect opacity="0.35" x="116.229" y="54.1934" width="5.24741" height="10.6081" rx="2.62371" transform="rotate(77.3362 116.229 54.1934)" fill="var(--clr-core-ntrl-60)"/>
<path opacity="0.4" fill-rule="evenodd" clip-rule="evenodd" d="M34.2371 73.3345L36.5758 54.7919L38.5771 53.4266C40.8583 51.8705 43.9691 52.4583 45.5253 54.7395L47.7794 58.0439C49.3355 60.3251 48.7478 63.4358 46.4666 64.992L34.2371 73.3345Z" fill="var(--clr-core-pop-50)"/>
<path opacity="0.35" d="M36.5757 54.793L24.5335 63.0077C22.2523 64.5638 21.6645 67.6746 23.2207 69.9558L25.4748 73.2602C27.0309 75.5414 30.1417 76.1292 32.4229 74.573L34.237 73.3355L36.5757 54.793Z" fill="var(--clr-core-ntrl-60)"/>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import BranchHeader from './BranchHeader.svelte'; import BranchHeader from './BranchHeader.svelte';
import EmptyStatePlaceholder from '../components/EmptyStatePlaceholder.svelte';
import PullRequestCard from '../pr/PullRequestCard.svelte'; import PullRequestCard from '../pr/PullRequestCard.svelte';
import InfoMessage from '../shared/InfoMessage.svelte'; import InfoMessage from '../shared/InfoMessage.svelte';
import { PromptService } from '$lib/ai/promptService'; import { PromptService } from '$lib/ai/promptService';
@ -34,6 +33,7 @@
import { FileIdSelection } from '$lib/vbranches/fileIdSelection'; import { FileIdSelection } from '$lib/vbranches/fileIdSelection';
import { VirtualBranch } from '$lib/vbranches/types'; import { VirtualBranch } from '$lib/vbranches/types';
import Button from '@gitbutler/ui/Button.svelte'; import Button from '@gitbutler/ui/Button.svelte';
import EmptyStatePlaceholder from '@gitbutler/ui/EmptyStatePlaceholder.svelte';
import lscache from 'lscache'; import lscache from 'lscache';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import type { Writable } from 'svelte/store'; import type { Writable } from 'svelte/store';
@ -199,7 +199,7 @@
{:else if branch.commits.length === 0} {:else if branch.commits.length === 0}
<Dropzones> <Dropzones>
<div class="new-branch"> <div class="new-branch">
<EmptyStatePlaceholder image={laneNewSvg} width="11rem"> <EmptyStatePlaceholder image={laneNewSvg} width={180} bottomMargin={48}>
<svelte:fragment slot="title">This is a new branch</svelte:fragment> <svelte:fragment slot="title">This is a new branch</svelte:fragment>
<svelte:fragment slot="caption"> <svelte:fragment slot="caption">
You can drag and drop files or parts of files here. You can drag and drop files or parts of files here.
@ -210,7 +210,7 @@
{:else} {:else}
<Dropzones> <Dropzones>
<div class="no-changes"> <div class="no-changes">
<EmptyStatePlaceholder image={noChangesSvg} width="11rem" hasBottomMargin={false}> <EmptyStatePlaceholder image={noChangesSvg} width={180}>
<svelte:fragment slot="caption" <svelte:fragment slot="caption"
>No uncommitted changes on this branch</svelte:fragment >No uncommitted changes on this branch</svelte:fragment
> >

View File

@ -3,26 +3,16 @@
import BranchLabel from './BranchLabel.svelte'; import BranchLabel from './BranchLabel.svelte';
import BranchLaneContextMenu from './BranchLaneContextMenu.svelte'; import BranchLaneContextMenu from './BranchLaneContextMenu.svelte';
import DefaultTargetButton from './DefaultTargetButton.svelte'; import DefaultTargetButton from './DefaultTargetButton.svelte';
import PullRequestButton from '../pr/PullRequestButton.svelte';
import { Project } from '$lib/backend/projects';
import { BaseBranch } from '$lib/baseBranch/baseBranch';
import { BaseBranchService } from '$lib/baseBranch/baseBranchService';
import ContextMenu from '$lib/components/contextmenu/ContextMenu.svelte'; import ContextMenu from '$lib/components/contextmenu/ContextMenu.svelte';
import { mapErrorToToast } from '$lib/gitHost/github/errorMap';
import { getGitHost } from '$lib/gitHost/interface/gitHost'; import { getGitHost } from '$lib/gitHost/interface/gitHost';
import { getGitHostListingService } from '$lib/gitHost/interface/gitHostListingService';
import { getGitHostPrMonitor } from '$lib/gitHost/interface/gitHostPrMonitor'; import { getGitHostPrMonitor } from '$lib/gitHost/interface/gitHostPrMonitor';
import { getGitHostPrService } from '$lib/gitHost/interface/gitHostPrService'; import { getGitHostPrService } from '$lib/gitHost/interface/gitHostPrService';
import { showError, showToast } from '$lib/notifications/toasts'; import PrDetailsModal from '$lib/pr/PrDetailsModal.svelte';
import { getBranchNameFromRef } from '$lib/utils/branch';
import { getContext, getContextStore } from '$lib/utils/context'; import { getContext, getContextStore } from '$lib/utils/context';
import { sleep } from '$lib/utils/sleep';
import { error } from '$lib/utils/toasts';
import { BranchController } from '$lib/vbranches/branchController'; import { BranchController } from '$lib/vbranches/branchController';
import { VirtualBranch } from '$lib/vbranches/types'; import { VirtualBranch } from '$lib/vbranches/types';
import Button from '@gitbutler/ui/Button.svelte'; import Button from '@gitbutler/ui/Button.svelte';
import Icon from '@gitbutler/ui/Icon.svelte'; import Icon from '@gitbutler/ui/Icon.svelte';
import type { PullRequest } from '$lib/gitHost/interface/types';
import type { Persisted } from '$lib/persisted/persisted'; import type { Persisted } from '$lib/persisted/persisted';
interface Props { interface Props {
@ -34,22 +24,17 @@
const { uncommittedChanges = 0, isLaneCollapsed, onGenerateBranchName }: Props = $props(); const { uncommittedChanges = 0, isLaneCollapsed, onGenerateBranchName }: Props = $props();
const branchController = getContext(BranchController); const branchController = getContext(BranchController);
const baseBranchService = getContext(BaseBranchService);
const baseBranch = getContextStore(BaseBranch);
const prService = getGitHostPrService(); const prService = getGitHostPrService();
const gitListService = getGitHostListingService();
const branchStore = getContextStore(VirtualBranch); const branchStore = getContextStore(VirtualBranch);
const prMonitor = getGitHostPrMonitor(); const prMonitor = getGitHostPrMonitor();
const gitHost = getGitHost(); const gitHost = getGitHost();
const project = getContext(Project);
const baseBranchName = $derived($baseBranch.shortName);
const branch = $derived($branchStore); const branch = $derived($branchStore);
const pr = $derived($prMonitor?.pr); const pr = $derived($prMonitor?.pr);
let contextMenu = $state<ReturnType<typeof ContextMenu>>(); let contextMenu = $state<ReturnType<typeof ContextMenu>>();
let prDetailsModal = $state<ReturnType<typeof PrDetailsModal>>();
let meatballButtonEl = $state<HTMLDivElement>(); let meatballButtonEl = $state<HTMLDivElement>();
let isLoading = $state(false);
let isTargetBranchAnimated = $state(false); let isTargetBranchAnimated = $state(false);
function handleBranchNameChange(title: string) { function handleBranchNameChange(title: string) {
@ -70,100 +55,8 @@
let headerInfoHeight = $state(0); let headerInfoHeight = $state(0);
interface CreatePrOpts { function handleOpenPR() {
draft: boolean; prDetailsModal?.show();
}
const defaultPrOpts: CreatePrOpts = {
draft: true
};
async function createPr(createPrOpts: CreatePrOpts): Promise<PullRequest | undefined> {
const opts = { ...defaultPrOpts, ...createPrOpts };
if (!$gitHost) {
error('Pull request service not available');
return;
}
let title: string;
let body: string;
let pullRequestTemplateBody: string | undefined;
const prTemplatePath = project.git_host.pullRequestTemplatePath;
if (prTemplatePath) {
pullRequestTemplateBody = await $prService?.pullRequestTemplateContent(
prTemplatePath,
project.id
);
}
if (pullRequestTemplateBody) {
title = branch.name;
body = pullRequestTemplateBody;
} else {
// In case of a single commit, use the commit summary and description for the title and
// description of the PR.
if (branch.commits.length === 1) {
const commit = branch.commits[0];
title = commit?.descriptionTitle ?? '';
body = commit?.descriptionBody ?? '';
} else {
title = branch.name;
body = '';
}
}
isLoading = true;
try {
let upstreamBranchName = branch.upstreamName;
if (branch.commits.some((c) => !c.isRemote)) {
const firstPush = !branch.upstream;
const { refname, remote } = await branchController.pushBranch(
branch.id,
branch.requiresForce
);
upstreamBranchName = getBranchNameFromRef(refname, remote);
if (firstPush) {
// TODO: fix this hack for reactively available prService.
await sleep(500);
}
}
if (!baseBranchName) {
error('No base branch name determined');
return;
}
if (!upstreamBranchName) {
error('No upstream branch name determined');
return;
}
if (!$prService) {
error('Pull request service not available');
return;
}
await $prService.createPr({
title,
body,
draft: opts.draft,
baseBranchName,
upstreamName: upstreamBranchName
});
} catch (err: any) {
console.error(err);
const toast = mapErrorToToast(err);
if (toast) showToast(toast);
else showError('Error while creating pull request', err);
} finally {
isLoading = false;
}
await $gitListService?.refresh();
baseBranchService.fetchFromRemotes();
} }
</script> </script>
@ -258,6 +151,7 @@
<div class="header__actions"> <div class="header__actions">
<div class="header__buttons"> <div class="header__buttons">
<DefaultTargetButton <DefaultTargetButton
size="button"
selectedForChanges={branch.selectedForChanges} selectedForChanges={branch.selectedForChanges}
onclick={async () => { onclick={async () => {
isTargetBranchAnimated = true; isTargetBranchAnimated = true;
@ -269,14 +163,12 @@
<div class="relative"> <div class="relative">
<div class="header__buttons"> <div class="header__buttons">
{#if !$pr} {#if !$pr}
<PullRequestButton <Button
click={async ({ draft }) => await createPr({ draft })} style="ghost"
outline
disabled={branch.commits.length === 0 || !$gitHost || !$prService} disabled={branch.commits.length === 0 || !$gitHost || !$prService}
tooltip={!$gitHost || !$prService onclick={handleOpenPR}>Create PR</Button
? 'You can enable git host integration in the settings' >
: ''}
loading={isLoading}
/>
{/if} {/if}
<Button <Button
bind:el={meatballButtonEl} bind:el={meatballButtonEl}
@ -301,6 +193,8 @@
</div> </div>
{/if} {/if}
<PrDetailsModal bind:this={prDetailsModal} type="preview" />
<style> <style>
.header__wrapper { .header__wrapper {
z-index: var(--z-lifted); z-index: var(--z-lifted);

View File

@ -4,9 +4,10 @@
interface Props { interface Props {
selectedForChanges: boolean; selectedForChanges: boolean;
onclick: () => void; onclick: () => void;
size: 'button' | 'tag';
} }
const { selectedForChanges, onclick }: Props = $props(); const { selectedForChanges, size = 'button', onclick }: Props = $props();
</script> </script>
{#if selectedForChanges} {#if selectedForChanges}
@ -15,7 +16,7 @@
kind="soft" kind="soft"
tooltip="New changes will land here" tooltip="New changes will land here"
icon="target" icon="target"
size="tag" {size}
clickable={false} clickable={false}
> >
Default branch Default branch
@ -26,7 +27,7 @@
outline outline
tooltip="When selected, new changes land here" tooltip="When selected, new changes land here"
icon="target" icon="target"
size="tag" {size}
{onclick} {onclick}
> >
Set as default Set as default

View File

@ -2,26 +2,19 @@
import BranchLabel from './BranchLabel.svelte'; import BranchLabel from './BranchLabel.svelte';
import StackingStatusIcon from './StackingStatusIcon.svelte'; import StackingStatusIcon from './StackingStatusIcon.svelte';
import { getColorFromBranchType } from './stackingUtils'; import { getColorFromBranchType } from './stackingUtils';
import { Project } from '$lib/backend/projects';
import { BaseBranch } from '$lib/baseBranch/baseBranch'; import { BaseBranch } from '$lib/baseBranch/baseBranch';
import { BaseBranchService } from '$lib/baseBranch/baseBranchService';
import StackingBranchHeaderContextMenu from '$lib/branch/StackingBranchHeaderContextMenu.svelte'; import StackingBranchHeaderContextMenu from '$lib/branch/StackingBranchHeaderContextMenu.svelte';
import ContextMenu from '$lib/components/contextmenu/ContextMenu.svelte'; import ContextMenu from '$lib/components/contextmenu/ContextMenu.svelte';
import { mapErrorToToast } from '$lib/gitHost/github/errorMap';
import { getGitHost } from '$lib/gitHost/interface/gitHost'; import { getGitHost } from '$lib/gitHost/interface/gitHost';
import { getGitHostListingService } from '$lib/gitHost/interface/gitHostListingService'; import { getGitHostListingService } from '$lib/gitHost/interface/gitHostListingService';
import { getGitHostPrService } from '$lib/gitHost/interface/gitHostPrService'; import { getGitHostPrService } from '$lib/gitHost/interface/gitHostPrService';
import { showError, showToast } from '$lib/notifications/toasts'; import PrDetailsModal from '$lib/pr/PrDetailsModal.svelte';
import PullRequestButton from '$lib/pr/PullRequestButton.svelte';
import StackingPullRequestCard from '$lib/pr/StackingPullRequestCard.svelte'; import StackingPullRequestCard from '$lib/pr/StackingPullRequestCard.svelte';
import { getContext, getContextStore } from '$lib/utils/context'; import { getContext, getContextStore } from '$lib/utils/context';
import { sleep } from '$lib/utils/sleep';
import { error } from '$lib/utils/toasts';
import { openExternalUrl } from '$lib/utils/url'; import { openExternalUrl } from '$lib/utils/url';
import { BranchController } from '$lib/vbranches/branchController'; import { BranchController } from '$lib/vbranches/branchController';
import { DetailedCommit, VirtualBranch, type CommitStatus } from '$lib/vbranches/types'; import { DetailedCommit, VirtualBranch, type CommitStatus } from '$lib/vbranches/types';
import Button from '@gitbutler/ui/Button.svelte'; import Button from '@gitbutler/ui/Button.svelte';
import type { PullRequest } from '$lib/gitHost/interface/types';
interface Props { interface Props {
name: string; name: string;
@ -31,7 +24,6 @@
const { name, upstreamName, commits }: Props = $props(); const { name, upstreamName, commits }: Props = $props();
let isLoading = $state(false);
let descriptionVisible = $state(false); let descriptionVisible = $state(false);
const branchStore = getContextStore(VirtualBranch); const branchStore = getContextStore(VirtualBranch);
@ -39,16 +31,12 @@
const branchController = getContext(BranchController); const branchController = getContext(BranchController);
const baseBranch = getContextStore(BaseBranch); const baseBranch = getContextStore(BaseBranch);
const baseBranchService = getContext(BaseBranchService);
const prService = getGitHostPrService(); const prService = getGitHostPrService();
const gitListService = getGitHostListingService();
const gitHost = getGitHost(); const gitHost = getGitHost();
const gitHostBranch = $derived(upstreamName ? $gitHost?.branch(upstreamName) : undefined); const gitHostBranch = $derived(upstreamName ? $gitHost?.branch(upstreamName) : undefined);
const project = getContext(Project);
const baseBranchName = $derived($baseBranch.shortName);
let contextMenu = $state<ReturnType<typeof ContextMenu>>(); let contextMenu = $state<ReturnType<typeof ContextMenu>>();
let prDetailsModal = $state<ReturnType<typeof PrDetailsModal>>();
let meatballButtonEl = $state<HTMLDivElement>(); let meatballButtonEl = $state<HTMLDivElement>();
const branchColorType = $derived<CommitStatus>(branch.commits?.[0]?.status ?? 'local'); const branchColorType = $derived<CommitStatus>(branch.commits?.[0]?.status ?? 'local');
@ -66,95 +54,8 @@
const prMonitor = $derived(prNumber ? $prService?.prMonitor(prNumber) : undefined); const prMonitor = $derived(prNumber ? $prService?.prMonitor(prNumber) : undefined);
const pr = $derived(prMonitor?.pr); const pr = $derived(prMonitor?.pr);
interface CreatePrOpts { function handleOpenPR() {
draft: boolean; prDetailsModal?.show();
}
const defaultPrOpts: CreatePrOpts = {
draft: true
};
async function createPr(createPrOpts: CreatePrOpts): Promise<PullRequest | undefined> {
const opts = { ...defaultPrOpts, ...createPrOpts };
if (!$gitHost) {
error('Pull request service not available');
return;
}
let title: string;
let body: string;
let pullRequestTemplateBody: string | undefined;
const prTemplatePath = project.git_host.pullRequestTemplatePath;
if (prTemplatePath) {
pullRequestTemplateBody = await $prService?.pullRequestTemplateContent(
prTemplatePath,
project.id
);
}
if (pullRequestTemplateBody) {
title = name;
body = pullRequestTemplateBody;
} else {
// In case of a single commit, use the commit summary and description for the title and
// description of the PR.
if (commits.length === 1) {
const commit = commits[0];
title = commit?.descriptionTitle ?? '';
body = commit?.descriptionBody ?? '';
} else {
title = name;
body = '';
}
}
isLoading = true;
try {
let upstreamBranchName: string | undefined = upstreamName;
if (commits.some((c) => !c.isRemote)) {
const firstPush = !branch.upstream;
await branchController.pushBranch(branch.id, branch.requiresForce, true);
if (firstPush) {
// TODO: fix this hack for reactively available prService.
await sleep(500);
}
}
if (!baseBranchName) {
error('No base branch name determined');
return;
}
if (!upstreamBranchName) {
error('No upstream branch name determined');
return;
}
if (!$prService) {
error('Pull request service not available');
return;
}
await $prService.createPr({
title,
body,
draft: opts.draft,
baseBranchName,
upstreamName: upstreamBranchName
});
} catch (err: any) {
console.error(err);
const toast = mapErrorToToast(err);
if (toast) showToast(toast);
else showError('Error while creating pull request', err);
} finally {
isLoading = false;
}
await $gitListService?.refresh();
baseBranchService.fetchFromRemotes();
} }
function editTitle(title: string) { function editTitle(title: string) {
@ -221,19 +122,20 @@
{#if $pr} {#if $pr}
<StackingPullRequestCard pr={$pr} {prMonitor} sourceBranch={$pr.sourceBranch} /> <StackingPullRequestCard pr={$pr} {prMonitor} sourceBranch={$pr.sourceBranch} />
{:else} {:else}
<PullRequestButton <Button
click={async ({ draft }) => await createPr({ draft })} style="ghost"
wide
outline
disabled={commits.length === 0 || !$gitHost || !$prService} disabled={commits.length === 0 || !$gitHost || !$prService}
tooltip={!$gitHost || !$prService onclick={handleOpenPR}>Create pull request</Button
? 'You can enable git host integration in the settings' >
: ''}
loading={isLoading}
/>
{/if} {/if}
</div> </div>
</div> </div>
</div> </div>
<PrDetailsModal bind:this={prDetailsModal} type="preview-series" {upstreamName} {name} {commits} />
<style lang="postcss"> <style lang="postcss">
.branch-header { .branch-header {
display: flex; display: flex;

View File

@ -162,6 +162,8 @@
.branch { .branch {
height: 100%; height: 100%;
width: fit-content; width: fit-content;
/* disable lane outline on modal close */
outline: none;
} }
.draggable-branch { .draggable-branch {

View File

@ -10,6 +10,7 @@ export function parseGitHubDetailedPullRequest(
id: data.id, id: data.id,
number: data.number, number: data.number,
title: data.title, title: data.title,
body: data.body ?? undefined,
sourceBranch: data.base?.ref, sourceBranch: data.base?.ref,
draft: data.draft, draft: data.draft,
htmlUrl: data.html_url, htmlUrl: data.html_url,

View File

@ -30,6 +30,7 @@ export interface PullRequest {
export interface DetailedPullRequest { export interface DetailedPullRequest {
id: number; id: number;
title: string; title: string;
body: string | undefined;
number: number; number: number;
sourceBranch: string; sourceBranch: string;
draft?: boolean; draft?: boolean;

View File

@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import EmptyStatePlaceholder from '../components/EmptyStatePlaceholder.svelte';
import FullviewLoading from '../components/FullviewLoading.svelte'; import FullviewLoading from '../components/FullviewLoading.svelte';
import LazyloadContainer from '../shared/LazyloadContainer.svelte'; import LazyloadContainer from '../shared/LazyloadContainer.svelte';
import emptyFolderSvg from '$lib/assets/empty-state/empty-folder.svg?raw'; import emptyFolderSvg from '$lib/assets/empty-state/empty-folder.svg?raw';
@ -12,6 +11,7 @@
import { getContext } from '$lib/utils/context'; import { getContext } from '$lib/utils/context';
import { RemoteFile } from '$lib/vbranches/types'; import { RemoteFile } from '$lib/vbranches/types';
import Button from '@gitbutler/ui/Button.svelte'; import Button from '@gitbutler/ui/Button.svelte';
import EmptyStatePlaceholder from '@gitbutler/ui/EmptyStatePlaceholder.svelte';
import Icon from '@gitbutler/ui/Icon.svelte'; import Icon from '@gitbutler/ui/Icon.svelte';
import { plainToInstance } from 'class-transformer'; import { plainToInstance } from 'class-transformer';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
@ -126,7 +126,7 @@
<!-- EMPTY STATE --> <!-- EMPTY STATE -->
{#if $snapshots.length === 0 && !$loading} {#if $snapshots.length === 0 && !$loading}
<EmptyStatePlaceholder image={emptyFolderSvg}> <EmptyStatePlaceholder image={emptyFolderSvg} bottomMargin={48}>
<svelte:fragment slot="title">No snapshots yet</svelte:fragment> <svelte:fragment slot="title">No snapshots yet</svelte:fragment>
<svelte:fragment slot="caption"> <svelte:fragment slot="caption">
Gitbutler saves your work, including file changes, so your progress is always secure. Gitbutler saves your work, including file changes, so your progress is always secure.

View File

@ -2,7 +2,6 @@
import GroupHeader from './GroupHeader.svelte'; import GroupHeader from './GroupHeader.svelte';
import noBranchesSvg from '$lib/assets/empty-state/no-branches.svg?raw'; import noBranchesSvg from '$lib/assets/empty-state/no-branches.svg?raw';
import { CombinedBranchListingService } from '$lib/branches/branchListing'; import { CombinedBranchListingService } from '$lib/branches/branchListing';
import EmptyStatePlaceholder from '$lib/components/EmptyStatePlaceholder.svelte';
import BranchListingSidebarEntry from '$lib/navigation/BranchListingSidebarEntry.svelte'; import BranchListingSidebarEntry from '$lib/navigation/BranchListingSidebarEntry.svelte';
import ChunkyList from '$lib/navigation/ChunkyList.svelte'; import ChunkyList from '$lib/navigation/ChunkyList.svelte';
import PullRequestSidebarEntry from '$lib/navigation/PullRequestSidebarEntry.svelte'; import PullRequestSidebarEntry from '$lib/navigation/PullRequestSidebarEntry.svelte';
@ -10,6 +9,7 @@
import ScrollableContainer from '$lib/scroll/ScrollableContainer.svelte'; import ScrollableContainer from '$lib/scroll/ScrollableContainer.svelte';
import { getContext } from '$lib/utils/context'; import { getContext } from '$lib/utils/context';
import Badge from '@gitbutler/ui/Badge.svelte'; import Badge from '@gitbutler/ui/Badge.svelte';
import EmptyStatePlaceholder from '@gitbutler/ui/EmptyStatePlaceholder.svelte';
import Icon from '@gitbutler/ui/Icon.svelte'; import Icon from '@gitbutler/ui/Icon.svelte';
import Segment from '@gitbutler/ui/segmentControl/Segment.svelte'; import Segment from '@gitbutler/ui/segmentControl/Segment.svelte';
import SegmentControl from '@gitbutler/ui/segmentControl/SegmentControl.svelte'; import SegmentControl from '@gitbutler/ui/segmentControl/SegmentControl.svelte';
@ -154,7 +154,7 @@
{/if} {/if}
</ScrollableContainer> </ScrollableContainer>
{:else} {:else}
<EmptyStatePlaceholder image={noBranchesSvg} width="11rem"> <EmptyStatePlaceholder image={noBranchesSvg} width={180} bottomMargin={48}>
<svelte:fragment slot="caption">No branches<br />match your filter</svelte:fragment> <svelte:fragment slot="caption">No branches<br />match your filter</svelte:fragment>
</EmptyStatePlaceholder> </EmptyStatePlaceholder>
{/if} {/if}

View File

@ -0,0 +1,554 @@
<script lang="ts" module>
export interface CreatePrParams {
title: string;
body: string;
draft: boolean;
}
</script>
<script lang="ts">
import { getPreferredPRAction, PRAction } from './pr';
import { AIService } from '$lib/ai/service';
import { Project } from '$lib/backend/projects';
import { BaseBranch } from '$lib/baseBranch/baseBranch';
import { BaseBranchService } from '$lib/baseBranch/baseBranchService';
import Markdown from '$lib/components/Markdown.svelte';
import { projectAiGenEnabled } from '$lib/config/config';
import { mapErrorToToast } from '$lib/gitHost/github/errorMap';
import { getGitHost } from '$lib/gitHost/interface/gitHost';
import { getGitHostListingService } from '$lib/gitHost/interface/gitHostListingService';
import { getGitHostPrService } from '$lib/gitHost/interface/gitHostPrService';
import { showError, showToast } from '$lib/notifications/toasts';
import { isFailure } from '$lib/result';
import ScrollableContainer from '$lib/scroll/ScrollableContainer.svelte';
import BorderlessTextarea from '$lib/shared/BorderlessTextarea.svelte';
import Toggle from '$lib/shared/Toggle.svelte';
import { User } from '$lib/stores/user';
import { autoHeight } from '$lib/utils/autoHeight';
import { getBranchNameFromRef } from '$lib/utils/branch';
import { getContext, getContextStore } from '$lib/utils/context';
import { KeyName, onMetaEnter } from '$lib/utils/hotkeys';
import { sleep } from '$lib/utils/sleep';
import { error } from '$lib/utils/toasts';
import { openExternalUrl } from '$lib/utils/url';
import { BranchController } from '$lib/vbranches/branchController';
import { DetailedCommit, VirtualBranch } from '$lib/vbranches/types';
import Button from '@gitbutler/ui/Button.svelte';
import Modal from '@gitbutler/ui/Modal.svelte';
import Segment from '@gitbutler/ui/segmentControl/Segment.svelte';
import SegmentControl from '@gitbutler/ui/segmentControl/SegmentControl.svelte';
import { tick } from 'svelte';
import type { DetailedPullRequest, PullRequest } from '$lib/gitHost/interface/types';
interface BaseProps {
type: 'display' | 'preview' | 'preview-series';
}
interface DisplayProps extends BaseProps {
type: 'display';
pr: DetailedPullRequest;
}
interface PreviewProps extends BaseProps {
type: 'preview';
}
interface PreviewSeriesProps {
type: 'preview-series';
name: string;
upstreamName?: string;
commits: DetailedCommit[];
}
type Props = DisplayProps | PreviewProps | PreviewSeriesProps;
let props: Props = $props();
const user = getContextStore(User);
const project = getContext(Project);
const baseBranch = getContextStore(BaseBranch);
const branchStore = getContextStore(VirtualBranch);
const branchController = getContext(BranchController);
const baseBranchService = getContext(BaseBranchService);
const gitListService = getGitHostListingService();
const prService = getGitHostPrService();
const aiService = getContext(AIService);
const aiGenEnabled = projectAiGenEnabled(project.id);
const gitHost = getGitHost();
const preferredPRAction = getPreferredPRAction();
const branch = $derived($branchStore);
const branchName = $derived(props.type === 'preview-series' ? props.name : branch.name);
const commits = $derived(props.type === 'preview-series' ? props.commits : branch.commits);
const upstreamName = $derived(
props.type === 'preview-series' ? props.upstreamName : branch.upstreamName
);
const baseBranchName = $derived($baseBranch.shortName);
const prTemplatePath = $derived(project.git_host.pullRequestTemplatePath);
let isDraft = $state<boolean>($preferredPRAction === PRAction.CreateDraft);
let modal = $state<ReturnType<typeof Modal>>();
// let inputTitleElem = $state<HTMLInputElement | null>(null);
let bodyTextArea = $state<HTMLTextAreaElement | null>(null);
let isEditing = $state<boolean>(true);
let isLoading = $state<boolean>(false);
let pullRequestTemplateBody = $state<string | undefined>(undefined);
let aiIsLoading = $state<boolean>(false);
let aiConfigurationValid = $state<boolean>(false);
let aiDescriptionDirective = $state<string | undefined>(undefined);
let showAiBox = $state<boolean>(false);
const canUseAI = $derived.by(() => {
return aiConfigurationValid || $aiGenEnabled;
});
const defaultTitle: string = $derived.by(() => {
if (props.type === 'display') return props.pr.title;
// In case of a single commit, use the commit summary for the title
if (commits.length === 1) {
const commit = commits[0];
return commit?.descriptionTitle ?? '';
} else {
return branchName;
}
});
const defaultBody: string = $derived.by(() => {
if (props.type === 'display') return props.pr.body ?? '';
if (pullRequestTemplateBody) return pullRequestTemplateBody;
// In case of a single commit, use the commit description for the body
if (commits.length === 1) {
const commit = commits[0];
return commit?.descriptionBody ?? '';
} else {
return '';
}
});
let inputBody = $state<string | undefined>(undefined);
let inputTitle = $state<string | undefined>(undefined);
const actualBody = $derived<string>(inputBody ?? defaultBody);
const actualTitle = $derived<string>(inputTitle ?? defaultTitle);
// Fetch PR template content
$effect(() => {
if ($prService && pullRequestTemplateBody === undefined && prTemplatePath) {
$prService.pullRequestTemplateContent(prTemplatePath, project.id).then((template) => {
pullRequestTemplateBody = template;
});
}
});
$effect(() => {
if (modal?.imports.open) {
aiService.validateConfiguration($user?.access_token).then((valid) => {
aiConfigurationValid = valid;
});
}
});
function updateFieldsHeight() {
if (bodyTextArea) autoHeight(bodyTextArea);
}
async function createPr(params: CreatePrParams): Promise<PullRequest | undefined> {
if (!$gitHost) {
error('Pull request service not available');
return;
}
isLoading = true;
try {
let upstreamBranchName = upstreamName;
if (commits.some((c) => !c.isRemote)) {
const firstPush = !branch.upstream;
const pushResult = await branchController.pushBranch(
branch.id,
branch.requiresForce,
props.type === 'preview-series'
);
if (pushResult) {
upstreamBranchName = getBranchNameFromRef(pushResult.refname, pushResult.remote);
}
if (firstPush) {
// TODO: fix this hack for reactively available prService.
await sleep(500);
}
}
if (!baseBranchName) {
error('No base branch name determined');
return;
}
if (!upstreamBranchName) {
error('No upstream branch name determined');
return;
}
if (!$prService) {
error('Pull request service not available');
return;
}
await $prService.createPr({
title: params.title,
body: params.body,
draft: params.draft,
baseBranchName,
upstreamName: upstreamBranchName
});
} catch (err: any) {
console.error(err);
const toast = mapErrorToToast(err);
if (toast) showToast(toast);
else showError('Error while creating pull request', err);
} finally {
isLoading = false;
}
await $gitListService?.refresh();
baseBranchService.fetchFromRemotes();
}
async function handleCreatePR(close: () => void) {
if (props.type === 'display') return;
await createPr({
title: actualTitle,
body: actualBody,
draft: isDraft
});
close();
}
function handleCheckDraft() {
isDraft = !isDraft;
}
async function handleAIButtonPressed() {
if (props.type === 'display') return;
if (!aiGenEnabled) return;
aiIsLoading = true;
await tick();
let firstToken = true;
const descriptionResult = await aiService?.describePR({
title: actualTitle,
body: actualBody,
directive: aiDescriptionDirective,
commitMessages: commits.map((c) => c.description),
prBodyTemplate: pullRequestTemplateBody,
userToken: $user.access_token,
onToken: (t) => {
if (firstToken) {
firstToken = false;
inputBody = '';
}
inputBody += t;
updateFieldsHeight();
}
});
if (isFailure(descriptionResult)) {
showError('Failed to generate commit message', descriptionResult.failure);
aiIsLoading = false;
return;
}
inputBody = descriptionResult.value;
aiIsLoading = false;
aiDescriptionDirective = undefined;
await tick();
updateFieldsHeight();
}
function handleModalKeydown(e: KeyboardEvent) {
switch (e.key) {
case 'e':
if (e.metaKey || e.ctrlKey) {
e.stopPropagation();
e.preventDefault();
}
break;
case 'g':
if ((e.metaKey || e.ctrlKey) && e.shiftKey) {
e.stopPropagation();
e.preventDefault();
handleAIButtonPressed();
}
break;
case KeyName.Enter:
if (isEditing || isLoading || aiIsLoading) break;
if (e.metaKey || e.ctrlKey) {
e.stopPropagation();
e.preventDefault();
handleCreatePR(() => modal?.close());
}
break;
}
}
function showBorderOnScroll(e: Event) {
const target = e.target as HTMLElement;
const scrollPosition = target.scrollTop;
const top = scrollPosition < 5;
if (top) {
target.style.borderTop = 'none';
} else {
target.style.borderTop = '1px solid var(--clr-border-3)';
}
}
function onClose() {
isEditing = true;
inputTitle = undefined;
inputBody = undefined;
}
let prLinkCopied = $state(false);
function handlePrLinkCopied(link: string) {
if (!navigator.clipboard) return;
navigator.clipboard.writeText(link);
prLinkCopied = true;
setTimeout(() => {
prLinkCopied = false;
}, 2000);
}
export function show() {
modal?.show();
}
export const imports = {
get open() {
return modal?.imports.open;
}
};
const isPreviewOnly = props.type === 'display';
</script>
<Modal bind:this={modal} width="medium-large" noPadding {onClose} onKeyDown={handleModalKeydown}>
<div class="pr-content">
<!-- MAIN FIELDS -->
<div class="pr-header">
<div class="pr-title">
<BorderlessTextarea
placeholder="PR title"
value={actualTitle}
fontSize={18}
readonly={!isEditing || isPreviewOnly}
oninput={(e) => {
inputTitle = e.currentTarget.value;
}}
/>
</div>
{#if !isPreviewOnly}
<SegmentControl
defaultIndex={isPreviewOnly ? 1 : 0}
onselect={(id) => {
if (id === 'write') {
isEditing = true;
} else {
isEditing = false;
}
}}
>
<Segment id="write">Edit</Segment>
<Segment id="preview">Preview</Segment>
</SegmentControl>
{/if}
</div>
<ScrollableContainer wide maxHeight="66vh" onscroll={showBorderOnScroll}>
{#if isPreviewOnly || !isEditing}
<div class="pr-description-preview">
<Markdown content={actualBody} />
</div>
{:else}
<BorderlessTextarea
value={actualBody}
padding={{ top: 0, right: 16, bottom: 16, left: 20 }}
placeholder="Add description…"
oninput={(e) => {
inputBody = e.currentTarget.value;
}}
/>
{/if}
<!-- AI GENRATION -->
{#if !isPreviewOnly && canUseAI && isEditing}
<div class="pr-ai" class:show-ai-box={showAiBox}>
{#if showAiBox}
<BorderlessTextarea
bind:value={aiDescriptionDirective}
padding={{ top: 16, right: 16, bottom: 0, left: 20 }}
placeholder={aiService.prSummaryMainDirective}
onkeydown={onMetaEnter(handleAIButtonPressed)}
oninput={(e) => {
aiDescriptionDirective = e.currentTarget.value;
}}
/>
<div class="pr-ai__actions">
<Button style="ghost" outline onclick={() => (showAiBox = false)}>Hide</Button>
<Button
style="neutral"
kind="solid"
icon="ai-small"
tooltip={!aiConfigurationValid
? 'You must be logged in or have provided your own API key'
: !$aiGenEnabled
? 'You must have summary generation enabled'
: undefined}
disabled={!canUseAI || aiIsLoading}
isLoading={aiIsLoading}
onclick={handleAIButtonPressed}
>
Generate
</Button>
</div>
{:else}
<div class="pr-ai__actions">
<Button
style="ghost"
outline
icon="ai-small"
tooltip={!aiConfigurationValid
? 'You must be logged in or have provided your own API key'
: !$aiGenEnabled
? 'You must have summary generation enabled'
: undefined}
disabled={!canUseAI || aiIsLoading}
isLoading={aiIsLoading}
onclick={() => {
showAiBox = true;
}}
>
Generate description
</Button>
</div>
{/if}
</div>
{/if}
</ScrollableContainer>
</div>
<!-- FOOTER -->
{#snippet controls(close)}
<div class="pr-footer">
{#if props.type !== 'display'}
<label class="draft-toggle__wrap">
<Toggle id="is-draft-toggle" small checked={isDraft} on:click={handleCheckDraft} />
<label class="text-12 draft-toggle__label" for="is-draft-toggle">Create as a draft</label>
</label>
<div class="pr-footer__actions">
<Button style="ghost" outline onclick={close}>Cancel</Button>
<Button
style="pop"
kind="solid"
disabled={isLoading || aiIsLoading}
{isLoading}
onclick={async () => await handleCreatePR(close)}
>{isDraft ? 'Create draft pull request' : 'Create pull request'}</Button
>
</div>
{:else}
<div class="pr-footer__actions">
<Button
style="ghost"
outline
icon={prLinkCopied ? 'tick-small' : 'copy-small'}
disabled={prLinkCopied}
onclick={() => {
handlePrLinkCopied(props.pr.htmlUrl);
}}>{prLinkCopied ? 'Link copied!' : 'Copy PR link'}</Button
>
<Button
style="ghost"
outline
icon="open-link"
onclick={() => {
openExternalUrl(props.pr.htmlUrl);
}}>Open in browser</Button
>
</div>
<Button style="ghost" outline onclick={close}>Close</Button>
{/if}
</div>
{/snippet}
</Modal>
<style lang="postcss">
.pr-content {
display: flex;
flex-direction: column;
}
.pr-header {
display: flex;
gap: 16px;
padding: 16px 16px 12px 20px;
}
.pr-title {
flex: 1;
margin-top: 4px;
}
.pr-description-preview {
overflow-y: auto;
display: flex;
padding: 0 16px 16px 20px;
}
/* AI BOX */
.pr-ai {
display: flex;
flex-direction: column;
}
.show-ai-box {
border-top: 1px solid var(--clr-border-3);
}
.pr-ai__actions {
display: flex;
gap: 6px;
padding: 12px 20px 16px;
}
/* FOOTER */
.pr-footer {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.pr-footer__actions {
display: flex;
gap: 8px;
}
.draft-toggle__wrap {
display: flex;
align-items: center;
gap: 10px;
}
.draft-toggle__label {
color: var(--clr-text-2);
}
</style>

View File

@ -1,19 +1,9 @@
<script lang="ts"> <script lang="ts">
import { getPreferredPRAction, PRAction, PRActionLabels, prActions } from './pr';
import ContextMenuItem from '$lib/components/contextmenu/ContextMenuItem.svelte'; import ContextMenuItem from '$lib/components/contextmenu/ContextMenuItem.svelte';
import ContextMenuSection from '$lib/components/contextmenu/ContextMenuSection.svelte'; import ContextMenuSection from '$lib/components/contextmenu/ContextMenuSection.svelte';
import { persisted } from '$lib/persisted/persisted';
import DropDownButton from '$lib/shared/DropDownButton.svelte'; import DropDownButton from '$lib/shared/DropDownButton.svelte';
enum Action {
Create = 'createPr',
CreateDraft = 'createDraftPr'
}
const actions = Object.values(Action);
const labels = {
[Action.Create]: 'Create PR',
[Action.CreateDraft]: 'Create Draft PR'
};
type Props = { type Props = {
loading: boolean; loading: boolean;
disabled?: boolean; disabled?: boolean;
@ -22,14 +12,8 @@
}; };
const { loading, disabled, tooltip, click }: Props = $props(); const { loading, disabled, tooltip, click }: Props = $props();
const preferredAction = persisted<Action>(Action.Create, 'projectDefaultPrAction'); const preferredAction = getPreferredPRAction();
let dropDown = $state<ReturnType<typeof DropDownButton>>(); let dropDown = $state<ReturnType<typeof DropDownButton>>();
$effect(() => {
if (!Object.values(Action).includes($preferredAction)) {
$preferredAction = Action.Create;
}
});
</script> </script>
<DropDownButton <DropDownButton
@ -39,14 +23,14 @@
{disabled} {disabled}
{loading} {loading}
bind:this={dropDown} bind:this={dropDown}
onclick={() => click({ draft: $preferredAction === Action.CreateDraft })} onclick={() => click({ draft: $preferredAction === PRAction.CreateDraft })}
> >
{labels[$preferredAction]} {PRActionLabels[$preferredAction]}
{#snippet contextMenuSlot()} {#snippet contextMenuSlot()}
<ContextMenuSection> <ContextMenuSection>
{#each actions as method} {#each prActions as method}
<ContextMenuItem <ContextMenuItem
label={labels[method]} label={PRActionLabels[method]}
on:click={() => { on:click={() => {
preferredAction.set(method); preferredAction.set(method);
dropDown?.close(); dropDown?.close();

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import MergeButton from './MergeButton.svelte'; import MergeButton from './MergeButton.svelte';
import ViewPrButton from './ViewPrButton.svelte'; import PrDetailsModal from './PrDetailsModal.svelte';
import InfoMessage from '../shared/InfoMessage.svelte'; import InfoMessage from '../shared/InfoMessage.svelte';
import { Project } from '$lib/backend/projects'; import { Project } from '$lib/backend/projects';
import { BaseBranchService } from '$lib/baseBranch/baseBranchService'; import { BaseBranchService } from '$lib/baseBranch/baseBranchService';
@ -25,7 +25,7 @@
type StatusInfo = { type StatusInfo = {
text: string; text: string;
icon: keyof typeof iconsJson | undefined; icon?: keyof typeof iconsJson | undefined;
style?: ComponentColor; style?: ComponentColor;
messageStyle?: MessageStyle; messageStyle?: MessageStyle;
}; };
@ -34,6 +34,8 @@
const baseBranchService = getContext(BaseBranchService); const baseBranchService = getContext(BaseBranchService);
const project = getContext(Project); const project = getContext(Project);
let prDetailsModal = $state<ReturnType<typeof PrDetailsModal>>();
const gitHostListingService = getGitHostListingService(); const gitHostListingService = getGitHostListingService();
const prStore = $derived($gitHostListingService?.prs); const prStore = $derived($gitHostListingService?.prs);
const prs = $derived(prStore ? $prStore : undefined); const prs = $derived(prStore ? $prStore : undefined);
@ -90,11 +92,7 @@
? 'success-small' ? 'success-small'
: 'error-small' : 'error-small'
: 'spinner'; : 'spinner';
const text = $checks.completed const text = $checks.completed ? 'Checks' : getChecksCount($checks);
? $checks.success
? 'Checks passed'
: 'Checks failed'
: getChecksCount($checks);
return { style, icon, text }; return { style, icon, text };
} }
if ($checksLoading) { if ($checksLoading) {
@ -104,22 +102,22 @@
const prStatusInfo: StatusInfo = $derived.by(() => { const prStatusInfo: StatusInfo = $derived.by(() => {
if (!$pr) { if (!$pr) {
return { text: 'Status', icon: 'spinner', style: 'neutral' }; return { text: 'Status', style: 'neutral' };
} }
if ($pr?.mergedAt) { if ($pr?.mergedAt) {
return { text: 'Merged', icon: 'merged-pr-small', style: 'purple' }; return { text: 'Merged', style: 'purple' };
} }
if ($pr?.closedAt) { if ($pr?.closedAt) {
return { text: 'Closed', icon: 'closed-pr-small', style: 'error' }; return { text: 'Closed', style: 'error' };
} }
if ($pr?.draft) { if ($pr?.draft) {
return { text: 'Draft', icon: 'draft-pr-small', style: 'neutral' }; return { text: 'Draft', style: 'neutral' };
} }
return { text: 'Open', icon: 'pr-small', style: 'success' }; return { text: 'Open', style: 'success' };
}); });
const infoProps: StatusInfo | undefined = $derived.by(() => { const infoProps: StatusInfo | undefined = $derived.by(() => {
@ -174,29 +172,14 @@
{#if $pr} {#if $pr}
<div class="card pr-card"> <div class="card pr-card">
<div class="floating-button">
<Button
icon="update-small"
size="tag"
style="ghost"
outline
loading={$mrLoading || $checksLoading}
tooltip={$timeAgo ? 'Updated ' + $timeAgo : ''}
onclick={async () => {
$checksMonitor?.update();
prMonitor?.refresh();
}}
/>
</div>
<div class="pr-title text-13 text-semibold"> <div class="pr-title text-13 text-semibold">
<span style="color: var(--clr-scale-ntrl-50)">PR #{$pr?.number}:</span> <span style="color: var(--clr-scale-ntrl-50)">PR #{$pr?.number}:</span>
{$pr.title} <span>{$pr.title}</span>
</div> </div>
<div class="pr-tags"> <div class="pr-tags">
<Button <Button
size="tag" size="tag"
clickable={false} clickable={false}
icon={prStatusInfo.icon}
style={prStatusInfo.style} style={prStatusInfo.style}
kind={prStatusInfo.text !== 'Open' && prStatusInfo.text !== 'Status' ? 'solid' : 'soft'} kind={prStatusInfo.text !== 'Open' && prStatusInfo.text !== 'Status' ? 'solid' : 'soft'}
> >
@ -207,13 +190,36 @@
size="tag" size="tag"
clickable={false} clickable={false}
icon={checksTagInfo.icon} icon={checksTagInfo.icon}
reversedDirection
style={checksTagInfo.style} style={checksTagInfo.style}
kind={checksTagInfo.icon === 'success-small' ? 'solid' : 'soft'} kind={checksTagInfo.icon === 'success-small' ? 'solid' : 'soft'}
> >
{checksTagInfo.text} {checksTagInfo.text}
</Button> </Button>
{/if} {/if}
<ViewPrButton url={$pr.htmlUrl} /> <Button
size="tag"
style="ghost"
outline
icon="description-small"
onclick={() => {
prDetailsModal?.show();
}}
>
PR details
</Button>
<Button
icon="update-small"
size="tag"
style="ghost"
outline
loading={$mrLoading}
tooltip={$timeAgo ? 'Updated ' + $timeAgo : ''}
onclick={async () => {
$checksMonitor?.update();
prMonitor?.refresh();
}}
/>
</div> </div>
<!-- <!--
@ -269,6 +275,10 @@
</div> </div>
{/if} {/if}
{#if $pr}
<PrDetailsModal bind:this={prDetailsModal} type="display" pr={$pr} />
{/if}
<style lang="postcss"> <style lang="postcss">
.pr-card { .pr-card {
position: relative; position: relative;
@ -295,10 +305,4 @@
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
} }
.floating-button {
position: absolute;
right: 6px;
top: 6px;
}
</style> </style>

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import MergeButton from './MergeButton.svelte'; import MergeButton from './MergeButton.svelte';
import ViewPrButton from './ViewPrButton.svelte'; import PrDetailsModal from './PrDetailsModal.svelte';
import InfoMessage from '../shared/InfoMessage.svelte'; import InfoMessage from '../shared/InfoMessage.svelte';
import { Project } from '$lib/backend/projects'; import { Project } from '$lib/backend/projects';
import { BaseBranchService } from '$lib/baseBranch/baseBranchService'; import { BaseBranchService } from '$lib/baseBranch/baseBranchService';
@ -57,6 +57,7 @@
// }); // });
let isMerging = $state(false); let isMerging = $state(false);
let prDetailsModal = $state<ReturnType<typeof PrDetailsModal>>();
const lastFetch = $derived(prMonitor?.lastFetch); const lastFetch = $derived(prMonitor?.lastFetch);
const timeAgo = $derived($lastFetch ? createTimeAgoStore($lastFetch) : undefined); const timeAgo = $derived($lastFetch ? createTimeAgoStore($lastFetch) : undefined);
@ -64,17 +65,9 @@
const mrLoading = $derived(prMonitor?.loading); const mrLoading = $derived(prMonitor?.loading);
const checksLoading = $derived(checksMonitor?.loading); const checksLoading = $derived(checksMonitor?.loading);
$effect(() => {
console.log($checksLoading);
});
const checksError = $derived(checksMonitor?.error); const checksError = $derived(checksMonitor?.error);
const detailsError = $derived(prMonitor?.error); const detailsError = $derived(prMonitor?.error);
$effect(() => {
console.log($checksError);
});
function getChecksCount(status: ChecksStatus): string { function getChecksCount(status: ChecksStatus): string {
if (!status) return 'Running checks'; if (!status) return 'Running checks';
@ -182,23 +175,9 @@
{#if pr} {#if pr}
<div class="pr-header"> <div class="pr-header">
<div class="floating-button">
<Button
icon="update-small"
size="tag"
style="ghost"
outline
loading={$mrLoading || $checksLoading}
tooltip={$timeAgo ? 'Updated ' + $timeAgo : ''}
onclick={async () => {
checksMonitor?.update();
prMonitor?.refresh();
}}
/>
</div>
<div class="text-13 text-semibold pr-header-title"> <div class="text-13 text-semibold pr-header-title">
<span style="color: var(--clr-scale-ntrl-50)">PR #{pr?.number}:</span> <span style="color: var(--clr-scale-ntrl-50)">PR #{pr?.number}:</span>
{pr.title} <span>{pr.title}</span>
</div> </div>
<div class="pr-header-tags"> <div class="pr-header-tags">
<Button <Button
@ -221,7 +200,29 @@
{checksTagInfo.text} {checksTagInfo.text}
</Button> </Button>
{/if} {/if}
<ViewPrButton url={pr.htmlUrl} /> <Button
size="tag"
style="ghost"
outline
icon="description-small"
onclick={() => {
prDetailsModal?.show();
}}
>
PR details
</Button>
<Button
icon="update-small"
size="tag"
style="ghost"
outline
loading={$mrLoading}
tooltip={$timeAgo ? 'Updated ' + $timeAgo : ''}
onclick={async () => {
checksMonitor?.update();
prMonitor?.refresh();
}}
/>
</div> </div>
<!-- <!--
@ -275,6 +276,8 @@
</div> </div>
{/if} {/if}
</div> </div>
<PrDetailsModal bind:this={prDetailsModal} type="display" {pr} />
{/if} {/if}
<style lang="postcss"> <style lang="postcss">

View File

@ -1,31 +0,0 @@
<script lang="ts">
import CopyLinkContextMenu from './CopyLinkContextMenu.svelte';
import { openExternalUrl } from '$lib/utils/url';
import Button from '@gitbutler/ui/Button.svelte';
const { url }: { url: string } = $props();
let copyLinkContextMenu = $state<CopyLinkContextMenu>();
let viewPrButton = $state<HTMLElement>();
</script>
<Button
size="tag"
icon="open-link"
style="ghost"
outline
shrinkable
bind:el={viewPrButton}
onclick={(e: MouseEvent) => {
openExternalUrl(url);
e.preventDefault();
e.stopPropagation();
}}
oncontextmenu={(e: MouseEvent) => {
e.preventDefault();
copyLinkContextMenu?.openByMouse(e);
}}
>
Open in browser
</Button>
<CopyLinkContextMenu bind:this={copyLinkContextMenu} target={viewPrButton} {url} />

View File

@ -0,0 +1,19 @@
import { persisted, type Persisted } from '$lib/persisted/persisted';
const PR_DEFAULT_ACTION_KEY_NAME = 'projectDefaultPrAction';
export enum PRAction {
Create = 'createPr',
CreateDraft = 'createDraftPr'
}
export const prActions = Object.values(PRAction);
export const PRActionLabels = {
[PRAction.Create]: 'Create PR',
[PRAction.CreateDraft]: 'Create Draft PR'
} as const;
export function getPreferredPRAction(): Persisted<PRAction> {
return persisted<PRAction>(PRAction.Create, PR_DEFAULT_ACTION_KEY_NAME);
}

View File

@ -15,7 +15,8 @@
horz?: boolean; horz?: boolean;
onthumbdrag?: (dragging: boolean) => void; onthumbdrag?: (dragging: boolean) => void;
children: Snippet; children: Snippet;
scrollEndVisible?: boolean; onscrollEnd?: (visible: boolean) => void;
onscroll?: (e: Event) => void;
} }
let { let {
@ -30,12 +31,14 @@
horz, horz,
children, children,
onthumbdrag, onthumbdrag,
scrollEndVisible = $bindable(false) onscroll,
onscrollEnd
}: Props = $props(); }: Props = $props();
let viewport = $state<HTMLDivElement>(); let viewport = $state<HTMLDivElement>();
let contents = $state<HTMLDivElement>(); let contents = $state<HTMLDivElement>();
let scrollable = $state<boolean>(); let scrollable = $state<boolean>();
let scrollEndVisible = $state<boolean>(false);
let observer: ResizeObserver; let observer: ResizeObserver;
@ -47,13 +50,17 @@
}); });
if (viewport) observer.observe(viewport); if (viewport) observer.observe(viewport);
if (contents) observer.observe(contents); if (contents) observer.observe(contents);
scrollEndVisible = viewport
? viewport.scrollTop + viewport.clientHeight >= viewport.scrollHeight
: false;
}); });
onDestroy(() => observer.disconnect()); onDestroy(() => observer.disconnect());
$effect(() => {
if (scrollEndVisible) {
onscrollEnd?.(true);
} else {
onscrollEnd?.(false);
}
});
</script> </script>
<div class="scrollable" style:flex-grow={wide ? 1 : 0} style:max-height={maxHeight}> <div class="scrollable" style:flex-grow={wide ? 1 : 0} style:max-height={maxHeight}>
@ -65,6 +72,8 @@
onscroll={(e) => { onscroll={(e) => {
const target = e.target as HTMLDivElement; const target = e.target as HTMLDivElement;
scrollEndVisible = target.scrollTop + target.clientHeight >= target.scrollHeight; scrollEndVisible = target.scrollTop + target.clientHeight >= target.scrollHeight;
onscroll?.(e);
}} }}
> >
<div bind:this={contents} class="contents" class:fill-viewport={fillViewport}> <div bind:this={contents} class="contents" class:fill-viewport={fillViewport}>

View File

@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import notFoundSvg from '$lib/assets/empty-state/not-found.svg?raw';
import { Project, ProjectService } from '$lib/backend/projects'; import { Project, ProjectService } from '$lib/backend/projects';
import SectionCard from '$lib/components/SectionCard.svelte'; import SectionCard from '$lib/components/SectionCard.svelte';
import { getGitHost } from '$lib/gitHost/interface/gitHost'; import { getGitHost } from '$lib/gitHost/interface/gitHost';
@ -6,9 +7,11 @@
import Select from '$lib/select/Select.svelte'; import Select from '$lib/select/Select.svelte';
import SelectItem from '$lib/select/SelectItem.svelte'; import SelectItem from '$lib/select/SelectItem.svelte';
import Section from '$lib/settings/Section.svelte'; import Section from '$lib/settings/Section.svelte';
import Link from '$lib/shared/Link.svelte';
import Spacer from '$lib/shared/Spacer.svelte'; import Spacer from '$lib/shared/Spacer.svelte';
import Toggle from '$lib/shared/Toggle.svelte'; import Toggle from '$lib/shared/Toggle.svelte';
import { getContext } from '$lib/utils/context'; import { getContext } from '$lib/utils/context';
import EmptyStatePlaceholder from '@gitbutler/ui/EmptyStatePlaceholder.svelte';
const projectService = getContext(ProjectService); const projectService = getContext(ProjectService);
const project = getContext(Project); const project = getContext(Project);
@ -19,6 +22,7 @@
let useTemplate = $state(!!project.git_host?.pullRequestTemplatePath); let useTemplate = $state(!!project.git_host?.pullRequestTemplatePath);
let selectedTemplate = $state(project.git_host?.pullRequestTemplatePath ?? ''); let selectedTemplate = $state(project.git_host?.pullRequestTemplatePath ?? '');
let allAvailableTemplates = $state<{ label: string; value: string }[]>([]); let allAvailableTemplates = $state<{ label: string; value: string }[]>([]);
let isTemplatesAvailable = $state(false);
$effect(() => { $effect(() => {
if (!project.path) return; if (!project.path) return;
@ -31,6 +35,8 @@
value: relativePath value: relativePath
}; };
}); });
isTemplatesAvailable = allAvailableTemplates.length > 0;
} }
}); });
}); });
@ -50,13 +56,13 @@
</script> </script>
<Section> <Section>
<div> <div class="stack-v">
<SectionCard <SectionCard
roundedBottom={false} roundedBottom={!useTemplate}
orientation="row" orientation="row"
labelFor="use-pull-request-template-boolean" labelFor="use-pull-request-template-boolean"
> >
<svelte:fragment slot="title">Enable pull request templates</svelte:fragment> <svelte:fragment slot="title">Pull request templates</svelte:fragment>
<svelte:fragment slot="actions"> <svelte:fragment slot="actions">
<Toggle <Toggle
id="use-pull-request-template-boolean" id="use-pull-request-template-boolean"
@ -73,8 +79,10 @@
on this project through GitButler. on this project through GitButler.
</svelte:fragment> </svelte:fragment>
</SectionCard> </SectionCard>
<SectionCard roundedTop={false} orientation="row" labelFor="use-pull-request-template-path">
<svelte:fragment slot="caption"> {#if useTemplate}
<SectionCard roundedTop={false} orientation="row">
{#if isTemplatesAvailable}
<Select <Select
value={selectedTemplate} value={selectedTemplate}
options={allAvailableTemplates.map(({ label, value }) => ({ label, value }))} options={allAvailableTemplates.map(({ label, value }) => ({ label, value }))}
@ -92,8 +100,21 @@
</SelectItem> </SelectItem>
{/snippet} {/snippet}
</Select> </Select>
{:else}
<EmptyStatePlaceholder image={notFoundSvg} topBottomPadding={20}>
<svelte:fragment slot="caption"
>No templates found in the project
<span class="text-11">
<Link
href="https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/creating-a-pull-request-template-for-your-repository"
>How to create a template</Link
></span
>
</svelte:fragment> </svelte:fragment>
</EmptyStatePlaceholder>
{/if}
</SectionCard> </SectionCard>
{/if}
</div> </div>
</Section> </Section>
<Spacer /> <Spacer />

View File

@ -0,0 +1,90 @@
<script lang="ts">
import { autoHeight } from '$lib/utils/autoHeight';
import { resizeObserver } from '$lib/utils/resizeObserver';
import { pxToRem } from '@gitbutler/ui/utils/pxToRem';
import { onMount } from 'svelte';
interface Props {
ref?: HTMLTextAreaElement;
value: string | undefined;
placeholder?: string;
readonly?: boolean;
fontSize?: number;
maxHeight?: string;
padding?: {
top: number;
right: number;
bottom: number;
left: number;
};
oninput: (e: Event & { currentTarget: EventTarget & HTMLTextAreaElement }) => void;
onfocus?: (e: Event & { currentTarget: EventTarget & HTMLTextAreaElement }) => void;
onkeydown?: (e: KeyboardEvent) => void;
}
let {
ref = $bindable(),
value = $bindable(),
placeholder,
readonly,
fontSize = 14,
maxHeight = 'none',
padding = { top: 0, right: 0, bottom: 0, left: 0 },
oninput,
onfocus,
onkeydown
}: Props = $props();
onMount(() => {
setTimeout(() => {
if (ref) autoHeight(ref);
}, 0);
});
</script>
<textarea
bind:this={ref}
bind:value
use:resizeObserver={(e) => {
autoHeight(e.currentTarget as HTMLTextAreaElement);
}}
class="borderless-textarea scrollbar"
rows={1}
{placeholder}
{readonly}
oninput={(e) => {
autoHeight(e.currentTarget);
oninput(e);
}}
onfocus={(e) => {
autoHeight(e.currentTarget);
onfocus?.(e);
}}
{onkeydown}
style:font-size={pxToRem(fontSize)}
style:max-height={maxHeight}
style:padding-top={pxToRem(padding.top)}
style:padding-right={pxToRem(padding.right)}
style:padding-bottom={pxToRem(padding.bottom)}
style:padding-left={pxToRem(padding.left)}
></textarea>
<style lang="postcss">
.borderless-textarea {
resize: none;
outline: none;
font-size: 14px;
width: 100%;
padding: 0;
margin: 0;
color: var(--clr-text-1);
overflow-y: auto; /* Enable scrolling when max height is reached */
background-color: transparent;
/* background-color: rgba(0, 0, 0, 0.1); */
}
/* placeholder */
::placeholder {
color: var(--clr-text-3);
}
</style>

View File

@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import StackHeader from './StackHeader.svelte'; import StackHeader from './StackHeader.svelte';
import StackSeries from './StackSeries.svelte'; import StackSeries from './StackSeries.svelte';
import EmptyStatePlaceholder from '../components/EmptyStatePlaceholder.svelte';
import InfoMessage from '../shared/InfoMessage.svelte'; import InfoMessage from '../shared/InfoMessage.svelte';
import { PromptService } from '$lib/ai/promptService'; import { PromptService } from '$lib/ai/promptService';
import { AIService } from '$lib/ai/service'; import { AIService } from '$lib/ai/service';
@ -30,6 +29,7 @@
import { FileIdSelection } from '$lib/vbranches/fileIdSelection'; import { FileIdSelection } from '$lib/vbranches/fileIdSelection';
import { VirtualBranch } from '$lib/vbranches/types'; import { VirtualBranch } from '$lib/vbranches/types';
import Button from '@gitbutler/ui/Button.svelte'; import Button from '@gitbutler/ui/Button.svelte';
import EmptyStatePlaceholder from '@gitbutler/ui/EmptyStatePlaceholder.svelte';
import lscache from 'lscache'; import lscache from 'lscache';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import type { Writable } from 'svelte/store'; import type { Writable } from 'svelte/store';
@ -144,7 +144,7 @@
top: 12, top: 12,
bottom: 12 bottom: 12
}} }}
bind:scrollEndVisible onscrollEnd={(visible) => (scrollEndVisible = visible)}
> >
<div <div
bind:this={rsViewport} bind:this={rsViewport}
@ -191,7 +191,7 @@
{:else if branch.commits.length === 0} {:else if branch.commits.length === 0}
<Dropzones> <Dropzones>
<div class="new-branch card"> <div class="new-branch card">
<EmptyStatePlaceholder image={laneNewSvg} width="11rem"> <EmptyStatePlaceholder image={laneNewSvg} width={180} bottomMargin={48}>
<svelte:fragment slot="title">This is a new branch</svelte:fragment> <svelte:fragment slot="title">This is a new branch</svelte:fragment>
<svelte:fragment slot="caption"> <svelte:fragment slot="caption">
You can drag and drop files or parts of files here. You can drag and drop files or parts of files here.
@ -202,7 +202,7 @@
{:else} {:else}
<Dropzones> <Dropzones>
<div class="no-changes card"> <div class="no-changes card">
<EmptyStatePlaceholder image={noChangesSvg} width="11rem" hasBottomMargin={false}> <EmptyStatePlaceholder image={noChangesSvg} width={180}>
<svelte:fragment slot="caption"> <svelte:fragment slot="caption">
No uncommitted changes on this branch No uncommitted changes on this branch
</svelte:fragment> </svelte:fragment>
@ -279,9 +279,6 @@
position: sticky; position: sticky;
padding: 14px; padding: 14px;
bottom: 0px; bottom: 0px;
transition:
background-color 0.3s ease,
box-shadow 0.3s ease;
&:not(.scroll-end-visible) { &:not(.scroll-end-visible) {
background-color: var(--clr-bg-1); background-color: var(--clr-bg-1);

View File

@ -112,6 +112,7 @@
<BranchLabel name={branch.name} onChange={(name) => handleBranchNameChange(name)} /> <BranchLabel name={branch.name} onChange={(name) => handleBranchNameChange(name)} />
<span class="button-group"> <span class="button-group">
<DefaultTargetButton <DefaultTargetButton
size="tag"
selectedForChanges={branch.selectedForChanges} selectedForChanges={branch.selectedForChanges}
onclick={async () => { onclick={async () => {
isTargetBranchAnimated = true; isTargetBranchAnimated = true;

View File

@ -43,3 +43,11 @@ export function createKeybind(keybinds: KeybindDefinitions) {
return createKeybindingsHandler(keys); return createKeybindingsHandler(keys);
} }
export function onMetaEnter(callback: () => void) {
return (e: KeyboardEvent) => {
if (e.key === KeyName.Enter && (e.metaKey || e.ctrlKey)) {
callback();
}
};
}

View File

@ -311,10 +311,10 @@ export class BranchController {
branchId: string, branchId: string,
withForce: boolean, withForce: boolean,
stack: boolean = false stack: boolean = false
): Promise<BranchPushResult> { ): Promise<BranchPushResult | undefined> {
try { try {
const command = stack ? 'push_stack' : 'push_virtual_branch'; const command = stack ? 'push_stack' : 'push_virtual_branch';
const pushResult = await invoke<BranchPushResult>(command, { const pushResult = await invoke<BranchPushResult | undefined>(command, {
projectId: this.projectId, projectId: this.projectId,
branchId, branchId,
withForce withForce

View File

@ -1,14 +1,18 @@
<script lang="ts"> <script lang="ts">
import { pxToRem } from '@gitbutler/ui/utils/pxToRem';
export let image: string; export let image: string;
export let width: string = '18rem'; export let width: number = 256;
export let hasBottomMargin: boolean = true; export let bottomMargin: number = 0;
export let topBottomPadding: number = 48;
export let leftRightPadding: number = 0;
</script> </script>
<div class="empty-state-container"> <div class="empty-state-container">
<div <div
class="empty-state" class="empty-state"
style:max-width={width} style:max-width={pxToRem(width)}
style:margin-bottom={hasBottomMargin ? '48px' : '0'} style:margin-bottom={pxToRem(bottomMargin)}
style:padding={`${pxToRem(topBottomPadding)} ${pxToRem(leftRightPadding)}`}
> >
<div class="empty-state__image"> <div class="empty-state__image">
{@html image} {@html image}
@ -48,7 +52,6 @@
color: var(--clr-scale-ntrl-60); color: var(--clr-scale-ntrl-60);
background: var(--clr-bg-1); background: var(--clr-bg-1);
justify-content: center; justify-content: center;
padding: 48px 0;
width: 100%; width: 100%;
gap: 16px; gap: 16px;
border-radius: var(--radius-m); border-radius: var(--radius-m);

View File

@ -5,12 +5,13 @@
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
interface Props { interface Props {
width?: 'default' | 'large' | 'small' | 'xsmall'; width?: 'default' | 'medium-large' | 'large' | 'small' | 'xsmall';
title?: string; title?: string;
icon?: keyof typeof iconsJson; icon?: keyof typeof iconsJson;
noPadding?: boolean; noPadding?: boolean;
onClose?: () => void; onClose?: () => void;
onSubmit?: (close: () => void) => void; onSubmit?: (close: () => void) => void;
onKeyDown?: (e: KeyboardEvent) => void;
children: Snippet<[item: any, close: () => void]>; children: Snippet<[item: any, close: () => void]>;
controls?: Snippet<[close: () => void, item: any]>; controls?: Snippet<[close: () => void, item: any]>;
} }
@ -23,6 +24,7 @@
children, children,
controls, controls,
onSubmit, onSubmit,
onKeyDown,
noPadding = false noPadding = false
}: Props = $props(); }: Props = $props();
@ -50,9 +52,14 @@
}; };
</script> </script>
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<dialog <dialog
tabindex="0"
onkeydown={onKeyDown}
bind:this={dialogElement} bind:this={dialogElement}
class:default={width === 'default'} class:default={width === 'default'}
class:medium-large={width === 'medium-large'}
class:large={width === 'large'} class:large={width === 'large'}
class:small={width === 'small'} class:small={width === 'small'}
class:xsmall={width === 'xsmall'} class:xsmall={width === 'xsmall'}
@ -105,6 +112,7 @@
flex-direction: column; flex-direction: column;
max-height: calc(100vh - 80px); max-height: calc(100vh - 80px);
overflow: hidden;
border-radius: var(--radius-l); border-radius: var(--radius-l);
background-color: var(--clr-bg-1); background-color: var(--clr-bg-1);
box-shadow: var(--fx-shadow-l); box-shadow: var(--fx-shadow-l);
@ -116,6 +124,10 @@
width: 580px; width: 580px;
} }
&.medium-large {
width: 640px;
}
&.large { &.large {
width: 840px; width: 840px;
} }
@ -129,16 +141,19 @@
} }
} }
dialog[open]::backdrop { /* backdrop global */
/* NOTE: temporarily hardcoded var(--clr-overlay-bg); */ /* NOTE: temporarily hardcoded var(--clr-overlay-bg); */
background-color: color(srgb 0 0 0 / 0.34901960784313724); :global(dialog[open]::backdrop) {
background-color: rgba(214, 214, 214, 0.4);
animation: dialog-fade 0.15s ease-in; animation: dialog-fade 0.15s ease-in;
} }
html.dark dialog[open]::backdrop { /* backdrop dark */
/* NOTE: temporarily hardcoded dark var(--clr-overlay-bg); */ /* NOTE: temporarily hardcoded dark var(--clr-overlay-bg); */
background-color: color(srgb 0.8392156862745098 0.8392156862745098 0.8392156862745098 / 0.4); :global(html.dark dialog[open]::backdrop) {
background-color: rgba(0, 0, 0, 0.35);
} }
.modal__header { .modal__header {
display: flex; display: flex;
padding: 16px; padding: 16px;

View File

@ -56,12 +56,9 @@
position: relative; position: relative;
display: flex; display: flex;
& :global(> *) { & :global(> span) {
display: flex;
margin-right: -4px; margin-right: -4px;
&:last-child {
margin-right: 0;
}
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"ai": "M0.703924 10.0391C0.432026 9.94966 0.432025 9.57 0.703923 9.48054L3.43823 8.58081C4.32877 8.28777 5.02758 7.59794 5.32442 6.71884L6.23586 4.01965C6.32649 3.75125 6.71108 3.75125 6.80171 4.01965L7.71315 6.71884C8.01 7.59794 8.7088 8.28777 9.59934 8.58081L12.3336 9.48054C12.6055 9.57 12.6055 9.94966 12.3336 10.0391L9.59934 10.9389C8.7088 11.2319 8.01 11.9217 7.71315 12.8008L6.80171 15.5C6.71108 15.7684 6.32649 15.7684 6.23586 15.5L5.32442 12.8008C5.02758 11.9217 4.32877 11.2319 3.43823 10.9389L0.703924 10.0391Z M8.5 4.45601C8.2281 4.36654 8.2281 3.98689 8.5 3.89742L10.221 3.33114C10.6662 3.18462 11.0156 2.83971 11.164 2.40016L11.7377 0.701309C11.8283 0.432904 12.2129 0.432904 12.3036 0.701309L12.8772 2.40016C13.0256 2.83971 13.375 3.18462 13.8203 3.33114L15.5413 3.89742C15.8132 3.98689 15.8132 4.36654 15.5413 4.45601L13.8203 5.02229C13.375 5.16881 13.0256 5.51373 12.8772 5.95328L12.3036 7.65213C12.2129 7.92053 11.8283 7.92053 11.7377 7.65212L11.164 5.95328C11.0156 5.51373 10.6662 5.16881 10.221 5.02229L8.5 4.45601Z", "ai": "M0.219005 4.30385C-0.0730015 4.20652 -0.0730015 3.79348 0.219005 3.69615L2.06723 3.08007C2.54543 2.92067 2.92067 2.54543 3.08007 2.06723L3.69615 0.219004C3.79348 -0.0730016 4.20652 -0.0730014 4.30385 0.219005L4.91993 2.06723C5.07933 2.54543 5.45457 2.92067 5.93277 3.08007L7.781 3.69615C8.073 3.79348 8.073 4.20652 7.781 4.30385L5.93277 4.91993C5.45457 5.07933 5.07933 5.45457 4.91993 5.93277L4.30385 7.781C4.20652 8.073 3.79348 8.073 3.69615 7.78099L3.08007 5.93277C2.92067 5.45457 2.54543 5.07933 2.06723 4.91993L0.219005 4.30385ZM0.164253 13.2279C-0.0547511 13.1549 -0.0547511 12.8451 0.164253 12.7721L1.55042 12.3101C1.90907 12.1905 2.1905 11.9091 2.31005 11.5504L2.77211 10.1643C2.84511 9.94525 3.15489 9.94525 3.22789 10.1643L3.68995 11.5504C3.8095 11.9091 4.09093 12.1905 4.44958 12.3101L5.83575 12.7721C6.05475 12.8451 6.05475 13.1549 5.83575 13.2279L4.44958 13.6899C4.09093 13.8095 3.8095 14.0909 3.68995 14.4496L3.22789 15.8357C3.15489 16.0548 2.84511 16.0548 2.77211 15.8357L2.31005 14.4496C2.1905 14.0909 1.90907 13.8095 1.55042 13.6899L0.164253 13.2279ZM5.18635 9.24146C4.93788 9.32428 4.93788 9.67572 5.18635 9.75854L7.68497 10.5914C8.49875 10.8627 9.13732 11.5013 9.40858 12.315L10.2415 14.8137C10.3243 15.0621 10.6757 15.0621 10.7585 14.8137L11.5914 12.315C11.8627 11.5013 12.5013 10.8627 13.315 10.5914L15.8137 9.75854C16.0621 9.67572 16.0621 9.32428 15.8137 9.24146L13.315 8.40858C12.5013 8.13732 11.8627 7.49875 11.5914 6.68497L10.7585 4.18635C10.6757 3.93788 10.3243 3.93788 10.2415 4.18635L9.40858 6.68497C9.13732 7.49875 8.49875 8.13732 7.68497 8.40858L5.18635 9.24146Z",
"ai-small": "M2.83484 9.45537C2.6525 9.39459 2.6525 9.13668 2.83484 9.0759L4.66852 8.46467C5.26573 8.2656 5.73436 7.79697 5.93343 7.19976L6.54465 5.36609C6.60543 5.18375 6.86334 5.18375 6.92412 5.36609L7.53535 7.19976C7.73442 7.79697 8.20305 8.26561 8.80026 8.46468L10.6339 9.0759C10.8163 9.13668 10.8163 9.39459 10.6339 9.45537L8.80026 10.0666C8.20305 10.2657 7.73442 10.7343 7.53535 11.3315L6.92412 13.1652C6.86334 13.3475 6.60543 13.3475 6.54465 13.1652L5.93343 11.3315C5.73436 10.7343 5.26573 10.2657 4.66852 10.0666L2.83484 9.45537Z M8.56922 5.11994C8.38688 5.05916 8.38688 4.80125 8.56922 4.74047L9.72332 4.35577C10.0219 4.25623 10.2562 4.02192 10.3558 3.72331L10.7405 2.56921C10.8013 2.38687 11.0592 2.38687 11.1199 2.56921L11.5046 3.72331C11.6042 4.02192 11.8385 4.25623 12.1371 4.35577L13.2912 4.74047C13.4735 4.80125 13.4735 5.05916 13.2912 5.11994L12.1371 5.50464C11.8385 5.60417 11.6042 5.83849 11.5046 6.13709L11.1199 7.29119C11.0592 7.47353 10.8013 7.47353 10.7405 7.29119L10.3558 6.13709C10.2562 5.83849 10.0219 5.60417 9.72332 5.50464L8.56922 5.11994Z", "ai-small": "M0.691629 4.76587C0.436124 4.6807 0.436124 4.3193 0.691629 4.23413L2.30883 3.69506C2.72725 3.55559 3.05559 3.22725 3.19506 2.80883L3.73413 1.19163C3.8193 0.936124 4.1807 0.936124 4.26587 1.19163L4.80494 2.80883C4.94441 3.22725 5.27275 3.55559 5.69117 3.69506L7.30837 4.23413C7.56388 4.3193 7.56388 4.6807 7.30837 4.76587L5.69117 5.30494C5.27275 5.44441 4.94441 5.77275 4.80494 6.19117L4.26587 7.80837C4.1807 8.06388 3.8193 8.06388 3.73413 7.80837L3.19506 6.19117C3.05559 5.77275 2.72725 5.44441 2.30883 5.30494L0.691629 4.76587ZM0.636878 12.6899C0.454374 12.6291 0.454374 12.3709 0.636878 12.3101L1.79202 11.925C2.09089 11.8254 2.32542 11.5909 2.42505 11.292L2.81009 10.1369C2.87093 9.95437 3.12907 9.95437 3.18991 10.1369L3.57495 11.292C3.67458 11.5909 3.90911 11.8254 4.20798 11.925L5.36312 12.3101C5.54563 12.3709 5.54563 12.6291 5.36312 12.6899L4.20798 13.075C3.90911 13.1746 3.67458 13.4091 3.57495 13.708L3.18991 14.8631C3.12907 15.0456 2.87093 15.0456 2.81009 14.8631L2.42505 13.708C2.32542 13.4091 2.09089 13.1746 1.79202 13.075L0.636878 12.6899ZM5.66941 8.76496C5.44353 8.84025 5.44353 9.15975 5.66941 9.23504L7.94088 9.9922C8.68068 10.2388 9.2612 10.8193 9.5078 11.5591L10.265 13.8306C10.3403 14.0565 10.6597 14.0565 10.735 13.8306L11.4922 11.5591C11.7388 10.8193 12.3193 10.2388 13.0591 9.9922L15.3306 9.23504C15.5565 9.15975 15.5565 8.84025 15.3306 8.76496L13.0591 8.0078C12.3193 7.7612 11.7388 7.18068 11.4922 6.44088L10.735 4.16941C10.6597 3.94353 10.3403 3.94353 10.265 4.16941L9.5078 6.44088C9.2612 7.18068 8.68068 7.7612 7.94088 8.0078L5.66941 8.76496Z",
"bin": "M6.04999 11V8H7.54999V11H6.04999Z M8.45001 8V11H9.95001V8H8.45001Z M11.15 3V4.85H15.2V6.35H13.55V12C13.55 13.5188 12.3188 14.75 10.8 14.75H5.20001C3.68123 14.75 2.45001 13.5188 2.45001 12V6.35H0.799988V4.85H4.84998V3C4.84998 2.0335 5.63348 1.25 6.59998 1.25H9.39998C10.3665 1.25 11.15 2.0335 11.15 3ZM6.34998 3C6.34998 2.86193 6.46191 2.75 6.59998 2.75H9.39998C9.53805 2.75 9.64998 2.86193 9.64998 3V4.85H6.34998V3ZM3.95001 6.35H12.05V12C12.05 12.6904 11.4904 13.25 10.8 13.25H5.20001C4.50966 13.25 3.95001 12.6904 3.95001 12V6.35Z", "bin": "M6.04999 11V8H7.54999V11H6.04999Z M8.45001 8V11H9.95001V8H8.45001Z M11.15 3V4.85H15.2V6.35H13.55V12C13.55 13.5188 12.3188 14.75 10.8 14.75H5.20001C3.68123 14.75 2.45001 13.5188 2.45001 12V6.35H0.799988V4.85H4.84998V3C4.84998 2.0335 5.63348 1.25 6.59998 1.25H9.39998C10.3665 1.25 11.15 2.0335 11.15 3ZM6.34998 3C6.34998 2.86193 6.46191 2.75 6.59998 2.75H9.39998C9.53805 2.75 9.64998 2.86193 9.64998 3V4.85H6.34998V3ZM3.95001 6.35H12.05V12C12.05 12.6904 11.4904 13.25 10.8 13.25H5.20001C4.50966 13.25 3.95001 12.6904 3.95001 12V6.35Z",
"bin-small": "M6.25 10.5V8H7.75V10.5H6.25Z M8.25 8V10.5H9.75V8H8.25Z M10.75 4V5.25H14V6.75H12.75V11C12.75 12.5188 11.5188 13.75 10 13.75H6C4.48122 13.75 3.25 12.5188 3.25 11V6.75H2V5.25H5.25V4C5.25 3.0335 6.0335 2.25 7 2.25H9C9.9665 2.25 10.75 3.0335 10.75 4ZM6.75 4C6.75 3.86193 6.86193 3.75 7 3.75H9C9.13807 3.75 9.25 3.86193 9.25 4V5.25H6.75V4ZM4.75 6.75H11.25V11C11.25 11.6904 10.6904 12.25 10 12.25H6C5.30964 12.25 4.75 11.6904 4.75 11V6.75Z", "bin-small": "M6.25 10.5V8H7.75V10.5H6.25Z M8.25 8V10.5H9.75V8H8.25Z M10.75 4V5.25H14V6.75H12.75V11C12.75 12.5188 11.5188 13.75 10 13.75H6C4.48122 13.75 3.25 12.5188 3.25 11V6.75H2V5.25H5.25V4C5.25 3.0335 6.0335 2.25 7 2.25H9C9.9665 2.25 10.75 3.0335 10.75 4ZM6.75 4C6.75 3.86193 6.86193 3.75 7 3.75H9C9.13807 3.75 9.25 3.86193 9.25 4V5.25H6.75V4ZM4.75 6.75H11.25V11C11.25 11.6904 10.6904 12.25 10 12.25H6C5.30964 12.25 4.75 11.6904 4.75 11V6.75Z",
"branch": "M8.75 3C8.75 1.48122 9.98122 0.25 11.5 0.25C13.0188 0.25 14.25 1.48122 14.25 3C14.25 4.27987 13.3757 5.35553 12.1917 5.66227C11.8789 7.41747 10.3451 8.75 8.5 8.75H7.5C6.47256 8.75 5.60597 9.43866 5.33663 10.3796C6.44632 10.7336 7.24999 11.7729 7.24999 13C7.24999 14.5188 6.01878 15.75 4.5 15.75C2.98122 15.75 1.75 14.5188 1.75 13C1.75 11.7412 2.59575 10.68 3.75 10.3535L3.75 5.64648C2.59575 5.32001 1.75 4.25877 1.75 3C1.75 1.48122 2.98122 0.25 4.5 0.25C6.01878 0.25 7.24999 1.48122 7.24999 3C7.24999 4.25877 6.40425 5.32001 5.25 5.64648V7.99973C5.87675 7.52896 6.6558 7.25 7.5 7.25H8.5C9.52744 7.25 10.394 6.56134 10.6634 5.62042C9.55368 5.26643 8.75 4.22707 8.75 3ZM11.5 1.75C10.8096 1.75 10.25 2.30964 10.25 3C10.25 3.69035 10.8096 4.24999 11.5 4.24999C12.1904 4.24999 12.75 3.69035 12.75 3C12.75 2.30964 12.1904 1.75 11.5 1.75ZM4.5 11.75C3.80964 11.75 3.25 12.3096 3.25 13C3.25 13.6904 3.80964 14.25 4.5 14.25C5.19035 14.25 5.74999 13.6904 5.74999 13C5.74999 12.3096 5.19035 11.75 4.5 11.75ZM3.25 3C3.25 2.30964 3.80964 1.75 4.5 1.75C5.19035 1.75 5.74999 2.30964 5.74999 3C5.74999 3.69035 5.19035 4.24999 4.5 4.24999C3.80964 4.24999 3.25 3.69035 3.25 3Z", "branch": "M8.75 3C8.75 1.48122 9.98122 0.25 11.5 0.25C13.0188 0.25 14.25 1.48122 14.25 3C14.25 4.27987 13.3757 5.35553 12.1917 5.66227C11.8789 7.41747 10.3451 8.75 8.5 8.75H7.5C6.47256 8.75 5.60597 9.43866 5.33663 10.3796C6.44632 10.7336 7.24999 11.7729 7.24999 13C7.24999 14.5188 6.01878 15.75 4.5 15.75C2.98122 15.75 1.75 14.5188 1.75 13C1.75 11.7412 2.59575 10.68 3.75 10.3535L3.75 5.64648C2.59575 5.32001 1.75 4.25877 1.75 3C1.75 1.48122 2.98122 0.25 4.5 0.25C6.01878 0.25 7.24999 1.48122 7.24999 3C7.24999 4.25877 6.40425 5.32001 5.25 5.64648V7.99973C5.87675 7.52896 6.6558 7.25 7.5 7.25H8.5C9.52744 7.25 10.394 6.56134 10.6634 5.62042C9.55368 5.26643 8.75 4.22707 8.75 3ZM11.5 1.75C10.8096 1.75 10.25 2.30964 10.25 3C10.25 3.69035 10.8096 4.24999 11.5 4.24999C12.1904 4.24999 12.75 3.69035 12.75 3C12.75 2.30964 12.1904 1.75 11.5 1.75ZM4.5 11.75C3.80964 11.75 3.25 12.3096 3.25 13C3.25 13.6904 3.80964 14.25 4.5 14.25C5.19035 14.25 5.74999 13.6904 5.74999 13C5.74999 12.3096 5.19035 11.75 4.5 11.75ZM3.25 3C3.25 2.30964 3.80964 1.75 4.5 1.75C5.19035 1.75 5.74999 2.30964 5.74999 3C5.74999 3.69035 5.19035 4.24999 4.5 4.24999C3.80964 4.24999 3.25 3.69035 3.25 3Z",
@ -18,7 +18,7 @@
"copy-small": "M7.10832 4.75H7.66667C9.18545 4.75 10.4167 5.98122 10.4167 7.5V9.75H11C11.6903 9.75 12.25 9.19036 12.25 8.5V5C12.25 4.30964 11.6903 3.75 11 3.75H8.33331C7.72857 3.75 7.22413 4.17944 7.10832 4.75ZM10.4055 11.25C10.2791 12.6516 9.10118 13.75 7.66667 13.75H5C3.48121 13.75 2.25 12.5188 2.25 11V7.5C2.25 5.98122 3.48122 4.75 5 4.75H5.59452C5.72083 3.34837 6.8988 2.25 8.33331 2.25H11C12.5188 2.25 13.75 3.48122 13.75 5V8.5C13.75 10.0188 12.5188 11.25 11 11.25H10.4055ZM3.75 7.5C3.75 6.80964 4.30964 6.25 5 6.25H7.66667C8.35702 6.25 8.91667 6.80964 8.91667 7.5V11C8.91667 11.6904 8.35702 12.25 7.66667 12.25H5C4.30964 12.25 3.75 11.6904 3.75 11V7.5Z", "copy-small": "M7.10832 4.75H7.66667C9.18545 4.75 10.4167 5.98122 10.4167 7.5V9.75H11C11.6903 9.75 12.25 9.19036 12.25 8.5V5C12.25 4.30964 11.6903 3.75 11 3.75H8.33331C7.72857 3.75 7.22413 4.17944 7.10832 4.75ZM10.4055 11.25C10.2791 12.6516 9.10118 13.75 7.66667 13.75H5C3.48121 13.75 2.25 12.5188 2.25 11V7.5C2.25 5.98122 3.48122 4.75 5 4.75H5.59452C5.72083 3.34837 6.8988 2.25 8.33331 2.25H11C12.5188 2.25 13.75 3.48122 13.75 5V8.5C13.75 10.0188 12.5188 11.25 11 11.25H10.4055ZM3.75 7.5C3.75 6.80964 4.30964 6.25 5 6.25H7.66667C8.35702 6.25 8.91667 6.80964 8.91667 7.5V11C8.91667 11.6904 8.35702 12.25 7.66667 12.25H5C4.30964 12.25 3.75 11.6904 3.75 11V7.5Z",
"cross": "M8.00001 9.06065L12.4194 13.4801L13.4801 12.4194L9.06067 7.99999L13.4801 3.58057L12.4194 2.51991L8.00001 6.93933L3.58059 2.51991L2.51993 3.58057L6.93935 7.99999L2.51993 12.4194L3.58059 13.4801L8.00001 9.06065Z", "cross": "M8.00001 9.06065L12.4194 13.4801L13.4801 12.4194L9.06067 7.99999L13.4801 3.58057L12.4194 2.51991L8.00001 6.93933L3.58059 2.51991L2.51993 3.58057L6.93935 7.99999L2.51993 12.4194L3.58059 13.4801L8.00001 9.06065Z",
"cross-small": "M8 9.06066L11.4697 12.5303L12.5303 11.4697L9.06066 8L12.5303 4.53033L11.4697 3.46967L8 6.93934L4.53033 3.46967L3.46967 4.53033L6.93934 8L3.46967 11.4697L4.53033 12.5303L8 9.06066Z", "cross-small": "M8 9.06066L11.4697 12.5303L12.5303 11.4697L9.06066 8L12.5303 4.53033L11.4697 3.46967L8 6.93934L4.53033 3.46967L3.46967 4.53033L6.93934 8L3.46967 11.4697L4.53033 12.5303L8 9.06066Z",
"description": "M3 1.75C2.30964 1.75 1.75 2.30964 1.75 3L1.75 10.5C1.75 11.1904 2.30964 11.75 3 11.75L13 11.75C13.6904 11.75 14.25 11.1904 14.25 10.5L14.25 3C14.25 2.30964 13.6904 1.75 13 1.75L3 1.75ZM0.25 3C0.25 1.48122 1.48122 0.25 3 0.25L13 0.25C14.5188 0.25 15.75 1.48122 15.75 3L15.75 10.5C15.75 12.0188 14.5188 13.25 13 13.25L3 13.25C1.48122 13.25 0.25 12.0188 0.25 10.5L0.25 3ZM12 5.75L4 5.75L4 4.25L12 4.25V5.75ZM4 8.75L12 8.75L12 7.25L4 7.25V8.75Z", "description-small": "M13 5.75H3V4.25H13V5.75ZM13 8.75H3V7.25H13V8.75ZM3 11.75H9V10.25H3V11.75Z",
"discord": "M13.5535 3.08875C12.5178 2.58012 11.4104 2.21048 10.2526 2C10.1104 2.26983 9.94429 2.63275 9.82976 2.92145C8.599 2.72718 7.37956 2.72718 6.17144 2.92145C6.05693 2.63275 5.88704 2.26983 5.74357 2C4.58454 2.21048 3.47584 2.58148 2.44013 3.09144C0.351095 6.40485 -0.215207 9.63596 0.0679444 12.8212C1.4535 13.9072 2.79627 14.567 4.11638 14.9987C4.44233 14.5278 4.73302 14.0273 4.98345 13.4998C4.5065 13.3096 4.04969 13.0748 3.61805 12.8023C3.73256 12.7133 3.84457 12.6202 3.95279 12.5244C6.58546 13.8168 9.44593 13.8168 12.0472 12.5244C12.1566 12.6202 12.2686 12.7133 12.3819 12.8023C11.949 13.0762 11.4909 13.3109 11.014 13.5012C11.2644 14.0273 11.5538 14.5292 11.881 15C13.2024 14.5683 14.5464 13.9086 15.932 12.8212C16.2642 9.1287 15.3644 5.92726 13.5535 3.08875ZM5.34212 10.8623C4.55181 10.8623 3.9037 10.0879 3.9037 9.14488C3.9037 8.20186 4.53797 7.42612 5.34212 7.42612C6.14628 7.42612 6.79437 8.2005 6.78053 9.14488C6.78178 10.0879 6.14628 10.8623 5.34212 10.8623ZM10.6578 10.8623C9.86752 10.8623 9.21941 10.0879 9.21941 9.14488C9.21941 8.20186 9.85366 7.42612 10.6578 7.42612C11.462 7.42612 12.1101 8.2005 12.0962 9.14488C12.0962 10.0879 11.462 10.8623 10.6578 10.8623Z", "discord": "M13.5535 3.08875C12.5178 2.58012 11.4104 2.21048 10.2526 2C10.1104 2.26983 9.94429 2.63275 9.82976 2.92145C8.599 2.72718 7.37956 2.72718 6.17144 2.92145C6.05693 2.63275 5.88704 2.26983 5.74357 2C4.58454 2.21048 3.47584 2.58148 2.44013 3.09144C0.351095 6.40485 -0.215207 9.63596 0.0679444 12.8212C1.4535 13.9072 2.79627 14.567 4.11638 14.9987C4.44233 14.5278 4.73302 14.0273 4.98345 13.4998C4.5065 13.3096 4.04969 13.0748 3.61805 12.8023C3.73256 12.7133 3.84457 12.6202 3.95279 12.5244C6.58546 13.8168 9.44593 13.8168 12.0472 12.5244C12.1566 12.6202 12.2686 12.7133 12.3819 12.8023C11.949 13.0762 11.4909 13.3109 11.014 13.5012C11.2644 14.0273 11.5538 14.5292 11.881 15C13.2024 14.5683 14.5464 13.9086 15.932 12.8212C16.2642 9.1287 15.3644 5.92726 13.5535 3.08875ZM5.34212 10.8623C4.55181 10.8623 3.9037 10.0879 3.9037 9.14488C3.9037 8.20186 4.53797 7.42612 5.34212 7.42612C6.14628 7.42612 6.79437 8.2005 6.78053 9.14488C6.78178 10.0879 6.14628 10.8623 5.34212 10.8623ZM10.6578 10.8623C9.86752 10.8623 9.21941 10.0879 9.21941 9.14488C9.21941 8.20186 9.85366 7.42612 10.6578 7.42612C11.462 7.42612 12.1101 8.2005 12.0962 9.14488C12.0962 10.0879 11.462 10.8623 10.6578 10.8623Z",
"discord-outline": "M7.1875 8.09375C7.1875 9.07427 6.48095 9.86914 5.60937 9.86914C4.7378 9.86914 4.03125 9.07427 4.03125 8.09375C4.03125 7.11323 4.7378 6.31836 5.60937 6.31836C6.48095 6.31836 7.1875 7.11323 7.1875 8.09375Z M10.4062 9.86914C11.2778 9.86914 11.9844 9.07427 11.9844 8.09375C11.9844 7.11323 11.2778 6.31836 10.4062 6.31836C9.53467 6.31836 8.82812 7.11323 8.82812 8.09375C8.82812 9.07427 9.53467 9.86914 10.4062 9.86914Z M9.57024 2.16805C10.8879 2.3997 12.16 2.75884 13.3701 3.33354C15.1841 6.08312 15.9619 9.27384 15.6567 12.5544C14.2682 13.6081 12.7593 14.3558 11.1039 14.8788C10.6836 14.294 10.2765 13.6972 9.95775 13.0496C8.66794 13.3387 7.34539 13.3302 6.05146 13.025C5.72793 13.6841 5.32063 14.2832 4.89477 14.8781C3.24161 14.3553 1.72443 13.6016 0.34286 12.5543C0.0412364 9.27319 0.824091 6.09492 2.62211 3.33711C3.8387 2.75784 5.10281 2.40058 6.42434 2.16851C6.56957 2.43265 6.71893 2.69484 6.85253 2.96515C7.61301 2.88657 8.37771 2.88689 9.1458 2.96572C9.27778 2.69492 9.42614 2.43248 9.57024 2.16805ZM3.25609 12.649C3.55846 11.9971 3.84358 11.339 4.12816 10.6791C6.76234 12.0881 9.31057 12.0966 12.0049 10.6594C12.2571 11.3294 12.5205 11.9613 12.8609 12.5902C13.3088 12.359 13.7614 12.0845 14.2204 11.7557C14.4057 9.02811 13.713 6.64929 12.348 4.51097C11.7228 4.22909 11.0672 4.00661 10.3877 3.84973C10.2747 4.09991 10.1752 4.35621 10.071 4.61012C8.67283 4.3967 7.32313 4.39516 5.93062 4.6117C5.82612 4.35693 5.72686 4.09939 5.61219 3.84895C4.93138 4.00599 4.27444 4.22923 3.64855 4.51206C2.08732 6.98426 1.61928 9.38089 1.77978 11.7556C2.27892 12.1128 2.77032 12.406 3.25609 12.649Z", "discord-outline": "M7.1875 8.09375C7.1875 9.07427 6.48095 9.86914 5.60937 9.86914C4.7378 9.86914 4.03125 9.07427 4.03125 8.09375C4.03125 7.11323 4.7378 6.31836 5.60937 6.31836C6.48095 6.31836 7.1875 7.11323 7.1875 8.09375Z M10.4062 9.86914C11.2778 9.86914 11.9844 9.07427 11.9844 8.09375C11.9844 7.11323 11.2778 6.31836 10.4062 6.31836C9.53467 6.31836 8.82812 7.11323 8.82812 8.09375C8.82812 9.07427 9.53467 9.86914 10.4062 9.86914Z M9.57024 2.16805C10.8879 2.3997 12.16 2.75884 13.3701 3.33354C15.1841 6.08312 15.9619 9.27384 15.6567 12.5544C14.2682 13.6081 12.7593 14.3558 11.1039 14.8788C10.6836 14.294 10.2765 13.6972 9.95775 13.0496C8.66794 13.3387 7.34539 13.3302 6.05146 13.025C5.72793 13.6841 5.32063 14.2832 4.89477 14.8781C3.24161 14.3553 1.72443 13.6016 0.34286 12.5543C0.0412364 9.27319 0.824091 6.09492 2.62211 3.33711C3.8387 2.75784 5.10281 2.40058 6.42434 2.16851C6.56957 2.43265 6.71893 2.69484 6.85253 2.96515C7.61301 2.88657 8.37771 2.88689 9.1458 2.96572C9.27778 2.69492 9.42614 2.43248 9.57024 2.16805ZM3.25609 12.649C3.55846 11.9971 3.84358 11.339 4.12816 10.6791C6.76234 12.0881 9.31057 12.0966 12.0049 10.6594C12.2571 11.3294 12.5205 11.9613 12.8609 12.5902C13.3088 12.359 13.7614 12.0845 14.2204 11.7557C14.4057 9.02811 13.713 6.64929 12.348 4.51097C11.7228 4.22909 11.0672 4.00661 10.3877 3.84973C10.2747 4.09991 10.1752 4.35621 10.071 4.61012C8.67283 4.3967 7.32313 4.39516 5.93062 4.6117C5.82612 4.35693 5.72686 4.09939 5.61219 3.84895C4.93138 4.00599 4.27444 4.22923 3.64855 4.51206C2.08732 6.98426 1.61928 9.38089 1.77978 11.7556C2.27892 12.1128 2.77032 12.406 3.25609 12.649Z",
"docs": "M12 5.75H4V4.25H12V5.75Z M4 8.75H10V7.25H4V8.75Z M5.35241e-05 12.9852L0 3.22C0 2.08102 0 1.51153 0.225173 1.07805C0.414924 0.712765 0.712765 0.414924 1.07805 0.225173C1.51153 0 2.08102 0 3.22 0H16V16H2C1.46143 16 0.972588 15.7871 0.613012 15.4409C0.587144 15.416 0.56195 15.3904 0.537457 15.3642C0.204038 15.0069 0 14.5273 0 14V13L5.35241e-05 12.9852ZM3.22 1.5H14.5V11H2C1.82735 11 1.65981 11.0219 1.5 11.063V3.22C1.5 2.62533 1.50121 2.27105 1.523 2.0086C1.53628 1.84867 1.55378 1.78152 1.55949 1.76346C1.60596 1.67691 1.67691 1.60596 1.76346 1.55949C1.78152 1.55378 1.84867 1.53628 2.0086 1.523C2.27105 1.50121 2.62533 1.5 3.22 1.5ZM3.22 14.5C2.62533 14.5 2.27105 14.4988 2.0086 14.477C1.84867 14.4637 1.78152 14.4462 1.76346 14.4405C1.67691 14.394 1.60596 14.3231 1.55949 14.2365C1.55378 14.2185 1.53628 14.1513 1.523 13.9914C1.50391 13.7615 1.50061 13.4611 1.50009 12.9903C1.50527 12.7186 1.7271 12.5 2 12.5H14.5L14.5 14.5H3.22Z", "docs": "M12 5.75H4V4.25H12V5.75Z M4 8.75H10V7.25H4V8.75Z M5.35241e-05 12.9852L0 3.22C0 2.08102 0 1.51153 0.225173 1.07805C0.414924 0.712765 0.712765 0.414924 1.07805 0.225173C1.51153 0 2.08102 0 3.22 0H16V16H2C1.46143 16 0.972588 15.7871 0.613012 15.4409C0.587144 15.416 0.56195 15.3904 0.537457 15.3642C0.204038 15.0069 0 14.5273 0 14V13L5.35241e-05 12.9852ZM3.22 1.5H14.5V11H2C1.82735 11 1.65981 11.0219 1.5 11.063V3.22C1.5 2.62533 1.50121 2.27105 1.523 2.0086C1.53628 1.84867 1.55378 1.78152 1.55949 1.76346C1.60596 1.67691 1.67691 1.60596 1.76346 1.55949C1.78152 1.55378 1.84867 1.53628 2.0086 1.523C2.27105 1.50121 2.62533 1.5 3.22 1.5ZM3.22 14.5C2.62533 14.5 2.27105 14.4988 2.0086 14.477C1.84867 14.4637 1.78152 14.4462 1.76346 14.4405C1.67691 14.394 1.60596 14.3231 1.55949 14.2365C1.55378 14.2185 1.53628 14.1513 1.523 13.9914C1.50391 13.7615 1.50061 13.4611 1.50009 12.9903C1.50527 12.7186 1.7271 12.5 2 12.5H14.5L14.5 14.5H3.22Z",

View File

@ -1,6 +1,6 @@
.text-input { .text-input {
padding: 4px 10px; padding: 4px 10px;
color: var(--clr-scale-ntrl-0); color: var(--clr-text-1);
background-color: var(--clr-bg-1); background-color: var(--clr-bg-1);
border: 1px solid var(--clr-border-2); border: 1px solid var(--clr-border-2);
border-radius: var(--radius-s); border-radius: var(--radius-s);

View File

@ -1,4 +1,8 @@
@layer sharable { @layer sharable {
.markdown {
user-select: text;
}
.markdown p:last-child, .markdown p:last-child,
.markdown ul:last-child, .markdown ul:last-child,
.markdown ol:last-child, .markdown ol:last-child,
@ -65,10 +69,11 @@
} }
.markdown code { .markdown code {
font-family: monospace; font-family: var(--mono-font-family);
background-color: var(--clr-scale-ntrl-90); background-color: var(--clr-scale-ntrl-90);
border: 1px solid var(--clr-scale-ntrl-70); border: 1px solid var(--clr-scale-ntrl-70);
padding: 0 0.5em; padding: 1px 3px;
border-radius: var(--radius-s);
} }
.markdown a { .markdown a {