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:
Pavel Laptev 2024-10-24 17:37:27 +02:00 committed by GitHub
parent 479973b2fb
commit d3a0f3108b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 543 additions and 460 deletions

View File

@ -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>

View File

@ -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">

View File

@ -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}
/>

View File

@ -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">

View File

@ -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);
}
};
}

View File

@ -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}

View File

@ -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);

View File

@ -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>

View File

@ -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>

View File

@ -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}
/>

View File

@ -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';

View File

@ -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"

View File

@ -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';

View File

@ -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>

View File

@ -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;
}}
/>

View File

@ -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

View File

@ -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}

View File

@ -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"
/>

View File

@ -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);
}}

View File

@ -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>

View File

@ -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>

View File

@ -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">

View File

@ -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>

View File

@ -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"
/>

View File

@ -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()}

View File

@ -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>

View File

@ -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>

View 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>

View File

@ -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();
}}

View File

@ -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...'
}
};

View File

@ -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>

View 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
}
};

View 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>

View File

@ -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);
}