# UnitNumberEditor

A specialized numeric input component with unit conversion functionality. Allows users to input numbers with different units (like kg, lb, cm, inch) and automatically converts between them. Features a dropdown unit selector and automatic ratio-based conversions.

## Installation

```bash
npm install @ticatec/uniface-element
```

## Import

```typescript
import UnitNumberEditor, { type UnitOption } from "@ticatec/uniface-element/UnitNumberEditor";
```

## Basic Usage

```svelte
<script>
  import UnitNumberEditor, { type UnitOption } from "@ticatec/uniface-element/UnitNumberEditor";
  
  let weightValue = 75; // Standard unit value (kg)
  let weightUnit = 'kg';
  
  const weightUnits: UnitOption[] = [
    { code: 'kg', text: 'Kilograms', ratio: 1, precision: 2 },
    { code: 'lb', text: 'Pounds', ratio: 2.205, precision: 2 },
    { code: 'g', text: 'Grams', ratio: 1000, precision: 0 }
  ];
</script>

<UnitNumberEditor 
  bind:value={weightValue}
  bind:unitCode={weightUnit}
  units={weightUnits}
  placeholder="Enter weight"
/>
```

## Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `value` | `number \| null` | Required | Numeric value in standard unit |
| `unitCode` | `string` | Required | Current selected unit code |
| `units` | `Array<UnitOption>` | Required | Available unit options |
| `disabled` | `boolean` | `false` | Whether the input is disabled |
| `readonly` | `boolean` | `false` | Whether the input is read-only |
| `variant` | `"" \| "plain" \| "outlined" \| "filled"` | `""` | Input visual variant |
| `compact` | `boolean` | `false` | Whether to use compact spacing |
| `style` | `string` | `""` | Additional CSS styles |
| `prefix` | `string` | `""` | Text prefix to display |
| `allowNegative` | `boolean` | `false` | Whether to allow negative numbers |
| `displayMode` | `DisplayMode` | `DisplayMode.Edit` | Display mode (Edit/View) |
| `removable` | `boolean` | `true` | Whether to show clear button |
| `menu$height` | `number` | `0` | Fixed height for unit dropdown |
| `placeholder` | `string` | `""` | Placeholder text |
| `class` | `string` | `""` | CSS class name |
| `onchange` | `OnChangeHandler<number \| null>` | `null` | Callback when value changes |
| `onfocus` | `((event: FocusEvent) => void) \| null` | `null` | Callback when input gains focus |
| `onblur` | `((event: FocusEvent) => void) \| null` | `null` | Callback when input loses focus |

## UnitOption Interface

```typescript
interface UnitOption {
  code: string;        // Unit identifier (e.g., 'kg', 'lb')
  text?: string;       // Display name (e.g., 'Kilograms')
  ratio?: number;      // Conversion ratio to standard unit
  precision?: number;  // Decimal places for this unit
  max?: number;        // Maximum value for this unit
  min?: number;        // Minimum value for this unit
}
```

## Methods

| Method | Description |
|--------|-------------|
| `setFocus()` | Programmatically focus the input |

## Examples

### Weight Converter

```svelte
<script>
  import UnitNumberEditor, { type UnitOption } from "@ticatec/uniface-element/UnitNumberEditor";
  
  let weight = 70; // 70 kg as standard unit
  let weightUnit = 'kg';
  
  const weightUnits: UnitOption[] = [
    { code: 'kg', text: 'Kilograms', ratio: 1, precision: 2 },
    { code: 'lb', text: 'Pounds', ratio: 2.20462, precision: 2 },
    { code: 'g', text: 'Grams', ratio: 1000, precision: 0 },
    { code: 'oz', text: 'Ounces', ratio: 35.274, precision: 2 }
  ];
  
  function handleWeightChange(newWeight) {
    console.log(`Weight changed: ${newWeight} kg`);
  }
</script>

<div class="converter">
  <label>Weight:</label>
  <UnitNumberEditor 
    bind:value={weight}
    bind:unitCode={weightUnit}
    units={weightUnits}
    onchange={handleWeightChange}
    placeholder="Enter weight"
  />
</div>

<style>
  .converter {
    margin-bottom: 16px;
  }
  
  label {
    display: block;
    margin-bottom: 4px;
    font-weight: 500;
  }
</style>
```

### Distance Measurement

```svelte
<script>
  import UnitNumberEditor, { type UnitOption } from "@ticatec/uniface-element/UnitNumberEditor";
  
  let distance = null;
  let distanceUnit = 'm';
  
  const distanceUnits: UnitOption[] = [
    { code: 'm', text: 'Meters', ratio: 1, precision: 2 },
    { code: 'ft', text: 'Feet', ratio: 3.28084, precision: 2 },
    { code: 'in', text: 'Inches', ratio: 39.3701, precision: 1 },
    { code: 'cm', text: 'Centimeters', ratio: 100, precision: 0 },
    { code: 'mm', text: 'Millimeters', ratio: 1000, precision: 0 }
  ];
</script>

<div class="form-group">
  <label>Distance:</label>
  <UnitNumberEditor 
    bind:value={distance}
    bind:unitCode={distanceUnit}
    units={distanceUnits}
    placeholder="Enter distance"
  />
</div>

{#if distance !== null}
  <div class="conversion-display">
    <h4>Conversions:</h4>
    {#each distanceUnits as unit}
      <p>
        {unit.text}: {distance !== null ? (distance * (unit.ratio ?? 1)).toFixed(unit.precision ?? 2) : 'N/A'} {unit.code}
      </p>
    {/each}
  </div>
{/if}

<style>
  .form-group {
    margin-bottom: 16px;
  }
  
  .conversion-display {
    background: #f8f9fa;
    padding: 16px;
    border-radius: 4px;
    margin-top: 16px;
  }
  
  .conversion-display h4 {
    margin: 0 0 8px 0;
  }
  
  .conversion-display p {
    margin: 4px 0;
    font-family: monospace;
  }
</style>
```

### Temperature Converter

```svelte
<script>
  import UnitNumberEditor, { type UnitOption } from "@ticatec/uniface-element/UnitNumberEditor";
  
  let temperature = 20; // 20°C as standard
  let tempUnit = 'C';
  
  // Note: Temperature conversions need special handling due to offset-based conversion
  // This example shows a simplified approach - real implementation would need custom conversion logic
  const temperatureUnits: UnitOption[] = [
    { code: 'C', text: '°Celsius', ratio: 1, precision: 1 },
    { code: 'F', text: '°Fahrenheit', ratio: 1.8, precision: 1 }, // Simplified - doesn't account for +32 offset
    { code: 'K', text: 'Kelvin', ratio: 1, precision: 2 } // Simplified - doesn't account for +273.15 offset
  ];
  
  // For accurate temperature conversion, you'd implement custom conversion logic
  function convertTemperature(value, fromUnit, toUnit) {
    if (value === null) return null;
    
    // Convert to Celsius first
    let celsius = value;
    if (fromUnit === 'F') celsius = (value - 32) / 1.8;
    if (fromUnit === 'K') celsius = value - 273.15;
    
    // Convert from Celsius to target unit
    if (toUnit === 'F') return celsius * 1.8 + 32;
    if (toUnit === 'K') return celsius + 273.15;
    return celsius;
  }
</script>

<div class="form-group">
  <label>Temperature:</label>
  <UnitNumberEditor 
    bind:value={temperature}
    bind:unitCode={tempUnit}
    units={temperatureUnits}
    placeholder="Enter temperature"
    allowNegative={true}
  />
</div>
```

### With Event Handlers

```svelte
<script>
  import UnitNumberEditor, { type UnitOption } from "@ticatec/uniface-element/UnitNumberEditor";
  
  let value = null;
  let unit = 'kg';
  let isFocused = false;
  
  const units: UnitOption[] = [
    { code: 'kg', text: 'Kilograms', ratio: 1, precision: 2 },
    { code: 'lb', text: 'Pounds', ratio: 2.20462, precision: 2 }
  ];
  
  function handleChange(newValue) {
    console.log('Value changed:', newValue);
  }
  
  function handleFocus(event) {
    console.log('Input focused');
    isFocused = true;
  }
  
  function handleBlur(event) {
    console.log('Input blurred');
    isFocused = false;
  }
</script>

<UnitNumberEditor 
  bind:value
  bind:unitCode={unit}
  {units}
  onchange={handleChange}
  onfocus={handleFocus}
  onblur={handleBlur}
  placeholder="Enter value"
/>

{#if isFocused}
  <p>Input is currently focused</p>
{/if}
```

### Different Variants

```svelte
<script>
  import UnitNumberEditor, { type UnitOption } from "@ticatec/uniface-element/UnitNumberEditor";
  
  let value1 = 100, value2 = 200, value3 = 300;
  let unit1 = 'm', unit2 = 'm', unit3 = 'm';
  
  const units: UnitOption[] = [
    { code: 'm', text: 'Meters', ratio: 1, precision: 2 },
    { code: 'ft', text: 'Feet', ratio: 3.28084, precision: 2 }
  ];
</script>

<!-- Default variant -->
<UnitNumberEditor bind:value={value1} bind:unitCode={unit1} {units} placeholder="Default" />

<!-- Outlined variant -->
<UnitNumberEditor bind:value={value2} bind:unitCode={unit2} {units} variant="outlined" placeholder="Outlined" />

<!-- Filled variant -->
<UnitNumberEditor bind:value={value3} bind:unitCode={unit3} {units} variant="filled" placeholder="Filled" />
```

### With Min/Max Constraints

```svelte
<script>
  import UnitNumberEditor, { type UnitOption } from "@ticatec/uniface-element/UnitNumberEditor";
  
  let age = null;
  let ageUnit = 'years';
  
  const ageUnits: UnitOption[] = [
    { code: 'years', text: 'Years', ratio: 1, precision: 0, min: 0, max: 150 },
    { code: 'months', text: 'Months', ratio: 12, precision: 0, min: 0, max: 1800 },
    { code: 'days', text: 'Days', ratio: 365.25, precision: 0, min: 0, max: 54750 }
  ];
</script>

<div class="form-group">
  <label>Age:</label>
  <UnitNumberEditor 
    bind:value={age}
    bind:unitCode={ageUnit}
    units={ageUnits}
    placeholder="Enter age"
  />
</div>

{#if age !== null}
  <p>Age: {age} {ageUnits.find(u => u.code === ageUnit)?.text?.toLowerCase()}</p>
{/if}
```

### Disabled and Readonly States

```svelte
<script>
  import UnitNumberEditor, { type UnitOption } from "@ticatec/uniface-element/UnitNumberEditor";
  
  let normalValue = 50;
  let readonlyValue = 75;
  let disabledValue = null;
  let unit = 'kg';
  
  const units: UnitOption[] = [
    { code: 'kg', text: 'Kilograms', ratio: 1, precision: 2 },
    { code: 'lb', text: 'Pounds', ratio: 2.20462, precision: 2 }
  ];
</script>

<!-- Normal input -->
<UnitNumberEditor bind:value={normalValue} bind:unitCode={unit} {units} placeholder="Normal" />

<!-- Readonly input -->
<UnitNumberEditor bind:value={readonlyValue} bind:unitCode={unit} {units} readonly />

<!-- Disabled input -->
<UnitNumberEditor bind:value={disabledValue} bind:unitCode={unit} {units} disabled placeholder="Disabled" />
```

### Recipe Ingredient Converter

```svelte
<script>
  import UnitNumberEditor, { type UnitOption } from "@ticatec/uniface-element/UnitNumberEditor";
  import Button from "@ticatec/uniface-element/Button";
  
  let ingredients = [
    { name: 'Flour', amount: 500, unit: 'g' },
    { name: 'Sugar', amount: 200, unit: 'g' },
    { name: 'Milk', amount: 250, unit: 'ml' }
  ];
  
  const weightUnits: UnitOption[] = [
    { code: 'g', text: 'Grams', ratio: 1, precision: 0 },
    { code: 'kg', text: 'Kilograms', ratio: 0.001, precision: 3 },
    { code: 'oz', text: 'Ounces', ratio: 0.035274, precision: 2 },
    { code: 'lb', text: 'Pounds', ratio: 0.00220462, precision: 4 }
  ];
  
  const volumeUnits: UnitOption[] = [
    { code: 'ml', text: 'Milliliters', ratio: 1, precision: 0 },
    { code: 'l', text: 'Liters', ratio: 0.001, precision: 3 },
    { code: 'cup', text: 'Cups', ratio: 0.00422675, precision: 3 },
    { code: 'tbsp', text: 'Tablespoons', ratio: 0.067628, precision: 2 }
  ];
  
  function getUnitsForIngredient(name) {
    return name.toLowerCase().includes('milk') ? volumeUnits : weightUnits;
  }
  
  function addIngredient() {
    ingredients = [...ingredients, { name: '', amount: null, unit: 'g' }];
  }
  
  function removeIngredient(index) {
    ingredients = ingredients.filter((_, i) => i !== index);
  }
</script>

<div class="recipe-converter">
  <h3>Recipe Ingredient Converter</h3>
  
  {#each ingredients as ingredient, index}
    <div class="ingredient-row">
      <input 
        bind:value={ingredient.name} 
        placeholder="Ingredient name"
        class="ingredient-name"
      />
      <UnitNumberEditor 
        bind:value={ingredient.amount}
        bind:unitCode={ingredient.unit}
        units={getUnitsForIngredient(ingredient.name)}
        placeholder="Amount"
        compact
      />
      <Button 
        label="×" 
        onClick={() => removeIngredient(index)}
        style="width: 32px; height: 32px;"
      />
    </div>
  {/each}
  
  <Button label="Add Ingredient" onClick={addIngredient} />
</div>

<style>
  .recipe-converter {
    max-width: 600px;
    padding: 20px;
    border: 1px solid #ddd;
    border-radius: 8px;
  }
  
  .ingredient-row {
    display: grid;
    grid-template-columns: 1fr 1fr auto;
    gap: 12px;
    align-items: center;
    margin-bottom: 12px;
  }
  
  .ingredient-name {
    padding: 8px 12px;
    border: 1px solid #ccc;
    border-radius: 4px;
    font-size: 14px;
  }
  
  h3 {
    margin-bottom: 20px;
  }
</style>
```

### Programmatic Focus

```svelte
<script>
  import UnitNumberEditor, { type UnitOption } from "@ticatec/uniface-element/UnitNumberEditor";
  import Button from "@ticatec/uniface-element/Button";
  
  let unitEditor;
  let value = null;
  let unit = 'kg';
  
  const units: UnitOption[] = [
    { code: 'kg', text: 'Kilograms', ratio: 1, precision: 2 },
    { code: 'lb', text: 'Pounds', ratio: 2.20462, precision: 2 }
  ];
  
  function focusInput() {
    unitEditor.setFocus();
  }
</script>

<Button label="Focus Input" onClick={focusInput} />

<UnitNumberEditor 
  bind:this={unitEditor}
  bind:value
  bind:unitCode={unit}
  {units}
  placeholder="Click button to focus me"
/>
```

## Features

- **Automatic Conversion**: Seamless conversion between different units based on ratio
- **Flexible Precision**: Each unit can have its own decimal precision
- **Validation**: Min/max constraints per unit with automatic validation
- **Clear Functionality**: Built-in clear button to reset values
- **Focus Management**: Proper focus handling for accessibility
- **Custom Styling**: Multiple visual variants and custom CSS support
- **Unit Dropdown**: Interactive dropdown for unit selection
- **Negative Numbers**: Optional support for negative values
- **Prefix Support**: Add text prefixes for enhanced context

## Unit Conversion Logic

The component uses a ratio-based conversion system:

1. **Standard Unit**: One unit should have `ratio: 1` as the base unit
2. **Conversion**: Other units multiply by their ratio to convert to the standard unit
3. **Display**: Values are automatically converted for display in the selected unit
4. **Storage**: The `value` prop always contains the value in the standard unit

Example:
```typescript
// If kg is standard (ratio: 1) and lb has ratio: 2.20462
// User enters 100 lb → stored as ~45.36 kg
// User switches to kg → displays ~45.36 kg
```

## Accessibility

- Proper keyboard navigation between input and dropdown
- Screen reader compatible with semantic HTML
- Focus indicators for interactive elements
- ARIA attributes for dropdown accessibility
- Clear visual feedback for interaction states

## Best Practices

1. **Standard Unit**: Always define one unit with `ratio: 1` as your base unit
2. **Precision**: Set appropriate precision for each unit (whole numbers vs decimals)
3. **Constraints**: Use min/max values to prevent invalid inputs
4. **Unit Names**: Provide clear, descriptive unit names in the `text` property
5. **Conversion Logic**: For complex conversions (like temperature), implement custom logic
6. **Validation**: Add additional validation in change handlers if needed

## Browser Support

- Modern browsers with full input event support
- Compatible with Svelte 5+
- Keyboard navigation support
- Touch-friendly dropdown interface
- Full TypeScript support

## Related Components

- `NumberEditor` - Basic numeric input without units
- `TextEditor` - Text input component
- `OptionsSelect` - Dropdown selection component
- `TimeEditor` - Time input with time units