Repository: podman-desktop/podman-desktop
Pull request #12475: fix: migrate Input, NumberInput and SearchInput to svelte v5
Author: login: dgolovin
Created: 2025-05-14T03:29:38Z | Merged: 2025-05-20T08:50:24Z

### What does this PR do?

This PR migrates components in subject to svelte v5

### Screenshot / video of UI

<!-- If this PR is changing UI, please include
screenshots or screencasts showing the difference -->

### What issues does this PR fix or reference?

Fix #11578.

### How to test this PR?

<!-- Please explain steps to verify the functionality,
do not forget to provide unit/component tests -->

- [ ] Tests are covering the bug fix or the new feature


Linked issue #11578: Migrate UI component inputs to Svelte 5
https://github.com/podman-desktop/podman-desktop/blob/ce53e6c5bd49e87ed204b4275510079557f03a92/packages/ui/src/lib/inputs/Input.svelte#L1

https://github.com/podman-desktop/podman-desktop/blob/ce53e6c5bd49e87ed204b4275510079557f03a92/packages/ui/src/lib/inputs/NumberInput.svelte#L1

https://github.com/podman-desktop/podman-desktop/blob/ce53e6c5bd49e87ed204b4275510079557f03a92/packages/ui/src/lib/inputs/SearchInput.svelte#L1

Diff:
diff --git a/packages/renderer/src/lib/preferences/PreferencesRendering.svelte b/packages/renderer/src/lib/preferences/PreferencesRendering.svelte
index f47ea5e6be5da..d16c412869c0f 100644
--- a/packages/renderer/src/lib/preferences/PreferencesRendering.svelte
+++ b/packages/renderer/src/lib/preferences/PreferencesRendering.svelte
@@ -73,7 +73,7 @@ function updateSearchValue(event: any): void {
 
 <Route path="/" breadcrumb={key}>
   <SettingsPage title="Preferences">
-    <SearchInput slot="header" title="preferences" class="mt-4" on:input={updateSearchValue} />
+    <SearchInput slot="header" title="preferences" class="mt-4" oninput={updateSearchValue} />
     <div class="flex flex-col space-y-5 text-[var(--pd-content-header)]">
       {#if matchingRecords.size === 0}
         <div>No Settings Found</div>
diff --git a/packages/renderer/src/lib/ui/PasswordInput.svelte b/packages/renderer/src/lib/ui/PasswordInput.svelte
index f9c1c861605db..d777cac5038af 100644
--- a/packages/renderer/src/lib/ui/PasswordInput.svelte
+++ b/packages/renderer/src/lib/ui/PasswordInput.svelte
@@ -36,7 +36,7 @@ async function onShowHide(): Promise<void> {
   bind:readonly={readonly}
   on:input
   bind:element={element}>
-  <svelte:fragment slot="right">
+  {#snippet right()}
     <button
       class="px-1 cursor-pointer text-[var(--pd-input-field-stroke)] group-hover:text-[var(--pd-input-field-hover-stroke)] group-focus-within:text-[var(--pd-input-field-hover-stroke)]"
       class:hidden={!password || readonly}
@@ -48,5 +48,5 @@ async function onShowHide(): Promise<void> {
         <Fa icon={faEyeSlash} />
       {/if}
     </button>
-  </svelte:fragment>
+  {/snippet}
 </Input>
diff --git a/packages/renderer/src/lib/ui/TerminalSearchControls.svelte b/packages/renderer/src/lib/ui/TerminalSearchControls.svelte
index 46f93653cc442..fdf3657e680f0 100644
--- a/packages/renderer/src/lib/ui/TerminalSearchControls.svelte
+++ b/packages/renderer/src/lib/ui/TerminalSearchControls.svelte
@@ -66,9 +66,8 @@ function onKeyUp(e: KeyboardEvent): void {
       bind:element={input}
       placeholder="Find"
       aria-label="Find"
-      type="text"
-      on:keypress={onKeyPressed}
-      on:input={onSearch}
+      onkeypress={onKeyPressed}
+      oninput={onSearch}
       value={searchTerm}
     />
   </div>
diff --git a/packages/ui/src/lib/inputs/Input.spec.ts b/packages/ui/src/lib/inputs/Input.spec.ts
index e7c1a44784700..40fa1767df1bc 100644
--- a/packages/ui/src/lib/inputs/Input.spec.ts
+++ b/packages/ui/src/lib/inputs/Input.spec.ts
@@ -28,11 +28,10 @@ import Input from './Input.svelte';
 function renderInput(
   value: string,
   placeholder?: string,
-  readonly?: boolean,
+  readonly: boolean = false,
   disabled?: boolean,
   clearable?: boolean,
   error?: string,
-  onClick?: any,
   inputClass?: string,
 ): void {
   render(Input, {
@@ -42,7 +41,6 @@ function renderInput(
     readonly: readonly,
     clearable: clearable,
     error: error,
-    onClick: onClick,
     inputClass: inputClass,
   });
 }
@@ -157,7 +155,7 @@ test('Expect basic error styling', async () => {
 test('Expect inputClass styling', async () => {
   const value = 'test';
   const thisClass = 'this-class';
-  renderInput(value, value, false, false, true, undefined, undefined, thisClass);
+  renderInput(value, value, false, false, true, undefined, thisClass);
 
   const element = screen.getByRole('textbox');
   expect(element).toBeInTheDocument();
diff --git a/packages/ui/src/lib/inputs/Input.svelte b/packages/ui/src/lib/inputs/Input.svelte
index 08987d46d58f2..53992b684f8ca 100644
--- a/packages/ui/src/lib/inputs/Input.svelte
+++ b/packages/ui/src/lib/inputs/Input.svelte
@@ -1,33 +1,67 @@
 <script lang="ts">
 import { faCircleExclamation, faXmark } from '@fortawesome/free-solid-svg-icons';
-import { createEventDispatcher } from 'svelte';
+import { createEventDispatcher, type Snippet } from 'svelte';
+import type { Booleanish, FormEventHandler, KeyboardEventHandler } from 'svelte/elements';
+import { createBubbler } from 'svelte/legacy';
 import Fa from 'svelte-fa';
 
-export let placeholder: string | undefined = undefined;
-export let id: string | undefined = undefined;
-export let name: string | undefined = undefined;
-export let value: string | undefined = undefined;
-export let readonly: boolean = false;
-export let required: boolean = false;
-export let clearable: boolean = false;
-export let disabled: boolean = false;
-export let error: string | undefined = undefined;
-export let inputClass: string | undefined = undefined;
-export let showError: boolean = true;
+const dispatch = createEventDispatcher();
+const bubble = createBubbler();
 
-export let element: HTMLInputElement | undefined = undefined;
+interface Props {
+  placeholder?: string;
+  id?: string;
+  name?: string;
+  value?: string;
+  readonly?: boolean;
+  required?: boolean;
+  clearable?: boolean;
+  disabled?: boolean;
+  error?: string;
+  class?: string;
+  inputClass?: string;
+  showError?: boolean;
+  element?: HTMLInputElement;
+  'aria-label'?: string;
+  'aria-invalid'?: Booleanish | 'grammar' | 'spelling' | null;
+  left?: Snippet;
+  right?: Snippet;
+  oninput?: FormEventHandler<HTMLInputElement>;
+  onkeypress?: KeyboardEventHandler<HTMLInputElement>;
+  title?: string;
+}
 
-let enabled: boolean = true;
-$: enabled = !readonly && !disabled;
+let {
+  left,
+  right,
+  placeholder,
+  id,
+  name,
+  value = $bindable(),
+  readonly = $bindable(false),
+  required = false,
+  clearable = false,
+  disabled = false,
+  error,
+  inputClass = '',
+  class: propsClass = '',
+  showError = true,
+  element = $bindable(),
+  'aria-label': ariaLabel,
+  'aria-invalid': ariaInvalid,
+  oninput = bubble('input'),
+  onkeypress = bubble('keypress'),
+  title = '',
+}: Props = $props();
 
-const dispatch = createEventDispatcher();
+let enabled = $derived(!readonly && !disabled);
 
 // clear the value if the parent doesn't override
 async function onClear(): Promise<void> {
   if (dispatch('action', { cancelable: true })) {
     value = '';
     if (element) {
-      // need to trigger normal input event for on:input listeners
+      // need to trigger normal input event for oninput listeners
       element.value = value;
       element.dispatchEvent(new InputEvent('input'));
     }
@@ -37,8 +71,7 @@ async function onClear(): Promise<void> {
 
 <div class="flex flex-col grow">
   <div
-    class="flex flex-row grow items-center px-1 py-1 group bg-[var(--pd-input-field-bg)] border-[1px] border-transparent {$$props.class ??
-      ''}"
+    class="flex flex-row grow items-center px-1 py-1 group bg-[var(--pd-input-field-bg)] border-[1px] border-transparent {propsClass}"
     class:not(focus-within):hover:bg-[var(--pd-input-field-hover-bg)]={enabled}
     class:focus-within:bg-[var(--pd-input-field-focused-bg)]={enabled}
     class:focus-within:rounded-md={enabled}
@@ -49,28 +82,28 @@ async function onClear(): Promise<void> {
     class:focus-within:border-[var(--pd-input-field-hover-stroke)]={enabled && !error}
     class:focus-within:border-[var(--pd-input-field-stroke-error)]={enabled && error}
     class:border-b-[var(--pd-input-field-stroke-readonly)]={readonly || disabled}>
-    <slot name="left" />
+    {@render left?.()}
     <input
       bind:this={element}
-      on:input
-      on:keypress
-      class="w-full px-0.5 outline-0 bg-[var(--pd-input-field-bg)] placeholder:text-[color:var(--pd-input-field-placeholder-text)] overflow-hidden {inputClass ??
-        ''}"
+      oninput={oninput}
+      onkeypress={onkeypress}
+      class="w-full px-0.5 outline-0 bg-[var(--pd-input-field-bg)] placeholder:text-[color:var(--pd-input-field-placeholder-text)] overflow-hidden {inputClass}"
       class:text-[color:var(--pd-input-field-focused-text)]={!disabled}
       class:text-[color:var(--pd-input-field-disabled-text)]={disabled}
       class:group-hover:bg-[var(--pd-input-field-hover-bg)]={enabled}
       class:group-focus-within:bg-[var(--pd-input-field-hover-bg)]={enabled}
       class:group-hover-placeholder:text-[color:var(--pd-input-field-placeholder-text)]={enabled}
-      name={name}
+      {name}
       type="text"
-      disabled={disabled}
-      readonly={readonly}
-      required={required}
-      placeholder={placeholder}
-      id={id}
-      aria-label={$$props['aria-label']}
-      aria-invalid={$$props['aria-invalid']}
-      bind:value={value} />
+      {disabled}
+      {readonly}
+      {required}
+      {placeholder}
+      {id}
+      {title}
+      aria-label={ariaLabel}
+      aria-invalid={ariaInvalid}
+      bind:value />
     {#if error && showError}
       <span class="px-0.5 text-[color:var(--pd-input-field-error-text)]" aria-label="error">
         <Fa icon={faCircleExclamation} />
@@ -81,11 +114,11 @@ async function onClear(): Promise<void> {
         class="px-0.5 cursor-pointer text-[color:var(--pd-input-field-icon)] group-hover:text-[color:var(--pd-input-field-hover-icon)] group-focus-within:text-[color:var(--pd-input-field-focused-icon)]"
         class:hidden={!value || readonly || disabled}
         aria-label="clear"
-        on:click={onClear}>
+        onclick={onClear}>
         <Fa icon={faXmark} />
       </button>
     {/if}
-    <slot name="right" />
+    {@render right?.()}
   </div>
   {#if error && error.length > 0 && showError}
     <span class="text-sm text-[color:var(--pd-input-field-error-text)]">{error}</span>
diff --git a/packages/ui/src/lib/inputs/NumberInput.svelte b/packages/ui/src/lib/inputs/NumberInput.svelte
index 3ddff09a50a09..d12f263767b24 100644
--- a/packages/ui/src/lib/inputs/NumberInput.svelte
+++ b/packages/ui/src/lib/inputs/NumberInput.svelte
@@ -1,29 +1,58 @@
 <script lang="ts">
+import type { Booleanish, FormEventHandler } from 'svelte/elements';
+
 import Input from './Input.svelte';
 
-export let name: string | undefined = undefined;
-export let value: number;
-export let required: boolean = false;
-export let disabled: boolean = false;
-export let minimum: number | undefined = undefined;
-export let maximum: number | undefined = undefined;
-export let error: string | undefined = undefined;
-export let showError: boolean = true;
-export let type: 'number' | 'integer';
-export let step: number | undefined = undefined;
-
-// callback after validation occurs
-export let onValidation = (_value: number, _error?: string): void => {};
-
-let minimumEnabled: boolean;
-let maximumEnabled: boolean;
-let valueAsString: string | undefined = undefined;
-$: valueAsString = value !== undefined ? String(value) : undefined;
-
-$: if (valueAsString !== undefined || disabled) {
-  validateNumber();
+interface Props {
+  name?: string;
+  value: number;
+  required?: boolean;
+  disabled?: boolean;
+  minimum?: number;
+  maximum?: number;
+  error?: string;
+  showError?: boolean;
+  type: 'number' | 'integer';
+  step?: number;
+  oninput?: FormEventHandler<HTMLInputElement>;
+  onValidation?: (value: number, error?: string) => void;
+  class?: string;
+  'aria-label'?: string;
+  'aria-invalid'?: Booleanish | 'grammar' | 'spelling' | null;
 }
 
+let {
+  name,
+  value = $bindable(),
+  required = false,
+  disabled = false,
+  minimum,
+  maximum,
+  error = $bindable(),
+  showError = true,
+  type,
+  step,
+  oninput,
+  onValidation = (_value: number, _error?: string): void => {}, // callback after validation occurs
+  class: className = '',
+  'aria-label': ariaLabel,
+  'aria-invalid': ariaInvalid,
+}: Props = $props();
+
+let valueAsString = $derived(value !== undefined ? String(value) : undefined);
+let minimumEnabled: boolean | undefined = $derived(
+  !disabled && (minimum === undefined || minimum < Number(valueAsString)),
+);
+let maximumEnabled: boolean | undefined = $derived(
+  !disabled && (maximum === undefined || maximum > Number(valueAsString)),
+);
+
+$effect(() => {
+  if (valueAsString !== undefined || disabled) {
+    validateNumber();
+  }
+});
+
 function validateNumber(): void {
   const numberToValidate = Number(valueAsString);
   if (maximum !== undefined && numberToValidate > maximum) {
@@ -33,8 +62,6 @@ function validateNumber(): void {
   } else {
     error = undefined;
   }
-  minimumEnabled = !disabled && (minimum === undefined || minimum < numberToValidate);
-  maximumEnabled = !disabled && (maximum === undefined || maximum > numberToValidate);
 
   // update converted value
   value = numberToValidate;
@@ -91,36 +118,36 @@ function onIncrement(e: MouseEvent): void {
 </script>
 
 <Input
-  class={$$props.class ?? ''}
+  class={className}
   inputClass="text-center"
   name={name}
   bind:value={valueAsString}
-  on:keypress={onKeyPress}
-  on:input
+  onkeypress={onKeyPress}
+  oninput={oninput}
   showError={showError}
   error={error}
   disabled={disabled}
   required={required}
-  aria-label={$$props['aria-label']}
-  aria-invalid={$$props['aria-invalid']}>
-  <button
-    class="pr-0.5"
-    class:text-[var(--pd-input-field-stroke)]={minimumEnabled}
-    class:text-[var(--pd-input-field-disabled-text)]={!minimumEnabled}
-    class:group-hover:text-[var(--pd-input-field-hover-stroke)]={minimumEnabled}
-    data-action="decrement"
-    aria-label="decrement"
-    on:click={onDecrement}
-    disabled={!minimumEnabled}
-    slot="left">-</button>
-  <button
-    class="pl-0.5"
-    class:text-[var(--pd-input-field-stroke)]={maximumEnabled}
-    class:text-[var(--pd-input-field-disabled-text)]={!maximumEnabled}
-    class:group-hover:text-[var(--pd-input-field-hover-stroke)]={maximumEnabled}
-    data-action="increment"
-    aria-label="increment"
-    on:click={onIncrement}
-    disabled={!maximumEnabled}
-    slot="right">+</button>
+  aria-label={ariaLabel}
+  aria-invalid={ariaInvalid}>
+  {#snippet left()}
+    <button
+      class="pr-0.5"
+      class:text-[var(--pd-input-field-stroke)]={minimumEnabled}
+      class:text-[var(--pd-input-field-disabled-text)]={!minimumEnabled}
+      class:group-hover:text-[var(--pd-input-field-hover-stroke)]={minimumEnabled}
+      data-action="decrement"
+      aria-label="decrement"
+      onclick={onDecrement}
+      disabled={!minimumEnabled}>-</button>
+    <button
+      class="pl-0.5"
+      class:text-[var(--pd-input-field-stroke)]={maximumEnabled}
+      class:text-[var(--pd-input-field-disabled-text)]={!maximumEnabled}
+      class:group-hover:text-[var(--pd-input-field-hover-stroke)]={maximumEnabled}
+      data-action="increment"
+      aria-label="increment"
+      onclick={onIncrement}
+      disabled={!maximumEnabled}>+</button>
+  {/snippet}
 </Input>
diff --git a/packages/ui/src/lib/inputs/SearchInput.spec.ts b/packages/ui/src/lib/inputs/SearchInput.spec.ts
index 55c4812125204..6b2a9e9f2aa35 100644
--- a/packages/ui/src/lib/inputs/SearchInput.spec.ts
+++ b/packages/ui/src/lib/inputs/SearchInput.spec.ts
@@ -25,8 +25,8 @@ import { expect, test } from 'vitest';
 
 import SearchInput from './SearchInput.svelte';
 
-function renderInput(title: string, value: string, searchTerm?: string): void {
-  render(SearchInput, { title: title, value: value, searchTerm: searchTerm });
+function renderInput(title: string, searchTerm: string): void {
+  render(SearchInput, { title: title, searchTerm: searchTerm });
 }
 
 test('Expect basic styling', async () => {
@@ -38,14 +38,14 @@ test('Expect basic styling', async () => {
 });
 
 test('Expect placeholder', async () => {
-  renderInput('a-title', 'a value');
+  renderInput('a-title', 'a search term');
 
   const element = screen.getByRole('textbox');
   expect(element).toHaveProperty('placeholder', 'Search a-title...');
 });
 
 test('Expect id and name', async () => {
-  renderInput('a-title', 'a value');
+  renderInput('a-title', 'a search term');
 
   const element = screen.getByRole('textbox');
   expect(element).toHaveProperty('id', 'search-a-title');
@@ -53,13 +53,13 @@ test('Expect id and name', async () => {
 });
 
 test('Expect aria-label', async () => {
-  renderInput('a-title', 'a value');
+  renderInput('a-title', 'a search term');
 
   screen.getByLabelText('search a-title');
 });
 
 test('Expect value', async () => {
-  renderInput('a-title', 'a value', 'a search term');
+  renderInput('a-title', 'a search term');
 
   const element = screen.getByRole('textbox');
   expect(element).toHaveValue('a search term');
diff --git a/packages/ui/src/lib/inputs/SearchInput.svelte b/packages/ui/src/lib/inputs/SearchInput.svelte
index 0ff4c6a932d01..7b9e4443fe750 100644
--- a/packages/ui/src/lib/inputs/SearchInput.svelte
+++ b/packages/ui/src/lib/inputs/SearchInput.svelte
@@ -1,20 +1,30 @@
 <script lang="ts">
+import { createBubbler } from 'svelte/legacy';
+
 import { Input } from '..';
 
-export let title: string;
-export let searchTerm: string = '';
+interface Props {
+  title?: string;
+  searchTerm?: string;
+  oninput?: (event: Event) => void;
+  class?: string;
+}
+
+const bubble = createBubbler();
+
+let { title = '', searchTerm = $bindable(''), oninput = bubble('input'), class: className = '' }: Props = $props();
 </script>
 
 <Input
-  class={$$props.class ?? ''}
+  class={className}
   id="search-{title}"
   name="search-{title}"
   placeholder="Search {title}..."
   bind:value={searchTerm}
   aria-label="search {title}"
   clearable
-  on:input>
-  <svelte:fragment slot="left">
+  {oninput}>
+  {#snippet left()}
     <svg
       xmlns="http://www.w3.org/2000/svg"
       class="w-4 h-4 mx-1 text-gray-700"
@@ -28,5 +38,5 @@ export let searchTerm: string = '';
         stroke-width="2"
         d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
     </svg>
-  </svelte:fragment>
+  {/snippet}
 </Input>


Reviews:
user: login: sourcery-ai[bot]
body: Hey @dgolovin - I've reviewed your changes - here's some feedback:

- Add explicit type="button" to all increment/decrement/clear buttons to prevent them from acting as submit controls when used inside forms.
- Unify your custom slot rendering—either stick with the new {#snippet left()/right()} syntax everywhere or use {@render left?.()} throughout, instead of mixing the two patterns.

<details>
<summary>Here's what I looked at during the review</summary>

- 🟢 **General issues**: all looks good
- 🟢 **Security**: all looks good
- 🟢 **Testing**: all looks good
- 🟢 **Complexity**: all looks good
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T03:34:28Z
user: login: gastoner
body: Cant we use props for 
```
export let id: string;
export let password: string | undefined = undefined;
export let passwordHidden: boolean = true;
export let readonly: boolean = false;
```
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T13:41:18Z
user: login: dgolovin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T20:38:15Z
user: login: gastoner
body: Codewise LGTM
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T06:39:55Z

Review comments:
diff_hunk: @@ -1,28 +1,56 @@
 <script lang="ts">
+import type { Booleanish, FormEventHandler } from 'svelte/elements';
+
 import Input from './Input.svelte';
 
-export let name: string | undefined = undefined;
-export let value: number;
-export let required: boolean = false;
-export let disabled: boolean = false;
-export let minimum: number | undefined = undefined;
-export let maximum: number | undefined = undefined;
-export let error: string | undefined = undefined;
-export let showError: boolean = true;
-export let type: 'number' | 'integer';
-export let step: number | undefined = undefined;
+interface Props {
+  name?: string;
+  value: number;
+  required?: boolean;
+  disabled?: boolean;
+  minimum?: number;
+  maximum?: number;
+  error?: string;
+  showError?: boolean;
+  type: 'number' | 'integer';
+  step?: number;
+  oninput?: FormEventHandler<HTMLInputElement>;
+  onValidation?: (value: number, error?: string) => void;
+  class?: string;
+  'aria-label'?: string;
+  'aria-invalid'?: Booleanish | 'grammar' | 'spelling' | null;
+}
+
+let {
+  name,
+  value,
+  required = false,
+  disabled = false,
+  minimum,
+  maximum,
+  error,
+  showError = true,
+  type,
+  step,
+  oninput,
+  onValidation = (_value: number, _error?: string): void => {},
+  class: className = '',
+  'aria-label': ariaLabel,
+  'aria-invalid': ariaInvalid,
+}: Props = $props();
 
 // callback after validation occurs
-export let onValidation = (_value: number, _error?: string): void => {};
+// export let onValidation = (_value: number, _error?: string): void => {};
path: packages/ui/src/lib/inputs/NumberInput.svelte
user: login: gastoner
body: To remove?
created_at: 2025-05-14T13:34:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,28 +1,56 @@
 <script lang="ts">
+import type { Booleanish, FormEventHandler } from 'svelte/elements';
+
 import Input from './Input.svelte';
 
-export let name: string | undefined = undefined;
-export let value: number;
-export let required: boolean = false;
-export let disabled: boolean = false;
-export let minimum: number | undefined = undefined;
-export let maximum: number | undefined = undefined;
-export let error: string | undefined = undefined;
-export let showError: boolean = true;
-export let type: 'number' | 'integer';
-export let step: number | undefined = undefined;
+interface Props {
+  name?: string;
+  value: number;
+  required?: boolean;
+  disabled?: boolean;
+  minimum?: number;
+  maximum?: number;
+  error?: string;
+  showError?: boolean;
+  type: 'number' | 'integer';
+  step?: number;
+  oninput?: FormEventHandler<HTMLInputElement>;
+  onValidation?: (value: number, error?: string) => void;
+  class?: string;
+  'aria-label'?: string;
+  'aria-invalid'?: Booleanish | 'grammar' | 'spelling' | null;
+}
+
+let {
+  name,
+  value,
+  required = false,
+  disabled = false,
+  minimum,
+  maximum,
+  error,
+  showError = true,
+  type,
+  step,
+  oninput,
+  onValidation = (_value: number, _error?: string): void => {},
+  class: className = '',
+  'aria-label': ariaLabel,
+  'aria-invalid': ariaInvalid,
+}: Props = $props();
 
 // callback after validation occurs
-export let onValidation = (_value: number, _error?: string): void => {};
+// export let onValidation = (_value: number, _error?: string): void => {};
 
-let minimumEnabled: boolean;
-let maximumEnabled: boolean;
-let valueAsString: string | undefined = undefined;
-$: valueAsString = value !== undefined ? String(value) : undefined;
+let minimumEnabled: boolean | undefined = $state();
path: packages/ui/src/lib/inputs/NumberInput.svelte
user: login: gastoner
body: Shoudn't be there false as default value?
created_at: 2025-05-14T13:38:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,28 +1,56 @@
 <script lang="ts">
+import type { Booleanish, FormEventHandler } from 'svelte/elements';
+
 import Input from './Input.svelte';
 
-export let name: string | undefined = undefined;
-export let value: number;
-export let required: boolean = false;
-export let disabled: boolean = false;
-export let minimum: number | undefined = undefined;
-export let maximum: number | undefined = undefined;
-export let error: string | undefined = undefined;
-export let showError: boolean = true;
-export let type: 'number' | 'integer';
-export let step: number | undefined = undefined;
+interface Props {
+  name?: string;
+  value: number;
+  required?: boolean;
+  disabled?: boolean;
+  minimum?: number;
+  maximum?: number;
+  error?: string;
+  showError?: boolean;
+  type: 'number' | 'integer';
+  step?: number;
+  oninput?: FormEventHandler<HTMLInputElement>;
+  onValidation?: (value: number, error?: string) => void;
+  class?: string;
+  'aria-label'?: string;
+  'aria-invalid'?: Booleanish | 'grammar' | 'spelling' | null;
+}
+
+let {
+  name,
+  value,
+  required = false,
+  disabled = false,
+  minimum,
+  maximum,
+  error,
+  showError = true,
+  type,
+  step,
+  oninput,
+  onValidation = (_value: number, _error?: string): void => {},
+  class: className = '',
+  'aria-label': ariaLabel,
+  'aria-invalid': ariaInvalid,
+}: Props = $props();
 
 // callback after validation occurs
-export let onValidation = (_value: number, _error?: string): void => {};
+// export let onValidation = (_value: number, _error?: string): void => {};
 
-let minimumEnabled: boolean;
-let maximumEnabled: boolean;
-let valueAsString: string | undefined = undefined;
-$: valueAsString = value !== undefined ? String(value) : undefined;
+let minimumEnabled: boolean | undefined = $state();
path: packages/ui/src/lib/inputs/NumberInput.svelte
user: login: dgolovin
body: I updated it to use $derived().
created_at: 2025-05-14T20:38:15Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-14T03:46:03Z
body: <!-- refined-cf-pages-action:deployment-summary:podman-desktop -->

###### built with [Refined Cloudflare Pages Action](https://github.com/AdrianGonz97/refined-cf-pages-action)
## ⚡ Cloudflare Pages Deployment
| Name | Status | Preview | Last Commit |
| :--- | :----- | :------ | :---------- |
| **podman-desktop-website-pr** | ✅ Ready ([View Log](https://github.com/podman-desktop/podman-desktop/actions/runs/15011601849)) | [Visit Preview](https://dgolovin-i11578.podman-desktop-website-pr.pages.dev) | 3b656acab2a7d28b75ed106eb5732099a621db6c |

author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-05-14T18:21:19Z
body: ## [Codecov](https://app.codecov.io/gh/podman-desktop/podman-desktop/pull/12475?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=podman-desktop) Report
All modified and coverable lines are covered by tests :white_check_mark:

:loudspeaker: Thoughts on this report? [Let us know!](https://github.com/codecov/feedback/issues/255)
author_association: NONE
user: login: dgolovin
created_at: 2025-05-14T19:46:15Z
body: > Cant we use props for
> 
> ```
> export let id: string;
> export let password: string | undefined = undefined;
> export let passwordHidden: boolean = true;
> export let readonly: boolean = false;
> ```

The change would be bigger than just props and it is not in the scope of the issue.
author_association: CONTRIBUTOR