mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-11-30 01:17:37 +03:00
Update text areas (#5297)
* reedit "borderlessTextArea" to "Textarea" * replace old textareas * Delete TextArea.svelte * move `TextBox` to UI * mock the keyboard event * migrate `Textbox` to svelte 5 * Update PrDetailsModal.svelte * fix event * fix text formatting * Update DialogBubble.svelte * codereview fixes
This commit is contained in:
parent
479973b2fb
commit
d3a0f3108b
@ -7,12 +7,12 @@
|
||||
import ContextMenuSection from '$lib/components/contextmenu/ContextMenuSection.svelte';
|
||||
import { projectAiGenEnabled } from '$lib/config/config';
|
||||
import { stackingFeature } from '$lib/config/uiFeatureFlags';
|
||||
import TextBox from '$lib/shared/TextBox.svelte';
|
||||
import { BranchController } from '$lib/vbranches/branchController';
|
||||
import { VirtualBranch } from '$lib/vbranches/types';
|
||||
import { getContext, getContextStore } from '@gitbutler/shared/context';
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
import Modal from '@gitbutler/ui/Modal.svelte';
|
||||
import Textbox from '@gitbutler/ui/Textbox.svelte';
|
||||
import Toggle from '@gitbutler/ui/Toggle.svelte';
|
||||
import Tooltip from '@gitbutler/ui/Tooltip.svelte';
|
||||
|
||||
@ -220,7 +220,7 @@
|
||||
close();
|
||||
}}
|
||||
>
|
||||
<TextBox label="Remote branch name" id="newRemoteName" bind:value={newRemoteName} focus />
|
||||
<Textbox label="Remote branch name" id="newRemoteName" bind:value={newRemoteName} autofocus />
|
||||
|
||||
{#snippet controls(close)}
|
||||
<Button style="ghost" outline type="reset" onclick={close}>Cancel</Button>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import TextBox from '../shared/TextBox.svelte';
|
||||
import { showError } from '$lib/notifications/toasts';
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
import Textbox from '@gitbutler/ui/Textbox.svelte';
|
||||
import type { SystemPromptHandle } from '$lib/backend/prompt';
|
||||
|
||||
export let prompt: SystemPromptHandle | undefined;
|
||||
@ -26,12 +26,12 @@
|
||||
<span class="text-body-11 text-body passbox__helper-text">
|
||||
{prompt?.prompt}
|
||||
</span>
|
||||
<TextBox
|
||||
focus
|
||||
<Textbox
|
||||
autofocus
|
||||
type="password"
|
||||
bind:value
|
||||
on:keydown={(e) => {
|
||||
if (e.detail.key === 'Enter') submit();
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter') submit();
|
||||
}}
|
||||
/>
|
||||
<div class="passbox__actions">
|
||||
|
@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import TextBox from '$lib/shared/TextBox.svelte';
|
||||
import { error } from '$lib/utils/toasts';
|
||||
import { BranchController } from '$lib/vbranches/branchController';
|
||||
import { VirtualBranch } from '$lib/vbranches/types';
|
||||
import { getContext, getContextStore } from '@gitbutler/shared/context';
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
import Modal from '@gitbutler/ui/Modal.svelte';
|
||||
import Textbox from '@gitbutler/ui/Textbox.svelte';
|
||||
import { slugify } from '@gitbutler/ui/utils/string';
|
||||
|
||||
interface Props {
|
||||
@ -50,11 +50,11 @@
|
||||
onClose={onModalClose}
|
||||
>
|
||||
{#snippet children()}
|
||||
<TextBox
|
||||
<Textbox
|
||||
label="Branch name"
|
||||
id="newRemoteName"
|
||||
bind:value={createRefName}
|
||||
focus
|
||||
autofocus
|
||||
helperText={generatedNameDiverges ? `Will be created as '${slugifiedRefName}'` : undefined}
|
||||
/>
|
||||
|
||||
|
@ -5,12 +5,12 @@
|
||||
import ContextMenuItem from '$lib/components/contextmenu/ContextMenuItem.svelte';
|
||||
import ContextMenuSection from '$lib/components/contextmenu/ContextMenuSection.svelte';
|
||||
import { projectAiGenEnabled } from '$lib/config/config';
|
||||
import TextBox from '$lib/shared/TextBox.svelte';
|
||||
import { BranchController } from '$lib/vbranches/branchController';
|
||||
import { VirtualBranch } from '$lib/vbranches/types';
|
||||
import { getContext, getContextStore } from '@gitbutler/shared/context';
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
import Modal from '@gitbutler/ui/Modal.svelte';
|
||||
import Textbox from '@gitbutler/ui/Textbox.svelte';
|
||||
|
||||
interface Props {
|
||||
contextMenuEl?: ReturnType<typeof ContextMenu>;
|
||||
@ -140,7 +140,7 @@
|
||||
close();
|
||||
}}
|
||||
>
|
||||
<TextBox placeholder="New name" id="newSeriesName" bind:value={newHeadName} focus />
|
||||
<Textbox placeholder="New name" id="newSeriesName" bind:value={newHeadName} autofocus />
|
||||
|
||||
{#if hasGitHostBranch}
|
||||
<div class="text-12 text-light helper-text">
|
||||
|
@ -1,21 +0,0 @@
|
||||
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('pointerdown', onClick, true);
|
||||
document.addEventListener('contextmenu', onClick, true);
|
||||
return {
|
||||
destroy() {
|
||||
document.removeEventListener('pointerdown', onClick, true);
|
||||
document.removeEventListener('contextmenu', onClick, true);
|
||||
}
|
||||
};
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { MessageRole, type UserPrompt } from '$lib/ai/types';
|
||||
import DialogBubble from '$lib/components/AIPromptEdit/DialogBubble.svelte';
|
||||
import TextBox from '$lib/shared/TextBox.svelte';
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
import Icon from '@gitbutler/ui/Icon.svelte';
|
||||
import Textbox from '@gitbutler/ui/Textbox.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let displayMode: 'readOnly' | 'writable' = 'writable';
|
||||
@ -112,7 +112,7 @@
|
||||
<Icon name={expanded ? 'chevron-up' : 'chevron-down'} />
|
||||
</div>
|
||||
{:else}
|
||||
<TextBox bind:value={promptName} wide on:click={(e) => e.stopPropagation()} />
|
||||
<Textbox bind:value={promptName} wide onclick={(e) => e.stopPropagation()} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@ -120,7 +120,8 @@
|
||||
<div class="content" class:default-mode={prompt.id === 'default'} class:editing={isInEditing}>
|
||||
{#each promptMessages as promptMessage, index}
|
||||
<DialogBubble
|
||||
bind:promptMessage
|
||||
bind:promptMessage={promptMessage.content}
|
||||
role={promptMessage.role}
|
||||
editing={isInEditing}
|
||||
isLast={index + 1 === promptMessages.length || promptMessages.length === 1}
|
||||
disableRemove={promptMessages.length === 1}
|
||||
|
@ -3,48 +3,43 @@
|
||||
import Markdown from '$lib/components/Markdown.svelte';
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
import Icon from '@gitbutler/ui/Icon.svelte';
|
||||
import { autoHeight } from '@gitbutler/ui/utils/autoHeight';
|
||||
import Textarea from '@gitbutler/ui/Textarea.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let disableRemove = false;
|
||||
export let isError = false;
|
||||
export let isLast = false;
|
||||
export let autofocus = false;
|
||||
export let editing = false;
|
||||
export let promptMessage: { role: MessageRole; content: string };
|
||||
interface Props {
|
||||
role: MessageRole;
|
||||
disableRemove?: boolean;
|
||||
isError?: boolean;
|
||||
isLast?: boolean;
|
||||
editing?: boolean;
|
||||
promptMessage: string;
|
||||
}
|
||||
|
||||
let {
|
||||
role,
|
||||
disableRemove = false,
|
||||
isError = false,
|
||||
isLast = false,
|
||||
editing = false,
|
||||
promptMessage = $bindable()
|
||||
}: Props = $props();
|
||||
|
||||
const dispatcher = createEventDispatcher<{
|
||||
removeLastExample: void;
|
||||
addExample: void;
|
||||
input: string;
|
||||
}>();
|
||||
let textareaElement: HTMLTextAreaElement | undefined;
|
||||
|
||||
function focusTextareaOnMount(
|
||||
textareaElement: HTMLTextAreaElement | undefined,
|
||||
autofocus: boolean,
|
||||
editing: boolean
|
||||
) {
|
||||
if (textareaElement && autofocus && editing) {
|
||||
textareaElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
textareaElement.focus();
|
||||
}
|
||||
}
|
||||
|
||||
$: focusTextareaOnMount(textareaElement, autofocus, editing);
|
||||
|
||||
$: if (textareaElement) autoHeight(textareaElement);
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="bubble-wrap"
|
||||
class:editing
|
||||
class:bubble-wrap_user={promptMessage.role === MessageRole.User}
|
||||
class:bubble-wrap_assistant={promptMessage.role === MessageRole.Assistant}
|
||||
class:bubble-wrap_user={role === MessageRole.User}
|
||||
class:bubble-wrap_assistant={role === MessageRole.Assistant}
|
||||
>
|
||||
<div class="bubble">
|
||||
<div class="bubble__header text-13 text-bold">
|
||||
{#if promptMessage.role === MessageRole.User}
|
||||
{#if role === MessageRole.User}
|
||||
<Icon name="profile" />
|
||||
<span>User</span>
|
||||
{:else}
|
||||
@ -54,24 +49,19 @@
|
||||
</div>
|
||||
|
||||
{#if editing}
|
||||
<textarea
|
||||
bind:this={textareaElement}
|
||||
bind:value={promptMessage.content}
|
||||
class="textarea scrollbar text-13 text-body"
|
||||
class:is-error={isError}
|
||||
rows={1}
|
||||
on:input={(e) => {
|
||||
autoHeight(e.currentTarget);
|
||||
|
||||
dispatcher('input', e.currentTarget.value);
|
||||
<div class="textarea" class:is-error={isError}>
|
||||
<Textarea
|
||||
unstyled
|
||||
bind:value={promptMessage}
|
||||
oninput={(e: Event) => {
|
||||
const target = e.currentTarget as HTMLTextAreaElement;
|
||||
dispatcher('input', target.value);
|
||||
}}
|
||||
on:change={(e) => {
|
||||
autoHeight(e.currentTarget);
|
||||
}}
|
||||
></textarea>
|
||||
></Textarea>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="bubble-message scrollbar text-13 text-body">
|
||||
<Markdown content={promptMessage.content} />
|
||||
<Markdown content={promptMessage} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@ -166,11 +156,6 @@
|
||||
|
||||
.textarea {
|
||||
width: 100%;
|
||||
resize: none;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 12px;
|
||||
background-color: var(--clr-bg-1);
|
||||
border: 1px solid var(--clr-border-2);
|
||||
border-radius: 0 0 var(--radius-l) var(--radius-l);
|
||||
|
@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import TextBox from '../shared/TextBox.svelte';
|
||||
import { PromptService } from '$lib/backend/prompt';
|
||||
import { getContext } from '@gitbutler/shared/context';
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
import Modal from '@gitbutler/ui/Modal.svelte';
|
||||
import Textbox from '@gitbutler/ui/Textbox.svelte';
|
||||
|
||||
const promptService = getContext(PromptService);
|
||||
const [prompt, error] = promptService.reactToPrompt({ timeoutMs: 30000 });
|
||||
@ -64,7 +64,7 @@
|
||||
<code>{$prompt?.prompt}</code>
|
||||
{/if}
|
||||
</div>
|
||||
<TextBox focus type="password" bind:value disabled={!!$error || loading} />
|
||||
<Textbox autofocus type="password" bind:value disabled={!!$error || loading} />
|
||||
|
||||
{#snippet controls()}
|
||||
<Button style="ghost" type="reset" outline disabled={loading} onclick={cancel}>Cancel</Button>
|
||||
|
@ -5,7 +5,6 @@
|
||||
import Markdown from '$lib/components/Markdown.svelte';
|
||||
import { RemotesService } from '$lib/remotes/service';
|
||||
import Link from '$lib/shared/Link.svelte';
|
||||
import TextBox from '$lib/shared/TextBox.svelte';
|
||||
import * as toasts from '$lib/utils/toasts';
|
||||
import { remoteUrlIsHttp } from '$lib/utils/url';
|
||||
import { BranchController } from '$lib/vbranches/branchController';
|
||||
@ -13,6 +12,7 @@
|
||||
import { getContext } from '@gitbutler/shared/context';
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
import Modal from '@gitbutler/ui/Modal.svelte';
|
||||
import Textbox from '@gitbutler/ui/Textbox.svelte';
|
||||
import { get } from 'svelte/store';
|
||||
import type { PullRequest } from '$lib/gitHost/interface/types';
|
||||
import { goto } from '$app/navigation';
|
||||
@ -85,7 +85,7 @@
|
||||
<p class="text-15 fork-notice">
|
||||
In order to apply a branch from a fork, GitButler must first add a remote.
|
||||
</p>
|
||||
<TextBox label="Choose a remote name" bind:value={remoteName}></TextBox>
|
||||
<Textbox label="Choose a remote name" bind:value={remoteName}></Textbox>
|
||||
{#snippet controls(close)}
|
||||
<Button style="ghost" outline onclick={() => closeModal(close)}>Cancel</Button>
|
||||
<Button style="pop" kind="solid" type="submit" grow {loading}>Confirm</Button>
|
||||
|
@ -1,6 +1,4 @@
|
||||
<script lang="ts">
|
||||
import TextArea from '../shared/TextArea.svelte';
|
||||
import TextBox from '../shared/TextBox.svelte';
|
||||
import { invoke, listen } from '$lib/backend/ipc';
|
||||
import * as zip from '$lib/backend/zip';
|
||||
import { User } from '$lib/stores/user';
|
||||
@ -10,6 +8,8 @@
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
import Checkbox from '@gitbutler/ui/Checkbox.svelte';
|
||||
import Modal from '@gitbutler/ui/Modal.svelte';
|
||||
import Textarea from '@gitbutler/ui/Textarea.svelte';
|
||||
import Textbox from '@gitbutler/ui/Textbox.svelte';
|
||||
import { getVersion } from '@tauri-apps/api/app';
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
@ -166,7 +166,7 @@
|
||||
</p>
|
||||
|
||||
{#if !$user}
|
||||
<TextBox
|
||||
<Textbox
|
||||
label="Email"
|
||||
placeholder="Provide an email so that we can get back to you"
|
||||
type="email"
|
||||
@ -175,19 +175,19 @@
|
||||
autocomplete={false}
|
||||
autocorrect={false}
|
||||
spellcheck
|
||||
focus
|
||||
autofocus
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<TextArea
|
||||
<Textarea
|
||||
label="Comments"
|
||||
placeholder="Provide any steps necessary to reproduce the problem."
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
spellcheck
|
||||
id="comments"
|
||||
rows={6}
|
||||
maxHeight={400}
|
||||
minRows={6}
|
||||
maxRows={10}
|
||||
bind:value={messageInputValue}
|
||||
/>
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { clickOutside } from '$lib/clickOutside';
|
||||
import { createKeybind } from '$lib/utils/hotkeys';
|
||||
import { clickOutside } from '@gitbutler/ui/utils/clickOutside';
|
||||
import { focusTrap } from '@gitbutler/ui/utils/focusTrap';
|
||||
import { portal } from '@gitbutler/ui/utils/portal';
|
||||
import { type Snippet } from 'svelte';
|
||||
|
@ -3,7 +3,6 @@
|
||||
import FileListItemSmart from './FileListItem.svelte';
|
||||
import { conflictEntryHint } from '$lib/conflictEntryPresence';
|
||||
import LazyloadContainer from '$lib/shared/LazyloadContainer.svelte';
|
||||
import TextBox from '$lib/shared/TextBox.svelte';
|
||||
import { chunk } from '$lib/utils/array';
|
||||
import { copyToClipboard } from '$lib/utils/clipboard';
|
||||
import { KeyName } from '$lib/utils/hotkeys';
|
||||
@ -15,6 +14,7 @@
|
||||
import { SelectedOwnership, updateOwnership } from '$lib/vbranches/ownership';
|
||||
import { getContext, maybeGetContextStore } from '@gitbutler/shared/context';
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
import Textbox from '@gitbutler/ui/Textbox.svelte';
|
||||
import FileListItem from '@gitbutler/ui/file/FileListItem.svelte';
|
||||
import type { AnyFile, ConflictEntries } from '$lib/vbranches/types';
|
||||
import type { Writable } from 'svelte/store';
|
||||
@ -115,7 +115,7 @@
|
||||
GitHub, or run the following command in your project directory:
|
||||
</p>
|
||||
<div class="command">
|
||||
<TextBox value={MERGE_DIFF_COMMAND + $commit.id.slice(0, 7)} wide readonly />
|
||||
<Textbox value={MERGE_DIFF_COMMAND + $commit.id.slice(0, 7)} wide readonly />
|
||||
<Button
|
||||
icon="copy"
|
||||
style="ghost"
|
||||
|
@ -3,7 +3,6 @@
|
||||
import LazyloadContainer from '../shared/LazyloadContainer.svelte';
|
||||
import emptyFolderSvg from '$lib/assets/empty-state/empty-folder.svg?raw';
|
||||
import { Project } from '$lib/backend/projects';
|
||||
import { clickOutside } from '$lib/clickOutside';
|
||||
import FileCard from '$lib/file/FileCard.svelte';
|
||||
import SnapshotCard from '$lib/history/SnapshotCard.svelte';
|
||||
import { HistoryService, createdOnDay } from '$lib/history/history';
|
||||
@ -13,6 +12,7 @@
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
import EmptyStatePlaceholder from '@gitbutler/ui/EmptyStatePlaceholder.svelte';
|
||||
import Icon from '@gitbutler/ui/Icon.svelte';
|
||||
import { clickOutside } from '@gitbutler/ui/utils/clickOutside';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type { Snapshot, SnapshotDiff } from '$lib/history/types';
|
||||
|
@ -4,11 +4,11 @@
|
||||
import Section from '$lib/settings/Section.svelte';
|
||||
import InfoMessage, { type MessageStyle } from '$lib/shared/InfoMessage.svelte';
|
||||
import Spacer from '$lib/shared/Spacer.svelte';
|
||||
import TextBox from '$lib/shared/TextBox.svelte';
|
||||
import { parseRemoteUrl } from '$lib/url/gitUrl';
|
||||
import { getContext } from '@gitbutler/shared/context';
|
||||
import { persisted } from '@gitbutler/shared/persisted';
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
import Textbox from '@gitbutler/ui/Textbox.svelte';
|
||||
import * as Sentry from '@sentry/sveltekit';
|
||||
import { open } from '@tauri-apps/api/dialog';
|
||||
import { documentDir } from '@tauri-apps/api/path';
|
||||
@ -99,11 +99,11 @@
|
||||
<Section>
|
||||
<div class="clone__field repositoryUrl">
|
||||
<div class="text-13 text-semibold clone__field--label">Clone URL</div>
|
||||
<TextBox bind:value={repositoryUrl} />
|
||||
<Textbox bind:value={repositoryUrl} />
|
||||
</div>
|
||||
<div class="clone__field repositoryTargetPath">
|
||||
<div class="text-13 text-semibold clone__field--label">Where to clone</div>
|
||||
<TextBox bind:value={targetDirPath} placeholder={'/Users/tipsy/Documents'} />
|
||||
<Textbox bind:value={targetDirPath} placeholder={'/Users/tipsy/Documents'} />
|
||||
<Button style="ghost" outline kind="solid" disabled={loading} onclick={handleCloneTargetSelect}>
|
||||
Choose..
|
||||
</Button>
|
||||
|
@ -26,7 +26,6 @@
|
||||
import { isFailure } from '$lib/result';
|
||||
import ScrollableContainer from '$lib/scroll/ScrollableContainer.svelte';
|
||||
import DropDownButton from '$lib/shared/DropDownButton.svelte';
|
||||
import TextBox from '$lib/shared/TextBox.svelte';
|
||||
import { getBranchNameFromRef } from '$lib/utils/branch';
|
||||
import { KeyName, onMetaEnter } from '$lib/utils/hotkeys';
|
||||
import { sleep } from '$lib/utils/sleep';
|
||||
@ -35,9 +34,10 @@
|
||||
import { BranchController } from '$lib/vbranches/branchController';
|
||||
import { PatchSeries, VirtualBranch } from '$lib/vbranches/types';
|
||||
import { getContext, getContextStore } from '@gitbutler/shared/context';
|
||||
import BorderlessTextarea from '@gitbutler/ui/BorderlessTextarea.svelte';
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
import Modal from '@gitbutler/ui/Modal.svelte';
|
||||
import Textarea from '@gitbutler/ui/Textarea.svelte';
|
||||
import Textbox from '@gitbutler/ui/Textbox.svelte';
|
||||
import ToggleButton from '@gitbutler/ui/ToggleButton.svelte';
|
||||
import { tick } from 'svelte';
|
||||
import type { DetailedPullRequest, PullRequest } from '$lib/gitHost/interface/types';
|
||||
@ -345,12 +345,12 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class="pr-fields">
|
||||
<TextBox
|
||||
<Textbox
|
||||
placeholder="PR title"
|
||||
value={actualTitle}
|
||||
readonly={!isEditing || isDisplay}
|
||||
on:input={(e) => {
|
||||
inputTitle = e.detail;
|
||||
oninput={(value: string) => {
|
||||
inputTitle = value;
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -380,14 +380,15 @@
|
||||
|
||||
<!-- DESCRIPTION FIELD -->
|
||||
<div class="pr-description-field text-input">
|
||||
<BorderlessTextarea
|
||||
<Textarea
|
||||
unstyled
|
||||
value={actualBody}
|
||||
rows={2}
|
||||
minRows={4}
|
||||
autofocus
|
||||
padding={{ top: 12, right: 12, bottom: 12, left: 12 }}
|
||||
placeholder="Add description…"
|
||||
oninput={(e: InputEvent) => {
|
||||
const target = e.target as HTMLTextAreaElement;
|
||||
const target = e.currentTarget as HTMLTextAreaElement;
|
||||
inputBody = target.value;
|
||||
}}
|
||||
/>
|
||||
@ -395,14 +396,15 @@
|
||||
<!-- AI GENRATION -->
|
||||
<div class="pr-ai" class:show-ai-box={showAiBox}>
|
||||
{#if showAiBox}
|
||||
<BorderlessTextarea
|
||||
<Textarea
|
||||
unstyled
|
||||
autofocus
|
||||
bind:value={aiDescriptionDirective}
|
||||
padding={{ top: 12, right: 12, bottom: 0, left: 12 }}
|
||||
placeholder={aiService.prSummaryMainDirective}
|
||||
onkeydown={onMetaEnter(handleAIButtonPressed)}
|
||||
oninput={(e: InputEvent) => {
|
||||
const target = e.target as HTMLTextAreaElement;
|
||||
const target = e.currentTarget as HTMLTextAreaElement;
|
||||
aiDescriptionDirective = target.value;
|
||||
}}
|
||||
/>
|
||||
|
@ -11,9 +11,9 @@
|
||||
<script lang="ts" generics="T extends string">
|
||||
import OptionsGroup from './OptionsGroup.svelte';
|
||||
import SearchItem from './SearchItem.svelte';
|
||||
import TextBox from '../shared/TextBox.svelte';
|
||||
import ScrollableContainer from '$lib/scroll/ScrollableContainer.svelte';
|
||||
import { KeyName } from '$lib/utils/hotkeys';
|
||||
import Textbox from '@gitbutler/ui/Textbox.svelte';
|
||||
import { portal } from '@gitbutler/ui/utils/portal';
|
||||
import { resizeObserver } from '@gitbutler/ui/utils/resizeObserver';
|
||||
import { type Snippet } from 'svelte';
|
||||
@ -128,14 +128,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDown(e: CustomEvent<KeyboardEvent>) {
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
if (!listOpen) {
|
||||
return;
|
||||
}
|
||||
e.detail.stopPropagation();
|
||||
e.detail.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
const { key } = e.detail;
|
||||
const { key } = e;
|
||||
|
||||
switch (key) {
|
||||
case KeyName.Escape:
|
||||
@ -158,7 +158,7 @@
|
||||
{#if label}
|
||||
<label for={id} class="select__label text-13 text-body text-semibold">{label}</label>
|
||||
{/if}
|
||||
<TextBox
|
||||
<Textbox
|
||||
{id}
|
||||
{placeholder}
|
||||
noselect
|
||||
@ -168,8 +168,8 @@
|
||||
icon="select-chevron"
|
||||
value={options.find((item) => item.value === value)?.label}
|
||||
disabled={disabled || loading}
|
||||
on:mousedown={toggleList}
|
||||
on:keydown={(ev) => handleKeyDown(ev)}
|
||||
onmousedown={toggleList}
|
||||
onkeydown={(ev) => handleKeyDown(ev)}
|
||||
/>
|
||||
{#if listOpen}
|
||||
<div
|
||||
|
@ -8,8 +8,8 @@
|
||||
import Link from '$lib/shared/Link.svelte';
|
||||
import ProjectNameLabel from '$lib/shared/ProjectNameLabel.svelte';
|
||||
import RadioButton from '$lib/shared/RadioButton.svelte';
|
||||
import TextBox from '$lib/shared/TextBox.svelte';
|
||||
import { getContext, getContextStore } from '@gitbutler/shared/context';
|
||||
import Textbox from '@gitbutler/ui/Textbox.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
const project = getContext(Project);
|
||||
@ -125,7 +125,7 @@
|
||||
{#if selectedType === 'local'}
|
||||
<SectionCard topDivider roundedTop={false} roundedBottom={false} orientation="row">
|
||||
<div class="inputs-group">
|
||||
<TextBox
|
||||
<Textbox
|
||||
label="Path to private key"
|
||||
placeholder="for example: ~/.ssh/id_rsa"
|
||||
bind:value={privateKeyPath}
|
||||
|
@ -8,9 +8,9 @@
|
||||
import Section from '$lib/settings/Section.svelte';
|
||||
import InfoMessage from '$lib/shared/InfoMessage.svelte';
|
||||
import Link from '$lib/shared/Link.svelte';
|
||||
import TextBox from '$lib/shared/TextBox.svelte';
|
||||
import { getContext } from '@gitbutler/shared/context';
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
import Textbox from '@gitbutler/ui/Textbox.svelte';
|
||||
import Toggle from '@gitbutler/ui/Toggle.svelte';
|
||||
import { invoke } from '@tauri-apps/api/tauri';
|
||||
import { onMount } from 'svelte';
|
||||
@ -131,18 +131,18 @@
|
||||
{/snippet}
|
||||
</Select>
|
||||
|
||||
<TextBox
|
||||
<Textbox
|
||||
label="Signing key"
|
||||
bind:value={signingKey}
|
||||
required
|
||||
on:change={updateSigningInfo}
|
||||
onchange={updateSigningInfo}
|
||||
placeholder="ex: /Users/bob/.ssh/id_rsa.pub"
|
||||
/>
|
||||
|
||||
<TextBox
|
||||
<Textbox
|
||||
label="Signing program (optional)"
|
||||
bind:value={signingProgram}
|
||||
on:change={updateSigningInfo}
|
||||
onchange={updateSigningInfo}
|
||||
placeholder="ex: /Applications/1Password.app/Contents/MacOS/op-ssh-sign"
|
||||
/>
|
||||
|
||||
|
@ -4,11 +4,11 @@
|
||||
import Section from '$lib/settings/Section.svelte';
|
||||
import Link from '$lib/shared/Link.svelte';
|
||||
import Spacer from '$lib/shared/Spacer.svelte';
|
||||
import TextArea from '$lib/shared/TextArea.svelte';
|
||||
import TextBox from '$lib/shared/TextBox.svelte';
|
||||
import { User } from '$lib/stores/user';
|
||||
import * as toasts from '$lib/utils/toasts';
|
||||
import { getContext, getContextStore } from '@gitbutler/shared/context';
|
||||
import Textarea from '@gitbutler/ui/Textarea.svelte';
|
||||
import Textbox from '@gitbutler/ui/Textbox.svelte';
|
||||
import Toggle from '@gitbutler/ui/Toggle.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { PUBLIC_API_BASE_URL } from '$env/static/public';
|
||||
@ -69,25 +69,26 @@
|
||||
<SectionCard>
|
||||
<form>
|
||||
<fieldset class="fields-wrapper">
|
||||
<TextBox label="Project path" readonly id="path" value={project?.path} />
|
||||
<Textbox label="Project path" readonly id="path" value={project?.path} />
|
||||
<section class="description-wrapper">
|
||||
<TextBox
|
||||
<Textbox
|
||||
label="Project name"
|
||||
id="name"
|
||||
placeholder="Project name can't be empty"
|
||||
bind:value={title}
|
||||
required
|
||||
on:change={(e) => {
|
||||
project.title = e.detail;
|
||||
onchange={(value: string) => {
|
||||
project.title = value;
|
||||
projectsService.updateProject(project);
|
||||
}}
|
||||
/>
|
||||
<TextArea
|
||||
<Textarea
|
||||
id="description"
|
||||
rows={3}
|
||||
minRows={3}
|
||||
maxRows={6}
|
||||
placeholder="Project description"
|
||||
bind:value={description}
|
||||
on:change={() => {
|
||||
onchange={() => {
|
||||
project.description = description;
|
||||
projectsService.updateProject(project);
|
||||
}}
|
||||
|
@ -3,8 +3,8 @@
|
||||
import SectionCard from '$lib/components/SectionCard.svelte';
|
||||
import { projectRunCommitHooks } from '$lib/config/config';
|
||||
import Section from '$lib/settings/Section.svelte';
|
||||
import TextBox from '$lib/shared/TextBox.svelte';
|
||||
import { getContext } from '@gitbutler/shared/context';
|
||||
import Textbox from '@gitbutler/ui/Textbox.svelte';
|
||||
import Toggle from '@gitbutler/ui/Toggle.svelte';
|
||||
|
||||
const projectsService = getContext(ProjectsService);
|
||||
@ -72,7 +72,7 @@
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<TextBox
|
||||
<Textbox
|
||||
type="number"
|
||||
width={100}
|
||||
textAlign="center"
|
||||
@ -80,8 +80,8 @@
|
||||
minVal={5}
|
||||
maxVal={1000}
|
||||
showCountActions
|
||||
on:change={(e) => {
|
||||
setSnapshotLinesThreshold(parseInt(e.detail));
|
||||
onchange={(value: string) => {
|
||||
setSnapshotLinesThreshold(parseInt(value));
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
|
@ -1,79 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { autoHeight } from '@gitbutler/ui/utils/autoHeight';
|
||||
import { pxToRem } from '@gitbutler/ui/utils/pxToRem';
|
||||
import { resizeObserver } from '@gitbutler/ui/utils/resizeObserver';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let value: string | undefined;
|
||||
export let placeholder: string | undefined = undefined;
|
||||
export let required = false;
|
||||
export let rows = 4;
|
||||
export let maxHeight: number | undefined = undefined;
|
||||
export let id: string | undefined = undefined;
|
||||
export let disabled = false;
|
||||
export let autocomplete: string | undefined = undefined;
|
||||
export let autocorrect: string | undefined = undefined;
|
||||
export let spellcheck = false;
|
||||
export let label: string | undefined = undefined;
|
||||
|
||||
const dispatch = createEventDispatcher<{ input: string; change: string }>();
|
||||
|
||||
let textareaElement: HTMLTextAreaElement;
|
||||
</script>
|
||||
|
||||
<div class="textarea-wrapper">
|
||||
{#if label}
|
||||
<label class="textbox__label text-13 text-semibold" for={id}>
|
||||
{label}
|
||||
</label>
|
||||
{/if}
|
||||
<textarea
|
||||
bind:this={textareaElement}
|
||||
class="text-input text-13 text-body textarea scrollbar"
|
||||
bind:value
|
||||
{disabled}
|
||||
{id}
|
||||
name={id}
|
||||
{placeholder}
|
||||
{required}
|
||||
{rows}
|
||||
{autocomplete}
|
||||
{autocorrect}
|
||||
{spellcheck}
|
||||
on:input={(e) => {
|
||||
dispatch('input', e.currentTarget.value);
|
||||
autoHeight(e.currentTarget);
|
||||
}}
|
||||
on:change={(e) => {
|
||||
dispatch('change', e.currentTarget.value);
|
||||
autoHeight(e.currentTarget);
|
||||
}}
|
||||
use:resizeObserver={(e) => {
|
||||
autoHeight(e.currentTarget as HTMLTextAreaElement);
|
||||
}}
|
||||
on:focus={(e) => autoHeight(e.currentTarget)}
|
||||
style:max-height={maxHeight ? pxToRem(maxHeight) : undefined}
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.textarea-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.textarea {
|
||||
width: 100%;
|
||||
resize: none;
|
||||
padding: 12px;
|
||||
|
||||
&::-webkit-resizer {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.textbox__label {
|
||||
color: var(--clr-scale-ntrl-50);
|
||||
}
|
||||
</style>
|
@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { getGitHost } from '$lib/gitHost/interface/gitHost';
|
||||
import TextArea from '$lib/shared/TextArea.svelte';
|
||||
import TextBox from '$lib/shared/TextBox.svelte';
|
||||
import { TopicService, type Topic } from '$lib/topics/service';
|
||||
import { createKeybind } from '$lib/utils/hotkeys';
|
||||
import { getContext } from '@gitbutler/shared/context';
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
import Modal from '@gitbutler/ui/Modal.svelte';
|
||||
import Textarea from '@gitbutler/ui/Textarea.svelte';
|
||||
import Textbox from '@gitbutler/ui/Textbox.svelte';
|
||||
|
||||
interface Props {
|
||||
registerKeypress?: boolean;
|
||||
@ -80,12 +80,12 @@
|
||||
|
||||
<div class="input">
|
||||
<p class="text-14 label">Title</p>
|
||||
<TextBox bind:value={title} />
|
||||
<Textbox bind:value={title} />
|
||||
</div>
|
||||
|
||||
<div class="input">
|
||||
<p class="text-14 label">Body</p>
|
||||
<TextArea bind:value={body} />
|
||||
<Textarea minRows={4} bind:value={body} />
|
||||
</div>
|
||||
|
||||
<div class="labels">
|
||||
|
@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import TextArea from '$lib/shared/TextArea.svelte';
|
||||
import TextBox from '$lib/shared/TextBox.svelte';
|
||||
import { TopicService, type Topic } from '$lib/topics/service';
|
||||
import { createKeybind } from '$lib/utils/hotkeys';
|
||||
import { getContext } from '@gitbutler/shared/context';
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
import Icon from '@gitbutler/ui/Icon.svelte';
|
||||
import Modal from '@gitbutler/ui/Modal.svelte';
|
||||
import Textarea from '@gitbutler/ui/Textarea.svelte';
|
||||
import Textbox from '@gitbutler/ui/Textbox.svelte';
|
||||
|
||||
interface Props {
|
||||
registerKeypress?: boolean;
|
||||
@ -67,7 +67,7 @@
|
||||
|
||||
<div class="input">
|
||||
<p class="text-14 label">Title</p>
|
||||
<TextBox bind:value={title} />
|
||||
<Textbox bind:value={title} />
|
||||
</div>
|
||||
|
||||
<div class="details">
|
||||
@ -86,7 +86,7 @@
|
||||
<div class="details__expanded" class:hidden={!detailsExpanded}>
|
||||
<div class="input">
|
||||
<p class="text-14 label">Body</p>
|
||||
<TextArea bind:value={body} />
|
||||
<Textarea bind:value={body} minRows={4} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -13,9 +13,9 @@
|
||||
import InfoMessage from '$lib/shared/InfoMessage.svelte';
|
||||
import RadioButton from '$lib/shared/RadioButton.svelte';
|
||||
import Spacer from '$lib/shared/Spacer.svelte';
|
||||
import TextBox from '$lib/shared/TextBox.svelte';
|
||||
import { UserService } from '$lib/stores/user';
|
||||
import { getContext } from '@gitbutler/shared/context';
|
||||
import Textbox from '@gitbutler/ui/Textbox.svelte';
|
||||
import { onMount, tick } from 'svelte';
|
||||
|
||||
const gitConfigService = getContext(GitConfigService);
|
||||
@ -196,7 +196,7 @@
|
||||
{/if}
|
||||
|
||||
{#if openAIKeyOption === KeyOption.BringYourOwn}
|
||||
<TextBox label="API key" bind:value={openAIKey} required placeholder="sk-..." />
|
||||
<Textbox label="API key" bind:value={openAIKey} required placeholder="sk-..." />
|
||||
|
||||
<Select
|
||||
value={openAIModelName}
|
||||
@ -260,7 +260,7 @@
|
||||
{/if}
|
||||
|
||||
{#if anthropicKeyOption === KeyOption.BringYourOwn}
|
||||
<TextBox
|
||||
<Textbox
|
||||
label="API key"
|
||||
bind:value={anthropicKey}
|
||||
required
|
||||
@ -301,13 +301,13 @@
|
||||
{#if modelKind === ModelKind.Ollama}
|
||||
<SectionCard roundedTop={false} orientation="row" topDivider>
|
||||
<div class="inputs-group">
|
||||
<TextBox
|
||||
<Textbox
|
||||
label="Endpoint"
|
||||
bind:value={ollamaEndpoint}
|
||||
placeholder="http://127.0.0.1:11434"
|
||||
/>
|
||||
|
||||
<TextBox label="Model" bind:value={ollamaModel} placeholder="llama3" />
|
||||
<Textbox label="Model" bind:value={ollamaModel} placeholder="llama3" />
|
||||
</div>
|
||||
</SectionCard>
|
||||
{/if}
|
||||
@ -321,14 +321,14 @@
|
||||
How many characters of your git diff should be provided to AI
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<TextBox
|
||||
<Textbox
|
||||
type="number"
|
||||
width={80}
|
||||
textAlign="center"
|
||||
value={diffLengthLimit?.toString()}
|
||||
minVal={100}
|
||||
on:input={(e) => {
|
||||
diffLengthLimit = parseInt(e.detail);
|
||||
oninput={(value: string) => {
|
||||
diffLengthLimit = parseInt(value);
|
||||
}}
|
||||
placeholder="5000"
|
||||
/>
|
||||
|
@ -13,9 +13,9 @@
|
||||
type CodeEditorSettings
|
||||
} from '$lib/settings/userSettings';
|
||||
import RadioButton from '$lib/shared/RadioButton.svelte';
|
||||
import TextBox from '$lib/shared/TextBox.svelte';
|
||||
import { type Hunk } from '$lib/vbranches/types';
|
||||
import { getContextStoreBySymbol } from '@gitbutler/shared/context';
|
||||
import Textbox from '@gitbutler/ui/Textbox.svelte';
|
||||
import Toggle from '@gitbutler/ui/Toggle.svelte';
|
||||
import type { ContentSection } from '$lib/utils/fileSections';
|
||||
import type { Writable } from 'svelte/store';
|
||||
@ -128,14 +128,14 @@
|
||||
>Sets the font for the diff view. The first font name is the default, others are fallbacks.
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<TextBox
|
||||
<Textbox
|
||||
wide
|
||||
bind:value={$userSettings.diffFont}
|
||||
required
|
||||
on:change={(e) => {
|
||||
onchange={(value: string) => {
|
||||
userSettings.update((s) => ({
|
||||
...s,
|
||||
diffFont: e.detail
|
||||
diffFont: value
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
@ -170,7 +170,7 @@
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<TextBox
|
||||
<Textbox
|
||||
type="number"
|
||||
width={100}
|
||||
textAlign="center"
|
||||
@ -178,10 +178,10 @@
|
||||
minVal={1}
|
||||
maxVal={8}
|
||||
showCountActions
|
||||
on:change={(e) => {
|
||||
onchange={(value: string) => {
|
||||
userSettings.update((s) => ({
|
||||
...s,
|
||||
tabSize: parseInt(e.detail) || $userSettings.tabSize
|
||||
tabSize: parseInt(value) || $userSettings.tabSize
|
||||
}));
|
||||
}}
|
||||
placeholder={$userSettings.tabSize.toString()}
|
||||
|
@ -6,12 +6,12 @@
|
||||
import SettingsPage from '$lib/layout/SettingsPage.svelte';
|
||||
import { showError } from '$lib/notifications/toasts';
|
||||
import Spacer from '$lib/shared/Spacer.svelte';
|
||||
import TextBox from '$lib/shared/TextBox.svelte';
|
||||
import { UserService } from '$lib/stores/user';
|
||||
import * as toasts from '$lib/utils/toasts';
|
||||
import { getContext } from '@gitbutler/shared/context';
|
||||
import Button from '@gitbutler/ui/Button.svelte';
|
||||
import Modal from '@gitbutler/ui/Modal.svelte';
|
||||
import Textbox from '@gitbutler/ui/Textbox.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
const userService = getContext(UserService);
|
||||
@ -113,8 +113,8 @@
|
||||
|
||||
<div id="contact-info" class="contact-info">
|
||||
<div class="contact-info__fields">
|
||||
<TextBox label="Full name" bind:value={newName} required />
|
||||
<TextBox label="Email" bind:value={$user.email} readonly />
|
||||
<Textbox label="Full name" bind:value={newName} required />
|
||||
<Textbox label="Email" bind:value={$user.email} readonly />
|
||||
</div>
|
||||
|
||||
<Button type="submit" style="pop" kind="solid" loading={saving}>Update profile</Button>
|
||||
|
@ -1,111 +0,0 @@
|
||||
<script lang="ts" module>
|
||||
export interface Props {
|
||||
ref?: HTMLTextAreaElement;
|
||||
value: string | undefined;
|
||||
placeholder?: string;
|
||||
readonly?: boolean;
|
||||
fontSize?: number;
|
||||
maxHeight?: string;
|
||||
rows?: number;
|
||||
autofocus?: boolean;
|
||||
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;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { autoHeight } from '$lib/utils/autoHeight';
|
||||
import { pxToRem } from '$lib/utils/pxToRem';
|
||||
import { resizeObserver } from '$lib/utils/resizeObserver';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let {
|
||||
ref = $bindable(),
|
||||
value = $bindable(),
|
||||
placeholder,
|
||||
readonly,
|
||||
fontSize = 14,
|
||||
maxHeight = 'none',
|
||||
rows = 1,
|
||||
autofocus = false,
|
||||
padding = { top: 0, right: 0, bottom: 0, left: 0 },
|
||||
oninput,
|
||||
onfocus,
|
||||
onkeydown
|
||||
}: Props = $props();
|
||||
|
||||
onMount(() => {
|
||||
setTimeout(() => {
|
||||
if (ref) {
|
||||
autoHeight(ref);
|
||||
if (autofocus) {
|
||||
ref.focus();
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (ref) {
|
||||
// reference the value to trigger
|
||||
// the effect when it changes
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
value;
|
||||
autoHeight(ref);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<textarea
|
||||
tabindex="0"
|
||||
bind:this={ref}
|
||||
bind:value
|
||||
use:resizeObserver={(e) => {
|
||||
autoHeight(e.currentTarget as HTMLTextAreaElement);
|
||||
}}
|
||||
class="borderless-textarea scrollbar"
|
||||
{rows}
|
||||
{placeholder}
|
||||
{readonly}
|
||||
oninput={(e) => {
|
||||
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>
|
204
packages/ui/src/lib/Textarea.svelte
Normal file
204
packages/ui/src/lib/Textarea.svelte
Normal file
@ -0,0 +1,204 @@
|
||||
<script lang="ts" module>
|
||||
export interface Props {
|
||||
id?: string;
|
||||
textBoxEl?: HTMLDivElement;
|
||||
label?: string;
|
||||
value: string | undefined;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
fontSize?: number;
|
||||
minRows?: number;
|
||||
maxRows?: number;
|
||||
autofocus?: boolean;
|
||||
padding?: {
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
left: number;
|
||||
};
|
||||
borderless?: boolean;
|
||||
borderTop?: boolean;
|
||||
borderRight?: boolean;
|
||||
borderBottom?: boolean;
|
||||
borderLeft?: boolean;
|
||||
unstyled?: boolean;
|
||||
oninput?: (e: Event & { currentTarget: EventTarget & HTMLTextAreaElement }) => void;
|
||||
onfocus?: (
|
||||
this: void,
|
||||
e: FocusEvent & { currentTarget: EventTarget & HTMLTextAreaElement }
|
||||
) => void;
|
||||
onkeydown?: (e: KeyboardEvent & { currentTarget: EventTarget & HTMLTextAreaElement }) => void;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { pxToRem } from '$lib/utils/pxToRem';
|
||||
|
||||
let {
|
||||
id,
|
||||
textBoxEl = $bindable(),
|
||||
label,
|
||||
value = $bindable(),
|
||||
placeholder,
|
||||
disabled,
|
||||
fontSize = 13,
|
||||
minRows = 1,
|
||||
maxRows = 100,
|
||||
autofocus,
|
||||
padding = { top: 12, right: 12, bottom: 12, left: 12 },
|
||||
borderless,
|
||||
borderTop = true,
|
||||
borderRight = true,
|
||||
borderBottom = true,
|
||||
borderLeft = true,
|
||||
unstyled,
|
||||
oninput,
|
||||
onfocus,
|
||||
onkeydown
|
||||
}: Props = $props();
|
||||
|
||||
function getSelectionRange() {
|
||||
const selection = window.getSelection();
|
||||
if (selection) {
|
||||
const range = selection.getRangeAt(0);
|
||||
return range;
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (autofocus) {
|
||||
textBoxEl?.focus();
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (textBoxEl) {
|
||||
if (!disabled) {
|
||||
textBoxEl.setAttribute('contenteditable', 'true');
|
||||
} else {
|
||||
textBoxEl.removeAttribute('contenteditable');
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="textarea-container"
|
||||
style:--placeholder-text={`"${placeholder || placeholder !== '' ? placeholder : ' '}"`}
|
||||
style:--font-size={pxToRem(fontSize)}
|
||||
style:--min-rows={minRows}
|
||||
style:--max-rows={maxRows}
|
||||
>
|
||||
{#if label}
|
||||
<label class="textarea-label text-13 text-semibold" for={id}>
|
||||
{label}
|
||||
</label>
|
||||
{/if}
|
||||
<div
|
||||
bind:this={textBoxEl}
|
||||
{id}
|
||||
role="textbox"
|
||||
aria-multiline="true"
|
||||
tabindex={disabled ? -1 : 0}
|
||||
contenteditable
|
||||
bind:innerText={value}
|
||||
onfocus={(e: Event) => {
|
||||
if (e.currentTarget) {
|
||||
onfocus?.(e as FocusEvent & { currentTarget: EventTarget & HTMLTextAreaElement });
|
||||
}
|
||||
}}
|
||||
oninput={(e: Event) => {
|
||||
const innerText = (e.target as HTMLDivElement).innerText;
|
||||
const eventMock = { currentTarget: { value: innerText } } as Event & {
|
||||
currentTarget: EventTarget & HTMLTextAreaElement;
|
||||
};
|
||||
|
||||
oninput?.(eventMock);
|
||||
}}
|
||||
onkeydown={(e: KeyboardEvent) => {
|
||||
const selection = getSelectionRange();
|
||||
|
||||
const eventMock = {
|
||||
key: e.key,
|
||||
code: e.code,
|
||||
altKey: e.altKey,
|
||||
metaKey: e.metaKey,
|
||||
ctrlKey: e.ctrlKey,
|
||||
shiftKey: e.shiftKey,
|
||||
location: e.location,
|
||||
currentTarget: {
|
||||
value: (e.currentTarget as HTMLDivElement).innerText,
|
||||
selectionStart: selection?.startOffset,
|
||||
selectionEnd: selection?.endOffset
|
||||
}
|
||||
} as unknown as KeyboardEvent & { currentTarget: EventTarget & HTMLTextAreaElement };
|
||||
|
||||
onkeydown?.(eventMock);
|
||||
}}
|
||||
class:disabled
|
||||
class="textarea scrollbar"
|
||||
class:text-input={!unstyled}
|
||||
class:textarea-placeholder={value === ''}
|
||||
style:padding-top={pxToRem(padding.top)}
|
||||
style:padding-right={pxToRem(padding.right)}
|
||||
style:padding-bottom={pxToRem(padding.bottom)}
|
||||
style:padding-left={pxToRem(padding.left)}
|
||||
style:border-top-width={borderTop && !borderless && !unstyled ? '1px' : '0'}
|
||||
style:border-right-width={borderRight && !borderless && !unstyled ? '1px' : '0'}
|
||||
style:border-bottom-width={borderBottom && !borderless && !unstyled ? '1px' : '0'}
|
||||
style:border-left-width={borderLeft && !borderless && !unstyled ? '1px' : '0'}
|
||||
style:border-top-right-radius={!borderTop || !borderRight ? '0' : undefined}
|
||||
style:border-top-left-radius={!borderTop || !borderLeft ? '0' : undefined}
|
||||
style:border-bottom-right-radius={!borderBottom || !borderRight ? '0' : undefined}
|
||||
style:border-bottom-left-radius={!borderBottom || !borderLeft ? '0' : undefined}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.textarea-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
font-family: var(--base-font-family);
|
||||
line-height: var(--body-line-height);
|
||||
font-weight: var(--base-font-weight);
|
||||
white-space: pre-wrap;
|
||||
cursor: text;
|
||||
|
||||
resize: none;
|
||||
outline: none;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: var(--font-size);
|
||||
min-height: calc(var(--font-size) * 1.5 * var(--min-rows));
|
||||
max-height: calc(var(--font-size) * 1.5 * var(--max-rows));
|
||||
overflow-y: auto; /* Enable scrolling when max height is reached */
|
||||
|
||||
&.disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&.textarea-placeholder {
|
||||
display: block;
|
||||
white-space: pre-wrap;
|
||||
|
||||
&:before {
|
||||
content: var(--placeholder-text);
|
||||
color: var(--clr-text-3);
|
||||
cursor: text;
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.textarea-label {
|
||||
color: var(--clr-text-2);
|
||||
}
|
||||
</style>
|
@ -1,51 +1,97 @@
|
||||
<script lang="ts">
|
||||
import { clickOutside } from '$lib/clickOutside';
|
||||
import { clickOutside } from '$lib/utils/clickOutside';
|
||||
import Icon from '@gitbutler/ui/Icon.svelte';
|
||||
import { pxToRem } from '@gitbutler/ui/utils/pxToRem';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import type iconsJson from '@gitbutler/ui/data/icons.json';
|
||||
|
||||
export let element: HTMLElement | undefined = undefined;
|
||||
export let id: string | undefined = undefined; // Required to make label clickable
|
||||
export let icon: keyof typeof iconsJson | undefined = undefined;
|
||||
export let value: string | undefined = undefined;
|
||||
export let width: number | undefined = undefined;
|
||||
export let textAlign: 'left' | 'center' | 'right' = 'left';
|
||||
export let placeholder: string | undefined = undefined;
|
||||
export let helperText: string | undefined = undefined;
|
||||
export let label: string | undefined = undefined;
|
||||
export let reversedDirection: boolean = false;
|
||||
export let wide: boolean = false;
|
||||
export let minVal: number | undefined = undefined;
|
||||
export let maxVal: number | undefined = undefined;
|
||||
export let showCountActions = false;
|
||||
export let disabled = false;
|
||||
export let readonly = false;
|
||||
export let required = false;
|
||||
export let noselect = false;
|
||||
export let selectall = false;
|
||||
export let spellcheck = false;
|
||||
export let autocorrect = false;
|
||||
export let autocomplete = false;
|
||||
export let focus = false;
|
||||
// eslint-disable-next-line func-style
|
||||
export let onClickOutside = () => {};
|
||||
interface Props {
|
||||
element?: HTMLElement;
|
||||
id?: string;
|
||||
type?: inputType;
|
||||
icon?: keyof typeof iconsJson;
|
||||
value?: string;
|
||||
width?: number;
|
||||
textAlign?: 'left' | 'center' | 'right';
|
||||
placeholder?: string;
|
||||
helperText?: string;
|
||||
label?: string;
|
||||
reversedDirection?: boolean;
|
||||
wide?: boolean;
|
||||
minVal?: number;
|
||||
maxVal?: number;
|
||||
showCountActions?: boolean;
|
||||
disabled?: boolean;
|
||||
readonly?: boolean;
|
||||
required?: boolean;
|
||||
noselect?: boolean;
|
||||
selectall?: boolean;
|
||||
spellcheck?: boolean;
|
||||
autocorrect?: boolean;
|
||||
autocomplete?: boolean;
|
||||
autofocus?: boolean;
|
||||
onclick?: (e: MouseEvent & { currentTarget: EventTarget & HTMLInputElement }) => void;
|
||||
onmousedown?: (e: MouseEvent & { currentTarget: EventTarget & HTMLInputElement }) => void;
|
||||
oninput?: (val: string) => void;
|
||||
onchange?: (val: string) => void;
|
||||
onkeydown?: (e: KeyboardEvent & { currentTarget: EventTarget & HTMLInputElement }) => void;
|
||||
}
|
||||
|
||||
export let type: 'text' | 'password' | 'select' | 'number' | 'email' = 'text';
|
||||
let {
|
||||
element = $bindable(),
|
||||
id,
|
||||
type = 'text',
|
||||
icon,
|
||||
value = $bindable(),
|
||||
width,
|
||||
textAlign = 'left',
|
||||
placeholder,
|
||||
helperText,
|
||||
label,
|
||||
reversedDirection,
|
||||
wide,
|
||||
minVal,
|
||||
maxVal,
|
||||
showCountActions,
|
||||
disabled,
|
||||
readonly,
|
||||
required,
|
||||
noselect,
|
||||
selectall,
|
||||
spellcheck,
|
||||
autocorrect,
|
||||
autocomplete,
|
||||
autofocus,
|
||||
onclick,
|
||||
onmousedown,
|
||||
oninput,
|
||||
onchange,
|
||||
onkeydown
|
||||
}: Props = $props();
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
input: string;
|
||||
change: string;
|
||||
keydown: KeyboardEvent;
|
||||
}>();
|
||||
|
||||
let showPassword = false;
|
||||
let isInputValid = true;
|
||||
let showPassword = $state(false);
|
||||
let isInputValid = $state(true);
|
||||
let htmlInput: HTMLInputElement;
|
||||
|
||||
export function onClickOutside() {
|
||||
htmlInput.blur();
|
||||
}
|
||||
|
||||
type inputType =
|
||||
| 'text'
|
||||
| 'password'
|
||||
| 'number'
|
||||
| 'select'
|
||||
| 'email'
|
||||
| 'tel'
|
||||
| 'url'
|
||||
| 'search'
|
||||
| 'date'
|
||||
| 'time';
|
||||
|
||||
onMount(() => {
|
||||
if (selectall) htmlInput.select();
|
||||
else if (focus) htmlInput.focus();
|
||||
else if (autofocus) htmlInput.focus();
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -93,15 +139,16 @@
|
||||
style:text-align={textAlign}
|
||||
bind:value
|
||||
bind:this={htmlInput}
|
||||
on:click
|
||||
on:mousedown
|
||||
on:input={(e) => {
|
||||
dispatch('input', e.currentTarget.value);
|
||||
|
||||
{onclick}
|
||||
{onmousedown}
|
||||
oninput={(e) => {
|
||||
oninput?.(e.currentTarget.value);
|
||||
isInputValid = e.currentTarget.checkValidity();
|
||||
}}
|
||||
on:change={(e) => dispatch('change', e.currentTarget.value)}
|
||||
on:keydown={(e) => dispatch('keydown', e)}
|
||||
onchange={(e) => {
|
||||
onchange?.(e.currentTarget.value);
|
||||
}}
|
||||
{onkeydown}
|
||||
/>
|
||||
|
||||
{#if type === 'number' && showCountActions}
|
||||
@ -109,10 +156,11 @@
|
||||
<button
|
||||
type="button"
|
||||
class="textbox__count-btn"
|
||||
on:click={() => {
|
||||
onclick={() => {
|
||||
htmlInput.stepDown();
|
||||
dispatch('input', htmlInput.value);
|
||||
dispatch('change', htmlInput.value);
|
||||
|
||||
oninput?.(htmlInput.value);
|
||||
onchange?.(htmlInput.value);
|
||||
|
||||
isInputValid = htmlInput.checkValidity();
|
||||
}}
|
||||
@ -122,10 +170,11 @@
|
||||
<button
|
||||
type="button"
|
||||
class="textbox__count-btn"
|
||||
on:click={() => {
|
||||
onclick={() => {
|
||||
htmlInput.stepUp();
|
||||
dispatch('input', htmlInput.value);
|
||||
dispatch('change', htmlInput.value);
|
||||
|
||||
oninput?.(htmlInput.value);
|
||||
onchange?.(htmlInput.value);
|
||||
|
||||
isInputValid = htmlInput.checkValidity();
|
||||
}}
|
||||
@ -139,7 +188,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="textbox__show-hide-icon"
|
||||
on:click={() => {
|
||||
onclick={() => {
|
||||
showPassword = !showPassword;
|
||||
htmlInput.focus();
|
||||
}}
|
@ -1,18 +0,0 @@
|
||||
import BorderlessTextarea from './DemoBorderlessTextarea.svelte';
|
||||
import type { Meta, StoryObj } from '@storybook/svelte';
|
||||
|
||||
const meta = {
|
||||
title: 'Inputs / BorderlessTextarea',
|
||||
component: BorderlessTextarea
|
||||
} satisfies Meta<BorderlessTextarea>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const CheckboxStory: Story = {
|
||||
name: 'BorderlessTextarea',
|
||||
args: {
|
||||
value: 'Hello, World!',
|
||||
placeholder: 'Type here...'
|
||||
}
|
||||
};
|
@ -1,33 +0,0 @@
|
||||
<script lang="ts">
|
||||
import BorderlessTextarea, {
|
||||
type Props as BorderlessTextareaProps
|
||||
} from '$lib/BorderlessTextarea.svelte';
|
||||
import Button from '$lib/Button.svelte';
|
||||
|
||||
const props: BorderlessTextareaProps = $props();
|
||||
let value = $state('Hello world');
|
||||
|
||||
function fillTheForm() {
|
||||
value =
|
||||
'If this is a WIP PR and you have todos left, feel free to uncomment this and turn this PR into a draft, see https://github.blog/2019-02-14-introducing-draft-pull-requests/';
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
<BorderlessTextarea
|
||||
bind:value
|
||||
oninput={(e) => {
|
||||
console.log(e.currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
<Button style="ghost" outline onclick={fillTheForm}>Fill the form</Button>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 300px;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
34
packages/ui/src/stories/textarea/textarea.stories.ts
Normal file
34
packages/ui/src/stories/textarea/textarea.stories.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import Textarea from './textareaDemo.svelte';
|
||||
import type { Meta, StoryObj } from '@storybook/svelte';
|
||||
|
||||
const meta = {
|
||||
title: 'Inputs / Textarea',
|
||||
component: Textarea
|
||||
} satisfies Meta<Textarea>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const CheckboxStory: Story = {
|
||||
name: 'Textarea',
|
||||
args: {
|
||||
label: '',
|
||||
value: `## ☕️ Reasoning
|
||||
|
||||
|
||||
## 🧢 Changesd
|
||||
|
||||
|
||||
## 📌 Todos`,
|
||||
placeholder: 'Type here...',
|
||||
minRows: 1,
|
||||
maxRows: 5,
|
||||
autofocus: false,
|
||||
disabled: false,
|
||||
borderTop: true,
|
||||
borderRight: true,
|
||||
borderBottom: true,
|
||||
borderLeft: true,
|
||||
unstyled: false
|
||||
}
|
||||
};
|
67
packages/ui/src/stories/textarea/textareaDemo.svelte
Normal file
67
packages/ui/src/stories/textarea/textareaDemo.svelte
Normal file
@ -0,0 +1,67 @@
|
||||
<script lang="ts">
|
||||
import Button from '$lib/Button.svelte';
|
||||
import Textarea, { type Props as TextareaProps } from '$lib/Textarea.svelte';
|
||||
|
||||
const props: TextareaProps = $props();
|
||||
|
||||
let changableValue = $state('');
|
||||
|
||||
function fillTheForm() {
|
||||
changableValue = `## ☕️ Reasoning
|
||||
|
||||
|
||||
## 🧢 Changesd
|
||||
|
||||
|
||||
## 📌 Todos`;
|
||||
}
|
||||
|
||||
function handleDescriptionKeyDown(e: KeyboardEvent & { currentTarget: HTMLTextAreaElement }) {
|
||||
if (e.key === 'Escape') {
|
||||
console.log('keyboard', e.key);
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
||||
console.log('keyboard', e.key);
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
<Textarea
|
||||
label={props.label}
|
||||
value={!changableValue ? props.value : changableValue}
|
||||
placeholder={props.placeholder}
|
||||
minRows={props.minRows}
|
||||
maxRows={props.maxRows}
|
||||
autofocus={props.autofocus}
|
||||
disabled={props.disabled}
|
||||
borderBottom={props.borderBottom}
|
||||
borderLeft={props.borderLeft}
|
||||
borderRight={props.borderRight}
|
||||
borderTop={props.borderTop}
|
||||
borderless={props.borderless}
|
||||
unstyled={props.unstyled}
|
||||
oninput={(e) => {
|
||||
console.log('input', e);
|
||||
}}
|
||||
onkeydown={handleDescriptionKeyDown}
|
||||
onfocus={(e) => {
|
||||
console.log('focus', e);
|
||||
}}
|
||||
/>
|
||||
<Button style="ghost" outline onclick={fillTheForm}>Fill the form</Button>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 300px;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
@ -20,12 +20,14 @@
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:invalid {
|
||||
&:invalid,
|
||||
&.invalid {
|
||||
border-color: var(--clr-theme-err-element);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: var(--clr-scale-ntrl-60);
|
||||
&:disabled,
|
||||
&.disabled {
|
||||
color: var(--clr-text-2);
|
||||
border-color: var(--clr-scale-ntrl-70);
|
||||
background-color: var(--clr-bg-2);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user