time-calculator-web-component
Version:
A web component for time calculation with expression evaluation
367 lines (290 loc) โข 10.1 kB
Markdown
with TypeScript following Domain-Driven Design (DDD) principles. Supports complex time expressions with proper operator precedence and provides a beautiful, accessible UI.
## ๐ Quick Start
### Installation
```bash
# Install via npm (when published)
npm install time-calculator-web-component
# Or use directly from CDN
<script type="module" src="https://unpkg.com/time-calculator-web-component/dist/index.esm.js"></script>
```
```html
<!DOCTYPE html>
<html>
<head>
<script type="module" src="dist/index.esm.js"></script>
</head>
<body>
<!-- Simple usage -->
<time-calculator open></time-calculator>
<!-- With configuration -->
<time-calculator
mode="popover"
locale="pt-BR"
persist-position="true">
</time-calculator>
</body>
</html>
```
- **Multiple Time Formats**: H:mm (1:30), unit format (1h 30m), pure numbers
- **Arithmetic Operations**: Addition, subtraction, multiplication, division
- **Operator Precedence**: Proper mathematical precedence with parentheses support
- **Context-Aware Numbers**: Numbers interpreted as minutes in +/- operations, scalars in */รท
- **Unlimited Hours**: Support for hours > 23 (e.g., 500:30)
- **Negative Time**: Full support for negative time calculations
### ๐จ User Interface
- **Draggable Window**: Fully draggable floating calculator window
- **Keyboard Navigation**: Complete keyboard-only operation support
- **Multiple Display Modes**: H:mm format or pure minutes
- **Accessibility**: ARIA labels, focus trap, screen reader support
- **Position Persistence**: Optional localStorage position saving
- **Responsive Design**: Works on mobile, tablet, and desktop
### โ๏ธ Technical
- **Framework Agnostic**: Works with React, Vue, Angular, or vanilla HTML
- **TypeScript**: Full TypeScript support with complete type definitions
- **Zero Dependencies**: No runtime dependencies, lightweight bundle
- **DDD Architecture**: Clean domain-driven design with separation of concerns
- **Comprehensive Testing**: >90% test coverage on core functionality
## ๐ Expression Examples
### Basic Operations
```javascript
"10:00 + 0:30" // โ 10:30
"10:00 + 70m" // โ 11:10
"30m + 30m" // โ 1:00
"1h 30m * 2" // โ 3:00
"90 / 3" // โ 0:30 (90 minutes รท 3)
```
### Complex Expressions
```javascript
"(1h + 30m) * 2 - 15m" // โ 2:45
"500:30 + 20:30 - 521:00" // โ 0:00
"-1:30 + 2h" // โ 0:30
"1h 30m ร 2 รท 3" // โ 1:00
```
### Supported Input Formats
- **H:mm Format**: `1:30`, `500:30`, `-2:15`
- **Unit Format**: `1h 30m`, `45m`, `2h`, `90min`
- **Mixed**: `1:30 + 45m`, `2h - 0:15`
- **Pure Numbers**: `90 + 30` (interpreted as minutes in +/-)
## ๐ง API Reference
### Web Component Attributes
| Attribute | Type | Default | Description |
|-----------|------|---------|-------------|
| `open` | boolean | `false` | Opens the calculator |
| `mode` | `"popover"` \| `"fixed"` | `"popover"` | Window behavior mode |
| `locale` | `"pt-BR"` \| `"en-US"` | `"pt-BR"` | Locale for number parsing |
| `decimal-separator` | `","` \| `"."` | auto | Decimal separator override |
| `rounding` | `"floor"` \| `"ceil"` \| `"halfUp"` | `"halfUp"` | Rounding mode for division |
| `allow-negative` | boolean | `true` | Allow negative results |
| `max-abs-minutes` | number | `null` | Maximum absolute minutes |
| `close-on-outside-click` | boolean | auto | Close when clicking outside |
| `persist-position` | boolean | `false` | Save position to localStorage |
### JavaScript API
```javascript
// Get the web component
const calculator = document.querySelector('time-calculator');
// Control visibility
calculator.open();
calculator.close();
calculator.toggle();
// Set input and evaluate
calculator.setInput('1h 30m + 45m');
calculator.evaluate();
// Get results
const result = calculator.getInput();
console.log(result); // "1h 30m + 45m"
```
```javascript
import { evaluateExpression, ExpressionEvaluator } from 'time-calculator-web-component';
// Quick evaluation
const result = evaluateExpression('1h 30m + 45m');
console.log(result.formatted); // "2:15"
console.log(result.minutes); // 135
// Advanced usage with options
const evaluator = new ExpressionEvaluator({
locale: 'pt-BR',
rounding: 'ceil',
allowNegative: false
});
const result = evaluator.evaluateExpression('1h 30m * 2,5');
console.log(result.formatted); // "3:45"
```
- **`popover`** (default): Closes when clicking outside, floating behavior
- **`fixed`**: Stays open when clicking outside, modal-like behavior
- **`pt-BR`**: Portuguese (Brazil) - comma as decimal separator
- **`en-US`**: English (US) - dot as decimal separator
- **`halfUp`** (default): Round to nearest minute (0.5 โ 1)
- **`floor`**: Always round down (0.9 โ 0)
- **`ceil`**: Always round up (0.1 โ 1)
## โจ๏ธ Keyboard Shortcuts
When the calculator is focused:
| Shortcut | Action |
|----------|--------|
| `Enter` | Evaluate expression |
| `Esc` | Close calculator |
| `Ctrl+M` / `Cmd+M` | Toggle display mode |
| `Ctrl+C` / `Cmd+C` | Copy result |
| `Ctrl+Shift+C` | Copy result as minutes |
| `Ctrl+Z` / `Cmd+Z` | Undo |
| `Ctrl+Y` / `Cmd+Y` | Redo |
## ๐จ Customization
### CSS Classes
The component exposes several CSS classes for styling:
```css
.time-calc-container { /* Main container */ }
.time-calc-header { /* Header with title and buttons */ }
.time-calc-body { /* Body with input and result */ }
.time-calc-input { /* Input field */ }
.time-calc-result { /* Result display */ }
.time-calc-button { /* Header buttons */ }
.time-calc-overlay { /* Background overlay */ }
```
```html
<time-calculator
container-class="my-custom-container"
header-class="my-custom-header"
input-class="my-custom-input">
</time-calculator>
```
The component uses CSS custom properties for theming:
```css
time-calculator {
--calc-bg-color:
--calc-border-color:
--calc-text-color:
--calc-accent-color:
}
```
```jsx
import 'time-calculator-web-component';
function App() {
const [isOpen, setIsOpen] = useState(false);
return (
<time-calculator
open={isOpen}
onClose={() => setIsOpen(false)}
locale="en-US"
/>
);
}
```
```vue
<template>
<time-calculator
:open="isOpen"
@close="isOpen = false"
locale="pt-BR"
/>
</template>
<script setup>
import 'time-calculator-web-component';
const isOpen = ref(false);
</script>
```
```typescript
// app.module.ts
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import 'time-calculator-web-component';
@NgModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA],
// ...
})
// component.html
<time-calculator
[ ]="isOpen"
(close)="isOpen = false">
</time-calculator>
```
The calculator uses a formal grammar for expression parsing:
```ebnf
expr := term (("+" | "-") term)*
term := factor (("*" | "/") factor)*
factor := group | duration | scalarOrMinutes | unary
group := "(" expr ")"
duration := TIME_HMM
| NUMBER "h" [" " NUMBER "m"]
| NUMBER "m"
scalarOrMinutes := NUMBER
unary := ("+" | "-") factor
```
- **TIME_HMM**: `-?\d+:[0-5]\d` (unlimited hours, valid minutes)
- **NUMBER**: Integer or decimal (locale-aware separator)
- **UNITS**: `h|hora|horas|m|min|minuto|minutos` (normalized)
- **OPERATORS**: `+`, `-`, `*`, `/`, `ร`, `รท`, `(`, `)`
## ๐ Error Handling
The calculator provides detailed error messages:
```javascript
try {
const result = evaluateExpression('1h + ');
} catch (error) {
console.log(error.message); // "Expected time, duration, or number"
}
```
Common errors:
- **Syntax errors**: Invalid expression format
- **Division by zero**: Attempting to divide by 0
- **Overflow errors**: Result exceeds `maxAbsMinutes` limit
- **Invalid time format**: Malformed time input (e.g., "1:60")
## ๐ Development
### Building from Source
```bash
# Clone and install
git clone <repository-url>
cd time-calculator
yarn install
# Build
yarn build # or: npx rollup -c
# Run tests
yarn test # or: npx jest
# Serve demo
yarn serve # or: python3 -m http.server 8000
```
### Project Structure
```
src/
โโโ domain/ # Core domain logic (DDD)
โ โโโ types.ts # Type definitions
โ โโโ time-amount.ts # TimeAmount entity
โ โโโ lexer.ts # Expression lexer
โ โโโ parser.ts # Expression parser
โ โโโ evaluator.ts # Expression evaluator
โ โโโ formatters.ts # Output formatters
โโโ ui/ # User interface
โ โโโ controller.ts # UI controller
โ โโโ web-component.ts # Web component
โโโ utils/ # Utilities
โ โโโ event-emitter.ts
โ โโโ focus-trap.ts
โ โโโ storage.ts
โโโ __tests__/ # Test files
```
### Architecture Principles
- **Domain-Driven Design**: Clear separation between domain logic and UI
- **Framework Agnostic**: Core logic independent of UI framework
- **Type Safety**: Full TypeScript coverage with strict types
- **Testability**: High test coverage with isolated unit tests
- **Accessibility**: WCAG 2.1 AA compliance
## ๐ License
MIT License - see [LICENSE](LICENSE) file for details.
## ๐ค Contributing
Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) for details on our code of conduct and development process.
## ๐ Issues
Found a bug or have a feature request? Please open an issue on our [GitHub Issues](https://github.com/your-username/time-calculator/issues) page.
---
Made with โค๏ธ by gustavodamazio
A framework-agnostic time calculator web component built