# semantic-duration-input > Vue 3 input component that parses human-friendly durations ("2h", "3 days", "1h30", "1:30", "PT1H30M") into minutes, seconds, milliseconds or ISO 8601, plus a framework-free parser and formatter. Key facts: - Needs Vue 3.5+. Install with `npm install semantic-duration-input` and use `import { DurationInput } from 'semantic-duration-input'`. - `v-model` holds whole minutes by default, and `null` when the field is empty. `valueFormat` switches it to `'seconds'`, `'ms'`, `'iso'` (`'PT1H30M'`) or a custom `{ toModel, fromModel }`. - Numeric `min`, `max`, `step` and `presets` are in the model's unit. Duration text such as `'8h'` works with every format. - The core functions work in seconds: `parseDuration` returns `{ ok: true, seconds, minutes }` or `{ ok: false, error, token?, index?, suggestion? }`, and `formatDuration`, `toIso` and the `min`/`max` parse options take seconds. They are also exported without Vue from `semantic-duration-input/core`. - Built-in locales: `en` and `de` are accepted by default. `es`, `fr`, `pt`, `hi`, `ar` and `zh` are exported too and must be passed in `locales` (e.g. `:locales="[fr, en]"`); the first one, or the `locale` prop, sets the language of the normalized text and error messages. - A bare number (`45`) is a `missing_unit` error unless `defaultUnit` is set. Seconds (`30s`) are only accepted with `precision="second"`. - Error codes: `empty`, `invalid_format`, `unknown_unit`, `missing_unit`, `out_of_range`. - Styles: with Tailwind v4, `@import 'semantic-duration-input/tailwind.css';` after `@import 'tailwindcss';`. Without Tailwind, `import 'semantic-duration-input/style.css'`. Neither is needed when rendering another input with `as` or through the renderless default slot. - UI libraries: `:as="Input"` renders shadcn-vue, Nuxt UI, Vuetify or PrimeVue inputs, with the `nuxtUi`, `vuetify` and `primevue` presets for `invalid-props`. - Nuxt: add `'semantic-duration-input/nuxt'` to `modules`. It auto-imports the component and adds the stylesheet. - Forms: `name` submits the model value through a hidden input. `durationSchema()` is a Standard Schema for Valibot, ArkType, TanStack Form, VeeValidate and others. The whole documentation is in [llms-full.txt](https://nicoalbrecht.github.io/semantic-duration-input/llms-full.txt). The npm package ships both files for its version in `node_modules/semantic-duration-input/dist/`. --- # Getting started ## Install ```sh npm install semantic-duration-input ``` Vue 3.5 or newer is required. ## Use it ```vue ``` `v-model` holds whole minutes, or `null` when the field is empty. To store seconds, milliseconds or ISO 8601 instead, see [Value formats](https://nicoalbrecht.github.io/semantic-duration-input/guide/value-formats.md). ## Add the styles **With Tailwind v4**, import the Tailwind entry after Tailwind. It lets Tailwind generate the component's classes and adds the design tokens: ```css @import 'tailwindcss'; @import 'semantic-duration-input/tailwind.css'; ``` **Without Tailwind**, import the prebuilt stylesheet. It contains only the utilities the component uses, with no preflight or reset, and is wrapped in cascade layers so your own CSS wins: ```ts import 'semantic-duration-input/style.css' ``` You don't need either stylesheet if you render your own input with [`as`](https://nicoalbrecht.github.io/semantic-duration-input/guide/integrations.md) or the [renderless slot](https://nicoalbrecht.github.io/semantic-duration-input/guide/integrations.md#any-other-component-renderless-slot). See [Styling](https://nicoalbrecht.github.io/semantic-duration-input/guide/styling.md) for theming. ## Register globally ```ts import { createApp } from 'vue' import { plugin as DurationInputPlugin } from 'semantic-duration-input' createApp(App).use(DurationInputPlugin).mount('#app') ``` The plugin registers `` and optionally takes [app-wide defaults](https://nicoalbrecht.github.io/semantic-duration-input/guide/integrations.md#app-wide-defaults). In Nuxt, use the [Nuxt module](https://nicoalbrecht.github.io/semantic-duration-input/guide/nuxt.md) instead. ## Entry points | Import | Contents | | --- | --- | | `semantic-duration-input` | The component, `plugin`, `useDurationInput`, the `invalidProps` presets, the theme, and everything from `/core` | | `semantic-duration-input/core` | Parser, formatter, locales and `durationSchema`, without Vue or Tailwind ([Headless usage](https://nicoalbrecht.github.io/semantic-duration-input/guide/headless.md)) | | `semantic-duration-input/nuxt` | The [Nuxt module](https://nicoalbrecht.github.io/semantic-duration-input/guide/nuxt.md) | | `semantic-duration-input/style.css` | Prebuilt stylesheet | | `semantic-duration-input/tailwind.css` | Entry for Tailwind v4 projects | ## For AI coding agents The docs are available as Markdown: [`llms.txt`](https://nicoalbrecht.github.io/semantic-duration-input/llms.txt) lists the key facts and pages, and [`llms-full.txt`](https://nicoalbrecht.github.io/semantic-duration-input/llms-full.txt) contains every page. Append `.md` to a page's URL for its Markdown version. The npm package ships both files for its version in `node_modules/semantic-duration-input/dist/`. To teach your agent the setup and the usual pitfalls, install the skill: ```sh npx skills add nicoalbrecht/semantic-duration-input ``` --- # Syntax Case and extra whitespace are ignored everywhere. ## Units Seconds (only with `precision="second"`), minutes, hours, days and weeks. Months and years are left out because their length varies. `m` always means minutes. | Unit | English | German | | --- | --- | --- | | second | `s` `sec` `secs` `second` `seconds` | `s` `sek` `sekunde` `sekunden` | | minute | `m` `min` `mins` `minute` `minutes` | `m` `min` `minute` `minuten` | | hour | `h` `hr` `hrs` `hour` `hours` | `h` `std` `stunde` `stunden` | | day | `d` `day` `days` | `d` `t` `tag` `tage` `tagen` | | week | `w` `wk` `wks` `week` `weeks` | `w` `wo` `woche` `wochen` | The other built-in locales are accepted when you pass them in the `locales` prop, e.g. `:locales="[fr, en]"`. Each of them also accepts `h`, `m`, `min`, `s`, `d` and `w`. | Unit | Spanish (`es`) | French (`fr`) | Portuguese (`pt`) | | --- | --- | --- | --- | | second | `seg` `segs` `segundo` `segundos` | `sec` `secs` `seconde` `secondes` | `seg` `segs` `segundo` `segundos` | | minute | `mins` `minuto` `minutos` | `mn` `mins` `minute` `minutes` | `mins` `minuto` `minutos` | | hour | `hr` `hrs` `hora` `horas` | `heure` `heures` | `hr` `hrs` `hora` `horas` | | day | `día` `días` `dia` `dias` | `j` `jour` `jours` | `dia` `dias` | | week | `sem` `semana` `semanas` | `sem` `semaine` `semaines` | `sem` `semana` `semanas` | | Unit | Hindi (`hi`) | Arabic (`ar`) | Chinese (`zh`) | | --- | --- | --- | --- | | second | `से` `सेकंड` `सेकेंड` `सेकण्ड` | `ث` `ثانية` `ثانيتان` `ثوان` `ثواني` | `秒` `秒钟` | | minute | `मि` `मिनट` `मिनिट` | `د` `دقيقة` `دقيقتان` `دقائق` | `分` `分钟` `分鐘` | | hour | `घं` `घंटा` `घंटे` `घण्टा` `घण्टे` | `س` `ساعة` `ساعتان` `ساعات` | `时` `小时` `个小时` `钟头` `小時` `個小時` `鐘頭` | | day | `दि` `दिन` | `ي` `يوم` `يومان` `أيام` `يومًا` | `天` `日` | | week | `सप्ताह` `हफ़्ता` `हफ़्ते` | `أ` `أسبوع` `أسبوعان` `أسابيع` `أسبوعًا` | `周` `星期` `个星期` `礼拜` `週` `禮拜` | Arabic also accepts the forms after `ين` (`ساعتين`), and spellings without hamza or with `ه` for `ة` (`اسبوع`, `ساعه`). Which names are accepted depends on the `locales` prop (English and German by default). To add a language or more names, see [Locales](https://nicoalbrecht.github.io/semantic-duration-input/guide/locales.md). With the default minute precision, seconds are not a unit, so `30s` is an `unknown_unit` error. ## Compound values `1d 2h 30min`, `2d4h`, `1 hour, 30 minutes`. Parts can be separated by spaces, `,`, `+` or `&`, and by the separator words of the active locales (`and`, `und`, `y`, `et`, `e`, `और`, `و`, `和`). Unit names may end with a period, as in `2 Std. 30 Min.` Chinese is written without spaces, so its separators (`和`, `又`, `零`) may be attached to the unit before them: `1小时零5分钟`. Chinese and Japanese separators in custom locales work the same way. ## Implicit units A single trailing number takes the next smaller unit: `1h30` is 1h 30min and `1d 4` is 1d 4h. - Only one trailing number is allowed: `1h 30 15` is a `missing_unit` error. - The "next smaller unit" depends on the precision. `1m 30` is 1m 30s with `precision="second"`, and a `missing_unit` error otherwise. - [Custom units](https://nicoalbrecht.github.io/semantic-duration-input/guide/units.md#implicit-units) never take the trailing number: it goes to the next smaller built-in unit. - Turn it off with `:implicit-units="false"`. ## Decimals `1.5h` or `1,5h`. Results are rounded to whole minutes, or to whole seconds with `precision="second"`. ## Digits Besides `0`–`9`, Arabic-Indic (`٣٠`), Persian (`۳۰`), Devanagari (`३०`) and full-width digits (`30`) are accepted, in every locale. So are the Arabic decimal separator `٫`, the commas `،` `、` `,`, and the full-width `:` and `+`: `١٫٥ ساعة`, `1小时,30分钟`, `1:30`. Error tokens show the input as typed. ## Clock format `h:mm`, e.g. `1:30` or `26:05`. With `precision="second"`, `h:mm:ss` works too. Minutes and seconds need two digits between `00` and `59`, so `1:5` is invalid. ## ISO 8601 `PT1H30M`, `P1DT2H` or `P2W`, e.g. pasted from an API. Lowercase, decimals (`PT1,5H`) and weeks together with days (`P1W2D`) are accepted. Years and months (`P1Y`, `P1M`) are not. Seconds are accepted in ISO input even at minute precision, and they get rounded: `PT30S` becomes 1 minute. ## Bare numbers A number without a unit (`45`) is rejected with `missing_unit`, because it's ambiguous. Set `default-unit="minute"` to accept it: ## Typos Unknown units get a suggestion from the unit names of the active locales and precision: `2 huors` gives *Unknown unit "huors". Did you mean "hours"?* Words shorter than 5 characters may be one edit away from a unit name, longer ones two. Names shorter than 3 characters (`h`, `d`, ...) are never suggested. ## Errors | Code | When | | --- | --- | | `empty` | Nothing entered, with `required` | | `invalid_format` | Not a duration at all (`hello`, `1:5`, `P1Y`), a stray word (`2h foo`), or longer than 256 characters | | `unknown_unit` | A number followed by an unknown word (`5 parsecs`) | | `missing_unit` | A number without a unit (`45`, `1h 30 15`) | | `out_of_range` | Below `min` or above `max` | --- # Behaviour ## Typing and committing - Valid input updates `v-model` while you type. Invalid input leaves it unchanged. - Clearing the field sets `v-model` to `null`. This also happens with `required`, which additionally reports an `empty` error. - On blur and Enter, valid text is rewritten to a normalized form: `189h` becomes `7d 21h`. - If `v-model` changes from outside, the text is reformatted. ## When errors appear When an error is shown: - The input gets `aria-invalid="true"` and `data-error=""`. - The root gets `data-invalid`. - A message appears below the field, linked through `aria-describedby`. - The `error` event fires with the code, and again with `null` once the error clears. `validateOn` controls when that happens: | `validateOn` | Behaviour | | --- | --- | | `'eager'` (default) | Not while you're still typing. The error appears on blur or Enter. While it's shown, it updates live, so fixing the input clears it right away. | | `'blur'` | The shown error only changes on blur or Enter. Stepping, reverting and outside changes to `v-model` also clear it. | | `'input'` | The error updates on every keystroke. | Whatever the mode, calling the exposed `validate()` shows the current error, e.g. on submit. ## Preview With `preview`, the normalized value appears inside the field as you type (`= 1h 30min`), but only when it differs from what you typed. It's also announced to screen readers through a polite live region, after a 600 ms pause in typing so not every keystroke is read out. `preview` only works with the built-in field. With [`as`](https://nicoalbrecht.github.io/semantic-duration-input/guide/integrations.md), use the `preview` slot prop of the [renderless slot](https://nicoalbrecht.github.io/semantic-duration-input/guide/integrations.md#any-other-component-renderless-slot) instead. ### Custom preview `formatPreview` renders the preview yourself. It gets the duration in seconds and a context with the default preview (`normalized`), the typed `text`, the model `value` and the display `locale`: ```vue ``` Its output is shown and announced as is, without the leading `= `. The preview still only appears while the text differs from its normalized form, so it goes away on blur. Return `null` (or an empty string) to hide it for a value. `formatPreview` changes the `preview` slot prop and the `preview` of `useDurationInput` too. It only affects the preview: the text is still normalized with `displayStyle` and `displayUnits` on blur. ## Keyboard | Key | Action | | --- | --- | | ↑ / ↓ | Step by `step` (default 15min), snapping to multiples: `1h07` → `1h15` / `1h` | | Shift + ↑ / ↓ | Step by 1 hour, snapping to whole hours | | PgUp / PgDn | Step by 1 day (as configured in [`customUnits`](https://nicoalbrecht.github.io/semantic-duration-input/guide/units.md#working-time)), snapping to whole days | | Enter | Normalize and show errors. The form still submits, unless a preset is highlighted in the open menu, in which case that preset is picked | | Esc | Close the presets menu, or revert to the last committed value. When reverting, the event only stops propagating if something was reverted, so closing a dialog still works | | Alt + ↓ | Open the presets menu | Details: - Steps stay within `min`/`max` and never go below zero. - If the text is invalid, stepping starts from the last valid value. From an empty field, ↑ gives one step and ↓ gives zero. - `:step="false"` turns all stepping off, including Shift and PgUp/PgDn. - Keys pressed with Ctrl, Cmd or Alt are left alone (apart from Alt+↓), so browser shortcuts keep working. ### Snapping and clamping - `snapToStep` rounds to the nearest `step` on blur and Enter, then keeps the result within `min`/`max`. - `clamp` keeps values within `min`/`max` instead of reporting `out_of_range`. It applies while typing and on commit. ## Presets `presets` shows suggestions in a menu below the field. The input is an ARIA combobox. - **Opens** on focus or click while the field is empty, on Alt+↓, and while typing, filtered by label. - **While it's open:** the arrow keys move through the options, Enter picks the highlighted one, and Esc closes it. Tab and blur close it too. ```vue ``` Each entry is one of: - duration text, - a number in the model's unit, - `{ label, value }`. Entries that can't be parsed are dropped. Style the menu through `ui.menu` and `ui.option`; the highlighted option has `data-active`. The built-in menu is not rendered with `as`. To show presets with your own component, use the [renderless slot](https://nicoalbrecht.github.io/semantic-duration-input/guide/integrations.md#any-other-component-renderless-slot), which gets `presets` and `selectPreset`. --- # Value formats The parser works in seconds. `valueFormat` decides what ends up in `v-model`: | `valueFormat` | `1h 30m` becomes | | --- | --- | | `'minutes'` (default) | `90` | | `'seconds'` | `5400` | | `'ms'` | `5400000` | | `'iso'` | `'PT1H30M'` | | `{ toModel(seconds), fromModel(value) }` | whatever you return | Empty input is always `null`. For numeric formats, the model must be a number: a numeric string such as `"90"` counts as empty. ISO output is written in the `PnDTnHnMnS` form without weeks, which is the most widely supported: two weeks become `P14D`. ## Bounds, steps and presets Numeric `min`, `max`, `step` and `presets` are in the model's unit: - minutes for `'minutes'` and `'iso'`, - seconds for `'seconds'`, - milliseconds for `'ms'`, - whatever `fromModel` accepts for a custom format. Duration text like `'8h'` works with any format. Text that can't be parsed is ignored. ## Seconds precision With `precision="second"`, seconds are accepted (`30s`, `1:02:03`) and kept. With `valueFormat: 'minutes'`, the model can then be fractional: `1m30s` becomes `1.5`. ## Custom formats ```ts import type { CustomValueFormat } from 'semantic-duration-input' // Store hours as a decimal number. const hours: CustomValueFormat = { toModel: (seconds) => seconds / 3600, fromModel: (value) => (typeof value === 'number' ? value * 3600 : null), } ``` ```vue ``` `fromModel` should return `null` for values that aren't a duration. --- # Locales Eight languages are built in. English and German are accepted by default; the others are accepted once you pass them in `locales`. Two props control languages: - `locales`: which unit names and separator words the parser accepts. Default `[en, de]`. - `locale`: the language of the normalized text and the error messages. Defaults to the first of `locales`. ```vue ``` ## Built-in locales | Export | Language | Input | Normalized (short / long) | | --- | --- | --- | --- | | `en` | English | `2 hours and 15 min` | `2h 15min` / `2 hours 15 minutes` | | `de` | German | `2 Std. und 15 Min.` | `2h 15min` / `2 Stunden 15 Minuten` | | `es` | Spanish | `2 horas y 15 minutos` | `2h 15min` / `2 horas 15 minutos` | | `fr` | French | `1 j et 2 heures` | `1j 2h` / `1 jour 2 heures` | | `pt` | Portuguese | `2 horas e 15 minutos` | `2h 15min` / `2 horas 15 minutos` | | `hi` | Hindi | `2 घंटे और 15 मिनट` | `2घं 15मि` / `2 घंटे 15 मिनट` | | `ar` | Arabic | `2 ساعة و 15 دقيقة`, `٢ ساعة و ١٥ دقيقة` | `2س 15د` / `2 ساعتان 15 دقيقة` | | `zh` | Chinese (Mandarin) | `2小时15分钟`, `2個小時15分鐘` | `2小时 15分钟` / `2 小时 15 分钟` | The unit names of each locale are listed in [Syntax](https://nicoalbrecht.github.io/semantic-duration-input/guide/syntax.md#units). Each built-in locale also accepts `h`, `m`, `min`, `s`, `d` and `w`, and every locale accepts native digits (`٣٠`, `३०`, `30`). - Portuguese messages use Brazilian wording. Chinese labels and messages are Simplified, and Traditional unit names are accepted too. - Bundlers leave out the locales you don't import. - Accepting a locale doesn't change the language of the text: set `locale`, or put that locale first in `locales`. ```vue ``` ### Right-to-left For Arabic, set `dir="rtl"` on the page or on an element around the field. `dir` on `` itself only reaches the ``, not the leading and trailing content or the preview. ```vue
``` ## Display | Prop | Effect | | --- | --- | | `displayStyle` | `'short'`: `1d 2h 30min`, `'long'`: `1 day 2 hours 30 minutes` | | `displayUnits` | Units the normalized text is built from. Default: days, hours and minutes (plus seconds with `precision="second"`). Add `'week'` to get `1w 2d`, or pass only `['hour']` to get `1.5h`. | ## Custom locales A locale is a plain object with unit aliases, separator words, labels for the normalized text and error messages: ```ts import { defineLocale, en } from 'semantic-duration-input' const hu = defineLocale({ ...en, code: 'hu', aliases: { minute: ['p', 'perc'], hour: ['ó', 'óra'], day: ['nap'], week: ['hét'] }, separators: ['és'], labels: undefined, // use Intl.DurationFormat for the normalized text where available messages: { ...en.messages, empty: 'Adj meg egy időtartamot.' }, }) ``` ```vue ``` - **Aliases** are matched case-insensitively and in Unicode normal form (NFC), so composed and decomposed letters like `फ़` match. Combining marks (Devanagari vowel signs, Arabic diacritics) are part of a name. If two active locales use the same alias for different units, the later locale in `locales` wins. - **Separators** are extra words that may join parts, like `and`. The symbols `,` `+` `&` always work. - **Labels:** without `labels`, the text is formatted with `Intl.DurationFormat` using `code`, falling back to English labels where that API isn't available. Make sure the parser can read what the formatter writes: add the words it produces to `aliases` and `separators`. - **Plural forms:** a long label is `[one, other]`, picked by the plural rules of `code` (`Intl.PluralRules`): French writes `0 minute` and `2 minutes`. For languages with more forms, pass a `PluralLabels` object instead. Missing categories use `other`: ```ts long: { ...en.labels!.long, day: { one: 'diwrnod', two: 'ddiwrnod', other: 'diwrnod' }, // Welsh } ``` - **Messages** may use `{min}`, `{max}`, `{token}` and `{suggestion}`. See [Error messages](https://nicoalbrecht.github.io/semantic-duration-input/guide/forms.md#error-messages) for the keys. Locales can also name and label [custom units](https://nicoalbrecht.github.io/semantic-duration-input/guide/units.md#translations). To add aliases to a built-in language, spread it: ```ts const myEn = defineLocale({ ...en, aliases: { ...en.aliases, hour: [...en.aliases.hour!, 'hs'] } }) ``` --- # Custom units `customUnits` adds units of your own and changes the length of `day` and `week`. Each entry is a length in seconds, or a definition with aliases and labels: ```vue ``` | You type | `v-model` | After blur | | --- | --- | --- | | `2 sprints 3d` | `44640` | `2sp 3d` | | `3 pomodoro` | `75` | `1h 15min` | | `1 sprint 2` | `40320` | `2sp` (`2` takes weeks, see [below](#implicit-units)) | ## Working time Override `day` and `week` to count working time, as in Jira, where a day is 8 hours and a week is 5 days: ```vue ``` `1w 2d` is now 56 hours, and 10 hours are shown as `1d 2h`. PgUp/PgDn step by the configured day. Overriding `day` doesn't change `week`, so set both. Only `day` and `week` can be overridden. `second`, `minute` and `hour` keep their lengths, so precision, rounding and clock input stay predictable. The day must stay longer than an hour and shorter than the week. Some input always uses the standard lengths, because it has a fixed meaning outside your app: - ISO 8601: `P1D` is 24 hours even when `1d` is 8. `valueFormat: 'iso'` writes standard lengths too. - Clock format: `26:00` is 26 hours. ## Definitions | Field | | | --- | --- | | `seconds` | Length in seconds: a whole number greater than 0 | | `aliases` | Extra names the parser accepts in every locale, matched case-insensitively | | `labels` | `{ short?, long?: [one, other] }` for the normalized text. Not allowed on `day` and `week`, whose labels come from the locale | - A custom unit's name is accepted too, and is its label when it has none: `{ pomodoro: 1500 }` accepts `2 pomodoro` and shows `2 pomodoro`. Without a short label, the short style writes the long form. - Names, aliases and labels must be letters only, since unit words in the input are. A label may end with a period (`Std.`). - Labels are accepted by the parser as well, so the normalized text can always be read back. - Names from `customUnits` win over the unit names of the locales: an alias `s` takes over seconds. Two custom units can't share a name. - Units shorter than the precision are ignored: a 30-second unit is an `unknown_unit` unless `precision="second"`. - Invalid definitions throw an `Error`, when the component mounts or, with the [Nuxt module](https://nicoalbrecht.github.io/semantic-duration-input/guide/nuxt.md), at build time. A name in `displayUnits` or `defaultUnit` that is neither built in nor in `customUnits` throws as well, so typos don't go unnoticed. ## Showing custom units Custom units appear in the normalized text, the preview, the presets and the error messages when they're in `displayUnits`. Units are always shown longest first, whatever their order in `displayUnits`. The smallest display unit takes the rest as a decimal with up to two places, like `1.5h` with `displayUnits: ['hour']`. With long units such as sprints, that can round off a few minutes; add a smaller unit to keep them. ## Implicit units A trailing number takes the longest *built-in* unit that is shorter than the last unit, so custom units never change what `1h30` means: | Input | Means | | --- | --- | | `1 workday 3` (workday = 8h) | 1 workday 3 hours | | `1 pomodoro 5` | 1 pomodoro 5 minutes | | `1 sprint 2` (sprint = 14 days) | 1 sprint 2 weeks | | `1w 2` (week = 5 × 8h, day = 8h) | 1 week 2 days of 8 hours | ## Translations Unit names in `customUnits` work in every locale. To translate a unit, give it aliases and labels in a locale, under the same name: ```ts import { de, defineLocale } from 'semantic-duration-input' const deAgile = defineLocale({ ...de, aliases: { ...de.aliases, sprint: ['Sprints'] }, labels: { short: { ...de.labels!.short, sprint: 'Sp' }, long: { ...de.labels!.long, sprint: ['Sprint', 'Sprints'] }, }, }) ``` The display locale's labels win over the labels in `customUnits`. The parser accepts the labels that the active locales give custom units, so the translated text parses back too. Locales without `labels` format built-in units with `Intl` and custom units with their labels from `customUnits`. ## App-wide units Pass `customUnits` to the plugin (or the Nuxt module) to use them everywhere. A `customUnits` prop replaces the app-wide units rather than merging with them. ```ts app.use(plugin, { customUnits: { day: 8 * 3600, week: 5 * 8 * 3600 } }) ``` The core functions take the same option: `parseDuration`, `formatDuration`, `formatErrorMessage`, `resolveAmount` and `durationSchema`. See the [Core API](https://nicoalbrecht.github.io/semantic-duration-input/reference/core.md). --- # Forms and validation ## Native forms With a `name`, a hidden input carries the model value (`90`, or `PT1H30M` with `'iso'`), so the form submits what `v-model` holds, not the typed text. Plain objects and arrays from a custom `valueFormat` are submitted as JSON; other objects (like `Temporal.Duration`) use their `toString()`. This also works with `as`. The renderless slot gets `hiddenInputProps` to bind it yourself. While the text is invalid, the input reports it to the browser through `setCustomValidity()`, with the error message as text. So the browser blocks submitting the form instead of sending the last valid value, just like it does for an empty `required` field. This happens with the built-in field and with `as`. With the renderless slot, call `setCustomValidity()` yourself, e.g. from `rawErrorDetail`. ```vue
``` ## Required, min and max - `required` makes empty input an `empty` error. `v-model` still becomes `null` when the field is cleared, so the model always matches the field. - `min` and `max` are inclusive. They take a number in the model's unit or text like `'8h'`. Values outside them are `out_of_range` errors, or with `clamp`, get clamped. In development, a `min` greater than `max` logs a warning; `durationSchema` throws a `RangeError` for it. ## Show errors on submit Call `validate()` through a template ref. It shows the current error, whatever `validateOn` is set to, and returns whether the input is valid: ```vue ``` ## Error messages Override texts per key with `messages`, or pass `false` to hide the built-in message (the `error` event and `aria-invalid` still work): ```vue ``` | Key | Used for | | --- | --- | | `empty` | `empty` | | `invalid_format` | `invalid_format` | | `unknown_unit` | `unknown_unit` without a suggestion | | `unknown_unit_suggestion` | `unknown_unit` with a suggestion | | `missing_unit` | `missing_unit` | | `out_of_range` | `out_of_range` with both bounds | | `out_of_range_min` | `out_of_range` with only `min` | | `out_of_range_max` | `out_of_range` with only `max` | - Overriding a base key also covers its variants: `unknown_unit` covers `unknown_unit_suggestion`, and `out_of_range` covers `out_of_range_min` and `out_of_range_max`. - Placeholders: `{min}`, `{max}`, `{token}` and `{suggestion}`. Each is filled in once per message. ## Schema validation `durationSchema()` is a [Standard Schema](https://standardschema.dev), so it works directly with Valibot, ArkType, TanStack Form, VeeValidate and others. - **Text** is parsed into the model value. - **Values that already have the output type** (e.g. numbers) are only range-checked. Values it can't convert fail with `invalid_format`. ```ts import { durationSchema } from 'semantic-duration-input/core' const estimate = durationSchema({ required: true, max: '8h', valueFormat: 'minutes' }) estimate['~standard'].validate('1h30') // { value: 90 } estimate['~standard'].validate('9h') // { issues: [{ message: 'Must be at most 8h.' }] } estimate['~standard'].validate(90) // { value: 90 } ``` Options: - Everything the component uses for parsing: `valueFormat`, `min`, `max`, `required`, `locales`, `precision`, `implicitUnits` and `defaultUnit`. - `locale` and `messages` for the issue texts. Without `required`, empty input, `null` and `undefined` validate to `{ value: null }`. ### Zod With Zod, use `parseDuration` in a transform: ```ts import { z } from 'zod' import { formatErrorMessage, parseDuration } from 'semantic-duration-input/core' const minutes = z.string().transform((text, ctx) => { const result = parseDuration(text) if (result.ok) return result.minutes // null for empty input ctx.addIssue({ code: 'custom', message: formatErrorMessage(result) }) return z.NEVER }) ``` --- # Styling The built-in field is styled with Tailwind CSS v4 utilities. It follows your theme through a few CSS variables, and each variable falls back to the shadcn-vue and Nuxt UI tokens when those are present. See [Getting started](https://nicoalbrecht.github.io/semantic-duration-input/guide/getting-started.md#add-the-styles) for which stylesheet to import. ## Sizes and variants `size` is `'sm'`, `'md'` (default) or `'lg'`. `variant` is `'outline'` (default), `'soft'` or `'ghost'`. ## Tokens | Variable | Falls back to (shadcn-vue → Nuxt UI → neutral) | | --- | --- | | `--sdi-fg` | `--foreground` → `--ui-text` → `currentColor` | | `--sdi-bg` | `transparent` | | `--sdi-border` | `--input` → `--ui-border-accented` → 25% `currentColor` | | `--sdi-ring` | `--ring` → `--ui-primary` → `Highlight` | | `--sdi-invalid` | `--destructive` → `--ui-error` → `#dc2626` | | `--sdi-muted` | `--muted-foreground` → `--ui-text-muted` → 55% `currentColor` | | `--sdi-soft` | `--muted` → `--ui-bg-elevated` → 8% `currentColor` | | `--sdi-popover` | `--popover` → `--ui-bg` → `Canvas` | The neutral fallbacks are derived from `currentColor`, so dark mode works without extra setup. Set the tokens on any ancestor, such as `:root` or a section of your page, or on the component itself: ```css :root { --sdi-ring: var(--color-emerald-500); } ``` ```vue ``` This site sets them once on `:root` to follow the VitePress theme. ## Classes per part Every part takes extra classes through `ui`. They are merged with [tailwind-merge](https://github.com/dcastil/tailwind-merge), so conflicting defaults get replaced: `rounded-full` removes `rounded-md`. A `class` attribute is merged into `root` the same way. ```vue ``` ### Structure ``` root ├─ field │ ├─ leading │ ├─ input │ ├─ preview │ ├─ trailing │ └─ menu │ └─ option └─ message ``` - Each element carries `data-slot=""`. - The root also carries `data-invalid`, `data-disabled`, `data-size` and `data-variant`. It has the `group` class, so children can use Tailwind variants such as `group-data-invalid:`. - The input carries `data-error=""` while an error is shown. ## Unstyled `unstyled` drops all default classes. The root keeps only the `sdi` class plus whatever you pass through `ui`, `class` and the app-wide `ui` defaults. These are joined as-is, without tailwind-merge. ```vue ``` ## Changing the defaults everywhere For small changes across an app, pass `ui` as an [app-wide default](https://nicoalbrecht.github.io/semantic-duration-input/guide/integrations.md#app-wide-defaults). It is merged with the defaults and with each component's own `ui`: ```ts app.use(DurationInputPlugin, { ui: { field: 'rounded-none', menu: 'rounded-none' } }) ``` The default theme is exported as `durationInputTheme`, a [tailwind-variants](https://www.tailwind-variants.org) component. Use it to read the defaults, or as the base for your own theme combined with `unstyled`: ```ts import { tv } from 'tailwind-variants' import { durationInputTheme } from 'semantic-duration-input' const theme = tv({ extend: durationInputTheme, slots: { field: 'rounded-none' } }) const { root, field, input, message } = theme({ size: 'md', variant: 'outline' }) ``` ```vue ``` Because the root keeps the `group` class, the invalid state can be styled with `group-data-invalid:` classes in your theme. --- # Integrations ## Rendering another input with `as` Use `as` to render your UI library's input. The component binds these to it: - `modelValue`, `onUpdate:modelValue`, `onBlur` and `onKeydown` - `disabled`, `readonly` and `required` - `aria-invalid` (while an error is shown) and `data-error` - `size` and `variant` Attributes such as `id`, `placeholder` and `class`, and all slots except `default`, are passed through. Your own listeners (`@blur`, `@keydown`, ...) run alongside the component's. The library's own props and slots keep working. If `as` is a string such as `'input'`, it gets native props (`value`/`onInput`) instead of `modelValue`. Things to know in `as` mode: - **Error message:** the component renders no message of its own. Show it through the library, as the Vuetify preset does, or with the renderless slot. - **Preview and presets:** `preview` and the presets menu belong to the built-in field and are not rendered. Use the [renderless slot](#any-other-component-renderless-slot) for those. - **Styles:** the built-in stylesheet isn't needed. ### `invalidProps` `invalidProps` maps the error state onto library-specific props. It receives `{ invalid, error, message, onBlur }` and returns props. Presets are included: | Preset | Sets | | --- | --- | | `vuetify` | `error`, `error-messages`, and commits on `update:focused` | | `primevue` | `invalid` | | `nuxtUi` | `color: 'error'` and `highlight` while invalid | Your own function works the same way: ```ts import type { InvalidPropsFn } from 'semantic-duration-input' const myLib: InvalidPropsFn = ({ invalid, message }) => ({ state: invalid ? 'error' : undefined, hint: message }) ``` ### shadcn-vue Its `Input` already styles `aria-invalid`, so no preset is needed. The built-in field also picks up shadcn's tokens automatically. ```vue ``` ### Nuxt UI ```vue ``` In Nuxt, `UInput` is auto-imported for templates, but `as` needs the component itself, hence the import from `#components`. The [Nuxt module](https://nicoalbrecht.github.io/semantic-duration-input/guide/nuxt.md) can set this up app-wide. ### Vuetify ```vue ``` The preset sets `error` and `error-messages`, and normalizes the text on `update:focused`. If you also use Tailwind next to Vuetify, some class names collide (`.border`, `.rounded-*`, ...). Order the cascade layers so Tailwind's utilities win: ```css @layer theme, base, vuetify-core, components, vuetify-components, vuetify-overrides, vuetify-utilities, utilities, vuetify-final; ``` ### PrimeVue ```vue ``` The adapter works with PrimeVue 4 and 5. ## Any other component (renderless slot) For full control, pass a default slot. The component then renders nothing but your slot, and handles only the parsing: ```vue ``` The slot props: | Prop | | | --- | --- | | `inputProps` | For components with a `modelValue` contract. Includes attributes, `class` and `style` | | `nativeInputProps` | For a plain ``: `value`, `onInput`, `onBlur`, `onKeydown`. Includes attributes | | `hiddenInputProps` | For an `` carrying the model value, or `null` without a `name` | | `text` | The text in the field | | `value` | The model value | | `error`, `errorDetail` | The shown error code, and the shown failure with `token`, `index` and `suggestion` | | `rawError`, `rawErrorDetail` | The current error, shown or not, and its failure details | | `invalid`, `message`, `messageId` | Whether an error is shown, its text, and the id to give the message element for `aria-describedby` | | `preview` | The normalized text (or the `formatPreview` output) while it differs from what was typed, else `null` | | `presets`, `selectPreset(preset)` | The resolved presets (`{ id, label, seconds }`), to render your own suggestions | | `onBlur`, `onKeydown`, `commit`, `validate`, `revert`, `stepBy` | The handlers and methods | A plain input with its own presets list: ```vue

{{ s.message }}

``` ## App-wide defaults Pass defaults to the plugin, and they apply to every instance. Props set on a component still win, and `ui` defaults are merged with the component's `ui`. ```ts import { de, plugin as DurationInputPlugin } from 'semantic-duration-input' import { Input } from '@/components/ui/input' app.use(DurationInputPlugin, { as: Input, locale: de, validateOn: 'blur' }) ``` Every prop can be a default except `min`, `max`, `required`, `disabled` and `readonly`, which belong to the individual field. --- # Nuxt Add the module. It: - auto-imports ``, `useDurationInput`, `parseDuration` and `formatDuration`, - applies the defaults from `nuxt.config`, - adds the prebuilt stylesheet. ```ts export default defineNuxtConfig({ modules: ['@nuxt/ui', 'semantic-duration-input/nuxt'], durationInput: { as: 'UInput', // any component name from #components invalidProps: 'nuxtUi', // 'nuxtUi' | 'vuetify' | 'primevue' validateOn: 'blur', }, }) ``` ## Options The options are the [app-wide defaults](https://nicoalbrecht.github.io/semantic-duration-input/guide/integrations.md#app-wide-defaults), in a form that can be serialized into the generated plugin: | Option | | | --- | --- | | `as` | A component name resolved from `#components`, e.g. `'UInput'`. It must be a valid identifier, or the build fails | | `invalidProps` | A preset name: `'nuxtUi'`, `'vuetify'` or `'primevue'` | | `valueFormat` | `'minutes'`, `'seconds'`, `'ms'` or `'iso'`. Custom conversions can't be serialized, so pass them as a prop instead | | `formatPreview` | Not available: functions can't be serialized, so pass it as a prop instead | | `css` | Adds `semantic-duration-input/style.css`. Defaults to `true`, or `false` when `as` is set. `true` forces it | | Everything else | As on the component: `locales`, `validateOn`, `presets`, `ui`, ... | Locales are plain data, so they serialize fine. Import them from the core entry in `nuxt.config`: ```ts import { de, en } from 'semantic-duration-input/core' export default defineNuxtConfig({ modules: ['semantic-duration-input/nuxt'], durationInput: { locales: [de, en] }, }) ``` `preview` and the presets menu only work with the built-in field (see [`as` mode](https://nicoalbrecht.github.io/semantic-duration-input/guide/integrations.md#rendering-another-input-with-as)), so they have no effect together with `as`. If you use Tailwind v4 in the Nuxt app, set `css: false` and import `semantic-duration-input/tailwind.css` in your main CSS instead, as described in [Getting started](https://nicoalbrecht.github.io/semantic-duration-input/guide/getting-started.md#add-the-styles). --- # Headless usage ## Core The parser, formatter and validation have their own entry point, without Vue or Tailwind. It works in Node, e.g. to validate on the server: ```ts import { parseDuration, formatDuration, formatErrorMessage, toIso, de } from 'semantic-duration-input/core' parseDuration('1h 30m') // { ok: true, seconds: 5400, minutes: 90 } parseDuration('45') // { ok: false, error: 'missing_unit', token: '45', index: 0 } parseDuration('2 huors') // { ok: false, error: 'unknown_unit', token: 'huors', index: 2, suggestion: 'hours' } formatDuration(680400, { style: 'long', locale: de }) // '7 Tage 21 Stunden' (takes seconds) formatErrorMessage('out_of_range', { min: 1800, max: 28800 }) // 'Must be between 30min and 8h.' toIso(5400) // 'PT1H30M' ``` All of it is described in the [Core API reference](https://nicoalbrecht.github.io/semantic-duration-input/reference/core.md). ## `useDurationInput` The component's logic is available as a composable, to build your own input in Vue: ```vue ``` The first argument is the model ref. The second takes the component's parsing, display and validation options, as an object, ref or getter: `valueFormat`, `locales`, `locale`, `displayStyle`, `displayUnits`, `precision`, `implicitUnits`, `defaultUnit`, `required`, `min`, `max`, `step`, `snapToStep`, `clamp`, `validateOn`, `readonly`, `formatPreview` It returns: | | | | --- | --- | | `text` | Ref of the text in the field | | `seconds` | The model value in seconds | | `error`, `errorDetail` | The shown error code, and the shown failure (see `validateOn`) | | `rawError`, `rawErrorDetail`, `isValid` | The current error and failure, shown or not, and whether there is none | | `preview` | The normalized text (or the `formatPreview` output) while it differs from what was typed | | `settings` | Resolved options: bounds and step in seconds, locale, display units | | `format(seconds)` | Formats with the current locale and display options | | `onInput(event \| text \| null)` | Input handler; also takes the new text, or `null` to clear | | `onBlur`, `onKeydown` | Blur and keyboard handlers | | `commit()`, `validate()`, `revert()` | Return booleans: whether the text was valid / is valid / whether anything was reverted | | `stepBy(direction, size?)` | Step by `direction` steps of `size` seconds (default: `step`) | The component adds error messages, the presets menu, the preview announcement and the hidden form input on top. Use the [renderless slot](https://nicoalbrecht.github.io/semantic-duration-input/guide/integrations.md#any-other-component-renderless-slot) if you want those but not the markup. --- # Component ```ts import { DurationInput } from 'semantic-duration-input' ``` ## Props | Prop | Type | Default | | | --- | --- | --- | --- | | `modelValue` | `number \| string \| null` | `null` | The duration, see `valueFormat` | | `valueFormat` | `'minutes' \| 'seconds' \| 'ms' \| 'iso' \| { toModel, fromModel }` | `'minutes'` | How `v-model` stores the duration, see [Value formats](https://nicoalbrecht.github.io/semantic-duration-input/guide/value-formats.md) | | `precision` | `'minute' \| 'second'` | `'minute'` | `'second'` accepts `30s` and `h:mm:ss` and keeps seconds | | `locales` | `DurationLocale[]` | `[en, de]` | Which unit names and separator words are accepted, see [Locales](https://nicoalbrecht.github.io/semantic-duration-input/guide/locales.md) | | `locale` | `DurationLocale` | first of `locales` | Language of the normalized text and the messages | | `displayStyle` | `'short' \| 'long'` | `'short'` | `1d 2h 30min` vs `1 day 2 hours 30 minutes` | | `displayUnits` | `UnitName[]` | `['day', 'hour', 'minute']` (+ `'second'`) | Units used in the normalized text, including custom ones | | `implicitUnits` | `boolean` | `true` | `1h30` means 1h 30min | | `defaultUnit` | `UnitName` | – | Unit for a bare number, e.g. `'minute'` | | `customUnits` | `CustomUnits` | – | Units of your own and other lengths for `day` and `week`, see [Custom units](https://nicoalbrecht.github.io/semantic-duration-input/guide/units.md) | | `min` / `max` | `number \| string` | – | Inclusive bounds: a number in the model's unit, or text like `'8h'` (`out_of_range`) | | `step` | `number \| string \| false` | `'15m'` | Arrow-key step | | `snapToStep` | `boolean` | `false` | Round to the nearest `step` on blur/Enter | | `clamp` | `boolean` | `false` | Clamp to `min`/`max` instead of reporting `out_of_range` | | `validateOn` | `'eager' \| 'blur' \| 'input'` | `'eager'` | When errors are shown, see [Behaviour](https://nicoalbrecht.github.io/semantic-duration-input/guide/behaviour.md#when-errors-appear) | | `required` | `boolean` | `false` | Empty input is an `empty` error | | `disabled` | `boolean` | `false` | | | `readonly` | `boolean` | `false` | Read-only: no stepping, reverting or presets menu | | `presets` | `(number \| string \| { label?, value })[]` | – | Suggestions menu, see [Presets](https://nicoalbrecht.github.io/semantic-duration-input/guide/behaviour.md#presets). Built-in field only | | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Field size. With `as`, any value is passed on | | `variant` | `'outline' \| 'soft' \| 'ghost'` | `'outline'` | Field look. With `as`, any value is passed on | | `preview` | `boolean` | `false` | Shows the normalized value inside the field while typing, and announces it to screen readers. Built-in field only | | `formatPreview` | `(seconds, { normalized, text, value, locale }) => string \| null` | – | Renders the preview instead of the normalized text, see [Custom preview](https://nicoalbrecht.github.io/semantic-duration-input/guide/behaviour.md#custom-preview) | | `messages` | `{ [key]: string } \| false` | – | Overrides the error texts, or `false` to hide them, see [Error messages](https://nicoalbrecht.github.io/semantic-duration-input/guide/forms.md#error-messages) | | `ui` | `{ root?, field?, input?, leading?, trailing?, preview?, message?, menu?, option? }` | – | Extra classes per part, see [Styling](https://nicoalbrecht.github.io/semantic-duration-input/guide/styling.md#classes-per-part) | | `unstyled` | `boolean` | `false` | Drops all default classes | | `as` | `Component \| string` | – | Renders another input component, see [Integrations](https://nicoalbrecht.github.io/semantic-duration-input/guide/integrations.md) | | `invalidProps` | `(state) => props` | – | Maps the invalid state onto the `as` component's props | ## Attributes - **Built-in field:** `class` and `style` go to the root element. `name` goes to a hidden input carrying the model value (see [Forms](https://nicoalbrecht.github.io/semantic-duration-input/guide/forms.md#native-forms)). All other attributes (`id`, `placeholder`, `data-*`, ...) go to the ``. - **With `as` or the renderless slot:** `class` and `style` go to the `as` component or into `inputProps`, together with the other attributes. ## Events | Event | Payload | | | --- | --- | --- | | `update:modelValue` | `value \| null` | The model changed | | `error` | `code \| null` | The shown error changed (see `validateOn`). Codes: `empty`, `invalid_format`, `unknown_unit`, `missing_unit`, `out_of_range` | ## Slots | Slot | | | --- | --- | | `leading` | Before the input: an icon, a unit hint. With `as`, all named slots are passed on to that component | | `trailing` | After the input: a unit hint, a clear button | | `default` | Renderless mode, see [the slot props](https://nicoalbrecht.github.io/semantic-duration-input/guide/integrations.md#any-other-component-renderless-slot) | ## Exposed methods Through a template ref: | Method | Returns | | | --- | --- | --- | | `focus()` | | Focuses the input | | `blur()` | | Blurs the input, which normalizes the text | | `commit()` | `boolean` | Normalizes the text, shows any error and writes the value, like blur. Returns whether the text was valid | | `validate()` | `boolean` | Shows the current error, whatever `validateOn` is, and returns whether the text is valid | | `revert()` | `boolean` | Goes back to the last committed value. Returns whether anything changed | | `stepBy(direction, seconds?)` | | Steps like the arrow keys, e.g. `stepBy(-2)` or `stepBy(1, 3600)` | ## Plugin ```ts import { plugin } from 'semantic-duration-input' app.use(plugin, defaults?) ``` Registers `` globally. The optional `defaults` apply to every instance, see [App-wide defaults](https://nicoalbrecht.github.io/semantic-duration-input/guide/integrations.md#app-wide-defaults). They are provided under the `DURATION_INPUT_DEFAULTS` injection key, which you can also `provide` yourself for a subtree. ## Types Exported from `semantic-duration-input`, besides everything from [`/core`](https://nicoalbrecht.github.io/semantic-duration-input/reference/core.md#types): | Type | | | --- | --- | | `DurationInputProps` | The props above | | `DurationInputDefaults` | App-wide defaults: the props except `min`, `max`, `required`, `disabled` and `readonly` | | `DurationInputUi`, `DurationInputPart` | The `ui` prop and its part names | | `DurationInputSize`, `DurationInputVariant`, `DurationInputVariants` | Built-in sizes and looks | | `DurationPreset`, `DurationPresetItem` | A `presets` entry, and a resolved one as given to the slot | | `DurationInputSlotProps` | Props of the renderless slot | | `InvalidPropsFn`, `InvalidState` | An `invalidProps` function and what it receives | | `DurationInputOptions`, `ValidateOn` | Options of `useDurationInput` | | `PreviewFormatter`, `PreviewContext` | A `formatPreview` function and what it receives | --- # Core API ```ts import { parseDuration } from 'semantic-duration-input/core' ``` Everything here is also exported from `semantic-duration-input`. The core entry has no Vue or Tailwind dependency. All durations are in **seconds** unless noted. ## `parseDuration(input, options?)` Parses duration text. Returns `{ ok: true, seconds, minutes }`, with both `null` for empty input, or a failure. Input longer than 256 characters (after trimming) is rejected as `invalid_format` without being parsed, so it's safe to call on untrusted input, e.g. on the server. ```ts parseDuration('1h 30m') // { ok: true, seconds: 5400, minutes: 90 } parseDuration('') // { ok: true, seconds: null, minutes: null } parseDuration('45') // { ok: false, error: 'missing_unit', token: '45', index: 0 } parseDuration('2 huors') // { ok: false, error: 'unknown_unit', token: 'huors', index: 2, suggestion: 'hours' } parseDuration('9h', { max: 8 * 3600 }) // { ok: false, error: 'out_of_range' } ``` | Option | Default | | | --- | --- | --- | | `locales` | `[en, de]` | Locales whose unit names and separators are accepted | | `precision` | `'minute'` | `'second'` accepts seconds; results are rounded to the precision | | `implicitUnits` | `true` | `1h30` means 1h 30min | | `defaultUnit` | – | Unit for a bare number | | `customUnits` | – | Units of your own and other lengths for `day` and `week`, see [Custom units](https://nicoalbrecht.github.io/semantic-duration-input/guide/units.md) | | `min`, `max` | – | Inclusive bounds in seconds | | `required` | `false` | Empty input is an `empty` failure instead of `null` | A failure (`ParseFailure`) has: - `error`: see [Errors](https://nicoalbrecht.github.io/semantic-duration-input/guide/syntax.md#errors). - `token` and `index`: the offending part of the input and its position. They are missing for `empty` and `out_of_range`. - `suggestion`: for `unknown_unit`, when a unit name is close. ## `formatDuration(seconds, options?)` ```ts formatDuration(5400) // '1h 30min' formatDuration(680400, { style: 'long', locale: de }) // '7 Tage 21 Stunden' formatDuration(5400, { units: ['hour'] }) // '1.5h' ``` | Option | Default | | | --- | --- | --- | | `locale` | `en` | Labels to use. Locales without `labels` use `Intl.DurationFormat` | | `style` | `'short'` | `'short'` or `'long'` | | `units` | `['day', 'hour', 'minute']` | Units to decompose into, custom ones included. The smallest one takes the rest, as a decimal if needed | | `customUnits` | – | See [Custom units](https://nicoalbrecht.github.io/semantic-duration-input/guide/units.md) | Throws a `RangeError` for negative, `NaN` or infinite input, and an `Error` for an unknown unit in `units`. ## `formatErrorMessage(error, options?)` Readable text for an error code or a whole `ParseFailure`. Pass the failure to get `{token}` and `{suggestion}` filled in. ```ts formatErrorMessage(parseDuration('2 huors')) // 'Unknown unit "huors". Did you mean "hours"?' formatErrorMessage('out_of_range', { min: 1800, max: 28800 }) // 'Must be between 30min and 8h.' ``` | Option | | | --- | --- | | `locale` | Language, default `en` | | `min`, `max` | Bounds in seconds, for `{min}` and `{max}`. Which is set also picks `out_of_range_min` / `out_of_range_max` | | `units` | Units to format the bounds with | | `customUnits` | See [Custom units](https://nicoalbrecht.github.io/semantic-duration-input/guide/units.md) | | `overrides` | Replacement texts per key, see [Error messages](https://nicoalbrecht.github.io/semantic-duration-input/guide/forms.md#error-messages) | ## `durationSchema(options?)` A [Standard Schema](https://standardschema.dev) for form libraries, see [Schema validation](https://nicoalbrecht.github.io/semantic-duration-input/guide/forms.md#schema-validation). ## ISO 8601 ```ts toIso(5400) // 'PT1H30M' toIso(1209600) // 'P14D' (no weeks, for the widest support) fromIso('P1DT2H') // 93600 fromIso('1h') // null ``` `toIso` throws a `RangeError` for negative, `NaN` or infinite input. ## Model values These convert between seconds and a `valueFormat`. They're used by the component and exported for custom inputs: | Function | | | --- | --- | | `toModelValue(seconds, format)` | Seconds → model value. `null` stays `null` | | `fromModelValue(value, format)` | Model value → seconds, or `null` when empty or not a duration (including negative numbers) | | `resolveAmount(amount, format, { locales, customUnits })` | A bound or step (number in the model's unit, or text like `'8h'`) → seconds, or `undefined` if invalid | ## Locales | Export | | | --- | --- | | `en`, `de` | English and German, accepted by default | | `es`, `fr`, `pt`, `hi`, `ar`, `zh` | Spanish, French, Portuguese, Hindi, Arabic and Chinese. Pass them in `locales` to accept them, see [Built-in locales](https://nicoalbrecht.github.io/semantic-duration-input/guide/locales.md#built-in-locales) | | `DEFAULT_LOCALES` | `[en, de]` | | `defineLocale(locale)` | Typed helper for custom locales, see [Custom locales](https://nicoalbrecht.github.io/semantic-duration-input/guide/locales.md#custom-locales) | ## Constants | Export | | | --- | --- | | `UNIT_SECONDS` | Standard length of each built-in unit in seconds: `{ second: 1, minute: 60, hour: 3600, day: 86400, week: 604800 }` | | `UNITS_DESC` | `['week', 'day', 'hour', 'minute', 'second']` | | `DEFAULT_DISPLAY_UNITS` | `['day', 'hour', 'minute']` | ## Types | Type | | | --- | --- | | `ParseResult`, `ParseFailure`, `ParseOptions`, `ParseErrorCode` | `parseDuration` | | `FormatOptions`, `FormatStyle` | `formatDuration` | | `ErrorMessageOptions`, `ErrorMessages`, `MessageKey` | `formatErrorMessage` and the `messages` prop | | `DurationLocale` | A locale | | `LongLabel`, `PluralLabels` | A long label: `[one, other]`, or `{ one?, two?, few?, many?, zero?, other }` per plural category | | `UnitKey`, `Precision` | `'second' \| 'minute' \| 'hour' \| 'day' \| 'week'`, `'minute' \| 'second'` | | `UnitName` | A `UnitKey` or the name of a custom unit | | `CustomUnits`, `CustomUnit` | `customUnits`, see [Custom units](https://nicoalbrecht.github.io/semantic-duration-input/guide/units.md) | | `ValueFormat`, `CustomValueFormat`, `DurationAmount` | `valueFormat`, and a bound or step | | `DurationSchemaOptions`, `StandardSchemaV1`, `StandardSchemaResult` | `durationSchema` | --- # Migrating to 0.2 ## Core works in seconds `parseDuration` returns `seconds` next to `minutes`. Several inputs changed from minutes to seconds: - `min` and `max` in `ParseOptions` - the input of `formatDuration` - the bounds of `formatErrorMessage` ```ts // 0.1 formatDuration(90) // 0.2 formatDuration(90 * 60) ``` The component's `v-model` still holds minutes by default, see [Value formats](https://nicoalbrecht.github.io/semantic-duration-input/guide/value-formats.md). ## Locales are objects - `locales` takes locale objects: `:locales="[en, de]"` instead of `['en', 'de']`. - `locale` replaces `displayLocale`. - `customAliases` is gone. Extend a locale with [`defineLocale`](https://nicoalbrecht.github.io/semantic-duration-input/guide/locales.md#custom-locales) instead. - `LOCALE_ALIASES`, `ERROR_MESSAGES` and `UNIT_MINUTES` are replaced by the locale objects and `UNIT_SECONDS`. ## Errors wait for blur Errors are no longer shown while typing by default (`validateOn: 'eager'`). Use `validate-on="input"` for the old behaviour. ## Implicit units `1h 30` is now 90 minutes instead of a `missing_unit` error. Set `:implicit-units="false"` to keep the old behaviour. ## Forms `name` now goes to a hidden input carrying the model value, instead of the text input. ## Renderless slot The slot prop `minutes` is now `value`.