@manik02/vue3-timepicker 0.4.5 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +489 -256
  2. package/package.json +23 -4
package/README.md CHANGED
@@ -1,22 +1,32 @@
1
- # Vue3 Timepicker
1
+ # Vue 3 Time Picker
2
2
 
3
3
  [![Live Playground](https://img.shields.io/badge/Live-Playground-4f46e5?style=for-the-badge&logo=storybook&logoColor=white)](https://manos02.github.io/vue3-time-picker/) [![npm version](https://img.shields.io/npm/v/@manik02/vue3-timepicker?style=for-the-badge)](https://www.npmjs.com/package/@manik02/vue3-timepicker) [![License: MIT](https://img.shields.io/badge/License-MIT-green?style=for-the-badge)](LICENSE)
4
4
 
5
- A flexible, customisable timepicker component for Vue 3 with TypeScript support.
6
- If this project helps you, a [GitHub star](https://github.com/manos02/vue3-time-picker) would be greatly appreciated.
7
- Found a bug? Please open an issue on [GitHub](https://github.com/manos02/vue3-time-picker/issues).
5
+ A Vue 3 time picker component with TypeScript support, multiple display formats, range selection, min/max constraints, disabled times, validation events, and full CSS variable theming.
6
+
7
+ - Live docs: [Interactive Playground](https://manos02.github.io/vue3-time-picker/)
8
+ - Package page: [npm package](https://www.npmjs.com/package/@manik02/vue3-timepicker)
9
+ - Issues: [GitHub issues](https://github.com/manos02/vue3-time-picker/issues)
10
+
11
+ If this project helps you, a [GitHub star](https://github.com/manos02/vue3-time-picker) helps a lot.
12
+
8
13
  | Demo | Default | Dark |
9
- | ---------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
14
+ | --- | --- | --- |
10
15
  | ![Demo](https://raw.githubusercontent.com/manos02/vue3-time-picker/main/assets/demo.gif) | ![Default](https://raw.githubusercontent.com/manos02/vue3-time-picker/main/assets/screenshot-default.png) | ![Dark](https://raw.githubusercontent.com/manos02/vue3-time-picker/main/assets/screenshot-dark.png) |
11
16
 
12
17
  ![All themes](https://raw.githubusercontent.com/manos02/vue3-time-picker/main/assets/screenshot-all.png)
13
18
 
19
+ ## Features
20
+
14
21
  - Single and range time selection
15
- - Multiple hour formats: 24-hour (`HH`/`H`), 12-hour (`hh`/`h` with AM/PM), 1-24 (`kk`/`k`)
22
+ - 24-hour, 12-hour, and `k`/`kk` 1-24 hour display formats
16
23
  - Optional seconds
17
- - Inline masked input with overwrite-only editing
24
+ - Typing support with an overwrite-only masked input
18
25
  - Step intervals for hours, minutes, and seconds
19
- - Fully styleable via CSS custom properties
26
+ - `minTime`, `maxTime`, `disabledTimes`, and callback-based disable rules
27
+ - Validation and error events for form workflows
28
+ - Size presets, width control, custom classes, and CSS variable theming
29
+ - TypeScript types exported with the package
20
30
 
21
31
  ## Installation
22
32
 
@@ -24,12 +34,12 @@ Found a bug? Please open an issue on [GitHub](https://github.com/manos02/vue3-ti
24
34
  npm install @manik02/vue3-timepicker
25
35
  ```
26
36
 
27
- Try it live: [Interactive Playground](https://manos02.github.io/vue3-time-picker/)
37
+ This package expects Vue 3 as a peer dependency.
28
38
 
29
- ## Quick start
39
+ ## Quick Start
30
40
 
31
41
  ```vue
32
- <script setup>
42
+ <script setup lang="ts">
33
43
  import { ref } from "vue";
34
44
  import { TimePicker } from "@manik02/vue3-timepicker";
35
45
  import "@manik02/vue3-timepicker/style.css";
@@ -38,199 +48,114 @@ const time = ref("14:30:00");
38
48
  </script>
39
49
 
40
50
  <template>
41
- <TimePicker v-model="time" format="HH:mm:ss" />
51
+ <TimePicker v-model="time" format="HH:mm" />
42
52
  </template>
43
53
  ```
44
54
 
45
- ## Interactive Playground (Storybook)
55
+ ### Important Value Behavior
56
+
57
+ - `format` only changes how the value is displayed and edited.
58
+ - The bound `v-model` value is always normalized as `HH:mm:ss` when present.
59
+ - In single mode, use `string | null | undefined`.
60
+ - In range mode, use `[string, string] | null | undefined`.
61
+
62
+ Example: with `format="HH:mm"`, the UI may show `14:30`, but `v-model` still contains `14:30:00`.
46
63
 
47
- Run Storybook locally and experiment with all props via controls:
64
+ ## Playground
65
+
66
+ Run Storybook locally:
48
67
 
49
68
  ```bash
50
69
  npm run storybook
51
70
  ```
52
71
 
53
- Build static Storybook:
72
+ Build the static docs site:
54
73
 
55
74
  ```bash
56
75
  npm run build-storybook
57
76
  ```
58
77
 
59
- The repository includes [storybook deploy workflow](.github/workflows/storybook.yml) to publish Storybook to GitHub Pages on push to `main`.
60
-
61
- ## Props
62
-
63
- | Prop | Type | Default | Description |
64
- | ---------------- | ----------------------------------------------- | --------------- | --------------------------------------------------------------------------------------- |
65
- | `modelValue` | `string \| [string, string] \| null` | `undefined` | Time value in `HH:mm:ss` format. Use a two-element array for range mode. |
66
- | `format` | `TimeFormat` | `"HH:mm"` | Display format (see format tokens below). |
67
- | `placeholder` | `string` | `"Select time"` | Placeholder text shown when input is empty. |
68
- | `id` | `string` | `undefined` | Input id. In range mode, second input uses `${id}-end`. |
69
- | `name` | `string` | `undefined` | Input name. In range mode, second input uses `${name}-end`. |
70
- | `tabindex` | `number` | `0` | Tab order index applied to input field(s). |
71
- | `autocomplete` | `string` | `"off"` | Native HTML autocomplete attribute for input field(s). |
72
- | `inputClass` | `string \| string[] \| Record<string, boolean>` | `undefined` | Extra class(es) applied to the input field(s). |
73
- | `inputWidth` | `string \| number` | `undefined` | Width for each input field. Number values are treated as `px`. |
74
- | `minInputWidth` | `string \| number` | `undefined` | Minimum width for each input field. Number values are treated as `px`. |
75
- | `maxInputWidth` | `string \| number` | `undefined` | Maximum width for each input field. Number values are treated as `px`. |
76
- | `componentWidth` | `string \| number` | `undefined` | Width of the outer component shell. Number values are treated as `px`. |
77
- | `range` | `boolean` | `false` | Enable range selection with two time inputs. |
78
- | `disabled` | `boolean` | `false` | Disables the timepicker input(s) and prevents opening/selecting. |
79
- | `hideDropdown` | `boolean` | `false` | Hides time columns so selection is typing-only. |
80
- | `hourStep` | `number` | `1` | Step interval for the hour column. |
81
- | `minuteStep` | `number` | `1` | Step interval for the minute column. |
82
- | `secondStep` | `number` | `1` | Step interval for the second column. |
83
- | `minTime` | `string` | `undefined` | Lower bound in `HH:mm` or `HH:mm:ss`; input and dropdown are constrained. |
84
- | `maxTime` | `string` | `undefined` | Upper bound in `HH:mm` or `HH:mm:ss`; input and dropdown are constrained. |
85
- | `disabledTimes` | `(string \| [string, string])[]` | `undefined` | Disabled points/ranges in `HH:mm:ss`; e.g. `"12:00:00"` or `[["13:00:00","14:00:00"]]`. |
86
- | `isTimeDisabled` | `(time: InternalFormat) => boolean` | `undefined` | Callback for custom disable rules. Return `true` to block a time. |
87
- | `size` | `"xs" \| "sm" \| "md" \| "lg" \| "xl"` | `"md"` | Size preset that maps to CSS variables. |
88
-
89
- ### Autocomplete notes
90
-
91
- - `autocomplete` is passed directly to the native `<input>` element(s).
92
- - In range mode, both inputs receive the same `autocomplete` value.
93
- - Browser autofill behavior depends on form context and input metadata (`id`, `name`, surrounding `<form>`).
94
- - Specific tokens (when useful for your form): e.g. `"one-time-code"`.
78
+ The repo includes a GitHub Pages workflow for publishing Storybook from `.github/workflows/storybook.yml`.
95
79
 
96
- ## Validation API
80
+ ## Examples
97
81
 
98
- The component exposes a validation state and emits validation events for predictable form handling:
82
+ Unless noted otherwise, the examples below assume this shared setup:
99
83
 
100
- - `update:validationState` emits one of: `"valid"`, `"invalid"`, `"out-of-range"`
101
- - `validate` emits payload:
102
-
103
- ```ts
104
- {
105
- target: "first" | "second";
106
- state: "valid" | "invalid" | "out-of-range";
107
- reason?: "BAD_TIME" | "OUT_OF_RANGE" | "DISABLED";
108
- value: string | null; // always HH:mm:ss when present
109
- }
84
+ ```vue
85
+ <script setup lang="ts">
86
+ import { computed, ref } from "vue";
87
+ import { TimePicker } from "@manik02/vue3-timepicker";
88
+ import "@manik02/vue3-timepicker/style.css";
89
+ </script>
110
90
  ```
111
91
 
112
- `out-of-range` means the value was outside `minTime`/`maxTime` and got clamped.
113
- `invalid` means bad/incomplete input or a blocked value from `disabledTimes`/`isTimeDisabled`.
114
-
115
- ### Example
92
+ ### Basic Single Picker
116
93
 
117
94
  ```vue
118
95
  <script setup lang="ts">
119
96
  import { ref } from "vue";
120
97
 
121
- const time = ref("12:00:00");
122
- const validationState = ref<"valid" | "invalid" | "out-of-range">("valid");
123
-
124
- function onValidate(payload: {
125
- state: "valid" | "invalid" | "out-of-range";
126
- reason?: "BAD_TIME" | "OUT_OF_RANGE" | "DISABLED";
127
- }) {
128
- console.log("validate", payload.state, payload.reason);
129
- }
98
+ const time = ref("09:30:00");
130
99
  </script>
131
100
 
132
101
  <template>
133
- <TimePicker
134
- v-model="time"
135
- v-model:validationState="validationState"
136
- minTime="09:00:00"
137
- maxTime="18:00:00"
138
- @validate="onValidate"
139
- />
140
-
141
- <small>State: {{ validationState }}</small>
102
+ <TimePicker v-model="time" format="HH:mm" />
142
103
  </template>
143
104
  ```
144
105
 
145
- ### Range Example
106
+ ### 24-Hour Format with Seconds
146
107
 
147
108
  ```vue
148
109
  <script setup lang="ts">
149
110
  import { ref } from "vue";
150
111
 
151
- const range = ref<[string, string]>(["09:00:00", "17:00:00"]);
152
- const validationState = ref<"valid" | "invalid" | "out-of-range">("valid");
153
-
154
- function onValidate(payload: {
155
- target: "first" | "second";
156
- state: "valid" | "invalid" | "out-of-range";
157
- reason?: "BAD_TIME" | "OUT_OF_RANGE" | "DISABLED";
158
- }) {
159
- console.log(payload.target, payload.state, payload.reason);
160
- }
112
+ const time = ref("14:30:45");
161
113
  </script>
162
114
 
163
115
  <template>
164
- <TimePicker
165
- v-model="range"
166
- :range="true"
167
- v-model:validationState="validationState"
168
- :disabled-times="[["12:00:00", "13:00:00"]]"
169
- @validate="onValidate"
170
- />
171
-
172
- <small>State: {{ validationState }}</small>
116
+ <TimePicker v-model="time" format="HH:mm:ss" />
173
117
  </template>
174
118
  ```
175
119
 
176
- ## Format tokens
177
-
178
- | Token | Output | Description |
179
- | --------- | ---------------------- | ---------------------- |
180
- | `HH` | `00`-`23` | 24-hour, zero-padded |
181
- | `H` | `0`-`23` | 24-hour |
182
- | `hh` | `01`-`12` | 12-hour, zero-padded |
183
- | `h` | `1`-`12` | 12-hour |
184
- | `kk` | `01`-`24` | 1-24 hour, zero-padded |
185
- | `k` | `1`-`24` | 1-24 hour |
186
- | `mm` | `00`-`59` | Minutes, zero-padded |
187
- | `m` | `0`-`59` | Minutes |
188
- | `ss` | `00`-`59` | Seconds, zero-padded |
189
- | `s` | `0`-`59` | Seconds |
190
- | `A` / `a` | `AM`/`PM` or `am`/`pm` | AM/PM indicator |
191
-
192
- Combine tokens with `:` separators: `HH:mm`, `hh:mm:ss A`, `kk:mm`, etc.
193
-
194
- ## Range mode
120
+ ### 12-Hour Format
195
121
 
196
122
  ```vue
197
- <script setup>
123
+ <script setup lang="ts">
198
124
  import { ref } from "vue";
199
- import { TimePicker } from "@manik02/vue3-timepicker";
200
- import "@manik02/vue3-timepicker/style.css";
201
125
 
202
- const range = ref(["09:00:00", "17:00:00"]);
126
+ const time = ref("14:30:00");
203
127
  </script>
204
128
 
205
129
  <template>
206
- <TimePicker v-model="range" format="HH:mm" :range="true" />
130
+ <TimePicker v-model="time" format="hh:mm A" />
207
131
  </template>
208
132
  ```
209
133
 
210
- When `range` is `true`, `modelValue` must be a `[string, string]` array when set, or `null`/`undefined` for an empty state.
134
+ While the input is focused, press `a` or `p` to toggle AM/PM.
211
135
 
212
- ## 12-hour format
136
+ ### Lowercase am/pm
213
137
 
214
138
  ```vue
215
- <TimePicker v-model="time" format="hh:mm A" />
216
- // AM/PM
217
- <TimePicker v-model="time" format="hh:mm a" />
218
- // am/pm
139
+ <template>
140
+ <TimePicker v-model="time" format="hh:mm a" />
141
+ </template>
219
142
  ```
220
143
 
221
- Press `a` or `p` while focused to toggle between AM and PM.
222
-
223
- ## Step intervals
144
+ ### `k` Format (1-24 Hours)
224
145
 
225
146
  ```vue
226
- <TimePicker v-model="time" format="HH:mm" :minute-step="15" />
227
- ```
147
+ <script setup lang="ts">
148
+ import { ref } from "vue";
228
149
 
229
- The dropdown columns will show values at the specified intervals (e.g. 00, 15, 30, 45 for a 15-minute step).
150
+ const time = ref("23:00:00");
151
+ </script>
230
152
 
231
- ## More quick examples
153
+ <template>
154
+ <TimePicker v-model="time" format="kk:mm" />
155
+ </template>
156
+ ```
232
157
 
233
- ### Start empty and allow clearing
158
+ ### Start Empty and Clear Programmatically
234
159
 
235
160
  ```vue
236
161
  <script setup lang="ts">
@@ -244,15 +169,54 @@ function clearTime() {
244
169
  </script>
245
170
 
246
171
  <template>
247
- <TimePicker v-model="time" format="HH:mm" />
172
+ <TimePicker v-model="time" format="HH:mm" placeholder="Select a time" />
248
173
  <button type="button" @click="clearTime">Clear</button>
249
174
  <pre>{{ time }}</pre>
250
175
  </template>
251
176
  ```
252
177
 
253
- ### Typing-only input (no dropdown)
178
+ ### Range Picker
179
+
180
+ ```vue
181
+ <script setup lang="ts">
182
+ import { ref } from "vue";
183
+
184
+ const range = ref<[string, string]>(["09:00:00", "17:00:00"]);
185
+ </script>
186
+
187
+ <template>
188
+ <TimePicker v-model="range" :range="true" format="HH:mm" />
189
+ </template>
190
+ ```
191
+
192
+ ### Range Picker with 30-Minute Intervals
254
193
 
255
194
  ```vue
195
+ <script setup lang="ts">
196
+ import { ref } from "vue";
197
+
198
+ const range = ref<[string, string]>(["09:00:00", "17:00:00"]);
199
+ </script>
200
+
201
+ <template>
202
+ <TimePicker
203
+ v-model="range"
204
+ :range="true"
205
+ format="HH:mm"
206
+ :minute-step="30"
207
+ />
208
+ </template>
209
+ ```
210
+
211
+ ### Typing-Only Input
212
+
213
+ ```vue
214
+ <script setup lang="ts">
215
+ import { ref } from "vue";
216
+
217
+ const time = ref("13:45:00");
218
+ </script>
219
+
256
220
  <template>
257
221
  <TimePicker
258
222
  v-model="time"
@@ -263,9 +227,35 @@ function clearTime() {
263
227
  </template>
264
228
  ```
265
229
 
266
- ### Working-hours bounds
230
+ ### Step Intervals
267
231
 
268
232
  ```vue
233
+ <script setup lang="ts">
234
+ import { ref } from "vue";
235
+
236
+ const time = ref("10:00:00");
237
+ </script>
238
+
239
+ <template>
240
+ <TimePicker
241
+ v-model="time"
242
+ format="HH:mm:ss"
243
+ :hour-step="2"
244
+ :minute-step="15"
245
+ :second-step="10"
246
+ />
247
+ </template>
248
+ ```
249
+
250
+ ### Working-Hours Bounds
251
+
252
+ ```vue
253
+ <script setup lang="ts">
254
+ import { ref } from "vue";
255
+
256
+ const time = ref("08:00:00");
257
+ </script>
258
+
269
259
  <template>
270
260
  <TimePicker
271
261
  v-model="time"
@@ -276,9 +266,17 @@ function clearTime() {
276
266
  </template>
277
267
  ```
278
268
 
279
- ### Disable specific slots and ranges
269
+ If the user enters a value outside the allowed bounds, the component clamps it and emits `out-of-range` validation.
270
+
271
+ ### Disable Specific Times and Ranges
280
272
 
281
273
  ```vue
274
+ <script setup lang="ts">
275
+ import { ref } from "vue";
276
+
277
+ const time = ref("09:00:00");
278
+ </script>
279
+
282
280
  <template>
283
281
  <TimePicker
284
282
  v-model="time"
@@ -292,7 +290,29 @@ function clearTime() {
292
290
  </template>
293
291
  ```
294
292
 
295
- ### React to validation state in UI
293
+ ### Disable Times with a Callback
294
+
295
+ ```vue
296
+ <script setup lang="ts">
297
+ import { ref } from "vue";
298
+
299
+ const time = ref("09:15:00");
300
+
301
+ function isTimeDisabled(value: { h: number; m: number; s: number }) {
302
+ return value.m === 45 || (value.h >= 11 && value.h <= 12);
303
+ }
304
+ </script>
305
+
306
+ <template>
307
+ <TimePicker
308
+ v-model="time"
309
+ format="HH:mm"
310
+ :is-time-disabled="isTimeDisabled"
311
+ />
312
+ </template>
313
+ ```
314
+
315
+ ### React to Validation State
296
316
 
297
317
  ```vue
298
318
  <script setup lang="ts">
@@ -302,8 +322,12 @@ const time = ref("08:00:00");
302
322
  const validationState = ref<"valid" | "invalid" | "out-of-range">("valid");
303
323
 
304
324
  const message = computed(() => {
305
- if (validationState.value === "out-of-range") return "Adjusted to nearest allowed time";
306
- if (validationState.value === "invalid") return "Please enter a valid time";
325
+ if (validationState.value === "out-of-range") {
326
+ return "Adjusted to the nearest allowed time";
327
+ }
328
+ if (validationState.value === "invalid") {
329
+ return "Please enter a valid time";
330
+ }
307
331
  return "Looks good";
308
332
  });
309
333
  </script>
@@ -316,17 +340,57 @@ const message = computed(() => {
316
340
  min-time="09:00:00"
317
341
  max-time="17:00:00"
318
342
  />
343
+
319
344
  <small>{{ message }}</small>
320
345
  </template>
321
346
  ```
322
347
 
323
- ### Range picker with 30-minute intervals
348
+ ### Listen to Validation and Error Events
324
349
 
325
350
  ```vue
326
351
  <script setup lang="ts">
327
352
  import { ref } from "vue";
353
+ import type { ValidationReason, ValidationState } from "@manik02/vue3-timepicker";
354
+
355
+ const time = ref("12:00:00");
356
+ const validationState = ref<ValidationState>("valid");
357
+
358
+ function onValidate(payload: {
359
+ target: "first" | "second";
360
+ state: ValidationState;
361
+ reason?: ValidationReason;
362
+ value: string | null;
363
+ }) {
364
+ console.log("validate", payload);
365
+ }
366
+
367
+ function onError(payload: { code: ValidationReason; message: string }) {
368
+ console.log("error", payload);
369
+ }
370
+ </script>
371
+
372
+ <template>
373
+ <TimePicker
374
+ v-model="time"
375
+ v-model:validationState="validationState"
376
+ format="HH:mm"
377
+ min-time="09:00:00"
378
+ max-time="18:00:00"
379
+ @validate="onValidate"
380
+ @error="onError"
381
+ />
382
+ </template>
383
+ ```
384
+
385
+ ### Range Validation Example
386
+
387
+ ```vue
388
+ <script setup lang="ts">
389
+ import { ref } from "vue";
390
+ import type { ValidationState } from "@manik02/vue3-timepicker";
328
391
 
329
392
  const range = ref<[string, string]>(["09:00:00", "17:00:00"]);
393
+ const validationState = ref<ValidationState>("valid");
330
394
  </script>
331
395
 
332
396
  <template>
@@ -334,128 +398,170 @@ const range = ref<[string, string]>(["09:00:00", "17:00:00"]);
334
398
  v-model="range"
335
399
  :range="true"
336
400
  format="HH:mm"
337
- :minute-step="30"
401
+ :disabled-times="[['12:00:00', '13:00:00']]"
402
+ v-model:validationState="validationState"
338
403
  />
404
+
405
+ <small>Validation: {{ validationState }}</small>
339
406
  </template>
340
407
  ```
341
408
 
342
- ## Size presets
409
+ ### Form-Friendly Attributes
343
410
 
344
411
  ```vue
412
+ <script setup lang="ts">
413
+ import { ref } from "vue";
414
+
415
+ const time = ref("09:30:00");
416
+ </script>
417
+
345
418
  <template>
346
- <div class="sizes">
347
- <TimePicker v-model="time" format="HH:mm" size="xs" />
348
- <TimePicker v-model="time" format="HH:mm" size="sm" />
349
- <TimePicker v-model="time" format="HH:mm" size="md" />
350
- <TimePicker v-model="time" format="HH:mm" size="lg" />
351
- <TimePicker v-model="time" format="HH:mm" size="xl" />
352
- </div>
419
+ <form>
420
+ <label for="meeting-time">Meeting time</label>
421
+ <TimePicker
422
+ v-model="time"
423
+ id="meeting-time"
424
+ name="meetingTime"
425
+ autocomplete="off"
426
+ format="HH:mm"
427
+ />
428
+ </form>
353
429
  </template>
354
430
  ```
355
431
 
356
- Each preset sets a handful of CSS custom properties (`--vtp-font-size`, `--vtp-padding`, `--vtp-option-padding`, `--vtp-dropdown-radius`). You can still override any of them manually.
432
+ In range mode, the second input automatically uses `${id}-end` and `${name}-end`.
433
+
434
+ ### Custom Input Class
435
+
436
+ ```vue
437
+ <script setup lang="ts">
438
+ import { ref } from "vue";
357
439
 
358
- ## Width control
440
+ const time = ref("11:20:00");
441
+ </script>
442
+
443
+ <template>
444
+ <TimePicker
445
+ v-model="time"
446
+ format="HH:mm"
447
+ input-class="my-time-input"
448
+ />
449
+ </template>
450
+
451
+ <style>
452
+ .my-time-input {
453
+ letter-spacing: 0.04em;
454
+ font-variant-numeric: tabular-nums;
455
+ }
456
+ </style>
457
+ ```
359
458
 
360
- The component keeps a smart default width based on `format`/`placeholder`, but you can override it when needed.
459
+ ### Width Control
361
460
 
362
461
  ```vue
363
- <TimePicker
364
- v-model="time"
365
- format="HH:mm:ss"
366
- :input-width="220"
367
- min-input-width="12ch"
368
- max-input-width="320px"
369
- component-width="100%"
370
- />
462
+ <script setup lang="ts">
463
+ import { ref } from "vue";
464
+
465
+ const time = ref("11:20:00");
466
+ </script>
467
+
468
+ <template>
469
+ <TimePicker
470
+ v-model="time"
471
+ format="hh:mm A"
472
+ :input-width="220"
473
+ min-input-width="12ch"
474
+ max-input-width="320px"
475
+ component-width="100%"
476
+ />
477
+ </template>
371
478
  ```
372
479
 
373
480
  Width precedence for each input field:
374
481
 
375
482
  1. `inputWidth` prop
376
483
  2. `--vtp-input-width` CSS variable
377
- 3. Built-in heuristic (`6ch` to `20ch`, based on `format`/`placeholder`)
484
+ 3. Built-in width heuristic based on `format` and `placeholder`
378
485
 
379
- ## CSS custom properties
486
+ ### Size Presets
380
487
 
381
- Style the component by setting CSS custom properties on `.timepicker-shell` or any ancestor element.
488
+ ```vue
489
+ <script setup lang="ts">
490
+ import { ref } from "vue";
382
491
 
383
- ```css
384
- .my-theme .timepicker-shell {
385
- --vtp-font-family: "Inter", sans-serif;
386
- --vtp-font-size: 14px;
387
- --vtp-bg: #fff;
388
- --vtp-color: #333;
389
- --vtp-border: #d1d5db;
390
- --vtp-border-radius: 8px;
391
- --vtp-padding: 0.5rem 0.75rem;
392
- --vtp-focus-border: #3b82f6;
393
- --vtp-focus-ring: 0 0 0 3px rgba(59, 130, 246, 0.2);
394
- --vtp-error-border: #ef4444;
395
- --vtp-error-ring: 0 0 0 3px rgba(239, 68, 68, 0.15);
396
- --vtp-component-width: auto;
397
- --vtp-input-width: 12ch;
398
- --vtp-input-min-width: 0;
399
- --vtp-input-max-width: none;
400
- --vtp-separator-color: #9ca3af;
401
- --vtp-dropdown-bg: #fff;
402
- --vtp-dropdown-border: #e5e7eb;
403
- --vtp-dropdown-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
404
- --vtp-dropdown-radius: 8px;
405
- --vtp-dropdown-max-height: 240px;
406
- --vtp-option-padding: 0.375rem 0.75rem;
407
- --vtp-option-radius: 6px;
408
- --vtp-option-hover-bg: #f3f4f6;
409
- --vtp-option-active-bg: #dbeafe;
410
- --vtp-option-active-color: #1e40af;
411
- --vtp-option-active-weight: 600;
412
- --vtp-columns-gap: 0.5rem;
492
+ const time = ref("09:30:00");
493
+ </script>
494
+
495
+ <template>
496
+ <div class="sizes">
497
+ <TimePicker v-model="time" format="HH:mm" size="xs" />
498
+ <TimePicker v-model="time" format="HH:mm" size="sm" />
499
+ <TimePicker v-model="time" format="HH:mm" size="md" />
500
+ <TimePicker v-model="time" format="HH:mm" size="lg" />
501
+ <TimePicker v-model="time" format="HH:mm" size="xl" />
502
+ </div>
503
+ </template>
504
+
505
+ <style>
506
+ .sizes {
507
+ display: grid;
508
+ gap: 0.75rem;
413
509
  }
510
+ </style>
414
511
  ```
415
512
 
416
- All properties have sensible defaults and the component inherits font and colour from its parent by default.
417
-
418
- ### Dark theme example
513
+ ### CSS Variables Theme Example
419
514
 
420
515
  ```vue
516
+ <script setup lang="ts">
517
+ import { ref } from "vue";
518
+
519
+ const time = ref("19:45:00");
520
+ </script>
521
+
421
522
  <template>
422
- <div class="dark-theme">
523
+ <div class="night-theme">
423
524
  <TimePicker v-model="time" format="HH:mm:ss" />
424
525
  </div>
425
526
  </template>
426
527
 
427
528
  <style>
428
- .dark-theme .timepicker-shell {
429
- --vtp-bg: #1e1e2e;
430
- --vtp-color: #cdd6f4;
431
- --vtp-border: #45475a;
529
+ .night-theme .timepicker-shell {
530
+ --vtp-bg: #0f172a;
531
+ --vtp-color: #e2e8f0;
532
+ --vtp-border: #334155;
432
533
  --vtp-border-radius: 10px;
433
- --vtp-focus-border: #89b4fa;
434
- --vtp-focus-ring: 0 0 0 3px rgba(137, 180, 250, 0.25);
435
- --vtp-separator-color: #6c7086;
436
- --vtp-dropdown-bg: #1e1e2e;
437
- --vtp-dropdown-border: #45475a;
438
- --vtp-dropdown-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
439
- --vtp-option-hover-bg: #313244;
440
- --vtp-option-active-bg: #89b4fa;
441
- --vtp-option-active-color: #1e1e2e;
534
+ --vtp-focus-border: #38bdf8;
535
+ --vtp-focus-ring: 0 0 0 3px rgba(56, 189, 248, 0.2);
536
+ --vtp-separator-color: #94a3b8;
537
+ --vtp-dropdown-bg: #0b1220;
538
+ --vtp-dropdown-border: #1e293b;
539
+ --vtp-dropdown-shadow: 0 10px 30px rgba(2, 6, 23, 0.45);
540
+ --vtp-option-hover-bg: #1e293b;
541
+ --vtp-option-active-bg: #38bdf8;
542
+ --vtp-option-active-color: #082f49;
442
543
  }
443
544
  </style>
444
545
  ```
445
546
 
446
- ### Minimal rounded example
547
+ ### Rounded Minimal Theme Example
447
548
 
448
549
  ```vue
550
+ <script setup lang="ts">
551
+ import { ref } from "vue";
552
+
553
+ const time = ref("08:15:00");
554
+ </script>
555
+
449
556
  <template>
450
557
  <div class="rounded-theme">
451
- <TimePicker v-model="time" format="HH:mm" />
558
+ <TimePicker v-model="time" format="hh:mm A" />
452
559
  </div>
453
560
  </template>
454
561
 
455
562
  <style>
456
563
  .rounded-theme .timepicker-shell {
457
- --vtp-font-family: "Georgia", serif;
458
- --vtp-font-size: 16px;
564
+ --vtp-font-family: Georgia, serif;
459
565
  --vtp-border: #a78bfa;
460
566
  --vtp-border-radius: 999px;
461
567
  --vtp-padding: 0.5rem 1.25rem;
@@ -469,53 +575,180 @@ All properties have sensible defaults and the component inherits font and colour
469
575
  </style>
470
576
  ```
471
577
 
472
- ### Compact flat example
578
+ ## Props
473
579
 
474
- ```vue
475
- <template>
476
- <div class="flat-theme">
477
- <TimePicker v-model="time" format="hh:mm A" />
478
- </div>
479
- </template>
580
+ | Prop | Type | Default | Description |
581
+ | --- | --- | --- | --- |
582
+ | `modelValue` | `string \| [string, string] \| null` | `undefined` | Current value. In range mode use a two-item tuple. |
583
+ | `format` | `TimeFormat` | `"HH:mm"` | Display and input format. |
584
+ | `placeholder` | `string` | `"Select time"` | Placeholder text for empty input(s). |
585
+ | `id` | `string` | `undefined` | Input id. In range mode the second input uses `${id}-end`. |
586
+ | `name` | `string` | `undefined` | Input name. In range mode the second input uses `${name}-end`. |
587
+ | `tabindex` | `number` | `0` | Tab index for input field(s). |
588
+ | `autocomplete` | `string` | `"off"` | Native HTML autocomplete value. |
589
+ | `inputClass` | `string \| string[] \| Record<string, boolean>` | `undefined` | Extra class or classes applied to each input. |
590
+ | `inputWidth` | `string \| number` | `undefined` | Explicit width for each input. Numeric values are treated as `px`. |
591
+ | `minInputWidth` | `string \| number` | `undefined` | Minimum width for each input. Numeric values are treated as `px`. |
592
+ | `maxInputWidth` | `string \| number` | `undefined` | Maximum width for each input. Numeric values are treated as `px`. |
593
+ | `componentWidth` | `string \| number` | `undefined` | Width for the outer shell. Numeric values are treated as `px`. |
594
+ | `range` | `boolean` | `false` | Enables two inputs for a time range. |
595
+ | `disabled` | `boolean` | `false` | Disables typing and dropdown interaction. |
596
+ | `hideDropdown` | `boolean` | `false` | Hides the column picker and keeps the input typing-only. |
597
+ | `hourStep` | `number` | `1` | Hour interval in the dropdown. |
598
+ | `minuteStep` | `number` | `1` | Minute interval in the dropdown. |
599
+ | `secondStep` | `number` | `1` | Second interval in the dropdown. |
600
+ | `minTime` | `string` | `undefined` | Minimum allowed time in `HH:mm` or `HH:mm:ss`. |
601
+ | `maxTime` | `string` | `undefined` | Maximum allowed time in `HH:mm` or `HH:mm:ss`. |
602
+ | `disabledTimes` | `(string \| [string, string])[]` | `undefined` | Disabled time points or ranges. |
603
+ | `isTimeDisabled` | `(time: InternalFormat) => boolean` | `undefined` | Callback for custom disabled-time rules. Return `true` to block a time. |
604
+ | `size` | `"xs" \| "sm" \| "md" \| "lg" \| "xl"` | `"md"` | Size preset mapped to CSS variables. |
605
+
606
+ ### Autocomplete Notes
607
+
608
+ - `autocomplete` is forwarded directly to the native `<input>` element.
609
+ - In range mode, both inputs receive the same `autocomplete` value.
610
+ - Browser autofill behavior also depends on the surrounding form, `id`, and `name` attributes.
480
611
 
481
- <style>
482
- .flat-theme .timepicker-shell {
483
- --vtp-font-family: "Roboto Mono", monospace;
484
- --vtp-font-size: 13px;
485
- --vtp-bg: #f8fafc;
486
- --vtp-color: #0f172a;
487
- --vtp-border: transparent;
488
- --vtp-border-radius: 4px;
489
- --vtp-padding: 0.375rem 0.5rem;
490
- --vtp-focus-border: #0ea5e9;
491
- --vtp-focus-ring: none;
492
- --vtp-dropdown-bg: #f8fafc;
493
- --vtp-dropdown-border: #e2e8f0;
494
- --vtp-dropdown-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
495
- --vtp-dropdown-radius: 4px;
496
- --vtp-option-radius: 2px;
497
- --vtp-option-hover-bg: #e2e8f0;
498
- --vtp-option-active-bg: #0ea5e9;
499
- --vtp-option-active-color: #fff;
500
- --vtp-option-active-weight: 500;
612
+ ## Events
613
+
614
+ | Event | Payload | Description |
615
+ | --- | --- | --- |
616
+ | `update:modelValue` | `string \| [string, string] \| null` | Emitted when the value changes. |
617
+ | `update:validationState` | `"valid" \| "invalid" \| "out-of-range"` | Emitted whenever the aggregated validation state changes. |
618
+ | `validate` | `{ target, state, reason?, value }` | Emitted after validation runs for one input. |
619
+ | `error` | `{ code, message }` | Emitted when invalid or disabled input is encountered. |
620
+
621
+ ### `validate` Payload
622
+
623
+ ```ts
624
+ {
625
+ target: "first" | "second";
626
+ state: "valid" | "invalid" | "out-of-range";
627
+ reason?: "BAD_TIME" | "OUT_OF_RANGE" | "DISABLED";
628
+ value: string | null;
501
629
  }
502
- </style>
503
630
  ```
504
631
 
505
- You can scope the overrides to any CSS class, or apply them globally to `.timepicker-shell` to affect all instances.
632
+ - `value` is always normalized to `HH:mm:ss` when present.
633
+ - `target` is always included, even in single-input mode.
634
+
635
+ ### Validation States
636
+
637
+ - `valid`: the value is accepted.
638
+ - `invalid`: the value is incomplete, malformed, or blocked by disable rules.
639
+ - `out-of-range`: the value was outside `minTime`/`maxTime` and was clamped.
640
+
641
+ ## Format Tokens
642
+
643
+ | Token | Output | Description |
644
+ | --- | --- | --- |
645
+ | `HH` | `00`-`23` | 24-hour, zero-padded |
646
+ | `H` | `0`-`23` | 24-hour |
647
+ | `hh` | `01`-`12` | 12-hour, zero-padded |
648
+ | `h` | `1`-`12` | 12-hour |
649
+ | `kk` | `01`-`24` | 1-24 hour, zero-padded |
650
+ | `k` | `1`-`24` | 1-24 hour |
651
+ | `mm` | `00`-`59` | Minutes, zero-padded |
652
+ | `m` | `0`-`59` | Minutes |
653
+ | `ss` | `00`-`59` | Seconds, zero-padded |
654
+ | `s` | `0`-`59` | Seconds |
655
+ | `A` / `P` | `AM` / `PM` | Uppercase AM/PM |
656
+ | `a` / `p` | `am` / `pm` | Lowercase am/pm |
657
+
658
+ Examples:
659
+
660
+ - `HH:mm`
661
+ - `HH:mm:ss`
662
+ - `hh:mm A`
663
+ - `hh:mm:ss a`
664
+ - `kk:mm`
665
+
666
+ ## Keyboard Behavior
667
+
668
+ - Typing is overwrite-only rather than free-form insertion.
669
+ - The mask auto-inserts `:` separators.
670
+ - In 12-hour mode, press `a` or `p` while focused to toggle AM/PM.
671
+ - `Backspace` moves the cursor left without clearing the entire value.
672
+ - `Escape` closes the dropdown columns.
673
+
674
+ ## Styling
675
+
676
+ The component exposes CSS custom properties on `.timepicker-shell`, so you can theme it from any parent container.
677
+
678
+ ```css
679
+ .my-theme .timepicker-shell {
680
+ --vtp-font-family: Inter, sans-serif;
681
+ --vtp-font-size: 14px;
682
+ --vtp-bg: #ffffff;
683
+ --vtp-color: #111827;
684
+ --vtp-border: #d1d5db;
685
+ --vtp-border-radius: 8px;
686
+ --vtp-padding: 0.5rem 0.75rem;
687
+ --vtp-focus-border: #2563eb;
688
+ --vtp-focus-ring: 0 0 0 3px rgba(37, 99, 235, 0.18);
689
+ --vtp-error-border: #ef4444;
690
+ --vtp-error-ring: 0 0 0 3px rgba(239, 68, 68, 0.15);
691
+ --vtp-component-width: auto;
692
+ --vtp-input-width: 12ch;
693
+ --vtp-input-min-width: 0;
694
+ --vtp-input-max-width: none;
695
+ --vtp-separator-color: #9ca3af;
696
+ --vtp-dropdown-bg: #ffffff;
697
+ --vtp-dropdown-border: #e5e7eb;
698
+ --vtp-dropdown-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
699
+ --vtp-dropdown-radius: 8px;
700
+ --vtp-dropdown-max-height: 240px;
701
+ --vtp-option-padding: 0.375rem 0.75rem;
702
+ --vtp-option-radius: 6px;
703
+ --vtp-option-hover-bg: #f3f4f6;
704
+ --vtp-option-active-bg: #dbeafe;
705
+ --vtp-option-active-color: #1e40af;
706
+ --vtp-option-active-weight: 600;
707
+ --vtp-columns-gap: 0.5rem;
708
+ }
709
+ ```
710
+
711
+ Common styling variables:
712
+
713
+ | Variable | Purpose |
714
+ | --- | --- |
715
+ | `--vtp-bg` | Input background |
716
+ | `--vtp-color` | Input text color |
717
+ | `--vtp-border` | Input border color |
718
+ | `--vtp-focus-border` | Focused border color |
719
+ | `--vtp-focus-ring` | Focus ring shadow |
720
+ | `--vtp-dropdown-bg` | Dropdown background |
721
+ | `--vtp-dropdown-border` | Dropdown border color |
722
+ | `--vtp-option-hover-bg` | Hovered option background |
723
+ | `--vtp-option-active-bg` | Active option background |
724
+ | `--vtp-option-active-color` | Active option text color |
725
+ | `--vtp-input-width` | Default input width |
726
+ | `--vtp-component-width` | Outer shell width |
506
727
 
507
728
  ## TypeScript
508
729
 
509
- The package exports the following types:
730
+ The package exports these useful types:
510
731
 
511
732
  ```ts
512
733
  import type {
513
- TimeFormat,
734
+ DisabledTimeInput,
514
735
  InternalFormat,
736
+ TimeFormat,
515
737
  TimePickerProps,
738
+ ValidationReason,
739
+ ValidationState,
516
740
  } from "@manik02/vue3-timepicker";
517
741
  ```
518
742
 
743
+ ## Development
744
+
745
+ ```bash
746
+ npm run dev
747
+ npm run storybook
748
+ npm run test
749
+ npm run build
750
+ ```
751
+
519
752
  ## License
520
753
 
521
754
  MIT
package/package.json CHANGED
@@ -1,9 +1,16 @@
1
1
  {
2
2
  "name": "@manik02/vue3-timepicker",
3
- "version": "0.4.5",
4
- "description": "A flexible Vue 3 timepicker component with multiple format support, range selection, and fully customisable styling",
3
+ "version": "1.0.0",
4
+ "description": "Vue 3 time picker component with multiple formats, range selection, min/max constraints, disabled times, validation, custom styling, TypeScript support and more.",
5
5
  "license": "MIT",
6
- "author": "Manos Savvides",
6
+ "author": {
7
+ "name": "Manos Savvides",
8
+ "url": "https://github.com/manos02"
9
+ },
10
+ "homepage": "https://manos02.github.io/vue3-time-picker/",
11
+ "bugs": {
12
+ "url": "https://github.com/manos02/vue3-time-picker/issues"
13
+ },
7
14
  "repository": {
8
15
  "type": "git",
9
16
  "url": "git+https://github.com/manos02/vue3-time-picker.git"
@@ -27,15 +34,24 @@
27
34
  "keywords": [
28
35
  "vue",
29
36
  "vue3",
37
+ "vue-3",
38
+ "vue-timepicker",
39
+ "vue-time-picker",
30
40
  "timepicker",
31
41
  "time-picker",
42
+ "time-input",
43
+ "time-range-picker",
44
+ "time-selector",
45
+ "time-select",
32
46
  "time",
33
47
  "component",
48
+ "typescript",
49
+ "form",
34
50
  "range",
35
51
  "12h",
36
52
  "24h",
37
53
  "input",
38
- "UI"
54
+ "ui"
39
55
  ],
40
56
  "scripts": {
41
57
  "dev": "vite",
@@ -66,5 +82,8 @@
66
82
  "vite-plugin-dts": "^4.5.4",
67
83
  "vitest": "^4.0.18",
68
84
  "vue-tsc": "^3.0.6"
85
+ },
86
+ "dependencies": {
87
+ "pump": "^1.0.0"
69
88
  }
70
89
  }